labfreed 0.3.0a0__py3-none-any.whl → 0.3.0a1__py3-none-any.whl

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.
labfreed/__init__.py CHANGED
@@ -2,7 +2,7 @@
2
2
  Python implementation of LabFREED building blocks
3
3
  '''
4
4
 
5
- __version__ = "0.3.0a"
5
+ __version__ = "0.3.0a1"
6
6
 
7
7
  from labfreed.pac_id import * # noqa: F403
8
8
  from labfreed.pac_cat import * # noqa: F403
@@ -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
+
@@ -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')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labfreed
3
- Version: 0.3.0a0
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
@@ -1,5 +1,11 @@
1
- labfreed/__init__.py,sha256=NXi5VbeTAbNd98NSoKvzjqrmIRUlYv0SdoaalKH_8XM,337
1
+ labfreed/__init__.py,sha256=IYsUcim01YjjUCrNxMPI1Sg-waYsQ2jOFpSLVA0XB7E,338
2
2
  labfreed/labfreed_infrastructure.py,sha256=YZmU-kgopyB1tvpTR_k_uIt1Q2ezexMrWvu-HaP65IE,10104
3
+ labfreed/labfreed_extended/app/app_infrastructure.py,sha256=fy6eEp1PkHuZoVPT4E3JRziJwoslvl7NC9K6WQRn1Fs,4383
4
+ labfreed/labfreed_extended/app/pac_info.py,sha256=WbjV4oY7aAxLRF__jijmsLoM-SW-mv9x_rR9gS6n4_Y,2598
5
+ labfreed/labfreed_extended/pac_attributes/py_attributes.py,sha256=5VA1NUTJA1L9Su6Rj7dhkAZaW0ZcUR8sI50qbHfY4fc,5138
6
+ labfreed/labfreed_extended/pac_attributes/server/attribute_server_factory.py,sha256=88cV80Yyt459YUkDffnOiuPXQCjKK982YQA6uvc-TaU,3853
7
+ labfreed/labfreed_extended/pac_attributes/server/excel_attribute_data_source.py,sha256=EN9fgzu4hP_1GxhShFet8FYFY-bXgVYG2_r63-HJpME,4317
8
+ labfreed/labfreed_extended/utilities/formatted_print.py,sha256=DcwWP0ix1e_wYNIdceIp6cETkJdG2DqpU8Gs3aZAL40,1930
3
9
  labfreed/pac_attributes/well_knonw_attribute_keys.py,sha256=ruHawnr1AffuhdKz1j1Nx67QphvcpmWsObgZhkuwkH0,318
4
10
  labfreed/pac_attributes/api_data_models/request.py,sha256=x-GuFYhCIpqAKa1AHs968OfD8O2cJKg14YUw2hTKVg4,1871
5
11
  labfreed/pac_attributes/api_data_models/response.py,sha256=czqQ59myLsN4WNgCdI_dnxRG8hSw4YI062XOPydfePU,5715
@@ -50,7 +56,7 @@ labfreed/well_known_keys/labfreed/well_known_keys.py,sha256=p-hXwEEIs7p2SKn9DQeL
50
56
  labfreed/well_known_keys/unece/UneceUnits.json,sha256=kwfQSp_nTuWbADfBBgqTWrvPl6XtM5SedEVLbMJrM7M,898953
51
57
  labfreed/well_known_keys/unece/__init__.py,sha256=MSP9lmjg9_D9iqG9Yq2_ajYfQSNS9wIT7FXA1c--59M,122
52
58
  labfreed/well_known_keys/unece/unece_units.py,sha256=J20d64H69qKDE3XlGdJoXIIh0G-d0jKoiIDsg9an5pk,1655
53
- labfreed-0.3.0a0.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
54
- labfreed-0.3.0a0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
55
- labfreed-0.3.0a0.dist-info/METADATA,sha256=0Fl6mtbyjD7ihk836WY79Shb-hsUXfw-HTbk9cSjYcU,19707
56
- labfreed-0.3.0a0.dist-info/RECORD,,
59
+ labfreed-0.3.0a1.dist-info/licenses/LICENSE,sha256=gHFOv9FRKHxO8cInP3YXyPoJnuNeqrvcHjaE_wPSsQ8,1100
60
+ labfreed-0.3.0a1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
61
+ labfreed-0.3.0a1.dist-info/METADATA,sha256=D-5o498iIUfdmlBvxCx9ErKj_HX5CPu7fpKaeCAmadI,19707
62
+ labfreed-0.3.0a1.dist-info/RECORD,,