labfreed 1.0.0a4__tar.gz → 1.0.0a8__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.0a4 → labfreed-1.0.0a8}/CHANGELOG.md +14 -0
  2. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/PKG-INFO +1 -1
  3. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/__init__.py +1 -1
  4. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/app_infrastructure.py +2 -2
  5. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/pac_info.py +15 -7
  6. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/response.py +54 -32
  7. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/attribute_cache.py +8 -21
  8. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/client.py +4 -2
  9. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -2
  10. labfreed-1.0.0a8/labfreed/pac_attributes/pythonic/py_attributes.py +165 -0
  11. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/attribute_data_sources.py +7 -11
  12. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/server.py +11 -2
  13. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/category_base.py +1 -1
  14. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/predefined_categories.py +52 -4
  15. labfreed-1.0.0a4/labfreed/pac_attributes/pythonic/py_attributes.py +0 -133
  16. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/workflows/pypi-publish.yml +0 -0
  18. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/.github/workflows/run-tests.yml +0 -0
  19. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/LICENSE +0 -0
  20. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/README.md +0 -0
  21. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
  22. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
  23. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
  24. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
  25. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
  26. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
  27. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/labfreed_infrastructure.py +0 -0
  28. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/__init__.py +0 -0
  29. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/request.py +0 -0
  30. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  31. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/client/__init__.py +0 -0
  32. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -0
  33. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
  34. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/__init__.py +0 -0
  35. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  36. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
  37. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/__init__.py +0 -0
  38. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_cat/pac_cat.py +0 -0
  39. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/__init__.py +0 -0
  40. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/extension.py +0 -0
  41. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/id_segment.py +0 -0
  42. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/pac_id.py +0 -0
  43. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/url_parser.py +0 -0
  44. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id/url_serializer.py +0 -0
  45. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/__init__.py +0 -0
  46. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_common.py +0 -0
  47. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_v1.py +0 -0
  48. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/cit_v2.py +0 -0
  49. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/resolver.py +0 -0
  50. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/pac_id_resolver/services.py +0 -0
  51. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/qr/__init__.py +0 -0
  52. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/qr/generate_qr.py +0 -0
  53. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/__init__.py +0 -0
  54. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/__init__.py +0 -0
  55. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/data_table.py +0 -0
  56. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/pyTREX.py +0 -0
  57. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/pythonic/quantity.py +0 -0
  58. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/table_segment.py +0 -0
  59. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/trex.py +0 -0
  60. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/trex_base_models.py +0 -0
  61. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/trex/value_segments.py +0 -0
  62. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/base36.py +0 -0
  63. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/ensure_utc_time.py +0 -0
  64. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/utilities/translations.py +0 -0
  65. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/__init__.py +0 -0
  66. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
  67. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/display_name_extension.py +0 -0
  68. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_extensions/trex_extension.py +0 -0
  69. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  70. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  71. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  72. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  73. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/__init__.py +0 -0
  74. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/labfreed/well_known_keys/unece/unece_units.py +0 -0
  75. {labfreed-1.0.0a4 → labfreed-1.0.0a8}/pyproject.toml +0 -0
@@ -1,4 +1,18 @@
1
1
  ## Change Log
2
+
3
+ ### v1.0.0
4
+ PAC-CAT
5
+ - added new categories
6
+ - BREAKING: Renamed category MM to MX
7
+
8
+
9
+ PAC-ID Attributes
10
+ - new building block
11
+
12
+
13
+
14
+
15
+
2
16
  ### v0.2.12
3
17
  - bugfix:no warning message if PAC-CAT has same segment key in two segments
4
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 1.0.0a4
3
+ Version: 1.0.0a8
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
@@ -2,7 +2,7 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "1.0.0a4"
5
+ __version__ = "1.0.0a8"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -32,7 +32,7 @@ class Labfreed_App_Infrastructure():
32
32
  self._http_client= http_client
33
33
  callback = http_attribute_request_default_callback_factory(http_client)
34
34
 
35
- self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache())
35
+ self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
36
36
 
37
37
 
38
38
  def add_cit(self, cit:str):
