labfreed 1.0.0a13__py3-none-any.whl → 1.0.0a15__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.0a13"
5
+ __version__ = "1.0.0a15"
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
@@ -20,10 +17,11 @@ from labfreed.pac_id_resolver.services import ServiceGroup
20
17
 
21
18
 
22
19
  class Labfreed_App_Infrastructure():
23
- def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None):
20
+ def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None, use_issuer_resolver_config=True):
24
21
  if isinstance(language_preferences, str):
25
22
  language_preferences = [language_preferences]
26
23
  self._language_preferences = language_preferences
24
+ self._use_issuer_resolver_config = use_issuer_resolver_config
27
25
 
28
26
  self._resolver = PAC_ID_Resolver()
29
27
 
@@ -35,11 +33,15 @@ class Labfreed_App_Infrastructure():
35
33
  self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
36
34
 
37
35
 
38
- def add_cit(self, cit:str):
36
+ def add_resolver_config(self, cit:str):
39
37
  cit = cit_from_str(cit)
40
38
  if not cit:
41
39
  raise ValueError('the cit could not be parsed. Neither as v1 or v2')
42
- self._resolver._cits.add(cit)
40
+ self._resolver._resolver_configs.add(cit)
41
+
42
+ def remove_resolver_config(self, resolver_config:str):
43
+ resolver_config = cit_from_str(resolver_config)
44
+ self._resolver._resolver_configs.discard(resolver_config)
43
45
 
44
46
 
45
47
  def process_pac(self, pac_url, markup=None):
@@ -47,7 +49,7 @@ class Labfreed_App_Infrastructure():
47
49
  pac = PAC_ID.from_url(pac_url)
48
50
  else:
49
51
  pac = pac_url
50
- service_groups = self._resolver.resolve(pac, check_service_status=False)
52
+ service_groups = self._resolver.resolve(pac, check_service_status=False, use_issuer_resolver_config=self._use_issuer_resolver_config)
51
53
 
52
54
  pac_info = PacInfo(pac_id=pac)
53
55
 
@@ -63,6 +65,15 @@ class Labfreed_App_Infrastructure():
63
65
  sg_user_handovers.append(ServiceGroup(origin=sg.origin, services=user_handovers))
64
66
  pac_info.user_handovers = sg_user_handovers
65
67
 
68
+ # Actions
69
+ sg_actions = []
70
+ for sg in service_groups:
71
+ actions = [s for s in sg.services if s.service_type == 'action-generic']
72
+
73
+ if actions:
74
+ sg_actions.append(ServiceGroup(origin=sg.origin, services=actions))
75
+ pac_info.actions = sg_actions
76
+
66
77
  # Attributes
67
78
  attribute_groups = {}
68
79
  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,114 @@ 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]
73
113
 
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
+ return self
125
+
126
+ class ResourceListAttribute(AttributeBase):
127
+ type: Literal["resource-list"]
128
+ value: list[str]
78
129
 
79
130
  @model_validator(mode='after')
80
131
  def _validate_value(self):
81
132
  value_list = self.value if isinstance(self.value, list) else [self.value]
82
133
  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
- )
134
+ _validate_resource(self, v)
99
135
  return self
100
136
 
137
+ def _validate_resource(mdl:LabFREED_BaseModel, v):
138
+ r = urlparse(v)
139
+ if not all([r.scheme, r.netloc]):
140
+ mdl._add_validation_message(
141
+ source="Resource Attribute",
142
+ level=ValidationMsgLevel.ERROR, # noqa: F821
143
+ msg="Must be a valid url",
144
+ highlight_pattern = f'{v}'
145
+ )
146
+ pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
147
+ if not bool(pattern.search(v)):
148
+ mdl._add_validation_message(
149
+ source="Resource Attribute",
150
+ level=ValidationMsgLevel.WARNING, # noqa: F821
151
+ msg="It is RECOMMENDED resource links end with a file extension",
152
+ highlight_pattern = f'{v}'
153
+ )
154
+
101
155
 
102
156
 
103
157
  class NumericValue(LabFREED_BaseModel):
@@ -151,11 +205,22 @@ class NumericValue(LabFREED_BaseModel):
151
205
 
152
206
  class NumericAttribute(AttributeBase):
153
207
  type: Literal["numeric"]
154
- value: NumericValue | list[NumericValue]
208
+ value: NumericValue
209
+
210
+ class NumericListAttribute(AttributeBase):
211
+ type: Literal["numeric-list"]
212
+ value: list[NumericValue]
213
+
214
+
155
215
 
156
216
  class ObjectAttribute(AttributeBase):
157
217
  type: Literal["object"]
158
- value: dict[str, Any] |list[dict[str, Any]]
218
+ value: dict[str, Any]
219
+
220
+
221
+ class ObjectListAttribute(AttributeBase):
222
+ type: Literal["object-list"]
223
+ value: list[dict[str, Any]]
159
224
 
160
225
 
161
226
 
@@ -168,7 +233,15 @@ Attribute = Annotated[
168
233
  TextAttribute,
169
234
  NumericAttribute,
170
235
  ResourceAttribute,
171
- ObjectAttribute
236
+ ObjectAttribute,
237
+
238
+ ReferenceListAttribute,
239
+ DateTimeListAttribute,
240
+ BoolListAttribute,
241
+ TextListAttribute,
242
+ NumericListAttribute,
243
+ ResourceListAttribute,
244
+ ObjectListAttribute
172
245
  ],
173
246
  Field(discriminator="type")
174
247
  ]
@@ -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: ...
@@ -75,9 +75,10 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
75
75
  Subclasses implement `_read_rows_and_last_changed()`.
76
76
  """
77
77
 
78
- def __init__(self, *, base_url: str = "", cache_duration_seconds: int = 0, uses_pac_cat_short_form:bool=True, **kwargs):
78
+ def __init__(self, *, base_url: str = "", cache_duration_seconds: int = 0, uses_pac_cat_short_form:bool=True, pac_to_key=None, **kwargs):
79
79
  self._base_url = base_url
80
80
  self._uses_pac_cat_short_form = uses_pac_cat_short_form
81
+ self._pac_to_key = pac_to_key
81
82
  # allow instance-level TTL override
82
83
  try:
83
84
  _cache.ttl = int(cache_duration_seconds)
@@ -106,11 +107,16 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
106
107
  except:
107
108
  ... # might as well try to match the original input
108
109
 
110
+ if f:= self._pac_to_key:
111
+ key = f(pac_url)
112
+ else:
113
+ key = pac_url
114
+
109
115
  rows, last_changed = self._read_rows_and_last_changed()
110
- d = _get_row_by_first_cell(rows, pac_url, self._base_url)
116
+ d = _get_row_by_first_cell(rows, key, self._base_url)
111
117
  if not d:
112
118
  return None
113
- attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
119
+ attributes = [pyAttribute(key=k, value=v) for k, v in d.items() if v is not None]
114
120
  return AttributeGroup(
115
121
  key=self._attribute_group_key,
116
122
  attributes=pyAttributes(attributes).to_payload_attributes()