Completed
Push — master ( 7c157d...b04ec2 )
by Koen
9s
created

ConceptScheme.__init__()   B

Complexity

Conditions 5

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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