@@ -71,7 +71,7 @@ class Labfreed_App_Infrastructure():
71
71
  ags = {ag.key: pyAttributeGroup.from_attribute_group(ag) for ag in self._attribute_client.get_attributes(url, pac_id=pac.to_url(include_extensions=False), language_preferences=self._language_preferences)}
72
72
  if ags:
73
73
  attribute_groups.update(ags)
74
- pac_info.attributes = attribute_groups
74
+ pac_info.attribute_groups = attribute_groups
75
75
 
76
76
  return pac_info
77
77
 
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from urllib.parse import urlparse
6
6
  from jinja2 import Environment, FileSystemLoader, select_autoescape
7
7
  from pydantic import BaseModel, Field
8
- from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes, pyReference
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
11
  from labfreed.pac_id.pac_id import PAC_ID
@@ -20,7 +20,7 @@ class PacInfo(BaseModel):
20
20
  """A convenient collection of information about a PAC-ID"""
21
21
  pac_id:PAC_ID
22
22
  user_handovers: list[ServiceGroup] = Field(default_factory=list)
23
- attributes:dict[str, pyAttributeGroup] = Field(default_factory=dict)
23
+ attribute_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
24
24
 
25
25
  @property
26
26
  def pac_url(self):
@@ -44,8 +44,10 @@ class PacInfo(BaseModel):
44
44
 
45
45
  @property
46
46
  def image_url(self) -> str:
47
- if meta := self.attributes.get(MetaAttributeKeys.GROUPKEY.value):
48
- image_attr = meta.attributes.get(MetaAttributeKeys.IMAGE.value)
47
+ image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
48
+ if isinstance(image_attr.value, pyResource):
49
+ return image_attr.value.root
50
+ if isinstance(image_attr.value, str):
49
51
  return image_attr.value
50
52
 
51
53
 
@@ -68,12 +70,18 @@ class PacInfo(BaseModel):
68
70
  def safety_pictograms(self) -> dict[str, pyAttribute]:
69
71
  pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
70
72
  return pictogram_attributes
73
+
74
+
75
+ @property
76
+ def qualification_state(self) -> pyAttribute:
77
+ if state := self._all_attributes.get("https://labfreed.org/qualification/status"):
78
+ return state
71
79
 
72
80
 
73
81
  @cached_property
74
82
  def _all_attributes(self) -> dict[str, pyAttribute]:
75
83
  out = {}
76
- for ag in self.attributes.values():
84
+ for ag in self.attribute_groups.values():
77
85
  out.update(ag.attributes)
78
86
  return out
79
87
 
@@ -105,12 +113,12 @@ class PacInfo(BaseModel):
105
113
 
106
114
 
107
115
  printout.title1("Attributes")
108
- for ag in self.attributes.values():
116
+ for ag in self.attribute_groups.values():
109
117
  printout.title2(f'{ag.label} (from {ag.origin})')
110
118
  for v in ag.attributes.values():
111
119
  v:pyAttribute
112
120
  #print(f'{k}: ({v.label}) :: {v.value} ')
113
- printout.key_value(v.label, v.value)
121
+ printout.key_value(v.label, ', '.join([str(e) for e in v.value_list]))
114
122
 
115
123
  out = printout.getvalue()
116
124
 
@@ -3,6 +3,8 @@ from abc import ABC
3
3
  from datetime import datetime
4
4
  import re
5
5
  from typing import Annotated, Any, Literal, Union, get_args
6
+ from urllib.parse import urlparse
7
+
6
8
  from labfreed.utilities.ensure_utc_time import ensure_utc
7
9
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
8
10
  from pydantic import Field, field_validator, model_validator
@@ -12,22 +14,13 @@ class AttributeBase(LabFREED_BaseModel, ABC):
12
14
  key: str
13
15
  value: Any
14
16
  label: str = ""
15
-
16
- observed_at: datetime | None = None
17
-
17
+
18
18
  def __init__(self, **data):
19
19
  # Automatically inject the Literal value for `type`
20
20
  discriminator_value = self._get_discriminator_value()
21
21
  data["type"] = discriminator_value
22
22
  super().__init__(**data)
23
23
 
24
- @field_validator('observed_at', mode='before')
25
- def set_utc_observed_at_if_naive(cls, value):
26
- if isinstance(value, datetime):
27
- return ensure_utc(value)
28
- else:
29
- return value
30
-
31
24
  @classmethod
