labfreed 1.0.0a4__tar.gz → 1.0.0a8__tar.gz
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-1.0.0a4 → labfreed-1.0.0a8}/CHANGELOG.md +14 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/PKG-INFO +1 -1
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/__init__.py +1 -1
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/app_infrastructure.py +2 -2
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/pac_info.py +15 -7
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/response.py +54 -32
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/attribute_cache.py +8 -21
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/client.py +4 -2
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -2
- labfreed-1.0.0a8/labfreed/pac_attributes/pythonic/py_attributes.py +165 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/attribute_data_sources.py +7 -11
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/server.py +11 -2
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/category_base.py +1 -1
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/predefined_categories.py +52 -4
- labfreed-1.0.0a4/labfreed/pac_attributes/pythonic/py_attributes.py +0 -133
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/workflows/pypi-publish.yml +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/workflows/run-tests.yml +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/LICENSE +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/README.md +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_infrastructure.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/request.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/pac_cat.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/extension.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/id_segment.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/pac_id.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/url_parser.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/url_serializer.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_common.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_v1.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_v2.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/resolver.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/services.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/qr/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/qr/generate_qr.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/data_table.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/pyTREX.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/quantity.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/table_segment.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/trex.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/trex_base_models.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/value_segments.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/base36.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/ensure_utc_time.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/translations.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/display_name_extension.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/trex_extension.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/gs1/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/__init__.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/unece_units.py +0 -0
- {labfreed-1.0.0a4 → labfreed-1.0.0a8}/pyproject.toml +0 -0
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
## Change Log
|
|
2
|
+
|
|
3
|
+
### v1.0.0
|
|
4
|
+
PAC-CAT
|
|
5
|
+
- added new categories
|
|
6
|
+
- BREAKING: Renamed category MM to MX
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
PAC-ID Attributes
|
|
10
|
+
- new building block
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
2
16
|
### v0.2.12
|
|
3
17
|
- bugfix:no warning message if PAC-CAT has same segment key in two segments
|
|
4
18
|
|
|
@@ -32,7 +32,7 @@ class Labfreed_App_Infrastructure():
|
|
|
32
32
|
self._http_client= http_client
|
|
33
33
|
callback = http_attribute_request_default_callback_factory(http_client)
|
|
34
34
|
|
|
35
|
-
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache())
|
|
35
|
+
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def add_cit(self, cit:str):
|
|
@@ -71,7 +71,7 @@ class Labfreed_App_Infrastructure():
|
|
|
71
71
|
ags = {ag.key: pyAttributeGroup.from_attribute_group(ag) for ag in self._attribute_client.get_attributes(url, pac_id=pac.to_url(include_extensions=False), language_preferences=self._language_preferences)}
|
|
72
72
|
if ags:
|
|
73
73
|
attribute_groups.update(ags)
|
|
74
|
-
pac_info.
|
|
74
|
+
pac_info.attribute_groups = attribute_groups
|
|
75
75
|
|
|
76
76
|
return pac_info
|
|
77
77
|
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
|
-
from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes, pyReference
|
|
8
|
+
from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes, pyReference, pyResource
|
|
9
9
|
from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
|
|
10
10
|
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
11
11
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
@@ -20,7 +20,7 @@ class PacInfo(BaseModel):
|
|
|
20
20
|
"""A convenient collection of information about a PAC-ID"""
|
|
21
21
|
pac_id:PAC_ID
|
|
22
22
|
user_handovers: list[ServiceGroup] = Field(default_factory=list)
|
|
23
|
-
|
|
23
|
+
attribute_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
|
|
24
24
|
|
|
25
25
|
@property
|
|
26
26
|
def pac_url(self):
|
|
@@ -44,8 +44,10 @@ class PacInfo(BaseModel):
|
|
|
44
44
|
|
|
45
45
|
@property
|
|
46
46
|
def image_url(self) -> str:
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
|
|
48
|
+
if isinstance(image_attr.value, pyResource):
|
|
49
|
+
return image_attr.value.root
|
|
50
|
+
if isinstance(image_attr.value, str):
|
|
49
51
|
return image_attr.value
|
|
50
52
|
|
|
51
53
|
|
|
@@ -68,12 +70,18 @@ class PacInfo(BaseModel):
|
|
|
68
70
|
def safety_pictograms(self) -> dict[str, pyAttribute]:
|
|
69
71
|
pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
|
|
70
72
|
return pictogram_attributes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def qualification_state(self) -> pyAttribute:
|
|
77
|
+
if state := self._all_attributes.get("https://labfreed.org/qualification/status"):
|
|
78
|
+
return state
|
|
71
79
|
|
|
72
80
|
|
|
73
81
|
@cached_property
|
|
74
82
|
def _all_attributes(self) -> dict[str, pyAttribute]:
|
|
75
83
|
out = {}
|
|
76
|
-
for ag in self.
|
|
84
|
+
for ag in self.attribute_groups.values():
|
|
77
85
|
out.update(ag.attributes)
|
|
78
86
|
return out
|
|
79
87
|
|
|
@@ -105,12 +113,12 @@ class PacInfo(BaseModel):
|
|
|
105
113
|
|
|
106
114
|
|
|
107
115
|
printout.title1("Attributes")
|
|
108
|
-
for ag in self.
|
|
116
|
+
for ag in self.attribute_groups.values():
|
|
109
117
|
printout.title2(f'{ag.label} (from {ag.origin})')
|
|
110
118
|
for v in ag.attributes.values():
|
|
111
119
|
v:pyAttribute
|
|
112
120
|
#print(f'{k}: ({v.label}) :: {v.value} ')
|
|
113
|
-
printout.key_value(v.label, v.
|
|
121
|
+
printout.key_value(v.label, ', '.join([str(e) for e in v.value_list]))
|
|
114
122
|
|
|
115
123
|
out = printout.getvalue()
|
|
116
124
|
|
|
@@ -3,6 +3,8 @@ from abc import ABC
|
|
|
3
3
|
from datetime import datetime
|
|
4
4
|
import re
|
|
5
5
|
from typing import Annotated, Any, Literal, Union, get_args
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
6
8
|
from labfreed.utilities.ensure_utc_time import ensure_utc
|
|
7
9
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
8
10
|
from pydantic import Field, field_validator, model_validator
|
|
@@ -12,22 +14,13 @@ class AttributeBase(LabFREED_BaseModel, ABC):
|
|
|
12
14
|
key: str
|
|
13
15
|
value: Any
|
|
14
16
|
label: str = ""
|
|
15
|
-
|
|
16
|
-
observed_at: datetime | None = None
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
def __init__(self, **data):
|
|
19
19
|
# Automatically inject the Literal value for `type`
|
|
20
20
|
discriminator_value = self._get_discriminator_value()
|
|
21
21
|
data["type"] = discriminator_value
|
|
22
22
|
super().__init__(**data)
|
|
23
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
24
|
@classmethod
|
|
32
25
|
def _get_discriminator_value(cls) -> str:
|
|
33
26
|
"""Extract the Literal value from the 'type' annotation."""
|
|
@@ -40,16 +33,10 @@ class AttributeBase(LabFREED_BaseModel, ABC):
|
|
|
40
33
|
f"{cls.__name__} must define `type: Literal[<value>]` annotation"
|
|
41
34
|
) from e
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class ReferenceAttribute(AttributeBase):
|
|
47
|
-
type: Literal["reference"]
|
|
48
|
-
value: str
|
|
49
36
|
|
|
50
37
|
class DateTimeAttribute(AttributeBase):
|
|
51
38
|
type: Literal["datetime"]
|
|
52
|
-
value: datetime
|
|
39
|
+
value: datetime | list[datetime]
|
|
53
40
|
|
|
54
41
|
@field_validator('value', mode='before')
|
|
55
42
|
def set_utc__if_naive(cls, value):
|
|
@@ -60,15 +47,59 @@ class DateTimeAttribute(AttributeBase):
|
|
|
60
47
|
|
|
61
48
|
class BoolAttribute(AttributeBase):
|
|
62
49
|
type: Literal["bool"]
|
|
63
|
-
value: bool
|
|
50
|
+
value: bool | list[bool]
|
|
64
51
|
|
|
65
52
|
class TextAttribute(AttributeBase):
|
|
66
53
|
type: Literal["text"]
|
|
67
|
-
value: str
|
|
54
|
+
value: str | list[str]
|
|
55
|
+
|
|
56
|
+
@model_validator(mode='after')
|
|
57
|
+
def _validate_value(self):
|
|
58
|
+
l = [self.value] if isinstance(self.value, str) else self.value
|
|
59
|
+
for v in l:
|
|
60
|
+
if len(v) > 5000:
|
|
61
|
+
self._add_validation_message(
|
|
62
|
+
source="Text Attribute",
|
|
63
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
64
|
+
msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
|
|
65
|
+
highlight_pattern = f'{v}'
|
|
66
|
+
)
|
|
67
|
+
return self
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ReferenceAttribute(AttributeBase):
|
|
71
|
+
type: Literal["reference"]
|
|
72
|
+
value: str | list[str]
|
|
68
73
|
|
|
74
|
+
|
|
75
|
+
class ResourceAttribute(AttributeBase):
|
|
76
|
+
type: Literal["resource"]
|
|
77
|
+
value: str | list[str]
|
|
69
78
|
|
|
79
|
+
@model_validator(mode='after')
|
|
80
|
+
def _validate_value(self):
|
|
81
|
+
value_list = self.value if isinstance(self.value, list) else [self.value]
|
|
82
|
+
for v in value_list:
|
|
83
|
+
r = urlparse(v)
|
|
84
|
+
if not all([r.scheme, r.netloc]):
|
|
85
|
+
self._add_validation_message(
|
|
86
|
+
source="Resource Attribute",
|
|
87
|
+
level=ValidationMsgLevel.ERROR, # noqa: F821
|
|
88
|
+
msg=f"Must be a valid url",
|
|
89
|
+
highlight_pattern = f'{v}'
|
|
90
|
+
)
|
|
91
|
+
pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
|
|
92
|
+
if not bool(pattern.search(v)):
|
|
93
|
+
self._add_validation_message(
|
|
94
|
+
source="Resource Attribute",
|
|
95
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
96
|
+
msg=f"It is RECOMMENDED resource links end with a file extension",
|
|
97
|
+
highlight_pattern = f'{v}'
|
|
98
|
+
)
|
|
99
|
+
return self
|
|
70
100
|
|
|
71
101
|
|
|
102
|
+
|
|
72
103
|
class NumericValue(LabFREED_BaseModel):
|
|
73
104
|
numerical_value: str
|
|
74
105
|
unit: str
|
|
@@ -120,11 +151,11 @@ class NumericValue(LabFREED_BaseModel):
|
|
|
120
151
|
|
|
121
152
|
class NumericAttribute(AttributeBase):
|
|
122
153
|
type: Literal["numeric"]
|
|
123
|
-
value: NumericValue
|
|
154
|
+
value: NumericValue | list[NumericValue]
|
|
124
155
|
|
|
125
156
|
class ObjectAttribute(AttributeBase):
|
|
126
157
|
type: Literal["object"]
|
|
127
|
-
value: dict[str, Any]
|
|
158
|
+
value: dict[str, Any] |list[dict[str, Any]]
|
|
128
159
|
|
|
129
160
|
|
|
130
161
|
|
|
@@ -136,27 +167,18 @@ Attribute = Annotated[
|
|
|
136
167
|
BoolAttribute,
|
|
137
168
|
TextAttribute,
|
|
138
169
|
NumericAttribute,
|
|
170
|
+
ResourceAttribute,
|
|
139
171
|
ObjectAttribute
|
|
140
172
|
],
|
|
141
173
|
Field(discriminator="type")
|
|
142
174
|
]
|
|
143
175
|
|
|
144
|
-
VALID_FOREVER = "forever"
|
|
145
176
|
|
|
146
177
|
class AttributeGroup(LabFREED_BaseModel):
|
|
147
178
|
key: str
|
|
148
179
|
label: str = ""
|
|
149
180
|
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
|
|
181
|
+
|
|
160
182
|
|
|
161
183
|
|
|
162
184
|
class AttributesOfPACID(LabFREED_BaseModel):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
from datetime import datetime
|
|
3
|
+
from datetime import UTC, datetime, timedelta
|
|
4
4
|
from typing import Literal, Protocol
|
|
5
5
|
|
|
6
6
|
|
|
@@ -12,28 +12,15 @@ from labfreed.pac_id.pac_id import PAC_ID
|
|
|
12
12
|
class CacheableAttributeGroup(AttributeGroup):
|
|
13
13
|
origin:str
|
|
14
14
|
language:str
|
|
15
|
-
|
|
15
|
+
value_from: datetime | None = None
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
17
|
+
def still_valid(self, accept_cache_for_minutes):
|
|
18
|
+
if self.value_from is None:
|
|
32
19
|
return False
|
|
33
|
-
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
else:
|
|
21
|
+
return ( datetime.now(tz=UTC) - timedelta(minutes=accept_cache_for_minutes)) > self.value_from
|
|
22
|
+
|
|
23
|
+
|
|
37
24
|
|
|
38
25
|
|
|
39
26
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
|
+
from datetime import UTC, datetime
|
|
4
5
|
from typing import Protocol, runtime_checkable
|
|
5
6
|
|
|
6
7
|
import requests
|
|
@@ -85,6 +86,7 @@ class AttributeClient():
|
|
|
85
86
|
|
|
86
87
|
http_post_callback:AttributeRequestCallback
|
|
87
88
|
cache_store:AttributeCache
|
|
89
|
+
always_use_cached_value_for_minutes:int
|
|
88
90
|
|
|
89
91
|
def get_attributes(self,
|
|
90
92
|
server_url:str,
|
|
@@ -120,7 +122,7 @@ class AttributeClient():
|
|
|
120
122
|
else:
|
|
121
123
|
attribute_groups = self.cache_store.get_all(server_url, pac_id)
|
|
122
124
|
|
|
123
|
-
if attribute_groups and all([ag.still_valid for ag in attribute_groups]):
|
|
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]):
|
|
124
126
|
return attribute_groups
|
|
125
127
|
|
|
126
128
|
# no valid data found in cache > request to server
|
|
@@ -154,7 +156,7 @@ class AttributeClient():
|
|
|
154
156
|
origin=server_url,
|
|
155
157
|
language=r.language,
|
|
156
158
|
label=ag.label,
|
|
157
|
-
|
|
159
|
+
value_from=datetime.now(tz=UTC))
|
|
158
160
|
for ag in ag_for_pac.attribute_groups
|
|
159
161
|
]
|
|
160
162
|
self.cache_store.update(server_url, pac_from_response, ags)
|
|
@@ -113,8 +113,7 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
|
|
|
113
113
|
attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
|
|
114
114
|
return AttributeGroup(
|
|
115
115
|
key=self._attribute_group_key,
|
|
116
|
-
attributes=pyAttributes(attributes).to_payload_attributes()
|
|
117
|
-
state_of=last_changed,
|
|
116
|
+
attributes=pyAttributes(attributes).to_payload_attributes()
|
|
118
117
|
)
|
|
119
118
|
|
|
120
119
|
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import date, datetime, time
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal
|
|
5
|
+
import warnings
|
|
6
|
+
from pydantic import RootModel, field_validator
|
|
7
|
+
|
|
8
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
9
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ResourceAttribute, TextAttribute
|
|
10
|
+
from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
|
|
11
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
|
+
from labfreed.trex.pythonic.quantity import Quantity
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class pyReference(RootModel[str]):
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return str(self.root)
|
|
19
|
+
|
|
20
|
+
class pyResource(RootModel[str]):
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return str(self.root)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# the allowed scalar types
|
|
27
|
+
AllowedValue = str | bool | datetime | pyReference | pyResource | Quantity | int | float | dict | object
|
|
28
|
+
# homogeneous list of those
|
|
29
|
+
AllowedList = list[AllowedValue]
|
|
30
|
+
|
|
31
|
+
class pyAttribute(LabFREED_BaseModel):
|
|
32
|
+
key:str
|
|
33
|
+
label:str = ""
|
|
34
|
+
value: AllowedValue | AllowedList
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def value_list(self):
|
|
38
|
+
'''helper function to more conveniently iterate over value elements, even if it's scalar'''
|
|
39
|
+
return self.value if isinstance(self.value, list) else [self.value]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@field_validator('value', mode='before')
|
|
43
|
+
def handle_one_element_list(v):
|
|
44
|
+
if isinstance(v, list) and len(v)==1:
|
|
45
|
+
return v[0]
|
|
46
|
+
else:
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
class pyAttributes(RootModel[list[pyAttribute]]):
|
|
50
|
+
def to_payload_attributes(self) -> list[AttributeBase]:
|
|
51
|
+
out = []
|
|
52
|
+
for e in self.root:
|
|
53
|
+
apt = self._attribute_to_attribute_payload_type(e)
|
|
54
|
+
if isinstance(apt.value, list) and len(apt.value) ==1:
|
|
55
|
+
apt.value = apt.value[0]
|
|
56
|
+
out.append(apt)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
|
|
62
|
+
common_args = {
|
|
63
|
+
"key": attribute.key,
|
|
64
|
+
"label": attribute.label,
|
|
65
|
+
}
|
|
66
|
+
value_list = attribute.value_list
|
|
67
|
+
first_value = value_list[0]
|
|
68
|
+
if isinstance(first_value, bool):
|
|
69
|
+
return BoolAttribute(value=value_list, **common_args)
|
|
70
|
+
|
|
71
|
+
elif isinstance(first_value, datetime | date | time):
|
|
72
|
+
for v in value_list:
|
|
73
|
+
if not v.tzinfo:
|
|
74
|
+
warnings.warn(f'No timezone given for {v}. Assuming it is in UTC.')
|
|
75
|
+
return DateTimeAttribute(value=value_list, **common_args)
|
|
76
|
+
# return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
elif isinstance(first_value, Quantity|int|float):
|
|
80
|
+
values = []
|
|
81
|
+
for v in value_list:
|
|
82
|
+
if not isinstance(v, Quantity):
|
|
83
|
+
v = Quantity(value=v, unit='dimensionless')
|
|
84
|
+
values.append(NumericValue(numerical_value=v.value_as_str(),
|
|
85
|
+
unit = v.unit))
|
|
86
|
+
num_attribute = NumericAttribute(value = values, **common_args)
|
|
87
|
+
num_attribute.print_validation_messages()
|
|
88
|
+
return num_attribute
|
|
89
|
+
|
|
90
|
+
elif isinstance(first_value, str):
|
|
91
|
+
# capture quantities in the form of "100.0e5 g/L"
|
|
92
|
+
if Quantity.from_str_with_unit(first_value):
|
|
93
|
+
values = []
|
|
94
|
+
for v in value_list:
|
|
95
|
+
q = Quantity.from_str_with_unit(v)
|
|
96
|
+
values.append( NumericValue(numerical_value=q.value_as_str(), unit = q.unit) )
|
|
97
|
+
return NumericAttribute(value = values,
|
|
98
|
+
**common_args)
|
|
99
|
+
|
|
100
|
+
else:
|
|
101
|
+
return TextAttribute(value = value_list, **common_args)
|
|
102
|
+
|
|
103
|
+
elif isinstance(first_value, pyReference):
|
|
104
|
+
return ReferenceAttribute(value = [v.root for v in value_list], **common_args)
|
|
105
|
+
|
|
106
|
+
elif isinstance(first_value, pyResource):
|
|
107
|
+
return ResourceAttribute(value = [v.root for v in value_list], **common_args)
|
|
108
|
+
|
|
109
|
+
elif isinstance(first_value, PAC_ID):
|
|
110
|
+
return ReferenceAttribute(value = [v.to_url(include_extensions=False) for v in value_list], **common_args)
|
|
111
|
+
|
|
112
|
+
else: #this covers the last resort case of arbitrary objects. Must be json serializable.
|
|
113
|
+
try :
|
|
114
|
+
values = [json.loads(json.dumps(v)) for v in value_list]
|
|
115
|
+
return ObjectAttribute(value=values, **common_args)
|
|
116
|
+
except TypeError as e: # noqa: F841
|
|
117
|
+
raise ValueError(f'Invalid Type: {type(first_value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
|
|
123
|
+
out = list()
|
|
124
|
+
for a in attributes:
|
|
125
|
+
value_list = a.value if isinstance(a.value, list) else [a.value]
|
|
126
|
+
match a:
|
|
127
|
+
case ReferenceAttribute():
|
|
128
|
+
values = [pyReference(v) for v in value_list]
|
|
129
|
+
|
|
130
|
+
case ResourceAttribute():
|
|
131
|
+
values = [pyResource(v) for v in value_list]
|
|
132
|
+
|
|
133
|
+
case NumericAttribute():
|
|
134
|
+
values = [ Quantity.from_str_value(value=v.numerical_value, unit=v.unit) for v in value_list]
|
|
135
|
+
|
|
136
|
+
case BoolAttribute():
|
|
137
|
+
values = value_list
|
|
138
|
+
|
|
139
|
+
case TextAttribute():
|
|
140
|
+
values = value_list
|
|
141
|
+
|
|
142
|
+
case DateTimeAttribute():
|
|
143
|
+
values = value_list
|
|
144
|
+
|
|
145
|
+
case ObjectAttribute():
|
|
146
|
+
values = value_list
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
attr = pyAttribute(key=a.key,
|
|
150
|
+
label=a.label,
|
|
151
|
+
value=values
|
|
152
|
+
)
|
|
153
|
+
out.append(attr )
|
|
154
|
+
return out
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class pyAttributeGroup(CacheableAttributeGroup):
|
|
159
|
+
attributes:dict[str,pyAttribute]
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def from_attribute_group(attribute_group:AttributeGroup):
|
|
163
|
+
data = vars(attribute_group).copy()
|
|
164
|
+
data["attributes"] = {a.key: a for a in pyAttributes.from_payload_attributes(attribute_group.attributes)}
|
|
165
|
+
return pyAttributeGroup(**data)
|
{labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/attribute_data_sources.py
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod, abstractproperty
|
|
2
2
|
from datetime import datetime, timezone
|
|
3
|
-
from labfreed.pac_attributes.api_data_models.response import
|
|
3
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup
|
|
4
4
|
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
5
5
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
6
6
|
|
|
@@ -31,13 +31,13 @@ class AttributeGroupDataSource(ABC):
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
class Dict_DataSource(AttributeGroupDataSource):
|
|
34
|
-
def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, *args, **kwargs):
|
|
34
|
+
def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, pac_to_key: callable = None, *args, **kwargs):
|
|
35
35
|
if not all([isinstance(e, list) for e in data.values()]):
|
|
36
36
|
raise ValueError('Invalid data')
|
|
37
37
|
|
|
38
38
|
self._data = data
|
|
39
|
-
self._state_of = datetime.now(tz=timezone.utc)
|
|
40
39
|
self.uses_pac_cat_short_form = uses_pac_cat_short_form
|
|
40
|
+
self._pac_to_key = pac_to_key
|
|
41
41
|
|
|
42
42
|
super().__init__(*args, **kwargs)
|
|
43
43
|
|
|
@@ -54,18 +54,14 @@ class Dict_DataSource(AttributeGroupDataSource):
|
|
|
54
54
|
except:
|
|
55
55
|
... # might as well try to match the original input
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
|
|
58
|
+
lookup_key = self._pac_to_key(pac_url) if self._pac_to_key else pac_url
|
|
59
|
+
attributes = self._data.get(lookup_key)
|
|
58
60
|
if not attributes:
|
|
59
61
|
return None
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
valid_until = VALID_FOREVER if self._is_static else None
|
|
63
|
-
|
|
64
|
-
|
|
65
63
|
return AttributeGroup(key=self._attribute_group_key,
|
|
66
|
-
attributes=attributes
|
|
67
|
-
state_of=self._state_of,
|
|
68
|
-
valid_until=valid_until)
|
|
64
|
+
attributes=attributes)
|
|
69
65
|
|
|
70
66
|
|
|
71
67
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import string
|
|
1
3
|
import traceback
|
|
2
4
|
import warnings
|
|
3
5
|
|
|
@@ -132,15 +134,22 @@ class AttributeServerRequestHandler():
|
|
|
132
134
|
if dn := self._get_display_name_for_key(ag.key, language):
|
|
133
135
|
ag.label = dn
|
|
134
136
|
else:
|
|
135
|
-
ag.label = ag.key
|
|
137
|
+
ag.label = self.fallback_label(ag.key)
|
|
136
138
|
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{ag.key}' in '{language}'. Falling back to '{ag.label}'")
|
|
137
139
|
for a in ag.attributes:
|
|
138
140
|
if dn := self._get_display_name_for_key(a.key, language):
|
|
139
141
|
a.label = dn
|
|
140
142
|
else:
|
|
141
|
-
a.label = a.key
|
|
143
|
+
a.label = self.fallback_label(a.key)
|
|
142
144
|
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{a.key}' in '{language}'. Falling back to '{a.label}' ")
|
|
143
145
|
|
|
146
|
+
|
|
147
|
+
def fallback_label(self, key:str):
|
|
148
|
+
l = key.split('/')[-1]
|
|
149
|
+
l = re.sub(r'([a-z])([A-Z])', r'\1 \2', l)
|
|
150
|
+
l = re.sub(r'[-_]', ' ', l)
|
|
151
|
+
l = string.capwords(l)
|
|
152
|
+
return l
|
|
144
153
|
|
|
145
154
|
|
|
146
155
|
def _get_display_name_for_key(self, key, language:str):
|
|
@@ -116,9 +116,9 @@ class Material_Consumable(PredefinedCategory):
|
|
|
116
116
|
return self
|
|
117
117
|
|
|
118
118
|
class Material_Misc(Material_Consumable):
|
|
119
|
-
'''Represents the -
|
|
119
|
+
'''Represents the -MX category'''
|
|
120
120
|
# same fields as Consumable
|
|
121
|
-
key: str = Field(default='-
|
|
121
|
+
key: str = Field(default='-MX', frozen=True)
|
|
122
122
|
product_number:str|None = Field( alias='240')
|
|
123
123
|
batch_number:str|None = Field(default=None, alias='10')
|
|
124
124
|
packaging_size:str|None = Field(default=None, alias='20')
|
|
@@ -187,15 +187,63 @@ class Data_Static(Data_Abstract):
|
|
|
187
187
|
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
188
188
|
''' Category segments, which are not defined in the specification'''
|
|
189
189
|
|
|
190
|
+
class Data_Misc(Data_Abstract):
|
|
191
|
+
'''Represents the -DX category'''
|
|
192
|
+
key: str = Field(default='-DX', frozen=True)
|
|
193
|
+
id:str|None = Field( alias='21')
|
|
194
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
195
|
+
''' Category segments, which are not defined in the specification'''
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class Processor_Abstract(PredefinedCategory, ABC):
|
|
201
|
+
'''@private'''
|
|
202
|
+
key: str
|
|
203
|
+
processor_instance:str|None = Field( alias='21')
|
|
204
|
+
processor_code:str|None = Field( alias='240')
|
|
205
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
206
|
+
''' Category segments, which are not defined in the specification'''
|
|
207
|
+
|
|
208
|
+
@model_validator(mode='after')
|
|
209
|
+
def _validate_mandatory_fields(self):
|
|
210
|
+
if not self.id:
|
|
211
|
+
self._add_validation_message(
|
|
212
|
+
source=f"Category {self.key}",
|
|
213
|
+
level = ValidationMsgLevel.ERROR,
|
|
214
|
+
msg=f"Category key {self.key} is missing mandatory field 'processor instance'",
|
|
215
|
+
highlight_pattern = f"{self.key}"
|
|
216
|
+
)
|
|
217
|
+
return self
|
|
218
|
+
|
|
219
|
+
class Processor_Software(Processor_Abstract):
|
|
220
|
+
'''Represents the -PS category'''
|
|
221
|
+
key: str = Field(default='-PS', frozen=True)
|
|
222
|
+
processor_instance:str|None = Field( alias='21')
|
|
223
|
+
processor_code:str|None = Field( alias='240')
|
|
224
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
225
|
+
''' Category segments, which are not defined in the specification'''
|
|
226
|
+
|
|
227
|
+
class Processor_Misc(Processor_Abstract):
|
|
228
|
+
'''Represents the -PX category'''
|
|
229
|
+
key: str = Field(default='-PX', frozen=True)
|
|
230
|
+
processor_instance:str|None = Field( alias='21')
|
|
231
|
+
processor_code:str|None = Field( alias='240')
|
|
232
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
233
|
+
''' Category segments, which are not defined in the specification'''
|
|
234
|
+
|
|
190
235
|
|
|
191
236
|
category_key_to_class_map = {
|
|
192
237
|
'-MD': Material_Device,
|
|
193
238
|
'-MS': Material_Substance,
|
|
194
239
|
'-MC': Material_Consumable,
|
|
195
|
-
'-
|
|
240
|
+
'-MX': Material_Misc,
|
|
196
241
|
'-DM': Data_Method,
|
|
197
242
|
'-DR': Data_Result,
|
|
198
243
|
'-DC': Data_Calibration,
|
|
199
244
|
'-DP': Data_Progress,
|
|
200
|
-
'-DS': Data_Static
|
|
245
|
+
'-DS': Data_Static,
|
|
246
|
+
'-DX': Data_Misc,
|
|
247
|
+
'-PS': Processor_Software,
|
|
248
|
+
'-PX': Processor_Misc
|
|
201
249
|
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
from datetime import date, datetime, time
|
|
3
|
-
import json
|
|
4
|
-
from typing import Literal
|
|
5
|
-
import warnings
|
|
6
|
-
from pydantic import RootModel
|
|
7
|
-
|
|
8
|
-
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
9
|
-
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, TextAttribute
|
|
10
|
-
from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
|
|
11
|
-
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
|
-
from labfreed.trex.pythonic.quantity import Quantity
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class pyReference(RootModel[str]):
|
|
16
|
-
pass
|
|
17
|
-
|
|
18
|
-
def __str__(self):
|
|
19
|
-
return str(self.root)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class pyAttribute(LabFREED_BaseModel):
|
|
23
|
-
key:str
|
|
24
|
-
label:str = ""
|
|
25
|
-
value: str|bool|datetime|pyReference|Quantity|int|float|dict|object
|
|
26
|
-
valid_until: datetime | Literal["forever"] | None = None
|
|
27
|
-
observed_at: datetime | None = None
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
class pyAttributes(RootModel[list[pyAttribute]]):
|
|
32
|
-
def to_payload_attributes(self) -> list[AttributeBase]:
|
|
33
|
-
return [self._attribute_to_attribute_payload_type(e) for e in self.root]
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
|
|
38
|
-
common_args = {
|
|
39
|
-
"key": attribute.key,
|
|
40
|
-
"label": attribute.label,
|
|
41
|
-
"observed_at": attribute.observed_at
|
|
42
|
-
}
|
|
43
|
-
value = attribute.value
|
|
44
|
-
|
|
45
|
-
if isinstance(value, bool):
|
|
46
|
-
return BoolAttribute(value=value, **common_args)
|
|
47
|
-
|
|
48
|
-
elif isinstance(value, datetime | date | time):
|
|
49
|
-
if not value.tzinfo:
|
|
50
|
-
warnings.warn(f'No timezone given for {value}. Assuming it is in UTC.')
|
|
51
|
-
return DateTimeAttribute(value =value, **common_args)
|
|
52
|
-
# return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
elif isinstance(attribute.value, Quantity|int|float):
|
|
56
|
-
if not isinstance(attribute.value, Quantity):
|
|
57
|
-
value = Quantity(value=attribute.value, unit='dimensionless')
|
|
58
|
-
num_attribute = NumericAttribute(value = NumericValue(numerical_value=value.value_as_str(),
|
|
59
|
-
unit = value.unit),
|
|
60
|
-
**common_args)
|
|
61
|
-
num_attribute.print_validation_messages()
|
|
62
|
-
return num_attribute
|
|
63
|
-
|
|
64
|
-
elif isinstance(value, str):
|
|
65
|
-
# capture quantities in the form of "100.0e5 g/L"
|
|
66
|
-
if q := Quantity.from_str_with_unit(value):
|
|
67
|
-
return NumericAttribute(value = NumericValue(numerical_value=q.value_as_str(),
|
|
68
|
-
unit = q.unit),
|
|
69
|
-
**common_args)
|
|
70
|
-
else:
|
|
71
|
-
return TextAttribute(value = value, **common_args)
|
|
72
|
-
|
|
73
|
-
elif isinstance(value, pyReference):
|
|
74
|
-
return ReferenceAttribute(value = value.root, **common_args)
|
|
75
|
-
|
|
76
|
-
elif isinstance(value, PAC_ID):
|
|
77
|
-
return ReferenceAttribute(value = value.to_url(include_extensions=False), **common_args)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
else: #this covers the last resort case of arbitrary objects. Must be json serializable.
|
|
82
|
-
try :
|
|
83
|
-
value = json.loads(json.dumps(value))
|
|
84
|
-
return ObjectAttribute(value=value, **common_args)
|
|
85
|
-
except TypeError as e: # noqa: F841
|
|
86
|
-
raise ValueError(f'Invalid Type: {type(value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
@staticmethod
|
|
90
|
-
def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
|
|
91
|
-
out = list()
|
|
92
|
-
for a in attributes:
|
|
93
|
-
match a:
|
|
94
|
-
|
|
95
|
-
case ReferenceAttribute():
|
|
96
|
-
value = pyReference(a.value)
|
|
97
|
-
|
|
98
|
-
case NumericAttribute():
|
|
99
|
-
value = Quantity.from_str_value(value=a.value.numerical_value, unit=a.value.unit)
|
|
100
|
-
|
|
101
|
-
case BoolAttribute():
|
|
102
|
-
value = a.value
|
|
103
|
-
|
|
104
|
-
case TextAttribute():
|
|
105
|
-
value = a.value
|
|
106
|
-
|
|
107
|
-
case DateTimeAttribute():
|
|
108
|
-
value = a.value
|
|
109
|
-
|
|
110
|
-
case ObjectAttribute():
|
|
111
|
-
value = a.value
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
attr = pyAttribute(key=a.key,
|
|
115
|
-
label=a.label,
|
|
116
|
-
value=value,
|
|
117
|
-
observed_at=a.observed_at
|
|
118
|
-
# valid_until=datetime(**_parse_date_time_str(a.valid_until)),
|
|
119
|
-
# observed_at=datetime(**_parse_date_time_str(a.value))
|
|
120
|
-
)
|
|
121
|
-
out.append(attr )
|
|
122
|
-
return out
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
class pyAttributeGroup(CacheableAttributeGroup):
|
|
127
|
-
attributes:dict[str,pyAttribute]
|
|
128
|
-
|
|
129
|
-
@staticmethod
|
|
130
|
-
def from_attribute_group(attribute_group:AttributeGroup):
|
|
131
|
-
data = vars(attribute_group).copy()
|
|
132
|
-
data["attributes"] = {a.key: a for a in pyAttributes.from_payload_attributes(attribute_group.attributes)}
|
|
133
|
-
return pyAttributeGroup(**data)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/attribute_server_factory.py
RENAMED
|
File without changes
|
{labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/py_dict_data_source.py
RENAMED
|
File without changes
|
|
File without changes
|
{labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/translation_data_sources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/display_name_extension.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|