labfreed 0.0.20__py2.py3-none-any.whl → 0.1.1__py2.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/IO/parse_pac.py +5 -4
- labfreed/PAC_CAT/data_model.py +41 -21
- labfreed/PAC_ID/data_model.py +21 -26
- labfreed/PAC_ID_Resolver/cit.yaml +92 -0
- labfreed/PAC_ID_Resolver/data_types.py +85 -0
- labfreed/PAC_ID_Resolver/resolver.py +221 -0
- labfreed/TREX/data_model.py +14 -14
- labfreed/__init__.py +1 -1
- labfreed/validation.py +126 -38
- labfreed-0.1.1.dist-info/METADATA +279 -0
- labfreed-0.1.1.dist-info/RECORD +24 -0
- labfreed-0.0.20.dist-info/METADATA +0 -230
- labfreed-0.0.20.dist-info/RECORD +0 -21
- {labfreed-0.0.20.dist-info → labfreed-0.1.1.dist-info}/WHEEL +0 -0
- {labfreed-0.0.20.dist-info → labfreed-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import yaml
|
|
4
|
+
import json
|
|
5
|
+
import jsonpath_ng.ext as jsonpath
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from labfreed.IO.parse_pac import PAC_Parser, PACID_With_Extensions
|
|
9
|
+
from labfreed.PAC_ID_Resolver.data_types import CIT, CITEntry, CITEvaluated, Service
|
|
10
|
+
|
|
11
|
+
from labfreed.PAC_ID_Resolver.non_needed.query_tools import JSONPathTools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_cit(path):
|
|
15
|
+
with open(path, 'r') as f:
|
|
16
|
+
cit = yaml.safe_load(f)
|
|
17
|
+
cit = CIT.model_validate(cit)
|
|
18
|
+
return cit
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PAC_ID_Resolver():
|
|
22
|
+
def __init__(self, cits:list[CIT]=None):
|
|
23
|
+
if not cits:
|
|
24
|
+
cits = []
|
|
25
|
+
self.cits = cits
|
|
26
|
+
|
|
27
|
+
# load the default cit
|
|
28
|
+
dir = os.path.dirname(__file__)
|
|
29
|
+
fn ='cit.yaml'
|
|
30
|
+
p = os.path.join(dir, fn)
|
|
31
|
+
with open(p, 'r') as f:
|
|
32
|
+
cit = yaml.safe_load(f)
|
|
33
|
+
cit = CIT.model_validate(cit)
|
|
34
|
+
self.cits.append(cit)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve(self, pac_id:PACID_With_Extensions|str):
|
|
38
|
+
if isinstance(pac_id, str):
|
|
39
|
+
pac_id = PAC_Parser().parse(pac_id)
|
|
40
|
+
|
|
41
|
+
pac_id_json = pac_id.model_dump(by_alias=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# dir = os.path.dirname(__file__)
|
|
45
|
+
# p = os.path.join(dir, 'pac-id.json')
|
|
46
|
+
# with open(p , 'r') as f:
|
|
47
|
+
# _json = f.read()
|
|
48
|
+
# pac_id_json = json.loads(_json)
|
|
49
|
+
|
|
50
|
+
matches = [self._evaluate_against_cit(pac_id_json, cit) for cit in self.cits]
|
|
51
|
+
return matches
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _evaluate_against_cit(self, pac_id_json, cit:CIT):
|
|
55
|
+
cit_evaluated = CITEvaluated(origin=cit.origin)
|
|
56
|
+
for block in cit.cit:
|
|
57
|
+
_, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
|
|
58
|
+
if not is_applicable:
|
|
59
|
+
continue
|
|
60
|
+
|
|
61
|
+
for e in block.entries:
|
|
62
|
+
url = self.eval_url_template(pac_id_json, e.template_url)
|
|
63
|
+
cit_evaluated.services.append(Service(
|
|
64
|
+
service_name=e.service_name,
|
|
65
|
+
application_intents=e.application_intents,
|
|
66
|
+
service_type=e.service_type,
|
|
67
|
+
url = url
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
return cit_evaluated
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
|
|
74
|
+
expression = self._apply_convenience_substitutions(expression)
|
|
75
|
+
|
|
76
|
+
tokens = self._tokenize_jsonpath_expression(expression)
|
|
77
|
+
expression_for_eval = self._expression_from_tokens(pac_id_json, tokens)
|
|
78
|
+
applicable = eval(expression_for_eval, {}, {})
|
|
79
|
+
|
|
80
|
+
return expression_for_eval, applicable
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _apply_convenience_substitutions(self, query):
|
|
84
|
+
''' applies a few substitutions, which enable abbreviated syntax.'''
|
|
85
|
+
|
|
86
|
+
# allow access to array elements by key
|
|
87
|
+
q_mod = re.sub(r"\[('.+?')\]", r"[?(@.key == \1)]", query )
|
|
88
|
+
|
|
89
|
+
# allow shorter path
|
|
90
|
+
# substitutions = [
|
|
91
|
+
# (r'(?<=^)id', 'pac.id'),
|
|
92
|
+
# (r'(?<=^)cat', 'pac.id.cat'),
|
|
93
|
+
# (r'(?<=\.)id(?=\.)', 'identifier'),
|
|
94
|
+
# (r'(?<=\.)cat$', 'categories'),
|
|
95
|
+
# (r'(?<=\.)cat(?=\[)', 'categories'),
|
|
96
|
+
# (r'(?<=\.)seg$', 'segments'),
|
|
97
|
+
# (r'(?<=\.)seg(?=\[)', 'segments'),
|
|
98
|
+
# (r'(?<=^)isu', 'pac.isu'),
|
|
99
|
+
# (r'(?<=\.)isu', 'issuer'),
|
|
100
|
+
# (r'(?<=^)ext', 'pac.ext'),
|
|
101
|
+
# (r'(?<=\.)ext(?=$)', 'extensions'),
|
|
102
|
+
# (r'(?<=\.)ext(?=\[)', 'extensions'),
|
|
103
|
+
# ]
|
|
104
|
+
# for sub in substitutions:
|
|
105
|
+
# q_mod = re.sub(sub[0], sub[1], q_mod)
|
|
106
|
+
|
|
107
|
+
return q_mod
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tokenize_jsonpath_expression(self, expr: str):
|
|
111
|
+
token_pattern = re.compile(
|
|
112
|
+
r"""
|
|
113
|
+
(?P<LPAREN>\() |
|
|
114
|
+
(?P<RPAREN>\)) |
|
|
115
|
+
(?P<LOGIC>\bAND\b|\bOR\b|\bNOT\b) |
|
|
116
|
+
(?P<OPERATOR>==|!=|<=|>=|<|>) |
|
|
117
|
+
(?P<JSONPATH>
|
|
118
|
+
\$ # starts with $
|
|
119
|
+
(?:
|
|
120
|
+
[^\s\[\]()]+ # path segments, dots, etc.
|
|
121
|
+
|
|
|
122
|
+
\[ # open bracket
|
|
123
|
+
(?: # non-capturing group
|
|
124
|
+
[^\[\]]+ # anything but brackets
|
|
125
|
+
|
|
|
126
|
+
\[[^\[\]]*\] # nested brackets (1 level)
|
|
127
|
+
)*
|
|
128
|
+
\]
|
|
129
|
+
)+ # one or more bracket/segment blocks
|
|
130
|
+
) |
|
|
131
|
+
(?P<LITERAL>
|
|
132
|
+
[A-Za-z_][\w\.\-]*[A-Za-z0-9] # domain-like literals
|
|
133
|
+
)
|
|
134
|
+
""",
|
|
135
|
+
re.VERBOSE
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
tokens = []
|
|
139
|
+
pos = 0
|
|
140
|
+
while pos < len(expr):
|
|
141
|
+
match = token_pattern.match(expr, pos)
|
|
142
|
+
if match:
|
|
143
|
+
group_type = match.lastgroup
|
|
144
|
+
value = match.group().strip()
|
|
145
|
+
tokens.append((value, group_type))
|
|
146
|
+
pos = match.end()
|
|
147
|
+
elif expr[pos].isspace():
|
|
148
|
+
pos += 1 # skip whitespace
|
|
149
|
+
else:
|
|
150
|
+
raise SyntaxError(f"Unexpected character at position {pos}: {expr[pos]}")
|
|
151
|
+
|
|
152
|
+
return tokens
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _expression_from_tokens(self, pac_id_json:str, tokens: tuple[str, str]):
|
|
156
|
+
out = []
|
|
157
|
+
for i in range(len(tokens)):
|
|
158
|
+
prev_token = tokens[i-1] if i > 0 else (None, None)
|
|
159
|
+
curr_token = tokens[i]
|
|
160
|
+
next_token = tokens[i+1] if i < len(tokens)-1 else (None, None)
|
|
161
|
+
if curr_token[1] == 'JSONPATH':
|
|
162
|
+
res = self._evaluate_jsonpath(pac_id_json, curr_token[0])
|
|
163
|
+
|
|
164
|
+
if prev_token[1] == 'OPERATOR' or next_token[1] == 'OPERATOR':
|
|
165
|
+
# if token is part of comparison return the value of the node
|
|
166
|
+
if len(res) == 0:
|
|
167
|
+
out.append('""')
|
|
168
|
+
else:
|
|
169
|
+
out.append(f'"{res[0].upper()}"')
|
|
170
|
+
else:
|
|
171
|
+
# if token is not part of comparison evaluate to boolean
|
|
172
|
+
if len(res) == 0:
|
|
173
|
+
out.append(False)
|
|
174
|
+
else:
|
|
175
|
+
out.append(True)
|
|
176
|
+
|
|
177
|
+
elif curr_token[1] == 'LOGIC':
|
|
178
|
+
out.append(curr_token[0].lower())
|
|
179
|
+
|
|
180
|
+
elif curr_token[1] == 'LITERAL':
|
|
181
|
+
t = curr_token[0]
|
|
182
|
+
if t[0] != '"':
|
|
183
|
+
t = '"' + t
|
|
184
|
+
if t[-1] != '"':
|
|
185
|
+
t = t + '"'
|
|
186
|
+
out.append(t.upper())
|
|
187
|
+
else:
|
|
188
|
+
out.append(curr_token[0])
|
|
189
|
+
|
|
190
|
+
s = ' '.join([str(e) for e in out])
|
|
191
|
+
return s
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def eval_url_template(self, pac_id_json, url_template):
|
|
197
|
+
url = url_template
|
|
198
|
+
placeholders = re.findall(r'\{(.+?)\}', url_template)
|
|
199
|
+
for placeholder in placeholders:
|
|
200
|
+
expanded_placeholder = self._apply_convenience_substitutions(placeholder)
|
|
201
|
+
res = self._evaluate_jsonpath(pac_id_json, expanded_placeholder) or ['']
|
|
202
|
+
url = url.replace(f'{{{placeholder}}}', str(res[0]))
|
|
203
|
+
# res = self.substitute_jsonpath_expressions(expanded_placeholder, Patterns.jsonpath.value, as_bool=False)
|
|
204
|
+
# url = url.replace(f'{{{placeholder}}}', res)
|
|
205
|
+
return url
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _evaluate_jsonpath(self, pac_id_json, jp_query):
|
|
211
|
+
jsonpath_expr = jsonpath.parse(jp_query)
|
|
212
|
+
matches = [match.value for match in jsonpath_expr.find(pac_id_json)]
|
|
213
|
+
return matches
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == '__main__':
|
|
220
|
+
r = PAC_ID_Resolver()
|
|
221
|
+
r.resolve()
|
labfreed/TREX/data_model.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import Annotated, Literal
|
|
|
8
8
|
from pydantic import PrivateAttr, RootModel, ValidationError, field_validator, model_validator, Field
|
|
9
9
|
from labfreed.TREX.unece_units import unece_unit, unece_unit_codes, unece_units, unit_name, unit_symbol
|
|
10
10
|
from labfreed.utilities.utility_types import DataTable, Quantity, Unit, unece_unit_code_from_quantity
|
|
11
|
-
from labfreed.validation import BaseModelWithValidationMessages
|
|
11
|
+
from labfreed.validation import BaseModelWithValidationMessages, ValidationMsgLevel
|
|
12
12
|
from abc import ABC, abstractmethod
|
|
13
13
|
|
|
14
14
|
from labfreed.PAC_ID.extensions import Extension
|
|
@@ -180,7 +180,7 @@ class BoolValue(ValueMixin):
|
|
|
180
180
|
if not self.value in ['T', 'F']:
|
|
181
181
|
self.add_validation_message(
|
|
182
182
|
source=f"TREX boolean value {self.value}",
|
|
183
|
-
|
|
183
|
+
level= ValidationMsgLevel.ERROR,
|
|
184
184
|
msg=f'{self.value} is no valid boolean. Must be T or F',
|
|
185
185
|
highlight_pattern = f'{self.value}',
|
|
186
186
|
highlight_sub=[c for c in self.value]
|
|
@@ -207,7 +207,7 @@ class AlphanumericValue(ValueMixin):
|
|
|
207
207
|
if re.match(r'[a-z]', self.value):
|
|
208
208
|
self.add_validation_message(
|
|
209
209
|
source=f"TREX value {self.value}",
|
|
210
|
-
|
|
210
|
+
level= ValidationMsgLevel.ERROR,
|
|
211
211
|
msg=f"Lower case characters are not allowed.",
|
|
212
212
|
highlight_pattern = self.value
|
|
213
213
|
)
|
|
@@ -215,7 +215,7 @@ class AlphanumericValue(ValueMixin):
|
|
|
215
215
|
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
|
|
216
216
|
self.add_validation_message(
|
|
217
217
|
source=f"TREX value {self.value}",
|
|
218
|
-
|
|
218
|
+
level= ValidationMsgLevel.ERROR,
|
|
219
219
|
msg=f"Characters {','.join(not_allowed_chars)} are not allowed in alphanumeric segment",
|
|
220
220
|
highlight_pattern = self.value,
|
|
221
221
|
highlight_sub=not_allowed_chars
|
|
@@ -242,7 +242,7 @@ class TextValue(ValueMixin):
|
|
|
242
242
|
if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
|
|
243
243
|
self.add_validation_message(
|
|
244
244
|
source=f"TREX value {self.value}",
|
|
245
|
-
|
|
245
|
+
level= ValidationMsgLevel.ERROR,
|
|
246
246
|
msg=f"Characters {','.join(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
|
|
247
247
|
highlight_pattern = self.value,
|
|
248
248
|
highlight_sub=not_allowed_chars
|
|
@@ -268,7 +268,7 @@ class BinaryValue(ValueMixin):
|
|
|
268
268
|
if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
|
|
269
269
|
self.add_validation_message(
|
|
270
270
|
source=f"TREX value {self.value}",
|
|
271
|
-
|
|
271
|
+
tlevel= ValidationMsgLevel.ERROR,
|
|
272
272
|
msg=f"Characters {','.join(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
|
|
273
273
|
highlight_pattern = self.value,
|
|
274
274
|
highlight_sub=not_allowed_chars
|
|
@@ -286,7 +286,7 @@ class ErrorValue(ValueMixin):
|
|
|
286
286
|
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
|
|
287
287
|
self.add_validation_message(
|
|
288
288
|
source=f"TREX value {self.value}",
|
|
289
|
-
|
|
289
|
+
level= ValidationMsgLevel.ERROR,
|
|
290
290
|
msg=f"Characters {','.join(not_allowed_chars)} are not allowed in error segment",
|
|
291
291
|
highlight_pattern = self.value,
|
|
292
292
|
highlight_sub=not_allowed_chars
|
|
@@ -309,7 +309,7 @@ class ValueSegment(TREX_Segment, ValueMixin, ABC):
|
|
|
309
309
|
if not self.type in valid_types:
|
|
310
310
|
self.add_validation_message(
|
|
311
311
|
source=f"TREX value segment {self.key}",
|
|
312
|
-
|
|
312
|
+
level= ValidationMsgLevel.ERROR,
|
|
313
313
|
msg=f"Type {self.type} is invalid. Must be 'T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E' or a UNECE unit",
|
|
314
314
|
highlight_pattern = self.type
|
|
315
315
|
)
|
|
@@ -385,7 +385,7 @@ class ColumnHeader(BaseModelWithValidationMessages):
|
|
|
385
385
|
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.key)):
|
|
386
386
|
self.add_validation_message(
|
|
387
387
|
source=f"TREX table column {self.key}",
|
|
388
|
-
|
|
388
|
+
level= ValidationMsgLevel.ERROR,
|
|
389
389
|
msg=f"Column header key contains invalid characters: {','.join(not_allowed_chars)}",
|
|
390
390
|
highlight_pattern = f'{self.key}$',
|
|
391
391
|
highlight_sub=not_allowed_chars
|
|
@@ -398,7 +398,7 @@ class ColumnHeader(BaseModelWithValidationMessages):
|
|
|
398
398
|
if not self.type in valid_types:
|
|
399
399
|
self.add_validation_message(
|
|
400
400
|
source=f"TREX table column {self.key}",
|
|
401
|
-
|
|
401
|
+
level= ValidationMsgLevel.ERROR,
|
|
402
402
|
msg=f"Type '{self.type}' is invalid. Must be 'T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E' or a UNECE unit",
|
|
403
403
|
highlight_pattern = self.type
|
|
404
404
|
)
|
|
@@ -435,7 +435,7 @@ class TREX_Table(TREX_Segment):
|
|
|
435
435
|
if len(self.column_headers) != most_common_len:
|
|
436
436
|
self.add_validation_message(
|
|
437
437
|
source=f"Table {self.key}",
|
|
438
|
-
|
|
438
|
+
level= ValidationMsgLevel.ERROR,
|
|
439
439
|
msg=f"Size mismatch: Table header contains {self.column_names} keys, while most rows have {most_common_len}",
|
|
440
440
|
highlight_pattern = self.key
|
|
441
441
|
)
|
|
@@ -448,7 +448,7 @@ class TREX_Table(TREX_Segment):
|
|
|
448
448
|
if len(row) != expected_row_len:
|
|
449
449
|
self.add_validation_message(
|
|
450
450
|
source=f"Table {self.key}",
|
|
451
|
-
|
|
451
|
+
level= ValidationMsgLevel.ERROR,
|
|
452
452
|
msg=f"Size mismatch: Table row {i} contains {len(row)} elements. Expected size is {expected_row_len}",
|
|
453
453
|
highlight_pattern = row.serialize_for_trex()
|
|
454
454
|
)
|
|
@@ -480,7 +480,7 @@ class TREX_Table(TREX_Segment):
|
|
|
480
480
|
except AssertionError:
|
|
481
481
|
self.add_validation_message(
|
|
482
482
|
source=f"Table {self.key}",
|
|
483
|
-
|
|
483
|
+
level= ValidationMsgLevel.ERROR,
|
|
484
484
|
msg=f"Type mismatch: Table row {i}, column {nm} is of wrong type. According to the header it should be {t_expected}",
|
|
485
485
|
highlight_pattern = row.serialize_for_trex(),
|
|
486
486
|
highlight_sub=[c for c in e.value]
|
|
@@ -490,7 +490,7 @@ class TREX_Table(TREX_Segment):
|
|
|
490
490
|
for m in msg:
|
|
491
491
|
self.add_validation_message(
|
|
492
492
|
source=f"Table {self.key}",
|
|
493
|
-
|
|
493
|
+
level= ValidationMsgLevel.ERROR,
|
|
494
494
|
msg=m.problem_msg,
|
|
495
495
|
highlight_pattern = row.serialize_for_trex(),
|
|
496
496
|
highlight_sub=[c for c in e.value]
|
labfreed/__init__.py
CHANGED
labfreed/validation.py
CHANGED
|
@@ -1,37 +1,52 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
import re
|
|
1
3
|
from pydantic import BaseModel, Field, PrivateAttr
|
|
2
4
|
from typing import List, Set, Tuple
|
|
3
5
|
|
|
4
6
|
from rich import print
|
|
5
7
|
from rich.text import Text
|
|
8
|
+
from rich.table import Table
|
|
6
9
|
|
|
7
10
|
|
|
8
11
|
domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
|
|
9
12
|
hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
|
|
10
13
|
|
|
11
14
|
|
|
15
|
+
class ValidationMsgLevel(Enum):
|
|
16
|
+
ERROR = auto()
|
|
17
|
+
ERROR_AUTO_FIX = auto()
|
|
18
|
+
WARNING = auto()
|
|
19
|
+
RECOMMENDATION = auto()
|
|
20
|
+
INFO = auto()
|
|
21
|
+
|
|
12
22
|
class ValidationMessage(BaseModel):
|
|
23
|
+
source_id:int
|
|
13
24
|
source:str
|
|
14
|
-
|
|
25
|
+
level: ValidationMsgLevel
|
|
15
26
|
problem_msg:str
|
|
16
27
|
recommendation_msg: str = ""
|
|
17
28
|
highlight:str = "" #this can be used to highlight problematic parts
|
|
18
|
-
|
|
29
|
+
highlight_sub_patterns:list[str] = Field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
19
33
|
|
|
20
|
-
@property
|
|
21
|
-
def emphazised_highlight(self):
|
|
22
|
-
fmt = lambda s: f'[emph]{s}[/emph]'
|
|
23
34
|
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
# @property
|
|
36
|
+
# def emphazised_highlight(self):
|
|
37
|
+
# fmt = lambda s: f'[emph]{s}[/emph]'
|
|
38
|
+
|
|
39
|
+
# if not self.highlight_sub_patterns:
|
|
40
|
+
# return fmt(self.highlight)
|
|
26
41
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
# result = []
|
|
43
|
+
# for c in self.highlight:
|
|
44
|
+
# if c in self.highlight_sub_patterns:
|
|
45
|
+
# result.append(fmt(c))
|
|
46
|
+
# else:
|
|
47
|
+
# result.append(c)
|
|
33
48
|
|
|
34
|
-
|
|
49
|
+
# return ''.join(result)
|
|
35
50
|
|
|
36
51
|
|
|
37
52
|
class LabFREEDValidationError(ValueError):
|
|
@@ -51,10 +66,10 @@ class BaseModelWithValidationMessages(BaseModel):
|
|
|
51
66
|
The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
|
|
52
67
|
_validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
|
|
53
68
|
|
|
54
|
-
def add_validation_message(self, *, msg: str,
|
|
69
|
+
def add_validation_message(self, *, msg: str, level:ValidationMsgLevel, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
|
|
55
70
|
if not highlight_sub:
|
|
56
71
|
highlight_sub = []
|
|
57
|
-
w = ValidationMessage(problem_msg=msg, recommendation_msg=recommendation, source=source,
|
|
72
|
+
w = ValidationMessage(problem_msg=msg, recommendation_msg=recommendation, source=source, level=level, highlight=highlight_pattern, highlight_sub_patterns=highlight_sub, source_id=id(self))
|
|
58
73
|
|
|
59
74
|
if not w in self._validation_messages:
|
|
60
75
|
self._validation_messages.append(w)
|
|
@@ -113,43 +128,116 @@ class BaseModelWithValidationMessages(BaseModel):
|
|
|
113
128
|
return filter_warnings(self.get_nested_validation_messages())
|
|
114
129
|
|
|
115
130
|
|
|
116
|
-
def
|
|
117
|
-
if
|
|
118
|
-
|
|
131
|
+
def str_for_validation_msg(self, validation_msg:ValidationMessage):
|
|
132
|
+
if validation_msg.source_id == id(self):
|
|
133
|
+
return validation_msg.source_id
|
|
134
|
+
#return validation_msg.emphasize_in(self(str))
|
|
135
|
+
else:
|
|
136
|
+
return str(self)
|
|
137
|
+
|
|
138
|
+
def str_highlighted(self):
|
|
139
|
+
raise NotImplementedError("Subclasses must implement format_special()")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _emphasize_in(self, validation_msg, validation_node_str:str, fmt, color='black'):
|
|
144
|
+
if validation_msg.highlight_sub_patterns:
|
|
145
|
+
replacements = validation_msg.highlight_sub_patterns
|
|
146
|
+
else:
|
|
147
|
+
replacements = [validation_msg.highlight]
|
|
148
|
+
# Sort patterns by length descending to avoid subpattern clobbering
|
|
149
|
+
sorted_patterns = sorted(replacements, key=len, reverse=True)
|
|
150
|
+
# Escape the patterns for regex safety
|
|
151
|
+
escaped_patterns = [re.escape(p) for p in sorted_patterns]
|
|
152
|
+
# Create one regex pattern with alternation (longest first)
|
|
153
|
+
pattern = re.compile("|".join(escaped_patterns))
|
|
154
|
+
|
|
155
|
+
out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def print_validation_messages(self, target='console'):
|
|
119
160
|
msgs = self.get_nested_validation_messages()
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
161
|
+
|
|
162
|
+
table = Table(title=f"Validation Results", show_header=False)
|
|
163
|
+
|
|
164
|
+
col = lambda s: table.add_column(s, vertical='top')
|
|
165
|
+
col("-")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if not msgs:
|
|
169
|
+
table.add_row('All clear!', end_section=True)
|
|
170
|
+
return
|
|
127
171
|
|
|
128
172
|
for m in msgs:
|
|
129
|
-
if m.
|
|
173
|
+
if m.level == ValidationMsgLevel.ERROR:
|
|
130
174
|
color = 'red'
|
|
131
175
|
else:
|
|
132
176
|
color = 'yellow'
|
|
133
177
|
|
|
134
|
-
text = Text.from_markup(f'\n [bold {color}]{m.type} [/bold {color}] in \t {m.source}' )
|
|
135
|
-
print(text)
|
|
136
178
|
match target:
|
|
137
179
|
case 'markdown':
|
|
138
|
-
|
|
180
|
+
fmt = lambda s: f'🔸{s}🔸'
|
|
139
181
|
case 'console':
|
|
140
|
-
|
|
182
|
+
fmt = lambda s: f'[{color} bold]{s}[/{color} bold]'
|
|
141
183
|
case 'html':
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
184
|
+
fmt = lambda s: f'<span class="val_{color}">{s}</span>'
|
|
185
|
+
case 'html_styled':
|
|
186
|
+
fmt = lambda s: f'<b style="color:{color}>{s}</b>'
|
|
187
|
+
|
|
188
|
+
serialized = str(self)
|
|
189
|
+
emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt, color=color)
|
|
190
|
+
|
|
191
|
+
txt = f'[bold {color}]{m.level.name} [/bold {color}]'
|
|
192
|
+
txt += '\n' + f'{m.problem_msg}'
|
|
193
|
+
txt += '\n' + emphazised_highlight
|
|
194
|
+
|
|
195
|
+
table.add_row( txt)
|
|
196
|
+
table.add_section()
|
|
197
|
+
|
|
198
|
+
print(table)
|
|
199
|
+
|
|
200
|
+
# def print_validation_messages_(self, str_to_highlight_in=None, target='console'):
|
|
201
|
+
# if not str_to_highlight_in:
|
|
202
|
+
# str_to_highlight_in = str(self)
|
|
203
|
+
# msgs = self.get_nested_validation_messages()
|
|
204
|
+
# print('\n'.join(['\n',
|
|
205
|
+
# '=======================================',
|
|
206
|
+
# 'Validation Results',
|
|
207
|
+
# '---------------------------------------'
|
|
208
|
+
# ]
|
|
209
|
+
# )
|
|
210
|
+
# )
|
|
211
|
+
|
|
212
|
+
# if not msgs:
|
|
213
|
+
# print('All clear!')
|
|
214
|
+
# return
|
|
215
|
+
|
|
216
|
+
# for m in msgs:
|
|
217
|
+
# if m.level.casefold() == "error":
|
|
218
|
+
# color = 'red'
|
|
219
|
+
# else:
|
|
220
|
+
# color = 'yellow'
|
|
221
|
+
|
|
222
|
+
# text = Text.from_markup(f'\n [bold {color}]{m.level} [/bold {color}] in \t {m.source}' )
|
|
223
|
+
# print(text)
|
|
224
|
+
# match target:
|
|
225
|
+
# case 'markdown':
|
|
226
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'🔸').replace('[/', '').replace('[', '').replace(']', '')
|
|
227
|
+
# case 'console':
|
|
228
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
|
|
229
|
+
# case 'html':
|
|
230
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'b').replace('[', '<').replace(']', '>')
|
|
231
|
+
# fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
|
|
232
|
+
# fmtd = Text.from_markup(fmtd)
|
|
233
|
+
# print(fmtd)
|
|
234
|
+
# print(Text.from_markup(f'{m.problem_msg}'))
|
|
147
235
|
|
|
148
236
|
|
|
149
237
|
|
|
150
238
|
def filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
151
|
-
return [ m for m in val_msg if m.
|
|
239
|
+
return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
|
|
152
240
|
|
|
153
241
|
def filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
154
|
-
return [ m for m in val_msg if m.
|
|
242
|
+
return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
|
|
155
243
|
|