netrias_client 0.1.0__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.
- netrias_client/__init__.py +18 -0
- netrias_client/_adapter.py +288 -0
- netrias_client/_client.py +559 -0
- netrias_client/_config.py +101 -0
- netrias_client/_core.py +560 -0
- netrias_client/_data_model_store.py +366 -0
- netrias_client/_discovery.py +525 -0
- netrias_client/_errors.py +37 -0
- netrias_client/_gateway_bypass.py +217 -0
- netrias_client/_http.py +234 -0
- netrias_client/_io.py +28 -0
- netrias_client/_logging.py +46 -0
- netrias_client/_models.py +115 -0
- netrias_client/_validators.py +192 -0
- netrias_client/scripts.py +313 -0
- netrias_client-0.1.0.dist-info/METADATA +178 -0
- netrias_client-0.1.0.dist-info/RECORD +20 -0
- netrias_client-0.1.0.dist-info/WHEEL +4 -0
- netrias_client-0.1.0.dist-info/entry_points.txt +5 -0
- netrias_client-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Query data models, CDEs, and permissible values from the Data Model Store.
|
|
2
|
+
|
|
3
|
+
'why': provide typed access to reference data for validation use cases
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ._errors import DataModelStoreError, NetriasAPIUnavailable
|
|
13
|
+
from ._http import fetch_cdes, fetch_data_models, fetch_pvs
|
|
14
|
+
from ._models import CDE, DataModel, PermissibleValue, Settings
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def list_data_models_async(
|
|
18
|
+
settings: Settings,
|
|
19
|
+
query: str | None = None,
|
|
20
|
+
include_versions: bool = False,
|
|
21
|
+
include_counts: bool = False,
|
|
22
|
+
limit: int | None = None,
|
|
23
|
+
offset: int = 0,
|
|
24
|
+
) -> tuple[DataModel, ...]:
|
|
25
|
+
"""Fetch data models from the Data Model Store.
|
|
26
|
+
|
|
27
|
+
'why': expose available data commons for schema selection
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
endpoints = settings.data_model_store_endpoints
|
|
31
|
+
if endpoints is None:
|
|
32
|
+
raise DataModelStoreError("data model store endpoints not configured")
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
response = await fetch_data_models(
|
|
36
|
+
base_url=endpoints.base_url,
|
|
37
|
+
api_key=settings.api_key,
|
|
38
|
+
timeout=settings.timeout,
|
|
39
|
+
query=query,
|
|
40
|
+
include_versions=include_versions,
|
|
41
|
+
include_counts=include_counts,
|
|
42
|
+
limit=limit,
|
|
43
|
+
offset=offset,
|
|
44
|
+
)
|
|
45
|
+
except httpx.TimeoutException as exc:
|
|
46
|
+
raise NetriasAPIUnavailable("data model store request timed out") from exc
|
|
47
|
+
except httpx.HTTPError as exc:
|
|
48
|
+
raise NetriasAPIUnavailable(f"data model store request failed: {exc}") from exc
|
|
49
|
+
|
|
50
|
+
body = _interpret_response(response)
|
|
51
|
+
return _parse_data_models(body)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
async def list_cdes_async(
|
|
55
|
+
settings: Settings,
|
|
56
|
+
model_key: str,
|
|
57
|
+
version: str,
|
|
58
|
+
include_description: bool = False,
|
|
59
|
+
query: str | None = None,
|
|
60
|
+
limit: int | None = None,
|
|
61
|
+
offset: int = 0,
|
|
62
|
+
) -> tuple[CDE, ...]:
|
|
63
|
+
"""Fetch CDEs for a data model version from the Data Model Store.
|
|
64
|
+
|
|
65
|
+
'why': expose available fields for a schema version
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
endpoints = settings.data_model_store_endpoints
|
|
69
|
+
if endpoints is None:
|
|
70
|
+
raise DataModelStoreError("data model store endpoints not configured")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
response = await fetch_cdes(
|
|
74
|
+
base_url=endpoints.base_url,
|
|
75
|
+
api_key=settings.api_key,
|
|
76
|
+
timeout=settings.timeout,
|
|
77
|
+
model_key=model_key,
|
|
78
|
+
version=version,
|
|
79
|
+
include_description=include_description,
|
|
80
|
+
query=query,
|
|
81
|
+
limit=limit,
|
|
82
|
+
offset=offset,
|
|
83
|
+
)
|
|
84
|
+
except httpx.TimeoutException as exc:
|
|
85
|
+
raise NetriasAPIUnavailable("data model store request timed out") from exc
|
|
86
|
+
except httpx.HTTPError as exc:
|
|
87
|
+
raise NetriasAPIUnavailable(f"data model store request failed: {exc}") from exc
|
|
88
|
+
|
|
89
|
+
body = _interpret_response(response)
|
|
90
|
+
return _parse_cdes(body)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async def list_pvs_async(
|
|
94
|
+
settings: Settings,
|
|
95
|
+
model_key: str,
|
|
96
|
+
version: str,
|
|
97
|
+
cde_key: str,
|
|
98
|
+
include_inactive: bool = False,
|
|
99
|
+
query: str | None = None,
|
|
100
|
+
limit: int | None = None,
|
|
101
|
+
offset: int = 0,
|
|
102
|
+
) -> tuple[PermissibleValue, ...]:
|
|
103
|
+
"""Fetch permissible values for a CDE from the Data Model Store.
|
|
104
|
+
|
|
105
|
+
'why': expose allowed values for validation
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
endpoints = settings.data_model_store_endpoints
|
|
109
|
+
if endpoints is None:
|
|
110
|
+
raise DataModelStoreError("data model store endpoints not configured")
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
response = await fetch_pvs(
|
|
114
|
+
base_url=endpoints.base_url,
|
|
115
|
+
api_key=settings.api_key,
|
|
116
|
+
timeout=settings.timeout,
|
|
117
|
+
model_key=model_key,
|
|
118
|
+
version=version,
|
|
119
|
+
cde_key=cde_key,
|
|
120
|
+
include_inactive=include_inactive,
|
|
121
|
+
query=query,
|
|
122
|
+
limit=limit,
|
|
123
|
+
offset=offset,
|
|
124
|
+
)
|
|
125
|
+
except httpx.TimeoutException as exc:
|
|
126
|
+
raise NetriasAPIUnavailable("data model store request timed out") from exc
|
|
127
|
+
except httpx.HTTPError as exc:
|
|
128
|
+
raise NetriasAPIUnavailable(f"data model store request failed: {exc}") from exc
|
|
129
|
+
|
|
130
|
+
body = _interpret_response(response)
|
|
131
|
+
return _parse_pvs(body)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
async def get_pv_set_async(
|
|
135
|
+
settings: Settings,
|
|
136
|
+
model_key: str,
|
|
137
|
+
version: str,
|
|
138
|
+
cde_key: str,
|
|
139
|
+
include_inactive: bool = False,
|
|
140
|
+
) -> frozenset[str]:
|
|
141
|
+
"""Return all permissible values as a set for membership testing.
|
|
142
|
+
|
|
143
|
+
'why': validation use case requires O(1) lookup; pagination is hidden
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
all_values: list[str] = []
|
|
147
|
+
offset = 0
|
|
148
|
+
page_size = 1000
|
|
149
|
+
|
|
150
|
+
while True:
|
|
151
|
+
pvs = await list_pvs_async(
|
|
152
|
+
settings=settings,
|
|
153
|
+
model_key=model_key,
|
|
154
|
+
version=version,
|
|
155
|
+
cde_key=cde_key,
|
|
156
|
+
include_inactive=include_inactive,
|
|
157
|
+
limit=page_size,
|
|
158
|
+
offset=offset,
|
|
159
|
+
)
|
|
160
|
+
all_values.extend(pv.value for pv in pvs)
|
|
161
|
+
|
|
162
|
+
if len(pvs) < page_size:
|
|
163
|
+
break
|
|
164
|
+
offset += page_size
|
|
165
|
+
|
|
166
|
+
return frozenset(all_values)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def list_data_models(
|
|
170
|
+
settings: Settings,
|
|
171
|
+
query: str | None = None,
|
|
172
|
+
include_versions: bool = False,
|
|
173
|
+
include_counts: bool = False,
|
|
174
|
+
limit: int | None = None,
|
|
175
|
+
offset: int = 0,
|
|
176
|
+
) -> tuple[DataModel, ...]:
|
|
177
|
+
"""Synchronous wrapper for list_data_models_async."""
|
|
178
|
+
|
|
179
|
+
return asyncio.run(
|
|
180
|
+
list_data_models_async(
|
|
181
|
+
settings=settings,
|
|
182
|
+
query=query,
|
|
183
|
+
include_versions=include_versions,
|
|
184
|
+
include_counts=include_counts,
|
|
185
|
+
limit=limit,
|
|
186
|
+
offset=offset,
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def list_cdes(
|
|
192
|
+
settings: Settings,
|
|
193
|
+
model_key: str,
|
|
194
|
+
version: str,
|
|
195
|
+
include_description: bool = False,
|
|
196
|
+
query: str | None = None,
|
|
197
|
+
limit: int | None = None,
|
|
198
|
+
offset: int = 0,
|
|
199
|
+
) -> tuple[CDE, ...]:
|
|
200
|
+
"""Synchronous wrapper for list_cdes_async."""
|
|
201
|
+
|
|
202
|
+
return asyncio.run(
|
|
203
|
+
list_cdes_async(
|
|
204
|
+
settings=settings,
|
|
205
|
+
model_key=model_key,
|
|
206
|
+
version=version,
|
|
207
|
+
include_description=include_description,
|
|
208
|
+
query=query,
|
|
209
|
+
limit=limit,
|
|
210
|
+
offset=offset,
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def list_pvs(
|
|
216
|
+
settings: Settings,
|
|
217
|
+
model_key: str,
|
|
218
|
+
version: str,
|
|
219
|
+
cde_key: str,
|
|
220
|
+
include_inactive: bool = False,
|
|
221
|
+
query: str | None = None,
|
|
222
|
+
limit: int | None = None,
|
|
223
|
+
offset: int = 0,
|
|
224
|
+
) -> tuple[PermissibleValue, ...]:
|
|
225
|
+
"""Synchronous wrapper for list_pvs_async."""
|
|
226
|
+
|
|
227
|
+
return asyncio.run(
|
|
228
|
+
list_pvs_async(
|
|
229
|
+
settings=settings,
|
|
230
|
+
model_key=model_key,
|
|
231
|
+
version=version,
|
|
232
|
+
cde_key=cde_key,
|
|
233
|
+
include_inactive=include_inactive,
|
|
234
|
+
query=query,
|
|
235
|
+
limit=limit,
|
|
236
|
+
offset=offset,
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_pv_set(
|
|
242
|
+
settings: Settings,
|
|
243
|
+
model_key: str,
|
|
244
|
+
version: str,
|
|
245
|
+
cde_key: str,
|
|
246
|
+
include_inactive: bool = False,
|
|
247
|
+
) -> frozenset[str]:
|
|
248
|
+
"""Synchronous wrapper for get_pv_set_async."""
|
|
249
|
+
|
|
250
|
+
return asyncio.run(
|
|
251
|
+
get_pv_set_async(
|
|
252
|
+
settings=settings,
|
|
253
|
+
model_key=model_key,
|
|
254
|
+
version=version,
|
|
255
|
+
cde_key=cde_key,
|
|
256
|
+
include_inactive=include_inactive,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _interpret_response(response: httpx.Response) -> Mapping[str, object]:
|
|
262
|
+
_raise_for_error_status(response)
|
|
263
|
+
return _parse_json_body(response)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _raise_for_error_status(response: httpx.Response) -> None:
|
|
267
|
+
if response.status_code >= 500:
|
|
268
|
+
raise NetriasAPIUnavailable(f"data model store server error: {_extract_error_message(response)}")
|
|
269
|
+
if response.status_code >= 400:
|
|
270
|
+
raise DataModelStoreError(f"data model store request failed: {_extract_error_message(response)}")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _parse_json_body(response: httpx.Response) -> Mapping[str, object]:
|
|
274
|
+
try:
|
|
275
|
+
body = response.json()
|
|
276
|
+
except Exception as exc:
|
|
277
|
+
raise DataModelStoreError(f"invalid JSON response: {exc}") from exc
|
|
278
|
+
|
|
279
|
+
if not isinstance(body, dict):
|
|
280
|
+
raise DataModelStoreError("unexpected response format: expected object")
|
|
281
|
+
|
|
282
|
+
return body
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _extract_error_message(response: httpx.Response) -> str:
|
|
286
|
+
message = _try_extract_message_from_json(response)
|
|
287
|
+
if message:
|
|
288
|
+
return message
|
|
289
|
+
if response.text:
|
|
290
|
+
return response.text[:200]
|
|
291
|
+
return f"HTTP {response.status_code}"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _try_extract_message_from_json(response: httpx.Response) -> str | None:
|
|
295
|
+
try:
|
|
296
|
+
body = response.json()
|
|
297
|
+
for key in ("message", "detail", "error", "description"):
|
|
298
|
+
if key in body and body[key]:
|
|
299
|
+
return str(body[key])
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _parse_data_models(body: Mapping[str, object]) -> tuple[DataModel, ...]:
|
|
306
|
+
items = body.get("items")
|
|
307
|
+
if not isinstance(items, list):
|
|
308
|
+
return ()
|
|
309
|
+
|
|
310
|
+
models: list[DataModel] = []
|
|
311
|
+
for item in items:
|
|
312
|
+
if not isinstance(item, dict):
|
|
313
|
+
continue
|
|
314
|
+
models.append(
|
|
315
|
+
DataModel(
|
|
316
|
+
data_commons_id=int(item.get("data_commons_id", 0)),
|
|
317
|
+
key=str(item.get("key", "")),
|
|
318
|
+
name=str(item.get("name", "")),
|
|
319
|
+
description=item.get("description") if item.get("description") else None,
|
|
320
|
+
is_active=bool(item.get("is_active", True)),
|
|
321
|
+
)
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return tuple(models)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _parse_cdes(body: Mapping[str, object]) -> tuple[CDE, ...]:
|
|
328
|
+
items = body.get("items")
|
|
329
|
+
if not isinstance(items, list):
|
|
330
|
+
return ()
|
|
331
|
+
|
|
332
|
+
cdes: list[CDE] = []
|
|
333
|
+
for item in items:
|
|
334
|
+
if not isinstance(item, dict):
|
|
335
|
+
continue
|
|
336
|
+
cdes.append(
|
|
337
|
+
CDE(
|
|
338
|
+
cde_key=str(item.get("cde_key", "")),
|
|
339
|
+
cde_id=int(item.get("cde_id", 0)),
|
|
340
|
+
cde_version_id=int(item.get("cde_version_id", 0)),
|
|
341
|
+
description=item.get("column_description") if item.get("column_description") else None,
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return tuple(cdes)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _parse_pvs(body: Mapping[str, object]) -> tuple[PermissibleValue, ...]:
|
|
349
|
+
items = body.get("items")
|
|
350
|
+
if not isinstance(items, list):
|
|
351
|
+
return ()
|
|
352
|
+
|
|
353
|
+
pvs: list[PermissibleValue] = []
|
|
354
|
+
for item in items:
|
|
355
|
+
if not isinstance(item, dict):
|
|
356
|
+
continue
|
|
357
|
+
pvs.append(
|
|
358
|
+
PermissibleValue(
|
|
359
|
+
pv_id=int(item.get("pv_id", 0)),
|
|
360
|
+
value=str(item.get("value", "")),
|
|
361
|
+
description=item.get("description") if item.get("description") else None,
|
|
362
|
+
is_active=bool(item.get("is_active", True)),
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
return tuple(pvs)
|