labfreed 1.0.0a13__tar.gz → 1.0.0a15__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.0a13 → labfreed-1.0.0a15}/PKG-INFO +2 -1
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/__init__.py +1 -1
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/app_infrastructure.py +18 -7
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/pac_info.py +137 -53
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_infrastructure.py +12 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/api_data_models/response.py +104 -31
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -1
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +9 -3
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/pythonic/py_attributes.py +50 -21
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_cat/category_base.py +1 -1
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_cat/predefined_categories.py +39 -3
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/extension.py +2 -1
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id_resolver/cit_v1.py +6 -4
- labfreed-1.0.0a15/labfreed/pac_id_resolver/resolver.py +122 -0
- labfreed-1.0.0a13/labfreed/pac_id_resolver/cit_v2.py → labfreed-1.0.0a15/labfreed/pac_id_resolver/resolver_config.py +18 -18
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_extensions/default_extension_interpreters.py +2 -2
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_extensions/display_name_extension.py +17 -9
- labfreed-1.0.0a15/labfreed/well_known_extensions/text_base36_extension.py +38 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/pyproject.toml +2 -1
- labfreed-1.0.0a13/labfreed/pac_id_resolver/resolver.py +0 -102
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/.github/workflows/pypi-publish.yml +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/.github/workflows/run-tests.yml +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/CHANGELOG.md +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/LICENSE +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/README.md +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/api_data_models/request.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/client/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/client/client.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/server/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/server/attribute_data_sources.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/server/server.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_cat/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_cat/pac_cat.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/id_segment.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/pac_id.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/url_parser.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id/url_serializer.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id_resolver/__init__.py +0 -0
- /labfreed-1.0.0a13/labfreed/pac_id_resolver/cit_common.py → /labfreed-1.0.0a15/labfreed/pac_id_resolver/resolver_config_common.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/pac_id_resolver/services.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/qr/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/qr/generate_qr.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/pythonic/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/pythonic/data_table.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/pythonic/pyTREX.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/pythonic/quantity.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/table_segment.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/trex.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/trex_base_models.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/trex/value_segments.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/utilities/base36.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/utilities/ensure_utc_time.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/utilities/translations.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_extensions/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_extensions/trex_extension.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_keys/gs1/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/labfreed/well_known_keys/unece/__init__.py +0 -0
- {labfreed-1.0.0a13 → labfreed-1.0.0a15}/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.0a15
|
|
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.0a13 → labfreed-1.0.0a15}/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
|
|
@@ -20,10 +17,11 @@ from labfreed.pac_id_resolver.services import ServiceGroup
|
|
|
20
17
|
|
|
21
18
|
|
|
22
19
|
class Labfreed_App_Infrastructure():
|
|
23
|
-
def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None):
|
|
20
|
+
def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None, use_issuer_resolver_config=True):
|
|
24
21
|
if isinstance(language_preferences, str):
|
|
25
22
|
language_preferences = [language_preferences]
|
|
26
23
|
self._language_preferences = language_preferences
|
|
24
|
+
self._use_issuer_resolver_config = use_issuer_resolver_config
|
|
27
25
|
|
|
28
26
|
self._resolver = PAC_ID_Resolver()
|
|
29
27
|
|
|
@@ -35,11 +33,15 @@ class Labfreed_App_Infrastructure():
|
|
|
35
33
|
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
|
|
36
34
|
|
|
37
35
|
|
|
38
|
-
def
|
|
36
|
+
def add_resolver_config(self, cit:str):
|
|
39
37
|
cit = cit_from_str(cit)
|
|
40
38
|
if not cit:
|
|
41
39
|
raise ValueError('the cit could not be parsed. Neither as v1 or v2')
|
|
42
|
-
self._resolver.
|
|
40
|
+
self._resolver._resolver_configs.add(cit)
|
|
41
|
+
|
|
42
|
+
def remove_resolver_config(self, resolver_config:str):
|
|
43
|
+
resolver_config = cit_from_str(resolver_config)
|
|
44
|
+
self._resolver._resolver_configs.discard(resolver_config)
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
def process_pac(self, pac_url, markup=None):
|
|
@@ -47,7 +49,7 @@ class Labfreed_App_Infrastructure():
|
|
|
47
49
|
pac = PAC_ID.from_url(pac_url)
|
|
48
50
|
else:
|
|
49
51
|
pac = pac_url
|
|
50
|
-
service_groups = self._resolver.resolve(pac, check_service_status=False)
|
|
52
|
+
service_groups = self._resolver.resolve(pac, check_service_status=False, use_issuer_resolver_config=self._use_issuer_resolver_config)
|
|
51
53
|
|
|
52
54
|
pac_info = PacInfo(pac_id=pac)
|
|
53
55
|
|
|
@@ -63,6 +65,15 @@ class Labfreed_App_Infrastructure():
|
|
|
63
65
|
sg_user_handovers.append(ServiceGroup(origin=sg.origin, services=user_handovers))
|
|
64
66
|
pac_info.user_handovers = sg_user_handovers
|
|
65
67
|
|
|
68
|
+
# Actions
|
|
69
|
+
sg_actions = []
|
|
70
|
+
for sg in service_groups:
|
|
71
|
+
actions = [s for s in sg.services if s.service_type == 'action-generic']
|
|
72
|
+
|
|
73
|
+
if actions:
|
|
74
|
+
sg_actions.append(ServiceGroup(origin=sg.origin, services=actions))
|
|
75
|
+
pac_info.actions = sg_actions
|
|
76
|
+
|
|
66
77
|
# Attributes
|
|
67
78
|
attribute_groups = {}
|
|
68
79
|
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,114 @@ 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]
|
|
73
113
|
|
|
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
|
+
return self
|
|
125
|
+
|
|
126
|
+
class ResourceListAttribute(AttributeBase):
|
|
127
|
+
type: Literal["resource-list"]
|
|
128
|
+
value: list[str]
|
|
78
129
|
|
|
79
130
|
@model_validator(mode='after')
|
|
80
131
|
def _validate_value(self):
|
|
81
132
|
value_list = self.value if isinstance(self.value, list) else [self.value]
|
|
82
133
|
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
|
-
)
|
|
134
|
+
_validate_resource(self, v)
|
|
99
135
|
return self
|
|
100
136
|
|
|
137
|
+
def _validate_resource(mdl:LabFREED_BaseModel, v):
|
|
138
|
+
r = urlparse(v)
|
|
139
|
+
if not all([r.scheme, r.netloc]):
|
|
140
|
+
mdl._add_validation_message(
|
|
141
|
+
source="Resource Attribute",
|
|
142
|
+
level=ValidationMsgLevel.ERROR, # noqa: F821
|
|
143
|
+
msg="Must be a valid url",
|
|
144
|
+
highlight_pattern = f'{v}'
|
|
145
|
+
)
|
|
146
|
+
pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
|
|
147
|
+
if not bool(pattern.search(v)):
|
|
148
|
+
mdl._add_validation_message(
|
|
149
|
+
source="Resource Attribute",
|
|
150
|
+
level=ValidationMsgLevel.WARNING, # noqa: F821
|
|
151
|
+
msg="It is RECOMMENDED resource links end with a file extension",
|
|
152
|
+
highlight_pattern = f'{v}'
|
|
153
|
+
)
|
|
154
|
+
|
|
101
155
|
|
|
102
156
|
|
|
103
157
|
class NumericValue(LabFREED_BaseModel):
|
|
@@ -151,11 +205,22 @@ class NumericValue(LabFREED_BaseModel):
|
|
|
151
205
|
|
|
152
206
|
class NumericAttribute(AttributeBase):
|
|
153
207
|
type: Literal["numeric"]
|
|
154
|
-
value: NumericValue
|
|
208
|
+
value: NumericValue
|
|
209
|
+
|
|
210
|
+
class NumericListAttribute(AttributeBase):
|
|
211
|
+
type: Literal["numeric-list"]
|
|
212
|
+
value: list[NumericValue]
|
|
213
|
+
|
|
214
|
+
|
|
155
215
|
|
|
156
216
|
class ObjectAttribute(AttributeBase):
|
|
157
217
|
type: Literal["object"]
|
|
158
|
-
value: dict[str, Any]
|
|
218
|
+
value: dict[str, Any]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class ObjectListAttribute(AttributeBase):
|
|
222
|
+
type: Literal["object-list"]
|
|
223
|
+
value: list[dict[str, Any]]
|
|
159
224
|
|
|
160
225
|
|
|
161
226
|
|
|
@@ -168,7 +233,15 @@ Attribute = Annotated[
|
|
|
168
233
|
TextAttribute,
|
|
169
234
|
NumericAttribute,
|
|
170
235
|
ResourceAttribute,
|
|
171
|
-
ObjectAttribute
|
|
236
|
+
ObjectAttribute,
|
|
237
|
+
|
|
238
|
+
ReferenceListAttribute,
|
|
239
|
+
DateTimeListAttribute,
|
|
240
|
+
BoolListAttribute,
|
|
241
|
+
TextListAttribute,
|
|
242
|
+
NumericListAttribute,
|
|
243
|
+
ResourceListAttribute,
|
|
244
|
+
ObjectListAttribute
|
|
172
245
|
],
|
|
173
246
|
Field(discriminator="type")
|
|
174
247
|
]
|
|
@@ -75,9 +75,10 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
|
|
|
75
75
|
Subclasses implement `_read_rows_and_last_changed()`.
|
|
76
76
|
"""
|
|
77
77
|
|
|
78
|
-
def __init__(self, *, base_url: str = "", cache_duration_seconds: int = 0, uses_pac_cat_short_form:bool=True, **kwargs):
|
|
78
|
+
def __init__(self, *, base_url: str = "", cache_duration_seconds: int = 0, uses_pac_cat_short_form:bool=True, pac_to_key=None, **kwargs):
|
|
79
79
|
self._base_url = base_url
|
|
80
80
|
self._uses_pac_cat_short_form = uses_pac_cat_short_form
|
|
81
|
+
self._pac_to_key = pac_to_key
|
|
81
82
|
# allow instance-level TTL override
|
|
82
83
|
try:
|
|
83
84
|
_cache.ttl = int(cache_duration_seconds)
|
|
@@ -106,11 +107,16 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
|
|
|
106
107
|
except:
|
|
107
108
|
... # might as well try to match the original input
|
|
108
109
|
|
|
110
|
+
if f:= self._pac_to_key:
|
|
111
|
+
key = f(pac_url)
|
|
112
|
+
else:
|
|
113
|
+
key = pac_url
|
|
114
|
+
|
|
109
115
|
rows, last_changed = self._read_rows_and_last_changed()
|
|
110
|
-
d = _get_row_by_first_cell(rows,
|
|
116
|
+
d = _get_row_by_first_cell(rows, key, self._base_url)
|
|
111
117
|
if not d:
|
|
112
118
|
return None
|
|
113
|
-
attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
|
|
119
|
+
attributes = [pyAttribute(key=k, value=v) for k, v in d.items() if v is not None]
|
|
114
120
|
return AttributeGroup(
|
|
115
121
|
key=self._attribute_group_key,
|
|
116
122
|
attributes=pyAttributes(attributes).to_payload_attributes()
|