labfreed 1.0.0a12__tar.gz → 1.0.0a14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/PKG-INFO +2 -1
  2. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/__init__.py +1 -1
  3. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/app_infrastructure.py +15 -5
  4. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/pac_info.py +137 -53
  5. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_infrastructure.py +12 -0
  6. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/response.py +103 -31
  7. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -1
  8. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/category_base.py +1 -1
  9. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/predefined_categories.py +61 -4
  10. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/extension.py +2 -1
  11. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/cit_v1.py +16 -4
  12. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/resolver.py +40 -23
  13. labfreed-1.0.0a12/labfreed/pac_id_resolver/cit_v2.py → labfreed-1.0.0a14/labfreed/pac_id_resolver/resolver_config.py +27 -16
  14. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/default_extension_interpreters.py +2 -2
  15. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/display_name_extension.py +17 -9
  16. labfreed-1.0.0a14/labfreed/well_known_extensions/text_base36_extension.py +38 -0
  17. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/pyproject.toml +2 -1
  18. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/workflows/pypi-publish.yml +0 -0
  20. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/.github/workflows/run-tests.yml +0 -0
  21. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/CHANGELOG.md +0 -0
  22. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/LICENSE +0 -0
  23. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/README.md +0 -0
  24. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
  25. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
  26. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
  27. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
  28. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
  29. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
  30. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/__init__.py +0 -0
  31. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/request.py +0 -0
  32. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  33. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/__init__.py +0 -0
  34. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
  35. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/client/client.py +0 -0
  36. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +0 -0
  37. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/py_attributes.py +0 -0
  38. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
  39. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/__init__.py +0 -0
  40. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/attribute_data_sources.py +0 -0
  41. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/server.py +0 -0
  42. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  43. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
  44. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/__init__.py +0 -0
  45. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_cat/pac_cat.py +0 -0
  46. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/__init__.py +0 -0
  47. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/id_segment.py +0 -0
  48. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/pac_id.py +0 -0
  49. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/url_parser.py +0 -0
  50. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id/url_serializer.py +0 -0
  51. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/__init__.py +0 -0
  52. /labfreed-1.0.0a12/labfreed/pac_id_resolver/cit_common.py → /labfreed-1.0.0a14/labfreed/pac_id_resolver/resolver_config_common.py +0 -0
  53. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/pac_id_resolver/services.py +0 -0
  54. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/qr/__init__.py +0 -0
  55. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/qr/generate_qr.py +0 -0
  56. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/__init__.py +0 -0
  57. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/__init__.py +0 -0
  58. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/data_table.py +0 -0
  59. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/pyTREX.py +0 -0
  60. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/pythonic/quantity.py +0 -0
  61. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/table_segment.py +0 -0
  62. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/trex.py +0 -0
  63. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/trex_base_models.py +0 -0
  64. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/trex/value_segments.py +0 -0
  65. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/base36.py +0 -0
  66. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/ensure_utc_time.py +0 -0
  67. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/utilities/translations.py +0 -0
  68. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/__init__.py +0 -0
  69. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_extensions/trex_extension.py +0 -0
  70. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  71. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  72. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  73. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  74. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/__init__.py +0 -0
  75. {labfreed-1.0.0a12 → labfreed-1.0.0a14}/labfreed/well_known_keys/unece/unece_units.py +0 -0
@@ -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"
@@ -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
  }