labfreed 1.0.0a24__py3-none-any.whl → 1.0.0b26__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labfreed/__init__.py +1 -1
- labfreed/labfreed_extended/app/app_infrastructure.py +2 -5
- labfreed/labfreed_extended/app/pac_info/pac_info.py +6 -8
- labfreed/labfreed_extended/pac_issuer_lib/lib/attribute.py +9 -6
- labfreed/labfreed_extended/pac_issuer_lib/static/external-link.svg +6 -6
- labfreed/labfreed_extended/pac_issuer_lib/static/logo.svg +44 -44
- labfreed/labfreed_extended/pac_issuer_lib/templates/main.jinja.html +4 -3
- labfreed/pac_attributes/api_data_models/request.py +78 -30
- labfreed/pac_attributes/api_data_models/response.py +107 -136
- labfreed/pac_attributes/client/client.py +30 -36
- labfreed/pac_attributes/client/client_attribute_group.py +18 -0
- labfreed/pac_attributes/pythonic/attribute_server_factory.py +39 -13
- labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -1
- labfreed/pac_attributes/pythonic/py_attributes.py +97 -125
- labfreed/pac_attributes/server/attribute_data_sources.py +9 -8
- labfreed/pac_attributes/server/server.py +33 -37
- labfreed/pac_id_resolver/resolver_config.py +2 -1
- {labfreed-1.0.0a24.dist-info → labfreed-1.0.0b26.dist-info}/METADATA +1 -1
- {labfreed-1.0.0a24.dist-info → labfreed-1.0.0b26.dist-info}/RECORD +21 -27
- labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -7
- labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -188
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -176
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -46
- labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -7
- labfreed/pac_attributes/client/__init__.py +0 -0
- labfreed/pac_attributes/client/attribute_cache.py +0 -65
- {labfreed-1.0.0a24.dist-info → labfreed-1.0.0b26.dist-info}/WHEEL +0 -0
- {labfreed-1.0.0a24.dist-info → labfreed-1.0.0b26.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,40 +2,47 @@
|
|
|
2
2
|
from abc import ABC
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
import re
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Annotated, Any, Literal, Union, get_args
|
|
6
6
|
from urllib.parse import urlparse
|
|
7
7
|
|
|
8
8
|
from labfreed.utilities.ensure_utc_time import ensure_utc
|
|
9
9
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
10
|
-
from pydantic import Field, field_validator, model_validator
|
|
10
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
|
|
14
|
+
class AttributeItemsElementBase(LabFREED_BaseModel, ABC):
|
|
15
15
|
value: Any
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def __init__(self, **data):
|
|
19
|
-
# Automatically inject the Literal value for `type`
|
|
20
|
-
discriminator_value = self._get_discriminator_value()
|
|
21
|
-
data["type"] = discriminator_value
|
|
22
|
-
super().__init__(**data)
|
|
16
|
+
type:str
|
|
23
17
|
|
|
24
|
-
@
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
literal_value = get_args(type_annotation)[0]
|
|
30
|
-
return literal_value
|
|
31
|
-
except Exception as e:
|
|
32
|
-
raise TypeError(
|
|
33
|
-
f"{cls.__name__} must define `type: Literal[<value>]` annotation"
|
|
34
|
-
) from e
|
|
18
|
+
@model_validator(mode="after")
|
|
19
|
+
def _no_base_instances(self):
|
|
20
|
+
if type(self) is AttributeItemsElementBase:
|
|
21
|
+
raise TypeError("AttributeItemsElementBase must not be instantiated")
|
|
22
|
+
return self
|
|
35
23
|
|
|
24
|
+
# def __init__(self, **data):
|
|
25
|
+
# # Automatically inject the Literal value for `type`
|
|
26
|
+
# discriminator_value = self._get_discriminator_value()
|
|
27
|
+
# data["type"] = discriminator_value
|
|
28
|
+
# super().__init__(**data)
|
|
29
|
+
|
|
30
|
+
# @classmethod
|
|
31
|
+
# def _get_discriminator_value(cls) -> str:
|
|
32
|
+
# """Extract the Literal value from the 'type' annotation."""
|
|
33
|
+
# try:
|
|
34
|
+
# type_annotation = cls.__annotations__["type"]
|
|
35
|
+
# literal_value = get_args(type_annotation)[0]
|
|
36
|
+
# return literal_value
|
|
37
|
+
# except Exception as e:
|
|
38
|
+
# raise TypeError(
|
|
39
|
+
# f"{cls.__name__} must define `type: Literal[<value>]` annotation"
|
|
40
|
+
# ) from e
|
|
41
|
+
|
|
42
|
+
|
|
36
43
|
|
|
37
|
-
class
|
|
38
|
-
type: Literal["datetime"]
|
|
44
|
+
class DateTimeAttributeItemsElement(AttributeItemsElementBase):
|
|
45
|
+
type: Literal["datetime"] = "datetime"
|
|
39
46
|
value: datetime
|
|
40
47
|
|
|
41
48
|
@field_validator('value', mode='after')
|
|
@@ -45,53 +52,24 @@ class DateTimeAttribute(AttributeBase):
|
|
|
45
52
|
else:
|
|
46
53
|
return value
|
|
47
54
|
|
|
48
|
-
class DateTimeListAttribute(AttributeBase):
|
|
49
|
-
type: Literal["datetime-list"]
|
|
50
|
-
value: list[datetime]
|
|
51
|
-
|
|
52
|
-
@field_validator('value', mode='after')
|
|
53
|
-
def set_utc__if_naive(cls, value):
|
|
54
|
-
value_out = []
|
|
55
|
-
for v in value:
|
|
56
|
-
if isinstance(v, datetime):
|
|
57
|
-
value_out.append(ensure_utc(v))
|
|
58
|
-
else:
|
|
59
|
-
raise ValueError(f'{v} is of type {type(v)}. It must be datetime')
|
|
60
|
-
return value_out
|
|
61
55
|
|
|
62
56
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type: Literal["bool"]
|
|
57
|
+
class BoolAttributeItemsElement(AttributeItemsElementBase):
|
|
58
|
+
type: Literal["bool"] = "bool"
|
|
66
59
|
value: bool
|
|
67
60
|
|
|
68
|
-
class BoolListAttribute(AttributeBase):
|
|
69
|
-
type: Literal["bool-list"]
|
|
70
|
-
value: list[bool]
|
|
71
|
-
|
|
72
61
|
|
|
73
62
|
|
|
74
63
|
|
|
75
|
-
class
|
|
76
|
-
type: Literal["text"]
|
|
64
|
+
class TextAttributeItemsElement(AttributeItemsElementBase):
|
|
65
|
+
type: Literal["text"] = "text"
|
|
77
66
|
value: str
|
|
78
67
|
|
|
79
68
|
@model_validator(mode='after')
|
|
80
69
|
def _validate_value(self):
|
|
81
70
|
_validate_text(self, self.value)
|
|
82
71
|
return self
|
|
83
|
-
|
|
84
|
-
class TextListAttribute(AttributeBase):
|
|
85
|
-
type: Literal["text-list"]
|
|
86
|
-
value: list[str]
|
|
87
|
-
|
|
88
|
-
@model_validator(mode='after')
|
|
89
|
-
def _validate_value(self):
|
|
90
|
-
l = [self.value] if isinstance(self.value, str) else self.value
|
|
91
|
-
for v in l:
|
|
92
|
-
_validate_text(self, v)
|
|
93
|
-
return self
|
|
94
|
-
|
|
72
|
+
|
|
95
73
|
|
|
96
74
|
def _validate_text(mdl:LabFREED_BaseModel, v):
|
|
97
75
|
if len(v) > 5000:
|
|
@@ -104,19 +82,14 @@ def _validate_text(mdl:LabFREED_BaseModel, v):
|
|
|
104
82
|
|
|
105
83
|
|
|
106
84
|
|
|
107
|
-
class
|
|
108
|
-
type: Literal["reference"]
|
|
85
|
+
class ReferenceAttributeItemsElement(AttributeItemsElementBase):
|
|
86
|
+
type: Literal["reference"] = "reference"
|
|
109
87
|
value: str
|
|
110
88
|
|
|
111
|
-
|
|
112
|
-
type: Literal["reference-list"]
|
|
113
|
-
value: list[str]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
89
|
+
|
|
117
90
|
|
|
118
|
-
class
|
|
119
|
-
type: Literal["resource"]
|
|
91
|
+
class ResourceAttributeItemsElement(AttributeItemsElementBase):
|
|
92
|
+
type: Literal["resource"] = "resource"
|
|
120
93
|
value: str
|
|
121
94
|
|
|
122
95
|
@model_validator(mode='after')
|
|
@@ -124,16 +97,6 @@ class ResourceAttribute(AttributeBase):
|
|
|
124
97
|
_validate_resource(self, self.value)
|
|
125
98
|
return self
|
|
126
99
|
|
|
127
|
-
class ResourceListAttribute(AttributeBase):
|
|
128
|
-
type: Literal["resource-list"]
|
|
129
|
-
value: list[str]
|
|
130
|
-
|
|
131
|
-
@model_validator(mode='after')
|
|
132
|
-
def _validate_value(self):
|
|
133
|
-
value_list = self.value if isinstance(self.value, list) else [self.value]
|
|
134
|
-
for v in value_list:
|
|
135
|
-
_validate_resource(self, v)
|
|
136
|
-
return self
|
|
137
100
|
|
|
138
101
|
def _validate_resource(mdl:LabFREED_BaseModel, v):
|
|
139
102
|
r = urlparse(v)
|
|
@@ -153,15 +116,23 @@ def _validate_resource(mdl:LabFREED_BaseModel, v):
|
|
|
153
116
|
highlight_pattern = f'{v}'
|
|
154
117
|
)
|
|
155
118
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
119
|
+
|
|
120
|
+
class NumericAttributeItemsElement(AttributeItemsElementBase):
|
|
121
|
+
type: Literal["numeric"] = "numeric"
|
|
122
|
+
value: str
|
|
123
|
+
_numerical_value:str
|
|
124
|
+
_unit:str
|
|
161
125
|
|
|
162
126
|
@model_validator(mode='after')
|
|
127
|
+
def _validate_model(self):
|
|
128
|
+
self._numerical_value, self._unit = self.value.split(' ', 1)
|
|
129
|
+
self._validate_value()
|
|
130
|
+
self._validate_unit()
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
|
|
163
134
|
def _validate_value(self):
|
|
164
|
-
value = self.
|
|
135
|
+
value = self._numerical_value
|
|
165
136
|
if not_allowed_chars := set(re.sub(r'[0-9\.\-\+Ee]', '', value)):
|
|
166
137
|
self._add_validation_message(
|
|
167
138
|
source="Numeric Attribute",
|
|
@@ -177,81 +148,76 @@ class NumericValue(LabFREED_BaseModel):
|
|
|
177
148
|
msg=f"{value} cannot be converted to number",
|
|
178
149
|
highlight_pattern = f'{value}'
|
|
179
150
|
)
|
|
180
|
-
return self
|
|
181
151
|
|
|
182
|
-
|
|
183
|
-
def _validate_units(self):
|
|
152
|
+
def _validate_unit(self):
|
|
184
153
|
'''A sanity check on unit complying with UCUM. NOTE: It is not a complete validation
|
|
185
154
|
- I check for blankspaces and ^, which are often used for units, but are invalid.
|
|
186
155
|
- the general structure of a ucum unit is validated, but 1)parentheses are not matched 2) units are not validated 3)prefixes are not checked
|
|
187
156
|
'''
|
|
188
|
-
if ' ' in self.
|
|
157
|
+
if ' ' in self._unit or '^' in self._unit:
|
|
189
158
|
self._add_validation_message(
|
|
190
159
|
source="Numeric Attribute",
|
|
191
160
|
level= ValidationMsgLevel.ERROR,
|
|
192
|
-
msg=f"Unit {self.
|
|
193
|
-
highlight_pattern = self.
|
|
161
|
+
msg=f"Unit {self._unit} is invalid. Must not contain blankspace or '^'.",
|
|
162
|
+
highlight_pattern = self._unit
|
|
194
163
|
)
|
|
195
|
-
elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self.
|
|
164
|
+
elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self._unit):
|
|
196
165
|
self._add_validation_message(
|
|
197
166
|
source="Numeric Attribute",
|
|
198
167
|
level= ValidationMsgLevel.WARNING,
|
|
199
|
-
msg=f"Unit {self.
|
|
200
|
-
highlight_pattern = self.
|
|
168
|
+
msg=f"Unit {self._unit} is probably invalid. Ensure it complies with UCUM specifications.",
|
|
169
|
+
highlight_pattern = self._unit
|
|
201
170
|
)
|
|
202
|
-
return self
|
|
203
171
|
|
|
204
172
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
class NumericAttribute(AttributeBase):
|
|
208
|
-
type: Literal["numeric"]
|
|
209
|
-
value: NumericValue
|
|
210
|
-
|
|
211
|
-
class NumericListAttribute(AttributeBase):
|
|
212
|
-
type: Literal["numeric-list"]
|
|
213
|
-
value: list[NumericValue]
|
|
214
|
-
|
|
215
173
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
type: Literal["object"]
|
|
174
|
+
class ObjectAttributeItemsElement(AttributeItemsElementBase):
|
|
175
|
+
type: Literal["object"] = "object"
|
|
219
176
|
value: dict[str, Any]
|
|
220
177
|
|
|
221
178
|
|
|
222
|
-
|
|
223
|
-
type: Literal["object-list"]
|
|
224
|
-
value: list[dict[str, Any]]
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Attribute = Annotated[
|
|
179
|
+
AttributeItemsElement = Annotated[
|
|
230
180
|
Union[
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
ReferenceListAttribute,
|
|
240
|
-
DateTimeListAttribute,
|
|
241
|
-
BoolListAttribute,
|
|
242
|
-
TextListAttribute,
|
|
243
|
-
NumericListAttribute,
|
|
244
|
-
ResourceListAttribute,
|
|
245
|
-
ObjectListAttribute
|
|
181
|
+
DateTimeAttributeItemsElement,
|
|
182
|
+
BoolAttributeItemsElement,
|
|
183
|
+
TextAttributeItemsElement,
|
|
184
|
+
NumericAttributeItemsElement,
|
|
185
|
+
ReferenceAttributeItemsElement,
|
|
186
|
+
ResourceAttributeItemsElement,
|
|
187
|
+
ObjectAttributeItemsElement
|
|
246
188
|
],
|
|
247
|
-
Field(discriminator="type")
|
|
189
|
+
Field(discriminator="type"),
|
|
248
190
|
]
|
|
191
|
+
|
|
249
192
|
|
|
193
|
+
|
|
194
|
+
class Attribute(LabFREED_BaseModel):
|
|
195
|
+
key: str|None = Field(exclude=True)
|
|
196
|
+
label: str = ""
|
|
197
|
+
items: list[AttributeItemsElement]
|
|
198
|
+
|
|
199
|
+
|
|
250
200
|
|
|
251
201
|
class AttributeGroup(LabFREED_BaseModel):
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
attributes:
|
|
202
|
+
group_key: str
|
|
203
|
+
group_label: str = ""
|
|
204
|
+
attributes: dict[str, Attribute]
|
|
205
|
+
|
|
206
|
+
@field_validator("attributes", mode="before")
|
|
207
|
+
@classmethod
|
|
208
|
+
def set_attribute_keys(cls, v):
|
|
209
|
+
if not isinstance(v, dict):
|
|
210
|
+
return v
|
|
211
|
+
|
|
212
|
+
out = {}
|
|
213
|
+
for k, a in v.items():
|
|
214
|
+
if isinstance(a, dict):
|
|
215
|
+
# raw input dict -> inject key if missing
|
|
216
|
+
out[k] = {**a, "key": a.get("key") or k}
|
|
217
|
+
else:
|
|
218
|
+
# already an Attribute (or something pydantic can parse)
|
|
219
|
+
out[k] = a
|
|
220
|
+
return out
|
|
255
221
|
|
|
256
222
|
|
|
257
223
|
|
|
@@ -260,14 +226,19 @@ class AttributesOfPACID(LabFREED_BaseModel):
|
|
|
260
226
|
attribute_groups: list[AttributeGroup]
|
|
261
227
|
|
|
262
228
|
|
|
229
|
+
|
|
230
|
+
IMPORT_URL = "https://vocab.labfreed.org/attributes/v1.jsonld"
|
|
263
231
|
|
|
264
232
|
class AttributeResponsePayload(LabFREED_BaseModel):
|
|
265
233
|
schema_version: str = Field(default='1.0')
|
|
266
234
|
language:str
|
|
267
|
-
|
|
235
|
+
data: list[AttributesOfPACID]
|
|
236
|
+
|
|
237
|
+
context: str = Field(alias='@context', default=IMPORT_URL)
|
|
238
|
+
|
|
268
239
|
|
|
269
240
|
def to_json(self):
|
|
270
|
-
return self.model_dump_json(exclude_none=True)
|
|
241
|
+
return self.model_dump_json(exclude_none=True, by_alias=True)
|
|
271
242
|
|
|
272
243
|
|
|
273
244
|
|
|
@@ -3,14 +3,16 @@ from __future__ import annotations
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from datetime import UTC, datetime
|
|
5
5
|
from typing import Protocol, runtime_checkable
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
import warnings
|
|
6
8
|
|
|
7
9
|
import requests
|
|
8
10
|
|
|
9
11
|
from pydantic import ValidationError
|
|
10
12
|
|
|
11
|
-
from labfreed.pac_attributes.api_data_models.request import
|
|
12
|
-
from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload
|
|
13
|
-
from labfreed.pac_attributes.client.
|
|
13
|
+
from labfreed.pac_attributes.api_data_models.request import AttributeRequestData
|
|
14
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeGroup, AttributeResponsePayload
|
|
15
|
+
from labfreed.pac_attributes.client.client_attribute_group import ClientAttributeGroup
|
|
14
16
|
from labfreed.pac_attributes.server.server import AttributeServerRequestHandler
|
|
15
17
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
16
18
|
|
|
@@ -31,7 +33,7 @@ class AttributeServerError(Exception):
|
|
|
31
33
|
|
|
32
34
|
@runtime_checkable
|
|
33
35
|
class AttributeRequestCallback(Protocol):
|
|
34
|
-
def __call__(self, url: str,
|
|
36
|
+
def __call__(self, url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
|
|
35
37
|
'''handle the request
|
|
36
38
|
returns a tuple of HTTP status code and the body of the response or an error message'''
|
|
37
39
|
...
|
|
@@ -49,9 +51,14 @@ def http_attribute_request_default_callback_factory(session: requests.Session =
|
|
|
49
51
|
if session is None:
|
|
50
52
|
session = requests.Session()
|
|
51
53
|
|
|
52
|
-
def callback(url: str,
|
|
54
|
+
def callback(url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
|
|
53
55
|
try:
|
|
54
|
-
|
|
56
|
+
url = url + '/' + quote(attribute_request_data.pac_id, safe='')
|
|
57
|
+
params = attribute_request_data.request_params()
|
|
58
|
+
resp = session.get(url,
|
|
59
|
+
params = params,
|
|
60
|
+
headers=attribute_request_data.language_preference_http_header(),
|
|
61
|
+
timeout=10)
|
|
55
62
|
return resp.status_code, resp.text
|
|
56
63
|
except requests.exceptions.RequestException as e:
|
|
57
64
|
return 500, str(e)
|
|
@@ -68,9 +75,9 @@ def local_attribute_request_callback_factory(request_handler:AttributeServerRequ
|
|
|
68
75
|
AttributeRequestCallback: a callback following the AttributeRequestCallback protocol.
|
|
69
76
|
"""
|
|
70
77
|
|
|
71
|
-
def callback(url: str,
|
|
78
|
+
def callback(url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
|
|
72
79
|
try:
|
|
73
|
-
resp = request_handler.handle_attribute_request(
|
|
80
|
+
resp = request_handler.handle_attribute_request(attribute_request_data)
|
|
74
81
|
return 200, resp
|
|
75
82
|
except requests.exceptions.RequestException as e:
|
|
76
83
|
return 500, str(e)
|
|
@@ -83,18 +90,14 @@ def local_attribute_request_callback_factory(request_handler:AttributeServerRequ
|
|
|
83
90
|
class AttributeClient():
|
|
84
91
|
""" Client handling attribute requests and caching thereof.
|
|
85
92
|
"""
|
|
86
|
-
|
|
87
93
|
http_post_callback:AttributeRequestCallback
|
|
88
|
-
|
|
89
|
-
always_use_cached_value_for_minutes:int
|
|
90
|
-
|
|
94
|
+
|
|
91
95
|
def get_attributes(self,
|
|
92
96
|
server_url:str,
|
|
93
97
|
pac_id:PAC_ID|str,
|
|
94
98
|
restrict_to_attribute_groups:list[str]|None=None,
|
|
95
|
-
language_preferences:list[str]|None=None
|
|
96
|
-
|
|
97
|
-
) -> list[CacheableAttributeGroup]:
|
|
99
|
+
language_preferences:list[str]|None=None
|
|
100
|
+
) -> list[AttributeGroup]:
|
|
98
101
|
"""gets the attributes from one attribute server for one PAC-ID. Uses a cached version if possible, otherwise requests from the server again.
|
|
99
102
|
|
|
100
103
|
Args:
|
|
@@ -114,28 +117,21 @@ class AttributeClient():
|
|
|
114
117
|
"""
|
|
115
118
|
if isinstance(pac_id, str):
|
|
116
119
|
pac_id = PAC_ID.from_url(pac_id)
|
|
117
|
-
|
|
118
|
-
# try the cache
|
|
119
|
-
if not force_server_request:
|
|
120
|
-
if restrict_to_attribute_groups:
|
|
121
|
-
attribute_groups = self.cache_store.get_attribute_groups(server_url, pac_id, restrict_to_attribute_groups)
|
|
122
|
-
else:
|
|
123
|
-
attribute_groups = self.cache_store.get_all(server_url, pac_id)
|
|
124
120
|
|
|
125
|
-
if attribute_groups and all([ag.still_valid(accept_cache_for_minutes=self.always_use_cached_value_for_minutes) for ag in attribute_groups]):
|
|
126
|
-
return attribute_groups
|
|
127
|
-
|
|
128
121
|
# no valid data found in cache > request to server
|
|
129
|
-
attribute_request_body =
|
|
130
|
-
|
|
131
|
-
|
|
122
|
+
attribute_request_body = AttributeRequestData(pac_id=pac_id.to_url(),
|
|
123
|
+
restrict_to_attribute_groups=restrict_to_attribute_groups,
|
|
124
|
+
language_preferences=language_preferences
|
|
132
125
|
)
|
|
133
|
-
response_code, response_body_str = self.http_post_callback(server_url, attribute_request_body
|
|
126
|
+
response_code, response_body_str = self.http_post_callback(server_url, attribute_request_body)
|
|
134
127
|
|
|
135
128
|
if response_code == 400:
|
|
136
129
|
raise AttributeClientInternalError(f"The server did not accept the request. Server message: '{response_body_str}'")
|
|
137
130
|
if response_code == 401:
|
|
138
131
|
raise AuthenticationError(f"Failed to authorize at the server. Server message: {response_body_str}")
|
|
132
|
+
if response_code == 404:
|
|
133
|
+
print(f'No atributes found for {pac_id.to_url()}')
|
|
134
|
+
return []
|
|
139
135
|
if response_code == 500:
|
|
140
136
|
raise AttributeServerError(f"The server accepted the request, but encountered an internal error. Contact the server admin. Server message: {response_body_str}")
|
|
141
137
|
try:
|
|
@@ -145,21 +141,19 @@ class AttributeClient():
|
|
|
145
141
|
raise AttributeServerError("The server accepted the request, and sent a reponse. However, the response is not adhering to the PAC Attributes specifications. Contact the server admin.")
|
|
146
142
|
|
|
147
143
|
|
|
148
|
-
# update cache
|
|
149
144
|
attribute_groups_out = []
|
|
150
|
-
for ag_for_pac in r.
|
|
145
|
+
for ag_for_pac in r.data:
|
|
151
146
|
pac_from_response = PAC_ID.from_url(ag_for_pac.pac_id)
|
|
152
147
|
ags = [
|
|
153
|
-
|
|
154
|
-
|
|
148
|
+
ClientAttributeGroup(
|
|
149
|
+
group_key= ag.group_key,
|
|
155
150
|
attributes=ag.attributes,
|
|
156
151
|
origin=server_url,
|
|
157
152
|
language=r.language,
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
group_label=ag.group_label
|
|
154
|
+
)
|
|
160
155
|
for ag in ag_for_pac.attribute_groups
|
|
161
156
|
]
|
|
162
|
-
self.cache_store.update(server_url, pac_from_response, ags)
|
|
163
157
|
|
|
164
158
|
# compare pac_id from response with pac_id we need attributes for.
|
|
165
159
|
# if identical this is the part of the response we care about. other PAC-ID are just for the cache
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeGroup
|
|
7
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ClientAttributeGroup(AttributeGroup):
|
|
12
|
+
''' extends attribute group with info the client needs'''
|
|
13
|
+
origin:str
|
|
14
|
+
language:str
|
|
15
|
+
value_from: datetime | None = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
2
4
|
from typing import Any, Protocol
|
|
5
|
+
from urllib.parse import unquote, unquote_plus
|
|
3
6
|
|
|
4
|
-
from flask import Blueprint, current_app, url_for
|
|
5
|
-
from labfreed.pac_attributes.api_data_models.request import
|
|
7
|
+
from flask import Blueprint, current_app, redirect, url_for
|
|
8
|
+
from labfreed.pac_attributes.api_data_models.request import AttributeRequestData
|
|
6
9
|
from labfreed.pac_attributes.server.server import AttributeGroupDataSource, AttributeServerRequestHandler, InvalidRequestError, TranslationDataSource
|
|
7
10
|
|
|
8
11
|
try:
|
|
@@ -70,16 +73,22 @@ class AttributeFlaskApp(Flask):
|
|
|
70
73
|
) -> Blueprint:
|
|
71
74
|
bp = Blueprint("attribute", __name__)
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
|
|
77
|
+
@bp.get("/<path:pac_id_url_encoded>", strict_slashes=False)
|
|
78
|
+
def handle_attribute_request(pac_id_url_encoded):
|
|
79
|
+
if pac_id_url_encoded in ['favicon.ico']:
|
|
80
|
+
return ''
|
|
81
|
+
|
|
75
82
|
if authenticator and not authenticator(request):
|
|
76
83
|
return Response(
|
|
77
84
|
"Unauthorized", 401,
|
|
78
85
|
{"WWW-Authenticate": 'Basic realm="Login required"'}
|
|
79
86
|
)
|
|
80
87
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
88
|
+
request_data = AttributeRequestData.from_http_request(pac_id = pac_id_url_encoded,
|
|
89
|
+
params = request.args,
|
|
90
|
+
headers = request.headers)
|
|
91
|
+
response_body = request_handler.handle_attribute_request(request_data)
|
|
83
92
|
except InvalidRequestError as e:
|
|
84
93
|
print(e)
|
|
85
94
|
return "Invalid request", 400
|
|
@@ -87,14 +96,26 @@ class AttributeFlaskApp(Flask):
|
|
|
87
96
|
print(e)
|
|
88
97
|
return "The request was valid, but the server encountered an error", 500
|
|
89
98
|
return (response_body, 200, {"Content-Type": "application/json; charset=utf-8"})
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@bp.post("/", strict_slashes=False)
|
|
102
|
+
def handle_attribute_request_legacy(pac_id):
|
|
103
|
+
if request.method == 'POST':
|
|
104
|
+
return '\n'.join(('POST request was part of the DRAFT specification, but was changed to GET.',
|
|
105
|
+
'You are probably using an pre-release version of the python package.',
|
|
106
|
+
'update to the newest version "pip install labfreed"'
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@bp.get("/", strict_slashes=False)
|
|
112
|
+
@bp.get("/capabilities", strict_slashes=False)
|
|
92
113
|
def capabilities():
|
|
93
114
|
doc_text = current_app.config.get('DOC_TEXT', "")
|
|
94
115
|
capabilities = request_handler.capabilities()
|
|
95
116
|
authentication_required = bool(current_app.config.get('AUTHENTICATOR'))
|
|
96
|
-
example_request =
|
|
97
|
-
server_address = request.url.rstrip('/')
|
|
117
|
+
example_request = AttributeRequestData(pac_id='HTTPS://PAC.METTORIUS.COM/EXAMPLE', language_preferences=['fr', 'de']).model_dump_json(indent=2, exclude_none=True, exclude_unset=True)
|
|
118
|
+
server_address = request.url.replace('/capabilities','').rstrip('/')
|
|
98
119
|
css_url = url_for("static", filename="style.css")
|
|
99
120
|
response = f'''
|
|
100
121
|
<html>
|
|
@@ -112,12 +133,17 @@ class AttributeFlaskApp(Flask):
|
|
|
112
133
|
|
|
113
134
|
|
|
114
135
|
<h2>How to use</h2>
|
|
115
|
-
Make a <b>
|
|
116
|
-
<
|
|
136
|
+
Make a <b>GET</b> request to <a href="{server_address}">{server_address}/<url encoded PAC-ID> </a>
|
|
137
|
+
<br><br>
|
|
138
|
+
Query parameters (optional):<br>
|
|
139
|
+
attr_grps (optional): An comma separated list of attribute group keys. MUST be url-encoded. <br>
|
|
140
|
+
attr_fwd_lkp (optional): Boolean flag ('true' or 'false'). Instructs the server to not include attributes of PAC-IDs which are attributes of type reference of the requested PAC-ID. Defaults to true <br>
|
|
141
|
+
|
|
142
|
+
<br>
|
|
117
143
|
Consult <a href="https://github.com/ApiniLabs/PAC-Attributes"> the specification </a> for details. <br>
|
|
118
144
|
|
|
119
145
|
|
|
120
|
-
{'This server <b> requires authentication </b> ' if authentication_required else ''}
|
|
146
|
+
{'<h2> Authentication </h2> This server <b> requires authentication </b> ' if authentication_required else ''}
|
|
121
147
|
<br>
|
|
122
148
|
|
|
123
149
|
{"<h2>Further Information</h2>"if doc_text else ""}
|
|
@@ -120,7 +120,7 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
|
|
|
120
120
|
return None
|
|
121
121
|
attributes = [pyAttribute(key= self._header_mappings.get(k, k), value=v) for k, v in d.items() if v is not None]
|
|
122
122
|
return AttributeGroup(
|
|
123
|
-
|
|
123
|
+
group_key=self._attribute_group_key,
|
|
124
124
|
attributes=pyAttributes(attributes).to_payload_attributes()
|
|
125
125
|
)
|
|
126
126
|
|