Passed
Push — issue579-batch-suggest-operati... ( 69d7dd...466504 )
by Juho
03:05
created

annif.rest._is_error()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
1
"""Definitions for REST API operations. These are wired via Connexion to
2
methods defined in the OpenAPI specification."""
3
4
import importlib
5
6
import connexion
7
8
import annif.registry
9
from annif.corpus import Document, DocumentList, SubjectSet
10
from annif.exception import AnnifException
11
from annif.project import Access
12
from annif.suggestion import SuggestionFilter
13
14
15
def project_not_found_error(project_id):
16
    """return a Connexion error object when a project is not found"""
17
18
    return connexion.problem(
19
        status=404,
20
        title="Project not found",
21
        detail="Project '{}' not found".format(project_id),
22
    )
23
24
25
def server_error(err):
26
    """return a Connexion error object when there is a server error (project
27
    or backend problem)"""
28
29
    return connexion.problem(
30
        status=503, title="Service unavailable", detail=err.format_message()
31
    )
32
33
34
def show_info():
35
    """return version of annif and a title for the api according to OpenAPI spec"""
36
37
    return {"title": "Annif REST API", "version": importlib.metadata.version("annif")}
38
39
40
def language_not_supported_error(lang):
41
    """return a Connexion error object when attempting to use unsupported language"""
42
43
    return connexion.problem(
44
        status=400,
45
        title="Bad Request",
46
        detail=f'language "{lang}" not supported by vocabulary',
47
    )
48
49
50
def list_projects():
51
    """return a dict with projects formatted according to OpenAPI spec"""
52
53
    return {
54
        "projects": [
55
            proj.dump()
56
            for proj in annif.registry.get_projects(min_access=Access.public).values()
57
        ]
58
    }
59
60
61
def show_project(project_id):
62
    """return a single project formatted according to OpenAPI spec"""
63
64
    try:
65
        project = annif.registry.get_project(project_id, min_access=Access.hidden)
66
    except ValueError:
67
        return project_not_found_error(project_id)
68
    return project.dump()
69
70
71
def _suggestion_to_dict(suggestion, subject_index, language):
72
    subject = subject_index[suggestion.subject_id]
73
    return {
74
        "uri": subject.uri,
75
        "label": subject.labels[language],
76
        "notation": subject.notation,
77
        "score": suggestion.score,
78
    }
79
80
81
def _hit_sets_to_list(hit_sets, hit_filter, subjects, lang):
82
    return [
83
        {
84
            "results": [
85
                _suggestion_to_dict(hit, subjects, lang)
86
                for hit in hit_filter(hits).as_list()
87
            ]
88
        }
89
        for hits in hit_sets
90
    ]
91
92
93
def _is_error(result):
94
    return (
95
        isinstance(result, connexion.lifecycle.ConnexionResponse)
96
        and result.status_code >= 400
97
    )
98
99
100
def suggest(project_id, body):
101
    """suggest subjects for the given text and return a dict with results
102
    formatted according to OpenAPI spec"""
103
104
    parameters = dict(
105
        (key, body[key]) for key in ["language", "limit", "threshold"] if key in body
106
    )
107
    documents = [{"text": body["text"]}]
108
    result = _suggest(project_id, documents, parameters)
109
110
    if _is_error(result):
111
        return result
112
    return result[0]
113
114
115
def suggest_batch(project_id, body, **query_parameters):
116
    """suggest subjects for the given documents and return a list of dicts with results
117
    formatted according to OpenAPI spec"""
118
119
    documents = body["documents"]
120
    result = _suggest(project_id, documents, query_parameters)
121
122
    if _is_error(result):
123
        return result
124
    for document_results, document in zip(result, documents):
125
        document_results["id"] = document.get("id")
126
    return result
127
128
129
def _suggest(project_id, documents, parameters):
130
    corpus = _documents_to_corpus(documents, subject_index=None)
131
    try:
132
        project = annif.registry.get_project(project_id, min_access=Access.hidden)
133
    except ValueError:
134
        return project_not_found_error(project_id)
135
136
    try:
137
        lang = parameters.get("language") or project.vocab_lang
138
    except AnnifException as err:
139
        return server_error(err)
140
141
    if lang not in project.vocab.languages:
142
        return language_not_supported_error(lang)
143
144
    limit = parameters.get("limit", 10)
145
    threshold = parameters.get("threshold", 0.0)
146
147
    try:
148
        hit_filter = SuggestionFilter(project.subjects, limit, threshold)
149
        hit_sets = project.suggest_corpus(corpus)
150
    except AnnifException as err:
151
        return server_error(err)
152
153
    return _hit_sets_to_list(hit_sets, hit_filter, project.subjects, lang)
154
155
156
def _documents_to_corpus(documents, subject_index):
157
    corpus = []
158
    subject_set = None
159
    for doc in documents:
160
        if subject_index is not None:
161
            subject_set = SubjectSet(
162
                [subject_index.by_uri(subj["uri"]) for subj in doc["subjects"]]
163
            )
164
        corpus.append(Document(text=doc["text"], subject_set=subject_set))
165
    return DocumentList(corpus)
166
167
168
def learn(project_id, body):
169
    """learn from documents and return an empty 204 response if succesful"""
170
171
    try:
172
        project = annif.registry.get_project(project_id, min_access=Access.hidden)
173
    except ValueError:
174
        return project_not_found_error(project_id)
175
176
    try:
177
        corpus = _documents_to_corpus(body, project.subjects)
178
        project.learn(corpus)
179
    except AnnifException as err:
180
        return server_error(err)
181
182
    return None, 204
183