oehrpy 0.1.0__tar.gz → 0.1.1__tar.gz
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.0 → oehrpy-0.1.1}/CHANGELOG.md +9 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/PKG-INFO +1 -1
- {oehrpy-0.1.0 → oehrpy-0.1.1}/pyproject.toml +1 -1
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/aql/builder.py +9 -9
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/client/ehrbase.py +54 -18
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/rm/rm_types.py +1 -1
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/test_aql_queries.py +2 -2
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/test_compositions.py +2 -1
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/test_round_trip.py +24 -6
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/test_aql.py +2 -2
- {oehrpy-0.1.0 → oehrpy-0.1.1}/.github/workflows/ci.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/.github/workflows/publish.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/.github/workflows/release.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/.github/workflows-temp/fetch-webtemplate.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/.gitignore +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/CLAUDE.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/CONTRIBUTING.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/INTEGRATION_TEST_ANALYSIS.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/INTEGRATION_TEST_STATUS.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/LICENSE +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/README.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docker-compose.local.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docker-compose.yml +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/FLAT_FORMAT_VERSIONS.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/RESEARCH_FLAT_FORMAT_DISCOURSE.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0000-record-architecture-decisions.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0001-odin-parsing-and-rm-1.1.0-support.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0002-integration-testing-with-ehrbase.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0003-pre-commit-hooks-for-code-quality.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0004-python-semantic-release-for-release-automation.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/brand-kit.html +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/docs.html +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/ehrbase-issues/001-flat-format-documentation-gap.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/ehrbase-issues/README.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/flat-format-learnings.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/index.html +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/integration-testing-journey.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/prd/PRD-0000-python-openehr-sdk.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/docs/prd/PRD-0001-odin-parser.md +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/examples/generate_builder_from_opt.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/fetch_webtemplate_from_ci.sh +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/generator/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/generator/bmm_parser.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/generator/generate_rm_1_1_0.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/generator/json_schema_parser.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/generator/pydantic_generator.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/init-db.sql +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/init-postgres.sql +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/aql/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/client/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/rm/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/serialization/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/serialization/canonical.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/serialization/flat.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/templates/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/templates/builder_generator.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/templates/builders.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/src/openehr_sdk/templates/opt_parser.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/changelog_header.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/changelog_init.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/changelog_update.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/changes.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/first_release.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/macros.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/unreleased_changes.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.components/versioned_changes.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/.release_notes.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/templates/CHANGELOG.md.j2 +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/test_flat_submission.sh +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/test_web_template.sh +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/fixtures/vital_signs.opt +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/__init__.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/conftest.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/test_canonical_format.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/integration/test_ehr_operations.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/test_flat.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/test_rm_types.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/test_serialization.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/tests/test_templates.py +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/web_template.json +0 -0
- {oehrpy-0.1.0 → oehrpy-0.1.1}/web_template_formatted.json +0 -0
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.1.1 (2026-01-31)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Resolve integration test failures against EHRBase 2.0 (#18)
|
|
9
|
+
([#18](https://github.com/platzhersh/oehrpy/pull/18),
|
|
10
|
+
[`ca63003`](https://github.com/platzhersh/oehrpy/commit/ca6300326200e5a497f895c6a6f0ce67d4fecffc))
|
|
11
|
+
|
|
12
|
+
|
|
4
13
|
## v0.1.0 (2026-01-31)
|
|
5
14
|
|
|
6
15
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: oehrpy
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oehrpy"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1"
|
|
8
8
|
description = "A Python SDK for openEHR with type-safe Reference Model classes, template builders, and EHRBase client"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -66,7 +66,7 @@ class FromClause:
|
|
|
66
66
|
|
|
67
67
|
# EHR clause
|
|
68
68
|
if self.ehr_id_param:
|
|
69
|
-
parts.append(f"EHR {self.ehr_alias}[ehr_id/value
|
|
69
|
+
parts.append(f"EHR {self.ehr_alias}[ehr_id/value=${self.ehr_id_param}]")
|
|
70
70
|
else:
|
|
71
71
|
parts.append(f"EHR {self.ehr_alias}")
|
|
72
72
|
|
|
@@ -178,7 +178,7 @@ class AQLBuilder:
|
|
|
178
178
|
... .select("c/context/start_time/value", alias="time")
|
|
179
179
|
... .from_ehr("e")
|
|
180
180
|
... .contains_composition("c", "IDCR - Vital Signs Encounter.v1")
|
|
181
|
-
... .where("e/ehr_id/value =
|
|
181
|
+
... .where("e/ehr_id/value = $ehr_id")
|
|
182
182
|
... .order_by("c/context/start_time/value", descending=True)
|
|
183
183
|
... .limit(10)
|
|
184
184
|
... .build()
|
|
@@ -269,7 +269,7 @@ class AQLBuilder:
|
|
|
269
269
|
|
|
270
270
|
if archetype_id:
|
|
271
271
|
param_name = f"{alias}_archetype_id"
|
|
272
|
-
containment = f"{rm_type} {alias}[archetype_id/value
|
|
272
|
+
containment = f"{rm_type} {alias}[archetype_id/value=${param_name}]"
|
|
273
273
|
self._parameters[param_name] = archetype_id
|
|
274
274
|
else:
|
|
275
275
|
containment = f"{rm_type} {alias}"
|
|
@@ -299,7 +299,7 @@ class AQLBuilder:
|
|
|
299
299
|
self._from_clause = FromClause()
|
|
300
300
|
|
|
301
301
|
if archetype_id:
|
|
302
|
-
containment = f"COMPOSITION {alias}[archetype_id/value
|
|
302
|
+
containment = f"COMPOSITION {alias}[archetype_id/value=${alias}_archetype_id]"
|
|
303
303
|
self._parameters[f"{alias}_archetype_id"] = archetype_id
|
|
304
304
|
else:
|
|
305
305
|
containment = f"COMPOSITION {alias}"
|
|
@@ -309,7 +309,7 @@ class AQLBuilder:
|
|
|
309
309
|
# Add template_id filter as parameterized WHERE clause
|
|
310
310
|
if template_id:
|
|
311
311
|
self._where_clause.add(
|
|
312
|
-
f"{alias}/archetype_details/template_id/value =
|
|
312
|
+
f"{alias}/archetype_details/template_id/value = ${template_id_param}"
|
|
313
313
|
)
|
|
314
314
|
self._parameters[template_id_param] = template_id
|
|
315
315
|
|
|
@@ -373,7 +373,7 @@ class AQLBuilder:
|
|
|
373
373
|
Returns:
|
|
374
374
|
Self for method chaining.
|
|
375
375
|
"""
|
|
376
|
-
return self.where(f"{ehr_alias}/ehr_id/value =
|
|
376
|
+
return self.where(f"{ehr_alias}/ehr_id/value = ${param_name}")
|
|
377
377
|
|
|
378
378
|
def where_template(
|
|
379
379
|
self,
|
|
@@ -391,7 +391,7 @@ class AQLBuilder:
|
|
|
391
391
|
Returns:
|
|
392
392
|
Self for method chaining.
|
|
393
393
|
"""
|
|
394
|
-
self.where(f"{composition_alias}/archetype_details/template_id/value =
|
|
394
|
+
self.where(f"{composition_alias}/archetype_details/template_id/value = ${param_name}")
|
|
395
395
|
if template_id:
|
|
396
396
|
self._parameters[param_name] = template_id
|
|
397
397
|
return self
|
|
@@ -417,10 +417,10 @@ class AQLBuilder:
|
|
|
417
417
|
Self for method chaining.
|
|
418
418
|
"""
|
|
419
419
|
if start:
|
|
420
|
-
self.where(f"{path} >=
|
|
420
|
+
self.where(f"{path} >= ${start_param}")
|
|
421
421
|
self._parameters[start_param] = start
|
|
422
422
|
if end:
|
|
423
|
-
self.where(f"{path} <=
|
|
423
|
+
self.where(f"{path} <= ${end_param}")
|
|
424
424
|
self._parameters[end_param] = end
|
|
425
425
|
return self
|
|
426
426
|
|
|
@@ -110,13 +110,25 @@ class CompositionResponse:
|
|
|
110
110
|
@classmethod
|
|
111
111
|
def from_response(cls, data: dict[str, Any], ehr_id: str | None = None) -> CompositionResponse:
|
|
112
112
|
"""Create from API response."""
|
|
113
|
+
# Try canonical format first (uid is a dict with "value" key)
|
|
113
114
|
uid_data = data.get("uid")
|
|
114
115
|
uid = uid_data.get("value", "") if isinstance(uid_data, dict) else uid_data or ""
|
|
116
|
+
|
|
117
|
+
template_id = data.get("archetype_details", {}).get("template_id", {}).get("value")
|
|
118
|
+
archetype_id = data.get("archetype_details", {}).get("archetype_id", {}).get("value")
|
|
119
|
+
|
|
120
|
+
# For FLAT format responses, extract uid from */_uid key
|
|
121
|
+
if not uid:
|
|
122
|
+
for key, value in data.items():
|
|
123
|
+
if key.endswith("/_uid") and isinstance(value, str):
|
|
124
|
+
uid = value
|
|
125
|
+
break
|
|
126
|
+
|
|
115
127
|
return cls(
|
|
116
128
|
uid=uid,
|
|
117
129
|
ehr_id=ehr_id,
|
|
118
|
-
template_id=
|
|
119
|
-
archetype_id=
|
|
130
|
+
template_id=template_id,
|
|
131
|
+
archetype_id=archetype_id,
|
|
120
132
|
composition=data,
|
|
121
133
|
)
|
|
122
134
|
|
|
@@ -318,24 +330,37 @@ class EHRBaseClient:
|
|
|
318
330
|
Returns:
|
|
319
331
|
EHRResponse with the created EHR details.
|
|
320
332
|
"""
|
|
321
|
-
headers = {}
|
|
333
|
+
headers: dict[str, str] = {"Prefer": "return=representation"}
|
|
322
334
|
if ehr_id:
|
|
323
|
-
headers["Prefer"] = "return=representation"
|
|
324
335
|
response = await self.client.put(
|
|
325
336
|
f"/rest/openehr/v1/ehr/{ehr_id}",
|
|
326
337
|
headers=headers,
|
|
327
338
|
)
|
|
328
339
|
else:
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
340
|
+
body = None
|
|
341
|
+
if subject_id and subject_namespace:
|
|
342
|
+
body = {
|
|
343
|
+
"_type": "EHR_STATUS",
|
|
344
|
+
"archetype_node_id": "openEHR-EHR-EHR_STATUS.generic.v1",
|
|
345
|
+
"name": {"value": "EHR Status"},
|
|
346
|
+
"subject": {
|
|
347
|
+
"external_ref": {
|
|
348
|
+
"id": {
|
|
349
|
+
"_type": "GENERIC_ID",
|
|
350
|
+
"value": subject_id,
|
|
351
|
+
"scheme": "id_scheme",
|
|
352
|
+
},
|
|
353
|
+
"namespace": subject_namespace,
|
|
354
|
+
"type": "PERSON",
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
"is_modifiable": True,
|
|
358
|
+
"is_queryable": True,
|
|
359
|
+
}
|
|
335
360
|
response = await self.client.post(
|
|
336
361
|
"/rest/openehr/v1/ehr",
|
|
337
362
|
headers=headers,
|
|
338
|
-
|
|
363
|
+
json=body,
|
|
339
364
|
)
|
|
340
365
|
|
|
341
366
|
data = self._handle_response(response)
|
|
@@ -439,9 +464,17 @@ class EHRBaseClient:
|
|
|
439
464
|
"""
|
|
440
465
|
format_str = format.value if isinstance(format, CompositionFormat) else format
|
|
441
466
|
|
|
467
|
+
# Extract versioned object UID (uuid::system::version -> uuid::system)
|
|
468
|
+
uid_parts = composition_uid.split("::")
|
|
469
|
+
versioned_object_uid = "::".join(uid_parts[:2]) if len(uid_parts) >= 2 else composition_uid
|
|
470
|
+
|
|
471
|
+
params: dict[str, str] = {}
|
|
472
|
+
if format_str:
|
|
473
|
+
params["format"] = format_str
|
|
474
|
+
|
|
442
475
|
response = await self.client.get(
|
|
443
|
-
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{
|
|
444
|
-
params=
|
|
476
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{versioned_object_uid}",
|
|
477
|
+
params=params if params else None,
|
|
445
478
|
)
|
|
446
479
|
|
|
447
480
|
data = self._handle_response(response)
|
|
@@ -469,8 +502,9 @@ class EHRBaseClient:
|
|
|
469
502
|
"""
|
|
470
503
|
format_str = format.value if isinstance(format, CompositionFormat) else format
|
|
471
504
|
|
|
472
|
-
# Extract
|
|
473
|
-
|
|
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
|
|
474
508
|
|
|
475
509
|
headers = {
|
|
476
510
|
"Prefer": "return=representation",
|
|
@@ -485,7 +519,7 @@ class EHRBaseClient:
|
|
|
485
519
|
params["format"] = format_str
|
|
486
520
|
|
|
487
521
|
response = await self.client.put(
|
|
488
|
-
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{
|
|
522
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{versioned_object_uid}",
|
|
489
523
|
json=composition,
|
|
490
524
|
headers=headers,
|
|
491
525
|
params=params if params else None,
|
|
@@ -505,10 +539,12 @@ class EHRBaseClient:
|
|
|
505
539
|
ehr_id: The EHR ID.
|
|
506
540
|
composition_uid: The composition UID.
|
|
507
541
|
"""
|
|
508
|
-
|
|
542
|
+
# Extract versioned object UID (uuid::system::version -> uuid::system)
|
|
543
|
+
uid_parts = composition_uid.split("::")
|
|
544
|
+
versioned_object_uid = "::".join(uid_parts[:2]) if len(uid_parts) >= 2 else composition_uid
|
|
509
545
|
|
|
510
546
|
response = await self.client.delete(
|
|
511
|
-
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{
|
|
547
|
+
f"/rest/openehr/v1/ehr/{ehr_id}/composition/{versioned_object_uid}",
|
|
512
548
|
)
|
|
513
549
|
self._handle_response(response)
|
|
514
550
|
|
|
@@ -1030,7 +1030,7 @@ class HISTORY(BaseModel):
|
|
|
1030
1030
|
archetype_details: ARCHETYPED | None = None
|
|
1031
1031
|
feeder_audit: FEEDER_AUDIT | None = None
|
|
1032
1032
|
links: list[LINK] | None = None
|
|
1033
|
-
origin: DV_DATE_TIME | None
|
|
1033
|
+
origin: DV_DATE_TIME | None = None
|
|
1034
1034
|
period: DV_DURATION | None = None
|
|
1035
1035
|
duration: DV_DURATION | None = None
|
|
1036
1036
|
summary: Any | None = None
|
|
@@ -115,7 +115,7 @@ class TestAQLQueries:
|
|
|
115
115
|
o/data[at0001]/events[at0006]/data[at0003]/items[at0005]/value/magnitude AS diastolic
|
|
116
116
|
FROM EHR e[ehr_id/value='{test_ehr}']
|
|
117
117
|
CONTAINS COMPOSITION c
|
|
118
|
-
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.
|
|
118
|
+
CONTAINS OBSERVATION o[openEHR-EHR-OBSERVATION.blood_pressure.v1]
|
|
119
119
|
"""
|
|
120
120
|
|
|
121
121
|
result = await ehrbase_client.query(aql)
|
|
@@ -152,7 +152,7 @@ class TestAQLQueries:
|
|
|
152
152
|
)
|
|
153
153
|
|
|
154
154
|
# Query using parameters
|
|
155
|
-
aql = "SELECT c FROM EHR e CONTAINS COMPOSITION c WHERE e/ehr_id/value =
|
|
155
|
+
aql = "SELECT c FROM EHR e CONTAINS COMPOSITION c WHERE e/ehr_id/value = $ehr_id"
|
|
156
156
|
|
|
157
157
|
result = await ehrbase_client.query(
|
|
158
158
|
aql=aql,
|
|
@@ -32,8 +32,8 @@ class TestCompositionOperations:
|
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
assert composition.uid is not None
|
|
35
|
+
assert "::" in composition.uid # Versioned UID format
|
|
35
36
|
assert composition.ehr_id == test_ehr
|
|
36
|
-
assert composition.template_id == vital_signs_template
|
|
37
37
|
|
|
38
38
|
async def test_create_composition_all_vitals(
|
|
39
39
|
self,
|
|
@@ -208,6 +208,7 @@ class TestCompositionOperations:
|
|
|
208
208
|
await ehrbase_client.get_composition(
|
|
209
209
|
ehr_id=test_ehr,
|
|
210
210
|
composition_uid=fake_uid,
|
|
211
|
+
format=CompositionFormat.FLAT,
|
|
211
212
|
)
|
|
212
213
|
|
|
213
214
|
async def test_create_composition_without_template_fails(
|
|
@@ -4,7 +4,7 @@ from datetime import datetime, timezone
|
|
|
4
4
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
|
-
from openehr_sdk.client import CompositionFormat, EHRBaseClient
|
|
7
|
+
from openehr_sdk.client import CompositionFormat, EHRBaseClient, EHRBaseError
|
|
8
8
|
from openehr_sdk.templates import VitalSignsBuilder
|
|
9
9
|
|
|
10
10
|
|
|
@@ -76,7 +76,7 @@ class TestRoundTripWorkflows:
|
|
|
76
76
|
spo2 = 96
|
|
77
77
|
|
|
78
78
|
builder = VitalSignsBuilder(composer_name="Dr. Workflow Test")
|
|
79
|
-
builder.add_temperature(temperature, unit="
|
|
79
|
+
builder.add_temperature(temperature, unit="°C")
|
|
80
80
|
builder.add_oxygen_saturation(spo2=spo2)
|
|
81
81
|
flat_data = builder.build()
|
|
82
82
|
|
|
@@ -196,7 +196,7 @@ class TestRoundTripWorkflows:
|
|
|
196
196
|
|
|
197
197
|
# Query all compositions
|
|
198
198
|
aql = f"""
|
|
199
|
-
SELECT c/uid/value AS uid
|
|
199
|
+
SELECT c/uid/value AS uid, c/context/start_time/value AS start_time
|
|
200
200
|
FROM EHR e[ehr_id/value='{test_ehr}']
|
|
201
201
|
CONTAINS COMPOSITION c
|
|
202
202
|
ORDER BY c/context/start_time/value DESC
|
|
@@ -221,9 +221,27 @@ class TestRoundTripWorkflows:
|
|
|
221
221
|
vital_signs_opt_path,
|
|
222
222
|
) -> None:
|
|
223
223
|
"""Test uploading template, then creating composition with it."""
|
|
224
|
-
# Upload template
|
|
224
|
+
# Upload template (may already exist from other tests)
|
|
225
225
|
template_xml = vital_signs_opt_path.read_text(encoding="utf-8")
|
|
226
|
-
|
|
226
|
+
try:
|
|
227
|
+
template_response = await ehrbase_client.upload_template(template_xml)
|
|
228
|
+
except EHRBaseError as e:
|
|
229
|
+
if e.status_code == 409:
|
|
230
|
+
# Template already exists, extract ID from XML
|
|
231
|
+
import xml.etree.ElementTree as ET
|
|
232
|
+
|
|
233
|
+
root = ET.fromstring(template_xml)
|
|
234
|
+
ns = "{http://schemas.openehr.org/v1}"
|
|
235
|
+
elem = root.find(f".//{ns}template_id/{ns}value")
|
|
236
|
+
if elem is None:
|
|
237
|
+
elem = root.find(".//template_id/value")
|
|
238
|
+
from openehr_sdk.client.ehrbase import TemplateResponse
|
|
239
|
+
|
|
240
|
+
template_response = TemplateResponse(
|
|
241
|
+
template_id=elem.text if elem is not None else ""
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
raise
|
|
227
245
|
|
|
228
246
|
assert template_response.template_id is not None
|
|
229
247
|
|
|
@@ -245,7 +263,7 @@ class TestRoundTripWorkflows:
|
|
|
245
263
|
)
|
|
246
264
|
|
|
247
265
|
assert composition.uid is not None
|
|
248
|
-
assert
|
|
266
|
+
assert "::" in composition.uid
|
|
249
267
|
|
|
250
268
|
async def test_canonical_to_flat_format_conversion(
|
|
251
269
|
self,
|
|
@@ -87,7 +87,7 @@ class TestAQLBuilder:
|
|
|
87
87
|
query = AQLBuilder().select("c").from_ehr().contains_composition().where_ehr_id().build()
|
|
88
88
|
sql = query.to_string()
|
|
89
89
|
|
|
90
|
-
assert "e/ehr_id/value =
|
|
90
|
+
assert "e/ehr_id/value = $ehr_id" in sql
|
|
91
91
|
|
|
92
92
|
def test_order_by(self) -> None:
|
|
93
93
|
"""Test ORDER BY clause."""
|
|
@@ -163,7 +163,7 @@ class TestAQLBuilder:
|
|
|
163
163
|
|
|
164
164
|
assert "CONTAINS COMPOSITION c" in sql
|
|
165
165
|
assert "CONTAINS OBSERVATION o" in sql
|
|
166
|
-
assert "
|
|
166
|
+
assert "$o_archetype_id" in sql # Parameterized
|
|
167
167
|
assert query.parameters["o_archetype_id"] == "openEHR-EHR-OBSERVATION.blood_pressure.v1"
|
|
168
168
|
|
|
169
169
|
def test_full_query(self) -> None:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{oehrpy-0.1.0 → oehrpy-0.1.1}/docs/adr/0004-python-semantic-release-for-release-automation.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|