32
25
  def _get_discriminator_value(cls) -> str:
33
26
  """Extract the Literal value from the 'type' annotation."""
@@ -40,16 +33,10 @@ class AttributeBase(LabFREED_BaseModel, ABC):
40
33
  f"{cls.__name__} must define `type: Literal[<value>]` annotation"
41
34
  ) from e
42
35
 
43
-
44
-
45
-
46
- class ReferenceAttribute(AttributeBase):
47
- type: Literal["reference"]
48
- value: str
49
36
 
50
37
  class DateTimeAttribute(AttributeBase):
51
38
  type: Literal["datetime"]
52
- value: datetime
39
+ value: datetime | list[datetime]
53
40
 
54
41
  @field_validator('value', mode='before')
55
42
  def set_utc__if_naive(cls, value):
@@ -60,15 +47,59 @@ class DateTimeAttribute(AttributeBase):
60
47
 
61
48
  class BoolAttribute(AttributeBase):
62
49
  type: Literal["bool"]
63
- value: bool
50
+ value: bool | list[bool]
64
51
 
65
52
  class TextAttribute(AttributeBase):
66
53
  type: Literal["text"]
67
- value: str
54
+ value: str | list[str]
55
+
56
+ @model_validator(mode='after')
57
+ def _validate_value(self):
58
+ l = [self.value] if isinstance(self.value, str) else self.value
59
+ 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
+ )
67
+ return self
68
+
69
+
70
+ class ReferenceAttribute(AttributeBase):
71
+ type: Literal["reference"]
72
+ value: str | list[str]
68
73
 
74
+
75
+ class ResourceAttribute(AttributeBase):
76
+ type: Literal["resource"]
77
+ value: str | list[str]
69
78
 
79
+ @model_validator(mode='after')
80
+ def _validate_value(self):
81
+ value_list = self.value if isinstance(self.value, list) else [self.value]
82
+ 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
+ )
99
+ return self
70
100
 
71
101
 
102
+
72
103
  class NumericValue(LabFREED_BaseModel):
73
104
  numerical_value: str
74
105
  unit: str
@@ -120,11 +151,11 @@ class NumericValue(LabFREED_BaseModel):
120
151
 
121
152
  class NumericAttribute(AttributeBase):
122
153
  type: Literal["numeric"]
123
- value: NumericValue
154
+ value: NumericValue | list[NumericValue]
124
155
 
125
156
  class ObjectAttribute(AttributeBase):
126
157
  type: Literal["object"]
127
- value: dict[str, Any]
158
+ value: dict[str, Any] |list[dict[str, Any]]
128
159
 
129
160
 
130
161
 
@@ -136,27 +167,18 @@ Attribute = Annotated[
136
167
  BoolAttribute,
137
168
  TextAttribute,
138
169
  NumericAttribute,
170
+ ResourceAttribute,
139
171
  ObjectAttribute
140
172
  ],
141
173
  Field(discriminator="type")
142
174
  ]
143
175
 
144
- VALID_FOREVER = "forever"
145
176
 
146
177
  class AttributeGroup(LabFREED_BaseModel):
147
178
  key: str
148
179
  label: str = ""
149
180
  attributes: list[Attribute]
150
-
151
- state_of: datetime
152
- valid_until: datetime | Literal["forever"] | None = None
153
-
154
- @field_validator('valid_until', mode='before')
155
- def set_utc_valid_until_if_naive(cls, value):
156
- if isinstance(value, datetime):
157
- return ensure_utc(value)
158
- else:
159
- return value
181
+
160
182
 
161
183
 
162
184
  class AttributesOfPACID(LabFREED_BaseModel):
@@ -1,6 +1,6 @@
1
1
 
2
2
 
3
- from datetime import datetime
3
+ from datetime import UTC, datetime, timedelta
4
4
  from typing import Literal, Protocol
5
5
 
6
6
 
@@ -12,28 +12,15 @@ from labfreed.pac_id.pac_id import PAC_ID
12
12
  class CacheableAttributeGroup(AttributeGroup):
13
13
  origin:str
14
14
  language:str
