Completed
Push — master ( b28c9e...48d743 )
by Koen
01:04
created

dict_to_note()   A

Complexity

Conditions 2

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 2
dl 0
loc 19
rs 9.4285
1
# -*- coding: utf-8 -*-
2
3
'''
4
This module contains a read-only model of the :term:`SKOS` specification.
5
6
To complement the :term:`SKOS` specification, some elements were borrowed
7
from the :term:`SKOS-THES` specification (eg. superordinate and
8
subordinate array).
9
10
.. versionadded:: 0.2.0
11
'''
12
13
from __future__ import unicode_literals
14
15
from language_tags import tags
16
17
18
class Label:
19
    '''
20
    A :term:`SKOS` Label.
21
    '''
22
23
    label = None
24
    '''
25
    The label itself (eg. `churches`, `trees`, `Spitfires`, ...)
26
    '''
27
28
    type = "prefLabel"
29
    '''
30
    The type of this label (`prefLabel`, `altLabel`, `hiddenLabel`, 'sortLabel').
31
    '''
32
33
    language = "und"
34
    '''
35
    The language the label is in (eg. `en`, `en-US`, `nl`, `nl-BE`).
36
    '''
37
38
    valid_types = [
39
        'prefLabel',
40
        'altLabel',
41
        'hiddenLabel',
42
        'sortLabel'
43
    ]
44
    '''
45
    The valid types for a label
46
    '''
47
48
    def __init__(self, label, type="prefLabel", language="und"):
49
        self.label = label
50
        self.type = type
51
        self.language = language
52
53
    def __eq__(self, other):
54
        return self.__dict__ == (other if type(other) == dict else other.__dict__)
55
56
    def __ne__(self, other):
57
        return not self == other
58
59
    @staticmethod
60
    def is_valid_type(type):
61
        '''
62
        Check if the argument is a valid SKOS label type.
63
64
        :param string type: The type to be checked.
65
        '''
66
        return type in Label.valid_types
67
68
    def __repr__(self):
69
        return "Label('%s', '%s', '%s')" % (self.label, self.type, self.language)
70
71
72
class Note:
73
    '''
74
    A :term:`SKOS` Note.
75
    '''
76
77
    note = None
78
    '''The note itself'''
79
80
    type = "note"
81
    '''
82
    The type of this note ( `note`, `definition`, `scopeNote`, ...).
83
    '''
84
85
    language = "und"
86
    '''
87
    The language the label is in (eg. `en`, `en-US`, `nl`, `nl-BE`).
88
    '''
89
90
    markup = None
91
    '''
92
    What kind of markup does the note contain?
93
94
    If not none, the note should be treated as a certain type of markup.
95
    Currently only HTML is allowed.
96
    '''
97
98
    valid_types = [
99
            'note',
100
            'changeNote',
101
            'definition',
102
            'editorialNote',
103
            'example',
104
            'historyNote',
105
            'scopeNote'
106
        ]
107
    '''
108
    The valid types for a note.
109
    '''
110
111
    valid_markup = [
112
        None,
113
        'HTML'
114
    ]
115
    '''
116
    Valid types of markup for a note.
117
    '''
118
119
    def __init__(self, note, type="note", language="und", markup=None):
120
        self.note = note
121
        self.type = type
122
        self.language = language
123
        self.markup = markup
124
125
    def __eq__(self, other):
126
        return self.__dict__ == (other if type(other) == dict else other.__dict__)
127
128
    def __ne__(self, other):
129
        return not self == other
130
131
    @staticmethod
132
    def is_valid_type(type):
133
        '''
134
        Check if the argument is a valid SKOS note type.
135
136
        :param string type: The type to be checked.
137
        '''
138
        return type in Note.valid_types
139
140
    @staticmethod
141
    def is_valid_markup(markup):
142
        '''
143
        Check is the argument is a valid type of markup.
144
145
        :param string markup: The type to be checked.
146
        '''
147
        return markup in Note.valid_markup
148
149
150
class Source:
151
    '''
152
    A `Source` for a concept, collection or scheme.
153
154
    '''
155
156
    citation = None
157
    '''A bibliographic citation for this source.'''
158
159
    def __init__(self, citation):
160
        self.citation = citation
161
162
163
class ConceptScheme:
164
    '''
165
    A :term:`SKOS` ConceptScheme.
166
167
    :param string uri: A :term:`URI` for this conceptscheme.
168
    :param list labels: A list of :class:`skosprovider.skos.Label` instances.
169
    :param list notes: A list of :class:`skosprovider.skos.Note` instances.
170
    '''
