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()
|
||||
def backup() -> None:
|
||||
"""Encrypted DB backup management (SP17)."""
|
||||
|
||||
@@ -193,3 +193,16 @@ def test_submit_file_uses_default_paramiko_factory(monkeypatch):
|
||||
)
|
||||
assert result.outcome == SubmitOutcome.SUBMITTED
|
||||
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