add comprehensive test suite, CI/CD quality gates, and package publishing
Some checks failed
ci/woodpecker/push/deploy Pipeline failed
ci/woodpecker/push/ci Pipeline failed

- 5519 unit tests covering all modules (aco, bcda, bls, cms, pfs, rex, bib)
- ruff lint + format enforcement across entire codebase (377 files reformatted)
- pre-commit hook: ruff check, ruff format, pytest
- Woodpecker CI split into ci.yml (quality gate) and deploy.yml (package + images)
- ci.yml: lint → test → validate-compose, runs on every push/PR
- deploy.yml: build + publish Python package to Gitea PyPI registry, then
  container image builds, Trivy scans, and registry push (main branch only)
- Gitea branch protection on main: requires CI status checks to pass
- .gitignore updated for .coverage, dist/, *.egg-info/
- grafana config moved to dev/grafana/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kert
2026-02-28 14:58:48 -05:00
parent 3d4797606d
commit 8f99eeb154
659 changed files with 45005 additions and 2833 deletions

3
.gitignore vendored
View File

@@ -10,3 +10,6 @@ notebooks/__marimo__/
.env
.deploy_key.pub
**__pycache__**
.coverage
dist/
*.egg-info/

33
.woodpecker/ci.yml Normal file
View File

@@ -0,0 +1,33 @@
# ── Quality gate ─────────────────────────────────────────────────
# Runs on every push and PR. Must pass before merge to main.
# Gitea branch protection requires this pipeline's status checks.
when:
- event: [push, pull_request, manual]
steps:
- name: lint
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
commands:
- uv sync --dev
- uv pip install -e .
- uv run ruff check src/ tests/ --output-format=concise
- uv run ruff format --check src/ tests/
- name: test
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
commands:
- uv sync --dev
- uv pip install -e .
- uv run pytest tests/ --no-cov --tb=short -q
depends_on:
- lint
- name: validate-compose
image: docker:cli
volumes:
- /run/user/1000/docker.sock:/var/run/docker.sock
commands:
- docker compose config --quiet
depends_on:
- lint

View File

@@ -1,13 +1,34 @@
# ── Deploy ───────────────────────────────────────────────────────
# Builds the Python package, pushes it to Gitea's PyPI registry,
# then builds, scans, and pushes container images.
# Only runs on pushes to main (i.e. after PR merge).
when:
- event: [push, pull_request, manual]
- event: push
branch: main
steps:
- name: validate
image: docker:cli
volumes:
- /run/user/1000/docker.sock:/var/run/docker.sock
- name: build-package
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
commands:
- docker compose config --quiet
# Stamp a unique version: base version + .dev{commit_count}
- COMMIT_NUM=$(git rev-list --count HEAD)
- BASE=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
- uv version "$BASE.dev$COMMIT_NUM" --no-sync
- uv build --out-dir dist/
- ls -lh dist/
- name: publish-package
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
environment:
REGISTRY_USER:
from_secret: registry_user
REGISTRY_PASS:
from_secret: registry_pass
commands:
- uv publish --publish-url http://gitea:3000/api/packages/homelab/pypi --username "$REGISTRY_USER" --password "$REGISTRY_PASS" dist/*
depends_on:
- build-package
- name: build-notebooks
image: docker:cli
@@ -36,6 +57,8 @@ steps:
commands:
- trivy image --severity HIGH,CRITICAL --exit-code 0 --format table localhost:3000/homelab/notebooks:${CI_COMMIT_SHA:0:8}
- trivy image --severity HIGH,CRITICAL --format json -o notebooks-scan.json localhost:3000/homelab/notebooks:${CI_COMMIT_SHA:0:8}
depends_on:
- build-notebooks
when:
- path: "notebooks/**"
@@ -46,6 +69,8 @@ steps:
commands:
- trivy image --severity HIGH,CRITICAL --exit-code 0 --format table localhost:3000/homelab/zotero:${CI_COMMIT_SHA:0:8}
- trivy image --severity HIGH,CRITICAL --format json -o zotero-scan.json localhost:3000/homelab/zotero:${CI_COMMIT_SHA:0:8}
depends_on:
- build-zotero
when:
- path: "zotero/**"
@@ -61,6 +86,9 @@ steps:
source: "*.json"
target: /ci/${CI_REPO}/${CI_COMMIT_SHA:0:8}/
path_style: true
depends_on:
- scan-notebooks
- scan-zotero
when:
- path:
- "notebooks/**"
@@ -81,20 +109,6 @@ steps:
- docker push localhost:3000/homelab/notebooks:latest || true
- docker push localhost:3000/homelab/zotero:${CI_COMMIT_SHA:0:8} || true
- docker push localhost:3000/homelab/zotero:latest || true
when:
- event: push
branch: main
# Deploy step disabled - use rebuild-all pipeline for full deploy
# The deploy step was causing the CI to redeploy itself and crash
# - name: deploy
# image: docker:cli
# volumes:
# - /run/user/1000/docker.sock:/var/run/docker.sock
# commands:
# - docker compose pull notebooks zotero
# - docker compose up -d notebooks zotero
# when:
# - event: push
# branch: main
depends_on:
- scan-notebooks
- scan-zotero

View File