171
172
    uri = None
173
    '''A :term:`URI` for this conceptscheme.'''
174
175
    labels = []
176
    '''A :class:`lst` of :class:`skosprovider.skos.label` instances.'''
177
178
    notes = []
179
    '''A :class:`lst` of :class:`skosprovider.skos.Note` instances.'''
180
181
    sources = []
182
    '''A :class:`lst` of :class:`skosprovider.skos.Source` instances.'''
183
184
    languages = []
185
    '''
186
    A :class:`lst` of languages that are being used in the ConceptScheme.
187
188
    There's no guarantuee that labels or notes in other languages do not exist.
189
    '''
190
191
    def __init__(self, uri, labels=[], notes=[], sources=[], languages=[]):
192
        self.uri = uri
193
        self.labels = [dict_to_label(l) for l in labels]
194
        self.notes = [dict_to_note(n) for n in notes]
195
        self.sources = [dict_to_source(s) for s in sources]
196
        self.languages = languages
197
198
    def label(self, language='any'):
199
        '''
200
        Provide a single label for this conceptscheme.
201
202
        This uses the :func:`label` function to determine which label to
203
        return.
204
205
        :param string language: The preferred language to receive the label in.
206
            This should be a valid IANA language tag.
207
        :rtype: :class:`skosprovider.skos.Label` or False if no labels were found.
208
        '''
209
        return label(self.labels, language)
210
211
    def _sortkey(self, key='uri', language='any'):
212
        '''
213
        Provide a single sortkey for this conceptscheme.
214
215
        :param string key: Either `uri`, `label` or `sortlabel`.
216
        :param string language: The preferred language to receive the label in
217
            if key is `label` or `sortlabel`. This should be a valid IANA language tag.
218
        :rtype: :class:`str`
219
        '''
220
        if key == 'uri':
221
            return self.uri
222
        else:
223
            l = label(self.labels, language, key == 'sortlabel')
224
            return l.label.lower() if l else ''
225
226
    def __repr__(self):
227
        return "ConceptScheme('%s')" % self.uri
228
229
230
class Concept:
231
    '''
232
    A :term:`SKOS` Concept.
233
    '''
234
235
    id = None
236
    '''An id for this Concept within a vocabulary
237
238
    eg. 12345
239
    '''
240
241
    uri = None
242
    '''A proper uri for this Concept
243
244
    eg. `http://id.example.com/skos/trees/1`
245
    '''
246
247
    type = 'concept'
248
    '''The type of this concept or collection.
249
250
    eg. 'concept'
251
    '''
252
253
    concept_scheme = None
254
    '''The :class:`ConceptScheme` this Concept is a part of.'''
255
256
    labels = []
257
    '''A :class:`lst` of :class:`Label` instances.'''
258
259
    notes = []
260
    '''A :class:`lst` of :class:`Note` instances.'''
261
262
    sources = []
263
    '''A :class:`lst` of :class:`skosprovider.skos.Source` instances.'''
264
265
    broader = []
266
    '''A :class:`lst` of concept ids.'''
267
268
    narrower = []
269
    '''A :class:`lst` of concept ids.'''
270
271
    related = []
272
    '''A :class:`lst` of concept ids.'''
273
274
    member_of = []
275
    '''A :class:`lst` of collection ids.'''
276
277
    subordinate_arrays = []
278
    '''A :class:`list` of collection ids.'''
279
280
    matches = {},
281
    '''
282
    A :class:`dictionary`. Each key is a matchtype and contains a :class:`list` of URI's.
283
    '''
284
285
    matchtypes = [
286
        'close',
287
        'exact',
288
        'related',
289
        'broad',
290
        'narrow'
291
    ]
292
    '''Matches with Concepts in other ConceptSchemes.
293
294
    This dictionary contains a key for each type of Match (close, exact,
295
    related, broad, narrow). Attached to each key is a list of URI's.
296
    '''
297
298
    def __init__(self, id, uri=None,
299
                 concept_scheme=None,
300
                 labels=[], notes=[], sources=[],
301
                 broader=[], narrower=[], related=[],
302
                 member_of=[], subordinate_arrays=[],
303
                 matches={}):
304
        self.id = id
305
        self.uri = uri
306
        self.type = 'concept'
307
        self.concept_scheme = concept_scheme
308
        self.labels = [dict_to_label(l) for l in labels]
