Merge P36: CY2026 PFS proposed rule — capture, ingest, APM analysis
All checks were successful
CI / lint (push) Successful in 32s
CI / notebooks-smoke (push) Successful in 1m27s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Successful in 1m54s
Deploy / llm (push) Successful in 1m43s
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m1s
Infra CI / zotero (push) Successful in 6m50s
CI / test (push) Successful in 15m5s
Infra CI / docs (push) Successful in 1m23s
Infra CI / api (push) Successful in 13s
Infra CI / llm (push) Successful in 12s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 14s
Package Supply Chain / pkg-supply-chain (push) Successful in 1m2s
All checks were successful
CI / lint (push) Successful in 32s
CI / notebooks-smoke (push) Successful in 1m27s
Deploy / notebooks (push) Has been skipped
Deploy / zotero (push) Has been skipped
Deploy / docs (push) Has been skipped
Deploy / api (push) Successful in 1m54s
Deploy / llm (push) Successful in 1m43s
Deploy / mc (push) Has been skipped
Infra CI / notebooks (push) Successful in 1m1s
Infra CI / zotero (push) Successful in 6m50s
CI / test (push) Successful in 15m5s
Infra CI / docs (push) Successful in 1m23s
Infra CI / api (push) Successful in 13s
Infra CI / llm (push) Successful in 12s
Infra CI / mc (push) Successful in 14s
Deploy / report (push) Successful in 14s
Package Supply Chain / pkg-supply-chain (push) Successful in 1m2s
This commit is contained in:
@@ -308,7 +308,7 @@
|
||||
},
|
||||
{
|
||||
"name": "cryptography",
|
||||
"version": ">=48.0.1",
|
||||
"version": ">=50.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
@@ -346,6 +346,46 @@
|
||||
"version": ">=0.4",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "httpx",
|
||||
"version": ">=0.28.1",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "fastapi",
|
||||
"version": ">=0.139.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "uvicorn",
|
||||
"version": ">=0.41.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "langchain-core",
|
||||
"version": ">=0.3.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "langchain-ollama",
|
||||
"version": ">=0.2.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "langchain-postgres",
|
||||
"version": ">=0.0.12",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "psycopg",
|
||||
"version": ">=3.2.0",
|
||||
"extras": "binary"
|
||||
},
|
||||
{
|
||||
"name": "sqlalchemy",
|
||||
"version": ">=2.0.0",
|
||||
"extras": ""
|
||||
},
|
||||
{
|
||||
"name": "pydantic",
|
||||
"version": ">=2.0.0",
|
||||
|
||||
230
dev/scripts/fetch_nprm_addenda.py
Normal file
230
dev/scripts/fetch_nprm_addenda.py
Normal file
@@ -0,0 +1,230 @@
|
||||
"""Fetch + register the CY2026 PFS NPRM (proposed rule) addenda.
|
||||
|
||||
Downloads the CMS-1832-P "CY 2026 PFS Proposed Rule Addenda" ZIP, which
|
||||
contains Addendum B (proposed RVUs by HCPCS), and optionally registers
|
||||
Addendum B in bib against the CY2026 PFS NPRM rule item (2KVJ2HKX),
|
||||
tagged ``sup:2026_PFS_NPRM`` so Task 5's loader can discover the
|
||||
*proposed* Addendum B without touching final-rule files (which carry
|
||||
``sup:2026_PFS_FR`` — the two namespaces must never collide).
|
||||
|
||||
Source page (CMS-1832-P detail page — the canonical
|
||||
``.../pfs-federal-regulation-notices/cms-1832-p`` URL 404s; this is the
|
||||
current path found via the federal-regulation-notices listing):
|
||||
https://www.cms.gov/medicare/payment/fee-schedules/physician/federal-regulation-notices/cms-1832-p
|
||||
Download link text on that page: "CY 2026 PFS Proposed Rule Addenda -
|
||||
Updated 07/29/2025" (the URL path segment literally contains
|
||||
"07/29/2025" for the update-date suffix — that's CMS's own href, not a
|
||||
typo here).
|
||||
|
||||
Usage:
|
||||
uv run python dev/scripts/fetch_nprm_addenda.py --dry-run
|
||||
uv run python dev/scripts/fetch_nprm_addenda.py
|
||||
uv run python dev/scripts/fetch_nprm_addenda.py --register
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DEST_DIR = ROOT / "data" / "cms" / "pfs_nprm" / "2026"
|
||||
|
||||
USER_AGENT = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
||||
)
|
||||
|
||||
# CY2026 PFS Proposed Rule (CMS-1832-P) Addenda ZIP — resolved from the
|
||||
# CMS-1832-P detail page:
|
||||
# https://www.cms.gov/medicare/payment/fee-schedules/physician/federal-regulation-notices/cms-1832-p
|
||||
ADDENDA_URL = (
|
||||
"https://www.cms.gov/files/zip/"
|
||||
"cy-2026-pfs-proposed-rule-addenda-updated-07/29/2025.zip"
|
||||
)
|
||||
ADDENDA_ZIP_NAME = "cy2026_pfs_nprm_addenda.zip"
|
||||
|
||||
ADDENDUM_B_RE = re.compile(r"(?i)addendum[_ ]?b")
|
||||
|
||||
RULE_ITEM_KEY = "2KVJ2HKX"
|
||||
ADDENDUM_B_TITLE = "CY2026 PFS NPRM Addendum B (proposed RVUs)"
|
||||
REGISTER_TAGS = ["sup:2026_PFS_NPRM", "module:pfs", "file:rvu", "year:2026"]
|
||||
|
||||
|
||||
def download_zip(dest: Path, *, retries: int = 3) -> bool:
|
||||
"""Download the addenda ZIP if it doesn't already exist."""
|
||||
import time
|
||||
|
||||
if dest.exists():
|
||||
print(f" EXISTS {dest.name}")
|
||||
return True
|
||||
print(f" GET {dest.name} ...", end="", flush=True)
|
||||
for attempt in range(1, retries + 1):
|
||||
try:
|
||||
resp = httpx.get(
|
||||
ADDENDA_URL,
|
||||
headers={"User-Agent": USER_AGENT},
|
||||
timeout=120,
|
||||
follow_redirects=True,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
dest.write_bytes(resp.content)
|
||||
print(f" {len(resp.content) / 1024 / 1024:.1f} MB")
|
||||
return True
|
||||
if resp.status_code < 500 or attempt == retries:
|
||||
print(f" HTTP {resp.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
if attempt == retries:
|
||||
print(f" ERROR: {e}")
|
||||
return False
|
||||
time.sleep(2 * attempt)
|
||||
return False
|
||||
|
||||
|
||||
def extract_zip(zip_path: Path, dest_dir: Path) -> list[Path]:
|
||||
"""Unzip into dest_dir. Idempotent — extraction overwrites in place."""
|
||||
with zipfile.ZipFile(zip_path) as zf:
|
||||
names = [n for n in zf.namelist() if not n.endswith("/")]
|
||||
zf.extractall(dest_dir)
|
||||
return sorted(dest_dir / n for n in names)
|
||||
|
||||
|
||||
def find_addendum_b(files: list[Path]) -> Path | None:
|
||||
"""Find the extracted file whose name matches Addendum B."""
|
||||
candidates = [
|
||||
f
|
||||
for f in files
|
||||
if ADDENDUM_B_RE.search(f.name) and f.suffix.lower() in (".xlsx", ".csv")
|
||||
]
|
||||
if not candidates:
|
||||
return None
|
||||
# Prefer .xlsx over .csv if both are present.
|
||||
candidates.sort(key=lambda f: f.suffix.lower() != ".xlsx")
|
||||
return candidates[0]
|
||||
|
||||
|
||||
def preview_addendum_b(path: Path) -> None:
|
||||
"""Print the header row (and a couple of data rows) for verification."""
|
||||
import polars as pl
|
||||
|
||||
if path.suffix.lower() == ".xlsx":
|
||||
df = pl.read_excel(path, read_options={"header_row": None})
|
||||
else:
|
||||
df = pl.read_csv(path, n_rows=5, has_header=False)
|
||||
print(f" columns/first rows of {path.name}:")
|
||||
with pl.Config(tbl_cols=-1, tbl_width_chars=200):
|
||||
print(df.head(5))
|
||||
|
||||
|
||||
def _existing_attachment(store: Any, item_key: str, filename: str) -> str | None:
|
||||
"""Return the attachment key on item_key whose filename matches, if any."""
|
||||
con = store._con() # noqa: SLF001 — same pattern as bib/oig.py, bib/iom.py
|
||||
row = con.execute(
|
||||
"""SELECT a.key FROM attachments a
|
||||
JOIN items i ON a.item_id = i.id
|
||||
WHERE i.key = ? AND a.filename = ?""",
|
||||
(item_key, filename),
|
||||
).fetchone()
|
||||
return row["key"] if row else None
|
||||
|
||||
|
||||
def register(addendum_b_path: Path) -> None:
|
||||
"""Attach Addendum B to the NPRM rule item and tag it.
|
||||
|
||||
Idempotent: if an attachment with the Addendum B title already exists
|
||||
on the rule item, skip attach_file (which has no dedup of its own and
|
||||
would otherwise create a duplicate on every --register re-run) and
|
||||
only (re-)apply the tags, which are already idempotent via add_tag's
|
||||
INSERT OR IGNORE.
|
||||
"""
|
||||
from conf import connect
|
||||
|
||||
store = connect.bib()
|
||||
|
||||
existing_key = _existing_attachment(store, RULE_ITEM_KEY, ADDENDUM_B_TITLE)
|
||||
if existing_key is not None:
|
||||
print(
|
||||
f"SKIP attach — {RULE_ITEM_KEY} already has an attachment titled "
|
||||
f"{ADDENDUM_B_TITLE!r} (key {existing_key})"
|
||||
)
|
||||
else:
|
||||
att_key = store.attach_file(
|
||||
RULE_ITEM_KEY, addendum_b_path, title=ADDENDUM_B_TITLE
|
||||
)
|
||||
con = store._con() # noqa: SLF001 — same pattern as bib/oig.py, bib/iom.py
|
||||
row = con.execute(
|
||||
"SELECT storage_path FROM attachments WHERE key = ?", (att_key,)
|
||||
).fetchone()
|
||||
storage_path = row["storage_path"] if row else "<unknown>"
|
||||
print(f"Attached {addendum_b_path.name} (key {att_key}) -> {storage_path}")
|
||||
|
||||
for tag in REGISTER_TAGS:
|
||||
store.add_tag(RULE_ITEM_KEY, tag)
|
||||
print(f"Tagged {RULE_ITEM_KEY} with: {', '.join(REGISTER_TAGS)}")
|
||||
item = store.get(RULE_ITEM_KEY)
|
||||
print(f"Current tags on {RULE_ITEM_KEY}: {item.tags}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Fetch + register the CY2026 PFS NPRM addenda"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print what would be downloaded/extracted without doing it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--register",
|
||||
action="store_true",
|
||||
help="After fetching, attach Addendum B to bib item "
|
||||
f"{RULE_ITEM_KEY} and apply the {', '.join(REGISTER_TAGS)} tags.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
zip_dest = DEST_DIR / ADDENDA_ZIP_NAME
|
||||
|
||||
if args.dry_run:
|
||||
print(f"Would GET {ADDENDA_URL}")
|
||||
print(f"Would save {zip_dest}")
|
||||
print(f"Would extract into {DEST_DIR}")
|
||||
if args.register:
|
||||
print(
|
||||
f"Would attach Addendum B to {RULE_ITEM_KEY} "
|
||||
f"and tag with: {', '.join(REGISTER_TAGS)}"
|
||||
)
|
||||
return
|
||||
|
||||
DEST_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not download_zip(zip_dest):
|
||||
raise SystemExit(f"Failed to download {ADDENDA_URL}")
|
||||
|
||||
files = extract_zip(zip_dest, DEST_DIR)
|
||||
print(f"\nExtracted {len(files)} files to {DEST_DIR}:")
|
||||
for f in files:
|
||||
size_kb = f.stat().st_size / 1024
|
||||
print(f" {f.relative_to(DEST_DIR)} ({size_kb:.0f} KB)")
|
||||
|
||||
addendum_b = find_addendum_b(files)
|
||||
if addendum_b is None:
|
||||
raise SystemExit(
|
||||
"Could not find an Addendum B file (pattern "
|
||||
f"{ADDENDUM_B_RE.pattern!r}) among extracted files."
|
||||
)
|
||||
print(f"\nAddendum B: {addendum_b.relative_to(DEST_DIR)}")
|
||||
preview_addendum_b(addendum_b)
|
||||
|
||||
if args.register:
|
||||
print()
|
||||
register(addendum_b)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -33,10 +33,19 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="skip publishing to the DuckLake lakehouse",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nprm",
|
||||
action="store_true",
|
||||
help="also load the NPRM Addendum B proposed RVUs into pfs.rvu_proposed",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
from cms import ingest_log
|
||||
from conf.connect import duckdb_batch, publish_replica
|
||||
from pfs.pipe import load_all
|
||||
from pfs.rules import RULES
|
||||
|
||||
run_id = ingest_log.new_run_id()
|
||||
|
||||
print("Ingesting CMS PFS files into DuckDB ...")
|
||||
with duckdb_batch("aco") as con:
|
||||
@@ -44,6 +53,31 @@ def main() -> int:
|
||||
print("\n--- PFS tables ---")
|
||||
for name, info in sorted(summary.items()):
|
||||
print(f" pfs.{name:24s}: {info}")
|
||||
ingest_log.log_ingest(
|
||||
con,
|
||||
module="pfs",
|
||||
table_name=name,
|
||||
rows=info.get("rows", 0) if isinstance(info, dict) else 0,
|
||||
run_id=run_id,
|
||||
)
|
||||
|
||||
if args.nprm:
|
||||
from pfs.nprm import load_rvu_proposed
|
||||
|
||||
print("\n--- NPRM Addendum B (proposed RVUs) ---")
|
||||
nprm_out = load_rvu_proposed(con)
|
||||
print(f" pfs.rvu_proposed : {nprm_out}")
|
||||
ingest_log.log_ingest(
|
||||
con,
|
||||
module="pfs",
|
||||
table_name="pfs.rvu_proposed",
|
||||
rows=nprm_out["rows"],
|
||||
source_file=nprm_out["source_file"],
|
||||
rule_id="CMS-1832-P",
|
||||
fr_citation=RULES[2026].proposed.federal_register_citation,
|
||||
pincite_key="2KVJ2HKX",
|
||||
run_id=run_id,
|
||||
)
|
||||
|
||||
replica = publish_replica("aco")
|
||||
print(f"replica → {replica}")
|
||||
@@ -52,7 +86,7 @@ def main() -> int:
|
||||
print("\n--- Publishing to DuckLake ---")
|
||||
import _lake
|
||||
|
||||
_lake.publish_lake(("pfs",))
|
||||
_lake.publish_lake(("pfs", "cms"))
|
||||
|
||||
print("\nDone.")
|
||||
return 0
|
||||
|
||||
@@ -30,8 +30,10 @@ SCHEMAS: dict[str, tuple[str, ...]] = {
|
||||
"medical_supply",
|
||||
"physician_work_time",
|
||||
"rvu",
|
||||
"rvu_proposed",
|
||||
"zip_carrier_locality",
|
||||
),
|
||||
"cms": ("ingest_log",),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -180,3 +180,49 @@ Dependency order: 1–3 independent; 4 needs 2; 5 needs 3+4+7; 6 needs 2;
|
||||
- MIPS scoring/measure modeling beyond the QP/Advanced-APM parameters above.
|
||||
- Cloud LLM usage — the llm module stays local-only per project policy.
|
||||
- Modeling every NPRM addendum; only Addendum B (RVUs) is tabled.
|
||||
|
||||
## P36 build outcomes (2026-08-13)
|
||||
|
||||
Issues #588–#597, milestone P36 (17 commits, 4a9be6f..a444d75, branch
|
||||
`p36-cy2026-rule`). Deviations and measurements vs the design:
|
||||
|
||||
- **Comment farm was already complete** — the design's "only ~2,905 of
|
||||
14,092 extracted" understated prior work: all 2,905 attachment-bearing
|
||||
comments had extractions; the remaining ~11.2k are body-only (abstract
|
||||
fallback covers them), and the whole corpus (164k comments, 726,620
|
||||
chunks incl. 62,486 for CMS-2025-0304) was already embedded post-P33.
|
||||
#596 reduced to rule-text indexing.
|
||||
- **Rules collection**: 5,769 chunks (2,254→2,208 NPRM CMS-1832-P,
|
||||
3,611→3,561 final CMS-1832-F) after an inline-HTML-strip fix round and
|
||||
force reindex; final-rule bib item gained `cms-rule:CMS-1832-F`.
|
||||
- **pfs.rvu_proposed**: 14,169 rows (NPRM Addendum B), in aco.duckdb +
|
||||
replica + DuckLake; `cms.ingest_log` provenance (18 rows/2 runs) with
|
||||
sha256, FR citation, pincite. Column names deviate from `pfs.rvu`
|
||||
(`modifier`/`mod`, `nonfac_pe_rvu`/`non_fac_pe_rvu`) — follow-up #607.
|
||||
- **CY2026 NPRM parameters** (all source-verified at 90 FR 32352 et seq.):
|
||||
proposed CF 33.4209 non-QP / 33.5875 QP, anesthesia 20.5728/20.6754,
|
||||
BN adjustor 1.0055, telehealth originating-site fee $31.85, comment
|
||||
close 2025-09-12.
|
||||
- **qpp module**: QP thresholds 75%/50% (partial 50%/35% — patient-count
|
||||
value is a disclosed inference), revenue nominal-risk standard 8%
|
||||
(414.1415(c)(3)(i)(A), established 83 FR / 2018-24170); the
|
||||
benchmark-based standard (c)(3)(i)(B) is stated nowhere in the corpus
|
||||
as a bare percentage → `None` with notes. APM incentive sunset and the
|
||||
first-year QP/non-QP CF split (payment year 2026) encoded with
|
||||
citations at 90 FR 49928/49979–49980.
|
||||
- **Citation correction found by final review**: `RULES[2026]`'s
|
||||
final-rule citation had been "90 FR 101174" — that is the *OPPS* final
|
||||
rule's citation; the PFS final rule is **90 FR 49266–50481** (CFs at
|
||||
49961). Corrected in registry + notebook (a444d75). Design note: the
|
||||
spec itself restated the wrong citation as current state; future rule
|
||||
cycles should re-verify pre-existing registry citations against newly
|
||||
captured text.
|
||||
- **Interstitials landed on the branch**: #598 (test poisoned global
|
||||
polars.DataFrame → duckdb replacement-scan cache corruption; full
|
||||
suite 14,023 green post-fix), #599 (stale .git/hooks copies removed),
|
||||
CVE bumps aiohttp 3.14.3 / pyasn1 0.6.4 / cryptography 50.0.0
|
||||
(#581–#587 closed with per-CVE exploitability rationale).
|
||||
- **Notebook**: `notebooks/cy2026_pfs_proposed_rule.py`, 5 sections, all
|
||||
values registry/lake-sourced (thresholds interpolated after review
|
||||
fix), 18/18 headless integration green. CMS Table 92 (specialty
|
||||
impact) is a scanned graphic → clearly-labeled CPT-range proxy.
|
||||
|
||||
783
notebooks/cy2026_pfs_proposed_rule.py
Normal file
783
notebooks/cy2026_pfs_proposed_rule.py
Normal file
@@ -0,0 +1,783 @@
|
||||
import marimo
|
||||
|
||||
__generated_with = "0.23.1"
|
||||
app = marimo.App(width="medium")
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _():
|
||||
import marimo as mo
|
||||
|
||||
return (mo,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
# CY2026 PFS Proposed Rule — Financial Changes & Advanced APM
|
||||
|
||||
CMS's CY2026 Physician Fee Schedule rulemaking is unusual: the NPRM
|
||||
(CMS-1832-P, 90 FR 32352, July 16, 2025) proposed the **first-ever
|
||||
split conversion factor** — separate dollar amounts for clinicians who
|
||||
qualify as Advanced APM Participants (QPs) versus everyone else — and
|
||||
the Final Rule (90 FR 49266) changed several of those numbers again
|
||||
before taking effect.
|
||||
|
||||
**Mechanism:** MACRA 2015 (section 1848(d)(20) of the Act) sets two
|
||||
statutory annual updates starting payment year 2026 — +0.75%/yr for
|
||||
the qualifying-APM (QP) conversion factor, +0.25%/yr for everyone
|
||||
else — on top of a one-time 2.50% increase and the usual budget
|
||||
neutrality adjustment. This notebook walks the CF from CY2025 through
|
||||
the NPRM to the Final Rule, quantifies which HCPCS codes moved the
|
||||
most between the proposed and final RVU tables, and lays out what
|
||||
Advanced APM (QP) status is worth in dollars for CY2026.
|
||||
|
||||
**What this means:** every number below is read live from
|
||||
`pfs.rules.RULES` / `qpp.QPP` (the repo's rule registries) and the
|
||||
`pfs`/`cms` DuckLake schemas — nothing here is a hard-coded figure.
|
||||
All dollar figures use **national, GPCI-unadjusted RVUs (GPCI = 1.0)**
|
||||
— a deliberate scope simplification, disclosed once here rather than
|
||||
on every figure; real payment varies by locality via `pfs.gpci`,
|
||||
which this notebook does not join.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _():
|
||||
import altair as alt
|
||||
import polars as pl
|
||||
|
||||
from conf import connect
|
||||
from pfs.rules import RULES
|
||||
from qpp import QPP, for_payment_year
|
||||
|
||||
connect.theme()
|
||||
|
||||
# connect.theme() puts assets/ on sys.path — reuse the HTI-5 design
|
||||
# tokens directly instead of re-typing hex literals for the gain/loss
|
||||
# diverging pair used in Sections 2-3.
|
||||
from fhirworx import AMBER, TEAL
|
||||
|
||||
# PFS reference data lives in the DuckLake lakehouse (M5, #514) —
|
||||
# notebooks connect read-only.
|
||||
con = connect.ducklake()
|
||||
|
||||
def q(sql):
|
||||
return con.execute(sql).pl()
|
||||
|
||||
return AMBER, QPP, RULES, TEAL, alt, con, for_payment_year, pl, q
|
||||
|
||||
|
||||
# ── 1. The conversion-factor walk ─────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 1. The Conversion Factor Walk — CY2025 → NPRM → Final Rule
|
||||
|
||||
CY2026 is the first year the PFS publishes **four** conversion
|
||||
factors at once: a standard and an anesthesia CF, each split into a
|
||||
qualifying-APM (QP) and nonqualifying-APM (non-QP) track. The chart
|
||||
below places every published value — CY2025's single CF, the NPRM's
|
||||
proposed values, and the Final Rule's finalized values — on one
|
||||
timeline.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(RULES, pl):
|
||||
_cy25 = RULES[2025]
|
||||
_cy26 = RULES[2026]
|
||||
_prop = _cy26.proposed
|
||||
|
||||
_rows = [
|
||||
{"vintage": "CY2025 Final", "family": "Standard", "track": "Non-QP", "cf": _cy25.conversion_factor},
|
||||
{"vintage": "CY2026 Proposed", "family": "Standard", "track": "Non-QP", "cf": _prop.conversion_factor},
|
||||
{"vintage": "CY2026 Final", "family": "Standard", "track": "Non-QP", "cf": _cy26.conversion_factor},
|
||||
{"vintage": "CY2026 Proposed", "family": "Standard", "track": "QP", "cf": _prop.cf_qp},
|
||||
{"vintage": "CY2026 Final", "family": "Standard", "track": "QP", "cf": _cy26.cf_qp},
|
||||
{"vintage": "CY2026 Proposed", "family": "Anesthesia", "track": "Non-QP", "cf": _prop.anesthesia_cf},
|
||||
{"vintage": "CY2026 Final", "family": "Anesthesia", "track": "Non-QP", "cf": _cy26.anesthesia_cf},
|
||||
{"vintage": "CY2026 Proposed", "family": "Anesthesia", "track": "QP", "cf": _prop.anesthesia_cf_qp},
|
||||
# NOTE: RULES[2026].anesthesia_cf models the non-QP anesthesia CF
|
||||
# only — a final QP anesthesia CF is not yet captured in the
|
||||
# registry (see pfs.rules module docstring), so that bar is
|
||||
# deliberately omitted rather than guessed.
|
||||
]
|
||||
cf_walk = pl.DataFrame(_rows)
|
||||
cf_walk
|
||||
return (cf_walk,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(alt, cf_walk, mo):
|
||||
_vintage_order = ["CY2025 Final", "CY2026 Proposed", "CY2026 Final"]
|
||||
_track_domain = ["Non-QP", "QP"]
|
||||
|
||||
cf_chart = (
|
||||
alt.Chart(cf_walk.to_pandas())
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("vintage:N", title=None, sort=_vintage_order),
|
||||
xOffset=alt.XOffset("track:N", sort=_track_domain),
|
||||
y=alt.Y("cf:Q", title="Conversion factor ($/RVU)", scale=alt.Scale(zero=False)),
|
||||
color=alt.Color("track:N", title="Track", scale=alt.Scale(domain=_track_domain)),
|
||||
tooltip=["vintage", "family", "track", alt.Tooltip("cf:Q", format="$.4f")],
|
||||
)
|
||||
.properties(width=260, height=280)
|
||||
.facet(column=alt.Column("family:N", title=None))
|
||||
.resolve_scale(y="independent")
|
||||
.properties(title="CY2026 PFS Conversion Factors — Proposed vs. Final")
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
cf_chart,
|
||||
mo.md("""
|
||||
*Anesthesia QP (final) is not shown — the Final Rule did not
|
||||
restate a separate QP anesthesia CF in a form captured by the
|
||||
`pfs.rules` registry yet.*
|
||||
|
||||
**Sources:** CY2025 — 89 FR 98452. CY2026 NPRM — 90 FR 32352
|
||||
(CMS-1832-P). CY2026 Final Rule — 90 FR 49266.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
### Budget-neutrality adjustor decomposition
|
||||
|
||||
CMS's own NPRM narrative (90 FR 32802) attributes the CY2026 CF
|
||||
change to three multiplicative pieces: a one-time +2.50% statutory
|
||||
increase, the +0.25%/+0.75% nonqualifying/qualifying-APM annual
|
||||
updates, and the budget-neutrality (BN) adjustor. Compounding those
|
||||
stated percentages against the CY2025 CF reproduces the registry's
|
||||
actual proposed/final values to within rounding — a useful
|
||||
cross-check that the registry's transcription is internally
|
||||
consistent with CMS's narrative.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(RULES, mo, pl):
|
||||
_cy25_cf = RULES[2025].conversion_factor
|
||||
_cy26 = RULES[2026]
|
||||
_prop = _cy26.proposed
|
||||
|
||||
# CMS's own stated CF-update components (90 FR 32802) — a one-time
|
||||
# statutory increase plus the MACRA 2015 sec. 1848(d)(20) annual
|
||||
# updates. NONE of these three percentages is a field anywhere in
|
||||
# `pfs.rules` or `qpp` — there is nothing to attribute-access. They
|
||||
# are kept as literals ONLY because CMS's narrative states them as
|
||||
# bare numbers, not derived from any other registry value; this is a
|
||||
# cited transcription of that narrative, not a second source of
|
||||
# truth for `budget_neutrality_adjustor` or the CFs themselves
|
||||
# (both of which — `bn_adjustor` / `registry_cf` below — ARE read
|
||||
# live from the registry).
|
||||
_ONE_TIME_INCREASE_MULT = 1.0250 # +2.50% single-year statutory increase, 90 FR 32802
|
||||
_ANNUAL_UPDATE_MULT = {
|
||||
"Non-QP": 1.0025, # +0.25%/yr nonqualifying-APM update, 90 FR 32802
|
||||
"QP": 1.0075, # +0.75%/yr qualifying-APM update, 90 FR 32802
|
||||
}
|
||||
|
||||
def _reconstruct(track, bn_adjustor):
|
||||
return _cy25_cf * _ONE_TIME_INCREASE_MULT * _ANNUAL_UPDATE_MULT[track] * bn_adjustor
|
||||
|
||||
_bn_rows = [
|
||||
{
|
||||
"vintage": "CY2026 Proposed",
|
||||
"track": "Non-QP",
|
||||
"one_time_mult": _ONE_TIME_INCREASE_MULT,
|
||||
"annual_update_mult": _ANNUAL_UPDATE_MULT["Non-QP"],
|
||||
"bn_adjustor": _prop.budget_neutrality_adjustor,
|
||||
"reconstructed_cf": round(_reconstruct("Non-QP", _prop.budget_neutrality_adjustor), 4),
|
||||
"registry_cf": _prop.conversion_factor,
|
||||
},
|
||||
{
|
||||
"vintage": "CY2026 Proposed",
|
||||
"track": "QP",
|
||||
"one_time_mult": _ONE_TIME_INCREASE_MULT,
|
||||
"annual_update_mult": _ANNUAL_UPDATE_MULT["QP"],
|
||||
"bn_adjustor": _prop.budget_neutrality_adjustor,
|
||||
"reconstructed_cf": round(_reconstruct("QP", _prop.budget_neutrality_adjustor), 4),
|
||||
"registry_cf": _prop.cf_qp,
|
||||
},
|
||||
{
|
||||
"vintage": "CY2026 Final",
|
||||
"track": "Non-QP",
|
||||
"one_time_mult": _ONE_TIME_INCREASE_MULT,
|
||||
"annual_update_mult": _ANNUAL_UPDATE_MULT["Non-QP"],
|
||||
"bn_adjustor": _cy26.budget_neutrality_adjustor,
|
||||
"reconstructed_cf": round(_reconstruct("Non-QP", _cy26.budget_neutrality_adjustor), 4),
|
||||
"registry_cf": _cy26.conversion_factor,
|
||||
},
|
||||
{
|
||||
"vintage": "CY2026 Final",
|
||||
"track": "QP",
|
||||
"one_time_mult": _ONE_TIME_INCREASE_MULT,
|
||||
"annual_update_mult": _ANNUAL_UPDATE_MULT["QP"],
|
||||
"bn_adjustor": _cy26.budget_neutrality_adjustor,
|
||||
"reconstructed_cf": round(_reconstruct("QP", _cy26.budget_neutrality_adjustor), 4),
|
||||
"registry_cf": _cy26.cf_qp,
|
||||
},
|
||||
]
|
||||
bn_decomp = pl.DataFrame(_bn_rows).with_columns(
|
||||
(pl.col("reconstructed_cf") - pl.col("registry_cf")).abs().round(4).alias("abs_diff")
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
bn_decomp,
|
||||
mo.md(f"""
|
||||
`reconstructed_cf = CY2025_CF × one_time_mult × annual_update_mult × bn_adjustor`
|
||||
— `bn_adjustor` and `registry_cf` are read live from
|
||||
`RULES[2026]` / `RULES[2026].proposed`. `one_time_mult`
|
||||
({_ONE_TIME_INCREASE_MULT}) and `annual_update_mult`
|
||||
({_ANNUAL_UPDATE_MULT["Non-QP"]} non-QP /
|
||||
{_ANNUAL_UPDATE_MULT["QP"]} QP) are **cited transcriptions of
|
||||
CMS's NPRM narrative (90 FR 32802)**, not registry fields —
|
||||
no field in `pfs.rules` or `qpp` models the 2.50%/0.25%/0.75%
|
||||
statutory update components individually, only their combined
|
||||
effect on `conversion_factor` / `cf_qp`.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# ── 2. RVU-level deltas ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 2. RVU-Level Deltas — Which HCPCS Codes Move the Most
|
||||
|
||||
`pfs.rvu_proposed` (14,169 rows as of this ingest, CMS-1832-P) is
|
||||
compared against `pfs.rvu` for CY2025 (pre-rule baseline) and CY2026
|
||||
(what actually got finalized). Both `pfs.rvu` and `pfs.rvu_proposed`
|
||||
are deduplicated to one row per HCPCS base code (no modifier),
|
||||
`status_code = 'A'` (actively priced), and — for the proposed table
|
||||
specifically — a non-null non-facility PE RVU, since ~29% of
|
||||
`pfs.rvu_proposed` rows (as of this ingest) carry a null
|
||||
non-facility *or* facility PE RVU (CMS's Addendum B only populates
|
||||
the setting a code is actually priced in).
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(q):
|
||||
rvu_delta_raw = q("""
|
||||
WITH proposed AS (
|
||||
SELECT hcpcs, description, work_rvu, nonfac_pe_rvu, mp_rvu,
|
||||
work_rvu + nonfac_pe_rvu + mp_rvu AS total_nf_rvu,
|
||||
CASE
|
||||
WHEN hcpcs BETWEEN '99201' AND '99499' THEN 'E/M'
|
||||
WHEN hcpcs BETWEEN '10000' AND '19999' THEN 'Integumentary'
|
||||
WHEN hcpcs BETWEEN '20000' AND '29999' THEN 'Musculoskeletal'
|
||||
WHEN hcpcs BETWEEN '30000' AND '39999' THEN 'Resp/Cardiovascular'
|
||||
WHEN hcpcs BETWEEN '40000' AND '49999' THEN 'Digestive'
|
||||
WHEN hcpcs BETWEEN '50000' AND '59999' THEN 'Urinary/Genital'
|
||||
WHEN hcpcs BETWEEN '60000' AND '69999' THEN 'Endocrine/Nervous'
|
||||
WHEN hcpcs BETWEEN '70000' AND '79999' THEN 'Radiology'
|
||||
WHEN hcpcs BETWEEN '80000' AND '89999' THEN 'Path/Lab'
|
||||
WHEN hcpcs BETWEEN '90000' AND '99199' THEN 'Medicine'
|
||||
WHEN hcpcs LIKE 'G%' THEN 'G-codes'
|
||||
ELSE 'Other'
|
||||
END AS category
|
||||
FROM pfs.rvu_proposed
|
||||
WHERE (modifier IS NULL OR modifier = '')
|
||||
AND status_code = 'A'
|
||||
AND nonfac_pe_rvu IS NOT NULL
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
),
|
||||
final_2025 AS (
|
||||
SELECT hcpcs, work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
|
||||
FROM pfs.rvu
|
||||
WHERE year = 2025 AND (mod IS NULL OR mod = '') AND status_code = 'A'
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
),
|
||||
final_2026 AS (
|
||||
SELECT hcpcs, work_rvu + non_fac_pe_rvu + mp_rvu AS total_nf_rvu
|
||||
FROM pfs.rvu
|
||||
WHERE year = 2026 AND (mod IS NULL OR mod = '') AND status_code = 'A'
|
||||
QUALIFY row_number() OVER (PARTITION BY hcpcs ORDER BY hcpcs) = 1
|
||||
)
|
||||
SELECT
|
||||
p.hcpcs,
|
||||
p.description,
|
||||
p.category,
|
||||
p.total_nf_rvu AS total_nf_rvu_proposed,
|
||||
f25.total_nf_rvu AS total_nf_rvu_2025,
|
||||
f26.total_nf_rvu AS total_nf_rvu_2026final
|
||||
FROM proposed p
|
||||
JOIN final_2025 f25 USING (hcpcs)
|
||||
JOIN final_2026 f26 USING (hcpcs)
|
||||
""")
|
||||
rvu_delta_raw
|
||||
return (rvu_delta_raw,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(RULES, pl, rvu_delta_raw):
|
||||
_cf_2025 = RULES[2025].conversion_factor
|
||||
_cf_2026_proposed = RULES[2026].proposed.conversion_factor
|
||||
_cf_2026_final = RULES[2026].conversion_factor
|
||||
|
||||
rvu_delta = (
|
||||
rvu_delta_raw.with_columns(
|
||||
(pl.col("total_nf_rvu_proposed") * _cf_2026_proposed).round(2).alias("dollar_proposed"),
|
||||
(pl.col("total_nf_rvu_2025") * _cf_2025).round(2).alias("dollar_2025"),
|
||||
(pl.col("total_nf_rvu_2026final") * _cf_2026_final).round(2).alias("dollar_2026final"),
|
||||
)
|
||||
.with_columns(
|
||||
(pl.col("dollar_proposed") - pl.col("dollar_2025")).round(2).alias("delta_vs_2025"),
|
||||
(pl.col("dollar_proposed") - pl.col("dollar_2026final")).round(2).alias("delta_vs_final"),
|
||||
)
|
||||
)
|
||||
return (rvu_delta,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
baseline_picker = mo.ui.radio(
|
||||
options={
|
||||
"NPRM impact — proposed vs. CY2025 final (what the rule proposed to change)": "delta_vs_2025",
|
||||
"Proposed→finalized drift — proposed vs. CY2026 final (what the comment period changed)": "delta_vs_final",
|
||||
},
|
||||
value="NPRM impact — proposed vs. CY2025 final (what the rule proposed to change)",
|
||||
label="Compare",
|
||||
)
|
||||
baseline_picker
|
||||
return (baseline_picker,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(AMBER, RULES, TEAL, alt, baseline_picker, mo, rvu_delta):
|
||||
_col = baseline_picker.value
|
||||
_n = 20
|
||||
|
||||
_winners = rvu_delta.sort(_col, descending=True).head(_n)
|
||||
_losers = rvu_delta.sort(_col, descending=False).head(_n)
|
||||
_top40 = _winners.vstack(_losers)
|
||||
|
||||
_plot_df = _top40.to_pandas()
|
||||
_plot_df["label"] = _plot_df["hcpcs"] + " — " + _plot_df["description"].str.slice(0, 40)
|
||||
_plot_df["sign"] = _plot_df[_col].apply(lambda v: "Gain" if v >= 0 else "Loss")
|
||||
|
||||
winners_losers_chart = (
|
||||
alt.Chart(_plot_df)
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X(f"{_col}:Q", title="$ change (national unadjusted, GPCI=1.0)"),
|
||||
y=alt.Y("label:N", title=None, sort=alt.SortField(field=_col, order="descending")),
|
||||
color=alt.Color(
|
||||
"sign:N",
|
||||
title=None,
|
||||
scale=alt.Scale(domain=["Gain", "Loss"], range=[TEAL, AMBER]),
|
||||
legend=alt.Legend(orient="top"),
|
||||
),
|
||||
tooltip=["hcpcs", "description", alt.Tooltip(f"{_col}:Q", format="$.2f")],
|
||||
)
|
||||
.properties(
|
||||
title=f"Top 20 Winners / Top 20 Losers — {baseline_picker.value}",
|
||||
width=700,
|
||||
height=600,
|
||||
)
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
winners_losers_chart,
|
||||
mo.md(f"""
|
||||
**Sources:** `pfs.rvu_proposed` — CMS-1832-P NPRM,
|
||||
{RULES[2026].proposed.federal_register_citation}. Baseline —
|
||||
`pfs.rvu` CY2025 Final ({RULES[2025].federal_register_citation})
|
||||
or CY2026 Final ({RULES[2026].federal_register_citation}), per
|
||||
the comparison selected above.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo, rvu_delta):
|
||||
mo.vstack(
|
||||
[
|
||||
mo.md(f"""
|
||||
**Full detail — {rvu_delta.height:,} codes** matched across
|
||||
proposed, CY2025-final, and CY2026-final RVU tables (search
|
||||
the table below by HCPCS or description).
|
||||
"""),
|
||||
mo.ui.table(
|
||||
rvu_delta.sort("delta_vs_2025").to_pandas(),
|
||||
page_size=25,
|
||||
label="RVU / Payment Deltas by HCPCS",
|
||||
),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(con, mo):
|
||||
_new = con.execute("""
|
||||
SELECT count(*) FROM pfs.rvu_proposed p
|
||||
WHERE (p.modifier IS NULL OR p.modifier='') AND p.status_code='A' AND p.nonfac_pe_rvu IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM pfs.rvu f WHERE f.year=2025 AND f.hcpcs=p.hcpcs AND (f.mod IS NULL OR f.mod=''))
|
||||
""").fetchone()[0]
|
||||
_dropped = con.execute("""
|
||||
SELECT count(*) FROM pfs.rvu f
|
||||
WHERE f.year=2025 AND (f.mod IS NULL OR f.mod='') AND f.status_code='A'
|
||||
AND NOT EXISTS (SELECT 1 FROM pfs.rvu_proposed p WHERE p.hcpcs=f.hcpcs AND (p.modifier IS NULL OR p.modifier=''))
|
||||
""").fetchone()[0]
|
||||
mo.md(f"""
|
||||
> **Coverage caveat:** {_new} HCPCS codes appear in `pfs.rvu_proposed`
|
||||
> with no CY2025-final counterpart (new/renumbered codes), and {_dropped}
|
||||
> CY2025-final codes have no match in `pfs.rvu_proposed` (dropped,
|
||||
> bundled, or excluded from the Addendum B extract used to build the
|
||||
> lake table). Both groups are excluded from the delta analysis above
|
||||
> rather than shown with a fabricated baseline.
|
||||
|
||||
**Source:** `pfs.rvu_proposed` — CMS-1832-P Addendum B, 90 FR 32352.
|
||||
`pfs.rvu` — annual PFS Final Rule Addendum B.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
# ── 3. Specialty impact ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 3. Specialty Impact
|
||||
|
||||
CMS's own specialty-impact table — **Table 92, "CY 2026 PFS Estimated
|
||||
Impact on Total Allowed Charges by Specialty"** (NPRM, 90 FR
|
||||
32802–32806) — is what the brief calls for here. It is **not
|
||||
available as transcribable text**: the Federal Register's own
|
||||
full-text extraction of the NPRM embeds Table 92 as five scanned TIFF
|
||||
graphics (`EP16JY25.178`–`.182`), not selectable text, in both the
|
||||
plain-text and PDF-text-layer versions of `data/fr_downloads/
|
||||
2025-13271.{txt,pdf}` that are ingested into this repository. There is
|
||||
also no specialty-to-HCPCS mapping in the lake (`information_schema`
|
||||
shows only `cms`, `opps`, and `pfs` schemas — no `reference_data`
|
||||
schema exists to join against). CMS's real specialty attribution uses
|
||||
claims-weighted utilization by specialty, which this repo does not
|
||||
ingest.
|
||||
|
||||
**What follows instead** is a coarse CPT-range proxy — the same
|
||||
category buckets used in `skin_sub_budget_neutrality.py` — applied to
|
||||
the RVU deltas computed in Section 2. It approximates *which kinds of
|
||||
services* moved, not *which specialties* were affected, and is
|
||||
explicitly **not** a substitute for Table 92.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(pl, rvu_delta):
|
||||
category_impact = (
|
||||
rvu_delta.group_by("category")
|
||||
.agg(
|
||||
pl.col("hcpcs").count().alias("codes"),
|
||||
pl.col("delta_vs_2025").sum().round(0).alias("total_delta_vs_2025"),
|
||||
pl.col("delta_vs_final").sum().round(0).alias("total_delta_vs_final"),
|
||||
)
|
||||
.sort("total_delta_vs_2025", descending=True)
|
||||
)
|
||||
category_impact
|
||||
return (category_impact,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(AMBER, TEAL, alt, category_impact, mo):
|
||||
_df = category_impact.to_pandas()
|
||||
_df["sign"] = _df["total_delta_vs_2025"].apply(lambda v: "Net gain" if v >= 0 else "Net loss")
|
||||
|
||||
category_chart = (
|
||||
alt.Chart(_df)
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("total_delta_vs_2025:Q", title="Aggregate $ change vs. CY2025 (proxy category)"),
|
||||
y=alt.Y("category:N", title=None, sort="-x"),
|
||||
color=alt.Color(
|
||||
"sign:N",
|
||||
title=None,
|
||||
scale=alt.Scale(domain=["Net gain", "Net loss"], range=[TEAL, AMBER]),
|
||||
legend=alt.Legend(orient="top"),
|
||||
),
|
||||
tooltip=["category", "codes", alt.Tooltip("total_delta_vs_2025:Q", format="$,.0f")],
|
||||
)
|
||||
.properties(title="CPT-Range Proxy — Not CMS's Official Specialty Table", width=700, height=350)
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
category_chart,
|
||||
mo.md("""
|
||||
*Proxy category buckets, not specialty attribution. CMS's
|
||||
official specialty impacts: Table 92, NPRM 90 FR 32802–32806
|
||||
(embedded graphic, not machine-readable in this repo's FR
|
||||
corpus). Public-use file with granular specialty impacts:
|
||||
cms.gov, CY2026 PFS proposed rule downloads.*
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# ── 4. Advanced APM requirements ───────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 4. Advanced APM (QP) Requirements
|
||||
|
||||
Under MACRA 2015, clinicians who participate heavily enough in an
|
||||
Advanced Alternative Payment Model (Advanced APM) during a "QP
|
||||
Performance Period" become Qualifying APM Participants (QPs) for the
|
||||
payment year two years later (`payment_year = performance_year + 2`).
|
||||
QP status has, historically, meant MIPS exclusion and a lump-sum
|
||||
incentive payment; starting payment year 2026 it also means a
|
||||
**different conversion factor**.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(for_payment_year, mo):
|
||||
_governs_2026 = for_payment_year(2026)
|
||||
mo.md(f"""
|
||||
`qpp.for_payment_year(2026)` resolves to QP Performance Period
|
||||
**{_governs_2026.performance_year}** ({_governs_2026.citation}) — the
|
||||
performance period whose determinations govern CY2026 payment, and
|
||||
the first performance period where `qp_cf_applies` is
|
||||
**{_governs_2026.qp_cf_applies}**.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(QPP, pl):
|
||||
_rows = []
|
||||
for _perf_year, _qy in sorted(QPP.items()):
|
||||
_prop = _qy.proposed
|
||||
_rows.append(
|
||||
{
|
||||
"performance_year": _perf_year,
|
||||
"payment_year": _qy.payment_year,
|
||||
"qp_payment_pct": _qy.qp_thresholds.payment_amount_pct,
|
||||
"qp_patient_pct": _qy.qp_thresholds.patient_count_pct,
|
||||
"partial_qp_payment_pct": _qy.partial_qp_thresholds.payment_amount_pct,
|
||||
"partial_qp_patient_pct": _qy.partial_qp_thresholds.patient_count_pct,
|
||||
"revenue_nominal_pct": _qy.risk_standards.revenue_nominal_pct,
|
||||
"benchmark_nominal_pct": _qy.risk_standards.benchmark_nominal_pct,
|
||||
"apm_incentive_pct": _qy.apm_incentive_pct,
|
||||
"qp_cf_applies": _qy.qp_cf_applies,
|
||||
"cehrt_required": _qy.cehrt_required,
|
||||
"proposed_numeric_change": (
|
||||
"none — see disposition table below" if _prop is not None else "n/a"
|
||||
),
|
||||
"citation": _qy.citation,
|
||||
}
|
||||
)
|
||||
apm_thresholds = pl.DataFrame(_rows)
|
||||
apm_thresholds
|
||||
return (apm_thresholds,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(alt, apm_thresholds, mo):
|
||||
_df = apm_thresholds.to_pandas()
|
||||
_df["apm_incentive_pct"] = _df["apm_incentive_pct"].fillna(0.0)
|
||||
_df["status"] = _df["apm_incentive_pct"].apply(lambda v: "Paid" if v > 0 else "Sunset")
|
||||
|
||||
incentive_chart = (
|
||||
alt.Chart(_df)
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("payment_year:O", title="Payment year"),
|
||||
y=alt.Y("apm_incentive_pct:Q", title="APM incentive payment (% of base-year Part B paid claims)"),
|
||||
color=alt.Color("status:N", title=None, scale=alt.Scale(domain=["Paid", "Sunset"])),
|
||||
tooltip=["performance_year", "payment_year", "apm_incentive_pct", "status"],
|
||||
)
|
||||
.properties(title="APM Incentive Payment Sunset", width=600, height=300)
|
||||
)
|
||||
|
||||
_citations = ", ".join(sorted(set(_df["citation"])))
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
incentive_chart,
|
||||
mo.md(f"""
|
||||
Payment year 2026 (QP Performance Period 2024) is, under
|
||||
current law, the **last** year with a lump-sum APM incentive
|
||||
payment (1.88% of base-year paid claims). From payment year
|
||||
2027 onward, QP status stops paying a bonus and starts
|
||||
(instead) determining *which conversion factor applies* — see
|
||||
below.
|
||||
|
||||
**Sources:** `qpp.QPP` performance years 2023–2026
|
||||
({_citations}).
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(QPP, mo):
|
||||
_cy = QPP[2026]
|
||||
_proposed = _cy.proposed
|
||||
_qp = _cy.qp_thresholds
|
||||
_partial = _cy.partial_qp_thresholds
|
||||
mo.md(f"""
|
||||
### CY2026 NPRM proposed QPP changes — proposed vs. finalized disposition
|
||||
|
||||
Every FR page-pincite in the "Disposition" column below is quoted
|
||||
**verbatim from `qpp.QPP[2026].proposed.changes`**
|
||||
({_proposed.federal_register_citation}, {_proposed.cms_rule_id}) — that
|
||||
field is the single source of truth for this table; nothing here is a
|
||||
second, independently-typed citation.
|
||||
|
||||
| Proposal | Disposition |
|
||||
|---|---|
|
||||
| Individual-level QP determination (new Threshold Score calc, Sec. 414.1425(b)(3)) | **Finalized as proposed** (90 FR 49923–49928, 49980) |
|
||||
| Attribution-eligible beneficiary definition — expand 6th criterion beyond E/M services | **Finalized WITH MODIFICATION** — dual E/M + covered-professional-services methodology (90 FR 49980) |
|
||||
| Sunset the Medical Home Model 50-clinician limit (Sec. 414.1415(c)(7)) after the 2025 QP Performance Period | **Finalized as proposed** (90 FR 49929) |
|
||||
| QP / Partial QP percentage thresholds | **No numeric change proposed** — remain `qp_thresholds` {_qp.payment_amount_pct:.0f}%/{_qp.patient_count_pct:.0f}% (QP), `partial_qp_thresholds` {_partial.payment_amount_pct:.0f}%/{_partial.patient_count_pct:.0f}% (Partial QP) |
|
||||
| Nominal-amount risk standards (42 CFR 414.1415(c)(3)(i)) / CEHRT requirement | **No change proposed** — cross-referenced by section only |
|
||||
|
||||
Full sourced narrative: `qpp.QPP[2026].proposed.changes`.
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
### What QP status is worth in dollars — 3 example HCPCS
|
||||
|
||||
National, unadjusted (GPCI = 1.0) payment for a single unit of
|
||||
service, computed as `(work_rvu + non_fac_pe_rvu + mp_rvu) × CF`,
|
||||
comparing the QP and non-QP conversion factors for both the NPRM and
|
||||
the Final Rule:
|
||||
|
||||
- **99213** — established-patient office visit, low complexity (E/M)
|
||||
- **27447** — total knee arthroplasty (major procedure)
|
||||
- **70553** — MRI brain, without and with contrast (imaging)
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(RULES, pl, q):
|
||||
_example_rvu = q("""
|
||||
SELECT hcpcs, description, work_rvu, non_fac_pe_rvu, mp_rvu,
|
||||
work_rvu + non_fac_pe_rvu + mp_rvu AS total_rvu
|
||||
FROM pfs.rvu
|
||||
WHERE year = 2026 AND (mod IS NULL OR mod = '')
|
||||
AND hcpcs IN ('99213', '27447', '70553')
|
||||
""")
|
||||
|
||||
_cy26 = RULES[2026]
|
||||
_prop = _cy26.proposed
|
||||
_rows = []
|
||||
for _r in _example_rvu.iter_rows(named=True):
|
||||
for _vintage, _cf_nonqp, _cf_qp in (
|
||||
("CY2026 Proposed", _prop.conversion_factor, _prop.cf_qp),
|
||||
("CY2026 Final", _cy26.conversion_factor, _cy26.cf_qp),
|
||||
):
|
||||
_pay_nonqp = round(_r["total_rvu"] * _cf_nonqp, 2)
|
||||
_pay_qp = round(_r["total_rvu"] * _cf_qp, 2)
|
||||
_rows.append(
|
||||
{
|
||||
"hcpcs": _r["hcpcs"],
|
||||
"description": _r["description"],
|
||||
"vintage": _vintage,
|
||||
"non_qp_payment": _pay_nonqp,
|
||||
"qp_payment": _pay_qp,
|
||||
"qp_differential": round(_pay_qp - _pay_nonqp, 2),
|
||||
}
|
||||
)
|
||||
|
||||
qp_differential = pl.DataFrame(_rows)
|
||||
qp_differential
|
||||
return (qp_differential,)
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(alt, mo, qp_differential):
|
||||
differential_chart = (
|
||||
alt.Chart(qp_differential.to_pandas())
|
||||
.mark_bar()
|
||||
.encode(
|
||||
x=alt.X("hcpcs:N", title="HCPCS"),
|
||||
xOffset=alt.XOffset("vintage:N", sort=["CY2026 Proposed", "CY2026 Final"]),
|
||||
y=alt.Y("qp_differential:Q", title="QP minus non-QP payment ($, national unadjusted)"),
|
||||
color=alt.Color("vintage:N", title=None, sort=["CY2026 Proposed", "CY2026 Final"]),
|
||||
tooltip=["hcpcs", "description", "vintage", alt.Tooltip("qp_differential:Q", format="$.2f")],
|
||||
)
|
||||
.properties(title="Dollar Value of QP Status — 3 Example Codes", width=500, height=320)
|
||||
)
|
||||
|
||||
mo.vstack(
|
||||
[
|
||||
differential_chart,
|
||||
mo.md("""
|
||||
**Sources:** RVUs — `pfs.rvu`, CY2026 (90 FR 49266). CFs —
|
||||
`pfs.rules.RULES[2026]` (final, 90 FR 49266) and
|
||||
`RULES[2026].proposed` (NPRM, 90 FR 32352). National unadjusted
|
||||
means GPCI = 1.0 — actual payment varies by locality.
|
||||
"""),
|
||||
]
|
||||
)
|
||||
return
|
||||
|
||||
|
||||
# ── 5. Provenance ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo):
|
||||
mo.md("""
|
||||
## 5. Provenance
|
||||
|
||||
Ingest-log entries (`cms.ingest_log`) for every table this notebook
|
||||
reads from — `pfs.rvu` and `pfs.rvu_proposed` — so every figure above
|
||||
can be traced back to a specific ingest run, source file, and Federal
|
||||
Register citation. (`pfs.gpci` is not queried anywhere in this
|
||||
notebook — see the intro's GPCI = 1.0 scope note — so it is
|
||||
deliberately excluded here rather than claimed as a source.)
|
||||
""")
|
||||
return
|
||||
|
||||
|
||||
@app.cell(hide_code=True)
|
||||
def _(mo, q):
|
||||
provenance = q("""
|
||||
SELECT run_id, ingested_at, module, table_name, rule_id,
|
||||
source_file, sha256, rows, fr_citation, pincite_key
|
||||
FROM cms.ingest_log
|
||||
WHERE table_name IN ('pfs.rvu', 'pfs.rvu_proposed')
|
||||
ORDER BY ingested_at DESC
|
||||
""")
|
||||
mo.ui.table(provenance.to_pandas(), label="Ingest Log — Tables Used in This Notebook")
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
@@ -33,7 +33,7 @@ api = [
|
||||
"uvicorn>=0.41.0",
|
||||
"httpx>=0.28.1",
|
||||
"pyjwt>=2.13.0",
|
||||
"cryptography>=48.0.1",
|
||||
"cryptography>=50.0.0",
|
||||
]
|
||||
bcda = [
|
||||
"stack[conf]",
|
||||
@@ -94,6 +94,10 @@ pfs = [
|
||||
"duckdb>=1.0.0",
|
||||
"narwhals>=2.17.0",
|
||||
]
|
||||
qpp = [
|
||||
"stack[conf]",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
rec = [
|
||||
"stack[conf]",
|
||||
"stack[pfs]",
|
||||
@@ -159,6 +163,7 @@ all = [
|
||||
"stack[llm]",
|
||||
"stack[perf]",
|
||||
"stack[pfs]",
|
||||
"stack[qpp]",
|
||||
"stack[rec]",
|
||||
"stack[rex]",
|
||||
"stack[lake]",
|
||||
@@ -212,6 +217,6 @@ source = ["src"]
|
||||
concurrency = ["multiprocessing", "thread"]
|
||||
|
||||
[tool.uv.build-backend]
|
||||
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "rec", "rex", "sem"]
|
||||
module-name = ["aco", "api", "bcda", "bib", "bls", "ccw", "cli", "cms", "conf", "llm", "mail", "opps", "perf", "pfs", "prisma", "qpp", "rec", "rex", "sem"]
|
||||
namespace = true
|
||||
source-exclude = ["compose.yml","infra/**","data/**","notebooks/**","tuva/**","assets/**","docs/**","dev/**","bundle/**","cloud/**"]
|
||||
|
||||
@@ -8,20 +8,21 @@ app = typer.Typer(no_args_is_help=True)
|
||||
@app.command()
|
||||
def index(
|
||||
collection: str = typer.Option(
|
||||
"comments", help="Which collection: comments | corpus."
|
||||
"comments", help="Which collection: comments | corpus | rules."
|
||||
),
|
||||
docket: str = typer.Option("", help="Limit comments to one docket id."),
|
||||
key: list[str] = typer.Option([], "--key", help="Limit rules to these item keys."),
|
||||
force: bool = typer.Option(False, help="Re-embed even when unchanged."),
|
||||
limit: int = typer.Option(0, help="Stop after N docs (0 = all)."),
|
||||
) -> None:
|
||||
"""Embed comments/corpus into pgvector (incremental, resumable)."""
|
||||
"""Embed comments/corpus/rules into pgvector (incremental, resumable)."""
|
||||
import itertools
|
||||
|
||||
from conf.connect import bib
|
||||
from llm import config as llm_config
|
||||
from llm.index import index_docs
|
||||
from llm.pool import HostPool
|
||||
from llm.source import iter_comment_docs, iter_corpus_docs
|
||||
from llm.source import iter_comment_docs, iter_corpus_docs, iter_rule_docs
|
||||
|
||||
cfg = llm_config.load()
|
||||
store = bib()
|
||||
@@ -29,8 +30,10 @@ def index(
|
||||
docs = iter_comment_docs(store, docket=docket)
|
||||
elif collection == "corpus":
|
||||
docs = iter_corpus_docs(store)
|
||||
elif collection == "rules":
|
||||
docs = iter_rule_docs(store, keys=tuple(key))
|
||||
else:
|
||||
raise typer.BadParameter("collection must be 'comments' or 'corpus'")
|
||||
raise typer.BadParameter("collection must be 'comments', 'corpus' or 'rules'")
|
||||
if limit:
|
||||
docs = itertools.islice(docs, limit)
|
||||
stats = index_docs(
|
||||
|
||||
128
src/cms/ingest_log.py
Normal file
128
src/cms/ingest_log.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Append-only DuckDB provenance table for ingest operations.
|
||||
|
||||
Complements the JSONL logs in `cms.log`, does not replace them.
|
||||
Tracks which reference tables were ingested, when, and by which module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
def new_run_id() -> str:
|
||||
"""Generate a new run ID as a UUID4 hex string."""
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
def file_sha256(path: str | Path) -> str:
|
||||
"""Compute SHA256 hash of a file.
|
||||
|
||||
Args:
|
||||
path: Path to the file to hash.
|
||||
|
||||
Returns:
|
||||
Hexadecimal SHA256 hash of the file contents.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file does not exist.
|
||||
"""
|
||||
path = Path(path)
|
||||
sha256_hash = hashlib.sha256()
|
||||
with open(path, "rb") as f:
|
||||
for chunk in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(chunk)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
|
||||
def ensure_table(con) -> None:
|
||||
"""Create the cms.ingest_log table if it does not exist.
|
||||
|
||||
Args:
|
||||
con: An open DuckDB connection.
|
||||
"""
|
||||
con.execute("""
|
||||
CREATE SCHEMA IF NOT EXISTS cms
|
||||
""")
|
||||
con.execute("""
|
||||
CREATE TABLE IF NOT EXISTS cms.ingest_log (
|
||||
run_id VARCHAR,
|
||||
ingested_at TIMESTAMP,
|
||||
module VARCHAR,
|
||||
table_name VARCHAR,
|
||||
rule_id VARCHAR,
|
||||
source_file VARCHAR,
|
||||
sha256 VARCHAR,
|
||||
rows BIGINT,
|
||||
fr_citation VARCHAR,
|
||||
pincite_key VARCHAR
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def log_ingest(
|
||||
con,
|
||||
*,
|
||||
module: str,
|
||||
table_name: str,
|
||||
rows: int,
|
||||
source_file: str = "",
|
||||
rule_id: str = "",
|
||||
fr_citation: str = "",
|
||||
pincite_key: str = "",
|
||||
run_id: str = "",
|
||||
) -> None:
|
||||
"""Append an ingest log entry to the provenance table.
|
||||
|
||||
Args:
|
||||
con: An open DuckDB connection.
|
||||
module: Name of the module performing the ingest.
|
||||
table_name: Name of the table being ingested.
|
||||
rows: Number of rows ingested.
|
||||
source_file: Path to the source file (optional). If provided and exists,
|
||||
its SHA256 hash is computed and stored.
|
||||
rule_id: Rule ID associated with this ingest (optional).
|
||||
fr_citation: Federal Register citation (optional).
|
||||
pincite_key: Pincite key (optional).
|
||||
run_id: Run ID for this ingest operation (optional). If not provided,
|
||||
a new one is generated.
|
||||
"""
|
||||
ensure_table(con)
|
||||
|
||||
# Generate run_id if not provided
|
||||
if not run_id:
|
||||
run_id = new_run_id()
|
||||
|
||||
# Compute SHA256 if source_file exists
|
||||
sha256 = ""
|
||||
if source_file:
|
||||
source_path = Path(source_file)
|
||||
if source_path.exists():
|
||||
sha256 = file_sha256(source_path)
|
||||
|
||||
# Get current timestamp in UTC
|
||||
ingested_at = datetime.now(timezone.utc)
|
||||
|
||||
# Insert the row
|
||||
con.execute(
|
||||
"""
|
||||
INSERT INTO cms.ingest_log (
|
||||
run_id, ingested_at, module, table_name, rule_id,
|
||||
source_file, sha256, rows, fr_citation, pincite_key
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
[
|
||||
run_id,
|
||||
ingested_at,
|
||||
module,
|
||||
table_name,
|
||||
rule_id,
|
||||
source_file,
|
||||
sha256,
|
||||
rows,
|
||||
fr_citation,
|
||||
pincite_key,
|
||||
],
|
||||
)
|
||||
@@ -15,6 +15,8 @@ so parsing the comment id is the reliable path to a comment's docket.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import html
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Iterator
|
||||
|
||||
@@ -123,6 +125,89 @@ def _attachment_text(store: Store, item_key: str) -> str:
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
_TAG_RE = re.compile(r"</?[A-Za-z][^>]*>")
|
||||
|
||||
|
||||
def _strip_html_wrapper(text: str) -> str:
|
||||
"""Drop the FR ``.txt`` attachment's HTML wrapper and inline markup.
|
||||
|
||||
FR TXT attachments are HTML-wrapped: ``<html><head>...<body><pre>``
|
||||
then the plain text, closed with ``</pre></body></html>``. Strip
|
||||
everything through the first ``<pre>`` and any trailing close tags;
|
||||
text without that wrapper passes through unchanged.
|
||||
|
||||
The FR text itself also carries inline HTML — e.g. Cloudflare's
|
||||
obfuscated ``<a href="/cdn-cgi/l/email-protection#...">`` mailto
|
||||
anchors and page-break anchors. Those are stripped with a
|
||||
``</?[A-Za-z][^>]*>`` regex (this is a controlled, known corpus —
|
||||
not attacker-supplied HTML — so no HTML-parsing dependency is
|
||||
needed), keeping the anchor's inner text. The tag must start with a
|
||||
letter (optionally preceded by ``/``) so free-text comparison
|
||||
fragments like ``p<0.05`` or ``x>2`` are never mistaken for markup.
|
||||
HTML entities (``&`` etc.) are then unescaped. Plain-text
|
||||
markers like ``[[Page NNNNN]]`` contain no angle brackets and pass
|
||||
through untouched.
|
||||
"""
|
||||
idx = text.find("<pre>")
|
||||
if idx == -1:
|
||||
body = text
|
||||
else:
|
||||
body = text[idx + len("<pre>") :]
|
||||
stripped = body.rstrip()
|
||||
for suffix in ("</pre></body></html>", "</pre>"):
|
||||
if stripped.endswith(suffix):
|
||||
body = stripped[: -len(suffix)]
|
||||
break
|
||||
body = _TAG_RE.sub("", body)
|
||||
return html.unescape(body)
|
||||
|
||||
|
||||
def _rule_text(store: Store, item_key: str) -> str:
|
||||
"""TXT attachment (HTML wrapper stripped) preferred; else PDF-extract."""
|
||||
rows = (
|
||||
store._con()
|
||||
.execute(
|
||||
"SELECT a.storage_path FROM attachments a "
|
||||
"JOIN items i ON i.id = a.item_id WHERE i.key = ?",
|
||||
(item_key,),
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
for (storage_path,) in rows:
|
||||
path = Path(storage_path)
|
||||
if path.suffix.lower() == ".txt" and path.exists():
|
||||
return _strip_html_wrapper(
|
||||
path.read_text(encoding="utf-8", errors="replace")
|
||||
)
|
||||
return _attachment_text(store, item_key)
|
||||
|
||||
|
||||
def iter_rule_docs(
|
||||
store: Store, *, keys: tuple[str, ...] = (), tag: str = ""
|
||||
) -> Iterator[Doc]:
|
||||
"""One Doc per FR rule item: TXT attachment preferred, PDF-extract fallback."""
|
||||
for item in store.list_items(item_type="rule", tag=tag):
|
||||
if keys and item.key not in keys:
|
||||
continue
|
||||
text = _rule_text(store, item.key)
|
||||
if not text.strip():
|
||||
continue
|
||||
cms_rule = next(
|
||||
(t.split(":", 1)[1] for t in item.tags if t.startswith("cms-rule:")), ""
|
||||
)
|
||||
yield Doc(
|
||||
key=item.key,
|
||||
text=text,
|
||||
metadata={
|
||||
"doctype": "rule",
|
||||
"cms_rule_id": cms_rule,
|
||||
"fr_document_number": item.document_number or "",
|
||||
"year": _year_of(store, item.key),
|
||||
"item_key": item.key,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def iter_corpus_docs(store: Store, *, tag: str = "") -> Iterator[Doc]:
|
||||
"""Every non-comment item; attachment text + abstract."""
|
||||
for item in store.list_items(tag=tag):
|
||||
|
||||
172
src/pfs/nprm.py
Normal file
172
src/pfs/nprm.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""NPRM Addendum B loader → ``pfs.rvu_proposed`` (#592).
|
||||
|
||||
Loads the CY2026 NPRM Addendum B "Relative Value Units and Related
|
||||
Information" workbook — registered in bib under the
|
||||
``sup:2026_PFS_NPRM`` tag — into ``pfs.rvu_proposed``, the
|
||||
proposed-rule counterpart to the final-rule ``pfs.rvu`` table that
|
||||
``pfs.pipe.load_all`` populates. Reuses ``pfs.pipe``'s header-detect
|
||||
readers and column map instead of reimplementing them; the target
|
||||
schema is intentionally narrower than ``pfs.rvu`` (no PPRRVU-only
|
||||
columns) since Addendum B never carries them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_ADDENDUM_B_TAG = "sup:2026_PFS_NPRM"
|
||||
_ADDENDUM_B_RE = re.compile(r"(?i)addendum[_ ]?b")
|
||||
|
||||
# pfs.pipe._RVU_COLUMNS normalizes raw headers to these intermediate
|
||||
# names; map them onto the pfs.rvu_proposed target schema.
|
||||
_TARGET_RENAME = {
|
||||
"hcpcs": "hcpcs",
|
||||
"mod": "modifier",
|
||||
"description": "description",
|
||||
"status_code": "status_code",
|
||||
"work_rvu": "work_rvu",
|
||||
"non_fac_pe_rvu": "nonfac_pe_rvu",
|
||||
"fac_pe_rvu": "fac_pe_rvu",
|
||||
"mp_rvu": "mp_rvu",
|
||||
}
|
||||
|
||||
_TARGET_COLUMNS = [
|
||||
"hcpcs",
|
||||
"modifier",
|
||||
"description",
|
||||
"status_code",
|
||||
"work_rvu",
|
||||
"nonfac_pe_rvu",
|
||||
"fac_pe_rvu",
|
||||
"mp_rvu",
|
||||
"cms_rule_id",
|
||||
]
|
||||
|
||||
# Intermediate (pre-rename) names as produced by pfs.pipe._normalize_columns.
|
||||
_RAW_NUMERIC_COLUMNS = ("work_rvu", "non_fac_pe_rvu", "fac_pe_rvu", "mp_rvu")
|
||||
|
||||
|
||||
def _discover_addendum_b() -> Path:
|
||||
"""Locate the NPRM Addendum B attachment in the bib store.
|
||||
|
||||
Queries attachments of items tagged ``sup:2026_PFS_NPRM`` for one
|
||||
whose filename matches ``addendum[_ ]?b`` (case-insensitive),
|
||||
mirroring the join in ``llm/source.py::_attachment_text``.
|
||||
"""
|
||||
from conf import connect
|
||||
|
||||
store = connect.bib()
|
||||
rows = (
|
||||
store._con()
|
||||
.execute(
|
||||
"SELECT a.filename, a.storage_path FROM attachments a "
|
||||
"JOIN items i ON i.id = a.item_id "
|
||||
"JOIN item_tags it ON it.item_id = i.id "
|
||||
"JOIN tags t ON t.id = it.tag_id "
|
||||
"WHERE t.name = ? "
|
||||
"ORDER BY a.id",
|
||||
(_ADDENDUM_B_TAG,),
|
||||
)
|
||||
.fetchall()
|
||||
)
|
||||
for row in rows:
|
||||
filename = row["filename"] or ""
|
||||
if _ADDENDUM_B_RE.search(filename):
|
||||
return Path(row["storage_path"])
|
||||
raise FileNotFoundError(
|
||||
f"No Addendum B attachment found among bib items tagged {_ADDENDUM_B_TAG!r}"
|
||||
)
|
||||
|
||||
|
||||
def load_rvu_proposed(con: Any, *, cms_rule_id: str = "CMS-1832-P") -> dict:
|
||||
"""Load the NPRM Addendum B workbook into ``pfs.rvu_proposed``.
|
||||
|
||||
Idempotent: deletes any existing rows for ``cms_rule_id`` before
|
||||
inserting, so re-running the ingest is safe.
|
||||
|
||||
Returns ``{"rows": int, "source_file": str}``.
|
||||
"""
|
||||
import polars as pl
|
||||
|
||||
from pfs.pipe import (
|
||||
_RVU_COLUMNS,
|
||||
_normalize_columns,
|
||||
_read_csv_with_header,
|
||||
_read_excel,
|
||||
)
|
||||
|
||||
path = _discover_addendum_b()
|
||||
ext = path.suffix.lower()
|
||||
if ext == ".csv":
|
||||
df = _read_csv_with_header(str(path))
|
||||
else:
|
||||
df = _read_excel(str(path))
|
||||
|
||||
if "hcpcs" not in df.columns:
|
||||
df = _normalize_columns(df, _RVU_COLUMNS)
|
||||
if "hcpcs" not in df.columns:
|
||||
raise ValueError(f"{path}: no hcpcs column found after normalization")
|
||||
|
||||
for col in (*_RAW_NUMERIC_COLUMNS, "mod", "description", "status_code"):
|
||||
if col not in df.columns:
|
||||
df = df.with_columns(pl.lit(None).alias(col))
|
||||
|
||||
df = df.with_columns(
|
||||
# Addendum B numeric cells carry leading/trailing padding
|
||||
# (" 0.00") that Polars' string->float cast won't tolerate —
|
||||
# strip before casting or every padded value silently becomes
|
||||
# null. Bare cast(strict=False) still nulls genuine non-numeric
|
||||
# markers like "NA".
|
||||
[
|
||||
pl.col(c).cast(pl.Utf8).str.strip_chars().cast(pl.Float64, strict=False)
|
||||
for c in _RAW_NUMERIC_COLUMNS
|
||||
]
|
||||
)
|
||||
df = df.with_columns(
|
||||
pl.col(c).cast(pl.Utf8, strict=False).str.strip_chars()
|
||||
for c in ("hcpcs", "mod", "description", "status_code")
|
||||
)
|
||||
# Addendum B workbooks append copyright/footnote text in column A
|
||||
# below the data (e.g. "1 CPT codes and descriptors only are
|
||||
# copyright ..."). Real HCPCS/CPT codes are always exactly 5
|
||||
# alphanumeric characters, so that length check both drops blank
|
||||
# rows and these footnotes without needing to special-case them.
|
||||
df = df.filter(
|
||||
pl.col("hcpcs").is_not_null()
|
||||
& pl.col("hcpcs").str.contains(r"^[A-Za-z0-9]{5}$")
|
||||
)
|
||||
|
||||
df = df.select(list(_TARGET_RENAME)).rename(_TARGET_RENAME)
|
||||
df = df.with_columns(pl.lit(cms_rule_id).alias("cms_rule_id"))
|
||||
df = df.select(_TARGET_COLUMNS)
|
||||
|
||||
con.execute("CREATE SCHEMA IF NOT EXISTS pfs")
|
||||
con.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS pfs.rvu_proposed (
|
||||
hcpcs VARCHAR,
|
||||
modifier VARCHAR,
|
||||
description VARCHAR,
|
||||
status_code VARCHAR,
|
||||
work_rvu DOUBLE,
|
||||
nonfac_pe_rvu DOUBLE,
|
||||
fac_pe_rvu DOUBLE,
|
||||
mp_rvu DOUBLE,
|
||||
cms_rule_id VARCHAR
|
||||
)
|
||||
"""
|
||||
)
|
||||
con.execute("DELETE FROM pfs.rvu_proposed WHERE cms_rule_id = ?", [cms_rule_id])
|
||||
con.register("_nprm_df", df)
|
||||
try:
|
||||
con.execute(
|
||||
"INSERT INTO pfs.rvu_proposed SELECT "
|
||||
"hcpcs, modifier, description, status_code, work_rvu, "
|
||||
"nonfac_pe_rvu, fac_pe_rvu, mp_rvu, cms_rule_id FROM _nprm_df"
|
||||
)
|
||||
finally:
|
||||
con.unregister("_nprm_df")
|
||||
|
||||
return {"rows": df.height, "source_file": str(path)}
|
||||
@@ -66,6 +66,71 @@ class CfSegment(BaseModel):
|
||||
(``"CAA 2024 §3201"``, etc.) or derivation note."""
|
||||
|
||||
|
||||
class ProposedRule(BaseModel):
|
||||
"""Parameters as *proposed* in the year's NPRM (pre-final).
|
||||
|
||||
CMS publishes a Notice of Proposed Rulemaking (NPRM) months before
|
||||
the Final Rule; the two can and do differ (comment period,
|
||||
statutory adjustments, and CMS's own re-estimates all move the
|
||||
numbers between proposed and final). This model captures the
|
||||
NPRM's own stated values so they can be compared against the
|
||||
eventual ``RuleYear`` final values — it is never itself used for
|
||||
payment math.
|
||||
|
||||
CY2026 values are sourced from the CY 2026 PFS NPRM (CMS-1832-P,
|
||||
90 FR 32352, July 16, 2025), ingested into Zotero as
|
||||
:pincite:`2KVJ2HKX`."""
|
||||
|
||||
cms_rule_id: str
|
||||
"""CMS internal rule identifier, e.g. ``"CMS-1832-P"`` (the ``-P``
|
||||
suffix denotes "proposed"; the Final Rule reuses the number with
|
||||
``-F``)."""
|
||||
|
||||
fr_document_number: str
|
||||
"""Federal Register document number, e.g. ``"2025-13271"`` — the
|
||||
identifier stamped on the captured full-text file."""
|
||||
|
||||
federal_register_citation: str
|
||||
"""Federal Register citation of the NPRM, e.g. ``"90 FR 32352"``
|
||||
— from the txt header."""
|
||||
|
||||
published: date
|
||||
"""NPRM publication date in the Federal Register."""
|
||||
|
||||
comment_close: date
|
||||
"""End of the public comment period (from the NPRM's DATES
|
||||
section)."""
|
||||
|
||||
conversion_factor: float
|
||||
"""Proposed CY2026 nonqualifying-APM (non-QP) standard conversion
|
||||
factor. See :pincite:`2KVJ2HKX`."""
|
||||
|
||||
cf_qp: float | None = None
|
||||
"""Proposed CY2026 qualifying-APM (QP) standard conversion
|
||||
factor. See :pincite:`2KVJ2HKX`."""
|
||||
|
||||
anesthesia_cf: float | None = None
|
||||
"""Proposed CY2026 nonqualifying-APM (non-QP) anesthesia
|
||||
conversion factor. See :pincite:`2KVJ2HKX`."""
|
||||
|
||||
anesthesia_cf_qp: float | None = None
|
||||
"""Proposed CY2026 qualifying-APM (QP) anesthesia conversion
|
||||
factor. See :pincite:`2KVJ2HKX`."""
|
||||
|
||||
budget_neutrality_adjustor: float = 1.0
|
||||
"""Budget-neutrality adjustment the NPRM applied when deriving
|
||||
the proposed CFs from the CY2025 conversion factor. See
|
||||
:pincite:`2KVJ2HKX`."""
|
||||
|
||||
telehealth_originating_site_fee: float | None = None
|
||||
"""Proposed telehealth originating site facility fee ($) for
|
||||
HCPCS code Q3014. See :pincite:`2KVJ2HKX`."""
|
||||
|
||||
notes: str = ""
|
||||
"""Free-text transcription notes — caveats, values not stated in
|
||||
the NPRM, or discrepancies with the eventual Final Rule."""
|
||||
|
||||
|
||||
class RuleYear(BaseModel):
|
||||
"""Annual PFS rule parameters for a single calendar year."""
|
||||
|
||||
@@ -161,6 +226,14 @@ class RuleYear(BaseModel):
|
||||
federal_register_citation: str = ""
|
||||
"""Federal Register citation (e.g. '89 FR 67668')."""
|
||||
|
||||
proposed: ProposedRule | None = None
|
||||
"""NPRM (proposed-rule) parameters for this year, when captured.
|
||||
|
||||
The fields above this one are the **Final Rule** values and remain
|
||||
authoritative for all payment math — ``proposed`` is informational
|
||||
only, for comparing what CMS proposed against what it finalized.
|
||||
Never read ``proposed.*`` in place of a final field."""
|
||||
|
||||
# ── Derived lookups ───────────────────────────────────────────
|
||||
|
||||
def cf_for_date(self, d: date) -> float:
|
||||
@@ -345,7 +418,36 @@ RULES: dict[int, RuleYear] = {
|
||||
budget_neutrality_adjustor=1.0049, # +0.49% BN adjustment
|
||||
clinical_labor_rate_update=True,
|
||||
telehealth_originating_site_fee=30.38,
|
||||
federal_register_citation="90 FR 101174",
|
||||
# Corrected from "90 FR 101174" (mistranscribed; no such page in
|
||||
# this rule). The Final Rule (RIN 0938-AV50, Vol. 90 No. 212,
|
||||
# Nov 5, 2025) runs 90 FR 49266-50481; per this registry's
|
||||
# convention (see other years above) the citation is the rule's
|
||||
# first page. The CF statement itself ($33.5675 QP / $33.4009
|
||||
# non-QP) is at the pincite 90 FR 49961.
|
||||
federal_register_citation="90 FR 49266",
|
||||
# NPRM values (pre-final) per :pincite:`2KVJ2HKX` — CMS-1832-P,
|
||||
# 90 FR 32352, transcribed from data/fr_downloads/2025-13271.txt.
|
||||
proposed=ProposedRule(
|
||||
cms_rule_id="CMS-1832-P",
|
||||
fr_document_number="2025-13271",
|
||||
federal_register_citation="90 FR 32352",
|
||||
published=date(2025, 7, 16),
|
||||
comment_close=date(2025, 9, 12),
|
||||
conversion_factor=33.4209, # nonqualifying APM standard CF
|
||||
cf_qp=33.5875, # qualifying APM standard CF
|
||||
anesthesia_cf=20.5728, # nonqualifying APM anesthesia CF
|
||||
anesthesia_cf_qp=20.6754, # qualifying APM anesthesia CF
|
||||
budget_neutrality_adjustor=1.0055, # "0.55 percent positive"
|
||||
telehealth_originating_site_fee=31.85, # HCPCS Q3014
|
||||
notes=(
|
||||
"Transcribed from data/fr_downloads/2025-13271.txt "
|
||||
"(NPRM full text) per :pincite:`2KVJ2HKX`. CFs and "
|
||||
"anesthesia CFs at file line ~39205-39220 (90 FR "
|
||||
"32800); telehealth fee at line ~4710 (90 FR 32396); "
|
||||
"comment-close DATES at line ~106 (90 FR 32352). "
|
||||
"Final Rule values differ (see fields above)."
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
446
src/qpp/__init__.py
Normal file
446
src/qpp/__init__.py
Normal file
@@ -0,0 +1,446 @@
|
||||
"""Quality Payment Program (QPP) — QP / Advanced-APM registry.
|
||||
|
||||
Under MACRA 2015 (section 1848(q) and section 1833(z) of the Act),
|
||||
eligible clinicians who participate heavily enough in an Advanced
|
||||
Alternative Payment Model (Advanced APM) become Qualifying APM
|
||||
Participants (QPs) for a "QP Performance Period" and are, as a
|
||||
result, excluded from MIPS reporting and payment adjustments for the
|
||||
corresponding payment year. Clinicians who clear a lower bar become
|
||||
"Partial QPs" and may elect whether to report to MIPS.
|
||||
|
||||
The ``QppYear`` model captures, for a single QP Performance Period,
|
||||
the thresholds and financial-risk standards that govern QP/Partial
|
||||
QP status, the payment-year APM Incentive Payment (where still in
|
||||
effect), and whether the payment year's differential (QP vs. non-QP)
|
||||
Advanced-APM conversion factor applies.
|
||||
|
||||
Performance year vs. payment year
|
||||
----------------------------------
|
||||
|
||||
QP status is determined for a "QP Performance Period" (a calendar
|
||||
year); the resulting MIPS exclusion / APM Incentive Payment /
|
||||
differential CF apply two years later, in the "payment year"::
|
||||
|
||||
payment_year = performance_year + 2
|
||||
|
||||
e.g. QP Performance Period 2024 determines payment-year-2026
|
||||
treatment. ``for_payment_year(2026)`` looks up the ``QppYear`` whose
|
||||
``payment_year`` matches.
|
||||
|
||||
QP conversion-factor split
|
||||
---------------------------
|
||||
|
||||
Section 1848(d)(20) of the Act establishes two separate PFS
|
||||
conversion factors beginning payment year 2026: a "qualifying APM"
|
||||
CF (update +0.75%/yr) for QPs, and a "nonqualifying APM" CF (update
|
||||
+0.25%/yr) for everyone else. ``QppYear.qp_cf_applies`` is ``True``
|
||||
starting with performance year 2024 (whose payment year, 2026, is
|
||||
the first split-CF year) and ``False`` for earlier performance
|
||||
years. See :class:`pfs.rules.RuleYear` (``cf_qp`` /
|
||||
``conversion_factor`` on ``RULES[2026]``) for the actual dollar CFs.
|
||||
|
||||
APM Incentive Payment sunset
|
||||
------------------------------
|
||||
|
||||
The lump-sum APM Incentive Payment (a fixed percentage of a QP's
|
||||
prior-year Part B paid claims) is statutorily phased out: 5% for
|
||||
payment years 2019-2024, 3.5% for payment year 2025 (QP Performance
|
||||
Period 2023), 1.88% for payment year 2026 (QP Performance Period
|
||||
2024) — and payment year 2026 is, under current law, the *last* year
|
||||
these payments are made. ``QppYear.apm_incentive_pct`` is ``None``
|
||||
for QP Performance Periods 2025 and later.
|
||||
|
||||
CMS-1832-P (CY2026 PFS proposed rule, 90 FR 32352, transcribed from
|
||||
``data/fr_downloads/2025-13271.txt``) proposed several QPP
|
||||
methodology changes effective beginning the 2026 QP Performance
|
||||
Period — individual-level QP determinations, an expanded
|
||||
attribution-eligible-beneficiary definition, and sunsetting the
|
||||
Medical Home Model 50-eligible-clinician limit — captured on
|
||||
``QPP[2026].proposed``. It proposed **no change** to the QP /
|
||||
Partial QP percentage thresholds, the generally applicable nominal
|
||||
amount standards, or the CEHRT requirement; see ``proposed.changes``
|
||||
for the sourced disposition, including the Final Rule's correction
|
||||
of an internal inconsistency in the NPRM's own regulatory-impact-
|
||||
analysis text. Ingested into Zotero as :pincite:`2KVJ2HKX`.
|
||||
|
||||
Nominal amount standard sourcing caveat
|
||||
-----------------------------------------
|
||||
|
||||
Neither the CY2026 NPRM nor the CY2026 Final Rule (``2025-19787.txt``)
|
||||
restates the *numeric* generally applicable nominal amount standard
|
||||
(42 CFR 414.1415(c)(3)(i)(A)/(B)) — both only cross-reference the
|
||||
regulation by section number (e.g. NPRM line 26819-26821, Final Rule
|
||||
line 50693-50696), which is itself evidence CMS proposed no change to
|
||||
it. The revenue-based standard, 42 CFR 414.1415(c)(3)(i)(A) (8%), is
|
||||
transcribed from the CY2019 PFS Final Rule
|
||||
(``data/fr_downloads/2018-24170.txt:47248-47252``, 83 FR 59923 —
|
||||
"we are finalizing our proposal to revise Sec. 414.1415(c)(3)(i)(A) to
|
||||
maintain the generally applicable revenue-based nominal amount
|
||||
standard at 8 percent ... for QP Performance Periods 2021 through
|
||||
2024"), already present in this repository's ``fr_downloads`` corpus —
|
||||
commenters asked CMS to raise it for 2025+ and CMS did not finalize an
|
||||
increase, so it is carried forward unchanged. This document also
|
||||
separately restates an **8 percent** standard for the *Other Payer*
|
||||
Advanced APM criterion (42 CFR 414.1420(d)(3)(i), same file, line
|
||||
48189-48192) — a different provision that happens to share the same
|
||||
number; do not conflate the two when re-verifying this citation.
|
||||
|
||||
The benchmark-based standard, 42 CFR 414.1415(c)(3)(i)(B), could
|
||||
**not** be found stated as a bare percentage anywhere in this
|
||||
repository's FR corpus (``2025-13271.txt``, ``2025-19787.txt``,
|
||||
``2018-24170.txt``, and ``2019-16041.txt`` were all searched). The
|
||||
only nearby quoted figure — "at least 3 percent of the expected
|
||||
expenditures" — belongs to the textually distinct Other Payer Advanced
|
||||
APM standard at 42 CFR 414.1420(d)(3)(i)
|
||||
(``2018-24170.txt:48825-48827``, 83 FR 59939), which CMS designed to
|
||||
*align with* the Medicare-option standard but which is not itself a
|
||||
statement of the Medicare-option figure. Rather than reuse that
|
||||
number under a different provision's field, ``benchmark_nominal_pct``
|
||||
is left ``None`` on every entry, with the gap documented in
|
||||
``RiskStandards.notes``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class QpThresholds(BaseModel):
|
||||
"""A pair of QP (or Partial QP) determination thresholds.
|
||||
|
||||
An eligible clinician (or APM Entity) attains status by meeting
|
||||
*either* threshold — they are not additive requirements."""
|
||||
|
||||
payment_amount_pct: float
|
||||
"""Minimum % of Medicare Part B payments for covered professional
|
||||
services that must flow through Advanced APMs."""
|
||||
|
||||
patient_count_pct: float
|
||||
"""Minimum % of Medicare patients that must be attributed through
|
||||
Advanced APMs."""
|
||||
|
||||
|
||||
class RiskStandards(BaseModel):
|
||||
"""The "more than nominal" financial-risk standard an Advanced APM
|
||||
must impose on its APM Entities, per 42 CFR 414.1415(c)(3)(i)."""
|
||||
|
||||
revenue_nominal_pct: float
|
||||
"""Revenue-based nominal amount standard — % of the average
|
||||
estimated total Medicare Parts A and B revenue of all providers
|
||||
and suppliers in participating APM Entities that must be at risk.
|
||||
42 CFR 414.1415(c)(3)(i)(A)."""
|
||||
|
||||
benchmark_nominal_pct: float | None = None
|
||||
"""Expenditure/benchmark-based nominal amount standard — minimum %
|
||||
of expected expenditures the APM Entity must be liable for.
|
||||
42 CFR 414.1415(c)(3)(i)(B). ``None`` when this repository's FR
|
||||
corpus does not state the figure for this specific provision — see
|
||||
module docstring "Nominal amount standard sourcing caveat" and
|
||||
``notes``; do **not** assume it equals the numerically similar but
|
||||
textually distinct Other Payer Advanced APM standard at 42 CFR
|
||||
414.1420(d)(3)(i)."""
|
||||
|
||||
notes: str = ""
|
||||
"""Sourcing caveats — see module docstring "Nominal amount
|
||||
standard sourcing caveat" for why ``revenue_nominal_pct`` is not
|
||||
directly restated as a bare number in the CY2026 NPRM/Final Rule,
|
||||
and why ``benchmark_nominal_pct`` is ``None``."""
|
||||
|
||||
|
||||
class QppProposed(BaseModel):
|
||||
"""QPP parameters as *proposed* in a year's PFS NPRM (pre-final).
|
||||
|
||||
Mirrors :class:`pfs.rules.ProposedRule` — captures the NPRM's own
|
||||
stated QPP changes so they can be compared against the eventual
|
||||
Final Rule disposition. Never itself used for QP/Partial QP
|
||||
determinations; ``changes`` is the authoritative sourced summary
|
||||
of what was proposed *and* finalized."""
|
||||
|
||||
cms_rule_id: str
|
||||
"""CMS internal rule identifier, e.g. ``"CMS-1832-P"``."""
|
||||
|
||||
federal_register_citation: str
|
||||
"""Federal Register citation of the NPRM, e.g. ``"90 FR 32352"``."""
|
||||
|
||||
changes: str
|
||||
"""Sourced prose summary of the QPP-relevant proposals in this
|
||||
NPRM and their Final Rule disposition (finalized as proposed,
|
||||
finalized with modification, not finalized, or "proposed no
|
||||
change to X"), with FR page citations for each claim."""
|
||||
|
||||
qp_thresholds: QpThresholds | None = None
|
||||
"""Proposed QP thresholds, only if the NPRM proposed a numeric
|
||||
change from the current-law values on the enclosing ``QppYear``."""
|
||||
|
||||
partial_qp_thresholds: QpThresholds | None = None
|
||||
"""Proposed Partial QP thresholds, only if the NPRM proposed a
|
||||
numeric change."""
|
||||
|
||||
risk_standards: RiskStandards | None = None
|
||||
"""Proposed nominal amount standards, only if the NPRM proposed a
|
||||
numeric change."""
|
||||
|
||||
|
||||
class QppYear(BaseModel):
|
||||
"""QP / Advanced-APM parameters for a single QP Performance Period.
|
||||
|
||||
Keyed in :data:`QPP` by *performance year*; use
|
||||
:func:`for_payment_year` to look up by the payment year that
|
||||
performance period's determinations affect (``performance_year +
|
||||
2``)."""
|
||||
|
||||
performance_year: int
|
||||
"""The QP Performance Period (calendar year in which Advanced-APM
|
||||
participation is measured)."""
|
||||
|
||||
payment_year: int
|
||||
"""The payment year affected by this performance period's QP
|
||||
determinations — always ``performance_year + 2``."""
|
||||
|
||||
qp_thresholds: QpThresholds
|
||||
"""Current-law QP determination thresholds for this performance
|
||||
period (Medicare Option, generally applicable — section
|
||||
1833(z)(2) of the Act)."""
|
||||
|
||||
partial_qp_thresholds: QpThresholds
|
||||
"""Current-law Partial QP determination thresholds for this
|
||||
performance period."""
|
||||
|
||||
risk_standards: RiskStandards
|
||||
"""Current-law generally applicable nominal amount standard for
|
||||
this performance period."""
|
||||
|
||||
apm_incentive_pct: float | None
|
||||
"""Lump-sum APM Incentive Payment, as a % of the QP's base-year
|
||||
(payment_year - 1) paid claims for covered professional services —
|
||||
``None`` once the statutory sunset takes effect (payment years
|
||||
2027+, i.e. performance years 2025+)."""
|
||||
|
||||
qp_cf_applies: bool
|
||||
"""``True`` once the payment year for this performance period uses
|
||||
the differential QP vs. non-QP conversion factor (payment year
|
||||
2026 onward, i.e. performance year 2024 onward)."""
|
||||
|
||||
cehrt_required: bool = True
|
||||
"""Whether an Advanced APM must require its participants to use
|
||||
Certified EHR Technology (CEHRT) — 42 CFR 414.1415(a)."""
|
||||
|
||||
citation: str = ""
|
||||
"""Federal Register citation this entry's current-law values were
|
||||
transcribed from."""
|
||||
|
||||
proposed: QppProposed | None = None
|
||||
"""NPRM (proposed-rule) QPP changes for this performance period,
|
||||
when captured. Informational only — never used in place of the
|
||||
fields above."""
|
||||
|
||||
|
||||
# ── Annual QPP registry ──────────────────────────────────────────
|
||||
#
|
||||
# QP / Partial QP thresholds: the statutory schedule (section
|
||||
# 1833(z)(2) of the Act) raised the Medicare-option thresholds to 75%
|
||||
# (payment amount) / 50% (patient count) effective QP Performance
|
||||
# Period 2023, with Partial QP thresholds set at the prior period's
|
||||
# QP levels — 50% / 35%. Transcribed from data/fr_downloads/
|
||||
# 2025-13271.txt (NPRM) and cross-checked against data/fr_downloads/
|
||||
# 2025-19787.txt (Final Rule):
|
||||
#
|
||||
# - NPRM 90 FR 32772 (line 36531-36537): "...the QP thresholds have
|
||||
# increased relative to previous years and, effective with the
|
||||
# 2025 QP Performance Period are now at 75 percent for the
|
||||
# payment amount and 50 percent for the patient count."
|
||||
# - NPRM 90 FR 32819 (line 40647-40656, Regulatory Impact Analysis):
|
||||
# "the thresholds to achieve QP status beginning in the 2026 QP
|
||||
# Performance Period ... will increase from 50 percent to 75
|
||||
# percent for the payment amount, and from 35 percent to 50
|
||||
# percent for the patient count." This RIA sentence is internally
|
||||
# inconsistent with the 90 FR 32772 recital above (both appear in
|
||||
# the same NPRM) — it reads as stale boilerplate describing the
|
||||
# pre-2023 -> 2023 statutory step-up, not a change CMS-1832-P
|
||||
# itself proposes.
|
||||
# - Final Rule 90 FR 49980 (line 69796-69798; corrected from a
|
||||
# mistranscribed "90 FR 49928" — that page marker is at line 64990,
|
||||
# these lines fall between the [[Page 49980]] marker at line 69792
|
||||
# and the [[Page 49981]] marker at line 69913) drops the "increase
|
||||
# from" framing entirely: "The thresholds to achieve QP status in
|
||||
# the 2026 QP Performance Period (2028 payment year) are set to
|
||||
# 75 percent for the payment amount, and 50 percent for the
|
||||
# patient count" — consistent with 75/50 being the unchanged,
|
||||
# already-current value, not a new increase.
|
||||
# - Final Rule 90 FR 49928 (line 65021-65026): "...if the proposed
|
||||
# policy would result in an ACO's payment amount score increasing
|
||||
# from 48 percent to 51 percent, that would cross the 50 percent
|
||||
# Partial QP threshold" — confirms Partial QP payment-amount
|
||||
# threshold = 50%. The Partial QP patient-count threshold (35%) is
|
||||
# not independently restated as a bare number in either document;
|
||||
# it is inferred from the same NPRM RIA sentence's "35 percent"
|
||||
# figure (the pre-2023 QP patient-count digit, which by the
|
||||
# statute's step-down design becomes the post-2023 Partial QP
|
||||
# digit) — see module docstring caveat.
|
||||
#
|
||||
# Risk standards: see module docstring "Nominal amount standard
|
||||
# sourcing caveat" — 8% revenue-based (42 CFR 414.1415(c)(3)(i)(A)),
|
||||
# unchanged across 2023-2026 (no proposed or finalized change found in
|
||||
# either CY2026 document). Benchmark-based standard (42 CFR
|
||||
# 414.1415(c)(3)(i)(B)) left None — not stated as a bare number
|
||||
# anywhere in this repository's FR corpus.
|
||||
#
|
||||
# APM incentive %% and qp_cf_applies: transcribed from NPRM 90 FR
|
||||
# 32818 / Final Rule 90 FR 49979-49980 ("b. APM Incentive Payments to
|
||||
# QPs in Advanced APMs and Other Payer Advanced APMs"), and the
|
||||
# sunset statement at NPRM 90 FR 32696 / Final Rule 90 FR 49838
|
||||
# ("Under current law, payment year 2026 is the last year for these
|
||||
# payments.").
|
||||
|
||||
_RISK_NOTES = (
|
||||
"Revenue-based 8% per CY2019 PFS Final Rule (83 FR 59923, "
|
||||
"data/fr_downloads/2018-24170.txt:47248-47252) 'to maintain the "
|
||||
"generally applicable revenue-based nominal amount standard at 8 "
|
||||
"percent ... for QP Performance Periods 2021 through 2024' (Sec. "
|
||||
"414.1415(c)(3)(i)(A)); not extended or amended in either CY2026 "
|
||||
"document (no proposed/final change found) — carried forward. "
|
||||
"(This document separately restates 8% for the *different* Other "
|
||||
"Payer Advanced APM standard, Sec. 414.1420(d)(3)(i), at line "
|
||||
"48189-48192 — do not conflate the two.) "
|
||||
"Benchmark-based standard (Sec. 414.1415(c)(3)(i)(B)) is left "
|
||||
"None: not stated as a bare percentage anywhere in this "
|
||||
"repository's FR corpus (2025-13271.txt, 2025-19787.txt, "
|
||||
"2018-24170.txt, 2019-16041.txt all searched). The only nearby "
|
||||
"quoted figure, 'at least 3 percent of the expected expenditures' "
|
||||
"(2018-24170.txt:48825-48827, 83 FR 59939), is for the textually "
|
||||
"distinct Other Payer Advanced APM standard at Sec. "
|
||||
"414.1420(d)(3)(i), not this provision."
|
||||
)
|
||||
|
||||
|
||||
def _risk_standards() -> RiskStandards:
|
||||
return RiskStandards(
|
||||
revenue_nominal_pct=8.0,
|
||||
benchmark_nominal_pct=None,
|
||||
notes=_RISK_NOTES,
|
||||
)
|
||||
|
||||
|
||||
def _qp_thresholds() -> QpThresholds:
|
||||
return QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0)
|
||||
|
||||
|
||||
def _partial_qp_thresholds() -> QpThresholds:
|
||||
return QpThresholds(payment_amount_pct=50.0, patient_count_pct=35.0)
|
||||
|
||||
|
||||
QPP: dict[int, QppYear] = {
|
||||
2023: QppYear(
|
||||
performance_year=2023,
|
||||
payment_year=2025,
|
||||
qp_thresholds=_qp_thresholds(),
|
||||
partial_qp_thresholds=_partial_qp_thresholds(),
|
||||
risk_standards=_risk_standards(),
|
||||
apm_incentive_pct=3.5,
|
||||
qp_cf_applies=False,
|
||||
citation="90 FR 49979-49980",
|
||||
),
|
||||
2024: QppYear(
|
||||
# Payment year 2026 is the first year with a differential
|
||||
# QP/non-QP conversion factor (section 1848(d)(20) of the
|
||||
# Act) AND still receives the (final year of) lump-sum APM
|
||||
# Incentive Payment — see NPRM 90 FR 32818, Final Rule 90 FR
|
||||
# 49979-49980.
|
||||
performance_year=2024,
|
||||
payment_year=2026,
|
||||
qp_thresholds=_qp_thresholds(),
|
||||
partial_qp_thresholds=_partial_qp_thresholds(),
|
||||
risk_standards=_risk_standards(),
|
||||
apm_incentive_pct=1.88,
|
||||
qp_cf_applies=True,
|
||||
citation="90 FR 49979-49980",
|
||||
),
|
||||
2025: QppYear(
|
||||
# Under current law, payment year 2026 (performance year
|
||||
# 2024) was the last year with a lump-sum APM Incentive
|
||||
# Payment — NPRM 90 FR 32696, Final Rule 90 FR 49838.
|
||||
performance_year=2025,
|
||||
payment_year=2027,
|
||||
qp_thresholds=_qp_thresholds(),
|
||||
partial_qp_thresholds=_partial_qp_thresholds(),
|
||||
risk_standards=_risk_standards(),
|
||||
apm_incentive_pct=None,
|
||||
qp_cf_applies=True,
|
||||
citation="90 FR 49838",
|
||||
),
|
||||
2026: QppYear(
|
||||
performance_year=2026,
|
||||
payment_year=2028,
|
||||
qp_thresholds=_qp_thresholds(),
|
||||
partial_qp_thresholds=_partial_qp_thresholds(),
|
||||
risk_standards=_risk_standards(),
|
||||
apm_incentive_pct=None,
|
||||
qp_cf_applies=True,
|
||||
# Corrected from "90 FR 49928" alone: the QP threshold statement
|
||||
# itself is at 90 FR 49980 (see docstring pincite above); 49928
|
||||
# is retained alongside it since it's independently the source
|
||||
# for the Partial QP payment-amount confirmation (docstring
|
||||
# pincite, line 65021-65026).
|
||||
citation="90 FR 49928, 49980",
|
||||
# NPRM proposals per :pincite:`2KVJ2HKX` — CMS-1832-P, 90 FR
|
||||
# 32352, transcribed from data/fr_downloads/2025-13271.txt;
|
||||
# disposition from data/fr_downloads/2025-19787.txt.
|
||||
proposed=QppProposed(
|
||||
cms_rule_id="CMS-1832-P",
|
||||
federal_register_citation="90 FR 32352",
|
||||
changes=(
|
||||
"Individual-level QP determination: proposed a new "
|
||||
"Threshold Score calculation at the individual "
|
||||
"eligible-clinician level (Sec. 414.1425(b)(3)) for "
|
||||
"all Advanced APM participants beginning with the "
|
||||
"2026 QP Performance Period, alongside the existing "
|
||||
"APM Entity-level calculation (NPRM 90 FR 32771). "
|
||||
"Finalized as proposed (Final Rule 90 FR 49923-49928, "
|
||||
"49980). "
|
||||
"Attribution-eligible beneficiary definition: "
|
||||
"proposed to re-expand the sixth criterion at Sec. "
|
||||
"414.1305 to include any beneficiary with a claim for "
|
||||
"any covered professional service (not just E/M "
|
||||
"services), beginning with the 2026 QP Performance "
|
||||
"Period (NPRM 90 FR 32773) — a re-proposal of a CY2025 "
|
||||
"proposal CMS did not finalize. Finalized WITH "
|
||||
"MODIFICATION: CMS adopted a dual methodology that "
|
||||
"continues to use E/M services alongside a covered- "
|
||||
"professional-services methodology, rather than "
|
||||
"replacing E/M outright (Final Rule 90 FR 49980). "
|
||||
"Medical Home Model 50-eligible-clinician limit: "
|
||||
"proposed to sunset Sec. 414.1415(c)(7) (and the "
|
||||
"parallel Sec. 414.1420(d)(8) for Aligned Other Payer/"
|
||||
"Medicaid Medical Home Models) so the 50-clinician cap "
|
||||
"applies only through the 2025 QP Performance Period, "
|
||||
"beginning with the 2026 QP Performance Period (NPRM "
|
||||
"90 FR 32773). Finalized as proposed (Final Rule 90 FR "
|
||||
"49929: 'finalizing this policy as proposed'). "
|
||||
"QP/Partial QP thresholds: proposed no numeric change "
|
||||
"— thresholds remain 75%/50% (QP) and 50%/35% "
|
||||
"(Partial QP); see QPP[2026].qp_thresholds and the "
|
||||
"module docstring re: the NPRM's internally "
|
||||
"inconsistent RIA framing, corrected in the Final "
|
||||
"Rule (NPRM 90 FR 32772 vs 90 FR 32819; Final Rule 90 "
|
||||
"FR 49928). "
|
||||
"Nominal amount standards (42 CFR "
|
||||
"414.1415(c)(3)(i)(A)/(B)) and the CEHRT requirement "
|
||||
"(Sec. 414.1415(a)(i)): proposed no change — neither "
|
||||
"document restates or amends these provisions beyond "
|
||||
"cross-referencing the section numbers (NPRM/Final "
|
||||
"26819-26821 and 50693-50696 respectively)."
|
||||
),
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def for_payment_year(year: int) -> QppYear:
|
||||
"""Return the :class:`QppYear` whose ``payment_year`` matches.
|
||||
|
||||
Raises ``KeyError`` if no performance period in :data:`QPP` maps
|
||||
to this payment year."""
|
||||
try:
|
||||
return next(q for q in QPP.values() if q.payment_year == year)
|
||||
except StopIteration:
|
||||
raise KeyError(year) from None
|
||||
@@ -141,12 +141,18 @@ CLASSIFY_TOOL = LLMTool(
|
||||
},
|
||||
)
|
||||
|
||||
# Docket ↔ CMS rule identity for the dockets this classifier targets.
|
||||
# Verified against the regulations.gov docket abstract (2026-08-12):
|
||||
# CMS-2025-0304 is the CY2026 *PFS* NPRM, not the OPPS rule — skin-substitute
|
||||
# payment moved into the PFS rule in CY2026.
|
||||
DOCKET_RULES: dict[str, str] = {"CMS-2025-0304": "CMS-1832-P"}
|
||||
|
||||
_SYSTEM = """\
|
||||
You classify public comments submitted on CMS's CY2026 OPPS proposed rule
|
||||
(CMS-1834-P, docket CMS-2025-0304), specifically regarding the proposal to
|
||||
You classify public comments submitted on CMS's CY2026 PFS proposed rule
|
||||
(CMS-1832-P, docket CMS-2025-0304), specifically regarding the proposal to
|
||||
reclassify skin substitutes (cellular and tissue-based products, CTPs) from
|
||||
drugs/biologicals paid at ASP+6% to incident-to supplies paid a flat packaged
|
||||
rate (~$127.28/cm²), replacing Q-codes with C5271–C5278.
|
||||
rate, replacing Q-codes with C5271–C5278.
|
||||
|
||||
Position semantics:
|
||||
- strongly_oppose: wants ASP+6% preserved; rejects reclassification outright
|
||||
|
||||
@@ -71,6 +71,35 @@ class TestIndexCorpus:
|
||||
assert "indexed=3 skipped=1 chunks=7" in result.output
|
||||
|
||||
|
||||
class TestIndexRules:
|
||||
@patch("llm.source.iter_rule_docs")
|
||||
@patch("llm.pool.HostPool.from_config")
|
||||
@patch("llm.index.index_docs")
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
def test_rules_collection(
|
||||
self, mock_load, mock_bib, mock_index_docs, mock_from_config, mock_iter
|
||||
):
|
||||
cfg = MagicMock()
|
||||
mock_load.return_value = cfg
|
||||
store = MagicMock()
|
||||
mock_bib.return_value = store
|
||||
mock_iter.return_value = iter(["doc1"])
|
||||
mock_from_config.return_value = MagicMock()
|
||||
mock_index_docs.return_value = _STATS
|
||||
|
||||
result = runner.invoke(
|
||||
app,
|
||||
["index", "--collection", "rules", "--key", "abc123", "--key", "def456"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0
|
||||
mock_iter.assert_called_once_with(store, keys=("abc123", "def456"))
|
||||
kwargs = mock_index_docs.call_args.kwargs
|
||||
assert kwargs["collection"] == "rules"
|
||||
assert "indexed=3 skipped=1 chunks=7" in result.output
|
||||
|
||||
|
||||
class TestIndexBadCollection:
|
||||
@patch("conf.connect.bib")
|
||||
@patch("llm.config.load")
|
||||
@@ -81,7 +110,7 @@ class TestIndexBadCollection:
|
||||
result = runner.invoke(app, ["index", "--collection", "bogus"])
|
||||
|
||||
assert result.exit_code == 2
|
||||
assert "collection must be 'comments' or 'corpus'" in result.output
|
||||
assert "collection must be 'comments', 'corpus' or 'rules'" in result.output
|
||||
|
||||
|
||||
class TestIndexLimit:
|
||||
|
||||
34
tests/cms/test_ingest_log.py
Normal file
34
tests/cms/test_ingest_log.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Tests for cms.ingest_log — provenance table writer."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import duckdb
|
||||
|
||||
from cms import ingest_log
|
||||
|
||||
|
||||
def test_log_ingest_appends_row(tmp_path):
|
||||
src = tmp_path / "f.csv"
|
||||
src.write_text("a,b\n1,2\n")
|
||||
con = duckdb.connect()
|
||||
ingest_log.log_ingest(
|
||||
con,
|
||||
module="pfs",
|
||||
table_name="pfs.rvu_proposed",
|
||||
rows=2,
|
||||
source_file=str(src),
|
||||
rule_id="CMS-1832-P",
|
||||
fr_citation="90 FR 1",
|
||||
pincite_key="2KVJ2HKX",
|
||||
run_id=ingest_log.new_run_id(),
|
||||
)
|
||||
row = con.execute(
|
||||
"SELECT module, table_name, rows, rule_id, length(sha256) FROM cms.ingest_log"
|
||||
).fetchone()
|
||||
assert row == ("pfs", "pfs.rvu_proposed", 2, "CMS-1832-P", 64)
|
||||
|
||||
|
||||
def test_missing_source_file_logs_empty_hash():
|
||||
con = duckdb.connect()
|
||||
ingest_log.log_ingest(con, module="pfs", table_name="t", rows=0)
|
||||
assert con.execute("SELECT sha256 FROM cms.ingest_log").fetchone()[0] == ""
|
||||
@@ -1,14 +1,17 @@
|
||||
"""llm.source — comment + corpus Doc iterators."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from bib.item import Item
|
||||
from bib.item import Item, Rule
|
||||
from bib.store import Store
|
||||
from llm.source import (
|
||||
_default_root,
|
||||
comment_key_map,
|
||||
iter_comment_docs,
|
||||
iter_corpus_docs,
|
||||
iter_rule_docs,
|
||||
)
|
||||
|
||||
DOCKET = "CMS-2019-0111"
|
||||
@@ -104,6 +107,101 @@ class TestCorpusDocs:
|
||||
assert key not in {d.key for d in iter_corpus_docs(store)}
|
||||
|
||||
|
||||
class TestRuleDocs:
|
||||
@pytest.fixture
|
||||
def rule_key(self, store):
|
||||
key = store.create(
|
||||
Rule(
|
||||
title="CY2026 PFS Proposed Rule",
|
||||
document_number="2025-13271",
|
||||
)
|
||||
)
|
||||
for tag in ("cms-rule:CMS-1832-P", "year:2026"):
|
||||
store.add_tag(key, tag)
|
||||
return key
|
||||
|
||||
def test_txt_attachment_yields_doc_with_metadata(self, store, rule_key, tmp_path):
|
||||
txt = tmp_path / "2025-13271.txt"
|
||||
txt.write_text(
|
||||
"<html><head><title>x</title></head><body><pre>\n"
|
||||
"The Secretary proposes to amend 42 CFR part 414.\n"
|
||||
"</pre></body></html>"
|
||||
)
|
||||
store.attach_file(rule_key, txt)
|
||||
|
||||
docs = list(iter_rule_docs(store))
|
||||
|
||||
assert len(docs) == 1
|
||||
doc = docs[0]
|
||||
assert doc.key == rule_key
|
||||
assert "The Secretary proposes to amend 42 CFR part 414." in doc.text
|
||||
assert "<html>" not in doc.text
|
||||
assert "<pre>" not in doc.text
|
||||
assert doc.metadata == {
|
||||
"doctype": "rule",
|
||||
"cms_rule_id": "CMS-1832-P",
|
||||
"fr_document_number": "2025-13271",
|
||||
"year": "2026",
|
||||
"item_key": rule_key,
|
||||
}
|
||||
|
||||
def test_inline_html_and_entities_stripped(self, store, rule_key, tmp_path):
|
||||
txt = tmp_path / "2025-13271.txt"
|
||||
txt.write_text(
|
||||
"<html><head><title>x</title></head><body><pre>\n"
|
||||
"[[Page 12345]]\n"
|
||||
'Contact us at <a href="/cdn-cgi/l/email-protection#x">'
|
||||
"someone</a> regarding CMS & PFS.\n"
|
||||
"A finding was significant (p<0.05 and >2 cm).\n"
|
||||
"</pre></body></html>"
|
||||
)
|
||||
store.attach_file(rule_key, txt)
|
||||
|
||||
docs = list(iter_rule_docs(store))
|
||||
|
||||
assert len(docs) == 1
|
||||
text = docs[0].text
|
||||
assert "<a href=" not in text
|
||||
assert "</a>" not in text
|
||||
assert "someone" in text
|
||||
assert "CMS & PFS" in text
|
||||
assert "[[Page 12345]]" in text
|
||||
assert "p<0.05 and >2 cm" in text
|
||||
|
||||
def test_pdf_only_falls_back_to_extract_attachment(self, store, rule_key, tmp_path):
|
||||
pdf = tmp_path / "rule.pdf"
|
||||
pdf.write_bytes(b"%PDF-1.4 fake")
|
||||
store.attach_file(rule_key, pdf)
|
||||
|
||||
with patch("rex.comments.combine.extract_attachment") as mock_extract:
|
||||
from rex.comments.extract import ExtractResult
|
||||
|
||||
mock_extract.return_value = ExtractResult(
|
||||
text="Extracted PDF body.", status="ok", chars=20
|
||||
)
|
||||
docs = list(iter_rule_docs(store))
|
||||
|
||||
assert len(docs) == 1
|
||||
assert docs[0].text == "Extracted PDF body."
|
||||
|
||||
def test_keys_filters_to_named_items(self, store, rule_key, tmp_path):
|
||||
other_key = store.create(Rule(title="Other rule", document_number="2025-00001"))
|
||||
store.add_tag(other_key, "cms-rule:CMS-9999-P")
|
||||
for key in (rule_key, other_key):
|
||||
txt = tmp_path / f"{key}.txt"
|
||||
txt.write_text("<pre>Some rule text.</pre>")
|
||||
store.attach_file(key, txt)
|
||||
|
||||
docs = list(iter_rule_docs(store, keys=(rule_key,)))
|
||||
|
||||
assert [d.key for d in docs] == [rule_key]
|
||||
|
||||
def test_non_rule_items_not_yielded(self, store):
|
||||
assert all(d.metadata["doctype"] == "rule" for d in iter_rule_docs(store))
|
||||
# the "report" comment item from the `store` fixture is never yielded
|
||||
assert store._comment_key not in {d.key for d in iter_rule_docs(store)}
|
||||
|
||||
|
||||
class TestDefaultRoot:
|
||||
def test_default_root_under_state_comments(self):
|
||||
from conf import ROOT
|
||||
|
||||
92
tests/pfs/test_nprm.py
Normal file
92
tests/pfs/test_nprm.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Tests for pfs.nprm — NPRM Addendum B loader → pfs.rvu_proposed (#592)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import duckdb
|
||||
|
||||
from pfs import nprm
|
||||
|
||||
|
||||
def _fixture(tmp_path):
|
||||
p = tmp_path / "CY2026_NPRM_Addendum_B.csv"
|
||||
p.write_text(
|
||||
"HCPCS,MOD,DESCRIPTION,STATUS CODE,WORK RVU,"
|
||||
"NON-FAC PE RVU,FAC PE RVU,MP RVU\n"
|
||||
"99213,,Office visit est,A,1.3,1.5,0.55,0.1\n"
|
||||
"0001A,,Admin covid,X,0.0,0.0,0.0,0.0\n"
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
def test_load_rvu_proposed_loads_and_reloads(tmp_path, monkeypatch):
|
||||
path = _fixture(tmp_path)
|
||||
monkeypatch.setattr(nprm, "_discover_addendum_b", lambda: path)
|
||||
con = duckdb.connect()
|
||||
out = nprm.load_rvu_proposed(con)
|
||||
assert out["rows"] == 2
|
||||
row = con.execute(
|
||||
"SELECT hcpcs, work_rvu, cms_rule_id FROM pfs.rvu_proposed WHERE hcpcs='99213'"
|
||||
).fetchone()
|
||||
assert row == ("99213", 1.3, "CMS-1832-P")
|
||||
nprm.load_rvu_proposed(con) # idempotent delete-and-reload
|
||||
assert con.execute("SELECT count(*) FROM pfs.rvu_proposed").fetchone()[0] == 2
|
||||
|
||||
|
||||
def _dirty_fixture(tmp_path):
|
||||
"""Real-file quirks: padded cells, "NA" markers, a footnote row.
|
||||
|
||||
Mirrors what the actual CY2026 Addendum B workbook contains — this
|
||||
combination broke silently on the real file (see task-5-report.md)
|
||||
and was fixed via manual smoke-testing rather than a fixture test,
|
||||
so these rows lock the fix in.
|
||||
"""
|
||||
p = tmp_path / "CY2026_NPRM_Addendum_B_dirty.csv"
|
||||
p.write_text(
|
||||
"HCPCS,MOD,DESCRIPTION,STATUS CODE,WORK RVU,"
|
||||
"NON-FAC PE RVU,FAC PE RVU,MP RVU\n"
|
||||
" 99213 , ,Office visit est,A, 1.30 , 1.50 , 0.55 , 0.10 \n"
|
||||
"99214,,Office visit NA test,A,NA,NA,NA,NA\n"
|
||||
"1 CPT codes and descriptors only are copyright 2025 American "
|
||||
"Medical Association. All Rights Reserved.,,,,,,,\n"
|
||||
)
|
||||
return p
|
||||
|
||||
|
||||
def test_load_rvu_proposed_strips_padding(tmp_path, monkeypatch):
|
||||
"""Whitespace-padded HCPCS/numeric cells load clean, not null."""
|
||||
path = _dirty_fixture(tmp_path)
|
||||
monkeypatch.setattr(nprm, "_discover_addendum_b", lambda: path)
|
||||
con = duckdb.connect()
|
||||
nprm.load_rvu_proposed(con)
|
||||
row = con.execute(
|
||||
"SELECT hcpcs, description, work_rvu, nonfac_pe_rvu, fac_pe_rvu, mp_rvu "
|
||||
"FROM pfs.rvu_proposed WHERE hcpcs='99213'"
|
||||
).fetchone()
|
||||
assert row == ("99213", "Office visit est", 1.3, 1.5, 0.55, 0.1)
|
||||
|
||||
|
||||
def test_load_rvu_proposed_na_marker_becomes_null(tmp_path, monkeypatch):
|
||||
""" "NA" in a numeric cell loads as SQL NULL, not a cast error."""
|
||||
path = _dirty_fixture(tmp_path)
|
||||
monkeypatch.setattr(nprm, "_discover_addendum_b", lambda: path)
|
||||
con = duckdb.connect()
|
||||
nprm.load_rvu_proposed(con)
|
||||
row = con.execute(
|
||||
"SELECT work_rvu, nonfac_pe_rvu, fac_pe_rvu, mp_rvu "
|
||||
"FROM pfs.rvu_proposed WHERE hcpcs='99214'"
|
||||
).fetchone()
|
||||
assert row == (None, None, None, None)
|
||||
|
||||
|
||||
def test_load_rvu_proposed_drops_footnote_rows(tmp_path, monkeypatch):
|
||||
"""A footnote row (first cell = long sentence) is not a HCPCS code."""
|
||||
path = _dirty_fixture(tmp_path)
|
||||
monkeypatch.setattr(nprm, "_discover_addendum_b", lambda: path)
|
||||
con = duckdb.connect()
|
||||
out = nprm.load_rvu_proposed(con)
|
||||
assert out["rows"] == 2 # only 99213 and 99214 — footnote row excluded
|
||||
assert con.execute("SELECT count(*) FROM pfs.rvu_proposed").fetchone()[0] == 2
|
||||
footnote = con.execute(
|
||||
"SELECT count(*) FROM pfs.rvu_proposed WHERE hcpcs LIKE '1 CPT%'"
|
||||
).fetchone()[0]
|
||||
assert footnote == 0
|
||||
@@ -489,7 +489,22 @@ class TestLoadZipCarrierEdges:
|
||||
p.write_text(line)
|
||||
f = _f("ZIP5_2024.txt", 2024, ".txt", path=str(p))
|
||||
|
||||
# Patch pl.DataFrame to drop year_quarter before the if-check
|
||||
# Patch pl.DataFrame to drop year_quarter before the if-check.
|
||||
#
|
||||
# IMPORTANT: patch the module *binding* pfs.pipe.pl, not the
|
||||
# attribute pfs.pipe.pl.DataFrame. `pl` inside pfs.pipe is the
|
||||
# real `polars` module object, so `patch("pfs.pipe.pl.DataFrame")`
|
||||
# would replace `polars.DataFrame` globally (process-wide) for
|
||||
# the duration of the `with` block. If duckdb resolves its
|
||||
# Python replacement-scan type for polars DataFrames for the
|
||||
# first time while that global is a MagicMock, it caches the
|
||||
# MagicMock as "the polars DataFrame type" for the rest of the
|
||||
# process, and every later `SELECT * FROM df` scan anywhere in
|
||||
# the suite fails with
|
||||
# "TypeError: isinstance() arg 2 must be a type, a tuple of
|
||||
# types, or a union". Using a standalone proxy module for the
|
||||
# `pl` name (with the real polars attributes copied in) keeps
|
||||
# the real `polars.DataFrame` class untouched.
|
||||
_orig_df = pl.DataFrame
|
||||
|
||||
def _df_no_yq(rows):
|
||||
@@ -498,7 +513,11 @@ class TestLoadZipCarrierEdges:
|
||||
df = df.drop("year_quarter")
|
||||
return df
|
||||
|
||||
with patch("pfs.pipe.pl.DataFrame", side_effect=_df_no_yq):
|
||||
fake_pl = types.ModuleType("fake_polars_proxy")
|
||||
fake_pl.__dict__.update(pl.__dict__)
|
||||
fake_pl.DataFrame = _df_no_yq
|
||||
|
||||
with patch("pfs.pipe.pl", fake_pl):
|
||||
result = _load_zip_carrier_files([f], con)
|
||||
assert result["rows"] >= 1
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from pfs.rules import RULES, RuleYear, cy2024, cy2025, cy2026
|
||||
from pfs.rules import RULES, ProposedRule, RuleYear, cy2024, cy2025, cy2026
|
||||
|
||||
# ── RuleYear model ───────────────────────────────────────────────
|
||||
|
||||
@@ -149,7 +149,7 @@ class TestSpecificValues:
|
||||
assert cy2026.telehealth_originating_site_fee == 30.38
|
||||
|
||||
def test_cy2026_federal_register(self):
|
||||
assert cy2026.federal_register_citation == "90 FR 101174"
|
||||
assert cy2026.federal_register_citation == "90 FR 49266"
|
||||
|
||||
def test_cy2026_clinical_labor_update(self):
|
||||
assert cy2026.clinical_labor_rate_update is True
|
||||
@@ -232,3 +232,67 @@ class TestCfForDate:
|
||||
)
|
||||
with pytest.raises(ValueError, match="outside every CF segment"):
|
||||
ry.cf_for_date(date(2024, 1, 15))
|
||||
|
||||
|
||||
# ── ProposedRule — CY2026 NPRM parameters ────────────────────────
|
||||
|
||||
|
||||
class TestProposedRule:
|
||||
"""ProposedRule model and RULES[2026].proposed values."""
|
||||
|
||||
def test_cy2026_has_proposed_rule(self):
|
||||
p = RULES[2026].proposed
|
||||
assert p is not None
|
||||
assert p.cms_rule_id == "CMS-1832-P"
|
||||
assert p.fr_document_number == "2025-13271"
|
||||
assert p.published.year == 2025
|
||||
assert p.comment_close > p.published
|
||||
# exact transcribed values — from data/fr_downloads/2025-13271.txt
|
||||
assert p.conversion_factor == 33.4209
|
||||
assert p.cf_qp == 33.5875
|
||||
assert p.conversion_factor != RULES[2026].conversion_factor # proposed ≠ final
|
||||
|
||||
def test_cy2026_proposed_anesthesia_cfs(self):
|
||||
p = RULES[2026].proposed
|
||||
assert p.anesthesia_cf == 20.5728
|
||||
assert p.anesthesia_cf_qp == 20.6754
|
||||
|
||||
def test_cy2026_proposed_budget_neutrality_adjustor(self):
|
||||
assert RULES[2026].proposed.budget_neutrality_adjustor == 1.0055
|
||||
|
||||
def test_cy2026_proposed_telehealth_fee(self):
|
||||
assert RULES[2026].proposed.telehealth_originating_site_fee == 31.85
|
||||
|
||||
def test_cy2026_proposed_federal_register_citation(self):
|
||||
assert RULES[2026].proposed.federal_register_citation == "90 FR 32352"
|
||||
|
||||
def test_cy2026_proposed_published_date(self):
|
||||
from datetime import date
|
||||
|
||||
assert RULES[2026].proposed.published == date(2025, 7, 16)
|
||||
|
||||
def test_cy2026_proposed_comment_close_date(self):
|
||||
from datetime import date
|
||||
|
||||
assert RULES[2026].proposed.comment_close == date(2025, 9, 12)
|
||||
|
||||
def test_pre_2026_years_have_no_proposed_block(self):
|
||||
assert RULES[2025].proposed is None
|
||||
|
||||
def test_proposed_rule_default_notes(self):
|
||||
from datetime import date
|
||||
|
||||
p = ProposedRule(
|
||||
cms_rule_id="CMS-0000-P",
|
||||
fr_document_number="2099-00000",
|
||||
federal_register_citation="99 FR 1",
|
||||
published=date(2099, 1, 1),
|
||||
comment_close=date(2099, 3, 1),
|
||||
conversion_factor=1.0,
|
||||
)
|
||||
assert p.notes == ""
|
||||
assert p.cf_qp is None
|
||||
assert p.anesthesia_cf is None
|
||||
assert p.anesthesia_cf_qp is None
|
||||
assert p.budget_neutrality_adjustor == 1.0
|
||||
assert p.telehealth_originating_site_fee is None
|
||||
|
||||
0
tests/qpp/__init__.py
Normal file
0
tests/qpp/__init__.py
Normal file
240
tests/qpp/test_rules.py
Normal file
240
tests/qpp/test_rules.py
Normal file
@@ -0,0 +1,240 @@
|
||||
"""Tests for qpp — QP / Advanced-APM registry.
|
||||
|
||||
Covers src/qpp/__init__.py:
|
||||
- QpThresholds / RiskStandards / QppProposed model construction
|
||||
- QppYear model construction with required and default fields
|
||||
- QPP dict: performance years 2023-2026, payment_year = performance_year + 2
|
||||
- qp_cf_applies: False before performance year 2024, True from 2024 onward
|
||||
- Every entry has a non-empty citation
|
||||
- QPP[2026].proposed is CMS-1832-P
|
||||
- for_payment_year(year) lookup, including KeyError on miss
|
||||
- Exact-value asserts for transcribed thresholds (see task-6-report.md)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from qpp import QPP, QppProposed, QppYear, QpThresholds, RiskStandards, for_payment_year
|
||||
|
||||
# ── QpThresholds model ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestQpThresholds:
|
||||
def test_construction(self):
|
||||
t = QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0)
|
||||
assert t.payment_amount_pct == 75.0
|
||||
assert t.patient_count_pct == 50.0
|
||||
|
||||
|
||||
# ── RiskStandards model ──────────────────────────────────────────
|
||||
|
||||
|
||||
class TestRiskStandards:
|
||||
def test_construction(self):
|
||||
r = RiskStandards(revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0)
|
||||
assert r.revenue_nominal_pct == 8.0
|
||||
assert r.benchmark_nominal_pct == 3.0
|
||||
|
||||
def test_default_notes(self):
|
||||
r = RiskStandards(revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0)
|
||||
assert r.notes == ""
|
||||
|
||||
def test_default_benchmark_nominal_pct(self):
|
||||
r = RiskStandards(revenue_nominal_pct=8.0)
|
||||
assert r.benchmark_nominal_pct is None
|
||||
|
||||
|
||||
# ── QppProposed model ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestQppProposed:
|
||||
def test_required_fields(self):
|
||||
p = QppProposed(
|
||||
cms_rule_id="CMS-0000-P",
|
||||
federal_register_citation="99 FR 1",
|
||||
changes="proposed no change to anything",
|
||||
)
|
||||
assert p.cms_rule_id == "CMS-0000-P"
|
||||
assert p.changes == "proposed no change to anything"
|
||||
|
||||
def test_default_optional_blocks(self):
|
||||
p = QppProposed(
|
||||
cms_rule_id="CMS-0000-P",
|
||||
federal_register_citation="99 FR 1",
|
||||
changes="no change",
|
||||
)
|
||||
assert p.qp_thresholds is None
|
||||
assert p.partial_qp_thresholds is None
|
||||
assert p.risk_standards is None
|
||||
|
||||
|
||||
# ── QppYear model ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestQppYear:
|
||||
def _minimal_kwargs(self):
|
||||
return dict(
|
||||
performance_year=2099,
|
||||
payment_year=2101,
|
||||
qp_thresholds=QpThresholds(payment_amount_pct=75.0, patient_count_pct=50.0),
|
||||
partial_qp_thresholds=QpThresholds(
|
||||
payment_amount_pct=50.0, patient_count_pct=35.0
|
||||
),
|
||||
risk_standards=RiskStandards(
|
||||
revenue_nominal_pct=8.0, benchmark_nominal_pct=3.0
|
||||
),
|
||||
apm_incentive_pct=None,
|
||||
qp_cf_applies=True,
|
||||
)
|
||||
|
||||
def test_required_fields(self):
|
||||
y = QppYear(**self._minimal_kwargs())
|
||||
assert y.performance_year == 2099
|
||||
assert y.payment_year == 2101
|
||||
|
||||
def test_default_cehrt_required(self):
|
||||
y = QppYear(**self._minimal_kwargs())
|
||||
assert y.cehrt_required is True
|
||||
|
||||
def test_default_citation(self):
|
||||
y = QppYear(**self._minimal_kwargs())
|
||||
assert y.citation == ""
|
||||
|
||||
def test_default_proposed(self):
|
||||
y = QppYear(**self._minimal_kwargs())
|
||||
assert y.proposed is None
|
||||
|
||||
|
||||
# ── QPP dict structure ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestQppDict:
|
||||
EXPECTED_YEARS = [2023, 2024, 2025, 2026]
|
||||
|
||||
def test_count(self):
|
||||
assert len(QPP) == 4
|
||||
|
||||
def test_keys(self):
|
||||
assert sorted(QPP.keys()) == self.EXPECTED_YEARS
|
||||
|
||||
@pytest.mark.parametrize("year", EXPECTED_YEARS)
|
||||
def test_values_are_qppyear(self, year):
|
||||
assert isinstance(QPP[year], QppYear)
|
||||
|
||||
@pytest.mark.parametrize("year", EXPECTED_YEARS)
|
||||
def test_performance_year_field_matches_key(self, year):
|
||||
assert QPP[year].performance_year == year
|
||||
|
||||
@pytest.mark.parametrize("year", EXPECTED_YEARS)
|
||||
def test_payment_year_is_performance_year_plus_2(self, year):
|
||||
assert QPP[year].payment_year == year + 2
|
||||
|
||||
@pytest.mark.parametrize("year", EXPECTED_YEARS)
|
||||
def test_citation_non_empty(self, year):
|
||||
assert QPP[year].citation != ""
|
||||
|
||||
|
||||
class TestQpCfApplies:
|
||||
"""payment year 2026 (performance year 2024) is the first split-CF year."""
|
||||
|
||||
def test_2023_false(self):
|
||||
assert QPP[2023].qp_cf_applies is False
|
||||
|
||||
def test_2024_true(self):
|
||||
assert QPP[2024].qp_cf_applies is True
|
||||
|
||||
def test_2025_true(self):
|
||||
assert QPP[2025].qp_cf_applies is True
|
||||
|
||||
def test_2026_true(self):
|
||||
assert QPP[2026].qp_cf_applies is True
|
||||
|
||||
|
||||
class TestApmIncentivePct:
|
||||
"""Lump-sum APM Incentive Payment sunsets after payment year 2026."""
|
||||
|
||||
def test_2023_incentive(self):
|
||||
assert QPP[2023].apm_incentive_pct == 3.5
|
||||
|
||||
def test_2024_incentive(self):
|
||||
assert QPP[2024].apm_incentive_pct == 1.88
|
||||
|
||||
def test_2025_no_incentive(self):
|
||||
assert QPP[2025].apm_incentive_pct is None
|
||||
|
||||
def test_2026_no_incentive(self):
|
||||
assert QPP[2026].apm_incentive_pct is None
|
||||
|
||||
|
||||
# ── QPP[2026].proposed — CMS-1832-P ──────────────────────────────
|
||||
|
||||
|
||||
class TestProposed:
|
||||
def test_2026_has_proposed(self):
|
||||
p = QPP[2026].proposed
|
||||
assert p is not None
|
||||
assert p.cms_rule_id == "CMS-1832-P"
|
||||
assert p.federal_register_citation == "90 FR 32352"
|
||||
|
||||
def test_2026_changes_non_empty(self):
|
||||
assert len(QPP[2026].proposed.changes) > 0
|
||||
|
||||
def test_earlier_years_have_no_proposed_block(self):
|
||||
assert QPP[2023].proposed is None
|
||||
assert QPP[2024].proposed is None
|
||||
assert QPP[2025].proposed is None
|
||||
|
||||
|
||||
# ── Exact transcribed threshold values ───────────────────────────
|
||||
|
||||
|
||||
class TestSpecificValues:
|
||||
"""Verify transcribed QP/Partial QP thresholds and risk standards.
|
||||
|
||||
See task-6-report.md transcription table for sourcing per value."""
|
||||
|
||||
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
|
||||
def test_qp_thresholds(self, year):
|
||||
assert QPP[year].qp_thresholds.payment_amount_pct == 75.0
|
||||
assert QPP[year].qp_thresholds.patient_count_pct == 50.0
|
||||
|
||||
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
|
||||
def test_partial_qp_thresholds(self, year):
|
||||
assert QPP[year].partial_qp_thresholds.payment_amount_pct == 50.0
|
||||
assert QPP[year].partial_qp_thresholds.patient_count_pct == 35.0
|
||||
|
||||
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
|
||||
def test_risk_standards(self, year):
|
||||
# Revenue-based standard (42 CFR 414.1415(c)(3)(i)(A)) is
|
||||
# transcribed (8%); the benchmark-based standard (42 CFR
|
||||
# 414.1415(c)(3)(i)(B)) is not stated as a bare number in this
|
||||
# repository's FR corpus and is left None — see
|
||||
# RiskStandards.notes / module docstring.
|
||||
assert QPP[year].risk_standards.revenue_nominal_pct == 8.0
|
||||
assert QPP[year].risk_standards.benchmark_nominal_pct is None
|
||||
|
||||
@pytest.mark.parametrize("year", [2023, 2024, 2025, 2026])
|
||||
def test_cehrt_required(self, year):
|
||||
assert QPP[year].cehrt_required is True
|
||||
|
||||
|
||||
# ── for_payment_year lookup ──────────────────────────────────────
|
||||
|
||||
|
||||
class TestForPaymentYear:
|
||||
def test_2025_maps_to_performance_2023(self):
|
||||
assert for_payment_year(2025) is QPP[2023]
|
||||
|
||||
def test_2026_maps_to_performance_2024(self):
|
||||
assert for_payment_year(2026) is QPP[2024]
|
||||
|
||||
def test_2027_maps_to_performance_2025(self):
|
||||
assert for_payment_year(2027) is QPP[2025]
|
||||
|
||||
def test_2028_maps_to_performance_2026(self):
|
||||
assert for_payment_year(2028) is QPP[2026]
|
||||
|
||||
def test_unknown_payment_year_raises_keyerror(self):
|
||||
with pytest.raises(KeyError):
|
||||
for_payment_year(1999)
|
||||
@@ -150,3 +150,17 @@ class TestClassify:
|
||||
provider = _FakeProvider([])
|
||||
with pytest.raises(ValueError, match="no classify_comment tool call"):
|
||||
classify(provider, "text")
|
||||
|
||||
|
||||
def test_docket_rules_maps_cy2026_pfs():
|
||||
from rex.comments.classify import DOCKET_RULES
|
||||
|
||||
assert DOCKET_RULES["CMS-2025-0304"] == "CMS-1832-P"
|
||||
|
||||
|
||||
def test_system_prompt_names_the_pfs_rule():
|
||||
from rex.comments.classify import _SYSTEM
|
||||
|
||||
assert "CMS-1832-P" in _SYSTEM
|
||||
assert "CMS-1834-P" not in _SYSTEM
|
||||
assert "PFS proposed rule" in _SYSTEM
|
||||
|
||||
268
uv.lock
generated
268
uv.lock
generated
@@ -76,7 +76,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.14.1"
|
||||
version = "3.14.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohappyeyeballs" },
|
||||
@@ -88,90 +88,90 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -655,52 +655,52 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "49.0.0"
|
||||
version = "50.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3148,11 +3148,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4083,6 +4083,11 @@ prisma = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "resend" },
|
||||
]
|
||||
qpp = [
|
||||
{ name = "duckdb" },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
rec = [
|
||||
{ name = "duckdb" },
|
||||
{ name = "fsspec" },
|
||||
@@ -4133,7 +4138,7 @@ requires-dist = [
|
||||
{ name = "azure-storage-blob", marker = "extra == 'azure'", specifier = ">=12.23.0" },
|
||||
{ name = "boto3", marker = "extra == 'aws'", specifier = ">=1.35.0" },
|
||||
{ name = "coverage", marker = "extra == 'sem'", specifier = ">=7.13.4" },
|
||||
{ name = "cryptography", marker = "extra == 'api'", specifier = ">=48.0.1" },
|
||||
{ name = "cryptography", marker = "extra == 'api'", specifier = ">=50.0.0" },
|
||||
{ name = "databricks-bundles", marker = "extra == 'lake'", specifier = ">=0.295.0" },
|
||||
{ name = "databricks-sdk", marker = "extra == 'lake'", specifier = ">=0.85.0" },
|
||||
{ name = "duckdb", marker = "extra == 'aco'", specifier = ">=1.0.0" },
|
||||
@@ -4182,6 +4187,7 @@ requires-dist = [
|
||||
{ name = "pydantic", marker = "extra == 'ccw'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic", marker = "extra == 'cms'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic", marker = "extra == 'conf'", specifier = ">=2.0.0" },
|
||||
{ name = "pydantic", marker = "extra == 'qpp'", specifier = ">=2.0.0" },
|
||||
{ name = "pydo", marker = "extra == 'mail'", specifier = ">=0.29.0" },
|
||||
{ name = "pydo", marker = "extra == 'prisma'", specifier = ">=0.29.0" },
|
||||
{ name = "pyjwt", marker = "extra == 'api'", specifier = ">=2.13.0" },
|
||||
@@ -4216,6 +4222,7 @@ requires-dist = [
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'perf'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'pfs'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'prisma'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'qpp'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'rec'" },
|
||||
{ name = "stack", extras = ["conf"], marker = "extra == 'rex'" },
|
||||
{ name = "stack", extras = ["lake"], marker = "extra == 'all'" },
|
||||
@@ -4225,6 +4232,7 @@ requires-dist = [
|
||||
{ name = "stack", extras = ["perf"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["pfs"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["pfs"], marker = "extra == 'rec'" },
|
||||
{ name = "stack", extras = ["qpp"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["rec"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["rex"], marker = "extra == 'all'" },
|
||||
{ name = "stack", extras = ["rex"], marker = "extra == 'opps'" },
|
||||
@@ -4236,7 +4244,7 @@ requires-dist = [
|
||||
{ name = "uvicorn", marker = "extra == 'llm'", specifier = ">=0.41.0" },
|
||||
{ name = "xlrd", specifier = ">=2.0.2" },
|
||||
]
|
||||
provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "cli", "mail", "cms", "opps", "pfs", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"]
|
||||
provides-extras = ["conf", "aco", "api", "bcda", "bib", "bls", "llm", "ccw", "cli", "mail", "cms", "opps", "pfs", "qpp", "rec", "rex", "prisma", "perf", "sem", "lake", "aws", "gcp", "azure", "all"]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
|
||||
Reference in New Issue
Block a user