labfreed 0.3.1a4__tar.gz → 0.3.1a6__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 (71) hide show
  1. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/PKG-INFO +1 -1
  2. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/__init__.py +1 -1
  3. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_extended/app/app_infrastructure.py +8 -15
  4. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_extended/app/pac_info.py +30 -7
  5. labfreed-0.3.1a6/labfreed/pac_attributes/__init__.py +0 -0
  6. labfreed-0.3.1a6/labfreed/pac_attributes/client/__init__.py +0 -0
  7. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/client/client.py +4 -2
  8. {labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes/server → labfreed-0.3.1a6/labfreed/pac_attributes/pythonic}/attribute_server_factory.py +2 -2
  9. labfreed-0.3.1a6/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +181 -0
  10. {labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes → labfreed-0.3.1a6/labfreed/pac_attributes/pythonic}/py_attributes.py +14 -4
  11. labfreed-0.3.1a6/labfreed/pac_attributes/pythonic/py_dict_data_source.py +13 -0
  12. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/__init__.py +3 -3
  13. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/attribute_data_sources.py +8 -9
  14. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/server.py +1 -1
  15. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/resolver.py +2 -2
  16. {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/data_table.py +1 -1
  17. {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/pyTREX.py +2 -2
  18. labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py +0 -129
  19. labfreed-0.3.1a4/labfreed/pac_attributes/client/__init__.py +0 -1
  20. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  21. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/workflows/pypi-publish.yml +0 -0
  22. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/workflows/run-tests.yml +0 -0
  23. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/CHANGELOG.md +0 -0
  24. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/LICENSE +0 -0
  25. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/README.md +0 -0
  26. {labfreed-0.3.1a4/labfreed/labfreed_extended/utilities → labfreed-0.3.1a6/labfreed/labfreed_extended/app}/formatted_print.py +0 -0
  27. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_infrastructure.py +0 -0
  28. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/request.py +0 -0
  29. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/response.py +0 -0
  30. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
  31. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
  32. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
  33. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
  34. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/__init__.py +0 -0
  35. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/category_base.py +0 -0
  36. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/pac_cat.py +0 -0
  37. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/predefined_categories.py +0 -0
  38. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/__init__.py +0 -0
  39. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/extension.py +0 -0
  40. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/id_segment.py +0 -0
  41. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/pac_id.py +0 -0
  42. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/url_parser.py +0 -0
  43. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/url_serializer.py +0 -0
  44. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/__init__.py +0 -0
  45. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_common.py +0 -0
  46. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_v1.py +0 -0
  47. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_v2.py +0 -0
  48. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/services.py +0 -0
  49. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/qr/__init__.py +0 -0
  50. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/qr/generate_qr.py +0 -0
  51. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/__init__.py +0 -0
  52. {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/__init__.py +0 -0
  53. {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/quantity.py +0 -0
  54. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/table_segment.py +0 -0
  55. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/trex.py +0 -0
  56. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/trex_base_models.py +0 -0
  57. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/value_segments.py +0 -0
  58. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/base36.py +0 -0
  59. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/ensure_utc_time.py +0 -0
  60. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/translations.py +0 -0
  61. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/__init__.py +0 -0
  62. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
  63. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/display_name_extension.py +0 -0
  64. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/trex_extension.py +0 -0
  65. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/gs1/__init__.py +0 -0
  66. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
  67. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
  68. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
  69. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/__init__.py +0 -0
  70. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/unece_units.py +0 -0
  71. {labfreed-0.3.1a4 → labfreed-0.3.1a6}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 0.3.1a4
3
+ Version: 0.3.1a6
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.1a4"
5
+ __version__ = "0.3.1a6"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -3,11 +3,14 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
3
3
 
4
4
  import requests
5
5
 
6
+ from labfreed.labfreed_extended.app.pac_info import PacInfo
6
7
  from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
7
8
  from labfreed.pac_attributes.client.client import AttributeClient, attribute_request_default_callback_factory
9
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttributeGroup
8
10
  from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
9
11
  from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
10
- from labfreed_extended.app.pac_info import PacInfo
12
+
13
+
11
14
  from labfreed.pac_id.pac_id import PAC_ID
12
15
  from labfreed.pac_id_resolver.resolver import PAC_ID_Resolver, cit_from_str
13
16
  from labfreed.pac_id_resolver.services import ServiceGroup
@@ -61,25 +64,15 @@ class Labfreed_App_Infrastructure():
61
64
  pac_info.user_handovers = sg_user_handovers
62
65
 
63
66
  # Attributes
64
- attribute_groups = []
67
+ attribute_groups = {}
65
68
  for sg in service_groups:
66
69
  attributes_urls = [s.url for s in sg.services if s.service_type == 'attributes-generic']
67
70
  for url in attributes_urls:
68
- 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)}
69
72
  if ags:
70
- attribute_groups.extend(ags)
73
+ attribute_groups.update(ags)
71
74
  pac_info.attributes = attribute_groups
72
-
73
- if dn := pac.get_extension('N'):
74
- dn = DisplayNameExtension.from_extension(dn)
75
- pac_info.display_name = dn.display_name or ""
76
- # there can be a display name in attributes, too
77
- if meta := [ag for ag in pac_info.attributes if ag.key == MetaAttributeKeys.GROUPKEY.value]:
78
- dn_attr = [a for a in meta[0].attributes if a.key == MetaAttributeKeys.DISPLAYNAME.value]
79
- if dn_attr:
80
- dn = dn_attr[0].value
81
- pac_info.display_name += f' ( aka {dn} )'
82
-
75
+
83
76
  return pac_info
84
77
 
85
78
 
@@ -1,19 +1,20 @@
1
1
 
2
2
 
3
3
  from pydantic import BaseModel, Field
4
- from labfreed_extended.pac_attributes.py_attributes import pyAttribute, pyAttributes
4
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributeGroup, pyAttributes
5
+ from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
5
6
  from labfreed.pac_cat.pac_cat import PAC_CAT
6
7
  from labfreed.pac_id.pac_id import PAC_ID
7
8
  from labfreed.pac_id_resolver.services import ServiceGroup
8
- from labfreed_extended.utilities.formatted_print import StringIOLineBreak
9
+ from labfreed.labfreed_extended.app.formatted_print import StringIOLineBreak
10
+ from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
9
11
 
10
12
 
11
13
  class PacInfo(BaseModel):
12
14
  """A convenient collection of information about a PAC-ID"""
13
15
  pac_id:PAC_ID
14
- display_name:str|None = None
15
16
  user_handovers: list[ServiceGroup] = Field(default_factory=list)
16
- attributes:pyAttributes = Field(default_factory=list)
17
+ attributes:dict[str, pyAttributeGroup] = Field(default_factory=dict)
17
18
 
18
19
  @property
19
20
  def pac_url(self):
@@ -34,6 +35,29 @@ class PacInfo(BaseModel):
34
35
  def summary(self):
35
36
  return self.pac_id.get_extension('SUM')
36
37
 
38
+ @property
39
+ def image_url(self) -> str:
40
+ if meta := self.attributes.get(MetaAttributeKeys.GROUPKEY.value):
41
+ image_attr = meta.attributes.get(MetaAttributeKeys.IMAGE.value)
42
+ return image_attr.value
43
+
44
+
45
+ @property
46
+ def display_name(self) -> str|None:
47
+ display_name = None
48
+ pac = self.pac_id
49
+ if dn := pac.get_extension('N'):
50
+ dn = DisplayNameExtension.from_extension(dn)
51
+ display_name = dn.display_name or ""
52
+ # there can be a display name in attributes, too
53
+ if meta := self.attributes.get(MetaAttributeKeys.GROUPKEY.value):
54
+ dn_attr = meta.attributes.get(MetaAttributeKeys.DISPLAYNAME.value)
55
+ if dn_attr:
56
+ dn = dn_attr.value
57
+ display_name += f' ( aka {dn} )'
58
+ return display_name
59
+
60
+
37
61
 
38
62
 
39
63
  def format_for_print(self, markup:str='rich') -> str:
@@ -62,10 +86,9 @@ class PacInfo(BaseModel):
62
86
 
63
87
 
64
88
  printout.title1("Attributes")
65
- for ag in self.attributes:
89
+ for ag in self.attributes.values():
66
90
  printout.title2(f'{ag.label} (from {ag.origin})')
67
- attributes = pyAttributes.from_payload_attributes(ag.attributes)
68
- for k, v in attributes.items():
91
+ for v in ag.attributes.values():
69
92
  v:pyAttribute
70
93
  #print(f'{k}: ({v.label}) :: {v.value} ')
71
94
  printout.key_value(v.label, v.value)
File without changes
@@ -49,7 +49,7 @@ def attribute_request_default_callback_factory(session: requests.Session = None)
49
49
 
50
50
  def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
51
51
  try:
52
- resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json'})
52
+ resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json'}, timeout=10)
53
53
  return resp.status_code, resp.text
54
54
  except requests.exceptions.RequestException as e:
55
55
  return 500, str(e)
@@ -140,7 +140,9 @@ class AttributeClient():
140
140
 
141
141
  if pac_id == pac:
142
142
  attribute_groups_out = ags
143
- return attribute_groups_out
143
+ return attribute_groups_out
144
+ else:
145
+ return []
144
146
 
145
147
 
146
148
 
@@ -68,7 +68,7 @@ class AttributeFlaskApp(Flask):
68
68
  ) -> Blueprint:
69
69
  bp = Blueprint("attribute", __name__)
70
70
 
71
- @bp.route("/", methods=["POST"])
71
+ @bp.route("/", methods=["POST"], strict_slashes=False)
72
72
  def handle_attribute_request():
73
73
  if authenticator and not authenticator(request):
74
74
  return Response(
@@ -86,7 +86,7 @@ class AttributeFlaskApp(Flask):
86
86
  return "The request was valid, but the server encountered an error", 500
87
87
  return response_body
88
88
 
89
- @bp.route("/capabilities", methods=["GET"])
89
+ @bp.route("/capabilities", methods=["GET"], strict_slashes=False)
90
90
  def capabilities():
91
91
  return request_handler.capabilities()
92
92
 
@@ -0,0 +1,181 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from typing import Optional, Tuple, List, Dict
6
+ from urllib.parse import urlparse, urlsplit, urlunsplit, parse_qsl, urlencode
7
+
8
+ from cachetools import TTLCache, cached
9
+
10
+ from labfreed.pac_attributes.api_data_models.response import AttributeGroup
11
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttribute, pyAttributes
12
+ from labfreed.pac_attributes.server.server import AttributeGroupDataSource
13
+
14
+ try:
15
+ from openpyxl import load_workbook
16
+ except ImportError:
17
+ raise ImportError("Please install labfreed with the [extended] extra: pip install labfreed[extended]")
18
+
19
+ # ---------------------------------------------------------------------
20
+ # Cache (shared by all instances). TTL can be overridden per instance.
21
+ # ---------------------------------------------------------------------
22
+ _cache = TTLCache(maxsize=128, ttl=0)
23
+
24
+
25
+ # ---------------------------------------------------------------------
26
+ # Helpers
27
+ # ---------------------------------------------------------------------
28
+ def _is_sharepoint_url(s: str) -> bool:
29
+ try:
30
+ parsed = urlparse(s)
31
+ if parsed.scheme not in {"http", "https"}:
32
+ return False
33
+ host = (parsed.netloc or "").lower()
34
+ return ("sharepoint.com" in host) or ("1drv.ms" in host)
35
+ except Exception:
36
+ return False
37
+
38
+
39
+ def _is_local_path(s: str) -> bool:
40
+ return os.path.exists(s) if not _is_sharepoint_url(s) else False
41
+
42
+
43
+ def _ensure_download_query(u: str) -> str:
44
+ parts = urlsplit(u)
45
+ q = dict(parse_qsl(parts.query, keep_blank_values=True))
46
+ # Preserve all params (guest tokens, etc.), just force file response.
47
+ q["download"] = "1"
48
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, urlencode(q, doseq=True), parts.fragment))
49
+
50
+
51
+ def _get_row_by_first_cell(sheet_rows: List[tuple], match_value: str, base_url: str) -> Optional[Dict[str, object]]:
52
+ if not sheet_rows:
53
+ return None
54
+ headers = sheet_rows[0]
55
+ for row in sheet_rows[1:]:
56
+ if not row:
57
+ continue
58
+ first = str(row[0]).strip() if row[0] is not None else ""
59
+ if first == match_value:
60
+ return {
61
+ base_url + str(headers[i]).strip(): row[i]
62
+ for i in range(1, len(headers))
63
+ if headers[i] is not None
64
+ }
65
+ return None
66
+
67
+
68
+ # ---------------------------------------------------------------------
69
+ # Base class
70
+ # ---------------------------------------------------------------------
71
+ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
72
+ """
73
+ Common mapping logic from Excel rows to AttributeGroup.
74
+ Subclasses implement `_read_rows_and_last_changed()`.
75
+ """
76
+
77
+ def __init__(self, *, base_url: str = "", cache_duration_seconds: int = 0, **kwargs):
78
+ self._base_url = base_url
79
+ # allow instance-level TTL override
80
+ try:
81
+ _cache.ttl = int(cache_duration_seconds)
82
+ except Exception:
83
+ pass
84
+ super().__init__(**kwargs)
85
+
86
+ def is_static(self) -> bool:
87
+ return False
88
+
89
+ def _read_rows_and_last_changed(self) -> Tuple[List[tuple], Optional[datetime]]:
90
+ raise NotImplementedError
91
+
92
+ @property
93
+ def provides_attributes(self) -> List[str]:
94
+ rows, _ = self._read_rows_and_last_changed()
95
+ if not rows:
96
+ return []
97
+ return [self._base_url + r for r in rows[0][1:]]
98
+
99
+ def attributes(self, pac_url: str) -> Optional[AttributeGroup]:
100
+ if not self._include_extensions:
101
+ pac_url = pac_url.split('*')[0]
102
+ rows, last_changed = self._read_rows_and_last_changed()
103
+ d = _get_row_by_first_cell(rows, pac_url, self._base_url)
104
+ if not d:
105
+ return None
106
+ attributes = [pyAttribute(key=k, value=v) for k, v in d.items()]
107
+ return AttributeGroup(
108
+ key=self._attribute_group_key,
109
+ attributes=pyAttributes(attributes).to_payload_attributes(),
110
+ state_of=last_changed,
111
+ )
112
+
113
+
114
+ # ---------------------------------------------------------------------
115
+ # Local file implementation
116
+ # ---------------------------------------------------------------------
117
+ class LocalExcelAttributeDataSource(_BaseExcelAttributeDataSource):
118
+ def __init__(self, file_path: str, **kwargs):
119
+ self._file_path = file_path
120
+ super().__init__(**kwargs)
121
+
122
+ @cached(_cache)
123
+ def _read_rows_and_last_changed(self) -> Tuple[List[tuple], Optional[datetime]]:
124
+ wb = load_workbook(filename=self._file_path, read_only=True, data_only=True)
125
+ ws = wb.active
126
+ rows = list(ws.iter_rows(values_only=True))
127
+ last_changed = wb.properties.modified
128
+ wb.close()
129
+ return rows, last_changed
130
+
131
+
132
+ # # ---------------------------------------------------------------------
133
+ # # SharePoint/OneDrive (anonymous only)
134
+ # # ---------------------------------------------------------------------
135
+ # class SharePointExcelAttributeDataSource(_BaseExcelAttributeDataSource):
136
+ # """
137
+ # Anonymous 'anyone with the link' reader for SharePoint/OneDrive Excel.
138
+ # No MSAL, no AAD app permissions.
139
+ # """
140
+
141
+ # def __init__(self, url: str, *, timeout: int = 30, **kwargs):
142
+ # self._url = url
143
+ # self._timeout = timeout
144
+ # super().__init__(**kwargs)
145
+
146
+ # @cached(_cache)
147
+ # def _read_rows_and_last_changed(self) -> Tuple[List[tuple], Optional[datetime]]:
148
+ # content, last_changed = self._download_bytes_anon(self._url, timeout=self._timeout)
149
+ # with io.BytesIO(content) as fh:
150
+ # wb = load_workbook(filename=fh, read_only=True, data_only=True)
151
+ # ws = wb.active
152
+ # rows = list(ws.iter_rows(values_only=True))
153
+ # wb_last = wb.properties.modified # often None for streamed files
154
+ # wb.close()
155
+ # return rows, (wb_last or last_changed)
156
+
157
+ # def _download_bytes_anon(self, url: str, *, timeout: int) -> Tuple[bytes, Optional[datetime]]:
158
+ # u = _ensure_download_query(url)
159
+ # headers = {"User-Agent": "python-requests/anon-sharepoint-downloader"}
160
+ # resp = requests.get(u, headers=headers, timeout=timeout, allow_redirects=True)
161
+ # resp.raise_for_status()
162
+
163
+ # last = None
164
+ # lm_hdr = resp.headers.get("Last-Modified")
165
+ # if lm_hdr:
166
+ # try:
167
+ # last = parsedate_to_datetime(lm_hdr)
168
+ # if last.tzinfo is None:
169
+ # last = last.replace(tzinfo=timezone.utc)
170
+ # else:
171
+ # last = last.astimezone(timezone.utc)
172
+ # except Exception:
173
+ # last = None
174
+ # return resp.content, last
175
+
176
+
177
+
178
+
179
+ __all__ = [
180
+ "LocalExcelAttributeDataSource",
181
+ ]
@@ -6,9 +6,10 @@ 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
- from labfreed.trex.python_convenience.quantity import Quantity
12
+ from labfreed.trex.pythonic.quantity import Quantity
12
13
 
