feat(sp37): cyclone submit-batch CLI
Walks batch-*-claims/*.x12 under --ingest-dir, calls submit_file per file, prints submitted/skipped/failed counts. Exits 0 even on per-file failures (details in stdout); exits 2 on config-level errors (no clearhouse, stub mode, missing dir). This is the canonical outbound path; resubmit-rejected-claims remains for one-off cases where no DB row is wanted.
This commit is contained in:
@@ -768,6 +768,99 @@ def resubmit_rejected_claims(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@main.command("submit-batch")
|
||||||
|
@click.option("--ingest-dir", default="ingest",
|
||||||
|
show_default=True,
|
||||||
|
help="Root dir holding batch-*-claims/ subfolders of .x12 files.")
|
||||||
|
@click.option("--actor", default="cli-submit-batch",
|
||||||
|
show_default=True,
|
||||||
|
help="Audit-log actor tag for the clearhouse.submitted events.")
|
||||||
|
@click.option("--validate/--no-validate", default=True,
|
||||||
|
help="Parse each file via parse_837 before upload.")
|
||||||
|
@click.option("--limit", type=int, default=None,
|
||||||
|
help="Stop after N files (smoke-tests).")
|
||||||
|
def submit_batch(
|
||||||
|
ingest_dir: str,
|
||||||
|
actor: str,
|
||||||
|
validate: bool,
|
||||||
|
limit: int | None,
|
||||||
|
) -> None:
|
||||||
|
"""Parse → DB-write → SFTP-upload each batch-*-claims/*.x12 (SP37).
|
||||||
|
|
||||||
|
Canonical outbound path. Mirrors ``resubmit-rejected-claims`` but
|
||||||
|
routes through ``cyclone.submission.submit_file``, which writes to
|
||||||
|
the DB before uploading. Use this as the preferred outbound path;
|
||||||
|
``resubmit-rejected-claims`` remains for one-off cases where you
|
||||||
|
don't want a DB row (dzinesco-generated fixes not ready for
|
||||||
|
canonical tracking).
|
||||||
|
|
||||||
|
Exit codes: 0 = run completed (even with per-file failures), 1 =
|
||||||
|
unexpected exception, 2 = config-level failure (no clearhouse,
|
||||||
|
SFTP block in stub mode, ingest-dir missing).
|
||||||
|
"""
|
||||||
|
from cyclone import db as db_mod
|
||||||
|
from cyclone.submission.core import submit_file
|
||||||
|
from cyclone.submission.result import SubmitOutcome
|
||||||
|
from cyclone.store import store as cycl_store
|
||||||
|
|
||||||
|
db_mod.init_db()
|
||||||
|
cycl_store.ensure_clearhouse_seeded()
|
||||||
|
clearhouse = cycl_store.get_clearhouse()
|
||||||
|
if clearhouse is None:
|
||||||
|
click.echo("No clearhouse seeded; cannot resolve SFTP block.", err=True)
|
||||||
|
sys.exit(2)
|
||||||
|
sftp_block = clearhouse.sftp_block
|
||||||
|
if sftp_block.stub:
|
||||||
|
click.echo("Clearhouse SFTP block is in stub mode; refusing to upload.", err=True)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
root = Path(ingest_dir).resolve()
|
||||||
|
if not root.exists():
|
||||||
|
click.echo(f"--ingest-dir does not exist: {root}", err=True)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
files: list[Path] = []
|
||||||
|
for batch_dir in sorted(root.glob("batch-*-claims")):
|
||||||
|
files.extend(sorted(p for p in batch_dir.glob("*.x12")
|
||||||
|
if not p.name.startswith("._")))
|
||||||
|
if not files:
|
||||||
|
click.echo(f"No .x12 files under {root}", err=True)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
click.echo(f"found {len(files)} files under {root}")
|
||||||
|
|
||||||
|
submitted = 0
|
||||||
|
skipped = 0
|
||||||
|
failed = 0
|
||||||
|
for i, src in enumerate(files, 1):
|
||||||
|
if limit is not None and i > limit:
|
||||||
|
break
|
||||||
|
try:
|
||||||
|
result = submit_file(
|
||||||
|
src, sftp_block=sftp_block, actor=actor, validate=validate,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
click.echo(f"UNEXPECTED {src.name}: {exc.__class__.__name__}: {exc}", err=True)
|
||||||
|
failed += 1
|
||||||
|
continue
|
||||||
|
if result.outcome == SubmitOutcome.SUBMITTED:
|
||||||
|
submitted += 1
|
||||||
|
click.echo(f"submitted {src.name} batch={result.batch_id}")
|
||||||
|
elif result.outcome == SubmitOutcome.SKIPPED:
|
||||||
|
skipped += 1
|
||||||
|
click.echo(f"skipped {src.name} (already uploaded)")
|
||||||
|
else:
|
||||||
|
failed += 1
|
||||||
|
click.echo(
|
||||||
|
f"{result.outcome.value:<10} {src.name} {result.error or ''}",
|
||||||
|
err=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
click.echo(f"\nsummary: submitted={submitted} skipped={skipped} failed={failed}")
|
||||||
|
# Per-file failures don't bump exit code; details in stdout.
|
||||||
|
# (Per cyclone-cli convention.)
|
||||||
|
|
||||||
|
|
||||||
@main.group()
|
@main.group()
|
||||||
def backup() -> None:
|
def backup() -> None:
|
||||||
"""Encrypted DB backup management (SP17)."""
|
"""Encrypted DB backup management (SP17)."""
|
||||||
|
|||||||
@@ -193,3 +193,16 @@ def test_submit_file_uses_default_paramiko_factory(monkeypatch):
|
|||||||
)
|
)
|
||||||
assert result.outcome == SubmitOutcome.SUBMITTED
|
assert result.outcome == SubmitOutcome.SUBMITTED
|
||||||
assert len(fake_paramiko_sftp.put_calls) == 1
|
assert len(fake_paramiko_sftp.put_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_submit_batch_cli_help():
|
||||||
|
"""`cyclone submit-batch --help` exits 0 and shows the flags."""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, "-m", "cyclone.cli", "submit-batch", "--help"],
|
||||||
|
capture_output=True, text=True,
|
||||||
|
cwd=str(Path(__file__).parent.parent),
|
||||||
|
)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "--ingest-dir" in result.stdout
|
||||||
|
|||||||
Reference in New Issue
Block a user