labfreed 1.0.0a12__py3-none-any.whl → 1.0.0a14__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/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "1.0.0a12"
5
+ __version__ = "1.0.0a14"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -7,9 +7,6 @@ from labfreed.labfreed_extended.app.pac_info.pac_info import PacInfo
7
7
  from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
8
8
  from labfreed.pac_attributes.client.client import AttributeClient, http_attribute_request_default_callback_factory
9
9
  from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
10
- from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
11
- from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
12
-
13
10
 
14
11
  from labfreed.pac_id.pac_id import PAC_ID
15
12
  from labfreed.pac_id_resolver.resolver import PAC_ID_Resolver, cit_from_str
@@ -35,11 +32,15 @@ class Labfreed_App_Infrastructure():
35
32
  self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
36
33
 
37
34
 
38
- def add_cit(self, cit:str):
35
+ def add_resolver_config(self, cit:str):
39
36
  cit = cit_from_str(cit)
40
37
  if not cit:
41
38
  raise ValueError('the cit could not be parsed. Neither as v1 or v2')
42
- self._resolver._cits.append(cit)
39
+ self._resolver._resolver_configs.add(cit)
40
+
41
+ def remove_resolver_config(self, resolver_config:str):
42
+ resolver_config = cit_from_str(resolver_config)
43
+ self._resolver._resolver_configs.discard(resolver_config)
43
44
 
44
45
 
45
46
  def process_pac(self, pac_url, markup=None):
@@ -63,6 +64,15 @@ class Labfreed_App_Infrastructure():
63
64
  sg_user_handovers.append(ServiceGroup(origin=sg.origin, services=user_handovers))
64
65
  pac_info.user_handovers = sg_user_handovers
65
66
 
67
+ # Actions
68
+ sg_actions = []
69
+ for sg in service_groups:
70
+ actions = [s for s in sg.services if s.service_type == 'action-generic']
71
+
72
+ if actions:
73
+ sg_actions.append(ServiceGroup(origin=sg.origin, services=actions))
74
+ pac_info.actions = sg_actions
75
+
66
76
  # Attributes
67
77
  attribute_groups = {}
68
78
  for sg in service_groups:
@@ -8,8 +8,9 @@ from pydantic import BaseModel, Field
8
8
  from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes, pyReference, pyResource
9
9
  from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
10
10
  from labfreed.pac_cat.pac_cat import PAC_CAT
11
+ from labfreed.pac_cat.predefined_categories import PredefinedCategory
11
12
  from labfreed.pac_id.pac_id import PAC_ID
12
- from labfreed.pac_id_resolver.services import ServiceGroup
13
+ from labfreed.pac_id_resolver.services import ServiceGroup, Service
13
14
  from labfreed.labfreed_extended.app.formatted_print import StringIOLineBreak
14
15
  from labfreed.trex.pythonic.data_table import DataTable
15
16
  from labfreed.trex.pythonic.pyTREX import pyTREX
@@ -19,30 +20,133 @@ from labfreed.well_known_extensions.display_name_extension import DisplayNameExt
19
20
  class PacInfo(BaseModel):
20
21
  """A convenient collection of information about a PAC-ID"""
21
22
  pac_id:PAC_ID
23
+
22
24
  user_handovers: list[ServiceGroup] = Field(default_factory=list)
25
+ actions: list[ServiceGroup] = Field(default_factory=list)
23
26
  attribute_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
24
27
 
25
- @property
28
+
29
+ # info about pac-id
30
+
31
+ @cached_property
32
+ def is_item_serialized(self) -> bool|None: #indicates if the item is at product level (e.g. BAL500), as opposed to a serialized instance thereof (e.g. BAL500 with SN 1234)
33
+ if not isinstance(self.pac_id, PAC_CAT):
34
+ return None
35
+ cat = self.main_category
36
+ if not isinstance(cat, PredefinedCategory):
37
+ return None
38
+
39
+ return cat.is_serialized
40
+
41
+
42
+ @cached_property
26
43
  def pac_url(self):
27
44
  return self.pac_id.to_url(include_extensions=False)
28
45
 
29
- @property
46
+ @cached_property
30
47
  def main_category(self):
31
48
  if isinstance(self.pac_id, PAC_CAT):
32
49
  return self.pac_id.categories[0]
33
50
  else:
34
51
  return None
35
52
 
36
- @property
37
- def attached_data(self):
53
+
54
+
55
+ # attached data
56
+
57
+ @cached_property
58
+ def attached_data(self) -> dict[str, pyTREX]:
38
59
  return { trex_ext.name: pyTREX.from_trex(trex=trex_ext.trex) for trex_ext in self.pac_id.get_extension_of_type('TREX')}
39
60
 
40
61
 
41
- @property
42
- def summary(self):
43
- return self.pac_id.get_extension('SUM')
62
+ @cached_property
63
+ def summary(self) -> pyTREX:
64
+ return pyTREX.from_trex(self.pac_id.get_extension('SUM').trex)
65
+
66
+
67
+ @cached_property
68
+ def status(self) -> pyTREX:
69
+ return pyTREX.from_trex(self.pac_id.get_extension('STATUS').trex)
70
+
71
+
72
+
73
+
74
+ # Handovers and Actions
75
+
76
+ def get_user_handovers_by_intent(self, intent:str, partial_match=False) -> list[Service]:
77
+ services = [s for sg in self.user_handovers for s in sg.services if self._match_intent(intent, s.application_intents, partial_match)]
78
+ return services
79
+
80
+ def get_user_handover_by_intent(self, intent:str, partial_match=False, mode="first"):
81
+ handovers = self.get_user_handovers_by_intent(intent=intent, partial_match=partial_match)
82
+ return self._pick_from_list(handovers, mode)
83
+
84
+
85
+
86
+ def get_actions_by_intent(self, intent:str, partial_match=False) -> list[Service]:
87
+ actions = [s for sg in self.actions for s in sg.services if self._match_intent(intent, s.application_intents, partial_match)]
88
+ return actions
89
+
90
+ def get_action_by_intent(self, intent:str, partial_match=False, mode="first"):
91
+ actions = self.get_actions_by_intent(intent=intent, partial_match=partial_match)
92
+ return self._pick_from_list(actions, mode)
93
+
94
+
95
+ def _match_intent(self, intent, intents, partial_match):
96
+ if partial_match:
97
+ # intent 'document' should match 'document-operation-manual' etc
98
+ return any([intent in i for i in intents])
99
+ else:
100
+ # only exact match
101
+ return intent in intents
102
+
103
+
104
+ @cached_property
105
+ def important_handovers(self) -> list[Service]:
106
+ return self.get_user_handovers_by_intent('important')
107
+
108
+
109
+ @cached_property
110
+ def important_actions(self) -> list[Service]:
111
+ return self.get_actions_by_intent('important')
112
+
113
+
114
+
115
+
116
+
117
+
118
+ # Attributes
119
+
120
+ @cached_property
121
+ def _all_attributes(self) -> dict[str, pyAttribute]:
122
+ out = {}
123
+ for ag in self.attribute_groups.values():
124
+ out.update(ag.attributes)
125
+ return out
126
+
127
+
128
+ def get_attributes(self, key:str) -> list[pyAttribute]:
129
+ attributes = [a for k, a in self._all_attributes.items() if key in a.key]
130
+ return attributes
44
131
 
