Passed
Pull Request — main (#839)
by Osma
04:47 queued 01:48
created

annif.vocab.AnnifVocabulary.skos()   B

Complexity

Conditions 6

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nop 1
dl 0
loc 28
rs 8.5166
c 0
b 0
f 0
1
"""Vocabulary management functionality for Annif"""
2
3
from __future__ import annotations
4
5
import os.path
6
from typing import TYPE_CHECKING
7
8
import annif
9
import annif.corpus
10
import annif.util
11
from annif.datadir import DatadirMixin
12
from annif.exception import NotInitializedException
13
14
if TYPE_CHECKING:
15
    from rdflib.graph import Graph
16
17
    from annif.corpus.skos import SubjectFileSKOS
18
    from annif.corpus.subject import SubjectCorpus, SubjectIndex
19
20
21
logger = annif.logger
22
23
24
class AnnifVocabulary(DatadirMixin):
25
    """Class representing a subject vocabulary which can be used by multiple
26
    Annif projects."""
27
28
    # defaults for uninitialized instances
29
    _subjects = None
30
31
    # constants
32
    INDEX_FILENAME_DUMP = "subjects.dump.gz"
33
    INDEX_FILENAME_TTL = "subjects.ttl"
34
    INDEX_FILENAME_CSV = "subjects.csv"
35
36
    def __init__(self, vocab_id: str, datadir: str) -> None:
37
        DatadirMixin.__init__(self, datadir, "vocabs", vocab_id)
38
        self.vocab_id = vocab_id
39
        self._skos_vocab = None
40
41
    def _create_subject_index(self, subject_corpus: SubjectCorpus) -> SubjectIndex:
42
        subjects = annif.corpus.SubjectIndex()
43
        subjects.load_subjects(subject_corpus)
44
        annif.util.atomic_save(subjects, self.datadir, self.INDEX_FILENAME_CSV)
45
        return subjects
46
47
    def _update_subject_index(self, subject_corpus: SubjectCorpus) -> SubjectIndex:
48
        old_subjects = self.subjects
49
        new_subjects = annif.corpus.SubjectIndex()
50
        new_subjects.load_subjects(subject_corpus)
51
        updated_subjects = annif.corpus.SubjectIndex()
52
53
        for old_subject in old_subjects:
54
            if new_subjects.contains_uri(old_subject.uri):
55
                new_subject = new_subjects[new_subjects.by_uri(old_subject.uri)]
56
            else:  # subject removed from new corpus
57
                new_subject = annif.corpus.Subject(
58
                    uri=old_subject.uri, labels=None, notation=None
59
                )
60
            updated_subjects.append(new_subject)
61
        for new_subject in new_subjects:
62
            if not old_subjects.contains_uri(new_subject.uri):
63
                updated_subjects.append(new_subject)
64
        annif.util.atomic_save(updated_subjects, self.datadir, self.INDEX_FILENAME_CSV)
65
        return updated_subjects
66
67
    @property
68
    def subjects(self) -> SubjectIndex:
69
        if self._subjects is None:
70
            path = os.path.join(self.datadir, self.INDEX_FILENAME_CSV)
71
            if os.path.exists(path):
72
                logger.debug("loading subjects from %s", path)
73
                self._subjects = annif.corpus.SubjectIndex.load(path)
74
            else:
75
                raise NotInitializedException("subject file {} not found".format(path))
76
        return self._subjects
77
78
    @property
79
    def skos(self) -> SubjectFileSKOS:
80
        """return the subject vocabulary from SKOS file"""
81
        if self._skos_vocab is not None:
82
            return self._skos_vocab
83
84
        # attempt to load graph from dump file
85
        dumppath = os.path.join(self.datadir, self.INDEX_FILENAME_DUMP)
86
        if os.path.exists(dumppath):
87
            logger.debug(f"loading graph dump from {dumppath}")
88
            try:
89
                self._skos_vocab = annif.corpus.SubjectFileSKOS(dumppath)
90
            except ModuleNotFoundError:
91
                # Probably dump has been saved using a different rdflib version
92
                logger.debug("could not load graph dump, using turtle file")
93
            else:
94
                return self._skos_vocab
95
96
        # graph dump file not found - parse ttl file instead
97
        path = os.path.join(self.datadir, self.INDEX_FILENAME_TTL)
98
        if os.path.exists(path):
99
            logger.debug(f"loading graph from {path}")
100
            self._skos_vocab = annif.corpus.SubjectFileSKOS(path)
101
            # store the dump file so we can use it next time
102
            self._skos_vocab.save_skos(path)
103
            return self._skos_vocab
104
105
        raise NotInitializedException(f"graph file {path} not found")
106
107
    def __len__(self) -> int:
108
        return len(self.subjects)
109
110
    @property
111
    def languages(self) -> list[str]:
112
        try:
113
            return self.subjects.languages
114
        except NotInitializedException:
115
            return []
116
117
    def load_vocabulary(
118
        self,
119
        subject_corpus: SubjectCorpus,
120
        force: bool = False,
121
    ) -> None:
122
        """Load subjects from a subject corpus and save them into one
123
        or more subject index files as well as a SKOS/Turtle file for later
124
        use. If force=True, replace the existing subject index completely."""
125
126
        if not force and os.path.exists(
127
            os.path.join(self.datadir, self.INDEX_FILENAME_CSV)
128
        ):
129
            logger.info("updating existing subject index")
130
            self._subjects = self._update_subject_index(subject_corpus)
131
        else:
132
            logger.info("creating subject index")
133
            self._subjects = self._create_subject_index(subject_corpus)
134
135
        skosfile = os.path.join(self.datadir, self.INDEX_FILENAME_TTL)
136
        logger.info(f"saving vocabulary into SKOS file {skosfile}")
137
        subject_corpus.save_skos(skosfile)
138
139
    def as_graph(self) -> Graph:
140
        """return the vocabulary as an rdflib graph"""
141
        return self.skos.graph
142
143
    def dump(self) -> dict[str, str | list | int | bool]:
144
        """return this vocabulary as a dict"""
145
146
        try:
147
            languages = list(sorted(self.languages))
148
            size = len(self)
149
            loaded = True
150
        except NotInitializedException:
151
            languages = []
152
            size = None
153
            loaded = False
154
155
        return {
156
            "vocab_id": self.vocab_id,
157
            "languages": languages,
158
            "size": size,
159
            "loaded": loaded,
160
        }
161