13
14
 
14
15
  class pyReference(RootModel[str]):
@@ -30,6 +31,7 @@ class pyAttribute(LabFREED_BaseModel):
30
31
  class pyAttributes(RootModel[list[pyAttribute]]):
31
32
  def to_payload_attributes(self) -> list[AttributeBase]:
32
33
  return [self._attribute_to_attribute_payload_type(e) for e in self.root]
34
+
33
35
 
34
36
  @staticmethod
35
37
  def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
@@ -86,7 +88,7 @@ class pyAttributes(RootModel[list[pyAttribute]]):
86
88
 
87
89
  @staticmethod
88
90
  def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
89
- out = dict()
91
+ out = list()
90
92
  for a in attributes:
91
93
  match a:
92
94
 
@@ -116,8 +118,16 @@ class pyAttributes(RootModel[list[pyAttribute]]):
116
118
  # valid_until=datetime(**_parse_date_time_str(a.valid_until)),
117
119
  # observed_at=datetime(**_parse_date_time_str(a.value))
118
120
  )
119
- out.update( { a.key: attr } )
121
+ out.append(attr )
120
122
  return out
121
123
 
122
124
 
123
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)
@@ -0,0 +1,13 @@
1
+ from labfreed.pac_attributes.pythonic.py_attributes import pyAttributes
2
+ from labfreed.pac_attributes.server.attribute_data_sources import Dict_DataSource
3
+
4
+
5
+ class pyDict_DataSource(Dict_DataSource):
6
+ def __init__(self, data:dict[str, pyAttributes], *args, **kwargs):
7
+ if not all([isinstance(e, pyAttributes) for e in data.values()]):
8
+ raise ValueError('Invalid data')
9
+ d = {k: v.to_payload_attributes() for k,v in data.items()}
10
+ super().__init__(data=d, *args, **kwargs)
11
+
12
+
13
+
@@ -1,3 +1,3 @@
1
- from labfreed.pac_attributes.server.server import AttributeServerRequestHandler
2
- from labfreed.pac_attributes.server.attribute_data_sources import Dict_DataSource
3
- from labfreed.pac_attributes.server.translation_data_sources import DictTranslationDataSource
1
+ from labfreed.pac_attributes.server.server import AttributeServerRequestHandler # noqa: F401
2
+ from labfreed.pac_attributes.server.attribute_data_sources import Dict_DataSource # noqa: F401
3
+ from labfreed.pac_attributes.server.translation_data_sources import DictTranslationDataSource # noqa: F401
@@ -1,7 +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, AttributeGroup
4
- from labfreed_extended.pac_attributes.py_attributes import pyAttributes
3
+ from labfreed.pac_attributes.api_data_models.response import VALID_FOREVER, AttributeBase, AttributeGroup
5
4
 
