Completed
Push — master ( 3be9da...64710f )
by Koen
01:05
created

Concept.__init__()   B

Complexity

Conditions 5

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

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