45
- @property
132
+ def get_attribute(self, key:str, mode="first"):
133
+ attributes = self.get_attributes(key)
134
+ return self._pick_from_list(attributes, mode)
135
+
136
+
137
+ def _pick_from_list(self, list, mode):
138
+ if mode not in ['first', 'last']:
139
+ raise ValueError('mode must be "first or "last" ')
140
+
141
+ if not list:
142
+ return None
143
+ if mode == 'first':
144
+ return list[0]
145
+ if mode == 'last':
146
+ return list[-1]
147
+
148
+
149
+ @cached_property
46
150
  def image_url(self) -> str:
47
151
  image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
48
152
  if isinstance(image_attr.value, pyResource):
@@ -51,7 +155,7 @@ class PacInfo(BaseModel):
51
155
  return image_attr.value
52
156
 
53
157
 
54
- @property
158
+ @cached_property
55
159
  def display_name(self) -> str|None:
56
160
  display_name = None
57
161
  pac = self.pac_id
@@ -63,30 +167,41 @@ class PacInfo(BaseModel):
63
167
  if dn_attr := self._all_attributes.get(MetaAttributeKeys.DISPLAYNAME.value):
64
168
  dn = dn_attr.value
65
169
  display_name = dn + f' ( aka {display_name} )' if display_name else dn
170
+
171
+ if not display_name and self.main_category:
172
+ seg_240 = [s for s in self.main_category.segments if s.key=="240"]
173
+ display_name = seg_240[0].value
174
+
66
175
  return display_name
67
176
 
68
177
 
69
- @property
178
+ @cached_property
70
179
  def safety_pictograms(self) -> dict[str, pyAttribute]:
71
180
  pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
72
181
  return pictogram_attributes
73
182
 
74
183
 
75
- @property
184
+ @cached_property
76
185
  def qualification_state(self) -> pyAttribute:
77
186
  if state := self._all_attributes.get("https://labfreed.org/qualification/status"):
78
187
  return state
188
+
189
+
190
+
191
+
192
+
79
193
 
80
-
81
- @cached_property
82
- def _all_attributes(self) -> dict[str, pyAttribute]:
83
- out = {}
84
- for ag in self.attribute_groups.values():
85
- out.update(ag.attributes)
86
- return out
87
194
 
88
195
 
89
196
 
197
+
198
+
199
+
200
+
201
+
202
+ ########
203
+
204
+
90
205
  def format_for_print(self, markup:str='rich') -> str:
91
206
 
92
207
  printout = StringIOLineBreak(markup=markup)
@@ -126,37 +241,6 @@ class PacInfo(BaseModel):
126
241
 
127
242
 
128
243
 
129
- def render_html(self, hide_attribute_groups:list[str]=[]) -> str:
130
- return PACInfo_HTMLRenderer.render_template('pac_info_main.jinja.html',
131
- pac_info = self,
132
- hide_attribute_groups=hide_attribute_groups
133
- )
134
-
135
- def render_html_card(self) -> str:
136
- return PACInfo_HTMLRenderer.render_template('pac_info_card.jinja.html',
137
- pac_info = self
138
- )
139
-
140
-
141
- class PACInfo_HTMLRenderer():
142
- TEMPLATES_DIR = Path(__file__).parent / "html_renderer"
143
- jinja_env = Environment(
144
- loader=FileSystemLoader(str(TEMPLATES_DIR), encoding="utf-8"),
145
- autoescape=select_autoescape(enabled_extensions=("html", "jinja", "jinja2", "jinja.html")),
146
- )
147
-
148
- @classmethod
149
- def render_template(cls, template_name:str, pac_info:PacInfo, hide_attribute_groups):
150
- # --- Jinja env pointing at /html_renderer ---
151
- template = cls.jinja_env.get_template("pac_info.jinja.html")
152
- html = template.render(
153
- pac=pac_info.pac_id,
154
- pac_info=pac_info, # your object
155
- hide_attribute_groups=hide_attribute_groups,
156
- is_data_table = lambda value: isinstance(value, DataTable),
157
- is_url = lambda s: isinstance(s, str) and urlparse(s).scheme in ('http', 'https') and bool(urlparse(s).netloc),
158
- is_image = lambda s: isinstance(s, str) and s.lower().startswith('http') and s.lower().endswith(('.jpg','.jpeg','.png','.gif','.bmp','.webp','.svg','.tif','.tiff')),
159
- is_reference = lambda s: isinstance(s, pyReference) ,
160
- )
161
- return html
162
-
244
+
245
+
246
+
@@ -4,6 +4,10 @@ import re
4
4
  from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
5
5
  from typing import Any, List, Set
6
6
 
7
+ import warnings
8
+ import functools
9
+ import inspect
10
+
7
11
  from rich import print
8
12
  from rich.table import Table
9
13
 
@@ -256,3 +260,11 @@ def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]
256
260
  def _quote_texts(texts:list[str]):
257
261
  return ','.join([f"'{t}'" for t in texts])
258
262
 
263
+
264
+
265
+
266
+
267
+
268
+
269
+
270
+
@@ -36,7 +36,7 @@ class AttributeBase(LabFREED_BaseModel, ABC):
36
36
 
37
37
  class DateTimeAttribute(AttributeBase):
38
38
  type: Literal["datetime"]
39
- value: datetime | list[datetime]
39
+ value: datetime
40
40
 
41
41
  @field_validator('value', mode='before')
42
42
  def set_utc__if_naive(cls, value):
@@ -44,60 +44,113 @@ class DateTimeAttribute(AttributeBase):
44
44
  return ensure_utc(value)
45
45
  else:
46
46
  return value
47
+
48
+ class DateTimeListAttribute(AttributeBase):
49
+ type: Literal["datetime-list"]
50
+ value: list[datetime]
51
+
52
+ @field_validator('value', mode='before')
53
+ def set_utc__if_naive(cls, value):
54
+ value_out = []
55
+ for v in value:
56
+ if isinstance(v, datetime):
57
+ value_out.append(ensure_utc(v))
58
+ else:
59
+ return ValueError(f'{v} is of type {type(v)}. It must be datetime')
60
+
61
+
47
62
 
48
63
  class BoolAttribute(AttributeBase):
49
64
  type: Literal["bool"]
50
- value: bool | list[bool]
65
+ value: bool
66
+
67
+ class BoolListAttribute(AttributeBase):
68
+ type: Literal["bool-list"]
69
+ value: list[bool]
70
+
71
+
72
+
51
73
 
