labfreed 0.2.5a21__py3-none-any.whl → 0.2.6__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.

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