15
- valid_until: Literal['forever'] | datetime | None = None
15
+ value_from: datetime | None = None
16
16
 
17
- # @model_validator(mode='after')
18
- # def set_valid_until(self) -> 'CacheableAttributeGroup':
19
- # vals = [a.valid_until for a in self.attributes]
20
- # if all(e == 'forever' for e in vals):
21
- # self.valid_until = 'forever'
22
- # elif any(e is None for e in vals):
23
- # self.valid_until = None
24
- # else:
25
- # self.valid_until = min(v for v in vals if isinstance(v, datetime))
26
- # return self
27
-
28
-
29
- @property
30
- def still_valid(self):
31
- if self.valid_until is None:
17
+ def still_valid(self, accept_cache_for_minutes):
18
+ if self.value_from is None:
32
19
  return False
33
- if self.valid_until == 'forever':
34
- return True
35
-
36
- return self.valid_until > datetime.now()
20
+ else:
21
+ return ( datetime.now(tz=UTC) - timedelta(minutes=accept_cache_for_minutes)) > self.value_from
22
+
23
+
37
24
 
38
25
 
39
26
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
+ from datetime import UTC, datetime
4
5
  from typing import Protocol, runtime_checkable
5
6
 
6
7
  import requests
@@ -85,6 +86,7 @@ class AttributeClient():
85
86
 
86
87
  http_post_callback:AttributeRequestCallback
87
88
  cache_store:AttributeCache
89
+ always_use_cached_value_for_minutes:int
88
90
 
89
91
  def get_attributes(self,
90
92
  server_url:str,
@@ -120,7 +122,7 @@ class AttributeClient():
120
122
  else:
121
123
  attribute_groups = self.cache_store.get_all(server_url, pac_id)
122
124
 
123
- if attribute_groups and all([ag.still_valid for ag in attribute_groups]):
125
+ if attribute_groups and all([ag.still_valid(accept_cache_for_minutes=self.always_use_cached_value_for_minutes) for ag in attribute_groups]):
124
126
  return attribute_groups
125
127
 
126
128
  # no valid data found in cache > request to server
@@ -154,7 +156,7 @@ class AttributeClient():
154
156
  origin=server_url,
155
157
  language=r.language,
156
158
  label=ag.label,
157
- state_of=ag.state_of)
159
+ value_from=datetime.now(tz=UTC))
158
160
  for ag in ag_for_pac.attribute_groups
159
161
  ]
160
162
  self.cache_store.update(server_url, pac_from_response, ags)
@@ -113,8 +113,7 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
113
113
  attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
114
114
  return AttributeGroup(
115
115
  key=self._attribute_group_key,
116
- attributes=pyAttributes(attributes).to_payload_attributes(),
117
- state_of=last_changed,
116
+ attributes=pyAttributes(attributes).to_payload_attributes()
118
117
  )
119
118
 
120
119
 