52
74
  class TextAttribute(AttributeBase):
53
75
  type: Literal["text"]
54
- value: str | list[str]
76
+ value: str
77
+
78
+ @model_validator(mode='after')
79
+ def _validate_value(self):
80
+ _validate_text(self, self.value)
81
+ return self
82
+
83
+ class TextListAttribute(AttributeBase):
84
+ type: Literal["text-list"]
85
+ value: list[str]
55
86
 
56
87
  @model_validator(mode='after')
57
88
  def _validate_value(self):
58
89
  l = [self.value] if isinstance(self.value, str) else self.value
59
90
  for v in l:
60
- if len(v) > 5000:
61
- self._add_validation_message(
62
- source="Text Attribute",
63
- level=ValidationMsgLevel.WARNING, # noqa: F821
64
- msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
65
- highlight_pattern = f'{v}'
66
- )
91
+ _validate_text(self, v)
67
92
  return self
93
+
94
+
95
+ def _validate_text(mdl:LabFREED_BaseModel, v):
96
+ if len(v) > 5000:
97
+ mdl._add_validation_message(
98
+ source="Text Attribute",
99
+ level=ValidationMsgLevel.WARNING, # noqa: F821
100
+ msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
101
+ highlight_pattern = f'{v}'
102
+ )
68
103
 
69
104
 
105
+
70
106
  class ReferenceAttribute(AttributeBase):
71
107
  type: Literal["reference"]
72
- value: str | list[str]
108
+ value: str
109
+
110
+ class ReferenceListAttribute(AttributeBase):
111
+ type: Literal["reference-list"]
112
+ value: list[str]
113
+
73
114
 
115
+
74
116
 
75
117
  class ResourceAttribute(AttributeBase):
76
118
  type: Literal["resource"]
77
- value: str | list[str]
119
+ value: str
120
+
121
+ @model_validator(mode='after')
122
+ def _validate_value(self):
123
+ _validate_resource(self, self.value)
124
+
125
+ class ResourceListAttribute(AttributeBase):
126
+ type: Literal["resource-list"]
127
+ value: list[str]
78
128
 
79
129
  @model_validator(mode='after')
80
130
  def _validate_value(self):
81
131
  value_list = self.value if isinstance(self.value, list) else [self.value]
82
132
  for v in value_list:
83
- r = urlparse(v)
84
- if not all([r.scheme, r.netloc]):
85
- self._add_validation_message(
86
- source="Resource Attribute",
87
- level=ValidationMsgLevel.ERROR, # noqa: F821
88
- msg=f"Must be a valid url",
89
- highlight_pattern = f'{v}'
90
- )
91
- pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
92
- if not bool(pattern.search(v)):
93
- self._add_validation_message(
94
- source="Resource Attribute",
95
- level=ValidationMsgLevel.WARNING, # noqa: F821
96
- msg=f"It is RECOMMENDED resource links end with a file extension",
97
- highlight_pattern = f'{v}'
98
- )
133
+ _validate_resource(self, v)
99
134
  return self
100
135
 
136
+ def _validate_resource(mdl:LabFREED_BaseModel, v):
137
+ r = urlparse(v)
138
+ if not all([r.scheme, r.netloc]):
139
+ mdl._add_validation_message(
140
+ source="Resource Attribute",
141
+ level=ValidationMsgLevel.ERROR, # noqa: F821
142
+ msg="Must be a valid url",
143
+ highlight_pattern = f'{v}'
144
+ )
145
+ pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
146
+ if not bool(pattern.search(v)):
147
+ mdl._add_validation_message(
148
+ source="Resource Attribute",
149
+ level=ValidationMsgLevel.WARNING, # noqa: F821
150
+ msg="It is RECOMMENDED resource links end with a file extension",
151
+ highlight_pattern = f'{v}'
152
+ )
153
+
101
154
 
102
155
 
103
156
  class NumericValue(LabFREED_BaseModel):
@@ -151,11 +204,22 @@ class NumericValue(LabFREED_BaseModel):
151
204
 
152
205
  class NumericAttribute(AttributeBase):
153
206
  type: Literal["numeric"]
154
- value: NumericValue | list[NumericValue]
207
+ value: NumericValue
208
+
209
+ class NumericListAttribute(AttributeBase):
210
+ type: Literal["numeric-list"]
211
+ value: list[NumericValue]
212
+
213
+
155
214
 
156
215
  class ObjectAttribute(AttributeBase):
157
216
  type: Literal["object"]
158
- value: dict[str, Any] |list[dict[str, Any]]
217
+ value: dict[str, Any]
218
+
219
+
220
+ class ObjectListAttribute(AttributeBase):
221
+ type: Literal["object-list"]
222
+ value: list[dict[str, Any]]
159
223
 
160
224
 
161
225
 
@@ -168,7 +232,15 @@ Attribute = Annotated[
168
232
  TextAttribute,
169
233
  NumericAttribute,
170
234
  ResourceAttribute,
171
- ObjectAttribute
235
+ ObjectAttribute,
236
+
237
+ ReferenceListAttribute,
238
+ DateTimeListAttribute,
239
+ BoolListAttribute,
240
+ TextListAttribute,
241
+ NumericListAttribute,
242
+ ResourceListAttribute,
243
+ ObjectListAttribute
172
244
  ],
173
245
  Field(discriminator="type")
174
246
  ]
@@ -11,7 +11,6 @@ except ImportError:
11
11
  raise ImportError("Please install labfreed with the [extended] extra: pip install labfreed[extended]")
12
12
 
13
13
 
14
- # from fastapi import FastAPI, Request
15
14
 
16
15
  class Authenticator(Protocol):
17
16
  def __call__(self, request) -> bool: ...
@@ -55,7 +55,7 @@ class Category(LabFREED_BaseModel):
55
55
  k = f"{field_name} ({ field_info.alias})"
56
56
  else:
57
57
  k = f"{field_name}"
58
- out.update({k : v } )
58
+ out.update({k : v } )
59
59
 
60
60
  for s in getattr(self, 'additional_segments', []):
61
61
  out.update( {s.key or '' : s.value })
@@ -1,5 +1,5 @@
1
1
  ## Materials
2
- from abc import ABC
2
+ from abc import ABC, abstractproperty
3
3
  from pydantic import Field, computed_field, model_validator
4
4
 
5
5
  from labfreed.labfreed_infrastructure import ValidationMsgLevel
@@ -18,6 +18,10 @@ class PredefinedCategory(Category, ABC):
18
18
  def segments(self) -> list[IDSegment]:
19
19
  return self._get_segments(use_short_notation=False)
20
20
 
21
+ @abstractproperty
22
+ def is_serialized(self) -> bool:
23
+ pass
24
+
21
25
  def _get_segments(self, use_short_notation=False) -> list[IDSegment]:
22
26
  segments = []
23
27
  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
