Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion openmed/clinical/exporters/fhir/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
from __future__ import annotations

from .bundle import to_bundle
from .references import deterministic_fullurl

__all__ = ["to_bundle"]
__all__ = ["deterministic_fullurl", "to_bundle"]
23 changes: 8 additions & 15 deletions openmed/clinical/exporters/fhir/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,24 @@
so there are no dangling internal references;
* for ``transaction``/``batch`` bundles, each entry carries the ``request``
block (``method``/``url``) a server needs to create the resource.
* exporters may pre-compute deterministic ``urn:uuid`` references via
``deterministic_fullurl(doc_id, index)``; those references survive
Bundle assembly unchanged, while literal ``"ResourceType/id"``
references continue to be rewritten when their targets are present
in the Bundle;

The assembler is purely mechanical: it never synthesises resources (a Patient
removed by de-identification stays absent) and does not validate profiles.
"""

from __future__ import annotations

import uuid
from typing import Any, Mapping, Sequence

from .references import deterministic_fullurl

__all__ = ["to_bundle"]

# Fixed namespace so the generated ``urn:uuid`` values are reproducible across
# runs and machines (uuid5 is a deterministic hash of namespace + name).
_BUNDLE_NAMESPACE = uuid.uuid5(
uuid.NAMESPACE_URL,
"https://openmed.ai/fhir/bundle",
)

# Bundle types whose entries must carry a ``request`` block.
_REQUEST_BUNDLE_TYPES = frozenset({"transaction", "batch"})
Expand Down Expand Up @@ -75,7 +75,7 @@ def to_bundle(
f"resource at index {index} is not a FHIR resource "
"(missing 'resourceType')"
)
urns.append(_stable_urn(doc_id, index))
urns.append(deterministic_fullurl(doc_id, index))

# Map ``"ResourceType/id"`` -> ``fullUrl`` so references can be resolved
# against the resources that are actually present in this Bundle.
Expand All @@ -99,13 +99,6 @@ def to_bundle(
return {"resourceType": "Bundle", "type": bundle_type, "entry": entries}


def _stable_urn(doc_id: str, index: int) -> str:
"""Return a reproducible ``urn:uuid`` for ``doc_id`` at ``index``."""

name = f"{doc_id}:{index}"
return f"urn:uuid:{uuid.uuid5(_BUNDLE_NAMESPACE, name)}"


def _rewrite_references(node: Any, reference_map: Mapping[str, str]) -> Any:
"""Deep-copy ``node``, rewriting in-Bundle references to their ``fullUrl``.

Expand Down
26 changes: 26 additions & 0 deletions openmed/clinical/exporters/fhir/references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Deterministic FHIR Bundle reference helpers.

Use :func:`deterministic_fullurl` when an exporter needs to pre-compute the
``urn:uuid`` fullUrl that :func:`openmed.clinical.exporters.fhir.to_bundle`
will assign to a resource at a known position. The helper shares the Bundle
assembler's namespace and seed format, so exporter-provided references created
with it survive Bundle assembly unchanged.
"""

from __future__ import annotations

import uuid

__all__ = ["deterministic_fullurl"]

_BUNDLE_NAMESPACE = uuid.uuid5(
uuid.NAMESPACE_URL,
"https://openmed.ai/fhir/bundle",
)


def deterministic_fullurl(doc_id: str, index: int) -> str:
"""Return the deterministic Bundle fullUrl for ``doc_id`` and ``index``."""

name = f"{doc_id}:{index}"
return f"urn:uuid:{uuid.uuid5(_BUNDLE_NAMESPACE, name)}"
103 changes: 103 additions & 0 deletions tests/unit/clinical/test_fhir_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""Tests for deterministic FHIR reference helpers."""

from openmed.clinical.exporters.fhir import (
deterministic_fullurl as exported_deterministic_fullurl,
)
from openmed.clinical.exporters.fhir import to_bundle
from openmed.clinical.exporters.fhir.references import deterministic_fullurl


def test_helper_matches_bundle_fullurl():
"""Helper should reproduce the Bundle assembler fullUrl exactly."""

bundle = to_bundle(
[{"resourceType": "Patient", "id": "pat1"}],
doc_id="doc-1",
)

assert bundle["entry"][0]["fullUrl"] == deterministic_fullurl(
"doc-1",
0,
)


def test_helper_is_reexported_from_fhir_package():
assert exported_deterministic_fullurl("doc-1", 0) == deterministic_fullurl(
"doc-1",
0,
)


def test_helper_preserves_legacy_uuid_seed():
assert (
deterministic_fullurl("doc-1", 0)
== "urn:uuid:44a61302-7fe7-537c-a5a5-965a5e3ef526"
)


def test_precomputed_urn_reference_survives_assembly():
"""Exporter-provided deterministic URNs should remain unchanged."""

obs_ref = deterministic_fullurl("doc-1", 0)

resources = [
{
"resourceType": "Observation",
"id": "obs1",
},
{
"resourceType": "DiagnosticReport",
"id": "dr1",
"result": [{"reference": obs_ref}],
},
]

bundle = to_bundle(resources, doc_id="doc-1")

report = bundle["entry"][1]["resource"]

assert report["result"][0]["reference"] == obs_ref


def test_bundle_output_remains_byte_stable():
resources = [
{
"resourceType": "Observation",
"id": "obs1",
"status": "final",
},
{
"resourceType": "DiagnosticReport",
"id": "dr1",
"status": "final",
"result": [{"reference": "Observation/obs1"}],
},
]

assert to_bundle(resources, doc_id="doc-1") == {
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "urn:uuid:44a61302-7fe7-537c-a5a5-965a5e3ef526",
"resource": {
"resourceType": "Observation",
"id": "obs1",
"status": "final",
},
"request": {"method": "POST", "url": "Observation"},
},
{
"fullUrl": "urn:uuid:b410d1fe-83a1-5a60-955d-51919aab54ae",
"resource": {
"resourceType": "DiagnosticReport",
"id": "dr1",
"status": "final",
"result": [
{"reference": ("urn:uuid:44a61302-7fe7-537c-a5a5-965a5e3ef526")}
],
},
"request": {"method": "POST", "url": "DiagnosticReport"},
},
],
}