@@ -0,0 +1,165 @@
1
+
2
+ from datetime import date, datetime, time
3
+ import json
4
+ from typing import Literal
5
+ import warnings
6
+ from pydantic import RootModel, field_validator
7
+
8
+ from labfreed.labfreed_infrastructure import LabFREED_BaseModel
9
+ from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, ResourceAttribute, TextAttribute
10
+ from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
11
+ from labfreed.pac_id.pac_id import PAC_ID
12
+ from labfreed.trex.pythonic.quantity import Quantity
13
+
14
+
15
+ class pyReference(RootModel[str]):
16
+
17
+ def __str__(self):
18
+ return str(self.root)
19
+
20
+ class pyResource(RootModel[str]):
21
+
22
+ def __str__(self):
23
+ return str(self.root)
24
+
25
+
26
+ # the allowed scalar types
27
+ AllowedValue = str | bool | datetime | pyReference | pyResource | Quantity | int | float | dict | object
28
+ # homogeneous list of those
29
+ AllowedList = list[AllowedValue]
30
+
31
+ class pyAttribute(LabFREED_BaseModel):
32
+ key:str
33
+ label:str = ""
34
+ value: AllowedValue | AllowedList
35
+
36
+ @property
37
+ def value_list(self):
38
+ '''helper function to more conveniently iterate over value elements, even if it's scalar'''
39
+ return self.value if isinstance(self.value, list) else [self.value]
40
+
41
+
42
+ @field_validator('value', mode='before')
43
+ def handle_one_element_list(v):
44
+ if isinstance(v, list) and len(v)==1:
45
+ return v[0]
46
+ else:
47
+ return v
48
+
49
+ class pyAttributes(RootModel[list[pyAttribute]]):
50
+ def to_payload_attributes(self) -> list[AttributeBase]:
51
+ out = []
52
+ for e in self.root:
53
+ apt = self._attribute_to_attribute_payload_type(e)
54
+ if isinstance(apt.value, list) and len(apt.value) ==1:
55
+ apt.value = apt.value[0]
56
+ out.append(apt)
57
+ return out
58
+
59
+
60
+ @staticmethod
61
+ def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
62
+ common_args = {
63
+ "key": attribute.key,
64
+ "label": attribute.label,
65
+ }
66
+ value_list = attribute.value_list
67
+ first_value = value_list[0]
68
+ if isinstance(first_value, bool):
69
+ return BoolAttribute(value=value_list, **common_args)
70
+
71
+ elif isinstance(first_value, datetime | date | time):
72
+ for v in value_list:
73
+ if not v.tzinfo:
74
+ warnings.warn(f'No timezone given for {v}. Assuming it is in UTC.')
75
+ return DateTimeAttribute(value=value_list, **common_args)
76
+ # return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
77
+
78
+
79
+ elif isinstance(first_value, Quantity|int|float):
80
+ values = []
81
+ for v in value_list:
82
+ if not isinstance(v, Quantity):
83
+ v = Quantity(value=v, unit='dimensionless')
84
+ values.append(NumericValue(numerical_value=v.value_as_str(),
85
+ unit = v.unit))
86
+ num_attribute = NumericAttribute(value = values, **common_args)
87
+ num_attribute.print_validation_messages()
88
+ return num_attribute
89
+
90
+ elif isinstance(first_value, str):
91
+ # capture quantities in the form of "100.0e5 g/L"
92
+ if Quantity.from_str_with_unit(first_value):
93
+ values = []
94
+ for v in value_list:
95
+ q = Quantity.from_str_with_unit(v)
96
+ values.append( NumericValue(numerical_value=q.value_as_str(), unit = q.unit) )
97
+ return NumericAttribute(value = values,
98
+ **common_args)
99
+
100
+ else:
101
+ return TextAttribute(value = value_list, **common_args)
102
+
103
+ elif isinstance(first_value, pyReference):
104
+ return ReferenceAttribute(value = [v.root for v in value_list], **common_args)
105
+
106
+ elif isinstance(first_value, pyResource):
107
+ return ResourceAttribute(value = [v.root for v in value_list], **common_args)
108
+
109
+ elif isinstance(first_value, PAC_ID):
110
+ return ReferenceAttribute(value = [v.to_url(include_extensions=False) for v in value_list], **common_args)
111
+
112
+ else: #this covers the last resort case of arbitrary objects. Must be json serializable.
113
+ try :
114
+ values = [json.loads(json.dumps(v)) for v in value_list]
115
+ return ObjectAttribute(value=values, **common_args)
116
+ except TypeError as e: # noqa: F841
117
+ raise ValueError(f'Invalid Type: {type(first_value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
118
+
119
+
120
+
121
+ @staticmethod
122
+ def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
123
+ out = list()
124
+ for a in attributes:
125
+ value_list = a.value if isinstance(a.value, list) else [a.value]
126
+ match a:
127
+ case ReferenceAttribute():
128
+ values = [pyReference(v) for v in value_list]
129
+
130
+ case ResourceAttribute():
131
+ values = [pyResource(v) for v in value_list]
132
+
133
+ case NumericAttribute():
134
+ values = [ Quantity.from_str_value(value=v.numerical_value, unit=v.unit) for v in value_list]
135
+
136
+ case BoolAttribute():
137
+ values = value_list
138
+
139
+ case TextAttribute():
140
+ values = value_list
141
+
142
+ case DateTimeAttribute():
143
+ values = value_list
144
+
145
+ case ObjectAttribute():
146
+ values = value_list
147
+
148
+
149
+ attr = pyAttribute(key=a.key,
150
+ label=a.label,
151
+ value=values
152
+ )
153
+ out.append(attr )
154
+ return out
155
+
156
+
157
+
158
+ class pyAttributeGroup(CacheableAttributeGroup):
159
+ attributes:dict[str,pyAttribute]
160
+
161
+ @staticmethod
162
+ def from_attribute_group(attribute_group:AttributeGroup):
163
+ data = vars(attribute_group).copy()
164
+ data["attributes"] = {a.key: a for a in pyAttributes.from_payload_attributes(attribute_group.attributes)}
165
+ return pyAttributeGroup(**data)
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod, abstractproperty
2
2
  from datetime import datetime, timezone
3
- from labfreed.pac_attributes.api_data_models.response import VALID_FOREVER, AttributeBase, AttributeGroup
3
+ from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup
4
4
  from labfreed.pac_cat.pac_cat import PAC_CAT
5
5
  from labfreed.pac_id.pac_id import PAC_ID
6
6
 
@@ -31,13 +31,13 @@ class AttributeGroupDataSource(ABC):
31
31
 
32
32
 
33
33
  class Dict_DataSource(AttributeGroupDataSource):
34
- def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, *args, **kwargs):
34
+ def __init__(self, data:dict[str, list[AttributeBase]], uses_pac_cat_short_form=True, pac_to_key: callable = None, *args, **kwargs):
35
35
  if not all([isinstance(e, list) for e in data.values()]):
