labfreed 1.0.0a12__tar.gz → 1.0.0a14__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.0a12 → labfreed-1.0.0a14}/PKG-INFO +2 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/__init__.py +1 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/app_infrastructure.py +15 -5
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/pac_info.py +137 -53
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_infrastructure.py +12 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/response.py +103 -31
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/category_base.py +1 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/predefined_categories.py +61 -4
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/extension.py +2 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/cit_v1.py +16 -4
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/resolver.py +40 -23
- labfreed-1.0.0a12/labfreed/pac_id_resolver/cit_v2.py → labfreed-1.0.0a14/labfreed/pac_id_resolver/resolver_config.py +27 -16
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/default_extension_interpreters.py +2 -2
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/display_name_extension.py +17 -9
- labfreed-1.0.0a14/labfreed/well_known_extensions/text_base36_extension.py +38 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/pyproject.toml +2 -1
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/workflows/pypi-publish.yml +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/workflows/run-tests.yml +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/CHANGELOG.md +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/LICENSE +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/README.md +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/request.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/client.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/py_attributes.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/attribute_data_sources.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/server.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/pac_cat.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/id_segment.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/pac_id.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/url_parser.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/url_serializer.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/__init__.py +0 -0
- /labfreed-1.0.0a12/labfreed/pac_id_resolver/cit_common.py → /labfreed-1.0.0a14/labfreed/pac_id_resolver/resolver_config_common.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/services.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/qr/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/qr/generate_qr.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/data_table.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/pyTREX.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/quantity.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/table_segment.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/trex.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/trex_base_models.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/value_segments.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/base36.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/ensure_utc_time.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/translations.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/trex_extension.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/gs1/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/__init__.py +0 -0
- {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/unece_units.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labfreed
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0a14
|
|
4
4
|
Summary: Python implementation of LabFREED building blocks
|
|
5
5
|
Author-email: Reto Thürer <thuerer.r@buchi.com>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -25,6 +25,7 @@ Requires-Dist: jsonpath-ng>=1.7.0
|
|
|
25
25
|
Requires-Dist: requests>=2.32.3
|
|
26
26
|
Requires-Dist: requests_cache>=1.2.1
|
|
27
27
|
Requires-Dist: cachetools>=6.1.0
|
|
28
|
+
Requires-Dist: deprecated>=1.2.18
|
|
28
29
|
Requires-Dist: pytest>=8.3.5 ; extra == "dev"
|
|
29
30
|
Requires-Dist: pdoc>=15.0.1 ; extra == "dev"
|
|
30
31
|
Requires-Dist: flit>=3.12.0 ; extra == "dev"
|
{labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/app_infrastructure.py
RENAMED
|
@@ -7,9 +7,6 @@ from labfreed.labfreed_extended.app.pac_info.pac_info import PacInfo
|
|
|
7
7
|
from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
|
|
8
8
|
from labfreed.pac_attributes.client.client import AttributeClient, http_attribute_request_default_callback_factory
|
|
9
9
|
from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
|
|
10
|
-
from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
|
|
11
|
-
from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
15
12
|
from labfreed.pac_id_resolver.resolver import PAC_ID_Resolver, cit_from_str
|
|
@@ -35,11 +32,15 @@ class Labfreed_App_Infrastructure():
|
|
|
35
32
|
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
|
|
36
33
|
|
|
37
34
|
|
|
38
|
-
def
|
|
35
|
+
def add_resolver_config(self, cit:str):
|
|
39
36
|
cit = cit_from_str(cit)
|
|
40
37
|
if not cit:
|
|
41
38
|
raise ValueError('the cit could not be parsed. Neither as v1 or v2')
|
|
42
|
-
self._resolver.
|
|
39
|
+
self._resolver._resolver_configs.add(cit)
|
|
40
|
+
|
|
41
|
+
def remove_resolver_config(self, resolver_config:str):
|
|
42
|
+
resolver_config = cit_from_str(resolver_config)
|
|
43
|
+
self._resolver._resolver_configs.discard(resolver_config)
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def process_pac(self, pac_url, markup=None):
|
|
@@ -63,6 +64,15 @@ class Labfreed_App_Infrastructure():
|
|
|
63
64
|
sg_user_handovers.append(ServiceGroup(origin=sg.origin, services=user_handovers))
|
|
64
65
|
pac_info.user_handovers = sg_user_handovers
|
|
65
66
|
|
|
67
|
+
# Actions
|
|
68
|
+
sg_actions = []
|
|
69
|
+
for sg in service_groups:
|
|
70
|
+
actions = [s for s in sg.services if s.service_type == 'action-generic']
|
|
71
|
+
|
|
72
|
+
if actions:
|
|
73
|
+
sg_actions.append(ServiceGroup(origin=sg.origin, services=actions))
|
|
74
|
+
pac_info.actions = sg_actions
|
|
75
|
+
|
|
66
76
|
# Attributes
|
|
67
77
|
attribute_groups = {}
|
|
68
78
|
for sg in service_groups:
|
|
@@ -8,8 +8,9 @@ from pydantic import BaseModel, Field
|
|
|
8
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
|
+
from labfreed.pac_cat.predefined_categories import PredefinedCategory
|
|
11
12
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
|
-
from labfreed.pac_id_resolver.services import ServiceGroup
|
|
13
|
+
from labfreed.pac_id_resolver.services import ServiceGroup, Service
|
|
13
14
|
from labfreed.labfreed_extended.app.formatted_print import StringIOLineBreak
|
|
14
15
|
from labfreed.trex.pythonic.data_table import DataTable
|
|
15
16
|
from labfreed.trex.pythonic.pyTREX import pyTREX
|
|
@@ -19,30 +20,133 @@ from labfreed.well_known_extensions.display_name_extension import DisplayNameExt
|
|
|
19
20
|
class PacInfo(BaseModel):
|
|
20
21
|
"""A convenient collection of information about a PAC-ID"""
|
|
21
22
|
pac_id:PAC_ID
|
|
23
|
+
|
|
22
24
|
user_handovers: list[ServiceGroup] = Field(default_factory=list)
|
|
25
|
+
actions: list[ServiceGroup] = Field(default_factory=list)
|
|
23
26
|
attribute_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
# info about pac-id
|
|
30
|
+
|
|
31
|
+
@cached_property
|
|
32
|
+
def is_item_serialized(self) -> bool|None: #indicates if the item is at product level (e.g. BAL500), as opposed to a serialized instance thereof (e.g. BAL500 with SN 1234)
|
|
33
|
+
if not isinstance(self.pac_id, PAC_CAT):
|
|
34
|
+
return None
|
|
35
|
+
cat = self.main_category
|
|
36
|
+
if not isinstance(cat, PredefinedCategory):
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
return cat.is_serialized
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cached_property
|
|
26
43
|
def pac_url(self):
|
|
27
44
|
return self.pac_id.to_url(include_extensions=False)
|
|
28
45
|
|
|
29
|
-
@
|
|
46
|
+
@cached_property
|
|
30
47
|
def main_category(self):
|
|
31
48
|
if isinstance(self.pac_id, PAC_CAT):
|
|
32
49
|
return self.pac_id.categories[0]
|
|
33
50
|
else:
|
|
34
51
|
return None
|
|
35
52
|
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# attached data
|
|
56
|
+
|
|
57
|
+
@cached_property
|
|
58
|
+
def attached_data(self) -> dict[str, pyTREX]:
|
|
38
59
|
return { trex_ext.name: pyTREX.from_trex(trex=trex_ext.trex) for trex_ext in self.pac_id.get_extension_of_type('TREX')}
|
|
39
60
|
|
|
40
61
|
|
|
41
|
-
@
|
|
42
|
-
def summary(self):
|
|
43
|
-
return self.pac_id.get_extension('SUM')
|
|
62
|
+
@cached_property
|
|
63
|
+
def summary(self) -> pyTREX:
|
|
64
|
+
return pyTREX.from_trex(self.pac_id.get_extension('SUM').trex)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@cached_property
|
|
68
|
+
def status(self) -> pyTREX:
|
|
69
|
+
return pyTREX.from_trex(self.pac_id.get_extension('STATUS').trex)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Handovers and Actions
|
|
75
|
+
|
|
76
|
+
def get_user_handovers_by_intent(self, intent:str, partial_match=False) -> list[Service]:
|
|
77
|
+
services = [s for sg in self.user_handovers for s in sg.services if self._match_intent(intent, s.application_intents, partial_match)]
|
|
78
|
+
return services
|
|
79
|
+
|
|
80
|
+
def get_user_handover_by_intent(self, intent:str, partial_match=False, mode="first"):
|
|
81
|
+
handovers = self.get_user_handovers_by_intent(intent=intent, partial_match=partial_match)
|
|
82
|
+
return self._pick_from_list(handovers, mode)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_actions_by_intent(self, intent:str, partial_match=False) -> list[Service]:
|
|
87
|
+
actions = [s for sg in self.actions for s in sg.services if self._match_intent(intent, s.application_intents, partial_match)]
|
|
88
|
+
return actions
|
|
89
|
+
|
|
90
|
+
def get_action_by_intent(self, intent:str, partial_match=False, mode="first"):
|
|
91
|
+
actions = self.get_actions_by_intent(intent=intent, partial_match=partial_match)
|
|
92
|
+
return self._pick_from_list(actions, mode)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _match_intent(self, intent, intents, partial_match):
|
|
96
|
+
if partial_match:
|
|
97
|
+
# intent 'document' should match 'document-operation-manual' etc
|
|
98
|
+
return any([intent in i for i in intents])
|
|
99
|
+
else:
|
|
100
|
+
# only exact match
|
|
101
|
+
return intent in intents
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@cached_property
|
|
105
|
+
def important_handovers(self) -> list[Service]:
|
|
106
|
+
return self.get_user_handovers_by_intent('important')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@cached_property
|
|
110
|
+
def important_actions(self) -> list[Service]:
|
|
111
|
+
return self.get_actions_by_intent('important')
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# Attributes
|
|
119
|
+
|
|
120
|
+
@cached_property
|
|
121
|
+
def _all_attributes(self) -> dict[str, pyAttribute]:
|
|
122
|
+
out = {}
|
|
123
|
+
for ag in self.attribute_groups.values():
|
|
124
|
+
out.update(ag.attributes)
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def get_attributes(self, key:str) -> list[pyAttribute]:
|
|
129
|
+
attributes = [a for k, a in self._all_attributes.items() if key in a.key]
|
|
130
|
+
return attributes
|
|
44
131
|
|
|
45
|
-
|
|
132
|
+
def get_attribute(self, key:str, mode="first"):
|
|
133
|
+
attributes = self.get_attributes(key)
|
|
134
|
+
return self._pick_from_list(attributes, mode)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _pick_from_list(self, list, mode):
|
|
138
|
+
if mode not in ['first', 'last']:
|
|
139
|
+
raise ValueError('mode must be "first or "last" ')
|
|
140
|
+
|
|
141
|
+
if not list:
|
|
142
|
+
return None
|
|
143
|
+
if mode == 'first':
|
|
144
|
+
return list[0]
|
|
145
|
+
if mode == 'last':
|
|
146
|
+
return list[-1]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cached_property
|
|
46
150
|
def image_url(self) -> str:
|
|
47
151
|
image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
|
|
48
152
|
if isinstance(image_attr.value, pyResource):
|
|
@@ -51,7 +155,7 @@ class PacInfo(BaseModel):
|
|
|
51
155
|
return image_attr.value
|
|
52
156
|
|
|
53
157
|
|
|
54
|
-
@
|
|
158
|
+
@cached_property
|
|
55
159
|
def display_name(self) -> str|None:
|
|
56
160
|
display_name = None
|
|
57
161
|
pac = self.pac_id
|
|
@@ -63,30 +167,41 @@ class PacInfo(BaseModel):
|
|
|
63
167
|
if dn_attr := self._all_attributes.get(MetaAttributeKeys.DISPLAYNAME.value):
|
|
64
168
|
dn = dn_attr.value
|
|
65
169
|
display_name = dn + f' ( aka {display_name} )' if display_name else dn
|
|
170
|
+
|
|
171
|
+
if not display_name and self.main_category:
|
|
172
|
+
seg_240 = [s for s in self.main_category.segments if s.key=="240"]
|
|
173
|
+
display_name = seg_240[0].value
|
|
174
|
+
|
|
66
175
|
return display_name
|
|
67
176
|
|
|
68
177
|
|
|
69
|
-
@
|
|
178
|
+
@cached_property
|
|
70
179
|
def safety_pictograms(self) -> dict[str, pyAttribute]:
|
|
71
180
|
pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
|
|
72
181
|
return pictogram_attributes
|
|
73
182
|
|
|
74
183
|
|
|
75
|
-
@
|
|
184
|
+
@cached_property
|
|
76
185
|
def qualification_state(self) -> pyAttribute:
|
|
77
186
|
if state := self._all_attributes.get("https://labfreed.org/qualification/status"):
|
|
78
187
|
return state
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
|
|
79
193
|
|
|
80
|
-
|
|
81
|
-
@cached_property
|
|
82
|
-
def _all_attributes(self) -> dict[str, pyAttribute]:
|
|
83
|
-
out = {}
|
|
84
|
-
for ag in self.attribute_groups.values():
|
|
85
|
-
out.update(ag.attributes)
|
|
86
|
-
return out
|
|
87
194
|
|
|
88
195
|
|
|
89
196
|
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
########
|
|
203
|
+
|
|
204
|
+
|
|
90
205
|
def format_for_print(self, markup:str='rich') -> str:
|
|
91
206
|
|
|
92
207
|
printout = StringIOLineBreak(markup=markup)
|
|
@@ -126,37 +241,6 @@ class PacInfo(BaseModel):
|
|
|
126
241
|
|
|
127
242
|
|
|
128
243
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
hide_attribute_groups=hide_attribute_groups
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
def render_html_card(self) -> str:
|
|
136
|
-
return PACInfo_HTMLRenderer.render_template('pac_info_card.jinja.html',
|
|
137
|
-
pac_info = self
|
|
138
|
-
)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class PACInfo_HTMLRenderer():
|
|
142
|
-
TEMPLATES_DIR = Path(__file__).parent / "html_renderer"
|
|
143
|
-
jinja_env = Environment(
|
|
144
|
-
loader=FileSystemLoader(str(TEMPLATES_DIR), encoding="utf-8"),
|
|
145
|
-
autoescape=select_autoescape(enabled_extensions=("html", "jinja", "jinja2", "jinja.html")),
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
@classmethod
|
|
149
|
-
def render_template(cls, template_name:str, pac_info:PacInfo, hide_attribute_groups):
|
|
150
|
-
# --- Jinja env pointing at /html_renderer ---
|
|
151
|
-
template = cls.jinja_env.get_template("pac_info.jinja.html")
|
|
152
|
-
html = template.render(
|
|
153
|
-
pac=pac_info.pac_id,
|
|
154
|
-
pac_info=pac_info, # your object
|
|
155
|
-
hide_attribute_groups=hide_attribute_groups,
|
|
156
|
-
is_data_table = lambda value: isinstance(value, DataTable),
|
|
157
|
-
is_url = lambda s: isinstance(s, str) and urlparse(s).scheme in ('http', 'https') and bool(urlparse(s).netloc),
|
|
158
|
-
is_image = lambda s: isinstance(s, str) and s.lower().startswith('http') and s.lower().endswith(('.jpg','.jpeg','.png','.gif','.bmp','.webp','.svg','.tif','.tiff')),
|
|
159
|
-
is_reference = lambda s: isinstance(s, pyReference) ,
|
|
160
|
-
)
|
|
161
|
-
return html
|
|
162
|
-
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
|
|
@@ -4,6 +4,10 @@ import re
|
|
|
4
4
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
5
5
|
from typing import Any, List, Set
|
|
6
6
|
|
|
7
|
+
import warnings
|
|
8
|
+
import functools
|
|
9
|
+
import inspect
|
|
10
|
+
|
|
7
11
|
from rich import print
|
|
8
12
|
from rich.table import Table
|
|
9
13
|
|
|
@@ -256,3 +260,11 @@ def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]
|
|
|
256
260
|
def _quote_texts(texts:list[str]):
|
|
257
261
|
return ','.join([f"'{t}'" for t in texts])
|
|
258
262
|
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
|
|
@@ -36,7 +36,7 @@ class AttributeBase(LabFREED_BaseModel, ABC):
|
|
|
36
36
|
|
|
37
37
|
class DateTimeAttribute(AttributeBase):
|
|
38
38
|
type: Literal["datetime"]
|
|
39
|
-
value: datetime
|
|
39
|
+
value: datetime
|
|
40
40
|
|
|
41
41
|
@field_validator('value', mode='before')
|
|
42
42
|
def set_utc__if_naive(cls, value):
|
|
@@ -44,60 +44,113 @@ class DateTimeAttribute(AttributeBase):
|
|
|
44
44
|
return ensure_utc(value)
|
|
45
45
|
else:
|
|
46
46
|
return value
|
|
47
|
+
|
|
48
|
+
class DateTimeListAttribute(AttributeBase):
|
|
49
|
+
type: Literal["datetime-list"]
|
|
50
|
+
value: list[datetime]
|
|
51
|
+
|
|
52
|
+
@field_validator('value', mode='before')
|
|
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
|
+
return ValueError(f'{v} is of type {type(v)}. It must be datetime')
|
|
60
|
+
|
|
61
|
+
|
|
47
62
|
|
|
48
63
|
class BoolAttribute(AttributeBase):
|
|
49
64
|
type: Literal["bool"]
|
|
50
|
-
value: bool
|
|
65
|
+
value: bool
|
|
66
|
+
|
|
67
|
+
class BoolListAttribute(AttributeBase):
|
|
68
|
+
type: Literal["bool-list"]
|
|
69
|
+
value: list[bool]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
51
73
|
|
|
52
74
|
class TextAttribute(AttributeBase):
|
|
53
75
|
type: Literal["text"]
|
|
54
|
-
value: str
|
|
76
|
+
value: str
|
|
77
|
+
|
|
78
|
+
@model_validator(mode='after')
|
|
79
|
+
def _validate_value(self):
|
|
80
|
+
_validate_text(self, self.value)
|
|
81
|
+
return self
|
|
82
|
+
|
|
83
|
+
class TextListAttribute(AttributeBase):
|
|
84
|
+
type: Literal["text-list"]
|
|
85
|
+
value: list[str]
|
|
55
86
|
|
|
56
87
|
@model_validator(mode='after')
|
|
57
88
|
def _validate_value(self):
|
|
58
89
|
l = [self.value] if isinstance(self.value, str) else self.value
|
|
59
90
|
for v in l:
|
|
60
|
-
|
|
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
|
-
)
|
|
91
|
+
_validate_text(self, v)
|
|
67
92
|
return self
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _validate_text(mdl:LabFREED_BaseModel, v):
|
|
96
|
+
if len(v) > 5000:
|
|
97
|
+
mdl._add_validation_message(
|
|
98
|
+
source="Text Attribute",
|
|
99
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
100
|
+
msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
|
|
101
|
+
highlight_pattern = f'{v}'
|
|
102
|
+
)
|
|
68
103
|
|
|
69
104
|
|
|
105
|
+
|
|
70
106
|
class ReferenceAttribute(AttributeBase):
|
|
71
107
|
type: Literal["reference"]
|
|
72
|
-
value: str
|
|
108
|
+
value: str
|
|
109
|
+
|
|
110
|
+
class ReferenceListAttribute(AttributeBase):
|
|
111
|
+
type: Literal["reference-list"]
|
|
112
|
+
value: list[str]
|
|
113
|
+
|
|
73
114
|
|
|
115
|
+
|
|
74
116
|
|
|
75
117
|
class ResourceAttribute(AttributeBase):
|
|
76
118
|
type: Literal["resource"]
|
|
77
|
-
value: str
|
|
119
|
+
value: str
|
|
120
|
+
|
|
121
|
+
@model_validator(mode='after')
|
|
122
|
+
def _validate_value(self):
|
|
123
|
+
_validate_resource(self, self.value)
|
|
124
|
+
|
|
125
|
+
class ResourceListAttribute(AttributeBase):
|
|
126
|
+
type: Literal["resource-list"]
|
|
127
|
+
value: list[str]
|
|
78
128
|
|
|
79
129
|
@model_validator(mode='after')
|
|
80
130
|
def _validate_value(self):
|
|
81
131
|
value_list = self.value if isinstance(self.value, list) else [self.value]
|
|
82
132
|
for v in value_list:
|
|
83
|
-
|
|
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
|
-
)
|
|
133
|
+
_validate_resource(self, v)
|
|
99
134
|
return self
|
|
100
135
|
|
|
136
|
+
def _validate_resource(mdl:LabFREED_BaseModel, v):
|
|
137
|
+
r = urlparse(v)
|
|
138
|
+
if not all([r.scheme, r.netloc]):
|
|
139
|
+
mdl._add_validation_message(
|
|
140
|
+
source="Resource Attribute",
|
|
141
|
+
level=ValidationMsgLevel.ERROR, # noqa: F821
|
|
142
|
+
msg="Must be a valid url",
|
|
143
|
+
highlight_pattern = f'{v}'
|
|
144
|
+
)
|
|
145
|
+
pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
|
|
146
|
+
if not bool(pattern.search(v)):
|
|
147
|
+
mdl._add_validation_message(
|
|
148
|
+
source="Resource Attribute",
|
|
149
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
150
|
+
msg="It is RECOMMENDED resource links end with a file extension",
|
|
151
|
+
highlight_pattern = f'{v}'
|
|
152
|
+
)
|
|
153
|
+
|
|
101
154
|
|
|
102
155
|
|
|
103
156
|
class NumericValue(LabFREED_BaseModel):
|
|
@@ -151,11 +204,22 @@ class NumericValue(LabFREED_BaseModel):
|
|
|
151
204
|
|
|
152
205
|
class NumericAttribute(AttributeBase):
|
|
153
206
|
type: Literal["numeric"]
|
|
154
|
-
value: NumericValue
|
|
207
|
+
value: NumericValue
|
|
208
|
+
|
|
209
|
+
class NumericListAttribute(AttributeBase):
|
|
210
|
+
type: Literal["numeric-list"]
|
|
211
|
+
value: list[NumericValue]
|
|
212
|
+
|
|
213
|
+
|
|
155
214
|
|
|
156
215
|
class ObjectAttribute(AttributeBase):
|
|
157
216
|
type: Literal["object"]
|
|
158
|
-
value: dict[str, Any]
|
|
217
|
+
value: dict[str, Any]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class ObjectListAttribute(AttributeBase):
|
|
221
|
+
type: Literal["object-list"]
|
|
222
|
+
value: list[dict[str, Any]]
|
|
159
223
|
|
|
160
224
|
|
|
161
225
|
|
|
@@ -168,7 +232,15 @@ Attribute = Annotated[
|
|
|
168
232
|
TextAttribute,
|
|
169
233
|
NumericAttribute,
|
|
170
234
|
ResourceAttribute,
|
|
171
|
-
ObjectAttribute
|
|
235
|
+
ObjectAttribute,
|
|
236
|
+
|
|
237
|
+
ReferenceListAttribute,
|
|
238
|
+
DateTimeListAttribute,
|
|
239
|
+
BoolListAttribute,
|
|
240
|
+
TextListAttribute,
|
|
241
|
+
NumericListAttribute,
|
|
242
|
+
ResourceListAttribute,
|
|
243
|
+
ObjectListAttribute
|
|
172
244
|
],
|
|
173
245
|
Field(discriminator="type")
|
|
174
246
|
]
|
|
@@ -55,7 +55,7 @@ class Category(LabFREED_BaseModel):
|
|
|
55
55
|
k = f"{field_name} ({ field_info.alias})"
|
|
56
56
|
else:
|
|
57
57
|
k = f"{field_name}"
|
|
58
|
-
|
|
58
|
+
out.update({k : v } )
|
|
59
59
|
|
|
60
60
|
for s in getattr(self, 'additional_segments', []):
|
|
61
61
|
out.update( {s.key or '' : s.value })
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
## Materials
|
|
2
|
-
from abc import ABC
|
|
2
|
+
from abc import ABC, abstractproperty
|
|
3
3
|
from pydantic import Field, computed_field, model_validator
|
|
4
4
|
|
|
5
5
|
from labfreed.labfreed_infrastructure import ValidationMsgLevel
|
|
@@ -18,6 +18,10 @@ class PredefinedCategory(Category, ABC):
|
|
|
18
18
|
def segments(self) -> list[IDSegment]:
|
|
19
19
|
return self._get_segments(use_short_notation=False)
|
|
20
20
|
|
|
21
|
+
@abstractproperty
|
|
22
|
+
def is_serialized(self) -> bool:
|
|
23
|
+
pass
|
|
24
|
+
|
|
21
25
|
def _get_segments(self, use_short_notation=False) -> list[IDSegment]:
|
|
22
26
|
segments = []
|
|
23
27
|
can_omit_keys = use_short_notation # keeps track of whether keys can still be omitted. That is the case when the segment recommendation is followed
|
|
@@ -65,12 +69,17 @@ class Material_Device(PredefinedCategory):
|
|
|
65
69
|
if not self.serial_number:
|
|
66
70
|
self._add_validation_message(
|
|
67
71
|
source=f"Category {self.key}",
|
|
68
|
-
level = ValidationMsgLevel.
|
|
69
|
-
msg=f'Category key {self.key} is missing
|
|
72
|
+
level = ValidationMsgLevel.WARNING,
|
|
73
|
+
msg=f'Category key {self.key} is missing field Serial Number. Check that you are indeed to a product and not a specific device.',
|
|
70
74
|
highlight_pattern = f"{self.key}"
|
|
71
75
|
)
|
|
72
76
|
return self
|
|
73
77
|
|
|
78
|
+
@property
|
|
79
|
+
def is_serialized(self) -> bool:
|
|
80
|
+
return bool(self.serial_number)
|
|
81
|
+
|
|
82
|
+
|
|
74
83
|
class Material_Substance(PredefinedCategory):
|
|
75
84
|
'''Represents the -MS category'''
|
|
76
85
|
key: str = Field(default='-MS', frozen=True)
|
|
@@ -93,6 +102,11 @@ class Material_Substance(PredefinedCategory):
|
|
|
93
102
|
)
|
|
94
103
|
return self
|
|
95
104
|
|
|
105
|
+
@property
|
|
106
|
+
def is_serialized(self) -> bool:
|
|
107
|
+
return bool(self.batch_number or self.container_number or self.aliquot)
|
|
108
|
+
|
|
109
|
+
|
|
96
110
|
class Material_Consumable(PredefinedCategory):
|
|
97
111
|
'''Represents the -MC category'''
|
|
98
112
|
key: str = Field(default='-MC', frozen=True)
|
|
@@ -115,6 +129,11 @@ class Material_Consumable(PredefinedCategory):
|
|
|
115
129
|
)
|
|
116
130
|
return self
|
|
117
131
|
|
|
132
|
+
@property
|
|
133
|
+
def is_serialized(self) -> bool:
|
|
134
|
+
return bool(self.batch_number or self.serial_number or self.aliquot)
|
|
135
|
+
|
|
136
|
+
|
|
118
137
|
class Material_Misc(Material_Consumable):
|
|
119
138
|
'''Represents the -MX category'''
|
|
120
139
|
# same fields as Consumable
|
|
@@ -128,6 +147,8 @@ class Material_Misc(Material_Consumable):
|
|
|
128
147
|
''' Category segments, which are not defined in the specification'''
|
|
129
148
|
|
|
130
149
|
|
|
150
|
+
|
|
151
|
+
|
|
131
152
|
|
|
132
153
|
## Data
|
|
133
154
|
class Data_Abstract(PredefinedCategory, ABC):
|
|
@@ -147,6 +168,11 @@ class Data_Abstract(PredefinedCategory, ABC):
|
|
|
147
168
|
highlight_pattern = f"{self.key}"
|
|
148
169
|
)
|
|
149
170
|
return self
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def is_serialized(self) -> bool:
|
|
174
|
+
return True
|
|
175
|
+
|
|
150
176
|
|
|
151
177
|
class Data_Result(Data_Abstract):
|
|
152
178
|
'''Represents the -DR category'''
|
|
@@ -215,6 +241,11 @@ class Processor_Abstract(PredefinedCategory, ABC):
|
|
|
215
241
|
highlight_pattern = f"{self.key}"
|
|
216
242
|
)
|
|
217
243
|
return self
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def is_serialized(self) -> bool:
|
|
247
|
+
return bool(self.processor_instance)
|
|
248
|
+
|
|
218
249
|
|
|
219
250
|
class Processor_Software(Processor_Abstract):
|
|
220
251
|
'''Represents the -PS category'''
|
|
@@ -233,6 +264,31 @@ class Processor_Misc(Processor_Abstract):
|
|
|
233
264
|
''' Category segments, which are not defined in the specification'''
|
|
234
265
|
|
|
235
266
|
|
|
267
|
+
|
|
268
|
+
class Misc(Category, ABC):
|
|
269
|
+
'''@private'''
|
|
270
|
+
key: str = Field(default='-X', frozen=True)
|
|
271
|
+
id:str|None = Field( alias='21')
|
|
272
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
273
|
+
''' Category segments, which are not defined in the specification'''
|
|
274
|
+
|
|
275
|
+
@model_validator(mode='after')
|
|
276
|
+
def _validate_mandatory_fields(self):
|
|
277
|
+
if not self.id:
|
|
278
|
+
self._add_validation_message(
|
|
279
|
+
source=f"Category {self.key}",
|
|
280
|
+
level = ValidationMsgLevel.ERROR,
|
|
281
|
+
msg=f"Category key {self.key} is missing mandatory field 'ID'",
|
|
282
|
+
highlight_pattern = f"{self.key}"
|
|
283
|
+
)
|
|
284
|
+
return self
|
|
285
|
+
|
|
286
|
+
@property
|
|
287
|
+
def is_serialized(self) -> bool:
|
|
288
|
+
return bool(self.id)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
236
292
|
category_key_to_class_map = {
|
|
237
293
|
'-MD': Material_Device,
|
|
238
294
|
'-MS': Material_Substance,
|
|
@@ -245,5 +301,6 @@ category_key_to_class_map = {
|
|
|
245
301
|
'-DS': Data_Static,
|
|
246
302
|
'-DX': Data_Misc,
|
|
247
303
|
'-PS': Processor_Software,
|
|
248
|
-
'-PX': Processor_Misc
|
|
304
|
+
'-PX': Processor_Misc,
|
|
305
|
+
'-X':Misc
|
|
249
306
|
}
|