labfreed 0.2.8__py3-none-any.whl → 0.2.9__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.
Potentially problematic release.
This version of labfreed might be problematic. Click here for more details.
- labfreed/__init__.py +11 -11
- labfreed/labfreed_infrastructure.py +258 -258
- labfreed/pac_cat/__init__.py +19 -19
- labfreed/pac_cat/category_base.py +51 -51
- labfreed/pac_cat/pac_cat.py +150 -150
- labfreed/pac_cat/predefined_categories.py +200 -200
- labfreed/pac_id/__init__.py +19 -19
- labfreed/pac_id/extension.py +48 -48
- labfreed/pac_id/id_segment.py +89 -89
- labfreed/pac_id/pac_id.py +140 -140
- labfreed/pac_id/url_parser.py +155 -155
- labfreed/pac_id/url_serializer.py +85 -84
- labfreed/pac_id_resolver/__init__.py +2 -2
- labfreed/pac_id_resolver/cit_common.py +81 -81
- labfreed/pac_id_resolver/cit_v1.py +244 -244
- labfreed/pac_id_resolver/cit_v2.py +313 -313
- labfreed/pac_id_resolver/resolver.py +97 -97
- labfreed/pac_id_resolver/services.py +82 -82
- labfreed/qr/__init__.py +1 -1
- labfreed/qr/generate_qr.py +422 -422
- labfreed/trex/__init__.py +16 -16
- labfreed/trex/python_convenience/__init__.py +3 -3
- labfreed/trex/python_convenience/data_table.py +87 -87
- labfreed/trex/python_convenience/pyTREX.py +248 -248
- labfreed/trex/python_convenience/quantity.py +66 -66
- labfreed/trex/table_segment.py +245 -245
- labfreed/trex/trex.py +69 -69
- labfreed/trex/trex_base_models.py +209 -209
- labfreed/trex/value_segments.py +99 -99
- labfreed/utilities/base36.py +82 -82
- labfreed/well_known_extensions/__init__.py +4 -4
- labfreed/well_known_extensions/default_extension_interpreters.py +6 -6
- labfreed/well_known_extensions/display_name_extension.py +40 -40
- labfreed/well_known_extensions/trex_extension.py +30 -30
- labfreed/well_known_keys/gs1/__init__.py +5 -5
- labfreed/well_known_keys/gs1/gs1.py +3 -3
- labfreed/well_known_keys/labfreed/well_known_keys.py +15 -15
- labfreed/well_known_keys/unece/__init__.py +3 -3
- labfreed/well_known_keys/unece/unece_units.py +67 -67
- {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/METADATA +11 -8
- labfreed-0.2.9.dist-info/RECORD +45 -0
- {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/licenses/LICENSE +21 -21
- labfreed-0.2.8.dist-info/RECORD +0 -45
- {labfreed-0.2.8.dist-info → labfreed-0.2.9.dist-info}/WHEEL +0 -0
|
@@ -1,313 +1,313 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
|
-
import json
|
|
3
|
-
import re
|
|
4
|
-
from typing import Self
|
|
5
|
-
from pydantic import Field, field_validator, model_validator
|
|
6
|
-
import yaml
|
|
7
|
-
import jsonpath_ng.ext as jsonpath
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
11
|
-
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
12
|
-
from labfreed.pac_id_resolver.cit_common import ( _add_msg_to_cit_entry_model,
|
|
13
|
-
_validate_service_name,
|
|
14
|
-
_validate_application_intent,
|
|
15
|
-
_validate_service_type,
|
|
16
|
-
ServiceType)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
__all__ = [
|
|
20
|
-
"CIT_v2",
|
|
21
|
-
"CITBlock_v2",
|
|
22
|
-
"CITEntry_v2"
|
|
23
|
-
]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class CITEntry_v2(LabFREED_BaseModel):
|
|
28
|
-
service_name: str
|
|
29
|
-
application_intents:list[str]
|
|
30
|
-
service_type:ServiceType |str
|
|
31
|
-
template_url:str
|
|
32
|
-
|
|
33
|
-
@model_validator(mode='after')
|
|
34
|
-
def _validate_service_name(self):
|
|
35
|
-
# service_name
|
|
36
|
-
if not_allowed_chars := set(re.sub(r'[A-Za-z0-9\-\x20]', '', self.service_name)):
|
|
37
|
-
self._add_validation_message(
|
|
38
|
-
level=ValidationMsgLevel.ERROR,
|
|
39
|
-
source=f'Service {self.service_name}',
|
|
40
|
-
msg=f'Service name ontains invalid characters {_quote_texts(not_allowed_chars)}',
|
|
41
|
-
highlight_sub=not_allowed_chars
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
if len(self.service_name) == 0 or len(self.service_name) > 255:
|
|
45
|
-
self._add_validation_message(
|
|
46
|
-
level=ValidationMsgLevel.ERROR,
|
|
47
|
-
source=f'Service {self.service_name}',
|
|
48
|
-
msg='Service name must be at least one and maximum 255 characters long'
|
|
49
|
-
)
|
|
50
|
-
return self
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
@model_validator(mode='after')
|
|
54
|
-
def _validate_application_intent(self):
|
|
55
|
-
for intent in self.application_intents:
|
|
56
|
-
if re.fullmatch('.*-generic$', intent):
|
|
57
|
-
self._add_validation_message(
|
|
58
|
-
level=ValidationMsgLevel.ERROR,
|
|
59
|
-
source=f'Application intent {intent}',
|
|
60
|
-
msg="Ends with '-generic'. This is not permitted, since it is reserved for future uses'",
|
|
61
|
-
highlight_sub=[intent]
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
if not_allowed_chars := set(re.sub(r'[A-Za-z0-9\-]', '', intent)):
|
|
65
|
-
self._add_validation_message(
|
|
66
|
-
level=ValidationMsgLevel.ERROR,
|
|
67
|
-
source=f'Application intent {self.service_name}',
|
|
68
|
-
msg=f'Contains invalid characters {_quote_texts(not_allowed_chars)}',
|
|
69
|
-
highlight_sub=not_allowed_chars
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
if len(intent) == 0 or len(intent) > 255:
|
|
73
|
-
self._add_validation_message(
|
|
74
|
-
level=ValidationMsgLevel.ERROR,
|
|
75
|
-
source=f'Application intent {intent}',
|
|
76
|
-
msg='Must be at least one and maximum 255 characters long'
|
|
77
|
-
)
|
|
78
|
-
return self
|
|
79
|
-
|
|
80
|
-
@model_validator(mode='after')
|
|
81
|
-
def _validate_service_type(self):
|
|
82
|
-
allowed_types = [ServiceType.ATTRIBUTE_SERVICE_GENERIC.value, ServiceType.USER_HANDOVER_GENERIC.value]
|
|
83
|
-
if self.service_type not in allowed_types:
|
|
84
|
-
if isinstance(self.service_type, ServiceType):
|
|
85
|
-
s= self.service_type.value
|
|
86
|
-
else:
|
|
87
|
-
s= self.service_type
|
|
88
|
-
for at in allowed_types:
|
|
89
|
-
s = s.replace(at,'')
|
|
90
|
-
self._add_validation_message(
|
|
91
|
-
level=ValidationMsgLevel.ERROR,
|
|
92
|
-
source=f'Service Type {self.service_type}',
|
|
93
|
-
msg=f'Invalid service type. Must be {_quote_texts(allowed_types)} must be at least one and maximum 255 characters long',
|
|
94
|
-
highlight_sub=s
|
|
95
|
-
)
|
|
96
|
-
return self
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class CITBlock_v2(LabFREED_BaseModel):
|
|
101
|
-
applicable_if: str = Field(default='True', alias='if')
|
|
102
|
-
entries: list[CITEntry_v2]
|
|
103
|
-
|
|
104
|
-
@field_validator('applicable_if', mode='before')
|
|
105
|
-
@classmethod
|
|
106
|
-
def _convert_if(cls, v):
|
|
107
|
-
return v if v is not None else 'True'
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class CIT_v2(LabFREED_BaseModel):
|
|
113
|
-
'''Coupling Information Table (CIT)'''
|
|
114
|
-
origin: str = ''
|
|
115
|
-
model_config = {
|
|
116
|
-
"extra": "allow"
|
|
117
|
-
}
|
|
118
|
-
'''@private'''
|
|
119
|
-
cit: list[CITBlock_v2] = Field(default_factory=list)
|
|
120
|
-
|
|
121
|
-
@model_validator(mode='after')
|
|
122
|
-
def _validate_origin(self):
|
|
123
|
-
if len(self.origin) == 0:
|
|
124
|
-
self._add_validation_message(level=ValidationMsgLevel.WARNING,
|
|
125
|
-
source='CIT origin',
|
|
126
|
-
msg='Origin should not be empty'
|
|
127
|
-
)
|
|
128
|
-
return self
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@classmethod
|
|
132
|
-
def from_yaml(cls, yml:str) -> Self:
|
|
133
|
-
try:
|
|
134
|
-
d = yaml.safe_load(yml)
|
|
135
|
-
except yaml.YAMLError as e:
|
|
136
|
-
# not a valid yaml
|
|
137
|
-
raise ValueError("This is not a valid yaml") from e
|
|
138
|
-
return cls.model_validate(d)
|
|
139
|
-
|
|
140
|
-
def __str__(self):
|
|
141
|
-
yml = yaml.dump(self.model_dump() )
|
|
142
|
-
return yml
|
|
143
|
-
|
|
144
|
-
def evaluate_pac_id(self, pac):
|
|
145
|
-
pac_id_json = pac.to_dict()
|
|
146
|
-
cit_evaluated = ServiceGroup(origin=self.origin)
|
|
147
|
-
for block in self.cit:
|
|
148
|
-
_, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
|
|
149
|
-
if not is_applicable:
|
|
150
|
-
continue
|
|
151
|
-
|
|
152
|
-
for e in block.entries:
|
|
153
|
-
if e.errors():
|
|
154
|
-
continue #make this stable against errors in the cit
|
|
155
|
-
url = self._eval_url_template(pac_id_json, e.template_url)
|
|
156
|
-
cit_evaluated.services.append(Service(
|
|
157
|
-
service_name=e.service_name,
|
|
158
|
-
application_intents=e.application_intents,
|
|
159
|
-
service_type=e.service_type,
|
|
160
|
-
url = url
|
|
161
|
-
)
|
|
162
|
-
)
|
|
163
|
-
return cit_evaluated
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
|
|
167
|
-
expression = self._apply_convenience_substitutions(expression)
|
|
168
|
-
|
|
169
|
-
tokens = self._tokenize_jsonpath_expression(expression)
|
|
170
|
-
expression_for_eval = self._expression_from_tokens(pac_id_json, tokens)
|
|
171
|
-
applicable = eval(expression_for_eval, {}, {})
|
|
172
|
-
|
|
173
|
-
return expression_for_eval, applicable
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
def _apply_convenience_substitutions(self, query):
|
|
177
|
-
''' applies a few substitutions, which enable abbreviated syntax.'''
|
|
178
|
-
|
|
179
|
-
# allow access to array elements by key
|
|
180
|
-
q_mod = re.sub(r'\[(".+?")\]', r'[?(@.key == \1)]', query )
|
|
181
|
-
|
|
182
|
-
# allow shorter path
|
|
183
|
-
# substitutions = [
|
|
184
|
-
# (r'(?<=^)id', 'pac.id'),
|
|
185
|
-
# (r'(?<=^)cat', 'pac.id.cat'),
|
|
186
|
-
# (r'(?<=\.)id(?=\.)', 'identifier'),
|
|
187
|
-
# (r'(?<=\.)cat$', 'categories'),
|
|
188
|
-
# (r'(?<=\.)cat(?=\[)', 'categories'),
|
|
189
|
-
# (r'(?<=\.)seg$', 'segments'),
|
|
190
|
-
# (r'(?<=\.)seg(?=\[)', 'segments'),
|
|
191
|
-
# (r'(?<=^)isu', 'pac.isu'),
|
|
192
|
-
# (r'(?<=\.)isu', 'issuer'),
|
|
193
|
-
# (r'(?<=^)ext', 'pac.ext'),
|
|
194
|
-
# (r'(?<=\.)ext(?=$)', 'extensions'),
|
|
195
|
-
# (r'(?<=\.)ext(?=\[)', 'extensions'),
|
|
196
|
-
# ]
|
|
197
|
-
# for sub in substitutions:
|
|
198
|
-
# q_mod = re.sub(sub[0], sub[1], q_mod)
|
|
199
|
-
|
|
200
|
-
return q_mod
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def _tokenize_jsonpath_expression(self, expr: str):
|
|
204
|
-
token_pattern = re.compile(
|
|
205
|
-
r"""
|
|
206
|
-
(?P<LPAREN>\() |
|
|
207
|
-
(?P<RPAREN>\)) |
|
|
208
|
-
(?P<LOGIC>\bAND\b|\bOR\b|\bNOT\b) |
|
|
209
|
-
(?P<OPERATOR>==|!=|<=|>=|<|>) |
|
|
210
|
-
(?P<JSONPATH>
|
|
211
|
-
\$ # starts with $
|
|
212
|
-
(?:
|
|
213
|
-
[^\s\[\]()]+ # path segments, dots, etc.
|
|
214
|
-
|
|
|
215
|
-
\[ # open bracket
|
|
216
|
-
(?: # non-capturing group
|
|
217
|
-
[^\[\]]+ # anything but brackets
|
|
218
|
-
|
|
|
219
|
-
\[[^\[\]]*\] # nested brackets (1 level)
|
|
220
|
-
)*
|
|
221
|
-
\]
|
|
222
|
-
)+ # one or more bracket/segment blocks
|
|
223
|
-
) |
|
|
224
|
-
(?P<LITERAL>
|
|
225
|
-
-?[\w\.\-]+ # domain-like literals
|
|
226
|
-
)
|
|
227
|
-
""",
|
|
228
|
-
re.VERBOSE
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
tokens = []
|
|
232
|
-
pos = 0
|
|
233
|
-
while pos < len(expr):
|
|
234
|
-
match = token_pattern.match(expr, pos)
|
|
235
|
-
if match:
|
|
236
|
-
group_type = match.lastgroup
|
|
237
|
-
value = match.group().strip()
|
|
238
|
-
tokens.append((value, group_type))
|
|
239
|
-
pos = match.end()
|
|
240
|
-
elif expr[pos].isspace():
|
|
241
|
-
pos += 1 # skip whitespace
|
|
242
|
-
else:
|
|
243
|
-
raise SyntaxError(f"Unexpected character at position {pos}: {expr[pos]}")
|
|
244
|
-
|
|
245
|
-
return tokens
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def _expression_from_tokens(self, pac_id_json:str, tokens: tuple[str, str]):
|
|
249
|
-
out = []
|
|
250
|
-
for i in range(len(tokens)):
|
|
251
|
-
prev_token = tokens[i-1] if i > 0 else (None, None)
|
|
252
|
-
curr_token = tokens[i]
|
|
253
|
-
next_token = tokens[i+1] if i < len(tokens)-1 else (None, None)
|
|
254
|
-
if curr_token[1] == 'JSONPATH':
|
|
255
|
-
res = self._evaluate_jsonpath(pac_id_json, curr_token[0])
|
|
256
|
-
|
|
257
|
-
if prev_token[1] == 'OPERATOR' or next_token[1] == 'OPERATOR':
|
|
258
|
-
# if token is part of comparison return the value of the node
|
|
259
|
-
if len(res) == 0:
|
|
260
|
-
out.append('""')
|
|
261
|
-
else:
|
|
262
|
-
out.append(f'"{res[0].upper()}"')
|
|
263
|
-
else:
|
|
264
|
-
# if token is not part of comparison evaluate to boolean
|
|
265
|
-
if len(res) == 0:
|
|
266
|
-
out.append(False)
|
|
267
|
-
else:
|
|
268
|
-
out.append(True)
|
|
269
|
-
|
|
270
|
-
elif curr_token[1] == 'LOGIC':
|
|
271
|
-
out.append(curr_token[0].lower())
|
|
272
|
-
|
|
273
|
-
elif curr_token[1] == 'LITERAL':
|
|
274
|
-
t = curr_token[0]
|
|
275
|
-
if t[0] != '"':
|
|
276
|
-
t = '"' + t
|
|
277
|
-
if t[-1] != '"':
|
|
278
|
-
t = t + '"'
|
|
279
|
-
out.append(t.upper())
|
|
280
|
-
else:
|
|
281
|
-
out.append(curr_token[0])
|
|
282
|
-
|
|
283
|
-
s = ' '.join([str(e) for e in out])
|
|
284
|
-
return s
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def _eval_url_template(self, pac_id_json, url_template):
|
|
290
|
-
url = url_template
|
|
291
|
-
placeholders = re.findall(r'\{(.+?)\}', url_template)
|
|
292
|
-
for placeholder in placeholders:
|
|
293
|
-
expanded_placeholder = self._apply_convenience_substitutions(placeholder)
|
|
294
|
-
res = self._evaluate_jsonpath(pac_id_json, expanded_placeholder) or ['']
|
|
295
|
-
url = url.replace(f'{{{placeholder}}}', str(res[0]))
|
|
296
|
-
# res = self.substitute_jsonpath_expressions(expanded_placeholder, Patterns.jsonpath.value, as_bool=False)
|
|
297
|
-
# url = url.replace(f'{{{placeholder}}}', res)
|
|
298
|
-
return url
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def _evaluate_jsonpath(self, pac_id_json, jp_query):
|
|
303
|
-
if isinstance(pac_id_json, str):
|
|
304
|
-
pac_id_json = json.loads(pac_id_json)
|
|
305
|
-
jsonpath_expr = jsonpath.parse(jp_query)
|
|
306
|
-
matches = [match.value for match in jsonpath_expr.find(pac_id_json)]
|
|
307
|
-
return matches
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
1
|
+
from enum import Enum
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from typing import Self
|
|
5
|
+
from pydantic import Field, field_validator, model_validator
|
|
6
|
+
import yaml
|
|
7
|
+
import jsonpath_ng.ext as jsonpath
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
11
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
12
|
+
from labfreed.pac_id_resolver.cit_common import ( _add_msg_to_cit_entry_model,
|
|
13
|
+
_validate_service_name,
|
|
14
|
+
_validate_application_intent,
|
|
15
|
+
_validate_service_type,
|
|
16
|
+
ServiceType)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"CIT_v2",
|
|
21
|
+
"CITBlock_v2",
|
|
22
|
+
"CITEntry_v2"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CITEntry_v2(LabFREED_BaseModel):
|
|
28
|
+
service_name: str
|
|
29
|
+
application_intents:list[str]
|
|
30
|
+
service_type:ServiceType |str
|
|
31
|
+
template_url:str
|
|
32
|
+
|
|
33
|
+
@model_validator(mode='after')
|
|
34
|
+
def _validate_service_name(self):
|
|
35
|
+
# service_name
|
|
36
|
+
if not_allowed_chars := set(re.sub(r'[A-Za-z0-9\-\x20]', '', self.service_name)):
|
|
37
|
+
self._add_validation_message(
|
|
38
|
+
level=ValidationMsgLevel.ERROR,
|
|
39
|
+
source=f'Service {self.service_name}',
|
|
40
|
+
msg=f'Service name ontains invalid characters {_quote_texts(not_allowed_chars)}',
|
|
41
|
+
highlight_sub=not_allowed_chars
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if len(self.service_name) == 0 or len(self.service_name) > 255:
|
|
45
|
+
self._add_validation_message(
|
|
46
|
+
level=ValidationMsgLevel.ERROR,
|
|
47
|
+
source=f'Service {self.service_name}',
|
|
48
|
+
msg='Service name must be at least one and maximum 255 characters long'
|
|
49
|
+
)
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@model_validator(mode='after')
|
|
54
|
+
def _validate_application_intent(self):
|
|
55
|
+
for intent in self.application_intents:
|
|
56
|
+
if re.fullmatch('.*-generic$', intent):
|
|
57
|
+
self._add_validation_message(
|
|
58
|
+
level=ValidationMsgLevel.ERROR,
|
|
59
|
+
source=f'Application intent {intent}',
|
|
60
|
+
msg="Ends with '-generic'. This is not permitted, since it is reserved for future uses'",
|
|
61
|
+
highlight_sub=[intent]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not_allowed_chars := set(re.sub(r'[A-Za-z0-9\-]', '', intent)):
|
|
65
|
+
self._add_validation_message(
|
|
66
|
+
level=ValidationMsgLevel.ERROR,
|
|
67
|
+
source=f'Application intent {self.service_name}',
|
|
68
|
+
msg=f'Contains invalid characters {_quote_texts(not_allowed_chars)}',
|
|
69
|
+
highlight_sub=not_allowed_chars
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
if len(intent) == 0 or len(intent) > 255:
|
|
73
|
+
self._add_validation_message(
|
|
74
|
+
level=ValidationMsgLevel.ERROR,
|
|
75
|
+
source=f'Application intent {intent}',
|
|
76
|
+
msg='Must be at least one and maximum 255 characters long'
|
|
77
|
+
)
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
@model_validator(mode='after')
|
|
81
|
+
def _validate_service_type(self):
|
|
82
|
+
allowed_types = [ServiceType.ATTRIBUTE_SERVICE_GENERIC.value, ServiceType.USER_HANDOVER_GENERIC.value]
|
|
83
|
+
if self.service_type not in allowed_types:
|
|
84
|
+
if isinstance(self.service_type, ServiceType):
|
|
85
|
+
s= self.service_type.value
|
|
86
|
+
else:
|
|
87
|
+
s= self.service_type
|
|
88
|
+
for at in allowed_types:
|
|
89
|
+
s = s.replace(at,'')
|
|
90
|
+
self._add_validation_message(
|
|
91
|
+
level=ValidationMsgLevel.ERROR,
|
|
92
|
+
source=f'Service Type {self.service_type}',
|
|
93
|
+
msg=f'Invalid service type. Must be {_quote_texts(allowed_types)} must be at least one and maximum 255 characters long',
|
|
94
|
+
highlight_sub=s
|
|
95
|
+
)
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CITBlock_v2(LabFREED_BaseModel):
|
|
101
|
+
applicable_if: str = Field(default='True', alias='if')
|
|
102
|
+
entries: list[CITEntry_v2]
|
|
103
|
+
|
|
104
|
+
@field_validator('applicable_if', mode='before')
|
|
105
|
+
@classmethod
|
|
106
|
+
def _convert_if(cls, v):
|
|
107
|
+
return v if v is not None else 'True'
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class CIT_v2(LabFREED_BaseModel):
|
|
113
|
+
'''Coupling Information Table (CIT)'''
|
|
114
|
+
origin: str = ''
|
|
115
|
+
model_config = {
|
|
116
|
+
"extra": "allow"
|
|
117
|
+
}
|
|
118
|
+
'''@private'''
|
|
119
|
+
cit: list[CITBlock_v2] = Field(default_factory=list)
|
|
120
|
+
|
|
121
|
+
@model_validator(mode='after')
|
|
122
|
+
def _validate_origin(self):
|
|
123
|
+
if len(self.origin) == 0:
|
|
124
|
+
self._add_validation_message(level=ValidationMsgLevel.WARNING,
|
|
125
|
+
source='CIT origin',
|
|
126
|
+
msg='Origin should not be empty'
|
|
127
|
+
)
|
|
128
|
+
return self
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def from_yaml(cls, yml:str) -> Self:
|
|
133
|
+
try:
|
|
134
|
+
d = yaml.safe_load(yml)
|
|
135
|
+
except yaml.YAMLError as e:
|
|
136
|
+
# not a valid yaml
|
|
137
|
+
raise ValueError("This is not a valid yaml") from e
|
|
138
|
+
return cls.model_validate(d)
|
|
139
|
+
|
|
140
|
+
def __str__(self):
|
|
141
|
+
yml = yaml.dump(self.model_dump() )
|
|
142
|
+
return yml
|
|
143
|
+
|
|
144
|
+
def evaluate_pac_id(self, pac):
|
|
145
|
+
pac_id_json = pac.to_dict()
|
|
146
|
+
cit_evaluated = ServiceGroup(origin=self.origin)
|
|
147
|
+
for block in self.cit:
|
|
148
|
+
_, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
|
|
149
|
+
if not is_applicable:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
for e in block.entries:
|
|
153
|
+
if e.errors():
|
|
154
|
+
continue #make this stable against errors in the cit
|
|
155
|
+
url = self._eval_url_template(pac_id_json, e.template_url)
|
|
156
|
+
cit_evaluated.services.append(Service(
|
|
157
|
+
service_name=e.service_name,
|
|
158
|
+
application_intents=e.application_intents,
|
|
159
|
+
service_type=e.service_type,
|
|
160
|
+
url = url
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
return cit_evaluated
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
|
|
167
|
+
expression = self._apply_convenience_substitutions(expression)
|
|
168
|
+
|
|
169
|
+
tokens = self._tokenize_jsonpath_expression(expression)
|
|
170
|
+
expression_for_eval = self._expression_from_tokens(pac_id_json, tokens)
|
|
171
|
+
applicable = eval(expression_for_eval, {}, {})
|
|
172
|
+
|
|
173
|
+
return expression_for_eval, applicable
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _apply_convenience_substitutions(self, query):
|
|
177
|
+
''' applies a few substitutions, which enable abbreviated syntax.'''
|
|
178
|
+
|
|
179
|
+
# allow access to array elements by key
|
|
180
|
+
q_mod = re.sub(r'\[(".+?")\]', r'[?(@.key == \1)]', query )
|
|
181
|
+
|
|
182
|
+
# allow shorter path
|
|
183
|
+
# substitutions = [
|
|
184
|
+
# (r'(?<=^)id', 'pac.id'),
|
|
185
|
+
# (r'(?<=^)cat', 'pac.id.cat'),
|
|
186
|
+
# (r'(?<=\.)id(?=\.)', 'identifier'),
|
|
187
|
+
# (r'(?<=\.)cat$', 'categories'),
|
|
188
|
+
# (r'(?<=\.)cat(?=\[)', 'categories'),
|
|
189
|
+
# (r'(?<=\.)seg$', 'segments'),
|
|
190
|
+
# (r'(?<=\.)seg(?=\[)', 'segments'),
|
|
191
|
+
# (r'(?<=^)isu', 'pac.isu'),
|
|
192
|
+
# (r'(?<=\.)isu', 'issuer'),
|
|
193
|
+
# (r'(?<=^)ext', 'pac.ext'),
|
|
194
|
+
# (r'(?<=\.)ext(?=$)', 'extensions'),
|
|
195
|
+
# (r'(?<=\.)ext(?=\[)', 'extensions'),
|
|
196
|
+
# ]
|
|
197
|
+
# for sub in substitutions:
|
|
198
|
+
# q_mod = re.sub(sub[0], sub[1], q_mod)
|
|
199
|
+
|
|
200
|
+
return q_mod
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _tokenize_jsonpath_expression(self, expr: str):
|
|
204
|
+
token_pattern = re.compile(
|
|
205
|
+
r"""
|
|
206
|
+
(?P<LPAREN>\() |
|
|
207
|
+
(?P<RPAREN>\)) |
|
|
208
|
+
(?P<LOGIC>\bAND\b|\bOR\b|\bNOT\b) |
|
|
209
|
+
(?P<OPERATOR>==|!=|<=|>=|<|>) |
|
|
210
|
+
(?P<JSONPATH>
|
|
211
|
+
\$ # starts with $
|
|
212
|
+
(?:
|
|
213
|
+
[^\s\[\]()]+ # path segments, dots, etc.
|
|
214
|
+
|
|
|
215
|
+
\[ # open bracket
|
|
216
|
+
(?: # non-capturing group
|
|
217
|
+
[^\[\]]+ # anything but brackets
|
|
218
|
+
|
|
|
219
|
+
\[[^\[\]]*\] # nested brackets (1 level)
|
|
220
|
+
)*
|
|
221
|
+
\]
|
|
222
|
+
)+ # one or more bracket/segment blocks
|
|
223
|
+
) |
|
|
224
|
+
(?P<LITERAL>
|
|
225
|
+
-?[\w\.\-]+ # domain-like literals
|
|
226
|
+
)
|
|
227
|
+
""",
|
|
228
|
+
re.VERBOSE
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
tokens = []
|
|
232
|
+
pos = 0
|
|
233
|
+
while pos < len(expr):
|
|
234
|
+
match = token_pattern.match(expr, pos)
|
|
235
|
+
if match:
|
|
236
|
+
group_type = match.lastgroup
|
|
237
|
+
value = match.group().strip()
|
|
238
|
+
tokens.append((value, group_type))
|
|
239
|
+
pos = match.end()
|
|
240
|
+
elif expr[pos].isspace():
|
|
241
|
+
pos += 1 # skip whitespace
|
|
242
|
+
else:
|
|
243
|
+
raise SyntaxError(f"Unexpected character at position {pos}: {expr[pos]}")
|
|
244
|
+
|
|
245
|
+
return tokens
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _expression_from_tokens(self, pac_id_json:str, tokens: tuple[str, str]):
|
|
249
|
+
out = []
|
|
250
|
+
for i in range(len(tokens)):
|
|
251
|
+
prev_token = tokens[i-1] if i > 0 else (None, None)
|
|
252
|
+
curr_token = tokens[i]
|
|
253
|
+
next_token = tokens[i+1] if i < len(tokens)-1 else (None, None)
|
|
254
|
+
if curr_token[1] == 'JSONPATH':
|
|
255
|
+
res = self._evaluate_jsonpath(pac_id_json, curr_token[0])
|
|
256
|
+
|
|
257
|
+
if prev_token[1] == 'OPERATOR' or next_token[1] == 'OPERATOR':
|
|
258
|
+
# if token is part of comparison return the value of the node
|
|
259
|
+
if len(res) == 0:
|
|
260
|
+
out.append('""')
|
|
261
|
+
else:
|
|
262
|
+
out.append(f'"{res[0].upper()}"')
|
|
263
|
+
else:
|
|
264
|
+
# if token is not part of comparison evaluate to boolean
|
|
265
|
+
if len(res) == 0:
|
|
266
|
+
out.append(False)
|
|
267
|
+
else:
|
|
268
|
+
out.append(True)
|
|
269
|
+
|
|
270
|
+
elif curr_token[1] == 'LOGIC':
|
|
271
|
+
out.append(curr_token[0].lower())
|
|
272
|
+
|
|
273
|
+
elif curr_token[1] == 'LITERAL':
|
|
274
|
+
t = curr_token[0]
|
|
275
|
+
if t[0] != '"':
|
|
276
|
+
t = '"' + t
|
|
277
|
+
if t[-1] != '"':
|
|
278
|
+
t = t + '"'
|
|
279
|
+
out.append(t.upper())
|
|
280
|
+
else:
|
|
281
|
+
out.append(curr_token[0])
|
|
282
|
+
|
|
283
|
+
s = ' '.join([str(e) for e in out])
|
|
284
|
+
return s
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _eval_url_template(self, pac_id_json, url_template):
|
|
290
|
+
url = url_template
|
|
291
|
+
placeholders = re.findall(r'\{(.+?)\}', url_template)
|
|
292
|
+
for placeholder in placeholders:
|
|
293
|
+
expanded_placeholder = self._apply_convenience_substitutions(placeholder)
|
|
294
|
+
res = self._evaluate_jsonpath(pac_id_json, expanded_placeholder) or ['']
|
|
295
|
+
url = url.replace(f'{{{placeholder}}}', str(res[0]))
|
|
296
|
+
# res = self.substitute_jsonpath_expressions(expanded_placeholder, Patterns.jsonpath.value, as_bool=False)
|
|
297
|
+
# url = url.replace(f'{{{placeholder}}}', res)
|
|
298
|
+
return url
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _evaluate_jsonpath(self, pac_id_json, jp_query):
|
|
303
|
+
if isinstance(pac_id_json, str):
|
|
304
|
+
pac_id_json = json.loads(pac_id_json)
|
|
305
|
+
jsonpath_expr = jsonpath.parse(jp_query)
|
|
306
|
+
matches = [match.value for match in jsonpath_expr.find(pac_id_json)]
|
|
307
|
+
return matches
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
|