309
        self.notes = [dict_to_note(n) for n in notes]
310
        self.sources = [dict_to_source(s) for s in sources]
311
        self.broader = broader
312
        self.narrower = narrower
313
        self.related = related
314
        self.member_of = member_of
315
        self.subordinate_arrays = subordinate_arrays
316
        self.matches = {}
317
        for match_type in self.matchtypes:
318
            if match_type not in matches.keys():
319
                matches[match_type] = []
320
        for match_type in matches.keys():
321
            if match_type in self.matchtypes:
322
                self.matches[match_type] = matches.get(match_type, [])
323
324
    def label(self, language='any'):
325
        '''
326
        Provide a single label for this concept.
327
328
        This uses the :func:`label` function to determine which label to return.
329
330
        :param string language: The preferred language to receive the label in.
331
            This should be a valid IANA language tag.
332
        :rtype: :class:`skosprovider.skos.Label` or False if no labels were found.
333
        '''
334
        return label(self.labels, language)
335
336
    def _sortkey(self, key='id', language='any'):
337
        '''
338
        Provide a single sortkey for this collection.
339
340
        :param string key: Either `id`, `uri`, `label` or `sortlabel`.
341
        :param string language: The preferred language to receive the label in
342
            if key is `label` or `sortlabel`. This should be a valid IANA language tag.
343
        :rtype: :class:`str`
344
        '''
345
        if key == 'id':
346
            return str(self.id)
347
        elif key == 'uri':
348
            return self.uri if self.uri else ''
349
        else:
350
            l = label(self.labels, language, key == 'sortlabel')
351
            return l.label.lower() if l else ''
352
353
    def __repr__(self):
354
        return "Concept('%s')" % self.id
355
356
357
class Collection:
358
    '''
359
    A :term:`SKOS` Collection.
360
    '''
361
362
    id = None
363
    '''An id for this Collection within a vocabulary'''
364
365
    uri = None
366
    '''A proper uri for this Collection'''
367
368
    type = 'collection'
369
    '''The type of this concept or collection.
370
371
    eg. 'collection'
372
    '''
373
374
    concept_scheme = None
375
    '''The :class:`ConceptScheme` this Collection is a part of.'''
376
377
    labels = []
378
    '''A :class:`lst` of :class:`skosprovider.skos.label` instances.'''
379
380
    notes = []
381
    '''A :class:`lst` of :class:`skosprovider.skos.Note` instances.'''
382
383
    sources = []
384
    '''A :class:`lst` of :class:`skosprovider.skos.Source` instances.'''
385
386
    members = []
387
    '''A :class:`lst` of concept or collection ids.'''
388
389
    member_of = []
390
    '''A :class:`lst` of collection ids.'''
391
392
    superordinates = []
393
    '''A :class:`lst` of concept ids.'''
394
395
    def __init__(self, id, uri=None,
396
                 concept_scheme=None,
397
                 labels=[], notes=[], sources=[],
398
                 members=[], member_of=[],
399
                 superordinates=[]):
400
        self.id = id
401
        self.uri = uri
402
        self.type = 'collection'
403
        self.concept_scheme = concept_scheme
404
        self.labels = [dict_to_label(l) for l in labels]
405
        self.notes = [dict_to_note(n) for n in notes]
406
        self.sources = [dict_to_source(s) for s in sources]
407
        self.members = members
408
        self.member_of = member_of
409
        self.superordinates = superordinates
410
411
    def label(self, language='any'):
412
        '''
413
        Provide a single label for this collection.
414
415
        This uses the :func:`label` function to determine which label to return.
416
417
        :param string language: The preferred language to receive the label in.
418
            This should be a valid IANA language tag.
419
        :rtype: :class:`skosprovider.skos.Label` or False if no labels were found.
420
        '''
421
        return label(self.labels, language, False)
422
423
    def _sortkey(self, key='id', language='any'):
424
        '''
425
        Provide a single sortkey for this collection.
426
427
        :param string key: Either `id`, `uri`, `label` or `sortlabel`.
428
        :param string language: The preferred language to receive the label in
429
            if key is `label` or `sortlabel`. This should be a valid IANA language tag.
430
        :rtype: :class:`str`
431
        '''
432
        if key == 'id':
433
            return str(self.id)
434
        elif key == 'uri':
435
            return self.uri if self.uri else ''
436
        else:
437
            l = label(self.labels, language, key == 'sortlabel')
438
            return l.label.lower() if l else ''
439
440
    def __repr__(self):
