oehrpy 0.1.1__tar.gz → 0.2.0__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.1 → oehrpy-0.2.0}/CHANGELOG.md +15 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/PKG-INFO +1 -1
- oehrpy-0.2.0/docs/prd/PRD-0002-composition-lifecycle.md +135 -0
- oehrpy-0.2.0/docs/prd/PRD-0003-audit-and-contributions.md +102 -0
- oehrpy-0.2.0/docs/prd/PRD-0004-dynamic-composition-builders.md +107 -0
- oehrpy-0.2.0/docs/prd/PRD-0005-ehr-management-and-query-extensions.md +180 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/pyproject.toml +1 -1
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/client/__init__.py +6 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/client/ehrbase.py +179 -7
- oehrpy-0.2.0/tests/integration/test_composition_lifecycle.py +208 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_compositions.py +5 -1
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_round_trip.py +4 -1
- oehrpy-0.2.0/tests/test_composition_lifecycle.py +206 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/ci.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/publish.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/release.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows-temp/fetch-webtemplate.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/.gitignore +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/CLAUDE.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/CONTRIBUTING.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/INTEGRATION_TEST_ANALYSIS.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/INTEGRATION_TEST_STATUS.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/LICENSE +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/README.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docker-compose.local.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docker-compose.yml +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/FLAT_FORMAT_VERSIONS.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/RESEARCH_FLAT_FORMAT_DISCOURSE.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0000-record-architecture-decisions.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0001-odin-parsing-and-rm-1.1.0-support.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0002-integration-testing-with-ehrbase.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0003-pre-commit-hooks-for-code-quality.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0004-python-semantic-release-for-release-automation.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/brand-kit.html +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/docs.html +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/ehrbase-issues/001-flat-format-documentation-gap.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/ehrbase-issues/README.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/flat-format-learnings.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/index.html +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/integration-testing-journey.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/prd/PRD-0000-python-openehr-sdk.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/prd/PRD-0001-odin-parser.md +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/examples/generate_builder_from_opt.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/fetch_webtemplate_from_ci.sh +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/bmm_parser.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/generate_rm_1_1_0.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/json_schema_parser.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/pydantic_generator.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/init-db.sql +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/init-postgres.sql +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/aql/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/aql/builder.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/rm/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/rm/rm_types.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/canonical.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/flat.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/builder_generator.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/builders.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/opt_parser.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_header.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_init.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_update.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changes.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/first_release.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/macros.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/unreleased_changes.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/versioned_changes.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.release_notes.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/CHANGELOG.md.j2 +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/test_flat_submission.sh +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/test_web_template.sh +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/fixtures/vital_signs.opt +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/__init__.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/conftest.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_aql_queries.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_canonical_format.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_ehr_operations.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_aql.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_flat.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_rm_types.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_serialization.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_templates.py +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/web_template.json +0 -0
- {oehrpy-0.1.1 → oehrpy-0.2.0}/web_template_formatted.json +0 -0
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.2.0 (2026-02-04)
|
|
5
|
+
|
|
6
|
+
### Documentation
|
|
7
|
+
|
|
8
|
+
- Add PRDs for composition lifecycle, audit, builders, and EHR management (#19)
|
|
9
|
+
([#19](https://github.com/platzhersh/oehrpy/pull/19),
|
|
10
|
+
[`1542994`](https://github.com/platzhersh/oehrpy/commit/1542994dd9c5319b268041ab6f114d45377a3791))
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- Add composition versioning and update operations (PRD-0002) (#20)
|
|
15
|
+
([#20](https://github.com/platzhersh/oehrpy/pull/20),
|
|
16
|
+
[`eadb0c9`](https://github.com/platzhersh/oehrpy/commit/eadb0c9f7714aa4bbfb0e7fce3cbd8e6481a7ce9))
|
|
17
|
+
|
|
18
|
+
|
|
4
19
|
## v0.1.1 (2026-01-31)
|
|
5
20
|
|
|
6
21
|
### Bug Fixes
|
|
@@ -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
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# PRD-0002: Composition Lifecycle (Update & Versioning)
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Date:** 2026-01-31
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Owner:** Open CIS Project
|
|
7
|
+
**Priority:** P0 (Critical)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Executive Summary
|
|
12
|
+
|
|
13
|
+
Extend the oehrpy SDK to support the full composition lifecycle: updating/amending existing compositions and retrieving prior versions. These capabilities are fundamental to clinical data management — without them, records are write-once and audit history is inaccessible.
|
|
14
|
+
|
|
15
|
+
This PRD covers two tightly coupled gaps identified in the oehrpy gap analysis:
|
|
16
|
+
1. **Composition Update/Amendment** — `PUT /ehr/{ehr_id}/composition/{versioned_object_uid}`
|
|
17
|
+
2. **Composition Versioning** — retrieving compositions at a point in time, listing version history, and accessing the versioned composition container
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Problem Statement
|
|
22
|
+
|
|
23
|
+
oehrpy currently supports creating and deleting compositions but provides no way to:
|
|
24
|
+
|
|
25
|
+
1. **Amend a composition** — Clinicians routinely correct errors, add addenda, or update status fields on existing records. The openEHR model supports this through versioning: each update creates a new version while preserving the original.
|
|
26
|
+
|
|
27
|
+
2. **Retrieve previous versions** — Regulatory requirements (e.g., HIPAA, GDPR health data provisions) mandate access to the full history of changes to a clinical record. Without version retrieval, there is no way to answer "what did this record say last Tuesday?"
|
|
28
|
+
|
|
29
|
+
3. **List version history** — Governance workflows need to enumerate all versions of a composition to display audit trails, compare changes, or roll back.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Requirements
|
|
34
|
+
|
|
35
|
+
### Functional Requirements
|
|
36
|
+
|
|
37
|
+
#### FR-1: Update Composition
|
|
38
|
+
|
|
39
|
+
| Field | Detail |
|
|
40
|
+
|---|---|
|
|
41
|
+
| Endpoint | `PUT /ehr/{ehr_id}/composition/{versioned_object_uid}` |
|
|
42
|
+
| SDK method | `EHRBaseClient.update_composition(ehr_id, versioned_object_uid, preceding_version_uid, template_id, composition, format)` |
|
|
43
|
+
| Input | EHR ID, `versioned_object_uid` (the composition's UUID, used in the request path), `preceding_version_uid` (the full version string, e.g. `uuid::domain::1`, sent in the `If-Match` header per RFC 7232), template ID, updated composition body, format (CANONICAL / FLAT / STRUCTURED) |
|
|
44
|
+
| Output | Updated composition with new version UID |
|
|
45
|
+
| Headers | `If-Match: {preceding_version_uid}` for optimistic concurrency |
|
|
46
|
+
|
|
47
|
+
- Must support all three composition formats (CANONICAL, FLAT, STRUCTURED)
|
|
48
|
+
- Must return the new version UID on success
|
|
49
|
+
- Must raise a clear error on version conflict (HTTP 412 Precondition Failed, per RFC 7232 `If-Match` semantics)
|
|
50
|
+
- Must raise a clear error if the composition has been deleted (HTTP 404)
|
|
51
|
+
|
|
52
|
+
#### FR-2: Get Composition at Version
|
|
53
|
+
|
|
54
|
+
| Field | Detail |
|
|
55
|
+
|---|---|
|
|
56
|
+
| Endpoint | `GET /ehr/{ehr_id}/composition/{versioned_object_uid}` with `version_at_time` parameter |
|
|
57
|
+
| SDK method | `EHRBaseClient.get_composition_at_time(ehr_id, versioned_object_uid, version_at_time, format)` |
|
|
58
|
+
| Input | EHR ID, `versioned_object_uid` (the composition's UUID), ISO 8601 timestamp, optional format |
|
|
59
|
+
| Output | Composition as it existed at the given point in time |
|
|
60
|
+
|
|
61
|
+
#### FR-3: Get Versioned Composition
|
|
62
|
+
|
|
63
|
+
| Field | Detail |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Endpoint | `GET /ehr/{ehr_id}/versioned_composition/{versioned_object_uid}` |
|
|
66
|
+
| SDK method | `EHRBaseClient.get_versioned_composition(ehr_id, versioned_object_uid)` |
|
|
67
|
+
| Output | Versioned composition metadata (UID, owner ID, time created) |
|
|
68
|
+
|
|
69
|
+
#### FR-4: Get Version by Version UID
|
|
70
|
+
|
|
71
|
+
| Field | Detail |
|
|
72
|
+
|---|---|
|
|
73
|
+
| Endpoint | `GET /ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version/{version_uid}` |
|
|
74
|
+
| SDK method | `EHRBaseClient.get_composition_version(ehr_id, versioned_object_uid, version_uid)` |
|
|
75
|
+
| Output | Specific version of the composition with full audit metadata |
|
|
76
|
+
|
|
77
|
+
#### FR-5: List Composition Versions
|
|
78
|
+
|
|
79
|
+
| Field | Detail |
|
|
80
|
+
|---|---|
|
|
81
|
+
| Endpoint | `GET /ehr/{ehr_id}/versioned_composition/{versioned_object_uid}/version` |
|
|
82
|
+
| SDK method | `EHRBaseClient.list_composition_versions(ehr_id, versioned_object_uid)` |
|
|
83
|
+
| Output | List of version descriptors (version UID, commit audit, lifecycle state) |
|
|
84
|
+
|
|
85
|
+
### Non-Functional Requirements
|
|
86
|
+
|
|
87
|
+
- **NFR-1**: All new methods must be async (consistent with existing client)
|
|
88
|
+
- **NFR-2**: Optimistic concurrency via `If-Match` must be handled transparently — the caller passes the preceding version UID and the SDK sets the header
|
|
89
|
+
- **NFR-3**: Version conflict errors (HTTP 412 Precondition Failed) must raise a typed `PreconditionFailedError` exception
|
|
90
|
+
- **NFR-4**: Full test coverage with both unit tests (mocked HTTP) and integration tests against EHRBase
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## API Design
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
# Update a composition
|
|
98
|
+
new_version_uid = await client.update_composition(
|
|
99
|
+
ehr_id=ehr_id,
|
|
100
|
+
versioned_object_uid=versioned_object_uid,
|
|
101
|
+
preceding_version_uid=preceding_version_uid,
|
|
102
|
+
template_id="vital_signs",
|
|
103
|
+
composition=updated_flat_data,
|
|
104
|
+
format=CompositionFormat.FLAT,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Get composition as it was at a specific time
|
|
108
|
+
old_composition = await client.get_composition_at_time(
|
|
109
|
+
ehr_id=ehr_id,
|
|
110
|
+
versioned_object_uid=versioned_object_uid,
|
|
111
|
+
version_at_time="2026-01-15T10:00:00Z",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# List all versions
|
|
115
|
+
versions = await client.list_composition_versions(
|
|
116
|
+
ehr_id=ehr_id,
|
|
117
|
+
versioned_object_uid=versioned_object_uid,
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Testing Strategy
|
|
124
|
+
|
|
125
|
+
- **Unit tests**: Mock HTTP responses for each endpoint, verify request construction (headers, URL, body)
|
|
126
|
+
- **Integration tests**: Create → Update → Retrieve version history against EHRBase 2.0
|
|
127
|
+
- **Edge cases**: Version conflict (concurrent update), deleted composition, invalid timestamp format
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Success Criteria
|
|
132
|
+
|
|
133
|
+
1. All five SDK methods implemented and passing integration tests against EHRBase 2.0
|
|
134
|
+
2. Version conflict handling works correctly with `If-Match` / HTTP 412
|
|
135
|
+
3. Round-trip test: create → update → retrieve both versions → verify content differs
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# PRD-0003: Audit & Contributions
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Date:** 2026-01-31
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Owner:** Open CIS Project
|
|
7
|
+
**Priority:** P1 (High)
|
|
8
|
+
**Depends on:** PRD-0002 (Composition Lifecycle)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Executive Summary
|
|
13
|
+
|
|
14
|
+
Add support for openEHR Contributions — atomic changesets that group one or more composition changes with audit metadata. Contributions provide the answer to "who changed what, when, and why" and are required for regulatory compliance in any production clinical system.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Problem Statement
|
|
19
|
+
|
|
20
|
+
Every composition change in openEHR is wrapped in a Contribution, but oehrpy currently ignores this layer entirely. This means:
|
|
21
|
+
|
|
22
|
+
1. **No audit trail** — There is no way to retrieve the provenance of a change (committer identity, timestamp, change description)
|
|
23
|
+
2. **No atomic multi-composition commits** — Clinical workflows sometimes require multiple compositions to be committed as a single logical unit (e.g., a medication order and a corresponding encounter note). Without Contributions, each composition is an independent request with no transactional grouping.
|
|
24
|
+
3. **Regulatory gaps** — Healthcare regulations require demonstrable audit trails. Without Contribution access, an oehrpy-based system cannot satisfy audit requirements.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
### Functional Requirements
|
|
31
|
+
|
|
32
|
+
#### FR-1: Create Contribution
|
|
33
|
+
|
|
34
|
+
| Field | Detail |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Endpoint | `POST /ehr/{ehr_id}/contribution` |
|
|
37
|
+
| SDK method | `EHRBaseClient.create_contribution(ehr_id, contribution)` |
|
|
38
|
+
| Input | EHR ID, Contribution object containing: list of version changes (each with composition + change type), audit details (committer, description, time) |
|
|
39
|
+
| Output | Contribution UID |
|
|
40
|
+
|
|
41
|
+
Change types per the openEHR spec:
|
|
42
|
+
- `creation` — new composition
|
|
43
|
+
- `amendment` — update to existing composition
|
|
44
|
+
- `modification` — structural change
|
|
45
|
+
- `deleted` — logical deletion
|
|
46
|
+
|
|
47
|
+
#### FR-2: Get Contribution
|
|
48
|
+
|
|
49
|
+
| Field | Detail |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Endpoint | `GET /ehr/{ehr_id}/contribution/{contribution_uid}` |
|
|
52
|
+
| SDK method | `EHRBaseClient.get_contribution(ehr_id, contribution_uid)` |
|
|
53
|
+
| Output | Contribution object with audit metadata and list of version references |
|
|
54
|
+
|
|
55
|
+
### Model Requirements
|
|
56
|
+
|
|
57
|
+
#### MR-1: Contribution Model
|
|
58
|
+
|
|
59
|
+
A `Contribution` Pydantic model (or use the existing RM `CONTRIBUTION` class) with:
|
|
60
|
+
- `uid`: Contribution identifier
|
|
61
|
+
- `versions`: List of object references to the versioned objects included
|
|
62
|
+
- `audit`: `AUDIT_DETAILS` with committer, time_committed, change_type, description
|
|
63
|
+
|
|
64
|
+
#### MR-2: Contribution Builder
|
|
65
|
+
|
|
66
|
+
A helper to construct Contribution request bodies:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
contribution = (
|
|
70
|
+
ContributionBuilder(ehr_id=ehr_id)
|
|
71
|
+
.add_creation(template_id="vital_signs", composition=vitals_data)
|
|
72
|
+
.add_amendment(uid=existing_uid, template_id="vital_signs", composition=updated_data)
|
|
73
|
+
.set_audit(committer="Dr. Smith", description="Routine vitals and correction")
|
|
74
|
+
.build()
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
uid = await client.create_contribution(ehr_id, contribution)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Non-Functional Requirements
|
|
81
|
+
|
|
82
|
+
- **NFR-1**: Async methods consistent with existing client
|
|
83
|
+
- **NFR-2**: Audit details must support both minimal (auto-filled by server) and explicit (caller-provided) modes
|
|
84
|
+
- **NFR-3**: Full test coverage including multi-composition atomic commits
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Testing Strategy
|
|
89
|
+
|
|
90
|
+
- **Unit tests**: Mock HTTP, verify Contribution request body structure matches openEHR spec
|
|
91
|
+
- **Integration tests**:
|
|
92
|
+
- Create a contribution with a single composition creation, retrieve it, verify audit fields
|
|
93
|
+
- Create a contribution with multiple operations (create + amend), verify atomicity
|
|
94
|
+
- Verify contribution UID appears in composition version metadata
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Success Criteria
|
|
99
|
+
|
|
100
|
+
1. `create_contribution()` and `get_contribution()` implemented and passing integration tests
|
|
101
|
+
2. `ContributionBuilder` provides a fluent API for multi-operation contributions
|
|
102
|
+
3. Audit metadata (committer, timestamp, description) round-trips correctly
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# PRD-0004: Dynamic Composition Builders
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Date:** 2026-01-31
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Owner:** Open CIS Project
|
|
7
|
+
**Priority:** P1 (High)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Executive Summary
|
|
12
|
+
|
|
13
|
+
Unlock oehrpy's existing OPT parser and template generator so that developers can generate type-safe composition builders from any OPT file — at build time or at runtime. Ship a small set of pre-built builders for common clinical document types alongside the generic capability.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Problem Statement
|
|
18
|
+
|
|
19
|
+
oehrpy ships a single hand-crafted `VitalSignsBuilder`. The OPT parser (`opt_parser.py`) and builder generator (`builder_generator.py`) exist in the codebase and can already produce builder classes from OPT files, but:
|
|
20
|
+
|
|
21
|
+
1. **The capability is undiscoverable** — There is no public API or CLI command to generate a builder from an OPT file. The only example is a script in `examples/`.
|
|
22
|
+
2. **No runtime generation** — Developers must run a script, capture the output, and paste it into their project. There is no way to load a template and get a builder object at runtime.
|
|
23
|
+
3. **Only one pre-built builder** — `VitalSignsBuilder` covers one template. Common clinical document types (medications, problem lists, lab results) have no builder support.
|
|
24
|
+
4. **No validation against template constraints** — Generated builders set FLAT paths but do not enforce cardinality, mandatory fields, or terminology bindings from the OPT.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
### Functional Requirements
|
|
31
|
+
|
|
32
|
+
#### FR-1: Runtime Builder Factory
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from openehr_sdk.templates import CompositionBuilder
|
|
36
|
+
|
|
37
|
+
# Load from a local OPT file
|
|
38
|
+
builder = CompositionBuilder.from_opt("path/to/medication_order.opt")
|
|
39
|
+
|
|
40
|
+
# Use the builder with FLAT format
|
|
41
|
+
composition = (
|
|
42
|
+
builder
|
|
43
|
+
.set("medication/order:0/medication_item|value", "Amoxicillin")
|
|
44
|
+
.set("medication/order:0/dose|magnitude", 500)
|
|
45
|
+
.set("medication/order:0/dose|unit", "mg")
|
|
46
|
+
.build()
|
|
47
|
+
)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
- Parse the OPT at runtime and return a builder instance with template-aware path helpers
|
|
51
|
+
- Builder must produce valid FLAT format output
|
|
52
|
+
|
|
53
|
+
#### FR-2: CLI / Script Generation (improve existing)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Generate a builder module from an OPT file
|
|
57
|
+
python -m openehr_sdk.templates.generate path/to/template.opt --output my_builder.py
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- Wrap the existing `builder_generator.py` in a proper CLI entry point
|
|
61
|
+
- Generated code should be a self-contained Python module that can be imported
|
|
62
|
+
|
|
63
|
+
#### FR-3: Pre-built Builders
|
|
64
|
+
|
|
65
|
+
Ship builders for commonly used openEHR templates:
|
|
66
|
+
|
|
67
|
+
| Builder | Template |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `VitalSignsBuilder` | (already exists) |
|
|
70
|
+
| `MedicationOrderBuilder` | Medication order / prescription |
|
|
71
|
+
| `ProblemListBuilder` | Problem / diagnosis list |
|
|
72
|
+
| `LabResultBuilder` | Laboratory result report |
|
|
73
|
+
|
|
74
|
+
Pre-built builders should be generated from OPT files included in the repository under `templates/`.
|
|
75
|
+
|
|
76
|
+
#### FR-4: Template Constraint Validation (optional / stretch)
|
|
77
|
+
|
|
78
|
+
Builders optionally validate values against OPT constraints:
|
|
79
|
+
- Required fields raise an error on `build()` if missing
|
|
80
|
+
- Cardinality constraints (max occurrences) enforced on indexed paths
|
|
81
|
+
- Terminology bindings validated for coded fields
|
|
82
|
+
|
|
83
|
+
### Non-Functional Requirements
|
|
84
|
+
|
|
85
|
+
- **NFR-1**: Runtime builder creation from an OPT file must complete in under 500ms for typical templates
|
|
86
|
+
- **NFR-2**: Generated builder code must pass ruff and mypy checks
|
|
87
|
+
- **NFR-3**: Pre-built builders must be importable from `openehr_sdk.templates.builders`
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Testing Strategy
|
|
92
|
+
|
|
93
|
+
- **Unit tests**:
|
|
94
|
+
- `from_opt()` produces a builder with correct paths for the Vital Signs OPT (already in repo)
|
|
95
|
+
- CLI generation produces importable, lint-clean Python code
|
|
96
|
+
- Pre-built builders produce valid FLAT output
|
|
97
|
+
- **Integration tests**: Compositions built with generated builders are accepted by EHRBase 2.0
|
|
98
|
+
- **Round-trip**: Generate builder → build composition → upload → retrieve → verify content
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Success Criteria
|
|
103
|
+
|
|
104
|
+
1. `CompositionBuilder.from_opt()` works for any valid OPT 1.4 file
|
|
105
|
+
2. CLI entry point generates importable builder modules
|
|
106
|
+
3. At least 3 pre-built builders ship alongside `VitalSignsBuilder`
|
|
107
|
+
4. All generated compositions pass EHRBase validation in integration tests
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# PRD-0005: EHR Management & Query Extensions
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Date:** 2026-01-31
|
|
5
|
+
**Status:** Draft
|
|
6
|
+
**Owner:** Open CIS Project
|
|
7
|
+
**Priority:** P2 (Medium)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Executive Summary
|
|
12
|
+
|
|
13
|
+
Round out the oehrpy SDK's REST API coverage by adding three independent feature groups:
|
|
14
|
+
|
|
15
|
+
1. **EHR Directory** — Folder-based organization of compositions within an EHR
|
|
16
|
+
2. **EHR Status Updates** — Modify EHR metadata (subject link, modifiable flag, active/inactive)
|
|
17
|
+
3. **Stored Queries** — Register, retrieve, and execute named AQL queries on the server
|
|
18
|
+
|
|
19
|
+
These are medium-value, low-to-medium effort additions that complete the SDK's coverage of the openEHR REST API.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Problem Statement
|
|
24
|
+
|
|
25
|
+
After PRDs 0002–0004, three areas of the openEHR REST API remain uncovered:
|
|
26
|
+
|
|
27
|
+
1. **No folder organization** — All compositions in an EHR exist as a flat list. Clinical systems typically organize records by episode, department, or encounter type using the EHR Directory (FOLDER structure). Without this, applications must implement their own organization layer outside of openEHR.
|
|
28
|
+
|
|
29
|
+
2. **No EHR status management** — Once an EHR is created, its metadata cannot be modified. There is no way to link a subject after creation, mark an EHR as non-modifiable (locked), or deactivate it.
|
|
30
|
+
|
|
31
|
+
3. **No stored query support** — Every AQL query must be sent in full on each request. Production systems benefit from registering commonly used queries once and executing them by name with parameters, reducing payload size and enabling server-side query optimization.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
### Feature Group 1: EHR Directory
|
|
38
|
+
|
|
39
|
+
#### FR-1.1: Create / Update Directory
|
|
40
|
+
|
|
41
|
+
| Field | Detail |
|
|
42
|
+
|---|---|
|
|
43
|
+
| Endpoint | `PUT /ehr/{ehr_id}/directory` |
|
|
44
|
+
| SDK method | `EHRBaseClient.update_directory(ehr_id, directory, preceding_version_uid=None)` |
|
|
45
|
+
| Input | EHR ID, FOLDER structure (name, folders, items), optional `preceding_version_uid` (the version string sent in the `If-Match` header per RFC 7232; required for updates, omitted on first creation) |
|
|
46
|
+
| Output | Updated directory with new version UID |
|
|
47
|
+
|
|
48
|
+
#### FR-1.2: Get Directory
|
|
49
|
+
|
|
50
|
+
| Field | Detail |
|
|
51
|
+
|---|---|
|
|
52
|
+
| Endpoint | `GET /ehr/{ehr_id}/directory` |
|
|
53
|
+
| SDK method | `EHRBaseClient.get_directory(ehr_id, version_at_time=None, path=None)` |
|
|
54
|
+
| Input | EHR ID, optional timestamp, optional sub-path |
|
|
55
|
+
| Output | FOLDER structure |
|
|
56
|
+
|
|
57
|
+
#### FR-1.3: Delete Directory
|
|
58
|
+
|
|
59
|
+
| Field | Detail |
|
|
60
|
+
|---|---|
|
|
61
|
+
| Endpoint | `DELETE /ehr/{ehr_id}/directory` |
|
|
62
|
+
| SDK method | `EHRBaseClient.delete_directory(ehr_id, preceding_version_uid)` |
|
|
63
|
+
| Input | EHR ID, preceding version UID |
|
|
64
|
+
| Output | None (HTTP 204) |
|
|
65
|
+
|
|
66
|
+
#### FR-1.4: Directory Builder
|
|
67
|
+
|
|
68
|
+
Helper for constructing FOLDER hierarchies:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
directory = (
|
|
72
|
+
DirectoryBuilder()
|
|
73
|
+
.add_folder("episodes", items=[composition_ref_1])
|
|
74
|
+
.add_folder("encounters/2026-01", items=[composition_ref_2, composition_ref_3])
|
|
75
|
+
.build()
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
await client.update_directory(ehr_id, directory, preceding_version_uid=dir_version_uid)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Feature Group 2: EHR Status Updates
|
|
82
|
+
|
|
83
|
+
#### FR-2.1: Get EHR Status
|
|
84
|
+
|
|
85
|
+
| Field | Detail |
|
|
86
|
+
|---|---|
|
|
87
|
+
| Endpoint | `GET /ehr/{ehr_id}/ehr_status` |
|
|
88
|
+
| SDK method | `EHRBaseClient.get_ehr_status(ehr_id)` |
|
|
89
|
+
| Output | EHR_STATUS object (subject, is_modifiable, is_queryable) |
|
|
90
|
+
|
|
91
|
+
#### FR-2.2: Update EHR Status
|
|
92
|
+
|
|
93
|
+
| Field | Detail |
|
|
94
|
+
|---|---|
|
|
95
|
+
| Endpoint | `PUT /ehr/{ehr_id}/ehr_status` |
|
|
96
|
+
| SDK method | `EHRBaseClient.update_ehr_status(ehr_id, status, preceding_version_uid)` |
|
|
97
|
+
| Input | EHR ID, updated EHR_STATUS, preceding version UID (If-Match) |
|
|
98
|
+
| Output | Updated EHR_STATUS with new version UID |
|
|
99
|
+
|
|
100
|
+
Common use cases:
|
|
101
|
+
- Link a subject (patient) to an EHR after anonymous creation
|
|
102
|
+
- Mark an EHR as non-modifiable (locked for legal hold)
|
|
103
|
+
- Set `is_queryable` to false to exclude from AQL queries
|
|
104
|
+
|
|
105
|
+
### Feature Group 3: Stored Queries
|
|
106
|
+
|
|
107
|
+
#### FR-3.1: Register Query
|
|
108
|
+
|
|
109
|
+
| Field | Detail |
|
|
110
|
+
|---|---|
|
|
111
|
+
| Endpoint | `PUT /definition/query/{qualified_query_name}/{version}` |
|
|
112
|
+
| SDK method | `EHRBaseClient.register_query(name, version, aql, query_type="AQL")` |
|
|
113
|
+
| Input | Qualified name (e.g., `org.example::vitals_latest`), version, AQL string |
|
|
114
|
+
| Output | Query definition metadata |
|
|
115
|
+
|
|
116
|
+
#### FR-3.2: Get Query Definition
|
|
117
|
+
|
|
118
|
+
| Field | Detail |
|
|
119
|
+
|---|---|
|
|
120
|
+
| Endpoint | `GET /definition/query/{qualified_query_name}/{version}` |
|
|
121
|
+
| SDK method | `EHRBaseClient.get_query(name, version=None)` |
|
|
122
|
+
| Output | Stored query definition (name, version, AQL text, saved timestamp) |
|
|
123
|
+
|
|
124
|
+
#### FR-3.3: List Stored Queries
|
|
125
|
+
|
|
126
|
+
| Field | Detail |
|
|
127
|
+
|---|---|
|
|
128
|
+
| Endpoint | `GET /definition/query` |
|
|
129
|
+
| SDK method | `EHRBaseClient.list_queries()` |
|
|
130
|
+
| Output | List of registered query definitions |
|
|
131
|
+
|
|
132
|
+
#### FR-3.4: Execute Stored Query
|
|
133
|
+
|
|
134
|
+
| Field | Detail |
|
|
135
|
+
|---|---|
|
|
136
|
+
| Endpoint | `GET /query/{qualified_query_name}/{version}` |
|
|
137
|
+
| SDK method | `EHRBaseClient.execute_stored_query(name, version=None, params=None, fetch=None, offset=None)` |
|
|
138
|
+
| Input | Query name, optional version, optional parameters dict, optional pagination |
|
|
139
|
+
| Output | AQL result set (same format as ad-hoc query execution) |
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
# Register a reusable query
|
|
143
|
+
await client.register_query(
|
|
144
|
+
name="org.example::latest_bp",
|
|
145
|
+
version="1.0.0",
|
|
146
|
+
aql="SELECT c FROM COMPOSITION c WHERE c/archetype_details/template_id/value = 'vital_signs' ORDER BY c/context/start_time DESC LIMIT 1",
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Execute by name with parameters
|
|
150
|
+
result = await client.execute_stored_query(
|
|
151
|
+
name="org.example::latest_bp",
|
|
152
|
+
version="1.0.0",
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Non-Functional Requirements
|
|
157
|
+
|
|
158
|
+
- **NFR-1**: All methods async, consistent with existing client
|
|
159
|
+
- **NFR-2**: Directory operations must use `If-Match` for optimistic concurrency (same pattern as PRD-0002)
|
|
160
|
+
- **NFR-3**: Stored query names must follow the openEHR qualified name format (`{namespace}::{query-name}`)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Testing Strategy
|
|
165
|
+
|
|
166
|
+
- **Unit tests**: Mock HTTP for all endpoints, verify request/response mapping
|
|
167
|
+
- **Integration tests**:
|
|
168
|
+
- Directory: Create directory → add compositions → retrieve by path → delete
|
|
169
|
+
- EHR Status: Create EHR → update status → verify changes persist
|
|
170
|
+
- Stored Queries: Register → list → execute → verify results match ad-hoc query
|
|
171
|
+
- **Edge cases**: Directory version conflicts, updating locked EHR, executing non-existent stored query
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Success Criteria
|
|
176
|
+
|
|
177
|
+
1. All three feature groups implemented with full unit test coverage
|
|
178
|
+
2. Integration tests passing against EHRBase 2.0
|
|
179
|
+
3. Directory builder provides a usable API for constructing folder hierarchies
|
|
180
|
+
4. Stored queries round-trip correctly (register → execute → same results as ad-hoc)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "oehrpy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
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"
|
|
@@ -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
|
]
|