@@ -65,12 +69,17 @@ class Material_Device(PredefinedCategory):
65
69
  if not self.serial_number:
66
70
  self._add_validation_message(
67
71
  source=f"Category {self.key}",
68
- level = ValidationMsgLevel.ERROR,
69
- msg=f'Category key {self.key} is missing mandatory field Serial Number',
72
+ level = ValidationMsgLevel.WARNING,
73
+ msg=f'Category key {self.key} is missing field Serial Number. Check that you are indeed to a product and not a specific device.',
70
74
  highlight_pattern = f"{self.key}"
71
75
  )
72
76
  return self
73
77
 
78
+ @property
79
+ def is_serialized(self) -> bool:
80
+ return bool(self.serial_number)
81
+
82
+
74
83
  class Material_Substance(PredefinedCategory):
75
84
  '''Represents the -MS category'''
76
85
  key: str = Field(default='-MS', frozen=True)
@@ -93,6 +102,11 @@ class Material_Substance(PredefinedCategory):
93
102
  )
94
103
  return self
95
104
 
105
+ @property
106
+ def is_serialized(self) -> bool:
107
+ return bool(self.batch_number or self.container_number or self.aliquot)
108
+
109
+
96
110
  class Material_Consumable(PredefinedCategory):
97
111
  '''Represents the -MC category'''
98
112
  key: str = Field(default='-MC', frozen=True)
@@ -115,6 +129,11 @@ class Material_Consumable(PredefinedCategory):
115
129
  )
116
130
  return self
117
131
 
132
+ @property
133
+ def is_serialized(self) -> bool:
134
+ return bool(self.batch_number or self.serial_number or self.aliquot)
135
+
136
+
118
137
  class Material_Misc(Material_Consumable):
119
138
  '''Represents the -MX category'''
120
139
  # same fields as Consumable
@@ -128,6 +147,8 @@ class Material_Misc(Material_Consumable):
128
147
  ''' Category segments, which are not defined in the specification'''
129
148
 
130
149
 
150
+
151
+
131
152
 
132
153
  ## Data
133
154
  class Data_Abstract(PredefinedCategory, ABC):
@@ -147,6 +168,11 @@ class Data_Abstract(PredefinedCategory, ABC):
147
168
  highlight_pattern = f"{self.key}"
148
169
  )
149
170
  return self
171
+
172
+ @property
173
+ def is_serialized(self) -> bool:
174
+ return True
175
+
150
176
 
151
177
  class Data_Result(Data_Abstract):
152
178
  '''Represents the -DR category'''
@@ -215,6 +241,11 @@ class Processor_Abstract(PredefinedCategory, ABC):
215
241
  highlight_pattern = f"{self.key}"
216
242
  )
217
243
  return self
244
+
245
+ @property
246
+ def is_serialized(self) -> bool:
247
+ return bool(self.processor_instance)
248
+
218
249
 
219
250
  class Processor_Software(Processor_Abstract):
220
251
  '''Represents the -PS category'''
@@ -233,6 +264,31 @@ class Processor_Misc(Processor_Abstract):
233
264
  ''' Category segments, which are not defined in the specification'''
234
265
 
235
266
 