441
        return "Collection('%s')" % self.id
442
443
444
def label(labels=[], language='any', sortLabel=False):
445
    '''
446
    Provide a label for a list of labels.
447
448
    The items in the list of labels are assumed to be either instances of
449
    :class:`Label`, or dicts with at least the key `label` in them. These will
450
    be passed to the :func:`dict_to_label` function.
451
452
    This method tries to find a label by looking if there's
453
    a pref label for the specified language. If there's no pref label,
454
    it looks for an alt label. It disregards hidden labels.
455
456
    While matching languages, preference will be given to exact matches. But,
457
    if no exact match is present, an inexact match will be attempted. This might
458
    be because a label in language `nl-BE` is being requested, but only `nl` or
459
    even `nl-NL` is present. Similarly, when requesting `nl`, a label with
460
    language `nl-NL` or even `nl-Latn-NL` will also be considered,
461
    providing no label is present that has an exact match with the
462
    requested language.
463
464
    If language 'any' was specified, all labels will be considered,
465
    regardless of language.
466
467
    To find a label without a specified language, pass `None` as language.
468
469
    If a language or None was specified, and no label could be found, this
470
    method will automatically try to find a label in some other language.
471
472
    Finally, if no label could be found, None is returned.
473
474
    :param string language: The preferred language to receive the label in. This
475
        should be a valid IANA language tag.
476
    :param boolean sortLabel: Should sortLabels be considered or not? If True,
477
        sortLabels will be preferred over prefLabels. Bear in mind that these
478
        are still language dependent. So, it's possible to have a different
479
        sortLabel per language.
480
    '''
481
    # Normalise the tag
482
    broader_language_tag = None
483
    if language != 'any':
484
        language = tags.tag(language).format
485
        broader_language_tag = tags.tag(language).language
486
    pref = None
487
    alt = None
488
    sort = None
489
    for l in labels:
490
        l = dict_to_label(l)
491
        if language == 'any' or l.language == language:
492
            if l.type == 'prefLabel' and (pref is None or pref.language != language):
493
                pref = l
494
            if l.type == 'altLabel' and (alt is None or alt.language != language):
495
                alt = l
496
            if l.type == 'sortLabel' and (sort is None or sort.language != language):
497
                sort = l
498
        if broader_language_tag and tags.tag(l.language).language and tags.tag(l.language).language.format == broader_language_tag.format:
499
            if l.type == 'prefLabel' and pref is None:
500
                pref = l
501
            if l.type == 'altLabel' and alt is None:
502
                alt = l
503
            if l.type == 'sortLabel' and sort is None:
504
                sort = l
505
    if sortLabel and sort is not None:
506
        return sort
507
    if pref is not None:
508
        return pref
509
    elif alt is not None:
510
        return alt
511
    return label(labels, 'any', sortLabel) if language != 'any' else None
512
513
514
def dict_to_label(dict):
515
    '''
516
    Transform a dict with keys `label`, `type` and `language` into a
517
    :class:`Label`.
518
519
    Only the `label` key is mandatory. If `type` is not present, it will
520
    default to `prefLabel`. If `language` is not present, it will default
521
    to `und`.
522
523
    If the argument passed is already a :class:`Label`, this method just
524
    returns the argument.
525
    '''
526
    if isinstance(dict, Label):
527
        return dict
528
    return Label(
529
        dict['label'],
530
        dict.get('type', 'prefLabel'),
531
        dict.get('language', 'und')
532
    )
533
534
535
def dict_to_note(dict):
536
    '''
537
    Transform a dict with keys `note`, `type` and `language` into a
538
    :class:`Note`.
539
540
    Only the `note` key is mandatory. If `type` is not present, it will
541
    default to `note`. If `language` is not present, it will default to `und`.
542
    If `markup` is not present it will default to `None`.
543
544
    If the argument passed is already a :class:`Note`, this method just returns
545
    the argument.
546
    '''
547
    if isinstance(dict, Note):
548
        return dict
549
    return Note(
550
        dict['note'],
551
        dict.get('type', 'note'),
552
        dict.get('language', 'und'),
553
        dict.get('markup')
554
    )
555
556
557
def dict_to_source(dict):
558
    '''
559
    Transform a dict with key 'citation' into a :class:`Source`.
560
561
    If the argument passed is already a :class:`Source`, this method just
562
    returns the argument.
563
    '''
564
565
    if isinstance(dict, Source):
566
        return dict
567
    return Source(
568
        citation=dict['citation'],
569
    )
570