labfreed 0.2.7__py3-none-any.whl → 0.2.8__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 +84 -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 -79
- 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.7.dist-info → labfreed-0.2.8.dist-info}/METADATA +17 -9
- labfreed-0.2.8.dist-info/RECORD +45 -0
- {labfreed-0.2.7.dist-info → labfreed-0.2.8.dist-info}/licenses/LICENSE +21 -21
- labfreed-0.2.7.dist-info/RECORD +0 -45
- {labfreed-0.2.7.dist-info → labfreed-0.2.8.dist-info}/WHEEL +0 -0
labfreed/__init__.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
'''
|
|
2
|
-
Python implementation of LabFREED building blocks
|
|
3
|
-
'''
|
|
4
|
-
|
|
5
|
-
__version__ = "0.2.
|
|
6
|
-
|
|
7
|
-
from labfreed.pac_id import * # noqa: F403
|
|
8
|
-
from labfreed.pac_cat import * # noqa: F403
|
|
9
|
-
from labfreed.pac_id_resolver import * # noqa: F403
|
|
10
|
-
from labfreed.trex import * # noqa: F403
|
|
11
|
-
from labfreed.labfreed_infrastructure import * # noqa: F403
|
|
1
|
+
'''
|
|
2
|
+
Python implementation of LabFREED building blocks
|
|
3
|
+
'''
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.8"
|
|
6
|
+
|
|
7
|
+
from labfreed.pac_id import * # noqa: F403
|
|
8
|
+
from labfreed.pac_cat import * # noqa: F403
|
|
9
|
+
from labfreed.pac_id_resolver import * # noqa: F403
|
|
10
|
+
from labfreed.trex import * # noqa: F403
|
|
11
|
+
from labfreed.labfreed_infrastructure import * # noqa: F403
|
|
@@ -1,258 +1,258 @@
|
|
|
1
|
-
from enum import Enum, auto
|
|
2
|
-
import logging
|
|
3
|
-
import re
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
5
|
-
from typing import Any, List, Set
|
|
6
|
-
|
|
7
|
-
from rich import print
|
|
8
|
-
from rich.table import Table
|
|
9
|
-
|
|
10
|
-
''' Configure pdoc'''
|
|
11
|
-
__all__ = ["LabFREED_BaseModel", "ValidationMessage", "ValidationMsgLevel", "LabFREED_ValidationError"]
|
|
12
|
-
|
|
13
|
-
class PDOC_Workaround_Base(BaseModel):
|
|
14
|
-
'''@private
|
|
15
|
-
This class only exists to make pdoc work better with Pydantic models. It is set up such, that some things are not showing up in docu
|
|
16
|
-
'''
|
|
17
|
-
model_config = ConfigDict(extra="forbid")
|
|
18
|
-
"""@private"""
|
|
19
|
-
def model_post_init(self, context: Any) -> None:
|
|
20
|
-
'''@private'''
|
|
21
|
-
super().model_post_init(context)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class ValidationMsgLevel(Enum):
|
|
28
|
-
'''
|
|
29
|
-
Level of validation messages
|
|
30
|
-
'''
|
|
31
|
-
ERROR = auto()
|
|
32
|
-
'''Model is **invalid**'''
|
|
33
|
-
WARNING = auto()
|
|
34
|
-
'''Model is **valid**, but has issues'''
|
|
35
|
-
RECOMMENDATION = auto()
|
|
36
|
-
'''Model is **valid**, but recommendations apply'''
|
|
37
|
-
INFO = auto()
|
|
38
|
-
'''Model is **valid**. Something of interest was detected, which is not a recommendation.'''
|
|
39
|
-
|
|
40
|
-
class ValidationMessage(PDOC_Workaround_Base):
|
|
41
|
-
'''
|
|
42
|
-
Represents one problem in the model
|
|
43
|
-
'''
|
|
44
|
-
source_id:int
|
|
45
|
-
source:str
|
|
46
|
-
level: ValidationMsgLevel
|
|
47
|
-
msg:str
|
|
48
|
-
highlight:str = "" #this can be used to highlight problematic parts
|
|
49
|
-
highlight_sub_patterns:list[str] = Field(default_factory=list)
|
|
50
|
-
|
|
51
|
-
@field_validator('highlight_sub_patterns', mode='before')
|
|
52
|
-
@classmethod
|
|
53
|
-
def _ensure_list(cls, v):
|
|
54
|
-
if isinstance(v, str):
|
|
55
|
-
return [v]
|
|
56
|
-
return v
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class LabFREED_ValidationError(ValueError):
|
|
61
|
-
'''Error which is raised, when LabFREED validation fails, i.e. when the model contains at least one error.'''
|
|
62
|
-
|
|
63
|
-
def __init__(self, message=None, validation_msgs=None):
|
|
64
|
-
'''@private'''
|
|
65
|
-
super().__init__(message)
|
|
66
|
-
self._validation_msgs = validation_msgs
|
|
67
|
-
|
|
68
|
-
@property
|
|
69
|
-
def validation_msgs(self):
|
|
70
|
-
''' The validation messages (errors, recommendations, info) present in the invalid model'''
|
|
71
|
-
return self._validation_msgs
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
class LabFREED_BaseModel(PDOC_Workaround_Base):
|
|
77
|
-
""" Extension of Pydantic BaseModel, so that validator can issue warnings.
|
|
78
|
-
The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
|
|
79
|
-
|
|
80
|
-
_validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
|
|
81
|
-
"""Validation messages for this model"""
|
|
82
|
-
|
|
83
|
-
@property
|
|
84
|
-
def is_valid(self) -> bool:
|
|
85
|
-
return len(self.errors()) == 0
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def validation_messages(self, nested=True) -> list[ValidationMessage]:
|
|
89
|
-
if nested:
|
|
90
|
-
msgs = self._get_nested_validation_messages()
|
|
91
|
-
else:
|
|
92
|
-
msgs = self._validation_messages
|
|
93
|
-
return msgs
|
|
94
|
-
|
|
95
|
-
def errors(self, nested=True) -> list[ValidationMessage]:
|
|
96
|
-
return _filter_errors(self.validation_messages(nested=nested))
|
|
97
|
-
|
|
98
|
-
def warnings(self, nested=True) -> list[ValidationMessage]:
|
|
99
|
-
return _filter_warnings(self.validation_messages(nested=nested))
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _add_validation_message(self, *, msg: str, level:ValidationMsgLevel, source:str="", highlight_pattern="", highlight_sub=None):
|
|
103
|
-
if not highlight_sub:
|
|
104
|
-
highlight_sub = []
|
|
105
|
-
w = ValidationMessage(msg=msg, source=source, level=level, highlight=highlight_pattern, highlight_sub_patterns=highlight_sub, source_id=id(self))
|
|
106
|
-
|
|
107
|
-
if w not in self._validation_messages:
|
|
108
|
-
self._validation_messages.append(w)
|
|
109
|
-
|
|
110
|
-
# Function to extract warnings from a model and its nested models
|
|
111
|
-
def _get_nested_validation_messages(self, parent_name: str = "", visited: Set[int] = None) -> List['ValidationMessage']:
|
|
112
|
-
"""
|
|
113
|
-
Recursively extract warnings from a Pydantic model and its nested fields, including computed fields.
|
|
114
|
-
|
|
115
|
-
:param parent_name: The name of the parent model to track the path.
|
|
116
|
-
:param visited: Set of visited object IDs to prevent infinite loops.
|
|
117
|
-
:return: List of ValidationMessages from this and nested models.
|
|
118
|
-
"""
|
|
119
|
-
if visited is None:
|
|
120
|
-
visited = set()
|
|
121
|
-
|
|
122
|
-
model_id = id(self)
|
|
123
|
-
if model_id in visited:
|
|
124
|
-
return []
|
|
125
|
-
visited.add(model_id)
|
|
126
|
-
|
|
127
|
-
warnings_list = [warning for warning in self.validation_messages(nested=False)]
|
|
128
|
-
|
|
129
|
-
# Traverse regular fields
|
|
130
|
-
for field_name, field in self.__fields__.items():
|
|
131
|
-
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
132
|
-
value = getattr(self, field_name)
|
|
133
|
-
|
|
134
|
-
if isinstance(value, LabFREED_BaseModel):
|
|
135
|
-
warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
|
|
136
|
-
elif isinstance(value, list):
|
|
137
|
-
for index, item in enumerate(value):
|
|
138
|
-
if isinstance(item, LabFREED_BaseModel):
|
|
139
|
-
list_path = f"{full_path}[{index}]"
|
|
140
|
-
warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
|
|
141
|
-
|
|
142
|
-
# Traverse computed fields
|
|
143
|
-
computed_fields = getattr(self, '__pydantic_decorators__', {}).computed_fields or {}
|
|
144
|
-
for field_name in computed_fields:
|
|
145
|
-
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
146
|
-
try:
|
|
147
|
-
value = getattr(self, field_name)
|
|
148
|
-
except Exception:
|
|
149
|
-
continue # Safely skip computed properties that raise errors
|
|
150
|
-
|
|
151
|
-
if isinstance(value, LabFREED_BaseModel):
|
|
152
|
-
warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
|
|
153
|
-
elif isinstance(value, list):
|
|
154
|
-
for index, item in enumerate(value):
|
|
155
|
-
if isinstance(item, LabFREED_BaseModel):
|
|
156
|
-
list_path = f"{full_path}[{index}]"
|
|
157
|
-
warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
|
|
158
|
-
|
|
159
|
-
return warnings_list
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def print_validation_messages(self, target='console'):
|
|
164
|
-
msgs = self.format_validation_messages(target=target)
|
|
165
|
-
|
|
166
|
-
table = Table(title="Validation Results", show_header=False, title_justify='left')
|
|
167
|
-
|
|
168
|
-
def col(s):
|
|
169
|
-
return table.add_column(s, vertical='top')
|
|
170
|
-
col("-")
|
|
171
|
-
|
|
172
|
-
if not msgs:
|
|
173
|
-
table.add_row('All clear!', end_section=True)
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
for m in msgs:
|
|
177
|
-
table.add_row(m)
|
|
178
|
-
table.add_section()
|
|
179
|
-
|
|
180
|
-
logging.info(table)
|
|
181
|
-
print(table)
|
|
182
|
-
return table
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def format_validation_messages(self, target='console') -> list[str]:
|
|
186
|
-
"""Format validation messages
|
|
187
|
-
|
|
188
|
-
Args:
|
|
189
|
-
target (str, optional): Target format: 'markdown', 'console', 'html', 'html_styled'.
|
|
190
|
-
|
|
191
|
-
Returns:
|
|
192
|
-
list[str]: formated messages
|
|
193
|
-
"""
|
|
194
|
-
formatted_msg = list()
|
|
195
|
-
for m in self.validation_messages():
|
|
196
|
-
if m.level == ValidationMsgLevel.ERROR:
|
|
197
|
-
color = '#d70000'
|
|
198
|
-
else:
|
|
199
|
-
color = '#d78700'
|
|
200
|
-
|
|
201
|
-
match target:
|
|
202
|
-
case 'markdown':
|
|
203
|
-
fmt = lambda s: f'👉{s}👈' # noqa: E731
|
|
204
|
-
fmt_title = lambda s: f'**{s}**' # noqa: E731
|
|
205
|
-
br = '\n'
|
|
206
|
-
case 'console':
|
|
207
|
-
fmt = lambda s: f'[{color} bold]{s}[/{color} bold]' # noqa: E731
|
|
208
|
-
fmt_title = fmt
|
|
209
|
-
br = '\n'
|
|
210
|
-
case 'html':
|
|
211
|
-
lvl = m.level.name.lower()
|
|
212
|
-
fmt = lambda s: f'<span class="validation-problem {lvl}">{s}</span>' # noqa: E731
|
|
213
|
-
fmt_title = lambda s: f'<span class="validation-title {lvl}">{s}</span>' # noqa: E731
|
|
214
|
-
br = '<br>'
|
|
215
|
-
case 'html_styled':
|
|
216
|
-
fmt = lambda s: f'<b style="color:{color}">{s}</b>' # noqa: E731
|
|
217
|
-
fmt_title = fmt
|
|
218
|
-
br = '<br>'
|
|
219
|
-
|
|
220
|
-
serialized = str(self)
|
|
221
|
-
serialized = serialized.replace('\n', br)
|
|
222
|
-
emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt)
|
|
223
|
-
emphazised_highlight = emphazised_highlight.replace('👈👉','') # removes two consecutive markers, to make it cleaner
|
|
224
|
-
|
|
225
|
-
txt = f'{fmt_title(m.level.name)} in {m.source}'
|
|
226
|
-
txt += br + f'{m.msg}'
|
|
227
|
-
txt += br+br + emphazised_highlight
|
|
228
|
-
|
|
229
|
-
formatted_msg.append(txt)
|
|
230
|
-
return formatted_msg
|
|
231
|
-
|
|
232
|
-
def _emphasize_in(self, validation_msg, validation_node_str:str, fmt):
|
|
233
|
-
if validation_msg.highlight_sub_patterns:
|
|
234
|
-
replacements = validation_msg.highlight_sub_patterns
|
|
235
|
-
else:
|
|
236
|
-
replacements = [validation_msg.highlight]
|
|
237
|
-
# Sort patterns by length descending to avoid subpattern clobbering
|
|
238
|
-
sorted_patterns = sorted(replacements, key=len, reverse=True)
|
|
239
|
-
# Escape the patterns for regex safety
|
|
240
|
-
escaped_patterns = [re.escape(p) for p in sorted_patterns]
|
|
241
|
-
# Create one regex pattern with alternation (longest first)
|
|
242
|
-
pattern = re.compile("|".join(escaped_patterns))
|
|
243
|
-
|
|
244
|
-
out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
|
|
245
|
-
return out
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def _filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
250
|
-
return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
|
|
251
|
-
|
|
252
|
-
def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
253
|
-
return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
|
|
254
|
-
|
|
255
|
-
def _quote_texts(texts:list[str]):
|
|
256
|
-
return ','.join([f"'{t}'" for t in texts])
|
|
257
|
-
|
|
258
|
-
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
|
|
5
|
+
from typing import Any, List, Set
|
|
6
|
+
|
|
7
|
+
from rich import print
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
''' Configure pdoc'''
|
|
11
|
+
__all__ = ["LabFREED_BaseModel", "ValidationMessage", "ValidationMsgLevel", "LabFREED_ValidationError"]
|
|
12
|
+
|
|
13
|
+
class PDOC_Workaround_Base(BaseModel):
|
|
14
|
+
'''@private
|
|
15
|
+
This class only exists to make pdoc work better with Pydantic models. It is set up such, that some things are not showing up in docu
|
|
16
|
+
'''
|
|
17
|
+
model_config = ConfigDict(extra="forbid")
|
|
18
|
+
"""@private"""
|
|
19
|
+
def model_post_init(self, context: Any) -> None:
|
|
20
|
+
'''@private'''
|
|
21
|
+
super().model_post_init(context)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ValidationMsgLevel(Enum):
|
|
28
|
+
'''
|
|
29
|
+
Level of validation messages
|
|
30
|
+
'''
|
|
31
|
+
ERROR = auto()
|
|
32
|
+
'''Model is **invalid**'''
|
|
33
|
+
WARNING = auto()
|
|
34
|
+
'''Model is **valid**, but has issues'''
|
|
35
|
+
RECOMMENDATION = auto()
|
|
36
|
+
'''Model is **valid**, but recommendations apply'''
|
|
37
|
+
INFO = auto()
|
|
38
|
+
'''Model is **valid**. Something of interest was detected, which is not a recommendation.'''
|
|
39
|
+
|
|
40
|
+
class ValidationMessage(PDOC_Workaround_Base):
|
|
41
|
+
'''
|
|
42
|
+
Represents one problem in the model
|
|
43
|
+
'''
|
|
44
|
+
source_id:int
|
|
45
|
+
source:str
|
|
46
|
+
level: ValidationMsgLevel
|
|
47
|
+
msg:str
|
|
48
|
+
highlight:str = "" #this can be used to highlight problematic parts
|
|
49
|
+
highlight_sub_patterns:list[str] = Field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
@field_validator('highlight_sub_patterns', mode='before')
|
|
52
|
+
@classmethod
|
|
53
|
+
def _ensure_list(cls, v):
|
|
54
|
+
if isinstance(v, str):
|
|
55
|
+
return [v]
|
|
56
|
+
return v
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class LabFREED_ValidationError(ValueError):
|
|
61
|
+
'''Error which is raised, when LabFREED validation fails, i.e. when the model contains at least one error.'''
|
|
62
|
+
|
|
63
|
+
def __init__(self, message=None, validation_msgs=None):
|
|
64
|
+
'''@private'''
|
|
65
|
+
super().__init__(message)
|
|
66
|
+
self._validation_msgs = validation_msgs
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def validation_msgs(self):
|
|
70
|
+
''' The validation messages (errors, recommendations, info) present in the invalid model'''
|
|
71
|
+
return self._validation_msgs
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class LabFREED_BaseModel(PDOC_Workaround_Base):
|
|
77
|
+
""" Extension of Pydantic BaseModel, so that validator can issue warnings.
|
|
78
|
+
The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
|
|
79
|
+
|
|
80
|
+
_validation_messages: list[ValidationMessage] = PrivateAttr(default_factory=list)
|
|
81
|
+
"""Validation messages for this model"""
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def is_valid(self) -> bool:
|
|
85
|
+
return len(self.errors()) == 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def validation_messages(self, nested=True) -> list[ValidationMessage]:
|
|
89
|
+
if nested:
|
|
90
|
+
msgs = self._get_nested_validation_messages()
|
|
91
|
+
else:
|
|
92
|
+
msgs = self._validation_messages
|
|
93
|
+
return msgs
|
|
94
|
+
|
|
95
|
+
def errors(self, nested=True) -> list[ValidationMessage]:
|
|
96
|
+
return _filter_errors(self.validation_messages(nested=nested))
|
|
97
|
+
|
|
98
|
+
def warnings(self, nested=True) -> list[ValidationMessage]:
|
|
99
|
+
return _filter_warnings(self.validation_messages(nested=nested))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _add_validation_message(self, *, msg: str, level:ValidationMsgLevel, source:str="", highlight_pattern="", highlight_sub=None):
|
|
103
|
+
if not highlight_sub:
|
|
104
|
+
highlight_sub = []
|
|
105
|
+
w = ValidationMessage(msg=msg, source=source, level=level, highlight=highlight_pattern, highlight_sub_patterns=highlight_sub, source_id=id(self))
|
|
106
|
+
|
|
107
|
+
if w not in self._validation_messages:
|
|
108
|
+
self._validation_messages.append(w)
|
|
109
|
+
|
|
110
|
+
# Function to extract warnings from a model and its nested models
|
|
111
|
+
def _get_nested_validation_messages(self, parent_name: str = "", visited: Set[int] = None) -> List['ValidationMessage']:
|
|
112
|
+
"""
|
|
113
|
+
Recursively extract warnings from a Pydantic model and its nested fields, including computed fields.
|
|
114
|
+
|
|
115
|
+
:param parent_name: The name of the parent model to track the path.
|
|
116
|
+
:param visited: Set of visited object IDs to prevent infinite loops.
|
|
117
|
+
:return: List of ValidationMessages from this and nested models.
|
|
118
|
+
"""
|
|
119
|
+
if visited is None:
|
|
120
|
+
visited = set()
|
|
121
|
+
|
|
122
|
+
model_id = id(self)
|
|
123
|
+
if model_id in visited:
|
|
124
|
+
return []
|
|
125
|
+
visited.add(model_id)
|
|
126
|
+
|
|
127
|
+
warnings_list = [warning for warning in self.validation_messages(nested=False)]
|
|
128
|
+
|
|
129
|
+
# Traverse regular fields
|
|
130
|
+
for field_name, field in self.__fields__.items():
|
|
131
|
+
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
132
|
+
value = getattr(self, field_name)
|
|
133
|
+
|
|
134
|
+
if isinstance(value, LabFREED_BaseModel):
|
|
135
|
+
warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
|
|
136
|
+
elif isinstance(value, list):
|
|
137
|
+
for index, item in enumerate(value):
|
|
138
|
+
if isinstance(item, LabFREED_BaseModel):
|
|
139
|
+
list_path = f"{full_path}[{index}]"
|
|
140
|
+
warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
|
|
141
|
+
|
|
142
|
+
# Traverse computed fields
|
|
143
|
+
computed_fields = getattr(self, '__pydantic_decorators__', {}).computed_fields or {}
|
|
144
|
+
for field_name in computed_fields:
|
|
145
|
+
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
146
|
+
try:
|
|
147
|
+
value = getattr(self, field_name)
|
|
148
|
+
except Exception:
|
|
149
|
+
continue # Safely skip computed properties that raise errors
|
|
150
|
+
|
|
151
|
+
if isinstance(value, LabFREED_BaseModel):
|
|
152
|
+
warnings_list.extend(value._get_nested_validation_messages(full_path, visited))
|
|
153
|
+
elif isinstance(value, list):
|
|
154
|
+
for index, item in enumerate(value):
|
|
155
|
+
if isinstance(item, LabFREED_BaseModel):
|
|
156
|
+
list_path = f"{full_path}[{index}]"
|
|
157
|
+
warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
|
|
158
|
+
|
|
159
|
+
return warnings_list
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def print_validation_messages(self, target='console'):
|
|
164
|
+
msgs = self.format_validation_messages(target=target)
|
|
165
|
+
|
|
166
|
+
table = Table(title="Validation Results", show_header=False, title_justify='left')
|
|
167
|
+
|
|
168
|
+
def col(s):
|
|
169
|
+
return table.add_column(s, vertical='top')
|
|
170
|
+
col("-")
|
|
171
|
+
|
|
172
|
+
if not msgs:
|
|
173
|
+
table.add_row('All clear!', end_section=True)
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
for m in msgs:
|
|
177
|
+
table.add_row(m)
|
|
178
|
+
table.add_section()
|
|
179
|
+
|
|
180
|
+
logging.info(table)
|
|
181
|
+
print(table)
|
|
182
|
+
return table
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def format_validation_messages(self, target='console') -> list[str]:
|
|
186
|
+
"""Format validation messages
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
target (str, optional): Target format: 'markdown', 'console', 'html', 'html_styled'.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
list[str]: formated messages
|
|
193
|
+
"""
|
|
194
|
+
formatted_msg = list()
|
|
195
|
+
for m in self.validation_messages():
|
|
196
|
+
if m.level == ValidationMsgLevel.ERROR:
|
|
197
|
+
color = '#d70000'
|
|
198
|
+
else:
|
|
199
|
+
color = '#d78700'
|
|
200
|
+
|
|
201
|
+
match target:
|
|
202
|
+
case 'markdown':
|
|
203
|
+
fmt = lambda s: f'👉{s}👈' # noqa: E731
|
|
204
|
+
fmt_title = lambda s: f'**{s}**' # noqa: E731
|
|
205
|
+
br = '\n'
|
|
206
|
+
case 'console':
|
|
207
|
+
fmt = lambda s: f'[{color} bold]{s}[/{color} bold]' # noqa: E731
|
|
208
|
+
fmt_title = fmt
|
|
209
|
+
br = '\n'
|
|
210
|
+
case 'html':
|
|
211
|
+
lvl = m.level.name.lower()
|
|
212
|
+
fmt = lambda s: f'<span class="validation-problem {lvl}">{s}</span>' # noqa: E731
|
|
213
|
+
fmt_title = lambda s: f'<span class="validation-title {lvl}">{s}</span>' # noqa: E731
|
|
214
|
+
br = '<br>'
|
|
215
|
+
case 'html_styled':
|
|
216
|
+
fmt = lambda s: f'<b style="color:{color}">{s}</b>' # noqa: E731
|
|
217
|
+
fmt_title = fmt
|
|
218
|
+
br = '<br>'
|
|
219
|
+
|
|
220
|
+
serialized = str(self)
|
|
221
|
+
serialized = serialized.replace('\n', br)
|
|
222
|
+
emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt)
|
|
223
|
+
emphazised_highlight = emphazised_highlight.replace('👈👉','') # removes two consecutive markers, to make it cleaner
|
|
224
|
+
|
|
225
|
+
txt = f'{fmt_title(m.level.name)} in {m.source}'
|
|
226
|
+
txt += br + f'{m.msg}'
|
|
227
|
+
txt += br+br + emphazised_highlight
|
|
228
|
+
|
|
229
|
+
formatted_msg.append(txt)
|
|
230
|
+
return formatted_msg
|
|
231
|
+
|
|
232
|
+
def _emphasize_in(self, validation_msg, validation_node_str:str, fmt):
|
|
233
|
+
if validation_msg.highlight_sub_patterns:
|
|
234
|
+
replacements = validation_msg.highlight_sub_patterns
|
|
235
|
+
else:
|
|
236
|
+
replacements = [validation_msg.highlight]
|
|
237
|
+
# Sort patterns by length descending to avoid subpattern clobbering
|
|
238
|
+
sorted_patterns = sorted(replacements, key=len, reverse=True)
|
|
239
|
+
# Escape the patterns for regex safety
|
|
240
|
+
escaped_patterns = [re.escape(p) for p in sorted_patterns]
|
|
241
|
+
# Create one regex pattern with alternation (longest first)
|
|
242
|
+
pattern = re.compile("|".join(escaped_patterns))
|
|
243
|
+
|
|
244
|
+
out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
250
|
+
return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
|
|
251
|
+
|
|
252
|
+
def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
253
|
+
return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
|
|
254
|
+
|
|
255
|
+
def _quote_texts(texts:list[str]):
|
|
256
|
+
return ','.join([f"'{t}'" for t in texts])
|
|
257
|
+
|
|
258
|
+
|
labfreed/pac_cat/__init__.py
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
|
-
from .pac_cat import PAC_CAT
|
|
2
|
-
from .category_base import Category
|
|
3
|
-
from .predefined_categories import (
|
|
4
|
-
Material_Device, Material_Substance, Material_Consumable, Material_Misc, Data_Method, Data_Result, Data_Progress,
|
|
5
|
-
Data_Calibration, Data_Abstract, category_key_to_class_map # noqa: F401
|
|
6
|
-
)
|
|
7
|
-
|
|
8
|
-
__all__ = [
|
|
9
|
-
"PAC_CAT",
|
|
10
|
-
"Category",
|
|
11
|
-
"Material_Device",
|
|
12
|
-
"Material_Substance",
|
|
13
|
-
"Material_Consumable",
|
|
14
|
-
"Material_Misc",
|
|
15
|
-
"Data_Method",
|
|
16
|
-
"Data_Result",
|
|
17
|
-
"Data_Progress",
|
|
18
|
-
"Data_Calibration",
|
|
19
|
-
"Data_Abstract"
|
|
1
|
+
from .pac_cat import PAC_CAT
|
|
2
|
+
from .category_base import Category
|
|
3
|
+
from .predefined_categories import (
|
|
4
|
+
Material_Device, Material_Substance, Material_Consumable, Material_Misc, Data_Method, Data_Result, Data_Progress,
|
|
5
|
+
Data_Calibration, Data_Abstract, category_key_to_class_map # noqa: F401
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"PAC_CAT",
|
|
10
|
+
"Category",
|
|
11
|
+
"Material_Device",
|
|
12
|
+
"Material_Substance",
|
|
13
|
+
"Material_Consumable",
|
|
14
|
+
"Material_Misc",
|
|
15
|
+
"Data_Method",
|
|
16
|
+
"Data_Result",
|
|
17
|
+
"Data_Progress",
|
|
18
|
+
"Data_Calibration",
|
|
19
|
+
"Data_Abstract"
|
|
20
20
|
]
|