# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
log = logging.getLogger(__name__)
from language_tags import tags
from sqlalchemy import (
Column,
Integer,
Text,
String,
ForeignKey,
UniqueConstraint,
Table,
event
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
relationship,
backref
)
Base = declarative_base()
concept_label = Table(
'concept_label',
Base.metadata,
Column('concept_id', Integer, ForeignKey('concept.id'), primary_key=True),
Column('label_id', Integer, ForeignKey('label.id'), primary_key=True)
)
conceptscheme_label = Table(
'conceptscheme_label',
Base.metadata,
Column(
'conceptscheme_id',
Integer,
ForeignKey('conceptscheme.id'),
primary_key=True
),
Column('label_id', Integer, ForeignKey('label.id'), primary_key=True)
)
concept_note = Table(
'concept_note',
Base.metadata,
Column('concept_id', Integer, ForeignKey('concept.id'), primary_key=True),
Column('note_id', Integer, ForeignKey('note.id'), primary_key=True)
)
conceptscheme_note = Table(
'conceptscheme_note',
Base.metadata,
Column(
'conceptscheme_id',
Integer,
ForeignKey('conceptscheme.id'),
primary_key=True
),
Column('note_id', Integer, ForeignKey('note.id'), primary_key=True)
)
collection_concept = Table(
'collection_concept',
Base.metadata,
Column(
'collection_id',
Integer,
ForeignKey('concept.id'),
primary_key=True
),
Column('concept_id', Integer, ForeignKey('concept.id'), primary_key=True)
)
concept_related_concept = Table(
'concept_related_concept',
Base.metadata,
Column(
'concept_id_from',
Integer,
ForeignKey('concept.id'),
primary_key=True
),
Column(
'concept_id_to',
Integer,
ForeignKey('concept.id'),
primary_key=True
)
)
concept_hierarchy_concept = Table(
'concept_hierarchy_concept',
Base.metadata,
Column(
'concept_id_broader',
Integer,
ForeignKey('concept.id'),
primary_key=True
),
Column(
'concept_id_narrower',
Integer,
ForeignKey('concept.id'),
primary_key=True
)
)
concept_hierarchy_collection = Table(
'concept_hierarchy_collection',
Base.metadata,
Column(
'concept_id_broader',
Integer,
ForeignKey('concept.id'),
primary_key=True
),
Column(
'collection_id_narrower',
Integer,
ForeignKey('concept.id'),
primary_key=True
)
)
[docs]class Thing(Base):
'''
Abstract class for both :class:`Concept` and :class:`Collection`.
'''
__tablename__ = 'concept'
__table_args__ = (
UniqueConstraint('conceptscheme_id', 'concept_id'),
)
id = Column(Integer, primary_key=True)
type = Column(String(30))
concept_id = Column(
Integer,
nullable=False,
index=True
)
uri = Column(String(512))
labels = relationship(
'Label',
secondary=concept_label,
backref=backref('concept', uselist=False),
cascade='all, delete-orphan',
single_parent=True
)
notes = relationship(
'Note',
secondary=concept_note,
backref=backref('concept', uselist=False),
cascade='all, delete-orphan',
single_parent=True
)
conceptscheme = relationship('ConceptScheme', backref='concepts')
conceptscheme_id = Column(
Integer,
ForeignKey('conceptscheme.id'),
nullable=False,
index=True
)
def label(self, language='any'):
return label(self.labels, language)
__mapper_args__ = {
'polymorphic_on': 'type',
'polymorphic_identity': 'thing'
}
[docs]class Concept(Thing):
'''
A concept as know by :term:`skosprovider:SKOS`.
'''
related_concepts = relationship(
'Concept',
secondary=concept_related_concept,
primaryjoin='Concept.id==concept_related_concept.c.concept_id_to',
secondaryjoin='Concept.id==concept_related_concept.c.concept_id_from',
collection_class=set
)
narrower_concepts = relationship(
'Concept',
secondary=concept_hierarchy_concept,
backref=backref('broader_concepts', collection_class=set),
primaryjoin='Concept.id==concept_hierarchy_concept.c.concept_id_broader',
secondaryjoin='Concept.id==concept_hierarchy_concept.c.concept_id_narrower',
collection_class=set
)
narrower_collections = relationship(
'Collection',
secondary=concept_hierarchy_collection,
backref=backref('broader_concepts', collection_class=set),
primaryjoin='Concept.id==concept_hierarchy_collection.c.concept_id_broader',
secondaryjoin='Concept.id==concept_hierarchy_collection.c.collection_id_narrower',
collection_class=set
)
__mapper_args__ = {
'polymorphic_identity': 'concept'
}
event.listen(
Concept.related_concepts,
'append',
related_concepts_append_listener
)
event.listen(Concept.related_concepts, 'remove', related_concepts_remove_listener)
[docs]class Collection(Thing):
'''
A collection as know by :term:`skosprovider:SKOS`.
'''
__mapper_args__ = {
'polymorphic_identity': 'collection'
}
members = relationship(
'Thing',
secondary=collection_concept,
backref=backref('member_of', collection_class=set),
primaryjoin='Thing.id==collection_concept.c.collection_id',
secondaryjoin='Thing.id==collection_concept.c.concept_id',
collection_class=set
)
[docs]class ConceptScheme(Base):
'''
A :term:`skosprovider:SKOS` conceptscheme.
'''
__tablename__ = 'conceptscheme'
id = Column(Integer, primary_key=True)
uri = Column(String(512))
labels = relationship(
'Label',
secondary=conceptscheme_label,
backref=backref('conceptscheme', uselist=False)
)
notes = relationship(
'Note',
secondary=conceptscheme_note,
backref=backref('conceptscheme', uselist=False)
)
def label(self, language='any'):
return label(self.labels, language)
[docs]class Language(Base):
'''
A Language.
'''
__tablename__ = 'language'
id = Column(String(64), primary_key=True)
name = Column(String(255))
def __init__(self, id, name):
self.id = id
self.name = name
def __str__(self):
return self.name
[docs]class LabelType(Base):
'''
A labelType according to :term:`skosprovider:SKOS`.
'''
__tablename__ = 'labeltype'
name = Column(String(20), primary_key=True)
description = Column(Text)
def __init__(self, name, description):
self.name = name
self.description = description
def __str__(self):
return self.name
[docs]class Label(Base):
'''
A label for a :class:`Concept`, :class:`Collection` or
:class:`ConceptScheme`.
'''
__tablename__ = 'label'
id = Column(Integer, primary_key=True)
label = Column(
String(512),
nullable=False
)
labeltype = relationship('LabelType', uselist=False)
language = relationship('Language', uselist=False)
labeltype_id = Column(
String(20),
ForeignKey('labeltype.name'),
nullable=False,
index=True
)
language_id = Column(
String(64),
ForeignKey('language.id'),
nullable=True,
index=True
)
def __init__(self, label, labeltype_id='prefLabel', language_id=None):
self.labeltype_id = labeltype_id
self.language_id = language_id
self.label = label
def __str__(self):
return self.label
[docs]class NoteType(Base):
'''
A noteType according to :term:`skosprovider:SKOS`.
'''
__tablename__ = 'notetype'
name = Column(String(20), primary_key=True)
description = Column(Text)
def __init__(self, name, description):
self.name = name
self.description = description
def __str__(self):
return self.name
[docs]class Note(Base):
'''
A note for a :class:`Concept`, :class:`Collection` or
:class:`ConceptScheme`.
'''
__tablename__ = 'note'
id = Column(Integer, primary_key=True)
note = Column(
Text,
nullable=False
)
notetype = relationship('NoteType', uselist=False)
notetype_id = Column(
String(20),
ForeignKey('notetype.name'),
nullable=False,
index=True
)
language = relationship('Language', uselist=False)
language_id = Column(
String(64),
ForeignKey('language.id'),
nullable=True,
index=True
)
def __init__(self, note, notetype_id, language_id):
self.notetype_id = notetype_id
self.language_id = language_id
self.note = note
def __str__(self):
return self.note
[docs]class MatchType(Base):
'''
A matchType according to :term:`skosprovider:SKOS`.
'''
__tablename__ = 'matchtype'
name = Column(String(20), primary_key=True)
description = Column(Text)
def __init__(self, name, description):
self.name = name
self.description = description
def __str__(self):
return self.name
[docs]class Match(Base):
'''
A match between a :class:`Concept` in one ConceptScheme and those in
another one.
'''
__tablename__ = 'match'
concept = relationship('Concept', backref=backref('matches',
cascade='save-update, merge, '
'delete, delete-orphan'))
concept_id = Column(
Integer,
ForeignKey('concept.id'),
primary_key=True
)
matchtype = relationship('MatchType', uselist=False)
matchtype_id = Column(
String(20),
ForeignKey('matchtype.name'),
primary_key=True
)
uri = Column(
String(512),
primary_key=True
)
def __str__(self):
return self.uri
[docs]class Visitation(Base):
'''
Holds several nested sets.
The visitation object and table hold several nested sets. Each
:class:`skosprovider_sqlalchemy.models.Visitation` holds the positional
information for one conceptplacement in a certain nested set.
Each conceptscheme gets its own separate nested set.
'''
__tablename__ = 'visitation'
id = Column(Integer, primary_key=True)
lft = Column(Integer, index=True, nullable=False)
rght = Column(Integer, index=True, nullable=False)
depth = Column(Integer, index=True, nullable=False)
conceptscheme = relationship('ConceptScheme')
conceptscheme_id = Column(
Integer,
ForeignKey('conceptscheme.id'),
nullable=False,
index=True
)
concept = relationship('Concept')
concept_id = Column(
Integer,
ForeignKey('concept.id'),
nullable=False,
index=True
)
[docs]def label(labels=[], language='any'):
'''
Provide a label for a list of labels.
The items in the list of labels are assumed to be instances of
:class:`Label`.
This method tries to find a label by looking if there's
a pref label for the specified language. If there's no pref label,
it looks for an alt label. It disregards hidden labels.
While matching languages, preference will be given to exact matches. But,
if no exact match is present, an inexact match will be attempted. This might
be because a label in language `nl-BE` is being requested, but only `nl` or
even `nl-NL` is present. Similarly, when requesting `nl`, a label with
language `nl-NL` or even `nl-Latn-NL` will also be considered,
providing no label is present that has an exact match with the
requested language.
If language 'any' was specified, all labels will be considered,
regardless of language.
To find a label without a specified language, pass `None` as language.
If a language or None was specified, and no label could be found, this
method will automatically try to find a label in some other language.
Finally, if no label could be found, None is returned.
:param list labels: A list of :class:`labels <Label>`.
:param str language: The language for which a label should preferentially
be returned. This should be a valid IANA language tag.
:rtype: A :class:`Label` or `None` if no label could be found.
'''
# Normalise the tag
broader_language_tag = None
if language != 'any':
language = tags.tag(language).format
broader_language_tag = tags.tag(language).language
pref = None
alt = None
for l in labels:
labeltype = l.labeltype_id or l.labeltype.name
if language == 'any' or l.language_id == language:
if labeltype == 'prefLabel' and (pref is None or pref.language_id != language):
pref = l
if labeltype == 'altLabel' and (alt is None or alt.language_id != language):
alt = l
if broader_language_tag and tags.tag(l.language_id).language and tags.tag(
l.language_id).language.format == broader_language_tag.format:
if labeltype == 'prefLabel' and pref is None:
pref = l
if labeltype == 'altLabel' and alt is None:
alt = l
if pref is not None:
return pref
elif alt is not None:
return alt
return label(labels, 'any') if language != 'any' else None
[docs]class Initialiser(object):
'''
Initialises a database.
Adds necessary values for labelType, noteType and language to the database.
The list of languages added by default is very small and will probably need
to be expanded for your local needs.
'''
def __init__(self, session):
self.session = session
[docs] def init_all(self):
'''
Initialise all objects (labeltype, notetype, language).
'''
self.init_labeltype()
self.init_notetype()
self.init_matchtypes()
self.init_languages()
[docs] def init_notetype(self):
'''
Initialise the notetypes.
'''
notetypes = [
('changeNote', 'A change note.'),
('definition', 'A definition.'),
('editorialNote', 'An editorial note.'),
('example', 'An example.'),
('historyNote', 'A historynote.'),
('scopeNote', 'A scopenote.'),
('note', 'A note.')
]
for n in notetypes:
nt = NoteType(n[0], n[1])
self.session.add(nt)
[docs] def init_labeltype(self):
'''
Initialise the labeltypes.
'''
labeltypes = [
('hiddenLabel', 'A hidden label.'),
('altLabel', 'An alternative label.'),
('prefLabel', 'A preferred label.')
]
for l in labeltypes:
lt = LabelType(l[0], l[1])
self.session.add(lt)
[docs] def init_matchtypes(self):
'''
Initialise the matchtypes.
'''
matchtypes = [
('closeMatch',
'Indicates that two concepts are sufficiently similar that they can be used interchangeably in some information retrieval applications.'),
('exactMatch',
'Indicates that there is a high degree of confidence that two concepts can be used interchangeably across a wide range of information retrieval applications.'),
('broadMatch', 'Indicates that one concept has a broader match with another one.'),
('narrowMatch', 'Indicates that one concept has a narrower match with another one.'),
('relatedMatch', 'Indicates that there is an associative mapping between two concepts.')
]
for m in matchtypes:
mt = MatchType(m[0], m[1])
self.session.add(mt)
[docs] def init_languages(self):
'''
Initialise the languages.
Only adds a small set of languages. Will probably not be sufficient
for most use cases.
'''
languages = [
('la', 'Latin'),
('nl', 'Dutch'),
('vls', '(West) Flemish'),
('en', 'English'),
('fr', 'French'),
('de', 'German')
]
for l in languages:
lan = Language(l[0], l[1])
self.session.add(lan)