labfreed 0.0.5__py3-none-any.whl → 0.2.0b0__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.
- labfreed/PAC_CAT/__init__.py +16 -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/__init__.py +4 -1
- labfreed/labfreed_infrastructure.py +276 -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.0b0.dist-info/METADATA +329 -0
- labfreed-0.2.0b0.dist-info/RECORD +44 -0
- {labfreed-0.0.5.dist-info → labfreed-0.2.0b0.dist-info}/WHEEL +1 -1
- labfreed/DisplayNameExtension/DisplayNameExtension.py +0 -34
- labfreed/PAC_CAT/data_model.py +0 -109
- 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.0b0.dist-info}/licenses/LICENSE +0 -0
labfreed/PAC_CAT/__init__.py
CHANGED
|
@@ -1 +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
|
|
1
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
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
## Materials
|
|
2
|
+
from abc import ABC
|
|
3
|
+
from pydantic import Field, computed_field, model_validator
|
|
4
|
+
|
|
5
|
+
from labfreed.labfreed_infrastructure import ValidationMsgLevel
|
|
6
|
+
from labfreed.pac_cat.category_base import Category
|
|
7
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
8
|
+
|
|
9
|
+
class PredefinedCategory(Category, ABC):
|
|
10
|
+
'''@private
|
|
11
|
+
Base for Predefined catergories
|
|
12
|
+
'''
|
|
13
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
14
|
+
''' Category segments, which are not defined in the specification'''
|
|
15
|
+
|
|
16
|
+
@computed_field
|
|
17
|
+
@property
|
|
18
|
+
def segments(self) -> list[IDSegment]:
|
|
19
|
+
return self._get_segments(use_short_notation=False)
|
|
20
|
+
|
|
21
|
+
def _get_segments(self, use_short_notation=False) -> list[IDSegment]:
|
|
22
|
+
segments = []
|
|
23
|
+
can_omit_keys = use_short_notation # keeps track of whether keys can still be omitted. That is the case when the segment recommendation is followed
|
|
24
|
+
for field_name, field_info in self.model_fields.items():
|
|
25
|
+
if field_name in ['key', 'additional_segments']:
|
|
26
|
+
continue
|
|
27
|
+
if value := getattr(self, field_name):
|
|
28
|
+
if can_omit_keys:
|
|
29
|
+
key = None
|
|
30
|
+
else:
|
|
31
|
+
key = field_info.alias
|
|
32
|
+
segments.append(IDSegment(key= key, value= value) )
|
|
33
|
+
else:
|
|
34
|
+
can_omit_keys = False
|
|
35
|
+
if self.additional_segments:
|
|
36
|
+
segments.extend(self.additional_segments)
|
|
37
|
+
return segments
|
|
38
|
+
|
|
39
|
+
model_config = {
|
|
40
|
+
"populate_by_name": True
|
|
41
|
+
}
|
|
42
|
+
''' @private Pydantic tweak to allows model fields to be populated using their Python name, even if they have an alias defined.
|
|
43
|
+
The alias we need to use the GS1 code in serialization
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Material_Device(PredefinedCategory):
|
|
49
|
+
'''Represents the -MD category'''
|
|
50
|
+
key: str = Field(default='-MD', frozen=True)
|
|
51
|
+
model_number: str|None = Field( alias='240')
|
|
52
|
+
serial_number: str|None = Field( alias='21')
|
|
53
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
54
|
+
''' Category segments, which are not defined in the specification'''
|
|
55
|
+
|
|
56
|
+
@model_validator(mode='after')
|
|
57
|
+
def _validate_mandatory_fields(self):
|
|
58
|
+
if not self.model_number:
|
|
59
|
+
self._add_validation_message(
|
|
60
|
+
source=f"Category {self.key}",
|
|
61
|
+
level = ValidationMsgLevel.ERROR,
|
|
62
|
+
msg=f'Category key {self.key} is missing mandatory field Model Number',
|
|
63
|
+
highlight_pattern = f"{self.key}"
|
|
64
|
+
)
|
|
65
|
+
if not self.serial_number:
|
|
66
|
+
self._add_validation_message(
|
|
67
|
+
source=f"Category {self.key}",
|
|
68
|
+
level = ValidationMsgLevel.ERROR,
|
|
69
|
+
msg=f'Category key {self.key} is missing mandatory field Serial Number',
|
|
70
|
+
highlight_pattern = f"{self.key}"
|
|
71
|
+
)
|
|
72
|
+
return self
|
|
73
|
+
|
|
74
|
+
class Material_Substance(PredefinedCategory):
|
|
75
|
+
'''Represents the -MS category'''
|
|
76
|
+
key: str = Field(default='-MS', frozen=True)
|
|
77
|
+
product_number:str|None = Field( alias='240')
|
|
78
|
+
batch_number:str|None = Field(default=None, alias='10')
|
|
79
|
+
container_size:str|None = Field(default=None, alias='20')
|
|
80
|
+
container_number:str|None = Field(default=None, alias='21')
|
|
81
|
+
aliquot:str|None = Field(default=None, alias='250')
|
|
82
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
83
|
+
''' Category segments, which are not defined in the specification'''
|
|
84
|
+
|
|
85
|
+
@model_validator(mode='after')
|
|
86
|
+
def _validate_mandatory_fields(self):
|
|
87
|
+
if not self.product_number:
|
|
88
|
+
self._add_validation_message(
|
|
89
|
+
source=f"Category {self.key}",
|
|
90
|
+
level = ValidationMsgLevel.ERROR,
|
|
91
|
+
msg=f'Category key {self.key} is missing mandatory field Product Number',
|
|
92
|
+
highlight_pattern = f"{self.key}"
|
|
93
|
+
)
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
class Material_Consumable(PredefinedCategory):
|
|
97
|
+
'''Represents the -MC category'''
|
|
98
|
+
key: str = Field(default='-MC', frozen=True)
|
|
99
|
+
product_number:str|None = Field( alias='240')
|
|
100
|
+
batch_number:str|None = Field(default=None, alias='10')
|
|
101
|
+
packaging_size:str|None = Field(default=None, alias='20')
|
|
102
|
+
serial_number:str|None = Field(default=None, alias='21')
|
|
103
|
+
aliquot:str|None = Field(default=None, alias='250')
|
|
104
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
105
|
+
''' Category segments, which are not defined in the specification'''
|
|
106
|
+
|
|
107
|
+
@model_validator(mode='after')
|
|
108
|
+
def _validate_mandatory_fields(self):
|
|
109
|
+
if not self.product_number:
|
|
110
|
+
self._add_validation_message(
|
|
111
|
+
source=f"Category {self.key}",
|
|
112
|
+
level = ValidationMsgLevel.ERROR,
|
|
113
|
+
msg=f"Category key {self.key} is missing mandatory field 'Product Number'",
|
|
114
|
+
highlight_pattern = f"{self.key}"
|
|
115
|
+
)
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
class Material_Misc(Material_Consumable):
|
|
119
|
+
'''Represents the -MC category'''
|
|
120
|
+
# same fields as Consumable
|
|
121
|
+
key: str = Field(default='-MM', frozen=True)
|
|
122
|
+
product_number:str|None = Field( alias='240')
|
|
123
|
+
batch_number:str|None = Field(default=None, alias='10')
|
|
124
|
+
packaging_size:str|None = Field(default=None, alias='20')
|
|
125
|
+
serial_number:str|None = Field(default=None, alias='21')
|
|
126
|
+
aliquot:str|None = Field(default=None, alias='250')
|
|
127
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
128
|
+
''' Category segments, which are not defined in the specification'''
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
## Data
|
|
133
|
+
class Data_Abstract(PredefinedCategory, ABC):
|
|
134
|
+
'''@private'''
|
|
135
|
+
key: str
|
|
136
|
+
id:str|None = Field( alias='21')
|
|
137
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
138
|
+
''' Category segments, which are not defined in the specification'''
|
|
139
|
+
|
|
140
|
+
@model_validator(mode='after')
|
|
141
|
+
def _validate_mandatory_fields(self):
|
|
142
|
+
if not self.id:
|
|
143
|
+
self._add_validation_message(
|
|
144
|
+
source=f"Category {self.key}",
|
|
145
|
+
level = ValidationMsgLevel.ERROR,
|
|
146
|
+
msg=f"Category key {self.key} is missing mandatory field 'ID'",
|
|
147
|
+
highlight_pattern = f"{self.key}"
|
|
148
|
+
)
|
|
149
|
+
return self
|
|
150
|
+
|
|
151
|
+
class Data_Result(Data_Abstract):
|
|
152
|
+
'''Represents the -DR category'''
|
|
153
|
+
key: str = Field(default='-DR', frozen=True)
|
|
154
|
+
id:str|None = Field( alias='21')
|
|
155
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
156
|
+
''' Category segments, which are not defined in the specification'''
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Data_Method(Data_Abstract):
|
|
160
|
+
'''Represents the -DM category'''
|
|
161
|
+
key: str = Field(default='-DM', frozen=True)
|
|
162
|
+
id:str|None = Field( alias='21')
|
|
163
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
164
|
+
''' Category segments, which are not defined in the specification'''
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class Data_Calibration(Data_Abstract):
|
|
168
|
+
'''Represents the -DC category'''
|
|
169
|
+
key: str = Field(default='-DC', frozen=True)
|
|
170
|
+
id:str|None = Field( alias='21')
|
|
171
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
172
|
+
''' Category segments, which are not defined in the specification'''
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class Data_Progress(Data_Abstract):
|
|
176
|
+
'''Represents the -DP category'''
|
|
177
|
+
key: str = Field(default='-DP', frozen=True)
|
|
178
|
+
id:str|None = Field( alias='21')
|
|
179
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
180
|
+
''' Category segments, which are not defined in the specification'''
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class Data_Static(Data_Abstract):
|
|
184
|
+
'''Represents the -DS category'''
|
|
185
|
+
key: str = Field(default='-DS', frozen=True)
|
|
186
|
+
id:str|None = Field( alias='21')
|
|
187
|
+
additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
|
|
188
|
+
''' Category segments, which are not defined in the specification'''
|
|
189
|
+
|
|
190
|
+
|
labfreed/PAC_ID/__init__.py
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .pac_id import PAC_ID
|
|
2
|
+
from .id_segment import IDSegment
|
|
3
|
+
from .extension import Extension
|
|
4
|
+
|
|
5
|
+
'''@private
|
|
6
|
+
From a SW engineering perspective it would be best to have no dependencies from other modules to pac_id.
|
|
7
|
+
However from a Python users convenience perspective it is better to have one place where a pac url can be parsed and magically the extensions are in a meaningful type (e.g. TREX in TREX aware format) and categories are known of possible.
|
|
8
|
+
|
|
9
|
+
>> We have given priority to convenient usage and therefore chose to have dependencies from pac_id to pac_cat and well_known_extensions
|
|
10
|
+
'''
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PAC_ID",
|
|
15
|
+
"IDSegment",
|
|
16
|
+
"Extension"
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
from abc import ABC, abstractproperty
|
|
3
|
+
|
|
4
|
+
from pydantic import model_validator
|
|
5
|
+
|
|
6
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExtensionBase(ABC):
|
|
10
|
+
name: str
|
|
11
|
+
type: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@abstractproperty
|
|
15
|
+
def data(self) -> str:
|
|
16
|
+
raise NotImplementedError("Subclasses must implement 'data'")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Extension(LabFREED_BaseModel,ExtensionBase):
|
|
22
|
+
'''Implementation of Extension for unknown extension types'''
|
|
23
|
+
name:str
|
|
24
|
+
type:str
|
|
25
|
+
data_:str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def data(self) -> str:
|
|
29
|
+
return self.data_
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def create(*, name, type, data):
|
|
33
|
+
return Extension(name=name, type=type, data=data)
|
|
34
|
+
|
|
35
|
+
@model_validator(mode='before')
|
|
36
|
+
@classmethod
|
|
37
|
+
def move_data_field(cls, values):
|
|
38
|
+
if "data" in values:
|
|
39
|
+
values["data_"] = values.pop("data")
|
|
40
|
+
return values
|
|
41
|
+
|
|
42
|
+
model_config = {
|
|
43
|
+
"extra": "allow", # Allow extra keys during pre-validation
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __str__(self):
|
|
47
|
+
return f'{self.name}${self.type}/{self.data}'
|
|
48
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pydantic import model_validator
|
|
3
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
4
|
+
from labfreed.well_known_keys.labfreed.well_known_keys import WellKnownKeys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
|
|
8
|
+
|
|
9
|
+
class IDSegment(LabFREED_BaseModel):
|
|
10
|
+
""" Represents an id segment of a PAC-ID. It can be a value or a key value pair.
|
|
11
|
+
"""
|
|
12
|
+
key:str|None = None
|
|
13
|
+
''' The key of the segment. This is optional.'''
|
|
14
|
+
value:str
|
|
15
|
+
''' The value of the segment. (mandatory)'''
|
|
16
|
+
|
|
17
|
+
@model_validator(mode="after")
|
|
18
|
+
def _validate_segment(self):
|
|
19
|
+
key = self.key or ""
|
|
20
|
+
value = self.value
|
|
21
|
+
|
|
22
|
+
# MUST be a valid hsegment according to RFC 1738, but without * (see PAC-ID Extension)
|
|
23
|
+
# This means it must be true for both, key and value
|
|
24
|
+
if not_allowed_chars := set(re.sub(_hsegment_pattern, '', key)):
|
|
25
|
+
self._add_validation_message(
|
|
26
|
+
source=f"id segment key {key}",
|
|
27
|
+
level = ValidationMsgLevel.ERROR,
|
|
28
|
+
msg=f"{_quote_texts(not_allowed_chars)} must not be used. The segment key must be a valid hsegment",
|
|
29
|
+
highlight_pattern = key,
|
|
30
|
+
highlight_sub = not_allowed_chars
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if not_allowed_chars := set(re.sub(_hsegment_pattern, '', value)):
|
|
34
|
+
self._add_validation_message(
|
|
35
|
+
source=f"id segment key {value}",
|
|
36
|
+
level = ValidationMsgLevel.ERROR,
|
|
37
|
+
msg=f"{_quote_texts(not_allowed_chars)} must not be used. The segment key must be a valid hsegment",
|
|
38
|
+
highlight_pattern = value,
|
|
39
|
+
highlight_sub = not_allowed_chars
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Segment key SHOULD be limited to A-Z, 0-9, and -+..
|
|
43
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', key)):
|
|
44
|
+
self._add_validation_message(
|
|
45
|
+
source=f"id segment key {key}",
|
|
46
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
47
|
+
msg=f"{_quote_texts(not_recommended_chars)} should not be used. Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' ",
|
|
48
|
+
highlight_pattern = key,
|
|
49
|
+
highlight_sub = not_recommended_chars
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Segment key should be in Well know keys
|
|
53
|
+
if key and key not in [k.value for k in WellKnownKeys]:
|
|
54
|
+
self._add_validation_message(
|
|
55
|
+
source=f"id segment key {key}",
|
|
56
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
57
|
+
msg=f"{key} is not a well known segment key. It is RECOMMENDED to use well-known keys.",
|
|
58
|
+
highlight_pattern = key,
|
|
59
|
+
highlight_sub=[key]
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# Segment value SHOULD be limited to A-Z, 0-9, and -+..
|
|
64
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', value)):
|
|
65
|
+
self._add_validation_message(
|
|
66
|
+
source=f"id segment value {value}",
|
|
67
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
68
|
+
msg=f"Characters {_quote_texts(not_recommended_chars)} should not be used., Characters SHOULD be limited to upper case letters (A-Z), numbers (0-9), '-' and '+' ",
|
|
69
|
+
highlight_pattern = value,
|
|
70
|
+
highlight_sub = not_recommended_chars
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Segment value SHOULD be limited to A-Z, 0-9, and :-+ for new designs.
|
|
74
|
+
# this means that ":" in key or value is problematic
|
|
75
|
+
if ':' in key:
|
|
76
|
+
self._add_validation_message(
|
|
77
|
+
source=f"id segment key {key}",
|
|
78
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
79
|
+
msg="Character ':' should not be used in segment key, since this character is used to separate key and value this can lead to undefined behaviour.",
|
|
80
|
+
highlight_pattern = key
|
|
81
|
+
)
|
|
82
|
+
if ':' in value:
|
|
83
|
+
self._add_validation_message(
|
|
84
|
+
source=f"id segment value {value}",
|
|
85
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
86
|
+
msg="Character ':' should not be used in segment value, since this character is used to separate key and value this can lead to undefined behaviour.",
|
|
87
|
+
highlight_pattern = value
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return self
|