labfreed 0.0.3__tar.gz → 0.0.5__tar.gz
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-0.0.3 → labfreed-0.0.5}/PKG-INFO +22 -3
- labfreed-0.0.5/README.md +21 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/PAC_CAT/data_model.py +5 -5
- labfreed-0.0.5/labfreed/PAC_ID/data_model.py +215 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/PAC_ID/parse.py +14 -5
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/PAC_ID/serialize.py +5 -2
- labfreed-0.0.5/labfreed/PAC_ID/well_known_segment_keys.py +16 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/TREXExtension/unit_utilities.py +13 -4
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/__init__.py +1 -1
- labfreed-0.0.5/labfreed/validation.py +71 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/pyproject.toml +2 -2
- {labfreed-0.0.3 → labfreed-0.0.5}/tests/test_PAC_ID/test_pac_id_parse.py +48 -42
- {labfreed-0.0.3 → labfreed-0.0.5}/tests/test_PAC_ID/test_pac_id_serialization.py +17 -1
- labfreed-0.0.3/README.md +0 -3
- labfreed-0.0.3/labfreed/PAC_ID/data_model.py +0 -114
- {labfreed-0.0.3 → labfreed-0.0.5}/.vscode/launch.json +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/.vscode/settings.json +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/LICENSE +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/DisplayNameExtension/DisplayNameExtension.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/DisplayNameExtension/base36.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/PAC_CAT/__init__.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/PAC_ID/__init__.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/TREXExtension/data_model.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/TREXExtension/parse.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/labfreed/TREXExtension/uncertainty.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/main.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/pytest.ini +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/tests/test_PAC_CAT/test_PAC_CAT.py +0 -0
- {labfreed-0.0.3 → labfreed-0.0.5}/tests/test_TREXExtension/test_TREX.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labfreed
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Python implementation of LabFREED building blocks
|
|
5
|
-
Author-email: Reto Thürer <
|
|
6
|
-
Requires-Python: >=3.
|
|
5
|
+
Author-email: Reto Thürer <thuerer.r@buchi.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-Expression: MIT
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -13,3 +13,22 @@ License-File: LICENSE
|
|
|
13
13
|
# LabFREED for Python
|
|
14
14
|
|
|
15
15
|
A python implementation of [LabFREED](www.labfreed.com).
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# Examples:
|
|
20
|
+
|
|
21
|
+
## Parse a PAC ID
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Parse a PAC-ID with extensions
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
## Create a PAC-ID
|
|
29
|
+
|
|
30
|
+
## Create a PAC-ID and T-REX for a titration curve
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
labfreed-0.0.5/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# LabFREED for Python
|
|
2
|
+
|
|
3
|
+
A python implementation of [LabFREED](www.labfreed.com).
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# Examples:
|
|
8
|
+
|
|
9
|
+
## Parse a PAC ID
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## Parse a PAC-ID with extensions
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Create a PAC-ID
|
|
17
|
+
|
|
18
|
+
## Create a PAC-ID and T-REX for a titration curve
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
@@ -65,23 +65,23 @@ class Material_Misc(Material_Consumable):
|
|
|
65
65
|
|
|
66
66
|
class Data_Result(CATBase):
|
|
67
67
|
category_key: str = Field(default='-DR', frozen=True)
|
|
68
|
-
id:str = Field( alias='
|
|
68
|
+
id:str = Field( alias='21', min_length=1)
|
|
69
69
|
|
|
70
70
|
class Data_Method(CATBase):
|
|
71
71
|
category_key: str = Field(default='-DM', frozen=True)
|
|
72
|
-
id:str = Field( alias='
|
|
72
|
+
id:str = Field( alias='21', min_length=1)
|
|
73
73
|
|
|
74
74
|
class Data_Calibration(CATBase):
|
|
75
75
|
category_key: str = Field(default='-DC', frozen=True)
|
|
76
|
-
id:str = Field( alias='
|
|
76
|
+
id:str = Field( alias='21', min_length=1)
|
|
77
77
|
|
|
78
78
|
class Data_Progress(CATBase):
|
|
79
79
|
category_key: str = Field(default='-DP', frozen=True)
|
|
80
|
-
id:str = Field( alias='
|
|
80
|
+
id:str = Field( alias='21', min_length=1)
|
|
81
81
|
|
|
82
82
|
class Data_Static(CATBase):
|
|
83
83
|
category_key: str = Field(default='-DS', frozen=True)
|
|
84
|
-
id:str = Field( alias='
|
|
84
|
+
id:str = Field( alias='21', min_length=1)
|
|
85
85
|
|
|
86
86
|
|
|
87
87
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from typing_extensions import Self
|
|
4
|
+
from pydantic import Field, ValidationInfo, computed_field, conlist, model_validator, field_validator
|
|
5
|
+
from ..validation import BaseModelWithWarnings, ValidationWarning, hsegment_pattern, domain_name_pattern
|
|
6
|
+
from abc import ABC, abstractproperty, abstractstaticmethod
|
|
7
|
+
from .well_known_segment_keys import WellKnownSegmentKeys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IDSegment(BaseModelWithWarnings):
|
|
11
|
+
key:str|None = None
|
|
12
|
+
value:str
|
|
13
|
+
@model_validator(mode="after")
|
|
14
|
+
def validate_segment(cls, model):
|
|
15
|
+
key = model.key or ""
|
|
16
|
+
value = model.value
|
|
17
|
+
|
|
18
|
+
# MUST be a valid hsegment according to RFC 1738, but without * (see PAC-ID Extension)
|
|
19
|
+
# This means it must be true for both, key and value
|
|
20
|
+
if not_allowed_chars := set(re.sub(hsegment_pattern, '', key)):
|
|
21
|
+
raise ValueError(f"id segment key {key} contains invalid characters {' '.join(not_allowed_chars)}.")
|
|
22
|
+
|
|
23
|
+
if not_allowed_chars := set(re.sub(hsegment_pattern, '', value)):
|
|
24
|
+
raise ValueError(f"id segment key {value} contains invalid characters {' '.join(not_allowed_chars)}.")
|
|
25
|
+
|
|
26
|
+
# Segment key SHOULD be limited to A-Z, 0-9, and -+..
|
|
27
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', key)):
|
|
28
|
+
model.add_warning(
|
|
29
|
+
source=f"id segment key {key}",
|
|
30
|
+
type="Recommendation",
|
|
31
|
+
msg=f"{' '.join(not_recommended_chars)} should not be used.",
|
|
32
|
+
recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
|
|
33
|
+
highlight_pattern = key,
|
|
34
|
+
highlight_sub = not_recommended_chars
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# Segment key should be in Well know keys
|
|
38
|
+
if key and key not in [k.value for k in WellKnownSegmentKeys]:
|
|
39
|
+
model.add_warning(
|
|
40
|
+
source=f"id segment key {key}",
|
|
41
|
+
type="Recommendation",
|
|
42
|
+
msg=f"{key} is not a well known segment key.",
|
|
43
|
+
recommendation = "RECOMMENDED to be a well-known id segment key.",
|
|
44
|
+
highlight_pattern = key
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# Segment value SHOULD be limited to A-Z, 0-9, and -+..
|
|
49
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9-:+]', '', value)):
|
|
50
|
+
model.add_warning(
|
|
51
|
+
source=f"id segment value {value}",
|
|
52
|
+
type="Recommendation",
|
|
53
|
+
msg=f"Characters {' '.join(not_recommended_chars)} should not be used.",
|
|
54
|
+
recommendation = "SHOULD be limited to A-Z, 0-9, and -+",
|
|
55
|
+
highlight_pattern = value,
|
|
56
|
+
highlight_sub = not_recommended_chars
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Segment value SHOULD be limited to A-Z, 0-9, and :-+ for new designs.
|
|
60
|
+
# this means that ":" in key or value is problematic
|
|
61
|
+
if ':' in key:
|
|
62
|
+
model.add_warning(
|
|
63
|
+
source=f"id segment key {key}",
|
|
64
|
+
type="Recommendation",
|
|
65
|
+
msg=f"Character ':' should not be used in segment key, since this character is used to separate key and value this can lead to undefined behaviour.",
|
|
66
|
+
highlight_pattern = key
|
|
67
|
+
)
|
|
68
|
+
if ':' in value:
|
|
69
|
+
model.add_warning(
|
|
70
|
+
source=f"id segment value {value}",
|
|
71
|
+
type="Recommendation",
|
|
72
|
+
msg=f"Character ':' should not be used in segment value, since this character is used to separate key and value this can lead to undefined behaviour.",
|
|
73
|
+
highlight_pattern = value
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return model
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class Category(BaseModelWithWarnings):
|
|
82
|
+
key:str|None = None
|
|
83
|
+
segments: list[IDSegment]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Identifier(BaseModelWithWarnings):
|
|
87
|
+
segments: conlist(IDSegment, min_length=1) = Field(..., exclude=True) # type: ignore # exclude=True prevents this from being serialized by Pydantic
|
|
88
|
+
|
|
89
|
+
@computed_field
|
|
90
|
+
@property
|
|
91
|
+
def categories(self) -> list[Category]:
|
|
92
|
+
categories = list()
|
|
93
|
+
c = Category(segments=[])
|
|
94
|
+
categories.append(c)
|
|
95
|
+
for s in self.segments:
|
|
96
|
+
# new category starts with "-"
|
|
97
|
+
if s.value[0] == '-':
|
|
98
|
+
cat_key = s.value
|
|
99
|
+
c = Category(key=cat_key, segments=[])
|
|
100
|
+
categories.append(c)
|
|
101
|
+
else:
|
|
102
|
+
c.segments.append(s)
|
|
103
|
+
|
|
104
|
+
# the first category might have no segments. remove categories without segments
|
|
105
|
+
if not categories[0].segments:
|
|
106
|
+
categories = categories[1:]
|
|
107
|
+
|
|
108
|
+
return categories
|
|
109
|
+
|
|
110
|
+
@model_validator(mode='after')
|
|
111
|
+
def check_keys_are_unique_in_each_category(self) -> Self:
|
|
112
|
+
for c in self.categories:
|
|
113
|
+
keys = [s.key for s in c.segments if s.key]
|
|
114
|
+
duplicate_keys = [k for k in set(keys) if keys.count(k) > 1]
|
|
115
|
+
if duplicate_keys:
|
|
116
|
+
raise ValueError(f'Duplicate keys {",".join(duplicate_keys)} in category {c.key}')
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
@model_validator(mode='after')
|
|
120
|
+
def check_length(self) -> Self:
|
|
121
|
+
l = 0
|
|
122
|
+
for s in self.segments:
|
|
123
|
+
if s.key:
|
|
124
|
+
l += len(s.key)
|
|
125
|
+
l += 1 # for ":"
|
|
126
|
+
l += len(s.value)
|
|
127
|
+
l += len(self.segments) - 1 # account for "/" separating the segments
|
|
128
|
+
|
|
129
|
+
if l > 256:
|
|
130
|
+
raise ValueError(f'Identifier is {l} characters long, Identifier must not exceed 256 characters.')
|
|
131
|
+
return self
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def from_categories(categories:list[Category]) :
|
|
135
|
+
segments = list()
|
|
136
|
+
for c in categories:
|
|
137
|
+
if c.key:
|
|
138
|
+
segments.append(IDSegment(value=c.key))
|
|
139
|
+
segments.extend(c.segments)
|
|
140
|
+
return Identifier(segments=segments)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class Extension(ABC, BaseModelWithWarnings):
|
|
145
|
+
|
|
146
|
+
@abstractproperty
|
|
147
|
+
def name(self)->str:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
@abstractproperty
|
|
151
|
+
def type(self)->str:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
@abstractproperty
|
|
155
|
+
def data(self)->str:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@abstractstaticmethod
|
|
159
|
+
def from_spec_fields(name, type, data):
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class UnknownExtension(Extension):
|
|
164
|
+
name_:str
|
|
165
|
+
type_:str
|
|
166
|
+
data_:str
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def name(self)->str:
|
|
170
|
+
return self.name_
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def type(self)->str:
|
|
174
|
+
return self.type_
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def data(self)->str:
|
|
178
|
+
return self.data_
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def from_spec_fields(name, type, data):
|
|
182
|
+
return UnknownExtension(name_=name, type_=type, data_=data)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class PACID(BaseModelWithWarnings):
|
|
187
|
+
issuer:str
|
|
188
|
+
identifier: Identifier
|
|
189
|
+
|
|
190
|
+
@model_validator(mode="after")
|
|
191
|
+
def validate_issuer(cls, model):
|
|
192
|
+
if not re.fullmatch(domain_name_pattern, model.issuer):
|
|
193
|
+
raise ValueError("Issuer must be a valid domain name.")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# recommendation that A-Z, 0-9, -, and . should be used
|
|
197
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9\.-]', '', model.issuer)):
|
|
198
|
+
model.add_warning(
|
|
199
|
+
source="PAC-ID",
|
|
200
|
+
type="Recommendation",
|
|
201
|
+
highlight_pattern=model.issuer,
|
|
202
|
+
highlight_sub=not_recommended_chars,
|
|
203
|
+
msg=f"Characters {' '.join(not_recommended_chars)} should not be used. Issuer SHOULD contain only the characters A-Z, 0-9, -, and . "
|
|
204
|
+
)
|
|
205
|
+
return model
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PACID_With_Extensions(BaseModelWithWarnings):
|
|
209
|
+
pac_id: PACID
|
|
210
|
+
extensions: list[Extension] = Field(default_factory=list)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
@@ -4,6 +4,8 @@ import re
|
|
|
4
4
|
from types import MappingProxyType
|
|
5
5
|
from .data_model import *
|
|
6
6
|
|
|
7
|
+
from ..validation import extract_warnings, ValidationWarning
|
|
8
|
+
|
|
7
9
|
|
|
8
10
|
category_conventions = MappingProxyType(
|
|
9
11
|
{
|
|
@@ -29,7 +31,7 @@ class PAC_Parser():
|
|
|
29
31
|
def __init__(self, extension_interpreters:dict[str, Extension]=None):
|
|
30
32
|
self.extension_interpreters = extension_interpreters or {}
|
|
31
33
|
|
|
32
|
-
def parse_pac_url(self, pac_url:str) -> PACID_With_Extensions:
|
|
34
|
+
def parse_pac_url(self, pac_url:str) -> tuple[PACID_With_Extensions, list[ValidationWarning] ]:
|
|
33
35
|
if '*' in pac_url:
|
|
34
36
|
id_str, ext_str = pac_url.split('*', 1)
|
|
35
37
|
else:
|
|
@@ -37,8 +39,12 @@ class PAC_Parser():
|
|
|
37
39
|
ext_str = ""
|
|
38
40
|
|
|
39
41
|
pac_id = self.parse_pac_id(id_str)
|
|
40
|
-
extensions = self.parse_extensions(ext_str)
|
|
41
|
-
|
|
42
|
+
extensions = self.parse_extensions(ext_str)
|
|
43
|
+
|
|
44
|
+
pac_with_extension = PACID_With_Extensions(pac_id=pac_id, extensions=extensions)
|
|
45
|
+
warnings = extract_warnings(pac_with_extension)
|
|
46
|
+
|
|
47
|
+
return pac_with_extension, warnings
|
|
42
48
|
|
|
43
49
|
|
|
44
50
|
def parse_id_segments(self, identifier:str):
|
|
@@ -46,6 +52,8 @@ class PAC_Parser():
|
|
|
46
52
|
return []
|
|
47
53
|
|
|
48
54
|
id_segments = list()
|
|
55
|
+
if len(identifier) > 0 and identifier[0] == '/':
|
|
56
|
+
identifier = identifier[1:]
|
|
49
57
|
for s in identifier.split('/'):
|
|
50
58
|
tmp = s.split(':')
|
|
51
59
|
|
|
@@ -85,10 +93,11 @@ class PAC_Parser():
|
|
|
85
93
|
default_keys = None
|
|
86
94
|
id_segments = self.parse_id_segments(d.get('identifier'))
|
|
87
95
|
id_segments = self._apply_category_defaults(id_segments)
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
|
|
97
|
+
pac = PACID(issuer= d.get('issuer'),
|
|
90
98
|
identifier=Identifier(segments=id_segments)
|
|
91
99
|
)
|
|
100
|
+
return pac
|
|
92
101
|
|
|
93
102
|
|
|
94
103
|
def parse_extensions(self, extensions_str:str|None) -> list[Extension]:
|
|
@@ -4,7 +4,7 @@ from .data_model import *
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
class PAC_Serializer():
|
|
7
|
-
def to_url(self, pac:PACID|PACID_With_Extensions, extensions:list[Extension]=None, use_short_notation_for_extensions=False) -> str:
|
|
7
|
+
def to_url(self, pac:PACID|PACID_With_Extensions, extensions:list[Extension]=None, use_short_notation_for_extensions=False, uppercase_only=False) -> str:
|
|
8
8
|
if isinstance(pac, PACID_With_Extensions):
|
|
9
9
|
if extensions:
|
|
10
10
|
raise ValueError('Extensions were given twice, as part of PACID_With_Extension and as method parameter.')
|
|
@@ -13,7 +13,10 @@ class PAC_Serializer():
|
|
|
13
13
|
issuer = pac.issuer
|
|
14
14
|
extensions_str = self._serialize_extensions(extensions, use_short_notation_for_extensions)
|
|
15
15
|
id_segments = self._serialize_id_segments(pac.identifier.segments)
|
|
16
|
-
|
|
16
|
+
out = f"HTTPS://PAC.{issuer}{id_segments}{extensions_str}"
|
|
17
|
+
if uppercase_only:
|
|
18
|
+
out = out.upper()
|
|
19
|
+
return out
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
def _serialize_id_segments(self, segments):
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WellKnownSegmentKeys(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'
|
|
@@ -60,26 +60,35 @@ class PydanticUncertainQuantity(BaseModel):
|
|
|
60
60
|
|
|
61
61
|
unit_map = [
|
|
62
62
|
('MGM', units.milligram),
|
|
63
|
+
('GRM', units.gram),
|
|
64
|
+
('KGM', units.kilogram),
|
|
65
|
+
|
|
63
66
|
('CEL', units.celsius),
|
|
67
|
+
|
|
64
68
|
('LTR', units.liter),
|
|
65
69
|
('MLT', units.milliliter),
|
|
66
|
-
|
|
67
|
-
('KGM', units.kilogram),
|
|
70
|
+
|
|
68
71
|
('C34', units.mole),
|
|
69
72
|
('D43',units.atomic_mass_unit),
|
|
73
|
+
|
|
70
74
|
('1', units.dimensionless),
|
|
71
75
|
('C62', units.dimensionless),
|
|
76
|
+
|
|
72
77
|
('BAR',units.bar),
|
|
73
78
|
('MBR',units.millibar),
|
|
74
79
|
('KBA',units.kilobar),
|
|
80
|
+
|
|
75
81
|
('RPM', units.rpm),
|
|
76
|
-
|
|
82
|
+
|
|
77
83
|
('HTZ', units.hertz),
|
|
78
84
|
('KHZ', units.kilohertz),
|
|
79
85
|
('MHZ',units.megahertz),
|
|
86
|
+
|
|
80
87
|
('SEC', units.second),
|
|
81
88
|
('MIN', units.minute),
|
|
82
|
-
('
|
|
89
|
+
('HUR', units.hour),
|
|
90
|
+
|
|
91
|
+
('MTR', units.meter)
|
|
83
92
|
|
|
84
93
|
]
|
|
85
94
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field, PrivateAttr
|
|
2
|
+
from typing import List, Set, Tuple
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
|
|
6
|
+
hsegment_pattern = r"[A-Za-z0-9_\-\.~!$&'()+,:;=@]|%[0-9A-Fa-f]{2}"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ValidationWarning(BaseModel):
|
|
10
|
+
source:str
|
|
11
|
+
type: str
|
|
12
|
+
problem_msg:str
|
|
13
|
+
recommendation_msg: str = ""
|
|
14
|
+
highlight:str = "" #this can be used to highlight problematic parts
|
|
15
|
+
highlight_sub:list[str] = Field(default_factory=list())
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseModelWithWarnings(BaseModel):
|
|
21
|
+
""" Extension of Pydantic BaseModel, so that validator can issue warnings.
|
|
22
|
+
The purpose of that is to allow only minimal validation but on top check for stricter recommendations"""
|
|
23
|
+
_warnings: list[ValidationWarning] = PrivateAttr(default_factory=list)
|
|
24
|
+
|
|
25
|
+
def add_warning(self, *, msg: str, type:str, recommendation:str="", source:str="", highlight_pattern="", highlight_sub=None):
|
|
26
|
+
if not highlight_sub:
|
|
27
|
+
highlight_sub = []
|
|
28
|
+
w = ValidationWarning(problem_msg=msg, recommendation_msg=recommendation, source=source, type=type, highlight=highlight_pattern, highlight_sub=highlight_sub)
|
|
29
|
+
if not w in self._warnings:
|
|
30
|
+
self._warnings.append(w)
|
|
31
|
+
|
|
32
|
+
def get_warnings(self) -> list[ValidationWarning]:
|
|
33
|
+
return self._warnings
|
|
34
|
+
|
|
35
|
+
def clear_warnings(self):
|
|
36
|
+
self._warnings.clear()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Function to extract warnings from a model and its nested models
|
|
40
|
+
def extract_warnings(model: BaseModelWithWarnings, parent_name: str = "", visited: Set[int] = None) -> List[ValidationWarning]:
|
|
41
|
+
"""
|
|
42
|
+
Recursively extract warnings from a Pydantic model and its nested fields.
|
|
43
|
+
|
|
44
|
+
:param model: The Pydantic model instance to inspect.
|
|
45
|
+
:param parent_name: The name of the parent model to track the path.
|
|
46
|
+
:return: List of tuples containing (model name, warning message).
|
|
47
|
+
"""
|
|
48
|
+
if visited is None:
|
|
49
|
+
visited = set()
|
|
50
|
+
|
|
51
|
+
model_id = id(model)
|
|
52
|
+
if model_id in visited:
|
|
53
|
+
return []
|
|
54
|
+
visited.add(model_id)
|
|
55
|
+
|
|
56
|
+
warnings_list = [(parent_name or model.__class__.__name__, model_id, warning) for warning in model.get_warnings()]
|
|
57
|
+
|
|
58
|
+
for field_name, field in model.__fields__.items():
|
|
59
|
+
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
60
|
+
value = getattr(model, field_name)
|
|
61
|
+
|
|
62
|
+
if isinstance(value, BaseModelWithWarnings):
|
|
63
|
+
warnings_list.extend(extract_warnings(value, full_path, visited))
|
|
64
|
+
elif isinstance(value, list):
|
|
65
|
+
for index, item in enumerate(value):
|
|
66
|
+
if isinstance(item, BaseModelWithWarnings):
|
|
67
|
+
list_path = f"{full_path}[{index}]"
|
|
68
|
+
warnings_list.extend(extract_warnings(item, list_path, visited))
|
|
69
|
+
|
|
70
|
+
return warnings_list
|
|
71
|
+
|
|
@@ -4,13 +4,13 @@ build-backend = "flit_core.buildapi"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "labfreed"
|
|
7
|
-
authors = [{name = "Reto Thürer", email = "
|
|
7
|
+
authors = [{name = "Reto Thürer", email = "thuerer.r@buchi.com"}]
|
|
8
8
|
license = "MIT"
|
|
9
9
|
license-files = ["LICENSE"]
|
|
10
10
|
dynamic = ["version", "description"]
|
|
11
11
|
|
|
12
12
|
readme = "README.md"
|
|
13
|
-
requires-python = ">=3.
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
14
|
classifiers = [
|
|
15
15
|
"Programming Language :: Python :: 3",
|
|
16
16
|
"Operating System :: OS Independent",
|
|
@@ -14,20 +14,20 @@ parser = PAC_Parser()
|
|
|
14
14
|
|
|
15
15
|
# Issuer
|
|
16
16
|
def test_standard_base_gives_correct_issuer():
|
|
17
|
-
pac = parser.parse_pac_url("HTTPS://PAC.METTORIUS.COM/" + valid_standard_segments)
|
|
18
|
-
assert pac.issuer == "METTORIUS.COM"
|
|
17
|
+
pac, _ = parser.parse_pac_url("HTTPS://PAC.METTORIUS.COM/" + valid_standard_segments)
|
|
18
|
+
assert pac.pac_id.issuer == "METTORIUS.COM"
|
|
19
19
|
|
|
20
20
|
def test_pac_can_be_missing_from_domain():
|
|
21
|
-
pac = parser.parse_pac_url("HTTPS://METTORIUS.COM/" + valid_standard_segments)
|
|
22
|
-
assert pac.issuer == "METTORIUS.COM"
|
|
21
|
+
pac, _ = parser.parse_pac_url("HTTPS://METTORIUS.COM/" + valid_standard_segments)
|
|
22
|
+
assert pac.pac_id.issuer == "METTORIUS.COM"
|
|
23
23
|
|
|
24
24
|
def test_pac_can_be_missing_from_domain():
|
|
25
|
-
pac = parser.parse_pac_url("METTORIUS.COM/" + valid_standard_segments)
|
|
26
|
-
assert pac.issuer == "METTORIUS.COM"
|
|
25
|
+
pac, _ = parser.parse_pac_url("METTORIUS.COM/" + valid_standard_segments)
|
|
26
|
+
assert pac.pac_id.issuer == "METTORIUS.COM"
|
|
27
27
|
|
|
28
28
|
def test_issuer_must_be_valid_domain():
|
|
29
29
|
with pytest.raises(Exception):
|
|
30
|
-
pac = parser.parse_pac_url("HTTPS://METTORIUS/" + valid_standard_segments)
|
|
30
|
+
pac, _ = parser.parse_pac_url("HTTPS://METTORIUS/" + valid_standard_segments)
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
@@ -39,19 +39,20 @@ def test_pac_must_have_at_least_one_segment():
|
|
|
39
39
|
pac = parser.parse_pac_url(valid_base = "").pac_id
|
|
40
40
|
|
|
41
41
|
def test_identifier_named_segment():
|
|
42
|
-
pac = parser.parse_pac_url(valid_base + "KEY:VAL")
|
|
43
|
-
seg: IDSegment = pac.identifier.segments[0]
|
|
42
|
+
pac, _ = parser.parse_pac_url(valid_base + "KEY:VAL")
|
|
43
|
+
seg: IDSegment = pac.pac_id.identifier.segments[0]
|
|
44
44
|
assert seg.key == "KEY"
|
|
45
45
|
assert seg.value == "VAL"
|
|
46
46
|
|
|
47
47
|
def test_identifier_unnamed_segment():
|
|
48
|
-
pac = parser.parse_pac_url(valid_base + "VAL")
|
|
49
|
-
seg: IDSegment = pac.identifier.segments[0]
|
|
48
|
+
pac, _ = parser.parse_pac_url(valid_base + "VAL")
|
|
49
|
+
seg: IDSegment = pac.pac_id.identifier.segments[0]
|
|
50
50
|
assert not seg.key
|
|
51
51
|
assert seg.value == "VAL"
|
|
52
52
|
|
|
53
53
|
def test_identifier_combination_of_named_and_unnamed_segments():
|
|
54
|
-
pac = parser.parse_pac_url(valid_base + "KEY0:VAL0/VAL1/KEY2:VAL2")
|
|
54
|
+
pac, _ = parser.parse_pac_url(valid_base + "KEY0:VAL0/VAL1/KEY2:VAL2")
|
|
55
|
+
pac = pac.pac_id
|
|
55
56
|
seg: IDSegment = pac.identifier.segments[0]
|
|
56
57
|
assert seg.key == 'KEY0'
|
|
57
58
|
assert seg.value == "VAL0"
|
|
@@ -66,10 +67,10 @@ def test_identifier_combination_of_named_and_unnamed_segments():
|
|
|
66
67
|
|
|
67
68
|
def test_keys_must_be_unique():
|
|
68
69
|
with pytest.raises(Exception):
|
|
69
|
-
pac = parser.parse_pac_url(valid_base + "KEY:VAL/KEY:ANOTHERVAL/KEY:VAL/KEY2:ANOTHERVAL")
|
|
70
|
+
pac, _ = parser.parse_pac_url(valid_base + "KEY:VAL/KEY:ANOTHERVAL/KEY:VAL/KEY2:ANOTHERVAL")
|
|
70
71
|
|
|
71
72
|
def test_keys_can_repeat_accross_categories():
|
|
72
|
-
pac = parser.parse_pac_url(valid_base + "-MD/KEY:VAL/-MS/KEY:VAL")
|
|
73
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MD/KEY:VAL/-MS/KEY:VAL")
|
|
73
74
|
assert True # made it here without exception > it's fine
|
|
74
75
|
|
|
75
76
|
|
|
@@ -77,28 +78,28 @@ def test_keys_can_repeat_accross_categories():
|
|
|
77
78
|
|
|
78
79
|
# Identifier Segments Categories (Recommendation)
|
|
79
80
|
def test_basic_valid_category():
|
|
80
|
-
pac = parser.parse_pac_url(valid_base + "-MD/KEY:VAL")
|
|
81
|
-
cat: Category = pac.identifier.categories[0]
|
|
81
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MD/KEY:VAL")
|
|
82
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
82
83
|
assert cat.key == '-MD'
|
|
83
84
|
assert cat.segments[0].key == 'KEY'
|
|
84
85
|
assert cat.segments[0].value == 'VAL'
|
|
85
86
|
|
|
86
87
|
def test_if_no_category_is_specified_default_to_unnamed_category():
|
|
87
|
-
pac = parser.parse_pac_url(valid_base + "KEY:VAL")
|
|
88
|
-
cat: Category = pac.identifier.categories[0]
|
|
88
|
+
pac, _ = parser.parse_pac_url(valid_base + "KEY:VAL")
|
|
89
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
89
90
|
assert cat.key == None
|
|
90
91
|
assert cat.segments[0].key == 'KEY'
|
|
91
92
|
assert cat.segments[0].value == 'VAL'
|
|
92
93
|
|
|
93
94
|
def test_missing_dash_in_category_name_is_interpreted_as_unnamed_segment():
|
|
94
|
-
pac = parser.parse_pac_url(valid_base + "MD/KEY:VAL")
|
|
95
|
-
cat: Category = pac.identifier.categories[0]
|
|
95
|
+
pac, _ = parser.parse_pac_url(valid_base + "MD/KEY:VAL")
|
|
96
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
96
97
|
assert cat.key == None
|
|
97
98
|
assert cat.segments[0].value == 'MD'
|
|
98
99
|
|
|
99
100
|
def test_category_with_multiple_segments():
|
|
100
|
-
pac = parser.parse_pac_url(valid_base + "-MD/KEY0:VAL0/VAL1/KEY2:VAL2")
|
|
101
|
-
cat: Category = pac.identifier.categories[0]
|
|
101
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MD/KEY0:VAL0/VAL1/KEY2:VAL2")
|
|
102
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
102
103
|
assert cat.key == '-MD'
|
|
103
104
|
|
|
104
105
|
seg: IDSegment = cat.segments[0]
|
|
@@ -114,19 +115,20 @@ def test_category_with_multiple_segments():
|
|
|
114
115
|
assert seg.value == "VAL2"
|
|
115
116
|
|
|
116
117
|
def test_two_categories():
|
|
117
|
-
pac = parser.parse_pac_url(valid_base + "-DR/KEY:VAL/-MD/KEY:VAL")
|
|
118
|
-
cat: Category = pac.identifier.categories[0]
|
|
118
|
+
pac, _ = parser.parse_pac_url(valid_base + "-DR/KEY:VAL/-MD/KEY:VAL")
|
|
119
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
119
120
|
assert cat.key == '-DR'
|
|
120
121
|
assert cat.segments[0].key == 'KEY'
|
|
121
122
|
assert cat.segments[0].value == 'VAL'
|
|
122
123
|
|
|
123
|
-
cat: Category = pac.identifier.categories[1]
|
|
124
|
+
cat: Category = pac.pac_id.identifier.categories[1]
|
|
124
125
|
assert cat.key == '-MD'
|
|
125
126
|
assert cat.segments[0].key == 'KEY'
|
|
126
127
|
assert cat.segments[0].value == 'VAL'
|
|
127
128
|
|
|
128
129
|
def test_three_categories():
|
|
129
|
-
pac = parser.parse_pac_url(valid_base + "-DR/KEY0:VAL0/-MD/KEY1:VAL1/-CAT/KEY2:VAL2")
|
|
130
|
+
pac, _ = parser.parse_pac_url(valid_base + "-DR/KEY0:VAL0/-MD/KEY1:VAL1/-CAT/KEY2:VAL2")
|
|
131
|
+
pac = pac.pac_id
|
|
130
132
|
cat: Category = pac.identifier.categories[0]
|
|
131
133
|
assert cat.key == '-DR'
|
|
132
134
|
assert cat.segments[0].key == 'KEY0'
|
|
@@ -143,14 +145,14 @@ def test_three_categories():
|
|
|
143
145
|
assert cat.segments[0].value == 'VAL2'
|
|
144
146
|
|
|
145
147
|
def test_implied_segments_of_MD_category():
|
|
146
|
-
pac = parser.parse_pac_url(valid_base + "-MD/0/1")
|
|
147
|
-
cat: Category = pac.identifier.categories[0]
|
|
148
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MD/0/1")
|
|
149
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
148
150
|
assert cat.segments[0].key == '240'
|
|
149
151
|
assert cat.segments[1].key == '21'
|
|
150
152
|
|
|
151
153
|
def test_implied_segments_of_MS_category():
|
|
152
|
-
pac = parser.parse_pac_url(valid_base + "-MS/0/1/2/3/4")
|
|
153
|
-
cat: Category = pac.identifier.categories[0]
|
|
154
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MS/0/1/2/3/4")
|
|
155
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
154
156
|
assert cat.segments[0].key == '240'
|
|
155
157
|
assert cat.segments[1].key == '10'
|
|
156
158
|
assert cat.segments[2].key == '20'
|
|
@@ -158,8 +160,8 @@ def test_implied_segments_of_MS_category():
|
|
|
158
160
|
assert cat.segments[4].key == '250'
|
|
159
161
|
|
|
160
162
|
def test_implied_segments_of_MC_category():
|
|
161
|
-
pac = parser.parse_pac_url(valid_base + "-MC/0/1/2/3/4")
|
|
162
|
-
cat: Category = pac.identifier.categories[0]
|
|
163
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MC/0/1/2/3/4")
|
|
164
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
163
165
|
assert cat.segments[0].key == '240'
|
|
164
166
|
assert cat.segments[1].key == '10'
|
|
165
167
|
assert cat.segments[2].key == '20'
|
|
@@ -167,8 +169,8 @@ def test_implied_segments_of_MC_category():
|
|
|
167
169
|
assert cat.segments[4].key == '250'
|
|
168
170
|
|
|
169
171
|
def test_implied_segments_of_MM_category():
|
|
170
|
-
pac = parser.parse_pac_url(valid_base + "-MM/0/1/2/3/4")
|
|
171
|
-
cat: Category = pac.identifier.categories[0]
|
|
172
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MM/0/1/2/3/4")
|
|
173
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
172
174
|
assert cat.segments[0].key == '240'
|
|
173
175
|
assert cat.segments[1].key == '10'
|
|
174
176
|
assert cat.segments[2].key == '20'
|
|
@@ -176,8 +178,8 @@ def test_implied_segments_of_MM_category():
|
|
|
176
178
|
assert cat.segments[4].key == '250'
|
|
177
179
|
|
|
178
180
|
def test_stop_implying_segments_after_an_explicit_one_is_found():
|
|
179
|
-
pac = parser.parse_pac_url(valid_base + "-MS/0/1/KEY:2/3/4")
|
|
180
|
-
cat: Category = pac.identifier.categories[0]
|
|
181
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MS/0/1/KEY:2/3/4")
|
|
182
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
181
183
|
assert cat.segments[0].key == '240'
|
|
182
184
|
assert cat.segments[1].key == '10'
|
|
183
185
|
assert cat.segments[2].key == 'KEY'
|
|
@@ -185,15 +187,16 @@ def test_stop_implying_segments_after_an_explicit_one_is_found():
|
|
|
185
187
|
assert cat.segments[4].key == None
|
|
186
188
|
|
|
187
189
|
def test_more_segments_than_implicit_keys():
|
|
188
|
-
pac = parser.parse_pac_url(valid_base + "-MD/IMPLIED1/IMPLIED2/ADDITIONAL")
|
|
189
|
-
cat: Category = pac.identifier.categories[0]
|
|
190
|
+
pac, _ = parser.parse_pac_url(valid_base + "-MD/IMPLIED1/IMPLIED2/ADDITIONAL")
|
|
191
|
+
cat: Category = pac.pac_id.identifier.categories[0]
|
|
190
192
|
assert cat.segments[2].key == None
|
|
191
193
|
|
|
192
194
|
|
|
193
195
|
|
|
194
196
|
# Extensions
|
|
195
197
|
def test_valid_extensions():
|
|
196
|
-
|
|
198
|
+
pac, _ = parser.parse_pac_url(valid_base + valid_standard_segments + "*name1$t1/data1*name2$t2/data2")
|
|
199
|
+
extensions = pac.extensions
|
|
197
200
|
ext: Extension = extensions[0]
|
|
198
201
|
assert ext.name == 'name1'
|
|
199
202
|
assert ext.type == 't1'
|
|
@@ -226,7 +229,8 @@ def test_known_extension_types_are_parsed():
|
|
|
226
229
|
'KNOWN_EXTENSION': ExtensionMockType,
|
|
227
230
|
}
|
|
228
231
|
parser_with_known_extension = PAC_Parser(extension_interpreters)
|
|
229
|
-
|
|
232
|
+
pac, _ = parser_with_known_extension.parse_pac_url(valid_base + valid_standard_segments + "*name1$KNOWN_EXTENSION/data1")
|
|
233
|
+
extensions= pac.extensions
|
|
230
234
|
ext: Extension = extensions[0]
|
|
231
235
|
assert isinstance(ext, ExtensionMockType)
|
|
232
236
|
assert ext.name == 'name__foo'
|
|
@@ -235,7 +239,8 @@ def test_known_extension_types_are_parsed():
|
|
|
235
239
|
|
|
236
240
|
|
|
237
241
|
def test_imply_display_name_and_summary_extension():
|
|
238
|
-
|
|
242
|
+
pac, _ = parser.parse_pac_url(valid_base + valid_standard_segments + "*data1*data2")
|
|
243
|
+
extensions = pac.extensions
|
|
239
244
|
ext: Extension = extensions[0]
|
|
240
245
|
assert ext.name == 'N'
|
|
241
246
|
assert ext.type == 'N'
|
|
@@ -246,7 +251,8 @@ def test_imply_display_name_and_summary_extension():
|
|
|
246
251
|
|
|
247
252
|
def test_stop_imply_extensions_after_explicit():
|
|
248
253
|
with pytest.raises(Exception):
|
|
249
|
-
_ = parser.parse_pac_url(valid_base + valid_standard_segments + "*N$T/data1*data2")
|
|
254
|
+
pac, _ = parser.parse_pac_url(valid_base + valid_standard_segments + "*N$T/data1*data2")
|
|
255
|
+
pac.extensions
|
|
250
256
|
|
|
251
257
|
|
|
252
258
|
def test_extension_parsing():
|
|
@@ -19,13 +19,29 @@ pac_id = PACID(issuer = 'mettorius.com',
|
|
|
19
19
|
])
|
|
20
20
|
)
|
|
21
21
|
extensions= [
|
|
22
|
+
UnknownExtension(name_= 'N', type_= 'T.T', data_= 'DATA'),
|
|
23
|
+
UnknownExtension(name_= 'SUM', type_= 'TREX', data_= 'DATA'),
|
|
22
24
|
UnknownExtension(name_= 'FOO', type_= 'T', data_= 'DATA'),
|
|
23
25
|
UnknownExtension(name_= 'BAR', type_= 'T2', data_= 'DATA')
|
|
24
26
|
]
|
|
25
27
|
|
|
26
28
|
|
|
27
29
|
def test_url_serialization():
|
|
28
|
-
url = serializer.to_url(pac_id, extensions)
|
|
30
|
+
url = serializer.to_url(pac_id, extensions, use_short_notation_for_extensions=False, uppercase_only=True)
|
|
31
|
+
assert url == 'HTTPS://PAC.METTORIUS.COM/-DR/999/-MD/240:1/21:2*N$T.T/DATA*SUM$TREX/DATA*FOO$T/DATA*BAR$T2/DATA'
|
|
32
|
+
|
|
33
|
+
def test_url_serialization_short_notation_for_N_and_SUM_extensions():
|
|
34
|
+
url = serializer.to_url(pac_id, extensions, use_short_notation_for_extensions=True, uppercase_only=True)
|
|
35
|
+
assert url == 'HTTPS://PAC.METTORIUS.COM/-DR/999/-MD/240:1/21:2*DATA*DATA*FOO$T/DATA*BAR$T2/DATA'
|
|
36
|
+
|
|
37
|
+
def test_url_serialization_short_notation_for_N_extensions():
|
|
38
|
+
ext = [extensions[i] for i in (0,2,3)]
|
|
39
|
+
url = serializer.to_url(pac_id, ext, use_short_notation_for_extensions=True, uppercase_only=True)
|
|
40
|
+
assert url == 'HTTPS://PAC.METTORIUS.COM/-DR/999/-MD/240:1/21:2*DATA*FOO$T/DATA*BAR$T2/DATA'
|
|
41
|
+
|
|
42
|
+
def test_url_serialization_short_notation_stop_implying_when_specific_extension():
|
|
43
|
+
ext = [extensions[i] for i in (2,3)]
|
|
44
|
+
url = serializer.to_url(pac_id, ext, use_short_notation_for_extensions=True, uppercase_only=True)
|
|
29
45
|
assert url == 'HTTPS://PAC.METTORIUS.COM/-DR/999/-MD/240:1/21:2*FOO$T/DATA*BAR$T2/DATA'
|
|
30
46
|
|
|
31
47
|
|
labfreed-0.0.3/README.md
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
from typing import Optional
|
|
2
|
-
from typing_extensions import Self
|
|
3
|
-
from pydantic import BaseModel, Field, computed_field, conlist, model_validator
|
|
4
|
-
from abc import ABC, abstractproperty, abstractstaticmethod
|
|
5
|
-
|
|
6
|
-
class IDSegment(BaseModel):
|
|
7
|
-
key:Optional[str] = Field(None, pattern=r'^[A-Z0-9-+]+$', min_length=1)
|
|
8
|
-
value:str = Field(..., pattern=r'^[A-Z0-9-+]+$', min_length=1)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Category(BaseModel):
|
|
12
|
-
key:str|None = None
|
|
13
|
-
segments: list[IDSegment]
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class Identifier(BaseModel):
|
|
17
|
-
segments: conlist(IDSegment, min_length=1) = Field(..., exclude=True) # exclude=True prevents this from being serialized by Pydantic
|
|
18
|
-
|
|
19
|
-
@computed_field
|
|
20
|
-
@property
|
|
21
|
-
def categories(self) -> list[Category]:
|
|
22
|
-
categories = list()
|
|
23
|
-
c = Category(segments=[])
|
|
24
|
-
categories.append(c)
|
|
25
|
-
for s in self.segments:
|
|
26
|
-
# new category starts with "-"
|
|
27
|
-
if s.value[0] == '-':
|
|
28
|
-
cat_key = s.value
|
|
29
|
-
c = Category(key=cat_key, segments=[])
|
|
30
|
-
categories.append(c)
|
|
31
|
-
else:
|
|
32
|
-
c.segments.append(s)
|
|
33
|
-
|
|
34
|
-
# the first category might have no segments. remove categories without segments
|
|
35
|
-
if not categories[0].segments:
|
|
36
|
-
categories = categories[1:]
|
|
37
|
-
|
|
38
|
-
return categories
|
|
39
|
-
|
|
40
|
-
@model_validator(mode='after')
|
|
41
|
-
def check_keys_are_unique_in_each_category(self) -> Self:
|
|
42
|
-
for c in self.categories:
|
|
43
|
-
keys = [s.key for s in c.segments if s.key]
|
|
44
|
-
duplicate_keys = [k for k in set(keys) if keys.count(k) > 1]
|
|
45
|
-
if duplicate_keys:
|
|
46
|
-
raise ValueError(f'Duplicate keys {",".join(duplicate_keys)} in category {c.key}')
|
|
47
|
-
return self
|
|
48
|
-
|
|
49
|
-
@staticmethod
|
|
50
|
-
def from_categories(categories:list[Category]) :
|
|
51
|
-
segments = list()
|
|
52
|
-
for c in categories:
|
|
53
|
-
if c.key:
|
|
54
|
-
segments.append(IDSegment(value=c.key))
|
|
55
|
-
segments.extend(c.segments)
|
|
56
|
-
return Identifier(segments=segments)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class Extension(ABC, BaseModel):
|
|
61
|
-
|
|
62
|
-
@abstractproperty
|
|
63
|
-
def name(self)->str:
|
|
64
|
-
pass
|
|
65
|
-
|
|
66
|
-
@abstractproperty
|
|
67
|
-
def type(self)->str:
|
|
68
|
-
pass
|
|
69
|
-
|
|
70
|
-
@abstractproperty
|
|
71
|
-
def data(self)->str:
|
|
72
|
-
pass
|
|
73
|
-
|
|
74
|
-
@abstractstaticmethod
|
|
75
|
-
def from_spec_fields(name, type, data):
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
class UnknownExtension(Extension):
|
|
80
|
-
name_:str
|
|
81
|
-
type_:str
|
|
82
|
-
data_:str
|
|
83
|
-
|
|
84
|
-
@property
|
|
85
|
-
def name(self)->str:
|
|
86
|
-
return self.name_
|
|
87
|
-
|
|
88
|
-
@property
|
|
89
|
-
def type(self)->str:
|
|
90
|
-
return self.type_
|
|
91
|
-
|
|
92
|
-
@property
|
|
93
|
-
def data(self)->str:
|
|
94
|
-
return self.data_
|
|
95
|
-
|
|
96
|
-
@staticmethod
|
|
97
|
-
def from_spec_fields(name, type, data):
|
|
98
|
-
return UnknownExtension(name_=name, type_=type, data_=data)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
class PACID(BaseModel):
|
|
103
|
-
issuer:str
|
|
104
|
-
identifier: Identifier
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
class PACID_With_Extensions(BaseModel):
|
|
108
|
-
pac_id: PACID
|
|
109
|
-
extensions: list[Extension] = Field(default_factory=list)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|