diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 0ec3e8d..bdfa9d3 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,9 +1,6 @@ name: Deploy Demo to GitHub Pages on: - push: - branches: [main] - paths: [demo/**] workflow_dispatch: permissions: diff --git a/.gitignore b/.gitignore index 56d19fb..6d51a22 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,23 @@ invoices/ *.pyc output/ node_modules/ -demo/dist/ \ No newline at end of file +demo/dist/ + +# B0 hand-wired bootstrap scaffolding (caravan-conversion branch). +# The local-source pattern is transitional — gone once caravan-rpc lands on +# PyPI (post-M9-cloud) and the M1 compiler emits compose from caravan.yaml. +# Until then these files live on disk locally but stay out of git history. +infra/docker-compose.caravan-bootstrap.yaml +infra/rebuild-caravan-rpc-wheel.sh +services/processing/vendor/ + +# pip editable-install metadata +*.egg-info/ + +# B0 / M1 acceptance-gate run outputs +.b0-runs/ +.m1-runs/ + +# caravan-emitted artifacts. Regenerated by `caravan compile --target=`. +# Should never be hand-edited; should never be committed. +infra/*/generated/ \ No newline at end of file diff --git a/README.md b/README.md index 77384e2..c4a2dd4 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,40 @@ redis-cli XLEN queue:a && redis-cli XLEN queue:b | Processing | Python | CPU/API-bound; OCR + LLM libraries are Python-native | | Output | Rust | I/O-bound delivery; Excel gen is stable plumbing that rarely changes | +## Deployment via Caravan + +invoice-parse adopts [Caravan](https://github.com/paulxiep/caravan) — an application-definition compiler — as its deployment composition layer (Phase 1 close, 2026-05-21). Three RPC dispatch boundaries are declared as Caravan **seams** via the `caravan-rpc` SDK (`@wagon` interface + `provide(I, impl)` registration + `client(I).method()` dispatch); data-plane primitives are declared as Caravan **resources**. One yaml + one `caravan compile --target=` produces a per-target `docker-compose.override.generated.yaml` plus per-service patched `requirements.txt`. Source code is byte-identical across every target — yaml-line changes alone flip dispatch and composition. See [caravan.yaml](caravan.yaml) for the full descriptor and [docs/caravan-readiness.md](docs/caravan-readiness.md) for the pre-adoption structural analysis. + +**Declared seams** (live in `services/processing/invoice_processing/`): + +| Seam | Interface | Container peer service | Notes | +|---|---|---|---| +| `LLMExtraction` | `extraction.py` | `llm-extractor` | Gemini-backed; provider-swappable via `provide()`. | +| `OCRText` | `ocr.py` | `ocr-text` | PaddleOCR; impl eager-loads the model in `__init__` before binding the TCP port. | +| `OCRLayout` | `table_extract.py` | `ocr-layout` | Per-target impl choice: inproc default uses `SpatialClusterExtractor` (CPU-light); container peer uses `PPStructureExtractor` (PPStructureV3 model). | + +**Declared resources:** `invoice_queue` (queue, default `redis-streams`, can flip to `rabbitmq` via yaml composition override); `invoice_db` (Postgres). `invoice_blobs` is **intentionally not yet declared** — Caravan's only OSS-local bucket variant is MinIO, which would flip just the consumer it's declared on (`processing`) to S3-protocol calls while ingestion + output continue talking to the hand-authored `blobdata` volume mount; the storage-backend mismatch would break the queue pipeline. Re-introducing it requires declaring all three deploy units' `uses:` together (see the deferred-work comment in [caravan.yaml](caravan.yaml)). + +**Demo targets** (each is a one-line yaml change): + +| Target | LLMExtraction | OCRText | OCRLayout | Queue | +|---|---|---|---|---| +| `dev-bootstrap` | container | container | container | redis-streams | +| `dev-split-llm` | container | inproc | inproc | redis-streams | +| `dev-inproc` | inproc | inproc | inproc | redis-streams | +| `dev-rabbitmq-flip` | container | container | container | **rabbitmq** | + +```bash +# Install caravan, then from invoice-parse repo root: +caravan compile --target=dev-bootstrap +docker compose \ + -f infra/docker-compose.yaml \ + -f infra/dev-bootstrap/generated/docker-compose.override.generated.yaml \ + --profile app up +``` + +`git diff -- services/ libs/` between target compiles is empty — the thesis claim that yaml-line dispatch / composition flips require zero source edits. The Caravan-emitted override layers atop the hand-authored [infra/docker-compose.yaml](infra/docker-compose.yaml); the base compose still works standalone for the no-Caravan local-dev path (all seams default inproc, all resources default to the hand-authored Postgres + Redis containers). + ## Design Decisions | Decision | Rationale | @@ -127,7 +161,7 @@ redis-cli XLEN queue:a && redis-cli XLEN queue:b | **Deterministic pipeline, not agentic** | Invoice extraction is structured and repeatable. Conditional fallbacks (vision model, provider failover, VAT derivation) provide adaptability without LLM decision loops. | | **Queue-based async** | Redis Streams locally, SQS in production. Workers scale independently. Queue absorbs bursts. | | **Confidence from validation, not LLM self-report** | 50% validation checks + 30% OCR confidence + 20% field completeness. Deterministic and auditable. | -| **Adapter pattern** | `BlobStore` (local FS / S3) and `MessageQueue` (Redis / SQS) abstractions. Same code, swap config. | +| **Adapter pattern** | `BlobStore` (local FS / S3) and `MessageQueue` (Redis / RabbitMQ / SQS) abstractions. Selection at startup via env-var presence (Caravan-injected) with YAML-config fallback; see [Deployment via Caravan](#deployment-via-caravan). Same code, swap target. | | **Layout-aware OCR** | PaddleOCR with spatial clustering preserves table row/column structure critical for invoices. | | **LLM result cache** | SHA-256 hash of input file → cached extraction. First run hits LLM, subsequent runs are free. Enables load testing without API cost. | @@ -155,6 +189,7 @@ redis-cli XLEN queue:a && redis-cli XLEN queue:b ``` invoice_parse/ +├── caravan.yaml Caravan application descriptor (entries + seams + resources + targets) ├── services/ │ ├── ingestion/ Rust — IMAP poll, file ingest, job creation │ ├── processing/ Python — OCR, table extraction, LLM, validation @@ -164,10 +199,10 @@ invoice_parse/ │ ├── shared-rs/ Rust — models, DB, blob store, queue adapters │ └── shared-py/ Python — models, DB, blob store, queue adapters ├── config/ YAML config (local.yaml, docker.yaml, production.yaml) -├── infra/ Docker Compose (Postgres, Redis) +├── infra/ Docker Compose (hand-authored base) + per-target Caravan-emitted overrides under infra//generated/ ├── migrations/ SQL schema ├── invoices/ Sample invoice files for testing -└── docs/ Architecture, service plans, devlog +└── docs/ Architecture, service plans, devlog, caravan-readiness analysis ``` **Mono-repo for POC, multi-repo in production.** Each service under `services/` has its own `Cargo.toml` or `pyproject.toml`, `Dockerfile`, and dependency closure — no Cargo workspace, no shared virtualenv. Shared libraries under `libs/` use path dependencies locally and would publish to a private crate registry / PyPI in production. This means any service can be extracted to its own repo and CI pipeline without restructuring. diff --git a/caravan.yaml b/caravan.yaml new file mode 100644 index 0000000..4f45f3b --- /dev/null +++ b/caravan.yaml @@ -0,0 +1,178 @@ +# Caravan application descriptor for invoice-parse. +# +# This file is the user-authored input to `caravan compile --target=`. +# It maps the existing invoice-parse code (Python processing service + Rust +# ingestion/output) onto Caravan's vocabulary: entries (deploy units), +# seams (synchronous abstraction boundaries inside a single language), +# resources (data-plane primitives), and per-target dispatch overrides. +# +# Status: M0 seed. Only the LLMExtraction seam + the processing entry are +# fully declared — enough to drive the dev-bootstrap target that matches +# B0's hand-edited override. Other invoice-parse services (output, ingest, +# dashboard, model-init) and resources (postgres, redis, blob storage) are +# stubs / unmentioned and stay hand-authored in infra/docker-compose.yaml +# until M6 brings them under caravan declaration. + +name: invoice-parse +default_target: dev-bootstrap +# Pin Caravan's per-target write root to the pre-existing `infra//generated/` +# layout (otherwise the compiler defaults to `caravan-out/`). All committed +# generated artifacts + the layering hints in infra/docker-compose.yaml assume +# `infra/`. +output_dir: infra + +entries: + processing: + path: services/processing + dockerfile: services/processing/Dockerfile + triggers: + - queue: { from: invoice_queue } + uses: [invoice_queue, invoice_db, gemini_key] + + # ingest is a one-shot Rust CLI that enqueues PDFs from a mounted + # directory onto invoice_queue and writes the raw input blobs to + # invoice_blobs. The compose service is hand-authored in + # infra/docker-compose.yaml; declaring it here makes Caravan emit the + # data-plane env vars (S3_*, QUEUE_URL) onto the same service name in + # the generated override. No seams of its own — single-binary CLI. + ingest: + path: services/ingestion + dockerfile: services/ingestion/Dockerfile + uses: [invoice_queue] + + # output is a Rust queue-consumer that reads completed extractions + # from invoice_queue, fetches the input blob, generates an Excel, + # writes it back to invoice_blobs, and records final state in + # invoice_db. Compose service is hand-authored; this declaration + # unifies its env-var emission with processing's. + output: + path: services/output + dockerfile: services/output/Dockerfile + triggers: + - queue: { from: invoice_queue } + uses: [invoice_queue, invoice_db] + +seams: + LLMExtraction: + # Same code lives in the same module as the processing entry; the + # peer service in container-mode reuses the processing image with an + # overridden command (B0's pattern, now compiler-emitted at M1). + path: services/processing + dockerfile: services/processing/Dockerfile + uses: [gemini_key] + # `impl` tells the M1 compose-emitter which class to load when this + # seam dispatches as a container peer. Language-agnostic shape; the + # Python emitter parses `module:Class`. + impl: invoice_processing.extraction:GeminiExtractor + # Override the default kebab-case naming (`llm-extraction`) so the + # generated compose service name matches B0's hand-edit byte-for-byte. + service_name: llm-extractor + # The llm-extractor peer needs GEMINI_API_KEY to call out to + # Gemini. invoice-parse runs compose from `invoice-parse/`, so + # `../.env` from the generated override file resolves to the user's + # repo-level .env. Code-rag's Rust peers don't need any envvars + # beyond CARAVAN_RPC_SHARED_SECRET, so they omit this field. + env_file: ../.env + + OCRText: + # M3 second seam — PaddleOCR-backed raw OCR text extraction. + # Same module/image as LLMExtraction; differs by impl class and + # service_name. Container-mode peer eagerly loads PaddleOCR in its + # __init__ before binding the TCP port (avoids a cold-start race + # with consumer dispatch). + path: services/processing + dockerfile: services/processing/Dockerfile + uses: [] + impl: invoice_processing.ocr:PaddleOCRTextImpl + service_name: ocr-text + + OCRLayout: + # M6 third seam — table/layout extraction (PPStructureV3 model). + # Container-mode peer eagerly loads PPStructureV3 in its __init__. + # The worker's local provide() registers SpatialClusterExtractor + # (CPU-only, no model) for the inproc default; this `impl:` ref + # selects PPStructureExtractor for the container-mode peer where + # the heavy-model accuracy is worth its load cost. inproc and + # container thus run different impls of the same seam — both + # satisfy the OCRLayout contract; the choice is per-target. + path: services/processing + dockerfile: services/processing/Dockerfile + uses: [] + impl: invoice_processing.table_extract:PPStructureExtractor + service_name: ocr-layout + +resources: + invoice_queue: { type: queue, composition: oss-local } + # Credentials match the hand-authored infra/docker-compose.yaml's + # postgres service (POSTGRES_USER/PASSWORD/DB) so the Caravan-emitted + # DATABASE_URL points at the same engine. When Caravan also emits the + # postgres container (no collision), POSTGRES_* env on that container + # tracks these same values. + invoice_db: { type: db.sql, composition: oss-local, user: invoice, password: invoice, dbname: invoice_parse } + # invoice_blobs intentionally NOT declared as a Caravan resource yet. + # Caravan's only OSS-local bucket variant today is MinIO, which would + # flip just the consumer it's declared on (`processing` originally) to + # S3 wire calls while ingest + output continue to use the LocalFs + # adapter against the hand-authored `blobdata` volume mount — + # storage-backend mismatch breaks the queue pipeline (NoSuchKey). + # Re-introduce `invoice_blobs: { type: bucket, composition: oss-local }` + # AND add it to ingest + output + processing's `uses:` lists in the same + # change once all three deploy units consume MinIO consistently. A + # `variant: localfs` for buckets would be the IR-pure way to declare + # "Caravan-managed, but bound to the shared volume the user provides"; + # not in the compiler today. + +secrets: + gemini_key: { from: env, path: GEMINI_API_KEY } + +targets: + # dev-bootstrap is M6's canonical demo: all three seams as peer + # containers (llm-extractor, ocr-text, ocr-layout). Stresses the + # multi-seam emit + per-seam env-var wiring end-to-end. + dev-bootstrap: + runtime: docker-compose + default_composition: oss-local + entries: { processing: container, ingest: container, output: container } + seams: + LLMExtraction: container + OCRText: container + OCRLayout: container + + # dev-split-llm is the mix-and-match demo: LLMExtraction runs as a + # peer container while the OCR seams stay inproc inside the + # processing service. Proves per-seam dispatch flips independently — + # the load-bearing thesis claim. Source code is identical to + # dev-bootstrap; only this target's seams block differs. + dev-split-llm: + runtime: docker-compose + default_composition: oss-local + entries: { processing: container, ingest: container, output: container } + seams: + LLMExtraction: container + OCRText: inproc + OCRLayout: inproc + + # dev-inproc keeps everything in-process. Same source code; flip yaml + # lines and the inproc/http dispatch toggles. This is the thesis. + dev-inproc: + runtime: docker-compose + default_composition: oss-local + entries: { processing: container, ingest: container, output: container } + # seams omitted → all three seams default to inproc + + # dev-rabbitmq-flip is M4's composition-orthogonality demo. Same + # source, same per-seam dispatch as dev-bootstrap (all three seams + # container); only the queue engine swaps from redis-streams (default) + # to rabbitmq. Caravan emits a new `rabbitmq:` service and flips + # QUEUE_URL to amqp:// on the processing consumer. invoice-parse's + # queue adapter routes on URL scheme. + dev-rabbitmq-flip: + runtime: docker-compose + default_composition: oss-local + entries: { processing: container, ingest: container, output: container } + seams: + LLMExtraction: container + OCRText: container + OCRLayout: container + composition: + invoice_queue: { mode: oss-local, kind: rabbitmq } diff --git a/docs/caravan-readiness.md b/docs/caravan-readiness.md new file mode 100644 index 0000000..b623d10 --- /dev/null +++ b/docs/caravan-readiness.md @@ -0,0 +1,160 @@ +# Caravan SDK Thesis & invoice-parse Readiness Evaluation + +An analysis of how structurally compatible invoice-parse's current architecture is with the (currently unimplemented) [Caravan](https://github.com/paulxiep/caravan) SDK pattern — i.e., if Caravan were implemented tomorrow, how much of invoice-parse would need to change to deploy across packaging/placement permutations through Caravan's SDK seams? + +Scope: evaluation only, treating the Caravan SDK as hypothetical. Three candidate seams under review — PaddleOCR-text, PaddleOCR-layout, LLM-extraction. Several existing adapter-shaped components evaluated as Caravan **resources** (queue, blob, db.sql) rather than seams. + +--- + +## 1. Caravan's Thesis in One Paragraph + +Caravan is a **backend** application-definition compiler. The same source code deploys across three **orthogonal** axes — *packaging* (monolith / multi-container / multi-service), *placement* (local docker-compose / Fargate / Lambda / batch), and *composition* (local OSS engine / cloud managed service / referenced existing resource) — by reading a single `caravan.yaml`. The compiler emits HCL (Terraform) and Compose projections of an IR. The structural contract user code must obey is the `caravan-rpc` SDK at inter-component seams: a call site written as `client::().method()` dispatches as **inproc / HTTP / Lambda** based on a compiler-injected `CARAVAN_RPC_PEERS` table. **Each seam carries its own per-target dispatch config**, set independently of every other seam. Frontend deployment is out of Caravan's scope. + +**State today:** thesis + evidence catalogs + PoC specs. No CLI, no SDK packages, no compiler. + +--- + +## 2. Three Clarifications Before Evaluating + +**Clarification A — FE vs BE.** invoice-parse's workspace contains a Streamlit dashboard (frontend-ish; out of Caravan scope) plus three backend services. Caravan evaluates only the three BE services. + +**Clarification B — Adapter ≠ Seam.** invoice-parse already has a bespoke adapter pattern in [libs/shared-py/invoice_shared/adapters/](../libs/shared-py/invoice_shared/adapters/) — `BlobStore`, `Queue`, with a `factory.py` selecting between local and cloud implementations. These are well-shaped **resource adapters**, not Caravan seams. Under Caravan they fold into the *composition* axis (yaml-line swap between `oss-local` and `cloud-managed`), not the *packaging* axis. Don't conflate. + +**Clarification C — FFI ≠ RPC.** `libs/shared-rs` (Rust libs called from Rust services) is a foreign-function-interface boundary, not a Caravan seam. Caravan seams are RPC dispatch boundaries with at-most-one-process-per-side semantics; FFI is in-process function call. If FFI between Python and Rust becomes a thing here, it doesn't become a Caravan seam — it stays out of scope. + +| Surface | Kind | In Caravan's blast radius? | +|---|---|---| +| services/dashboard (Streamlit) | Frontend-ish | **No — out of scope by design** | +| services/ingestion (Rust async, tokio) | Backend daemon | Yes — fits Caravan's `http` or `cron` entry kind | +| services/processing (Python worker loop) | Backend daemon | Yes — fits Caravan's `http` or `batch` entry kind | +| services/output (Rust async, tokio) | Backend daemon | Yes — fits Caravan's `http` or `batch` entry kind | + +--- + +## 3. invoice-parse Architecture, with BE Boundary Made Explicit + +``` +FRONTEND (outside Caravan scope): + services/dashboard — Streamlit; reads Postgres directly for monitoring + +BACKEND (Caravan's domain): + Entries (deployable processes): + services/ingestion — Rust, file/IMAP intake [entry: cron or http] + services/processing — Python, OCR+LLM extraction loop [entry: batch or http] + services/output — Rust, Excel gen + delivery loop [entry: batch or http] + + Shared resources (Caravan composition axis, not seams): + libs/shared-py/invoice_shared/adapters/blob_store.py → caravan bucket group + libs/shared-py/invoice_shared/adapters/queue.py → caravan queue group + libs/shared-py/invoice_shared/db.py → caravan db.sql group + (parallel Rust adapters under libs/shared-rs/) + + Candidate seams (need @wagon declaration): + PaddleOCR text extractor (services/processing/invoice_processing/ocr.py) + PaddleOCR layout extractor (services/processing/invoice_processing/table_extract.py) + LLMExtractor ABC + impls (services/processing/invoice_processing/extraction.py) +``` + +The job state machine spans Postgres + queue messages. Caravan doesn't break this — the DB stays one shared resource; the cross-service flow stays queue-mediated. Caravan's contribution is making composition (Postgres-local vs Aurora-cloud, Redis Streams vs SQS) a yaml flip. + +--- + +## 4. Seam-by-Seam Readiness Evaluation + +For each candidate seam: today's coupling, the natural Caravan interface shape, and practical viability. + +### 4.1 PaddleOCR Raw Text Extraction ([services/processing/invoice_processing/ocr.py](../services/processing/invoice_processing/ocr.py)) + +- **Today's coupling:** concrete `PaddleOCR(...)` instantiation around L88; ~500MB model warm cached in a Docker volume. Wrapped in custom `OCREngine` adapter inside `ocr.py`. +- **Caravan-fit shape:** Clean. Natural interface is `extract_text(pdf_bytes: bytes) -> list[TextBox]`. Pure function over inputs (image in, structured text out). No shared state across calls. +- **Practical viability:** Strong. Heavy model warm makes container the natural cloud target; Lambda viable only with provisioned concurrency. Per-seam config means a target can set `OCRText: container` for cloud while staying `inproc` in dev — independent of any other seam's choice. +- **Readiness verdict: HIGH.** One refactor: declare `@wagon OCRText` and wrap the existing `OCREngine` adapter via `provide(OCRText, OCREngine())`. + +### 4.2 PaddleOCR PPStructureV3 Layout Detection ([services/processing/invoice_processing/table_extract.py](../services/processing/invoice_processing/table_extract.py)) + +- **Today's coupling:** concrete `PPStructureV3(...)` instantiation around L251; separate model from §4.1, separate computational profile. Called when document layout analysis is needed. +- **Caravan-fit shape:** Clean. Natural interface is `extract_layout(image: bytes) -> LayoutResult`. Independent of §4.1 — they can co-locate in one container or split across two; per-target yaml decides. +- **Practical viability:** Strong, same reasoning as §4.1. Two heavy models bundled in one process is the current default; per-seam splitting lets one model scale or upgrade without dragging the other. +- **Readiness verdict: HIGH.** Same shape as §4.1: declare `@wagon OCRLayout` + `provide(OCRLayout, PPStructureAdapter())`. + +### 4.3 LLM Extraction ([services/processing/invoice_processing/extraction.py](../services/processing/invoice_processing/extraction.py)) + +- **Today's coupling:** **already abstracted.** A `LLMExtractor` abstract base class with `GeminiExtractor` concrete impl + `ClaudeExtractor` / `OpenAIExtractor` stubbed as `NotImplementedError`. Factory pattern selects provider. **This is the closest seam to caravan-ready in the codebase.** +- **Caravan-fit shape:** Already a thin wrap away from caravan. `LLMExtractor.extract(invoice_text: str) -> InvoiceDTO` becomes `@wagon LLMExtraction`. The provider choice (Gemini vs Claude vs OpenAI vs Bedrock) maps onto Caravan's `llm` resource group with Tier-1 hard-pair semantics (per [poc_groups_to_code.md](../caravan/docs/poc_groups_to_code.md)). +- **Practical viability:** Very strong. LLM calls are network-bound, stateless, naturally async — works equally well inproc / container / lambda. +- **Readiness verdict: VERY HIGH.** Smallest refactor of the three seams. The bespoke `LLMExtractor` ABC dissolves; in its place a Caravan `@wagon` + a chosen `llm` resource composition. + +--- + +## 5. Architectural Strengths (Already SDK-Friendly) + +invoice-parse is *structurally further along* than a fresh project would be: + +- **Three already-separate BE services.** The multi-entry pattern Caravan assumes (multiple deployable processes from one codebase) is already real here, more so than in code-rag. +- **Adapter + factory pattern is half of Caravan's composition story.** [libs/shared-py/invoice_shared/adapters/](../libs/shared-py/invoice_shared/adapters/) separates "what the code calls" (BlobStore.put / Queue.publish) from "what's behind it" (local FS vs S3, Redis Streams vs SQS). Caravan formalizes the same idea as yaml-driven composition — invoice-parse already speaks the concept, just in non-Caravan vocabulary. +- **Stubbed cloud variants signal intent.** `S3BlobStore` and `SQSQueue` exist as `NotImplementedError` placeholders. Caravan's composition axis is exactly what those stubs were designed for: yaml-flip from local to cloud without source edits. +- **LLM extraction already uses an ABC + factory.** The hardest provider-swap case is already structurally solved at the language level; Caravan-ifying it is a wrap, not a redesign. +- **Production-shaped infra.** Per-service Dockerfile, Postgres migrations, Redis Streams consumer groups, Pydantic config models. Caravan emits all of these — less for the compiler to invent. +- **Polyglot real-world test case.** Rust + Python both shipping in production-shaped form; will exercise cross-language wire compatibility (M3 acceptance in the development plan). + +--- + +## 6. Architectural Gaps (What Would Need to Change) + +These are the concrete obstacles to Caravan dispatch over invoice-parse's call sites: + +- **Adapter pattern is bespoke, not caravan-rpc.** `BlobStore.put()` calls go through invoice-parse's custom abstract base class, not `client::().put()`. Migration is mechanical but spans both languages (shared-py + shared-rs). +- **LLMExtractor ABC parallels but doesn't match Caravan's `llm` resource group.** The provider-choice mechanism becomes `composition.llm: { provider: bedrock | gemini | ... }` in caravan.yaml, with the SDK call site becoming `client::().extract(...)`. The existing `litellm`-shaped abstraction layer can survive as the implementation; the ABC + factory disappears. +- **OCR seams currently in-process, no interface declared.** PaddleOCR text and PPStructure are instantiated directly inside the processing service. Need `@wagon` declarations + `provide()` registration before Caravan can dispatch them remotely. +- **State machine spans services + DB.** Caravan doesn't ask you to split DB ownership, but `caravan.yaml` needs `processing` and `output` to both declare `uses: [db.sql.jobs]` — exercises Caravan's full-access IAM derivation pattern. +- **FFI boundary at libs/shared-rs is not a Caravan seam.** Document this explicitly so future contributors don't try to wrap Rust libraries (called from Rust services in-process) with caravan-rpc. FFI ≠ RPC; one is in-process function call, the other is a network-or-direct dispatch decision. +- **Streamlit dashboard reads Postgres directly.** Not a Caravan concern — but worth flagging that if the dashboard ever moves behind an API rather than direct DB reads, that API boundary *would* become a candidate Caravan seam. + +--- + +## 7. Overall Readiness Verdict + +**Readiness rating: HIGH. Roughly 85% structurally — slightly ahead of code-rag.** + +Why ahead of code-rag: +1. **Multi-service split is already real.** Three BE services live in distinct directories with distinct Dockerfiles. No need to extract crates or break implicit deps before Caravan can see them. +2. **Adapter + factory + NotImplementedError-stubbed cloud variants** is *almost exactly* Caravan's composition axis expressed in non-Caravan vocabulary. Migration is renaming + wiring, not architectural rework. +3. **Production-shaped infra** (Docker per service, Postgres migrations, Redis Streams consumer groups) means Caravan emits familiar artifacts rather than inventing them. +4. **Postgres state machine is the only genuine "hard" point** — and Caravan handles it as a shared resource declared by multiple entries, not as a seam to split. The hard point doesn't actually fight Caravan's design. + +Why not 95%: +- The bespoke adapter ABCs are well-designed but don't *speak* caravan-rpc. Migration is mechanical, but it does span both shared-py and shared-rs. +- The `LLMExtractor` ABC parallels but doesn't match Caravan's `llm` resource group contract. Re-targeting onto the group's Tier-1 hard-pair semantics is required. +- **No inter-service interfaces declared today** (the queue + blob are resources, not seams). For Caravan to demonstrate the per-seam dispatch flip on this codebase, invoice-parse needs to declare at least one inter-service interface — `LLMExtraction` is the cleanest first target because it's already abstracted. + +**invoice-parse would not have to be redesigned to adopt Caravan. It would need to be re-wired** — roughly one PR per resource group migration (queue, blob, db.sql, llm) to move adapter→caravan resource binding, plus one PR to introduce `@wagon` declarations for the OCR pair and LLMExtraction. + +**Caveat.** Caravan is pre-implementation. The right time to act on this evaluation is when `caravan-rpc` and `caravan-rpc` ship v0.1 (M3 in the development plan) — at which point this document becomes the input to a focused refactor plan. + +--- + +## 8. Key Reference Files + +**invoice-parse — backend (Caravan-relevant):** +- [services/ingestion/](../services/ingestion/) — Rust ingest service (entry: cron/http) +- [services/processing/invoice_processing/ocr.py](../services/processing/invoice_processing/ocr.py) — seam 4.1 (PaddleOCR text) +- [services/processing/invoice_processing/table_extract.py](../services/processing/invoice_processing/table_extract.py) — seam 4.2 (PPStructure layout) +- [services/processing/invoice_processing/extraction.py](../services/processing/invoice_processing/extraction.py) — seam 4.3 (LLMExtractor ABC + Gemini impl + Claude/OpenAI stubs) +- [services/processing/invoice_processing/worker.py](../services/processing/invoice_processing/worker.py) — the worker loop that wires it all together +- [services/output/](../services/output/) — Rust output service (entry: batch/http) +- [libs/shared-py/invoice_shared/adapters/blob_store.py](../libs/shared-py/invoice_shared/adapters/blob_store.py) — bucket resource adapter (composition target) +- [libs/shared-py/invoice_shared/adapters/queue.py](../libs/shared-py/invoice_shared/adapters/queue.py) — queue resource adapter (composition target) +- [libs/shared-py/invoice_shared/adapters/factory.py](../libs/shared-py/invoice_shared/adapters/factory.py) — composition selection (the existing analog of Caravan's `composition:` block) +- [libs/shared-py/invoice_shared/db.py](../libs/shared-py/invoice_shared/db.py) — Postgres state machine (db.sql resource) +- [docs/architecture.md](architecture.md) — current design decisions +- [infra/docker-compose.yaml](../infra/docker-compose.yaml) — current multi-service runtime + +**invoice-parse — frontend (out of Caravan scope, listed for completeness):** +- [services/dashboard/](../services/dashboard/) — Streamlit; reads Postgres directly. FE bundling and hosting concerns are not Caravan's problem. + +**Caravan (sibling repo, `../caravan/`):** +- `thesis.md` — three orthogonal dimensions, source-unchanged principle +- `docs/poc_rpc_sdk.md` — SDK surface, wire contract, dispatch table +- `docs/poc_yaml_spec.md` — yaml schema, entries vs seams +- `docs/poc_groups_to_code.md` — 10 resource groups, local↔cloud swaps +- `cmd/caravan/main.go` — stub CLI, current state proof diff --git a/docs/pre-change-state.md b/docs/pre-change-state.md new file mode 100644 index 0000000..1e10918 --- /dev/null +++ b/docs/pre-change-state.md @@ -0,0 +1,226 @@ +# invoice-parse — Pre-Change State + +Snapshot of invoice-parse immediately before any Caravan-conversion changes begin on the `caravan-conversion` branch. Reference point for verifying that conversion work doesn't regress any of the four pre-existing deployment surfaces. + +## Repo and branch + +- **Repo**: `invoice-parse` +- **Branch**: `caravan-conversion` +- **Baseline commit**: `97fdb9b0ba831b5bda9d167fe42f5e05ba9f23ce` +- **Conversion target**: this repo is the **B0 bootstrap host** — milestone B0 in `caravan/docs/development_plan.md` lands here. The `LLMExtraction` seam is the lived spec for the entire SDK contract. + +## What this doc is + +The "what existed before" reference for Caravan-conversion. If any verification command below fails after a conversion-related commit, that commit regressed something. invoice-parse is more structurally-ready than code-rag (HIGH ~85% per `caravan-readiness.md` vs ~80% for code-rag), which is why it was selected as the bootstrap target. + +## Code structure + +Three backend services + one frontend dashboard + two shared library directories. Detailed per-service breakdown in [caravan-readiness.md](caravan-readiness.md) §3. + +``` +invoice-parse/ +├── services/ +│ ├── ingestion/ (Rust — IMAP/file ingest; entry: http or cron) +│ │ ├── Cargo.toml +│ │ ├── src/main.rs +│ │ └── Dockerfile +│ ├── processing/ (Python — OCR + LLM + validation; entry: http or batch) +│ │ ├── pyproject.toml +│ │ ├── invoice_processing/ +│ │ │ ├── __main__.py +│ │ │ ├── cli.py (single-file CLI; no DB/queue) +│ │ │ ├── worker.py (queue consumer loop; main entry) +│ │ │ ├── ocr.py ← seam candidate (PaddleOCR raw text) +│ │ │ ├── table_extract.py ← seam candidate (PPStructureV3) +│ │ │ ├── extraction.py ← seam candidate (LLMExtractor ABC + Gemini) +│ │ │ └── validation.py +│ │ └── Dockerfile +│ ├── output/ (Rust — Excel gen + delivery; entry: batch or http) +│ │ ├── Cargo.toml +│ │ ├── src/main.rs +│ │ └── Dockerfile +│ └── dashboard/ (Python/Streamlit; OUT OF CARAVAN SCOPE — frontend-ish) +│ ├── pyproject.toml +│ ├── dashboard/app.py +│ └── Dockerfile +├── libs/ +│ ├── shared-py/ (Python shared lib — used by processing + dashboard) +│ │ └── invoice_shared/ +│ │ ├── db.py (SQLAlchemy ORM + state transitions) +│ │ ├── models.py (Pydantic DTOs) +│ │ ├── config.py (YAML loader) +│ │ └── adapters/ +│ │ ├── blob_store.py ← resource: bucket (local FS impl) +│ │ ├── queue.py ← resource: queue (Redis Streams impl) +│ │ └── factory.py (local↔cloud factory pattern) +│ └── shared-rs/ (Rust shared lib — used by ingestion + output; FFI not RPC) +├── config/ +│ ├── local.yaml +│ ├── docker.yaml +│ └── production.yaml +├── infra/ +│ └── docker-compose.yaml (single compose, profile-based; Postgres + Redis + services) +├── migrations/ (SQL schema) +├── invoices/ (sample invoice files for testing) +├── seed.sql +├── .github/workflows/ +│ └── pages.yml (browser demo deploy) +├── demo/ (TypeScript/Vite browser pipeline; OUT OF CARAVAN SCOPE) +│ ├── package.json +│ ├── tsconfig.json +│ └── src/ +└── docs/ + ├── architecture.md + ├── caravan-readiness.md (seam-by-seam evaluation — HIGH ~85%) + └── pre-change-state.md (this file) +``` + +## Existing deployment surfaces + +Each surface is a distinct way the codebase runs today. Caravan-conversion must preserve all four. + +### 1. Docker compose, `--profile app` + +**Invocation:** `docker compose -f infra/docker-compose.yaml --profile app up -d` + +**What runs:** Postgres 18 + Redis 8 + model-init one-shot (downloads PaddleOCR models on first run) + processing worker + output worker + Streamlit dashboard. + +**Build context:** invoice-parse root (per-service Dockerfile at `services//Dockerfile`). + +**Persistence requirements:** +- Named volumes: `pgdata` (Postgres), `redisdata` (Redis), `blobdata` (shared `/data/blobs` between services), `paddlex_models` (PaddleOCR models, ~500MB). +- `.env` with `GEMINI_API_KEY`. +- Postgres migrations + seed run automatically via `/docker-entrypoint-initdb.d/`. + +### 2. Docker compose, `--profile ingest` + +**Invocation:** `docker compose -f infra/docker-compose.yaml --profile ingest run --rm ingest` + +**What runs:** One-shot ingestion service — enqueues all PDFs from `../invoices/` onto Redis Streams queue A. Exits when done. + +**Persistence requirements:** Same compose volumes as #1; reads from host `../invoices/` mounted at `/app/invoices:ro`. + +### 3. Local non-container run + +Postgres + Redis still in docker; application services run on host. + +**Invocations** (per README "Quick Start (Local Development)"): +```bash +# Infra in docker +docker compose -f infra/docker-compose.yaml up -d # without --profile, brings up just postgres + redis + +# Install Python deps +pip install -e libs/shared-py +pip install -e services/processing + +# Single invoice (CLI, no queue) +GEMINI_API_KEY=xxx python -m invoice_processing.cli invoices/sample_invoice.pdf -v + +# Full pipeline (queue mode) — all from project root +cargo run --manifest-path services/ingestion/Cargo.toml -- invoices/ +PIPELINE_CACHE=1 python -m invoice_processing.worker +cargo run --manifest-path services/output/Cargo.toml +streamlit run services/dashboard/dashboard/app.py +``` + +**Caravan implication:** the SDK must work for processes started directly with `python -m` and `cargo run` against host-local Postgres/Redis with **no `CARAVAN_RPC_PEERS` env var set**. No-config inertness is the load-bearing acceptance criterion for B0. + +### 4. GitHub Actions: pages deploy + +**Workflow:** [.github/workflows/pages.yml](../.github/workflows/pages.yml). Triggered on push to `demo/**` or manually. + +**What runs:** Builds a TypeScript/Vite app under `demo/` (npm ci → npm run build → upload `demo/dist/` to GitHub Pages). The app uses `paddleocr` (npm), `@google/genai`, `@napi-rs/canvas`, `wasm-xlsxwriter` to run the **entire invoice parsing pipeline in the browser**. + +**Caravan implication:** the `demo/` browser pipeline is out of Caravan scope (frontend), but it constrains the engine — Python and Rust shared libs in `libs/` must not pull in any dependency that would be incompatible with the browser counterpart. (In practice this is enforced by them being separate codebases — `demo/` is a TypeScript re-implementation, not a build target of the Python/Rust code.) + +## Seam candidates and resource bindings + +Cross-reference: [caravan-readiness.md](caravan-readiness.md) §4 (full per-seam evaluation, HIGH ~85% verdict — slightly ahead of code-rag). + +### Seams (to be declared via `caravan-rpc-py` / `caravan-rpc`) +- **LLMExtraction** (B0 bootstrap target) — `services/processing/invoice_processing/extraction.py`. Currently an `LLMExtractor` ABC + factory selecting Gemini/Claude/OpenAI. Already-abstracted; minimal lift to declare as `@wagon`. +- **OCRText** (M6) — `services/processing/invoice_processing/ocr.py`. Concrete `PaddleOCR(...)` instantiation; ~500MB model warm. To be wrapped via `provide(OCRText, OCREngine())`. +- **OCRLayout** (M6) — `services/processing/invoice_processing/table_extract.py`. `PPStructureV3(...)` instantiation. To be wrapped via `provide(OCRLayout, PPStructureAdapter())`. + +### Resources (Caravan composition axis — NOT seams) +- **queue** group: `libs/shared-py/invoice_shared/adapters/queue.py` (Redis Streams impl), `factory.py` (SQS stubbed). Maps to Caravan `queue` resource (Redis ↔ SQS). +- **bucket** group: `libs/shared-py/invoice_shared/adapters/blob_store.py` (local FS impl), `factory.py` (S3 stubbed). Maps to Caravan `bucket` resource (local FS ↔ S3). +- **db.sql** group: `libs/shared-py/invoice_shared/db.py` (Postgres state machine, 16 states). Maps to Caravan `db.sql` resource (Postgres ↔ Aurora). +- **cache** group: Redis is also used as a cache (`PIPELINE_CACHE=1` env var enables file-based caching of LLM responses). Currently file-based; could move to Redis or ElastiCache under Caravan. +- **llm** group: bound by the LLMExtraction seam (Gemini ↔ Bedrock ↔ Claude — Tier-1 hard-pair). + +### Entry kinds +- `http` or `cron`: `services/ingestion/` — Rust async, currently IMAP-stub + file ingest. +- `http` or `batch`: `services/processing/` — Python worker loop, queue consumer. +- `http` or `batch`: `services/output/` — Rust async, queue consumer. + +### Out of Caravan scope +- `services/dashboard/` — Streamlit, reads Postgres directly. Frontend-ish. +- `demo/` — TypeScript/Vite browser pipeline. Frontend, replicates the BE pipeline client-side. +- `libs/shared-rs/` — FFI boundary called from Rust services in-process. Not a Caravan seam (FFI ≠ RPC). + +## Invariants to preserve + +Caravan-conversion commits must not regress any of these: + +1. **`docker compose -f infra/docker-compose.yaml --profile app up -d`** brings up Postgres + Redis + model-init + processing + output + dashboard. Dashboard reachable at http://localhost:8501. +2. **`docker compose -f infra/docker-compose.yaml --profile ingest run --rm ingest`** completes successfully (enqueues 17 jobs from `invoices/`). +3. **`python -m invoice_processing.cli `** processes a single invoice end-to-end given a valid `GEMINI_API_KEY`. +4. **`python -m invoice_processing.worker`** consumes the queue end-to-end against host-local Postgres + Redis. +5. **`cargo run --manifest-path services/ingestion/Cargo.toml -- invoices/`** enqueues invoices successfully. +6. **`cargo run --manifest-path services/output/Cargo.toml`** consumes queue B and produces Excel output. +7. **`streamlit run services/dashboard/dashboard/app.py`** opens the dashboard against host-local Postgres. +8. **`cd demo && npm run build`** succeeds (browser pipeline build artifact). +9. **No-config inertness** — after SDK wrapping, all of #3–#7 run with no `CARAVAN_RPC_PEERS` set, identical behavior to pre-conversion. **This is the load-bearing acceptance for B0.** + +## Verification commands + +Run on the baseline commit `97fdb9b0ba831b5bda9d167fe42f5e05ba9f23ce` and after any conversion commit: + +```bash +# 1. All compose targets build. +docker compose -f infra/docker-compose.yaml build + +# 2. App profile boots and dashboard responds. +docker compose -f infra/docker-compose.yaml --profile app up -d +curl -sf http://localhost:8501/ > /dev/null && echo "dashboard OK" +docker compose -f infra/docker-compose.yaml --profile app down + +# 3. Ingest profile runs to completion (assumes app profile not running; uses isolated infra). +docker compose -f infra/docker-compose.yaml up -d postgres redis +docker compose -f infra/docker-compose.yaml --profile ingest run --rm ingest +docker compose -f infra/docker-compose.yaml down + +# 4. Python local-run smoke (CLI; requires GEMINI_API_KEY). +GEMINI_API_KEY=$GEMINI_API_KEY python -m invoice_processing.cli invoices/sample_invoice.pdf -v + +# 5. Worker can start (does NOT need to consume real messages; just import + init). +timeout 5s python -m invoice_processing.worker || true + +# 6. Rust services build. +cargo build --release --manifest-path services/ingestion/Cargo.toml +cargo build --release --manifest-path services/output/Cargo.toml + +# 7. Streamlit launches. +timeout 5s streamlit run services/dashboard/dashboard/app.py --server.headless=true || true + +# 8. Browser demo builds. +cd demo && npm ci && npm run build && cd .. +``` + +After Caravan conversion (B0 onward), additionally: + +```bash +# 9. No-config inertness — local-run, no env var set, behavior unchanged. +unset CARAVAN_RPC_PEERS +python -m invoice_processing.worker # should consume queue exactly as before B0 + +# 10. Inproc with env var set explicitly. +CARAVAN_RPC_PEERS='{"LLMExtraction":{"mode":"inproc"}}' python -m invoice_processing.worker + +# 11. HTTP with env var + sidecar. +CARAVAN_RPC_PEERS='{"LLMExtraction":{"mode":"http","url":"http://localhost:8080"}}' python -m invoice_processing.worker +# Requires `python -m caravan_rpc.serve LLMExtraction` running on :8080 +``` + +The six-criteria B0 acceptance matrix lives in [../../caravan/docs/development_plan.md](../../caravan/docs/development_plan.md) §B0. diff --git a/libs/shared-py/invoice_shared.egg-info/PKG-INFO b/libs/shared-py/invoice_shared.egg-info/PKG-INFO index 1b9604c..c7b307c 100644 --- a/libs/shared-py/invoice_shared.egg-info/PKG-INFO +++ b/libs/shared-py/invoice_shared.egg-info/PKG-INFO @@ -8,5 +8,8 @@ Requires-Dist: sqlalchemy<3,>=2.0 Requires-Dist: psycopg<4,>=3.2 Requires-Dist: redis<8,>=7.3 Requires-Dist: pyyaml<7,>=6.0 +Requires-Dist: boto3<2,>=1.35 +Requires-Dist: pika<2,>=1.3 Provides-Extra: dev Requires-Dist: pytest>=8.0; extra == "dev" +Requires-Dist: moto<6,>=5.0; extra == "dev" diff --git a/libs/shared-py/invoice_shared.egg-info/requires.txt b/libs/shared-py/invoice_shared.egg-info/requires.txt index 7af9909..d660ac7 100644 --- a/libs/shared-py/invoice_shared.egg-info/requires.txt +++ b/libs/shared-py/invoice_shared.egg-info/requires.txt @@ -3,6 +3,9 @@ sqlalchemy<3,>=2.0 psycopg<4,>=3.2 redis<8,>=7.3 pyyaml<7,>=6.0 +boto3<2,>=1.35 +pika<2,>=1.3 [dev] pytest>=8.0 +moto<6,>=5.0 diff --git a/libs/shared-py/invoice_shared/adapters/blob_store.py b/libs/shared-py/invoice_shared/adapters/blob_store.py index 2931411..690ed65 100644 --- a/libs/shared-py/invoice_shared/adapters/blob_store.py +++ b/libs/shared-py/invoice_shared/adapters/blob_store.py @@ -1,7 +1,16 @@ -"""Blob storage adapter — abstract interface + local filesystem implementation.""" +"""Blob storage adapter — abstract interface + local filesystem + S3 implementations. + +The factory in ``factory.py`` selects between impls based on env-var presence +(Caravan-injected ``S3_ENDPOINT_URL`` / ``AWS_*``) with a YAML-config fallback +for the no-Caravan local-dev path. ``S3BlobStore`` uses boto3 against the +endpoint URL — works against MinIO under docker compose (M4 oss-local +composition) and against real AWS S3 when the endpoint is unset (M4-cloud / +Phase 2). +""" from __future__ import annotations +import os import re from abc import ABC, abstractmethod from pathlib import Path @@ -65,3 +74,94 @@ def delete(self, path: str) -> None: p = self._safe_path(path) if p.exists(): p.unlink() + + +class S3BlobStore(BlobStore): + """S3-protocol blob store via boto3. + + Constructor takes a bucket name plus optional endpoint URL + creds + + region. Same code path works against MinIO (compose, endpoint set) and + against real AWS S3 (cloud, endpoint omitted, IAM-resolved creds). + + Path safety: the same UUID-segment + path-traversal validation as + ``LocalFsBlobStore`` applies — S3 doesn't have hierarchical safety + semantics, but consistent tenant_id/job_id key shape prevents accidents. + """ + + def __init__( + self, + bucket: str, + endpoint_url: str | None = None, + access_key_id: str | None = None, + secret_access_key: str | None = None, + region: str | None = None, + ) -> None: + import boto3 + + self._bucket = bucket + kwargs: dict = {} + if endpoint_url: + kwargs["endpoint_url"] = endpoint_url + if access_key_id: + kwargs["aws_access_key_id"] = access_key_id + if secret_access_key: + kwargs["aws_secret_access_key"] = secret_access_key + if region: + kwargs["region_name"] = region + self._client = boto3.client("s3", **kwargs) + # Best-effort bucket creation under MinIO; in production AWS the + # bucket is provisioned out-of-band and this call may fail with + # AccessDenied — that's fine. + try: + self._client.create_bucket(Bucket=bucket) + except Exception: # noqa: BLE001 — broad-but-bounded: any error means "bucket exists or no perm" + pass + + @classmethod + def from_env(cls) -> S3BlobStore: + """Construct from Caravan-injected env vars. + + Reads ``S3_ENDPOINT_URL`` (required — env-var presence is what + signals "use S3 instead of LocalFs"), ``AWS_ACCESS_KEY_ID``, + ``AWS_SECRET_ACCESS_KEY``, ``AWS_REGION`` (all optional — + AWS resolution chain handles missing values). Bucket name from + ``S3_BUCKET`` env var or falls back to ``invoice-parse``. + """ + return cls( + bucket=os.environ.get("S3_BUCKET", "invoice-parse"), + endpoint_url=os.environ.get("S3_ENDPOINT_URL"), + access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), + secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY"), + region=os.environ.get("AWS_REGION"), + ) + + def _validate_key(self, path: str) -> str: + if ".." in path: + raise ValueError(f"Path traversal detected in blob path: {path}") + key = path.lstrip("/") + # UUID validation on first two segments (tenant_id/job_id) when present + parts = key.split("/", 2) + if len(parts) >= 2: + tenant_id, job_id = parts[0], parts[1] + if not _UUID_RE.match(tenant_id): + raise ValueError(f"Invalid tenant_id in blob path: {tenant_id}") + if not _UUID_RE.match(job_id): + raise ValueError(f"Invalid job_id in blob path: {job_id}") + return key + + def put(self, path: str, data: bytes) -> None: + self._client.put_object(Bucket=self._bucket, Key=self._validate_key(path), Body=data) + + def get(self, path: str) -> bytes: + response = self._client.get_object(Bucket=self._bucket, Key=self._validate_key(path)) + return response["Body"].read() + + def exists(self, path: str) -> bool: + try: + self._client.head_object(Bucket=self._bucket, Key=self._validate_key(path)) + return True + except self._client.exceptions.ClientError: + return False + + def delete(self, path: str) -> None: + self._client.delete_object(Bucket=self._bucket, Key=self._validate_key(path)) diff --git a/libs/shared-py/invoice_shared/adapters/factory.py b/libs/shared-py/invoice_shared/adapters/factory.py index 2434085..e48582b 100644 --- a/libs/shared-py/invoice_shared/adapters/factory.py +++ b/libs/shared-py/invoice_shared/adapters/factory.py @@ -1,31 +1,77 @@ -"""Factory functions — create adapters from config.""" +"""Factory functions — create adapters with env-var-presence + YAML fallback. + +M6 refactor: Caravan-injected env vars take precedence; YAML config is the +fallback so the local non-container run (surface 3 in the deployment +inventory) still works without spinning up MinIO / RabbitMQ. + +Precedence rules: +- BlobStore: ``S3_ENDPOINT_URL`` set → ``S3BlobStore.from_env()``; else + YAML config ``blob_storage.type``. +- Queue: ``QUEUE_URL`` set + scheme ``amqp://`` → ``RabbitMQQueue.from_url(...)``; + scheme ``redis://`` → ``RedisStreamQueue`` against the env URL; else YAML + config ``queue.type``. +""" from __future__ import annotations +import os +from urllib.parse import urlparse + from ..config import AppConfig -from .blob_store import BlobStore, LocalFsBlobStore -from .queue import MessageQueue, RedisStreamQueue +from .blob_store import BlobStore, LocalFsBlobStore, S3BlobStore +from .queue import MessageQueue, RabbitMQQueue, RedisStreamQueue def create_blob_store(config: AppConfig) -> BlobStore: + # Caravan-injected env vars win. + if os.environ.get("S3_ENDPOINT_URL"): + return S3BlobStore.from_env() + + # YAML fallback — local-dev path without Caravan. match config.blob_storage.type: case "local_fs": if not config.blob_storage.base_path: raise ValueError("local_fs blob storage requires base_path") return LocalFsBlobStore(config.blob_storage.base_path) case "s3": - raise NotImplementedError("S3BlobStore not yet implemented") + if not config.blob_storage.bucket: + raise ValueError("s3 blob storage requires bucket (env S3_ENDPOINT_URL unset; falling back to YAML)") + return S3BlobStore( + bucket=config.blob_storage.bucket, + region=config.blob_storage.region, + ) case _: raise ValueError(f"Unknown blob storage type: {config.blob_storage.type}") def create_queue(config: AppConfig) -> MessageQueue: + consumer_group = config.queue.consumer_group + + # Caravan-injected env vars win. + env_url = os.environ.get("QUEUE_URL", "") + if env_url: + scheme = urlparse(env_url).scheme + if scheme in ("amqp", "amqps"): + return RabbitMQQueue.from_url(env_url) + if scheme in ("redis", "rediss"): + return RedisStreamQueue(env_url, consumer_group) + # SQS https://sqs..amazonaws.com/... → not yet implemented + raise NotImplementedError( + f"QUEUE_URL scheme {scheme!r} not supported in Phase 1 (compose-only). " + "Cloud variants (SQS) ship at M4-cloud." + ) + + # YAML fallback — local-dev path without Caravan. match config.queue.type: case "redis_stream": if not config.queue.url: raise ValueError("redis_stream queue requires url") - return RedisStreamQueue(config.queue.url, config.queue.consumer_group) + return RedisStreamQueue(config.queue.url, consumer_group) + case "rabbitmq": + if not config.queue.url: + raise ValueError("rabbitmq queue requires url") + return RabbitMQQueue(config.queue.url) case "sqs": - raise NotImplementedError("SqsQueue not yet implemented") + raise NotImplementedError("SqsQueue not yet implemented (lands at M4-cloud)") case _: raise ValueError(f"Unknown queue type: {config.queue.type}") diff --git a/libs/shared-py/invoice_shared/adapters/queue.py b/libs/shared-py/invoice_shared/adapters/queue.py index c473cfa..c607ec8 100644 --- a/libs/shared-py/invoice_shared/adapters/queue.py +++ b/libs/shared-py/invoice_shared/adapters/queue.py @@ -1,11 +1,20 @@ -"""Queue adapter — abstract interface + Redis Streams implementation.""" +"""Queue adapter — abstract interface + Redis Streams + RabbitMQ implementations. + +The factory in ``factory.py`` selects between impls based on +``QUEUE_URL`` env-var scheme: + redis://... → RedisStreamQueue (default; Caravan oss-local queue/redis-streams variant) + amqp://... → RabbitMQQueue (Caravan oss-local queue/rabbitmq variant) +SQS-protocol URLs (https://sqs..amazonaws.com/...) → not yet implemented; +those become first-class at M4-cloud / Phase 2. +""" from __future__ import annotations import json import uuid from abc import ABC, abstractmethod -from typing import Any, Callable +from typing import Any +from urllib.parse import urlparse import redis @@ -89,3 +98,104 @@ def pending(self, topic: str) -> list[dict[str, Any]]: return self._client.xpending_range( topic, self._consumer_group, min="-", max="+", count=100 ) + + +class RabbitMQQueue(MessageQueue): + """RabbitMQ-backed MessageQueue via pika (synchronous channel). + + Maps the ABC onto RabbitMQ primitives: + topic → queue name (declared durable on first touch) + publish → basic_publish (default exchange + routing_key=topic) + consume → basic_get with manual ack semantics + ack → basic_ack + extend_visibility → no-op (RabbitMQ has no SQS-style visibility timeout; + unacked messages requeue when the channel closes) + + Same wire-call surface as RedisStreamQueue so callers swap by URL scheme. + Connection lazily opened on first call; reconnects are not handled here + (callers running long-lived workers should wrap consume in a try/except + and reinit on connection drops — M6 PoC ships the happy path). + """ + + def __init__(self, url: str) -> None: + self._url = url + self._connection = None + self._channel = None + self._declared_queues: set[str] = set() + + @classmethod + def from_url(cls, url: str) -> RabbitMQQueue: + scheme = urlparse(url).scheme + if scheme not in ("amqp", "amqps"): + raise ValueError(f"RabbitMQQueue expects amqp://... or amqps://...; got scheme {scheme!r}") + return cls(url) + + def _channel_(self): + if self._channel is not None and not self._channel.is_closed: + return self._channel + import pika + + params = pika.URLParameters(self._url) + self._connection = pika.BlockingConnection(params) + self._channel = self._connection.channel() + return self._channel + + def _ensure_queue(self, topic: str) -> None: + if topic in self._declared_queues: + return + ch = self._channel_() + ch.queue_declare(queue=topic, durable=True) + self._declared_queues.add(topic) + + def publish(self, topic: str, message: dict) -> str: + self._ensure_queue(topic) + msg_id = uuid.uuid4().hex + import pika + + ch = self._channel_() + ch.basic_publish( + exchange="", + routing_key=topic, + body=json.dumps(message).encode(), + properties=pika.BasicProperties( + message_id=msg_id, + delivery_mode=2, # persistent + content_type="application/json", + ), + ) + return msg_id + + def consume(self, topic: str, count: int = 1, block_ms: int = 5000) -> list[tuple[str, dict]]: + """Pull-style consume to match RedisStreamQueue semantics. + + block_ms is honored by spinning until either ``count`` messages + arrive or the budget elapses. Returns the partial batch on + timeout. Each tuple is ``(delivery_tag_str, payload_dict)`` — + delivery_tag is passed back into ack(). + """ + import time + + self._ensure_queue(topic) + ch = self._channel_() + deadline = time.monotonic() + block_ms / 1000.0 + out: list[tuple[str, dict]] = [] + while len(out) < count: + method, _props, body = ch.basic_get(queue=topic, auto_ack=False) + if method is None: + if time.monotonic() >= deadline: + break + time.sleep(0.05) + continue + payload = json.loads(body.decode()) + out.append((str(method.delivery_tag), payload)) + return out + + def ack(self, topic: str, message_id: str) -> None: + ch = self._channel_() + ch.basic_ack(delivery_tag=int(message_id)) + + def extend_visibility(self, topic: str, message_id: str, seconds: int) -> None: + # RabbitMQ doesn't have SQS-style visibility timeout. Unacked + # messages requeue when the channel closes; long-running consumers + # are expected to ack within their own internal SLA. + pass diff --git a/libs/shared-py/invoice_shared/config.py b/libs/shared-py/invoice_shared/config.py index ccd274a..dcdacdf 100644 --- a/libs/shared-py/invoice_shared/config.py +++ b/libs/shared-py/invoice_shared/config.py @@ -36,16 +36,24 @@ class AppConfig(BaseModel): def load_config(path: str | Path | None = None) -> AppConfig: - """Load config from YAML file. + """Load config from YAML file, with env-var overrides. - Resolution order: + Resolution order for the YAML path: 1. Explicit path argument 2. INVOICE_PARSE_CONFIG env var 3. config/local.yaml (relative to cwd) + + Caravan-injected env vars override individual YAML fields (M6): + - DATABASE_URL → database.url """ if path is None: path = os.environ.get("INVOICE_PARSE_CONFIG", "config/local.yaml") path = Path(path) with open(path) as f: raw = yaml.safe_load(f) - return AppConfig(**raw) + cfg = AppConfig(**raw) + + if db_url := os.environ.get("DATABASE_URL"): + cfg.database.url = db_url + + return cfg diff --git a/libs/shared-py/invoice_shared/db.py b/libs/shared-py/invoice_shared/db.py index 68abe6b..047751b 100644 --- a/libs/shared-py/invoice_shared/db.py +++ b/libs/shared-py/invoice_shared/db.py @@ -93,7 +93,16 @@ class InvalidTransitionError(Exception): def engine_from_config(config: AppConfig): - return create_engine(config.database.url) + return create_engine(_psycopg3_url(config.database.url)) + + +def _psycopg3_url(url: str) -> str: + # SQLAlchemy resolves `postgresql://` to the psycopg2 driver by default; + # shared-py declares psycopg (v3) in pyproject so make that explicit. + # Already-qualified URLs (postgresql+://...) pass through. + if url.startswith("postgresql://"): + return "postgresql+psycopg://" + url[len("postgresql://"):] + return url def session_factory(config: AppConfig) -> sessionmaker[Session]: diff --git a/libs/shared-py/pyproject.toml b/libs/shared-py/pyproject.toml index 72bd113..73c3623 100644 --- a/libs/shared-py/pyproject.toml +++ b/libs/shared-py/pyproject.toml @@ -13,11 +13,16 @@ dependencies = [ "psycopg>=3.2,<4", "redis>=7.3,<8", "pyyaml>=6.0,<7", + # M6 — Caravan oss-local resource adapters (compose targets only; + # M4-cloud may swap to native cloud SDKs). + "boto3>=1.35,<2", + "pika>=1.3,<2", ] [project.optional-dependencies] dev = [ "pytest>=8.0", + "moto>=5.0,<6", ] [tool.setuptools.packages.find] diff --git a/libs/shared-rs/Cargo.lock b/libs/shared-rs/Cargo.lock index 0f7256b..a9c54f5 100644 --- a/libs/shared-rs/Cargo.lock +++ b/libs/shared-rs/Cargo.lock @@ -2,12 +2,71 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "amq-protocol" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "587d313f3a8b4a40f866cc84b6059fe83133bf172165ac3b583129dd211d8e1c" +dependencies = [ + "amq-protocol-tcp", + "amq-protocol-types", + "amq-protocol-uri", + "cookie-factory", + "nom", + "serde", +] + +[[package]] +name = "amq-protocol-tcp" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc707ab9aa964a85d9fc25908a3fdc486d2e619406883b3105b48bf304a8d606" +dependencies = [ + "amq-protocol-uri", + "tcp-stream", + "tracing", +] + +[[package]] +name = "amq-protocol-types" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf99351d92a161c61ec6ecb213bc7057f5b837dd4e64ba6cb6491358efd770c4" +dependencies = [ + "cookie-factory", + "nom", + "serde", + "serde_json", +] + +[[package]] +name = "amq-protocol-uri" +version = "7.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f89f8273826a676282208e5af38461a07fe939def57396af6ad5997fcf56577d" +dependencies = [ + "amq-protocol-types", + "percent-encoding", + "url", +] + [[package]] name = "android_system_properties" version = "0.1.5" @@ -30,1059 +89,2981 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" [[package]] -name = "atoi" -version = "2.0.0" +name = "asn1-rs" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", "num-traits", + "rusticata-macros", + "thiserror", + "time", ] [[package]] -name = "autocfg" -version = "1.5.0" +name = "asn1-rs-derive" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] [[package]] -name = "base64" -version = "0.22.1" +name = "asn1-rs-impl" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "base64ct" -version = "1.8.3" +name = "async-channel" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] [[package]] -name = "bitflags" -version = "2.11.0" +name = "async-executor" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ - "serde_core", + "async-task", + "concurrent-queue", + "fastrand 2.3.0", + "futures-lite 2.6.1", + "pin-project-lite", + "slab", ] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "async-global-executor" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "13f937e26114b93193065fd44f507aa2e9169ad0cdabbb996920b1fe1ddea7ba" dependencies = [ - "generic-array", + "async-channel", + "async-executor", + "async-io 2.6.0", + "async-lock 3.4.2", + "blocking", + "futures-lite 2.6.1", ] [[package]] -name = "bumpalo" -version = "3.20.2" +name = "async-global-executor-trait" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "9af57045d58eeb1f7060e7025a1631cbc6399e0a1d10ad6735b3d0ea7f8346ce" +dependencies = [ + "async-global-executor", + "async-trait", + "executor-trait", +] [[package]] -name = "byteorder" -version = "1.5.0" +name = "async-io" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.28", + "slab", + "socket2 0.4.10", + "waker-fn", +] [[package]] -name = "bytes" -version = "1.11.1" +name = "async-io" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.6.1", + "parking", + "polling 3.11.0", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] [[package]] -name = "cc" -version = "1.2.57" +name = "async-lock" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ - "find-msvc-tools", - "shlex", + "event-listener 2.5.3", ] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "async-lock" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener 5.4.1", + "event-listener-strategy", + "pin-project-lite", +] [[package]] -name = "chrono" -version = "0.4.44" +name = "async-reactor-trait" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "7a6012d170ad00de56c9ee354aef2e358359deb1ec504254e0e5a3774771de0e" dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", + "async-io 1.13.0", + "async-trait", + "futures-core", + "reactor-trait", ] [[package]] -name = "combine" -version = "4.6.7" +name = "async-task" +version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "concurrent-queue" -version = "2.5.0" +name = "atoi" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ - "crossbeam-utils", + "num-traits", ] [[package]] -name = "const-oid" -version = "0.9.6" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "autocfg" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "aws-config" +version = "1.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "517aa062d8bd9015ee23d6daa5e1c1372328412fdae4e6c4c1be9b69c6ad37a2" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "hex", + "http 1.4.0", + "sha1 0.10.6", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ - "libc", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", ] [[package]] -name = "crc" -version = "3.4.0" +name = "aws-lc-rs" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" dependencies = [ - "crc-catalog", + "aws-lc-sys", + "zeroize", ] [[package]] -name = "crc-catalog" -version = "2.4.0" +name = "aws-lc-sys" +version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] [[package]] -name = "crossbeam-queue" -version = "0.3.12" +name = "aws-runtime" +version = "1.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" dependencies = [ - "crossbeam-utils", + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "aws-sdk-s3" +version = "1.133.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "237aba2985e3c0a83e199cc7aa9a64a16c599875bc98170f00932f6199f19922" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand 2.3.0", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] [[package]] -name = "crypto-common" -version = "0.1.7" +name = "aws-sdk-sso" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "9f4055e6099b2ec264abdc0d9bbfffce306c1601809275c861594779a0b04b45" dependencies = [ - "generic-array", - "typenum", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", ] [[package]] -name = "der" -version = "0.7.10" +name = "aws-sdk-ssooidc" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "02f009ba0284c5d696425fd7b4dcc5b189f5726f4041b7a5794daecb3a68d598" dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", ] [[package]] -name = "digest" -version = "0.10.7" +name = "aws-sdk-sts" +version = "1.104.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aa6622798e19e6a76b690562085dd4771c736cd48343464a53ab4ae2f2c9f84" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "b7083fb918b38474ac65ffbf8a69fc8792d36879f4ac5f1667b43aec61efe9a5" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "sha2 0.11.0", "subtle", + "time", + "tracing", + "zeroize", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "aws-smithy-async" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "futures-util", + "pin-project-lite", + "tokio", ] [[package]] -name = "dotenvy" -version = "0.15.7" +name = "aws-smithy-checksums" +version = "0.64.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" +checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5 0.11.0", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] [[package]] -name = "either" -version = "1.15.0" +name = "aws-smithy-eventstream" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ - "serde", + "aws-smithy-types", + "bytes", + "crc32fast", ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "aws-smithy-http" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + [[package]] -name = "errno" -version = "0.3.14" +name = "aws-smithy-http-client" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" dependencies = [ - "libc", - "windows-sys 0.61.2", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.14", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.9", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", ] [[package]] -name = "etcetera" -version = "0.8.0" +name = "aws-smithy-json" +version = "0.62.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "517089205f18ab4adc5a3e02888cb139bbbbb2e168eac9f396216925d1fbeaf5" dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", ] [[package]] -name = "event-listener" -version = "5.4.1" +name = "aws-smithy-observability" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", + "aws-smithy-runtime-api", ] [[package]] -name = "fastrand" -version = "2.3.0" +name = "aws-smithy-query" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "aws-smithy-runtime" +version = "1.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] [[package]] -name = "flume" -version = "0.11.1" +name = "aws-smithy-runtime-api" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +checksum = "dc117c179ecf39a62a0a3f49f600e9ac26a7ad7dd172177999f83933af776c32" dependencies = [ - "futures-core", - "futures-sink", - "spin", + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", ] [[package]] -name = "foldhash" -version = "0.1.5" +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] -name = "form_urlencoded" -version = "1.2.2" +name = "aws-smithy-schema" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" dependencies = [ - "percent-encoding", + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", ] [[package]] -name = "futures-channel" -version = "0.3.32" +name = "aws-smithy-types" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "056b66dbce2f81cc0c1e2b05bb402eb58f8a3530479d650efadd5bbae9a4050b" dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", "futures-core", - "futures-sink", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", ] [[package]] -name = "futures-core" -version = "0.3.32" +name = "aws-smithy-xml" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] [[package]] -name = "futures-executor" -version = "0.3.32" +name = "aws-types" +version = "1.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" dependencies = [ - "futures-core", - "futures-task", - "futures-util", + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version", + "tracing", ] [[package]] -name = "futures-intrusive" -version = "0.5.0" +name = "base16ct" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" [[package]] -name = "futures-io" -version = "0.3.32" +name = "base64" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "futures-sink" -version = "0.3.32" +name = "base64-simd" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] [[package]] -name = "futures-task" -version = "0.3.32" +name = "base64ct" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] -name = "futures-util" -version = "0.3.32" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "generic-array" -version = "0.14.7" +name = "bitflags" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "typenum", - "version_check", + "serde_core", ] [[package]] -name = "getrandom" -version = "0.2.17" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "cfg-if", - "libc", - "wasi", + "generic-array", ] [[package]] -name = "getrandom" -version = "0.4.2" +name = "block-buffer" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", + "hybrid-array", ] [[package]] -name = "hashbrown" -version = "0.15.5" +name = "block-padding" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", + "generic-array", ] [[package]] -name = "hashbrown" -version = "0.16.1" +name = "blocking" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite 2.6.1", + "piper", +] [[package]] -name = "hashlink" -version = "0.10.0" +name = "bumpalo" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] -name = "heck" -version = "0.5.0" +name = "byteorder" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "hex" -version = "0.4.3" +name = "bytes" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "hkdf" -version = "0.12.4" +name = "bytes-utils" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" dependencies = [ - "hmac", + "bytes", + "either", ] [[package]] -name = "hmac" -version = "0.12.1" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "digest", + "cipher", ] [[package]] -name = "home" -version = "0.5.12" +name = "cc" +version = "1.2.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" dependencies = [ - "windows-sys 0.61.2", + "find-msvc-tools", + "jobserver", + "libc", + "shlex", ] [[package]] -name = "iana-time-zone" -version = "0.1.65" +name = "cfg-if" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", + "iana-time-zone", "js-sys", - "log", + "num-traits", + "serde", "wasm-bindgen", - "windows-core", + "windows-link", ] [[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "cc", + "crypto-common 0.1.7", + "inout", ] [[package]] -name = "icu_collections" -version = "2.1.1" +name = "cmake" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", + "cc", ] [[package]] -name = "icu_locale_core" -version = "2.1.1" +name = "cmov" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" [[package]] -name = "icu_normalizer" +name = "cms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b77c319abfd5219629c45c34c89ba945ed3c5e49fcde9d16b6c3885f118a730" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", + "x509-cert", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.0", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener 5.4.1", + "pin-project-lite", +] + +[[package]] +name = "executor-trait" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c39dff9342e4e0e16ce96be751eb21a94e94a87bb2f6e63ad1961c2ce109bf" +dependencies = [ + "async-trait", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin 0.9.8", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand 2.3.0", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.14", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http 1.4.0", + "hyper 1.9.0", + "hyper-util", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.9.0", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lapin" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2aa4725b9607915fa1a73e940710a3be6af508ce700e56897cbe8847fbb07" +dependencies = [ + "amq-protocol", + "async-global-executor-trait", + "async-reactor-trait", + "async-trait", + "executor-trait", + "flume", + "futures-core", + "futures-io", + "parking_lot", + "pinky-swear", + "reactor-trait", + "serde", + "tracing", + "waker-fn", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin 0.9.8", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", + "num-traits", ] [[package]] -name = "icu_normalizer_data" -version = "2.1.1" +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] [[package]] -name = "icu_properties" -version = "2.1.2" +name = "num-traits" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", + "autocfg", + "libm", ] [[package]] -name = "icu_properties_data" -version = "2.1.2" +name = "oid-registry" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] [[package]] -name = "icu_provider" -version = "2.1.1" +name = "once_cell" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p12-keystore" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cae83056e7cb770211494a0ecf66d9fa7eba7d00977e5bb91f0e925b40b937f" dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", + "cbc", + "cms", + "der", + "des", + "hex", + "hmac 0.12.1", + "pkcs12", + "pkcs5", + "rand 0.9.4", + "rc2", + "sha1 0.10.6", + "sha2 0.10.9", + "thiserror", + "x509-parser", ] [[package]] -name = "id-arena" -version = "2.3.0" +name = "p256" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] [[package]] -name = "idna" -version = "1.1.0" +name = "parking" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "idna_adapter", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", "smallvec", - "utf8_iter", + "windows-link", ] [[package]] -name = "idna_adapter" -version = "1.2.1" +name = "pbkdf2" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "icu_normalizer", - "icu_properties", + "digest 0.10.7", + "hmac 0.12.1", ] [[package]] -name = "indexmap" -version = "2.13.0" +name = "pem-rfc7468" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", + "base64ct", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "js-sys" -version = "0.3.91" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinky-swear" +version = "6.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1ea6e230dd3a64d61bcb8b79e597d3ab6b4c94ec7a234ce687dd718b4f2e657" dependencies = [ - "once_cell", - "wasm-bindgen", + "doc-comment", + "flume", + "parking_lot", + "tracing", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "piper" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ - "spin", + "atomic-waker", + "fastrand 2.3.0", + "futures-io", ] [[package]] -name = "leb128fmt" +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs12" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" +checksum = "695b3df3d3cc1015f12d70235e35b6b79befc5fa7a9b95b951eab1dd07c9efc2" +dependencies = [ + "cms", + "const-oid 0.9.6", + "der", + "digest 0.10.7", + "spki", + "x509-cert", + "zeroize", +] [[package]] -name = "libc" -version = "0.2.183" +name = "pkcs5" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2 0.10.9", + "spki", +] [[package]] -name = "libm" -version = "0.2.16" +name = "pkcs8" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] [[package]] -name = "libredox" -version = "0.1.14" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "bitflags", - "libc", - "plain", - "redox_syscall 0.7.3", + "unicode-ident", ] [[package]] -name = "libsqlite3-sys" -version = "0.30.1" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "pkg-config", - "vcpkg", + "proc-macro2", ] [[package]] -name = "linux-raw-sys" -version = "0.12.1" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "litemap" -version = "0.8.1" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] -name = "lock_api" -version = "0.4.14" +name = "rand" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "scopeguard", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] [[package]] -name = "log" -version = "0.4.29" +name = "rand" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] [[package]] -name = "md-5" -version = "0.10.6" +name = "rand_chacha" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ - "cfg-if", - "digest", + "ppv-lite86", + "rand_core 0.6.4", ] [[package]] -name = "memchr" -version = "2.8.0" +name = "rand_chacha" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] [[package]] -name = "mio" -version = "1.1.1" +name = "rand_core" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "getrandom 0.2.17", ] [[package]] -name = "num-bigint" -version = "0.4.6" +name = "rand_core" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "num-integer", - "num-traits", + "getrandom 0.3.4", ] [[package]] -name = "num-bigint-dig" -version = "0.8.6" +name = "rc2" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +checksum = "62c64daa8e9438b84aaae55010a93f396f8e60e3911590fcba770d04643fc1dd" dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand", - "smallvec", - "zeroize", + "cipher", ] [[package]] -name = "num-integer" -version = "0.1.46" +name = "reactor-trait" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "438a4293e4d097556730f4711998189416232f009c137389e0f961d2bc0ddc58" dependencies = [ - "num-traits", + "async-trait", + "futures-core", + "futures-io", ] [[package]] -name = "num-iter" -version = "0.1.45" +name = "redis" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +checksum = "b36964393906eb775b89b25b05b7b95685b8dd14062f1663a31ff93e75c452e5" dependencies = [ - "autocfg", - "num-integer", - "num-traits", + "arcstr", + "bytes", + "cfg-if", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.6.3", + "tokio", + "tokio-util", + "url", + "xxhash-rust", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "redox_syscall" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "autocfg", - "libm", + "bitflags 2.11.0", ] [[package]] -name = "once_cell" -version = "1.21.4" +name = "redox_syscall" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] [[package]] -name = "parking" -version = "2.2.1" +name = "regex-lite" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" [[package]] -name = "parking_lot" -version = "0.12.5" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "lock_api", - "parking_lot_core", + "hmac 0.12.1", + "subtle", ] [[package]] -name = "parking_lot_core" -version = "0.9.12" +name = "ring" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ + "cc", "cfg-if", + "getrandom 0.2.17", "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", + "untrusted", + "windows-sys 0.52.0", ] [[package]] -name = "pem-rfc7468" -version = "0.7.0" +name = "rsa" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "base64ct", + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", ] [[package]] -name = "percent-encoding" -version = "2.3.2" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] [[package]] -name = "pin-project-lite" -version = "0.2.17" +name = "rusticata-macros" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] [[package]] -name = "pkcs1" -version = "0.7.5" +name = "rustix" +version = "0.37.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" dependencies = [ - "der", - "pkcs8", - "spki", + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", ] [[package]] -name = "pkcs8" -version = "0.10.2" +name = "rustix" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "der", - "spki", + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", ] [[package]] -name = "pkg-config" -version = "0.3.32" +name = "rustls" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] [[package]] -name = "plain" -version = "0.2.3" +name = "rustls" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.13", + "subtle", + "zeroize", +] [[package]] -name = "potential_utf" -version = "0.1.4" +name = "rustls-connector" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "70cc376c6ba1823ae229bacf8ad93c136d93524eab0e4e5e0e4f96b9c4e5b212" dependencies = [ - "zerovec", + "log", + "rustls 0.23.40", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "rustls-webpki 0.103.13", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "rustls-native-certs" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "zerocopy", + "openssl-probe 0.1.6", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework 2.11.1", ] [[package]] -name = "prettyplease" -version = "0.2.37" +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "proc-macro2", - "syn", + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", ] [[package]] -name = "proc-macro2" -version = "1.0.106" +name = "rustls-pemfile" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "unicode-ident", + "rustls-pki-types", ] [[package]] -name = "quote" -version = "1.0.45" +name = "rustls-pki-types" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ - "proc-macro2", + "zeroize", ] [[package]] -name = "r-efi" -version = "6.0.0" +name = "rustls-webpki" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] [[package]] -name = "rand" -version = "0.8.5" +name = "rustls-webpki" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] -name = "rand_chacha" -version = "0.3.1" +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "salsa20" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" dependencies = [ - "ppv-lite86", - "rand_core", + "cipher", ] [[package]] -name = "rand_core" -version = "0.6.4" +name = "schannel" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ - "getrandom 0.2.17", + "windows-sys 0.61.2", ] [[package]] -name = "redis" -version = "1.0.5" +name = "scopeguard" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b36964393906eb775b89b25b05b7b95685b8dd14062f1663a31ff93e75c452e5" -dependencies = [ - "arcstr", - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "socket2", - "tokio", - "tokio-util", - "url", - "xxhash-rust", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] -name = "redox_syscall" -version = "0.5.18" +name = "scrypt" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ - "bitflags", + "pbkdf2", + "salsa20", + "sha2 0.10.9", ] [[package]] -name = "redox_syscall" -version = "0.7.3" +name = "sct" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "bitflags", + "ring", + "untrusted", ] [[package]] -name = "rsa" -version = "0.9.10" +name = "sec1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", + "base16ct", + "der", + "generic-array", "pkcs8", - "rand_core", - "signature", - "spki", "subtle", "zeroize", ] [[package]] -name = "rustix" -version = "1.1.4" +name = "security-framework" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags", - "errno", + "bitflags 2.11.0", + "core-foundation 0.9.4", + "core-foundation-sys", "libc", - "linux-raw-sys", - "windows-sys 0.61.2", + "security-framework-sys", ] [[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" +name = "security-framework" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] [[package]] -name = "scopeguard" -version = "1.2.0" +name = "security-framework-sys" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "semver" @@ -1165,8 +3146,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -1182,15 +3174,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] name = "shared-rs" version = "0.1.0" dependencies = [ + "aws-config", + "aws-sdk-s3", "chrono", + "lapin", "redis", "serde", "serde_json", @@ -1199,6 +3205,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "url", "uuid", ] @@ -1224,8 +3231,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -1243,6 +3250,26 @@ dependencies = [ "serde", ] +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "socket2" version = "0.6.3" @@ -1262,6 +3289,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -1297,7 +3330,7 @@ dependencies = [ "crc", "crossbeam-queue", "either", - "event-listener", + "event-listener 5.4.1", "futures-core", "futures-intrusive", "futures-io", @@ -1311,7 +3344,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror", "tokio", @@ -1349,7 +3382,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -1367,12 +3400,12 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.0", "byteorder", "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -1382,18 +3415,18 @@ dependencies = [ "generic-array", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", - "sha1", - "sha2", + "sha1 0.10.6", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -1411,7 +3444,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64", - "bitflags", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -1422,17 +3455,17 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -1513,16 +3546,28 @@ dependencies = [ "syn", ] +[[package]] +name = "tcp-stream" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495b0abdce3dc1f8fd27240651c9e68890c14e9d9c61527b1ce44d8a5a7bd3d5" +dependencies = [ + "cfg-if", + "p12-keystore", + "rustls-connector", + "rustls-pemfile", +] + [[package]] name = "tempfile" version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ - "fastrand", + "fastrand 2.3.0", "getrandom 0.4.2", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] @@ -1546,6 +3591,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.2" @@ -1583,7 +3659,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.6.3", "tokio-macros", "windows-sys 0.61.2", ] @@ -1599,6 +3675,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.40", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.18" @@ -1623,6 +3719,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.44" @@ -1655,6 +3773,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.19.0" @@ -1700,6 +3824,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.8" @@ -1712,6 +3842,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -1742,6 +3878,27 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1845,7 +4002,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -1861,6 +4018,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-core" version = "0.62.2" @@ -1926,7 +4105,16 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -1944,13 +4132,29 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -1959,42 +4163,90 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -2053,7 +4305,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -2089,6 +4341,40 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid 0.9.6", + "der", + "spki", +] + +[[package]] +name = "x509-parser" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4569f339c0c402346d4a75a9e39cf8dad310e287eef1ff56d4c68e5067f53460" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/libs/shared-rs/Cargo.toml b/libs/shared-rs/Cargo.toml index e1fb4ca..76079fa 100644 --- a/libs/shared-rs/Cargo.toml +++ b/libs/shared-rs/Cargo.toml @@ -13,6 +13,11 @@ thiserror = "2" sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "uuid", "chrono", "json"] } redis = { version = "1.0", features = ["tokio-comp", "streams"] } serde_yaml = "0.9" +# M6 — Caravan oss-local resource adapters (compose targets only). +aws-config = { version = "1", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1" +lapin = "2" +url = "2" [dev-dependencies] tempfile = "3" diff --git a/libs/shared-rs/README.md b/libs/shared-rs/README.md new file mode 100644 index 0000000..365f2e4 --- /dev/null +++ b/libs/shared-rs/README.md @@ -0,0 +1,69 @@ +# shared-rs + +Shared Rust infrastructure for invoice-parse's Rust services (`services/ingestion`, `services/output`). +Provides: + +- **Adapters** (`src/adapters/`) — `BlobStore` and `MessageQueue` traits plus concrete + implementations: `LocalFsBlobStore`, `S3BlobStore` (boto3-equivalent via `aws-sdk-s3`), + `RedisStreamQueue`, `RabbitMQQueue` (via `lapin`). Factory (`adapters/factory.rs`) + selects between implementations based on Caravan-injected env vars + (`S3_ENDPOINT_URL`, `QUEUE_URL`) with YAML-config fallback for the no-Caravan + local-dev path. +- **Config** (`src/config.rs`) — typed `AppConfig` loaded from YAML with env-var overrides + (`DATABASE_URL` overrides `config.database.url` when set). +- **Database helpers** (`src/db.rs`) — sqlx connection pool, job-state transitions. +- **Models** (`src/models.rs`) — shared types for inter-service messages (`QueueAMessage`, + `QueueBMessage`). + +## Why shared-rs is not a Caravan seam + +Caravan seams are RPC dispatch boundaries with at-most-one-process-per-side semantics. +A seam is declared via `#[wagon]` (Rust) or `@wagon` (Python), registered via +`provide(Interface, impl)`, and dispatched via `client(Interface).method(...)` — the +call's destination (in-process / HTTP container / Lambda) is decided per-target by +`caravan compile` reading a yaml line. + +`shared-rs` is **FFI** — Rust code linked directly into the binary of `services/ingestion` +and `services/output` (and tested in-process by `cargo test`). The call resolution is +purely compile-time: when `services/ingestion/src/main.rs` calls +`blob_store.put(path, data)`, the linker resolves the call to a method on the concrete +`LocalFsBlobStore` (or `S3BlobStore`, etc.) struct. There is no dispatch decision and +no possibility of the callee running in a different process. + +Future contributors should **not** wrap any function in `shared-rs` with `#[wagon]` or +`caravan-rpc`. FFI ≠ RPC; one is in-process function call, the other is a network-or- +direct dispatch decision. The structural shape Caravan asks for at seam boundaries +(JSON-serializable args + return, no shared mutable state across calls) doesn't apply +inside an in-process function boundary, so the SDK's promises don't apply either. + +If a hypothetical future split moves part of `shared-rs`'s functionality into a separate +service (e.g., a centralized "document store" that all backend services talk to), that +new boundary would be a candidate Caravan seam — and the `shared-rs` adapter would become +the seam's client. The boundary at the existing FFI surface stays out of Caravan scope. + +## Build + +```bash +cargo build --release --manifest-path libs/shared-rs/Cargo.toml +``` + +Pulls in `aws-sdk-s3` and `lapin` (M6 additions). First build is multi-minute (large +dependency tree); subsequent builds are incremental. + +## Adapter selection runtime contract + +When a Rust service starts, `factory::create_blob_store(&config)` and +`factory::create_queue(&config)` apply this precedence: + +1. **Env-var presence wins** (set by Caravan compose-emit): + - `S3_ENDPOINT_URL` set → `S3BlobStore` (talks to MinIO under compose, real S3 in + cloud post-M4-cloud). + - `QUEUE_URL=redis://...` → `RedisStreamQueue`. + - `QUEUE_URL=amqp://...` → `RabbitMQQueue`. +2. **YAML config fallback** when env-vars are absent (local non-container runs): + - `blob_storage.type: local_fs` → `LocalFsBlobStore` against `base_path`. + - `queue.type: redis_stream` / `rabbitmq` → matching adapter against `queue.url`. + +Same code path serves both compose targets (where Caravan injects env vars) and local +dev (where the user runs `cargo run` against host-local Postgres/Redis without a Caravan +compile step). diff --git a/libs/shared-rs/src/adapters/blob_store.rs b/libs/shared-rs/src/adapters/blob_store.rs index 07e390a..db45ffa 100644 --- a/libs/shared-rs/src/adapters/blob_store.rs +++ b/libs/shared-rs/src/adapters/blob_store.rs @@ -12,6 +12,8 @@ pub enum BlobError { PathEscape(String), #[error("IO error: {0}")] Io(#[from] std::io::Error), + #[error("S3 error: {0}")] + S3(String), } /// Abstract blob storage interface. @@ -90,6 +92,194 @@ impl BlobStore for LocalFsBlobStore { } } +/// S3-protocol blob store via aws-sdk-s3. +/// +/// Holds an async S3 client built once at construction. Trait methods are +/// sync (the `MessageQueue`/`BlobStore` traits use sync signatures because +/// the existing Redis impl + caller code are sync). To call async aws-sdk +/// methods from sync trait methods we use `block_in_place + +/// Handle::current().block_on()` — works when the caller is inside a +/// tokio runtime (which both ingestion + output `tokio::main` provide). +/// +/// Same code path serves MinIO (compose, endpoint_url set) and real AWS +/// S3 (cloud, endpoint_url unset, IAM-resolved creds). +pub struct S3BlobStore { + client: aws_sdk_s3::Client, + bucket: String, +} + +impl S3BlobStore { + /// Construct from explicit settings. `endpoint_url` is optional — + /// unset means "use AWS default endpoint resolution". + pub fn new( + bucket: &str, + endpoint_url: Option<&str>, + access_key_id: Option<&str>, + secret_access_key: Option<&str>, + region: Option<&str>, + ) -> Result { + let region_provider = region + .map(|r| aws_config::Region::new(r.to_string())) + .unwrap_or_else(|| aws_config::Region::new("us-east-1")); + + let client = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let mut loader = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(region_provider); + if let Some(url) = endpoint_url { + loader = loader.endpoint_url(url); + } + if let (Some(ak), Some(sk)) = (access_key_id, secret_access_key) { + let creds = aws_sdk_s3::config::Credentials::new( + ak, sk, None, None, "caravan-env", + ); + loader = loader.credentials_provider(creds); + } + let cfg = loader.load().await; + let s3_cfg = aws_sdk_s3::config::Builder::from(&cfg) + .force_path_style(true) // MinIO requires path-style addressing + .build(); + aws_sdk_s3::Client::from_conf(s3_cfg) + }) + }); + + // Best-effort bucket create (silent on conflict / AccessDenied — + // production AWS buckets are provisioned out of band). + let bucket_name = bucket.to_string(); + let client_for_create = client.clone(); + let _ = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + client_for_create + .create_bucket() + .bucket(&bucket_name) + .send() + .await + .map(|_| ()) + }) + }); + + Ok(Self { + client, + bucket: bucket.to_string(), + }) + } + + /// Construct from Caravan-injected env vars. + /// + /// Reads `S3_ENDPOINT_URL` (required env-var presence is what + /// signals "use S3 instead of LocalFs"), `S3_BUCKET` (defaults to + /// `invoice-parse`), `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, + /// `AWS_REGION`. + pub fn from_env() -> Result { + let bucket = std::env::var("S3_BUCKET").unwrap_or_else(|_| "invoice-parse".to_string()); + let endpoint_url = std::env::var("S3_ENDPOINT_URL").ok(); + let access_key_id = std::env::var("AWS_ACCESS_KEY_ID").ok(); + let secret_access_key = std::env::var("AWS_SECRET_ACCESS_KEY").ok(); + let region = std::env::var("AWS_REGION").ok(); + Self::new( + &bucket, + endpoint_url.as_deref(), + access_key_id.as_deref(), + secret_access_key.as_deref(), + region.as_deref(), + ) + } + + fn validate_key(path: &str) -> Result { + if path.contains("..") { + return Err(BlobError::PathTraversal(path.to_string())); + } + let key = path.trim_start_matches('/').to_string(); + let parts: Vec<&str> = key.splitn(3, '/').collect(); + if parts.len() >= 2 { + for segment in &parts[..2] { + if uuid::Uuid::parse_str(segment).is_err() { + return Err(BlobError::InvalidUuid(segment.to_string())); + } + } + } + Ok(key) + } +} + +impl BlobStore for S3BlobStore { + fn put(&self, path: &str, data: &[u8]) -> Result<(), BlobError> { + let key = Self::validate_key(path)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.client + .put_object() + .bucket(&self.bucket) + .key(&key) + .body(aws_sdk_s3::primitives::ByteStream::from(data.to_vec())) + .send() + .await + .map_err(|e| BlobError::S3(e.to_string())) + .map(|_| ()) + }) + }) + } + + fn get(&self, path: &str) -> Result, BlobError> { + let key = Self::validate_key(path)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let response = self + .client + .get_object() + .bucket(&self.bucket) + .key(&key) + .send() + .await + .map_err(|e| BlobError::S3(e.to_string()))?; + let bytes = response + .body + .collect() + .await + .map_err(|e| BlobError::S3(e.to_string()))? + .into_bytes() + .to_vec(); + Ok(bytes) + }) + }) + } + + fn exists(&self, path: &str) -> Result { + let key = Self::validate_key(path)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let result = self + .client + .head_object() + .bucket(&self.bucket) + .key(&key) + .send() + .await; + match result { + Ok(_) => Ok(true), + Err(_) => Ok(false), + } + }) + }) + } + + fn delete(&self, path: &str) -> Result<(), BlobError> { + let key = Self::validate_key(path)?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + self.client + .delete_object() + .bucket(&self.bucket) + .key(&key) + .send() + .await + .map_err(|e| BlobError::S3(e.to_string())) + .map(|_| ()) + }) + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/libs/shared-rs/src/adapters/factory.rs b/libs/shared-rs/src/adapters/factory.rs new file mode 100644 index 0000000..476fcb6 --- /dev/null +++ b/libs/shared-rs/src/adapters/factory.rs @@ -0,0 +1,95 @@ +//! Adapter factory — env-var-presence selection with YAML fallback. +//! +//! Mirrors the Python factory in `libs/shared-py/invoice_shared/adapters/factory.py`: +//! Caravan-injected env vars take precedence; YAML config is the fallback for +//! local non-container runs that don't go through `caravan compile`. + +use crate::adapters::blob_store::{BlobError, BlobStore, LocalFsBlobStore, S3BlobStore}; +use crate::adapters::queue::{MessageQueue, QueueError, RabbitMQQueue, RedisStreamQueue}; +use crate::config::AppConfig; + +/// Construct a BlobStore. `S3_ENDPOINT_URL` env presence selects S3; +/// otherwise falls back to YAML config (currently only local_fs is +/// supported in YAML mode; the YAML s3 path is exercised only when env +/// vars are also set). +pub fn create_blob_store(config: &AppConfig) -> Result, BlobError> { + if std::env::var("S3_ENDPOINT_URL").is_ok() { + return Ok(Box::new(S3BlobStore::from_env()?)); + } + + match config.blob_storage.storage_type.as_str() { + "local_fs" => { + let base = config + .blob_storage + .base_path + .as_deref() + .ok_or_else(|| BlobError::S3("local_fs blob storage requires base_path".into()))?; + Ok(Box::new(LocalFsBlobStore::new(base)?)) + } + "s3" => { + // YAML s3 fallback for the rare case where the user wired S3 in + // YAML directly. Caravan-injected env vars are the preferred path. + let bucket = config.blob_storage.bucket.as_deref().ok_or_else(|| { + BlobError::S3("s3 blob storage requires bucket (env S3_ENDPOINT_URL unset)".into()) + })?; + Ok(Box::new(S3BlobStore::new( + bucket, + None, + None, + None, + config.blob_storage.region.as_deref(), + )?)) + } + other => Err(BlobError::S3(format!( + "Unknown blob storage type: {other}" + ))), + } +} + +/// Construct a MessageQueue. `QUEUE_URL` env scheme selects: +/// redis:// → RedisStreamQueue +/// amqp:// → RabbitMQQueue +/// Falls back to YAML config when env unset. +pub fn create_queue(config: &AppConfig) -> Result, QueueError> { + let consumer_group = config + .queue + .consumer_group + .as_deref() + .unwrap_or("invoice_workers"); + + if let Ok(env_url) = std::env::var("QUEUE_URL") { + let scheme = url::Url::parse(&env_url) + .map_err(|e| QueueError::InvalidUrl(e.to_string()))? + .scheme() + .to_string(); + return match scheme.as_str() { + "redis" | "rediss" => Ok(Box::new(RedisStreamQueue::new(&env_url, consumer_group)?)), + "amqp" | "amqps" => Ok(Box::new(RabbitMQQueue::new(&env_url)?)), + other => Err(QueueError::InvalidUrl(format!( + "QUEUE_URL scheme {other:?} not supported in Phase 1 (compose-only)" + ))), + }; + } + + match config.queue.queue_type.as_str() { + "redis_stream" => { + let url = config + .queue + .url + .as_deref() + .ok_or_else(|| QueueError::InvalidUrl("redis_stream queue requires url".into()))?; + Ok(Box::new(RedisStreamQueue::new(url, consumer_group)?)) + } + "rabbitmq" => { + let url = config + .queue + .url + .as_deref() + .ok_or_else(|| QueueError::InvalidUrl("rabbitmq queue requires url".into()))?; + Ok(Box::new(RabbitMQQueue::new(url)?)) + } + other => Err(QueueError::InvalidUrl(format!( + "Unknown queue type: {other}" + ))), + } +} diff --git a/libs/shared-rs/src/adapters/mod.rs b/libs/shared-rs/src/adapters/mod.rs index a5a33d2..4ed263d 100644 --- a/libs/shared-rs/src/adapters/mod.rs +++ b/libs/shared-rs/src/adapters/mod.rs @@ -1,2 +1,3 @@ pub mod blob_store; +pub mod factory; pub mod queue; diff --git a/libs/shared-rs/src/adapters/queue.rs b/libs/shared-rs/src/adapters/queue.rs index 8dbd456..8f1e708 100644 --- a/libs/shared-rs/src/adapters/queue.rs +++ b/libs/shared-rs/src/adapters/queue.rs @@ -1,6 +1,7 @@ use redis::streams::{StreamReadOptions, StreamReadReply}; use redis::{Commands, RedisError}; use serde_json::Value; +use std::sync::Mutex; use thiserror::Error; #[derive(Debug, Error)] @@ -9,6 +10,10 @@ pub enum QueueError { Redis(#[from] RedisError), #[error("JSON error: {0}")] Json(#[from] serde_json::Error), + #[error("RabbitMQ error: {0}")] + RabbitMQ(String), + #[error("Invalid URL: {0}")] + InvalidUrl(String), } /// Abstract message queue interface. @@ -97,3 +102,206 @@ impl MessageQueue for RedisStreamQueue { Ok(()) } } + +/// RabbitMQ implementation via lapin. +/// +/// Lapin's API is async; the `MessageQueue` trait is sync to match the +/// existing RedisStreamQueue shape (and the way ingestion/output worker +/// loops call `queue.consume(...)` from sync code paths inside their +/// `tokio::main` async fn). We bridge by calling +/// `tokio::task::block_in_place + Handle::current().block_on(...)` from +/// within each trait method. This requires the caller to be inside a +/// multi-threaded tokio runtime (which `#[tokio::main]` provides by +/// default). +/// +/// Holds a lapin channel and connection inside a Mutex so the (Sync + +/// Send) bound on the MessageQueue trait holds. Lazy initialization on +/// first call. +pub struct RabbitMQQueue { + url: String, + state: Mutex, +} + +struct RabbitMQState { + connection: Option, + channel: Option, + declared: std::collections::HashSet, +} + +impl RabbitMQQueue { + pub fn new(url: &str) -> Result { + let scheme = url::Url::parse(url) + .map_err(|e| QueueError::InvalidUrl(e.to_string()))? + .scheme() + .to_string(); + if scheme != "amqp" && scheme != "amqps" { + return Err(QueueError::InvalidUrl(format!( + "RabbitMQQueue expects amqp://... or amqps://... ; got {scheme}" + ))); + } + Ok(Self { + url: url.to_string(), + state: Mutex::new(RabbitMQState { + connection: None, + channel: None, + declared: std::collections::HashSet::new(), + }), + }) + } + + fn ensure_channel(&self) -> Result { + // Lazy connect on first use; reconnect if previous channel closed. + let mut state = self.state.lock().unwrap(); + if let Some(ch) = &state.channel { + if ch.status().connected() { + return Ok(ch.clone()); + } + } + let url = self.url.clone(); + let (conn, ch) = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let conn = lapin::Connection::connect(&url, lapin::ConnectionProperties::default()) + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string()))?; + let ch = conn + .create_channel() + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string()))?; + Ok::<_, QueueError>((conn, ch)) + }) + })?; + state.connection = Some(conn); + state.channel = Some(ch.clone()); + state.declared.clear(); + Ok(ch) + } + + fn ensure_queue(&self, ch: &lapin::Channel, topic: &str) -> Result<(), QueueError> { + { + let state = self.state.lock().unwrap(); + if state.declared.contains(topic) { + return Ok(()); + } + } + let topic_owned = topic.to_string(); + let ch_clone = ch.clone(); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + ch_clone + .queue_declare( + &topic_owned, + lapin::options::QueueDeclareOptions { + durable: true, + ..Default::default() + }, + lapin::types::FieldTable::default(), + ) + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string())) + .map(|_| ()) + }) + })?; + let mut state = self.state.lock().unwrap(); + state.declared.insert(topic.to_string()); + Ok(()) + } +} + +impl MessageQueue for RabbitMQQueue { + fn publish(&self, topic: &str, message: &Value) -> Result { + let ch = self.ensure_channel()?; + self.ensure_queue(&ch, topic)?; + + let body = serde_json::to_vec(message)?; + let msg_id = uuid::Uuid::new_v4().to_string().replace('-', ""); + let topic_owned = topic.to_string(); + let msg_id_owned = msg_id.clone(); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + ch.basic_publish( + "", + &topic_owned, + lapin::options::BasicPublishOptions::default(), + &body, + lapin::BasicProperties::default() + .with_message_id(msg_id_owned.into()) + .with_delivery_mode(2) + .with_content_type("application/json".into()), + ) + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string()))? + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string()))?; + Ok::<_, QueueError>(()) + }) + })?; + Ok(msg_id) + } + + fn consume( + &self, + topic: &str, + count: usize, + block_ms: usize, + ) -> Result, QueueError> { + let ch = self.ensure_channel()?; + self.ensure_queue(&ch, topic)?; + + let deadline = std::time::Instant::now() + std::time::Duration::from_millis(block_ms as u64); + let topic_owned = topic.to_string(); + + let messages = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let mut out: Vec<(String, Value)> = Vec::new(); + while out.len() < count { + let now = std::time::Instant::now(); + if now >= deadline { + break; + } + let msg = ch + .basic_get(&topic_owned, lapin::options::BasicGetOptions::default()) + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string()))?; + match msg { + Some(delivery) => { + let payload: Value = serde_json::from_slice(&delivery.data)?; + out.push((delivery.delivery_tag.to_string(), payload)); + } + None => { + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + } + } + } + Ok::<_, QueueError>(out) + }) + })?; + + Ok(messages) + } + + fn ack(&self, _topic: &str, message_id: &str) -> Result<(), QueueError> { + let ch = self.ensure_channel()?; + let delivery_tag: u64 = message_id + .parse() + .map_err(|e: std::num::ParseIntError| QueueError::RabbitMQ(e.to_string()))?; + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + ch.basic_ack(delivery_tag, lapin::options::BasicAckOptions::default()) + .await + .map_err(|e| QueueError::RabbitMQ(e.to_string())) + }) + }) + } + + fn extend_visibility( + &self, + _topic: &str, + _message_id: &str, + _seconds: u64, + ) -> Result<(), QueueError> { + // RabbitMQ has no SQS-style visibility timeout — unacked messages + // requeue when the channel closes. Long-running consumers handle + // their own ack timing. + Ok(()) + } +} diff --git a/libs/shared-rs/src/config.rs b/libs/shared-rs/src/config.rs index fd30e70..69029cd 100644 --- a/libs/shared-rs/src/config.rs +++ b/libs/shared-rs/src/config.rs @@ -50,12 +50,15 @@ pub struct QueueConfig { pub region: Option, } -/// Load config from a YAML file. +/// Load config from a YAML file, with env-var overrides. /// -/// Resolution order: +/// Resolution order for the YAML path: /// 1. Explicit path argument /// 2. `INVOICE_PARSE_CONFIG` env var /// 3. `config/local.yaml` +/// +/// Caravan-injected env vars override individual fields (M6): +/// - `DATABASE_URL` → `database.url` pub fn load_config(path: Option<&Path>) -> Result> { let path = match path { Some(p) => p.to_path_buf(), @@ -67,6 +70,11 @@ pub fn load_config(path: Option<&Path>) -> Result/generated/build-context/. Caravan auto-adds the +# `caravan-rpc>=0.1.0` line to this manifest during compile (user's +# on-disk requirements.txt is untouched). After M9 Phase 1 close +# (2026-05-21) the package is on PyPI, so pip resolves it normally — +# the earlier `--find-links /vendor/` shim + vendored +# caravan_rpc-0.1.0.dev0 wheel are retired. +# +# CARAVAN_TARGET defaults to dev-bootstrap (the canonical M3 demo +# target). Override at build time when running a different target end- +# to-end: `docker compose build --build-arg CARAVAN_TARGET=dev-split-llm`. +ARG CARAVAN_TARGET=dev-bootstrap +COPY infra/${CARAVAN_TARGET}/generated/build-context/services/processing/requirements.txt /app/caravan-requirements.txt +RUN pip install --no-cache-dir -r /app/caravan-requirements.txt + # Install processing service deps COPY services/processing/pyproject.toml /app/ COPY services/processing/invoice_processing/ /app/invoice_processing/ diff --git a/services/processing/invoice_processing/cli.py b/services/processing/invoice_processing/cli.py index ef43256..c33810b 100644 --- a/services/processing/invoice_processing/cli.py +++ b/services/processing/invoice_processing/cli.py @@ -16,12 +16,32 @@ import logging from pathlib import Path -from .ocr import process_ocr -from .table_extract import create_table_extractor -from .extraction import create_extractor +from caravan_rpc import client, provide + +from .ocr import OCRText, PaddleOCRTextImpl, process_ocr +from .table_extract import OCRLayout, PPStructureExtractor, SpatialClusterExtractor +from .extraction import ( + ClaudeExtractor, + GeminiExtractor, + LLMExtraction, + OpenAIExtractor, +) from .validation import validate_extraction +_TABLE_METHODS: dict[str, type] = { + "spatial_cluster": SpatialClusterExtractor, + "ppstructure": PPStructureExtractor, +} + + +_PROVIDERS = { + "gemini": GeminiExtractor, + "claude": ClaudeExtractor, + "openai": OpenAIExtractor, +} + + def main() -> None: from dotenv import load_dotenv @@ -70,6 +90,13 @@ def main() -> None: logger.info("Read %d bytes from %s", len(pdf_bytes), args.pdf_path) args.output_dir.mkdir(parents=True, exist_ok=True) + # Register Caravan seam impls before any client(...) dispatches. + # CARAVAN_RPC_PEERS is unset in CLI mode so each call short-circuits + # to the local impl with zero overhead. + provide(OCRText, PaddleOCRTextImpl()) + if not args.raw_only and args.table_method != "none": + provide(OCRLayout, _TABLE_METHODS[args.table_method]()) + # --- OCR --- logger.info("Running OCR...") raw_ocr, images = process_ocr(pdf_bytes, filename=args.pdf_path.name) @@ -82,8 +109,7 @@ def main() -> None: table_extraction = None if not args.raw_only and args.table_method != "none": logger.info("Running table extraction (%s)...", args.table_method) - table_extractor = create_table_extractor(args.table_method) - table_extraction = table_extractor.extract(raw_ocr, images) + table_extraction = client(OCRLayout).extract(raw_ocr, pdf_bytes, args.pdf_path.name) table_path = args.output_dir / "table_extraction.json" table_path.write_text(json.dumps(table_extraction.to_dict(), indent=2, ensure_ascii=False)) @@ -101,8 +127,9 @@ def main() -> None: # --- LLM Extraction --- logger.info("Running LLM extraction with %s...", args.provider) - extractor = create_extractor(args.provider) - extraction = extractor.extract( + impl_cls = _PROVIDERS[args.provider] + provide(LLMExtraction, impl_cls()) + extraction = client(LLMExtraction).extract( raw_ocr=raw_ocr, table_extraction=table_extraction, ) diff --git a/services/processing/invoice_processing/extraction.py b/services/processing/invoice_processing/extraction.py index ec983e9..58a9dac 100644 --- a/services/processing/invoice_processing/extraction.py +++ b/services/processing/invoice_processing/extraction.py @@ -11,8 +11,8 @@ import json import logging import os -from abc import ABC, abstractmethod +from caravan_rpc import wagon from invoice_shared.models import InvoiceExtraction from .ocr import RawOcrOutput @@ -97,13 +97,19 @@ def build_extraction_prompt( ) -# --- LLM abstraction --- +# --- LLM seam interface --- -class LLMExtractor(ABC): - """Abstract interface for LLM-based invoice extraction.""" +@wagon +class LLMExtraction: + """Caravan seam interface for LLM-based invoice extraction. + + Decorated with ``@wagon`` so the caravan-rpc SDK can dispatch + ``client(LLMExtraction).extract(...)`` either inproc (direct call on the + registered impl) or over HTTP to a sidecar process — selected at runtime + by the ``CARAVAN_RPC_PEERS`` env var with zero source-code change. + """ - @abstractmethod def extract( self, raw_ocr: RawOcrOutput | None = None, @@ -111,7 +117,10 @@ def extract( ) -> InvoiceExtraction: ... -class GeminiExtractor(LLMExtractor): +# --- Concrete implementations --- + + +class GeminiExtractor: """Gemini Flash via google-genai SDK with structured JSON output.""" def __init__( @@ -148,7 +157,7 @@ def extract( return InvoiceExtraction.model_validate_json(response.text) -class ClaudeExtractor(LLMExtractor): +class ClaudeExtractor: """Claude Haiku via tool_use. Stub — not implemented for MVP.""" def extract( @@ -159,7 +168,7 @@ def extract( raise NotImplementedError("ClaudeExtractor is a production fallback, not in MVP scope") -class OpenAIExtractor(LLMExtractor): +class OpenAIExtractor: """GPT-4o-mini via response_format. Stub — not implemented for MVP.""" def extract( @@ -168,19 +177,3 @@ def extract( table_extraction: TableExtractionOutput | None = None, ) -> InvoiceExtraction: raise NotImplementedError("OpenAIExtractor is a production fallback, not in MVP scope") - - -# --- Factory --- - - -def create_extractor(provider: str = "gemini") -> LLMExtractor: - """Create an LLM extractor by provider name.""" - match provider: - case "gemini": - return GeminiExtractor() - case "claude": - return ClaudeExtractor() - case "openai": - return OpenAIExtractor() - case _: - raise ValueError(f"Unknown LLM provider: {provider}") diff --git a/services/processing/invoice_processing/ocr.py b/services/processing/invoice_processing/ocr.py index c18fec2..b9dfdca 100644 --- a/services/processing/invoice_processing/ocr.py +++ b/services/processing/invoice_processing/ocr.py @@ -2,16 +2,23 @@ Single responsibility: convert PDF to images, run OCR, output (text, x, y) per detected line. No table reconstruction, no formatting. + +OCR runs through the ``OCRText`` Caravan seam, so it can flip between +inproc (in-process call against the registered impl) and container (HTTP +dispatch to a sidecar) via the ``CARAVAN_RPC_PEERS`` env var with zero +source-code change. """ from __future__ import annotations import io import logging +import os from dataclasses import dataclass, field import fitz # PyMuPDF import numpy as np +from caravan_rpc import client, wagon from PIL import Image logger = logging.getLogger(__name__) @@ -63,7 +70,7 @@ def to_dict(self) -> dict: } -# --- PDF conversion --- +# --- PDF / image conversion --- def pdf_to_images(pdf_bytes: bytes, dpi: int = 300) -> list[Image.Image]: @@ -80,21 +87,50 @@ def pdf_to_images(pdf_bytes: bytes, dpi: int = 300) -> list[Image.Image]: return images -# --- Raw OCR --- +def _bytes_to_images(file_bytes: bytes, filename: str = "") -> list[Image.Image]: + """File bytes → list of PIL Images. + + Used by both the OCR seam impl (for OCR on the peer) AND the worker + (for table extraction, which needs the same images locally). The + duplicated decode is cheap — pdf_to_images is sub-second; doing it + twice avoids passing PIL Images across the wire. + """ + ext = filename.lower().rsplit(".", 1)[-1] if filename else "" + if ext == "pdf" or file_bytes[:5] == b"%PDF-": + return pdf_to_images(file_bytes) + img = Image.open(io.BytesIO(file_bytes)) + img = img.convert("RGB") + return [img] + + +# --- Raw OCR (legacy helper, retained for tests + the seam impl) --- def run_raw_ocr(images: list[Image.Image]) -> RawOcrOutput: - """Run PaddleOCR on images, return text lines with coordinates.""" + """Run PaddleOCR on images, return text lines with coordinates. + + Convenience wrapper that constructs a fresh PaddleOCR each call. + Production paths go through ``PaddleOCRTextImpl`` which caches the + model instance for the lifetime of the process. + """ from paddleocr import PaddleOCR - import os ocr = PaddleOCR( lang="en", text_detection_model_name=os.environ.get("OCR_DET_MODEL", "PP-OCRv5_server_det"), text_recognition_model_name=os.environ.get("OCR_REC_MODEL", "en_PP-OCRv5_server_rec"), ) - pages: list[OcrPage] = [] + return _ocr_images(ocr, images) + + +def _ocr_images(ocr, images: list[Image.Image]) -> RawOcrOutput: + """Inner loop — run a configured PaddleOCR instance over the images. + Factored out of ``run_raw_ocr`` so ``PaddleOCRTextImpl`` can share + the per-page detection / coord-extraction logic without re-loading + the model on every call. + """ + pages: list[OcrPage] = [] for page_num, img in enumerate(images, start=1): img_array = np.array(img) results = list(ocr.predict(input=img_array)) @@ -111,10 +147,63 @@ def run_raw_ocr(images: list[Image.Image]) -> RawOcrOutput: height=img.size[1], lines=lines, )) - return RawOcrOutput(pages=pages) +# --- Caravan seam --- + + +@wagon +class OCRText: + """Caravan seam interface for raw OCR text extraction. + + Wire contract: takes the input file's raw bytes (PDF or image) plus + its filename for format detection, returns a structured RawOcrOutput. + The seam impl owns the bytes-to-images decode so PIL Image objects + do not cross the wire (they aren't JSON-serializable). + + Dispatch flips between inproc and HTTP container via the + ``CARAVAN_RPC_PEERS`` env var — `caravan compile --target=...` + sets this per-target. Source code is identical across modes. + """ + + def extract_text(self, file_bytes: bytes, filename: str = "") -> RawOcrOutput: ... + + +# --- Concrete impl --- + + +class PaddleOCRTextImpl: + """OCR implementation backed by PaddleOCR with a cached model. + + Constructs the PaddleOCR instance eagerly in ``__init__`` so the + expensive model load happens before the peer service binds its TCP + port (caravan_rpc.serve calls ``provide(OCRText, PaddleOCRTextImpl())`` + and then starts the HTTP server — the model is loaded by the time + the port opens, avoiding a cold-start race with consumers). + """ + + def __init__(self) -> None: + from paddleocr import PaddleOCR + + self._ocr = PaddleOCR( + lang="en", + text_detection_model_name=os.environ.get("OCR_DET_MODEL", "PP-OCRv5_server_det"), + text_recognition_model_name=os.environ.get("OCR_REC_MODEL", "en_PP-OCRv5_server_rec"), + ) + logger.info("PaddleOCRTextImpl ready (PaddleOCR model loaded)") + + def extract_text(self, file_bytes: bytes, filename: str = "") -> RawOcrOutput: + images = _bytes_to_images(file_bytes, filename) + raw = _ocr_images(self._ocr, images) + logger.info( + "OCRText.extract_text: %d lines across %d page(s)", + sum(len(p.lines) for p in raw.pages), + len(raw.pages), + ) + return raw + + # --- Top-level entry point --- @@ -122,20 +211,14 @@ def process_ocr(file_bytes: bytes, filename: str = "") -> tuple[RawOcrOutput, li """File bytes → raw OCR output + images. Supports PDF and image files (PNG, JPG, WEBP, etc.). - Returns both because table extractors may need the images. - """ - ext = filename.lower().rsplit(".", 1)[-1] if filename else "" - if ext == "pdf" or file_bytes[:5] == b"%PDF-": - images = pdf_to_images(file_bytes) - logger.info("Converted PDF to %d page image(s)", len(images)) - else: - img = Image.open(io.BytesIO(file_bytes)) - img = img.convert("RGB") - images = [img] - logger.info("Loaded image (%dx%d)", img.size[0], img.size[1]) - - raw_ocr = run_raw_ocr(images) + Returns both because table extractors need the images locally + (PIL Image objects can't cross the seam wire). The OCR-heavy work + dispatches through ``client(OCRText).extract_text(...)``; the + image decode runs locally regardless of seam dispatch mode. + """ + raw_ocr = client(OCRText).extract_text(file_bytes, filename) + images = _bytes_to_images(file_bytes, filename) logger.info( "Raw OCR: %d lines across %d page(s)", sum(len(p.lines) for p in raw_ocr.pages), diff --git a/services/processing/invoice_processing/table_extract.py b/services/processing/invoice_processing/table_extract.py index a6a1a2e..23dd6ef 100644 --- a/services/processing/invoice_processing/table_extract.py +++ b/services/processing/invoice_processing/table_extract.py @@ -1,26 +1,28 @@ """Table extraction — reconstruct table structure from raw OCR or layout models. -Single responsibility: take raw OCR lines (text + coordinates) and/or images, -produce structured table regions. Multiple strategies available: +Single responsibility: take raw OCR lines (text + coordinates) and/or document +bytes, produce structured table regions. Multiple strategies available: - SpatialClusterExtractor: dynamic gap-based row/column clustering from coordinates - PPStructureExtractor: PaddleOCR PPStructureV3 layout detection + HTML table parsing -Both output the same TableExtractionOutput, so the LLM extraction step -can receive either, both, or neither. +Table extraction runs through the ``OCRLayout`` Caravan seam, so it can flip +between inproc (in-process call against the registered impl) and container +(HTTP dispatch to a sidecar) via the ``CARAVAN_RPC_PEERS`` env var with zero +source-code change. Both strategies satisfy the same seam contract and emit +the same TableExtractionOutput, so the LLM extraction step can receive either. """ from __future__ import annotations import logging -from abc import ABC, abstractmethod from dataclasses import dataclass, field from html.parser import HTMLParser import numpy as np -from PIL import Image +from caravan_rpc import wagon -from .ocr import OcrLine, OcrPage, RawOcrOutput +from .ocr import OcrLine, OcrPage, RawOcrOutput, _bytes_to_images logger = logging.getLogger(__name__) @@ -153,34 +155,49 @@ def _detect_gaps(values: list[int]) -> list[list[int]]: return clusters -# --- Extractor interface --- +# --- Caravan seam --- -class TableExtractor(ABC): - """Abstract interface for table extraction strategies.""" +@wagon +class OCRLayout: + """Caravan seam interface for table / layout extraction. + + Wire contract: takes the raw OCR output (always cheap to serialize) plus + the input file's raw bytes (PDF or image) + its filename for format + detection. Returns a structured TableExtractionOutput. Impls choose + which inputs they need: SpatialClusterExtractor reads only raw_ocr; + PPStructureExtractor regenerates images from file_bytes internally so + PIL Image objects don't cross the wire. + + Dispatch flips between inproc and HTTP container via the + ``CARAVAN_RPC_PEERS`` env var — `caravan compile --target=...` sets + this per-target. Source code is identical across modes. + """ - @abstractmethod def extract( self, raw_ocr: RawOcrOutput, - images: list[Image.Image] | None = None, + file_bytes: bytes, + filename: str = "", ) -> TableExtractionOutput: ... -# --- Spatial cluster extractor --- +# --- Spatial cluster extractor (CPU-only; default OCRLayout impl) --- -class SpatialClusterExtractor(TableExtractor): +class SpatialClusterExtractor: """Reconstruct tables from raw OCR coordinates using gap detection. Groups lines into rows by y-coordinate gaps, joins same-row items - with tabs. Inserts region separators at large y-gaps. + with tabs. Inserts region separators at large y-gaps. Stateless; + no model load. Ignores file_bytes — operates on raw_ocr alone. """ def extract( self, raw_ocr: RawOcrOutput, - images: list[Image.Image] | None = None, + file_bytes: bytes = b"", + filename: str = "", ) -> TableExtractionOutput: pages: list[TablePage] = [] for ocr_page in raw_ocr.pages: @@ -230,27 +247,26 @@ def _cluster_page(self, page: OcrPage) -> list[TableRegion]: return regions -# --- PPStructure extractor --- +# --- PPStructure extractor (ML model; opt-in OCRLayout impl) --- -class PPStructureExtractor(TableExtractor): +class PPStructureExtractor: """Extract tables using PaddleOCR PPStructureV3 layout detection. - Requires images (runs its own inference). Raw OCR is not used. - Detects titled sections + tables with HTML structure. - """ + Eager-load the PPStructureV3 pipeline in __init__ so the peer's + TCP port only opens after the model is ready (no cold-start race + with consumer dispatch in container mode). Regenerates images from + file_bytes internally — raw_ocr is ignored. - def extract( - self, - raw_ocr: RawOcrOutput, - images: list[Image.Image] | None = None, - ) -> TableExtractionOutput: - if not images: - raise ValueError("PPStructureExtractor requires images") + Heavier than SpatialClusterExtractor; recommended only when the + layout-model accuracy matters enough to justify the model load + cost. For light dev runs, SpatialClusterExtractor is the default. + """ + def __init__(self) -> None: from paddleocr import PPStructureV3 - pipeline = PPStructureV3( + self._pipeline = PPStructureV3( use_doc_orientation_classify=False, use_doc_unwarping=False, use_seal_recognition=False, @@ -259,10 +275,21 @@ def extract( use_table_recognition=True, device="cpu", ) + logger.info("PPStructureExtractor ready (PPStructureV3 model loaded)") + def extract( + self, + raw_ocr: RawOcrOutput, + file_bytes: bytes, + filename: str = "", + ) -> TableExtractionOutput: + if not file_bytes: + raise ValueError("PPStructureExtractor requires file_bytes") + + images = _bytes_to_images(file_bytes, filename) pages: list[TablePage] = [] for page_num, img in enumerate(images, start=1): - results = list(pipeline.predict(input=np.array(img))) + results = list(self._pipeline.predict(input=np.array(img))) regions: list[TableRegion] = [] for result in results: blocks = result.json["res"].get("parsing_res_list", []) @@ -272,6 +299,11 @@ def extract( regions.append(region) pages.append(TablePage(page_number=page_num, regions=regions)) + logger.info( + "PPStructureExtractor.extract: %d page(s), %d regions total", + len(pages), + sum(len(p.regions) for p in pages), + ) return TableExtractionOutput(pages=pages, method="ppstructure") @staticmethod @@ -291,17 +323,3 @@ def _build_region(block: dict) -> TableRegion | None: return TableRegion(label="text", content=content.strip()) return None - - -# --- Factory --- - - -def create_table_extractor(method: str = "spatial_cluster") -> TableExtractor: - """Create a table extractor by method name.""" - match method: - case "spatial_cluster": - return SpatialClusterExtractor() - case "ppstructure": - return PPStructureExtractor() - case _: - raise ValueError(f"Unknown table extraction method: {method}") diff --git a/services/processing/invoice_processing/worker.py b/services/processing/invoice_processing/worker.py index 12339cb..736cef0 100644 --- a/services/processing/invoice_processing/worker.py +++ b/services/processing/invoice_processing/worker.py @@ -17,6 +17,7 @@ import signal from pathlib import Path +from caravan_rpc import client, provide from invoice_shared.adapters.blob_store import BlobStore from invoice_shared.adapters.factory import create_blob_store, create_queue from invoice_shared.adapters.queue import MessageQueue @@ -29,9 +30,9 @@ QueueBMessage, ) -from .extraction import LLMExtractor, create_extractor -from .ocr import process_ocr -from .table_extract import TableExtractor, create_table_extractor +from .extraction import GeminiExtractor, LLMExtraction +from .ocr import OCRText, PaddleOCRTextImpl, process_ocr +from .table_extract import OCRLayout, SpatialClusterExtractor from .validation import ValidationCheck, ValidationResult, validate_extraction logger = logging.getLogger(__name__) @@ -106,19 +107,22 @@ def run_pipeline( tenant_id: str, blob_store: BlobStore, db_session_factory=None, - extractor: LLMExtractor | None = None, - table_extractor: TableExtractor | None = None, use_cache: bool = False, + filename: str = "", ) -> tuple[dict, ValidationResult]: """Run the full OCR → table extraction → LLM extraction → validation pipeline. + The LLM extraction step dispatches through ``client(LLMExtraction).extract(...)``, + which routes to the impl registered via ``provide(LLMExtraction, ...)`` at + process startup (see ``run_worker`` below). Tests swap impls by re-registering; + HTTP-mode flips happen via the ``CARAVAN_RPC_PEERS`` env var, no source edits. + Args: pdf_bytes: Raw PDF file content. job_id: Job UUID. tenant_id: Tenant UUID. blob_store: Where to write intermediate artifacts. db_session_factory: SQLAlchemy session factory. None to skip DB transitions. - extractor: LLM extractor instance. Defaults to Gemini. table_extractor: Table extraction strategy. Defaults to SpatialCluster. Returns: @@ -169,9 +173,13 @@ def _transition(status: JobStatus) -> None: len(raw_ocr.pages), sum(len(p.lines) for p in raw_ocr.pages)) # --- Substep 1b: Table extraction --- - if table_extractor is None: - table_extractor = create_table_extractor("spatial_cluster") - table_extraction = table_extractor.extract(raw_ocr, images) + # Dispatches through the OCRLayout Caravan seam. Inproc mode calls the + # registered impl directly (SpatialClusterExtractor by default — see + # run_worker below); container mode dispatches over HTTP to the + # ocr-layout peer service. The seam impl chooses which inputs it + # consumes; SpatialCluster uses raw_ocr, PPStructure regenerates + # images from pdf_bytes internally. + table_extraction = client(OCRLayout).extract(raw_ocr, pdf_bytes, filename) blob_store.put( f"{blob_prefix}/table_extraction.json", json.dumps(table_extraction.to_dict(), ensure_ascii=False).encode(), @@ -180,9 +188,9 @@ def _transition(status: JobStatus) -> None: # --- Substep 2: LLM extraction --- _transition(JobStatus.EXTRACTING) - if extractor is None: - extractor = create_extractor("gemini") - extraction = extractor.extract(raw_ocr=raw_ocr, table_extraction=table_extraction) + extraction = client(LLMExtraction).extract( + raw_ocr=raw_ocr, table_extraction=table_extraction + ) extraction_dict = extraction.model_dump() blob_store.put( f"{blob_prefix}/extraction.json", @@ -260,6 +268,30 @@ def run_worker() -> None: format="%(asctime)s %(name)s %(levelname)s %(message)s", ) + # Register the LLM extraction impl. When CARAVAN_RPC_PEERS is unset (or + # marks LLMExtraction as inproc), client(LLMExtraction).extract is the + # bound method of this Gemini instance — no dispatch overhead. When the + # env var marks it as http/lambda, the proxy dispatches over the wire and + # this local provide() registration becomes inert (size cost only). + provide(LLMExtraction, GeminiExtractor()) + + # Register the OCRText seam impl (M3). Same dispatch story as + # LLMExtraction: inproc by default; container-mode if the target's + # yaml sets seams.OCRText: container, in which case the env var + # routes client(OCRText).extract_text(...) to the ocr-text peer + # service. PaddleOCR is loaded eagerly in __init__ so the model is + # ready before any dispatch — both inproc (first call) and container + # (peer cold-start) paths get a warmed instance. + provide(OCRText, PaddleOCRTextImpl()) + + # Register the OCRLayout seam impl (M6). SpatialClusterExtractor is + # the default — CPU-only, no model load, operates on raw_ocr + # coordinates alone. Heavier-model alternative is PPStructureExtractor + # (uses PaddleOCR PPStructureV3); the yaml `seams.OCRLayout.impl:` + # field on a container-mode target selects which class the peer + # serves — inproc registrations here become inert under HTTP dispatch. + provide(OCRLayout, SpatialClusterExtractor()) + config = load_config() blob_store = create_blob_store(config) queue = create_queue(config) diff --git a/tests/python/test_blob_store.py b/tests/python/test_blob_store.py index b67952a..d4ded67 100644 --- a/tests/python/test_blob_store.py +++ b/tests/python/test_blob_store.py @@ -1,8 +1,10 @@ -"""Tests for LocalFsBlobStore — path safety, CRUD operations.""" +"""Tests for LocalFsBlobStore + S3BlobStore — path safety, CRUD operations.""" + +import os import pytest -from invoice_shared.adapters.blob_store import LocalFsBlobStore +from invoice_shared.adapters.blob_store import LocalFsBlobStore, S3BlobStore TENANT = "a0000000-0000-0000-0000-000000000001" JOB = "b0000000-0000-0000-0000-000000000002" @@ -52,3 +54,70 @@ def test_rejects_non_uuid_job(self, tmp_blob_dir): store = LocalFsBlobStore(tmp_blob_dir) with pytest.raises(ValueError, match="Invalid job_id"): store.put(f"{TENANT}/not-a-uuid/file.txt", b"data") + + +# Skip the S3 tests if moto isn't installed (dev-only dependency). +moto = pytest.importorskip("moto", reason="moto required for S3BlobStore tests") + + +@moto.mock_aws +class TestS3BlobStore: + """End-to-end S3BlobStore tests against an in-process mock S3 (moto). + + `from_env` is also exercised via env-var manipulation in + test_from_env_uses_caravan_injected_vars below. + """ + + bucket = "invoice-parse-test" + + def _store(self) -> S3BlobStore: + # moto's mock_aws stubs the boto3 client at the SDK level; no endpoint + # URL is needed — boto3 talks to the in-memory mock. + return S3BlobStore(bucket=self.bucket, region="us-east-1") + + def test_put_get_roundtrip(self): + store = self._store() + path = f"{TENANT}/{JOB}/input.pdf" + data = b"fake pdf content" + store.put(path, data) + assert store.get(path) == data + + def test_exists(self): + store = self._store() + path = f"{TENANT}/{JOB}/input.pdf" + assert not store.exists(path) + store.put(path, b"data") + assert store.exists(path) + + def test_delete(self): + store = self._store() + path = f"{TENANT}/{JOB}/input.pdf" + store.put(path, b"data") + assert store.exists(path) + store.delete(path) + assert not store.exists(path) + + def test_rejects_path_traversal(self): + store = self._store() + with pytest.raises(ValueError, match="Path traversal"): + store.put("../escape/file.txt", b"bad") + + def test_rejects_non_uuid_tenant(self): + store = self._store() + with pytest.raises(ValueError, match="Invalid tenant_id"): + store.put("not-a-uuid/also-bad/file.txt", b"data") + + def test_from_env_uses_caravan_injected_vars(self, monkeypatch): + # Simulate Caravan's compose-injected env vars. We deliberately + # leave S3_ENDPOINT_URL unset so moto's mock_aws intercepts at + # the AWS SDK level — setting an endpoint_url would route boto3 + # past the mock to an unreachable URL. + monkeypatch.delenv("S3_ENDPOINT_URL", raising=False) + monkeypatch.setenv("S3_BUCKET", self.bucket) + monkeypatch.setenv("AWS_ACCESS_KEY_ID", "minioadmin") + monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "minioadmin") + monkeypatch.setenv("AWS_REGION", "us-east-1") + store = S3BlobStore.from_env() + path = f"{TENANT}/{JOB}/from_env.txt" + store.put(path, b"hi") + assert store.get(path) == b"hi" diff --git a/tests/python/test_queue.py b/tests/python/test_queue.py index e8e7344..3395480 100644 --- a/tests/python/test_queue.py +++ b/tests/python/test_queue.py @@ -1,8 +1,13 @@ -"""Tests for RedisStreamQueue — requires running Redis on localhost:6379.""" +"""Tests for RedisStreamQueue (integration) + RabbitMQQueue (unit, mocked).""" + +from __future__ import annotations + +import json +from unittest.mock import MagicMock, patch import pytest -from invoice_shared.adapters.queue import RedisStreamQueue +from invoice_shared.adapters.queue import RabbitMQQueue, RedisStreamQueue TOPIC = "test_queue_topic" @@ -56,3 +61,78 @@ def test_consume_empty_returns_empty(self, queue): # Now consume again — should be empty results = queue.consume(TOPIC, count=1, block_ms=100) assert results == [] + + +class TestRabbitMQQueueScheme: + """Tests for RabbitMQQueue.from_url scheme validation (no live broker required).""" + + def test_accepts_amqp_scheme(self): + q = RabbitMQQueue.from_url("amqp://guest:guest@localhost:5672/") + assert q._url == "amqp://guest:guest@localhost:5672/" + + def test_accepts_amqps_scheme(self): + q = RabbitMQQueue.from_url("amqps://guest@rabbit.example.com/") + assert q._url == "amqps://guest@rabbit.example.com/" + + def test_rejects_redis_scheme(self): + with pytest.raises(ValueError, match="expects amqp"): + RabbitMQQueue.from_url("redis://localhost:6379") + + def test_rejects_https_scheme(self): + with pytest.raises(ValueError, match="expects amqp"): + RabbitMQQueue.from_url("https://sqs.us-east-1.amazonaws.com/123/q") + + +class TestRabbitMQQueueWireOps: + """Tests for publish/consume/ack against a mocked pika channel.""" + + def _queue_with_mock_channel(self) -> tuple[RabbitMQQueue, MagicMock]: + q = RabbitMQQueue("amqp://guest:guest@localhost:5672/") + channel = MagicMock() + channel.is_closed = False + q._channel = channel + q._connection = MagicMock() + return q, channel + + def test_publish_declares_queue_then_basic_publish(self): + q, ch = self._queue_with_mock_channel() + msg = {"job_id": "abc", "data": "hello"} + + with patch("pika.BasicProperties"): + msg_id = q.publish("topic-x", msg) + + ch.queue_declare.assert_called_once_with(queue="topic-x", durable=True) + # basic_publish receives the json-encoded body + publish_kwargs = ch.basic_publish.call_args.kwargs + assert publish_kwargs["routing_key"] == "topic-x" + assert json.loads(publish_kwargs["body"]) == msg + assert msg_id # hex id + + def test_consume_returns_delivery_tag_and_payload(self): + q, ch = self._queue_with_mock_channel() + method = MagicMock() + method.delivery_tag = 7 + payload = {"job_id": "abc"} + ch.basic_get.return_value = (method, None, json.dumps(payload).encode()) + + results = q.consume("topic-x", count=1, block_ms=100) + + assert results == [("7", payload)] + ch.queue_declare.assert_called_once_with(queue="topic-x", durable=True) + + def test_consume_returns_empty_on_timeout(self): + q, ch = self._queue_with_mock_channel() + ch.basic_get.return_value = (None, None, None) + results = q.consume("topic-x", count=1, block_ms=10) + assert results == [] + + def test_ack_passes_int_delivery_tag(self): + q, ch = self._queue_with_mock_channel() + q.ack("topic-x", "42") + ch.basic_ack.assert_called_once_with(delivery_tag=42) + + def test_extend_visibility_is_noop(self): + q, ch = self._queue_with_mock_channel() + q.extend_visibility("topic-x", "5", 30) + ch.basic_nack.assert_not_called() + ch.basic_ack.assert_not_called()