labfreed 1.0.0a25__py3-none-any.whl → 1.0.0b26__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.
Files changed (27) hide show
  1. labfreed/__init__.py +1 -1
  2. labfreed/labfreed_extended/app/app_infrastructure.py +2 -5
  3. labfreed/labfreed_extended/app/pac_info/pac_info.py +6 -8
  4. labfreed/labfreed_extended/pac_issuer_lib/lib/attribute.py +9 -6
  5. labfreed/labfreed_extended/pac_issuer_lib/static/external-link.svg +6 -6
  6. labfreed/labfreed_extended/pac_issuer_lib/static/logo.svg +44 -44
  7. labfreed/pac_attributes/api_data_models/request.py +78 -30
  8. labfreed/pac_attributes/api_data_models/response.py +107 -136
  9. labfreed/pac_attributes/client/client.py +30 -36
  10. labfreed/pac_attributes/client/client_attribute_group.py +18 -0
  11. labfreed/pac_attributes/pythonic/attribute_server_factory.py +39 -13
  12. labfreed/pac_attributes/pythonic/excel_attribute_data_source.py +1 -1
  13. labfreed/pac_attributes/pythonic/py_attributes.py +97 -125
  14. labfreed/pac_attributes/server/attribute_data_sources.py +9 -8
  15. labfreed/pac_attributes/server/server.py +33 -37
  16. labfreed/pac_id_resolver/resolver_config.py +2 -1
  17. {labfreed-1.0.0a25.dist-info → labfreed-1.0.0b26.dist-info}/METADATA +1 -1
  18. {labfreed-1.0.0a25.dist-info → labfreed-1.0.0b26.dist-info}/RECORD +20 -26
  19. labfreed/labfreed_extended/app/pac_info/html_renderer/external-link.svg +0 -7
  20. labfreed/labfreed_extended/app/pac_info/html_renderer/macros.jinja.html +0 -188
  21. labfreed/labfreed_extended/app/pac_info/html_renderer/pac-info-style.css +0 -176
  22. labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info.jinja.html +0 -46
  23. labfreed/labfreed_extended/app/pac_info/html_renderer/pac_info_card.jinja.html +0 -7
  24. labfreed/pac_attributes/client/__init__.py +0 -0
  25. labfreed/pac_attributes/client/attribute_cache.py +0 -65
  26. {labfreed-1.0.0a25.dist-info → labfreed-1.0.0b26.dist-info}/WHEEL +0 -0
  27. {labfreed-1.0.0a25.dist-info → labfreed-1.0.0b26.dist-info}/licenses/LICENSE +0 -0
@@ -2,40 +2,47 @@
2
2
  from abc import ABC
3
3
  from datetime import datetime
4
4
  import re
5
- from typing import Annotated, Any, Literal, Union, get_args
5
+ from typing import Annotated, Any, Literal, Union, get_args
6
6
  from urllib.parse import urlparse
7
7
 
8
8
  from labfreed.utilities.ensure_utc_time import ensure_utc
9
9
  from labfreed.labfreed_infrastructure import LabFREED_BaseModel, ValidationMsgLevel, _quote_texts
10
- from pydantic import Field, field_validator, model_validator
10
+ from pydantic import BaseModel, Field, field_validator, model_validator
11
11
 
12
12
 
13
- class AttributeBase(LabFREED_BaseModel, ABC):
14
- key: str
13
+
14
+ class AttributeItemsElementBase(LabFREED_BaseModel, ABC):
15
15
  value: Any
16
- label: str = ""
17
-
18
- def __init__(self, **data):
19
- # Automatically inject the Literal value for `type`
20
- discriminator_value = self._get_discriminator_value()
21
- data["type"] = discriminator_value
22
- super().__init__(**data)
16
+ type:str
23
17
 
24
- @classmethod
25
- def _get_discriminator_value(cls) -> str:
26
- """Extract the Literal value from the 'type' annotation."""
27
- try:
28
- type_annotation = cls.__annotations__["type"]
29
- literal_value = get_args(type_annotation)[0]
30
- return literal_value
31
- except Exception as e:
32
- raise TypeError(
33
- f"{cls.__name__} must define `type: Literal[<value>]` annotation"
34
- ) from e
18
+ @model_validator(mode="after")
19
+ def _no_base_instances(self):
20
+ if type(self) is AttributeItemsElementBase:
21
+ raise TypeError("AttributeItemsElementBase must not be instantiated")
22
+ return self
35
23
 
