labfreed 1.0.0a25__tar.gz → 1.0.0b27__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.

Potentially problematic release.


This version of labfreed might be problematic. Click here for more details.

Files changed (115) hide show
  1. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/CHANGELOG.md +9 -3
  2. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/PKG-INFO +1 -1
  3. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/__init__.py +1 -1
  4. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/app/app_infrastructure.py +2 -5
  5. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/app/pac_info/pac_info.py +6 -8
  6. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/lib/attribute.py +9 -6
  7. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/logo.svg +44 -44
  8. labfreed-1.0.0b27/labfreed/pac_attributes/api_data_models/request.py +108 -0
  9. labfreed-1.0.0b27/labfreed/pac_attributes/api_data_models/response.py +251 -0
  10. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/client/client.py +30 -36
  11. labfreed-1.0.0b27/labfreed/pac_attributes/client/client_attribute_group.py +18 -0
  12. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +39 -13
  13. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -1
  14. labfreed-1.0.0b27/labfreed/pac_attributes/pythonic/py_attributes.py +186 -0
  15. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/server/attribute_data_sources.py +9 -8
  16. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/server/server.py +33 -37
  17. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/resolver_config.py +2 -1
  18. labfreed-1.0.0b27/tst_attr.py +202 -0
  19. labfreed-1.0.0a25/labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -188
  20. labfreed-1.0.0a25/labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -176
  21. labfreed-1.0.0a25/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -46
  22. labfreed-1.0.0a25/labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -7
  23. labfreed-1.0.0a25/labfreed/labfreed_extended/pac_issuer_lib/static/external-link.svg +0 -7
  24. labfreed-1.0.0a25/labfreed/pac_attributes/api_data_models/request.py +0 -56
  25. labfreed-1.0.0a25/labfreed/pac_attributes/api_data_models/response.py +0 -280
  26. labfreed-1.0.0a25/labfreed/pac_attributes/client/__init__.py +0 -0
  27. labfreed-1.0.0a25/labfreed/pac_attributes/client/attribute_cache.py +0 -65
  28. labfreed-1.0.0a25/labfreed/pac_attributes/pythonic/py_attributes.py +0 -208
  29. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/.github/workflows/pypi-publish.yml +0 -0
  31. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/.github/workflows/run-tests.yml +0 -0
  32. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/LICENSE +0 -0
  33. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/README.md +0 -0
  34. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
  35. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/app_factory.py +0 -0
  36. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/lib/bp_landing_page.py +0 -0
  37. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/lib/utils.py +0 -0
  38. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cancel.svg +0 -0
  39. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-DC.svg +0 -0
  40. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-DM.svg +0 -0
  41. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-DP.svg +0 -0
  42. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-DR.svg +0 -0
  43. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-DS.svg +0 -0
  44. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-MC.svg +0 -0
  45. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-MD.svg +0 -0
  46. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-MS.svg +0 -0
  47. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/cat-MX.svg +0 -0
  48. {labfreed-1.0.0a25/labfreed/labfreed_extended/app/pac_info/html_renderer → labfreed-1.0.0b27/labfreed/labfreed_extended/pac_issuer_lib/static}/external-link.svg +0 -0
  49. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/menu.svg +0 -0
  50. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/search.svg +0 -0
  51. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/styles.css +0 -0
  52. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/styles_brand.css +0 -0
  53. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/static/styles_pac_info.css +0 -0
  54. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/flash_error_messages.jinja.html +0 -0
  55. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/footer.jinja.html +0 -0
  56. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/main.jinja.html +0 -0
  57. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/navbar.jinja.html +0 -0
  58. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_info/base-components.jinja.html +0 -0
  59. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_info/card-components.jinja.html +0 -0
  60. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_info/card.jinja.html +0 -0
  61. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_info/pac_info.jinja.html +0 -0
  62. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_issuer_error.jinja.html +0 -0
  63. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_extended/pac_issuer_lib/templates/pac_issuer_landing_page.jinja.html +0 -0
  64. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/labfreed_infrastructure.py +0 -0
  65. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/__init__.py +0 -0
  66. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  67. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
  68. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/server/__init__.py +0 -0
  69. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  70. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
  71. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_cat/__init__.py +0 -0
  72. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_cat/category_base.py +0 -0
  73. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_cat/pac_cat.py +0 -0
  74. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_cat/predefined_categories.py +0 -0
  75. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/__init__.py +0 -0
  76. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/extension.py +0 -0
  77. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/id_segment.py +0 -0
  78. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/pac_id.py +0 -0
  79. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/url_parser.py +0 -0
  80. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id/url_serializer.py +0 -0
  81. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/__init__.py +0 -0
  82. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/cit_v1.py +0 -0
  83. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/resolver.py +0 -0
  84. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/resolver_config_common.py +0 -0
  85. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/pac_id_resolver/services.py +0 -0
  86. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/qr/__init__.py +0 -0
  87. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/qr/generate_qr.py +0 -0
  88. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/__init__.py +0 -0
  89. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/pythonic/__init__.py +0 -0
  90. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/pythonic/data_table.py +0 -0
  91. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/pythonic/pyTREX.py +0 -0
  92. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/pythonic/quantity.py +0 -0
  93. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/table_segment.py +0 -0
  94. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/trex.py +0 -0
  95. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/trex_base_models.py +0 -0
  96. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/trex/value_segments.py +0 -0
  97. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/utilities/base36.py +0 -0
  98. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/utilities/ensure_utc_time.py +0 -0
  99. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/utilities/translations.py +0 -0
  100. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_extensions/__init__.py +0 -0
  101. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
  102. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_extensions/display_name_extension.py +0 -0
  103. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_extensions/text_base36_extension.py +0 -0
  104. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_extensions/trex_extension.py +0 -0
  105. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  106. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  107. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  108. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  109. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/unece/__init__.py +0 -0
  110. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed/well_known_keys/unece/unece_units.py +0 -0
  111. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed_experimental/pac_disco/ble_central.py +0 -0
  112. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed_experimental/pac_disco/ble_peripheral.py +0 -0
  113. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed_experimental/pac_disco/ble_uuid.py +0 -0
  114. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/labfreed_experimental/pac_disco/pac_disco_demo.py +0 -0
  115. {labfreed-1.0.0a25 → labfreed-1.0.0b27}/pyproject.toml +0 -0