36
36
  raise ValueError('Invalid data')
37
37
 
38
38
  self._data = data
39
- self._state_of = datetime.now(tz=timezone.utc)
40
39
  self.uses_pac_cat_short_form = uses_pac_cat_short_form
40
+ self._pac_to_key = pac_to_key
41
41
 
42
42
  super().__init__(*args, **kwargs)
43
43
 
@@ -54,18 +54,14 @@ class Dict_DataSource(AttributeGroupDataSource):
54
54
  except:
55
55
  ... # might as well try to match the original input
56
56
 
57
- attributes = self._data.get(pac_url)
57
+
58
+ lookup_key = self._pac_to_key(pac_url) if self._pac_to_key else pac_url
59
+ attributes = self._data.get(lookup_key)
58
60
  if not attributes:
59
61
  return None
60
62
 
61
-
62
- valid_until = VALID_FOREVER if self._is_static else None
63
-
64
-
65
63
  return AttributeGroup(key=self._attribute_group_key,
66
- attributes=attributes,
67
- state_of=self._state_of,
68
- valid_until=valid_until)
64
+ attributes=attributes)
69
65
 
70
66
 
71
67
 
@@ -1,3 +1,5 @@
1
+ import re
2
+ import string
1
3
  import traceback
2
4
  import warnings
3
5
 
@@ -132,15 +134,22 @@ class AttributeServerRequestHandler():
132
134
  if dn := self._get_display_name_for_key(ag.key, language):
133
135
  ag.label = dn
134
136
  else:
135
- ag.label = ag.key.split('/')[-1]
137
+ ag.label = self.fallback_label(ag.key)
136
138
  rich.print(f"[yellow]WARNING:[/yellow] No translation for '{ag.key}' in '{language}'. Falling back to '{ag.label}'")
137
139
  for a in ag.attributes:
138
140
  if dn := self._get_display_name_for_key(a.key, language):
139
141
  a.label = dn
140
142
  else:
141
- a.label = a.key.split('/')[-1]
143
+ a.label = self.fallback_label(a.key)
142
144
  rich.print(f"[yellow]WARNING:[/yellow] No translation for '{a.key}' in '{language}'. Falling back to '{a.label}' ")
143
145
 
146
+
147
+ def fallback_label(self, key:str):
148
+ l = key.split('/')[-1]
149
+ l = re.sub(r'([a-z])([A-Z])', r'\1 \2', l)
150
+ l = re.sub(r'[-_]', ' ', l)
151
+ l = string.capwords(l)
152
+ return l
144
153
 
145
154
 
146
155
  def _get_display_name_for_key(self, key, language:str):
@@ -57,7 +57,7 @@ class Category(LabFREED_BaseModel):
57
57
  k = f"{field_name}"
