annif.backend.http   A
last analyzed

Complexity

Total Complexity 17

Size/Duplication

Total Lines 112
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 84
dl 0
loc 112
rs 10
c 0
b 0
f 0
wmc 17

5 Methods

Rating   Name   Duplication   Size   Complexity  
A HTTPBackend.modification_time() 0 6 2
A HTTPBackend._get_project_info() 0 20 4
A HTTPBackend.headers() 0 8 2
A HTTPBackend.is_trained() 0 3 1
B HTTPBackend._suggest() 0 43 8
1
"""HTTP/REST client backend that makes calls to a web service
2
and returns the results"""
3
4
from __future__ import annotations
5
6
import importlib
7
from typing import TYPE_CHECKING, Any
8
9
import dateutil.parser
10
import requests
11
import requests.exceptions
12
13
from annif.exception import OperationFailedException
14
from annif.suggestion import SubjectSuggestion
15
16
from . import backend
17
18
if TYPE_CHECKING:
19
    from datetime import datetime
20
21
    from annif.corpus import Document
22
23
24
class HTTPBackend(backend.AnnifBackend):
25
    name = "http"
26
    _headers = None
27
28
    @property
29
    def headers(self) -> dict[str, str]:
30
        if self._headers is None:
31
            version = importlib.metadata.version("annif")
32
            self._headers = {
33
                "User-Agent": f"Annif/{version}",
34
            }
35
        return self._headers
36
37
    @property
38
    def is_trained(self) -> bool | None:
39
        return self._get_project_info("is_trained")
40
41
    @property
42
    def modification_time(self) -> datetime | None:
43
        mtime = self._get_project_info("modification_time")
44
        if mtime is None:
45
            return None
46
        return dateutil.parser.parse(mtime)
47
48
    def _get_project_info(self, key: str) -> bool | str | None:
49
        params = self._get_backend_params(None)
50
        try:
51
            req = requests.get(
52
                params["endpoint"].replace("/suggest", ""), headers=self.headers
53
            )
54
            req.raise_for_status()
55
        except requests.exceptions.RequestException as err:
56
            msg = f"HTTP request failed: {err}"
57
            raise OperationFailedException(msg) from err
58
        try:
59
            response = req.json()
60
        except ValueError as err:
61
            msg = f"JSON decode failed: {err}"
62
            raise OperationFailedException(msg) from err
63
64
        if key in response:
65
            return response[key]
66
        else:
67
            return None
68
69
    def _suggest(
70
        self, doc: Document, params: dict[str, Any]
71
    ) -> list[SubjectSuggestion]:
72
        data = {"text": doc.text}
73
        for key, value in doc.metadata.items():
74
            data[f"metadata_{key}"] = value
75
        if "project" in params:
76
            data["project"] = params["project"]
77
        if "limit" in params:
78
            data["limit"] = params["limit"]
79
80
        try:
81
            req = requests.post(params["endpoint"], data=data, headers=self.headers)
82
            req.raise_for_status()
83
        except requests.exceptions.RequestException as err:
84
            self.warning("HTTP request failed: {}".format(err))
85
            return []
86
87
        try:
88
            response = req.json()
89
        except ValueError as err:
90
            self.warning("JSON decode failed: {}".format(err))
91
            return []
92
93
        if "results" in response:
94
            results = response["results"]
95
        else:
96
            results = response
97
98
        try:
99
            subject_suggestions = [
100
                SubjectSuggestion(
101
                    subject_id=self.project.subjects.by_uri(hit["uri"]),
102
                    score=hit["score"],
103
                )
104
                for hit in results
105
                if hit["score"] > 0.0
106
            ]
107
        except (TypeError, ValueError) as err:
108
            self.warning("Problem interpreting JSON data: {}".format(err))
109
            return []
110
111
        return subject_suggestions
112