labfreed 0.3.1a5__tar.gz → 1.0.0a2__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 (69) hide show
  1. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/PKG-INFO +1 -1
  2. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/__init__.py +1 -1
  3. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/labfreed_extended/app/app_infrastructure.py +7 -16
  4. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/labfreed_extended/app/pac_info.py +45 -7
  5. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/api_data_models/request.py +3 -3
  6. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/api_data_models/response.py +3 -3
  7. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/client/client.py +27 -5
  8. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/pythonic/attribute_server_factory.py +41 -7
  9. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/pythonic/py_attributes.py +15 -6
  10. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/server/server.py +4 -45
  11. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/well_knonw_attribute_keys.py +1 -1
  12. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_cat/category_base.py +2 -2
  13. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/url_parser.py +1 -1
  14. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/cit_v2.py +1 -0
  15. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/resolver.py +2 -2
  16. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/pythonic/pyTREX.py +1 -1
  17. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/pythonic/quantity.py +5 -3
  18. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/.github/workflows/pypi-publish.yml +0 -0
  20. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/.github/workflows/run-tests.yml +0 -0
  21. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/CHANGELOG.md +0 -0
  22. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/LICENSE +0 -0
  23. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/README.md +0 -0
  24. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/labfreed_extended/app/formatted_print.py +0 -0
  25. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/labfreed_infrastructure.py +0 -0
  26. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/__init__.py +0 -0
  27. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  28. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/client/__init__.py +0 -0
  29. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
  30. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +0 -0
  31. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/pythonic/py_dict_data_source.py +0 -0
  32. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/server/__init__.py +0 -0
  33. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/server/attribute_data_sources.py +0 -0
  34. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  35. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_cat/__init__.py +0 -0
  36. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_cat/pac_cat.py +0 -0
  37. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_cat/predefined_categories.py +0 -0
  38. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/__init__.py +0 -0
  39. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/extension.py +0 -0
  40. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/id_segment.py +0 -0
  41. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/pac_id.py +0 -0
  42. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id/url_serializer.py +0 -0
  43. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/__init__.py +0 -0
  44. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/cit_common.py +0 -0
  45. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/cit_v1.py +0 -0
  46. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/pac_id_resolver/services.py +0 -0
  47. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/qr/__init__.py +0 -0
  48. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/qr/generate_qr.py +0 -0
  49. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/__init__.py +0 -0
  50. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/pythonic/__init__.py +0 -0
  51. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/pythonic/data_table.py +0 -0
  52. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/table_segment.py +0 -0
  53. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/trex.py +0 -0
  54. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/trex_base_models.py +0 -0
  55. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/trex/value_segments.py +0 -0
  56. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/utilities/base36.py +0 -0
  57. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/utilities/ensure_utc_time.py +0 -0
  58. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/utilities/translations.py +0 -0
  59. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_extensions/__init__.py +0 -0
  60. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
  61. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_extensions/display_name_extension.py +0 -0
  62. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_extensions/trex_extension.py +0 -0
  63. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  64. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  65. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  66. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  67. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/unece/__init__.py +0 -0
  68. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/labfreed/well_known_keys/unece/unece_units.py +0 -0
  69. {labfreed-0.3.1a5 → labfreed-1.0.0a2}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 0.3.1a5
3
+ Version: 1.0.0a2
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__ = "0.3.1a5"
5
+ __version__ = "1.0.0a2"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -5,7 +5,8 @@ import requests
5
5
 
6
6
  from labfreed.labfreed_extended.app.pac_info import PacInfo
7
7
  from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
8
- from labfreed.pac_attributes.client.client import AttributeClient, attribute_request_default_callback_factory
8
+ from labfreed.pac_attributes.client.client import AttributeClient, http_attribute_request_default_callback_factory
9
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
9
10
  from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
10
11
  from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
11
12
 
@@ -29,7 +30,7 @@ class Labfreed_App_Infrastructure():
29
30
  if not http_client:
30
31
  http_client = requests.Session()
31
32
  self._http_client= http_client
32
- callback = attribute_request_default_callback_factory(http_client)
33
+ callback = http_attribute_request_default_callback_factory(http_client)
33
34
 
34
35
  self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache())
35
36
 
@@ -63,25 +64,15 @@ class Labfreed_App_Infrastructure():
63
64
  pac_info.user_handovers = sg_user_handovers
64
65
 
65
66
  # Attributes
66
- attribute_groups = []
67
+ attribute_groups = {}
67
68
  for sg in service_groups:
68
69
  attributes_urls = [s.url for s in sg.services if s.service_type == 'attributes-generic']
69
70
  for url in attributes_urls:
70
- ags = self._attribute_client.get_attributes(url, pac_id=pac.to_url(include_extensions=False), language_preferences=self._language_preferences)
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)}
71
72
  if ags:
72
- attribute_groups.extend(ags)
73
+ attribute_groups.update(ags)
73
74
  pac_info.attributes = attribute_groups
74
-
75
- if dn := pac.get_extension('N'):
76
- dn = DisplayNameExtension.from_extension(dn)
77
- pac_info.display_name = dn.display_name or ""
78
- # there can be a display name in attributes, too
79
- if meta := [ag for ag in pac_info.attributes if ag.key == MetaAttributeKeys.GROUPKEY.value]:
80
- dn_attr = [a for a in meta[0].attributes if a.key == MetaAttributeKeys.DISPLAYNAME.value]
81
- if dn_attr:
82
- dn = dn_attr[0].value
83
- pac_info.display_name += f' ( aka {dn} )'
84
-
75
+
85
76
  return pac_info
86
77
 
87
78
 
@@ -1,19 +1,22 @@
1
1
 
2
2
 
3
+ from functools import cached_property
3
4
  from pydantic import BaseModel, Field
4
- from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributes
5
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes
6
+ from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
5
7
  from labfreed.pac_cat.pac_cat import PAC_CAT
6
8
  from labfreed.pac_id.pac_id import PAC_ID
7
9
  from labfreed.pac_id_resolver.services import ServiceGroup
8
10
  from labfreed.labfreed_extended.app.formatted_print import StringIOLineBreak
11
+ from labfreed.trex.pythonic.pyTREX import pyTREX
12
+ from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
9
13
 
10
14
 
11
15
  class PacInfo(BaseModel):
12
16
  """A convenient collection of information about a PAC-ID"""
13
17
  pac_id:PAC_ID
14
- display_name:str|None = None
15
18
  user_handovers: list[ServiceGroup] = Field(default_factory=list)
16
- attributes:pyAttributes = Field(default_factory=list)
19
+ attributes:dict[str, pyAttributeGroup] = Field(default_factory=dict)
17
20
 
18
21
  @property
19
22
  def pac_url(self):
@@ -28,12 +31,48 @@ class PacInfo(BaseModel):
28
31
 
29
32
  @property
30
33
  def attached_data(self):
31
- return self.pac_id.get_extension_of_type('TREX')
34
+ return { trex_ext.name: pyTREX.from_trex(trex=trex_ext.trex) for trex_ext in self.pac_id.get_extension_of_type('TREX')}
35
+
32
36
 
33
37
  @property
34
38
  def summary(self):
35
39
  return self.pac_id.get_extension('SUM')
36
40
 
