labfreed 0.0.4__py3-none-any.whl → 0.2.0__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/well_known_keys/labfreed/well_known_keys.py +16 -0
- 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.0.dist-info/METADATA +357 -0
- labfreed-0.2.0.dist-info/RECORD +44 -0
- {labfreed-0.0.4.dist-info → labfreed-0.2.0.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 -114
- labfreed/PAC_ID/parse.py +0 -133
- labfreed/PAC_ID/serialize.py +0 -57
- 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 -134
- labfreed-0.0.4.dist-info/METADATA +0 -15
- labfreed-0.0.4.dist-info/RECORD +0 -17
- {labfreed-0.0.4.dist-info → labfreed-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
from datetime import date, datetime, time
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
from pydantic import PrivateAttr, field_validator, model_validator
|
|
8
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
9
|
+
from abc import ABC, abstractclassmethod, abstractmethod
|
|
10
|
+
|
|
11
|
+
from labfreed.utilities.base36 import base36, to_base36, from_base36
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
''' Configure pdoc'''
|
|
15
|
+
v_segs = ["NumericSegment",
|
|
16
|
+
"DateSegment",
|
|
17
|
+
"BoolSegment",
|
|
18
|
+
"AlphanumericSegment",
|
|
19
|
+
"TextSegment",
|
|
20
|
+
"BinarySegment"]
|
|
21
|
+
__all__ = ["TREX"] + v_segs + ["TableSegment"] # noqa: F822
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Value(LabFREED_BaseModel, ABC):
|
|
27
|
+
'''@private
|
|
28
|
+
Helper to add validation for various types to ValueSegments and Tables
|
|
29
|
+
'''
|
|
30
|
+
value:str
|
|
31
|
+
|
|
32
|
+
def serialize(self):
|
|
33
|
+
return self.value
|
|
34
|
+
|
|
35
|
+
@abstractclassmethod
|
|
36
|
+
def _from_python_type(cls, v):
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def _value_to_python_type(self):
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NumericValue(Value):
|
|
45
|
+
@field_validator('value', mode='before')
|
|
46
|
+
@classmethod
|
|
47
|
+
def _from_python_type(cls, v:str| int|float):
|
|
48
|
+
if isinstance(v, str):
|
|
49
|
+
return v
|
|
50
|
+
return str(v)
|
|
51
|
+
|
|
52
|
+
@model_validator(mode='after')
|
|
53
|
+
def _validate(self):
|
|
54
|
+
value = self.value
|
|
55
|
+
if not_allowed_chars := set(re.sub(r'[0-9\.\-E]', '', value)):
|
|
56
|
+
self._add_validation_message(
|
|
57
|
+
source=f"TREX numeric value {value}",
|
|
58
|
+
level=ValidationMsgLevel.ERROR,
|
|
59
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in quantity segment. Base36 encoding only allows A-Z0-9",
|
|
60
|
+
highlight_pattern = f'{value}',
|
|
61
|
+
highlight_sub=not_allowed_chars
|
|
62
|
+
)
|
|
63
|
+
if not re.fullmatch(r'-?\d+(\.\d+)?(E-?\d+)?', value):
|
|
64
|
+
self._add_validation_message(
|
|
65
|
+
source=f"TREX numeric value {value}",
|
|
66
|
+
level=ValidationMsgLevel.ERROR,
|
|
67
|
+
msg=f"{value} cannot be converted to number",
|
|
68
|
+
highlight_pattern = f'{value}'
|
|
69
|
+
)
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def _value_to_python_type(self) -> str:
|
|
73
|
+
v = float(self.value)
|
|
74
|
+
if '.' not in self.value and 'E' not in self.value:
|
|
75
|
+
return int(v)
|
|
76
|
+
else:
|
|
77
|
+
return v
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class DateValue(Value):
|
|
81
|
+
_date_time_dict:dict|None = PrivateAttr(default=None)
|
|
82
|
+
@field_validator('value', mode='before')
|
|
83
|
+
@classmethod
|
|
84
|
+
def _from_python_type(cls, v:str| date|time|datetime):
|
|
85
|
+
if isinstance(v, str):
|
|
86
|
+
return v
|
|
87
|
+
|
|
88
|
+
sd = ""
|
|
89
|
+
st = ""
|
|
90
|
+
if isinstance(v, date) or isinstance(v, datetime):
|
|
91
|
+
sd = v.strftime('%Y%m%d')
|
|
92
|
+
if isinstance(v, time) or isinstance(v, datetime):
|
|
93
|
+
if v.microsecond:
|
|
94
|
+
st = v.strftime("T%H%M%S.") + f"{v.microsecond // 1000:03d}"
|
|
95
|
+
elif v.second:
|
|
96
|
+
st = v.strftime("T%H%M%S")
|
|
97
|
+
else:
|
|
98
|
+
st = v.strftime("T%H%M")
|
|
99
|
+
|
|
100
|
+
return sd + st
|
|
101
|
+
|
|
102
|
+
@model_validator(mode='after')
|
|
103
|
+
def _validate(self):
|
|
104
|
+
pattern:str = r'((?P<year>\d{4})(?P<month>\d{2})(?P<day>\d{2}))?(T(?P<hour>\d{2})(?P<minute>\d{2})(?P<second>\d{2})?(\.(?P<millisecond>\d{3}))?)?'
|
|
105
|
+
value=self.value
|
|
106
|
+
if not re.fullmatch(pattern, value):
|
|
107
|
+
self._add_validation_message(
|
|
108
|
+
source=f"TREX date value {value}",
|
|
109
|
+
level=ValidationMsgLevel.ERROR,
|
|
110
|
+
msg=f'{value} is not in a valid format. Valid format for date: YYYYMMDD; Valid for time: THHMM, THHMMSS, THHMMSS.SSS; Datetime any combination of valid date and time',
|
|
111
|
+
highlight_pattern = f'{value}'
|
|
112
|
+
)
|
|
113
|
+
return self
|
|
114
|
+
|
|
115
|
+
matches = re.match(pattern, value)
|
|
116
|
+
d = matches.groupdict()
|
|
117
|
+
d = {k: int(v) for k,v in d.items() if v }
|
|
118
|
+
if 'millisecond' in d.keys():
|
|
119
|
+
ms = d.pop('millisecond')
|
|
120
|
+
d.update({'microsecond': ms * 1000})
|
|
121
|
+
try:
|
|
122
|
+
if d.get('year'): # input is only a time
|
|
123
|
+
datetime(**d)
|
|
124
|
+
else:
|
|
125
|
+
time(**d)
|
|
126
|
+
except ValueError:
|
|
127
|
+
self._add_validation_message(
|
|
128
|
+
source=f"TREX date value {value}",
|
|
129
|
+
level=ValidationMsgLevel.ERROR,
|
|
130
|
+
msg=f'{value} is no valid date or time.',
|
|
131
|
+
highlight_pattern = f'{value}'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
self._date_time_dict = d
|
|
135
|
+
return self
|
|
136
|
+
|
|
137
|
+
def _value_to_python_type(self) -> str:
|
|
138
|
+
d = self._date_time_dict
|
|
139
|
+
if d.get('year') and d.get('hour'): # input is only a time
|
|
140
|
+
return datetime(**d)
|
|
141
|
+
elif d.get('year'):
|
|
142
|
+
return date(**d)
|
|
143
|
+
else:
|
|
144
|
+
return time(**d)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class BoolValue(Value):
|
|
148
|
+
@field_validator('value', mode='before')
|
|
149
|
+
@classmethod
|
|
150
|
+
def _from_python_type(cls, v:str| bool):
|
|
151
|
+
if isinstance(v, str):
|
|
152
|
+
return v
|
|
153
|
+
|
|
154
|
+
return 'T' if v else 'F'
|
|
155
|
+
|
|
156
|
+
@model_validator(mode='after')
|
|
157
|
+
def _validate(self):
|
|
158
|
+
if self.value not in ['T', 'F']:
|
|
159
|
+
self._add_validation_message(
|
|
160
|
+
source=f"TREX boolean value {self.value}",
|
|
161
|
+
level= ValidationMsgLevel.ERROR,
|
|
162
|
+
msg=f'{self.value} is no valid boolean. Must be T or F',
|
|
163
|
+
highlight_pattern = f'{self.value}',
|
|
164
|
+
highlight_sub=[c for c in self.value]
|
|
165
|
+
)
|
|
166
|
+
return self
|
|
167
|
+
|
|
168
|
+
def _value_to_python_type(self) -> str:
|
|
169
|
+
if self.value == 'T':
|
|
170
|
+
return True
|
|
171
|
+
elif self.value == 'F':
|
|
172
|
+
return False
|
|
173
|
+
else:
|
|
174
|
+
Exception(f'{self} is not valid boolean. That really should not have been possible -- Contact the maintainers of the library')
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class AlphanumericValue(Value):
|
|
178
|
+
@field_validator('value', mode='before')
|
|
179
|
+
@classmethod
|
|
180
|
+
def _from_python_type(cls, v:str):
|
|
181
|
+
return v
|
|
182
|
+
|
|
183
|
+
@model_validator(mode='after')
|
|
184
|
+
def _validate(self):
|
|
185
|
+
if re.match(r'[a-z]', self.value):
|
|
186
|
+
self._add_validation_message(
|
|
187
|
+
source=f"TREX value {self.value}",
|
|
188
|
+
level= ValidationMsgLevel.ERROR,
|
|
189
|
+
msg="Lower case characters are not allowed.",
|
|
190
|
+
highlight_pattern = self.value
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
|
|
194
|
+
self._add_validation_message(
|
|
195
|
+
source=f"TREX value {self.value}",
|
|
196
|
+
level= ValidationMsgLevel.ERROR,
|
|
197
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in alphanumeric segment",
|
|
198
|
+
highlight_pattern = self.value,
|
|
199
|
+
highlight_sub=not_allowed_chars
|
|
200
|
+
)
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def _value_to_python_type(self) -> str:
|
|
204
|
+
return self.value
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class TextValue(Value):
|
|
208
|
+
@field_validator('value', mode='before')
|
|
209
|
+
@classmethod
|
|
210
|
+
def _from_python_type(cls, v:base36|str):
|
|
211
|
+
if isinstance(v, str):
|
|
212
|
+
logging.info('Got str for text value > converting to base36')
|
|
213
|
+
return to_base36(v).root
|
|
214
|
+
else:
|
|
215
|
+
return v.root
|
|
216
|
+
|
|
217
|
+
@model_validator(mode='after')
|
|
218
|
+
def _validate(self):
|
|
219
|
+
if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
|
|
220
|
+
self._add_validation_message(
|
|
221
|
+
source=f"TREX value {self.value}",
|
|
222
|
+
level= ValidationMsgLevel.ERROR,
|
|
223
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
|
|
224
|
+
highlight_pattern = self.value,
|
|
225
|
+
highlight_sub=not_allowed_chars
|
|
226
|
+
)
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
def _value_to_python_type(self) -> str:
|
|
230
|
+
decoded = from_base36(self.value)
|
|
231
|
+
return decoded
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class BinaryValue(Value):
|
|
235
|
+
@field_validator('value', mode='before')
|
|
236
|
+
@classmethod
|
|
237
|
+
def _from_python_type(cls, v:base36|str):
|
|
238
|
+
if isinstance(v, str):
|
|
239
|
+
return v
|
|
240
|
+
else:
|
|
241
|
+
return v.root
|
|
242
|
+
|
|
243
|
+
@model_validator(mode='after')
|
|
244
|
+
def _validate(self):
|
|
245
|
+
if not_allowed_chars := set(re.sub(r'[A-Z0-9]', '', self.value)):
|
|
246
|
+
self._add_validation_message(
|
|
247
|
+
source=f"TREX value {self.value}",
|
|
248
|
+
level= ValidationMsgLevel.ERROR,
|
|
249
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in text segment. Base36 encoding only allows A-Z0-9",
|
|
250
|
+
highlight_pattern = self.value,
|
|
251
|
+
highlight_sub=not_allowed_chars
|
|
252
|
+
)
|
|
253
|
+
return self
|
|
254
|
+
|
|
255
|
+
def _value_to_python_type(self) -> bytes:
|
|
256
|
+
decoded = bytes(from_base36(self))
|
|
257
|
+
return decoded
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
class ErrorValue(Value):
|
|
261
|
+
@field_validator('value', mode='before')
|
|
262
|
+
@classmethod
|
|
263
|
+
def _from_python_type(cls, v:str):
|
|
264
|
+
return v
|
|
265
|
+
|
|
266
|
+
@model_validator(mode='after')
|
|
267
|
+
def _validate(self):
|
|
268
|
+
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.value)):
|
|
269
|
+
self._add_validation_message(
|
|
270
|
+
source=f"TREX value {self.value}",
|
|
271
|
+
level= ValidationMsgLevel.ERROR,
|
|
272
|
+
msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in error segment",
|
|
273
|
+
highlight_pattern = self.value,
|
|
274
|
+
highlight_sub=not_allowed_chars
|
|
275
|
+
)
|
|
276
|
+
return self
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _value_to_python_type(self) -> str:
|
|
280
|
+
return self.value
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class TREX_Segment(LabFREED_BaseModel, ABC):
|
|
284
|
+
'''@private
|
|
285
|
+
Abstract class representing a TREX_Segment
|
|
286
|
+
'''
|
|
287
|
+
key: str
|
|
288
|
+
|
|
289
|
+
@abstractmethod
|
|
290
|
+
def serialize(self):
|
|
291
|
+
raise NotImplementedError("Subclasses must implement 'serialize_as_trex()' method")
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@model_validator(mode='after')
|
|
295
|
+
def _validate_key(self):
|
|
296
|
+
if not_allowed_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.key)):
|
|
297
|
+
self._add_validation_message(
|
|
298
|
+
source=f"TREX segment key {self.key}",
|
|
299
|
+
level=ValidationMsgLevel.ERROR,
|
|
300
|
+
msg=f"Segment key contains invalid characters: {_quote_texts(not_allowed_chars)}",
|
|
301
|
+
highlight_pattern = f'{self.key}$',
|
|
302
|
+
highlight_sub=not_allowed_chars
|
|
303
|
+
)
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def str_to_value_type(s:str, t:str):
|
|
311
|
+
match t:
|
|
312
|
+
case 'T.D':
|
|
313
|
+
v = DateValue(value=s)
|
|
314
|
+
case 'T.B':
|
|
315
|
+
v = BoolValue(value=s)
|
|
316
|
+
case 'T.A':
|
|
317
|
+
v = AlphanumericValue(value=s)
|
|
318
|
+
case 'T.T':
|
|
319
|
+
try:
|
|
320
|
+
value = base36(s)
|
|
321
|
+
except ValueError:
|
|
322
|
+
logging.error('String given as T.T contains characters which base36 should not')
|
|
323
|
+
value = s
|
|
324
|
+
v = TextValue(value=value)
|
|
325
|
+
case 'T.X':
|
|
326
|
+
v = BinaryValue(value=s)
|
|
327
|
+
case 'E' :
|
|
328
|
+
v = ErrorValue(value=s)
|
|
329
|
+
case _ :
|
|
330
|
+
v = NumericValue(value=s)
|
|
331
|
+
return v
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
from typing import Literal
|
|
5
|
+
from pydantic import Field, model_validator
|
|
6
|
+
from labfreed.utilities.base36 import base36
|
|
7
|
+
from labfreed.well_known_keys.unece.unece_units import unece_unit_codes
|
|
8
|
+
from labfreed.labfreed_infrastructure import ValidationMsgLevel
|
|
9
|
+
from labfreed.trex.trex_base_models import AlphanumericValue, BinaryValue, BoolValue, DateValue, ErrorValue, NumericValue, TREX_Segment, TextValue, Value
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ValueSegment(TREX_Segment, Value, ABC):
|
|
14
|
+
'''@private: Abstract base class for value segments'''
|
|
15
|
+
type:str
|
|
16
|
+
|
|
17
|
+
@model_validator(mode='after')
|
|
18
|
+
def _validate_type(self):
|
|
19
|
+
valid_types = valid_types = unece_unit_codes() + ['T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E']
|
|
20
|
+
if self.type not in valid_types:
|
|
21
|
+
self._add_validation_message(
|
|
22
|
+
source=f"TREX value segment {self.key}",
|
|
23
|
+
level= ValidationMsgLevel.ERROR,
|
|
24
|
+
msg=f"Type {self.type} is invalid. Must be 'T.D', 'T.B', 'T.A', 'T.T', 'T.X', 'E' or a UNECE unit",
|
|
25
|
+
highlight_pattern = self.type
|
|
26
|
+
)
|
|
27
|
+
return self
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def serialize(self) -> str:
|
|
31
|
+
return f'{self.key}${self.type}:{self.value}'
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NumericSegment(ValueSegment, NumericValue):
|
|
35
|
+
'''Represents a TREX segment holding a numeric value'''
|
|
36
|
+
type: str
|
|
37
|
+
key:str
|
|
38
|
+
value:str
|
|
39
|
+
|
|
40
|
+
class DateSegment(ValueSegment, DateValue):
|
|
41
|
+
'''Represents a TREX segment holding a date'''
|
|
42
|
+
type: Literal['T.D'] = Field('T.D', frozen=True)
|
|
43
|
+
key:str
|
|
44
|
+
value:str
|
|
45
|
+
|
|
46
|
+
class BoolSegment(ValueSegment, BoolValue):
|
|
47
|
+
'''Represents a TREX segment holding a boolean value'''
|
|
48
|
+
type: Literal['T.B'] = Field('T.B', frozen=True)
|
|
49
|
+
key:str
|
|
50
|
+
value:str
|
|
51
|
+
class AlphanumericSegment(ValueSegment, AlphanumericValue):
|
|
52
|
+
'''Represents a TREX segment holding a alphanumeric text'''
|
|
53
|
+
type: Literal['T.A'] = Field('T.A', frozen=True)
|
|
54
|
+
key:str
|
|
55
|
+
value:str
|
|
56
|
+
class TextSegment(ValueSegment, TextValue):
|
|
57
|
+
'''Represents a TREX segment holding a text with arbitrary characters'''
|
|
58
|
+
type: Literal['T.T'] = Field('T.T', frozen=True)
|
|
59
|
+
key:str
|
|
60
|
+
value:str
|
|
61
|
+
|
|
62
|
+
class BinarySegment(ValueSegment, BinaryValue):
|
|
63
|
+
'''Represents a TREX segment holding binary data'''
|
|
64
|
+
type: Literal['T.X'] = Field('T.X', frozen=True)
|
|
65
|
+
key:str
|
|
66
|
+
value:str
|
|
67
|
+
|
|
68
|
+
class ErrorSegment(ValueSegment, ErrorValue):
|
|
69
|
+
'''Represents a TREX segment which has erroneous content'''
|
|
70
|
+
type: Literal['E'] = Field('E', frozen=True)
|
|
71
|
+
key:str
|
|
72
|
+
value:str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _deserialize_value_segment_from_trex_segment_str(trex_segment_str) -> ValueSegment:
|
|
76
|
+
#re_scalar_pattern = re.compile(f"(?P<name>[\w\.-]*?)\$(?P<unit>[\w\.]*?):(?P<value>.*)")
|
|
77
|
+
re_scalar_pattern = re.compile("(?P<name>.+?)\$(?P<unit>.+?):(?P<value>.+)")
|
|
78
|
+
matches = re_scalar_pattern.match(trex_segment_str)
|
|
79
|
+
if not matches:
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
key, type_, value = matches.groups()
|
|
83
|
+
|
|
84
|
+
match type_:
|
|
85
|
+
case 'T.D':
|
|
86
|
+
out = DateSegment(key=key, value=value, type=type_)
|
|
87
|
+
case 'T.B':
|
|
88
|
+
out = BoolSegment(key=key, value=value, type=type_)
|
|
89
|
+
case 'T.A':
|
|
90
|
+
out = AlphanumericSegment(key=key, value=value, type=type_)
|
|
91
|
+
case 'T.T':
|
|
92
|
+
try:
|
|
93
|
+
value = base36(value)
|
|
94
|
+
except ValueError:
|
|
95
|
+
logging.error('String given as T.T contains characters which base36 should not')
|
|
96
|
+
value = value
|
|
97
|
+
out = TextSegment(key=key, value=value, type=type_) # prevent repeated conversion from str to base36 and make explict that when parsing we assume the string tpo be base36 already
|
|
98
|
+
case 'T.X':
|
|
99
|
+
try:
|
|
100
|
+
value = base36(value)
|
|
101
|
+
except ValueError:
|
|
102
|
+
logging.error('String given as T.X contains characters which base36 should not')
|
|
103
|
+
value = value
|
|
104
|
+
out = BinarySegment(key=key, value=value, type=type_) # prevent repeated conversion from str to base36 and make explict that when parsing we assume the string tpo be base36 already
|
|
105
|
+
case 'E':
|
|
106
|
+
out = ErrorSegment(key=key, value=value, type=type_)
|
|
107
|
+
case _:
|
|
108
|
+
out = NumericSegment(value=value, key=key, type=type_)
|
|
109
|
+
|
|
110
|
+
return out
|
|
111
|
+
|
|
@@ -1,31 +1,37 @@
|
|
|
1
|
+
import re
|
|
1
2
|
import string
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
from pydantic import field_validator, RootModel
|
|
5
|
+
|
|
6
|
+
class base36(RootModel[str]):
|
|
7
|
+
@field_validator('root')
|
|
8
|
+
@classmethod
|
|
9
|
+
def validate_format(cls, v: str) -> str:
|
|
10
|
+
if not re.fullmatch(r'[A-Z0-9]*', v):
|
|
11
|
+
raise ValueError("Value must only contain uppercase letters and digits (A-Z, 0-9)")
|
|
12
|
+
return v
|
|
13
|
+
|
|
10
14
|
|
|
11
|
-
def to_base36(s:str):
|
|
15
|
+
def to_base36(s:str) -> base36:
|
|
12
16
|
"""Takes a string, encodes it in UTF-8 and then as base36 string."""
|
|
13
17
|
utf8_encoded = s.encode('utf-8')
|
|
14
18
|
num = int.from_bytes(utf8_encoded, byteorder='big', signed=False)
|
|
15
19
|
|
|
16
20
|
# note: this cannot be arbitrarily chosen. The choice here corresponds to what pythons int(s:str, base:int=10) function used.
|
|
17
|
-
base36_chars =
|
|
21
|
+
base36_chars = _alphabet(base=36)
|
|
18
22
|
if num == 0:
|
|
19
23
|
return base36_chars[0]
|
|
20
|
-
|
|
24
|
+
base_36 = []
|
|
21
25
|
_num = num
|
|
22
26
|
while _num:
|
|
23
27
|
_num, i = divmod(_num, 36)
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
base_36.append(base36_chars[i])
|
|
29
|
+
b36_str = ''.join(reversed(base_36))
|
|
30
|
+
b36_str = base36(b36_str)
|
|
31
|
+
return b36_str
|
|
26
32
|
|
|
27
33
|
|
|
28
|
-
def from_base36(s36:str
|
|
34
|
+
def from_base36(s36:base36) -> str:
|
|
29
35
|
"""inverse of to_base36"""
|
|
30
36
|
# this built in function interprets each character as number in a base represented by the standartd alphabet [0-9 (A-Z|a-z)][0:base] it is case INsensitive.
|
|
31
37
|
num = int(s36, 36)
|
|
@@ -34,6 +40,16 @@ def from_base36(s36:str):
|
|
|
34
40
|
s = _bytes.decode('utf-8')
|
|
35
41
|
return s
|
|
36
42
|
|
|
43
|
+
|
|
44
|
+
def _alphabet(base):
|
|
45
|
+
""" returns an alphabet, which corresponds to what pythons int(s:str, base:int=10) function used.
|
|
46
|
+
"""
|
|
47
|
+
if base < 2 or base > 36:
|
|
48
|
+
ValueError('base can only be between 2 and 36')
|
|
49
|
+
alphabet = (string.digits + string.ascii_uppercase)[0:base]
|
|
50
|
+
return alphabet
|
|
51
|
+
|
|
52
|
+
|
|
37
53
|
if __name__ == "__main__":
|
|
38
54
|
ss = ["A",
|
|
39
55
|
"B-500 B",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Literal, Self
|
|
3
|
+
from pydantic import computed_field
|
|
4
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
5
|
+
from labfreed.pac_id.extension import ExtensionBase
|
|
6
|
+
from labfreed.utilities.base36 import from_base36, to_base36
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DisplayNameExtension(ExtensionBase, LabFREED_BaseModel):
|
|
10
|
+
name:Literal['N'] = 'N'
|
|
11
|
+
type:Literal['N'] = 'N'
|
|
12
|
+
display_name: str
|
|
13
|
+
|
|
14
|
+
@computed_field
|
|
15
|
+
@property
|
|
16
|
+
def data(self)->str:
|
|
17
|
+
# return '/'.join([to_base36(dn) for dn in self.display_name])
|
|
18
|
+
return to_base36(self.display_name)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def from_extension(ext:ExtensionBase) -> Self:
|
|
22
|
+
return DisplayNameExtension.create(name=ext.name,
|
|
23
|
+
type=ext.type,
|
|
24
|
+
data=ext.data)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def create(*, name, type, data):
|
|
28
|
+
if name != 'N':
|
|
29
|
+
logging.warning(f'Name {name} was given, but this extension should only be used with name "N". Will ignore input')
|
|
30
|
+
|
|
31
|
+
if type != 'N':
|
|
32
|
+
logging.warning(f'Type {name} was given, but this extension should only be used with type "N". Will try to parse data as display names')
|
|
33
|
+
|
|
34
|
+
display_name = from_base36(data)
|
|
35
|
+
|
|
36
|
+
return DisplayNameExtension(display_name=display_name)
|
|
37
|
+
|
|
38
|
+
def __str__(self):
|
|
39
|
+
return 'Display name: '+ self.display_name
|
|
40
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Literal, Self
|
|
2
|
+
|
|
3
|
+
from pydantic import computed_field
|
|
4
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
5
|
+
from labfreed.pac_id.extension import ExtensionBase
|
|
6
|
+
from labfreed.trex.trex import TREX
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TREX_Extension(ExtensionBase, LabFREED_BaseModel):
|
|
10
|
+
name:str
|
|
11
|
+
type:Literal['TREX'] = 'TREX'
|
|
12
|
+
trex:TREX
|
|
13
|
+
|
|
14
|
+
@computed_field
|
|
15
|
+
@property
|
|
16
|
+
def data(self)->str:
|
|
17
|
+
trex_str = self.trex.serialize()
|
|
18
|
+
return trex_str
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def from_extension(ext:ExtensionBase) -> Self:
|
|
22
|
+
return TREX_Extension.create(name=ext.name,
|
|
23
|
+
type=ext.type,
|
|
24
|
+
data=ext.data)
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def create(*, name, data, type='TREX'):
|
|
28
|
+
trex_extension = TREX_Extension(name= name, trex = TREX.deserialize(data))
|
|
29
|
+
return trex_extension
|
|
30
|
+
|
|
31
|
+
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class GS1ApplicationIdentifier(Enum):
|
|
4
|
+
SSCC = "00"
|
|
5
|
+
GTIN = "01"
|
|
6
|
+
CONTENT = "02"
|
|
7
|
+
MTO_GTIN = "03"
|
|
8
|
+
BATCH_LOT = "10"
|
|
9
|
+
PROD_DATE = "11"
|
|
10
|
+
DUE_DATE = "12"
|
|
11
|
+
PACK_DATE = "13"
|
|
12
|
+
BEST_BEFORE_OR_BEST_BY = "15"
|
|
13
|
+
SELL_BY = "16"
|
|
14
|
+
USE_BY_OR_EXPIRY = "17"
|
|
15
|
+
VARIANT = "20"
|
|
16
|
+
SERIAL = "21"
|
|
17
|
+
CPV = "22"
|
|
18
|
+
VAR_COUNT = "30"
|
|
19
|
+
COUNT = "37"
|
|
20
|
+
INTERNAL = "90"
|
|
21
|
+
TPX = "235"
|
|
22
|
+
ADDITIONAL_ID = "240"
|
|
23
|
+
CUST_PART_NO = "241"
|
|
24
|
+
MTO_VARIANT = "242"
|
|
25
|
+
PCN = "243"
|
|
26
|
+
SECONDARY_SERIAL = "250"
|
|
27
|
+
REF_TO_SOURCE = "251"
|
|
28
|
+
GDTI = "253"
|
|
29
|
+
GLN_EXTENSION_COMPONENT = "254"
|
|
30
|
+
GCN = "255"
|
|
31
|
+
ORDER_NUMBER = "400"
|
|
32
|
+
GINC = "401"
|
|
33
|
+
GSIN = "402"
|
|
34
|
+
ROUTE = "403"
|
|
35
|
+
SHIP_TO_LOC = "410"
|
|
36
|
+
BILL_TO = "411"
|
|
37
|
+
PURCHASE_FROM = "412"
|
|
38
|
+
SHIP_FOR_LOC = "413"
|
|
39
|
+
LOC_NO = "414"
|
|
40
|
+
PAY_TO = "415"
|
|
41
|
+
PROD_SERV_LOC = "416"
|
|
42
|
+
PARTY = "417"
|
|
43
|
+
SHIP_TO_POST = "420"
|
|
44
|
+
ORIGIN = "422"
|
|
45
|
+
COUNTRY___INITIAL_PROCESS = "423"
|
|
46
|
+
COUNTRY___PROCESS = "424"
|
|
47
|
+
COUNTRY___DISASSEMBLY = "425"
|
|
48
|
+
COUNTRY___FULL_PROCESS = "426"
|
|
49
|
+
ORIGIN_SUBDIVISION = "427"
|
|
50
|
+
NHRN_PZN = "710"
|
|
51
|
+
NHRN_CIP = "711"
|
|
52
|
+
NHRN_CN = "712"
|
|
53
|
+
NHRN_DRN = "713"
|
|
54
|
+
NHRN_AIM = "714"
|
|
55
|
+
NHRN_NDC = "715"
|
|
56
|
+
NHRN_AIC = "716"
|
|
57
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WellKnownKeys(Enum):
|
|
5
|
+
GTIN = '01'
|
|
6
|
+
BATCH = '10'
|
|
7
|
+
SERIAL = '21'
|
|
8
|
+
ADDITIONAL_IDINTIFIER = '240'
|
|
9
|
+
RUN_ID_ABSOLUTE = 'RNR'
|
|
10
|
+
SAMPLE_ID = 'SMP'
|
|
11
|
+
EXPERIMENT_ID = 'EXP'
|
|
12
|
+
RESULT_ID = 'RST'
|
|
13
|
+
METHOD_ID = 'MTD'
|
|
14
|
+
REPORT_ID = 'RPT'
|
|
15
|
+
TIMESTAMP = 'TS'
|
|
16
|
+
VERSION = 'V'
|