@@ -0,0 +1,195 @@
"""Add 2010 and 2017 PFS carrier files to Zotero library.
Downloads from CMS, extracts, and creates Zotero items with proper
tags and storage-linked attachments.
Usage:
uv run python dev/add_carrier_to_zotero.py
"""
import os
import random
import shutil
import sqlite3
import string
import subprocess
import zipfile
from datetime import datetime, timezone
from pathlib import Path
ZOTERO_DB = "zotero/data/zotero.sqlite"
ZOTERO_STORAGE = "zotero/data/storage"
# Files already downloaded to /tmp
CARRIER_FILES = {
2010: {
"zip": "/tmp/pfs_carrier_dl/cy2010_carrier.zip",
"extracted": "/tmp/pfs_carrier_dl/2010",
"url": "https://www.cms.gov/medicare/medicare-fee-for-service-payment/physicianfeesched/downloads/cy2010qtr1carrierfiles3.zip",
"title": "CY 2010 PFS Carrier — Cy2010 Carrier Files",
},
2017: {
"zip": "/tmp/pfs_carrier_dl/cy2017_carrier.zip",
"extracted": "/tmp/pfs_carrier_dl/2017",
"url": "https://www.cms.gov/medicare/medicare-fee-for-service-payment/physicianfeesched/downloads/cy2017-carrierfiles.zip",
"title": "CY 2017 PFS Carrier — Cy2017 Carrier Files",
},
}
# Zotero schema constants
ITEM_TYPE_WEBPAGE = 40
ITEM_TYPE_ATTACHMENT = 3
FIELD_TITLE = 1
FIELD_DATE = 6
FIELD_URL = 10
FIELD_ACCESS_DATE = 11
FIELD_WEBSITE_TYPE = 42
FIELD_WEBSITE_TITLE = 123
TAG_MODULE_PFS = 8
TAG_YEARS = {2010: 36, 2017: 20}
def _zotero_key() -> str:
"""Generate a random 8-char Zotero key (uppercase + digits)."""
chars = string.ascii_uppercase + string.digits
return "".join(random.choices(chars, k=8))
def _get_or_create_value(db: sqlite3.Connection, value: str) -> int:
"""Get or create an itemDataValues row, return valueID."""
row = db.execute(
"SELECT valueID FROM itemDataValues WHERE value = ?", (value,)
).fetchone()
if row:
return row[0]
cur = db.execute("INSERT INTO itemDataValues (value) VALUES (?)", (value,))
return cur.lastrowid
def _next_item_id(db: sqlite3.Connection) -> int:
return db.execute("SELECT max(itemID) + 1 FROM items").fetchone()[0]
def add_carrier_year(db: sqlite3.Connection, year: int, info: dict) -> None:
"""Add one carrier year to Zotero: parent item + file attachments."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# --- Create parent item (webpage) ---
parent_id = _next_item_id(db)
parent_key = _zotero_key()
db.execute(
"""INSERT INTO items (itemID, itemTypeID, dateAdded, dateModified,
key, version, synced, libraryID)
VALUES (?, ?, ?, ?, ?, 0, 0, 1)""",
(parent_id, ITEM_TYPE_WEBPAGE, now, now, parent_key),
)
# Set fields: title, date, url, accessDate, websiteType, websiteTitle
fields = {
FIELD_TITLE: info["title"],
FIELD_DATE: f"{year}-01-01",
FIELD_URL: info["url"],
FIELD_ACCESS_DATE: now,
FIELD_WEBSITE_TYPE: "Government Data Portal",
FIELD_WEBSITE_TITLE: "Centers for Medicare & Medicaid Services",
}
for field_id, value in fields.items():
value_id = _get_or_create_value(db, value)
db.execute(
"INSERT INTO itemData (itemID, fieldID, valueID) VALUES (?, ?, ?)",
(parent_id, field_id, value_id),
)
# Tags: module:pfs + year:YYYY
db.execute(
"INSERT INTO itemTags (itemID, tagID, type) VALUES (?, ?, 0)",
(parent_id, TAG_MODULE_PFS),
)
db.execute(
"INSERT INTO itemTags (itemID, tagID, type) VALUES (?, ?, 0)",
(parent_id, TAG_YEARS[year]),
)
print(f"Created parent item: {parent_key} ({info['title']})")
# --- Create attachments for each .TXT and .pdf file ---
extracted_dir = Path(info["extracted"])
files = sorted(extracted_dir.iterdir())
att_count = 0
for filepath in files:
if filepath.suffix.upper() not in (".TXT", ".PDF"):
continue
att_id = _next_item_id(db)
att_key = _zotero_key()
# Copy file to Zotero storage (owned by container uid 100999)
storage_dir = Path(ZOTERO_STORAGE) / att_key
subprocess.run(["sudo", "mkdir", "-p", str(storage_dir)], check=True)
dest = storage_dir / filepath.name
subprocess.run(["sudo", "cp", str(filepath), str(dest)], check=True)
subprocess.run(
["sudo", "chown", "-R", "100999:100999", str(storage_dir)],
check=True,
)
# Determine content type
ext = filepath.suffix.upper()
content_type = "text/plain" if ext == ".TXT" else "application/pdf"
# Create item record
db.execute(
"""INSERT INTO items (itemID, itemTypeID, dateAdded, dateModified,
key, version, synced, libraryID)
VALUES (?, ?, ?, ?, ?, 0, 0, 1)""",
(att_id, ITEM_TYPE_ATTACHMENT, now, now, att_key),
)
# Create attachment record (linkMode=0 = imported file)
db.execute(
"""INSERT INTO itemAttachments
(itemID, parentItemID, linkMode, contentType, path)
VALUES (?, ?, 0, ?, ?)""",
(att_id, parent_id, content_type, f"storage:{filepath.name}"),
)
# Set title field on attachment
value_id = _get_or_create_value(db, filepath.name)
db.execute(
"INSERT INTO itemData (itemID, fieldID, valueID) VALUES (?, ?, ?)",
(att_id, FIELD_TITLE, value_id),
)
att_count += 1
print(f" Added {att_count} attachments for year {year}")
def main() -> None:
# Verify extracted files exist
for year, info in CARRIER_FILES.items():
extracted = Path(info["extracted"])
if not extracted.exists():
print(f"Extracting {info['zip']}...")
with zipfile.ZipFile(info["zip"]) as zf:
zf.extractall(extracted)
txt_count = len(list(extracted.glob("*.TXT")))
print(f"Year {year}: {txt_count} .TXT files ready")
db = sqlite3.connect(ZOTERO_DB)
try:
for year, info in sorted(CARRIER_FILES.items()):
add_carrier_year(db, year, info)
db.commit()
print("\nDone. Committed to Zotero database.")
except Exception:
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,233 @@
"""Add ZIP code carrier locality files to Zotero library.
Downloads from CMS, extracts, and creates Zotero items with proper
tags and storage-linked attachments.
Usage:
uv run python dev/add_zipcode_to_zotero.py
"""
import random
import sqlite3
import string
import subprocess
import zipfile
from datetime import datetime, timezone
from pathlib import Path
ZOTERO_DB = "zotero/data/zotero.sqlite"
ZOTERO_STORAGE = "zotero/data/storage"
ZIPCODE_FILES = {
2016: {
"zip": "/tmp/pfs_zipcode/zipcode_2016.zip",
"url": "https://www.cms.gov/medicare/medicare-fee-for-service-payment/prospmedicarefeesvcpmtgen/downloads/2016-yearend-zipcodefile.zip",
"title": "CY 2016 PFS Zip Carrier Locality — 2016 Year End",
},
2017: {
"zip": "/tmp/pfs_zipcode/zipcode_2017.zip",
"url": "https://www.cms.gov/medicare/medicare-fee-for-service-payment/prospmedicarefeesvcpmtgen/downloads/end-of-year-zip-code.zip",
"title": "CY 2017 PFS Zip Carrier Locality — 2017 Year End",
},
2018: {
"zip": "/tmp/pfs_zipcode/zipcode_2018.zip",
"url": "https://www.cms.gov/medicare/medicare-fee-for-service-payment/prospmedicarefeesvcpmtgen/downloads/2018-yearend-zipcodefile.zip",
"title": "CY 2018 PFS Zip Carrier Locality — 2018 Year End",
},
2019: {
"zip": "/tmp/pfs_zipcode/zipcode_2019.zip",
"url": "https://www.cms.gov/files/zip/2019-end-year-zip-code-file.zip",
"title": "CY 2019 PFS Zip Carrier Locality — 2019 Year End",
},
2020: {
"zip": "/tmp/pfs_zipcode/zipcode_2020.zip",
"url": "https://www.cms.gov/files/zip/2020-end-year-zip-code-file.zip",
"title": "CY 2020 PFS Zip Carrier Locality — 2020 Year End",
},
2021: {
"zip": "/tmp/pfs_zipcode/zipcode_2021.zip",
"url": "https://www.cms.gov/files/zip/2021-end-year-zip-code-file-revised-05/27/2022.zip",
"title": "CY 2021 PFS Zip Carrier Locality — 2021 Year End",
},
2022: {
"zip": "/tmp/pfs_zipcode/zipcode_2022.zip",
"url": "https://www.cms.gov/files/zip/2022-end-year-zip-code-file.zip",
"title": "CY 2022 PFS Zip Carrier Locality — 2022 Year End",
},
2023: {
"zip": "/tmp/pfs_zipcode/zipcode_2023.zip",
"url": "https://www.cms.gov/files/zip/2023-end-year-zip-code-file.zip",
"title": "CY 2023 PFS Zip Carrier Locality — 2023 Year End",
},
2024: {
"zip": "/tmp/pfs_zipcode/zipcode_2024.zip",
"url": "https://www.cms.gov/files/zip/2024-end-year-zip-code-file.zip",
"title": "CY 2024 PFS Zip Carrier Locality — 2024 Year End",
},
2025: {
"zip": "/tmp/pfs_zipcode/zipcode_2025.zip",
"url": "https://www.cms.gov/files/zip/2025-end-year-zip-code-file.zip",
"title": "CY 2025 PFS Zip Carrier Locality — 2025 Year End",
},
2026: {
"zip": "/tmp/pfs_zipcode/zipcode_2026.zip",
"url": "https://www.cms.gov/files/zip/zip-code-carrier-locality-file-revised-11-18-2025.zip",
"title": "CY 2026 PFS Zip Carrier Locality — January 2026",
},
}
# Zotero schema constants
ITEM_TYPE_WEBPAGE = 40
ITEM_TYPE_ATTACHMENT = 3
FIELD_TITLE = 1
FIELD_DATE = 6
FIELD_URL = 10
FIELD_ACCESS_DATE = 11
FIELD_WEBSITE_TYPE = 42
FIELD_WEBSITE_TITLE = 123
TAG_MODULE_PFS = 8
def _zotero_key() -> str:
chars = string.ascii_uppercase + string.digits
return "".join(random.choices(chars, k=8))
def _get_or_create_value(db: sqlite3.Connection, value: str) -> int:
row = db.execute(
"SELECT valueID FROM itemDataValues WHERE value = ?", (value,)
).fetchone()
if row:
return row[0]
cur = db.execute("INSERT INTO itemDataValues (value) VALUES (?)", (value,))
return cur.lastrowid
def _get_or_create_tag(db: sqlite3.Connection, name: str) -> int:
row = db.execute("SELECT tagID FROM tags WHERE name = ?", (name,)).fetchone()
if row:
return row[0]
cur = db.execute("INSERT INTO tags (name) VALUES (?)", (name,))
return cur.lastrowid
def _next_item_id(db: sqlite3.Connection) -> int:
return db.execute("SELECT max(itemID) + 1 FROM items").fetchone()[0]
def add_zipcode_year(db: sqlite3.Connection, year: int, info: dict) -> None:
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Extract ZIP
extract_dir = Path(f"/tmp/pfs_zipcode/ex_{year}")
extract_dir.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(info["zip"]) as zf:
zf.extractall(extract_dir)
# Create parent item
parent_id = _next_item_id(db)
parent_key = _zotero_key()
db.execute(
"""INSERT INTO items (itemID, itemTypeID, dateAdded, dateModified,
key, version, synced, libraryID)
VALUES (?, ?, ?, ?, ?, 0, 0, 1)""",
(parent_id, ITEM_TYPE_WEBPAGE, now, now, parent_key),
)
fields = {
FIELD_TITLE: info["title"],
FIELD_DATE: f"{year}-01-01",
FIELD_URL: info["url"],
FIELD_ACCESS_DATE: now,
FIELD_WEBSITE_TYPE: "Government Data Portal",
FIELD_WEBSITE_TITLE: "Centers for Medicare & Medicaid Services",
}
for field_id, value in fields.items():
value_id = _get_or_create_value(db, value)
db.execute(
"INSERT INTO itemData (itemID, fieldID, valueID) VALUES (?, ?, ?)",
(parent_id, field_id, value_id),
)
# Tags: module:pfs + year:YYYY
db.execute(
"INSERT INTO itemTags (itemID, tagID, type) VALUES (?, ?, 0)",
(parent_id, TAG_MODULE_PFS),
)
year_tag_id = _get_or_create_tag(db, f"year:{year}")
db.execute(
"INSERT INTO itemTags (itemID, tagID, type) VALUES (?, ?, 0)",
(parent_id, year_tag_id),
)
print(f"Created parent item: {parent_key} ({info['title']})")
# Add attachments for .txt and .xlsx files
att_count = 0
for filepath in sorted(extract_dir.iterdir()):
ext = filepath.suffix.upper()
if ext not in (".TXT", ".XLSX"):
continue
att_id = _next_item_id(db)
att_key = _zotero_key()
storage_dir = Path(ZOTERO_STORAGE) / att_key
subprocess.run(["sudo", "mkdir", "-p", str(storage_dir)], check=True)
dest = storage_dir / filepath.name
subprocess.run(["sudo", "cp", str(filepath), str(dest)], check=True)
subprocess.run(
["sudo", "chown", "-R", "100999:100999", str(storage_dir)],
check=True,
)
ct_map = {
".TXT": "text/plain",
".XLSX": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
}
content_type = ct_map.get(ext, "application/octet-stream")
db.execute(
"""INSERT INTO items (itemID, itemTypeID, dateAdded, dateModified,
key, version, synced, libraryID)
VALUES (?, ?, ?, ?, ?, 0, 0, 1)""",
(att_id, ITEM_TYPE_ATTACHMENT, now, now, att_key),
)
db.execute(
"""INSERT INTO itemAttachments
(itemID, parentItemID, linkMode, contentType, path)
VALUES (?, ?, 0, ?, ?)""",
(att_id, parent_id, content_type, f"storage:{filepath.name}"),
)
value_id = _get_or_create_value(db, filepath.name)
db.execute(
"INSERT INTO itemData (itemID, fieldID, valueID) VALUES (?, ?, ?)",
(att_id, FIELD_TITLE, value_id),
)
att_count += 1
print(f" Added {att_count} attachments for year {year}")
def main() -> None:
for year, info in sorted(ZIPCODE_FILES.items()):
zp = Path(info["zip"])
if not zp.exists():
print(f"ERROR: {zp} not found — download first")
return
db = sqlite3.connect(ZOTERO_DB)
try:
for year, info in sorted(ZIPCODE_FILES.items()):
add_zipcode_year(db, year, info)
db.commit()
print("\nDone. Committed to Zotero database.")
except Exception:
db.rollback()
raise
finally:
db.close()
if __name__ == "__main__":
main()

View File

327
dev/generate_cms_express.py Normal file
View File

@@ -0,0 +1,327 @@
"""Generate CMS express modules from table model introspection.
Reads every ``src/cms/table/*.py`` model and emits one
``@nw.narwhalify`` function per table grouped into domain modules
under ``src/cms/express/``.
Each generated function calls ``auto_cast(df)`` which inspects
column names at runtime and casts string columns matching CMS
numeric naming conventions to Float64.
Usage::
uv run python dev/generate_cms_express.py
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
# ── Constants ────────────────────────────────────────────────────
SRC = Path("src")
TABLE_DIR = SRC / "cms" / "table"
EXPRESS_DIR = SRC / "cms" / "express"
# ── Domain grouping ──────────────────────────────────────────────
DOMAIN_RULES: list[tuple[str, list[str]]] = [
(
"aco",
[
"accountable_care_organization",
"aco_reach_",
"number_accountable_care",
"county_level_aggregate_expenditure",
"performance_year_financial",
"reach_acos",
"advance_investment_payment",
"pioneer_aco",
"value_modifier",
],
),
(
"enrollment",
[
"medicare_monthly_enrollment",
"program_statistics_medicare_total_enrollment",
"program_statistics_original_medicare_enrollment",
"program_statistics_medicare_advantage_other_health_plan_enrollment",
"program_statistics_medicare_part_d_enrollment",
"program_statistics_medicare_newly_enrolled",
"program_statistics_medicare_medicaid_dual_enrollment",
"program_statistics_medicare_deaths",
"program_statistics_medicare_premiums",
"medicare_fee_service_public_provider_enrollment",
],
),
(
"provider",
[
"medicare_physician_other_practitioners",
"medicare_part_d_prescribers",
"physician_supplier_procedure",
"order_referring",
"opt_out_affidavits",
"fiscal_intermediary_shared_system",
"pending_initial_logging_tracking",
"revalidation_",
"provider_services_file_",
"public_reporting_missing",
"managing_clinician_",
"quality_payment_program",
"restructured_betos",
"medicare_provider_supplier_taxonomy",
"medicare_clinical_laboratory",
"medicare_fee_service_comprehensive",
],
),
(
"facility",
[
"hospital_",
"skilled_nursing_facility_",
"home_health_agency_",
"hospice_",
"federally_qualified_health_center_",
"rural_health_clinic_",
"long_term_care_facility",
"facility_level_minimum_data_set",
"minimum_data_set_frequency",
"nursing_home_chain",
"payroll_based_journal",
"deficit_reduction_act",
"end_stage_renal_disease",
"medicare_dialysis_facilities",
"home_infusion_therapy",
"income_asset_ownership",
"opioid_treatment_program",
],
),
(
"drug_spending",
[
"medicare_part_b_spending",
"medicare_part_b_discarded",
"medicare_part_d_spending",
"medicare_quarterly_part_b",
"medicare_quarterly_part_d",
"medicaid_spending_by_drug",
"medicaid_opioid_",
"medicare_part_d_opioid",
"monthly_prescription_drug",
"quarterly_prescription_drug",
],
),
(
"utilization",
[
"medicare_inpatient_hospitals",
"medicare_outpatient_hospitals",
"medicare_durable_medical_equipment",
"medicare_post_acute_care",
"medicare_geographic_variation",
"medicare_advantage_geographic_variation",
"medicare_telehealth",
"medicare_covid_19",
"medicare_current_beneficiary_survey",
"specialty_",
"beneficiary_",
"post_acute_care",
"ambulatory_surgery_center",
],
),
(
"market",
[
"market_saturation",
"medicare_advantage_",
"medicare_plan_finder",
"prescription_drug_plan_",
"medicaid_managed_care",
"medicare_demonstrations",
"medicare_diabetes",
],
),
(
"innovation",
[
"innovation_center_",
"cpc_initiative_",
"comprehensive_care_joint_replacement",
"kidney_care_choices",
"ambulatory_specialty_model",
"strong_start_",
"agency_healthcare_research",
],
),
(
"program_statistics",
[
"program_statistics_medicare_inpatient",
"program_statistics_medicare_outpatient",
"program_statistics_medicare_home_health",
"program_statistics_medicare_hospice",
"program_statistics_medicare_skilled_nursing",
"program_statistics_medicare_physician",
"program_statistics_medicare_part_d",
"program_statistics_medicare_part_part_b",
"program_statistics_medicare_providers",
"program_statistics_medicare_advantage_inpatient",
"program_statistics_medicare_advantage_outpatient",
"program_statistics_medicare_advantage_physician",
"program_statistics_medicare_advantage_skilled",
],
),
]
def classify_domain(table_stem: str) -> str:
"""Assign a table to a domain module."""
for domain, prefixes in DOMAIN_RULES:
for prefix in prefixes:
if table_stem.startswith(prefix) or prefix in table_stem:
return domain
return "other"
# ── Table introspection ──────────────────────────────────────────
def parse_table_file(path: Path) -> dict | None:
"""Extract class name, tablename, and field count from a table module."""
content = path.read_text()
class_match = re.search(r"class (\w+)\(SQLTable\):", content)
if not class_match:
return None
class_name = class_match.group(1)
tn_match = re.search(r'__tablename__\s*=\s*"([^"]+)"', content)
tablename = tn_match.group(1) if tn_match else path.stem
field_count = len(re.findall(r" \w+: (?:str|date) \| None", content))
return {
"class_name": class_name,
"tablename": tablename,
"stem": path.stem,
"field_count": field_count,
}
# ── Code generation ──────────────────────────────────────────────
def generate_function(table: dict) -> str:
"""Generate a single @nw.narwhalify function for a table."""
tablename = table["tablename"]
param = f"cms__{tablename}"
field_count = table["field_count"]
lines = [
"@nw.narwhalify",
f"def {tablename}({param}: FrameT) -> FrameT:",
f' """Clean and type-cast cms.{tablename}.',
f"",
f" {field_count} fields. Numeric columns auto-cast to Float64.",
f' """',
f" return auto_cast({param})",
]
return "\n".join(lines)
def generate_module(domain: str, tables: list[dict]) -> str:
"""Generate a full express module for a domain group."""
lines = [
f'"""CMS express — {domain} domain.',
f"",
f"Auto-generated narwhals functions for {len(tables)} CMS tables.",
f"Each function takes the raw table and returns a typed frame",
f"with numeric columns cast to Float64.",
f'"""',
f"",
f"from __future__ import annotations",
f"",
f"import narwhals as nw",
f"from narwhals.typing import FrameT",
f"",
f"from cms.express._helpers import auto_cast",
]
for tbl in sorted(tables, key=lambda t: t["tablename"]):
lines.append("")
lines.append("")
lines.append(generate_function(tbl))
lines.append("")
return "\n".join(lines)
def generate_init(domains: dict[str, list[dict]]) -> str:
"""Generate src/cms/express/__init__.py."""
lines = [
'"""CMS express layer — narwhals transformation functions.',
"",
"Auto-generated pure functions that clean, type-cast, and",
"standardize all 150 CMS public datasets.",
"",
"Each domain module provides one function per table:",
"",
]
for domain in sorted(domains):
count = len(domains[domain])
lines.append(f"- **{domain}**: {count} tables")
lines.append('"""')
lines.append("")
for domain in sorted(domains):
lines.append(f"from . import {domain} as {domain}")
lines.append("")
return "\n".join(lines)
# ── Main ─────────────────────────────────────────────────────────
def main() -> None:
tables = []
for f in sorted(TABLE_DIR.glob("*.py")):
if f.name == "__init__.py":
continue
info = parse_table_file(f)
if info and info["field_count"] >= 2:
tables.append(info)
print(f"Parsed {len(tables)} table models")
domains: dict[str, list[dict]] = {}
for tbl in tables:
domain = classify_domain(tbl["stem"])
domains.setdefault(domain, []).append(tbl)
for domain, tbls in sorted(domains.items()):
print(f" {domain}: {len(tbls)} tables")
EXPRESS_DIR.mkdir(parents=True, exist_ok=True)
for domain, tbls in domains.items():
module_path = EXPRESS_DIR / f"{domain}.py"
content = generate_module(domain, tbls)
module_path.write_text(content)
print(f" wrote {module_path} ({len(tbls)} functions)")
init_path = EXPRESS_DIR / "__init__.py"
content = generate_init(domains)
init_path.write_text(content)
print(f" wrote {init_path}")
total_fns = sum(len(v) for v in domains.values())
print(f"\nDone: {total_fns} functions in {len(domains)} modules")
if __name__ == "__main__":
sys.path.insert(0, str(SRC))
main()

0
dev/scrape_bls_data.py Normal file
View File

590
dev/scrape_cms_data.py Normal file
View File

@@ -0,0 +1,590 @@
"""Scrape all public datasets from data.cms.gov and generate SQLTable models.
Fetches the DCAT catalog at ``https://data.cms.gov/data.json``, discovers
column schemas via the ``data-viewer`` endpoint, generates Pydantic
SQLTable models under ``src/cms/table/``, and optionally downloads the
raw CSV data to ``data/cms/raw/``.
Usage::
uv run python dev/scrape_cms_data.py # generate models + download CSVs
uv run python dev/scrape_cms_data.py --no-download # models only
uv run python dev/scrape_cms_data.py --force # re-download existing CSVs
"""
from __future__ import annotations
import argparse
import json
import re
import zipfile
from pathlib import Path
from urllib.request import Request, urlopen
CATALOG_URL = "https://data.cms.gov/data.json"
DATA_VIEWER_URL = "https://data.cms.gov/data-api/v1/dataset/{uuid}/data-viewer"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
TABLE_DIR = PROJECT_ROOT / "src" / "cms" / "table"
RAW_DIR = PROJECT_ROOT / "data" / "cms" / "raw"
_STOP_WORDS = {
"a",
"an",
"and",
"for",
"in",
"of",
"on",
"the",
"to",
"with",
}
# ---------------------------------------------------------------------------
# Catalog + schema fetching
# ---------------------------------------------------------------------------
def _get_json(url: str) -> dict | list:
"""GET a URL and parse JSON response."""
req = Request(url, headers={"User-Agent": "cms-scraper/1.0"})
with urlopen(req, timeout=60) as resp:
return json.loads(resp.read())
def fetch_catalog() -> list[dict]:
"""Fetch the DCAT catalog and return the dataset list."""
catalog = _get_json(CATALOG_URL)
return catalog.get("dataset", [])
def extract_uuid(dataset: dict) -> str | None:
"""Extract the dataset UUID from the identifier URL."""
ident = dataset.get("identifier", "")
m = re.search(r"/dataset/([0-9a-f-]{36})/", ident)
return m.group(1) if m else None
def fetch_schema(uuid: str) -> dict[str, str] | None:
"""Fetch column types from the data-viewer endpoint.
Returns dict like ``{"ACO_ID": "TEXT", "Start_Date": "DATE"}``,
or None if the endpoint is unavailable.
"""
url = DATA_VIEWER_URL.format(uuid=uuid)
try:
data = _get_json(url)
except Exception:
return None
# csvColumnTypes lives at meta.data_file_meta_data.csvColumnTypes
meta = data.get("meta", {})
file_meta = meta.get("data_file_meta_data", {})
raw = file_meta.get("csvColumnTypes")
if not raw:
return None
if isinstance(raw, str):
return json.loads(raw)
return raw
# ---------------------------------------------------------------------------
# Name sanitization
# ---------------------------------------------------------------------------
def sanitize_table_name(title: str) -> str:
"""Convert dataset title to a snake_case table name.
>>> sanitize_table_name("Accountable Care Organization Participants")
'aco_participants'
>>> sanitize_table_name("CMS Program Statistics - Medicare Advantage - Inpatient Hospital")
'program_statistics_medicare_advantage_inpatient_hospital'
"""
name = title.strip()
# Strip leading "CMS " prefix
name = re.sub(r"^CMS\s+", "", name)
# Remove parenthetical content like "(AHRQ)" and "(PSI-11)"
name = re.sub(r"\([^)]*\)", " ", name)
# Replace non-alphanumeric chars with spaces
name = re.sub(r"[^a-zA-Z0-9]+", " ", name)
# Split into words, drop stop words, rejoin
words = name.split()
words = [w for w in words if w.lower() not in _STOP_WORDS]
name = "_".join(words)
# Collapse multiple underscores, lowercase
name = re.sub(r"_+", "_", name).strip("_").lower()
return name
def sanitize_class_name(title: str) -> str:
"""Convert dataset title to a PascalCase class name.
>>> sanitize_class_name("Accountable Care Organization Participants")
'AcoParticipants'
"""
name = title.strip()
name = re.sub(r"^CMS\s+", "", name)
# Remove parenthetical content
name = re.sub(r"\([^)]*\)", " ", name)
# Replace non-alphanumeric chars with spaces
name = re.sub(r"[^a-zA-Z0-9]+", " ", name)
words = name.split()
words = [w for w in words if w.lower() not in _STOP_WORDS]
parts = []
for w in words:
parts.append(w.capitalize())
return "".join(parts)
def sanitize_column_name(name: str) -> str:
"""Convert API column name to a valid Python field name.
>>> sanitize_column_name("ACO_ID")
'aco_id'
>>> sanitize_column_name("SNF_3-Day_Rule_Waiver")
'snf_3_day_rule_waiver'
"""
name = name.strip()
# Replace hyphens and spaces with underscores
name = re.sub(r"[-\s]+", "_", name)
# Remove any non-ASCII and non-alphanumeric chars (except underscore)
name = re.sub(r"[^a-zA-Z0-9_]", "", name)
name = name.lower()
# Prefix with underscore if starts with digit
if name and name[0].isdigit():
name = "_" + name
# Avoid shadowing Python builtins and imported types
if name in {
"date",
"type",
"id",
"list",
"set",
"dict",
"str",
"int",
"float",
"bool",
"bytes",
"hash",
"format",
}:
name = name + "_"
return name
def map_column_type(cms_type: str) -> str:
"""Map CMS csvColumnTypes value to Python type string.
TEXT → str, NUMERIC → str (codes, phones, zips), DATE → date.
"""
cms_type = cms_type.strip().upper()
if cms_type == "DATE":
return "date"
return "str"
# ---------------------------------------------------------------------------
# Code generation
# ---------------------------------------------------------------------------
def generate_table_module(
class_name: str,
table_name: str,
columns: dict[str, str],
title: str,
uuid: str,
) -> str:
"""Generate a Python module with a single SQLTable class."""
has_date = any(map_column_type(t) == "date" for t in columns.values())
lines = [
f'"""{class_name}{title}.',
"",
"Auto-generated from CMS Data API.",
f"UUID: {uuid}",
f"Source: https://data.cms.gov/data-api/v1/dataset/{uuid}/data",
'"""',
"",
"from __future__ import annotations",
"",
]
if has_date:
lines.append("from datetime import date")
lines.append("")
lines.extend(
[
"from aco.table.base import SQLTable",
"",
"",
f"class {class_name}(SQLTable):",
f' """{title}.',
"",
f" {len(columns)} fields from data.cms.gov.",
' """',
"",
' __schema__ = "cms"',
f' __tablename__ = "{table_name}"',
]
)
for col_name, col_type in columns.items():
py_name = sanitize_column_name(col_name)
py_type = map_column_type(col_type)
lines.append("")
lines.append(f" {py_name}: {py_type} | None = None")
lines.append("")
return "\n".join(lines)
def generate_init_module(
tables: list[tuple[str, str, str]],
) -> str:
"""Generate src/cms/table/__init__.py.
tables: list of (module_name, class_name, table_name) tuples.
"""
sorted_tables = sorted(tables)
lines = [
'"""CMS table models — auto-generated by dev/scrape_cms_data.py."""',
"",
]
for mod, cls, _tbl in sorted_tables:
lines.append(f"from cms.table.{mod} import {cls}")
lines.append("")
lines.append("__all__ = [")
for _mod, cls, _tbl in sorted_tables:
lines.append(f' "{cls}",')
lines.append("]")
lines.append("")
return "\n".join(lines)
def generate_namespace_init(tables: list[tuple[str, str, str]]) -> str:
"""Generate src/cms/__init__.py."""
table_list = "\n".join(
f" - {cls} (cms.{tbl})" for _mod, cls, tbl in sorted(tables)
)
return f'''"""CMS — Public datasets from data.cms.gov.
Auto-generated from the CMS Data API catalog.
Contains SQLTable models for all publicly available datasets.
Datasets cover Medicare Shared Savings Program (MSSP),
ACO REACH, CMS Program Statistics, and other CMS programs.
Tables::
{table_list}
Usage::
from cms.table import AcoParticipants
"""
'''
# ---------------------------------------------------------------------------
# CSV download
# ---------------------------------------------------------------------------
def get_download_url(dataset: dict) -> str | None:
"""Extract the best download URL from dataset distributions.
Prefers the most recent CSV. Falls back to ZIP.
"""
csv_urls = []
zip_urls = []
for dist in dataset.get("distribution", []):
url = dist.get("downloadURL")
if not url:
continue
media = (dist.get("mediaType") or "").lower()
if media == "text/csv" or url.endswith(".csv"):
csv_urls.append(url)
elif media == "application/zip" or url.endswith(".zip"):
zip_urls.append(url)
# Return last CSV (most recent) or last ZIP
if csv_urls:
return csv_urls[-1]
if zip_urls:
return zip_urls[-1]
return None
def download_file(url: str, dest: Path, *, force: bool = False) -> int:
"""Download a file from URL to dest. Returns file size in bytes."""
if dest.exists() and not force:
print(f" Skipping (exists): {dest.name}")
return dest.stat().st_size
dest.parent.mkdir(parents=True, exist_ok=True)
req = Request(url, headers={"User-Agent": "cms-scraper/1.0"})
with urlopen(req, timeout=600) as resp:
data = resp.read()
dest.write_bytes(data)
return len(data)
def download_and_extract_zip(
url: str,
dest_dir: Path,
*,
force: bool = False,
) -> list[Path]:
"""Download ZIP and extract CSVs/Excel files to dest_dir."""
dest_dir.mkdir(parents=True, exist_ok=True)
zip_path = dest_dir / "archive.zip"
if not zip_path.exists() or force:
req = Request(url, headers={"User-Agent": "cms-scraper/1.0"})
with urlopen(req, timeout=300) as resp:
zip_path.write_bytes(resp.read())
extracted = []
with zipfile.ZipFile(zip_path) as zf:
for name in zf.namelist():
if name.lower().endswith((".csv", ".xlsx", ".xls")):
zf.extract(name, dest_dir)
extracted.append(dest_dir / name)
return extracted
def infer_schema_from_csv(csv_path: Path) -> dict[str, str] | None:
"""Infer column types from a CSV file header + first rows.
Returns dict like {"COL_NAME": "TEXT", ...}.
Used as fallback when data-viewer endpoint is unavailable.
"""
import csv
try:
with open(csv_path, newline="", encoding="utf-8-sig") as f:
reader = csv.DictReader(f)
if not reader.fieldnames:
return None
# Read up to 100 rows for type inference
rows = []
for i, row in enumerate(reader):
rows.append(row)
if i >= 99:
break
result = {}
for col in reader.fieldnames:
values = [r[col] for r in rows if r.get(col)]
col_type = _infer_type(values)
result[col] = col_type
return result
except Exception:
return None
def infer_schema_from_excel(xlsx_path: Path) -> dict[str, str] | None:
"""Infer column types from an Excel file header + first rows.
Handles CMS xlsx files that have multi-row title headers by
scanning for the first row with 3+ non-null cells as the header.
Tries all sheets to handle workbooks where the first sheet is a
table of contents.
"""
try:
import openpyxl
wb = openpyxl.load_workbook(xlsx_path, read_only=True)
for ws in wb.worksheets:
rows_iter = ws.iter_rows(values_only=True)
header = None
for row in rows_iter:
non_null = [c for c in row if c is not None]
if len(non_null) >= 3:
header = row
break
if not header:
continue
cols = [str(h).strip() for h in header if h is not None]
sample = []
for i, row in enumerate(rows_iter):
sample.append(row)
if i >= 99:
break
result = {}
for j, col in enumerate(cols):
values = [str(r[j]) for r in sample if j < len(r) and r[j] is not None]
result[col] = _infer_type(values)
wb.close()
return result
wb.close()
return None
except Exception:
return None
def _infer_type(values: list[str]) -> str:
"""Infer CMS-style type from a list of string values."""
from datetime import datetime
if not values:
return "TEXT"
date_count = 0
for v in values:
try:
datetime.strptime(v.strip(), "%Y-%m-%d")
date_count += 1
except (ValueError, AttributeError):
pass
if date_count > len(values) * 0.8:
return "DATE"
return "TEXT"
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="Scrape CMS data.gov datasets and generate SQLTable models"
)
parser.add_argument(
"--no-download",
action="store_true",
help="Skip CSV downloads, only generate table models",
)
parser.add_argument(
"--force",
action="store_true",
help="Re-download CSVs even if they already exist",
)
args = parser.parse_args()
print("Fetching CMS catalog...")
datasets = fetch_catalog()
print(f"Found {len(datasets)} datasets\n")
TABLE_DIR.mkdir(parents=True, exist_ok=True)
RAW_DIR.mkdir(parents=True, exist_ok=True)
tables: list[tuple[str, str, str]] = [] # (module_name, class_name, table_name)
skipped: list[str] = []
for ds in datasets:
title = ds.get("title", "Unknown")
uuid = extract_uuid(ds)
print(f"--- {title}")
if not uuid:
print(" SKIP: no UUID found")
skipped.append(title)
continue
# Fetch schema from data-viewer endpoint
schema = fetch_schema(uuid)
# Fallback: download CSV and infer schema
if not schema and not args.no_download:
dl_url = get_download_url(ds)
if dl_url and dl_url.endswith(".csv"):
csv_dest = RAW_DIR / f"{sanitize_table_name(title)}.csv"
print(" No data-viewer; downloading CSV for schema...")
download_file(dl_url, csv_dest, force=args.force)
schema = infer_schema_from_csv(csv_dest)
elif dl_url and dl_url.endswith(".zip"):
zip_dir = RAW_DIR / sanitize_table_name(title)
print(" No data-viewer; downloading ZIP for schema...")
csvs = download_and_extract_zip(
dl_url,
zip_dir,
force=args.force,
)
if csvs:
# Try all files, pick schema with most columns
best = None
for f in csvs:
if f.suffix.lower() in (".xlsx", ".xls"):
s = infer_schema_from_excel(f)
else:
s = infer_schema_from_csv(f)
if s and (not best or len(s) > len(best)):
best = s
schema = best
if not schema:
print(" SKIP: could not determine schema")
skipped.append(title)
continue
table_name = sanitize_table_name(title)
class_name = sanitize_class_name(title)
# Generate table module
code = generate_table_module(
class_name,
table_name,
schema,
title,
uuid,
)
module_path = TABLE_DIR / f"{table_name}.py"
module_path.write_text(code)
tables.append((table_name, class_name, table_name))
print(f" Generated: {module_path.name} ({len(schema)} columns)")
# Download CSV data
if not args.no_download:
dl_url = get_download_url(ds)
if dl_url:
if dl_url.endswith(".zip"):
zip_dir = RAW_DIR / table_name
download_and_extract_zip(
dl_url,
zip_dir,
force=args.force,
)
print(f" Downloaded: {table_name}/ (ZIP)")
else:
csv_dest = RAW_DIR / f"{table_name}.csv"
size = download_file(
dl_url,
csv_dest,
force=args.force,
)
mb = size / 1024 / 1024
print(f" Downloaded: {csv_dest.name} ({mb:.1f} MB)")
else:
print(" No download URL available")
# Generate __init__.py files
init_code = generate_init_module(tables)
(TABLE_DIR / "__init__.py").write_text(init_code)
ns_init = generate_namespace_init(tables)
(TABLE_DIR.parent / "__init__.py").write_text(ns_init)
# Summary
print(f"\n{'=' * 60}")
print(f"Generated {len(tables)} table models in src/cms/table/")
for mod, cls, tbl in sorted(tables):
print(f" cms.{tbl}{cls}")
if skipped:
print(f"\nSkipped {len(skipped)} datasets:")
for s in skipped:
print(f" - {s}")
print(f"{'=' * 60}")
if __name__ == "__main__":
main()

View File

@@ -9,15 +9,31 @@ dependencies = [
"databricks-sdk>=0.85.0",
"fsspec>=2024.1.0",
"httpx>=0.28.1",
"narwhals>=2.17.0",
"pyarrow>=23.0.0",
"sqlglot>=26.0.0",
]
[dependency-groups]
dev = [
"coverage>=7.13.4",
"dbt-core==1.10.15",
"dbt-duckdb>=1.10,<1.11",
"polars>=1.38.1",
"pytest>=9.0.2",
"pytest-cov>=7.0.0",
"ruff>=0.11.0",
]
[tool.ruff]
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"]
ignore = ["E501", "E741"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.uv.build-backend]
namespace = true
source-exclude = ["compose.yml","grafana/**","nginx/**","prometheus/**","rustfs/**","data/**", "loki/**", "notebooks/**", "traefik/**", "woodpecker/**", "gitea/**", "polaris/**", "trino/**", "zotero/**", "tuva/**"]

View File

@@ -320,6 +320,7 @@ def int_pqi_01_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 01 (diabetes short-term complications), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -327,6 +328,7 @@ def int_pqi_01_denom(
def int_pqi_01_exclusions(
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
) -> FrameT:
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
@@ -337,6 +339,7 @@ def int_pqi_01_num(
ahrq_measures___int_pqi_01_denom: FrameT,
ahrq_measures___int_pqi_01_exclusions: FrameT,
) -> FrameT:
"""Diabetes short-term complications diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -357,6 +360,7 @@ def int_pqi_03_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 03 (diabetes long-term complications), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -364,6 +368,7 @@ def int_pqi_03_denom(
def int_pqi_03_exclusions(
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
) -> FrameT:
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
@@ -374,6 +379,7 @@ def int_pqi_03_num(
ahrq_measures___int_pqi_03_denom: FrameT,
ahrq_measures___int_pqi_03_exclusions: FrameT,
) -> FrameT:
"""Diabetes long-term complications diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -394,6 +400,7 @@ def int_pqi_05_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 05 (COPD/asthma), age >= 40."""
return _pqi_denom(
ahrq_measures___stg_pqi_member_months,
core__patient,
@@ -524,6 +531,7 @@ def int_pqi_07_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 07 (hypertension), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -641,6 +649,7 @@ def int_pqi_07_num(
ahrq_measures___int_pqi_07_denom: FrameT,
ahrq_measures___int_pqi_07_exclusions: FrameT,
) -> FrameT:
"""Hypertension diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -661,6 +670,7 @@ def int_pqi_08_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 08 (heart failure), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -721,6 +731,7 @@ def int_pqi_08_num(
ahrq_measures___int_pqi_08_denom: FrameT,
ahrq_measures___int_pqi_08_exclusions: FrameT,
) -> FrameT:
"""Heart failure diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -741,6 +752,7 @@ def int_pqi_11_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 11 (bacterial pneumonia), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -854,6 +866,7 @@ def int_pqi_11_num(
ahrq_measures___int_pqi_11_denom: FrameT,
ahrq_measures___int_pqi_11_exclusions: FrameT,
) -> FrameT:
"""Community-acquired bacterial pneumonia diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -874,6 +887,7 @@ def int_pqi_12_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 12 (urinary tract infection), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -987,6 +1001,7 @@ def int_pqi_12_num(
ahrq_measures___int_pqi_12_denom: FrameT,
ahrq_measures___int_pqi_12_exclusions: FrameT,
) -> FrameT:
"""Urinary tract infection diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -1007,6 +1022,7 @@ def int_pqi_14_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 14 (uncontrolled diabetes), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
@@ -1014,6 +1030,7 @@ def int_pqi_14_denom(
def int_pqi_14_exclusions(
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
) -> FrameT:
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
@@ -1024,6 +1041,7 @@ def int_pqi_14_num(
ahrq_measures___int_pqi_14_denom: FrameT,
ahrq_measures___int_pqi_14_exclusions: FrameT,
) -> FrameT:
"""Uncontrolled diabetes diagnosis, in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -1044,6 +1062,7 @@ def int_pqi_15_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 15 (asthma, younger adults), age 1839."""
return _pqi_denom(
ahrq_measures___stg_pqi_member_months,
core__patient,
@@ -1112,6 +1131,7 @@ def int_pqi_15_num(
ahrq_measures___int_pqi_15_denom: FrameT,
ahrq_measures___int_pqi_15_exclusions: FrameT,
) -> FrameT:
"""Asthma diagnosis (age 1839), in denom, not excluded."""
return _pqi_num(
ahrq_measures___stg_pqi_inpatient_encounter,
ahrq_measures___value_set_pqi,
@@ -1132,6 +1152,7 @@ def int_pqi_16_denom(
ahrq_measures___stg_pqi_member_months: FrameT,
core__patient: FrameT,
) -> FrameT:
"""Eligible members for PQI 16 (lower-extremity amputation), all ages."""
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)

View File

@@ -38,7 +38,6 @@ from typing import Callable
from pydantic import BaseModel, ConfigDict, Field, field_validator
from aco.pipe.runner import _param_to_table
from aco.table.base import SQLTable
from bib.tag import Tag
@@ -94,6 +93,8 @@ class Expr(BaseModel):
@property
def inputs(self) -> list[str]:
"""Derive input table refs from ``fn``'s parameter signature."""
from aco.pipe.runner import _param_to_table # lazy to avoid circular import
sig = inspect.signature(self.fn)
return [_param_to_table(p) for p in sig.parameters]

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
from narwhals.typing import FrameT
import narwhals as nw
from narwhals.typing import FrameT
@nw.narwhalify
def stg_claims_member_months(
claims_preprocessing__member_months: FrameT,
input_layer__input_layer__provider_attribution: FrameT
input_layer__input_layer__provider_attribution: FrameT,
) -> FrameT:
"""Build core._stg_claims_member_months

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from narwhals.typing import FrameT
import narwhals as nw
from narwhals.typing import FrameT
@nw.narwhalify
@@ -62,7 +63,10 @@ def stg_observation(core__observation: FrameT) -> FrameT:
nw.lit("clinical source").alias("payer"),
nw.col("observation_date"),
nw.col("result"),
nw.col("normalized_code_type").fill_null(nw.col("source_code_type")).str.to_lowercase().alias("code_type"),
nw.col("normalized_code_type")
.fill_null(nw.col("source_code_type"))
.str.to_lowercase()
.alias("code_type"),
nw.col("normalized_code").fill_null(nw.col("source_code")).alias("code"),
nw.col("data_source"),
)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from narwhals.typing import FrameT
import narwhals as nw
from narwhals.typing import FrameT
@nw.narwhalify
@@ -218,7 +219,7 @@ def input_layer__procedure(df: FrameT) -> FrameT:
@nw.narwhalify
def input_layer__provider_attribution(
input_layer__provider_attribution: FrameT
input_layer__provider_attribution: FrameT,
) -> FrameT:
"""Build input_layer.input_layer__provider_attribution

View File

@@ -45,7 +45,9 @@ from .context import Context as Context
from .context import DuckDBContext as DuckDBContext
from .context import EnterpriseContext as EnterpriseContext
from .context import IcebergContext as IcebergContext
from .context import ParquetContext as ParquetContext
from .context import TrinoContext as TrinoContext
from .context import WriteMode as WriteMode
from .engine import execute as execute
from .transpile import transpile as transpile

View File

@@ -2,8 +2,10 @@
Two complementary roles:
1. **Schema catalog** — discovers ``SQLTable`` models from ``aco.table``
to know what columns a table *should* have (the contract).
1. **Schema catalog** — discovers ``SQLTable`` models from all namespace
table packages (``aco.table``, ``cms.table``, ``bcda.table``,
``ccw.table``, ``pfs.table``) to know what columns a table *should*
have (the contract).
2. **Iceberg catalog** — talks to an Iceberg REST Catalog (Nessie,
Polaris, Unity Catalog) to know what tables *actually exist* in
@@ -122,24 +124,23 @@ class Catalog:
# ── Schema catalog (aco.table introspection) ─────────────────
def schemas(self) -> list[str]:
"""Return sorted schema names from ``aco.table``.
"""Return sorted schema names across all namespace table packages.
Discovers all modules in ``aco.table`` package and extracts
the ``__schema__`` attribute from ``SQLTable`` subclasses.
Discovers modules in every namespace listed in
``_TABLE_NAMESPACES`` and extracts the ``__schema__``
attribute from ``SQLTable`` subclasses.
Returns
-------
list[str]
Sorted unique schema names like ``['core', 'readmissions', ...]``.
Sorted unique schema names like ``['bcda', 'core', 'cms', ...]``.
"""
schemas = set()
# Discover all table modules
for module_name in self._discover_table_modules():
for ns, module_name in self._discover_table_modules():
try:
module = importlib.import_module(f"aco.table.{module_name}")
module = importlib.import_module(f"{ns}.table.{module_name}")
# Find all SQLTable subclasses in the module
for name, obj in inspect.getmembers(module, inspect.isclass):
if (
issubclass(obj, SQLTable)
@@ -149,7 +150,6 @@ class Catalog:
):
schemas.add(obj.__schema__)
except Exception:
# Skip modules that fail to import
continue
return sorted(schemas)
@@ -169,9 +169,9 @@ class Catalog:
"""
tables = []
for module_name in self._discover_table_modules():
for ns, module_name in self._discover_table_modules():
try:
module = importlib.import_module(f"aco.table.{module_name}")
module = importlib.import_module(f"{ns}.table.{module_name}")
for name, obj in inspect.getmembers(module, inspect.isclass):
if (
@@ -184,7 +184,6 @@ class Catalog:
):
qualified = f"{obj.__schema__}.{obj.__tablename__}"
tables.append(qualified)
# Cache the model for later use
self._model_cache[qualified] = obj
except Exception:
continue
@@ -235,10 +234,10 @@ class Catalog:
schema, table = table_ref.split(".", 1)
# Search for the model
for module_name in self._discover_table_modules():
# Search for the model across all namespaces
for ns, module_name in self._discover_table_modules():
try:
module = importlib.import_module(f"aco.table.{module_name}")
module = importlib.import_module(f"{ns}.table.{module_name}")
for name, obj in inspect.getmembers(module, inspect.isclass):
if (
@@ -290,23 +289,35 @@ class Catalog:
return self.column_map[table_ref].get(column_name, column_name)
return column_name
def _discover_table_modules(self) -> list[str]:
"""Discover all Python modules in aco.table package.
# Namespaces whose ``table`` packages contain SQLTable models.
# All share ``aco.table.base.SQLTable`` as base class.
_TABLE_NAMESPACES: list[str] = ["aco", "bcda", "ccw", "cms", "pfs"]
def _discover_table_modules(self) -> list[tuple[str, str]]:
"""Discover all Python modules across all namespace table packages.
Scans each namespace in ``_TABLE_NAMESPACES`` that has a
``{namespace}.table`` package importable.
Returns
-------
list[str]
Module names (without aco.table prefix).
list[tuple[str, str]]
``(namespace, module_name)`` pairs, e.g.
``[("aco", "core"), ("cms", "accountable_care_organizations"), ...]``.
"""
import importlib
import pkgutil
import aco.table as table_pkg
modules = []
for importer, modname, ispkg in pkgutil.iter_modules(table_pkg.__path__):
if not modname.startswith("_"):
modules.append(modname)
return modules
results: list[tuple[str, str]] = []
for ns in self._TABLE_NAMESPACES:
try:
pkg = importlib.import_module(f"{ns}.table")
except ImportError:
continue
for _, modname, _ in pkgutil.iter_modules(pkg.__path__):
if not modname.startswith("_"):
results.append((ns, modname))
return results
# ── Iceberg catalog (REST API via PyIceberg) ─────────────────

View File

@@ -66,13 +66,23 @@ Usage::
from __future__ import annotations
from typing import Any
from typing import Any, Literal
import narwhals as nw
from pydantic import BaseModel, ConfigDict
from aco.lake.catalog import Catalog
WriteMode = Literal["append", "replace"]
"""How ``save`` writes data.
- ``"append"`` — insert rows into an existing table (default).
Creates the table on first write. Preserves lineage and
Iceberg snapshots.
- ``"replace"`` — drop and recreate the table (cold start / force).
Use only for bootstrapping or schema changes.
"""
class Context(BaseModel):
"""Base class for all storage contexts.
@@ -80,6 +90,9 @@ class Context(BaseModel):
Subclasses implement ``load`` and ``save`` for their specific
storage backend. ``load`` returns a narwhals-compatible DataFrame;
``save`` accepts one and persists it.
The default write mode is ``"append"`` — pipeline runs add rows,
not replace tables. Pass ``mode="replace"`` for cold starts.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -99,7 +112,13 @@ class Context(BaseModel):
"""
raise NotImplementedError
def save(self, table_ref: str, df: Any) -> None:
def save(
self,
table_ref: str,
df: Any,
*,
mode: WriteMode = "append",
) -> None:
"""Write a DataFrame back to storage.
Parameters
@@ -108,6 +127,9 @@ class Context(BaseModel):
Qualified table name in ``schema.table`` format.
df : DataFrame
A narwhals-compatible DataFrame to persist.
mode : WriteMode
``"append"`` (default) inserts rows into an existing table,
creating it if needed. ``"replace"`` drops and recreates.
"""
raise NotImplementedError
@@ -217,7 +239,13 @@ class DuckDBContext(Context):
f"Failed to load table {physical_ref} from {self.database}: {e}"
) from e
def save(self, table_ref: str, df: Any) -> None:
def save(
self,
table_ref: str,
df: Any,
*,
mode: WriteMode = "append",
) -> None:
"""Write a DataFrame to DuckDB.
Parameters
@@ -226,6 +254,9 @@ class DuckDBContext(Context):
Qualified table name (e.g. ``readmissions.encounter``).
df : DataFrame
Narwhals DataFrame to persist.
mode : WriteMode
``"append"`` (default) inserts rows, creating the table
on first write. ``"replace"`` drops and recreates.
Raises
------
@@ -253,7 +284,6 @@ class DuckDBContext(Context):
# Apply column mapping if catalog provided
df_to_save = df
if self.catalog and table_ref in self.catalog.column_map:
# Rename canonical columns to physical names
rename_dict = {
canonical: physical
for canonical, physical in self.catalog.column_map[table_ref].items()
@@ -263,21 +293,24 @@ class DuckDBContext(Context):
if rename_dict:
df_to_save = df.rename(rename_dict)
# Get connection
con = self._get_connection()
# Convert to polars for DuckDB write
pl_df = nw.to_native(df_to_save)
pl_df = nw.to_native(df_to_save) # noqa: F841 used in SQL
try:
# Create schema if it doesn't exist
con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
# Drop table if exists and recreate (replace mode)
con.execute(f'DROP TABLE IF EXISTS "{schema}"."{table}"')
# Write table
con.execute(f'CREATE TABLE "{schema}"."{table}" AS SELECT * FROM pl_df')
if mode == "replace":
con.execute(f'DROP TABLE IF EXISTS "{schema}"."{table}"')
con.execute(f'CREATE TABLE "{schema}"."{table}" AS SELECT * FROM pl_df')
else:
# Append: create if not exists, then insert
try:
con.execute(f'INSERT INTO "{schema}"."{table}" SELECT * FROM pl_df')
except Exception:
# Table doesn't exist yet — create it
con.execute(
f'CREATE TABLE "{schema}"."{table}" AS SELECT * FROM pl_df'
)
except Exception as e:
raise RuntimeError(
@@ -310,6 +343,179 @@ class DuckDBContext(Context):
pass
# ── Parquet context ──────────────────────────────────────────────────
class ParquetContext(Context):
"""Read/write parquet files on local disk or S3-compatible storage.
Resolves ``schema.table`` to a file path using a configurable
template. Uses polars for IO (which delegates to fsspec for
remote paths like ``s3://``).
Suitable for: flat-file exports, S3 data lakes without a catalog,
lightweight local development without DuckDB.
Usage::
from aco.lake import ParquetContext
# Local filesystem
ctx = ParquetContext(base_path="./data")
encounters = ctx.load("core.encounter")
# S3 with credentials
ctx = ParquetContext(
base_path="s3://my-bucket/warehouse",
storage_options={"key": "...", "secret": "..."},
)
encounters = ctx.load("core.encounter")
# Custom path pattern (e.g. hive-style partitions)
ctx = ParquetContext(
base_path="./data",
pattern="{schema}/{table}/*.parquet",
)
"""
base_path: str
"""Root path for parquet files (local or ``s3://``, ``gs://``, etc.)."""
pattern: str = "{schema}/{table}.parquet"
"""Path template. ``{schema}`` and ``{table}`` are substituted
from the qualified table reference. Supports globs for reads
(e.g. ``{schema}/{table}/*.parquet``)."""
storage_options: dict[str, str] = {}
"""Extra kwargs passed to polars/fsspec for remote storage.
Common keys for S3:
- ``key`` / ``secret`` — AWS credentials
- ``endpoint_url`` — custom S3 endpoint (MinIO, RustFS)
- ``region`` — AWS region
"""
catalog: Catalog | None = None
"""Optional Catalog for schema/column mapping."""
def _resolve_path(self, table_ref: str) -> str:
"""Build file path from table reference."""
physical_ref = table_ref
if self.catalog:
physical_ref = self.catalog.physical_table_ref(table_ref)
schema, table = physical_ref.split(".", 1)
rel = self.pattern.format(schema=schema, table=table)
base = self.base_path.rstrip("/")
return f"{base}/{rel}"
def load(self, table_ref: str) -> Any:
"""Read parquet file(s) and return a narwhals DataFrame.
Parameters
----------
table_ref : str
Qualified table name (e.g. ``core.encounter``).
Returns
-------
DataFrame
Narwhals-wrapped polars DataFrame.
"""
if "." not in table_ref:
raise ValueError(f"Table ref must be qualified (schema.table): {table_ref}")
import polars as pl
path = self._resolve_path(table_ref)
try:
df = pl.read_parquet(
path,
storage_options=self.storage_options or None,
)
except Exception as e:
raise RuntimeError(f"Failed to load parquet {path}: {e}") from e
# Apply column mapping if catalog provided
if self.catalog and table_ref in self.catalog.column_map:
reverse_map = {
physical: canonical
for canonical, physical in self.catalog.column_map[table_ref].items()
}
rename_dict = {
col: reverse_map[col] for col in df.columns if col in reverse_map
}
if rename_dict:
df = df.rename(rename_dict)
return nw.from_native(df)
def save(
self,
table_ref: str,
df: Any,
*,
mode: WriteMode = "append",
) -> None:
"""Write a DataFrame as parquet.
Parameters
----------
table_ref : str
Qualified table name (e.g. ``core.encounter``).
df : DataFrame
Narwhals DataFrame to persist.
mode : WriteMode
``"append"`` (default) concatenates with existing parquet.
``"replace"`` overwrites the file entirely.
"""
if "." not in table_ref:
raise ValueError(f"Table ref must be qualified (schema.table): {table_ref}")
import polars as pl
path = self._resolve_path(table_ref)
# Apply column mapping if catalog provided
df_to_save = df
if self.catalog and table_ref in self.catalog.column_map:
rename_dict = {
canonical: physical
for canonical, physical in self.catalog.column_map[table_ref].items()
if canonical in df.columns
}
if rename_dict:
df_to_save = df.rename(rename_dict)
pl_df = nw.to_native(df_to_save)
# Append: read existing, concat, write back
if mode == "append":
try:
existing = pl.read_parquet(
path,
storage_options=self.storage_options or None,
)
pl_df = pl.concat([existing, pl_df])
except Exception:
pass # File doesn't exist yet — first write
# Create parent directories for local paths
if not path.startswith(("s3://", "gs://", "az://", "abfs://")):
from pathlib import Path
Path(path).parent.mkdir(parents=True, exist_ok=True)
try:
pl_df.write_parquet(
path,
storage_options=self.storage_options or None,
)
except Exception as e:
raise RuntimeError(f"Failed to save parquet {path}: {e}") from e
# ── Iceberg contexts ────────────────────────────────────────────────
@@ -427,7 +633,13 @@ class IcebergContext(Context):
f"Failed to load Iceberg table {physical_ref}: {e}"
) from e
def save(self, table_ref: str, df: Any) -> None:
def save(
self,
table_ref: str,
df: Any,
*,
mode: WriteMode = "append",
) -> None:
"""Write a DataFrame to Iceberg.
Parameters
@@ -436,6 +648,9 @@ class IcebergContext(Context):
Qualified table name.
df : DataFrame
Narwhals DataFrame to persist.
mode : WriteMode
``"append"`` (default) adds rows as a new snapshot.
``"replace"`` overwrites all data in the table.
"""
# Map to physical table if catalog provided
physical_ref = table_ref
@@ -459,8 +674,6 @@ class IcebergContext(Context):
df_to_save = df.rename(rename_dict)
# Convert to arrow
import polars as pl
pl_df = nw.to_native(df_to_save)
arrow_table = pl_df.to_arrow()
@@ -471,14 +684,15 @@ class IcebergContext(Context):
# Load or create table
try:
table = pyice_cat.load_table((*namespace, table_name))
# Append to existing table
table.append(arrow_table)
if mode == "replace":
table.overwrite(arrow_table)
else:
table.append(arrow_table)
except Exception:
# Table doesn't exist, create it
# Table doesn't exist create it
from pyiceberg.schema import Schema
from pyiceberg.types import NestedField
# Convert arrow schema to Iceberg schema
fields = []
for i, field in enumerate(arrow_table.schema):
fields.append(
@@ -492,18 +706,14 @@ class IcebergContext(Context):
schema = Schema(*fields)
# Create namespace if needed
try:
pyice_cat.create_namespace(namespace)
except Exception:
pass # Namespace already exists
# Create table
table = pyice_cat.create_table(
identifier=(*namespace, table_name), schema=schema
)
# Write data
table.append(arrow_table)
except Exception as e:

View File

@@ -67,7 +67,9 @@ from aco.lake.context import (
DuckDBContext,
EnterpriseContext,
IcebergContext,
ParquetContext,
TrinoContext,
WriteMode,
)
from aco.pipe.base import Pipeline
@@ -77,6 +79,7 @@ def execute(
context: Context,
save_outputs: bool = False,
output_filter: set[str] | None = None,
mode: WriteMode = "append",
) -> dict[str, Any]:
"""Run a pipeline against a storage context.
@@ -97,6 +100,9 @@ def execute(
If True, persist results back to storage via ``context.save``.
output_filter : set[str] | None
If provided, only save tables in this set. If None, save all.
mode : WriteMode
``"append"`` (default) inserts rows additively.
``"replace"`` drops and recreates (cold start / force).
Returns
-------
@@ -128,10 +134,10 @@ def execute(
)
"""
# Dispatch based on context type
if isinstance(context, (DuckDBContext, IcebergContext)):
return _execute_direct(pipeline, context, save_outputs, output_filter)
if isinstance(context, (DuckDBContext, IcebergContext, ParquetContext)):
return _execute_direct(pipeline, context, save_outputs, output_filter, mode)
elif isinstance(context, (TrinoContext, EnterpriseContext)):
return _execute_transpiled(pipeline, context, save_outputs, output_filter)
return _execute_transpiled(pipeline, context, save_outputs, output_filter, mode)
else:
raise TypeError(f"Unsupported context type: {type(context)}")
@@ -141,6 +147,7 @@ def _execute_direct(
context: Context,
save_outputs: bool = False,
output_filter: set[str] | None = None,
mode: WriteMode = "append",
) -> dict[str, Any]:
"""Run express functions locally via narwhals.
@@ -148,53 +155,36 @@ def _execute_direct(
1. ``context.load(table_ref)`` → narwhals DataFrame
2. ``pipeline.run(context.load)`` → cache of results
3. Optionally ``context.save(ref, df)`` for each result
For ``IcebergContext``, reads go through PyIceberg::
catalog.load_table("ns.table").scan().to_arrow()
→ polars.from_arrow() → narwhals.from_native()
Writes create new Iceberg snapshots::
narwhals → arrow → table.append(arrow_table)
For ``DuckDBContext``, reads go through DuckDB SQL::
con.execute("SELECT * FROM schema.table").pl()
→ narwhals.from_native()
3. Optionally ``context.save(ref, df, mode=mode)`` for each result
Parameters
----------
pipeline : Pipeline
Pipeline to execute.
context : Context
DuckDBContext or IcebergContext.
DuckDBContext, IcebergContext, or ParquetContext.
save_outputs : bool
Whether to persist results back to storage.
output_filter : set[str] | None
If provided, only save tables in this set.
mode : WriteMode
Passed through to ``context.save()``.
Returns
-------
dict[str, Any]
Cache of all computed DataFrames by table name.
"""
# Execute pipeline using context's load function
# The pipeline.run() method handles dependency resolution
# and calls context.load() for inputs as needed
results = pipeline.run(context.load)
# Optionally save outputs
if save_outputs:
tables_to_save = output_filter if output_filter else set(pipeline.names())
for table_ref in tables_to_save:
if table_ref in results:
try:
context.save(table_ref, results[table_ref])
context.save(table_ref, results[table_ref], mode=mode)
except Exception as e:
# Log error but don't fail entire execution
print(f"Warning: Failed to save {table_ref}: {e}")
return results
@@ -205,6 +195,7 @@ def _execute_transpiled(
context: Context,
save_outputs: bool = False,
output_filter: set[str] | None = None,
mode: WriteMode = "append",
) -> dict[str, Any]:
"""Transpile express functions to SQL and push to remote engine.
@@ -247,20 +238,8 @@ def _execute_transpiled(
NotImplementedError
Transpilation is not yet implemented.
"""
import duckdb
from aco.lake.transpile import transpile
# Transpilation requires a DuckDB connection with source schemas
# to create relations that capture SQL through narwhals.
if not hasattr(context, "_duckdb_path") or not context._duckdb_path:
raise ValueError(
"Transpiled execution requires a DuckDB path for schema reference. "
"Set context._duckdb_path or use transpile() directly."
)
con = duckdb.connect(context._duckdb_path, read_only=True)
# Determine target dialect and catalog from context
dialect = "databricks"
catalog_name = ""
@@ -271,13 +250,15 @@ def _execute_transpiled(
dialect = "trino"
catalog_name = context.catalog
# Map write mode to SQL output mode
output_mode = "ctas" if mode == "replace" else "insert"
# con=None → transpile auto-creates schema-only in-memory DuckDB
sql_map = transpile(
pipeline,
con,
target_dialect=dialect,
catalog=catalog_name,
output_mode="ctas",
output_mode=output_mode,
)
con.close()
return sql_map

View File

@@ -5,12 +5,24 @@ to capture the SQL that narwhals operations produce, then uses
sqlglot to transpile from DuckDB dialect to the target (Databricks,
Trino, etc.) and rewrite table references.
The ``con`` parameter is optional. When omitted, an in-memory DuckDB
is created automatically with schema-only tables derived from
``aco.table`` Pydantic models — no data file needed.
Usage::
import duckdb
from aco.lake.transpile import transpile
from aco.pipe import readmissions
# Schema-only (no DuckDB file required)
sql_map = transpile(
readmissions.pipeline,
target_dialect="databricks",
catalog="homelab",
)
# Or with an explicit DuckDB connection (for fidelity with real data)
import duckdb
con = duckdb.connect("notebooks/aco.duckdb", read_only=True)
sql_map = transpile(
readmissions.pipeline,
@@ -27,7 +39,10 @@ from __future__ import annotations
import inspect
import re
from typing import Any
import types
from datetime import date, datetime
from decimal import Decimal
from typing import Any, get_args, get_origin
import duckdb
import sqlglot
@@ -36,14 +51,80 @@ from sqlglot import exp
from aco.pipe.base import Pipeline
from aco.pipe.runner import _param_to_table
# ── Python type → DuckDB DDL mapping ────────────────────────────────
_PYTHON_TO_DUCKDB: dict[type, str] = {
str: "VARCHAR",
int: "BIGINT",
float: "DOUBLE",
bool: "BOOLEAN",
date: "DATE",
datetime: "TIMESTAMP",
Decimal: "DECIMAL(18,2)",
}
def _python_type_to_duckdb(annotation: Any) -> str:
"""Map a Pydantic field annotation to a DuckDB type string.
Handles ``str | None``, ``Optional[int]``, etc.
"""
origin = get_origin(annotation)
if origin is types.UnionType:
for arg in get_args(annotation):
if arg is not type(None):
return _python_type_to_duckdb(arg)
return _PYTHON_TO_DUCKDB.get(annotation, "VARCHAR")
def _schema_only_connection() -> duckdb.DuckDBPyConnection:
"""Create an in-memory DuckDB with empty tables from aco.table models.
Iterates all schemas and tables discovered by ``Catalog`` and
generates ``CREATE TABLE`` DDL from the Pydantic ``SQLTable``
field annotations. No data is loaded — only the schema structure
is created, which is sufficient for SQL transpilation.
Returns
-------
duckdb.DuckDBPyConnection
In-memory connection with all schemas and empty tables.
"""
from aco.lake.catalog import Catalog
cat = Catalog()
con = duckdb.connect(":memory:")
for schema_name in cat.schemas():
con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')
for table_ref in cat.tables(schema_name):
model = cat.model(table_ref)
_, table_name = table_ref.split(".", 1)
col_defs = []
for field_name, field_info in model.model_fields.items():
duckdb_type = _python_type_to_duckdb(field_info.annotation)
col_defs.append(f'"{field_name}" {duckdb_type}')
if not col_defs:
continue
columns_sql = ", ".join(col_defs)
ddl = f'CREATE TABLE "{schema_name}"."{table_name}" ({columns_sql})'
con.execute(ddl)
return con
def transpile(
pipeline: Pipeline,
con: duckdb.DuckDBPyConnection,
con: duckdb.DuckDBPyConnection | None = None,
*,
target_dialect: str = "databricks",
catalog: str = "",
output_mode: str = "ctas",
output_mode: str = "insert",
) -> dict[str, str]:
"""Generate target-dialect SQL for every expression in a pipeline.
@@ -51,9 +132,11 @@ def transpile(
----------
pipeline : Pipeline
Pipeline whose expressions will be transpiled.
con : duckdb.DuckDBPyConnection
con : duckdb.DuckDBPyConnection | None
DuckDB connection with source tables (read-only is fine).
Used to create relations that capture SQL through narwhals.
When ``None``, an in-memory DuckDB is created automatically
with schema-only tables from ``aco.table`` Pydantic models.
target_dialect : str
sqlglot output dialect (``databricks``, ``trino``, ``snowflake``).
catalog : str
@@ -61,7 +144,8 @@ def transpile(
E.g. ``"homelab"`` turns ``core.encounter`` into
``homelab.core.encounter``.
output_mode : str
``"ctas"`` — CREATE OR REPLACE TABLE ... AS SELECT
``"insert"`` — INSERT INTO ... SELECT (default, additive)
``"ctas"`` — CREATE OR REPLACE TABLE ... AS SELECT (cold start)
``"select"`` — bare SELECT statements
``"view"`` — CREATE OR REPLACE VIEW ... AS SELECT
@@ -70,52 +154,63 @@ def transpile(
dict[str, str]
Mapping of expression name to transpiled SQL string.
"""
own_connection = False
if con is None:
con = _schema_only_connection()
own_connection = True
cache: dict[str, Any] = {}
result: dict[str, str] = {}
view_registry: dict[str, str] = {}
for expr in pipeline.exprs:
name = expr.name
fn = expr.fn
try:
for expr in pipeline.exprs:
name = expr.name
fn = expr.fn
# Resolve dependencies via inspect.signature
sig = inspect.signature(fn)
kwargs = {}
for param in sig.parameters:
table_ref = _param_to_table(param)
if table_ref in cache:
kwargs[param] = cache[table_ref]
else:
kwargs[param] = _load_relation(con, table_ref)
# Resolve dependencies via inspect.signature
sig = inspect.signature(fn)
kwargs = {}
for param in sig.parameters:
table_ref = _param_to_table(param)
if table_ref in cache:
kwargs[param] = cache[table_ref]
else:
kwargs[param] = _load_relation(con, table_ref)
# Execute express function with DuckDB relations
try:
relation = fn(**kwargs)
except Exception as e:
result[name] = f"-- ERROR: {e}"
# Try to fall back to existing table for downstream deps
# Execute express function with DuckDB relations
try:
cache[name] = _load_relation(con, name)
except Exception:
pass
continue
relation = fn(**kwargs)
except Exception as e:
result[name] = f"-- ERROR: {e}"
# Try to fall back to existing table for downstream deps
try:
cache[name] = _load_relation(con, name)
except Exception:
pass
continue
# Extract DuckDB SQL
duckdb_sql = relation.sql_query()
# Extract DuckDB SQL
duckdb_sql = relation.sql_query()
# Register as temp view for downstream expressions
view_name = _table_ref_to_view_name(name)
con.execute(f"CREATE OR REPLACE TEMP VIEW {view_name} AS {duckdb_sql}")
cache[name] = con.sql(f"SELECT * FROM {view_name}")
# Register as temp view for downstream expressions
view_name = _table_ref_to_view_name(name, view_registry)
con.execute(f"CREATE OR REPLACE TEMP VIEW {view_name} AS {duckdb_sql}")
cache[name] = con.sql(f"SELECT * FROM {view_name}")
# Transpile to target dialect
target_sql = _transpile_sql(
duckdb_sql,
expr_name=name,
target_dialect=target_dialect,
catalog=catalog,
output_mode=output_mode,
)
result[name] = target_sql
# Transpile to target dialect
target_sql = _transpile_sql(
duckdb_sql,
expr_name=name,
target_dialect=target_dialect,
catalog=catalog,
output_mode=output_mode,
view_registry=view_registry,
)
result[name] = target_sql
finally:
if own_connection:
con.close()
return result
@@ -129,13 +224,21 @@ def _load_relation(
return con.sql(f'SELECT * FROM "{schema}"."{table}"')
def _table_ref_to_view_name(table_ref: str) -> str:
def _table_ref_to_view_name(table_ref: str, registry: dict[str, str]) -> str:
"""Convert a table ref to a valid DuckDB temp view name.
``readmissions._int_encounter`` → ``"_tv_readmissions___int_encounter"``
Uses ``_0_`` as the dot separator (``0`` cannot start an identifier
so this is unambiguous even when schemas contain underscores).
Registers the mapping in *registry* for lossless reversal during
SQL rewriting — no string-manipulation guesswork needed.
``readmissions._int_encounter`` → ``"_tv_readmissions_0__int_encounter"``
"""
safe = table_ref.replace(".", "__")
return f'"_tv_{safe}"'
safe = table_ref.replace(".", "_0_")
view_name = f"_tv_{safe}"
registry[view_name] = table_ref
return f'"{view_name}"'
def _transpile_sql(
@@ -145,6 +248,7 @@ def _transpile_sql(
target_dialect: str,
catalog: str,
output_mode: str,
view_registry: dict[str, str],
) -> str:
"""Transpile DuckDB SQL to target dialect with table ref rewriting."""
tree = sqlglot.parse_one(duckdb_sql, read="duckdb")
@@ -155,8 +259,9 @@ def _transpile_sql(
db = table.db # schema in sqlglot terminology
# Rewrite temp view names back to proper schema.table
if tbl_name.startswith("_tv_"):
real_ref = tbl_name[4:].replace("__", ".", 1)
# using the registry for lossless lookup
if tbl_name in view_registry:
real_ref = view_registry[tbl_name]
parts = real_ref.split(".", 1)
if len(parts) == 2:
table.set("db", exp.to_identifier(parts[0]))
@@ -181,7 +286,9 @@ def _transpile_sql(
if catalog:
target_ref = f"{catalog}.{expr_name}"
if output_mode == "ctas":
if output_mode == "insert":
return f"INSERT INTO {target_ref}\n{select_sql}"
elif output_mode == "ctas":
return f"CREATE OR REPLACE TABLE {target_ref} AS\n{select_sql}"
elif output_mode == "view":
return f"CREATE OR REPLACE VIEW {target_ref} AS\n{select_sql}"

View File

@@ -21,7 +21,7 @@ Usage::
from __future__ import annotations
import os
from typing import Any, Literal
from typing import Any
from databricks.sdk import WorkspaceClient
from databricks.sdk.service.catalog import (
@@ -31,7 +31,7 @@ from databricks.sdk.service.catalog import (
TableInfo,
VolumeInfo,
)
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, ConfigDict
# ── Pydantic Models ──────────────────────────────────────────────────
@@ -448,7 +448,6 @@ def _sql_table_to_column_infos(
model: type,
) -> list:
"""Convert SQLTable model fields to Databricks ColumnInfo objects."""
import json
from databricks.sdk.service.catalog import ColumnInfo, ColumnTypeName

View File

@@ -60,6 +60,27 @@ from __future__ import annotations
from aco.express import cclf as ex
from aco.express.base import Expr
from aco.pipe.base import Pipeline
from aco.table.cclf_pipe import (
CclfEligibility,
CclfIntDiagnosisPivot,
CclfIntDmeClaimAdr,
CclfIntDmeMedicalClaim,
CclfIntInstitutionalHeaderAdr,
CclfIntInstitutionalMedicalClaim,
CclfIntPharmacyClaimAdr,
CclfIntPhysicianClaimAdr,
CclfIntPhysicianMedicalClaim,
CclfIntProcedurePivot,
CclfMedicalClaim,
CclfPharmacyClaim,
CclfStgBeneficiaryDemographics,
CclfStgBeneficiaryXref,
CclfStgDmeClaim,
CclfStgInstitutionalHeader,
CclfStgPharmacyClaim,
CclfStgPhysicianClaim,
CclfStgRevenueCenter,
)
from bib.tag import Tag
# Shared refs for the CCLF module -- CMS CCLF Information Packet
@@ -74,6 +95,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_beneficiary_xref",
fn=ex.stg_beneficiary_xref,
output=CclfStgBeneficiaryXref,
after=["cclf.cclf9"],
refs=_REFS,
description=(
@@ -86,6 +108,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_institutional_header",
fn=ex.stg_institutional_header,
output=CclfStgInstitutionalHeader,
after=["cclf.cclf1", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=(
@@ -97,6 +120,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_institutional_header_adr",
fn=ex.int_institutional_header_adr,
output=CclfIntInstitutionalHeaderAdr,
after=["cclf._stg_institutional_header"],
refs=_REFS,
description=(
@@ -109,6 +133,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_revenue_center",
fn=ex.stg_revenue_center,
output=CclfStgRevenueCenter,
after=["cclf.cclf2", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=(
@@ -119,6 +144,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_diagnosis_pivot",
fn=ex.int_diagnosis_pivot,
output=CclfIntDiagnosisPivot,
after=["cclf.cclf4"],
refs=_REFS,
description=(
@@ -130,6 +156,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_procedure_pivot",
fn=ex.int_procedure_pivot,
output=CclfIntProcedurePivot,
after=["cclf.cclf3"],
refs=_REFS,
description=(
@@ -141,6 +168,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_institutional_medical_claim",
fn=ex.int_institutional_medical_claim,
output=CclfIntInstitutionalMedicalClaim,
after=[
"cclf._int_institutional_header_adr",
"cclf._stg_revenue_center",
@@ -160,6 +188,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_physician_claim",
fn=ex.stg_physician_claim,
output=CclfStgPhysicianClaim,
after=["cclf.cclf5", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=(
@@ -171,6 +200,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_physician_claim_adr",
fn=ex.int_physician_claim_adr,
output=CclfIntPhysicianClaimAdr,
after=["cclf._stg_physician_claim"],
refs=_REFS,
description=(
@@ -183,6 +213,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_physician_medical_claim",
fn=ex.int_physician_medical_claim,
output=CclfIntPhysicianMedicalClaim,
after=["cclf._int_physician_claim_adr"],
refs=_REFS,
description=(
@@ -195,6 +226,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_dme_claim",
fn=ex.stg_dme_claim,
output=CclfStgDmeClaim,
after=["cclf.cclf6", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=(
@@ -206,6 +238,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_dme_claim_adr",
fn=ex.int_dme_claim_adr,
output=CclfIntDmeClaimAdr,
after=["cclf._stg_dme_claim"],
refs=_REFS,
description=(
@@ -217,6 +250,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_dme_medical_claim",
fn=ex.int_dme_medical_claim,
output=CclfIntDmeMedicalClaim,
after=["cclf._int_dme_claim_adr"],
refs=_REFS,
description=(
@@ -230,6 +264,7 @@ pipeline = Pipeline(
Expr(
name="cclf.medical_claim",
fn=ex.medical_claim,
output=CclfMedicalClaim,
after=[
"cclf._int_institutional_medical_claim",
"cclf._int_physician_medical_claim",
@@ -245,6 +280,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_pharmacy_claim",
fn=ex.stg_pharmacy_claim,
output=CclfStgPharmacyClaim,
after=["cclf.cclf7", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=("Stages CCLF7 with MBI resolution via CCLF9 crosswalk."),
@@ -252,6 +288,7 @@ pipeline = Pipeline(
Expr(
name="cclf._int_pharmacy_claim_adr",
fn=ex.int_pharmacy_claim_adr,
output=CclfIntPharmacyClaimAdr,
after=["cclf._stg_pharmacy_claim"],
refs=_REFS,
description=(
@@ -263,6 +300,7 @@ pipeline = Pipeline(
Expr(
name="cclf.pharmacy_claim",
fn=ex.pharmacy_claim,
output=CclfPharmacyClaim,
after=["cclf._int_pharmacy_claim_adr"],
refs=_REFS,
description=(
@@ -275,6 +313,7 @@ pipeline = Pipeline(
Expr(
name="cclf._stg_beneficiary_demographics",
fn=ex.stg_beneficiary_demographics,
output=CclfStgBeneficiaryDemographics,
after=["cclf.cclf8", "cclf._stg_beneficiary_xref"],
refs=_REFS,
description=(
@@ -285,6 +324,7 @@ pipeline = Pipeline(
Expr(
name="cclf.eligibility",
fn=ex.eligibility,
output=CclfEligibility,
after=["cclf._stg_beneficiary_demographics"],
refs=_REFS,
description=(

View File

@@ -51,7 +51,9 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
)
if value_elem is not None and cell_type == "s":
idx = int(value_elem.text)
headers.append(shared_strings[idx] if idx < len(shared_strings) else None)
headers.append(
shared_strings[idx] if idx < len(shared_strings) else None
)
else:
headers.append(None)
@@ -83,7 +85,9 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
if value_elem is not None:
if cell_type == "s":
idx = int(value_elem.text)
value = shared_strings[idx] if idx < len(shared_strings) else None
value = (
shared_strings[idx] if idx < len(shared_strings) else None
)
else:
value = value_elem.text
row_data[field_mapping[i]] = value
@@ -97,9 +101,19 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
# Convert boolean fields
for field_name, value in row_data.items():
if field_name.startswith("i_attest"):
if value and str(value).strip().upper() in ("Y", "YES", "TRUE", "1"):
if value and str(value).strip().upper() in (
"Y",
"YES",
"TRUE",
"1",
):
row_data[field_name] = True
elif value and str(value).strip().upper() in ("N", "NO", "FALSE", "0"):
elif value and str(value).strip().upper() in (
"N",
"NO",
"FALSE",
"0",
):
row_data[field_name] = False
else:
row_data[field_name] = None
@@ -110,6 +124,7 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
def normalize_field_name(header: str) -> str:
"""Convert header to Python snake_case field name."""
import re
header = re.sub(r"\s+", " ", header.strip())
header = re.sub(r"[^\w\s]", "", header)
header = re.sub(r"\s+", "_", header)

View File

@@ -15,9 +15,6 @@ ingestion into the ACO data platform.
from __future__ import annotations
import csv
import re
from datetime import date, datetime
from pathlib import Path
from typing import Iterator
import openpyxl

View File

@@ -1,24 +1,26 @@
"""Generated Pydantic models for all DuckDB tables."""
from . import ahrq_measures
from . import ccsr
from . import chronic_conditions
from . import claims_preprocessing
from . import clinical_concept_library
from . import cms_hcc
from . import cms_provider_attribution
from . import core
from . import data_quality
from . import ed_classification
from . import financial_pmpm
from . import hcc_recapture
from . import hcc_suspecting
from . import input_layer
from . import main
from . import metadata
from . import pharmacy
from . import quality_measures
from . import readmissions
from . import reference_data
from . import terminology
from . import provider_attribution
from . import ( # noqa: F401
ahrq_measures,
ccsr,
chronic_conditions,
claims_preprocessing,
clinical_concept_library,
cms_hcc,
cms_provider_attribution,
core,
data_quality,
ed_classification,
financial_pmpm,
hcc_recapture,
hcc_suspecting,
input_layer,
main,
metadata,
pharmacy,
provider_attribution,
quality_measures,
readmissions,
reference_data,
terminology,
)

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class AhrqMeasuresIntPqi01Denom(SQLTable):
"""Schema: ahrq_measures / Table: _int_pqi_01_denom"""

View File

@@ -166,7 +166,7 @@ def classify(filename: str | Path) -> dict | None:
}
for pattern, pkg_type in package_types.items():
if m := pattern.match(fname):
if m := pattern.match(fname): # noqa: F841
parts = fname.replace(".zip", "").split("_")
return {
"type": "package",

View File

@@ -23,7 +23,4 @@ class SQLTable(BaseModel):
@classmethod
def column_names(cls) -> list[str]:
return [
name
for name in cls.model_fields
]
return [name for name in cls.model_fields]

View File

@@ -65,7 +65,6 @@ class Cclf0(SQLTable):
"""This field will be right-justified and left-padded with spaces."""
class Cclf1(SQLTable):
"""CCLF1: Part A Claims Header File
@@ -211,7 +210,6 @@ class Cclf1(SQLTable):
"""A number assigned by CMS identifying a MAC authorized to process Medicare claims."""
class Cclf2(SQLTable):
"""CCLF2: Part A Claims Revenue Center Detail File
@@ -291,7 +289,6 @@ class Cclf2(SQLTable):
"""A facilitys Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
class Cclf3(SQLTable):
"""CCLF3: Part A Procedure Code File
@@ -341,7 +338,6 @@ class Cclf3(SQLTable):
"""A facilitys Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
class Cclf4(SQLTable):
"""CCLF4: Part A Diagnosis Code File
@@ -394,7 +390,6 @@ class Cclf4(SQLTable):
"""A facilitys Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
class Cclf5(SQLTable):
"""CCLF5: Part B Physicians File
@@ -558,7 +553,6 @@ class Cclf5(SQLTable):
"""A number that identifies the provider that referred the service on the claim line. Each provider is assigned its own ..."""
class Cclf6(SQLTable):
"""CCLF6: Part B DME File
@@ -653,7 +647,6 @@ class Cclf6(SQLTable):
"""A number that identifies the provider ordering the indicated service on the claim line. Each provider is assigned its..."""
class Cclf7(SQLTable):
"""CCLF7: Part D File
@@ -730,7 +723,6 @@ class Cclf7(SQLTable):
"""The number associated with the indicated code in the Provider Prescribing Service Identification Qualifier Code field."""
class Cclf8(SQLTable):
"""CCLF8: Beneficiary Demographics File
@@ -834,7 +826,6 @@ class Cclf8(SQLTable):
"""A four-digit extension to a ZIP Code that represents a subdivision for mailing purposes of the ZIP Code."""
class Cclf9(SQLTable):
"""CCLF9: Beneficiary XREF File
@@ -863,7 +854,6 @@ class Cclf9(SQLTable):
"""Legacy RRB number. Note: To comply with MACRA of 2015, after the end of the New Medicare Card Transition Period in De..."""
class CclfA(SQLTable):
"""CCLFA: Part A Claims Benefit Enhancement and Demonstration Code File
@@ -973,7 +963,6 @@ class CclfA(SQLTable):
"""Capital MIPS payment costs"""
class CclfB(SQLTable):
"""CCLFB: Part B Claims Benefit Enhancement and Demonstration Code File

View File

@@ -59,34 +59,33 @@ from __future__ import annotations
import re
from typing import NamedTuple
# ── File ID -> CCLF table mapping ────────────────────────────────
CCLF_FILE_IDS: dict[str, str] = {
"0": "cclf0", # Summary Statistics Header Record
"1": "cclf1", # Part A Claims Header File
"2": "cclf2", # Part A Claims Revenue Center Detail File
"3": "cclf3", # Part A Procedure Code File
"4": "cclf4", # Part A Diagnosis Code File
"5": "cclf5", # Part B Physicians File
"6": "cclf6", # Part B DME File
"7": "cclf7", # Part D File
"8": "cclf8", # Beneficiary Demographics File
"9": "cclf9", # Beneficiary XREF File
"A": "cclfa", # Part A Benefit Enhancement and Demo Codes
"B": "cclfb", # Part B Benefit Enhancement and Demo Codes
"0": "cclf0", # Summary Statistics Header Record
"1": "cclf1", # Part A Claims Header File
"2": "cclf2", # Part A Claims Revenue Center Detail File
"3": "cclf3", # Part A Procedure Code File
"4": "cclf4", # Part A Diagnosis Code File
"5": "cclf5", # Part B Physicians File
"6": "cclf6", # Part B DME File
"7": "cclf7", # Part D File
"8": "cclf8", # Beneficiary Demographics File
"9": "cclf9", # Beneficiary XREF File
"A": "cclfa", # Part A Benefit Enhancement and Demo Codes
"B": "cclfb", # Part B Benefit Enhancement and Demo Codes
}
# ── Program prefix -> program name mapping ───────────────────────
PROGRAMS: dict[str, str] = {
"A": "sssp", # Medicare Shared Savings Program
"D": "reach", # ACO REACH Model
"K": "kcf", # Kidney Care First
"C": "ckcc", # Comprehensive Kidney Care Contracting
"P": "pcf", # Primary Care First
"IOTA": "iota", # Increasing Organ Transplant Access
"A": "sssp", # Medicare Shared Savings Program
"D": "reach", # ACO REACH Model
"K": "kcf", # Kidney Care First
"C": "ckcc", # Comprehensive Kidney Care Contracting
"P": "pcf", # Primary Care First
"IOTA": "iota", # Increasing Organ Transplant Access
}
@@ -133,37 +132,37 @@ class CclfFilename(NamedTuple):
# P.A1234.ACO.ZC1Y25.D250716.T1234567
CCLF_FILE_RE = re.compile(
r"P\."
r"(IOTA|[A-Z])" # program prefix
r"([A-Z0-9*]{3,6})" # ACO ID
r"(IOTA|[A-Z])" # program prefix
r"([A-Z0-9*]{3,6})" # ACO ID
r"\."
r"(ACO|PRT)" # entity type
r"(ACO|PRT)" # entity type
r"\."
r"ZC"
r"([0-9A-B])" # file ID
r"([YR])" # run type
r"(\d{2})" # performance year
r"([0-9A-B])" # file ID
r"([YR])" # run type
r"(\d{2})" # performance year
r"\."
r"D(\d{6})" # delivery date
r"D(\d{6})" # delivery date
r"\."
r"T(\d{7})" # delivery time
r"T(\d{7})" # delivery time
)
# Outer ZIP file (no file ID):
# P.A1234.ACO.ZCY25.D250716.T1234567
CCLF_ZIP_RE = re.compile(
r"P\."
r"(IOTA|[A-Z])" # program prefix
r"([A-Z0-9*]{3,6})" # ACO ID
r"(IOTA|[A-Z])" # program prefix
r"([A-Z0-9*]{3,6})" # ACO ID
r"\."
r"(ACO|PRT)" # entity type
r"(ACO|PRT)" # entity type
r"\."
r"ZC"
r"([YR])" # run type
r"(\d{2})" # performance year
r"([YR])" # run type
r"(\d{2})" # performance year
r"\."
r"D(\d{6})" # delivery date
r"D(\d{6})" # delivery date
r"\."
r"T(\d{7})" # delivery time
r"T(\d{7})" # delivery time
)

647
src/aco/table/cclf_pipe.py Normal file
View File

@@ -0,0 +1,647 @@
"""SQLTable contracts for the ACO CCLF connector pipeline intermediates.
Each class corresponds to one ``Expr`` in ``aco.pipe.cclf`` and documents
the output schema produced by the matching ``aco.express.cclf`` function.
"""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
# ── Stage 1: Beneficiary XREF (CCLF9) ────────────────────────────────────────
class CclfStgBeneficiaryXref(SQLTable):
"""cclf._stg_beneficiary_xref — deduplicated MBI crosswalk from CCLF9."""
__schema__ = "cclf"
__tablename__ = "_stg_beneficiary_xref"
crnt_num: str | None = None
"""Current (latest) Medicare Beneficiary Identifier."""
prvs_num: str | None = None
"""Previous MBI that maps to crnt_num."""
# ── Stage 2: Institutional header (CCLF1 + xref) ─────────────────────────────
class CclfStgInstitutionalHeader(SQLTable):
"""cclf._stg_institutional_header — CCLF1 with MBI resolved and amounts adjusted."""
__schema__ = "cclf"
__tablename__ = "_stg_institutional_header"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_efctv_dt: date | None = None
prvdr_oscar_num: str | None = None
clm_pmt_amt: Decimal | None = None
clm_mdcr_instnl_tot_chrg_amt: Decimal | None = None
clm_bill_fac_type_cd: str | None = None
clm_bill_clsfctn_cd: str | None = None
clm_bill_freq_cd: str | None = None
dgns_drg_cd: str | None = None
dgns_prcdr_icd_ind: str | None = None
clm_admsn_src_cd: str | None = None
clm_admsn_type_cd: str | None = None
bene_ptnt_stus_cd: str | None = None
clm_blg_prvdr_npi_num: str | None = None
oprtg_prvdr_npi_num: str | None = None
fac_prvdr_npi_num: str | None = None
# ── Stage 3: ADR-filtered institutional header ────────────────────────────────
class CclfIntInstitutionalHeaderAdr(SQLTable):
"""cclf._int_institutional_header_adr — latest-version, non-cancelled CCLF1 rows."""
__schema__ = "cclf"
__tablename__ = "_int_institutional_header_adr"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
prvdr_oscar_num: str | None = None
clm_pmt_amt: Decimal | None = None
clm_mdcr_instnl_tot_chrg_amt: Decimal | None = None
clm_bill_fac_type_cd: str | None = None
clm_bill_clsfctn_cd: str | None = None
clm_bill_freq_cd: str | None = None
dgns_drg_cd: str | None = None
dgns_prcdr_icd_ind: str | None = None
clm_admsn_src_cd: str | None = None
clm_admsn_type_cd: str | None = None
bene_ptnt_stus_cd: str | None = None
clm_blg_prvdr_npi_num: str | None = None
oprtg_prvdr_npi_num: str | None = None
fac_prvdr_npi_num: str | None = None
# ── Stage 4: Revenue center detail (CCLF2 + xref) ────────────────────────────
class CclfStgRevenueCenter(SQLTable):
"""cclf._stg_revenue_center — CCLF2 with MBI resolved."""
__schema__ = "cclf"
__tablename__ = "_stg_revenue_center"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_line_num: str | None = None
clm_line_from_dt: date | None = None
clm_line_thru_dt: date | None = None
clm_line_prod_rev_ctr_cd: str | None = None
clm_line_hcpcs_cd: str | None = None
clm_line_cvrd_pd_amt: Decimal | None = None
clm_line_srvc_unit_qty_rev: str | None = None
hcpcs_1_mdfr_cd: str | None = None
hcpcs_2_mdfr_cd: str | None = None
hcpcs_3_mdfr_cd: str | None = None
hcpcs_4_mdfr_cd: str | None = None
hcpcs_5_mdfr_cd: str | None = None
# ── Stage 5: Diagnosis pivot (CCLF4 long → wide) ─────────────────────────────
class CclfIntDiagnosisPivot(SQLTable):
"""cclf._int_diagnosis_pivot — CCLF4 pivoted to 25 wide dx slots per claim."""
__schema__ = "cclf"
__tablename__ = "_int_diagnosis_pivot"
cur_clm_uniq_id: str | None = None
bene_mbi_id: str | None = None
dgns_prcdr_icd_ind: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
diagnosis_code_4: str | None = None
diagnosis_code_5: str | None = None
diagnosis_code_6: str | None = None
diagnosis_code_7: str | None = None
diagnosis_code_8: str | None = None
diagnosis_code_9: str | None = None
diagnosis_code_10: str | None = None
diagnosis_code_11: str | None = None
diagnosis_code_12: str | None = None
diagnosis_code_13: str | None = None
diagnosis_code_14: str | None = None
diagnosis_code_15: str | None = None
diagnosis_code_16: str | None = None
diagnosis_code_17: str | None = None
diagnosis_code_18: str | None = None
diagnosis_code_19: str | None = None
diagnosis_code_20: str | None = None
diagnosis_code_21: str | None = None
diagnosis_code_22: str | None = None
diagnosis_code_23: str | None = None
diagnosis_code_24: str | None = None
diagnosis_code_25: str | None = None
diagnosis_poa_1: str | None = None
diagnosis_poa_2: str | None = None
diagnosis_poa_3: str | None = None
diagnosis_poa_4: str | None = None
diagnosis_poa_5: str | None = None
diagnosis_poa_6: str | None = None
diagnosis_poa_7: str | None = None
diagnosis_poa_8: str | None = None
diagnosis_poa_9: str | None = None
diagnosis_poa_10: str | None = None
diagnosis_poa_11: str | None = None
diagnosis_poa_12: str | None = None
diagnosis_poa_13: str | None = None
diagnosis_poa_14: str | None = None
diagnosis_poa_15: str | None = None
diagnosis_poa_16: str | None = None
diagnosis_poa_17: str | None = None
diagnosis_poa_18: str | None = None
diagnosis_poa_19: str | None = None
diagnosis_poa_20: str | None = None
diagnosis_poa_21: str | None = None
diagnosis_poa_22: str | None = None
diagnosis_poa_23: str | None = None
diagnosis_poa_24: str | None = None
diagnosis_poa_25: str | None = None
# ── Stage 6: Procedure pivot (CCLF3 long → wide) ─────────────────────────────
class CclfIntProcedurePivot(SQLTable):
"""cclf._int_procedure_pivot — CCLF3 pivoted to 25 wide px slots per claim."""
__schema__ = "cclf"
__tablename__ = "_int_procedure_pivot"
cur_clm_uniq_id: str | None = None
bene_mbi_id: str | None = None
dgns_prcdr_icd_ind: str | None = None
procedure_code_1: str | None = None
procedure_code_2: str | None = None
procedure_code_3: str | None = None
procedure_code_4: str | None = None
procedure_code_5: str | None = None
procedure_code_6: str | None = None
procedure_code_7: str | None = None
procedure_code_8: str | None = None
procedure_code_9: str | None = None
procedure_code_10: str | None = None
procedure_code_11: str | None = None
procedure_code_12: str | None = None
procedure_code_13: str | None = None
procedure_code_14: str | None = None
procedure_code_15: str | None = None
procedure_code_16: str | None = None
procedure_code_17: str | None = None
procedure_code_18: str | None = None
procedure_code_19: str | None = None
procedure_code_20: str | None = None
procedure_code_21: str | None = None
procedure_code_22: str | None = None
procedure_code_23: str | None = None
procedure_code_24: str | None = None
procedure_code_25: str | None = None
procedure_date_1: date | None = None
procedure_date_2: date | None = None
procedure_date_3: date | None = None
procedure_date_4: date | None = None
procedure_date_5: date | None = None
procedure_date_6: date | None = None
procedure_date_7: date | None = None
procedure_date_8: date | None = None
procedure_date_9: date | None = None
procedure_date_10: date | None = None
procedure_date_11: date | None = None
procedure_date_12: date | None = None
procedure_date_13: date | None = None
procedure_date_14: date | None = None
procedure_date_15: date | None = None
procedure_date_16: date | None = None
procedure_date_17: date | None = None
procedure_date_18: date | None = None
procedure_date_19: date | None = None
procedure_date_20: date | None = None
procedure_date_21: date | None = None
procedure_date_22: date | None = None
procedure_date_23: date | None = None
procedure_date_24: date | None = None
procedure_date_25: date | None = None
# ── Stage 7: Institutional medical_claim (CCLF1+2+3+4 joined) ────────────────
class CclfIntInstitutionalMedicalClaim(SQLTable):
"""cclf._int_institutional_medical_claim — joined institutional claim rows."""
__schema__ = "cclf"
__tablename__ = "_int_institutional_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
claim_line_start_date: date | None = None
claim_line_end_date: date | None = None
admission_date: date | None = None
discharge_date: date | None = None
admit_source_code: str | None = None
admit_type_code: str | None = None
discharge_disposition_code: str | None = None
place_of_service_code: str | None = None
bill_type_code: str | None = None
drg_code_type: str | None = None
drg_code: str | None = None
revenue_center_code: str | None = None
service_unit_quantity: int | None = None
hcpcs_code: str | None = None
hcpcs_modifier_1: str | None = None
hcpcs_modifier_2: str | None = None
hcpcs_modifier_3: str | None = None
hcpcs_modifier_4: str | None = None
hcpcs_modifier_5: str | None = None
rendering_npi: str | None = None
rendering_tin: str | None = None
billing_npi: str | None = None
billing_tin: str | None = None
facility_npi: str | None = None
paid_date: date | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
coinsurance_amount: float | None = None
copayment_amount: float | None = None
deductible_amount: float | None = None
total_cost_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
diagnosis_code_4: str | None = None
diagnosis_code_5: str | None = None
procedure_code_type: str | None = None
procedure_code_1: str | None = None
procedure_code_2: str | None = None
procedure_code_3: str | None = None
procedure_code_4: str | None = None
procedure_code_5: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 8: Physician claim staging (CCLF5 + xref) ──────────────────────────
class CclfStgPhysicianClaim(SQLTable):
"""cclf._stg_physician_claim — CCLF5 with MBI resolved and amounts adjusted."""
__schema__ = "cclf"
__tablename__ = "_stg_physician_claim"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_cntl_num: str | None = None
rndrg_prvdr_npi_num: str | None = None
clm_line_cvrd_pd_amt: Decimal | None = None
clm_line_alowd_chrg_amt: Decimal | None = None
clm_line_hcpcs_cd: str | None = None
clm_pos_cd: str | None = None
# ── Stage 9: ADR-filtered physician claim ────────────────────────────────────
class CclfIntPhysicianClaimAdr(SQLTable):
"""cclf._int_physician_claim_adr — latest-version, non-cancelled/denied CCLF5 rows."""
__schema__ = "cclf"
__tablename__ = "_int_physician_claim_adr"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_cntl_num: str | None = None
rndrg_prvdr_npi_num: str | None = None
clm_line_cvrd_pd_amt: Decimal | None = None
clm_line_alowd_chrg_amt: Decimal | None = None
clm_line_hcpcs_cd: str | None = None
clm_pos_cd: str | None = None
# ── Stage 10: Professional medical_claim (CCLF5) ─────────────────────────────
class CclfIntPhysicianMedicalClaim(SQLTable):
"""cclf._int_physician_medical_claim — CCLF5 mapped to medical_claim schema."""
__schema__ = "cclf"
__tablename__ = "_int_physician_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
claim_line_start_date: date | None = None
claim_line_end_date: date | None = None
place_of_service_code: str | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 11: DME claim staging (CCLF6 + xref) ───────────────────────────────
class CclfStgDmeClaim(SQLTable):
"""cclf._stg_dme_claim — CCLF6 with MBI resolved and amounts adjusted."""
__schema__ = "cclf"
__tablename__ = "_stg_dme_claim"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_cntrctr_num: str | None = None
clm_line_cvrd_pd_amt: Decimal | None = None
clm_line_alowd_chrg_amt: Decimal | None = None
clm_line_hcpcs_cd: str | None = None
# ── Stage 12: ADR-filtered DME claim ─────────────────────────────────────────
class CclfIntDmeClaimAdr(SQLTable):
"""cclf._int_dme_claim_adr — latest-version, non-cancelled CCLF6 rows."""
__schema__ = "cclf"
__tablename__ = "_int_dme_claim_adr"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_cntrctr_num: str | None = None
clm_line_cvrd_pd_amt: Decimal | None = None
clm_line_alowd_chrg_amt: Decimal | None = None
clm_line_hcpcs_cd: str | None = None
# ── Stage 13: DME medical_claim (CCLF6) ──────────────────────────────────────
class CclfIntDmeMedicalClaim(SQLTable):
"""cclf._int_dme_medical_claim — CCLF6 mapped to medical_claim schema."""
__schema__ = "cclf"
__tablename__ = "_int_dme_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
claim_line_start_date: date | None = None
claim_line_end_date: date | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 14: Final medical_claim (union inst + phys + dme) ──────────────────
class CclfMedicalClaim(SQLTable):
"""cclf.medical_claim — unioned institutional + professional + DME claims."""
__schema__ = "cclf"
__tablename__ = "medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
admission_date: date | None = None
discharge_date: date | None = None
bill_type_code: str | None = None
drg_code_type: str | None = None
drg_code: str | None = None
revenue_center_code: str | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
facility_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
procedure_code_type: str | None = None
procedure_code_1: str | None = None
procedure_code_2: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 15: Pharmacy claim staging (CCLF7 + xref) ──────────────────────────
class CclfStgPharmacyClaim(SQLTable):
"""cclf._stg_pharmacy_claim — CCLF7 with MBI resolved."""
__schema__ = "cclf"
__tablename__ = "_stg_pharmacy_claim"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_line_ndc_cd: str | None = None
clm_line_rx_srvc_ref_num: str | None = None
# ── Stage 16: ADR-filtered pharmacy claim ────────────────────────────────────
class CclfIntPharmacyClaimAdr(SQLTable):
"""cclf._int_pharmacy_claim_adr — latest-version, non-cancelled CCLF7 rows."""
__schema__ = "cclf"
__tablename__ = "_int_pharmacy_claim_adr"
cur_clm_uniq_id: str | None = None
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_adjsmt_type_cd: str | None = None
clm_line_ndc_cd: str | None = None
clm_line_rx_srvc_ref_num: str | None = None
# ── Stage 17: Final pharmacy_claim (CCLF7) ───────────────────────────────────
class CclfPharmacyClaim(SQLTable):
"""cclf.pharmacy_claim — CCLF7 mapped to pharmacy_claim schema."""
__schema__ = "cclf"
__tablename__ = "pharmacy_claim"
claim_id: str | None = None
claim_line_number: int | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
prescribing_provider_npi: str | None = None
dispensing_provider_npi: str | None = None
dispensing_date: date | None = None
ndc_code: str | None = None
quantity: float | None = None
days_supply: int | None = None
refills: int | None = None
paid_date: date | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
coinsurance_amount: float | None = None
copayment_amount: float | None = None
deductible_amount: float | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 18: Beneficiary demographics staging (CCLF8 + xref) ────────────────
class CclfStgBeneficiaryDemographics(SQLTable):
"""cclf._stg_beneficiary_demographics — CCLF8 with MBI resolved and deduplicated."""
__schema__ = "cclf"
__tablename__ = "_stg_beneficiary_demographics"
current_bene_mbi_id: str | None = None
bene_mbi_id: str | None = None
bene_dob: date | None = None
bene_sex_cd: str | None = None
bene_race_cd: str | None = None
bene_fips_state_cd: str | None = None
bene_fips_cnty_cd: str | None = None
bene_zip_cd: str | None = None
bene_1st_name: str | None = None
bene_last_name: str | None = None
# ── Stage 19: Final eligibility (CCLF8) ──────────────────────────────────────
class CclfEligibility(SQLTable):
"""cclf.eligibility — CCLF8 mapped to eligibility schema."""
__schema__ = "cclf"
__tablename__ = "eligibility"
person_id: str | None = None
member_id: str | None = None
gender: str | None = None
race: str | None = None
birth_date: date | None = None
death_date: date | None = None
death_flag: int | None = None
enrollment_start_date: date | None = None
enrollment_end_date: date | None = None
payer: str | None = None
plan: str | None = None
original_reason_entitlement_code: str | None = None
dual_status_code: str | None = None
medicare_status_code: str | None = None
first_name: str | None = None
last_name: str | None = None
address: str | None = None
city: str | None = None
state: str | None = None
zip_code: str | None = None
phone: str | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import datetime
from aco.table.base import SQLTable
class CcsrValueSetDxccsrV20231BodySystems(SQLTable):
"""Schema: ccsr / Table: _value_set_dxccsr_v2023_1_body_systems"""

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from datetime import date, datetime
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
class ChronicConditionsIntCmsChronicConditionAll(SQLTable):
@@ -145,7 +146,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
hepatitis_d: int | None = None
hepatitis_e: int | None = None
hip_pelvic_fracture: int | None = None
human_immunodeficiency_virus_and_or_acquired_immunodeficiency_syndrome_hiv_aids: int | None = None
human_immunodeficiency_virus_and_or_acquired_immunodeficiency_syndrome_hiv_aids: (
int | None
) = None
hyperlipidemia: int | None = None
hypertension: int | None = None
hypothyroidism: int | None = None
@@ -153,7 +156,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
ischemic_heart_disease: int | None = None
learning_disabilities: int | None = None
leukemias_and_lymphomas: int | None = None
liver_disease_cirrhosis_and_other_liver_conditions_except_viral_hepatitis: int | None = None
liver_disease_cirrhosis_and_other_liver_conditions_except_viral_hepatitis: (
int | None
) = None
migraine_and_chronic_headache: int | None = None
mobility_impairments: int | None = None
multiple_sclerosis_and_transverse_myelitis: int | None = None
@@ -179,7 +184,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
spinal_cord_injury: int | None = None
stroke_transient_ischemic_attack: int | None = None
tobacco_use: int | None = None
traumatic_brain_injury_and_nonpsychotic_mental_disorders_due_to_brain_damage: int | None = None
traumatic_brain_injury_and_nonpsychotic_mental_disorders_due_to_brain_damage: (
int | None
) = None
viral_hepatitis_general: int | None = None
pipeline_last_run: datetime | None = None

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class ClaimsPreprocessingIntAcuteInpatientInstitutionalMaternity(SQLTable):
"""Schema: claims_preprocessing / Table: _int_acute_inpatient_institutional_maternity"""
@@ -3951,7 +3952,9 @@ class ClaimsPreprocessingServiceCategoryOfficeBasedOtherProfessional(SQLTable):
pipeline_last_run: datetime | None = None
class ClaimsPreprocessingServiceCategoryOfficeBasedPhysicalTherapyProfessional(SQLTable):
class ClaimsPreprocessingServiceCategoryOfficeBasedPhysicalTherapyProfessional(
SQLTable
):
"""Schema: claims_preprocessing / Table: service_category__office_based_physical_therapy_professional"""
__schema__ = "claims_preprocessing"
@@ -4002,7 +4005,9 @@ class ClaimsPreprocessingServiceCategoryOfficeBasedSurgeryProfessional(SQLTable)
pipeline_last_run: datetime | None = None
class ClaimsPreprocessingServiceCategoryOutpatientPhysicalTherapyInstitutional(SQLTable):
class ClaimsPreprocessingServiceCategoryOutpatientPhysicalTherapyInstitutional(
SQLTable
):
"""Schema: claims_preprocessing / Table: service_category__outpatient_physical_therapy_institutional"""
__schema__ = "claims_preprocessing"

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from aco.table.base import SQLTable
class ClinicalConceptLibraryClinicalConcepts(SQLTable):
"""Schema: clinical_concept_library / Table: clinical_concepts"""

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class CmsHccIntDemographicFactors(SQLTable):
"""Schema: cms_hcc / Table: _int_demographic_factors"""

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
from aco.table.base import SQLTable

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class CoreStgClaimsCondition(SQLTable):
"""Schema: core / Table: _stg_claims_condition"""

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class DataQualityValueSetCrosswalkFieldInfo(SQLTable):
"""Schema: data_quality / Table: _value_set_crosswalk_field_info"""

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from decimal import Decimal
from aco.table.base import SQLTable
class EdClassificationIntByProviderParentOrganization(SQLTable):
"""Schema: ed_classification / Table: _int_by_provider_parent_organization"""

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import datetime
from decimal import Decimal
from aco.table.base import SQLTable
class FinancialPmpmIntPatientSpendWithServiceCategories(SQLTable):
"""Schema: financial_pmpm / Table: _int_patient_spend_with_service_categories"""

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from aco.table.base import SQLTable
class HccRecaptureGapStatus(SQLTable):
"""Schema: hcc_recapture / Table: gap_status"""

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class HccSuspectingIntAllConditions(SQLTable):
"""Schema: hcc_suspecting / Table: _int_all_conditions"""

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from datetime import date, datetime
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
class InputLayerAppointment(SQLTable):

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from datetime import date, datetime
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
class MainAlertsAnomalyDetection(SQLTable):

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class PharmacyIntBrandWithGenericAvailable(SQLTable):
"""Schema: pharmacy / Table: _int_brand_with_generic_available"""

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class ProviderAttributionIntCurrentSteps(SQLTable):
"""Schema: provider_attribution / Table: _int_current_steps"""

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class QualityMeasuresIntAdhDiabetesPerformancePeriod(SQLTable):
"""Schema: quality_measures / Table: _int_adh_diabetes__performance_period"""

View File

@@ -18,58 +18,58 @@ from aco.table.base import SQLTable
class ReachQuarterlyQualityReport(SQLTable):
"""PY2023 ACO REACH Overview Section 1.4.1 "Quarterly Claims-Based Quality Report" (p.22-23):
"The Quarterly Claims-Based Quality Report (QQR) provides quarterly
performance on four claims-based quality measures used for Pay-for-Performance
(P4P) quality scoring:
"The Quarterly Claims-Based Quality Report (QQR) provides quarterly
performance on four claims-based quality measures used for Pay-for-Performance
(P4P) quality scoring:
**Quality Measures:**
**Quality Measures:**
1. **ACR (All-Condition Readmission)** - All ACO types:
Percentage of hospitalizations resulting in unplanned readmission within
30 days per 100 index hospital admissions. Lower is better.
1. **ACR (All-Condition Readmission)** - All ACO types:
Percentage of hospitalizations resulting in unplanned readmission within
30 days per 100 index hospital admissions. Lower is better.
2. **UAMCC (Unplanned Admissions for Multiple Chronic Conditions)** - All ACO types:
Rate of risk-standardized acute unplanned hospital admissions for patients
with multiple chronic conditions per 100 person-years. Lower is better.
2. **UAMCC (Unplanned Admissions for Multiple Chronic Conditions)** - All ACO types:
Rate of risk-standardized acute unplanned hospital admissions for patients
with multiple chronic conditions per 100 person-years. Lower is better.
3. **DAH (Days at Home)** - High Needs Population ACOs only:
Risk factor-adjusted, mortality-adjusted, nursing home transition-adjusted
days at home averaged over all patients during 12-month measurement period.
Higher is better.
3. **DAH (Days at Home)** - High Needs Population ACOs only:
Risk factor-adjusted, mortality-adjusted, nursing home transition-adjusted
days at home averaged over all patients during 12-month measurement period.
Higher is better.
4. **TFU (Timely Follow-Up)** - Standard and New Entrant ACOs only:
ACO-level rate of follow-up within 7 days for patients with chronic
conditions who experienced acute exacerbation of six specified conditions
(asthma, COPD, heart failure, pneumonia, diabetes, hypertension).
Higher is better.
4. **TFU (Timely Follow-Up)** - Standard and New Entrant ACOs only:
ACO-level rate of follow-up within 7 days for patients with chronic
conditions who experienced acute exacerbation of six specified conditions
(asthma, COPD, heart failure, pneumonia, diabetes, hypertension).
Higher is better.
**Report Contents:**
- ACO's measure score for 12-month rolling period ending in prior quarter
- Mean score for ACO cohort (High Needs vs. Standard/New Entrant)
- Provisional measure percentile rank (informational)
- Highest provisional quality performance benchmark threshold reached
**Report Contents:**
- ACO's measure score for 12-month rolling period ending in prior quarter
- Mean score for ACO cohort (High Needs vs. Standard/New Entrant)
- Provisional measure percentile rank (informational)
- Highest provisional quality performance benchmark threshold reached
**Benchmarking (p.23):**
Percentile thresholds used for P4P scoring:
- <30%: 0 points
- 30-34%: 7.5 points
- 35-39%: 7.75 points
- [continues through 90%+: 10 points]
**Benchmarking (p.23):**
Percentile thresholds used for P4P scoring:
- <30%: 0 points
- 30-34%: 7.5 points
- 35-39%: 7.75 points
- [continues through 90%+: 10 points]
The QQR is delivered quarterly with 12-month rolling measurement periods."
The QQR is delivered quarterly with 12-month rolling measurement periods."
File Format: Excel Workbook (.xlsx)
File Code: QTLQR
Distribution: Quarterly (12-month rolling period ending in prior quarter)
Page Reference: p.22-23
File Format: Excel Workbook (.xlsx)
File Code: QTLQR
Distribution: Quarterly (12-month rolling period ending in prior quarter)
Page Reference: p.22-23
Measurement Period: 12-month rolling window (e.g., Q1 2023 report covers
Jan 2022 - Dec 2022)
Measurement Period: 12-month rolling window (e.g., Q1 2023 report covers
Jan 2022 - Dec 2022)
Stratified Reporting: Quality measures reported for three subgroups:
- Dual-eligible beneficiaries (full Medicaid)
- Low socioeconomic status (ADI percentile ≥81)
- Race/ethnicity other than white
Stratified Reporting: Quality measures reported for three subgroups:
- Dual-eligible beneficiaries (full Medicaid)
- Low socioeconomic status (ADI percentile ≥81)
- Race/ethnicity other than white
"""
__schema__: ClassVar[str] = "reach"
@@ -103,56 +103,56 @@ Stratified Reporting: Quality measures reported for three subgroups:
class ReachAnnualQualitySummary(SQLTable):
"""PY2023 ACO REACH Overview Section 1.4.2 "Annual Quality Report - Summary" (p.23-25):
"The Annual Quality Report (AQR) provides final quality performance scoring
for the performance year, used to calculate quality withhold earn back
and High Performers Pool bonus.
"The Annual Quality Report (AQR) provides final quality performance scoring
for the performance year, used to calculate quality withhold earn back
and High Performers Pool bonus.
**Table 1: Summary Information**
**Table 1: Summary Information**
**Quality Score Calculation:**
**Quality Score Calculation:**
1. **Initial Quality Score (0-100%)**:
- Total points earned across 4 P4P measures (ACR, UAMCC, DAH/TFU, CAHPS)
- Divided by total possible points (40 = 4 measures × 10 points each)
- Multiplied by 100
1. **Initial Quality Score (0-100%)**:
- Total points earned across 4 P4P measures (ACR, UAMCC, DAH/TFU, CAHPS)
- Divided by total possible points (40 = 4 measures × 10 points each)
- Multiplied by 100
2. **CI/SEP Gateway Multiplier**:
- Applies to prior-year ACOs (ACOs continuing from previous PY)
- 1.0 if ACO meets Continuous Improvement (CI) or Static Excellence Performance (SEP)
- 0.5 if ACO does not meet CI or SEP criteria
- New ACOs automatically receive 1.0 multiplier
2. **CI/SEP Gateway Multiplier**:
- Applies to prior-year ACOs (ACOs continuing from previous PY)
- 1.0 if ACO meets Continuous Improvement (CI) or Static Excellence Performance (SEP)
- 0.5 if ACO does not meet CI or SEP criteria
- New ACOs automatically receive 1.0 multiplier
3. **HEDR Adjustment (0-10% addition)**:
- Health Equity Data Reporting adjustment
- Up to 10% added to Initial Quality Score based on:
* Submission of stratified quality measure data
* Completion of health equity narratives
* Quality improvement initiatives targeting disparities
3. **HEDR Adjustment (0-10% addition)**:
- Health Equity Data Reporting adjustment
- Up to 10% added to Initial Quality Score based on:
* Submission of stratified quality measure data
* Completion of health equity narratives
* Quality improvement initiatives targeting disparities
4. **Total Quality Score (0-100%)**:
- Initial Quality Score × CI/SEP Multiplier + HEDR Adjustment
- Capped at 100%
4. **Total Quality Score (0-100%)**:
- Initial Quality Score × CI/SEP Multiplier + HEDR Adjustment
- Capped at 100%
**Financial Impact:**
**Financial Impact:**
- **Quality Withhold Earn Back (0-2%)**:
* 2% of benchmark withheld for quality
* Earn back = Total Quality Score × 2%
* E.g., 85% quality score earns 1.7% of benchmark
- **Quality Withhold Earn Back (0-2%)**:
* 2% of benchmark withheld for quality
* Earn back = Total Quality Score × 2%
* E.g., 85% quality score earns 1.7% of benchmark
- **High Performers Pool (HPP) Bonus**:
* Additional funding for ACOs meeting HPP criteria
* Criteria: Total Quality Score ≥75%, continuing ACOs, positive savings
* Bonus funded from quality withhold of non-qualifying ACOs"
- **High Performers Pool (HPP) Bonus**:
* Additional funding for ACOs meeting HPP criteria
* Criteria: Total Quality Score ≥75%, continuing ACOs, positive savings
* Bonus funded from quality withhold of non-qualifying ACOs"
File Format: Excel Workbook (.xlsx)
File Code: ANLQR
Distribution: Annually (prior performance year)
Page Reference: p.23-25
File Format: Excel Workbook (.xlsx)
File Code: ANLQR
Distribution: Annually (prior performance year)
Page Reference: p.23-25
CI/SEP Gateway (p.24):
- Continuous Improvement: ACO improves on ≥50% of measures from prior year
- Static Excellence Performance: ACO scores ≥30th percentile on all measures
CI/SEP Gateway (p.24):
- Continuous Improvement: ACO improves on ≥50% of measures from prior year
- Static Excellence Performance: ACO scores ≥30th percentile on all measures
"""
__schema__: ClassVar[str] = "reach"
@@ -201,63 +201,63 @@ CI/SEP Gateway (p.24):
class ReachAnnualQualityCahps(SQLTable):
"""PY2023 ACO REACH Overview Section 1.4.2 "Annual Quality Report - CAHPS Results" (p.25):
"**Table 4: CAHPS Survey Results**
"**Table 4: CAHPS Survey Results**
The Consumer Assessment of Healthcare Providers and Systems (CAHPS) survey
measures patient experience with their healthcare. CAHPS is one of four
P4P measures worth up to 10 points toward quality scoring.
The Consumer Assessment of Healthcare Providers and Systems (CAHPS) survey
measures patient experience with their healthcare. CAHPS is one of four
P4P measures worth up to 10 points toward quality scoring.
**8 Summary Survey Measures (SSMs):**
**8 Summary Survey Measures (SSMs):**
1. **Getting Timely Appointments, Care, and Information**
- Composite of items about appointment timeliness and phone access
1. **Getting Timely Appointments, Care, and Information**
- Composite of items about appointment timeliness and phone access
2. **How Well Providers Communicate**
- Composite of items about provider listening, explaining, respecting
2. **How Well Providers Communicate**
- Composite of items about provider listening, explaining, respecting
3. **Care Coordination**
- Questions about follow-up on test results and coordination between providers
3. **Care Coordination**
- Questions about follow-up on test results and coordination between providers
4. **Shared Decision-Making**
- Items about provider involving patient in care decisions
4. **Shared Decision-Making**
- Items about provider involving patient in care decisions
5. **Patient Rating of Provider**
- Overall provider rating (0-10 scale)
5. **Patient Rating of Provider**
- Overall provider rating (0-10 scale)
6. **Courteous and Helpful Office Staff**
- Items about front office interactions
6. **Courteous and Helpful Office Staff**
- Items about front office interactions
7. **Health Promotion and Education**
- Questions about preventive care discussions and health education
7. **Health Promotion and Education**
- Questions about preventive care discussions and health education
8. **Stewardship of Patient Resources**
- Items about discussing treatment costs and medication affordability
8. **Stewardship of Patient Resources**
- Items about discussing treatment costs and medication affordability
**CAHPS Scoring:**
**CAHPS Scoring:**
- Survey administered to random sample of aligned beneficiaries
- Minimum 200 completed surveys required for valid results
- Patient mix adjustment applied (age, self-reported health status, education)
- Linear mean scores calculated for each SSM
- ACO compared to all REACH ACOs using linear means
- Percentile rank determines benchmark threshold reached
- Same percentile thresholds as claims-based measures (30%-90%)
- Standard and New Entrant ACOs: Percentile rank reported and scored
- High Needs Population ACOs: Comparison to all REACH ACOs only (informational)
- Survey administered to random sample of aligned beneficiaries
- Minimum 200 completed surveys required for valid results
- Patient mix adjustment applied (age, self-reported health status, education)
- Linear mean scores calculated for each SSM
- ACO compared to all REACH ACOs using linear means
- Percentile rank determines benchmark threshold reached
- Same percentile thresholds as claims-based measures (30%-90%)
- Standard and New Entrant ACOs: Percentile rank reported and scored
- High Needs Population ACOs: Comparison to all REACH ACOs only (informational)
**Report Contents:**
- ACO's patient mix adjusted linear mean score for each SSM
- Mean score across all REACH ACOs for each SSM
- SSM percentile rank (Standard/New Entrant ACOs only)
- Highest benchmark threshold met based on percentile
- Points earned (up to 10 for CAHPS overall, distributed across 8 SSMs)"
**Report Contents:**
- ACO's patient mix adjusted linear mean score for each SSM
- Mean score across all REACH ACOs for each SSM
- SSM percentile rank (Standard/New Entrant ACOs only)
- Highest benchmark threshold met based on percentile
- Points earned (up to 10 for CAHPS overall, distributed across 8 SSMs)"
File Format: Excel Workbook (.xlsx), Table 4
Distribution: Annually (prior performance year)
Page Reference: p.25
File Format: Excel Workbook (.xlsx), Table 4
Distribution: Annually (prior performance year)
Page Reference: p.25
Survey Timing: CAHPS survey administered in Q2 of performance year for prior
year's patient experience. Results available in final settlement timeframe.
Survey Timing: CAHPS survey administered in Q2 of performance year for prior
year's patient experience. Results available in final settlement timeframe.
"""
__schema__: ClassVar[str] = "reach"

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from datetime import datetime
from datetime import date, datetime
from decimal import Decimal
from aco.table.base import SQLTable
class ReadmissionsIntEncounter(SQLTable):
"""Schema: readmissions / Table: _int_encounter"""

View File

@@ -1,7 +1,9 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from aco.table.base import SQLTable
class ReferenceDataAnsiFipsState(SQLTable):
"""Schema: reference_data / Table: ansi_fips_state"""

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from aco.table.base import SQLTable
from datetime import date
from decimal import Decimal
from aco.table.base import SQLTable
class TerminologyActSite(SQLTable):
"""Schema: terminology / Table: act_site"""

View File

@@ -518,7 +518,7 @@ class Client:
----------
job_url : str
The job status URL from ``start_export``.
"""https://github.com/CMSgov/bcda-app/blob/main/bcda/client/fhir/client.go
"""
self._request("DELETE", job_url)
log.info("Job cancelled: %s", job_url)

View File

@@ -42,6 +42,20 @@ def _build_pipeline():
import aco.pipe.runner # noqa: F401
from aco.express.base import Expr
from aco.pipe.base import Pipeline
from bcda.table.cclf_pipe import (
BcdaEligibility,
BcdaIntDmeMedicalClaim,
BcdaIntInstitutionalMedicalClaim,
BcdaIntPhysicianMedicalClaim,
BcdaMedicalClaim,
BcdaPharmacyClaim,
BcdaStgBeneficiaryDemographics,
BcdaStgBeneficiaryXref,
BcdaStgPartAHeader,
BcdaStgPartBDmeHeader,
BcdaStgPartBPhysicianHeader,
BcdaStgPartDHeader,
)
_REFS = [
Tag.module("bcda"),
@@ -54,6 +68,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_beneficiary_xref",
fn=ex.stg_beneficiary_xref,
output=BcdaStgBeneficiaryXref,
after=["bcda.patient"],
refs=_REFS,
description=(
@@ -67,6 +82,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_part_a_header",
fn=ex.stg_part_a_header,
output=BcdaStgPartAHeader,
after=[
"bcda.explanation_of_benefit",
"bcda._stg_beneficiary_xref",
@@ -81,6 +97,7 @@ def _build_pipeline():
Expr(
name="bcda._int_institutional_medical_claim",
fn=ex.int_institutional_medical_claim,
output=BcdaIntInstitutionalMedicalClaim,
after=["bcda._stg_part_a_header", "bcda.eob_item"],
refs=_REFS,
description=(
@@ -94,6 +111,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_part_b_physician_header",
fn=ex.stg_part_b_physician_header,
output=BcdaStgPartBPhysicianHeader,
after=[
"bcda.explanation_of_benefit",
"bcda._stg_beneficiary_xref",
@@ -108,6 +126,7 @@ def _build_pipeline():
Expr(
name="bcda._int_physician_medical_claim",
fn=ex.int_physician_medical_claim,
output=BcdaIntPhysicianMedicalClaim,
after=[
"bcda._stg_part_b_physician_header",
"bcda.eob_item",
@@ -124,6 +143,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_part_b_dme_header",
fn=ex.stg_part_b_dme_header,
output=BcdaStgPartBDmeHeader,
after=[
"bcda.explanation_of_benefit",
"bcda._stg_beneficiary_xref",
@@ -138,6 +158,7 @@ def _build_pipeline():
Expr(
name="bcda._int_dme_medical_claim",
fn=ex.int_dme_medical_claim,
output=BcdaIntDmeMedicalClaim,
after=["bcda._stg_part_b_dme_header", "bcda.eob_item"],
refs=_REFS,
description=(
@@ -150,6 +171,7 @@ def _build_pipeline():
Expr(
name="bcda.medical_claim",
fn=ex.medical_claim,
output=BcdaMedicalClaim,
after=[
"bcda._int_institutional_medical_claim",
"bcda._int_physician_medical_claim",
@@ -165,6 +187,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_part_d_header",
fn=ex.stg_part_d_header,
output=BcdaStgPartDHeader,
after=[
"bcda.explanation_of_benefit",
"bcda._stg_beneficiary_xref",
@@ -179,6 +202,7 @@ def _build_pipeline():
Expr(
name="bcda.pharmacy_claim",
fn=ex.pharmacy_claim,
output=BcdaPharmacyClaim,
after=["bcda._stg_part_d_header", "bcda.eob_item"],
refs=_REFS,
description=(
@@ -191,6 +215,7 @@ def _build_pipeline():
Expr(
name="bcda._stg_beneficiary_demographics",
fn=ex.stg_beneficiary_demographics,
output=BcdaStgBeneficiaryDemographics,
after=["bcda.patient", "bcda.coverage"],
refs=_REFS,
description=(
@@ -202,6 +227,7 @@ def _build_pipeline():
Expr(
name="bcda.eligibility",
fn=ex.eligibility,
output=BcdaEligibility,
after=["bcda._stg_beneficiary_demographics"],
refs=_REFS,
description=(

383
src/bcda/table/cclf_pipe.py Normal file
View File

@@ -0,0 +1,383 @@
"""SQLTable contracts for the BCDA FHIR R4 connector pipeline intermediates.
Each class corresponds to one ``Expr`` in ``bcda.pipe.cclf`` and documents
the output schema produced by the matching ``bcda.express.cclf`` function.
"""
from __future__ import annotations
from datetime import date, datetime
from aco.table.base import SQLTable
# ── Stage 1: Beneficiary XREF (Patient) ──────────────────────────────────────
class BcdaStgBeneficiaryXref(SQLTable):
"""bcda._stg_beneficiary_xref — deduplicated MBI identity from BCDA Patient."""
__schema__ = "bcda"
__tablename__ = "_stg_beneficiary_xref"
bene_id: str | None = None
"""Internal BCDA beneficiary identifier."""
current_bene_mbi_id: str | None = None
"""Current Medicare Beneficiary Identifier (delivered directly by BCDA)."""
# ── Stage 2: Part A institutional claim header ────────────────────────────────
class BcdaStgPartAHeader(SQLTable):
"""bcda._stg_part_a_header — EOB Part A filtered and MBI resolved."""
__schema__ = "bcda"
__tablename__ = "_stg_part_a_header"
clm_id: str | None = None
current_bene_mbi_id: str | None = None
bene_id: str | None = None
nch_near_line_rec_ident_cd: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_admsn_dt: date | None = None
nch_bene_dschrg_dt: date | None = None
clm_admsn_src_cd: str | None = None
clm_admsn_type_cd: str | None = None
ptnt_dschrg_stus_cd: str | None = None
clm_bill_fac_type_cd: str | None = None
clm_bill_clsfctn_cd: str | None = None
clm_bill_freq_cd: str | None = None
clm_drg_cd: str | None = None
dgns_prcdr_icd_ind: str | None = None
clm_pmt_amt: float | None = None
clm_tot_chrg_amt: float | None = None
org_npi_num: str | None = None
at_physn_npi: str | None = None
op_physn_npi: str | None = None
# ── Stage 3: Institutional medical_claim (Part A header + items) ──────────────
class BcdaIntInstitutionalMedicalClaim(SQLTable):
"""bcda._int_institutional_medical_claim — Part A joined to medical_claim schema."""
__schema__ = "bcda"
__tablename__ = "_int_institutional_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
admission_date: date | None = None
discharge_date: date | None = None
admit_source_code: str | None = None
admit_type_code: str | None = None
discharge_disposition_code: str | None = None
bill_type_code: str | None = None
drg_code_type: str | None = None
drg_code: str | None = None
revenue_center_code: str | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
facility_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
procedure_code_type: str | None = None
procedure_code_1: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 4: Part B physician claim header ────────────────────────────────────
class BcdaStgPartBPhysicianHeader(SQLTable):
"""bcda._stg_part_b_physician_header — EOB Part B carrier filtered and MBI resolved."""
__schema__ = "bcda"
__tablename__ = "_stg_part_b_physician_header"
clm_id: str | None = None
current_bene_mbi_id: str | None = None
bene_id: str | None = None
nch_near_line_rec_ident_cd: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_pmt_amt: float | None = None
clm_tot_chrg_amt: float | None = None
rndrng_physn_npi: str | None = None
org_npi_num: str | None = None
dgns_prcdr_icd_ind: str | None = None
# ── Stage 5: Professional medical_claim (Part B physician) ───────────────────
class BcdaIntPhysicianMedicalClaim(SQLTable):
"""bcda._int_physician_medical_claim — Part B physician joined to medical_claim schema."""
__schema__ = "bcda"
__tablename__ = "_int_physician_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
claim_line_start_date: date | None = None
claim_line_end_date: date | None = None
place_of_service_code: str | None = None
hcpcs_code: str | None = None
hcpcs_modifier_1: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 6: Part B DME claim header ─────────────────────────────────────────
class BcdaStgPartBDmeHeader(SQLTable):
"""bcda._stg_part_b_dme_header — EOB Part B DME filtered and MBI resolved."""
__schema__ = "bcda"
__tablename__ = "_stg_part_b_dme_header"
clm_id: str | None = None
current_bene_mbi_id: str | None = None
bene_id: str | None = None
nch_near_line_rec_ident_cd: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_pmt_amt: float | None = None
clm_tot_chrg_amt: float | None = None
rndrng_physn_npi: str | None = None
org_npi_num: str | None = None
# ── Stage 7: DME medical_claim (Part B DME) ───────────────────────────────────
class BcdaIntDmeMedicalClaim(SQLTable):
"""bcda._int_dme_medical_claim — Part B DME joined to medical_claim schema."""
__schema__ = "bcda"
__tablename__ = "_int_dme_medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
claim_line_start_date: date | None = None
claim_line_end_date: date | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 8: Final medical_claim (union inst + phys + dme) ───────────────────
class BcdaMedicalClaim(SQLTable):
"""bcda.medical_claim — unioned Part A + Part B carrier + Part B DME claims."""
__schema__ = "bcda"
__tablename__ = "medical_claim"
claim_id: str | None = None
claim_line_number: int | None = None
claim_type: str | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
claim_start_date: date | None = None
claim_end_date: date | None = None
admission_date: date | None = None
discharge_date: date | None = None
admit_source_code: str | None = None
admit_type_code: str | None = None
discharge_disposition_code: str | None = None
place_of_service_code: str | None = None
bill_type_code: str | None = None
drg_code_type: str | None = None
drg_code: str | None = None
revenue_center_code: str | None = None
hcpcs_code: str | None = None
rendering_npi: str | None = None
billing_npi: str | None = None
facility_npi: str | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
charge_amount: float | None = None
diagnosis_code_type: str | None = None
diagnosis_code_1: str | None = None
diagnosis_code_2: str | None = None
diagnosis_code_3: str | None = None
procedure_code_type: str | None = None
procedure_code_1: str | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 9: Part D pharmacy claim header ────────────────────────────────────
class BcdaStgPartDHeader(SQLTable):
"""bcda._stg_part_d_header — EOB Part D filtered and MBI resolved."""
__schema__ = "bcda"
__tablename__ = "_stg_part_d_header"
clm_id: str | None = None
current_bene_mbi_id: str | None = None
bene_id: str | None = None
nch_near_line_rec_ident_cd: str | None = None
clm_from_dt: date | None = None
clm_thru_dt: date | None = None
clm_pmt_amt: float | None = None
clm_tot_chrg_amt: float | None = None
prscrbr_id: str | None = None
rx_srvc_rfrnc_num: str | None = None
# ── Stage 10: Final pharmacy_claim (Part D) ───────────────────────────────────
class BcdaPharmacyClaim(SQLTable):
"""bcda.pharmacy_claim — Part D joined to pharmacy_claim schema."""
__schema__ = "bcda"
__tablename__ = "pharmacy_claim"
claim_id: str | None = None
claim_line_number: int | None = None
person_id: str | None = None
member_id: str | None = None
payer: str | None = None
plan: str | None = None
prescribing_provider_npi: str | None = None
dispensing_provider_npi: str | None = None
dispensing_date: date | None = None
ndc_code: str | None = None
quantity: float | None = None
days_supply: int | None = None
refills: int | None = None
paid_date: date | None = None
paid_amount: float | None = None
allowed_amount: float | None = None
coinsurance_amount: float | None = None
copayment_amount: float | None = None
deductible_amount: float | None = None
in_network_flag: int | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None
# ── Stage 11: Beneficiary demographics staging (Patient + Coverage) ───────────
class BcdaStgBeneficiaryDemographics(SQLTable):
"""bcda._stg_beneficiary_demographics — Patient + Coverage joined and deduplicated."""
__schema__ = "bcda"
__tablename__ = "_stg_beneficiary_demographics"
current_bene_mbi_id: str | None = None
bene_id: str | None = None
bene_birth_dt: date | None = None
bene_sex_cd: str | None = None
bene_race_cd: str | None = None
bene_fips_state_cd: str | None = None
bene_county_cd: str | None = None
bene_zip_cd: str | None = None
bene_1st_name: str | None = None
bene_last_name: str | None = None
crnt_bene_mdcr_stus_cd: str | None = None
covr_yr_mo: str | None = None
# ── Stage 12: Final eligibility (Patient + Coverage) ─────────────────────────
class BcdaEligibility(SQLTable):
"""bcda.eligibility — Patient + Coverage mapped to eligibility schema."""
__schema__ = "bcda"
__tablename__ = "eligibility"
person_id: str | None = None
member_id: str | None = None
gender: str | None = None
race: str | None = None
birth_date: date | None = None
death_date: date | None = None
death_flag: int | None = None
enrollment_start_date: date | None = None
enrollment_end_date: date | None = None
payer: str | None = None
plan: str | None = None
original_reason_entitlement_code: str | None = None
dual_status_code: str | None = None
medicare_status_code: str | None = None
first_name: str | None = None
last_name: str | None = None
address: str | None = None
city: str | None = None
state: str | None = None
zip_code: str | None = None
phone: str | None = None
data_source: str | None = None
file_name: str | None = None
file_date: date | None = None
ingest_datetime: datetime | None = None

View File

@@ -56,13 +56,13 @@ Usage
from .client import COLLECTIONS as COLLECTIONS
from .client import connect as connect
from .ingest import ingest as ingest
from .spider import crawl as crawl
from .spider import crawl_all as crawl_all
from .item import Download as Download
from .item import Item as Item
from .item import Manual as Manual
from .item import Regulation as Regulation
from .item import Rule as Rule
from .item import Source as Source
from .spider import crawl as crawl
from .spider import crawl_all as crawl_all
from .store import Store as Store
from .tag import Tag as Tag

Some files were not shown because too many files have changed in this diff Show More