sourcelock 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.
@@ -0,0 +1,207 @@
1
+ """``hc-source manifest``: build, sign, verify, and key management.
2
+
3
+ Registered onto the main app in ``cli.py``. Exit codes follow the CLI-wide
4
+ contract: 0 on success, 1 on any usage, validation, or verification failure.
5
+ A failing ``verify`` is exit 1 with the reason on stdout -- CI can gate on it
6
+ directly.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ from pathlib import Path
13
+ from typing import Optional
14
+
15
+ import typer
16
+ from pydantic import ValidationError
17
+
18
+ from .lockfile import default_lock_path
19
+ from .manifest import (
20
+ SIGNING_KEY_ENV,
21
+ ManifestError,
22
+ build_envelope,
23
+ generate_keypair,
24
+ load_envelope,
25
+ load_private_key,
26
+ load_public_key,
27
+ public_key_fingerprint,
28
+ save_envelope,
29
+ sign_envelope,
30
+ verify_envelope,
31
+ verify_lockfile,
32
+ verify_vendored_data,
33
+ )
34
+
35
+ __all__ = ["manifest_app"]
36
+
37
+ manifest_app = typer.Typer(
38
+ help="Signed manifests of the current pins, vendored data, and tool versions.",
39
+ no_args_is_help=True,
40
+ )
41
+
42
+ DEFAULT_MANIFEST_FILENAME = "sourcelock-manifest.json"
43
+
44
+
45
+ def _fail(message: str) -> None:
46
+ typer.echo(message)
47
+ raise typer.Exit(1)
48
+
49
+
50
+ @manifest_app.command("build")
51
+ def manifest_build(
52
+ lock: Optional[Path] = typer.Option(
53
+ None, "--lock", help="Path to source-lock.json (default: ./source-lock.json)."
54
+ ),
55
+ out: Path = typer.Option(
56
+ Path(DEFAULT_MANIFEST_FILENAME),
57
+ "--out",
58
+ help=f"Where to write the manifest (default: ./{DEFAULT_MANIFEST_FILENAME}).",
59
+ ),
60
+ ) -> None:
61
+ """Build an unsigned manifest from the lockfile, vendored data, and tool versions."""
62
+ lock_path = lock or default_lock_path()
63
+ try:
64
+ envelope = build_envelope(lock_path)
65
+ except FileNotFoundError as exc:
66
+ _fail(str(exc))
67
+ raise AssertionError # pragma: no cover - _fail always exits
68
+ except ValidationError as exc:
69
+ _fail(
70
+ f"source-lock.json at {lock_path} is not valid: {exc.error_count()} problem(s). "
71
+ "Regenerate it with `hc-source lock init`."
72
+ )
73
+ raise AssertionError # pragma: no cover
74
+ except ManifestError as exc:
75
+ _fail(str(exc))
76
+ raise AssertionError # pragma: no cover
77
+
78
+ save_envelope(envelope, out)
79
+ payload = envelope["payload"]
80
+ typer.echo(
81
+ f"wrote {out} -- {len(payload['lockfile']['sources'])} source(s), "
82
+ f"{len(payload['vendored_data'])} vendored file(s), unsigned"
83
+ )
84
+ typer.echo("sign it with `hc-source manifest sign` before shipping it to anyone.")
85
+
86
+
87
+ @manifest_app.command("sign")
88
+ def manifest_sign(
89
+ manifest: Path = typer.Argument(..., help="Manifest file produced by `manifest build`."),
90
+ key: Optional[Path] = typer.Option(
91
+ None,
92
+ "--key",
93
+ help=f"Private key path (default: ${SIGNING_KEY_ENV}).",
94
+ ),
95
+ out: Optional[Path] = typer.Option(
96
+ None, "--out", help="Where to write the signed manifest (default: in place)."
97
+ ),
98
+ ) -> None:
99
+ """Sign a manifest with the founding-service Ed25519 private key."""
100
+ key_path = key or (Path(os.environ[SIGNING_KEY_ENV]) if os.environ.get(SIGNING_KEY_ENV) else None)
101
+ if key_path is None:
102
+ _fail(
103
+ f"no signing key: set {SIGNING_KEY_ENV} to the private key path "
104
+ "(or pass --key). The key lives in the ops directory, never in this repo."
105
+ )
106
+ raise AssertionError # pragma: no cover
107
+
108
+ try:
109
+ envelope = load_envelope(manifest)
110
+ private_key = load_private_key(key_path)
111
+ signed = sign_envelope(envelope, private_key)
112
+ except ManifestError as exc:
113
+ _fail(str(exc))
114
+ raise AssertionError # pragma: no cover
115
+
116
+ destination = save_envelope(signed, out or manifest)
117
+ fingerprint = signed["signature"]["public_key_fingerprint"]
118
+ typer.echo(f"signed {destination} (Ed25519, key fingerprint {fingerprint[:16]}...)")
119
+
120
+
121
+ @manifest_app.command("verify")
122
+ def manifest_verify(
123
+ manifest: Path = typer.Argument(..., help="Signed manifest file to verify."),
124
+ key: Path = typer.Option(..., "--key", help="Trusted Ed25519 public key (PEM)."),
125
+ check_data: bool = typer.Option(
126
+ False,
127
+ "--check-data",
128
+ help="Also compare the manifest's vendored-data hashes against this install.",
129
+ ),
130
+ check_lock: Optional[Path] = typer.Option(
131
+ None,
132
+ "--check-lock",
133
+ help=(
134
+ "Also bind the manifest to a lockfile on disk: the file at this path must "
135
+ "hash to the one the manifest signed. Pass the source-lock.json doctor "
136
+ "actually reads."
137
+ ),
138
+ ),
139
+ ) -> None:
140
+ """Verify a manifest's signature against a trusted public key.
141
+
142
+ The signature alone proves that SOME lockfile was signed. `--check-lock`
143
+ proves it was THIS one -- the difference between "the pins were signed" and
144
+ "the pins in use were signed", which is the only version of the claim worth
145
+ anything in an audit.
146
+ """
147
+ try:
148
+ envelope = load_envelope(manifest)
149
+ public_key = load_public_key(key)
150
+ verify_envelope(envelope, public_key)
151
+ except ManifestError as exc:
152
+ _fail(f"verification FAILED: {exc}")
153
+ raise AssertionError # pragma: no cover
154
+
155
+ payload = envelope["payload"]
156
+ typer.echo(
157
+ f"verification OK: signature valid (Ed25519, key fingerprint "
158
+ f"{public_key_fingerprint(public_key)[:16]}...)"
159
+ )
160
+ typer.echo(
161
+ f" manifest generated_at={payload['generated_at']} "
162
+ f"sources={len(payload['lockfile']['sources'])} "
163
+ f"lockfile_sha256={payload['lockfile']['sha256'][:16]}..."
164
+ )
165
+ # Inside the signed payload since round 2; a signing time sitting beside the
166
+ # signature was one nobody had signed.
167
+ typer.echo(f" signed_at={payload.get('signed_at', '(not recorded)')}")
168
+
169
+ if check_lock is not None:
170
+ problems = verify_lockfile(payload, check_lock)
171
+ if problems:
172
+ for problem in problems:
173
+ typer.echo(f" lockfile mismatch: {problem}")
174
+ _fail("the lockfile on disk is not the one this manifest signed.")
175
+ raise AssertionError # pragma: no cover
176
+ typer.echo(f" lockfile at {check_lock} matches the signed manifest.")
177
+
178
+ if check_data:
179
+ problems = verify_vendored_data(payload)
180
+ if problems:
181
+ for problem in problems:
182
+ typer.echo(f" data mismatch: {problem}")
183
+ _fail("vendored data does not match the signed manifest.")
184
+ raise AssertionError # pragma: no cover
185
+ typer.echo(f" vendored data matches ({len(payload['vendored_data'])} file(s) checked).")
186
+
187
+
188
+ @manifest_app.command("keygen")
189
+ def manifest_keygen(
190
+ out_dir: Path = typer.Option(
191
+ ..., "--out-dir", help="Directory for the new keypair (outside any repo)."
192
+ ),
193
+ name: str = typer.Option(
194
+ "manifest-signing", "--name", help="Basename: writes <name>.key and <name>.pub."
195
+ ),
196
+ ) -> None:
197
+ """Generate an Ed25519 keypair for manifest signing. Never overwrites."""
198
+ try:
199
+ priv_path, pub_path = generate_keypair(out_dir, name)
200
+ except ManifestError as exc:
201
+ _fail(str(exc))
202
+ raise AssertionError # pragma: no cover
203
+
204
+ fingerprint = public_key_fingerprint(load_public_key(pub_path))
205
+ typer.echo(f"wrote {priv_path} (private, mode 0600 -- keep out of every git repo)")
206
+ typer.echo(f"wrote {pub_path} (public -- this one you distribute)")
207
+ typer.echo(f"fingerprint {fingerprint}")
Binary file
@@ -0,0 +1,75 @@
1
+ {
2
+ "route": "codes",
3
+ "generated_at": "2026-08-01",
4
+ "regenerate": "python hc_source/data/codes/regenerate.py",
5
+ "canonicalization": "canonical_sha256 = SHA-256 of the uncompressed CSV bytes (UTF-8, LF line endings, header row first, source row order preserved). Drift decisions compare canonical hashes; raw zip sha256 values are kept for forensics only.",
6
+ "cpt_exclusion": "Only the ANWEB fixed-width master file is ingested from the HCPCS zip; proc_notes_*.txt and all .xlsx members are excluded (CPT mentions / unverified content). Numeric-leading (CPT Level I) and D-prefixed (ADA CDT) records are dropped defensively.",
7
+ "cm_april_status": {
8
+ "2026": "posted",
9
+ "2027": "unknown"
10
+ },
11
+ "cm_releases": {
12
+ "fy2026": {
13
+ "source_url": "https://www.cms.gov/files/zip/2026-code-descriptions-tabular-order.zip",
14
+ "source_zip_sha256": "55a9124a27ca78a4a5c41e1101eda064ec2e08d105ff9e78d1fbd1a83298ef11",
15
+ "source_last_modified": "Fri, 06 Jun 2025 12:34:30 GMT",
16
+ "inner_file": "icd10cm_codes_2026.txt",
17
+ "inner_sha256": "a7e2a77e4627ed55c8afe2b8a7ae22efcbab6b6b162d96cf145213772c1246ba",
18
+ "fetched_at": "2026-08-01",
19
+ "effective_from": "2025-10-01",
20
+ "effective_to": "2026-09-30",
21
+ "table": "icd10cm_fy2026.csv.gz",
22
+ "canonical_sha256": "f85c0c389d1749389b05bd6e975427b811bd7348582a3a456dbdf6a487aa5328",
23
+ "row_count": 74719
24
+ },
25
+ "fy2026-april": {
26
+ "source_url": "https://www.cms.gov/files/zip/april-1-2026-code-descriptions-tabular-order.zip",
27
+ "source_zip_sha256": "4fd9d8b37f02ab42827c7e7be30595c005b0cc3a6bae7a515e3f4c86b6918688",
28
+ "source_last_modified": "Wed, 17 Dec 2025 14:15:25 GMT",
29
+ "inner_file": "Code Descriptions/icd10cm_codes_2026.txt",
30
+ "inner_sha256": "a7e2a77e4627ed55c8afe2b8a7ae22efcbab6b6b162d96cf145213772c1246ba",
31
+ "fetched_at": "2026-08-01",
32
+ "effective_from": "2026-04-01",
33
+ "effective_to": "2026-09-30",
34
+ "table": "icd10cm_fy2026.csv.gz",
35
+ "content_identical_to": "fy2026",
36
+ "canonical_sha256": "f85c0c389d1749389b05bd6e975427b811bd7348582a3a456dbdf6a487aa5328",
37
+ "row_count": 74719
38
+ },
39
+ "fy2027": {
40
+ "source_url": "https://www.cms.gov/files/zip/2027-code-descriptions-tabular-order.zip",
41
+ "source_zip_sha256": "91c6c9d1117764ce72375a0f3a5493b1725dafbc1ab283b55799076c9e194965",
42
+ "source_last_modified": "Fri, 05 Jun 2026 16:52:26 GMT",
43
+ "inner_file": "Code Descriptions/icd10cm_codes_2027.txt",
44
+ "inner_sha256": "3c0583a38ee0e848f7dc0ac8ce88e8f001bbba8c6385ed825d44d46f6e5297c9",
45
+ "fetched_at": "2026-08-01",
46
+ "effective_from": "2026-10-01",
47
+ "effective_to": "2027-09-30",
48
+ "table": "icd10cm_fy2027.csv.gz",
49
+ "canonical_sha256": "43fabd3a3640eb2bf9dab6d2989966f74235428df51c2eab7e69408ed5c16a4d",
50
+ "row_count": 74879
51
+ }
52
+ },
53
+ "hcpcs_releases": {
54
+ "2026q3": {
55
+ "source_url": "https://www.cms.gov/files/zip/july-2026-alpha-numeric-hcpcs-file.zip",
56
+ "source_zip_sha256": "5591fed257e4d2307c1d2dc9c66abc8e346b1f4c076e9e48559799e2de14b70a",
57
+ "source_last_modified": "Wed, 17 Jun 2026 15:59:47 GMT",
58
+ "inner_file": "HCPC2026_JUL_ANWEB_06172026.txt",
59
+ "inner_sha256": "38e9b478568bb267edc57de50e9a7ba6af71782a90d26f3b6c2d8d4ccdf25074",
60
+ "fetched_at": "2026-08-01",
61
+ "effective_from": "2026-07-01",
62
+ "effective_to": "2026-09-30",
63
+ "table": "hcpcs_2026q3.csv.gz",
64
+ "canonical_sha256": "ed67edc4414ccbd249f032ceb9d3ddd3ba3d6b787ce2b9fa1952994cc2559645",
65
+ "row_count": 16826,
66
+ "cpt_cdt_records_excluded": 0
67
+ }
68
+ },
69
+ "cdc_root_listing": {
70
+ "source_url": "https://ftp.cdc.gov/pub/Health_Statistics/NCHS/Publications/ICD10CM/",
71
+ "raw_sha256": "e34231a4161dfab8956ebd368c5fa024f0bbdb2bf9973392cee4d1287897c6ed",
72
+ "fetched_at": "2026-08-01",
73
+ "fixture": "tests/fixtures/codes/cdc_root_listing.html"
74
+ }
75
+ }
@@ -0,0 +1,291 @@
1
+ """Regenerate the vendored derived tables for the ``codes`` route.
2
+
3
+ Run from the repository root::
4
+
5
+ python hc_source/data/codes/regenerate.py
6
+
7
+ Downloads the pinned CMS release zips (ICD-10-CM FY + April update, HCPCS
8
+ Level II quarterly) and the CDC ICD-10-CM directory listing, derives compact
9
+ gzipped CSV tables from them, and rewrites ``manifest.json`` with full
10
+ provenance (source URL, raw zip sha256, inner-file sha256, fetch date,
11
+ canonical table hashes, row counts).
12
+
13
+ Design rules honoured here:
14
+
15
+ * **Derive, don't dump** -- only the code/description/date columns the adapter
16
+ reads are kept. Raw zips are cached locally and never committed.
17
+ * **Canonical content hashes** -- each table's ``canonical_sha256`` is the
18
+ SHA-256 of the *uncompressed* CSV bytes (UTF-8, ``\n`` line endings, stable
19
+ column order), so drift decisions survive gzip re-compression. Gzip output
20
+ uses ``mtime=0`` so the compressed bytes are reproducible too.
21
+ * **CPT (HCPCS Level I) exclusion** -- only the ANWEB fixed-width master file
22
+ is ingested from the HCPCS zip. ``proc_notes_*.txt`` (contains CPT code
23
+ mentions in prose) and every ``.xlsx`` are never read. Defensively, any
24
+ record whose code is numeric-leading (CPT Level I) or ``D``-prefixed (ADA
25
+ CDT dental) is dropped and counted in the manifest (expected count: 0 --
26
+ verified zero in the July 2026 file).
27
+
28
+ Requires network access. The test suite never runs this; it consumes the
29
+ committed outputs offline.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import csv
35
+ import gzip
36
+ import hashlib
37
+ import io
38
+ import json
39
+ import re
40
+ import sys
41
+ import zipfile
42
+ from datetime import date, timezone, datetime
43
+ from pathlib import Path
44
+
45
+ HERE = Path(__file__).resolve().parent
46
+ REPO = HERE.parents[3]
47
+ sys.path.insert(0, str(REPO))
48
+
49
+ from hc_source.http import fetch # noqa: E402
50
+
51
+ CACHE = Path.home() / ".cache" / "sourcelock-codes-raw"
52
+ CACHE.mkdir(parents=True, exist_ok=True)
53
+
54
+ # Inner filenames keep the FY year and the nesting varies by release (FY2026
55
+ # is flat; the April-1-2026 AND FY2027 zips nest under "Code Descriptions/"),
56
+ # so members are located by basename, never by full path.
57
+ CM_RELEASES = [
58
+ # label, url, inner basename, effective_from, nominal effective_to
59
+ (
60
+ "fy2026",
61
+ "https://www.cms.gov/files/zip/2026-code-descriptions-tabular-order.zip",
62
+ "icd10cm_codes_2026.txt",
63
+ "2025-10-01",
64
+ "2026-09-30",
65
+ ),
66
+ (
67
+ "fy2026-april",
68
+ "https://www.cms.gov/files/zip/april-1-2026-code-descriptions-tabular-order.zip",
69
+ "icd10cm_codes_2026.txt",
70
+ "2026-04-01",
71
+ "2026-09-30",
72
+ ),
73
+ (
74
+ "fy2027",
75
+ "https://www.cms.gov/files/zip/2027-code-descriptions-tabular-order.zip",
76
+ "icd10cm_codes_2027.txt",
77
+ "2026-10-01",
78
+ "2027-09-30",
79
+ ),
80
+ ]
81
+
82
+ HCPCS_RELEASES = [
83
+ (
84
+ "2026q3",
85
+ "https://www.cms.gov/files/zip/july-2026-alpha-numeric-hcpcs-file.zip",
86
+ re.compile(r"^HCPC\d{4}_[A-Z]{3}_ANWEB_\d{8}\.txt$"),
87
+ "2026-07-01",
88
+ "2026-09-30",
89
+ ),
90
+ ]
91
+
92
+ CDC_ROOT = "https://ftp.cdc.gov/pub/Health_Statistics/NCHS/Publications/ICD10CM/"
93
+
94
+ # April-update publication status per fiscal year, as verifiable on the fetch
95
+ # date. "posted" = the April package is published; "absent" = April 1 of that
96
+ # FY has passed with no April package (settled fact); "unknown" = April 1 of
97
+ # that FY is in the future relative to the fetch date, so the package may yet
98
+ # be published. The adapter fails closed on "unknown".
99
+ CM_APRIL_STATUS = {"2026": "posted", "2027": "unknown"}
100
+
101
+ # HCPCS Level I (CPT, 5-char numeric-leading) and ADA CDT (5-char D-prefix)
102
+ # exclusion. Two-character records are CMS Level II *modifiers* (they sit in
103
+ # cols 4-5 with cols 1-3 blank, e.g. "DA"), never CPT/CDT content.
104
+ _CPT_OR_CDT = re.compile(r"^(\d|D)[0-9A-Z]{4}$")
105
+
106
+
107
+ def _download(url: str) -> tuple[bytes, str, str]:
108
+ name = url.rsplit("/", 1)[-1]
109
+ cached = CACHE / name
110
+ meta = CACHE / (name + ".meta.json")
111
+ if cached.exists() and meta.exists():
112
+ info = json.loads(meta.read_text())
113
+ return cached.read_bytes(), info["sha256"], info.get("last_modified", "")
114
+ print(f"fetching {url}")
115
+ result = fetch(url, timeout=300.0)
116
+ cached.write_bytes(result.content)
117
+ last_modified = result.headers.get("last-modified", "")
118
+ meta.write_text(json.dumps({"sha256": result.sha256, "last_modified": last_modified}))
119
+ return result.content, result.sha256, last_modified
120
+
121
+
122
+ def _write_table(path: Path, header: list[str], rows: list[list[str]]) -> tuple[str, int]:
123
+ buf = io.StringIO()
124
+ writer = csv.writer(buf, lineterminator="\n")
125
+ writer.writerow(header)
126
+ writer.writerows(rows)
127
+ raw = buf.getvalue().encode("utf-8")
128
+ canonical = hashlib.sha256(raw).hexdigest()
129
+ with path.open("wb") as fh:
130
+ with gzip.GzipFile(fileobj=fh, mode="wb", mtime=0) as gz:
131
+ gz.write(raw)
132
+ return canonical, len(rows)
133
+
134
+
135
+ def _parse_cm(text: str) -> list[list[str]]:
136
+ rows = []
137
+ for line in text.splitlines():
138
+ if not line.strip():
139
+ continue
140
+ code = line[:8].strip()
141
+ desc = line[8:].strip()
142
+ rows.append([code, desc])
143
+ return rows
144
+
145
+
146
+ def _parse_hcpcs(text: str) -> tuple[list[list[str]], int]:
147
+ rows: list[list[str]] = []
148
+ excluded = 0
149
+ for line in text.splitlines():
150
+ if not line.strip():
151
+ continue
152
+ line = line.ljust(320)
153
+ code = line[0:5].strip() # modifiers sit in cols 4-5 with cols 1-3 blank
154
+ if _CPT_OR_CDT.match(code):
155
+ excluded += 1
156
+ continue
157
+ rows.append(
158
+ [
159
+ code,
160
+ line[5:10].strip(),
161
+ line[10:11].strip(),
162
+ line[11:91].strip(),
163
+ line[91:119].strip(),
164
+ line[268:276].strip(),
165
+ line[276:284].strip(),
166
+ line[284:292].strip(),
167
+ line[292:293].strip(),
168
+ ]
169
+ )
170
+ return rows, excluded
171
+
172
+
173
+ def main() -> None:
174
+ fetched_at = datetime.now(timezone.utc).date().isoformat()
175
+ manifest: dict = {
176
+ "route": "codes",
177
+ "generated_at": fetched_at,
178
+ "regenerate": "python hc_source/data/codes/regenerate.py",
179
+ "canonicalization": (
180
+ "canonical_sha256 = SHA-256 of the uncompressed CSV bytes (UTF-8, LF line "
181
+ "endings, header row first, source row order preserved). Drift decisions "
182
+ "compare canonical hashes; raw zip sha256 values are kept for forensics only."
183
+ ),
184
+ "cpt_exclusion": (
185
+ "Only the ANWEB fixed-width master file is ingested from the HCPCS zip; "
186
+ "proc_notes_*.txt and all .xlsx members are excluded (CPT mentions / "
187
+ "unverified content). Numeric-leading (CPT Level I) and D-prefixed (ADA CDT) "
188
+ "records are dropped defensively."
189
+ ),
190
+ "cm_april_status": CM_APRIL_STATUS,
191
+ "cm_releases": {},
192
+ "hcpcs_releases": {},
193
+ }
194
+
195
+ cm_tables: dict[str, str] = {} # inner sha -> table filename, for dedupe
196
+ for label, url, basename, eff_from, eff_to in CM_RELEASES:
197
+ content, zip_sha, last_modified = _download(url)
198
+ with zipfile.ZipFile(io.BytesIO(content)) as zf:
199
+ matches = [n for n in zf.namelist() if n.rsplit("/", 1)[-1] == basename]
200
+ if len(matches) != 1:
201
+ raise SystemExit(f"expected exactly one {basename} in {url}, got {matches}")
202
+ member = matches[0]
203
+ inner = zf.read(member)
204
+ inner_sha = hashlib.sha256(inner).hexdigest()
205
+ entry = {
206
+ "source_url": url,
207
+ "source_zip_sha256": zip_sha,
208
+ "source_last_modified": last_modified,
209
+ "inner_file": member,
210
+ "inner_sha256": inner_sha,
211
+ "fetched_at": fetched_at,
212
+ "effective_from": eff_from,
213
+ "effective_to": eff_to,
214
+ }
215
+ if inner_sha in cm_tables:
216
+ entry["table"] = cm_tables[inner_sha]
217
+ entry["content_identical_to"] = next(
218
+ k for k, v in manifest["cm_releases"].items() if v["table"] == cm_tables[inner_sha]
219
+ )
220
+ ref = manifest["cm_releases"][entry["content_identical_to"]]
221
+ entry["canonical_sha256"] = ref["canonical_sha256"]
222
+ entry["row_count"] = ref["row_count"]
223
+ else:
224
+ table_name = f"icd10cm_{label.replace('-', '_')}.csv.gz"
225
+ rows = _parse_cm(inner.decode("utf-8", errors="strict"))
226
+ canonical, count = _write_table(HERE / table_name, ["code", "description"], rows)
227
+ entry.update({"table": table_name, "canonical_sha256": canonical, "row_count": count})
228
+ cm_tables[inner_sha] = table_name
229
+ manifest["cm_releases"][label] = entry
230
+ print(f"{label}: {entry['row_count']} rows -> {entry['table']}")
231
+
232
+ for label, url, member_re, eff_from, eff_to in HCPCS_RELEASES:
233
+ content, zip_sha, last_modified = _download(url)
234
+ with zipfile.ZipFile(io.BytesIO(content)) as zf:
235
+ members = [n for n in zf.namelist() if member_re.match(n)]
236
+ if len(members) != 1:
237
+ raise SystemExit(f"expected exactly one ANWEB master in {url}, got {members}")
238
+ inner = zf.read(members[0])
239
+ inner_sha = hashlib.sha256(inner).hexdigest()
240
+ rows, excluded = _parse_hcpcs(inner.decode("latin-1"))
241
+ table_name = f"hcpcs_{label}.csv.gz"
242
+ canonical, count = _write_table(
243
+ HERE / table_name,
244
+ [
245
+ "code",
246
+ "seq",
247
+ "record_id",
248
+ "long_description",
249
+ "short_description",
250
+ "code_added",
251
+ "action_effective",
252
+ "termination",
253
+ "action_code",
254
+ ],
255
+ rows,
256
+ )
257
+ manifest["hcpcs_releases"][label] = {
258
+ "source_url": url,
259
+ "source_zip_sha256": zip_sha,
260
+ "source_last_modified": last_modified,
261
+ "inner_file": members[0],
262
+ "inner_sha256": inner_sha,
263
+ "fetched_at": fetched_at,
264
+ "effective_from": eff_from,
265
+ "effective_to": eff_to,
266
+ "table": table_name,
267
+ "canonical_sha256": canonical,
268
+ "row_count": count,
269
+ "cpt_cdt_records_excluded": excluded,
270
+ }
271
+ print(f"{label}: {count} rows ({excluded} CPT/CDT excluded) -> {table_name}")
272
+
273
+ listing = fetch(CDC_ROOT, timeout=60.0)
274
+ fixture = HERE.parent / "cdc_root_listing.html"
275
+ fixture.write_bytes(listing.content)
276
+ manifest["cdc_root_listing"] = {
277
+ "source_url": CDC_ROOT,
278
+ "raw_sha256": listing.sha256,
279
+ "fetched_at": fetched_at,
280
+ "fixture": "tests/fixtures/codes/cdc_root_listing.html",
281
+ }
282
+ print(f"cdc listing: {len(listing.content)} bytes -> {fixture}")
283
+
284
+ (HERE / "manifest.json").write_text(
285
+ json.dumps(manifest, indent=2, sort_keys=False) + "\n", encoding="utf-8"
286
+ )
287
+ print("wrote manifest.json")
288
+
289
+
290
+ if __name__ == "__main__":
291
+ main()
Binary file