@@ -5,12 +5,18 @@ PAC-CAT
5
5
  - added new categories
6
6
  - BREAKING: Renamed category MM to MX
7
7
 
8
+ PAC-ID Resolver
9
+ - Transition to improved resolver configuration ( replaces coupling information table )
8
10
 
9
- PAC-ID Attributes
11
+
12
+ PAC-ID Attributes (Beta)
10
13
  - new building block
11
-
12
14
 
13
-
15
+
16
+ General
17
+ - Minor Bugfixes
18
+ - BREAKING: reorganization of module structure > some import paths have changed
19
+
14
20
 
15
21
 
16
22
  ### v0.2.12
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 1.0.0a25
3
+ Version: 1.0.0b27
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.0a25"
5
+ __version__ = "1.0.0b27"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -4,7 +4,6 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
4
4
  import requests
5
5
 
6
6
  from labfreed.labfreed_extended.app.pac_info.pac_info import PacInfo
7
- from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
8
7
  from labfreed.pac_attributes.client.client import AttributeClient, http_attribute_request_default_callback_factory
9
8
  from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
10
9
 
@@ -14,8 +13,6 @@ from labfreed.pac_id_resolver.services import ServiceGroup
14
13
 
15
14
 
16
15
 
17
-
18
-
19
16
  class Labfreed_App_Infrastructure():
20
17
  def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None, use_issuer_resolver_config=True):
21
18
  if isinstance(language_preferences, str):
@@ -30,7 +27,7 @@ class Labfreed_App_Infrastructure():
30
27
  self._http_client= http_client
31
28
  callback = http_attribute_request_default_callback_factory(http_client)
32
29
 
33
- self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache(), always_use_cached_value_for_minutes=1)
30
+ self._attribute_client = AttributeClient(http_post_callback=callback)
34
31
 
35
32
 
36
33
  def add_resolver_config(self, cit:str):
@@ -79,7 +76,7 @@ class Labfreed_App_Infrastructure():
79
76
  for sg in service_groups:
80
77
  attributes_urls = [s.url for s in sg.services if s.service_type == 'attributes-generic']