6
5
 
7
6
  class AttributeGroupDataSource(ABC):
@@ -30,10 +29,11 @@ class AttributeGroupDataSource(ABC):
30
29
 
31
30
 
32
31
  class Dict_DataSource(AttributeGroupDataSource):
33
- def __init__(self, data:dict[str, pyAttributes], *args, **kwargs):
34
- if not all([isinstance(e, pyAttributes) for e in data.values()]):
32
+ def __init__(self, data:dict[str, list[AttributeBase]], *args, **kwargs):
33
+ if not all([isinstance(e, list) for e in data.values()]):
35
34
  raise ValueError('Invalid data')
36
- self._data:pyAttributes = data
35
+
36
+ self._data = data
37
37
  self._state_of = datetime.now(tz=timezone.utc)
38
38
 
39
39
  super().__init__(*args, **kwargs)
@@ -41,17 +41,16 @@ class Dict_DataSource(AttributeGroupDataSource):
41
41
 
42
42
  @property
43
43
  def provides_attributes(self):
44
- return [a.key for attributes in self._data.values() for a in attributes.root]
44
+ return [a.key for attributes in self._data.values() for a in attributes]
45
45
 
46
46
 
47
47
  def attributes(self, pac_url: str) -> AttributeGroup:
48
48
  if not self._include_extensions:
