labfreed 0.2.12__tar.gz → 0.3.0a1__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.2.12 → labfreed-0.3.0a1}/PKG-INFO +8 -1
- {labfreed-0.2.12 → labfreed-0.3.0a1}/README.md +4 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/__init__.py +1 -1
- labfreed-0.3.0a1/labfreed/labfreed_extended/app/app_infrastructure.py +104 -0
- labfreed-0.3.0a1/labfreed/labfreed_extended/app/pac_info.py +79 -0
- labfreed-0.3.0a1/labfreed/labfreed_extended/pac_attributes/py_attributes.py +123 -0
- labfreed-0.3.0a1/labfreed/labfreed_extended/pac_attributes/server/attribute_server_factory.py +103 -0
- labfreed-0.3.0a1/labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py +128 -0
- labfreed-0.3.0a1/labfreed/labfreed_extended/utilities/formatted_print.py +64 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/labfreed_infrastructure.py +3 -3
- labfreed-0.3.0a1/labfreed/pac_attributes/api_data_models/request.py +56 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/api_data_models/response.py +184 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/api_data_models/server_capabilities_response.py +7 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/client/__init__.py +1 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/client/attribute_cache.py +78 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/client/client.py +148 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/server/attribute_data_sources.py +69 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/server/server.py +237 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/server/translation_data_sources.py +60 -0
- labfreed-0.3.0a1/labfreed/pac_attributes/well_knonw_attribute_keys.py +11 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_cat/category_base.py +19 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_cat/pac_cat.py +1 -1
- labfreed-0.3.0a1/labfreed/pac_id/extension.py +73 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id/pac_id.py +2 -2
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id/url_parser.py +4 -4
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id/url_serializer.py +11 -5
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/cit_common.py +1 -1
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/cit_v1.py +0 -3
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/cit_v2.py +1 -6
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/resolver.py +12 -7
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/services.py +0 -1
- labfreed-0.3.0a1/labfreed/trex/python_convenience/quantity.py +147 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/value_segments.py +1 -1
- labfreed-0.3.0a1/labfreed/utilities/ensure_utc_time.py +6 -0
- labfreed-0.3.0a1/labfreed/utilities/translations.py +60 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_extensions/display_name_extension.py +3 -3
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_keys/gs1/gs1_ai_enum_sorted.py +4 -0
- labfreed-0.3.0a1/labfreed/well_known_keys/labfreed/well_known_keys.py +28 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_keys/unece/unece_units.py +2 -1
- {labfreed-0.2.12 → labfreed-0.3.0a1}/pyproject.toml +5 -0
- labfreed-0.2.12/labfreed/pac_id/extension.py +0 -48
- labfreed-0.2.12/labfreed/trex/python_convenience/quantity.py +0 -66
- labfreed-0.2.12/labfreed/well_known_keys/gs1/gs1.py +0 -4
- labfreed-0.2.12/labfreed/well_known_keys/labfreed/well_known_keys.py +0 -18
- {labfreed-0.2.12 → labfreed-0.3.0a1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/.github/workflows/pypi-publish.yml +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/.github/workflows/run-tests.yml +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/CHANGELOG.md +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/LICENSE +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_cat/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_cat/predefined_categories.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id/id_segment.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/pac_id_resolver/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/qr/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/qr/generate_qr.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/python_convenience/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/python_convenience/data_table.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/python_convenience/pyTREX.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/table_segment.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/trex.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/trex/trex_base_models.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/utilities/base36.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_extensions/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_extensions/default_extension_interpreters.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_extensions/trex_extension.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_keys/gs1/__init__.py +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_keys/unece/UneceUnits.json +0 -0
- {labfreed-0.2.12 → labfreed-0.3.0a1}/labfreed/well_known_keys/unece/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: labfreed
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0a1
|
|
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
|
|
@@ -28,16 +28,23 @@ Requires-Dist: pytest>=8.3.5 ; extra == "dev"
|
|
|
28
28
|
Requires-Dist: pdoc>=15.0.1 ; extra == "dev"
|
|
29
29
|
Requires-Dist: flit>=3.12.0 ; extra == "dev"
|
|
30
30
|
Requires-Dist: ruff>=0.11.5 ; extra == "dev"
|
|
31
|
+
Requires-Dist: Flask>=3.1.1 ; extra == "extended"
|
|
32
|
+
Requires-Dist: openpyxl>=3.1.5 ; extra == "extended"
|
|
31
33
|
Project-URL: Documentation, https://github.com/retothuerer/LabFREED?tab=readme-ov-file#readme
|
|
32
34
|
Project-URL: Homepage, https://github.com/retothuerer/LabFREED
|
|
33
35
|
Project-URL: Source, https://github.com/retothuerer/LabFREED
|
|
34
36
|
Project-URL: Tracker, https://github.com/retothuerer/LabFREED/issues
|
|
35
37
|
Provides-Extra: dev
|
|
38
|
+
Provides-Extra: extended
|
|
36
39
|
|
|
37
40
|
# LabFREED for Python
|
|
38
41
|
|
|
39
42
|
[](https://pypi.org/project/labfreed/)  [](https://github.com/retothuerer/LabFREED/actions/workflows/run-tests.yml) [](LICENSE)
|
|
40
43
|
|
|
44
|
+
<!--
|
|
45
|
+
[](https://github.com/astral-sh/ruff)
|
|
46
|
+
-->
|
|
47
|
+
|
|
41
48
|
|
|
42
49
|
This is a Python implementation of [LabFREED](https://labfreed.org/) building blocks.
|
|
43
50
|
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/labfreed/)  [](https://github.com/retothuerer/LabFREED/actions/workflows/run-tests.yml) [](LICENSE)
|
|
4
4
|
|
|
5
|
+
<!--
|
|
6
|
+
[](https://github.com/astral-sh/ruff)
|
|
7
|
+
-->
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
This is a Python implementation of [LabFREED](https://labfreed.org/) building blocks.
|
|
7
11
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from labfreed.pac_attributes.client.attribute_cache import MemoryAttributeCache
|
|
7
|
+
from labfreed.pac_attributes.client.client import AttributeClient, attribute_request_default_callback_factory
|
|
8
|
+
from labfreed.pac_attributes.well_knonw_attribute_keys import MetaAttributeKeys
|
|
9
|
+
from labfreed.well_known_extensions.display_name_extension import DisplayNameExtension
|
|
10
|
+
from labfreed_extended.app.pac_info import PacInfo
|
|
11
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
12
|
+
from labfreed.pac_id_resolver.resolver import PAC_ID_Resolver, cit_from_str
|
|
13
|
+
from labfreed.pac_id_resolver.services import ServiceGroup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Labfreed_App_Infrastructure():
|
|
20
|
+
def __init__(self, markup = 'rich', language_preferences:list[str]|str='en', http_client:requests.Session|None=None):
|
|
21
|
+
if isinstance(language_preferences, str):
|
|
22
|
+
language_preferences = [language_preferences]
|
|
23
|
+
self._language_preferences = language_preferences
|
|
24
|
+
|
|
25
|
+
self._resolver = PAC_ID_Resolver()
|
|
26
|
+
|
|
27
|
+
if not http_client:
|
|
28
|
+
http_client = requests.Session()
|
|
29
|
+
self._http_client= http_client
|
|
30
|
+
callback = attribute_request_default_callback_factory(http_client)
|
|
31
|
+
|
|
32
|
+
self._attribute_client = AttributeClient(http_post_callback=callback, cache_store=MemoryAttributeCache())
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def add_cit(self, cit:str):
|
|
36
|
+
cit = cit_from_str(cit)
|
|
37
|
+
if not cit:
|
|
38
|
+
raise ValueError('the cit could not be parsed. Neither as v1 or v2')
|
|
39
|
+
self._resolver._cits.append(cit)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def process_pac(self, pac_url, markup=None):
|
|
43
|
+
if not isinstance(pac_url, PAC_ID):
|
|
44
|
+
pac = PAC_ID.from_url(pac_url)
|
|
45
|
+
else:
|
|
46
|
+
pac = pac_url
|
|
47
|
+
service_groups = self._resolver.resolve(pac, check_service_status=False)
|
|
48
|
+
|
|
49
|
+
pac_info = PacInfo(pac_id=pac)
|
|
50
|
+
|
|
51
|
+
# update service states
|
|
52
|
+
(sg.update_states() for sg in service_groups)
|
|
53
|
+
|
|
54
|
+
# Services
|
|
55
|
+
sg_user_handovers = []
|
|
56
|
+
for sg in service_groups:
|
|
57
|
+
user_handovers = [s for s in sg.services if s.service_type == 'userhandover-generic']
|
|
58
|
+
|
|
59
|
+
if user_handovers:
|
|
60
|
+
sg_user_handovers.append(ServiceGroup(origin=sg.origin, services=user_handovers))
|
|
61
|
+
pac_info.user_handovers = sg_user_handovers
|
|
62
|
+
|
|
63
|
+
# Attributes
|
|
64
|
+
attribute_groups = []
|
|
65
|
+
for sg in service_groups:
|
|
66
|
+
attributes_urls = [s.url for s in sg.services if s.service_type == 'attributes-generic']
|
|
67
|
+
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)
|
|
69
|
+
if ags:
|
|
70
|
+
attribute_groups.extend(ags)
|
|
71
|
+
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
|
+
|
|
83
|
+
return pac_info
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def update_user_handover_states(self, services, session:requests.Session = None):
|
|
88
|
+
'''Triggers each service to check if the url can be reached'''
|
|
89
|
+
if not _has_internet_connection():
|
|
90
|
+
raise ConnectionError("No Internet Connection")
|
|
91
|
+
with ThreadPoolExecutor(max_workers=10) as executor:
|
|
92
|
+
futures = [executor.submit(s.check_service_status, session=session) for s in services]
|
|
93
|
+
for _ in as_completed(futures):
|
|
94
|
+
pass # just wait for all to finish
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _has_internet_connection():
|
|
98
|
+
try:
|
|
99
|
+
requests.head("https://1.1.1.1", timeout=3)
|
|
100
|
+
return True
|
|
101
|
+
except requests.RequestException:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from labfreed_extended.pac_attributes.py_attributes import pyAttribute, pyAttributes
|
|
5
|
+
from labfreed.pac_cat.pac_cat import PAC_CAT
|
|
6
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
7
|
+
from labfreed.pac_id_resolver.services import ServiceGroup
|
|
8
|
+
from labfreed_extended.utilities.formatted_print import StringIOLineBreak
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class PacInfo(BaseModel):
|
|
12
|
+
"""A convenient collection of information about a PAC-ID"""
|
|
13
|
+
pac_id:PAC_ID
|
|
14
|
+
display_name:str|None = None
|
|
15
|
+
user_handovers: list[ServiceGroup] = Field(default_factory=list)
|
|
16
|
+
attributes:pyAttributes = Field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def pac_url(self):
|
|
20
|
+
return self.pac_id.to_url(include_extensions=False)
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def main_category(self):
|
|
24
|
+
if isinstance(self.pac_id, PAC_CAT):
|
|
25
|
+
return self.pac_id.categories[0]
|
|
26
|
+
else:
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def attached_data(self):
|
|
31
|
+
return self.pac_id.get_extension_of_type('TREX')
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def summary(self):
|
|
35
|
+
return self.pac_id.get_extension('SUM')
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def format_for_print(self, markup:str='rich') -> str:
|
|
40
|
+
|
|
41
|
+
printout = StringIOLineBreak(markup=markup)
|
|
42
|
+
|
|
43
|
+
printout.write(f"for {self.pac_url}")
|
|
44
|
+
|
|
45
|
+
printout.title1("Info")
|
|
46
|
+
printout.key_value("Display Name", self.display_name)
|
|
47
|
+
|
|
48
|
+
if isinstance(self.pac_id, PAC_CAT):
|
|
49
|
+
printout.title1("Categories")
|
|
50
|
+
for c in self.pac_id.categories:
|
|
51
|
+
category_name = c.__class__.__name__
|
|
52
|
+
printout.title2(category_name)
|
|
53
|
+
for k,v in c.segments_as_dict().items():
|
|
54
|
+
printout.key_value(k, v)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
printout.title1("Services")
|
|
58
|
+
for sg in self.user_handovers:
|
|
59
|
+
printout.title2(f"(from {sg.origin})")
|
|
60
|
+
for s in sg.services:
|
|
61
|
+
printout.link(s.service_name, s.url)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
printout.title1("Attributes")
|
|
65
|
+
for ag in self.attributes:
|
|
66
|
+
printout.title2(f'{ag.label} (from {ag.origin})')
|
|
67
|
+
attributes = pyAttributes.from_payload_attributes(ag.attributes)
|
|
68
|
+
for k, v in attributes.items():
|
|
69
|
+
v:pyAttribute
|
|
70
|
+
#print(f'{k}: ({v.label}) :: {v.value} ')
|
|
71
|
+
printout.key_value(v.label, v.value)
|
|
72
|
+
|
|
73
|
+
out = printout.getvalue()
|
|
74
|
+
|
|
75
|
+
return out
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
|
|
2
|
+
from datetime import date, datetime, time
|
|
3
|
+
import json
|
|
4
|
+
from typing import Literal
|
|
5
|
+
import warnings
|
|
6
|
+
from pydantic import RootModel
|
|
7
|
+
|
|
8
|
+
from labfreed.labfreed_infrastructure import LabFREED_BaseModel
|
|
9
|
+
from labfreed.pac_attributes.api_data_models.response import AttributeBase, BoolAttribute, DateTimeAttribute, NumericAttribute, NumericValue, ObjectAttribute, ReferenceAttribute, TextAttribute
|
|
10
|
+
from labfreed.pac_id.pac_id import PAC_ID
|
|
11
|
+
from labfreed.trex.python_convenience.quantity import Quantity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class pyReference(RootModel[str]):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return str(self.root)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class pyAttribute(LabFREED_BaseModel):
|
|
22
|
+
key:str
|
|
23
|
+
label:str = ""
|
|
24
|
+
value: str|bool|datetime|pyReference|Quantity|int|float|dict|object
|
|
25
|
+
valid_until: datetime | Literal["forever"] | None = None
|
|
26
|
+
observed_at: datetime | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class pyAttributes(RootModel[list[pyAttribute]]):
|
|
31
|
+
def to_payload_attributes(self) -> list[AttributeBase]:
|
|
32
|
+
return [self._attribute_to_attribute_payload_type(e) for e in self.root]
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _attribute_to_attribute_payload_type(attribute:pyAttribute) -> AttributeBase:
|
|
36
|
+
common_args = {
|
|
37
|
+
"key": attribute.key,
|
|
38
|
+
"label": attribute.label,
|
|
39
|
+
"observed_at": attribute.observed_at
|
|
40
|
+
}
|
|
41
|
+
value = attribute.value
|
|
42
|
+
|
|
43
|
+
if isinstance(value, bool):
|
|
44
|
+
return BoolAttribute(value=value, **common_args)
|
|
45
|
+
|
|
46
|
+
elif isinstance(value, datetime | date | time):
|
|
47
|
+
if not value.tzinfo:
|
|
48
|
+
warnings.warn(f'No timezone given for {value}. Assuming it is in UTC.')
|
|
49
|
+
return DateTimeAttribute(value =value, **common_args)
|
|
50
|
+
# return DateTimeAttribute(value =_date_value_from_python_type(value).value, **common_args)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
elif isinstance(attribute.value, Quantity|int|float):
|
|
54
|
+
if not isinstance(attribute.value, Quantity):
|
|
55
|
+
value = Quantity(value=attribute.value, unit='dimensionless')
|
|
56
|
+
num_attribute = NumericAttribute(value = NumericValue(magnitude=value.value_as_str(),
|
|
57
|
+
unit = value.unit),
|
|
58
|
+
**common_args)
|
|
59
|
+
num_attribute.print_validation_messages()
|
|
60
|
+
return num_attribute
|
|
61
|
+
|
|
62
|
+
elif isinstance(value, str):
|
|
63
|
+
# capture quantities in the form of "100.0e5 g/L"
|
|
64
|
+
if q := Quantity.from_str_with_unit(value):
|
|
65
|
+
return NumericAttribute(value = NumericValue(magnitude=q.value_as_str(),
|
|
66
|
+
unit = q.unit),
|
|
67
|
+
**common_args)
|
|
68
|
+
else:
|
|
69
|
+
return TextAttribute(value = value, **common_args)
|
|
70
|
+
|
|
71
|
+
elif isinstance(value, pyReference):
|
|
72
|
+
return ReferenceAttribute(value = value.root, **common_args)
|
|
73
|
+
|
|
74
|
+
elif isinstance(value, PAC_ID):
|
|
75
|
+
return ReferenceAttribute(value = value.to_url(include_extensions=False), **common_args)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
else: #this covers the last resort case of arbitrary objects. Must be json serializable.
|
|
80
|
+
try :
|
|
81
|
+
value = json.loads(json.dumps(value))
|
|
82
|
+
return ObjectAttribute(value=value, **common_args)
|
|
83
|
+
except TypeError as e: # noqa: F841
|
|
84
|
+
raise ValueError(f'Invalid Type: {type(value)} cannot be converted to attribute. You may want to use ObjectAttribute, but would have to implement the conversion from your python type yourself.')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def from_payload_attributes(attributes:list[AttributeBase]) -> 'pyAttributes':
|
|
89
|
+
out = dict()
|
|
90
|
+
for a in attributes:
|
|
91
|
+
match a:
|
|
92
|
+
|
|
93
|
+
case ReferenceAttribute():
|
|
94
|
+
value = pyReference(a.value)
|
|
95
|
+
|
|
96
|
+
case NumericAttribute():
|
|
97
|
+
value = Quantity.from_str_value(value=a.value.magnitude, unit=a.value.unit)
|
|
98
|
+
|
|
99
|
+
case BoolAttribute():
|
|
100
|
+
value = a.value
|
|
101
|
+
|
|
102
|
+
case TextAttribute():
|
|
103
|
+
value = a.value
|
|
104
|
+
|
|
105
|
+
case DateTimeAttribute():
|
|
106
|
+
value = a.value
|
|
107
|
+
|
|
108
|
+
case ObjectAttribute():
|
|
109
|
+
value = a.value
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
attr = pyAttribute(key=a.key,
|
|
113
|
+
label=a.label,
|
|
114
|
+
value=value,
|
|
115
|
+
observed_at=a.observed_at
|
|
116
|
+
# valid_until=datetime(**_parse_date_time_str(a.valid_until)),
|
|
117
|
+
# observed_at=datetime(**_parse_date_time_str(a.value))
|
|
118
|
+
)
|
|
119
|
+
out.update( { a.key: attr } )
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import Any, Callable, Protocol
|
|
3
|
+
|
|
4
|
+
from flask import Blueprint
|
|
5
|
+
from labfreed.pac_attributes.server.server import AttributeGroupDataSource, AttributeServerRequestHandler, InvalidRequestError, TranslationDataSource
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from flask import Flask, Response, request
|
|
9
|
+
except ImportError:
|
|
10
|
+
raise ImportError("Please install labfreed with the [extended] extra: pip install labfreed[extended]")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# from fastapi import FastAPI, Request
|
|
14
|
+
|
|
15
|
+
class Authenticator(Protocol):
|
|
16
|
+
def __call__(self, request) -> bool: ...
|
|
17
|
+
|
|
18
|
+
class NoAuthRequiredAuthenticator(Authenticator):
|
|
19
|
+
def __call__(self, request) -> bool:
|
|
20
|
+
return True
|
|
21
|
+
|
|
22
|
+
class Webframework(Enum):
|
|
23
|
+
FLASK = "flask"
|
|
24
|
+
FASTAPI = 'fastapi'
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AttributeServerFactory():
|
|
28
|
+
@staticmethod
|
|
29
|
+
def create_server_app( datasources:list[AttributeGroupDataSource],
|
|
30
|
+
default_language:str,
|
|
31
|
+
translation_data_sources:list[TranslationDataSource],
|
|
32
|
+
authenticator: Authenticator|None,
|
|
33
|
+
framework:Webframework=Webframework.FLASK
|
|
34
|
+
):
|
|
35
|
+
|
|
36
|
+
if not authenticator:
|
|
37
|
+
raise ValueError("authenticator missing. Either define your own authenticator by implementing the 'Authenticator' Protocol, or - if you do not need authentication - explicitly pass a 'NoAuthRequiredAuthenticator' object")
|
|
38
|
+
|
|
39
|
+
request_handler = AttributeServerRequestHandler(data_sources=datasources,
|
|
40
|
+
translation_data_sources= translation_data_sources,
|
|
41
|
+
default_language=default_language
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
match(framework):
|
|
45
|
+
case Webframework.FLASK:
|
|
46
|
+
app = AttributeFlaskApp(request_handler,authenticator=authenticator)
|
|
47
|
+
return app
|
|
48
|
+
case Webframework.FASTAPI:
|
|
49
|
+
raise NotImplementedError('FastAPI webapp not implemented')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class AttributeFlaskApp(Flask):
|
|
56
|
+
def __init__(self, request_handler: AttributeServerRequestHandler, authenticator: Authenticator | None = None, **kwargs: Any):
|
|
57
|
+
super().__init__(__name__, **kwargs)
|
|
58
|
+
self.config['ATTRIBUTE_REQUEST_HANDLER'] = request_handler
|
|
59
|
+
self.config['AUTHENTICATOR'] = authenticator
|
|
60
|
+
|
|
61
|
+
bp = self.create_attribute_blueprint(request_handler, authenticator)
|
|
62
|
+
self.register_blueprint(bp)
|
|
63
|
+
|
|
64
|
+
@staticmethod
|
|
65
|
+
def create_attribute_blueprint(
|
|
66
|
+
request_handler: AttributeServerRequestHandler,
|
|
67
|
+
authenticator: Authenticator | None = None
|
|
68
|
+
) -> Blueprint:
|
|
69
|
+
bp = Blueprint("attribute", __name__)
|
|
70
|
+
|
|
71
|
+
@bp.route("/", methods=["POST"])
|
|
72
|
+
def handle_attribute_request():
|
|
73
|
+
if authenticator and not authenticator(request):
|
|
74
|
+
return Response(
|
|
75
|
+
"Unauthorized", 401,
|
|
76
|
+
{"WWW-Authenticate": 'Basic realm="Login required"'}
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
json_request_body = request.get_data(as_text=True)
|
|
80
|
+
response_body = request_handler.handle_attribute_request(json_request_body)
|
|
81
|
+
except InvalidRequestError as e:
|
|
82
|
+
print(e)
|
|
83
|
+
return "Invalid request", 400
|
|
84
|
+
except Exception as e:
|
|
85
|
+
print(e)
|
|
86
|
+
return "The request was valid, but the server encountered an error", 500
|
|
87
|
+
return response_body
|
|
88
|
+
|
|
89
|
+
@bp.route("/capabilities", methods=["GET"])
|
|
90
|
+
def capabilities():
|
|
91
|
+
return request_handler.capabilities()
|
|
92
|
+
|
|
93
|
+
return bp
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
|
labfreed-0.3.0a1/labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
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_extended.pac_attributes.py_attributes import pyAttribute, pyAttributes
|
|
9
|
+
from labfreed.pac_attributes.server.server import AttributeGroupDataSource
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from openpyxl import load_workbook
|
|
13
|
+
except ImportError:
|
|
14
|
+
raise ImportError("Please install labfreed with the [extended] extra: pip install labfreed[extended]")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
cache = TTLCache(maxsize=128, ttl=0)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ExcelAttributeDataSource(AttributeGroupDataSource):
|
|
21
|
+
'''
|
|
22
|
+
Demonstrates how to analyze the PAC-ID and it's extensions to provide some data
|
|
23
|
+
'''
|
|
24
|
+
def __init__(self, file_path:str, cache_duration_seconds:int=0, base_url:str="", *args, **kwargs):
|
|
25
|
+
self._file_path = file_path
|
|
26
|
+
self._base_url = base_url
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
if is_sharepoint_url(file_path):
|
|
30
|
+
self._file_location = "sharepoint"
|
|
31
|
+
else:
|
|
32
|
+
self._file_location = "local"
|
|
33
|
+
|
|
34
|
+
super().__init__(*args, **kwargs)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def is_static(self) -> bool:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def provides_attributes(self):
|
|
42
|
+
if self._file_location == "local":
|
|
43
|
+
rows, last_changed = read_excel_openpyxl(self._file_path)
|
|
44
|
+
headers = [self._base_url + r for r in rows[0][1:] ]
|
|
45
|
+
elif self._file_location == "sharepoint":
|
|
46
|
+
raise NotImplementedError('Sharepoint Access not implemented')
|
|
47
|
+
return headers
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def attributes(self, pac_url: str) -> AttributeGroup:
|
|
51
|
+
if not self._include_extensions:
|
|
52
|
+
pac_url = pac_url.split('*')[0]
|
|
53
|
+
|
|
54
|
+
if self._file_location == "local":
|
|
55
|
+
rows, last_changed = read_excel_openpyxl(self._file_path)
|
|
56
|
+
elif self._file_location == "sharepoint":
|
|
57
|
+
raise NotImplementedError('Sharepoint Access not implemented')
|
|
58
|
+
|
|
59
|
+
d = get_row_by_first_cell(rows, pac_url, self._base_url)
|
|
60
|
+
if not d:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
attributes = [pyAttribute(key=k, value=v) for k,v in d.items()]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
return AttributeGroup(key=self._attribute_group_key,
|
|
67
|
+
attributes=pyAttributes(attributes).to_payload_attributes(),
|
|
68
|
+
state_of=last_changed)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@cached(cache)
|
|
73
|
+
def read_excel_openpyxl(path: str, worksheet: str = None) -> list[tuple]:
|
|
74
|
+
"""
|
|
75
|
+
Read and cache Excel worksheet as list of rows (including headers),
|
|
76
|
+
then close the workbook.
|
|
77
|
+
"""
|
|
78
|
+
wb = load_workbook(filename=path, read_only=True, data_only=True)
|
|
79
|
+
ws = wb[worksheet] if worksheet else wb.active
|
|
80
|
+
|
|
81
|
+
rows = list(ws.iter_rows(values_only=True))
|
|
82
|
+
|
|
83
|
+
last_changed: datetime | None = wb.properties.modified
|
|
84
|
+
wb.close() # immediately release the file
|
|
85
|
+
|
|
86
|
+
return rows, last_changed # list of tuples (header + data rows)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_row_by_first_cell(sheet_rows: list[tuple], match_value: str, base_url:str) -> dict | None:
|
|
90
|
+
"""
|
|
91
|
+
Takes list of rows and returns the first row where the first cell == match_value,
|
|
92
|
+
as a dict using headers from the first row.
|
|
93
|
+
"""
|
|
94
|
+
if not sheet_rows:
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
headers = sheet_rows[0]
|
|
98
|
+
for row in sheet_rows[1:]:
|
|
99
|
+
if not row:
|
|
100
|
+
continue
|
|
101
|
+
first = str(row[0]).strip() if row[0] is not None else ""
|
|
102
|
+
if first == match_value:
|
|
103
|
+
return {
|
|
104
|
+
base_url + str(headers[i]).strip(): row[i]
|
|
105
|
+
for i in range(1, len(headers))
|
|
106
|
+
if headers[i] is not None
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_sharepoint_url(s: str) -> bool:
|
|
114
|
+
try:
|
|
115
|
+
parsed = urlparse(s)
|
|
116
|
+
if parsed.scheme not in {"http", "https"}:
|
|
117
|
+
return False
|
|
118
|
+
if "sharepoint.com" in parsed.netloc or "1drv.ms" in parsed.netloc:
|
|
119
|
+
return True
|
|
120
|
+
return False
|
|
121
|
+
except Exception:
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def is_local_path(s: str) -> bool:
|
|
125
|
+
if is_sharepoint_url(s):
|
|
126
|
+
return False
|
|
127
|
+
# Treat anything that's not a URL and points to an existing file as a local path
|
|
128
|
+
return os.path.exists(s)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
|
|
2
|
+
from io import StringIO
|
|
3
|
+
|
|
4
|
+
class StringIOLineBreak(StringIO):
|
|
5
|
+
def __init__(self, *args, markup=None, **kwargs):
|
|
6
|
+
self._markup = markup
|
|
7
|
+
super().__init__(*args, **kwargs)
|
|
8
|
+
|
|
9
|
+
def write(self, s:str):
|
|
10
|
+
s = s + '\n'
|
|
11
|
+
super().write(s)
|
|
12
|
+
|
|
13
|
+
def write_indented(self, s:str):
|
|
14
|
+
s = ' ' + s + '\n'
|
|
15
|
+
super().write(s)
|
|
16
|
+
|
|
17
|
+
def title1(self, s):
|
|
18
|
+
if self._markup == 'rich':
|
|
19
|
+
s = f'[bold][underline]{s}[/underline][/bold]'
|
|
20
|
+
elif self._markup == 'kivy':
|
|
21
|
+
s = f'[b][u]{s}[/u][/b]'
|
|
22
|
+
elif self._markup == 'html':
|
|
23
|
+
s = f'<h1>{s}</h1>'
|
|
24
|
+
self.new_section()
|
|
25
|
+
self.write(s)
|
|
26
|
+
|
|
27
|
+
def title2(self, s):
|
|
28
|
+
if self._markup == 'rich':
|
|
29
|
+
s = f'[bold]{s}[/bold]'
|
|
30
|
+
elif self._markup == 'kivy':
|
|
31
|
+
s = f'[b]{s}[/b]'
|
|
32
|
+
elif self._markup == 'html':
|
|
33
|
+
s = f'<h2>{s}</h2>'
|
|
34
|
+
self.new_paragraph()
|
|
35
|
+
self.write(s)
|
|
36
|
+
|
|
37
|
+
def key_value(self, k, v):
|
|
38
|
+
if not k:
|
|
39
|
+
self.write_indented(v)
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if self._markup == 'rich':
|
|
43
|
+
s = f'[bold]{k}[/bold]: {v}'
|
|
44
|
+
elif self._markup == 'kivy':
|
|
45
|
+
s = f'[b]{k}[/b]: {v}'
|
|
46
|
+
elif self._markup == 'html':
|
|
47
|
+
s = f'<b>{k}</b>: {v}'
|
|
48
|
+
self.write_indented(s)
|
|
49
|
+
|
|
50
|
+
def link(self, s, link):
|
|
51
|
+
if self._markup == 'rich':
|
|
52
|
+
s = f'[bold]{s}[/bold]: [link={link}]{link}[/link] '
|
|
53
|
+
elif self._markup == 'kivy':
|
|
54
|
+
s = f'[b]{s}[/b]: [ref={link}]{link}[/ref]'
|
|
55
|
+
elif self._markup == 'html':
|
|
56
|
+
s = f'<b>{s}</b>: <a href={link}>{link}</a>'
|
|
57
|
+
self.write_indented(s)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def new_paragraph(self):
|
|
61
|
+
super().write('\n')
|
|
62
|
+
|
|
63
|
+
def new_section(self):
|
|
64
|
+
super().write('\n\n')
|
|
@@ -108,7 +108,7 @@ class LabFREED_BaseModel(PDOC_Workaround_Base):
|
|
|
108
108
|
self._validation_messages.append(w)
|
|
109
109
|
|
|
110
110
|
# Function to extract warnings from a model and its nested models
|
|
111
|
-
def _get_nested_validation_messages(self, parent_name: str = "", visited: Set[int] = None) -> List['ValidationMessage']:
|
|
111
|
+
def _get_nested_validation_messages(self, parent_name: str = "", visited: Set[int]|None = None) -> List['ValidationMessage']:
|
|
112
112
|
"""
|
|
113
113
|
Recursively extract warnings from a Pydantic model and its nested fields, including computed fields.
|
|
114
114
|
|
|
@@ -140,7 +140,8 @@ class LabFREED_BaseModel(PDOC_Workaround_Base):
|
|
|
140
140
|
warnings_list.extend(item._get_nested_validation_messages(list_path, visited))
|
|
141
141
|
|
|
142
142
|
# Traverse computed fields
|
|
143
|
-
|
|
143
|
+
mdl:BaseModel = getattr(self, '__pydantic_decorators__', {})
|
|
144
|
+
computed_fields = mdl.computed_fields or {}
|
|
144
145
|
for field_name in computed_fields:
|
|
145
146
|
full_path = f"{parent_name}.{field_name}" if parent_name else field_name
|
|
146
147
|
try:
|
|
@@ -255,4 +256,3 @@ def _filter_warnings(val_msg:list[ValidationMessage]) -> list[ValidationMessage]
|
|
|
255
256
|
def _quote_texts(texts:list[str]):
|
|
256
257
|
return ','.join([f"'{t}'" for t in texts])
|
|
257
258
|
|
|
258
|
-
|