41
+ @property
42
+ def image_url(self) -> str:
43
+ if meta := self.attributes.get(MetaAttributeKeys.GROUPKEY.value):
44
+ image_attr = meta.attributes.get(MetaAttributeKeys.IMAGE.value)
45
+ return image_attr.value
46
+
47
+
48
+ @property
49
+ def display_name(self) -> str|None:
50
+ display_name = None
51
+ pac = self.pac_id
52
+ if dn := pac.get_extension('N'):
53
+ dn = DisplayNameExtension.from_extension(dn)
54
+ display_name = dn.display_name or ""
55
+ # there can be a display name in attributes, too
56
+
57
+ if dn_attr := self._all_attributes.get(MetaAttributeKeys.DISPLAYNAME.value):
58
+ dn = dn_attr.value
59
+ display_name = dn + f' ( aka {display_name} )' if display_name else dn
60
+ return display_name
61
+
62
+
63
+ @property
64
+ def safety_pictograms(self) -> dict[str, pyAttribute]:
65
+ pictogram_attributes = {k: a for k, a in self._all_attributes.items() if "https://labfreed.org/ghs/pictogram/" in a.key}
66
+ return pictogram_attributes
67
+
68
+
69
+ @cached_property
70
+ def _all_attributes(self) -> dict[str, pyAttribute]:
71
+ out = {}
72
+ for ag in self.attributes.values():
73
+ out.update(ag.attributes)
74
+ return out
75
+
37
76
 
38
77
 
39
78
  def format_for_print(self, markup:str='rich') -> str:
@@ -62,10 +101,9 @@ class PacInfo(BaseModel):
62
101
 
63
102
 
64
103
  printout.title1("Attributes")
65
- for ag in self.attributes:
104
+ for ag in self.attributes.values():
66
105
  printout.title2(f'{ag.label} (from {ag.origin})')
67
- attributes = pyAttributes.from_payload_attributes(ag.attributes)
68
- for k, v in attributes.items():
106
+ for v in ag.attributes.values():
69
107
  v:pyAttribute
70
108
  #print(f'{k}: ({v.label}) :: {v.value} ')
71
109
  printout.key_value(v.label, v.value)
@@ -7,7 +7,7 @@ from labfreed.pac_id.pac_id import PAC_ID
7
7
  class AttributeRequestPayload(LabFREED_BaseModel):
8
8
  model_config = ConfigDict(frozen=True)
9
9
 
10
- pac_urls: list[str]
10
+ pac_ids: list[str]
11
11
  language_preferences: list[str]
12
12
  restrict_to_attribute_groups: list[str]|None = None
13
13
  suppress_forward_lookup: bool = False
@@ -29,14 +29,14 @@ class AttributeRequestPayload(LabFREED_BaseModel):
29
29
 
30
30
  @model_validator(mode="after")
31
31
  def _validate_pacs(self) -> Self:
32
- if len(self.pac_urls) > 100:
32
+ if len(self.pac_ids) > 100:
33
33
  self._add_validation_message(
34
34
  source="pacs",
35
35
  level = ValidationMsgLevel.ERROR,
36
36
  msg='The number of pac-ids must be limited to 100'
37
37
  )
38
38
 
39
- for pac_url in self.pac_urls:
39
+ for pac_url in self.pac_ids:
40
40
  try:
41
41
  PAC_ID.from_url(pac_url)
42
42
  except LabFREED_ValidationError:
@@ -70,12 +70,12 @@ class TextAttribute(AttributeBase):
70
70
 
71
71
 
72
72
  class NumericValue(LabFREED_BaseModel):
73
- magnitude: str
73
+ numerical_value: str
74
74
  unit: str
75
75
 
76
76
  @model_validator(mode='after')
77
77
  def _validate_value(self):
78
- value = self.magnitude
78
+ value = self.numerical_value
79
79
  if not_allowed_chars := set(re.sub(r'[0-9\.\-\+Ee]', '', value)):