24
+ # def __init__(self, **data):
25
+ # # Automatically inject the Literal value for `type`
26
+ # discriminator_value = self._get_discriminator_value()
27
+ # data["type"] = discriminator_value
28
+ # super().__init__(**data)
29
+
30
+ # @classmethod
31
+ # def _get_discriminator_value(cls) -> str:
32
+ # """Extract the Literal value from the 'type' annotation."""
33
+ # try:
34
+ # type_annotation = cls.__annotations__["type"]
35
+ # literal_value = get_args(type_annotation)[0]
36
+ # return literal_value
37
+ # except Exception as e:
38
+ # raise TypeError(
39
+ # f"{cls.__name__} must define `type: Literal[<value>]` annotation"
40
+ # ) from e
41
+
42
+
36
43
 
37
- class DateTimeAttribute(AttributeBase):
38
- type: Literal["datetime"]
44
+ class DateTimeAttributeItemsElement(AttributeItemsElementBase):
45
+ type: Literal["datetime"] = "datetime"
39
46
  value: datetime
40
47
 
41
48
  @field_validator('value', mode='after')
@@ -45,53 +52,24 @@ class DateTimeAttribute(AttributeBase):
45
52
  else:
46
53
  return value
47
54
 
48
- class DateTimeListAttribute(AttributeBase):
49
- type: Literal["datetime-list"]
50
- value: list[datetime]
51
-
52
- @field_validator('value', mode='after')
53
- def set_utc__if_naive(cls, value):
54
- value_out = []
55
- for v in value:
56
- if isinstance(v, datetime):
57
- value_out.append(ensure_utc(v))
58
- else:
59
- raise ValueError(f'{v} is of type {type(v)}. It must be datetime')
60
- return value_out
61
55
 
62
56
 
63
-
64
- class BoolAttribute(AttributeBase):
65
- type: Literal["bool"]
57
+ class BoolAttributeItemsElement(AttributeItemsElementBase):
58
+ type: Literal["bool"] = "bool"
66
59
  value: bool
67
60
 
68
- class BoolListAttribute(AttributeBase):
69
- type: Literal["bool-list"]
70
- value: list[bool]
71
-
72
61
 
73
62
 
74
63
 
75
- class TextAttribute(AttributeBase):
76
- type: Literal["text"]
64
+ class TextAttributeItemsElement(AttributeItemsElementBase):
65
+ type: Literal["text"] = "text"
77
66
  value: str
78
67
 
79
68
  @model_validator(mode='after')
80
69
  def _validate_value(self):
81
70
  _validate_text(self, self.value)
82
71
  return self
83
-
84
- class TextListAttribute(AttributeBase):
85
- type: Literal["text-list"]
86
- value: list[str]
87
-
88
- @model_validator(mode='after')
89
- def _validate_value(self):
90
- l = [self.value] if isinstance(self.value, str) else self.value
91
- for v in l:
92
- _validate_text(self, v)
93
- return self
94
-
72
+
95
73
 
96
74
  def _validate_text(mdl:LabFREED_BaseModel, v):
97
75
  if len(v) > 5000:
@@ -104,19 +82,14 @@ def _validate_text(mdl:LabFREED_BaseModel, v):
104
82
 
105
83
 
106
84
 
107
- class ReferenceAttribute(AttributeBase):
108
- type: Literal["reference"]
85
+ class ReferenceAttributeItemsElement(AttributeItemsElementBase):
86
+ type: Literal["reference"] = "reference"
109
87
  value: str
110
88
 
111
- class ReferenceListAttribute(AttributeBase):
112
- type: Literal["reference-list"]
113
- value: list[str]
114
-
115
-
116
-
89
+
117
90
 
118
- class ResourceAttribute(AttributeBase):
119
- type: Literal["resource"]
91
+ class ResourceAttributeItemsElement(AttributeItemsElementBase):
92
+ type: Literal["resource"] = "resource"
120
93
  value: str
121
94
 
