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.
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/PKG-INFO +1 -1
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/__init__.py +1 -1
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_extended/app/app_infrastructure.py +8 -15
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_extended/app/pac_info.py +30 -7
- labfreed-0.3.1a6/labfreed/pac_attributes/__init__.py +0 -0
- labfreed-0.3.1a6/labfreed/pac_attributes/client/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/client/client.py +4 -2
- {labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes/server → labfreed-0.3.1a6/labfreed/pac_attributes/pythonic}/attribute_server_factory.py +2 -2
- labfreed-0.3.1a6/labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +181 -0
- {labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes → labfreed-0.3.1a6/labfreed/pac_attributes/pythonic}/py_attributes.py +14 -4
- labfreed-0.3.1a6/labfreed/pac_attributes/pythonic/py_dict_data_source.py +13 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/__init__.py +3 -3
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/attribute_data_sources.py +8 -9
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/server.py +1 -1
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/resolver.py +2 -2
- {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/data_table.py +1 -1
- {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/pyTREX.py +2 -2
- labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py +0 -129
- labfreed-0.3.1a4/labfreed/pac_attributes/client/__init__.py +0 -1
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/workflows/pypi-publish.yml +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/.github/workflows/run-tests.yml +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/CHANGELOG.md +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/LICENSE +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/README.md +0 -0
- {labfreed-0.3.1a4/labfreed/labfreed_extended/utilities → labfreed-0.3.1a6/labfreed/labfreed_extended/app}/formatted_print.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/labfreed_infrastructure.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/request.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/response.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/client/attribute_cache.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/translation_data_sources.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/well_knonw_attribute_keys.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/category_base.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/pac_cat.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_cat/predefined_categories.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/extension.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/id_segment.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/pac_id.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/url_parser.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id/url_serializer.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_common.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_v1.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/cit_v2.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_id_resolver/services.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/qr/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/qr/generate_qr.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/__init__.py +0 -0
- {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/__init__.py +0 -0
- {labfreed-0.3.1a4/labfreed/trex/python_convenience → labfreed-0.3.1a6/labfreed/trex/pythonic}/quantity.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/table_segment.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/trex.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/trex_base_models.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/trex/value_segments.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/base36.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/ensure_utc_time.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/utilities/translations.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/display_name_extension.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/trex_extension.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/gs1/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/__init__.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_keys/unece/unece_units.py +0 -0
- {labfreed-0.3.1a4 → labfreed-0.3.1a6}/pyproject.toml +0 -0
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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.
|
|
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
|
{labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/attribute_data_sources.py
RENAMED
|
@@ -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,
|
|
34
|
-
if not all([isinstance(e,
|
|
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
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
labfreed-0.3.1a4/labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/pac_attributes/server/translation_data_sources.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{labfreed-0.3.1a4 → labfreed-0.3.1a6}/labfreed/well_known_extensions/display_name_extension.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|