58
58
  out.update({k : v } )
59
59
 
60
- for s in getattr(self, 'additional_segments'):
60
+ for s in getattr(self, 'additional_segments', []):
61
61
  out.update( {s.key or '' : s.value })
62
62
  return out
63
63
 
@@ -116,9 +116,9 @@ class Material_Consumable(PredefinedCategory):
116
116
  return self
117
117
 
118
118
  class Material_Misc(Material_Consumable):
119
- '''Represents the -MC category'''
119
+ '''Represents the -MX category'''
120
120
  # same fields as Consumable
121
- key: str = Field(default='-MM', frozen=True)
121
+ key: str = Field(default='-MX', frozen=True)
122
122
  product_number:str|None = Field( alias='240')
123
123
  batch_number:str|None = Field(default=None, alias='10')
124
124
  packaging_size:str|None = Field(default=None, alias='20')
@@ -187,15 +187,63 @@ class Data_Static(Data_Abstract):
187
187
  additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
188
188
  ''' Category segments, which are not defined in the specification'''
189
189
 
190
+ class Data_Misc(Data_Abstract):
191
+ '''Represents the -DX category'''
192
+ key: str = Field(default='-DX', frozen=True)
193
+ id:str|None = Field( alias='21')
194
+ additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
195
+ ''' Category segments, which are not defined in the specification'''
196
+
197
+
198
+
199
+
200
+ class Processor_Abstract(PredefinedCategory, ABC):
201
+ '''@private'''
202
+ key: str
203
+ processor_instance:str|None = Field( alias='21')
204
+ processor_code:str|None = Field( alias='240')
205
+ additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
206
+ ''' Category segments, which are not defined in the specification'''
207
+
208
+ @model_validator(mode='after')
209
+ def _validate_mandatory_fields(self):
210
+ if not self.id:
211
+ self._add_validation_message(
212
+ source=f"Category {self.key}",
213
+ level = ValidationMsgLevel.ERROR,
214
+ msg=f"Category key {self.key} is missing mandatory field 'processor instance'",
215
+ highlight_pattern = f"{self.key}"
216
+ )
217
+ return self
218
+
219
+ class Processor_Software(Processor_Abstract):
220
+ '''Represents the -PS category'''
221
+ key: str = Field(default='-PS', frozen=True)
222
+ processor_instance:str|None = Field( alias='21')
223
+ processor_code:str|None = Field( alias='240')
224
+ additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
225
+ ''' Category segments, which are not defined in the specification'''
226
+
227
+ class Processor_Misc(Processor_Abstract):
228
+ '''Represents the -PX category'''
229
+ key: str = Field(default='-PX', frozen=True)
230
+ processor_instance:str|None = Field( alias='21')
231
+ processor_code:str|None = Field( alias='240')
232
+ additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
233
+ ''' Category segments, which are not defined in the specification'''
234
+
190
235
 
191
236
  category_key_to_class_map = {
192
237
  '-MD': Material_Device,
193
238
  '-MS': Material_Substance,
194
239
  '-MC': Material_Consumable,
195
- '-MM': Material_Misc,
240
+ '-MX': Material_Misc,
196
241
  '-DM': Data_Method,
197
242
  '-DR': Data_Result,
198
243
  '-DC': Data_Calibration,
199
244
  '-DP': Data_Progress,
200
- '-DS': Data_Static
245
+ '-DS': Data_Static,
246
+ '-DX': Data_Misc,
247
+ '-PS': Processor_Software,
248
+ '-PX': Processor_Misc
201
249
  }