122
95
  @model_validator(mode='after')
@@ -124,16 +97,6 @@ class ResourceAttribute(AttributeBase):
124
97
  _validate_resource(self, self.value)
125
98
  return self
126
99
 
127
- class ResourceListAttribute(AttributeBase):
128
- type: Literal["resource-list"]
129
- value: list[str]
130
-
131
- @model_validator(mode='after')
132
- def _validate_value(self):
133
- value_list = self.value if isinstance(self.value, list) else [self.value]
134
- for v in value_list:
135
- _validate_resource(self, v)
136
- return self
137
100
 
138
101
  def _validate_resource(mdl:LabFREED_BaseModel, v):
139
102
  r = urlparse(v)
@@ -153,15 +116,23 @@ def _validate_resource(mdl:LabFREED_BaseModel, v):
153
116
  highlight_pattern = f'{v}'
154
117
  )
155
118
 
156
-
157
-
158
- class NumericValue(LabFREED_BaseModel):
159
- numerical_value: str
160
- unit: str
119
+
120
+ class NumericAttributeItemsElement(AttributeItemsElementBase):
121
+ type: Literal["numeric"] = "numeric"
122
+ value: str
123
+ _numerical_value:str
124
+ _unit:str
161
125
 
162
126
  @model_validator(mode='after')
127
+ def _validate_model(self):
128
+ self._numerical_value, self._unit = self.value.split(' ', 1)
129
+ self._validate_value()
130
+ self._validate_unit()
131
+ return self
132
+
133
+
163
134
  def _validate_value(self):
164
- value = self.numerical_value
135
+ value = self._numerical_value
165
136
  if not_allowed_chars := set(re.sub(r'[0-9\.\-\+Ee]', '', value)):
166
137
  self._add_validation_message(
167
138
  source="Numeric Attribute",
@@ -177,81 +148,76 @@ class NumericValue(LabFREED_BaseModel):
177
148
  msg=f"{value} cannot be converted to number",
178
149
  highlight_pattern = f'{value}'
179
150
  )
180
- return self
181
151
 
182
- @model_validator(mode="after")
183
- def _validate_units(self):
152
+ def _validate_unit(self):
184
153
  '''A sanity check on unit complying with UCUM. NOTE: It is not a complete validation
185
154
  - I check for blankspaces and ^, which are often used for units, but are invalid.
186
155
  - the general structure of a ucum unit is validated, but 1)parentheses are not matched 2) units are not validated 3)prefixes are not checked
187
156
  '''
188
- if ' ' in self.unit or '^' in self.unit:
157
+ if ' ' in self._unit or '^' in self._unit:
189
158
  self._add_validation_message(
190
159
  source="Numeric Attribute",
191
160
  level= ValidationMsgLevel.ERROR,
192
- msg=f"Unit {self.unit} is invalid. Must not contain blankspace or '^'.",
193
- highlight_pattern = self.unit
161
+ msg=f"Unit {self._unit} is invalid. Must not contain blankspace or '^'.",
162
+ highlight_pattern = self._unit
194
163
  )
195
- elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self.unit):
164
+ elif not re.fullmatch(r"^(((?P<unit>[\w\[\]]+?)(?P<exponent>\-?\d+)?|(?P<annotation>)\{\w+?\})(?P<operator>[\./]?)?)+", self._unit):
196
165
  self._add_validation_message(
197
166
  source="Numeric Attribute",
198
167
  level= ValidationMsgLevel.WARNING,
199
- msg=f"Unit {self.unit} is probably invalid. Ensure it complies with UCUM specifications.",
200
- highlight_pattern = self.unit
168
+ msg=f"Unit {self._unit} is probably invalid. Ensure it complies with UCUM specifications.",
169
+ highlight_pattern = self._unit
201
170
  )
202
- return self
203
171
 
204
172
 
205
-
206
-
207
- class NumericAttribute(AttributeBase):
208
- type: Literal["numeric"]
209
- value: NumericValue
210
-
211
- class NumericListAttribute(AttributeBase):
212
- type: Literal["numeric-list"]
213
- value: list[NumericValue]
214
-
215
173
 