81
78
  for url in attributes_urls:
82
- 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)}
79
+ ags = {ag.group_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)}
83
80
  if ags:
84
81
  attribute_groups.update(ags)
85
82
  pac_info.attribute_groups = attribute_groups
@@ -1,5 +1,3 @@
1
-
2
-
3
1
  from functools import cached_property
4
2
  from pathlib import Path
5
3
  from urllib.parse import urlparse
@@ -164,10 +162,10 @@ class PacInfo(BaseModel):
164
162
  @cached_property
165
163
  def image_url(self) -> str:
166
164
  image_attr = self._all_attributes.get(MetaAttributeKeys.IMAGE.value)
167
- if isinstance(image_attr.value, pyResource):
168
- return image_attr.value.root
169
- if isinstance(image_attr.value, str):
170
- return image_attr.value
165
+ if isinstance(image_attr.values, pyResource):
166
+ return image_attr.values.root
167
+ if isinstance(image_attr.values, str):
168
+ return image_attr.values
171
169
 
172
170
 
173
171
  @cached_property
@@ -180,7 +178,7 @@ class PacInfo(BaseModel):
180
178
  # there can be a display name in attributes, too
181
179
 
182
180
  if dn_attr := self._all_attributes.get(MetaAttributeKeys.DISPLAYNAME.value):
183
- dn = dn_attr.value
181
+ dn = dn_attr.values
184
182
  display_name = dn + f' ( aka {display_name} )' if display_name else dn
185
183
 
186
184
  if not display_name and self.main_category:
@@ -244,7 +242,7 @@ class PacInfo(BaseModel):
244
242
 
245
243
  printout.title1("Attributes")
246
244
  for ag in self.attribute_groups.values():
247
- printout.title2(f'{ag.label} (from {ag.origin})')
245
+ printout.title2(f'{ag.group_label} (from {ag.origin})')
248
246
  for v in ag.attributes.values():
249
247
  v:pyAttribute
250
248
  #print(f'{k}: ({v.label}) :: {v.value} ')
@@ -1,6 +1,7 @@
1
1
 
2
2
  import logging
3
3
  from flask import render_template, request
4
+ from labfreed.pac_attributes.api_data_models.request import AttributeRequestData
4
5
  from labfreed.pac_attributes.api_data_models.response import AttributeGroup
5
6
  from labfreed.pac_cat.pac_cat import PAC_CAT
6
7
  from labfreed.pac_cat.predefined_categories import Material_Device, Material_Consumable, Material_Substance
@@ -75,7 +76,7 @@ class DynamicDemoAttributeGroup(AttributeGroupDataSource):
75
76
  return None
76
77
 
77
78
  attributes = pyAttributes( [pyAttribute(key=d[0], value=d[1]) for d in self._data] ).to_payload_attributes()
