oehrpy 0.1.1__py3-none-any.whl → 0.2.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.
- {oehrpy-0.1.1.dist-info → oehrpy-0.2.0.dist-info}/METADATA +1 -1
- {oehrpy-0.1.1.dist-info → oehrpy-0.2.0.dist-info}/RECORD +6 -6
- openehr_sdk/client/__init__.py +6 -0
- openehr_sdk/client/ehrbase.py +179 -7
- {oehrpy-0.1.1.dist-info → oehrpy-0.2.0.dist-info}/WHEEL +0 -0
- {oehrpy-0.1.1.dist-info → oehrpy-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oehrpy
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Python SDK for openEHR with type-safe Reference Model classes, template builders, and EHRBase client
|
|
5
5
|
Project-URL: Homepage, https://github.com/platzhersh/oehrpy
|
|
6
6
|
Project-URL: Documentation, https://github.com/platzhersh/oehrpy#readme
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
openehr_sdk/__init__.py,sha256=Rzs3NFgVa-P2ljZu2-OXcgkKBrhE_mAwWf9HnnVwrB8,1295
|
|
2
2
|
openehr_sdk/aql/__init__.py,sha256=auR0SKnnnAhzRzq2nU0YZSzUPKmAzYoXXdWs9Kxzoq8,958
|
|
3
3
|
openehr_sdk/aql/builder.py,sha256=XptfUJVuMbUAa-LY-XFWxWDXFzBbMSHZStNloz2PAGo,17053
|
|
4
|
-
openehr_sdk/client/__init__.py,sha256=
|
|
5
|
-
openehr_sdk/client/ehrbase.py,sha256=
|
|
4
|
+
openehr_sdk/client/__init__.py,sha256=GTa8fnE-6tLCYGCthxF_PAVVoJ_ESVkPfVWFdlemAlE,822
|
|
5
|
+
openehr_sdk/client/ehrbase.py,sha256=ZVR7sySYfIc6aB5WZnnx3-TDnAjCT7vPFJZr61ziJww,28480
|
|
6
6
|
openehr_sdk/rm/__init__.py,sha256=yS5JAFaUuoo3D-k_Q9V78j2cBQKTW4EMK9LYEl34ZAc,457
|
|
7
7
|
openehr_sdk/rm/rm_types.py,sha256=7DLcfubGplYPIhF6byBsrnEO0ayOw_bBeJbzrDqbZ-Q,50518
|
|
8
8
|
openehr_sdk/serialization/__init__.py,sha256=aucmds3NXlq1nQrjQNZnYR6PzqT63lAcCLh3og2AtM8,748
|
|
@@ -12,7 +12,7 @@ openehr_sdk/templates/__init__.py,sha256=_9Fwua5Mw_mM5n9CaKqXql12GSQyVvqdc_-J-jx
|
|
|
12
12
|
openehr_sdk/templates/builder_generator.py,sha256=oTYcsvmwGasUfGU3zmW9O79eCFyyprO4hkGJ5SvMB5I,13679
|
|
13
13
|
openehr_sdk/templates/builders.py,sha256=CJuOrobrCNWBbEyHbVJTzGqxApZ7CAyZrhUmY9ctvWg,13665
|
|
14
14
|
openehr_sdk/templates/opt_parser.py,sha256=WzOFYUHgAYTn7hk8LWghqP2gjiG9t3LWlYpQ3-999pg,12423
|
|
15
|
-
oehrpy-0.
|
|
16
|
-
oehrpy-0.
|
|
17
|
-
oehrpy-0.
|
|
18
|
-
oehrpy-0.
|
|
15
|
+
oehrpy-0.2.0.dist-info/METADATA,sha256=l4yTTO-SCYrJRjifCxqQ4wJUEN2i5gt8-jVuC-wCqKQ,11972
|
|
16
|
+
oehrpy-0.2.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
|
|
17
|
+
oehrpy-0.2.0.dist-info/licenses/LICENSE,sha256=2RQ4UN7dGDVJh_e9NTzuaUzdMWMDsy1jgLPxKxJahus,1073
|
|
18
|
+
oehrpy-0.2.0.dist-info/RECORD,,
|
openehr_sdk/client/__init__.py
CHANGED
|
@@ -9,13 +9,16 @@ from .ehrbase import (
|
|
|
9
9
|
AuthenticationError,
|
|
10
10
|
CompositionFormat,
|
|
11
11
|
CompositionResponse,
|
|
12
|
+
CompositionVersionResponse,
|
|
12
13
|
EHRBaseClient,
|
|
13
14
|
EHRBaseConfig,
|
|
14
15
|
EHRBaseError,
|
|
15
16
|
EHRResponse,
|
|
16
17
|
NotFoundError,
|
|
18
|
+
PreconditionFailedError,
|
|
17
19
|
QueryResponse,
|
|
18
20
|
ValidationError,
|
|
21
|
+
VersionedCompositionResponse,
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
__all__ = [
|
|
@@ -24,9 +27,12 @@ __all__ = [
|
|
|
24
27
|
"EHRResponse",
|
|
25
28
|
"CompositionResponse",
|
|
26
29
|
"CompositionFormat",
|
|
30
|
+
"CompositionVersionResponse",
|
|
27
31
|
"QueryResponse",
|
|
32
|
+
"VersionedCompositionResponse",
|
|
28
33
|
"EHRBaseError",
|
|
29
34
|
"AuthenticationError",
|
|
30
35
|
"NotFoundError",
|
|
36
|
+
"PreconditionFailedError",
|
|
31
37
|
"ValidationError",
|
|
32
38
|
]
|
openehr_sdk/client/ehrbase.py
CHANGED
|
@@ -72,6 +72,12 @@ class ValidationError(EHRBaseError):
|
|
|
72
72
|
pass
|
|
73
73
|
|
|
74
74
|
|
|
75
|
+
class PreconditionFailedError(EHRBaseError):
|
|
76
|
+
"""Version conflict — the If-Match header did not match (HTTP 412)."""
|
|
77
|
+
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
|
|
75
81
|
# Response dataclasses
|
|
76
82
|
|
|
77
83
|
|
|
@@ -178,6 +184,58 @@ class TemplateResponse:
|
|
|
178
184
|
)
|
|
179
185
|
|
|
180
186
|
|
|
187
|
+
@dataclass
|
|
188
|
+
class VersionedCompositionResponse:
|
|
189
|
+
"""Response from versioned composition metadata endpoint."""
|
|
190
|
+
|
|
191
|
+
uid: str
|
|
192
|
+
owner_id: str | None = None
|
|
193
|
+
time_created: str | None = None
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def from_response(cls, data: dict[str, Any]) -> VersionedCompositionResponse:
|
|
197
|
+
"""Create from API response."""
|
|
198
|
+
uid_data = data.get("uid", {})
|
|
199
|
+
uid = uid_data.get("value", "") if isinstance(uid_data, dict) else uid_data or ""
|
|
200
|
+
owner_data = data.get("owner_id", {})
|
|
201
|
+
owner_id = owner_data.get("value", "") if isinstance(owner_data, dict) else owner_data
|
|
202
|
+
time_created_data = data.get("time_created", {})
|
|
203
|
+
time_created = (
|
|
204
|
+
time_created_data.get("value", "")
|
|
205
|
+
if isinstance(time_created_data, dict)
|
|
206
|
+
else time_created_data
|
|
207
|
+
)
|
|
208
|
+
return cls(uid=uid, owner_id=owner_id, time_created=time_created)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class CompositionVersionResponse:
|
|
213
|
+
"""Response from a specific composition version endpoint."""
|
|
214
|
+
|
|
215
|
+
version_uid: str
|
|
216
|
+
preceding_version_uid: str | None = None
|
|
217
|
+
lifecycle_state: str | None = None
|
|
218
|
+
commit_audit: dict[str, Any] | None = None
|
|
219
|
+
data: dict[str, Any] | None = None
|
|
220
|
+
|
|
221
|
+
@classmethod
|
|
222
|
+
def from_response(cls, data: dict[str, Any]) -> CompositionVersionResponse:
|
|
223
|
+
"""Create from API response."""
|
|
224
|
+
uid_data = data.get("uid", {})
|
|
225
|
+
version_uid = uid_data.get("value", "") if isinstance(uid_data, dict) else uid_data or ""
|
|
226
|
+
preceding = data.get("preceding_version_uid", {})
|
|
227
|
+
preceding_uid = preceding.get("value", "") if isinstance(preceding, dict) else preceding
|
|
228
|
+
lifecycle = data.get("lifecycle_state", {})
|
|
229
|
+
lifecycle_state = lifecycle.get("value", "") if isinstance(lifecycle, dict) else lifecycle
|
|
230
|
+
return cls(
|
|
231
|
+
version_uid=version_uid,
|
|
232
|
+
preceding_version_uid=preceding_uid or None,
|
|
233
|
+
lifecycle_state=lifecycle_state or None,
|
|
234
|
+
commit_audit=data.get("commit_audit"),
|
|
235
|
+
data=data.get("data"),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
|
|
181
239
|
@dataclass
|
|
182
240
|
class EHRBaseConfig:
|
|
183
241
|
"""Configuration for EHRBase client."""
|
|
@@ -280,6 +338,11 @@ class EHRBaseClient:
|
|
|
280
338
|
"Resource not found",
|
|
281
339
|
status_code=response.status_code,
|
|
282
340
|
)
|
|
341
|
+
if response.status_code == 412:
|
|
342
|
+
raise PreconditionFailedError(
|
|
343
|
+
"Version conflict: the preceding version UID does not match the latest version",
|
|
344
|
+
status_code=response.status_code,
|
|
345
|
+
)
|
|
283
346
|
if response.status_code == 400 or response.status_code == 422:
|
|
284
347
|
try:
|
|
285
348
|
error_data = response.json()
|
|
@@ -483,7 +546,8 @@ class EHRBaseClient:
|
|
|
483
546
|
async def update_composition(
|
|
484
547
|
self,
|
|
485
548
|
ehr_id: str,
|
|
486
|
-
|
|
549
|
+
versioned_object_uid: str,
|
|
550
|
+
preceding_version_uid: str,
|
|
487
551
|
composition: dict[str, Any],
|
|
488
552
|
template_id: str | None = None,
|
|
489
553
|
format: str | CompositionFormat = CompositionFormat.FLAT,
|
|
@@ -492,24 +556,27 @@ class EHRBaseClient:
|
|
|
492
556
|
|
|
493
557
|
Args:
|
|
494
558
|
ehr_id: The EHR ID.
|
|
495
|
-
|
|
559
|
+
versioned_object_uid: The composition's UUID (used in the request path).
|
|
560
|
+
preceding_version_uid: Full version string (e.g. ``uuid::domain::1``),
|
|
561
|
+
sent as the ``If-Match`` header for optimistic concurrency.
|
|
496
562
|
composition: The updated composition data.
|
|
497
563
|
template_id: Template ID.
|
|
498
564
|
format: Composition format.
|
|
499
565
|
|
|
500
566
|
Returns:
|
|
501
567
|
CompositionResponse with updated composition.
|
|
568
|
+
|
|
569
|
+
Raises:
|
|
570
|
+
PreconditionFailedError: If the preceding version UID does not match
|
|
571
|
+
the latest version (HTTP 412).
|
|
572
|
+
NotFoundError: If the composition has been deleted (HTTP 404).
|
|
502
573
|
"""
|
|
503
574
|
format_str = format.value if isinstance(format, CompositionFormat) else format
|
|
504
575
|
|
|
505
|
-
# Extract versioned object UID (uuid::system::version -> uuid::system)
|
|
506
|
-
uid_parts = composition_uid.split("::")
|
|
507
|
-
versioned_object_uid = "::".join(uid_parts[:2]) if len(uid_parts) >= 2 else composition_uid
|
|
508
|
-
|
|
509
576
|
headers = {
|
|
510
577
|
"Prefer": "return=representation",
|
|
511
578
|
"Content-Type": "application/json",
|
|
512
|
-
"If-Match":
|
|
579
|
+
"If-Match": preceding_version_uid,
|
|
513
580
|
}
|
|
514
581
|
|
|
515
582
|
params = {}
|
|
@@ -548,6 +615,111 @@ class EHRBaseClient:
|
|
|
548
615
|
)
|
|
549
616
|
self._handle_response(response)
|
|
550
617
|
|
|
618
|
+
# Composition Versioning Operations
|
|
619
|
+
|
|
620
|
+
async def get_composition_at_time(
|
|
621
|
+
self,
|
|
622
|
+
ehr_id: str,
|
|
623
|
+
versioned_object_uid: str,
|
|
624
|
+
version_at_time: str,
|
|
625
|
+
format: str | CompositionFormat = CompositionFormat.CANONICAL,
|
|
626
|
+
) -> CompositionResponse:
|
|
627
|
+
"""Get a composition as it existed at a specific point in time.
|
|
628
|
+
|
|
629
|
+
Args:
|
|
630
|
+
ehr_id: The EHR ID.
|
|
631
|
+
versioned_object_uid: The composition's UUID.
|
|
632
|
+
version_at_time: ISO 8601 timestamp.
|
|
633
|
+
format: Desired response format.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
CompositionResponse with the composition at that time.
|
|
637
|
+
"""
|
|
638
|
+
format_str = format.value if isinstance(format, CompositionFormat) else format
|
|
639
|
+
|
|
640
|
+
params: dict[str, str] = {"version_at_time": version_at_time}
|
|
641
|
+
if format_str:
|
|
642
|
+
params["format"] = format_str
|
|
643
|
+
|
|
644
|
+
response = await self.client.get(
|
|
645
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{versioned_object_uid}",
|
|
646
|
+
params=params,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
data = self._handle_response(response)
|
|
650
|
+
return CompositionResponse.from_response(data, ehr_id)
|
|
651
|
+
|
|
652
|
+
async def get_versioned_composition(
|
|
653
|
+
self,
|
|
654
|
+
ehr_id: str,
|
|
655
|
+
versioned_object_uid: str,
|
|
656
|
+
) -> VersionedCompositionResponse:
|
|
657
|
+
"""Get versioned composition metadata.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
ehr_id: The EHR ID.
|
|
661
|
+
versioned_object_uid: The composition's UUID.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
VersionedCompositionResponse with metadata (UID, owner ID, time created).
|
|
665
|
+
"""
|
|
666
|
+
response = await self.client.get(
|
|
667
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/versioned_composition/{versioned_object_uid}",
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
data = self._handle_response(response)
|
|
671
|
+
return VersionedCompositionResponse.from_response(data)
|
|
672
|
+
|
|
673
|
+
async def get_composition_version(
|
|
674
|
+
self,
|
|
675
|
+
ehr_id: str,
|
|
676
|
+
versioned_object_uid: str,
|
|
677
|
+
version_uid: str,
|
|
678
|
+
) -> CompositionVersionResponse:
|
|
679
|
+
"""Get a specific version of a composition.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
ehr_id: The EHR ID.
|
|
683
|
+
versioned_object_uid: The composition's UUID.
|
|
684
|
+
version_uid: The specific version UID (e.g. ``uuid::domain::1``).
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
CompositionVersionResponse with full version and audit metadata.
|
|
688
|
+
"""
|
|
689
|
+
response = await self.client.get(
|
|
690
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version/{version_uid}",
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
data = self._handle_response(response)
|
|
694
|
+
return CompositionVersionResponse.from_response(data)
|
|
695
|
+
|
|
696
|
+
async def list_composition_versions(
|
|
697
|
+
self,
|
|
698
|
+
ehr_id: str,
|
|
699
|
+
versioned_object_uid: str,
|
|
700
|
+
) -> list[CompositionVersionResponse]:
|
|
701
|
+
"""List all versions of a composition.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
ehr_id: The EHR ID.
|
|
705
|
+
versioned_object_uid: The composition's UUID.
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
List of CompositionVersionResponse with version descriptors.
|
|
709
|
+
"""
|
|
710
|
+
response = await self.client.get(
|
|
711
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version",
|
|
712
|
+
)
|
|
713
|
+
|
|
714
|
+
data = self._handle_response(response)
|
|
715
|
+
if isinstance(data, list):
|
|
716
|
+
return [CompositionVersionResponse.from_response(v) for v in data]
|
|
717
|
+
# Some servers return the list under a key
|
|
718
|
+
versions = data.get("versions", data.get("items", []))
|
|
719
|
+
if isinstance(versions, list):
|
|
720
|
+
return [CompositionVersionResponse.from_response(v) for v in versions]
|
|
721
|
+
return []
|
|
722
|
+
|
|
551
723
|
# Query Operations
|
|
552
724
|
|
|
553
725
|
async def query(
|
|
File without changes
|
|
File without changes
|