216
-
217
- class ObjectAttribute(AttributeBase):
218
- type: Literal["object"]
174
+ class ObjectAttributeItemsElement(AttributeItemsElementBase):
175
+ type: Literal["object"] = "object"
219
176
  value: dict[str, Any]
220
177
 
221
178
 
222
- class ObjectListAttribute(AttributeBase):
223
- type: Literal["object-list"]
224
- value: list[dict[str, Any]]
225
-
226
-
227
-
228
-
229
- Attribute = Annotated[
179
+ AttributeItemsElement = Annotated[
230
180
  Union[
231
- ReferenceAttribute,
232
- DateTimeAttribute,
233
- BoolAttribute,
234
- TextAttribute,
235
- NumericAttribute,
236
- ResourceAttribute,
237
- ObjectAttribute,
238
-
239
- ReferenceListAttribute,
240
- DateTimeListAttribute,
241
- BoolListAttribute,
242
- TextListAttribute,
243
- NumericListAttribute,
244
- ResourceListAttribute,
245
- ObjectListAttribute
181
+ DateTimeAttributeItemsElement,
182
+ BoolAttributeItemsElement,
183
+ TextAttributeItemsElement,
184
+ NumericAttributeItemsElement,
185
+ ReferenceAttributeItemsElement,
186
+ ResourceAttributeItemsElement,
187
+ ObjectAttributeItemsElement
246
188
  ],
247
- Field(discriminator="type")
189
+ Field(discriminator="type"),
248
190
  ]
191
+
249
192
 
193
+
194
+ class Attribute(LabFREED_BaseModel):
195
+ key: str|None = Field(exclude=True)
196
+ label: str = ""
197
+ items: list[AttributeItemsElement]
198
+
199
+
250
200
 
251
201
  class AttributeGroup(LabFREED_BaseModel):
252
- key: str
253
- label: str = ""
254
- attributes: list[Attribute]
202
+ group_key: str
203
+ group_label: str = ""
204
+ attributes: dict[str, Attribute]
205
+
206
+ @field_validator("attributes", mode="before")
207
+ @classmethod
208
+ def set_attribute_keys(cls, v):
209
+ if not isinstance(v, dict):
210
+ return v
211
+
212
+ out = {}
213
+ for k, a in v.items():
214
+ if isinstance(a, dict):
215
+ # raw input dict -> inject key if missing
216
+ out[k] = {**a, "key": a.get("key") or k}
217
+ else:
218
+ # already an Attribute (or something pydantic can parse)
219
+ out[k] = a
220
+ return out
255
221
 
256
222
 
257
223
 
@@ -260,14 +226,19 @@ class AttributesOfPACID(LabFREED_BaseModel):
260
226
  attribute_groups: list[AttributeGroup]
261
227
 
262
228
 
229
+
230
+ IMPORT_URL = "https://vocab.labfreed.org/attributes/v1.jsonld"
263
231
 
264
232
  class AttributeResponsePayload(LabFREED_BaseModel):
265
233
  schema_version: str = Field(default='1.0')
266
234
  language:str
267
- pac_attributes: list[AttributesOfPACID]
235
+ data: list[AttributesOfPACID]
236
+
237
+ context: str = Field(alias='@context', default=IMPORT_URL)
238
+
268
239
 
269
240
  def to_json(self):
270
- return self.model_dump_json(exclude_none=True)
241
+ return self.model_dump_json(exclude_none=True, by_alias=True)
271
242
 
272
243
 
273
244
 
@@ -3,14 +3,16 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from datetime import UTC, datetime
5
5
  from typing import Protocol, runtime_checkable
6
+ from urllib.parse import quote
7
+ import warnings
6
8
 
7
9
  import requests
8
10
 
9
11
  from pydantic import ValidationError
10
12
 
11
- from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
12
- from labfreed.pac_attributes.api_data_models.response import AttributeResponsePayload
13
- from labfreed.pac_attributes.client.attribute_cache import AttributeCache, CacheableAttributeGroup
13
+ from labfreed.pac_attributes.api_data_models.request import AttributeRequestData
14
+ from labfreed.pac_attributes.api_data_models.response import AttributeGroup, AttributeResponsePayload
15
+ from labfreed.pac_attributes.client.client_attribute_group import ClientAttributeGroup
14
16
  from labfreed.pac_attributes.server.server import AttributeServerRequestHandler
15
17
  from labfreed.pac_id.pac_id import PAC_ID
16
18
 
@@ -31,7 +33,7 @@ class AttributeServerError(Exception):
31
33
 
32
34
  @runtime_checkable
33
35
  class AttributeRequestCallback(Protocol):
34
- def __call__(self, url: str, attribute_request_body: str) -> tuple[int, str]:
36
+ def __call__(self, url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
35
37
  '''handle the request
36
38
  returns a tuple of HTTP status code and the body of the response or an error message'''
37
39
  ...
@@ -49,9 +51,14 @@ def http_attribute_request_default_callback_factory(session: requests.Session =
49
51
  if session is None:
50
52
  session = requests.Session()
51
53
 
52
- def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
54
+ def callback(url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
53
55
  try:
54
- resp = session.post(url, data=attribute_request_body, headers={'Content-Type': 'application/json; ; charset=utf-8'}, timeout=10)
56
+ url = url + '/' + quote(attribute_request_data.pac_id, safe='')
57
+ params = attribute_request_data.request_params()
58
+ resp = session.get(url,
59
+ params = params,
60
+ headers=attribute_request_data.language_preference_http_header(),
61
+ timeout=10)
55
62
  return resp.status_code, resp.text
56
63
  except requests.exceptions.RequestException as e:
57
64
  return 500, str(e)
@@ -68,9 +75,9 @@ def local_attribute_request_callback_factory(request_handler:AttributeServerRequ
68
75
  AttributeRequestCallback: a callback following the AttributeRequestCallback protocol.
69
76
  """
70
77
 
71
- def callback(url: str, attribute_request_body: str) -> tuple[int, str]:
78
+ def callback(url: str, attribute_request_data: AttributeRequestData) -> tuple[int, str]:
72
79
  try:
73
- resp = request_handler.handle_attribute_request(attribute_request_body)
80
+ resp = request_handler.handle_attribute_request(attribute_request_data)
74
81
  return 200, resp
75
82
  except requests.exceptions.RequestException as e:
76
83
  return 500, str(e)
@@ -83,18 +90,14 @@ def local_attribute_request_callback_factory(request_handler:AttributeServerRequ
83
90
  class AttributeClient():
84
91
  """ Client handling attribute requests and caching thereof.
85
92
  """
86
-
87
93
  http_post_callback:AttributeRequestCallback
88
- cache_store:AttributeCache
89
- always_use_cached_value_for_minutes:int
90
-
94
+
91
95
  def get_attributes(self,
92
96
  server_url:str,
93
97
  pac_id:PAC_ID|str,
94
98
  restrict_to_attribute_groups:list[str]|None=None,
95
- language_preferences:list[str]|None=None,
96
- force_server_request=False
97
- ) -> list[CacheableAttributeGroup]:
99
+ language_preferences:list[str]|None=None
100
+ ) -> list[AttributeGroup]:
98
101
  """gets the attributes from one attribute server for one PAC-ID. Uses a cached version if possible, otherwise requests from the server again.
99
102
 
100
103
  Args:
@@ -114,28 +117,21 @@ class AttributeClient():
114
117
  """
115
118
  if isinstance(pac_id, str):
116
119
  pac_id = PAC_ID.from_url(pac_id)
117
-
118
- # try the cache
119
- if not force_server_request:
120
- if restrict_to_attribute_groups:
121
- attribute_groups = self.cache_store.get_attribute_groups(server_url, pac_id, restrict_to_attribute_groups)
122
- else:
123
- attribute_groups = self.cache_store.get_all(server_url, pac_id)
124
120
 
125
- if attribute_groups and all([ag.still_valid(accept_cache_for_minutes=self.always_use_cached_value_for_minutes) for ag in attribute_groups]):
126
- return attribute_groups
127
-
128
121
  # no valid data found in cache > request to server
129
- attribute_request_body = AttributeRequestPayload(pac_ids=[pac_id.to_url()],
130
- restrict_to_attribute_groups=restrict_to_attribute_groups,
131
- language_preferences=language_preferences
122
+ attribute_request_body = AttributeRequestData(pac_id=pac_id.to_url(),
123
+ restrict_to_attribute_groups=restrict_to_attribute_groups,
124
+ language_preferences=language_preferences
132
125
  )
133
- response_code, response_body_str = self.http_post_callback(server_url, attribute_request_body.model_dump_json())
126
+ response_code, response_body_str = self.http_post_callback(server_url, attribute_request_body)
134
127
 
135
128
  if response_code == 400:
136
129
  raise AttributeClientInternalError(f"The server did not accept the request. Server message: '{response_body_str}'")
137
130
  if response_code == 401:
138
131
  raise AuthenticationError(f"Failed to authorize at the server. Server message: {response_body_str}")
132
+ if response_code == 404:
133
+ print(f'No atributes found for {pac_id.to_url()}')
134
+ return []
139
135
  if response_code == 500:
140
136
  raise AttributeServerError(f"The server accepted the request, but encountered an internal error. Contact the server admin. Server message: {response_body_str}")
141
137
  try:
@@ -145,21 +141,19 @@ class AttributeClient():
145
141
  raise AttributeServerError("The server accepted the request, and sent a reponse. However, the response is not adhering to the PAC Attributes specifications. Contact the server admin.")
146
142
 
147
143
 
148
- # update cache
149
144
  attribute_groups_out = []
150
- for ag_for_pac in r.pac_attributes:
145
+ for ag_for_pac in r.data:
151
146
  pac_from_response = PAC_ID.from_url(ag_for_pac.pac_id)
152
147
  ags = [
153
- CacheableAttributeGroup(
154
- key= ag.key,
148
+ ClientAttributeGroup(
149
+ group_key= ag.group_key,
155
150
  attributes=ag.attributes,
156
151
  origin=server_url,
157
152
  language=r.language,
158
- label=ag.label,
159
- value_from=datetime.now(tz=UTC))
153
+ group_label=ag.group_label
154
+ )
160
155
  for ag in ag_for_pac.attribute_groups
161
156
  ]
162
- self.cache_store.update(server_url, pac_from_response, ags)
163
157
 
164
158
  # compare pac_id from response with pac_id we need attributes for.
165
159
  # if identical this is the part of the response we care about. other PAC-ID are just for the cache
@@ -0,0 +1,18 @@
1
+
2
+
3
+ from datetime import UTC, datetime, timedelta
4
+
5
+
6
+ from labfreed.pac_attributes.api_data_models.response import AttributeGroup
7
+ from labfreed.pac_id.pac_id import PAC_ID
8
+
9
+
10
+
11
+ class ClientAttributeGroup(AttributeGroup):
12
+ ''' extends attribute group with info the client needs'''
13
+ origin:str
14
+ language:str
15
+ value_from: datetime | None = None
16
+
17
+
18
+
@@ -1,8 +1,11 @@
1
1
  from enum import Enum
2
+ import json
3
+ import logging
2
4
  from typing import Any, Protocol
5
+ from urllib.parse import unquote, unquote_plus
3
6
 
4
- from flask import Blueprint, current_app, url_for
5
- from labfreed.pac_attributes.api_data_models.request import AttributeRequestPayload
7
+ from flask import Blueprint, current_app, redirect, url_for
8
+ from labfreed.pac_attributes.api_data_models.request import AttributeRequestData
6
9
  from labfreed.pac_attributes.server.server import AttributeGroupDataSource, AttributeServerRequestHandler, InvalidRequestError, TranslationDataSource
7
10
 
8
11
  try:
@@ -70,16 +73,22 @@ class AttributeFlaskApp(Flask):
70
73
  ) -> Blueprint:
71
74
  bp = Blueprint("attribute", __name__)
72
75
 
73
- @bp.route("/", methods=["POST"], strict_slashes=False)
74
- def handle_attribute_request():
76
+
77
+ @bp.get("/<path:pac_id_url_encoded>", strict_slashes=False)
78
+ def handle_attribute_request(pac_id_url_encoded):
79
+ if pac_id_url_encoded in ['favicon.ico']:
80
+ return ''
81
+
75
82
  if authenticator and not authenticator(request):
76
83
  return Response(
77
84
  "Unauthorized", 401,
78
85
  {"WWW-Authenticate": 'Basic realm="Login required"'}
79
86
  )
80
87
  try:
81
- json_request_body = request.get_data(as_text=True)
82
- response_body = request_handler.handle_attribute_request(json_request_body)
88
+ request_data = AttributeRequestData.from_http_request(pac_id = pac_id_url_encoded,
89
+ params = request.args,
90
+ headers = request.headers)
91
+ response_body = request_handler.handle_attribute_request(request_data)
83
92
  except InvalidRequestError as e:
84
93
  print(e)
85
94
  return "Invalid request", 400
@@ -87,14 +96,26 @@ class AttributeFlaskApp(Flask):
87
96
  print(e)
88
97
  return "The request was valid, but the server encountered an error", 500
89
98
  return (response_body, 200, {"Content-Type": "application/json; charset=utf-8"})
90
-
91
- @bp.route("/", methods=["GET"], strict_slashes=False)
99
+
100
+
101
+ @bp.post("/", strict_slashes=False)
102
+ def handle_attribute_request_legacy(pac_id):
103
+ if request.method == 'POST':
104
+ return '\n'.join(('POST request was part of the DRAFT specification, but was changed to GET.',
105
+ 'You are probably using an pre-release version of the python package.',
106
+ 'update to the newest version "pip install labfreed"'
107
+ )
108
+ )
109
+
110
+
111
+ @bp.get("/", strict_slashes=False)
112
+ @bp.get("/capabilities", strict_slashes=False)
92
113
  def capabilities():
93
114
  doc_text = current_app.config.get('DOC_TEXT', "")
94
115
  capabilities = request_handler.capabilities()
95
116
  authentication_required = bool(current_app.config.get('AUTHENTICATOR'))
96
- example_request = AttributeRequestPayload(pac_ids=['HTTPS://PAC.METTORIUS.COM/EXAMPLE'], language_preferences=['fr', 'de']).model_dump_json(indent=2, exclude_none=True, exclude_unset=True)
97
- server_address = request.url.rstrip('/')
117
+ example_request = AttributeRequestData(pac_id='HTTPS://PAC.METTORIUS.COM/EXAMPLE', language_preferences=['fr', 'de']).model_dump_json(indent=2, exclude_none=True, exclude_unset=True)
118
+ server_address = request.url.replace('/capabilities','').rstrip('/')
98
119
  css_url = url_for("static", filename="style.css")
99
120
  response = f'''
100
121
  <html>
@@ -112,12 +133,17 @@ class AttributeFlaskApp(Flask):
112
133
 
113
134
 
114
135
  <h2>How to use</h2>
115
- Make a <b>POST</b> request to <a href="{server_address}">{server_address}</a> with the following body:
116
- <pre>{example_request}</pre>
136
+ Make a <b>GET</b> request to <a href="{server_address}">{server_address}/<url encoded PAC-ID> </a>
137
+ <br><br>
138
+ Query parameters (optional):<br>
139
+ attr_grps (optional): An comma separated list of attribute group keys. MUST be url-encoded. <br>
140
+ attr_fwd_lkp (optional): Boolean flag ('true' or 'false'). Instructs the server to not include attributes of PAC-IDs which are attributes of type reference of the requested PAC-ID. Defaults to true <br>
141
+
142
+ <br>
117
143
  Consult <a href="https://github.com/ApiniLabs/PAC-Attributes"> the specification </a> for details. <br>
118
144
 
119
145
 
120
- {'This server <b> requires authentication </b> ' if authentication_required else ''}
146
+ {'<h2> Authentication </h2> This server <b> requires authentication </b> ' if authentication_required else ''}
121
147
  <br>
122
148
 
123
149
  {"<h2>Further Information</h2>"if doc_text else ""}
@@ -120,7 +120,7 @@ class _BaseExcelAttributeDataSource(AttributeGroupDataSource):
120
120
  return None
121
121
  attributes = [pyAttribute(key= self._header_mappings.get(k, k), value=v) for k, v in d.items() if v is not None]
122
122
  return AttributeGroup(
123
- key=self._attribute_group_key,
123
+ group_key=self._attribute_group_key,
124
124
  attributes=pyAttributes(attributes).to_payload_attributes()
125
125
  )
126
126