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
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing_extensions import Self
|
|
3
|
+
from pydantic import Field, conlist, model_validator
|
|
4
|
+
|
|
5
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
|
|
6
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
7
|
+
from labfreed.pac_id.extension import Extension
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_domain_name_pattern = r"(?!-)([A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}"
|
|
16
|
+
|
|
17
|
+
class PAC_ID(LabFREED_BaseModel):
|
|
18
|
+
'''Represents a PAC-ID.
|
|
19
|
+
Refer to the [specification](https://github.com/ApiniLabs/PAC-ID?tab=readme-ov-file#specification) for details.
|
|
20
|
+
'''
|
|
21
|
+
issuer:str
|
|
22
|
+
'''The issuer of the PAC-ID.'''
|
|
23
|
+
identifier: conlist(IDSegment) = Field(..., default_factory=list) # type: ignore # exclude=True prevents this from being serialized by Pydantic
|
|
24
|
+
'''The identifier of the PAC-ID is a series of IDSegments'''
|
|
25
|
+
|
|
26
|
+
extensions: list[Extension] = Field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_extension_of_type(self, type:str) -> list[Extension]:
|
|
30
|
+
'''Get all extensions of a certain type.'''
|
|
31
|
+
return [e for e in self.extensions if e.type == type]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_extension(self, name:str) -> Extension|None:
|
|
35
|
+
'''Get extension of certain name'''
|
|
36
|
+
out = [e for e in self.extensions if e.name == name]
|
|
37
|
+
if not out:
|
|
38
|
+
return None
|
|
39
|
+
return out[0]
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def from_url(cls, url, *, extension_interpreters='default',
|
|
43
|
+
try_pac_cat=True,
|
|
44
|
+
suppress_validation_errors=False) -> Self:
|
|
45
|
+
from labfreed.pac_id.url_parser import PAC_Parser
|
|
46
|
+
return PAC_Parser.from_url(url, try_pac_cat=try_pac_cat, suppress_validation_errors=suppress_validation_errors, extension_interpreters=extension_interpreters)
|
|
47
|
+
|
|
48
|
+
def to_url(self, use_short_notation=False, uppercase_only=False) -> str:
|
|
49
|
+
from labfreed.pac_id.url_serializer import PACID_Serializer
|
|
50
|
+
return PACID_Serializer.to_url(self, use_short_notation=use_short_notation, uppercase_only=uppercase_only)
|
|
51
|
+
|
|
52
|
+
def to_json(self, indent=None) -> str:
|
|
53
|
+
if not indent:
|
|
54
|
+
return self.model_dump_json()
|
|
55
|
+
else:
|
|
56
|
+
return self.model_dump_json(indent=indent)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> dict:
|
|
59
|
+
return self.model_dump()
|
|
60
|
+
|
|
61
|
+
def __str__(self):
|
|
62
|
+
return self.to_url()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@model_validator(mode='after')
|
|
66
|
+
def _check_at_least_one_segment(self) -> Self:
|
|
67
|
+
if not len(self.identifier) >= 1:
|
|
68
|
+
self._add_validation_message(
|
|
69
|
+
source="identifier",
|
|
70
|
+
level = ValidationMsgLevel.ERROR,
|
|
71
|
+
msg='Identifier must contain et least one segment.'
|
|
72
|
+
)
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@model_validator(mode='after')
|
|
77
|
+
def _check_length(self) -> Self:
|
|
78
|
+
length = 0
|
|
79
|
+
for s in self.identifier:
|
|
80
|
+
s:IDSegment = s
|
|
81
|
+
if s.key:
|
|
82
|
+
length += len(s.key)
|
|
83
|
+
length += 1 # for ":"
|
|
84
|
+
length += len(s.value)
|
|
85
|
+
length += len(self.identifier) - 1 # account for "/" separating the segments
|
|
86
|
+
|
|
87
|
+
if length > 256:
|
|
88
|
+
self._add_validation_message(
|
|
89
|
+
source="identifier",
|
|
90
|
+
level = ValidationMsgLevel.ERROR,
|
|
91
|
+
msg=f'Identifier is {length} characters long, Identifier must not exceed 256 characters.'
|
|
92
|
+
)
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@model_validator(mode="after")
|
|
97
|
+
def _validate_issuer(self):
|
|
98
|
+
if not re.fullmatch(_domain_name_pattern, self.issuer):
|
|
99
|
+
self._add_validation_message(
|
|
100
|
+
source="PAC-ID",
|
|
101
|
+
level = ValidationMsgLevel.ERROR,
|
|
102
|
+
highlight_pattern=self.issuer,
|
|
103
|
+
msg="Issuer must be a valid domain name. "
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# recommendation that A-Z, 0-9, -, and . should be used
|
|
107
|
+
if not_recommended_chars := set(re.sub(r'[A-Z0-9\.-]', '', self.issuer)):
|
|
108
|
+
self._add_validation_message(
|
|
109
|
+
source="PAC-ID",
|
|
110
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
111
|
+
highlight_pattern=self.issuer,
|
|
112
|
+
highlight_sub=not_recommended_chars,
|
|
113
|
+
msg=f"Characters {' '.join(not_recommended_chars)} should not be used. Issuer SHOULD contain only the characters A-Z, 0-9, -, and . "
|
|
114
|
+
)
|
|
115
|
+
return self
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@model_validator(mode='after')
|
|
119
|
+
def _check_identifier_segment_keys_are_unique(self) -> Self:
|
|
120
|
+
keys = [s.key for s in self.identifier if s.key]
|
|
121
|
+
duplicate_keys = [k for k in set(keys) if keys.count(k) > 1]
|
|
122
|
+
if duplicate_keys:
|
|
123
|
+
for k in duplicate_keys:
|
|
124
|
+
self._add_validation_message(
|
|
125
|
+
source=f"identifier {k}",
|
|
126
|
+
level = ValidationMsgLevel.RECOMMENDATION,
|
|
127
|
+
msg=f"Duplicate segment key {k}. This will probably lead to undefined behaviour",
|
|
128
|
+
highlight_pattern = k
|
|
129
|
+
)
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from types import MappingProxyType
|
|
5
|
+
|
|
6
|
+
from labfreed.labfreed_infrastructure import LabFREED_ValidationError
|
|
7
|
+
|
|
8
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
9
|
+
from labfreed.pac_id.extension import Extension
|
|
10
|
+
|
|
11
|
+
from labfreed.pac_cat import PAC_CAT
|
|
12
|
+
from labfreed.well_known_extensions import default_extension_interpreters
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
# only imported during type checking
|
|
18
|
+
from labfreed.pac_id import PAC_ID
|
|
19
|
+
from labfreed.pac_cat import PAC_CAT
|
|
20
|
+
|
|
21
|
+
class PAC_Parser():
|
|
22
|
+
'''@private
|
|
23
|
+
Knows how to parse a PAC-ID.
|
|
24
|
+
From a SW engineering perspective it would be best to have no dependencies from other modules to pac_id.
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
>> We have given priority to convenient usage and therefore chose to have dependencies from pac_id to pac_cat and well_known_extensions
|
|
28
|
+
'''
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def from_url(cls, pac_url:str,
|
|
32
|
+
*,
|
|
33
|
+
extension_interpreters = 'default',
|
|
34
|
+
try_pac_cat = True,
|
|
35
|
+
suppress_validation_errors=False
|
|
36
|
+
) -> "PAC_ID":
|
|
37
|
+
"""Parses a PAC-ID with extensions
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
pac_url (str): pac id with optional extensions: e.g. HTTPS://PAC.METTORIUS.COM/-MD/BAL500/1234*N$N/ABC*SUM$TREX/A$T.A:ABC
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
LabFREED_ValidationError: When validation fails. Note,that with suppress_errors no such error is raises
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
PACID including extensions. If possible PAC-CAT is applied and extensions are cast to a known type, which knows how to inetrprete the data
|
|
47
|
+
"""
|
|
48
|
+
if extension_interpreters == 'default':
|
|
49
|
+
extension_interpreters = default_extension_interpreters
|
|
50
|
+
|
|
51
|
+
if '*' in pac_url:
|
|
52
|
+
id_str, ext_str = pac_url.split('*', 1)
|
|
53
|
+
else:
|
|
54
|
+
id_str = pac_url
|
|
55
|
+
ext_str = ""
|
|
56
|
+
|
|
57
|
+
pac_id = cls._parse_pac_id(id_str)
|
|
58
|
+
|
|
59
|
+
# try converting to PAC-CAT. This can fail, in which case a regular PAC-ID is returned
|
|
60
|
+
if try_pac_cat:
|
|
61
|
+
try:
|
|
62
|
+
pac_cat = PAC_CAT.from_pac_id(pac_id)
|
|
63
|
+
if pac_cat.categories:
|
|
64
|
+
pac_id = pac_cat
|
|
65
|
+
except LabFREED_ValidationError:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
extensions = cls._parse_extensions(ext_str)
|
|
69
|
+
if extensions and extension_interpreters:
|
|
70
|
+
for i, e in enumerate(extensions):
|
|
71
|
+
if interpreter := extension_interpreters.get(e.type):
|
|
72
|
+
extensions[i] = interpreter.from_extension(e)
|
|
73
|
+
pac_id.extensions = extensions
|
|
74
|
+
|
|
75
|
+
if not pac_id.is_valid and not suppress_validation_errors:
|
|
76
|
+
raise LabFREED_ValidationError(validation_msgs = pac_id._get_nested_validation_messages())
|
|
77
|
+
|
|
78
|
+
return pac_id
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def _parse_pac_id(cls,id_str:str) -> "PAC_ID":
|
|
82
|
+
# m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?\..+?)/(?P<identifier>.*)', id_str)
|
|
83
|
+
m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?)/(?P<identifier>.*)', id_str)
|
|
84
|
+
d = m.groupdict()
|
|
85
|
+
|
|
86
|
+
id_segments = list()
|
|
87
|
+
id_segments = cls._parse_id_segments(d.get('identifier'))
|
|
88
|
+
|
|
89
|
+
from labfreed.pac_id import PAC_ID
|
|
90
|
+
pac = PAC_ID(issuer= d.get('issuer'),
|
|
91
|
+
identifier=id_segments
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return pac
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def _parse_id_segments(cls, identifier:str):
|
|
98
|
+
if not identifier:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
id_segments = list()
|
|
102
|
+
if len(identifier) > 0 and identifier[0] == '/':
|
|
103
|
+
identifier = identifier[1:]
|
|
104
|
+
for s in identifier.split('/'):
|
|
105
|
+
tmp = s.split(':')
|
|
106
|
+
|
|
107
|
+
if len(tmp) == 1:
|
|
108
|
+
segment = IDSegment(value=tmp[0])
|
|
109
|
+
elif len(tmp) == 2:
|
|
110
|
+
segment = IDSegment(key=tmp[0], value=tmp[1])
|
|
111
|
+
else:
|
|
112
|
+
raise ValueError(f'invalid segment: {s}')
|
|
113
|
+
|
|
114
|
+
id_segments.append(segment)
|
|
115
|
+
return id_segments
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def _parse_extensions(cls, extensions_str:str|None) -> list["Extension"]:
|
|
120
|
+
|
|
121
|
+
extensions = list()
|
|
122
|
+
|
|
123
|
+
if not extensions_str:
|
|
124
|
+
return extensions
|
|
125
|
+
|
|
126
|
+
defaults = MappingProxyType(
|
|
127
|
+
{
|
|
128
|
+
0: { 'name': 'N', 'type': 'N'},
|
|
129
|
+
1: { 'name': 'SUM', 'type': 'TREX'}
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
for i, e in enumerate(extensions_str.split('*')):
|
|
133
|
+
if e == '': #this will happen if first extension starts with *
|
|
134
|
+
continue
|
|
135
|
+
d = re.match('((?P<name>.+)\$(?P<type>.+)/)?(?P<data>.+)', e).groupdict()
|
|
136
|
+
|
|
137
|
+
name = d.get('name')
|
|
138
|
+
type = d.get('type')
|
|
139
|
+
data = d.get('data')
|
|
140
|
+
|
|
141
|
+
if name:
|
|
142
|
+
defaults = None # once a name was specified no longer assign defaults
|
|
143
|
+
else:
|
|
144
|
+
if defaults:
|
|
145
|
+
name = defaults.get(i).get('name')
|
|
146
|
+
type = defaults.get(i).get('type')
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError('extension number {i}, must have name and type')
|
|
149
|
+
|
|
150
|
+
#convert to subtype if they were given
|
|
151
|
+
e = Extension.create(name=name, type=type, data=data)
|
|
152
|
+
extensions.append(e)
|
|
153
|
+
|
|
154
|
+
return extensions
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
2
|
+
from labfreed.pac_cat.predefined_categories import PredefinedCategory
|
|
3
|
+
from labfreed.pac_id.id_segment import IDSegment
|
|
4
|
+
from labfreed.pac_id.extension import Extension
|
|
5
|
+
from labfreed.pac_id import PAC_ID
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PACID_Serializer():
|
|
9
|
+
'''Represents a PAC-ID including it's extensions'''
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def to_url(cls, pac:PAC_ID, use_short_notation=False, uppercase_only=False) -> str:
|
|
13
|
+
"""Serializes the PAC-ID including extensions.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
use_short_notation (bool, optional): Uses shortening conventions for extensions and categories..
|
|
17
|
+
uppercase_only (bool, optional): Forces all uppercase letters (results in smaller QR)..
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
str: Something like this HTTPS://PAC.METTORIUS.COM/-MD/BAL500/1234*N$N/ABC*SUM$TREX/A$T.A:ABC
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
extensions_str = cls._serialize_extensions(pac.extensions, use_short_notation=use_short_notation)
|
|
24
|
+
|
|
25
|
+
identifier_str = cls._serialize_identifier(pac, use_short_notation=use_short_notation)
|
|
26
|
+
out = f"HTTPS://PAC.{pac.issuer}{identifier_str}{extensions_str}"
|
|
27
|
+
|
|
28
|
+
if uppercase_only:
|
|
29
|
+
out = out.upper()
|
|
30
|
+
return out
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def _serialize_identifier(cls, pac:PAC_ID|PAC_CAT, use_short_notation=True):
|
|
34
|
+
''' Serializes the PAC-ID'''
|
|
35
|
+
if isinstance(pac, PAC_CAT):
|
|
36
|
+
for c in pac.categories:
|
|
37
|
+
segments = [IDSegment(value=c.key)]
|
|
38
|
+
if isinstance(c, PredefinedCategory):
|
|
39
|
+
segments += c._get_segments(use_short_notation=use_short_notation)
|
|
40
|
+
else:
|
|
41
|
+
segments += c.segments
|
|
42
|
+
else:
|
|
43
|
+
segments = pac.identifier
|
|
44
|
+
|
|
45
|
+
identifier_str = ''
|
|
46
|
+
for s in segments:
|
|
47
|
+
s:IDSegment = s
|
|
48
|
+
if s.key:
|
|
49
|
+
identifier_str += f'/{s.key}:{s.value}'
|
|
50
|
+
else:
|
|
51
|
+
identifier_str += f'/{s.value}'
|
|
52
|
+
return identifier_str
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def _serialize_extensions(cls, extensions:list[Extension], use_short_notation):
|
|
57
|
+
out = ''
|
|
58
|
+
short_notation = use_short_notation
|
|
59
|
+
for i, e in enumerate(extensions):
|
|
60
|
+
|
|
61
|
+
if short_notation and i==0:
|
|
62
|
+
if e.name=='N':
|
|
63
|
+
out += f'*{e.data}'
|
|
64
|
+
continue
|
|
65
|
+
else:
|
|
66
|
+
short_notation = False
|
|
67
|
+
if short_notation and i==1:
|
|
68
|
+
if e.name=='SUM':
|
|
69
|
+
out += f'*{e.data}'
|
|
70
|
+
continue
|
|
71
|
+
else:
|
|
72
|
+
short_notation = False
|
|
73
|
+
|
|
74
|
+
out += f'*{e.name}${e.type}/{e.data}'
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
|
|
2
|
+
from enum import Enum
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from pydantic import Field
|
|
7
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMessage, ValidationMsgLevel
|
|
8
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
9
|
+
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
10
|
+
|
|
11
|
+
class ServiceType(Enum):
|
|
12
|
+
USER_HANDOVER_GENERIC = 'userhandover-generic'
|
|
13
|
+
ATTRIBUTE_SERVICE_GENERIC = 'attributes-generic'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class CITEntry_v1(LabFREED_BaseModel):
|
|
17
|
+
applicable_if: str = Field(..., min_length=1)
|
|
18
|
+
service_name: str = Field(..., min_length=1)
|
|
19
|
+
application_intent:str = Field(..., min_length=1)
|
|
20
|
+
service_type:ServiceType
|
|
21
|
+
template_url:str = Field(..., min_length=1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CIT_v1(LabFREED_BaseModel):
|
|
25
|
+
origin:str = ''
|
|
26
|
+
entries:list[CITEntry_v1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_csv(cls, csv:str, origin=''):
|
|
31
|
+
lines = csv.splitlines()
|
|
32
|
+
entries = list()
|
|
33
|
+
errors = list()
|
|
34
|
+
for line in lines:
|
|
35
|
+
if not line: # empty line
|
|
36
|
+
continue
|
|
37
|
+
if line.strip()[0] == '#': #comment line
|
|
38
|
+
continue
|
|
39
|
+
if 'Service Name' in line.strip() : #header line
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
cols = [c.strip() for c in line.split('\t')]
|
|
43
|
+
try:
|
|
44
|
+
entry = CITEntry_v1(
|
|
45
|
+
service_name = cols[0],
|
|
46
|
+
application_intent = cols[1],
|
|
47
|
+
service_type = ServiceType(cols[2]),
|
|
48
|
+
applicable_if = cols[3],
|
|
49
|
+
template_url = cols[4]
|
|
50
|
+
)
|
|
51
|
+
except ValueError:
|
|
52
|
+
logging.error(f'invalid line {line}')
|
|
53
|
+
msg = ValidationMessage(
|
|
54
|
+
level=ValidationMsgLevel.WARNING,
|
|
55
|
+
source='CIT line',
|
|
56
|
+
source_id=0,
|
|
57
|
+
msg='Invalid line in CIT. Line was ignored. Remaining CIT is functional.',
|
|
58
|
+
highlight_sub_patterns=line
|
|
59
|
+
)
|
|
60
|
+
errors.append(msg)
|
|
61
|
+
|
|
62
|
+
entries.append(entry)
|
|
63
|
+
cit = CIT_v1(origin=origin, entries=entries)
|
|
64
|
+
cit._validation_messages.extend(errors)
|
|
65
|
+
cit._csv_original = csv
|
|
66
|
+
return cit
|
|
67
|
+
|
|
68
|
+
def evaluate_pac_id(self, pac):
|
|
69
|
+
cit_evaluated = ServiceGroup(origin=self.origin)
|
|
70
|
+
for e in self.entries:
|
|
71
|
+
conditions = e.applicable_if.split(';')
|
|
72
|
+
conditions_evaluated = list()
|
|
73
|
+
for c in conditions:
|
|
74
|
+
if '=' in c:
|
|
75
|
+
query, expected = c.split('=')
|
|
76
|
+
value = self._find_in_pac(query.strip(), pac)
|
|
77
|
+
conditions_evaluated.append(value == expected.strip())
|
|
78
|
+
else:
|
|
79
|
+
query = c.strip()
|
|
80
|
+
found = self._find_in_pac(query, pac)
|
|
81
|
+
conditions_evaluated.append(found)
|
|
82
|
+
is_applicable = all(conditions_evaluated)
|
|
83
|
+
|
|
84
|
+
if not is_applicable:
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
url = re.sub(r"\{([^}]+)\}", lambda v: self._find_in_pac(v.group(0), pac), e.template_url)
|
|
88
|
+
cit_evaluated.services.append(Service(
|
|
89
|
+
service_name=e.service_name,
|
|
90
|
+
application_intents= [ e.application_intent ],
|
|
91
|
+
service_type=e.service_type,
|
|
92
|
+
url = url
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
return cit_evaluated
|
|
96
|
+
|
|
97
|
+
def _find_in_pac(self, value, pac:PAC_ID):
|
|
98
|
+
pac_url =pac.to_url()
|
|
99
|
+
if value == '{isu}':
|
|
100
|
+
return pac.issuer
|
|
101
|
+
|
|
102
|
+
elif value == '{pac}':
|
|
103
|
+
return pac_url.split('*')[0]
|
|
104
|
+
|
|
105
|
+
elif value == '{id}':
|
|
106
|
+
m = re.match(r'^HTTPS://.+?/(.+?)(\*.*)*$', pac_url)
|
|
107
|
+
return m.group(1) if m else None
|
|
108
|
+
|
|
109
|
+
elif m := re.match(r'\{idSeg(\d+)\}', value):
|
|
110
|
+
i = int(m.group(1)) - 1 # CIT is 1 based
|
|
111
|
+
seg = pac.identifier[i] if i < len(pac.identifier) else None
|
|
112
|
+
if seg:
|
|
113
|
+
return f"{(seg.key + ':') if seg.key else ""}{seg.value}"
|
|
114
|
+
|
|
115
|
+
elif m := re.match(r'\{idVal(\w+)\}', value):
|
|
116
|
+
k = m.group(1)
|
|
117
|
+
seg = [s for s in pac.identifier if s.key and s.key == k]
|
|
118
|
+
if seg:
|
|
119
|
+
seg = seg[0]
|
|
120
|
+
return seg.value
|
|
121
|
+
else:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
elif value == '{ext}':
|
|
125
|
+
m = re.match(r'^.*?(\*.*)*$', pac_url)
|
|
126
|
+
ext_str = m.group(1) if m else None
|
|
127
|
+
return m.group(1)[1:] if ext_str else None
|
|
128
|
+
|
|
129
|
+
elif m := re.match(r'\{ext(\d+)\}', value):
|
|
130
|
+
i = int(m.group(1)) - 1 # CIT is 1 based
|
|
131
|
+
extensions = pac_url.split('*')
|
|
132
|
+
extensions.pop(0)# first element is not extension
|
|
133
|
+
return extensions[i] if i < len(extensions) else None
|
|
134
|
+
else:
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def __str__(self):
|
|
142
|
+
if csv:=self._csv_original:
|
|
143
|
+
return csv
|
|
144
|
+
|
|
145
|
+
s = "# coupling information table version: 1.0\n"
|
|
146
|
+
s += "Service Name\tApplication Intent\tService Type\tApplicable If\tTemplate Url\n"
|
|
147
|
+
for e in self.entries:
|
|
148
|
+
s += '\t'.join([e.service_name, e.application_intent, e.service_type.value, e.applicable_if, e.template_url]) + '\n'
|
|
149
|
+
return s
|