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,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"]
|