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.
Files changed (89) hide show
  1. {oehrpy-0.1.1 → oehrpy-0.2.0}/CHANGELOG.md +15 -0
  2. {oehrpy-0.1.1 → oehrpy-0.2.0}/PKG-INFO +1 -1
  3. oehrpy-0.2.0/docs/prd/PRD-0002-composition-lifecycle.md +135 -0
  4. oehrpy-0.2.0/docs/prd/PRD-0003-audit-and-contributions.md +102 -0
  5. oehrpy-0.2.0/docs/prd/PRD-0004-dynamic-composition-builders.md +107 -0
  6. oehrpy-0.2.0/docs/prd/PRD-0005-ehr-management-and-query-extensions.md +180 -0
  7. {oehrpy-0.1.1 → oehrpy-0.2.0}/pyproject.toml +1 -1
  8. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/client/__init__.py +6 -0
  9. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/client/ehrbase.py +179 -7
  10. oehrpy-0.2.0/tests/integration/test_composition_lifecycle.py +208 -0
  11. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_compositions.py +5 -1
  12. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_round_trip.py +4 -1
  13. oehrpy-0.2.0/tests/test_composition_lifecycle.py +206 -0
  14. {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/ci.yml +0 -0
  15. {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/publish.yml +0 -0
  16. {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows/release.yml +0 -0
  17. {oehrpy-0.1.1 → oehrpy-0.2.0}/.github/workflows-temp/fetch-webtemplate.yml +0 -0
  18. {oehrpy-0.1.1 → oehrpy-0.2.0}/.gitignore +0 -0
  19. {oehrpy-0.1.1 → oehrpy-0.2.0}/CLAUDE.md +0 -0
  20. {oehrpy-0.1.1 → oehrpy-0.2.0}/CONTRIBUTING.md +0 -0
  21. {oehrpy-0.1.1 → oehrpy-0.2.0}/INTEGRATION_TEST_ANALYSIS.md +0 -0
  22. {oehrpy-0.1.1 → oehrpy-0.2.0}/INTEGRATION_TEST_STATUS.md +0 -0
  23. {oehrpy-0.1.1 → oehrpy-0.2.0}/LICENSE +0 -0
  24. {oehrpy-0.1.1 → oehrpy-0.2.0}/README.md +0 -0
  25. {oehrpy-0.1.1 → oehrpy-0.2.0}/docker-compose.local.yml +0 -0
  26. {oehrpy-0.1.1 → oehrpy-0.2.0}/docker-compose.yml +0 -0
  27. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/FLAT_FORMAT_VERSIONS.md +0 -0
  28. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/RESEARCH_FLAT_FORMAT_DISCOURSE.md +0 -0
  29. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0000-record-architecture-decisions.md +0 -0
  30. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0001-odin-parsing-and-rm-1.1.0-support.md +0 -0
  31. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0002-integration-testing-with-ehrbase.md +0 -0
  32. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0003-pre-commit-hooks-for-code-quality.md +0 -0
  33. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/adr/0004-python-semantic-release-for-release-automation.md +0 -0
  34. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/brand-kit.html +0 -0
  35. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/docs.html +0 -0
  36. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/ehrbase-issues/001-flat-format-documentation-gap.md +0 -0
  37. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/ehrbase-issues/README.md +0 -0
  38. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/flat-format-learnings.md +0 -0
  39. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/index.html +0 -0
  40. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/integration-testing-journey.md +0 -0
  41. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/prd/PRD-0000-python-openehr-sdk.md +0 -0
  42. {oehrpy-0.1.1 → oehrpy-0.2.0}/docs/prd/PRD-0001-odin-parser.md +0 -0
  43. {oehrpy-0.1.1 → oehrpy-0.2.0}/examples/generate_builder_from_opt.py +0 -0
  44. {oehrpy-0.1.1 → oehrpy-0.2.0}/fetch_webtemplate_from_ci.sh +0 -0
  45. {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/__init__.py +0 -0
  46. {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/bmm_parser.py +0 -0
  47. {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/generate_rm_1_1_0.py +0 -0
  48. {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/json_schema_parser.py +0 -0
  49. {oehrpy-0.1.1 → oehrpy-0.2.0}/generator/pydantic_generator.py +0 -0
  50. {oehrpy-0.1.1 → oehrpy-0.2.0}/init-db.sql +0 -0
  51. {oehrpy-0.1.1 → oehrpy-0.2.0}/init-postgres.sql +0 -0
  52. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/__init__.py +0 -0
  53. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/aql/__init__.py +0 -0
  54. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/aql/builder.py +0 -0
  55. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/rm/__init__.py +0 -0
  56. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/rm/rm_types.py +0 -0
  57. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/__init__.py +0 -0
  58. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/canonical.py +0 -0
  59. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/serialization/flat.py +0 -0
  60. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/__init__.py +0 -0
  61. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/builder_generator.py +0 -0
  62. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/builders.py +0 -0
  63. {oehrpy-0.1.1 → oehrpy-0.2.0}/src/openehr_sdk/templates/opt_parser.py +0 -0
  64. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_header.md.j2 +0 -0
  65. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_init.md.j2 +0 -0
  66. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changelog_update.md.j2 +0 -0
  67. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/changes.md.j2 +0 -0
  68. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/first_release.md.j2 +0 -0
  69. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/macros.md.j2 +0 -0
  70. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/unreleased_changes.md.j2 +0 -0
  71. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.components/versioned_changes.md.j2 +0 -0
  72. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/.release_notes.md.j2 +0 -0
  73. {oehrpy-0.1.1 → oehrpy-0.2.0}/templates/CHANGELOG.md.j2 +0 -0
  74. {oehrpy-0.1.1 → oehrpy-0.2.0}/test_flat_submission.sh +0 -0
  75. {oehrpy-0.1.1 → oehrpy-0.2.0}/test_web_template.sh +0 -0
  76. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/__init__.py +0 -0
  77. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/fixtures/vital_signs.opt +0 -0
  78. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/__init__.py +0 -0
  79. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/conftest.py +0 -0
  80. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_aql_queries.py +0 -0
  81. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_canonical_format.py +0 -0
  82. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/integration/test_ehr_operations.py +0 -0
  83. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_aql.py +0 -0
  84. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_flat.py +0 -0
  85. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_rm_types.py +0 -0
  86. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_serialization.py +0 -0
  87. {oehrpy-0.1.1 → oehrpy-0.2.0}/tests/test_templates.py +0 -0
  88. {oehrpy-0.1.1 → oehrpy-0.2.0}/web_template.json +0 -0
  89. {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.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
@@ -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.1.1"
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
  ]