labfreed 1.0.0a5__tar.gz → 1.0.0a7__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.0a5 → labfreed-1.0.0a7}/CHANGELOG.md +14 -0
  2. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/PKG-INFO +1 -1
  3. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/__init__.py +1 -1
  4. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/app_infrastructure.py +2 -2
  5. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/pac_info.py +5 -5
  6. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/api_data_models/response.py +54 -32
  7. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/client/attribute_cache.py +8 -21
  8. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/client/client.py +4 -2
  9. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -2
  10. labfreed-1.0.0a7/labfreed/pac_attributes/pythonic/py_attributes.py +165 -0
  11. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/server/attribute_data_sources.py +2 -9
  12. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/server/server.py +8 -1
  13. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_cat/category_base.py +1 -1
  14. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_cat/predefined_categories.py +0 -1
  15. labfreed-1.0.0a5/labfreed/pac_attributes/pythonic/py_attributes.py +0 -133
  16. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  17. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/.github/workflows/pypi-publish.yml +0 -0
  18. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/.github/workflows/run-tests.yml +0 -0
  19. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/LICENSE +0 -0
  20. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/README.md +0 -0
  21. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
  22. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -0
  23. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -0
  24. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -0
  25. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -0
  26. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -0
  27. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/labfreed_infrastructure.py +0 -0
  28. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/__init__.py +0 -0
  29. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/api_data_models/request.py +0 -0
  30. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  31. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/client/__init__.py +0 -0
  32. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +0 -0
  33. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
  34. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/server/__init__.py +0 -0
  35. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  36. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
  37. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_cat/__init__.py +0 -0
  38. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_cat/pac_cat.py +0 -0
  39. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/__init__.py +0 -0
  40. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/extension.py +0 -0
  41. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/id_segment.py +0 -0
  42. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/pac_id.py +0 -0
  43. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/url_parser.py +0 -0
  44. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id/url_serializer.py +0 -0
  45. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/__init__.py +0 -0
  46. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/cit_common.py +0 -0
  47. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/cit_v1.py +0 -0
  48. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/cit_v2.py +0 -0
  49. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/resolver.py +0 -0
  50. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/pac_id_resolver/services.py +0 -0
  51. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/qr/__init__.py +0 -0
  52. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/qr/generate_qr.py +0 -0
  53. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/__init__.py +0 -0
  54. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/pythonic/__init__.py +0 -0
  55. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/pythonic/data_table.py +0 -0
  56. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/pythonic/pyTREX.py +0 -0
  57. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/pythonic/quantity.py +0 -0
  58. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/table_segment.py +0 -0
  59. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/trex.py +0 -0
  60. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/trex_base_models.py +0 -0
  61. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/trex/value_segments.py +0 -0
  62. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/utilities/base36.py +0 -0
  63. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/utilities/ensure_utc_time.py +0 -0
  64. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/utilities/translations.py +0 -0
  65. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_extensions/__init__.py +0 -0
  66. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
  67. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_extensions/display_name_extension.py +0 -0
  68. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_extensions/trex_extension.py +0 -0
  69. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  70. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  71. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  72. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  73. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/unece/__init__.py +0 -0
  74. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/labfreed/well_known_keys/unece/unece_units.py +0 -0
  75. {labfreed-1.0.0a5 → labfreed-1.0.0a7}/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.0a5
3
+ Version: 1.0.0a7
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.0a5"
5
+ __version__ = "1.0.0a7"
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.attributes_groups = attribute_groups
75
75
 
76
76
  return pac_info
77
77
 
@@ -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
+ attributes_groups:dict[str, pyAttributeGroup] = Field(default_factory=dict)
24
24
 
25
25
  @property
26
26
  def pac_url(self):
@@ -44,7 +44,7 @@ class PacInfo(BaseModel):
44
44
 
45
45
  @property
46
46
  def image_url(self) -> str:
47
- if meta := self.attributes.get(MetaAttributeKeys.GROUPKEY.value):
47
+ if meta := self.attributes_groups.get(MetaAttributeKeys.GROUPKEY.value):
48
48
  image_attr = meta.attributes.get(MetaAttributeKeys.IMAGE.value)
49
49
  return image_attr.value
50
50
 
@@ -73,7 +73,7 @@ class PacInfo(BaseModel):
73
73
  @cached_property
74
74
  def _all_attributes(self) -> dict[str, pyAttribute]:
75
75
  out = {}
76
- for ag in self.attributes.values():
76
+ for ag in self.attributes_groups.values():
77
77
  out.update(ag.attributes)
78
78
  return out
79
79
 
@@ -105,12 +105,12 @@ class PacInfo(BaseModel):
105
105
 
106
106
 
107
107
  printout.title1("Attributes")
108
- for ag in self.attributes.values():
108
+ for ag in self.attributes_groups.values():
109
109
  printout.title2(f'{ag.label} (from {ag.origin})')
110
110
  for v in ag.attributes.values():
111
111
  v:pyAttribute
112
112
  #print(f'{k}: ({v.label}) :: {v.value} ')
113
- printout.key_value(v.label, v.value)
113
+ printout.key_value(v.label, ', '.join([str(e) for e in v.value_list]))
114
114
 
115
115
  out = printout.getvalue()
116
116
 
@@ -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 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() - timedelta(min=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 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=datetime.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
 
@@ -36,7 +36,6 @@ class Dict_DataSource(AttributeGroupDataSource):
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
41
40
 
42
41
  super().__init__(*args, **kwargs)
@@ -58,14 +57,8 @@ class Dict_DataSource(AttributeGroupDataSource):
58
57
  if not attributes:
59
58
  return None
60
59
 
61
-
62
- valid_until = VALID_FOREVER if self._is_static else None
63
-
64
-
65
60
  return AttributeGroup(key=self._attribute_group_key,
66
- attributes=attributes,
67
- state_of=self._state_of,
68
- valid_until=valid_until)
61
+ attributes=attributes)
69
62
 
70
63
 
71
64
 
@@ -1,3 +1,5 @@
1
+ import re
2
+ import string
1
3
  import traceback
2
4
  import warnings
3
5
 
@@ -132,7 +134,12 @@ 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 = string.capwords(
138
+ re.sub(r'([a-z])([A-Z])', r'\1 \2',
139
+ re.sub('-_', '',
140
+ ag.key.split('/')[-1])
141
+ )
142
+ )
136
143
  rich.print(f"[yellow]WARNING:[/yellow] No translation for '{ag.key}' in '{language}'. Falling back to '{ag.label}'")
137
144
  for a in ag.attributes:
138
145
  if dn := self._get_display_name_for_key(a.key, language):
@@ -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
 
@@ -202,7 +202,6 @@ class Processor_Abstract(PredefinedCategory, ABC):
202
202
  key: str
203
203
  processor_instance:str|None = Field( alias='21')
204
204
  processor_code:str|None = Field( alias='240')
205
-
206
205
  additional_segments: list[IDSegment] = Field(default_factory=list, exclude=True)
207
206
  ''' Category segments, which are not defined in the specification'''
208
207
 
@@ -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