labfreed 1.0.0a13__py3-none-any.whl → 1.0.0a15__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 +18 -7
- 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 +104 -31
- labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -1
- labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +9 -3
- labfreed/pac_attributes/pythonic/py_attributes.py +50 -21
- labfreed/pac_cat/category_base.py +1 -1
- labfreed/pac_cat/predefined_categories.py +39 -3
- labfreed/pac_id/extension.py +2 -1
- labfreed/pac_id_resolver/cit_v1.py +6 -4
- labfreed/pac_id_resolver/resolver.py +48 -28
- labfreed/pac_id_resolver/{cit_v2.py → resolver_config.py} +18 -18
- 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.0a13.dist-info → labfreed-1.0.0a15.dist-info}/METADATA +2 -1
- {labfreed-1.0.0a13.dist-info → labfreed-1.0.0a15.dist-info}/RECORD +22 -21
- /labfreed/pac_id_resolver/{cit_common.py → resolver_config_common.py} +0 -0
- {labfreed-1.0.0a13.dist-info → labfreed-1.0.0a15.dist-info}/WHEEL +0 -0
- {labfreed-1.0.0a13.dist-info → labfreed-1.0.0a15.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
|
|
2
|
-
from datetime import date, datetime, time
|
|
2
|
+
from datetime import UTC, date, datetime, time
|
|
3
3
|
import json
|
|
4
4
|
from typing import Literal
|
|
5
5
|
import warnings
|
|
6
6
|
from pydantic import RootModel, field_validator
|
|
7
7
|
|
|
8
8
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
9
|
-
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ResourceAttribute, TextAttribute
|
|
9
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, BoolListAttribute, DateTimeAttribute, DateTimeListAttribute, NumericAttribute, NumericListAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ReferenceListAttribute, ResourceAttribute, ResourceListAttribute, TextAttribute, TextListAttribute
|
|
10
10
|
from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
|
|
11
11
|
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
12
|
from labfreed.trex.pythonic.quantity import Quantity
|
|
@@ -66,13 +66,21 @@ class pyAttributes(RootModel[list[pyAttribute]]):
|
|
|
66
66
|
value_list = attribute.value_list
|
|
67
67
|
first_value = value_list[0]
|
|
68
68
|
if isinstance(first_value, bool):
|
|
69
|
-
|
|
69
|
+
if len(value_list) == 1:
|
|
70
|
+
return BoolAttribute(value=value_list[0], **common_args)
|
|
71
|
+
else:
|
|
72
|
+
return BoolListAttribute(value=value_list, **common_args)
|
|
73
|
+
|
|
70
74
|
|
|
71
75
|
elif isinstance(first_value, datetime | date | time):
|
|
72
76
|
for v in value_list:
|
|
73
77
|
if not v.tzinfo:
|
|
74
78
|
warnings.warn(f'No timezone given for {v}. Assuming it is in UTC.')
|
|
75
|
-
|
|
79
|
+
v.replace(tzinfo=UTC)
|
|
80
|
+
if len(value_list) == 1:
|
|
81
|
+
return DateTimeAttribute(value=value_list[0], **common_args)
|
|
82
|
+
else:
|
|
83
|
+
return DateTimeListAttribute(value=value_list, **common_args)
|
|
76
84
|
# return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
|
|
77
85
|
|
|
78
86
|
|
|
@@ -83,9 +91,13 @@ class pyAttributes(RootModel[list[pyAttribute]]):
|
|
|
83
91
|
v = Quantity(value=v, unit='dimensionless')
|
|
84
92
|
values.append(NumericValue(numerical_value=v.value_as_str(),
|
|
85
93
|
unit = v.unit))
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if len(values) == 1:
|
|
95
|
+
num_attr = NumericAttribute(value=values[0], **common_args)
|
|
96
|
+
else:
|
|
97
|
+
num_attr = NumericListAttribute(value=values, **common_args)
|
|
98
|
+
num_attr.print_validation_messages()
|
|
99
|
+
|
|
100
|
+
return num_attr
|
|
89
101
|
|
|
90
102
|
elif isinstance(first_value, str):
|
|
91
103
|
# capture quantities in the form of "100.0e5 g/L"
|
|
@@ -94,20 +106,37 @@ class pyAttributes(RootModel[list[pyAttribute]]):
|
|
|
94
106
|
for v in value_list:
|
|
95
107
|
q = Quantity.from_str_with_unit(v)
|
|
96
108
|
values.append( NumericValue(numerical_value=q.value_as_str(), unit = q.unit) )
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
if len(values) == 1:
|
|
110
|
+
return NumericAttribute(value=values[0], **common_args)
|
|
111
|
+
else:
|
|
112
|
+
return NumericListAttribute(value=values, **common_args)
|
|
113
|
+
|
|
100
114
|
else:
|
|
101
|
-
|
|
115
|
+
if len(value_list) == 1:
|
|
116
|
+
return TextAttribute(value=value_list[0], **common_args)
|
|
117
|
+
else:
|
|
118
|
+
return TextListAttribute(value=value_list, **common_args)
|
|
102
119
|
|
|
103
120
|
elif isinstance(first_value, pyReference):
|
|
104
|
-
|
|
121
|
+
values = [v.root for v in value_list]
|
|
122
|
+
if len(values) == 1:
|
|
123
|
+
return ReferenceAttribute(value=values[0], **common_args)
|
|
124
|
+
else:
|
|
125
|
+
return ReferenceListAttribute(value=values, **common_args)
|
|
105
126
|
|
|
106
127
|
elif isinstance(first_value, pyResource):
|
|
107
|
-
|
|
128
|
+
values = [v.root for v in value_list]
|
|
129
|
+
if len(values) == 1:
|
|
130
|
+
return ResourceAttribute(value=values[0], **common_args)
|
|
131
|
+
else:
|
|
132
|
+
return ResourceListAttribute(value=values, **common_args)
|
|
108
133
|
|
|
109
134
|
elif isinstance(first_value, PAC_ID):
|
|
110
|
-
|
|
135
|
+
values = [v.to_url(include_extensions=False) for v in value_list]
|
|
136
|
+
if len(values) == 1:
|
|
137
|
+
return ReferenceAttribute(value=values[0], **common_args)
|
|
138
|
+
else:
|
|
139
|
+
return ReferenceListAttribute(value=values, **common_args)
|
|
111
140
|
|
|
112
141
|
else: #this covers the last resort case of arbitrary objects. Must be json serializable.
|
|
113
142
|
try :
|
|
@@ -124,25 +153,25 @@ class pyAttributes(RootModel[list[pyAttribute]]):
|
|
|
124
153
|
for a in attributes:
|
|
125
154
|
value_list = a.value if isinstance(a.value, list) else [a.value]
|
|
126
155
|
match a:
|
|
127
|
-
case ReferenceAttribute():
|
|
156
|
+
case ReferenceAttribute() | ReferenceListAttribute():
|
|
128
157
|
values = [pyReference(v) for v in value_list]
|
|
129
158
|
|
|
130
|
-
case ResourceAttribute():
|
|
159
|
+
case ResourceAttribute() | ResourceListAttribute():
|
|
131
160
|
values = [pyResource(v) for v in value_list]
|
|
132
161
|
|
|
133
|
-
case NumericAttribute():
|
|
162
|
+
case NumericAttribute() | NumericListAttribute():
|
|
134
163
|
values = [ Quantity.from_str_value(value=v.numerical_value, unit=v.unit) for v in value_list]
|
|
135
164
|
|
|
136
|
-
case BoolAttribute():
|
|
165
|
+
case BoolAttribute() | BoolAttribute():
|
|
137
166
|
values = value_list
|
|
138
167
|
|
|
139
|
-
case TextAttribute():
|
|
168
|
+
case TextAttribute() | TextListAttribute():
|
|
140
169
|
values = value_list
|
|
141
170
|
|
|
142
|
-
case DateTimeAttribute():
|
|
171
|
+
case DateTimeAttribute() | DateTimeAttribute():
|
|
143
172
|
values = value_list
|
|
144
173
|
|
|
145
|
-
case ObjectAttribute():
|
|
174
|
+
case ObjectAttribute() | ObjectAttribute():
|
|
146
175
|
values = value_list
|
|
147
176
|
|
|
148
177
|
|
|
@@ -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'''
|
|
@@ -252,6 +283,11 @@ class Misc(Category, ABC):
|
|
|
252
283
|
)
|
|
253
284
|
return self
|
|
254
285
|
|
|
286
|
+
@property
|
|
287
|
+
def is_serialized(self) -> bool:
|
|
288
|
+
return bool(self.id)
|
|
289
|
+
|
|
290
|
+
|
|
255
291
|
|
|
256
292
|
category_key_to_class_map = {
|
|
257
293
|
'-MD': Material_Device,
|
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]
|
|
@@ -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,18 +94,21 @@ 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
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
103
|
+
for cit in resolver_configs:
|
|
104
|
+
try:
|
|
105
|
+
if isinstance(cit, CIT_v1):
|
|
106
|
+
# cit v1 has no concept of categories and implied keys. It would treat these segments as value segment
|
|
107
|
+
matches.append(cit.evaluate_pac_id(pac_id_catless))
|
|
108
|
+
else:
|
|
109
|
+
matches.append(cit.evaluate_pac_id(pac_id))
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logging.error(f'Failed to resolve pac {pac_id.to_url()} with cit {cit.origin}')
|
|
92
112
|
|
|
93
113
|
if check_service_status:
|
|
94
114
|
for m in matches:
|
|
@@ -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
|
|
@@ -137,37 +137,37 @@ class CIT_v2(LabFREED_BaseModel):
|
|
|
137
137
|
yml = yaml.dump(self.model_dump() )
|
|
138
138
|
return yml
|
|
139
139
|
|
|
140
|
-
# hash and equal are only used to avoid adding the same
|
|
140
|
+
# hash and equal are only used to avoid adding the same resolver config multiple times.
|
|
141
141
|
# we can live with some instances, where it does not work
|
|
142
142
|
def __hash__(self):
|
|
143
143
|
return self.model_dump_json().__hash__()
|
|
144
144
|
|
|
145
145
|
def __eq__(self, other):
|
|
146
|
-
if not isinstance(other,
|
|
146
|
+
if not isinstance(other, ResolverConfig):
|
|
147
147
|
return False
|
|
148
148
|
return self.model_dump() == other.model_dump()
|
|
149
149
|
|
|
150
150
|
|
|
151
151
|
def evaluate_pac_id(self, pac):
|
|
152
152
|
pac_id_json = pac.to_dict()
|
|
153
|
-
|
|
154
|
-
for block in self.
|
|
153
|
+
resolver_config_evaluated = ServiceGroup(origin=self.origin)
|
|
154
|
+
for block in self.config:
|
|
155
155
|
_, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
|
|
156
156
|
if not is_applicable:
|
|
157
157
|
continue
|
|
158
158
|
|
|
159
159
|
for e in block.entries:
|
|
160
160
|
if e.errors():
|
|
161
|
-
continue #make this stable against errors in the
|
|
161
|
+
continue #make this stable against errors in the resolver config
|
|
162
162
|
url = self._eval_url_template(pac_id_json, e.template_url)
|
|
163
|
-
|
|
163
|
+
resolver_config_evaluated.services.append(Service(
|
|
164
164
|
service_name=e.service_name,
|
|
165
165
|
application_intents=e.application_intents,
|
|
166
166
|
service_type=e.service_type,
|
|
167
167
|
url = url
|
|
168
168
|
)
|
|
169
169
|
)
|
|
170
|
-
return
|
|
170
|
+
return resolver_config_evaluated
|
|
171
171
|
|
|
172
172
|
|
|
173
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.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"
|