267
+
268
+ class Misc(Category, ABC):
269
+ '''@private'''
270
+ key: str = Field(default='-X', frozen=True)
271
+ id:str|None = Field( alias='21')
272
+ additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
273
+ ''' Category segments, which are not defined in the specification'''
274
+
275
+ @model_validator(mode='after')
276
+ def _validate_mandatory_fields(self):
277
+ if not self.id:
278
+ self._add_validation_message(
279
+ source=f"Category {self.key}",
280
+ level = ValidationMsgLevel.ERROR,
281
+ msg=f"Category key {self.key} is missing mandatory field 'ID'",
282
+ highlight_pattern = f"{self.key}"
283
+ )
284
+ return self
285
+
286
+ @property
287
+ def is_serialized(self) -> bool:
288
+ return bool(self.id)
289
+
290
+
291
+
236
292
  category_key_to_class_map = {
237
293
  '-MD': Material_Device,
238
294
  '-MS': Material_Substance,
@@ -245,5 +301,6 @@ category_key_to_class_map = {
245
301
  '-DS': Data_Static,
246
302
  '-DX': Data_Misc,
247
303
  '-PS': Processor_Software,
248
- '-PX': Processor_Misc
304
+ '-PX': Processor_Misc,
305
+ '-X':Misc
249
306
  }
@@ -1,7 +1,7 @@
1
1
 
2
2
  from abc import ABC, abstractproperty
3
3
 
4
- from pydantic import model_validator
4
+ from pydantic import computed_field, model_validator
5
5
 
6
6
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel
7
7
 
@@ -29,6 +29,7 @@ class Extension(LabFREED_BaseModel,ExtensionBase):
29
29
  type:str|None
30
30
  data_:str
31
31
 
32
+ @computed_field
32
33
  @property
33
34
  def data(self) -> str:
34
35
  return self.data_
@@ -1,25 +1,27 @@
1
1
 
2
2
  import re
3
3
 
4
+ from deprecated import deprecated
5
+
4
6
  from pydantic import Field, model_validator
5
7
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMessage, ValidationMsgLevel
6
8
  from labfreed.pac_id.pac_id import PAC_ID
7
9
  from labfreed.pac_id_resolver.services import Service, ServiceGroup
8
- from labfreed.pac_id_resolver.cit_common import ( _add_msg_to_cit_entry_model,
10
+ from labfreed.pac_id_resolver.resolver_config_common import ( _add_msg_to_cit_entry_model,
9
11
  _validate_service_name,
10
12
  _validate_application_intent,
11
13
  _validate_service_type,
12
14
  ServiceType)
13
15
 
14
16
 
15
-
17
+ @deprecated("Use ResolverConfig")
16
18
  class CITEntry_v1(LabFREED_BaseModel):
17
19
  applicable_if: str = Field(..., min_length=1)
18
20
  service_name: str = Field(..., min_length=1)
19
21
  application_intent:str = Field(..., min_length=1)
20
22
  service_type:ServiceType|str
21
23
  template_url:str = Field(..., min_length=1)
22
-
24
+
23
25
 
24
26
  @model_validator(mode='after')
25
27
  def _validate_model(self):
@@ -67,7 +69,7 @@ class CITEntry_v1(LabFREED_BaseModel):
67
69
 
68
70
 
69
71
 
70
-
72
+ @deprecated("Use ResolverConfig")
71
73
  class CIT_v1(LabFREED_BaseModel):
72
74
  origin:str = ''
73
75
  entries:list[CITEntry_v1]
@@ -187,6 +189,16 @@ class CIT_v1(LabFREED_BaseModel):
187
189
  for e in self.entries:
188
190
  s += '\t'.join([e.service_name, e.application_intent, e.service_type.value, e.applicable_if, e.template_url]) + '\n'
189
191
  return s
192
+
193
+ # hash and equal are only used to avoid adding the same cit multiple times.
194
+ # we can live with some instances, where it does not work
195
+ def __hash__(self):
196
+ return self.model_dump_json().__hash__()
197
+
198
+ def __eq__(self, other):
199
+ if not isinstance(other, CIT_v1):
200
+ return False
201
+ return self.model_dump() == other.model_dump()
190
202
 
191
203
 
192
204
 
@@ -2,14 +2,13 @@ from functools import lru_cache
2
2
  import logging
3
3
  from typing import Self
4
4
  from requests import get
5
-
6
-
5
+ from deprecated import deprecated
7
6
 
8
7
  from labfreed.pac_cat.pac_cat import PAC_CAT
9
8
  from labfreed.pac_id.pac_id import PAC_ID
10
9
  from labfreed.pac_id_resolver.services import ServiceGroup
11
10
  from labfreed.pac_id_resolver.cit_v1 import CIT_v1
12
- from labfreed.pac_id_resolver.cit_v2 import CIT_v2
11
+ from labfreed.pac_id_resolver.resolver_config import ResolverConfig
13
12
 
14
13
 
15
14
 
@@ -22,49 +21,67 @@ def load_cit(path):
22
21
  return cit_from_str(s)
23
22
 
24
23
 
25
- def cit_from_str(s:str, origin:str='') -> CIT_v1|CIT_v2:
24
+ @deprecated("cit version 1 is deprecated. use resolvber config and load with ResolverConfig.from_yaml(s)")
25
+ def cit_from_str(s:str, origin:str='') -> CIT_v1|ResolverConfig:
26
26
  try:
27
- cit2 = CIT_v2.from_yaml(s)
27
+ cit2 = ResolverConfig.from_yaml(s)
28
28
  cit_version = 'v2'
29
- except Exception:
29
+ except Exception as e1:
30
30
  cit2 = None
31
31
  try:
32
32
  cit1 = CIT_v1.from_csv(s, origin)
33
33
  cit_version = 'v1' # noqa: F841
34
- except Exception:
34
+ except Exception as e2:
35
35
  cit1 = None
36
36
 
37
37
  cit = cit2 or cit1 or None
38
38
  return cit
39
39
 
40
40
  @lru_cache
41
- def _get_issuer_cit(issuer:str):
41
+ def _get_issuer_resolver_config(issuer:str):
42
42
  '''Gets the issuer's cit.'''
43
+ # V2
44
+ url = 'HTTPS://PAC.' + issuer + '/resolver_config.yaml'
45
+ try:
46
+ r = get(url, timeout=2)
47
+ if r.status_code < 400:
48
+ config_str = r.text
49
+ resolver_config = ResolverConfig.from_yaml(config_str)
50
+ return resolver_config
51
+ else:
52
+ logging.error(f"Could not get CIT V2 form {issuer}")
53
+ except Exception:
54
+ logging.error(f"Could not get CIT V2 form {issuer}")
55
+
56
+
57
+ # V1 (as fallback)
43
58
  url = 'HTTPS://PAC.' + issuer + '/coupling-information-table'
44
59
  try:
45
60
  r = get(url, timeout=2)
46
61
  if r.status_code < 400:
47
- cit_str = r.text
62
+ config_str = r.text
63
+ cit = CIT_v1.from_csv(config_str, '')
64
+ return cit
48
65
  else:
49
66
  logging.error(f"Could not get CIT form {issuer}")
50
- cit_str = None
51
67
  except Exception:
52
68
  logging.error(f"Could not get CIT form {issuer}")
53
- cit_str = None
54
- cit = cit_from_str(cit_str, origin=issuer)
55
- return cit
69
+
70
+ # no cit found
71
+ return None
72
+
56
73
 
57
74
 
58
75
 
59
76
  class PAC_ID_Resolver():
60
- def __init__(self, cits:list[CIT_v2|CIT_v1]=None) -> Self:
77
+ def __init__(self, resolver_configs:list[ResolverConfig|CIT_v1]=None) -> Self:
61
78
  '''Initialize the resolver with coupling information tables'''
62
- if not cits:
63
- cits = []
64
- self._cits = cits
79
+ if not resolver_configs:
80
+ resolver_configs = []
81
+ self._resolver_configs = set(resolver_configs)
65
82
 
66
83
 
67
- def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_cit=True) -> list[ServiceGroup]:
84
+ def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_resolver_config=True) -> list[ServiceGroup]:
68
85
  '''Resolve a PAC-ID'''
69
86
  if isinstance(pac_id, str):
70
87
  pac_id_catless = PAC_ID.from_url(pac_id, try_pac_cat=False)
@@ -77,13 +94,13 @@ class PAC_ID_Resolver():
77
94
  raise ValueError('pac_id is invalid. Should be a PAC-ID in url form or a PAC-ID object')
78
95
 
79
96
 
80
- cits = self._cits.copy()
81
- if use_issuer_cit:
82
- if issuer_cit := _get_issuer_cit(pac_id.issuer):
83
- cits.append(issuer_cit)
97
+ resolver_configs = self._resolver_configs.copy()
98
+ if use_issuer_resolver_config:
99
+ if issuer_resolver_config := _get_issuer_resolver_config(pac_id.issuer):
100
+ resolver_configs.add(issuer_resolver_config)
84
101
 
85
102
  matches = []
86
- for cit in cits:
103
+ for cit in resolver_configs:
87
104
  if isinstance(cit, CIT_v1):
88
105
  # cit v1 has no concept of categories and implied keys. It would treat these segments as value segment
89
106
  matches.append(cit.evaluate_pac_id(pac_id_catless))
@@ -8,18 +8,18 @@ import jsonpath_ng.ext as jsonpath
8
8
 
9
9
  from labfreed.pac_id_resolver.services import Service, ServiceGroup
10
10
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
11
- from labfreed.pac_id_resolver.cit_common import ( ServiceType)
11
+ from labfreed.pac_id_resolver.resolver_config_common import ( ServiceType)
12
12
 
13
13
 
14
14
  __all__ = [
15
- "CIT_v2",
16
- "CITBlock_v2",
17
- "CITEntry_v2"
15
+ "ResolverConfig",
16
+ "ResolverConfigBlock",
17
+ "ResolverConfigEntry"
18
18
  ]
19
19
 
20
20
 
21
21
 
22
- class CITEntry_v2(LabFREED_BaseModel):
22
+ class ResolverConfigEntry(LabFREED_BaseModel):
23
23
  service_name: str
24
24
  application_intents:list[str]
25
25
  service_type:ServiceType |str
@@ -92,9 +92,9 @@ class CITEntry_v2(LabFREED_BaseModel):
92
92
 
93
93
 
94
94
 
95
- class CITBlock_v2(LabFREED_BaseModel):
95
+ class ResolverConfigBlock(LabFREED_BaseModel):
96
96
  applicable_if: str = Field(default='True', alias='if')
