labfreed 0.0.5__py3-none-any.whl → 0.2.0b1__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 +4 -1
- labfreed/labfreed_infrastructure.py +276 -0
- labfreed/pac_cat/__init__.py +17 -0
- labfreed/pac_cat/category_base.py +51 -0
- labfreed/pac_cat/pac_cat.py +159 -0
- labfreed/pac_cat/predefined_categories.py +190 -0
- labfreed/pac_id/__init__.py +19 -0
- labfreed/pac_id/extension.py +48 -0
- labfreed/pac_id/id_segment.py +90 -0
- labfreed/pac_id/pac_id.py +140 -0
- labfreed/pac_id/url_parser.py +154 -0
- labfreed/pac_id/url_serializer.py +80 -0
- labfreed/pac_id_resolver/__init__.py +2 -0
- labfreed/pac_id_resolver/cit_v1.py +149 -0
- labfreed/pac_id_resolver/cit_v2.py +303 -0
- labfreed/pac_id_resolver/resolver.py +81 -0
- labfreed/pac_id_resolver/services.py +80 -0
- labfreed/qr/__init__.py +1 -0
- labfreed/qr/generate_qr.py +422 -0
- labfreed/trex/__init__.py +16 -0
- labfreed/trex/python_convenience/__init__.py +3 -0
- labfreed/trex/python_convenience/data_table.py +45 -0
- labfreed/trex/python_convenience/pyTREX.py +242 -0
- labfreed/trex/python_convenience/quantity.py +46 -0
- labfreed/trex/table_segment.py +227 -0
- labfreed/trex/trex.py +69 -0
- labfreed/trex/trex_base_models.py +336 -0
- labfreed/trex/value_segments.py +111 -0
- labfreed/{DisplayNameExtension → utilities}/base36.py +29 -13
- labfreed/well_known_extensions/__init__.py +5 -0
- labfreed/well_known_extensions/default_extension_interpreters.py +7 -0
- labfreed/well_known_extensions/display_name_extension.py +40 -0
- labfreed/well_known_extensions/trex_extension.py +31 -0
- labfreed/well_known_keys/gs1/__init__.py +6 -0
- labfreed/well_known_keys/gs1/gs1.py +4 -0
- labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +57 -0
- labfreed/{PAC_ID/well_known_segment_keys.py → well_known_keys/labfreed/well_known_keys.py} +1 -1
- labfreed/well_known_keys/unece/UneceUnits.json +33730 -0
- labfreed/well_known_keys/unece/__init__.py +4 -0
- labfreed/well_known_keys/unece/unece_units.py +68 -0
- labfreed-0.2.0b1.dist-info/METADATA +329 -0
- labfreed-0.2.0b1.dist-info/RECORD +44 -0
- {labfreed-0.0.5.dist-info → labfreed-0.2.0b1.dist-info}/WHEEL +1 -1
- labfreed/DisplayNameExtension/DisplayNameExtension.py +0 -34
- labfreed/PAC_CAT/__init__.py +0 -1
- labfreed/PAC_CAT/data_model.py +0 -109
- labfreed/PAC_ID/__init__.py +0 -0
- labfreed/PAC_ID/data_model.py +0 -215
- labfreed/PAC_ID/parse.py +0 -142
- labfreed/PAC_ID/serialize.py +0 -60
- labfreed/TREXExtension/data_model.py +0 -239
- labfreed/TREXExtension/parse.py +0 -46
- labfreed/TREXExtension/uncertainty.py +0 -32
- labfreed/TREXExtension/unit_utilities.py +0 -143
- labfreed/validation.py +0 -71
- labfreed-0.0.5.dist-info/METADATA +0 -34
- labfreed-0.0.5.dist-info/RECORD +0 -19
- {labfreed-0.0.5.dist-info → labfreed-0.2.0b1.dist-info}/licenses/LICENSE +0 -0
labfreed/__init__.py
CHANGED
|
@@ -0,0 +1,276 @@
|
|
|
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 _emphasize_in(self, validation_msg, validation_node_str:str, fmt, color='black'):
|
|
164
|
+
if validation_msg.highlight_sub_patterns:
|
|
165
|
+
replacements = validation_msg.highlight_sub_patterns
|
|
166
|
+
else:
|
|
167
|
+
replacements = [validation_msg.highlight]
|
|
168
|
+
# Sort patterns by length descending to avoid subpattern clobbering
|
|
169
|
+
sorted_patterns = sorted(replacements, key=len, reverse=True)
|
|
170
|
+
# Escape the patterns for regex safety
|
|
171
|
+
escaped_patterns = [re.escape(p) for p in sorted_patterns]
|
|
172
|
+
# Create one regex pattern with alternation (longest first)
|
|
173
|
+
pattern = re.compile("|".join(escaped_patterns))
|
|
174
|
+
|
|
175
|
+
out = pattern.sub(lambda m: fmt(m.group(0)), validation_node_str)
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def print_validation_messages(self, target='console'):
|
|
180
|
+
msgs = self._get_nested_validation_messages()
|
|
181
|
+
|
|
182
|
+
table = Table(title="Validation Results", show_header=False, title_justify='left')
|
|
183
|
+
|
|
184
|
+
def col(s):
|
|
185
|
+
return table.add_column(s, vertical='top')
|
|
186
|
+
col("-")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if not msgs:
|
|
190
|
+
table.add_row('All clear!', end_section=True)
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
for m in msgs:
|
|
194
|
+
if m.level == ValidationMsgLevel.ERROR:
|
|
195
|
+
color = 'red'
|
|
196
|
+
else:
|
|
197
|
+
color = 'yellow'
|
|
198
|
+
|
|
199
|
+
match target:
|
|
200
|
+
case 'markdown':
|
|
201
|
+
fmt = lambda s: f'👉{s}👈' # noqa: E731
|
|
202
|
+
case 'console':
|
|
203
|
+
fmt = lambda s: f'[{color} bold]{s}[/{color} bold]' # noqa: E731
|
|
204
|
+
case 'html':
|
|
205
|
+
fmt = lambda s: f'<span class="val_{color}">{s}</span>' # noqa: E731
|
|
206
|
+
case 'html_styled':
|
|
207
|
+
fmt = lambda s: f'<b style="color:{color}>{s}</b>' # noqa: E731
|
|
208
|
+
|
|
209
|
+
serialized = str(self)
|
|
210
|
+
emphazised_highlight = self._emphasize_in(m, serialized, fmt=fmt, color=color)
|
|
211
|
+
emphazised_highlight = emphazised_highlight.replace('👈👉','') # removes two consecutive markers, to make it cleaner
|
|
212
|
+
|
|
213
|
+
txt = f'[bold {color}]{m.level.name} [/bold {color}] in {m.source}'
|
|
214
|
+
txt += '\n' + f'{m.msg}'
|
|
215
|
+
txt += '\n\n' + emphazised_highlight
|
|
216
|
+
|
|
217
|
+
table.add_row( txt)
|
|
218
|
+
table.add_section()
|
|
219
|
+
|
|
220
|
+
logging.info(table)
|
|
221
|
+
print(table)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# def print_validation_messages_(self, str_to_highlight_in=None, target='console'):
|
|
228
|
+
# if not str_to_highlight_in:
|
|
229
|
+
# str_to_highlight_in = str(self)
|
|
230
|
+
# msgs = self.get_nested_validation_messages()
|
|
231
|
+
# print('\n'.join(['\n',
|
|
232
|
+
# '=======================================',
|
|
233
|
+
# 'Validation Results',
|
|
234
|
+
# '---------------------------------------'
|
|
235
|
+
# ]
|
|
236
|
+
# )
|
|
237
|
+
# )
|
|
238
|
+
|
|
239
|
+
# if not msgs:
|
|
240
|
+
# print('All clear!')
|
|
241
|
+
# return
|
|
242
|
+
|
|
243
|
+
# for m in msgs:
|
|
244
|
+
# if m.level.casefold() == "error":
|
|
245
|
+
# color = 'red'
|
|
246
|
+
# else:
|
|
247
|
+
# color = 'yellow'
|
|
248
|
+
|
|
249
|
+
# text = Text.from_markup(f'\n [bold {color}]{m.level} [/bold {color}] in \t {m.source}' )
|
|
250
|
+
# print(text)
|
|
251
|
+
# match target:
|
|
252
|
+
# case 'markdown':
|
|
253
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'🔸').replace('[/', '').replace('[', '').replace(']', '')
|
|
254
|
+
# case 'console':
|
|
255
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'bold {color}')
|
|
256
|
+
# case 'html':
|
|
257
|
+
# formatted_highlight = m.emphazised_highlight.replace('emph', f'b').replace('[', '<').replace(']', '>')
|
|
258
|
+
# fmtd = str_to_highlight_in.replace(m.highlight, formatted_highlight)
|
|
259
|
+
# fmtd = Text.from_markup(fmtd)
|
|
260
|
+
# print(fmtd)
|
|
261
|
+
# print(Text.from_markup(f'{m.problem_msg}'))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _filter_errors(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
268
|
+
return [ m for m in val_msg if m.level == ValidationMsgLevel.ERROR ]
|
|
269
|
+
|
|
270
|
+
def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]:
|
|
271
|
+
return [ m for m in val_msg if m.level != ValidationMsgLevel.ERROR ]
|
|
272
|
+
|
|
273
|
+
def _quote_texts(texts:list[str]):
|
|
274
|
+
return ','.join([f"'{t}'" for t in texts])
|
|
275
|
+
|
|
276
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .pac_cat import PAC_CAT
|
|
2
|
+
from .category_base import Category
|
|
3
|
+
from .predefined_categories import Material_Device, Material_Substance, Material_Consumable, Material_Misc, Data_Method, Data_Result, Data_Progress, Data_Calibration, Data_Abstract
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"PAC_CAT",
|
|
7
|
+
"Category",
|
|
8
|
+
"Material_Device",
|
|
9
|
+
"Material_Substance",
|
|
10
|
+
"Material_Consumable",
|
|
11
|
+
"Material_Misc",
|
|
12
|
+
"Data_Method",
|
|
13
|
+
"Data_Result",
|
|
14
|
+
"Data_Progress",
|
|
15
|
+
"Data_Calibration",
|
|
16
|
+
"Data_Abstract"
|
|
17
|
+
]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Any
|
|
3
|
+
from pydantic import PrivateAttr, computed_field, model_validator
|
|
4
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
|
|
5
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Category(LabFREED_BaseModel):
|
|
9
|
+
'''
|
|
10
|
+
Represents a category. \n
|
|
11
|
+
This is the base class for categories. If possible a more specific category should be used.
|
|
12
|
+
'''
|
|
13
|
+
key:str
|
|
14
|
+
'''The category key, e.g. "-MD"'''
|
|
15
|
+
_segments: list[IDSegment] = PrivateAttr(default_factory=list)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@computed_field
|
|
19
|
+
@property
|
|
20
|
+
def segments(self) -> list[IDSegment]:
|
|
21
|
+
return self._segments
|
|
22
|
+
|
|
23
|
+
def __init__(self, **data: Any):
|
|
24
|
+
'''@private'''
|
|
25
|
+
# Pop the user-provided value for computed segments
|
|
26
|
+
input_segments = data.pop("segments", None)
|
|
27
|
+
super().__init__(**data)
|
|
28
|
+
self._segments = input_segments
|
|
29
|
+
|
|
30
|
+
@model_validator(mode='after')
|
|
31
|
+
def _warn_unusual_category_key(self):
|
|
32
|
+
''' this base class is instantiated only if the key is not a known category key'''
|
|
33
|
+
if type(self) is Category:
|
|
34
|
+
self._add_validation_message(
|
|
35
|
+
source=f"Category {self.key}",
|
|
36
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
37
|
+
msg=f'Category key {self.key} is not a well known key. It is recommended to use well known keys only',
|
|
38
|
+
highlight_pattern = f"{self.key}"
|
|
39
|
+
)
|
|
40
|
+
return self
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def __str__(self):
|
|
44
|
+
s = '\n'.join( [f'{field_name} \t ({field_info.alias or ''}): \t {getattr(self, field_name)}' for field_name, field_info in self.model_fields.items() if getattr(self, field_name)])
|
|
45
|
+
return s
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations # optional in 3.11, but recommended for consistency
|
|
2
|
+
|
|
3
|
+
from typing import Self
|
|
4
|
+
from pydantic import computed_field, model_validator
|
|
5
|
+
|
|
6
|
+
from rich import print
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from labfreed.labfreed_infrastructure import ValidationMsgLevel
|
|
11
|
+
|
|
12
|
+
from labfreed.pac_cat.category_base import Category
|
|
13
|
+
from labfreed.pac_cat.predefined_categories import Data_Calibration, Data_Method, Data_Progress, Data_Result, Material_Consumable, Material_Device, Material_Misc, Material_Substance, Data_Static
|
|
14
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
15
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
16
|
+
|
|
17
|
+
''' Configure pdoc'''
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PAC_CAT(PAC_ID):
|
|
21
|
+
'''
|
|
22
|
+
Extends a PAC-ID with interpretation of the identifier as categories
|
|
23
|
+
'''
|
|
24
|
+
@computed_field
|
|
25
|
+
@property
|
|
26
|
+
def categories(self) -> list[Category]:
|
|
27
|
+
'''The categories present in the PAC-ID's identifier'''
|
|
28
|
+
category_segments = self._split_segments_by_category(self.identifier)
|
|
29
|
+
categories = list()
|
|
30
|
+
for c in category_segments:
|
|
31
|
+
categories.append(self._cat_from_cat_segments(c))
|
|
32
|
+
return categories
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_category(self, key) -> Category:
|
|
37
|
+
"""Helper to get a category by key
|
|
38
|
+
"""
|
|
39
|
+
tmp = [c for c in self.categories if c.key == key]
|
|
40
|
+
if not tmp:
|
|
41
|
+
return None
|
|
42
|
+
return tmp[0]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def from_categories(cls, issuer:str, categories:list[Category]) -> PAC_CAT:
|
|
47
|
+
identifier = list()
|
|
48
|
+
for category in categories:
|
|
49
|
+
identifier.append(IDSegment(value=category.key))
|
|
50
|
+
identifier.extend(category.segments)
|
|
51
|
+
return PAC_CAT(issuer=issuer, identifier=identifier)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_pac_id(cls, pac_id:PAC_ID) -> Self:
|
|
56
|
+
'''Constructs a PAC-CAT from a PAC-ID'''
|
|
57
|
+
return PAC_CAT(issuer=pac_id.issuer, identifier=pac_id.identifier)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def to_pac_id(self) -> PAC_ID:
|
|
62
|
+
return PAC_ID(issuer=self.issuer, identifier=self.identifier)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def _cat_from_cat_segments(cls, segments:list[IDSegment]) -> Category:
|
|
67
|
+
segments = segments.copy()
|
|
68
|
+
category_key = segments[0].value
|
|
69
|
+
segments.pop(0)
|
|
70
|
+
|
|
71
|
+
mapping = {
|
|
72
|
+
'-MD': Material_Device,
|
|
73
|
+
'-MS': Material_Substance,
|
|
74
|
+
'-MC': Material_Consumable,
|
|
75
|
+
'-MM': Material_Misc,
|
|
76
|
+
'-DM': Data_Method,
|
|
77
|
+
'-DR': Data_Result,
|
|
78
|
+
'-DC': Data_Calibration,
|
|
79
|
+
'-DP': Data_Progress,
|
|
80
|
+
'-DS': Data_Static
|
|
81
|
+
}
|
|
82
|
+
known_cat = mapping.get(category_key)
|
|
83
|
+
|
|
84
|
+
if not known_cat:
|
|
85
|
+
return Category(key=category_key, segments=segments)
|
|
86
|
+
|
|
87
|
+
# implicit segment keys
|
|
88
|
+
model_dict = {v.alias: None for k, v in known_cat.model_fields.items() if v.alias and k not in ['key','additional_segments']}
|
|
89
|
+
for k, seg in zip(model_dict.keys(), segments.copy()):
|
|
90
|
+
if seg.key:
|
|
91
|
+
break
|
|
92
|
+
model_dict[k] = seg.value
|
|
93
|
+
segments.pop(0)
|
|
94
|
+
|
|
95
|
+
# try to fill model keys if not already set
|
|
96
|
+
for s in segments.copy():
|
|
97
|
+
if s.key in model_dict.keys() and not model_dict.get(s.key):
|
|
98
|
+
model_dict[s.key] = s.value
|
|
99
|
+
segments.remove(s)
|
|
100
|
+
|
|
101
|
+
model_dict['additional_segments'] = segments
|
|
102
|
+
model_dict['key'] = category_key
|
|
103
|
+
cat= known_cat(**model_dict)
|
|
104
|
+
return cat
|
|
105
|
+
|
|
106
|
+
@staticmethod
|
|
107
|
+
def _split_segments_by_category(segments:list[IDSegment]) -> list[list[IDSegment]]:
|
|
108
|
+
category_segments = list()
|
|
109
|
+
c = list()
|
|
110
|
+
for s in segments:
|
|
111
|
+
# new category starts with "-"
|
|
112
|
+
if s.value[0] == '-':
|
|
113
|
+
c = [s]
|
|
114
|
+
category_segments.append(c)
|
|
115
|
+
else:
|
|
116
|
+
c.append(s)
|
|
117
|
+
|
|
118
|
+
# first cat can be empty > remove
|
|
119
|
+
category_segments = [c for c in category_segments if len(c) > 0]
|
|
120
|
+
|
|
121
|
+
return category_segments
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@model_validator(mode='after')
|
|
125
|
+
def _check_keys_are_unique_in_each_category(self) -> Self:
|
|
126
|
+
for c in self.categories:
|
|
127
|
+
keys = [s.key for s in c.segments if s.key]
|
|
128
|
+
duplicate_keys = [k for k in set(keys) if keys.count(k) > 1]
|
|
129
|
+
if duplicate_keys:
|
|
130
|
+
for k in duplicate_keys:
|
|
131
|
+
self._add_validation_message(
|
|
132
|
+
source=f"identifier {k}",
|
|
133
|
+
level = ValidationMsgLevel.ERROR,
|
|
134
|
+
msg=f"Duplicate key {k} in category {c.key}",
|
|
135
|
+
highlight_pattern = k
|
|
136
|
+
)
|
|
137
|
+
return self
|
|
138
|
+
|
|
139
|
+
def print_categories(self):
|
|
140
|
+
table = Table(title=f'Categories in {str(self)}', show_header=False)
|
|
141
|
+
table.add_column('0')
|
|
142
|
+
table.add_column('1')
|
|
143
|
+
for i, c in enumerate(self.categories):
|
|
144
|
+
if i == 0:
|
|
145
|
+
title = Text('Main Category', style='bold')
|
|
146
|
+
else:
|
|
147
|
+
title = Text('Category', style='bold')
|
|
148
|
+
|
|
149
|
+
table.add_row(title)
|
|
150
|
+
|
|
151
|
+
for field_name, field_info in c.model_fields.items():
|
|
152
|
+
if not getattr(c, field_name):
|
|
153
|
+
continue
|
|
154
|
+
table.add_row(f"{field_name} ({field_info.alias or ''})",
|
|
155
|
+
f" {getattr(c, field_name)}"
|
|
156
|
+
)
|
|
157
|
+
table.add_section()
|
|
158
|
+
print(table)
|
|
159
|
+
|