labfreed 0.2.12__py3-none-any.whl → 0.3.0a1__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 +104 -0
- labfreed/labfreed_extended/app/pac_info.py +79 -0
- labfreed/labfreed_extended/pac_attributes/py_attributes.py +123 -0
- labfreed/labfreed_extended/pac_attributes/server/attribute_server_factory.py +103 -0
- labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py +128 -0
- labfreed/labfreed_extended/utilities/formatted_print.py +64 -0
- labfreed/labfreed_infrastructure.py +3 -3
- labfreed/pac_attributes/api_data_models/request.py +56 -0
- labfreed/pac_attributes/api_data_models/response.py +184 -0
- labfreed/pac_attributes/api_data_models/server_capabilities_response.py +7 -0
- labfreed/pac_attributes/client/__init__.py +1 -0
- labfreed/pac_attributes/client/attribute_cache.py +78 -0
- labfreed/pac_attributes/client/client.py +148 -0
- labfreed/pac_attributes/server/attribute_data_sources.py +69 -0
- labfreed/pac_attributes/server/server.py +237 -0
- labfreed/pac_attributes/server/translation_data_sources.py +60 -0
- labfreed/pac_attributes/well_knonw_attribute_keys.py +11 -0
- labfreed/pac_cat/category_base.py +19 -0
- labfreed/pac_cat/pac_cat.py +1 -1
- labfreed/pac_id/extension.py +32 -7
- labfreed/pac_id/pac_id.py +2 -2
- labfreed/pac_id/url_parser.py +4 -4
- labfreed/pac_id/url_serializer.py +11 -5
- labfreed/pac_id_resolver/cit_common.py +1 -1
- labfreed/pac_id_resolver/cit_v1.py +0 -3
- labfreed/pac_id_resolver/cit_v2.py +1 -6
- labfreed/pac_id_resolver/resolver.py +12 -7
- labfreed/pac_id_resolver/services.py +0 -1
- labfreed/trex/python_convenience/quantity.py +86 -5
- labfreed/trex/value_segments.py +1 -1
- labfreed/utilities/ensure_utc_time.py +6 -0
- labfreed/utilities/translations.py +60 -0
- labfreed/well_known_extensions/display_name_extension.py +3 -3
- labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +4 -0
- labfreed/well_known_keys/labfreed/well_known_keys.py +12 -2
- labfreed/well_known_keys/unece/unece_units.py +2 -1
- {labfreed-0.2.12.dist-info → labfreed-0.3.0a1.dist-info}/METADATA +8 -1
- labfreed-0.3.0a1.dist-info/RECORD +62 -0
- labfreed/well_known_keys/gs1/gs1.py +0 -4
- labfreed-0.2.12.dist-info/RECORD +0 -45
- {labfreed-0.2.12.dist-info → labfreed-0.3.0a1.dist-info}/WHEEL +0 -0
- {labfreed-0.2.12.dist-info → labfreed-0.3.0a1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
import re
|
|
5
|
+
from typing import Annotated, Any, Literal, Union, get_args
|
|
6
|
+
from labfreed.utilities.ensure_utc_time import ensure_utc
|
|
7
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
8
|
+
from pydantic import Field, field_validator, model_validator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AttributeBase(LabFREED_BaseModel, ABC):
|
|
12
|
+
key: str
|
|
13
|
+
value: Any
|
|
14
|
+
label: str = ""
|
|
15
|
+
|
|
16
|
+
observed_at: datetime | None = None
|
|
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)
|
|
23
|
+
|
|
24
|
+
@field_validator('observed_at', mode='before')
|
|
25
|
+
def set_utc_observed_at_if_naive(cls, value):
|
|
26
|
+
if isinstance(value, datetime):
|
|
27
|
+
return ensure_utc(value)
|
|
28
|
+
else:
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def _get_discriminator_value(cls) -> str:
|
|
33
|
+
"""Extract the Literal value from the 'type' annotation."""
|
|
34
|
+
try:
|
|
35
|
+
type_annotation = cls.__annotations__["type"]
|
|
36
|
+
literal_value = get_args(type_annotation)[0]
|
|
37
|
+
return literal_value
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise TypeError(
|
|
40
|
+
f"{cls.__name__} must define `type: Literal[<value>]` annotation"
|
|
41
|
+
) from e
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ReferenceAttribute(AttributeBase):
|
|
47
|
+
type: Literal["reference"]
|
|
48
|
+
value: str
|
|
49
|
+
|
|
50
|
+
class DateTimeAttribute(AttributeBase):
|
|
51
|
+
type: Literal["datetime"]
|
|
52
|
+
value: datetime
|
|
53
|
+
|
|
54
|
+
@field_validator('value', mode='before')
|
|
55
|
+
def set_utc__if_naive(cls, value):
|
|
56
|
+
if isinstance(value, datetime):
|
|
57
|
+
return ensure_utc(value)
|
|
58
|
+
else:
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
class BoolAttribute(AttributeBase):
|
|
62
|
+
type: Literal["bool"]
|
|
63
|
+
value: bool
|
|
64
|
+
|
|
65
|
+
class TextAttribute(AttributeBase):
|
|
66
|
+
type: Literal["text"]
|
|
67
|
+
value: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class NumericValue(LabFREED_BaseModel):
|
|
73
|
+
magnitude: str
|
|
74
|
+
unit: str
|
|
75
|
+
|
|
76
|
+
@model_validator(mode='after')
|
|
77
|
+
def _validate_value(self):
|
|
78
|
+
value = self.magnitude
|
|
79
|
+
if not_allowed_chars := set(re.sub(r'[0-9\.\-\+Ee]', '', value)):
|
|
80
|
+
self._add_validation_message(
|
|
81
|
+
source="Numeric Attribute",
|
|
82
|
+
level=ValidationMsgLevel.ERROR, # noqa: F821
|
|
83
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in quantity segment. Must be a number.",
|
|
84
|
+
highlight_pattern = f'{value}',
|
|
85
|
+
highlight_sub=not_allowed_chars
|
|
86
|
+
)
|
|
87
|
+
if not re.fullmatch(r'-?\d+(\.\d+)?([Ee][\+-]?\d+)?', value):
|
|
88
|
+
self._add_validation_message(
|
|
89
|
+
source="Numeric Attribute",
|
|
90
|
+
level=ValidationMsgLevel.ERROR,
|
|
91
|
+
msg=f"{value} cannot be converted to number",
|
|
92
|
+
highlight_pattern = f'{value}'
|
|
93
|
+
)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
@model_validator(mode="after")
|
|
97
|
+
def _validate_units(self):
|
|
98
|
+
'''A sanity check on unit complying with UCUM. NOTE: It is not a complete validation
|
|
99
|
+
- I check for blankspaces and ^, which are often used for units, but are invalid.
|
|
100
|
+
- 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
|
|
101
|
+
'''
|
|
102
|
+
if ' ' in self.unit or '^' in self.unit:
|
|
103
|
+
self._add_validation_message(
|
|
104
|
+
source="Numeric Attribute",
|
|
105
|
+
level= ValidationMsgLevel.ERROR,
|
|
106
|
+
msg=f"Unit {self.unit} is invalid. Must not contain blankspace or '^'.",
|
|
107
|
+
highlight_pattern = self.unit
|
|
108
|
+
)
|
|
109
|
+
elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self.unit):
|
|
110
|
+
self._add_validation_message(
|
|
111
|
+
source="Numeric Attribute",
|
|
112
|
+
level= ValidationMsgLevel.WARNING,
|
|
113
|
+
msg=f"Unit {self.unit} is probably invalid. Ensure it complies with UCUM specifications.",
|
|
114
|
+
highlight_pattern = self.unit
|
|
115
|
+
)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class NumericAttribute(AttributeBase):
|
|
122
|
+
type: Literal["numeric"]
|
|
123
|
+
value: NumericValue
|
|
124
|
+
|
|
125
|
+
class ObjectAttribute(AttributeBase):
|
|
126
|
+
type: Literal["object"]
|
|
127
|
+
value: dict[str, Any]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
Attribute = Annotated[
|
|
133
|
+
Union[
|
|
134
|
+
ReferenceAttribute,
|
|
135
|
+
DateTimeAttribute,
|
|
136
|
+
BoolAttribute,
|
|
137
|
+
TextAttribute,
|
|
138
|
+
NumericAttribute,
|
|
139
|
+
ObjectAttribute
|
|
140
|
+
],
|
|
141
|
+
Field(discriminator="type")
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
VALID_FOREVER = "forever"
|
|
145
|
+
|
|
146
|
+
class AttributeGroup(LabFREED_BaseModel):
|
|
147
|
+
key: str
|
|
148
|
+
label: str = ""
|
|
149
|
+
attributes: list[Attribute]
|
|
150
|
+
|
|
151
|
+
state_of: datetime
|
|
152
|
+
valid_until: datetime | Literal["forever"] | None = None
|
|
153
|
+
|
|
154
|
+
@field_validator('valid_until', mode='before')
|
|
155
|
+
def set_utc_valid_until_if_naive(cls, value):
|
|
156
|
+
if isinstance(value, datetime):
|
|
157
|
+
return ensure_utc(value)
|
|
158
|
+
else:
|
|
159
|
+
return value
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class AttributesOfPACID(LabFREED_BaseModel):
|
|
163
|
+
pac_url: str
|
|
164
|
+
attribute_groups: list[AttributeGroup]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class AttributeResponsePayload(LabFREED_BaseModel):
|
|
169
|
+
schema_version: str = Field(default='1.0')
|
|
170
|
+
language:str
|
|
171
|
+
pac_attributes: list[AttributesOfPACID]
|
|
172
|
+
|
|
173
|
+
def to_json(self):
|
|
174
|
+
return self.model_dump_json(exclude_none=True)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import labfreed.utilities.translations
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Literal, Protocol
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeGroup
|
|
8
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CacheableAttributeGroup(AttributeGroup):
|
|
13
|
+
origin:str
|
|
14
|
+
language:str
|
|
15
|
+
valid_until: Literal['forever'] | datetime | None = None
|
|
16
|
+
|
|
17
|
+
# @model_validator(mode='after')
|
|
18
|
+
# def set_valid_until(self) -> 'CacheableAttributeGroup':
|
|
19
|
+
# vals = [a.valid_until for a in self.attributes]
|
|
20
|
+
# if all(e == 'forever' for e in vals):
|
|
21
|
+
# self.valid_until = 'forever'
|
|
22
|
+
# elif any(e is None for e in vals):
|
|
23
|
+
# self.valid_until = None
|
|
24
|
+
# else:
|
|
25
|
+
# self.valid_until = min(v for v in vals if isinstance(v, datetime))
|
|
26
|
+
# return self
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def still_valid(self):
|
|
31
|
+
if self.valid_until is None:
|
|
32
|
+
return False
|
|
33
|
+
if self.valid_until == 'forever':
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
return self.valid_until > datetime.now()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AttributeCache(Protocol):
|
|
42
|
+
def get_all(self, service_url:str, pac:PAC_ID) -> list[CacheableAttributeGroup]:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def get_attribute_groups(self, service_url:str, pac:PAC_ID, attribute_groups:list[str]):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
def update(self, service_url:str, pac:PAC_ID, attribute_groups:list[CacheableAttributeGroup]):
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MemoryAttributeCache(AttributeCache):
|
|
54
|
+
'''simple in-memory implementation of AttributeCache'''
|
|
55
|
+
def __init__(self):
|
|
56
|
+
self._store = dict()
|
|
57
|
+
|
|
58
|
+
def get_all(self, service_url:str, pac:PAC_ID|str) -> list[CacheableAttributeGroup]:
|
|
59
|
+
if isinstance(pac, str):
|
|
60
|
+
pac = PAC_ID.from_url(pac)
|
|
61
|
+
k = self._generate_dict_key(service_url=service_url, pac=pac)
|
|
62
|
+
|
|
63
|
+
ags = [CacheableAttributeGroup.model_validate(e) for e in self._store.get(k, [])]
|
|
64
|
+
return ags
|
|
65
|
+
|
|
66
|
+
def get_attribute_groups(self, service_url:str, pac:PAC_ID, attribute_groups:list[str]):
|
|
67
|
+
all_ags = self.get_all(service_url=service_url, pac=pac)
|
|
68
|
+
selected_ags = [ag for ag in all_ags if ag.key in attribute_groups]
|
|
69
|
+
return selected_ags
|
|
70
|
+
|
|
71
|
+
def update(self, service_url:str, pac:PAC_ID, attribute_groups: list[CacheableAttributeGroup] ):
|
|
72
|
+
k = self._generate_dict_key(service_url=service_url, pac=pac)
|
|
73
|
+
self._store.update({k: [e.model_dump() for e in attribute_groups]})
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _generate_dict_key(service_url:str, pac:PAC_ID):
|
|
77
|
+
key = service_url +";"+ pac.to_url(include_extensions=False)
|
|
78
|
+
return key
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from pydantic import ValidationError
|
|
9
|
+
|
|
10
|
+
from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
|
|
11
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload
|
|
12
|
+
from labfreed.pac_attributes.client.attribute_cache import AttributeCache, CacheableAttributeGroup
|
|
13
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AuthenticationError(Exception):
|
|
17
|
+
'''Server rejected authentication. '''
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class AttributeClientInternalError(Exception):
|
|
21
|
+
'''The error is on client side. Possibly a bug on client side'''
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
class AttributeServerError(Exception):
|
|
25
|
+
'''The error is on server side. Needs to contact the server admin'''
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@runtime_checkable
|
|
31
|
+
class AttributeRequestCallback(Protocol):
|
|
32
|
+
def __call__(self, url: str, attribute_request_body: str) -> tuple[int, str]:
|
|
33
|
+
'''handle the request
|
|
34
|
+
returns a tuple of HTTP status code and the body of the response or an error message'''
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def attribute_request_default_callback_factory(session: requests.Session = None) -> AttributeRequestCallback:
|
|
39
|
+
""" Returns a default implementation of AttributeRequestCallback using `requests` package.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
session (requests.Session, optional): A requests.Session object. Pass such an object if you need control over authentication and such things. If omitted a default Session is used.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
AttributeRequestCallback: a callback following the AttributeRequestCallback protocol.
|
|
46
|
+
"""
|
|
47
|
+
if session is None:
|
|
48
|
+
session = requests.Session()
|
|
49
|
+
|
|
50
|
+
def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
|
|
51
|
+
try:
|
|
52
|
+
resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json'})
|
|
53
|
+
return resp.status_code, resp.text
|
|
54
|
+
except requests.exceptions.RequestException as e:
|
|
55
|
+
return 500, str(e)
|
|
56
|
+
return callback
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class AttributeClient():
|
|
63
|
+
""" Client handling attribute requests and caching thereof.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
http_post_callback:AttributeRequestCallback
|
|
67
|
+
cache_store:AttributeCache
|
|
68
|
+
|
|
69
|
+
def get_attributes(self,
|
|
70
|
+
server_url:str,
|
|
71
|
+
pac_id:PAC_ID|str,
|
|
72
|
+
restrict_to_attribute_groups:list[str]|None=None,
|
|
73
|
+
language_preferences:list[str]|None=None,
|
|
74
|
+
force_server_request=False
|
|
75
|
+
) -> list[CacheableAttributeGroup]:
|
|
76
|
+
"""gets the attributes from one attribute server for one PAC-ID. Uses a cached version if possible, otherwise requests from the server again.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
server_url (str): _description_
|
|
80
|
+
pac_id (PAC_ID | str): _description_
|
|
81
|
+
restrict_to_attribute_groups (list[str] | None, optional): _description_. Defaults to None.
|
|
82
|
+
language_preferences (list[str] | None, optional): _description_. Defaults to None.
|
|
83
|
+
force_server_request (bool, optional): _description_. Defaults to False.
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
AuthenticationError:
|
|
87
|
+
AttributeClientInternalError:
|
|
88
|
+
AttributeServerError:
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
list[CacheableAttributeGroup]: attribute groups for the PAC-ID
|
|
92
|
+
"""
|
|
93
|
+
if isinstance(pac_id, str):
|
|
94
|
+
pac_id = PAC_ID.from_url(pac_id)
|
|
95
|
+
|
|
96
|
+
# try the cache
|
|
97
|
+
if not force_server_request:
|
|
98
|
+
if restrict_to_attribute_groups:
|
|
99
|
+
attribute_groups = self.cache_store.get_attribute_groups(server_url, pac_id, restrict_to_attribute_groups)
|
|
100
|
+
else:
|
|
101
|
+
attribute_groups = self.cache_store.get_all(server_url, pac_id)
|
|
102
|
+
|
|
103
|
+
if attribute_groups and all([ag.still_valid for ag in attribute_groups]):
|
|
104
|
+
return attribute_groups
|
|
105
|
+
|
|
106
|
+
# no valid data found in cache > request to server
|
|
107
|
+
attribute_request_body = AttributeRequestPayload(pac_urls=[pac_id.to_url()],
|
|
108
|
+
restrict_to_attribute_groups=restrict_to_attribute_groups,
|
|
109
|
+
language_preferences=language_preferences
|
|
110
|
+
)
|
|
111
|
+
response_code, response_body_str = self.http_post_callback(server_url, attribute_request_body.model_dump_json())
|
|
112
|
+
|
|
113
|
+
if response_code == 400:
|
|
114
|
+
raise AttributeClientInternalError(f"The server did not accept the request. Server message: '{response_body_str}'")
|
|
115
|
+
if response_code == 401:
|
|
116
|
+
raise AuthenticationError(f"Failed to authorize at the server. Server message: {response_body_str}")
|
|
117
|
+
if response_code == 500:
|
|
118
|
+
raise AttributeServerError(f"The server accepted the request, but encountered an internal error. Contact the server admin. Server message: {response_body_str}")
|
|
119
|
+
try:
|
|
120
|
+
r = AttributeResponsePayload.model_validate_json(response_body_str)
|
|
121
|
+
except ValidationError as e:
|
|
122
|
+
print(e)
|
|
123
|
+
raise AttributeServerError("The server accepted the request, and sent a reponse. However, the response is adhering to the PAC Attributes specifications. Contact the server admin.")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# update cache
|
|
127
|
+
for ag_for_pac in r.pac_attributes:
|
|
128
|
+
pac = PAC_ID.from_url(ag_for_pac.pac_url)
|
|
129
|
+
ags = [
|
|
130
|
+
CacheableAttributeGroup(
|
|
131
|
+
key= ag.key,
|
|
132
|
+
attributes=ag.attributes,
|
|
133
|
+
origin=server_url,
|
|
134
|
+
language=r.language,
|
|
135
|
+
label=ag.label,
|
|
136
|
+
state_of=ag.state_of)
|
|
137
|
+
for ag in ag_for_pac.attribute_groups
|
|
138
|
+
]
|
|
139
|
+
self.cache_store.update(server_url, pac, ags)
|
|
140
|
+
|
|
141
|
+
if pac_id == pac:
|
|
142
|
+
attribute_groups_out = ags
|
|
143
|
+
return attribute_groups_out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod, abstractproperty
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from labfreed.pac_attributes.api_data_models.response import VALID_FOREVER, AttributeGroup
|
|
4
|
+
from labfreed_extended.pac_attributes.py_attributes import pyAttributes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AttributeGroupDataSource(ABC):
|
|
8
|
+
|
|
9
|
+
def __init__(self, attribute_group_key:str, include_extensions:bool=False, is_static:bool=False, ):
|
|
10
|
+
self._attribute_group_key = attribute_group_key
|
|
11
|
+
self._include_extensions = include_extensions
|
|
12
|
+
self._is_static = is_static
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def is_static(self) -> bool:
|
|
16
|
+
return self._is_static
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def attribute_group_key(self):
|
|
21
|
+
return self._attribute_group_key
|
|
22
|
+
|
|
23
|
+
@abstractproperty
|
|
24
|
+
def provides_attributes(self):
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def attributes(self, pac_url: str) -> AttributeGroup:
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Dict_DataSource(AttributeGroupDataSource):
|
|
33
|
+
def __init__(self, data:dict[str, pyAttributes], *args, **kwargs):
|
|
34
|
+
if not all([isinstance(e, pyAttributes) for e in data.values()]):
|
|
35
|
+
raise ValueError('Invalid data')
|
|
36
|
+
self._data:pyAttributes = data
|
|
37
|
+
self._state_of = datetime.now(tz=timezone.utc)
|
|
38
|
+
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def provides_attributes(self):
|
|
44
|
+
return [a.key for attributes in self._data.values() for a in attributes.root]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def attributes(self, pac_url: str) -> AttributeGroup:
|
|
48
|
+
if not self._include_extensions:
|
|
49
|
+
pac_url = pac_url.split('*')[0]
|
|
50
|
+
|
|
51
|
+
attributes:pyAttributes = self._data.get(pac_url)
|
|
52
|
+
if not attributes:
|
|
53
|
+
return None
|
|
54
|
+
attributes = attributes.to_payload_attributes()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
valid_until = VALID_FOREVER if self._is_static else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
return AttributeGroup(key=self._attribute_group_key,
|
|
61
|
+
attributes=attributes,
|
|
62
|
+
state_of=self._state_of,
|
|
63
|
+
valid_until=valid_until)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|