49
49
  pac_url = pac_url.split('*')[0]
50
50
 
51
- attributes:pyAttributes = self._data.get(pac_url)
51
+ attributes = self._data.get(pac_url)
52
52
  if not attributes:
53
- return None
54
- attributes = attributes.to_payload_attributes()
53
+ return None
55
54
 
56
55
 
57
56
  valid_until = VALID_FOREVER if self._is_static else None
@@ -4,7 +4,7 @@ import warnings
4
4
  import rich
5
5
 
6
6
  from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
7
- from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload, AttributesOfPACID, ReferenceAttribute
7
+ from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload, AttributesOfPACID, ReferenceAttribute
8
8
  from labfreed.pac_attributes.api_data_models.server_capabilities_response import ServerCapabilities
9
9
  from labfreed.pac_attributes.server.attribute_data_sources import AttributeGroupDataSource
10
10
  from labfreed.pac_attributes.server.translation_data_sources import TranslationDataSource
@@ -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:
@@ -5,7 +5,7 @@ from typing import Union
5
5
  from pydantic import BaseModel, Field, PrivateAttr, model_validator
6
6
 
7
7
  from labfreed.utilities.base36 import base36
8
- from labfreed.trex.python_convenience.quantity import Quantity
8
+ from labfreed.trex.pythonic.quantity import Quantity
9
9
 
