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,207 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 MessageFoundry Organization and contributors
3
+ """Tolerant X12 *peek* — cheap routing-field extraction (the HL7 :class:`~messagefoundry.parsing.peek.Peek`
4
+ analog for X12 EDI).
5
+
6
+ Routing should never force a full parse, so :class:`X12Peek` does a **fixed-offset ISA read** for the
7
+ interchange identity (sender/receiver, version, control number, usage) plus a shallow ``GS``/``ST``
8
+ header walk for the functional groups and their transaction-set ids — the keys a Router branches on.
9
+
10
+ One ISA can carry **multiple ``GS`` groups**, and one ``GS`` **multiple ``ST`` sets**, so the group /
11
+ transaction ids are not single-valued: :meth:`X12Peek.groups` returns the **full list** of
12
+ :class:`X12Group` so a Router can fan out or filter precisely (returning only the first would silently
13
+ mis-route multi-group interchanges). The implementation-guide version a Router most often branches on
14
+ (e.g. ``005010X222A1`` for 837P) lives in **GS08**, distinct from the ISA12 envelope version exposed by
15
+ :attr:`X12Peek.version`.
16
+
17
+ Pure: works on ``str`` (or ``bytes``, decoded UTF-8/replace), no I/O, no engine imports.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from collections.abc import Iterator
23
+ from dataclasses import dataclass
24
+
25
+ from messagefoundry.parsing.x12.delimiters import (
26
+ DEFAULT_MAX_INTERCHANGE_BYTES,
27
+ Delimiters,
28
+ discover_delimiters,
29
+ find_isa_start,
30
+ )
31
+ from messagefoundry.parsing.x12.errors import X12PeekError
32
+
33
+ __all__ = ["X12Peek", "X12Group"]
34
+
35
+ # ISA fixed-offset field slices (relative to the ISA start): name -> (start, end). The ISA is fixed
36
+ # width, so these are read directly rather than tokenized.
37
+ _ISA_FIELDS: dict[str, tuple[int, int]] = {
38
+ "sender_qual": (32, 34), # ISA05
39
+ "sender_id": (35, 50), # ISA06
40
+ "receiver_qual": (51, 53), # ISA07
41
+ "receiver_id": (54, 69), # ISA08
42
+ "date": (70, 76), # ISA09 (YYMMDD)
43
+ "time": (77, 81), # ISA10 (HHMM)
44
+ "version": (84, 89), # ISA12 (envelope version, e.g. "00501")
45
+ "control_number": (90, 99), # ISA13
46
+ "usage": (102, 103), # ISA15 ("P" production / "T" test)
47
+ }
48
+
49
+
50
+ def _element(fields: list[str], index: int) -> str | None:
51
+ """The 1-based-by-X12-convention element at ``index`` (0 = segment tag), trimmed, or None."""
52
+ if index < len(fields):
53
+ return fields[index].strip() or None
54
+ return None
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class X12Group:
59
+ """A functional group (``GS``/``GE``) and the transaction-set ids it carries.
60
+
61
+ ``version`` is GS08 (Version/Release/Industry Identifier Code, the implementation-guide version,
62
+ e.g. ``005010X222A1``) — distinct from :attr:`X12Peek.version` (the ISA12 envelope version)."""
63
+
64
+ functional_id: str | None # GS01, e.g. "HC" (health care claim)
65
+ app_sender: str | None # GS02
66
+ app_receiver: str | None # GS03
67
+ control_number: str | None # GS06
68
+ version: str | None # GS08
69
+ transactions: tuple[str, ...] # ST01 ids in order, e.g. ("837",)
70
+
71
+
72
+ @dataclass(frozen=True)
73
+ class X12Peek:
74
+ """A tolerant view over one X12 interchange exposing routing fields. Construct via :meth:`parse`.
75
+
76
+ ``raw`` is the interchange text; ``delimiters`` are discovered from its ISA; ``isa_start`` is where
77
+ the ISA begins (≥0; nonzero when leading whitespace/BOM preceded it)."""
78
+
79
+ raw: str
80
+ delimiters: Delimiters
81
+ isa_start: int
82
+
83
+ @classmethod
84
+ def parse(
85
+ cls,
86
+ raw: str | bytes,
87
+ *,
88
+ max_bytes: int | None = DEFAULT_MAX_INTERCHANGE_BYTES,
89
+ ) -> X12Peek:
90
+ """Peek the interchange that starts ``raw`` (after any leading whitespace/BOM).
91
+
92
+ Raises :class:`X12PeekError` if the bytes are not a parseable X12 interchange (no ISA,
93
+ truncated/malformed ISA, non-distinct delimiters) or exceed ``max_bytes``."""
94
+ if isinstance(raw, (bytes, bytearray)):
95
+ raw = bytes(raw).decode("utf-8", "replace")
96
+ if max_bytes is not None and len(raw) > max_bytes:
97
+ raise X12PeekError(f"X12 interchange exceeds max size ({len(raw)} > {max_bytes} chars)")
98
+ isa_start = find_isa_start(raw)
99
+ delimiters = discover_delimiters(raw, isa_start)
100
+ return cls(raw=raw, delimiters=delimiters, isa_start=isa_start)
101
+
102
+ # --- interchange-level identity (ISA, by fixed offset) -------------------
103
+
104
+ def _isa(self, name: str) -> str:
105
+ start, end = _ISA_FIELDS[name]
106
+ return self.raw[self.isa_start + start : self.isa_start + end]
107
+
108
+ @property
109
+ def sender_qual(self) -> str | None:
110
+ """ISA05 — interchange sender ID qualifier (e.g. ``ZZ``, ``01``)."""
111
+ return self._isa("sender_qual").strip() or None
112
+
113
+ @property
114
+ def sender_id(self) -> str | None:
115
+ """ISA06 — interchange sender ID (trailing space-padding trimmed)."""
116
+ return self._isa("sender_id").strip() or None
117
+
118
+ @property
119
+ def receiver_qual(self) -> str | None:
120
+ """ISA07 — interchange receiver ID qualifier."""
121
+ return self._isa("receiver_qual").strip() or None
122
+
123
+ @property
124
+ def receiver_id(self) -> str | None:
125
+ """ISA08 — interchange receiver ID (trailing space-padding trimmed)."""
126
+ return self._isa("receiver_id").strip() or None
127
+
128
+ @property
129
+ def date(self) -> str | None:
130
+ """ISA09 — interchange date, ``YYMMDD``."""
131
+ return self._isa("date").strip() or None
132
+
133
+ @property
134
+ def time(self) -> str | None:
135
+ """ISA10 — interchange time, ``HHMM``."""
136
+ return self._isa("time").strip() or None
137
+
138
+ @property
139
+ def version(self) -> str | None:
140
+ """ISA12 — interchange control version number (the **envelope** version, e.g. ``00501``).
141
+ The implementation-guide version is per-group GS08 (see :attr:`X12Group.version`)."""
142
+ return self._isa("version").strip() or None
143
+
144
+ @property
145
+ def control_number(self) -> str | None:
146
+ """ISA13 — interchange control number (used for de-dup/correlation; ties to IEA02)."""
147
+ return self._isa("control_number").strip() or None
148
+
149
+ @property
150
+ def usage(self) -> str | None:
151
+ """ISA15 — usage indicator: ``P`` (production) or ``T`` (test)."""
152
+ return self._isa("usage").strip() or None
153
+
154
+ @property
155
+ def is_test(self) -> bool:
156
+ """True when the usage indicator (ISA15) is ``T`` (test)."""
157
+ return self.usage == "T"
158
+
159
+ # --- functional groups (GS/ST walk) -------------------------------------
160
+
161
+ def _iter_segments(self) -> Iterator[list[str]]:
162
+ """Yield each segment of the first interchange as a list of elements (``[tag, e1, e2, …]``),
163
+ stopping after ``IEA``. Tolerates cosmetic whitespace between segments."""
164
+ element = self.delimiters.element
165
+ terminator = self.delimiters.segment
166
+ for chunk in self.raw[self.isa_start :].split(terminator):
167
+ stripped = chunk.lstrip(" \t\r\n")
168
+ if not stripped:
169
+ continue
170
+ fields = stripped.split(element)
171
+ yield fields
172
+ if fields[0] == "IEA":
173
+ return
174
+
175
+ def groups(self) -> list[X12Group]:
176
+ """Every functional group in the interchange, with its transaction-set ids. Empty if none."""
177
+ # (gs-fields | None, [st01 …]); a new group opens at each GS, transactions attach to the last.
178
+ accumulated: list[tuple[list[str] | None, list[str]]] = []
179
+ for fields in self._iter_segments():
180
+ tag = fields[0]
181
+ if tag == "GS":
182
+ accumulated.append((fields, []))
183
+ elif tag == "ST":
184
+ if not accumulated:
185
+ accumulated.append((None, [])) # orphan ST (malformed) — don't lose it
186
+ st01 = _element(fields, 1)
187
+ if st01 is not None:
188
+ accumulated[-1][1].append(st01)
189
+ return [
190
+ X12Group(
191
+ functional_id=_element(gs, 1) if gs else None,
192
+ app_sender=_element(gs, 2) if gs else None,
193
+ app_receiver=_element(gs, 3) if gs else None,
194
+ control_number=_element(gs, 6) if gs else None,
195
+ version=_element(gs, 8) if gs else None,
196
+ transactions=tuple(tx),
197
+ )
198
+ for gs, tx in accumulated
199
+ ]
200
+
201
+ def transaction_ids(self) -> list[str]:
202
+ """Flat list of every ST01 transaction-set id across all groups, in order (e.g. ``["837"]``)."""
203
+ return [st for group in self.groups() for st in group.transactions]
204
+
205
+ def segment_ids(self) -> list[str]:
206
+ """Ordered segment ids of the first interchange (e.g. ``["ISA", "GS", "ST", …, "IEA"]``)."""
207
+ return [fields[0] for fields in self._iter_segments()]
@@ -0,0 +1,21 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (C) 2026 MessageFoundry Organization and contributors
3
+ """Per-message pipeline + connection supervision.
4
+
5
+ The inbound path (parse/validate → Router → Handler(s) → fan out to outbound outboxes →
6
+ ACK/NACK) and the per-outbound delivery workers live in :mod:`.wiring_runner`;
7
+ :mod:`.engine` supervises the :class:`RegistryRunner` over a shared store. Outbound
8
+ connections drain independently so one slow/failing destination never blocks the others.
9
+
10
+ Submodules:
11
+
12
+ * :mod:`.wiring_runner` — :class:`RegistryRunner` (runs a code-first wiring Registry)
13
+ * :mod:`.engine` — :class:`Engine`
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from messagefoundry.pipeline.engine import ConfigReloadDenied, Engine
19
+ from messagefoundry.pipeline.wiring_runner import RegistryRunner
20
+
21
+ __all__ = ["Engine", "ConfigReloadDenied", "RegistryRunner"]