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.
Files changed (142) hide show
  1. messagefoundry/__init__.py +108 -0
  2. messagefoundry/__main__.py +1155 -0
  3. messagefoundry/api/__init__.py +27 -0
  4. messagefoundry/api/app.py +1581 -0
  5. messagefoundry/api/approvals.py +184 -0
  6. messagefoundry/api/auth_models.py +211 -0
  7. messagefoundry/api/auth_routes.py +655 -0
  8. messagefoundry/api/field_authz.py +96 -0
  9. messagefoundry/api/models.py +374 -0
  10. messagefoundry/api/security.py +247 -0
  11. messagefoundry/api/tls.py +47 -0
  12. messagefoundry/auth/__init__.py +39 -0
  13. messagefoundry/auth/data/common_passwords.NOTICE +13 -0
  14. messagefoundry/auth/data/common_passwords.txt +10000 -0
  15. messagefoundry/auth/identity.py +71 -0
  16. messagefoundry/auth/ldap.py +264 -0
  17. messagefoundry/auth/notifications.py +68 -0
  18. messagefoundry/auth/passwords.py +53 -0
  19. messagefoundry/auth/permissions.py +120 -0
  20. messagefoundry/auth/policy.py +153 -0
  21. messagefoundry/auth/ratelimit.py +55 -0
  22. messagefoundry/auth/service.py +1323 -0
  23. messagefoundry/auth/tokens.py +26 -0
  24. messagefoundry/auth/totp.py +174 -0
  25. messagefoundry/checks.py +174 -0
  26. messagefoundry/config/__init__.py +30 -0
  27. messagefoundry/config/active_environment.py +80 -0
  28. messagefoundry/config/ai_policy.py +140 -0
  29. messagefoundry/config/code_sets.py +260 -0
  30. messagefoundry/config/connections_edit.py +200 -0
  31. messagefoundry/config/connections_file.py +287 -0
  32. messagefoundry/config/db_lookup.py +117 -0
  33. messagefoundry/config/environments.py +116 -0
  34. messagefoundry/config/ingest_time.py +83 -0
  35. messagefoundry/config/models.py +240 -0
  36. messagefoundry/config/reference.py +158 -0
  37. messagefoundry/config/response.py +83 -0
  38. messagefoundry/config/run_context.py +153 -0
  39. messagefoundry/config/settings.py +1311 -0
  40. messagefoundry/config/state.py +99 -0
  41. messagefoundry/config/tls_policy.py +110 -0
  42. messagefoundry/config/wiring.py +1918 -0
  43. messagefoundry/console/__init__.py +20 -0
  44. messagefoundry/console/__main__.py +274 -0
  45. messagefoundry/console/_async.py +107 -0
  46. messagefoundry/console/change_password.py +111 -0
  47. messagefoundry/console/client.py +552 -0
  48. messagefoundry/console/connections.py +324 -0
  49. messagefoundry/console/login.py +107 -0
  50. messagefoundry/console/mfa.py +205 -0
  51. messagefoundry/console/reauth.py +94 -0
  52. messagefoundry/console/search.py +57 -0
  53. messagefoundry/console/service_control.py +137 -0
  54. messagefoundry/console/sessions.py +122 -0
  55. messagefoundry/console/shell.py +410 -0
  56. messagefoundry/console/status.py +377 -0
  57. messagefoundry/console/users_page.py +282 -0
  58. messagefoundry/console/widgets.py +553 -0
  59. messagefoundry/generators/README.md +27 -0
  60. messagefoundry/generators/__init__.py +15 -0
  61. messagefoundry/generators/_core.py +589 -0
  62. messagefoundry/generators/_hl7data.py +428 -0
  63. messagefoundry/generators/adt.py +286 -0
  64. messagefoundry/generators/all_types.py +24 -0
  65. messagefoundry/generators/bar.py +28 -0
  66. messagefoundry/generators/dft.py +20 -0
  67. messagefoundry/generators/mdm.py +39 -0
  68. messagefoundry/generators/mfn.py +46 -0
  69. messagefoundry/generators/oml.py +32 -0
  70. messagefoundry/generators/orl.py +30 -0
  71. messagefoundry/generators/orm.py +23 -0
  72. messagefoundry/generators/oru.py +21 -0
  73. messagefoundry/generators/ras.py +20 -0
  74. messagefoundry/generators/rde.py +54 -0
  75. messagefoundry/generators/siu.py +64 -0
  76. messagefoundry/generators/vxu.py +20 -0
  77. messagefoundry/hl7schema.py +75 -0
  78. messagefoundry/last_resort.py +55 -0
  79. messagefoundry/logging_setup.py +332 -0
  80. messagefoundry/parsing/__init__.py +64 -0
  81. messagefoundry/parsing/consistency.py +166 -0
  82. messagefoundry/parsing/groups.py +228 -0
  83. messagefoundry/parsing/message.py +453 -0
  84. messagefoundry/parsing/peek.py +237 -0
  85. messagefoundry/parsing/split.py +120 -0
  86. messagefoundry/parsing/summary.py +46 -0
  87. messagefoundry/parsing/tree.py +128 -0
  88. messagefoundry/parsing/validate.py +95 -0
  89. messagefoundry/parsing/x12/__init__.py +46 -0
  90. messagefoundry/parsing/x12/delimiters.py +140 -0
  91. messagefoundry/parsing/x12/errors.py +30 -0
  92. messagefoundry/parsing/x12/interchange.py +232 -0
  93. messagefoundry/parsing/x12/message.py +200 -0
  94. messagefoundry/parsing/x12/peek.py +207 -0
  95. messagefoundry/pipeline/__init__.py +21 -0
  96. messagefoundry/pipeline/alert_sinks.py +486 -0
  97. messagefoundry/pipeline/alerts.py +100 -0
  98. messagefoundry/pipeline/cert_expiry.py +219 -0
  99. messagefoundry/pipeline/cluster.py +955 -0
  100. messagefoundry/pipeline/cluster_sqlserver.py +444 -0
  101. messagefoundry/pipeline/config_convergence.py +137 -0
  102. messagefoundry/pipeline/dryrun.py +450 -0
  103. messagefoundry/pipeline/engine.py +756 -0
  104. messagefoundry/pipeline/leader_tasks.py +158 -0
  105. messagefoundry/pipeline/reference_sync.py +369 -0
  106. messagefoundry/pipeline/retention.py +289 -0
  107. messagefoundry/pipeline/security_notify.py +168 -0
  108. messagefoundry/pipeline/state_convergence.py +143 -0
  109. messagefoundry/pipeline/wiring_runner.py +1722 -0
  110. messagefoundry/py.typed +0 -0
  111. messagefoundry/redaction.py +71 -0
  112. messagefoundry/scaffold.py +321 -0
  113. messagefoundry/secrets_dpapi.py +129 -0
  114. messagefoundry/store/__init__.py +46 -0
  115. messagefoundry/store/audit_tee.py +67 -0
  116. messagefoundry/store/base.py +758 -0
  117. messagefoundry/store/crypto.py +166 -0
  118. messagefoundry/store/keyprovider.py +192 -0
  119. messagefoundry/store/postgres.py +3447 -0
  120. messagefoundry/store/sqlserver.py +3014 -0
  121. messagefoundry/store/store.py +3790 -0
  122. messagefoundry/timezone.py +207 -0
  123. messagefoundry/transports/__init__.py +50 -0
  124. messagefoundry/transports/base.py +269 -0
  125. messagefoundry/transports/database.py +693 -0
  126. messagefoundry/transports/file.py +551 -0
  127. messagefoundry/transports/framing.py +164 -0
  128. messagefoundry/transports/loopback.py +53 -0
  129. messagefoundry/transports/mllp.py +644 -0
  130. messagefoundry/transports/remotefile.py +664 -0
  131. messagefoundry/transports/rest.py +281 -0
  132. messagefoundry/transports/signing.py +321 -0
  133. messagefoundry/transports/soap.py +507 -0
  134. messagefoundry/transports/tcp.py +307 -0
  135. messagefoundry/transports/timer.py +146 -0
  136. messagefoundry/transports/x12.py +323 -0
  137. messagefoundry-0.1.0.dist-info/METADATA +212 -0
  138. messagefoundry-0.1.0.dist-info/RECORD +142 -0
  139. messagefoundry-0.1.0.dist-info/WHEEL +4 -0
  140. messagefoundry-0.1.0.dist-info/entry_points.txt +2 -0
  141. messagefoundry-0.1.0.dist-info/licenses/LICENSE +662 -0
  142. 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
+ )