97
- entries: list[CITEntry_v2]
97
+ entries: list[ResolverConfigEntry]
98
98
 
99
99
  @field_validator('applicable_if', mode='before')
100
100
  @classmethod
@@ -104,21 +104,21 @@ class CITBlock_v2(LabFREED_BaseModel):
104
104
 
105
105
 
106
106
 
107
- class CIT_v2(LabFREED_BaseModel):
107
+ class ResolverConfig(LabFREED_BaseModel):
108
108
  schema_version: str = Field(default='2.0')
109
- '''Coupling Information Table (CIT)'''
109
+ '''Resolver Configuration'''
110
110
  origin: str = ''
111
111
  model_config = {
112
112
  "extra": "allow"
113
113
  }
114
114
  '''@private'''
115
- cit: list[CITBlock_v2] = Field(default_factory=list)
115
+ config: list[ResolverConfigBlock] = Field(default_factory=list)
116
116
 
117
117
  @model_validator(mode='after')
118
118
  def _validate_origin(self):
119
119
  if len(self.origin) == 0:
120
120
  self._add_validation_message(level=ValidationMsgLevel.WARNING,
121
- source='CIT origin',
121
+ source='ResolverConfig origin',
122
122
  msg='Origin should not be empty'
123
123
  )
124
124
  return self
@@ -136,27 +136,38 @@ class CIT_v2(LabFREED_BaseModel):
136
136
  def __str__(self):
137
137
  yml = yaml.dump(self.model_dump() )
138
138
  return yml
139
+
140
+ # hash and equal are only used to avoid adding the same resolver config multiple times.
141
+ # we can live with some instances, where it does not work
142
+ def __hash__(self):
143
+ return self.model_dump_json().__hash__()
144
+
145
+ def __eq__(self, other):
146
+ if not isinstance(other, ResolverConfig):
147
+ return False
148
+ return self.model_dump() == other.model_dump()
149
+
139
150
 
140
151
  def evaluate_pac_id(self, pac):
141
152
  pac_id_json = pac.to_dict()
142
- cit_evaluated = ServiceGroup(origin=self.origin)
143
- for block in self.cit:
153
+ resolver_config_evaluated = ServiceGroup(origin=self.origin)
154
+ for block in self.config:
144
155
  _, is_applicable = self._evaluate_applicable_if(pac_id_json, block.applicable_if)
145
156
  if not is_applicable:
146
157
  continue
147
158
 
148
159
  for e in block.entries:
149
160
  if e.errors():
150
- continue #make this stable against errors in the cit
161
+ continue #make this stable against errors in the resolver config
151
162
  url = self._eval_url_template(pac_id_json, e.template_url)
