add comprehensive test suite, CI/CD quality gates, and package publishing
- 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:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,3 +10,6 @@ notebooks/__marimo__/
|
|||||||
.env
|
.env
|
||||||
.deploy_key.pub
|
.deploy_key.pub
|
||||||
**__pycache__**
|
**__pycache__**
|
||||||
|
.coverage
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
|||||||
33
.woodpecker/ci.yml
Normal file
33
.woodpecker/ci.yml
Normal 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
|
||||||
@@ -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:
|
when:
|
||||||
- event: [push, pull_request, manual]
|
- event: push
|
||||||
|
branch: main
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: validate
|
- name: build-package
|
||||||
image: docker:cli
|
image: ghcr.io/astral-sh/uv:python3.13-bookworm-slim
|
||||||
volumes:
|
|
||||||
- /run/user/1000/docker.sock:/var/run/docker.sock
|
|
||||||
commands:
|
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
|
- name: build-notebooks
|
||||||
image: docker:cli
|
image: docker:cli
|
||||||
@@ -36,6 +57,8 @@ steps:
|
|||||||
commands:
|
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 --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}
|
- 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:
|
when:
|
||||||
- path: "notebooks/**"
|
- path: "notebooks/**"
|
||||||
|
|
||||||
@@ -46,6 +69,8 @@ steps:
|
|||||||
commands:
|
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 --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}
|
- 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:
|
when:
|
||||||
- path: "zotero/**"
|
- path: "zotero/**"
|
||||||
|
|
||||||
@@ -61,6 +86,9 @@ steps:
|
|||||||
source: "*.json"
|
source: "*.json"
|
||||||
target: /ci/${CI_REPO}/${CI_COMMIT_SHA:0:8}/
|
target: /ci/${CI_REPO}/${CI_COMMIT_SHA:0:8}/
|
||||||
path_style: true
|
path_style: true
|
||||||
|
depends_on:
|
||||||
|
- scan-notebooks
|
||||||
|
- scan-zotero
|
||||||
when:
|
when:
|
||||||
- path:
|
- path:
|
||||||
- "notebooks/**"
|
- "notebooks/**"
|
||||||
@@ -81,20 +109,6 @@ steps:
|
|||||||
- docker push localhost:3000/homelab/notebooks:latest || true
|
- 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:${CI_COMMIT_SHA:0:8} || true
|
||||||
- docker push localhost:3000/homelab/zotero:latest || true
|
- docker push localhost:3000/homelab/zotero:latest || true
|
||||||
when:
|
depends_on:
|
||||||
- event: push
|
- scan-notebooks
|
||||||
branch: main
|
- scan-zotero
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
195
dev/add_carrier_to_zotero.py
Normal file
195
dev/add_carrier_to_zotero.py
Normal 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()
|
||||||
233
dev/add_zipcode_to_zotero.py
Normal file
233
dev/add_zipcode_to_zotero.py
Normal 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()
|
||||||
0
dev/generate_bls_express.py
Normal file
0
dev/generate_bls_express.py
Normal file
327
dev/generate_cms_express.py
Normal file
327
dev/generate_cms_express.py
Normal 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
0
dev/scrape_bls_data.py
Normal file
590
dev/scrape_cms_data.py
Normal file
590
dev/scrape_cms_data.py
Normal 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()
|
||||||
@@ -9,15 +9,31 @@ dependencies = [
|
|||||||
"databricks-sdk>=0.85.0",
|
"databricks-sdk>=0.85.0",
|
||||||
"fsspec>=2024.1.0",
|
"fsspec>=2024.1.0",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
|
"narwhals>=2.17.0",
|
||||||
"pyarrow>=23.0.0",
|
"pyarrow>=23.0.0",
|
||||||
"sqlglot>=26.0.0",
|
"sqlglot>=26.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"coverage>=7.13.4",
|
||||||
"dbt-core==1.10.15",
|
"dbt-core==1.10.15",
|
||||||
"dbt-duckdb>=1.10,<1.11",
|
"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]
|
[tool.uv.build-backend]
|
||||||
namespace = true
|
namespace = true
|
||||||
source-exclude = ["compose.yml","grafana/**","nginx/**","prometheus/**","rustfs/**","data/**", "loki/**", "notebooks/**", "traefik/**", "woodpecker/**", "gitea/**", "polaris/**", "trino/**", "zotero/**", "tuva/**"]
|
source-exclude = ["compose.yml","grafana/**","nginx/**","prometheus/**","rustfs/**","data/**", "loki/**", "notebooks/**", "traefik/**", "woodpecker/**", "gitea/**", "polaris/**", "trino/**", "zotero/**", "tuva/**"]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -320,6 +320,7 @@ def int_pqi_01_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 01 (diabetes short-term complications), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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(
|
def int_pqi_01_exclusions(
|
||||||
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
|
||||||
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_01_exclusions: FrameT,
|
ahrq_measures___int_pqi_01_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Diabetes short-term complications diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -357,6 +360,7 @@ def int_pqi_03_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 03 (diabetes long-term complications), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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(
|
def int_pqi_03_exclusions(
|
||||||
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
|
||||||
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_03_exclusions: FrameT,
|
ahrq_measures___int_pqi_03_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Diabetes long-term complications diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -394,6 +400,7 @@ def int_pqi_05_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 05 (COPD/asthma), age >= 40."""
|
||||||
return _pqi_denom(
|
return _pqi_denom(
|
||||||
ahrq_measures___stg_pqi_member_months,
|
ahrq_measures___stg_pqi_member_months,
|
||||||
core__patient,
|
core__patient,
|
||||||
@@ -524,6 +531,7 @@ def int_pqi_07_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 07 (hypertension), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_07_exclusions: FrameT,
|
ahrq_measures___int_pqi_07_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Hypertension diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -661,6 +670,7 @@ def int_pqi_08_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 08 (heart failure), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_08_exclusions: FrameT,
|
ahrq_measures___int_pqi_08_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Heart failure diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -741,6 +752,7 @@ def int_pqi_11_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 11 (bacterial pneumonia), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_11_exclusions: FrameT,
|
ahrq_measures___int_pqi_11_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Community-acquired bacterial pneumonia diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -874,6 +887,7 @@ def int_pqi_12_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 12 (urinary tract infection), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_12_exclusions: FrameT,
|
ahrq_measures___int_pqi_12_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Urinary tract infection diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -1007,6 +1022,7 @@ def int_pqi_14_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 14 (uncontrolled diabetes), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
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(
|
def int_pqi_14_exclusions(
|
||||||
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
ahrq_measures___int_pqi_shared_exclusion_union: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Shared exclusions only (missing data, transfer, ungroupable DRG)."""
|
||||||
return _pqi_simple_exclusions(ahrq_measures___int_pqi_shared_exclusion_union)
|
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_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_14_exclusions: FrameT,
|
ahrq_measures___int_pqi_14_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Uncontrolled diabetes diagnosis, in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -1044,6 +1062,7 @@ def int_pqi_15_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 15 (asthma, younger adults), age 18–39."""
|
||||||
return _pqi_denom(
|
return _pqi_denom(
|
||||||
ahrq_measures___stg_pqi_member_months,
|
ahrq_measures___stg_pqi_member_months,
|
||||||
core__patient,
|
core__patient,
|
||||||
@@ -1112,6 +1131,7 @@ def int_pqi_15_num(
|
|||||||
ahrq_measures___int_pqi_15_denom: FrameT,
|
ahrq_measures___int_pqi_15_denom: FrameT,
|
||||||
ahrq_measures___int_pqi_15_exclusions: FrameT,
|
ahrq_measures___int_pqi_15_exclusions: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Asthma diagnosis (age 18–39), in denom, not excluded."""
|
||||||
return _pqi_num(
|
return _pqi_num(
|
||||||
ahrq_measures___stg_pqi_inpatient_encounter,
|
ahrq_measures___stg_pqi_inpatient_encounter,
|
||||||
ahrq_measures___value_set_pqi,
|
ahrq_measures___value_set_pqi,
|
||||||
@@ -1132,6 +1152,7 @@ def int_pqi_16_denom(
|
|||||||
ahrq_measures___stg_pqi_member_months: FrameT,
|
ahrq_measures___stg_pqi_member_months: FrameT,
|
||||||
core__patient: FrameT,
|
core__patient: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
|
"""Eligible members for PQI 16 (lower-extremity amputation), all ages."""
|
||||||
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
return _pqi_denom(ahrq_measures___stg_pqi_member_months, core__patient)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ from typing import Callable
|
|||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||||
|
|
||||||
from aco.pipe.runner import _param_to_table
|
|
||||||
from aco.table.base import SQLTable
|
from aco.table.base import SQLTable
|
||||||
from bib.tag import Tag
|
from bib.tag import Tag
|
||||||
|
|
||||||
@@ -94,6 +93,8 @@ class Expr(BaseModel):
|
|||||||
@property
|
@property
|
||||||
def inputs(self) -> list[str]:
|
def inputs(self) -> list[str]:
|
||||||
"""Derive input table refs from ``fn``'s parameter signature."""
|
"""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)
|
sig = inspect.signature(self.fn)
|
||||||
return [_param_to_table(p) for p in sig.parameters]
|
return [_param_to_table(p) for p in sig.parameters]
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from narwhals.typing import FrameT
|
|
||||||
import narwhals as nw
|
import narwhals as nw
|
||||||
|
from narwhals.typing import FrameT
|
||||||
|
|
||||||
|
|
||||||
@nw.narwhalify
|
@nw.narwhalify
|
||||||
def stg_claims_member_months(
|
def stg_claims_member_months(
|
||||||
claims_preprocessing__member_months: FrameT,
|
claims_preprocessing__member_months: FrameT,
|
||||||
input_layer__input_layer__provider_attribution: FrameT
|
input_layer__input_layer__provider_attribution: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
"""Build core._stg_claims_member_months
|
"""Build core._stg_claims_member_months
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from narwhals.typing import FrameT
|
|
||||||
import narwhals as nw
|
import narwhals as nw
|
||||||
|
from narwhals.typing import FrameT
|
||||||
|
|
||||||
|
|
||||||
@nw.narwhalify
|
@nw.narwhalify
|
||||||
@@ -62,7 +63,10 @@ def stg_observation(core__observation: FrameT) -> FrameT:
|
|||||||
nw.lit("clinical source").alias("payer"),
|
nw.lit("clinical source").alias("payer"),
|
||||||
nw.col("observation_date"),
|
nw.col("observation_date"),
|
||||||
nw.col("result"),
|
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("normalized_code").fill_null(nw.col("source_code")).alias("code"),
|
||||||
nw.col("data_source"),
|
nw.col("data_source"),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from narwhals.typing import FrameT
|
|
||||||
import narwhals as nw
|
import narwhals as nw
|
||||||
|
from narwhals.typing import FrameT
|
||||||
|
|
||||||
|
|
||||||
@nw.narwhalify
|
@nw.narwhalify
|
||||||
@@ -218,7 +219,7 @@ def input_layer__procedure(df: FrameT) -> FrameT:
|
|||||||
|
|
||||||
@nw.narwhalify
|
@nw.narwhalify
|
||||||
def input_layer__provider_attribution(
|
def input_layer__provider_attribution(
|
||||||
input_layer__provider_attribution: FrameT
|
input_layer__provider_attribution: FrameT,
|
||||||
) -> FrameT:
|
) -> FrameT:
|
||||||
"""Build input_layer.input_layer__provider_attribution
|
"""Build input_layer.input_layer__provider_attribution
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,9 @@ from .context import Context as Context
|
|||||||
from .context import DuckDBContext as DuckDBContext
|
from .context import DuckDBContext as DuckDBContext
|
||||||
from .context import EnterpriseContext as EnterpriseContext
|
from .context import EnterpriseContext as EnterpriseContext
|
||||||
from .context import IcebergContext as IcebergContext
|
from .context import IcebergContext as IcebergContext
|
||||||
|
from .context import ParquetContext as ParquetContext
|
||||||
from .context import TrinoContext as TrinoContext
|
from .context import TrinoContext as TrinoContext
|
||||||
|
from .context import WriteMode as WriteMode
|
||||||
from .engine import execute as execute
|
from .engine import execute as execute
|
||||||
from .transpile import transpile as transpile
|
from .transpile import transpile as transpile
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
Two complementary roles:
|
Two complementary roles:
|
||||||
|
|
||||||
1. **Schema catalog** — discovers ``SQLTable`` models from ``aco.table``
|
1. **Schema catalog** — discovers ``SQLTable`` models from all namespace
|
||||||
to know what columns a table *should* have (the contract).
|
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,
|
2. **Iceberg catalog** — talks to an Iceberg REST Catalog (Nessie,
|
||||||
Polaris, Unity Catalog) to know what tables *actually exist* in
|
Polaris, Unity Catalog) to know what tables *actually exist* in
|
||||||
@@ -122,24 +124,23 @@ class Catalog:
|
|||||||
# ── Schema catalog (aco.table introspection) ─────────────────
|
# ── Schema catalog (aco.table introspection) ─────────────────
|
||||||
|
|
||||||
def schemas(self) -> list[str]:
|
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
|
Discovers modules in every namespace listed in
|
||||||
the ``__schema__`` attribute from ``SQLTable`` subclasses.
|
``_TABLE_NAMESPACES`` and extracts the ``__schema__``
|
||||||
|
attribute from ``SQLTable`` subclasses.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
list[str]
|
list[str]
|
||||||
Sorted unique schema names like ``['core', 'readmissions', ...]``.
|
Sorted unique schema names like ``['bcda', 'core', 'cms', ...]``.
|
||||||
"""
|
"""
|
||||||
schemas = set()
|
schemas = set()
|
||||||
|
|
||||||
# Discover all table modules
|
for ns, module_name in self._discover_table_modules():
|
||||||
for module_name in self._discover_table_modules():
|
|
||||||
try:
|
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):
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if (
|
if (
|
||||||
issubclass(obj, SQLTable)
|
issubclass(obj, SQLTable)
|
||||||
@@ -149,7 +150,6 @@ class Catalog:
|
|||||||
):
|
):
|
||||||
schemas.add(obj.__schema__)
|
schemas.add(obj.__schema__)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Skip modules that fail to import
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return sorted(schemas)
|
return sorted(schemas)
|
||||||
@@ -169,9 +169,9 @@ class Catalog:
|
|||||||
"""
|
"""
|
||||||
tables = []
|
tables = []
|
||||||
|
|
||||||
for module_name in self._discover_table_modules():
|
for ns, module_name in self._discover_table_modules():
|
||||||
try:
|
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):
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if (
|
if (
|
||||||
@@ -184,7 +184,6 @@ class Catalog:
|
|||||||
):
|
):
|
||||||
qualified = f"{obj.__schema__}.{obj.__tablename__}"
|
qualified = f"{obj.__schema__}.{obj.__tablename__}"
|
||||||
tables.append(qualified)
|
tables.append(qualified)
|
||||||
# Cache the model for later use
|
|
||||||
self._model_cache[qualified] = obj
|
self._model_cache[qualified] = obj
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
@@ -235,10 +234,10 @@ class Catalog:
|
|||||||
|
|
||||||
schema, table = table_ref.split(".", 1)
|
schema, table = table_ref.split(".", 1)
|
||||||
|
|
||||||
# Search for the model
|
# Search for the model across all namespaces
|
||||||
for module_name in self._discover_table_modules():
|
for ns, module_name in self._discover_table_modules():
|
||||||
try:
|
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):
|
for name, obj in inspect.getmembers(module, inspect.isclass):
|
||||||
if (
|
if (
|
||||||
@@ -290,23 +289,35 @@ class Catalog:
|
|||||||
return self.column_map[table_ref].get(column_name, column_name)
|
return self.column_map[table_ref].get(column_name, column_name)
|
||||||
return column_name
|
return column_name
|
||||||
|
|
||||||
def _discover_table_modules(self) -> list[str]:
|
# Namespaces whose ``table`` packages contain SQLTable models.
|
||||||
"""Discover all Python modules in aco.table package.
|
# 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
|
Returns
|
||||||
-------
|
-------
|
||||||
list[str]
|
list[tuple[str, str]]
|
||||||
Module names (without aco.table prefix).
|
``(namespace, module_name)`` pairs, e.g.
|
||||||
|
``[("aco", "core"), ("cms", "accountable_care_organizations"), ...]``.
|
||||||
"""
|
"""
|
||||||
|
import importlib
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
import aco.table as table_pkg
|
results: list[tuple[str, str]] = []
|
||||||
|
for ns in self._TABLE_NAMESPACES:
|
||||||
modules = []
|
try:
|
||||||
for importer, modname, ispkg in pkgutil.iter_modules(table_pkg.__path__):
|
pkg = importlib.import_module(f"{ns}.table")
|
||||||
|
except ImportError:
|
||||||
|
continue
|
||||||
|
for _, modname, _ in pkgutil.iter_modules(pkg.__path__):
|
||||||
if not modname.startswith("_"):
|
if not modname.startswith("_"):
|
||||||
modules.append(modname)
|
results.append((ns, modname))
|
||||||
return modules
|
return results
|
||||||
|
|
||||||
# ── Iceberg catalog (REST API via PyIceberg) ─────────────────
|
# ── Iceberg catalog (REST API via PyIceberg) ─────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -66,13 +66,23 @@ Usage::
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
import narwhals as nw
|
import narwhals as nw
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from aco.lake.catalog import Catalog
|
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):
|
class Context(BaseModel):
|
||||||
"""Base class for all storage contexts.
|
"""Base class for all storage contexts.
|
||||||
@@ -80,6 +90,9 @@ class Context(BaseModel):
|
|||||||
Subclasses implement ``load`` and ``save`` for their specific
|
Subclasses implement ``load`` and ``save`` for their specific
|
||||||
storage backend. ``load`` returns a narwhals-compatible DataFrame;
|
storage backend. ``load`` returns a narwhals-compatible DataFrame;
|
||||||
``save`` accepts one and persists it.
|
``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)
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
@@ -99,7 +112,13 @@ class Context(BaseModel):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
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.
|
"""Write a DataFrame back to storage.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -108,6 +127,9 @@ class Context(BaseModel):
|
|||||||
Qualified table name in ``schema.table`` format.
|
Qualified table name in ``schema.table`` format.
|
||||||
df : DataFrame
|
df : DataFrame
|
||||||
A narwhals-compatible DataFrame to persist.
|
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
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -217,7 +239,13 @@ class DuckDBContext(Context):
|
|||||||
f"Failed to load table {physical_ref} from {self.database}: {e}"
|
f"Failed to load table {physical_ref} from {self.database}: {e}"
|
||||||
) from 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.
|
"""Write a DataFrame to DuckDB.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -226,6 +254,9 @@ class DuckDBContext(Context):
|
|||||||
Qualified table name (e.g. ``readmissions.encounter``).
|
Qualified table name (e.g. ``readmissions.encounter``).
|
||||||
df : DataFrame
|
df : DataFrame
|
||||||
Narwhals DataFrame to persist.
|
Narwhals DataFrame to persist.
|
||||||
|
mode : WriteMode
|
||||||
|
``"append"`` (default) inserts rows, creating the table
|
||||||
|
on first write. ``"replace"`` drops and recreates.
|
||||||
|
|
||||||
Raises
|
Raises
|
||||||
------
|
------
|
||||||
@@ -253,7 +284,6 @@ class DuckDBContext(Context):
|
|||||||
# Apply column mapping if catalog provided
|
# Apply column mapping if catalog provided
|
||||||
df_to_save = df
|
df_to_save = df
|
||||||
if self.catalog and table_ref in self.catalog.column_map:
|
if self.catalog and table_ref in self.catalog.column_map:
|
||||||
# Rename canonical columns to physical names
|
|
||||||
rename_dict = {
|
rename_dict = {
|
||||||
canonical: physical
|
canonical: physical
|
||||||
for canonical, physical in self.catalog.column_map[table_ref].items()
|
for canonical, physical in self.catalog.column_map[table_ref].items()
|
||||||
@@ -263,21 +293,24 @@ class DuckDBContext(Context):
|
|||||||
if rename_dict:
|
if rename_dict:
|
||||||
df_to_save = df.rename(rename_dict)
|
df_to_save = df.rename(rename_dict)
|
||||||
|
|
||||||
# Get connection
|
|
||||||
con = self._get_connection()
|
con = self._get_connection()
|
||||||
|
pl_df = nw.to_native(df_to_save) # noqa: F841 used in SQL
|
||||||
# Convert to polars for DuckDB write
|
|
||||||
pl_df = nw.to_native(df_to_save)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create schema if it doesn't exist
|
|
||||||
con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
|
con.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema}"')
|
||||||
|
|
||||||
# Drop table if exists and recreate (replace mode)
|
if mode == "replace":
|
||||||
con.execute(f'DROP TABLE IF EXISTS "{schema}"."{table}"')
|
con.execute(f'DROP TABLE IF EXISTS "{schema}"."{table}"')
|
||||||
|
|
||||||
# Write table
|
|
||||||
con.execute(f'CREATE TABLE "{schema}"."{table}" AS SELECT * FROM pl_df')
|
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:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
@@ -310,6 +343,179 @@ class DuckDBContext(Context):
|
|||||||
pass
|
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 ────────────────────────────────────────────────
|
# ── Iceberg contexts ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -427,7 +633,13 @@ class IcebergContext(Context):
|
|||||||
f"Failed to load Iceberg table {physical_ref}: {e}"
|
f"Failed to load Iceberg table {physical_ref}: {e}"
|
||||||
) from 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.
|
"""Write a DataFrame to Iceberg.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -436,6 +648,9 @@ class IcebergContext(Context):
|
|||||||
Qualified table name.
|
Qualified table name.
|
||||||
df : DataFrame
|
df : DataFrame
|
||||||
Narwhals DataFrame to persist.
|
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
|
# Map to physical table if catalog provided
|
||||||
physical_ref = table_ref
|
physical_ref = table_ref
|
||||||
@@ -459,8 +674,6 @@ class IcebergContext(Context):
|
|||||||
df_to_save = df.rename(rename_dict)
|
df_to_save = df.rename(rename_dict)
|
||||||
|
|
||||||
# Convert to arrow
|
# Convert to arrow
|
||||||
import polars as pl
|
|
||||||
|
|
||||||
pl_df = nw.to_native(df_to_save)
|
pl_df = nw.to_native(df_to_save)
|
||||||
arrow_table = pl_df.to_arrow()
|
arrow_table = pl_df.to_arrow()
|
||||||
|
|
||||||
@@ -471,14 +684,15 @@ class IcebergContext(Context):
|
|||||||
# Load or create table
|
# Load or create table
|
||||||
try:
|
try:
|
||||||
table = pyice_cat.load_table((*namespace, table_name))
|
table = pyice_cat.load_table((*namespace, table_name))
|
||||||
# Append to existing table
|
if mode == "replace":
|
||||||
|
table.overwrite(arrow_table)
|
||||||
|
else:
|
||||||
table.append(arrow_table)
|
table.append(arrow_table)
|
||||||
except Exception:
|
except Exception:
|
||||||
# Table doesn't exist, create it
|
# Table doesn't exist — create it
|
||||||
from pyiceberg.schema import Schema
|
from pyiceberg.schema import Schema
|
||||||
from pyiceberg.types import NestedField
|
from pyiceberg.types import NestedField
|
||||||
|
|
||||||
# Convert arrow schema to Iceberg schema
|
|
||||||
fields = []
|
fields = []
|
||||||
for i, field in enumerate(arrow_table.schema):
|
for i, field in enumerate(arrow_table.schema):
|
||||||
fields.append(
|
fields.append(
|
||||||
@@ -492,18 +706,14 @@ class IcebergContext(Context):
|
|||||||
|
|
||||||
schema = Schema(*fields)
|
schema = Schema(*fields)
|
||||||
|
|
||||||
# Create namespace if needed
|
|
||||||
try:
|
try:
|
||||||
pyice_cat.create_namespace(namespace)
|
pyice_cat.create_namespace(namespace)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass # Namespace already exists
|
pass # Namespace already exists
|
||||||
|
|
||||||
# Create table
|
|
||||||
table = pyice_cat.create_table(
|
table = pyice_cat.create_table(
|
||||||
identifier=(*namespace, table_name), schema=schema
|
identifier=(*namespace, table_name), schema=schema
|
||||||
)
|
)
|
||||||
|
|
||||||
# Write data
|
|
||||||
table.append(arrow_table)
|
table.append(arrow_table)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -67,7 +67,9 @@ from aco.lake.context import (
|
|||||||
DuckDBContext,
|
DuckDBContext,
|
||||||
EnterpriseContext,
|
EnterpriseContext,
|
||||||
IcebergContext,
|
IcebergContext,
|
||||||
|
ParquetContext,
|
||||||
TrinoContext,
|
TrinoContext,
|
||||||
|
WriteMode,
|
||||||
)
|
)
|
||||||
from aco.pipe.base import Pipeline
|
from aco.pipe.base import Pipeline
|
||||||
|
|
||||||
@@ -77,6 +79,7 @@ def execute(
|
|||||||
context: Context,
|
context: Context,
|
||||||
save_outputs: bool = False,
|
save_outputs: bool = False,
|
||||||
output_filter: set[str] | None = None,
|
output_filter: set[str] | None = None,
|
||||||
|
mode: WriteMode = "append",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Run a pipeline against a storage context.
|
"""Run a pipeline against a storage context.
|
||||||
|
|
||||||
@@ -97,6 +100,9 @@ def execute(
|
|||||||
If True, persist results back to storage via ``context.save``.
|
If True, persist results back to storage via ``context.save``.
|
||||||
output_filter : set[str] | None
|
output_filter : set[str] | None
|
||||||
If provided, only save tables in this set. If None, save all.
|
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
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -128,10 +134,10 @@ def execute(
|
|||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
# Dispatch based on context type
|
# Dispatch based on context type
|
||||||
if isinstance(context, (DuckDBContext, IcebergContext)):
|
if isinstance(context, (DuckDBContext, IcebergContext, ParquetContext)):
|
||||||
return _execute_direct(pipeline, context, save_outputs, output_filter)
|
return _execute_direct(pipeline, context, save_outputs, output_filter, mode)
|
||||||
elif isinstance(context, (TrinoContext, EnterpriseContext)):
|
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:
|
else:
|
||||||
raise TypeError(f"Unsupported context type: {type(context)}")
|
raise TypeError(f"Unsupported context type: {type(context)}")
|
||||||
|
|
||||||
@@ -141,6 +147,7 @@ def _execute_direct(
|
|||||||
context: Context,
|
context: Context,
|
||||||
save_outputs: bool = False,
|
save_outputs: bool = False,
|
||||||
output_filter: set[str] | None = None,
|
output_filter: set[str] | None = None,
|
||||||
|
mode: WriteMode = "append",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Run express functions locally via narwhals.
|
"""Run express functions locally via narwhals.
|
||||||
|
|
||||||
@@ -148,53 +155,36 @@ def _execute_direct(
|
|||||||
|
|
||||||
1. ``context.load(table_ref)`` → narwhals DataFrame
|
1. ``context.load(table_ref)`` → narwhals DataFrame
|
||||||
2. ``pipeline.run(context.load)`` → cache of results
|
2. ``pipeline.run(context.load)`` → cache of results
|
||||||
3. Optionally ``context.save(ref, df)`` for each result
|
3. Optionally ``context.save(ref, df, mode=mode)`` 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()
|
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
pipeline : Pipeline
|
pipeline : Pipeline
|
||||||
Pipeline to execute.
|
Pipeline to execute.
|
||||||
context : Context
|
context : Context
|
||||||
DuckDBContext or IcebergContext.
|
DuckDBContext, IcebergContext, or ParquetContext.
|
||||||
save_outputs : bool
|
save_outputs : bool
|
||||||
Whether to persist results back to storage.
|
Whether to persist results back to storage.
|
||||||
output_filter : set[str] | None
|
output_filter : set[str] | None
|
||||||
If provided, only save tables in this set.
|
If provided, only save tables in this set.
|
||||||
|
mode : WriteMode
|
||||||
|
Passed through to ``context.save()``.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
dict[str, Any]
|
dict[str, Any]
|
||||||
Cache of all computed DataFrames by table name.
|
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)
|
results = pipeline.run(context.load)
|
||||||
|
|
||||||
# Optionally save outputs
|
|
||||||
if save_outputs:
|
if save_outputs:
|
||||||
tables_to_save = output_filter if output_filter else set(pipeline.names())
|
tables_to_save = output_filter if output_filter else set(pipeline.names())
|
||||||
|
|
||||||
for table_ref in tables_to_save:
|
for table_ref in tables_to_save:
|
||||||
if table_ref in results:
|
if table_ref in results:
|
||||||
try:
|
try:
|
||||||
context.save(table_ref, results[table_ref])
|
context.save(table_ref, results[table_ref], mode=mode)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Log error but don't fail entire execution
|
|
||||||
print(f"Warning: Failed to save {table_ref}: {e}")
|
print(f"Warning: Failed to save {table_ref}: {e}")
|
||||||
|
|
||||||
return results
|
return results
|
||||||
@@ -205,6 +195,7 @@ def _execute_transpiled(
|
|||||||
context: Context,
|
context: Context,
|
||||||
save_outputs: bool = False,
|
save_outputs: bool = False,
|
||||||
output_filter: set[str] | None = None,
|
output_filter: set[str] | None = None,
|
||||||
|
mode: WriteMode = "append",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Transpile express functions to SQL and push to remote engine.
|
"""Transpile express functions to SQL and push to remote engine.
|
||||||
|
|
||||||
@@ -247,20 +238,8 @@ def _execute_transpiled(
|
|||||||
NotImplementedError
|
NotImplementedError
|
||||||
Transpilation is not yet implemented.
|
Transpilation is not yet implemented.
|
||||||
"""
|
"""
|
||||||
import duckdb
|
|
||||||
|
|
||||||
from aco.lake.transpile import transpile
|
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
|
# Determine target dialect and catalog from context
|
||||||
dialect = "databricks"
|
dialect = "databricks"
|
||||||
catalog_name = ""
|
catalog_name = ""
|
||||||
@@ -271,13 +250,15 @@ def _execute_transpiled(
|
|||||||
dialect = "trino"
|
dialect = "trino"
|
||||||
catalog_name = context.catalog
|
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(
|
sql_map = transpile(
|
||||||
pipeline,
|
pipeline,
|
||||||
con,
|
|
||||||
target_dialect=dialect,
|
target_dialect=dialect,
|
||||||
catalog=catalog_name,
|
catalog=catalog_name,
|
||||||
output_mode="ctas",
|
output_mode=output_mode,
|
||||||
)
|
)
|
||||||
con.close()
|
|
||||||
|
|
||||||
return sql_map
|
return sql_map
|
||||||
|
|||||||
@@ -5,12 +5,24 @@ to capture the SQL that narwhals operations produce, then uses
|
|||||||
sqlglot to transpile from DuckDB dialect to the target (Databricks,
|
sqlglot to transpile from DuckDB dialect to the target (Databricks,
|
||||||
Trino, etc.) and rewrite table references.
|
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::
|
Usage::
|
||||||
|
|
||||||
import duckdb
|
|
||||||
from aco.lake.transpile import transpile
|
from aco.lake.transpile import transpile
|
||||||
from aco.pipe import readmissions
|
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)
|
con = duckdb.connect("notebooks/aco.duckdb", read_only=True)
|
||||||
sql_map = transpile(
|
sql_map = transpile(
|
||||||
readmissions.pipeline,
|
readmissions.pipeline,
|
||||||
@@ -27,7 +39,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import re
|
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 duckdb
|
||||||
import sqlglot
|
import sqlglot
|
||||||
@@ -36,14 +51,80 @@ from sqlglot import exp
|
|||||||
from aco.pipe.base import Pipeline
|
from aco.pipe.base import Pipeline
|
||||||
from aco.pipe.runner import _param_to_table
|
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(
|
def transpile(
|
||||||
pipeline: Pipeline,
|
pipeline: Pipeline,
|
||||||
con: duckdb.DuckDBPyConnection,
|
con: duckdb.DuckDBPyConnection | None = None,
|
||||||
*,
|
*,
|
||||||
target_dialect: str = "databricks",
|
target_dialect: str = "databricks",
|
||||||
catalog: str = "",
|
catalog: str = "",
|
||||||
output_mode: str = "ctas",
|
output_mode: str = "insert",
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Generate target-dialect SQL for every expression in a pipeline.
|
"""Generate target-dialect SQL for every expression in a pipeline.
|
||||||
|
|
||||||
@@ -51,9 +132,11 @@ def transpile(
|
|||||||
----------
|
----------
|
||||||
pipeline : Pipeline
|
pipeline : Pipeline
|
||||||
Pipeline whose expressions will be transpiled.
|
Pipeline whose expressions will be transpiled.
|
||||||
con : duckdb.DuckDBPyConnection
|
con : duckdb.DuckDBPyConnection | None
|
||||||
DuckDB connection with source tables (read-only is fine).
|
DuckDB connection with source tables (read-only is fine).
|
||||||
Used to create relations that capture SQL through narwhals.
|
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
|
target_dialect : str
|
||||||
sqlglot output dialect (``databricks``, ``trino``, ``snowflake``).
|
sqlglot output dialect (``databricks``, ``trino``, ``snowflake``).
|
||||||
catalog : str
|
catalog : str
|
||||||
@@ -61,7 +144,8 @@ def transpile(
|
|||||||
E.g. ``"homelab"`` turns ``core.encounter`` into
|
E.g. ``"homelab"`` turns ``core.encounter`` into
|
||||||
``homelab.core.encounter``.
|
``homelab.core.encounter``.
|
||||||
output_mode : str
|
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
|
``"select"`` — bare SELECT statements
|
||||||
``"view"`` — CREATE OR REPLACE VIEW ... AS SELECT
|
``"view"`` — CREATE OR REPLACE VIEW ... AS SELECT
|
||||||
|
|
||||||
@@ -70,9 +154,16 @@ def transpile(
|
|||||||
dict[str, str]
|
dict[str, str]
|
||||||
Mapping of expression name to transpiled SQL string.
|
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] = {}
|
cache: dict[str, Any] = {}
|
||||||
result: dict[str, str] = {}
|
result: dict[str, str] = {}
|
||||||
|
view_registry: dict[str, str] = {}
|
||||||
|
|
||||||
|
try:
|
||||||
for expr in pipeline.exprs:
|
for expr in pipeline.exprs:
|
||||||
name = expr.name
|
name = expr.name
|
||||||
fn = expr.fn
|
fn = expr.fn
|
||||||
@@ -103,7 +194,7 @@ def transpile(
|
|||||||
duckdb_sql = relation.sql_query()
|
duckdb_sql = relation.sql_query()
|
||||||
|
|
||||||
# Register as temp view for downstream expressions
|
# Register as temp view for downstream expressions
|
||||||
view_name = _table_ref_to_view_name(name)
|
view_name = _table_ref_to_view_name(name, view_registry)
|
||||||
con.execute(f"CREATE OR REPLACE TEMP VIEW {view_name} AS {duckdb_sql}")
|
con.execute(f"CREATE OR REPLACE TEMP VIEW {view_name} AS {duckdb_sql}")
|
||||||
cache[name] = con.sql(f"SELECT * FROM {view_name}")
|
cache[name] = con.sql(f"SELECT * FROM {view_name}")
|
||||||
|
|
||||||
@@ -114,8 +205,12 @@ def transpile(
|
|||||||
target_dialect=target_dialect,
|
target_dialect=target_dialect,
|
||||||
catalog=catalog,
|
catalog=catalog,
|
||||||
output_mode=output_mode,
|
output_mode=output_mode,
|
||||||
|
view_registry=view_registry,
|
||||||
)
|
)
|
||||||
result[name] = target_sql
|
result[name] = target_sql
|
||||||
|
finally:
|
||||||
|
if own_connection:
|
||||||
|
con.close()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@@ -129,13 +224,21 @@ def _load_relation(
|
|||||||
return con.sql(f'SELECT * FROM "{schema}"."{table}"')
|
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.
|
"""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(".", "__")
|
safe = table_ref.replace(".", "_0_")
|
||||||
return f'"_tv_{safe}"'
|
view_name = f"_tv_{safe}"
|
||||||
|
registry[view_name] = table_ref
|
||||||
|
return f'"{view_name}"'
|
||||||
|
|
||||||
|
|
||||||
def _transpile_sql(
|
def _transpile_sql(
|
||||||
@@ -145,6 +248,7 @@ def _transpile_sql(
|
|||||||
target_dialect: str,
|
target_dialect: str,
|
||||||
catalog: str,
|
catalog: str,
|
||||||
output_mode: str,
|
output_mode: str,
|
||||||
|
view_registry: dict[str, str],
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Transpile DuckDB SQL to target dialect with table ref rewriting."""
|
"""Transpile DuckDB SQL to target dialect with table ref rewriting."""
|
||||||
tree = sqlglot.parse_one(duckdb_sql, read="duckdb")
|
tree = sqlglot.parse_one(duckdb_sql, read="duckdb")
|
||||||
@@ -155,8 +259,9 @@ def _transpile_sql(
|
|||||||
db = table.db # schema in sqlglot terminology
|
db = table.db # schema in sqlglot terminology
|
||||||
|
|
||||||
# Rewrite temp view names back to proper schema.table
|
# Rewrite temp view names back to proper schema.table
|
||||||
if tbl_name.startswith("_tv_"):
|
# using the registry for lossless lookup
|
||||||
real_ref = tbl_name[4:].replace("__", ".", 1)
|
if tbl_name in view_registry:
|
||||||
|
real_ref = view_registry[tbl_name]
|
||||||
parts = real_ref.split(".", 1)
|
parts = real_ref.split(".", 1)
|
||||||
if len(parts) == 2:
|
if len(parts) == 2:
|
||||||
table.set("db", exp.to_identifier(parts[0]))
|
table.set("db", exp.to_identifier(parts[0]))
|
||||||
@@ -181,7 +286,9 @@ def _transpile_sql(
|
|||||||
if catalog:
|
if catalog:
|
||||||
target_ref = f"{catalog}.{expr_name}"
|
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}"
|
return f"CREATE OR REPLACE TABLE {target_ref} AS\n{select_sql}"
|
||||||
elif output_mode == "view":
|
elif output_mode == "view":
|
||||||
return f"CREATE OR REPLACE VIEW {target_ref} AS\n{select_sql}"
|
return f"CREATE OR REPLACE VIEW {target_ref} AS\n{select_sql}"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ Usage::
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Any, Literal
|
from typing import Any
|
||||||
|
|
||||||
from databricks.sdk import WorkspaceClient
|
from databricks.sdk import WorkspaceClient
|
||||||
from databricks.sdk.service.catalog import (
|
from databricks.sdk.service.catalog import (
|
||||||
@@ -31,7 +31,7 @@ from databricks.sdk.service.catalog import (
|
|||||||
TableInfo,
|
TableInfo,
|
||||||
VolumeInfo,
|
VolumeInfo,
|
||||||
)
|
)
|
||||||
from pydantic import BaseModel, ConfigDict, Field
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
# ── Pydantic Models ──────────────────────────────────────────────────
|
# ── Pydantic Models ──────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -448,7 +448,6 @@ def _sql_table_to_column_infos(
|
|||||||
model: type,
|
model: type,
|
||||||
) -> list:
|
) -> list:
|
||||||
"""Convert SQLTable model fields to Databricks ColumnInfo objects."""
|
"""Convert SQLTable model fields to Databricks ColumnInfo objects."""
|
||||||
import json
|
|
||||||
|
|
||||||
from databricks.sdk.service.catalog import ColumnInfo, ColumnTypeName
|
from databricks.sdk.service.catalog import ColumnInfo, ColumnTypeName
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -60,6 +60,27 @@ from __future__ import annotations
|
|||||||
from aco.express import cclf as ex
|
from aco.express import cclf as ex
|
||||||
from aco.express.base import Expr
|
from aco.express.base import Expr
|
||||||
from aco.pipe.base import Pipeline
|
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
|
from bib.tag import Tag
|
||||||
|
|
||||||
# Shared refs for the CCLF module -- CMS CCLF Information Packet
|
# Shared refs for the CCLF module -- CMS CCLF Information Packet
|
||||||
@@ -74,6 +95,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_beneficiary_xref",
|
name="cclf._stg_beneficiary_xref",
|
||||||
fn=ex.stg_beneficiary_xref,
|
fn=ex.stg_beneficiary_xref,
|
||||||
|
output=CclfStgBeneficiaryXref,
|
||||||
after=["cclf.cclf9"],
|
after=["cclf.cclf9"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -86,6 +108,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_institutional_header",
|
name="cclf._stg_institutional_header",
|
||||||
fn=ex.stg_institutional_header,
|
fn=ex.stg_institutional_header,
|
||||||
|
output=CclfStgInstitutionalHeader,
|
||||||
after=["cclf.cclf1", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf1", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -97,6 +120,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_institutional_header_adr",
|
name="cclf._int_institutional_header_adr",
|
||||||
fn=ex.int_institutional_header_adr,
|
fn=ex.int_institutional_header_adr,
|
||||||
|
output=CclfIntInstitutionalHeaderAdr,
|
||||||
after=["cclf._stg_institutional_header"],
|
after=["cclf._stg_institutional_header"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -109,6 +133,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_revenue_center",
|
name="cclf._stg_revenue_center",
|
||||||
fn=ex.stg_revenue_center,
|
fn=ex.stg_revenue_center,
|
||||||
|
output=CclfStgRevenueCenter,
|
||||||
after=["cclf.cclf2", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf2", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -119,6 +144,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_diagnosis_pivot",
|
name="cclf._int_diagnosis_pivot",
|
||||||
fn=ex.int_diagnosis_pivot,
|
fn=ex.int_diagnosis_pivot,
|
||||||
|
output=CclfIntDiagnosisPivot,
|
||||||
after=["cclf.cclf4"],
|
after=["cclf.cclf4"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -130,6 +156,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_procedure_pivot",
|
name="cclf._int_procedure_pivot",
|
||||||
fn=ex.int_procedure_pivot,
|
fn=ex.int_procedure_pivot,
|
||||||
|
output=CclfIntProcedurePivot,
|
||||||
after=["cclf.cclf3"],
|
after=["cclf.cclf3"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -141,6 +168,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_institutional_medical_claim",
|
name="cclf._int_institutional_medical_claim",
|
||||||
fn=ex.int_institutional_medical_claim,
|
fn=ex.int_institutional_medical_claim,
|
||||||
|
output=CclfIntInstitutionalMedicalClaim,
|
||||||
after=[
|
after=[
|
||||||
"cclf._int_institutional_header_adr",
|
"cclf._int_institutional_header_adr",
|
||||||
"cclf._stg_revenue_center",
|
"cclf._stg_revenue_center",
|
||||||
@@ -160,6 +188,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_physician_claim",
|
name="cclf._stg_physician_claim",
|
||||||
fn=ex.stg_physician_claim,
|
fn=ex.stg_physician_claim,
|
||||||
|
output=CclfStgPhysicianClaim,
|
||||||
after=["cclf.cclf5", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf5", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -171,6 +200,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_physician_claim_adr",
|
name="cclf._int_physician_claim_adr",
|
||||||
fn=ex.int_physician_claim_adr,
|
fn=ex.int_physician_claim_adr,
|
||||||
|
output=CclfIntPhysicianClaimAdr,
|
||||||
after=["cclf._stg_physician_claim"],
|
after=["cclf._stg_physician_claim"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -183,6 +213,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_physician_medical_claim",
|
name="cclf._int_physician_medical_claim",
|
||||||
fn=ex.int_physician_medical_claim,
|
fn=ex.int_physician_medical_claim,
|
||||||
|
output=CclfIntPhysicianMedicalClaim,
|
||||||
after=["cclf._int_physician_claim_adr"],
|
after=["cclf._int_physician_claim_adr"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -195,6 +226,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_dme_claim",
|
name="cclf._stg_dme_claim",
|
||||||
fn=ex.stg_dme_claim,
|
fn=ex.stg_dme_claim,
|
||||||
|
output=CclfStgDmeClaim,
|
||||||
after=["cclf.cclf6", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf6", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -206,6 +238,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_dme_claim_adr",
|
name="cclf._int_dme_claim_adr",
|
||||||
fn=ex.int_dme_claim_adr,
|
fn=ex.int_dme_claim_adr,
|
||||||
|
output=CclfIntDmeClaimAdr,
|
||||||
after=["cclf._stg_dme_claim"],
|
after=["cclf._stg_dme_claim"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -217,6 +250,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_dme_medical_claim",
|
name="cclf._int_dme_medical_claim",
|
||||||
fn=ex.int_dme_medical_claim,
|
fn=ex.int_dme_medical_claim,
|
||||||
|
output=CclfIntDmeMedicalClaim,
|
||||||
after=["cclf._int_dme_claim_adr"],
|
after=["cclf._int_dme_claim_adr"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -230,6 +264,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf.medical_claim",
|
name="cclf.medical_claim",
|
||||||
fn=ex.medical_claim,
|
fn=ex.medical_claim,
|
||||||
|
output=CclfMedicalClaim,
|
||||||
after=[
|
after=[
|
||||||
"cclf._int_institutional_medical_claim",
|
"cclf._int_institutional_medical_claim",
|
||||||
"cclf._int_physician_medical_claim",
|
"cclf._int_physician_medical_claim",
|
||||||
@@ -245,6 +280,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_pharmacy_claim",
|
name="cclf._stg_pharmacy_claim",
|
||||||
fn=ex.stg_pharmacy_claim,
|
fn=ex.stg_pharmacy_claim,
|
||||||
|
output=CclfStgPharmacyClaim,
|
||||||
after=["cclf.cclf7", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf7", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=("Stages CCLF7 with MBI resolution via CCLF9 crosswalk."),
|
description=("Stages CCLF7 with MBI resolution via CCLF9 crosswalk."),
|
||||||
@@ -252,6 +288,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._int_pharmacy_claim_adr",
|
name="cclf._int_pharmacy_claim_adr",
|
||||||
fn=ex.int_pharmacy_claim_adr,
|
fn=ex.int_pharmacy_claim_adr,
|
||||||
|
output=CclfIntPharmacyClaimAdr,
|
||||||
after=["cclf._stg_pharmacy_claim"],
|
after=["cclf._stg_pharmacy_claim"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -263,6 +300,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf.pharmacy_claim",
|
name="cclf.pharmacy_claim",
|
||||||
fn=ex.pharmacy_claim,
|
fn=ex.pharmacy_claim,
|
||||||
|
output=CclfPharmacyClaim,
|
||||||
after=["cclf._int_pharmacy_claim_adr"],
|
after=["cclf._int_pharmacy_claim_adr"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -275,6 +313,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf._stg_beneficiary_demographics",
|
name="cclf._stg_beneficiary_demographics",
|
||||||
fn=ex.stg_beneficiary_demographics,
|
fn=ex.stg_beneficiary_demographics,
|
||||||
|
output=CclfStgBeneficiaryDemographics,
|
||||||
after=["cclf.cclf8", "cclf._stg_beneficiary_xref"],
|
after=["cclf.cclf8", "cclf._stg_beneficiary_xref"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -285,6 +324,7 @@ pipeline = Pipeline(
|
|||||||
Expr(
|
Expr(
|
||||||
name="cclf.eligibility",
|
name="cclf.eligibility",
|
||||||
fn=ex.eligibility,
|
fn=ex.eligibility,
|
||||||
|
output=CclfEligibility,
|
||||||
after=["cclf._stg_beneficiary_demographics"],
|
after=["cclf._stg_beneficiary_demographics"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
|
|||||||
)
|
)
|
||||||
if value_elem is not None and cell_type == "s":
|
if value_elem is not None and cell_type == "s":
|
||||||
idx = int(value_elem.text)
|
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:
|
else:
|
||||||
headers.append(None)
|
headers.append(None)
|
||||||
|
|
||||||
@@ -83,7 +85,9 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
|
|||||||
if value_elem is not None:
|
if value_elem is not None:
|
||||||
if cell_type == "s":
|
if cell_type == "s":
|
||||||
idx = int(value_elem.text)
|
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:
|
else:
|
||||||
value = value_elem.text
|
value = value_elem.text
|
||||||
row_data[field_mapping[i]] = value
|
row_data[field_mapping[i]] = value
|
||||||
@@ -97,9 +101,19 @@ def parse_reach_participants(file_path: str) -> Iterator[ReachParticipants]:
|
|||||||
# Convert boolean fields
|
# Convert boolean fields
|
||||||
for field_name, value in row_data.items():
|
for field_name, value in row_data.items():
|
||||||
if field_name.startswith("i_attest"):
|
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
|
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
|
row_data[field_name] = False
|
||||||
else:
|
else:
|
||||||
row_data[field_name] = None
|
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:
|
def normalize_field_name(header: str) -> str:
|
||||||
"""Convert header to Python snake_case field name."""
|
"""Convert header to Python snake_case field name."""
|
||||||
import re
|
import re
|
||||||
|
|
||||||
header = re.sub(r"\s+", " ", header.strip())
|
header = re.sub(r"\s+", " ", header.strip())
|
||||||
header = re.sub(r"[^\w\s]", "", header)
|
header = re.sub(r"[^\w\s]", "", header)
|
||||||
header = re.sub(r"\s+", "_", header)
|
header = re.sub(r"\s+", "_", header)
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ ingestion into the ACO data platform.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
import re
|
|
||||||
from datetime import date, datetime
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
|
||||||
import openpyxl
|
import openpyxl
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
"""Generated Pydantic models for all DuckDB tables."""
|
"""Generated Pydantic models for all DuckDB tables."""
|
||||||
|
|
||||||
from . import ahrq_measures
|
from . import ( # noqa: F401
|
||||||
from . import ccsr
|
ahrq_measures,
|
||||||
from . import chronic_conditions
|
ccsr,
|
||||||
from . import claims_preprocessing
|
chronic_conditions,
|
||||||
from . import clinical_concept_library
|
claims_preprocessing,
|
||||||
from . import cms_hcc
|
clinical_concept_library,
|
||||||
from . import cms_provider_attribution
|
cms_hcc,
|
||||||
from . import core
|
cms_provider_attribution,
|
||||||
from . import data_quality
|
core,
|
||||||
from . import ed_classification
|
data_quality,
|
||||||
from . import financial_pmpm
|
ed_classification,
|
||||||
from . import hcc_recapture
|
financial_pmpm,
|
||||||
from . import hcc_suspecting
|
hcc_recapture,
|
||||||
from . import input_layer
|
hcc_suspecting,
|
||||||
from . import main
|
input_layer,
|
||||||
from . import metadata
|
main,
|
||||||
from . import pharmacy
|
metadata,
|
||||||
from . import quality_measures
|
pharmacy,
|
||||||
from . import readmissions
|
provider_attribution,
|
||||||
from . import reference_data
|
quality_measures,
|
||||||
from . import terminology
|
readmissions,
|
||||||
from . import provider_attribution
|
reference_data,
|
||||||
|
terminology,
|
||||||
|
)
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class AhrqMeasuresIntPqi01Denom(SQLTable):
|
class AhrqMeasuresIntPqi01Denom(SQLTable):
|
||||||
"""Schema: ahrq_measures / Table: _int_pqi_01_denom"""
|
"""Schema: ahrq_measures / Table: _int_pqi_01_denom"""
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ def classify(filename: str | Path) -> dict | None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for pattern, pkg_type in package_types.items():
|
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("_")
|
parts = fname.replace(".zip", "").split("_")
|
||||||
return {
|
return {
|
||||||
"type": "package",
|
"type": "package",
|
||||||
|
|||||||
@@ -23,7 +23,4 @@ class SQLTable(BaseModel):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def column_names(cls) -> list[str]:
|
def column_names(cls) -> list[str]:
|
||||||
return [
|
return [name for name in cls.model_fields]
|
||||||
name
|
|
||||||
for name in cls.model_fields
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -65,7 +65,6 @@ class Cclf0(SQLTable):
|
|||||||
"""This field will be right-justified and left-padded with spaces."""
|
"""This field will be right-justified and left-padded with spaces."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf1(SQLTable):
|
class Cclf1(SQLTable):
|
||||||
"""CCLF1: Part A Claims Header File
|
"""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."""
|
"""A number assigned by CMS identifying a MAC authorized to process Medicare claims."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf2(SQLTable):
|
class Cclf2(SQLTable):
|
||||||
"""CCLF2: Part A Claims Revenue Center Detail File
|
"""CCLF2: Part A Claims Revenue Center Detail File
|
||||||
|
|
||||||
@@ -291,7 +289,6 @@ class Cclf2(SQLTable):
|
|||||||
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf3(SQLTable):
|
class Cclf3(SQLTable):
|
||||||
"""CCLF3: Part A Procedure Code File
|
"""CCLF3: Part A Procedure Code File
|
||||||
|
|
||||||
@@ -341,7 +338,6 @@ class Cclf3(SQLTable):
|
|||||||
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf4(SQLTable):
|
class Cclf4(SQLTable):
|
||||||
"""CCLF4: Part A Diagnosis Code File
|
"""CCLF4: Part A Diagnosis Code File
|
||||||
|
|
||||||
@@ -394,7 +390,6 @@ class Cclf4(SQLTable):
|
|||||||
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
"""A facility’s Medicare/Medicaid identification number, also known as a Medicare/Medicaid Provider Number, or CCN. This..."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf5(SQLTable):
|
class Cclf5(SQLTable):
|
||||||
"""CCLF5: Part B Physicians File
|
"""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 ..."""
|
"""A number that identifies the provider that referred the service on the claim line. Each provider is assigned its own ..."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf6(SQLTable):
|
class Cclf6(SQLTable):
|
||||||
"""CCLF6: Part B DME File
|
"""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..."""
|
"""A number that identifies the provider ordering the indicated service on the claim line. Each provider is assigned its..."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf7(SQLTable):
|
class Cclf7(SQLTable):
|
||||||
"""CCLF7: Part D File
|
"""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."""
|
"""The number associated with the indicated code in the Provider Prescribing Service Identification Qualifier Code field."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf8(SQLTable):
|
class Cclf8(SQLTable):
|
||||||
"""CCLF8: Beneficiary Demographics File
|
"""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."""
|
"""A four-digit extension to a ZIP Code that represents a subdivision for mailing purposes of the ZIP Code."""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Cclf9(SQLTable):
|
class Cclf9(SQLTable):
|
||||||
"""CCLF9: Beneficiary XREF File
|
"""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..."""
|
"""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):
|
class CclfA(SQLTable):
|
||||||
"""CCLFA: Part A Claims Benefit Enhancement and Demonstration Code File
|
"""CCLFA: Part A Claims Benefit Enhancement and Demonstration Code File
|
||||||
|
|
||||||
@@ -973,7 +963,6 @@ class CclfA(SQLTable):
|
|||||||
"""Capital MIPS payment costs"""
|
"""Capital MIPS payment costs"""
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class CclfB(SQLTable):
|
class CclfB(SQLTable):
|
||||||
"""CCLFB: Part B Claims Benefit Enhancement and Demonstration Code File
|
"""CCLFB: Part B Claims Benefit Enhancement and Demonstration Code File
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
|
||||||
# ── File ID -> CCLF table mapping ────────────────────────────────
|
# ── File ID -> CCLF table mapping ────────────────────────────────
|
||||||
|
|
||||||
CCLF_FILE_IDS: dict[str, str] = {
|
CCLF_FILE_IDS: dict[str, str] = {
|
||||||
|
|||||||
647
src/aco/table/cclf_pipe.py
Normal file
647
src/aco/table/cclf_pipe.py
Normal 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
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class CcsrValueSetDxccsrV20231BodySystems(SQLTable):
|
class CcsrValueSetDxccsrV20231BodySystems(SQLTable):
|
||||||
"""Schema: ccsr / Table: _value_set_dxccsr_v2023_1_body_systems"""
|
"""Schema: ccsr / Table: _value_set_dxccsr_v2023_1_body_systems"""
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
from aco.table.base import SQLTable
|
from aco.table.base import SQLTable
|
||||||
from datetime import date
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class ChronicConditionsIntCmsChronicConditionAll(SQLTable):
|
class ChronicConditionsIntCmsChronicConditionAll(SQLTable):
|
||||||
@@ -145,7 +146,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
|
|||||||
hepatitis_d: int | None = None
|
hepatitis_d: int | None = None
|
||||||
hepatitis_e: int | None = None
|
hepatitis_e: int | None = None
|
||||||
hip_pelvic_fracture: 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
|
hyperlipidemia: int | None = None
|
||||||
hypertension: int | None = None
|
hypertension: int | None = None
|
||||||
hypothyroidism: int | None = None
|
hypothyroidism: int | None = None
|
||||||
@@ -153,7 +156,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
|
|||||||
ischemic_heart_disease: int | None = None
|
ischemic_heart_disease: int | None = None
|
||||||
learning_disabilities: int | None = None
|
learning_disabilities: int | None = None
|
||||||
leukemias_and_lymphomas: 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
|
migraine_and_chronic_headache: int | None = None
|
||||||
mobility_impairments: int | None = None
|
mobility_impairments: int | None = None
|
||||||
multiple_sclerosis_and_transverse_myelitis: int | None = None
|
multiple_sclerosis_and_transverse_myelitis: int | None = None
|
||||||
@@ -179,7 +184,9 @@ class ChronicConditionsCmsChronicConditionsWide(SQLTable):
|
|||||||
spinal_cord_injury: int | None = None
|
spinal_cord_injury: int | None = None
|
||||||
stroke_transient_ischemic_attack: int | None = None
|
stroke_transient_ischemic_attack: int | None = None
|
||||||
tobacco_use: 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
|
viral_hepatitis_general: int | None = None
|
||||||
pipeline_last_run: datetime | None = None
|
pipeline_last_run: datetime | None = None
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class ClaimsPreprocessingIntAcuteInpatientInstitutionalMaternity(SQLTable):
|
class ClaimsPreprocessingIntAcuteInpatientInstitutionalMaternity(SQLTable):
|
||||||
"""Schema: claims_preprocessing / Table: _int_acute_inpatient_institutional_maternity"""
|
"""Schema: claims_preprocessing / Table: _int_acute_inpatient_institutional_maternity"""
|
||||||
@@ -3951,7 +3952,9 @@ class ClaimsPreprocessingServiceCategoryOfficeBasedOtherProfessional(SQLTable):
|
|||||||
pipeline_last_run: datetime | None = None
|
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 / Table: service_category__office_based_physical_therapy_professional"""
|
||||||
|
|
||||||
__schema__ = "claims_preprocessing"
|
__schema__ = "claims_preprocessing"
|
||||||
@@ -4002,7 +4005,9 @@ class ClaimsPreprocessingServiceCategoryOfficeBasedSurgeryProfessional(SQLTable)
|
|||||||
pipeline_last_run: datetime | None = None
|
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 / Table: service_category__outpatient_physical_therapy_institutional"""
|
||||||
|
|
||||||
__schema__ = "claims_preprocessing"
|
__schema__ = "claims_preprocessing"
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class ClinicalConceptLibraryClinicalConcepts(SQLTable):
|
class ClinicalConceptLibraryClinicalConcepts(SQLTable):
|
||||||
"""Schema: clinical_concept_library / Table: clinical_concepts"""
|
"""Schema: clinical_concept_library / Table: clinical_concepts"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class CmsHccIntDemographicFactors(SQLTable):
|
class CmsHccIntDemographicFactors(SQLTable):
|
||||||
"""Schema: cms_hcc / Table: _int_demographic_factors"""
|
"""Schema: cms_hcc / Table: _int_demographic_factors"""
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aco.table.base import SQLTable
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class CoreStgClaimsCondition(SQLTable):
|
class CoreStgClaimsCondition(SQLTable):
|
||||||
"""Schema: core / Table: _stg_claims_condition"""
|
"""Schema: core / Table: _stg_claims_condition"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class DataQualityValueSetCrosswalkFieldInfo(SQLTable):
|
class DataQualityValueSetCrosswalkFieldInfo(SQLTable):
|
||||||
"""Schema: data_quality / Table: _value_set_crosswalk_field_info"""
|
"""Schema: data_quality / Table: _value_set_crosswalk_field_info"""
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class EdClassificationIntByProviderParentOrganization(SQLTable):
|
class EdClassificationIntByProviderParentOrganization(SQLTable):
|
||||||
"""Schema: ed_classification / Table: _int_by_provider_parent_organization"""
|
"""Schema: ed_classification / Table: _int_by_provider_parent_organization"""
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class FinancialPmpmIntPatientSpendWithServiceCategories(SQLTable):
|
class FinancialPmpmIntPatientSpendWithServiceCategories(SQLTable):
|
||||||
"""Schema: financial_pmpm / Table: _int_patient_spend_with_service_categories"""
|
"""Schema: financial_pmpm / Table: _int_patient_spend_with_service_categories"""
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class HccRecaptureGapStatus(SQLTable):
|
class HccRecaptureGapStatus(SQLTable):
|
||||||
"""Schema: hcc_recapture / Table: gap_status"""
|
"""Schema: hcc_recapture / Table: gap_status"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class HccSuspectingIntAllConditions(SQLTable):
|
class HccSuspectingIntAllConditions(SQLTable):
|
||||||
"""Schema: hcc_suspecting / Table: _int_all_conditions"""
|
"""Schema: hcc_suspecting / Table: _int_all_conditions"""
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
from aco.table.base import SQLTable
|
from aco.table.base import SQLTable
|
||||||
from datetime import date
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class InputLayerAppointment(SQLTable):
|
class InputLayerAppointment(SQLTable):
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import date, datetime
|
||||||
|
|
||||||
from aco.table.base import SQLTable
|
from aco.table.base import SQLTable
|
||||||
from datetime import date
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
|
||||||
class MainAlertsAnomalyDetection(SQLTable):
|
class MainAlertsAnomalyDetection(SQLTable):
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class PharmacyIntBrandWithGenericAvailable(SQLTable):
|
class PharmacyIntBrandWithGenericAvailable(SQLTable):
|
||||||
"""Schema: pharmacy / Table: _int_brand_with_generic_available"""
|
"""Schema: pharmacy / Table: _int_brand_with_generic_available"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class ProviderAttributionIntCurrentSteps(SQLTable):
|
class ProviderAttributionIntCurrentSteps(SQLTable):
|
||||||
"""Schema: provider_attribution / Table: _int_current_steps"""
|
"""Schema: provider_attribution / Table: _int_current_steps"""
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class QualityMeasuresIntAdhDiabetesPerformancePeriod(SQLTable):
|
class QualityMeasuresIntAdhDiabetesPerformancePeriod(SQLTable):
|
||||||
"""Schema: quality_measures / Table: _int_adh_diabetes__performance_period"""
|
"""Schema: quality_measures / Table: _int_adh_diabetes__performance_period"""
|
||||||
|
|||||||
@@ -58,18 +58,18 @@ class ReachQuarterlyQualityReport(SQLTable):
|
|||||||
|
|
||||||
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 Format: Excel Workbook (.xlsx)
|
||||||
File Code: QTLQR
|
File Code: QTLQR
|
||||||
Distribution: Quarterly (12-month rolling period ending in prior quarter)
|
Distribution: Quarterly (12-month rolling period ending in prior quarter)
|
||||||
Page Reference: p.22-23
|
Page Reference: p.22-23
|
||||||
|
|
||||||
Measurement Period: 12-month rolling window (e.g., Q1 2023 report covers
|
Measurement Period: 12-month rolling window (e.g., Q1 2023 report covers
|
||||||
Jan 2022 - Dec 2022)
|
Jan 2022 - Dec 2022)
|
||||||
|
|
||||||
Stratified Reporting: Quality measures reported for three subgroups:
|
Stratified Reporting: Quality measures reported for three subgroups:
|
||||||
- Dual-eligible beneficiaries (full Medicaid)
|
- Dual-eligible beneficiaries (full Medicaid)
|
||||||
- Low socioeconomic status (ADI percentile ≥81)
|
- Low socioeconomic status (ADI percentile ≥81)
|
||||||
- Race/ethnicity other than white
|
- Race/ethnicity other than white
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__schema__: ClassVar[str] = "reach"
|
__schema__: ClassVar[str] = "reach"
|
||||||
@@ -145,14 +145,14 @@ class ReachAnnualQualitySummary(SQLTable):
|
|||||||
* Criteria: Total Quality Score ≥75%, continuing ACOs, positive savings
|
* Criteria: Total Quality Score ≥75%, continuing ACOs, positive savings
|
||||||
* Bonus funded from quality withhold of non-qualifying ACOs"
|
* Bonus funded from quality withhold of non-qualifying ACOs"
|
||||||
|
|
||||||
File Format: Excel Workbook (.xlsx)
|
File Format: Excel Workbook (.xlsx)
|
||||||
File Code: ANLQR
|
File Code: ANLQR
|
||||||
Distribution: Annually (prior performance year)
|
Distribution: Annually (prior performance year)
|
||||||
Page Reference: p.23-25
|
Page Reference: p.23-25
|
||||||
|
|
||||||
CI/SEP Gateway (p.24):
|
CI/SEP Gateway (p.24):
|
||||||
- Continuous Improvement: ACO improves on ≥50% of measures from prior year
|
- Continuous Improvement: ACO improves on ≥50% of measures from prior year
|
||||||
- Static Excellence Performance: ACO scores ≥30th percentile on all measures
|
- Static Excellence Performance: ACO scores ≥30th percentile on all measures
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__schema__: ClassVar[str] = "reach"
|
__schema__: ClassVar[str] = "reach"
|
||||||
@@ -252,12 +252,12 @@ class ReachAnnualQualityCahps(SQLTable):
|
|||||||
- Highest benchmark threshold met based on percentile
|
- Highest benchmark threshold met based on percentile
|
||||||
- Points earned (up to 10 for CAHPS overall, distributed across 8 SSMs)"
|
- Points earned (up to 10 for CAHPS overall, distributed across 8 SSMs)"
|
||||||
|
|
||||||
File Format: Excel Workbook (.xlsx), Table 4
|
File Format: Excel Workbook (.xlsx), Table 4
|
||||||
Distribution: Annually (prior performance year)
|
Distribution: Annually (prior performance year)
|
||||||
Page Reference: p.25
|
Page Reference: p.25
|
||||||
|
|
||||||
Survey Timing: CAHPS survey administered in Q2 of performance year for prior
|
Survey Timing: CAHPS survey administered in Q2 of performance year for prior
|
||||||
year's patient experience. Results available in final settlement timeframe.
|
year's patient experience. Results available in final settlement timeframe.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__schema__: ClassVar[str] = "reach"
|
__schema__: ClassVar[str] = "reach"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date, datetime
|
||||||
from datetime import datetime
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class ReadmissionsIntEncounter(SQLTable):
|
class ReadmissionsIntEncounter(SQLTable):
|
||||||
"""Schema: readmissions / Table: _int_encounter"""
|
"""Schema: readmissions / Table: _int_encounter"""
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class ReferenceDataAnsiFipsState(SQLTable):
|
class ReferenceDataAnsiFipsState(SQLTable):
|
||||||
"""Schema: reference_data / Table: ansi_fips_state"""
|
"""Schema: reference_data / Table: ansi_fips_state"""
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from aco.table.base import SQLTable
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from aco.table.base import SQLTable
|
||||||
|
|
||||||
|
|
||||||
class TerminologyActSite(SQLTable):
|
class TerminologyActSite(SQLTable):
|
||||||
"""Schema: terminology / Table: act_site"""
|
"""Schema: terminology / Table: act_site"""
|
||||||
|
|||||||
@@ -518,7 +518,7 @@ class Client:
|
|||||||
----------
|
----------
|
||||||
job_url : str
|
job_url : str
|
||||||
The job status URL from ``start_export``.
|
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)
|
self._request("DELETE", job_url)
|
||||||
log.info("Job cancelled: %s", job_url)
|
log.info("Job cancelled: %s", job_url)
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ def _build_pipeline():
|
|||||||
import aco.pipe.runner # noqa: F401
|
import aco.pipe.runner # noqa: F401
|
||||||
from aco.express.base import Expr
|
from aco.express.base import Expr
|
||||||
from aco.pipe.base import Pipeline
|
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 = [
|
_REFS = [
|
||||||
Tag.module("bcda"),
|
Tag.module("bcda"),
|
||||||
@@ -54,6 +68,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_beneficiary_xref",
|
name="bcda._stg_beneficiary_xref",
|
||||||
fn=ex.stg_beneficiary_xref,
|
fn=ex.stg_beneficiary_xref,
|
||||||
|
output=BcdaStgBeneficiaryXref,
|
||||||
after=["bcda.patient"],
|
after=["bcda.patient"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -67,6 +82,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_part_a_header",
|
name="bcda._stg_part_a_header",
|
||||||
fn=ex.stg_part_a_header,
|
fn=ex.stg_part_a_header,
|
||||||
|
output=BcdaStgPartAHeader,
|
||||||
after=[
|
after=[
|
||||||
"bcda.explanation_of_benefit",
|
"bcda.explanation_of_benefit",
|
||||||
"bcda._stg_beneficiary_xref",
|
"bcda._stg_beneficiary_xref",
|
||||||
@@ -81,6 +97,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._int_institutional_medical_claim",
|
name="bcda._int_institutional_medical_claim",
|
||||||
fn=ex.int_institutional_medical_claim,
|
fn=ex.int_institutional_medical_claim,
|
||||||
|
output=BcdaIntInstitutionalMedicalClaim,
|
||||||
after=["bcda._stg_part_a_header", "bcda.eob_item"],
|
after=["bcda._stg_part_a_header", "bcda.eob_item"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -94,6 +111,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_part_b_physician_header",
|
name="bcda._stg_part_b_physician_header",
|
||||||
fn=ex.stg_part_b_physician_header,
|
fn=ex.stg_part_b_physician_header,
|
||||||
|
output=BcdaStgPartBPhysicianHeader,
|
||||||
after=[
|
after=[
|
||||||
"bcda.explanation_of_benefit",
|
"bcda.explanation_of_benefit",
|
||||||
"bcda._stg_beneficiary_xref",
|
"bcda._stg_beneficiary_xref",
|
||||||
@@ -108,6 +126,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._int_physician_medical_claim",
|
name="bcda._int_physician_medical_claim",
|
||||||
fn=ex.int_physician_medical_claim,
|
fn=ex.int_physician_medical_claim,
|
||||||
|
output=BcdaIntPhysicianMedicalClaim,
|
||||||
after=[
|
after=[
|
||||||
"bcda._stg_part_b_physician_header",
|
"bcda._stg_part_b_physician_header",
|
||||||
"bcda.eob_item",
|
"bcda.eob_item",
|
||||||
@@ -124,6 +143,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_part_b_dme_header",
|
name="bcda._stg_part_b_dme_header",
|
||||||
fn=ex.stg_part_b_dme_header,
|
fn=ex.stg_part_b_dme_header,
|
||||||
|
output=BcdaStgPartBDmeHeader,
|
||||||
after=[
|
after=[
|
||||||
"bcda.explanation_of_benefit",
|
"bcda.explanation_of_benefit",
|
||||||
"bcda._stg_beneficiary_xref",
|
"bcda._stg_beneficiary_xref",
|
||||||
@@ -138,6 +158,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._int_dme_medical_claim",
|
name="bcda._int_dme_medical_claim",
|
||||||
fn=ex.int_dme_medical_claim,
|
fn=ex.int_dme_medical_claim,
|
||||||
|
output=BcdaIntDmeMedicalClaim,
|
||||||
after=["bcda._stg_part_b_dme_header", "bcda.eob_item"],
|
after=["bcda._stg_part_b_dme_header", "bcda.eob_item"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -150,6 +171,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda.medical_claim",
|
name="bcda.medical_claim",
|
||||||
fn=ex.medical_claim,
|
fn=ex.medical_claim,
|
||||||
|
output=BcdaMedicalClaim,
|
||||||
after=[
|
after=[
|
||||||
"bcda._int_institutional_medical_claim",
|
"bcda._int_institutional_medical_claim",
|
||||||
"bcda._int_physician_medical_claim",
|
"bcda._int_physician_medical_claim",
|
||||||
@@ -165,6 +187,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_part_d_header",
|
name="bcda._stg_part_d_header",
|
||||||
fn=ex.stg_part_d_header,
|
fn=ex.stg_part_d_header,
|
||||||
|
output=BcdaStgPartDHeader,
|
||||||
after=[
|
after=[
|
||||||
"bcda.explanation_of_benefit",
|
"bcda.explanation_of_benefit",
|
||||||
"bcda._stg_beneficiary_xref",
|
"bcda._stg_beneficiary_xref",
|
||||||
@@ -179,6 +202,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda.pharmacy_claim",
|
name="bcda.pharmacy_claim",
|
||||||
fn=ex.pharmacy_claim,
|
fn=ex.pharmacy_claim,
|
||||||
|
output=BcdaPharmacyClaim,
|
||||||
after=["bcda._stg_part_d_header", "bcda.eob_item"],
|
after=["bcda._stg_part_d_header", "bcda.eob_item"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -191,6 +215,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda._stg_beneficiary_demographics",
|
name="bcda._stg_beneficiary_demographics",
|
||||||
fn=ex.stg_beneficiary_demographics,
|
fn=ex.stg_beneficiary_demographics,
|
||||||
|
output=BcdaStgBeneficiaryDemographics,
|
||||||
after=["bcda.patient", "bcda.coverage"],
|
after=["bcda.patient", "bcda.coverage"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
@@ -202,6 +227,7 @@ def _build_pipeline():
|
|||||||
Expr(
|
Expr(
|
||||||
name="bcda.eligibility",
|
name="bcda.eligibility",
|
||||||
fn=ex.eligibility,
|
fn=ex.eligibility,
|
||||||
|
output=BcdaEligibility,
|
||||||
after=["bcda._stg_beneficiary_demographics"],
|
after=["bcda._stg_beneficiary_demographics"],
|
||||||
refs=_REFS,
|
refs=_REFS,
|
||||||
description=(
|
description=(
|
||||||
|
|||||||
383
src/bcda/table/cclf_pipe.py
Normal file
383
src/bcda/table/cclf_pipe.py
Normal 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
|
||||||
@@ -56,13 +56,13 @@ Usage
|
|||||||
from .client import COLLECTIONS as COLLECTIONS
|
from .client import COLLECTIONS as COLLECTIONS
|
||||||
from .client import connect as connect
|
from .client import connect as connect
|
||||||
from .ingest import ingest as ingest
|
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 Download as Download
|
||||||
from .item import Item as Item
|
from .item import Item as Item
|
||||||
from .item import Manual as Manual
|
from .item import Manual as Manual
|
||||||
from .item import Regulation as Regulation
|
from .item import Regulation as Regulation
|
||||||
from .item import Rule as Rule
|
from .item import Rule as Rule
|
||||||
from .item import Source as Source
|
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 .store import Store as Store
|
||||||
from .tag import Tag as Tag
|
from .tag import Tag as Tag
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user