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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oehrpy
3
- Version: 0.1.1
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=swqiwHCbwyIvq_newgkUEfGq6FvHV_CFfU_fYw2rw08,626
5
- openehr_sdk/client/ehrbase.py,sha256=m3Ml8MZ0YBzY6oWOWgpxc2aqWytfTth35E7e3FVWr-k,22242
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.1.1.dist-info/METADATA,sha256=oxbOaOJ4OQ0btd6x7NvUsWlz6Leo5N1KdeuJpWjb80g,11972
16
- oehrpy-0.1.1.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
17
- oehrpy-0.1.1.dist-info/licenses/LICENSE,sha256=2RQ4UN7dGDVJh_e9NTzuaUzdMWMDsy1jgLPxKxJahus,1073
18
- oehrpy-0.1.1.dist-info/RECORD,,
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,,
@@ -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
  ]
@@ -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
- composition_uid: str,
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
- composition_uid: The composition UID (versioned).
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": composition_uid,
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