152
- cit_evaluated.services.append(Service(
163
+ resolver_config_evaluated.services.append(Service(
153
164
  service_name=e.service_name,
154
165
  application_intents=e.application_intents,
155
166
  service_type=e.service_type,
156
167
  url = url
157
168
  )
158
169
  )
159
- return cit_evaluated
170
+ return resolver_config_evaluated
160
171
 
161
172
 
162
173
  def _evaluate_applicable_if(self, pac_id_json:str, expression) -> tuple[str, bool]:
@@ -1,7 +1,7 @@
1
- from .display_name_extension import DisplayNameExtension
1
+ from .text_base36_extension import TextBase36Extension
2
2
  from .trex_extension import TREX_Extension
3
3
 
4
4
  default_extension_interpreters = {
5
5
  'TREX': TREX_Extension,
6
- 'N': DisplayNameExtension
6
+ 'TEXT': TextBase36Extension
7
7
  }
@@ -1,27 +1,35 @@
1
1
  import logging
2
2
  from typing import Literal, Self
3
- from pydantic import computed_field
3
+ from pydantic import model_validator
4
4
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel
5
5
  from labfreed.pac_id.extension import ExtensionBase
6
- from labfreed.utilities.base36 import from_base36, to_base36
6
+ from labfreed.well_known_extensions.text_base36_extension import TextBase36Extension
7
7
 
8
+ from labfreed.utilities.base36 import from_base36
8
9
 
9
- class DisplayNameExtension(ExtensionBase, LabFREED_BaseModel):
10
+
11
+ class DisplayNameExtension(TextBase36Extension, LabFREED_BaseModel):
10
12
  name:Literal['N'] = 'N'
11
13
  type:Literal['TEXT'] = 'TEXT'
12
- display_name: str
14
+ display_name: str
13
15
 
14
- @computed_field
15
- @property
16
- def data(self)->str:
17
- # return '/'.join([to_base36(dn) for dn in self.display_name])
18
- return to_base36(self.display_name).root
16
+ @model_validator(mode='before')
17
+ def move_display_name_to_text(cls, data):
18
+ # if display_name provided, move it to text
19
+ if isinstance(data, dict) and 'display_name' in data:
20
+ data['text'] = data.pop('display_name')
21
+ return data
19
22
 
20
23
  @staticmethod
21
24
  def from_extension(ext:ExtensionBase) -> Self:
22
25
  return DisplayNameExtension.create(name=ext.name,
23
26
  type=ext.type,
24
27
  data=ext.data)
28
+
29
+ @property
30
+ def display_name(self) -> str:
31
+ return self.text
32
+
25
33
 
26
34
  @staticmethod
27
35
  def create(*, name, type, data):
@@ -0,0 +1,38 @@
1
+ import logging
2
+ from typing import Literal, Self
3
+ from pydantic import computed_field
4
+ from labfreed.labfreed_infrastructure import LabFREED_BaseModel
5
+ from labfreed.pac_id.extension import ExtensionBase
6
+ from labfreed.utilities.base36 import from_base36, to_base36
7
+
8
+
9
+ class TextBase36Extension(ExtensionBase, LabFREED_BaseModel):
10
+ name:str
11
+ type:Literal['TEXT'] = 'TEXT'
12
+ text: str
13
+
14
+ @computed_field
15
+ @property
16
+ def data(self)->str:
17
+ # return '/'.join([to_base36(dn) for dn in self.display_name])
18
+ return to_base36(self.text).root
19
+
20
+ @staticmethod
21
+ def from_extension(ext:ExtensionBase) -> Self:
22
+ return TextBase36Extension.create(name=ext.name,
23
+ type=ext.type,
24
+ data=ext.data)
25
+
26
+ @staticmethod
27
+ def create(*, name, type, data):
28
+
29
+ if type != 'TEXT':
30
+ logging.warning(f'Type {name} was given, but this extension should only be used with type "TEXT". Will try to parse data as display names')
31
+
32
+ text = from_base36(data)
33
+
34
+ return TextBase36Extension(name=name, text=text)
35
+
36
+ def __str__(self):
37
+ return 'Text: '+ self.text
38
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 1.0.0a12
3
+ Version: 1.0.0a14
4
4
  Summary: Python implementation of LabFREED building blocks
5
5
  Author-email: Reto Thürer <thuerer.r@buchi.com>
6
6
  Requires-Python: >=3.11
@@ -25,6 +25,7 @@ Requires-Dist: jsonpath-ng>=1.7.0
25
25
  Requires-Dist: requests>=2.32.3
26
26
  Requires-Dist: requests_cache>=1.2.1
27
27
  Requires-Dist: cachetools>=6.1.0
28
+ Requires-Dist: deprecated>=1.2.18
28
29
  Requires-Dist: pytest>=8.3.5 ; extra == "dev"
29
30
  Requires-Dist: pdoc>=15.0.1 ; extra == "dev"
30
31
  Requires-Dist: flit>=3.12.0 ; extra == "dev"
@@ -1,8 +1,8 @@
1
- labfreed/__init__.py,sha256=-fXxBGDTAB52CShnD0wjQuE28pAU8dOzgFwDx7EybBk,339
2
- labfreed/labfreed_infrastructure.py,sha256=YZmU-kgopyB1tvpTR_k_uIt1Q2ezexMrWvu-HaP65IE,10104
3
- labfreed/labfreed_extended/app/app_infrastructure.py,sha256=F5UHHt8-r7jigQudOsJ-yV4lWKHFsxNhI4uGP_h53lE,4035
1
+ labfreed/__init__.py,sha256=fen6oZE4WpF77ck1RZ-1lkwRwBtLfUO6RU1CO2W91Hs,339
2
+ labfreed/labfreed_infrastructure.py,sha256=ss1PyJl-7Es-lEcxptmdYI9kDAHmh7HB_tAGkPC6UVs,10173
3
+ labfreed/labfreed_extended/app/app_infrastructure.py,sha256=Kkpmw6VZZIxJ6mXRQmX_unS-coHVbKAnlr9VOZNSqyU,4418
4
4
  labfreed/labfreed_extended/app/formatted_print.py,sha256=DcwWP0ix1e_wYNIdceIp6cETkJdG2DqpU8Gs3aZAL40,1930
5
- labfreed/labfreed_extended/app/pac_info/pac_info.py,sha256=hsJunO4adKcLi7PBZNAJYeqzOH31_j95JzuvEV8lNVY,6427
5
+ labfreed/labfreed_extended/app/pac_info/pac_info.py,sha256=9Wre1wAmMZObeh7Ed46KA38nBWcXhqGD0m9vTHfkkS8,8397
6
6
  labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg,sha256=H5z9s4VvHq09UnHdqfrYNsx-Whljc0gE4qKJ6-3kfgQ,1158
7
7
  labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html,sha256=1S-dxibPwJshtdelsmyA4LpgOm84L6RTXPNO93gmPfg,5964
8
8
  labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css,sha256=C5pyD956fd6pJgUBjGxvxgL0Wbgq0v7ZLY4Vr-sJZ7A,4169
@@ -11,12 +11,12 @@ labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html,s
11
11
  labfreed/pac_attributes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  labfreed/pac_attributes/well_knonw_attribute_keys.py,sha256=axE81MeJ3G_Wy1PbmNAXH6SfPtl96NXvQJMyrvK10t4,324
13
13
  labfreed/pac_attributes/api_data_models/request.py,sha256=N-kXlJWYqh-F1TzNunCwHUPhme3bSLJMgb9aAHWGOy4,1880
14
- labfreed/pac_attributes/api_data_models/response.py,sha256=eGh474ILEcBC1ijhs1ZZfdhNWRxiPeccGS8aw0zzt0U,6934
14
+ labfreed/pac_attributes/api_data_models/response.py,sha256=MbpzaSnAQgvdrzYxCAtdfr480Vg4XMBBbZPKP9yirf4,8475
15
15
  labfreed/pac_attributes/api_data_models/server_capabilities_response.py,sha256=ypDm4f8xZZl036fp8PuIe6lJHNW5Zg1fItgUlnV75V0,178
16
16
  labfreed/pac_attributes/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  labfreed/pac_attributes/client/attribute_cache.py,sha256=ThUadWqQ5oM8DnAnvZuY4jeA3Mg06ePNEcRP5wCsadc,2222
18
18
  labfreed/pac_attributes/client/client.py,sha256=FjvyEpZEGYrZkuaQoqH9QFstrwHmrsIdaymxz8uQjwQ,7510
19
- labfreed/pac_attributes/pythonic/attribute_server_factory.py,sha256=AtoOICmAGQY_ZkCcnPW4t8kv3LHaro-a-Ylm04j-Dbc,6199
19
+ labfreed/pac_attributes/pythonic/attribute_server_factory.py,sha256=3OeFjBdlMR4DMIzHVo5-A_y935e2_lOqvFCLKgxjsEY,6159
20
20
  labfreed/pac_attributes/pythonic/excel_attribute_data_source.py,sha256=oP4OHj0DTlH4dD7OlL1qxtX4y9KcuDTCd9Bi_FruP6A,7276
21
21
  labfreed/pac_attributes/pythonic/py_attributes.py,sha256=FXSp9_P0o-GuZSDvXtD2fU4g82lglMu9f_-8KPMkEP0,6821
22
22
  labfreed/pac_attributes/pythonic/py_dict_data_source.py,sha256=nAz6GA7Xx_0IORPPpt_Wl3sFJa1Q5Fnq5vdf1uQiJF8,531
@@ -25,20 +25,20 @@ labfreed/pac_attributes/server/attribute_data_sources.py,sha256=7-YQeBcn5ndsZWee
25
25
  labfreed/pac_attributes/server/server.py,sha256=tPOPezRC3YEE0i-MJIc23Me6EARaSqzyFdUNjUzqtdI,9117
26
26
  labfreed/pac_attributes/server/translation_data_sources.py,sha256=axALOqfP840sOSdVCRYtrens97mm-hpfONMUyuVlCrY,2145
27
27
  labfreed/pac_cat/__init__.py,sha256=KNPtQzBD1XVohvG_ucOs7RJj-oi6biUTGB1k-T2o6pk,568
28
- labfreed/pac_cat/category_base.py,sha256=D7BzsdF0-JIgag5L2XZJRF4T2LOH5RLh1MMszflkmV8,2526
28
+ labfreed/pac_cat/category_base.py,sha256=XAuvfJSCsQ3Ypi5WujtaCmt5isj7Qsx7-zJa9Sm0XVY,2530
29
29
  labfreed/pac_cat/pac_cat.py,sha256=wcb_fhvgjS2xmqTsxS8_Oibvr1nsQt5zr8aUajLfK1E,5578
30
- labfreed/pac_cat/predefined_categories.py,sha256=wpaZtsZxOvWoJI9M-p1s7tW8Kp_Dmvi7_pw1YdBG-ug,11121
30
+ labfreed/pac_cat/predefined_categories.py,sha256=dyyF90mdPV-RGjk3NG8AQRbEQDcxtP95w2DwfDeO-_8,12775
31
31
  labfreed/pac_id/__init__.py,sha256=NGMbzkwQ4txKeT5pxdIZordwHO8J3_q84jzPanjKoHg,675
32
- labfreed/pac_id/extension.py,sha256=NgLexs1LbRMMm4ETrn5m4EY2iWoMDgOTb0UV556jatQ,2227
32
+ labfreed/pac_id/extension.py,sha256=4_cQ-N3x8bRxboqC44Qnf6rGnYXvsvHOnlBaY7-Hf-8,2264
33
33
  labfreed/pac_id/id_segment.py,sha256=r5JU1SJuRXhZJJxy5T3xjrb598wIDTLpivSJhIUAzjQ,4526
34
34
  labfreed/pac_id/pac_id.py,sha256=DDcSYJ8DBWqIoW_usOT7eDjHZ9700cTYxeUgenHluOA,5378
35
35
  labfreed/pac_id/url_parser.py,sha256=F3SPiscfbPwZ0uMzgirJ1vwgaXclN546lBW46Ywo3nk,5979
36
36
  labfreed/pac_id/url_serializer.py,sha256=01LB30pNMBtv2rYHsiE_4Ga2iVA515Boj4ikOIYhiBQ,3511
37
37
  labfreed/pac_id_resolver/__init__.py,sha256=RNBlrDOSR42gmSNH9wJVhK_xwEX45cvTKVgWW2bjh7Q,113
38
- labfreed/pac_id_resolver/cit_common.py,sha256=jzoDOxog8YW68q7vyvDGCZcVcgIzJHXlMt8KwgVnx6o,2885
39
- labfreed/pac_id_resolver/cit_v1.py,sha256=TVvOWJA6-wmBkwzoHBqNuwV-tndRTSKAK07k56eoJBU,9326
40
- labfreed/pac_id_resolver/cit_v2.py,sha256=iDEvUb9A3ocX_ijewTIK3p951CIESnKyI7upjrd3Vjw,11842
41
- labfreed/pac_id_resolver/resolver.py,sha256=IxI57XRZfGHZigym6_f5tnt3ycwU5LnfXs5n1D9pPDs,3077
38
+ labfreed/pac_id_resolver/cit_v1.py,sha256=JGlEH2d9awEu3HxPW7vu0uj4ZC3B02IdmFg7aJ4axQw,9833
39
+ labfreed/pac_id_resolver/resolver.py,sha256=ExVMuKN_g6VFmHkW8WFgXunxFbvfPsan9jBQmn_MYIs,3956
40
+ labfreed/pac_id_resolver/resolver_config.py,sha256=2j3j5UsVoWqU9NjciN3j-Fs53diGi4FQEvoz8ng103Y,12388
41
+ labfreed/pac_id_resolver/resolver_config_common.py,sha256=jzoDOxog8YW68q7vyvDGCZcVcgIzJHXlMt8KwgVnx6o,2885
42
42
  labfreed/pac_id_resolver/services.py,sha256=vtnxLm38t4PNOf73cXh6UZOtWZZOGxfBCfXUDRxGHog,2592
43
43
  labfreed/qr/__init__.py,sha256=fdKwP6W2Js--yMbBUdn-g_2uq2VqPpfQJeDLHsMDO-Y,61
44
44
  labfreed/qr/generate_qr.py,sha256=mSt-U872O3ReHB_UdS-MzYu0wRgdlKcAOEfTxg5CLRk,16616
@@ -55,8 +55,9 @@ labfreed/utilities/base36.py,sha256=_yX8aQ1OwrK5tnJU1NUEzQSFGr9xAVnNvPObpNzCPYs,
55
55
  labfreed/utilities/ensure_utc_time.py,sha256=1ZTTzyIt7IimQ4ArTzdgw5hxiabkkplltbQe3Wdt2ZQ,307
56
56
  labfreed/utilities/translations.py,sha256=XY4Wud_BfXswUOpebdh0U_D2bMzb2vqluuGWzFK-3uU,1851
57
57
  labfreed/well_known_extensions/__init__.py,sha256=CjZTjx8Cn8763Hhnv_--Wj1LcFpFs2cyQwWrrzOS4xM,246
58
- labfreed/well_known_extensions/default_extension_interpreters.py,sha256=3-BkJrAyBa99NN5Q2QPAm59CcWmPket-rvLzgltp8KY,201
59
- labfreed/well_known_extensions/display_name_extension.py,sha256=Si2uJ0IGSO3-XyqlYwG7MovnT10oJ3JdOQAyjoZt2Rc,1477
58
+ labfreed/well_known_extensions/default_extension_interpreters.py,sha256=6r2h47g8sZAyuN25DS0S0CdNQbyYRMFljdkDSrnnll0,201
59
+ labfreed/well_known_extensions/display_name_extension.py,sha256=txZelIj-B4_ycudaQJsFr0TFuiNRe5wEMQ9hE9wtd-Q,1737
60
+ labfreed/well_known_extensions/text_base36_extension.py,sha256=2SYWuKx2FlnkkSsZS5Z97QqMx10Kk67kzj5-BEoAo8E,1262
60
61
  labfreed/well_known_extensions/trex_extension.py,sha256=tffklaambkFPExcIDRAG9GJ7CHXeuFAagl6FuwS-2kI,929
61
62
  labfreed/well_known_keys/gs1/__init__.py,sha256=LOFycgqS6OuV8t22TmtHy-ZI2iuXc3jJfVFwRFVDM3I,103
62
63
  labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py,sha256=qSWD7bpTJQdQhXbZHJc20TRdqrnrxICD79goXWr6B-g,1405
@@ -64,7 +65,7 @@ labfreed/well_known_keys/labfreed/well_known_keys.py,sha256=p-hXwEEIs7p2SKn9DQeL
64
65
  labfreed/well_known_keys/unece/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
65
66
  labfreed/well_known_keys/unece/__init__.py,sha256=MSP9lmjg9_D9iqG9Yq2_ajYfQSNS9wIT7FXA1c--59M,122
66
67
  labfreed/well_known_keys/unece/unece_units.py,sha256=J20d64H69qKDE3XlGdJoXIIh0G-d0jKoiIDsg9an5pk,1655
67
- labfreed-1.0.0a12.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
68
- labfreed-1.0.0a12.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
69
- labfreed-1.0.0a12.dist-info/METADATA,sha256=GLF4TztQScD3K3HSoiTDs3_QrhSRwz1t-NsaeJn0lts,19741
70
- labfreed-1.0.0a12.dist-info/RECORD,,
68
+ labfreed-1.0.0a14.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
69
+ labfreed-1.0.0a14.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
70
+ labfreed-1.0.0a14.dist-info/METADATA,sha256=OiIaarhRnwP3FAkV3Q6_1tkW5v4pj_Y84RUxcmJWBBY,19775
71
+ labfreed-1.0.0a14.dist-info/RECORD,,