80
80
  self._add_validation_message(
81
81
  source="Numeric Attribute",
@@ -160,7 +160,7 @@ class AttributeGroup(LabFREED_BaseModel):
160
160
 
161
161
 
162
162
  class AttributesOfPACID(LabFREED_BaseModel):
163
- pac_url: str
163
+ pac_id: str
164
164
  attribute_groups: list[AttributeGroup]
165
165
 
166
166
 
@@ -10,6 +10,7 @@ from pydantic import ValidationError
10
10
  from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
11
11
  from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload
12
12
  from labfreed.pac_attributes.client.attribute_cache import AttributeCache, CacheableAttributeGroup
13
+ from labfreed.pac_attributes.server.server import AttributeServerRequestHandler
13
14
  from labfreed.pac_id.pac_id import PAC_ID
14
15
 
15
16
 
@@ -35,7 +36,7 @@ class AttributeRequestCallback(Protocol):
35
36
  ...
36
37
 
37
38
 
38
- def attribute_request_default_callback_factory(session: requests.Session = None) -> AttributeRequestCallback:
39
+ def http_attribute_request_default_callback_factory(session: requests.Session = None) -> AttributeRequestCallback:
39
40
  """ Returns a default implementation of AttributeRequestCallback using `requests` package.
40
41
 
41
42
  Args:
@@ -49,12 +50,31 @@ def attribute_request_default_callback_factory(session: requests.Session = None)
49
50
 
50
51
  def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
51
52
  try:
52
- resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json'})
53
+ resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json'}, timeout=10)
53
54
  return resp.status_code, resp.text
54
55
  except requests.exceptions.RequestException as e:
55
56
  return 500, str(e)
56
57
  return callback
57
58
 
59
+
60
+ def local_attribute_request_callback_factory(request_handler:AttributeServerRequestHandler) -> AttributeRequestCallback:
61
+ """ Returns a default implementation of AttributeRequestCallback using `requests` package.
62
+
63
+ Args:
64
+ request_handler: The request handler
65
+
66
+ Returns:
67
+ AttributeRequestCallback: a callback following the AttributeRequestCallback protocol.
68
+ """
69
+
70
+ def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
71
+ try:
72
+ resp = request_handler.handle_attribute_request(attribute_request_body)
73
+ return 200, resp
74
+ except requests.exceptions.RequestException as e:
75
+ return 500, str(e)
76
+ return callback
77
+
58
78
 
59
79
 
60
80
 
@@ -104,7 +124,7 @@ class AttributeClient():
104
124
  return attribute_groups
105
125
 
106
126
  # no valid data found in cache > request to server
107
- attribute_request_body = AttributeRequestPayload(pac_urls=[pac_id.to_url()],
127
+ attribute_request_body = AttributeRequestPayload(pac_ids=[pac_id.to_url()],
108
128
  restrict_to_attribute_groups=restrict_to_attribute_groups,
109
129
  language_preferences=language_preferences
110
130
  )
@@ -125,7 +145,7 @@ class AttributeClient():
125
145
 
126
146
  # update cache
127
147
  for ag_for_pac in r.pac_attributes:
128
- pac = PAC_ID.from_url(ag_for_pac.pac_url)
148
+ pac = PAC_ID.from_url(ag_for_pac.pac_id)
129
149
  ags = [
130
150
  CacheableAttributeGroup(
131
151
  key= ag.key,
@@ -140,7 +160,9 @@ class AttributeClient():
140
160
 
141
161
  if pac_id == pac:
142
162
  attribute_groups_out = ags
143
- return attribute_groups_out
163
+ return attribute_groups_out
164
+ else:
165
+ return []
144
166
 
145
167
 
146
168
 
@@ -1,7 +1,8 @@
1
1
  from enum import Enum
2
2
  from typing import Any, Protocol
3
3
 
4
- from flask import Blueprint
4
+ from flask import Blueprint, current_app
5
+ from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
5
6
  from labfreed.pac_attributes.server.server import AttributeGroupDataSource, AttributeServerRequestHandler, InvalidRequestError, TranslationDataSource
6
7
 
7
8
  try:
@@ -30,7 +31,8 @@ class AttributeServerFactory():
30
31
  default_language:str,
31
32
  translation_data_sources:list[TranslationDataSource],
32
33
  authenticator: Authenticator|None,
33
- framework:Webframework=Webframework.FLASK
34
+ framework:Webframework=Webframework.FLASK,
35
+ doc_text:str=""
34
36
  ):
35
37
 
36
38
  if not authenticator:
@@ -43,7 +45,7 @@ class AttributeServerFactory():
43
45
 
44
46
  match(framework):
45
47
  case Webframework.FLASK:
46
- app = AttributeFlaskApp(request_handler,authenticator=authenticator)
48
+ app = AttributeFlaskApp(request_handler,authenticator=authenticator, doc_text=doc_text)
47
49
  return app
48
50
  case Webframework.FASTAPI:
49
51
  raise NotImplementedError('FastAPI webapp not implemented')
@@ -53,10 +55,11 @@ class AttributeServerFactory():
53
55
 
54
56
 
55
57
  class AttributeFlaskApp(Flask):
56
- def __init__(self, request_handler: AttributeServerRequestHandler, authenticator: Authenticator | None = None, **kwargs: Any):
58
+ def __init__(self, request_handler: AttributeServerRequestHandler, authenticator: Authenticator | None = None, doc_text:str="", **kwargs: Any):
57
59
  super().__init__(__name__, **kwargs)
58
60
  self.config['ATTRIBUTE_REQUEST_HANDLER'] = request_handler
59
61
  self.config['AUTHENTICATOR'] = authenticator
62
+ self.config['DOC_TEXT'] = doc_text
60
63
 
61
64
  bp = self.create_attribute_blueprint(request_handler, authenticator)
62
65
  self.register_blueprint(bp)
@@ -68,7 +71,7 @@ class AttributeFlaskApp(Flask):
68
71
  ) -> Blueprint:
69
72
  bp = Blueprint("attribute", __name__)
70
73
 
71
- @bp.route("/", methods=["POST"])
74
+ @bp.route("/", methods=["POST"], strict_slashes=False)
72
75
  def handle_attribute_request():
73
76
  if authenticator and not authenticator(request):
74
77
  return Response(
@@ -86,9 +89,40 @@ class AttributeFlaskApp(Flask):
86
89
  return "The request was valid, but the server encountered an error", 500
87
90
  return response_body
88
91
 
89
- @bp.route("/capabilities", methods=["GET"])
92
+ @bp.route("/", methods=["GET"], strict_slashes=False)
90
93
  def capabilities():
91
- return request_handler.capabilities()
94
+ doc_text = current_app.config.get('DOC_TEXT', "")
95
+ capabilities = request_handler.capabilities()
96
+ authentication_required = bool(current_app.config['AUTHENTICATOR'])
97
+ example_request = AttributeRequestPayload(pac_ids=['HTTPS://PAC.METTORIUS.COM/EXAMPLE'], language_preferences=['fr', 'de']).model_dump_json(indent=2, exclude_none=True, exclude_unset=True)
98
+ server_address = request.url.rstrip('/')
99
+ response = f'''
100
+ <body>
101
+ This is a <h1>LabFREED attribute server </h1>
102
+ <h2>Capabilities</h2>
103
+ Available Attribute Groups: {', '.join([f'<a href="{ag}"> {ag} </a>' for ag in capabilities.available_attribute_groups])} <br>
104
+
105
+ Supported Languages: {', '.join([f'<b> {l} </b>' for l in capabilities.supported_languages])} <br>
106
+ Default Language: <b>{capabilities.default_language}</b> <br>
107
+
108
+
109
+ <h2>How to use</h2>
110
+ Make a <b>POST</b> request to <a href="{server_address}">{server_address}</a> with the following body:
111
+ <pre>{example_request}</pre>
112
+ Consult {'<a href="https://github.com/ApiniLabs/PAC-Attributes"> the specification </a>' if doc_text else ""} for details. <br>
113
+
114
+
115
+ {'This server <b> requires authentication </b> ' if authentication_required else ''}
116
+ <br>
117
+
118
+ {"<h2>Further Information</h2>"if doc_text else ""}
119
+ {doc_text or ""}
120
+
121
+
122
+ </body>
123
+ '''
124
+
125
+ return response
92
126
 
93
127
  return bp
94
128
 
@@ -6,7 +6,8 @@ import warnings
6
6
  from pydantic import RootModel
7
7
 
8
8
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel
9
- from labfreed.pac_attributes.api_data_models.response import AttributeBase, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, TextAttribute
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
10
11
  from labfreed.pac_id.pac_id import PAC_ID
11
12
  from labfreed.trex.pythonic.quantity import Quantity
12
13
 
@@ -54,7 +55,7 @@ class pyAttributes(RootModel[list[pyAttribute]]):
54
55
  elif isinstance(attribute.value, Quantity|int|float):
55
56
  if not isinstance(attribute.value, Quantity):
56
57
  value = Quantity(value=attribute.value, unit='dimensionless')
57
- num_attribute = NumericAttribute(value = NumericValue(magnitude=value.value_as_str(),
58
+ num_attribute = NumericAttribute(value = NumericValue(numerical_value=value.value_as_str(),
58
59
  unit = value.unit),
59
60
  **common_args)
60
61
  num_attribute.print_validation_messages()
@@ -63,7 +64,7 @@ class pyAttributes(RootModel[list[pyAttribute]]):
63
64
  elif isinstance(value, str):
64
65
  # capture quantities in the form of "100.0e5 g/L"
65
66
  if q := Quantity.from_str_with_unit(value):
66
- return NumericAttribute(value = NumericValue(magnitude=q.value_as_str(),
67
+ return NumericAttribute(value = NumericValue(numerical_value=q.value_as_str(),
67
68
  unit = q.unit),
68
69
  **common_args)
69
70
  else:
@@ -87,7 +88,7 @@ class pyAttributes(RootModel[list[pyAttribute]]):
87
88
 
88
89
  @staticmethod
89
90
  def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
90
- out = dict()
91
+ out = list()
91
92
  for a in attributes:
92
93
  match a:
93
94
 
@@ -95,7 +96,7 @@ class pyAttributes(RootModel[list[pyAttribute]]):
95
96
  value = pyReference(a.value)
96
97
 
97
98
  case NumericAttribute():
98
- value = Quantity.from_str_value(value=a.value.magnitude, unit=a.value.unit)
99
+ value = Quantity.from_str_value(value=a.value.numerical_value, unit=a.value.unit)
99
100
 
100
101
  case BoolAttribute():
101
102
  value = a.value
@@ -117,8 +118,16 @@ class pyAttributes(RootModel[list[pyAttribute]]):
117
118
  # valid_until=datetime(**_parse_date_time_str(a.valid_until)),
118
119
  # observed_at=datetime(**_parse_date_time_str(a.value))
119
120
  )
120
- out.update( { a.key: attr } )
121
+ out.append(attr )
121
122
  return out
122
123
 
123
124
 
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)
@@ -60,7 +60,7 @@ class AttributeServerRequestHandler():
60
60
  raise InvalidRequestError
61
61
  attributes_for_pac_id = []
62
62
  referenced_pac_ids = set()
63
- for pac_url in r.pac_urls:
63
+ for pac_url in r.pac_ids:
64
64
  attributes_for_pac = self._get_attributes_for_pac_id(pac_url=pac_url,
65
65
  restrict_to_attribute_groups = r.restrict_to_attribute_groups)
66
66
  attributes_for_pac_id.append(attributes_for_pac)
@@ -103,7 +103,7 @@ class AttributeServerRequestHandler():
103
103
  traceback.print_exc()
104
104
  raise e
105
105
 
106
- return AttributesOfPACID(pac_url=pac_url, # return the pac_url as given, i.e. with the extension if there was one
106
+ return AttributesOfPACID(pac_id=pac_url, # return the pac_url as given, i.e. with the extension if there was one
107
107
  attribute_groups=attribute_groups)
108
108
 
109
109
 
@@ -120,44 +120,6 @@ class AttributeServerRequestHandler():
120
120
  pass
121
121
  return referenced_pacs
122
122
 
123
- # def get_translations(self, attributes_for_pac_id):
124
- # ontology_map = {} # ontology name → list of TermTranslations
125
-
126
- # for ag_for_pac in attributes_for_pac_id:
127
- # for ag in ag_for_pac.attribute_groups:
128
- # ag: AttributeGroup
129
- # ontology_name = ag.ontology
130
- # translation_data_source: TranslationDataSource = self._translation_data_sources.get(ontology_name, {})
131
-
132
- # attribute_keys = [a.key for a in ag.attributes]
133
- # all_keys = set([ag.key] + attribute_keys)
134
-
135
- # for k in all_keys:
136
- # t = translation_data_source.get_translations_for(k)
137
- # if t:
138
- # ontology_map.setdefault(ontology_name, []).append(t)
139
-
140
- # translations_by_ontology = [ TranslationsForOntology(ontology=name, terms=terms) for name, terms in ontology_map.items()
141
- # ]
142
- # return translations_by_ontology
143
-
144
-
145
- # def _get_display_name_for_key(self, key, requested_languages:str):
146
- # for tds in self._translation_data_sources:
147
- # if term := tds.get_translations_for(key):
148
- # # try the languages requested by the user
149
- # for l in requested_languages:
150
- # if dn := term.in_language(l):
151
- # return dn
152
- # # remove the country codes and try the again
153
- # for l_fallback in [l.split('-')[0] for l in requested_languages]:
154
- # if dn := term.in_language(l_fallback):
155
- # return dn
156
- # # use the server fallback language
157
- # if dn := term.in_language(self._default_language):
158
- # return
159
- # warnings.warn(f'No translation for {key}')
160
- # return None
161
123
 
162
124
  def _add_display_names(self, attributes_of_pac:AttributesOfPACID, language:str) -> str:
163
125
  '''
@@ -181,9 +143,6 @@ class AttributeServerRequestHandler():
181
143
 
182
144
 
183
145
 
184
-
185
-
186
-
187
146
  def _get_display_name_for_key(self, key, language:str):
188
147
  '''call this only with a language you know there is a translation for'''
189
148
  for tds in self._translation_data_sources:
@@ -211,10 +170,10 @@ class AttributeServerRequestHandler():
211
170
 
212
171
 
213
172
 
214
- def capabilities(self):
173
+ def capabilities(self) -> ServerCapabilities:
215
174
  return ServerCapabilities(supported_languages=self._supported_languages,
216
175
  default_language=self._default_language,
217
- available_attribute_groups= [ds.attribute_group_key for ds in self._attribute_group_data_sources]).model_dump_json()
176
+ available_attribute_groups= [ds.attribute_group_key for ds in self._attribute_group_data_sources])
218
177
 
219
178
 
220
179
 
@@ -6,6 +6,6 @@ class MetaAttributeKeys(Enum):
6
6
  IMAGE = "https://schema.org/image"
7
7
  ALIAS = "https://schema.org/alternateName"
8
8
  DESCRIPTION = "https://schema.org/description"
9
- GROUPKEY = "https://labfreed.org/attribute_metadata_group"
9
+ GROUPKEY = "https://labfreed.org/terms/attribute_group_metadata"
10
10
 
11
11
 
@@ -44,14 +44,14 @@ class Category(LabFREED_BaseModel):
44
44
  s = '\n'.join( [f"{field_name} \t ({field_info.alias or ''}): \t {getattr(self, field_name)}" for field_name, field_info in self.model_fields.items() if getattr(self, field_name)])
45
45
  return s
46
46
 
47
- def segments_as_dict(self):
47
+ def segments_as_dict(self, include_alias=False):
48
48
  ''' returns the segments in a dict, with nice keys and values'''
49
49
  out = dict()
50
50
  for field_name, field_info in self.model_fields.items():
51
51
  if field_name =='additional_segments':
52
52
  continue
53
53
  if v := getattr(self, field_name):
54
- if field_info.alias:
54
+ if field_info.alias and include_alias:
55
55
  k = f"{field_name} ({ field_info.alias})"
56
56
  else:
57
57
  k = f"{field_name}"
@@ -82,7 +82,7 @@ class PAC_Parser():
82
82
  @classmethod
83
83
  def _parse_pac_id(cls,id_str:str) -> "PAC_ID":
84
84
  # m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?\..+?)/(?P<identifier>.*)', id_str)
85
- m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?)/(?P<identifier>.*)', id_str)
85
+ m = re.match('(HTTPS://)?(PAC.)?(?P<issuer>.+?)/(?P<identifier>.*)', id_str, re.IGNORECASE)
86
86
  if not m:
87
87
  raise LabFREED_ValidationError(f'{id_str} does not match the pattern expected for PAC-ID')
88
88
  d = m.groupdict()
@@ -105,6 +105,7 @@ class CITBlock_v2(LabFREED_BaseModel):
105
105
 
106
106
 
107
107
  class CIT_v2(LabFREED_BaseModel):
108
+ schema_version: str = Field(default='2.0')
108
109
  '''Coupling Information Table (CIT)'''
109
110
  origin: str = ''
110
111
  model_config = {
@@ -67,10 +67,10 @@ class PAC_ID_Resolver():
67
67
  def resolve(self, pac_id:PAC_ID|str, check_service_status=True, use_issuer_cit=True) -> list[ServiceGroup]:
68
68
  '''Resolve a PAC-ID'''
69
69
  if isinstance(pac_id, str):
70
- pac_id = PAC_CAT.from_url(pac_id)
71
70
  pac_id_catless = PAC_ID.from_url(pac_id, try_pac_cat=False)
71
+ pac_id = PAC_CAT.from_url(pac_id)
72
72
 
73
- # it's likely to h
73
+ # it's likely to
74
74
  if isinstance(pac_id, PAC_ID):
75
75
  pac_id_catless = PAC_ID.from_url(pac_id.to_url(), try_pac_cat=False)
76
76
  else:
@@ -215,7 +215,7 @@ def _trex_value_to_python_type(v):
215
215
 
216
216
  elif isinstance(v,DateValue):
217
217
  d = v._date_time_dict
218
- if d.get('year') and d.get('hour'): # input is only a time
218
+ if d.get('year') and d.get('hour') is not None: # input is only a time
219
219
  return datetime(**d)
220
220
  elif d.get('year'):
221
221
  return date(**d)
@@ -24,11 +24,12 @@ class Quantity(BaseModel):
24
24
  #dimensionless_unit
25
25
  unit:str= d.get('unit')
26
26
  if unit and unit in ['1', '', 'dimensionless']:
27
- d['unit'] = None
27
+ unit = None
28
+ d['unit'] = unit
28
29
 
29
30
  #try to coerce to ucum. catch the two most likely mistakes to use blanks for multiplication and ^ for exponents.
30
31
  if unit:
31
- unit = unit.replace('/ ', '/').replace(' /', '/').replace(' ', '.').replace('^', '')
32
+ unit = unit.replace('/ ', '/').replace(' /', '/').replace(' ', '.').replace('^', '').replace('·','.')
32
33
  d['unit'] = unit
33
34
 
34
35
  return d
@@ -122,8 +123,9 @@ class Quantity(BaseModel):
122
123
 
123
124
  def __str__(self):
124
125
  unit_symbol = self.unit
125
- if self.unit == "dimensionless" or not self.unit:
126
+ if self.unit in [ "1", "dimensionless"] or not self.unit:
126
127
  unit_symbol = ""
128
+ unit_symbol = unit_symbol.replace('.', '·')
127
129
  val = self.value_as_str()
128
130
  return f"{val} {unit_symbol}"
129
131
 
File without changes
File without changes
File without changes
File without changes