labfreed 0.2.11__py3-none-any.whl → 0.3.0a0__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 +1 -1
- labfreed/labfreed_infrastructure.py +3 -3
- labfreed/pac_attributes/api_data_models/request.py +56 -0
- labfreed/pac_attributes/api_data_models/response.py +184 -0
- labfreed/pac_attributes/api_data_models/server_capabilities_response.py +7 -0
- labfreed/pac_attributes/client/__init__.py +1 -0
- labfreed/pac_attributes/client/attribute_cache.py +78 -0
- labfreed/pac_attributes/client/client.py +148 -0
- labfreed/pac_attributes/server/attribute_data_sources.py +69 -0
- labfreed/pac_attributes/server/server.py +237 -0
- labfreed/pac_attributes/server/translation_data_sources.py +60 -0
- labfreed/pac_attributes/well_knonw_attribute_keys.py +11 -0
- labfreed/pac_cat/category_base.py +19 -0
- labfreed/pac_cat/pac_cat.py +8 -1
- labfreed/pac_id/extension.py +32 -7
- labfreed/pac_id/pac_id.py +2 -2
- labfreed/pac_id/url_parser.py +4 -4
- labfreed/pac_id/url_serializer.py +11 -5
- labfreed/pac_id_resolver/cit_common.py +1 -1
- labfreed/pac_id_resolver/cit_v1.py +0 -3
- labfreed/pac_id_resolver/cit_v2.py +1 -6
- labfreed/pac_id_resolver/resolver.py +12 -7
- labfreed/pac_id_resolver/services.py +0 -1
- labfreed/trex/python_convenience/quantity.py +86 -5
- labfreed/trex/value_segments.py +1 -1
- labfreed/utilities/ensure_utc_time.py +6 -0
- labfreed/utilities/translations.py +60 -0
- labfreed/well_known_extensions/display_name_extension.py +3 -3
- labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +4 -0
- labfreed/well_known_keys/labfreed/well_known_keys.py +12 -2
- labfreed/well_known_keys/unece/unece_units.py +2 -1
- {labfreed-0.2.11.dist-info → labfreed-0.3.0a0.dist-info}/METADATA +43 -29
- labfreed-0.3.0a0.dist-info/RECORD +56 -0
- labfreed/well_known_keys/gs1/gs1.py +0 -4
- labfreed-0.2.11.dist-info/RECORD +0 -45
- {labfreed-0.2.11.dist-info → labfreed-0.3.0a0.dist-info}/WHEEL +0 -0
- {labfreed-0.2.11.dist-info → labfreed-0.3.0a0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
import warnings
|
|
3
|
+
|
|
4
|
+
import rich
|
|
5
|
+
|
|
6
|
+
from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
|
|
7
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload, AttributesOfPACID, ReferenceAttribute
|
|
8
|
+
from labfreed.pac_attributes.api_data_models.server_capabilities_response import ServerCapabilities
|
|
9
|
+
from labfreed.pac_attributes.server.attribute_data_sources import AttributeGroupDataSource
|
|
10
|
+
from labfreed.pac_attributes.server.translation_data_sources import TranslationDataSource
|
|
11
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class InvalidRequestError(ValueError):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AttributeServerRequestHandler():
|
|
20
|
+
def __init__(self, data_sources:list[AttributeGroupDataSource], translation_data_sources:list[TranslationDataSource], default_language:str):
|
|
21
|
+
'''Initializes the AttributeServerRequestHandler.
|
|
22
|
+
- does some validation on availability of translations. NOTE: if data_sources or translation_data_sources change, this validation might get outdated'''
|
|
23
|
+
if isinstance(data_sources, AttributeGroupDataSource):
|
|
24
|
+
data_sources = [data_sources]
|
|
25
|
+
self._attribute_group_data_sources: list[AttributeGroupDataSource] = data_sources
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# find which languages the sources consistently know
|
|
30
|
+
self._translation_data_sources: list[TranslationDataSource] = translation_data_sources
|
|
31
|
+
supported_languages = set()
|
|
32
|
+
for tds in self._translation_data_sources:
|
|
33
|
+
supported_languages.update(tds.supported_languages)
|
|
34
|
+
if not supported_languages:
|
|
35
|
+
raise ValueError("translation_data_sources contain no common fully supported language")
|
|
36
|
+
|
|
37
|
+
if default_language not in supported_languages:
|
|
38
|
+
raise ValueError(f"fallback language {default_language} is not supported by all translation data sources.")
|
|
39
|
+
self._supported_languages = supported_languages
|
|
40
|
+
self._default_language = default_language
|
|
41
|
+
|
|
42
|
+
# check there are translations for all provided attributes
|
|
43
|
+
missing_translations = []
|
|
44
|
+
provided_attributes = [attr for ds in self._attribute_group_data_sources for attr in ds.provides_attributes]
|
|
45
|
+
for language in self._supported_languages:
|
|
46
|
+
for attribute_key in provided_attributes:
|
|
47
|
+
if not self._get_display_name_for_key(attribute_key, language):
|
|
48
|
+
missing_translations.append((attribute_key, language))
|
|
49
|
+
if missing_translations:
|
|
50
|
+
rich.print('[yellow bold]WARNING: Missing translations[/yellow bold]')
|
|
51
|
+
for mt in missing_translations:
|
|
52
|
+
rich.print(f"[yellow]WARNING:[/yellow] '{mt[1]}' translation missing for '{mt[0]}'' ")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def handle_attribute_request(self, json_request_body:str) -> str:
|
|
57
|
+
try:
|
|
58
|
+
r = AttributeRequestPayload.model_validate_json(json_request_body)
|
|
59
|
+
except Exception:
|
|
60
|
+
raise InvalidRequestError
|
|
61
|
+
attributes_for_pac_id = []
|
|
62
|
+
referenced_pac_ids = set()
|
|
63
|
+
for pac_url in r.pac_urls:
|
|
64
|
+
attributes_for_pac = self._get_attributes_for_pac_id(pac_url=pac_url,
|
|
65
|
+
restrict_to_attribute_groups = r.restrict_to_attribute_groups)
|
|
66
|
+
attributes_for_pac_id.append(attributes_for_pac)
|
|
67
|
+
ref = self._get_referenced_pac_ids(attributes_for_pac)
|
|
68
|
+
if ref:
|
|
69
|
+
referenced_pac_ids.update(ref)
|
|
70
|
+
|
|
71
|
+
# also find attributes of referenced pac-ids
|
|
72
|
+
if not r.suppress_forward_lookup:
|
|
73
|
+
for pac_url in referenced_pac_ids:
|
|
74
|
+
attributes_for_pac = self._get_attributes_for_pac_id(pac_url=pac_url,
|
|
75
|
+
restrict_to_attribute_groups = r.restrict_to_attribute_groups)
|
|
76
|
+
attributes_for_pac_id.append(attributes_for_pac)
|
|
77
|
+
|
|
78
|
+
# add translations
|
|
79
|
+
response_language = self._find_response_language(r.language_preferences)
|
|
80
|
+
for e in attributes_for_pac_id:
|
|
81
|
+
self._add_display_names(e, response_language)
|
|
82
|
+
|
|
83
|
+
response = AttributeResponsePayload(pac_attributes=attributes_for_pac_id, language=response_language
|
|
84
|
+
).to_json()
|
|
85
|
+
return response
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_attributes_for_pac_id(self, pac_url:str, restrict_to_attribute_groups:list[str]|None=None ) -> AttributesOfPACID:
|
|
91
|
+
attribute_groups = []
|
|
92
|
+
if restrict_to_attribute_groups:
|
|
93
|
+
relevant_data_sources = [ds for ds in self._attribute_group_data_sources if ds.attribute_group_key in restrict_to_attribute_groups]
|
|
94
|
+
else:
|
|
95
|
+
relevant_data_sources = self._attribute_group_data_sources
|
|
96
|
+
for ds in relevant_data_sources:
|
|
97
|
+
try:
|
|
98
|
+
ag = ds.attributes(pac_url)
|
|
99
|
+
if ag:
|
|
100
|
+
attribute_groups.append(ag)
|
|
101
|
+
except Exception as e:
|
|
102
|
+
e.add_note(f'Attribute Source {ds.attribute_group_key} encountered an error')
|
|
103
|
+
traceback.print_exc()
|
|
104
|
+
raise e
|
|
105
|
+
|
|
106
|
+
return AttributesOfPACID(pac_url=pac_url, # return the pac_url as given, i.e. with the extension if there was one
|
|
107
|
+
attribute_groups=attribute_groups)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _get_referenced_pac_ids(self, attributes_for_pac:AttributesOfPACID):
|
|
112
|
+
referenced_pacs = []
|
|
113
|
+
for ag in attributes_for_pac.attribute_groups:
|
|
114
|
+
for a in ag.attributes :
|
|
115
|
+
if isinstance(a, ReferenceAttribute):
|
|
116
|
+
try:
|
|
117
|
+
PAC_ID.from_url(a.value)
|
|
118
|
+
referenced_pacs.append(a.value)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
return referenced_pacs
|
|
122
|
+
|
|
123
|
+
# def get_translations(self, attributes_for_pac_id):
|
|
124
|
+
# ontology_map = {} # ontology name → list of TermTranslations
|
|
125
|
+
|
|
126
|
+
# for ag_for_pac in attributes_for_pac_id:
|
|
127
|
+
# for ag in ag_for_pac.attribute_groups:
|
|
128
|
+
# ag: AttributeGroup
|
|
129
|
+
# ontology_name = ag.ontology
|
|
130
|
+
# translation_data_source: TranslationDataSource = self._translation_data_sources.get(ontology_name, {})
|
|
131
|
+
|
|
132
|
+
# attribute_keys = [a.key for a in ag.attributes]
|
|
133
|
+
# all_keys = set([ag.key] + attribute_keys)
|
|
134
|
+
|
|
135
|
+
# for k in all_keys:
|
|
136
|
+
# t = translation_data_source.get_translations_for(k)
|
|
137
|
+
# if t:
|
|
138
|
+
# ontology_map.setdefault(ontology_name, []).append(t)
|
|
139
|
+
|
|
140
|
+
# translations_by_ontology = [ TranslationsForOntology(ontology=name, terms=terms) for name, terms in ontology_map.items()
|
|
141
|
+
# ]
|
|
142
|
+
# return translations_by_ontology
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# def _get_display_name_for_key(self, key, requested_languages:str):
|
|
146
|
+
# for tds in self._translation_data_sources:
|
|
147
|
+
# if term := tds.get_translations_for(key):
|
|
148
|
+
# # try the languages requested by the user
|
|
149
|
+
# for l in requested_languages:
|
|
150
|
+
# if dn := term.in_language(l):
|
|
151
|
+
# return dn
|
|
152
|
+
# # remove the country codes and try the again
|
|
153
|
+
# for l_fallback in [l.split('-')[0] for l in requested_languages]:
|
|
154
|
+
# if dn := term.in_language(l_fallback):
|
|
155
|
+
# return dn
|
|
156
|
+
# # use the server fallback language
|
|
157
|
+
# if dn := term.in_language(self._default_language):
|
|
158
|
+
# return
|
|
159
|
+
# warnings.warn(f'No translation for {key}')
|
|
160
|
+
# return None
|
|
161
|
+
|
|
162
|
+
def _add_display_names(self, attributes_of_pac:AttributesOfPACID, language:str) -> str:
|
|
163
|
+
'''
|
|
164
|
+
adds the display names in the requested language to attribute group and attributes.
|
|
165
|
+
if no translation can be found in this language it IMMEDIATELY falls back to some - probably inappropriate- magic.
|
|
166
|
+
Note: The server checks for completeness of translations at initialization. Make sure to resolve warnings there and
|
|
167
|
+
this function should never get into the situation not to find translations.
|
|
168
|
+
'''
|
|
169
|
+
for ag in attributes_of_pac.attribute_groups:
|
|
170
|
+
if dn := self._get_display_name_for_key(ag.key, language):
|
|
171
|
+
ag.label = dn
|
|
172
|
+
else:
|
|
173
|
+
ag.label = ag.key.split('/')[-1]
|
|
174
|
+
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{ag.key}' in '{language}'. Falling back to '{ag.label}'")
|
|
175
|
+
for a in ag.attributes:
|
|
176
|
+
if dn := self._get_display_name_for_key(a.key, language):
|
|
177
|
+
a.label = dn
|
|
178
|
+
else:
|
|
179
|
+
a.label = a.key.split('/')[-1]
|
|
180
|
+
rich.print(f"[yellow]WARNING:[/yellow] No translation for '{a.key}' in '{language}'. Falling back to '{a.label}' ")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _get_display_name_for_key(self, key, language:str):
|
|
188
|
+
'''call this only with a language you know there is a translation for'''
|
|
189
|
+
for tds in self._translation_data_sources:
|
|
190
|
+
if term := tds.get_translations_for(key):
|
|
191
|
+
return term.in_language(language)
|
|
192
|
+
warnings.warn(f'No translation for {key}.')
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _find_response_language(self, requested_languages):
|
|
197
|
+
'''finds the language the server will respond in'''
|
|
198
|
+
if not requested_languages:
|
|
199
|
+
return self._default_language
|
|
200
|
+
|
|
201
|
+
for language in requested_languages:
|
|
202
|
+
if language in self._supported_languages:
|
|
203
|
+
return language
|
|
204
|
+
|
|
205
|
+
# remove the country codes and try the again
|
|
206
|
+
for l_fallback in [lang.split('-')[0] for lang in requested_languages]:
|
|
207
|
+
if language in self._supported_languages:
|
|
208
|
+
return l_fallback
|
|
209
|
+
|
|
210
|
+
return self._default_language
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def capabilities(self):
|
|
215
|
+
return ServerCapabilities(supported_languages=self._supported_languages,
|
|
216
|
+
default_language=self._default_language,
|
|
217
|
+
available_attribute_groups= [ds.attribute_group_key for ds in self._attribute_group_data_sources]).model_dump_json()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod, abstractproperty
|
|
2
|
+
import json
|
|
3
|
+
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
import rich
|
|
6
|
+
from labfreed.utilities.translations import Terms, Term
|
|
7
|
+
|
|
8
|
+
class TranslationDataSource(ABC):
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def get_translations_for(self, key:str) -> Term:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
@abstractproperty
|
|
15
|
+
def supported_languages(self) -> set[str]:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DictTranslationDataSource(TranslationDataSource):
|
|
20
|
+
def __init__(self, data:Terms, supported_languages:set[str]) -> None:
|
|
21
|
+
|
|
22
|
+
self._data = data
|
|
23
|
+
|
|
24
|
+
#check that there are translations for the supported languages. Adjust supported_languages if needed
|
|
25
|
+
supported_languages = set(supported_languages) # to ensure uniqueness
|
|
26
|
+
for language in supported_languages.copy():
|
|
27
|
+
if translation_missing := self._list_missing_translations_to(language):
|
|
28
|
+
rich.print(f"[bold yellow]WARNING:[/bold yellow]: Translation to '{language}' missing for {translation_missing}")
|
|
29
|
+
supported_languages.remove(language)
|
|
30
|
+
|
|
31
|
+
self._supported_languages = supported_languages
|
|
32
|
+
|
|
33
|
+
def _list_missing_translations_to(self, language:str):
|
|
34
|
+
translation_missing_for_keys = []
|
|
35
|
+
for term in self._data.terms:
|
|
36
|
+
if language not in [t.language_code for t in term.translations]:
|
|
37
|
+
translation_missing_for_keys.append(term.key)
|
|
38
|
+
return translation_missing_for_keys
|
|
39
|
+
|
|
40
|
+
def get_translations_for(self, key:str) -> Term:
|
|
41
|
+
t = self._data.translations_for_term(key)
|
|
42
|
+
return t
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def supported_languages(self) -> set[str]:
|
|
46
|
+
return self._supported_languages
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class JsonFileTranslationDataSource(DictTranslationDataSource):
|
|
53
|
+
def __init__(self, path:str) -> None:
|
|
54
|
+
with open(path) as f:
|
|
55
|
+
data = json.load(f)
|
|
56
|
+
try:
|
|
57
|
+
super().__init__(data=data)
|
|
58
|
+
except ValidationError as e:
|
|
59
|
+
e.add_note('Json must be convertible to OnthologyTranslationDataSource')
|
|
60
|
+
raise e
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MetaAttributeKeys(Enum):
|
|
5
|
+
DISPLAYNAME = "https://schema.org/name"
|
|
6
|
+
IMAGE = "https://schema.org/image"
|
|
7
|
+
ALIAS = "https://schema.org/alternateName"
|
|
8
|
+
DESCRIPTION = "https://schema.org/description"
|
|
9
|
+
GROUPKEY = "https://labfreed.org/attribute_metadata_group"
|
|
10
|
+
|
|
11
|
+
|
|
@@ -43,6 +43,25 @@ class Category(LabFREED_BaseModel):
|
|
|
43
43
|
def __str__(self):
|
|
44
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
45
|
return s
|
|
46
|
+
|
|
47
|
+
def segments_as_dict(self):
|
|
48
|
+
''' returns the segments in a dict, with nice keys and values'''
|
|
49
|
+
out = dict()
|
|
50
|
+
for field_name, field_info in self.model_fields.items():
|
|
51
|
+
if field_name =='additional_segments':
|
|
52
|
+
continue
|
|
53
|
+
if v := getattr(self, field_name):
|
|
54
|
+
if field_info.alias:
|
|
55
|
+
k = f"{field_name} ({ field_info.alias})"
|
|
56
|
+
else:
|
|
57
|
+
k = f"{field_name}"
|
|
58
|
+
out.update({k : v } )
|
|
59
|
+
|
|
60
|
+
for s in getattr(self, 'additional_segments'):
|
|
61
|
+
out.update( {s.key or '' : s.value })
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
46
65
|
|
|
47
66
|
|
|
48
67
|
|
labfreed/pac_cat/pac_cat.py
CHANGED
|
@@ -52,7 +52,7 @@ class PAC_CAT(PAC_ID):
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
@classmethod
|
|
55
|
-
def from_pac_id(cls, pac_id:PAC_ID) ->
|
|
55
|
+
def from_pac_id(cls, pac_id:PAC_ID) -> PAC_CAT:
|
|
56
56
|
'''Constructs a PAC-CAT from a PAC-ID'''
|
|
57
57
|
return PAC_CAT(issuer=pac_id.issuer, identifier=pac_id.identifier)
|
|
58
58
|
|
|
@@ -125,6 +125,13 @@ class PAC_CAT(PAC_ID):
|
|
|
125
125
|
)
|
|
126
126
|
return self
|
|
127
127
|
|
|
128
|
+
|
|
129
|
+
@model_validator(mode='after')
|
|
130
|
+
def _check_identifier_segment_keys_are_unique(self) -> Self:
|
|
131
|
+
''' override the validator of PAC-ID: in PAC-CAT segments can replicate in different categories'''
|
|
132
|
+
return self
|
|
133
|
+
|
|
134
|
+
|
|
128
135
|
def print_categories(self):
|
|
129
136
|
table = Table(title=f'Categories in {str(self)}', show_header=False)
|
|
130
137
|
table.add_column('0')
|
labfreed/pac_id/extension.py
CHANGED
|
@@ -3,12 +3,12 @@ from abc import ABC, abstractproperty
|
|
|
3
3
|
|
|
4
4
|
from pydantic import model_validator
|
|
5
5
|
|
|
6
|
-
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
6
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class ExtensionBase(ABC):
|
|
10
|
-
name: str
|
|
11
|
-
type: str
|
|
10
|
+
name: str|None
|
|
11
|
+
type: str|None
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
@abstractproperty
|
|
@@ -16,14 +16,17 @@ class ExtensionBase(ABC):
|
|
|
16
16
|
raise NotImplementedError("Subclasses must implement 'data'")
|
|
17
17
|
|
|
18
18
|
def __str__(self):
|
|
19
|
-
|
|
19
|
+
if self.name and self.type:
|
|
20
|
+
return f'{self.name}${self.type}/{self.data}'
|
|
21
|
+
else:
|
|
22
|
+
return self.data
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
|
|
23
26
|
class Extension(LabFREED_BaseModel,ExtensionBase):
|
|
24
27
|
'''Implementation of Extension for unknown extension types'''
|
|
25
|
-
name:str
|
|
26
|
-
type:str
|
|
28
|
+
name:str|None
|
|
29
|
+
type:str|None
|
|
27
30
|
data_:str
|
|
28
31
|
|
|
29
32
|
@property
|
|
@@ -31,7 +34,7 @@ class Extension(LabFREED_BaseModel,ExtensionBase):
|
|
|
31
34
|
return self.data_
|
|
32
35
|
|
|
33
36
|
@staticmethod
|
|
34
|
-
def create(*, name, type, data):
|
|
37
|
+
def create(*, name:str|None, type:str|None, data:str):
|
|
35
38
|
return Extension(name=name, type=type, data=data)
|
|
36
39
|
|
|
37
40
|
@model_validator(mode='before')
|
|
@@ -44,5 +47,27 @@ class Extension(LabFREED_BaseModel,ExtensionBase):
|
|
|
44
47
|
model_config = {
|
|
45
48
|
"extra": "allow", # Allow extra keys during pre-validation
|
|
46
49
|
}
|
|
50
|
+
|
|
51
|
+
@model_validator(mode='after')
|
|
52
|
+
def validate_model(self):
|
|
53
|
+
if self.name and not self.type:
|
|
54
|
+
raise ValueError('Extension has a name, but no type. Either set both or none')
|
|
55
|
+
|
|
56
|
+
if self.type and not self.name:
|
|
57
|
+
raise ValueError('Extension has a type, but no name. Either set both or none')
|
|
58
|
+
|
|
59
|
+
if not self.type and not self.name:
|
|
60
|
+
self._add_validation_message(msg="Extensions has no name and type. It is RECOMMENDED to specify name and type.",
|
|
61
|
+
level=ValidationMsgLevel.RECOMMENDATION,
|
|
62
|
+
source=f"Extension '{self.data[0:10] if len(self.data)>10 else self.data}'",
|
|
63
|
+
highlight_pattern=self.data)
|
|
64
|
+
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
model_config = {
|
|
70
|
+
"extra": "allow", # Allow extra keys during pre-validation
|
|
71
|
+
}
|
|
47
72
|
|
|
48
73
|
|
labfreed/pac_id/pac_id.py
CHANGED
|
@@ -45,9 +45,9 @@ class PAC_ID(LabFREED_BaseModel):
|
|
|
45
45
|
from labfreed.pac_id.url_parser import PAC_Parser
|
|
46
46
|
return PAC_Parser.from_url(url, try_pac_cat=try_pac_cat, suppress_validation_errors=suppress_validation_errors, extension_interpreters=extension_interpreters)
|
|
47
47
|
|
|
48
|
-
def to_url(self, use_short_notation:None|bool=None, uppercase_only=False) -> str:
|
|
48
|
+
def to_url(self, use_short_notation:None|bool=None, uppercase_only=False, include_extensions:bool=True) -> str:
|
|
49
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)
|
|
50
|
+
return PACID_Serializer.to_url(self, use_short_notation=use_short_notation, uppercase_only=uppercase_only, include_extensions=include_extensions)
|
|
51
51
|
|
|
52
52
|
def to_json(self, indent=None) -> str:
|
|
53
53
|
if not indent:
|
labfreed/pac_id/url_parser.py
CHANGED
|
@@ -69,7 +69,7 @@ class PAC_Parser():
|
|
|
69
69
|
extensions = cls._parse_extensions(ext_str)
|
|
70
70
|
if extensions and extension_interpreters:
|
|
71
71
|
for i, e in enumerate(extensions):
|
|
72
|
-
if interpreter := extension_interpreters.get(e.type):
|
|
72
|
+
if interpreter := extension_interpreters.get(e.type or ''):
|
|
73
73
|
extensions[i] = interpreter.from_extension(e)
|
|
74
74
|
pac_id.extensions = extensions
|
|
75
75
|
|
|
@@ -83,6 +83,8 @@ class PAC_Parser():
|
|
|
83
83
|
def _parse_pac_id(cls,id_str:str) -> "PAC_ID":
|
|
84
84
|
# m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?\..+?)/(?P<identifier>.*)', id_str)
|
|
85
85
|
m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?)/(?P<identifier>.*)', id_str)
|
|
86
|
+
if not m:
|
|
87
|
+
raise LabFREED_ValidationError(f'{id_str} does not match the pattern expected for PAC-ID')
|
|
86
88
|
d = m.groupdict()
|
|
87
89
|
|
|
88
90
|
id_segments = list()
|
|
@@ -127,7 +129,7 @@ class PAC_Parser():
|
|
|
127
129
|
|
|
128
130
|
defaults = MappingProxyType(
|
|
129
131
|
{
|
|
130
|
-
0: { 'name': 'N', 'type': '
|
|
132
|
+
0: { 'name': 'N', 'type': 'TEXT'},
|
|
131
133
|
1: { 'name': 'SUM', 'type': 'TREX'}
|
|
132
134
|
}
|
|
133
135
|
)
|
|
@@ -146,8 +148,6 @@ class PAC_Parser():
|
|
|
146
148
|
if defaults:
|
|
147
149
|
name = defaults.get(i).get('name')
|
|
148
150
|
type = defaults.get(i).get('type')
|
|
149
|
-
else:
|
|
150
|
-
raise ValueError('extension number {i}, must have name and type')
|
|
151
151
|
|
|
152
152
|
#convert to subtype if they were given
|
|
153
153
|
e = Extension.create(name=name, type=type, data=data)
|
|
@@ -9,7 +9,7 @@ class PACID_Serializer():
|
|
|
9
9
|
'''Represents a PAC-ID including it's extensions'''
|
|
10
10
|
|
|
11
11
|
@classmethod
|
|
12
|
-
def to_url(cls, pac:PAC_ID, use_short_notation:bool|None=None, uppercase_only=False) -> str:
|
|
12
|
+
def to_url(cls, pac:PAC_ID, use_short_notation:bool|None=None, uppercase_only=False, include_extensions:bool=True) -> str:
|
|
13
13
|
"""Serializes the PAC-ID including extensions.
|
|
14
14
|
|
|
15
15
|
Args:
|
|
@@ -24,8 +24,12 @@ class PACID_Serializer():
|
|
|
24
24
|
"""
|
|
25
25
|
identifier_str = cls._serialize_identifier(pac, use_short_notation=use_short_notation)
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
if include_extensions:
|
|
28
|
+
use_short_notation_for_extensions = True if use_short_notation is None else use_short_notation
|
|
29
|
+
extensions_str = cls._serialize_extensions(pac.extensions, use_short_notation=use_short_notation_for_extensions)
|
|
30
|
+
else:
|
|
31
|
+
extensions_str = ""
|
|
32
|
+
|
|
29
33
|
out = f"HTTPS://PAC.{pac.issuer}{identifier_str}{extensions_str}"
|
|
30
34
|
|
|
31
35
|
if uppercase_only:
|
|
@@ -75,8 +79,10 @@ class PACID_Serializer():
|
|
|
75
79
|
continue
|
|
76
80
|
else:
|
|
77
81
|
short_notation = False
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
if e.name and e.type:
|
|
83
|
+
out += f'*{e.name}${e.type}/{e.data}'
|
|
84
|
+
else:
|
|
85
|
+
out += f'*{e.data}'
|
|
80
86
|
return out
|
|
81
87
|
|
|
82
88
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
1
|
import json
|
|
3
2
|
import re
|
|
4
3
|
from typing import Self
|
|
@@ -9,11 +8,7 @@ import jsonpath_ng.ext as jsonpath
|
|
|
9
8
|
|
|
10
9
|
from labfreed.pac_id_resolver.services import Service, ServiceGroup
|
|
11
10
|
from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
|
|
12
|
-
from labfreed.pac_id_resolver.cit_common import (
|
|
13
|
-
_validate_service_name,
|
|
14
|
-
_validate_application_intent,
|
|
15
|
-
_validate_service_type,
|
|
16
|
-
ServiceType)
|
|
11
|
+
from labfreed.pac_id_resolver.cit_common import ( ServiceType)
|
|
17
12
|
|
|
18
13
|
|
|
19
14
|
__all__ = [
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from functools import lru_cache
|
|
2
2
|
import logging
|
|
3
|
-
import traceback
|
|
4
3
|
from typing import Self
|
|
5
|
-
import yaml
|
|
6
4
|
from requests import get
|
|
7
5
|
|
|
8
6
|
|
|
@@ -32,7 +30,7 @@ def cit_from_str(s:str, origin:str='') -> CIT_v1|CIT_v2:
|
|
|
32
30
|
cit2 = None
|
|
33
31
|
try:
|
|
34
32
|
cit1 = CIT_v1.from_csv(s, origin)
|
|
35
|
-
cit_version = 'v1'
|
|
33
|
+
cit_version = 'v1' # noqa: F841
|
|
36
34
|
except Exception:
|
|
37
35
|
cit1 = None
|
|
38
36
|
|
|
@@ -66,11 +64,18 @@ class PAC_ID_Resolver():
|
|
|
66
64
|
self._cits = cits
|
|
67
65
|
|
|
68
66
|
|
|
69
|
-
def resolve(self,
|
|
67
|
+
def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_cit=True) -> list[ServiceGroup]:
|
|
70
68
|
'''Resolve a PAC-ID'''
|
|
71
|
-
if isinstance(
|
|
72
|
-
pac_id = PAC_CAT.from_url(
|
|
73
|
-
pac_id_catless = PAC_ID.from_url(
|
|
69
|
+
if isinstance(pac_id, str):
|
|
70
|
+
pac_id = PAC_CAT.from_url(pac_id)
|
|
71
|
+
pac_id_catless = PAC_ID.from_url(pac_id, try_pac_cat=False)
|
|
72
|
+
|
|
73
|
+
# it's likely to h
|
|
74
|
+
if isinstance(pac_id, PAC_ID):
|
|
75
|
+
pac_id_catless = PAC_ID.from_url(pac_id.to_url(), try_pac_cat=False)
|
|
76
|
+
else:
|
|
77
|
+
raise ValueError('pac_id is invalid. Should be a PAC-ID in url form or a PAC-ID object')
|
|
78
|
+
|
|
74
79
|
|
|
75
80
|
cits = self._cits.copy()
|
|
76
81
|
if use_issuer_cit:
|