10
10
 
11
11
  class DataTable(BaseModel):
@@ -6,10 +6,10 @@ from typing import Self
6
6
 
7
7
  from pydantic import RootModel
8
8
  from labfreed.well_known_keys.unece.unece_units import unece_unit
9
- from labfreed.trex.python_convenience.data_table import DataTable
9
+ from labfreed.trex.pythonic.data_table import DataTable
10
10
  from labfreed.utilities.base36 import from_base36, base36, to_base36
11
11
 
12
- from labfreed.trex.python_convenience.quantity import Quantity, unece_unit_code_from_quantity
12
+ from labfreed.trex.pythonic.quantity import Quantity, unece_unit_code_from_quantity
13
13
  from labfreed.trex.table_segment import ColumnHeader, TableSegment
14
14
  from labfreed.trex.trex import TREX
15
15
  from labfreed.trex.trex_base_models import AlphanumericValue, BinaryValue, BoolValue, DateValue, ErrorValue, NumericValue, TextValue
@@ -1,129 +0,0 @@
1
- from datetime import datetime
2
-
3
- import os
4
- from urllib.parse import urlparse
5
- from cachetools import TTLCache, cached
6
-
7
- from labfreed.pac_attributes.api_data_models.response import AttributeGroup
8
- from labfreed.pac_attributes.server.server import AttributeGroupDataSource
9
- from labfreed.labfreed_extended.pac_attributes.py_attributes import pyAttribute, pyAttributes
10
-
11
-
12
- try:
13
- from openpyxl import load_workbook
14
- except ImportError:
15
- raise ImportError("Please install labfreed with the [extended] extra: pip install labfreed[extended]")
16
-
17
-
18
- cache = TTLCache(maxsize=128, ttl=0)
19
-
20
-
21
- class ExcelAttributeDataSource(AttributeGroupDataSource):
22
- '''
23
- Demonstrates how to analyze the PAC-ID and it's extensions to provide some data
24
- '''
25
- def __init__(self, file_path:str, cache_duration_seconds:int=0, base_url:str="", *args, **kwargs):
26
- self._file_path = file_path
27
- self._base_url = base_url
28
-
29
-
30
- if is_sharepoint_url(file_path):
31
- self._file_location = "sharepoint"
32
- else:
33
- self._file_location = "local"
34
-
35
- super().__init__(*args, **kwargs)
36
-
37
-
38
- def is_static(self) -> bool:
39
- return False
40
-
41
- @property
42
- def provides_attributes(self):
43
- if self._file_location == "local":
44
- rows, last_changed = read_excel_openpyxl(self._file_path)
45
- headers = [self._base_url + r for r in rows[0][1:] ]
46
- elif self._file_location == "sharepoint":
47
- raise NotImplementedError('Sharepoint Access not implemented')
48
- return headers
49
-
50
-
51
- def attributes(self, pac_url: str) -> AttributeGroup:
52
- if not self._include_extensions:
53
- pac_url = pac_url.split('*')[0]
54
-
55
- if self._file_location == "local":
56
- rows, last_changed = read_excel_openpyxl(self._file_path)
57
- elif self._file_location == "sharepoint":
58
- raise NotImplementedError('Sharepoint Access not implemented')
59
-
60
- d = get_row_by_first_cell(rows, pac_url, self._base_url)
61
- if not d:
62
- return None
63
-
64
- attributes = [pyAttribute(key=k, value=v) for k,v in d.items()]
65
-
66
-
67
- return AttributeGroup(key=self._attribute_group_key,
68
- attributes=pyAttributes(attributes).to_payload_attributes(),
69
- state_of=last_changed)
70
-
71
-
72
-
73
- @cached(cache)
74
- def read_excel_openpyxl(path: str, worksheet: str = None) -> list[tuple]:
75
- """
76
- Read and cache Excel worksheet as list of rows (including headers),
77
- then close the workbook.
78
- """
79
- wb = load_workbook(filename=path, read_only=True, data_only=True)
80
- ws = wb[worksheet] if worksheet else wb.active
81
-
82
- rows = list(ws.iter_rows(values_only=True))
83
-
84
- last_changed: datetime | None = wb.properties.modified
85
- wb.close() # immediately release the file
86
-
87
- return rows, last_changed # list of tuples (header + data rows)
88
-
89
-
90
- def get_row_by_first_cell(sheet_rows: list[tuple], match_value: str, base_url:str) -> dict | None:
91
- """
92
- Takes list of rows and returns the first row where the first cell == match_value,
93
- as a dict using headers from the first row.
94
- """
95
- if not sheet_rows:
96
- return None
97
-
98
- headers = sheet_rows[0]
99
- for row in sheet_rows[1:]:
100
- if not row:
101
- continue
102
- first = str(row[0]).strip() if row[0] is not None else ""
103
- if first == match_value:
104
- return {
105
- base_url + str(headers[i]).strip(): row[i]
106
- for i in range(1, len(headers))
107
- if headers[i] is not None
108
- }
109
-
110
- return None
111
-
112
-
113
-
114
- def is_sharepoint_url(s: str) -> bool:
115
- try:
116
- parsed = urlparse(s)
117
- if parsed.scheme not in {"http", "https"}:
118
- return False
119
- if "sharepoint.com" in parsed.netloc or "1drv.ms" in parsed.netloc:
120
- return True
121
- return False
122
- except Exception:
123
- return False
124
-
125
- def is_local_path(s: str) -> bool:
126
- if is_sharepoint_url(s):
127
- return False
128
- # Treat anything that's not a URL and points to an existing file as a local path
129
- return os.path.exists(s)
@@ -1 +0,0 @@
1
- import labfreed.utilities.translations
File without changes
File without changes
File without changes
File without changes