78
- return AttributeGroup(key=self._attribute_group_key,
79
+ return AttributeGroup(group_key=self._attribute_group_key,
79
80
  attributes=attributes)
80
81
 
81
82
 
@@ -106,7 +107,7 @@ class SessionLocalDirectCall(requests.Session):
106
107
  self._request_handlers = request_handlers
107
108
 
108
109
 
109
- def post(self, url, *args, **kwargs):
110
+ def get(self, url, *args, **kwargs):
110
111
 
111
112
  if is_self_request(url):
112
113
  # Case: server calls itself
@@ -122,11 +123,15 @@ class SessionLocalDirectCall(requests.Session):
122
123
  # Example: directly call the Flask view function instead of HTTP
123
124
  # You could map URLs to functions if you know your routing
124
125
  # For now, just return a mock response
125
- rh = self._request_handlers.get(path.strip("/"))
126
+ path, pac = path.strip("/").rsplit("/", 1)
127
+ rh = self._request_handlers.get(path)
126
128
 
127
129
  r = Response()
128
130
  if rh:
129
- body = rh.handle_attribute_request(json_request_body=kwargs.get("data"))
131
+ request_data = AttributeRequestData.from_http_request(pac_id = pac,
132
+ params = kwargs.get('params'),
133
+ headers = kwargs.get('headers'))
134
+ body = rh.handle_attribute_request(request_data=request_data)
130
135
  r.status_code = 200
131
136
  r._content = body.encode("utf-8")
132
137
  r.encoding = "utf-8"
@@ -161,8 +166,6 @@ def is_self_request(url: str) -> bool:
161
166
  target_ip = resolve_ip(parsed.hostname.lower())
162
167
  current_ip = resolve_ip(request.host.split(":")[0].lower())
163
168
 
164
-
165
-
166
169
  return target_ip == current_ip
167
170
 
168
171
 
@@ -1,44 +1,44 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <svg id="LabfreedLogo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 233" width="200" height="116.5">
3
- <defs>
4
- <style>
5
- .cls-1 { fill: #51a1d7; }
6
- .cls-2 { fill: #438ad7; }
7
- .cls-3 { fill: #94d5e4; }
8
- .cls-4 { fill: #4db9d2; }
9
- .cls-5 { fill: #2b63bc; }
10
- .brand-text {
11
- font-family: Arial, sans-serif;
12
- font-size: 24px;
13
- font-weight: bold;
14
- fill: #51a1d7;
15
- }
16
- </style>
17
- </defs>
18
-
19
- <!-- Half-size dove -->
20
- <g transform="scale(0.5)">
21
- <g>
22
- <g>
23
- <polygon class="cls-3" points="115.8 143.51 115.8 143.55 115.77 143.51 115.8 143.51"/>
24
- <polygon class="cls-1" points="208.4 97.2 141.5 164.1 138.93 158.95 115.78 112.63 108.05 97.2 208.4 97.2"/>
25
- <polygon class="cls-2" points="208.4 97.19 108.05 97.19 138.93 27.74 169.8 58.61 208.4 97.19"/>
26
- <polygon class="cls-5" points="208.4 43.17 208.4 97.2 169.81 58.62 208.4 43.17"/>
27
- <polygon class="cls-2" points="247 74.05 208.4 74.05 208.4 43.17 247 74.05"/>
28
- </g>
29
- <path class="cls-4" d="M108.05,189.82h.02v-7.72h-.02v7.72ZM108.06,143.51v7.72h.02v-7.72h-.02ZM108.06,158.93v15.45h.02v-15.45h-.02Z"/>
30
- <path class="cls-2" d="M141.5,164.1l-2.57-5.15-23.15-46.32-7.72-15.43v46.3h.02v7.72h-.02v7.71h.02v15.45h-.02v7.72h.02v7.72h-.02v30.87l38.6-46.32-5.15-10.27ZM115.8,143.55l-.03-.05h.03v.05Z"/>
31
- <rect class="cls-2" x="30.88" y="43.16" width="15.43" height="15.45"/>
32
- <rect class="cls-2" x="15.43" y="27.73" width="15.44" height="15.44"/>
33
- <rect class="cls-1" y="12.3" width="15.44" height="15.44"/>
34
- <rect class="cls-2" x="61.75" y="74.04" width="15.43" height="15.45"/>
35
- <rect class="cls-1" x="46.32" y="74.05" width="15.44" height="15.44"/>
36
- <rect class="cls-1" x="30.87" y="89.49" width="15.44" height="15.44"/>
37
- <rect class="cls-1" y="58.61" width="15.44" height="15.44"/>
38
- </g>
39
- <polygon class="cls-5" points="138.93 27.74 108.05 97.19 92.63 97.19 92.63 89.48 77.18 89.48 77.18 74.04 61.75 74.04 61.75 58.61 46.31 58.61 46.31 43.16 30.88 43.16 30.88 27.75 30.86 27.74 138.93 27.74"/>
40
- </g>
41
-
42
- <!-- Branding text -->
43
- <text class="brand-text" x="0" y="130">Powered by LabFREED </text>
44
- </svg>
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="LabfreedLogo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 233" width="200" height="116.5">
3
+ <defs>
4
+ <style>
5
+ .cls-1 { fill: #51a1d7; }
6
+ .cls-2 { fill: #438ad7; }
7
+ .cls-3 { fill: #94d5e4; }
8
+ .cls-4 { fill: #4db9d2; }
9
+ .cls-5 { fill: #2b63bc; }
10
+ .brand-text {
11
+ font-family: Arial, sans-serif;
12
+ font-size: 24px;
13
+ font-weight: bold;
14
+ fill: #51a1d7;
15
+ }
16
+ </style>
17
+ </defs>
18
+
19
+ <!-- Half-size dove -->
20
+ <g transform="scale(0.5)">
21
+ <g>
22
+ <g>
23
+ <polygon class="cls-3" points="115.8 143.51 115.8 143.55 115.77 143.51 115.8 143.51"/>
24
+ <polygon class="cls-1" points="208.4 97.2 141.5 164.1 138.93 158.95 115.78 112.63 108.05 97.2 208.4 97.2"/>
25
+ <polygon class="cls-2" points="208.4 97.19 108.05 97.19 138.93 27.74 169.8 58.61 208.4 97.19"/>
26
+ <polygon class="cls-5" points="208.4 43.17 208.4 97.2 169.81 58.62 208.4 43.17"/>
27
+ <polygon class="cls-2" points="247 74.05 208.4 74.05 208.4 43.17 247 74.05"/>
28
+ </g>
29
+ <path class="cls-4" d="M108.05,189.82h.02v-7.72h-.02v7.72ZM108.06,143.51v7.72h.02v-7.72h-.02ZM108.06,158.93v15.45h.02v-15.45h-.02Z"/>
30
+ <path class="cls-2" d="M141.5,164.1l-2.57-5.15-23.15-46.32-7.72-15.43v46.3h.02v7.72h-.02v7.71h.02v15.45h-.02v7.72h.02v7.72h-.02v30.87l38.6-46.32-5.15-10.27ZM115.8,143.55l-.03-.05h.03v.05Z"/>
31
+ <rect class="cls-2" x="30.88" y="43.16" width="15.43" height="15.45"/>
32
+ <rect class="cls-2" x="15.43" y="27.73" width="15.44" height="15.44"/>
33
+ <rect class="cls-1" y="12.3" width="15.44" height="15.44"/>
34
+ <rect class="cls-2" x="61.75" y="74.04" width="15.43" height="15.45"/>
35
+ <rect class="cls-1" x="46.32" y="74.05" width="15.44" height="15.44"/>
36
+ <rect class="cls-1" x="30.87" y="89.49" width="15.44" height="15.44"/>
37
+ <rect class="cls-1" y="58.61" width="15.44" height="15.44"/>
38
+ </g>
39
+ <polygon class="cls-5" points="138.93 27.74 108.05 97.19 92.63 97.19 92.63 89.48 77.18 89.48 77.18 74.04 61.75 74.04 61.75 58.61 46.31 58.61 46.31 43.16 30.88 43.16 30.88 27.75 30.86 27.74 138.93 27.74"/>
40
+ </g>
41
+
42
+ <!-- Branding text -->
43
+ <text class="brand-text" x="0" y="130">Powered by LabFREED </text>
44
+ </svg>
@@ -0,0 +1,108 @@
1
+ from typing import Any, Self
2
+ from urllib.parse import unquote
3
+ from werkzeug.datastructures import LanguageAccept
4
+ from werkzeug.http import parse_accept_header
5
+ from pydantic import ConfigDict, field_validator, model_validator
6
+ from labfreed.labfreed_infrastructure import LabFREED_BaseModel, LabFREED_ValidationError, ValidationMsgLevel
7
+ from labfreed.pac_id.pac_id import PAC_ID
8
+
9
+ ATTR_GROUPS = 'attr_grps'
10
+ ATTR_GROUPS_FWD_LKP= 'attr_fwd_lkp'
11
+
12
+
13
+ class AttributeRequestData(LabFREED_BaseModel):
14
+ model_config = ConfigDict(arbitrary_types_allowed=True)
15
+
16
+ pac_id: str
17
+ language_preferences: LanguageAccept|None = None
18
+ restrict_to_attribute_groups: list[str]|None = None
19
+ do_forward_lookup: bool = True
20
+
21
+ def as_json(self):
22
+ return self.model_dump_json()
23
+
24
+ @classmethod
25
+ def from_json(cls, json) -> Self:
26
+ return cls.model_validate_json(json)
27
+
28
+ @classmethod
29
+ def from_http_request(cls, pac_id:str, params:dict, headers:dict):
30
+ restrict_to_attribute_groups = params.get(ATTR_GROUPS)
31
+ if restrict_to_attribute_groups == '':
32
+ restrict_to_attribute_groups = None
33
+ if restrict_to_attribute_groups:
34
+ restrict_to_attribute_groups = restrict_to_attribute_groups.split(',')
35
+
36
+ fwd_lkp = params.get(ATTR_GROUPS_FWD_LKP, True)
37
+ if fwd_lkp is True:
38
+ do_forward_lookup = True
39
+ else:
40
+ do_fwd_lookup = fwd_lkp.lower() not in ['false', 'no', '0', 'n', 'off']
41
+
42
+ lang_hdr = headers.get('Accept-Language')
43
+ language_preferences: LanguageAccept = parse_accept_header(lang_hdr, LanguageAccept)
44
+ out = cls(pac_id=pac_id,
45
+ restrict_to_attribute_groups = restrict_to_attribute_groups,
46
+ do_forward_lookup = do_forward_lookup,
47
+ language_preferences=language_preferences
48
+ )
49
+ return out
50
+
51
+
52
+
53
+ @model_validator(mode="before")
54
+ @classmethod
55
+ def _scalars_to_list(cls, d):
56
+ if isinstance(lp:= d.get("language_preferences"), str):
57
+ d["language_preferences"] = [lp]
58
+ if isinstance(rag := d.get("restrict_to_attribute_groups"), str):
59
+ d["restrict_to_attribute_groups"] = [rag]
60
+ return d
61
+
62
+
63
+ @field_validator('language_preferences', mode='before')
64
+ @classmethod
65
+ def convert_language_preferences(cls,lp):
66
+ if isinstance(lp, LanguageAccept):
67
+ return lp
68
+ lq = [(lng, 1-i/len(lp)) for i, lng in enumerate(lp)]
69
+ return LanguageAccept(lq)
70
+
71
+
72
+ @model_validator(mode="after")
73
+ def _revert_url_encoding(self):
74
+ self.pac_id = unquote(self.pac_id)
75
+ if self.restrict_to_attribute_groups:
76
+ self.restrict_to_attribute_groups = [unquote(g) for g in self.restrict_to_attribute_groups]
77
+ return self
78
+
79
+ @model_validator(mode="after")
80
+ def _validate_pacs(self) -> Self:
81
+ try:
82
+ PAC_ID.from_url(self.pac_id)
83
+ except LabFREED_ValidationError:
84
+ self._add_validation_message(
85
+ source="pac_id",
86
+ level = ValidationMsgLevel.ERROR,
87
+ msg='{self.pac_id} is not a valid PAC-ID'
88
+ )
89
+
90
+ if not self.is_valid:
91
+ raise LabFREED_ValidationError(message='Invalid request', validation_msgs=self.validation_messages())
92
+
93
+ return self
94
+
95
+ def language_preference_http_header(self) -> dict[str, str]:
96
+ if not self.language_preferences:
97
+ return {}
98
+ headers={'Accept-Language': LanguageAccept(self.language_preferences).to_header()}
99
+ return headers
100
+
101
+ def request_params(self) -> dict[str, Any]:
102
+ params = {ATTR_GROUPS_FWD_LKP: self.do_forward_lookup}
103
+ if self.restrict_to_attribute_groups:
104
+ params.update({ATTR_GROUPS: ','.join(self.restrict_to_attribute_groups)})
105
+ return params
106
+
107
+
108
+
@@ -0,0 +1,251 @@
1
+
2
+ from abc import ABC
3
+ from datetime import datetime
4
+ import re
5
+ from typing import Annotated, Any, Literal, Union, get_args
6
+ from urllib.parse import urlparse
7
+
8
+ from labfreed.utilities.ensure_utc_time import ensure_utc
9
+ from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
10
+ from pydantic import BaseModel, Field, field_validator, model_validator
11
+
12
+
13
+
14
+ class AttributeItemsElementBase(LabFREED_BaseModel, ABC):
15
+ value: Any
16
+ type:str
17
+
18
+ @model_validator(mode="after")
19
+ def _no_base_instances(self):
20
+ if type(self) is AttributeItemsElementBase:
21
+ raise TypeError("AttributeItemsElementBase must not be instantiated")
22
+ return self
23
+
24
+ # def __init__(self, **data):
25
+ # # Automatically inject the Literal value for `type`
26
+ # discriminator_value = self._get_discriminator_value()
27
+ # data["type"] = discriminator_value
28
+ # super().__init__(**data)
29
+
30
+ # @classmethod
31
+ # def _get_discriminator_value(cls) -> str:
32
+ # """Extract the Literal value from the 'type' annotation."""
33
+ # try:
34
+ # type_annotation = cls.__annotations__["type"]
35
+ # literal_value = get_args(type_annotation)[0]
36
+ # return literal_value
37
+ # except Exception as e:
38
+ # raise TypeError(
39
+ # f"{cls.__name__} must define `type: Literal[<value>]` annotation"
40
+ # ) from e
41
+
42
+
43
+
44
+ class DateTimeAttributeItemsElement(AttributeItemsElementBase):
45
+ type: Literal["datetime"] = "datetime"
46
+ value: datetime
47
+
48
+ @field_validator('value', mode='after')
49
+ def set_utc__if_naive(cls, value):
50
+ if isinstance(value, datetime):
51
+ return ensure_utc(value)
52
+ else:
53
+ return value
54
+
55
+
56
+
57
+ class BoolAttributeItemsElement(AttributeItemsElementBase):
58
+ type: Literal["bool"] = "bool"
59
+ value: bool
60
+
61
+
62
+
63
+
64
+ class TextAttributeItemsElement(AttributeItemsElementBase):
65
+ type: Literal["text"] = "text"
66
+ value: str
67
+
68
+ @model_validator(mode='after')
69
+ def _validate_value(self):
70
+ _validate_text(self, self.value)
71
+ return self
72
+
73
+
74
+ def _validate_text(mdl:LabFREED_BaseModel, v):
75
+ if len(v) > 5000:
76
+ mdl._add_validation_message(
77
+ source="Text Attribute",
78
+ level=ValidationMsgLevel.WARNING, # noqa: F821
79
+ msg=f"Text attribute {v} exceeds 5000 characters. It is recommended to stay below",
80
+ highlight_pattern = f'{v}'
81
+ )
82
+
83
+
84
+
85
+ class ReferenceAttributeItemsElement(AttributeItemsElementBase):
86
+ type: Literal["reference"] = "reference"
87
+ value: str
88
+
89
+
90
+
91
+ class ResourceAttributeItemsElement(AttributeItemsElementBase):
92
+ type: Literal["resource"] = "resource"
93
+ value: str
94
+
95
+ @model_validator(mode='after')
96
+ def _validate_value(self):
97
+ _validate_resource(self, self.value)
98
+ return self
99
+
100
+
101
+ def _validate_resource(mdl:LabFREED_BaseModel, v):
102
+ r = urlparse(v)
103
+ if not all([r.scheme, r.netloc]):
104
+ mdl._add_validation_message(
105
+ source="Resource Attribute",
106
+ level=ValidationMsgLevel.ERROR, # noqa: F821
107
+ msg="Must be a valid url",
108
+ highlight_pattern = f'{v}'
109
+ )
110
+ pattern = re.compile(r"\.\w{1,3}$", re.IGNORECASE)
111
+ if not bool(pattern.search(v)):
112
+ mdl._add_validation_message(
113
+ source="Resource Attribute",
114
+ level=ValidationMsgLevel.WARNING, # noqa: F821
115
+ msg="It is RECOMMENDED resource links end with a file extension",
116
+ highlight_pattern = f'{v}'
117
+ )
118
+
119
+
120
+ class NumericAttributeItemsElement(AttributeItemsElementBase):
121
+ type: Literal["numeric"] = "numeric"
122
+ value: str
123
+ _numerical_value:str
124
+ _unit:str
125
+
126
+ @model_validator(mode='after')
127
+ def _validate_model(self):
128
+ self._numerical_value, self._unit = self.value.split(' ', 1)
129
+ self._validate_value()
130
+ self._validate_unit()
131
+ return self
132
+
133
+
134
+ def _validate_value(self):
135
+ value = self._numerical_value
136
+ if not_allowed_chars := set(re.sub(r'[0-9\.\-\+Ee]', '', value)):
137
+ self._add_validation_message(
138
+ source="Numeric Attribute",
139
+ level=ValidationMsgLevel.ERROR, # noqa: F821
140
+ msg=f"Characters {_quote_texts(not_allowed_chars)} are not allowed in quantity segment. Must be a number.",
141
+ highlight_pattern = f'{value}',
142
+ highlight_sub=not_allowed_chars
143
+ )
144
+ if not re.fullmatch(r'-?\d+(\.\d+)?([Ee][\+-]?\d+)?', value):
145
+ self._add_validation_message(
146
+ source="Numeric Attribute",
147
+ level=ValidationMsgLevel.ERROR,
148
+ msg=f"{value} cannot be converted to number",
149
+ highlight_pattern = f'{value}'
150
+ )
151
+
152
+ def _validate_unit(self):
153
+ '''A sanity check on unit complying with UCUM. NOTE: It is not a complete validation
154
+ - I check for blankspaces and ^, which are often used for units, but are invalid.
155
+ - the general structure of a ucum unit is validated, but 1)parentheses are not matched 2) units are not validated 3)prefixes are not checked
156
+ '''
157
+ if ' ' in self._unit or '^' in self._unit:
158
+ self._add_validation_message(
159
+ source="Numeric Attribute",
160
+ level= ValidationMsgLevel.ERROR,
161
+ msg=f"Unit {self._unit} is invalid. Must not contain blankspace or '^'.",
162
+ highlight_pattern = self._unit
163
+ )
164
+ elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self._unit):
165
+ self._add_validation_message(
166
+ source="Numeric Attribute",
167
+ level= ValidationMsgLevel.WARNING,
168
+ msg=f"Unit {self._unit} is probably invalid. Ensure it complies with UCUM specifications.",
169
+ highlight_pattern = self._unit
170
+ )
171
+
172
+
173
+
174
+ class ObjectAttributeItemsElement(AttributeItemsElementBase):
175
+ type: Literal["object"] = "object"
176
+ value: dict[str, Any]
177
+
178
+
179
+ AttributeItemsElement = Annotated[
180
+ Union[
181
+ DateTimeAttributeItemsElement,
182
+ BoolAttributeItemsElement,
183
+ TextAttributeItemsElement,
184
+ NumericAttributeItemsElement,
185
+ ReferenceAttributeItemsElement,
186
+ ResourceAttributeItemsElement,
187
+ ObjectAttributeItemsElement
188
+ ],
189
+ Field(discriminator="type"),
190
+ ]
191
+
192
+
193
+
194
+ class Attribute(LabFREED_BaseModel):
195
+ key: str|None = Field(exclude=True)
196
+ label: str = ""
197
+ items: list[AttributeItemsElement]
198
+
199
+
200
+
201
+ class AttributeGroup(LabFREED_BaseModel):
202
+ group_key: str
203
+ group_label: str = ""
204
+ attributes: dict[str, Attribute]
205
+
206
+ @field_validator("attributes", mode="before")
207
+ @classmethod
208
+ def set_attribute_keys(cls, v):
209
+ if not isinstance(v, dict):
210
+ return v
211
+
212
+ out = {}
213
+ for k, a in v.items():
214
+ if isinstance(a, dict):
215
+ # raw input dict -> inject key if missing
216
+ out[k] = {**a, "key": a.get("key") or k}
217
+ else:
218
+ # already an Attribute (or something pydantic can parse)
219
+ out[k] = a
220
+ return out
221
+
222
+
223
+
224
+ class AttributesOfPACID(LabFREED_BaseModel):
225
+ pac_id: str
226
+ attribute_groups: list[AttributeGroup]
227
+
228
+
229
+
230
+ IMPORT_URL = "https://vocab.labfreed.org/attributes/v1.jsonld"
231
+
232
+ class AttributeResponsePayload(LabFREED_BaseModel):
233
+ schema_version: str = Field(default='1.0')
234
+ language:str
235
+ data: list[AttributesOfPACID]
236
+
237
+ context: str = Field(alias='@context', default=IMPORT_URL)
238
+
239
+
240
+ def to_json(self):
241
+ return self.model_dump_json(exclude_none=True, by_alias=True)
242
+
243
+
244
+
245
+
246
+
247
+
248
+
249
+
250
+
251
+