messagefoundry 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- messagefoundry/__init__.py +108 -0
- messagefoundry/__main__.py +1155 -0
- messagefoundry/api/__init__.py +27 -0
- messagefoundry/api/app.py +1581 -0
- messagefoundry/api/approvals.py +184 -0
- messagefoundry/api/auth_models.py +211 -0
- messagefoundry/api/auth_routes.py +655 -0
- messagefoundry/api/field_authz.py +96 -0
- messagefoundry/api/models.py +374 -0
- messagefoundry/api/security.py +247 -0
- messagefoundry/api/tls.py +47 -0
- messagefoundry/auth/__init__.py +39 -0
- messagefoundry/auth/data/common_passwords.NOTICE +13 -0
- messagefoundry/auth/data/common_passwords.txt +10000 -0
- messagefoundry/auth/identity.py +71 -0
- messagefoundry/auth/ldap.py +264 -0
- messagefoundry/auth/notifications.py +68 -0
- messagefoundry/auth/passwords.py +53 -0
- messagefoundry/auth/permissions.py +120 -0
- messagefoundry/auth/policy.py +153 -0
- messagefoundry/auth/ratelimit.py +55 -0
- messagefoundry/auth/service.py +1323 -0
- messagefoundry/auth/tokens.py +26 -0
- messagefoundry/auth/totp.py +174 -0
- messagefoundry/checks.py +174 -0
- messagefoundry/config/__init__.py +30 -0
- messagefoundry/config/active_environment.py +80 -0
- messagefoundry/config/ai_policy.py +140 -0
- messagefoundry/config/code_sets.py +260 -0
- messagefoundry/config/connections_edit.py +200 -0
- messagefoundry/config/connections_file.py +287 -0
- messagefoundry/config/db_lookup.py +117 -0
- messagefoundry/config/environments.py +116 -0
- messagefoundry/config/ingest_time.py +83 -0
- messagefoundry/config/models.py +240 -0
- messagefoundry/config/reference.py +158 -0
- messagefoundry/config/response.py +83 -0
- messagefoundry/config/run_context.py +153 -0
- messagefoundry/config/settings.py +1311 -0
- messagefoundry/config/state.py +99 -0
- messagefoundry/config/tls_policy.py +110 -0
- messagefoundry/config/wiring.py +1918 -0
- messagefoundry/console/__init__.py +20 -0
- messagefoundry/console/__main__.py +274 -0
- messagefoundry/console/_async.py +107 -0
- messagefoundry/console/change_password.py +111 -0
- messagefoundry/console/client.py +552 -0
- messagefoundry/console/connections.py +324 -0
- messagefoundry/console/login.py +107 -0
- messagefoundry/console/mfa.py +205 -0
- messagefoundry/console/reauth.py +94 -0
- messagefoundry/console/search.py +57 -0
- messagefoundry/console/service_control.py +137 -0
- messagefoundry/console/sessions.py +122 -0
- messagefoundry/console/shell.py +410 -0
- messagefoundry/console/status.py +377 -0
- messagefoundry/console/users_page.py +282 -0
- messagefoundry/console/widgets.py +553 -0
- messagefoundry/generators/README.md +27 -0
- messagefoundry/generators/__init__.py +15 -0
- messagefoundry/generators/_core.py +589 -0
- messagefoundry/generators/_hl7data.py +428 -0
- messagefoundry/generators/adt.py +286 -0
- messagefoundry/generators/all_types.py +24 -0
- messagefoundry/generators/bar.py +28 -0
- messagefoundry/generators/dft.py +20 -0
- messagefoundry/generators/mdm.py +39 -0
- messagefoundry/generators/mfn.py +46 -0
- messagefoundry/generators/oml.py +32 -0
- messagefoundry/generators/orl.py +30 -0
- messagefoundry/generators/orm.py +23 -0
- messagefoundry/generators/oru.py +21 -0
- messagefoundry/generators/ras.py +20 -0
- messagefoundry/generators/rde.py +54 -0
- messagefoundry/generators/siu.py +64 -0
- messagefoundry/generators/vxu.py +20 -0
- messagefoundry/hl7schema.py +75 -0
- messagefoundry/last_resort.py +55 -0
- messagefoundry/logging_setup.py +332 -0
- messagefoundry/parsing/__init__.py +64 -0
- messagefoundry/parsing/consistency.py +166 -0
- messagefoundry/parsing/groups.py +228 -0
- messagefoundry/parsing/message.py +453 -0
- messagefoundry/parsing/peek.py +237 -0
- messagefoundry/parsing/split.py +120 -0
- messagefoundry/parsing/summary.py +46 -0
- messagefoundry/parsing/tree.py +128 -0
- messagefoundry/parsing/validate.py +95 -0
- messagefoundry/parsing/x12/__init__.py +46 -0
- messagefoundry/parsing/x12/delimiters.py +140 -0
- messagefoundry/parsing/x12/errors.py +30 -0
- messagefoundry/parsing/x12/interchange.py +232 -0
- messagefoundry/parsing/x12/message.py +200 -0
- messagefoundry/parsing/x12/peek.py +207 -0
- messagefoundry/pipeline/__init__.py +21 -0
- messagefoundry/pipeline/alert_sinks.py +486 -0
- messagefoundry/pipeline/alerts.py +100 -0
- messagefoundry/pipeline/cert_expiry.py +219 -0
- messagefoundry/pipeline/cluster.py +955 -0
- messagefoundry/pipeline/cluster_sqlserver.py +444 -0
- messagefoundry/pipeline/config_convergence.py +137 -0
- messagefoundry/pipeline/dryrun.py +450 -0
- messagefoundry/pipeline/engine.py +756 -0
- messagefoundry/pipeline/leader_tasks.py +158 -0
- messagefoundry/pipeline/reference_sync.py +369 -0
- messagefoundry/pipeline/retention.py +289 -0
- messagefoundry/pipeline/security_notify.py +168 -0
- messagefoundry/pipeline/state_convergence.py +143 -0
- messagefoundry/pipeline/wiring_runner.py +1722 -0
- messagefoundry/py.typed +0 -0
- messagefoundry/redaction.py +71 -0
- messagefoundry/scaffold.py +321 -0
- messagefoundry/secrets_dpapi.py +129 -0
- messagefoundry/store/__init__.py +46 -0
- messagefoundry/store/audit_tee.py +67 -0
- messagefoundry/store/base.py +758 -0
- messagefoundry/store/crypto.py +166 -0
- messagefoundry/store/keyprovider.py +192 -0
- messagefoundry/store/postgres.py +3447 -0
- messagefoundry/store/sqlserver.py +3014 -0
- messagefoundry/store/store.py +3790 -0
- messagefoundry/timezone.py +207 -0
- messagefoundry/transports/__init__.py +50 -0
- messagefoundry/transports/base.py +269 -0
- messagefoundry/transports/database.py +693 -0
- messagefoundry/transports/file.py +551 -0
- messagefoundry/transports/framing.py +164 -0
- messagefoundry/transports/loopback.py +53 -0
- messagefoundry/transports/mllp.py +644 -0
- messagefoundry/transports/remotefile.py +664 -0
- messagefoundry/transports/rest.py +281 -0
- messagefoundry/transports/signing.py +321 -0
- messagefoundry/transports/soap.py +507 -0
- messagefoundry/transports/tcp.py +307 -0
- messagefoundry/transports/timer.py +146 -0
- messagefoundry/transports/x12.py +323 -0
- messagefoundry-0.1.0.dist-info/METADATA +212 -0
- messagefoundry-0.1.0.dist-info/RECORD +142 -0
- messagefoundry-0.1.0.dist-info/WHEEL +4 -0
- messagefoundry-0.1.0.dist-info/entry_points.txt +2 -0
- messagefoundry-0.1.0.dist-info/licenses/LICENSE +662 -0
- messagefoundry-0.1.0.dist-info/licenses/NOTICE +27 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate a corpus of conformant HL7 v2.5.1 **ADT** messages for testing.
|
|
4
|
+
|
|
5
|
+
For every ADT trigger event defined in 2.5.1 (A01–A62, excluding the query event A19 and
|
|
6
|
+
reserved A56–A59) this emits ``--count`` messages of fabricated-but-realistic data, one
|
|
7
|
+
``.hl7`` file each, laid out under ``--out`` as ``<TRIGGER>/0001.hl7``. The reference-driven
|
|
8
|
+
machinery lives in [_core.py](_core.py); this module supplies ADT's trigger→structure map,
|
|
9
|
+
its own segment builders (MRG/NPU/DB1/IAM), and a two-block validation fallback.
|
|
10
|
+
|
|
11
|
+
The 57 ADT trigger events share **25** message structures (e.g. A04/A08/A13 are all
|
|
12
|
+
``ADT_A01``); hl7apy keys validation by structure, so MSH-9.3 is set to the structure id
|
|
13
|
+
(CLAUDE.md §8). ``ADT_A17``/``ADT_A24``/``ADT_A37`` have two *ungrouped* PID blocks that
|
|
14
|
+
hl7apy cannot validate whole, so each patient block is validated independently as an
|
|
15
|
+
``ADT_A01`` superset (see :func:`_gate_two_block`). All data is synthetic — no real PHI.
|
|
16
|
+
|
|
17
|
+
Usage::
|
|
18
|
+
|
|
19
|
+
python -m messagefoundry.generators.adt # all triggers, 50 each
|
|
20
|
+
python messagefoundry/generators/adt.py --triggers A01,A04 --count 5
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import random
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from messagefoundry.parsing import HL7PeekError, Peek, normalize, validate
|
|
31
|
+
from messagefoundry.generators import _core
|
|
32
|
+
from messagefoundry.generators import _hl7data as d
|
|
33
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, next_seq, seg
|
|
34
|
+
|
|
35
|
+
DEFAULT_SEED = _core.DEFAULT_SEED
|
|
36
|
+
DEFAULT_OUT = "samples/messages/adt"
|
|
37
|
+
|
|
38
|
+
# Trigger event -> 2.5.1 message structure (HL7 Chapter 3). Every value is one of hl7apy's
|
|
39
|
+
# 25 ADT structures; shared triggers (e.g. A04->ADT_A01) carry the structure in MSH-9.3.
|
|
40
|
+
TRIGGER_TO_STRUCTURE: dict[str, str] = {
|
|
41
|
+
"A01": "ADT_A01",
|
|
42
|
+
"A04": "ADT_A01",
|
|
43
|
+
"A08": "ADT_A01",
|
|
44
|
+
"A13": "ADT_A01",
|
|
45
|
+
"A02": "ADT_A02",
|
|
46
|
+
"A03": "ADT_A03",
|
|
47
|
+
"A05": "ADT_A05",
|
|
48
|
+
"A14": "ADT_A05",
|
|
49
|
+
"A28": "ADT_A05",
|
|
50
|
+
"A31": "ADT_A05",
|
|
51
|
+
"A06": "ADT_A06",
|
|
52
|
+
"A07": "ADT_A06",
|
|
53
|
+
"A09": "ADT_A09",
|
|
54
|
+
"A10": "ADT_A09",
|
|
55
|
+
"A11": "ADT_A09",
|
|
56
|
+
"A12": "ADT_A12",
|
|
57
|
+
"A15": "ADT_A15",
|
|
58
|
+
"A16": "ADT_A16",
|
|
59
|
+
"A17": "ADT_A17",
|
|
60
|
+
"A18": "ADT_A18",
|
|
61
|
+
"A20": "ADT_A20",
|
|
62
|
+
"A21": "ADT_A21",
|
|
63
|
+
"A22": "ADT_A21",
|
|
64
|
+
"A23": "ADT_A21",
|
|
65
|
+
"A25": "ADT_A21",
|
|
66
|
+
"A26": "ADT_A21",
|
|
67
|
+
"A27": "ADT_A21",
|
|
68
|
+
"A29": "ADT_A21",
|
|
69
|
+
"A32": "ADT_A21",
|
|
70
|
+
"A33": "ADT_A21",
|
|
71
|
+
"A24": "ADT_A24",
|
|
72
|
+
"A30": "ADT_A30",
|
|
73
|
+
"A34": "ADT_A30",
|
|
74
|
+
"A35": "ADT_A30",
|
|
75
|
+
"A36": "ADT_A30",
|
|
76
|
+
"A46": "ADT_A30",
|
|
77
|
+
"A47": "ADT_A30",
|
|
78
|
+
"A48": "ADT_A30",
|
|
79
|
+
"A49": "ADT_A30",
|
|
80
|
+
"A37": "ADT_A37",
|
|
81
|
+
"A38": "ADT_A38",
|
|
82
|
+
"A39": "ADT_A39",
|
|
83
|
+
"A40": "ADT_A39",
|
|
84
|
+
"A41": "ADT_A39",
|
|
85
|
+
"A42": "ADT_A39",
|
|
86
|
+
"A43": "ADT_A43",
|
|
87
|
+
"A44": "ADT_A43",
|
|
88
|
+
"A45": "ADT_A45",
|
|
89
|
+
"A50": "ADT_A50",
|
|
90
|
+
"A51": "ADT_A50",
|
|
91
|
+
"A52": "ADT_A52",
|
|
92
|
+
"A53": "ADT_A52",
|
|
93
|
+
"A54": "ADT_A54",
|
|
94
|
+
"A55": "ADT_A54",
|
|
95
|
+
"A60": "ADT_A60",
|
|
96
|
+
"A61": "ADT_A61",
|
|
97
|
+
"A62": "ADT_A61",
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
ALL_TRIGGERS: list[str] = sorted(TRIGGER_TO_STRUCTURE)
|
|
101
|
+
|
|
102
|
+
# Optional segments we know how to build and will sprinkle in for realism where a structure
|
|
103
|
+
# permits them. Required segments are always emitted regardless of this set.
|
|
104
|
+
OPTIONAL_SEGMENT_ALLOWLIST: frozenset[str] = frozenset(
|
|
105
|
+
{"PD1", "PV1", "PV2", "NK1", "AL1", "DG1", "OBX", "DB1", "IAM"}
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Structures with two ungrouped PID blocks that hl7apy cannot validate as a whole.
|
|
109
|
+
TWO_BLOCK_STRUCTURES: frozenset[str] = frozenset({"ADT_A17", "ADT_A24", "ADT_A37"})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --- ADT-specific segment builders -------------------------------------------
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _build_mrg(rng: random.Random, ctx: Ctx) -> str:
|
|
116
|
+
return seg("MRG", {1: d.cx(str(rng.randint(1_000_000, 9_999_999)))})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _build_npu(rng: random.Random, ctx: Ctx) -> str:
|
|
120
|
+
status = rng.choice(d.BED_STATUSES)[0]
|
|
121
|
+
location = d.pl(
|
|
122
|
+
rng.choice(d.POINTS_OF_CARE),
|
|
123
|
+
rng.choice(d.ROOMS),
|
|
124
|
+
rng.choice(d.BEDS),
|
|
125
|
+
rng.choice(d.FACILITIES),
|
|
126
|
+
)
|
|
127
|
+
return seg("NPU", {1: location, 2: status})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _build_db1(rng: random.Random, ctx: Ctx) -> str:
|
|
131
|
+
return seg("DB1", {1: str(next_seq(ctx, "DB1")), 2: "PT"})
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _build_iam(rng: random.Random, ctx: Ctx) -> str:
|
|
135
|
+
code, text = rng.choice(d.ALLERGENS)
|
|
136
|
+
sev_code, sev_text = rng.choice(d.ALLERGY_SEVERITIES)
|
|
137
|
+
return seg(
|
|
138
|
+
"IAM",
|
|
139
|
+
{
|
|
140
|
+
1: str(next_seq(ctx, "IAM")),
|
|
141
|
+
2: d.cwe("DA", "Drug allergy", "HL70127"),
|
|
142
|
+
3: d.cwe(code, text, "L"),
|
|
143
|
+
6: d.cwe(sev_code, sev_text, "HL70128"),
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
_ADT_BUILDERS = {"MRG": _build_mrg, "NPU": _build_npu, "DB1": _build_db1, "IAM": _build_iam}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# --- compliance gate (with the two-block fallback) ---------------------------
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _gate(msg: str, structure: str) -> tuple[bool, list[str]]:
|
|
155
|
+
if structure in TWO_BLOCK_STRUCTURES:
|
|
156
|
+
return _gate_two_block(msg)
|
|
157
|
+
return _core.default_gate(msg, structure)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _gate_two_block(msg: str) -> tuple[bool, list[str]]:
|
|
161
|
+
"""Validate each patient block of an A17/A24/A37 message independently.
|
|
162
|
+
|
|
163
|
+
hl7apy cannot validate two ungrouped PID blocks at once. Each block (PID + its encounter
|
|
164
|
+
segments) is a valid subset of ``ADT_A01``, so we wrap each as a minimal ADT_A01 and
|
|
165
|
+
validate that — proving field/segment conformance of both blocks.
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
peek = Peek.parse(msg)
|
|
169
|
+
except HL7PeekError as exc:
|
|
170
|
+
return False, [f"peek parse error: {exc}"]
|
|
171
|
+
|
|
172
|
+
segment_ids = peek.segments()
|
|
173
|
+
pid_at = [i for i, s in enumerate(segment_ids) if s == "PID"]
|
|
174
|
+
if len(pid_at) != 2:
|
|
175
|
+
return False, [f"expected exactly 2 PID segments, found {len(pid_at)}"]
|
|
176
|
+
|
|
177
|
+
# normalize first: a message read back via Path.read_text has \r translated to \n.
|
|
178
|
+
lines = normalize(msg).strip("\r").split("\r")
|
|
179
|
+
blocks = (lines[pid_at[0] : pid_at[1]], lines[pid_at[1] :])
|
|
180
|
+
errors: list[str] = []
|
|
181
|
+
for n, block in enumerate(blocks, start=1):
|
|
182
|
+
wrapper = "\r".join(
|
|
183
|
+
[
|
|
184
|
+
r"MSH|^~\&|S|F|R|D|20260101000000||ADT^A08^ADT_A01|MEFORWRAP|P|2.5.1",
|
|
185
|
+
"EVN|A08|20260101000000",
|
|
186
|
+
*block,
|
|
187
|
+
]
|
|
188
|
+
)
|
|
189
|
+
result = validate(wrapper, expected_version="2.5.1")
|
|
190
|
+
if not result.ok:
|
|
191
|
+
errors.extend(f"block{n}: {e}" for e in result.errors)
|
|
192
|
+
return (not errors), errors
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _force(structure: str) -> frozenset[str]:
|
|
196
|
+
# Guarantee a PV1 in each two-block patient section so each validates as an ADT_A01.
|
|
197
|
+
return frozenset({"PV1"}) if structure in TWO_BLOCK_STRUCTURES else frozenset()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_core.register(
|
|
201
|
+
MessageSpec(
|
|
202
|
+
code="ADT",
|
|
203
|
+
trigger_to_structure=TRIGGER_TO_STRUCTURE,
|
|
204
|
+
builders=_ADT_BUILDERS,
|
|
205
|
+
optional_allowlist=OPTIONAL_SEGMENT_ALLOWLIST,
|
|
206
|
+
force=_force,
|
|
207
|
+
gate=_gate,
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# --- ADT-flavoured back-compat surface (used by tests / the CLI) -------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def generate_message(trigger: str, index: int, *, seed: str = DEFAULT_SEED) -> str:
|
|
216
|
+
"""Build one ``\\r``-delimited ADT message for ``trigger`` (deterministic given seed)."""
|
|
217
|
+
return _core.generate_message("ADT", trigger, index, seed=seed)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def gate(msg: str, structure: str) -> tuple[bool, list[str]]:
|
|
221
|
+
"""Return ``(ok, errors)`` from strict hl7apy validation (with the two-block fallback)."""
|
|
222
|
+
return _core.gate("ADT", msg, structure)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# --- CLI ---------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _parse_triggers(raw: str) -> list[str]:
|
|
229
|
+
if not raw:
|
|
230
|
+
return ALL_TRIGGERS
|
|
231
|
+
chosen = [t.strip().upper() for t in raw.split(",") if t.strip()]
|
|
232
|
+
unknown = [t for t in chosen if t not in TRIGGER_TO_STRUCTURE]
|
|
233
|
+
if unknown:
|
|
234
|
+
raise SystemExit(f"unknown ADT trigger(s): {', '.join(unknown)}")
|
|
235
|
+
return chosen
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv: list[str] | None = None) -> int:
|
|
239
|
+
parser = argparse.ArgumentParser(description="Generate conformant HL7 v2.5.1 ADT messages.")
|
|
240
|
+
parser.add_argument("--out", default=DEFAULT_OUT, help=f"output root (default {DEFAULT_OUT})")
|
|
241
|
+
parser.add_argument("--count", type=int, default=50, help="messages per trigger (default 50)")
|
|
242
|
+
parser.add_argument("--triggers", default="", help="comma-separated subset (default: all)")
|
|
243
|
+
parser.add_argument("--seed", default=DEFAULT_SEED, help="RNG seed for reproducible output")
|
|
244
|
+
parser.add_argument("--quiet", action="store_true", help="suppress per-trigger progress")
|
|
245
|
+
parser.add_argument(
|
|
246
|
+
"--show-message",
|
|
247
|
+
action="store_true",
|
|
248
|
+
help="on validation failure, print the offending message to stderr (off by default)",
|
|
249
|
+
)
|
|
250
|
+
args = parser.parse_args(argv)
|
|
251
|
+
|
|
252
|
+
triggers = _parse_triggers(args.triggers)
|
|
253
|
+
out_root = Path(args.out)
|
|
254
|
+
total = 0
|
|
255
|
+
|
|
256
|
+
for trigger in triggers:
|
|
257
|
+
structure = TRIGGER_TO_STRUCTURE[trigger]
|
|
258
|
+
# Generate + strict-validate + write via the shared corpus writer: its cleanup is scoped
|
|
259
|
+
# to its own NNNN.hl7 files, never an arbitrary *.hl7 a user placed in the dir (low-19).
|
|
260
|
+
try:
|
|
261
|
+
result = _core.write_corpus(
|
|
262
|
+
"ADT", triggers=[trigger], count=args.count, out=args.out, seed=args.seed
|
|
263
|
+
)
|
|
264
|
+
except _core.GenerationError as exc:
|
|
265
|
+
print(f"VALIDATION FAILED: {trigger} #{exc.index} ({structure})", file=sys.stderr)
|
|
266
|
+
for err in exc.errors:
|
|
267
|
+
print(f" - {err}", file=sys.stderr)
|
|
268
|
+
# Even though these are synthetic, don't dump a whole message to stderr by default —
|
|
269
|
+
# the pattern leaks if ever adapted to real data (docs/PHI.md §7). Opt in to debug.
|
|
270
|
+
if args.show_message:
|
|
271
|
+
print("--- offending message ---", file=sys.stderr)
|
|
272
|
+
print(exc.message.replace("\r", "\n"), file=sys.stderr)
|
|
273
|
+
else:
|
|
274
|
+
print("(re-run with --show-message to print it)", file=sys.stderr)
|
|
275
|
+
return 1
|
|
276
|
+
total += result.total
|
|
277
|
+
if not args.quiet:
|
|
278
|
+
print(f"{trigger} -> {structure}: {result.total} messages")
|
|
279
|
+
|
|
280
|
+
if not args.quiet:
|
|
281
|
+
print(f"\nGenerated {total} messages across {len(triggers)} triggers into {out_root}/")
|
|
282
|
+
return 0
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Side-effect import that registers every built-in message type with the generator registry.
|
|
4
|
+
|
|
5
|
+
Import this before reading ``_core.message_codes()`` (the harness GUI does). Each generator
|
|
6
|
+
module registers itself on import; list new types here as they're built.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# Each import runs the module's _core.register(...) side effect.
|
|
12
|
+
from messagefoundry.generators import adt # noqa: F401
|
|
13
|
+
from messagefoundry.generators import bar # noqa: F401
|
|
14
|
+
from messagefoundry.generators import dft # noqa: F401
|
|
15
|
+
from messagefoundry.generators import mdm # noqa: F401
|
|
16
|
+
from messagefoundry.generators import mfn # noqa: F401
|
|
17
|
+
from messagefoundry.generators import oml # noqa: F401
|
|
18
|
+
from messagefoundry.generators import orl # noqa: F401
|
|
19
|
+
from messagefoundry.generators import orm # noqa: F401
|
|
20
|
+
from messagefoundry.generators import oru # noqa: F401
|
|
21
|
+
from messagefoundry.generators import ras # noqa: F401
|
|
22
|
+
from messagefoundry.generators import rde # noqa: F401
|
|
23
|
+
from messagefoundry.generators import siu # noqa: F401
|
|
24
|
+
from messagefoundry.generators import vxu # noqa: F401
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **BAR** (add/change billing account) messages.
|
|
4
|
+
|
|
5
|
+
BAR_Pxx require MSH + EVN + PID + a VISIT group; we add procedures (PR1), guarantor (GT1) and
|
|
6
|
+
insurance (IN1) for realistic billing accounts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from messagefoundry.generators import _core
|
|
12
|
+
from messagefoundry.generators._core import MessageSpec
|
|
13
|
+
|
|
14
|
+
_core.register(
|
|
15
|
+
MessageSpec(
|
|
16
|
+
code="BAR",
|
|
17
|
+
# P10 omitted: requires GP1 (grouping/reimbursement), which we don't build.
|
|
18
|
+
trigger_to_structure={
|
|
19
|
+
"P01": "BAR_P01",
|
|
20
|
+
"P02": "BAR_P02",
|
|
21
|
+
"P05": "BAR_P05",
|
|
22
|
+
"P06": "BAR_P06",
|
|
23
|
+
"P12": "BAR_P12",
|
|
24
|
+
},
|
|
25
|
+
optional_allowlist=frozenset({"PV1", "PV2", "DG1", "GT1", "OBX", "AL1"}),
|
|
26
|
+
group_suffixes=frozenset({"_PROCEDURE", "_INSURANCE"}),
|
|
27
|
+
)
|
|
28
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **DFT** (detailed financial transaction) messages.
|
|
4
|
+
|
|
5
|
+
DFT_P03/P11 require MSH + EVN + PID + a FINANCIAL group (→ FT1); we add an optional visit and
|
|
6
|
+
diagnoses for realism.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from messagefoundry.generators import _core
|
|
12
|
+
from messagefoundry.generators._core import MessageSpec
|
|
13
|
+
|
|
14
|
+
_core.register(
|
|
15
|
+
MessageSpec(
|
|
16
|
+
code="DFT",
|
|
17
|
+
trigger_to_structure={"P03": "DFT_P03", "P11": "DFT_P11"},
|
|
18
|
+
optional_allowlist=frozenset({"PV1", "PV2", "DG1"}),
|
|
19
|
+
)
|
|
20
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **MDM** (medical document management) messages.
|
|
4
|
+
|
|
5
|
+
MDM_T01/T02 require MSH + EVN + PID + PV1 + TXA (T02 adds an observation/OBX).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import random
|
|
11
|
+
|
|
12
|
+
from messagefoundry.generators import _core
|
|
13
|
+
from messagefoundry.generators import _hl7data as d
|
|
14
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, next_seq, seg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_txa(rng: random.Random, ctx: Ctx) -> str:
|
|
18
|
+
return seg(
|
|
19
|
+
"TXA",
|
|
20
|
+
{
|
|
21
|
+
1: str(next_seq(ctx, "TXA")),
|
|
22
|
+
2: rng.choice(d.DOCUMENT_TYPES), # document type (required)
|
|
23
|
+
12: d.ei(
|
|
24
|
+
str(rng.randint(100_000, 999_999)), "DOC"
|
|
25
|
+
), # unique document number (required)
|
|
26
|
+
17: rng.choice(d.DOC_STATUSES), # document completion status (required)
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_core.register(
|
|
32
|
+
MessageSpec(
|
|
33
|
+
code="MDM",
|
|
34
|
+
trigger_to_structure={"T01": "MDM_T01", "T02": "MDM_T02"},
|
|
35
|
+
builders={"TXA": _build_txa},
|
|
36
|
+
optional_allowlist=frozenset({"PD1", "PV2", "NK1", "OBX", "DG1", "AL1"}),
|
|
37
|
+
group_suffixes=frozenset({"_COMMON_ORDER", "_OBSERVATION"}),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **MFN** (master file notification) messages.
|
|
4
|
+
|
|
5
|
+
We cover the generic master-file structures (M01, M13) that need only MFI + MFE. The typed
|
|
6
|
+
variants (M02→STF, M04→CDM, M05→LOC/LDP, …) require master-file-specific segments we don't build.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
|
|
13
|
+
from messagefoundry.generators import _core
|
|
14
|
+
from messagefoundry.generators import _hl7data as d
|
|
15
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, seg
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_mfi(rng: random.Random, ctx: Ctx) -> str:
|
|
19
|
+
return seg(
|
|
20
|
+
"MFI",
|
|
21
|
+
{
|
|
22
|
+
1: d.cwe("PRA", "Practitioner master file", "HL70175"), # master file id (required)
|
|
23
|
+
3: "REP", # file-level event code (required)
|
|
24
|
+
6: "NE", # response level code (required)
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _build_mfe(rng: random.Random, ctx: Ctx) -> str:
|
|
30
|
+
return seg(
|
|
31
|
+
"MFE",
|
|
32
|
+
{
|
|
33
|
+
1: rng.choice(("MAD", "MUP", "MDC")), # record-level event code (required)
|
|
34
|
+
4: d.cwe(f"KEY{rng.randint(100, 999)}", "Master record", "L"), # primary key (required)
|
|
35
|
+
5: "CWE", # primary key value type (required)
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_core.register(
|
|
41
|
+
MessageSpec(
|
|
42
|
+
code="MFN",
|
|
43
|
+
trigger_to_structure={"M01": "MFN_M01", "M13": "MFN_M13"},
|
|
44
|
+
builders={"MFI": _build_mfi, "MFE": _build_mfe},
|
|
45
|
+
)
|
|
46
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **OML** (laboratory order) messages — the modern,
|
|
4
|
+
cleanly-structured successor to ORM for OBR-based orders, with specimen (SPM) support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import random
|
|
10
|
+
|
|
11
|
+
from messagefoundry.generators import _core
|
|
12
|
+
from messagefoundry.generators import _hl7data as d
|
|
13
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, next_seq, seg
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _build_spm(rng: random.Random, ctx: Ctx) -> str:
|
|
17
|
+
code, text = rng.choice(d.SPECIMEN_TYPES)
|
|
18
|
+
return seg("SPM", {1: str(next_seq(ctx, "SPM")), 4: d.cwe(code, text, "HL70487")})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_core.register(
|
|
22
|
+
MessageSpec(
|
|
23
|
+
code="OML",
|
|
24
|
+
# O35 omitted: requires SAC (specimen container), which we don't build.
|
|
25
|
+
trigger_to_structure={"O21": "OML_O21", "O33": "OML_O33"},
|
|
26
|
+
builders={"SPM": _build_spm},
|
|
27
|
+
optional_allowlist=frozenset({"PD1", "PV2", "DG1", "OBX"}),
|
|
28
|
+
group_suffixes=frozenset(
|
|
29
|
+
{"_PATIENT", "_PATIENT_VISIT", "_OBSERVATION_REQUEST", "_OBSERVATION", "_SPECIMEN"}
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **ORL** (laboratory order response) messages.
|
|
4
|
+
|
|
5
|
+
ORL is an order acknowledgement: MSH + MSA, optionally echoing the patient/order. We emit an
|
|
6
|
+
AA acknowledgement plus the optional response patient block.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import random
|
|
12
|
+
|
|
13
|
+
from messagefoundry.generators import _core
|
|
14
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, seg
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_msa(rng: random.Random, ctx: Ctx) -> str:
|
|
18
|
+
# MSA-1 acknowledgement code, MSA-2 the (fabricated) control id being acknowledged.
|
|
19
|
+
return seg("MSA", {1: "AA", 2: f"REQ{rng.randint(10_000, 99_999)}"})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ORL is an acknowledgement: MSH + MSA is conformant for every ORL structure. We don't echo the
|
|
23
|
+
# patient/order block — O34/O36 nest a required SPM-bearing order group we don't populate.
|
|
24
|
+
_core.register(
|
|
25
|
+
MessageSpec(
|
|
26
|
+
code="ORL",
|
|
27
|
+
trigger_to_structure={"O22": "ORL_O22", "O34": "ORL_O34", "O36": "ORL_O36"},
|
|
28
|
+
builders={"MSA": _build_msa},
|
|
29
|
+
)
|
|
30
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **ORM** (general order) messages.
|
|
4
|
+
|
|
5
|
+
ORM_O01 only *requires* MSH + ORC; we include the optional PATIENT group (PID/PV1) for realism.
|
|
6
|
+
We deliberately omit ORDER_DETAIL: hl7apy models its OBR/RQD/RQ1/RXO/ODS/ODT subgroup as
|
|
7
|
+
all-required rather than a choice, so it can't be populated sensibly — OBR-based orders are
|
|
8
|
+
better expressed via OML (the modern lab order) instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from messagefoundry.generators import _core
|
|
14
|
+
from messagefoundry.generators._core import MessageSpec
|
|
15
|
+
|
|
16
|
+
_core.register(
|
|
17
|
+
MessageSpec(
|
|
18
|
+
code="ORM",
|
|
19
|
+
trigger_to_structure={"O01": "ORM_O01"},
|
|
20
|
+
optional_allowlist=frozenset({"PD1", "PV2"}),
|
|
21
|
+
group_suffixes=frozenset({"_PATIENT", "_PATIENT_VISIT"}),
|
|
22
|
+
)
|
|
23
|
+
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **ORU** (observation result) messages.
|
|
4
|
+
|
|
5
|
+
The required PATIENT_RESULT → ORDER_OBSERVATION groups give MSH + OBR; we include the optional
|
|
6
|
+
PATIENT (PID) and OBSERVATION (OBX) groups so results carry a patient and values.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from messagefoundry.generators import _core
|
|
12
|
+
from messagefoundry.generators._core import MessageSpec
|
|
13
|
+
|
|
14
|
+
_core.register(
|
|
15
|
+
MessageSpec(
|
|
16
|
+
code="ORU",
|
|
17
|
+
trigger_to_structure={"R01": "ORU_R01", "R30": "ORU_R30"},
|
|
18
|
+
optional_allowlist=frozenset({"ORC", "PD1", "NK1"}),
|
|
19
|
+
group_suffixes=frozenset({"_PATIENT", "_OBSERVATION"}),
|
|
20
|
+
)
|
|
21
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **RAS** (pharmacy/treatment administration) messages.
|
|
4
|
+
|
|
5
|
+
The required ADMINISTRATION group carries RXA; ORC/RXA/RXR are shared builders.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from messagefoundry.generators import _core
|
|
11
|
+
from messagefoundry.generators._core import MessageSpec
|
|
12
|
+
|
|
13
|
+
_core.register(
|
|
14
|
+
MessageSpec(
|
|
15
|
+
code="RAS",
|
|
16
|
+
trigger_to_structure={"O17": "RAS_O17"},
|
|
17
|
+
optional_allowlist=frozenset({"PD1", "PV2", "AL1", "RXR"}),
|
|
18
|
+
group_suffixes=frozenset({"_PATIENT", "_PATIENT_VISIT"}),
|
|
19
|
+
)
|
|
20
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
# Copyright (C) 2026 MessageFoundry Organization and contributors
|
|
3
|
+
"""Generate conformant HL7 v2.5.1 **RDE** (pharmacy/treatment encoded order) messages.
|
|
4
|
+
|
|
5
|
+
hl7apy's parser greedily assigns a TQ1 to the optional ``RDE_O11_TIMING`` group (which precedes
|
|
6
|
+
RXE) and never to the *required* ``RDE_O11_TIMING_ENCODED`` group — so it false-flags
|
|
7
|
+
TIMING_ENCODED as missing even though the message carries a TQ1 in the right place. We emit the
|
|
8
|
+
correct segments (ORC/RXE/TQ1/RXR) and a narrow gate tolerates that one tool-limitation error
|
|
9
|
+
(a spec-correct message), failing on anything else. (Same spirit as ADT's two-block fallback.)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import random
|
|
15
|
+
|
|
16
|
+
from messagefoundry.parsing import validate
|
|
17
|
+
from messagefoundry.generators import _core
|
|
18
|
+
from messagefoundry.generators import _hl7data as d
|
|
19
|
+
from messagefoundry.generators._core import Ctx, MessageSpec, next_seq, seg
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _build_rxe(rng: random.Random, ctx: Ctx) -> str:
|
|
23
|
+
code, text = rng.choice(d.MEDICATIONS)
|
|
24
|
+
return seg(
|
|
25
|
+
"RXE",
|
|
26
|
+
{
|
|
27
|
+
2: d.cwe(code, text, "RxNorm"), # give code (required)
|
|
28
|
+
3: "1", # give amount minimum (required)
|
|
29
|
+
5: d.cwe("mg", "milligram", "UCUM"), # give units (required)
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _build_tq1(rng: random.Random, ctx: Ctx) -> str:
|
|
35
|
+
return seg("TQ1", {1: str(next_seq(ctx, "TQ1"))})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _gate(msg: str, structure: str) -> tuple[bool, list[str]]:
|
|
39
|
+
result = validate(msg, expected_version="2.5.1")
|
|
40
|
+
# Tolerate only the known TIMING_ENCODED mis-grouping; any other error is a real failure.
|
|
41
|
+
remaining = [e for e in result.errors if "TIMING_ENCODED" not in e]
|
|
42
|
+
return (not remaining), remaining
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
_core.register(
|
|
46
|
+
MessageSpec(
|
|
47
|
+
code="RDE",
|
|
48
|
+
trigger_to_structure={"O11": "RDE_O11"},
|
|
49
|
+
builders={"RXE": _build_rxe, "TQ1": _build_tq1},
|
|
50
|
+
optional_allowlist=frozenset({"PD1", "PV2"}),
|
|
51
|
+
group_suffixes=frozenset({"_PATIENT", "_PATIENT_VISIT"}),
|
|
52
|
+
gate=_gate,
|
|
53
|
+
)
|
|
54
|
+
)
|