labfreed 1.0.0a12__py3-none-any.whl → 1.0.0a14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- labfreed/__init__.py +1 -1
- labfreed/labfreed_extended/app/app_infrastructure.py +15 -5
- labfreed/labfreed_extended/app/pac_info/pac_info.py +137 -53
- labfreed/labfreed_infrastructure.py +12 -0
- labfreed/pac_attributes/api_data_models/response.py +103 -31
- labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -1
- labfreed/pac_cat/category_base.py +1 -1
- labfreed/pac_cat/predefined_categories.py +61 -4
- labfreed/pac_id/extension.py +2 -1
- labfreed/pac_id_resolver/cit_v1.py +16 -4
- labfreed/pac_id_resolver/resolver.py +40 -23
- labfreed/pac_id_resolver/{cit_v2.py → resolver_config.py} +27 -16
- labfreed/well_known_extensions/default_extension_interpreters.py +2 -2
- labfreed/well_known_extensions/display_name_extension.py +17 -9
- labfreed/well_known_extensions/text_base36_extension.py +38 -0
- {labfreed-1.0.0a12.dist-info → labfreed-1.0.0a14.dist-info}/METADATA +2 -1
- {labfreed-1.0.0a12.dist-info → labfreed-1.0.0a14.dist-info}/RECORD +20 -19
- /labfreed/pac_id_resolver/{cit_common.py → resolver_config_common.py} +0 -0
- {labfreed-1.0.0a12.dist-info → labfreed-1.0.0a14.dist-info}/WHEEL +0 -0
- {labfreed-1.0.0a12.dist-info → labfreed-1.0.0a14.dist-info}/licenses/LICENSE +0 -0
labfreed/__init__.py
CHANGED
|
@@ -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
|
}
|
labfreed/pac_id/extension.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
from abc import ABC, abstractproperty
|
|
3
3
|
|
|
4
|
-
from pydantic import model_validator
|
|
4
|
+
from pydantic import computed_field, model_validator
|
|
5
5
|
|
|
6
6
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
|
|
7
7
|
|
|
@@ -29,6 +29,7 @@ class Extension(LabFREED_BaseModel,ExtensionBase):
|
|
|
29
29
|
type:str|None
|
|
30
30
|
data_:str
|
|
31
31
|
|
|
32
|
+
@computed_field
|
|
32
33
|
@property
|
|
33
34
|
def data(self) -> str:
|
|
34
35
|
return self.data_
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
|
|
2
2
|
import re
|
|
3
3
|
|
|
4
|
+
from deprecated import deprecated
|
|
5
|
+
|
|
4
6
|
from pydantic import Field, model_validator
|
|
5
7
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMessage, ValidationMsgLevel
|
|
6
8
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
7
9
|
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
8
|
-
from labfreed.pac_id_resolver.
|
|
10
|
+
from labfreed.pac_id_resolver.resolver_config_common import ( _add_msg_to_cit_entry_model,
|
|
9
11
|
_validate_service_name,
|
|
10
12
|
_validate_application_intent,
|
|
11
13
|
_validate_service_type,
|
|
12
14
|
ServiceType)
|
|
13
15
|
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
@deprecated("Use ResolverConfig")
|
|
16
18
|
class CITEntry_v1(LabFREED_BaseModel):
|
|
17
19
|
applicable_if: str = Field(..., min_length=1)
|
|
18
20
|
service_name: str = Field(..., min_length=1)
|
|
19
21
|
application_intent:str = Field(..., min_length=1)
|
|
20
22
|
service_type:ServiceType|str
|
|
21
23
|
template_url:str = Field(..., min_length=1)
|
|
22
|
-
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
@model_validator(mode='after')
|
|
25
27
|
def _validate_model(self):
|
|
@@ -67,7 +69,7 @@ class CITEntry_v1(LabFREED_BaseModel):
|
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
@deprecated("Use ResolverConfig")
|
|
71
73
|
class CIT_v1(LabFREED_BaseModel):
|
|
72
74
|
origin:str = ''
|
|
73
75
|
entries:list[CITEntry_v1]
|
|
@@ -187,6 +189,16 @@ class CIT_v1(LabFREED_BaseModel):
|
|
|
187
189
|
for e in self.entries:
|
|
188
190
|
s += '\t'.join([e.service_name, e.application_intent, e.service_type.value, e.applicable_if, e.template_url]) + '\n'
|
|
189
191
|
return s
|
|
192
|
+
|
|
193
|
+
# hash and equal are only used to avoid adding the same cit multiple times.
|
|
194
|
+
# we can live with some instances, where it does not work
|
|
195
|
+
def __hash__(self):
|
|
196
|
+
return self.model_dump_json().__hash__()
|
|
197
|
+
|
|
198
|
+
def __eq__(self, other):
|
|
199
|
+
if not isinstance(other, CIT_v1):
|
|
200
|
+
return False
|
|
201
|
+
return self.model_dump() == other.model_dump()
|
|
190
202
|
|
|
191
203
|
|
|
192
204
|
|
|
@@ -2,14 +2,13 @@ from functools import lru_cache
|
|
|
2
2
|
import logging
|
|
3
3
|
from typing import Self
|
|
4
4
|
from requests import get
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
from deprecated import deprecated
|
|
7
6
|
|
|
8
7
|
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
9
8
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
10
9
|
from labfreed.pac_id_resolver.services import ServiceGroup
|
|
11
10
|
from labfreed.pac_id_resolver.cit_v1 import CIT_v1
|
|
12
|
-
from labfreed.pac_id_resolver.
|
|
11
|
+
from labfreed.pac_id_resolver.resolver_config import ResolverConfig
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
|
|
@@ -22,49 +21,67 @@ def load_cit(path):
|
|
|
22
21
|
return cit_from_str(s)
|
|
23
22
|
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
@deprecated("cit version 1 is deprecated. use resolvber config and load with ResolverConfig.from_yaml(s)")
|
|
25
|
+
def cit_from_str(s:str, origin:str='') -> CIT_v1|ResolverConfig:
|
|
26
26
|
try:
|
|
27
|
-
cit2 =
|
|
27
|
+
cit2 = ResolverConfig.from_yaml(s)
|
|
28
28
|
cit_version = 'v2'
|
|
29
|
-
except Exception:
|
|
29
|
+
except Exception as e1:
|
|
30
30
|
cit2 = None
|
|
31
31
|
try:
|
|
32
32
|
cit1 = CIT_v1.from_csv(s, origin)
|
|
33
33
|
cit_version = 'v1' # noqa: F841
|
|
34
|
-
except Exception:
|
|
34
|
+
except Exception as e2:
|
|
35
35
|
cit1 = None
|
|
36
36
|
|
|
37
37
|
cit = cit2 or cit1 or None
|
|
38
38
|
return cit
|
|
39
39
|
|
|
40
40
|
@lru_cache
|
|
41
|
-
def
|
|
41
|
+
def _get_issuer_resolver_config(issuer:str):
|
|
42
42
|
'''Gets the issuer's cit.'''
|
|
43
|
+
# V2
|
|
44
|
+
url = 'HTTPS://PAC.' + issuer + '/resolver_config.yaml'
|
|
45
|
+
try:
|
|
46
|
+
r = get(url, timeout=2)
|
|
47
|
+
if r.status_code < 400:
|
|
48
|
+
config_str = r.text
|
|
49
|
+
resolver_config = ResolverConfig.from_yaml(config_str)
|
|
50
|
+
return resolver_config
|
|
51
|
+
else:
|
|
52
|
+
logging.error(f"Could not get CIT V2 form {issuer}")
|
|
53
|
+
except Exception:
|
|
54
|
+
logging.error(f"Could not get CIT V2 form {issuer}")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# V1 (as fallback)
|
|
43
58
|
url = 'HTTPS://PAC.' + issuer + '/coupling-information-table'
|
|
44
59
|
try:
|
|
45
60
|
r = get(url, timeout=2)
|
|
46
61
|
if r.status_code < 400:
|
|
47
|
-
|
|
62
|
+
config_str = r.text
|
|
63
|
+
cit = CIT_v1.from_csv(config_str, '')
|
|
64
|
+
return cit
|
|
48
65
|
else:
|
|
49
66
|
logging.error(f"Could not get CIT form {issuer}")
|
|
50
|
-
cit_str = None
|
|
51
67
|
except Exception:
|
|
52
68
|
logging.error(f"Could not get CIT form {issuer}")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return
|
|
69
|
+
|
|
70
|
+
# no cit found
|
|
71
|
+
return None
|
|
72
|
+
|
|
56
73
|
|
|
57
74
|
|
|
58
75
|
|
|
59
76
|
class PAC_ID_Resolver():
|
|
60
|
-
def __init__(self,
|
|
77
|
+
def __init__(self, resolver_configs:list[ResolverConfig|CIT_v1]=None) -> Self:
|
|
61
78
|
'''Initialize the resolver with coupling information tables'''
|
|
62
|
-
if not
|
|
63
|
-
|
|
64
|
-
self.
|
|
79
|
+
if not resolver_configs:
|
|
80
|
+
resolver_configs = []
|
|
81
|
+
self._resolver_configs = set(resolver_configs)
|
|
65
82
|
|
|
66
83
|
|
|
67
|
-
def resolve(self, pac_id:PAC_ID|str, check_service_status=True,
|
|
84
|
+
def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_resolver_config=True) -> list[ServiceGroup]:
|
|
68
85
|
'''Resolve a PAC-ID'''
|
|
69
86
|
if isinstance(pac_id, str):
|
|
70
87
|
pac_id_catless = PAC_ID.from_url(pac_id, try_pac_cat=False)
|
|
@@ -77,13 +94,13 @@ class PAC_ID_Resolver():
|
|
|
77
94
|
raise ValueError('pac_id is invalid. Should be a PAC-ID in url form or a PAC-ID object')
|
|
78
95
|
|
|
79
96
|
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
if
|
|
83
|
-
|
|
97
|
+
resolver_configs = self._resolver_configs.copy()
|
|
98
|
+
if use_issuer_resolver_config:
|
|
99
|
+
if issuer_resolver_config := _get_issuer_resolver_config(pac_id.issuer):
|
|
100
|
+
resolver_configs.add(issuer_resolver_config)
|
|
84
101
|
|
|
85
102
|
matches = []
|
|
86
|
-
for cit in
|
|
103
|
+
for cit in resolver_configs:
|
|
87
104
|
if isinstance(cit, CIT_v1):
|
|
88
105
|
# cit v1 has no concept of categories and implied keys. It would treat these segments as value segment
|
|
89
106
|
matches.append(cit.evaluate_pac_id(pac_id_catless))
|
|
@@ -8,18 +8,18 @@ import jsonpath_ng.ext as jsonpath
|
|
|
8
8
|
|
|
9
9
|
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
10
10
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
11
|
-
from labfreed.pac_id_resolver.
|
|
11
|
+
from labfreed.pac_id_resolver.resolver_config_common import ( ServiceType)
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
__all__ = [
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
15
|
+
"ResolverConfig",
|
|
16
|
+
"ResolverConfigBlock",
|
|
17
|
+
"ResolverConfigEntry"
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
class
|
|
22
|
+
class ResolverConfigEntry(LabFREED_BaseModel):
|
|
23
23
|
service_name: str
|
|
24
24
|
application_intents:list[str]
|
|
25
25
|
service_type:ServiceType |str
|
|
@@ -92,9 +92,9 @@ class CITEntry_v2(LabFREED_BaseModel):
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
|
|
95
|
-
class
|
|
95
|
+
class ResolverConfigBlock(LabFREED_BaseModel):
|
|
96
96
|
applicable_if: str = Field(default='True', alias='if')
|
|
97
|
-
entries: list[
|
|
97
|
+
entries: list[ResolverConfigEntry]
|
|
98
98
|
|
|
99
99
|
@field_validator('applicable_if', mode='before')
|
|
100
100
|
@classmethod
|
|
@@ -104,21 +104,21 @@ class CITBlock_v2(LabFREED_BaseModel):
|
|
|
104
104
|
|
|
105
105
|
|
|
106
106
|
|
|
107
|
-
class
|
|
107
|
+
class ResolverConfig(LabFREED_BaseModel):
|
|
108
108
|
schema_version: str = Field(default='2.0')
|
|
109
|
-
'''
|
|
109
|
+
'''Resolver Configuration'''
|
|
110
110
|
origin: str = ''
|
|
111
111
|
model_config = {
|
|
112
112
|
"extra": "allow"
|
|
113
113
|
}
|
|
114
114
|
'''@private'''
|
|
115
|
-
|
|
115
|
+
config: list[ResolverConfigBlock] = Field(default_factory=list)
|
|
116
116
|
|
|
117
117
|
@model_validator(mode='after')
|
|
118
118
|
def _validate_origin(self):
|
|
119
119
|
if len(self.origin) == 0:
|
|
120
120
|
self._add_validation_message(level=ValidationMsgLevel.WARNING,
|
|
121
|
-
source='
|
|
121
|
+
source='ResolverConfig origin',
|
|
122
122
|
msg='Origin should not be empty'
|
|
123
123
|
)
|
|
124
124
|
return self
|
|
@@ -136,27 +136,38 @@ class CIT_v2(LabFREED_BaseModel):
|
|
|
136
136
|
def __str__(self):
|
|
137
137
|
yml = yaml.dump(self.model_dump() )
|
|
138
138
|
return yml
|
|
139
|
+
|
|
140
|
+
# hash and equal are only used to avoid adding the same resolver config multiple times.
|
|
141
|
+
# we can live with some instances, where it does not work
|
|
142
|
+
def __hash__(self):
|
|
143
|
+
return self.model_dump_json().__hash__()
|
|
144
|
+
|
|
145
|
+
def __eq__(self, other):
|
|
146
|
+
if not isinstance(other, ResolverConfig):
|
|
147
|
+
return False
|
|
148
|
+
return self.model_dump() == other.model_dump()
|
|
149
|
+
|
|
139
150
|
|
|
140
151
|
def evaluate_pac_id(self, pac):
|
|
141
152
|
pac_id_json = pac.to_dict()
|
|
142
|
-
|
|
143
|
-
for block in self.
|
|
153
|
+
resolver_config_evaluated = ServiceGroup(origin=self.origin)
|
|
154
|
+
for block in self.config:
|
|
144
155
|
_, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
|
|
145
156
|
if not is_applicable:
|
|
146
157
|
continue
|
|
147
158
|
|
|
148
159
|
for e in block.entries:
|
|
149
160
|
if e.errors():
|
|
150
|
-
continue #make this stable against errors in the
|
|
161
|
+
continue #make this stable against errors in the resolver config
|
|
151
162
|
url = self._eval_url_template(pac_id_json, e.template_url)
|
|
152
|
-
|
|
163
|
+
resolver_config_evaluated.services.append(Service(
|
|
153
164
|
service_name=e.service_name,
|
|
154
165
|
application_intents=e.application_intents,
|
|
155
166
|
service_type=e.service_type,
|
|
156
167
|
url = url
|
|
157
168
|
)
|
|
158
169
|
)
|
|
159
|
-
return
|
|
170
|
+
return resolver_config_evaluated
|
|
160
171
|
|
|
161
172
|
|
|
162
173
|
def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from .text_base36_extension import TextBase36Extension
|
|
2
2
|
from .trex_extension import TREX_Extension
|
|
3
3
|
|
|
4
4
|
default_extension_interpreters = {
|
|
5
5
|
'TREX': TREX_Extension,
|
|
6
|
-
'
|
|
6
|
+
'TEXT': TextBase36Extension
|
|
7
7
|
}
|
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from typing import Literal, Self
|
|
3
|
-
from pydantic import
|
|
3
|
+
from pydantic import model_validator
|
|
4
4
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
5
5
|
from labfreed.pac_id.extension import ExtensionBase
|
|
6
|
-
from labfreed.
|
|
6
|
+
from labfreed.well_known_extensions.text_base36_extension import TextBase36Extension
|
|
7
7
|
|
|
8
|
+
from labfreed.utilities.base36 import from_base36
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
class DisplayNameExtension(TextBase36Extension, LabFREED_BaseModel):
|
|
10
12
|
name:Literal['N'] = 'N'
|
|
11
13
|
type:Literal['TEXT'] = 'TEXT'
|
|
12
|
-
display_name: str
|
|
14
|
+
display_name: str
|
|
13
15
|
|
|
14
|
-
@
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
@model_validator(mode='before')
|
|
17
|
+
def move_display_name_to_text(cls, data):
|
|
18
|
+
# if display_name provided, move it to text
|
|
19
|
+
if isinstance(data, dict) and 'display_name' in data:
|
|
20
|
+
data['text'] = data.pop('display_name')
|
|
21
|
+
return data
|
|
19
22
|
|
|
20
23
|
@staticmethod
|
|
21
24
|
def from_extension(ext:ExtensionBase) -> Self:
|
|
22
25
|
return DisplayNameExtension.create(name=ext.name,
|
|
23
26
|
type=ext.type,
|
|
24
27
|
data=ext.data)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def display_name(self) -> str:
|
|
31
|
+
return self.text
|
|
32
|
+
|
|
25
33
|
|
|
26
34
|
@staticmethod
|
|
27
35
|
def create(*, name, type, data):
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal, Self
|
|
3
|
+
from pydantic import computed_field
|
|
4
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
5
|
+
from labfreed.pac_id.extension import ExtensionBase
|
|
6
|
+
from labfreed.utilities.base36 import from_base36, to_base36
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TextBase36Extension(ExtensionBase, LabFREED_BaseModel):
|
|
10
|
+
name:str
|
|
11
|
+
type:Literal['TEXT'] = 'TEXT'
|
|
12
|
+
text: str
|
|
13
|
+
|
|
14
|
+
@computed_field
|
|
15
|
+
@property
|
|
16
|
+
def data(self)->str:
|
|
17
|
+
# return '/'.join([to_base36(dn) for dn in self.display_name])
|
|
18
|
+
return to_base36(self.text).root
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def from_extension(ext:ExtensionBase) -> Self:
|
|
22
|
+
return TextBase36Extension.create(name=ext.name,
|
|
23
|
+
type=ext.type,
|
|
24
|
+
data=ext.data)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def create(*, name, type, data):
|
|
28
|
+
|
|
29
|
+
if type != 'TEXT':
|
|
30
|
+
logging.warning(f'Type {name} was given, but this extension should only be used with type "TEXT". Will try to parse data as display names')
|
|
31
|
+
|
|
32
|
+
text = from_base36(data)
|
|
33
|
+
|
|
34
|
+
return TextBase36Extension(name=name, text=text)
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return 'Text: '+ self.text
|
|
38
|
+
|
|
@@ -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"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
labfreed/__init__.py,sha256
|
|
2
|
-
labfreed/labfreed_infrastructure.py,sha256=
|
|
3
|
-
labfreed/labfreed_extended/app/app_infrastructure.py,sha256=
|
|
1
|
+
labfreed/__init__.py,sha256=fen6oZE4WpF77ck1RZ-1lkwRwBtLfUO6RU1CO2W91Hs,339
|
|
2
|
+
labfreed/labfreed_infrastructure.py,sha256=ss1PyJl-7Es-lEcxptmdYI9kDAHmh7HB_tAGkPC6UVs,10173
|
|
3
|
+
labfreed/labfreed_extended/app/app_infrastructure.py,sha256=Kkpmw6VZZIxJ6mXRQmX_unS-coHVbKAnlr9VOZNSqyU,4418
|
|
4
4
|
labfreed/labfreed_extended/app/formatted_print.py,sha256=DcwWP0ix1e_wYNIdceIp6cETkJdG2DqpU8Gs3aZAL40,1930
|
|
5
|
-
labfreed/labfreed_extended/app/pac_info/pac_info.py,sha256=
|
|
5
|
+
labfreed/labfreed_extended/app/pac_info/pac_info.py,sha256=9Wre1wAmMZObeh7Ed46KA38nBWcXhqGD0m9vTHfkkS8,8397
|
|
6
6
|
labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg,sha256=H5z9s4VvHq09UnHdqfrYNsx-Whljc0gE4qKJ6-3kfgQ,1158
|
|
7
7
|
labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html,sha256=1S-dxibPwJshtdelsmyA4LpgOm84L6RTXPNO93gmPfg,5964
|
|
8
8
|
labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css,sha256=C5pyD956fd6pJgUBjGxvxgL0Wbgq0v7ZLY4Vr-sJZ7A,4169
|
|
@@ -11,12 +11,12 @@ labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html,s
|
|
|
11
11
|
labfreed/pac_attributes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
12
|
labfreed/pac_attributes/well_knonw_attribute_keys.py,sha256=axE81MeJ3G_Wy1PbmNAXH6SfPtl96NXvQJMyrvK10t4,324
|
|
13
13
|
labfreed/pac_attributes/api_data_models/request.py,sha256=N-kXlJWYqh-F1TzNunCwHUPhme3bSLJMgb9aAHWGOy4,1880
|
|
14
|
-
labfreed/pac_attributes/api_data_models/response.py,sha256=
|
|
14
|
+
labfreed/pac_attributes/api_data_models/response.py,sha256=MbpzaSnAQgvdrzYxCAtdfr480Vg4XMBBbZPKP9yirf4,8475
|
|
15
15
|
labfreed/pac_attributes/api_data_models/server_capabilities_response.py,sha256=ypDm4f8xZZl036fp8PuIe6lJHNW5Zg1fItgUlnV75V0,178
|
|
16
16
|
labfreed/pac_attributes/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
17
17
|
labfreed/pac_attributes/client/attribute_cache.py,sha256=ThUadWqQ5oM8DnAnvZuY4jeA3Mg06ePNEcRP5wCsadc,2222
|
|
18
18
|
labfreed/pac_attributes/client/client.py,sha256=FjvyEpZEGYrZkuaQoqH9QFstrwHmrsIdaymxz8uQjwQ,7510
|
|
19
|
-
labfreed/pac_attributes/pythonic/attribute_server_factory.py,sha256=
|
|
19
|
+
labfreed/pac_attributes/pythonic/attribute_server_factory.py,sha256=3OeFjBdlMR4DMIzHVo5-A_y935e2_lOqvFCLKgxjsEY,6159
|
|
20
20
|
labfreed/pac_attributes/pythonic/excel_attribute_data_source.py,sha256=oP4OHj0DTlH4dD7OlL1qxtX4y9KcuDTCd9Bi_FruP6A,7276
|
|
21
21
|
labfreed/pac_attributes/pythonic/py_attributes.py,sha256=FXSp9_P0o-GuZSDvXtD2fU4g82lglMu9f_-8KPMkEP0,6821
|
|
22
22
|
labfreed/pac_attributes/pythonic/py_dict_data_source.py,sha256=nAz6GA7Xx_0IORPPpt_Wl3sFJa1Q5Fnq5vdf1uQiJF8,531
|
|
@@ -25,20 +25,20 @@ labfreed/pac_attributes/server/attribute_data_sources.py,sha256=7-YQeBcn5ndsZWee
|
|
|
25
25
|
labfreed/pac_attributes/server/server.py,sha256=tPOPezRC3YEE0i-MJIc23Me6EARaSqzyFdUNjUzqtdI,9117
|
|
26
26
|
labfreed/pac_attributes/server/translation_data_sources.py,sha256=axALOqfP840sOSdVCRYtrens97mm-hpfONMUyuVlCrY,2145
|
|
27
27
|
labfreed/pac_cat/__init__.py,sha256=KNPtQzBD1XVohvG_ucOs7RJj-oi6biUTGB1k-T2o6pk,568
|
|
28
|
-
labfreed/pac_cat/category_base.py,sha256=
|
|
28
|
+
labfreed/pac_cat/category_base.py,sha256=XAuvfJSCsQ3Ypi5WujtaCmt5isj7Qsx7-zJa9Sm0XVY,2530
|
|
29
29
|
labfreed/pac_cat/pac_cat.py,sha256=wcb_fhvgjS2xmqTsxS8_Oibvr1nsQt5zr8aUajLfK1E,5578
|
|
30
|
-
labfreed/pac_cat/predefined_categories.py,sha256=
|
|
30
|
+
labfreed/pac_cat/predefined_categories.py,sha256=dyyF90mdPV-RGjk3NG8AQRbEQDcxtP95w2DwfDeO-_8,12775
|
|
31
31
|
labfreed/pac_id/__init__.py,sha256=NGMbzkwQ4txKeT5pxdIZordwHO8J3_q84jzPanjKoHg,675
|
|
32
|
-
labfreed/pac_id/extension.py,sha256=
|
|
32
|
+
labfreed/pac_id/extension.py,sha256=4_cQ-N3x8bRxboqC44Qnf6rGnYXvsvHOnlBaY7-Hf-8,2264
|
|
33
33
|
labfreed/pac_id/id_segment.py,sha256=r5JU1SJuRXhZJJxy5T3xjrb598wIDTLpivSJhIUAzjQ,4526
|
|
34
34
|
labfreed/pac_id/pac_id.py,sha256=DDcSYJ8DBWqIoW_usOT7eDjHZ9700cTYxeUgenHluOA,5378
|
|
35
35
|
labfreed/pac_id/url_parser.py,sha256=F3SPiscfbPwZ0uMzgirJ1vwgaXclN546lBW46Ywo3nk,5979
|
|
36
36
|
labfreed/pac_id/url_serializer.py,sha256=01LB30pNMBtv2rYHsiE_4Ga2iVA515Boj4ikOIYhiBQ,3511
|
|
37
37
|
labfreed/pac_id_resolver/__init__.py,sha256=RNBlrDOSR42gmSNH9wJVhK_xwEX45cvTKVgWW2bjh7Q,113
|
|
38
|
-
labfreed/pac_id_resolver/
|
|
39
|
-
labfreed/pac_id_resolver/
|
|
40
|
-
labfreed/pac_id_resolver/
|
|
41
|
-
labfreed/pac_id_resolver/
|
|
38
|
+
labfreed/pac_id_resolver/cit_v1.py,sha256=JGlEH2d9awEu3HxPW7vu0uj4ZC3B02IdmFg7aJ4axQw,9833
|
|
39
|
+
labfreed/pac_id_resolver/resolver.py,sha256=ExVMuKN_g6VFmHkW8WFgXunxFbvfPsan9jBQmn_MYIs,3956
|
|
40
|
+
labfreed/pac_id_resolver/resolver_config.py,sha256=2j3j5UsVoWqU9NjciN3j-Fs53diGi4FQEvoz8ng103Y,12388
|
|
41
|
+
labfreed/pac_id_resolver/resolver_config_common.py,sha256=jzoDOxog8YW68q7vyvDGCZcVcgIzJHXlMt8KwgVnx6o,2885
|
|
42
42
|
labfreed/pac_id_resolver/services.py,sha256=vtnxLm38t4PNOf73cXh6UZOtWZZOGxfBCfXUDRxGHog,2592
|
|
43
43
|
labfreed/qr/__init__.py,sha256=fdKwP6W2Js--yMbBUdn-g_2uq2VqPpfQJeDLHsMDO-Y,61
|
|
44
44
|
labfreed/qr/generate_qr.py,sha256=mSt-U872O3ReHB_UdS-MzYu0wRgdlKcAOEfTxg5CLRk,16616
|
|
@@ -55,8 +55,9 @@ labfreed/utilities/base36.py,sha256=_yX8aQ1OwrK5tnJU1NUEzQSFGr9xAVnNvPObpNzCPYs,
|
|
|
55
55
|
labfreed/utilities/ensure_utc_time.py,sha256=1ZTTzyIt7IimQ4ArTzdgw5hxiabkkplltbQe3Wdt2ZQ,307
|
|
56
56
|
labfreed/utilities/translations.py,sha256=XY4Wud_BfXswUOpebdh0U_D2bMzb2vqluuGWzFK-3uU,1851
|
|
57
57
|
labfreed/well_known_extensions/__init__.py,sha256=CjZTjx8Cn8763Hhnv_--Wj1LcFpFs2cyQwWrrzOS4xM,246
|
|
58
|
-
labfreed/well_known_extensions/default_extension_interpreters.py,sha256=
|
|
59
|
-
labfreed/well_known_extensions/display_name_extension.py,sha256=
|
|
58
|
+
labfreed/well_known_extensions/default_extension_interpreters.py,sha256=6r2h47g8sZAyuN25DS0S0CdNQbyYRMFljdkDSrnnll0,201
|
|
59
|
+
labfreed/well_known_extensions/display_name_extension.py,sha256=txZelIj-B4_ycudaQJsFr0TFuiNRe5wEMQ9hE9wtd-Q,1737
|
|
60
|
+
labfreed/well_known_extensions/text_base36_extension.py,sha256=2SYWuKx2FlnkkSsZS5Z97QqMx10Kk67kzj5-BEoAo8E,1262
|
|
60
61
|
labfreed/well_known_extensions/trex_extension.py,sha256=tffklaambkFPExcIDRAG9GJ7CHXeuFAagl6FuwS-2kI,929
|
|
61
62
|
labfreed/well_known_keys/gs1/__init__.py,sha256=LOFycgqS6OuV8t22TmtHy-ZI2iuXc3jJfVFwRFVDM3I,103
|
|
62
63
|
labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py,sha256=qSWD7bpTJQdQhXbZHJc20TRdqrnrxICD79goXWr6B-g,1405
|
|
@@ -64,7 +65,7 @@ labfreed/well_known_keys/labfreed/well_known_keys.py,sha256=p-hXwEEIs7p2SKn9DQeL
|
|
|
64
65
|
labfreed/well_known_keys/unece/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
|
|
65
66
|
labfreed/well_known_keys/unece/__init__.py,sha256=MSP9lmjg9_D9iqG9Yq2_ajYfQSNS9wIT7FXA1c--59M,122
|
|
66
67
|
labfreed/well_known_keys/unece/unece_units.py,sha256=J20d64H69qKDE3XlGdJoXIIh0G-d0jKoiIDsg9an5pk,1655
|
|
67
|
-
labfreed-1.0.
|
|
68
|
-
labfreed-1.0.
|
|
69
|
-
labfreed-1.0.
|
|
70
|
-
labfreed-1.0.
|
|
68
|
+
labfreed-1.0.0a14.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
|
|
69
|
+
labfreed-1.0.0a14.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
|
70
|
+
labfreed-1.0.0a14.dist-info/METADATA,sha256=OiIaarhRnwP3FAkV3Q6_1tkW5v4pj_Y84RUxcmJWBBY,19775
|
|
71
|
+
labfreed-1.0.0a14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|