@@ -1,133 +0,0 @@
1
-
2
- from datetime import date, datetime, time
3
- import json
4
- from typing import Literal
5
- import warnings
6
- from pydantic import RootModel
7
-
8
- from labfreed.labfreed_infrastructure import LabFREED_BaseModel
9
- from labfreed.pac_attributes.api_data_models.response import AttributeBase, AttributeGroup, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, TextAttribute
10
- from labfreed.pac_attributes.client.attribute_cache import CacheableAttributeGroup
11
- from labfreed.pac_id.pac_id import PAC_ID
12
- from labfreed.trex.pythonic.quantity import Quantity
13
-
14
-
15
- class pyReference(RootModel[str]):
16
- pass
17
-
18
- def __str__(self):
19
- return str(self.root)
20
-
21
-
22
- class pyAttribute(LabFREED_BaseModel):
23
- key:str
24
- label:str = ""
25
- value: str|bool|datetime|pyReference|Quantity|int|float|dict|object
26
- valid_until: datetime | Literal["forever"] | None = None
27
- observed_at: datetime | None = None
28
-
29
-
30
-
31
- class pyAttributes(RootModel[list[pyAttribute]]):
32
- def to_payload_attributes(self) -> list[AttributeBase]:
33
- return [self._attribute_to_attribute_payload_type(e) for e in self.root]
34
-
35
-
36
- @staticmethod
37
- def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
38
- common_args = {
39
- "key": attribute.key,
40
- "label": attribute.label,
41
- "observed_at": attribute.observed_at
42
- }
43
- value = attribute.value
44
-
45
- if isinstance(value, bool):
46
- return BoolAttribute(value=value, **common_args)
47
-
48
- elif isinstance(value, datetime | date | time):
49
- if not value.tzinfo:
50
- warnings.warn(f'No timezone given for {value}. Assuming it is in UTC.')
51
- return DateTimeAttribute(value =value, **common_args)
52
- # return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
53
-
54
-
55
- elif isinstance(attribute.value, Quantity|int|float):
56
- if not isinstance(attribute.value, Quantity):
57
- value = Quantity(value=attribute.value, unit='dimensionless')
58
- num_attribute = NumericAttribute(value = NumericValue(numerical_value=value.value_as_str(),
59
- unit = value.unit),
60
- **common_args)
61
- num_attribute.print_validation_messages()
62
- return num_attribute
63
-
64
- elif isinstance(value, str):
65
- # capture quantities in the form of "100.0e5 g/L"
66
- if q := Quantity.from_str_with_unit(value):
67
- return NumericAttribute(value = NumericValue(numerical_value=q.value_as_str(),
68
- unit = q.unit),
69
- **common_args)
70
- else:
71
- return TextAttribute(value = value, **common_args)
72
-
73
- elif isinstance(value, pyReference):
74
- return ReferenceAttribute(value = value.root, **common_args)
75
-
76
- elif isinstance(value, PAC_ID):
77
- return ReferenceAttribute(value = value.to_url(include_extensions=False), **common_args)
78
-
79
-
80
-
81
- else: #this covers the last resort case of arbitrary objects. Must be json serializable.
82
- try :
83
- value = json.loads(json.dumps(value))
84
- return ObjectAttribute(value=value, **common_args)
85
- except TypeError as e: # noqa: F841
86
- raise ValueError(f'Invalid Type: {type(value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
87
-
88
-
89
- @staticmethod
90
- def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
91
- out = list()
92
- for a in attributes:
93
- match a:
94
-
95
- case ReferenceAttribute():
96
- value = pyReference(a.value)
97
-
98
- case NumericAttribute():
99
- value = Quantity.from_str_value(value=a.value.numerical_value, unit=a.value.unit)
100
-
101
- case BoolAttribute():
102
- value = a.value
103
-
104
- case TextAttribute():
105
- value = a.value
106
-
107
- case DateTimeAttribute():
108
- value = a.value
109
-
110
- case ObjectAttribute():
111
- value = a.value
112
-
113
-
114
- attr = pyAttribute(key=a.key,
115
- label=a.label,
116
- value=value,
117
- observed_at=a.observed_at
118
- # valid_until=datetime(**_parse_date_time_str(a.valid_until)),
119
- # observed_at=datetime(**_parse_date_time_str(a.value))
120
- )
121
- out.append(attr )
122
- return out
123
-
124
-
125
-
126
- class pyAttributeGroup(CacheableAttributeGroup):
127
- attributes:dict[str,pyAttribute]
128
-
129
- @staticmethod
130
- def from_attribute_group(attribute_group:AttributeGroup):
131
- data = vars(attribute_group).copy()
132
- data["attributes"] = {a.key: a for a in pyAttributes.from_payload_attributes(attribute_group.attributes)}
133
- return pyAttributeGroup(**data)
File without changes
File without changes
File without changes