From 51d064e38115eea9710855fc6df2033438e39396 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:15:19 +0200 Subject: [PATCH] refactor(memory): S3-generic store on boto3; RustFS is the tested server (no MinIO naming) The plane's memory store no longer names one vendor. `MinioStore`/`SdkClient`/ `minio_settings`/`MINIO_KEYS` become `S3Store`/`S3Client`/`s3_settings`/`S3_KEYS`; the URL scheme is `s3://[/]` (`s3://` alone uses `S3_BUCKET`); the settings are `S3_ENDPOINT` (a full URL), `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optional `AWS_SESSION_TOKEN` and `S3_REGION`. The client is boto3 (extra `s3`, imported lazily): SigV4, path-style addressing, SDK checksum trailers off, paginated listing, versions and delete markers listed and deleted by version (`purge`, used by the contract tests' teardown so the bucket keeps nothing of a run). Behaviour is unchanged: the store never configures its bucket and verifies versioning, the `runs/` expiry rule, S3 Select and tag reads at startup; no local fallback; every row carries every queryable field with "" placeholders. The admin hints read `--endpoint-url "$S3_ENDPOINT"`. The fake client still reproduces RustFS's two Select failures (unbound leading-row field, null-typed column). Docs: rustfs.md stays the tested server's setup page (policy JSON unchanged, env names updated, any S3-compatible store with versioning, lifecycle, tags and Select works); memory.md, ops/ax/README.md, README.md, steering and the two current spec designs use the S3 naming. Retired specs and committed measurements are untouched. Real server (RustFS, OUSAST_MEMORY_TEST_S3=1): test_plane_memory + test_learn_compile 75 passed, 1 skipped. Store copy of the live local store to a temporary prefix: every count matched (776 row objects, 2982 rows, 22 facts, 788 excerpts, 2601 responses, 2 programs, 1793 index entries), 4190 versions purged afterwards. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo --- .env.example | 21 +- .kiro/specs/harnessx-removal/design.md | 22 +- .kiro/specs/harnessx-removal/tasks.md | 4 +- .kiro/specs/learned-decision-engine/design.md | 8 +- .kiro/specs/learned-decision-engine/tasks.md | 2 +- .kiro/steering/overview.md | 2 +- README.md | 2 +- docs/memory.md | 10 +- docs/rustfs.md | 71 ++--- ops/ax/README.md | 33 +-- pyproject.toml | 13 +- src/openultrasast/cli.py | 4 +- src/openultrasast/improve/memory.py | 2 +- src/openultrasast/model/audit.py | 2 +- src/openultrasast/plane/memory.py | 246 ++++++++++++------ src/openultrasast/plane/tasks/loop.py | 2 +- tests/test_docs_site.py | 4 +- tests/test_learn_compile.py | 12 +- tests/test_learn_embeddings.py | 6 +- tests/test_learn_examples.py | 6 +- tests/test_plane_memory.py | 134 +++++----- uv.lock | 241 ++++------------- 22 files changed, 410 insertions(+), 437 deletions(-) diff --git a/.env.example b/.env.example index 0050f20..118e634 100644 --- a/.env.example +++ b/.env.example @@ -11,13 +11,14 @@ OPENROUTER_API_KEY= OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small # Plane memory store. Default is a local FileStore under ~/ousast-results/plane/memory. -# For an S3-compatible server (MinIO, RustFS): OUSAST_MEMORY=minio:// -# OUSAST_MEMORY=minio://sast-memory -# MINIO_ENDPOINT=host[:port] -# MINIO_ACCESS_KEY= -# MINIO_SECRET_KEY= -# MINIO_SECURE=true -# MINIO_REGION=us-east-1 -# MINIO_BUCKET=sast-memory -# The bucket must have versioning enabled and a lifecycle rule expiring runs/ (see ops/ax/README.md); -# the store verifies both at startup and refuses to run otherwise. +# For an S3-compatible server (RustFS is the tested one): OUSAST_MEMORY=s3://[/], or +# OUSAST_MEMORY=s3:// to use S3_BUCKET. Needs the s3 extra (boto3). Example for a RustFS server: +# OUSAST_MEMORY=s3://sast-memory +# S3_ENDPOINT=https://files.example.com +# S3_REGION=us-east-1 +# S3_BUCKET=sast-memory +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_SESSION_TOKEN= +# The bucket must have versioning enabled, a lifecycle rule expiring runs/, S3 Select and readable object tags +# (see docs/rustfs.md); the store verifies them at startup and refuses to run otherwise. diff --git a/.kiro/specs/harnessx-removal/design.md b/.kiro/specs/harnessx-removal/design.md index 74f3a3d..e60ca22 100644 --- a/.kiro/specs/harnessx-removal/design.md +++ b/.kiro/specs/harnessx-removal/design.md @@ -218,19 +218,19 @@ Ingest is idempotent: an `index.jsonl` hit skips the delivery, and rows carry a refuses to write when `shutil.disk_usage` reports less than 1 GiB free. The disk is at 98% today, so it names the store path in the error. -**Storage backends (maintainer, 2026-09-30: "think of using minio features, including its json support. i have a -server").** The store is written against one interface, `MemoryStore` (`put_row`, `put_facts`, `get_facts`, +**Storage backends (maintainer, 2026-09-30: asked to use the object-store features of the S3 server he runs, +including its JSON support).** The store is written against one interface, `MemoryStore` (`put_row`, `put_facts`, `get_facts`, `rows(repo=..., pin=..., kind=..., where=...)`, `index`), with two backends of identical behaviour: - `FileStore`: the layout above, for this laptop and the unit tests. -- `MinioStore`: the maintainer's MinIO server, and the store of record once the plane runs on a separate +- `S3Store`: an S3-compatible bucket (boto3; RustFS is the tested server), and the store of record once the plane runs on a separate Kubernetes cluster. The layout maps onto one bucket with the same key paths: - **Object metadata and tags** on every row object: `repo`, `pin`, `kind`, `family`, `run`, `population`, `split`. Rules list by prefix (`repos//`) and filter by tag instead of scanning the bucket. - **Server-side JSON queries**: `rows(where=...)` pushes the filter down with S3 Select (`SelectObjectContent`, JSON Lines input, JSON output), so a proposal rule reads matching rows, not whole files. Support is probed once per process against the server (a small Select on a known object); when the - server lacks it (some MinIO releases removed S3 Select), the backend fetches and filters locally, logs the + server lacks it, the backend fetches and filters locally, logs the fallback once, and returns the same rows. A query never silently returns nothing because a feature is missing. - **Versioning** on the bucket: each proposal's provenance records the object key and version id of every @@ -240,16 +240,16 @@ server").** The store is written against one interface, `MemoryStore` (`put_row` `facts/` and `proposals/` are kept. The near-full laptop disk stops being the store's limit. - **Presigned URLs** (the step that removes the host receiver for the Kubernetes move, not built in this spec): the reconciler can hand a task presigned PUT URLs for its outputs and GET URLs for its inputs in the - start request, so tasks write to MinIO directly and never hold credentials. The interface keeps + start request, so tasks write to the bucket directly and never hold credentials. The interface keeps `presign_put`/`presign_get` so that follow-on changes the reconciler, not the store. - **Bucket notifications** (follow-on): an event on new `repos/` rows can trigger the loop Run (event-driven loop engineering). -- **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `minio://`); the MinIO - endpoint and credentials come from `.env` (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, - `MINIO_SECURE`), never from a manifest, never printed. Client: the `minio` Python SDK as an optional extra - (`openultrasast[minio]`); the core install stays PyYAML-only. -- **Tests**: the same contract tests run against `FileStore` and against `MinioStore` pointed at a MinIO - endpoint (skipped unless `OUSAST_MEMORY_TEST_MINIO` is set), including the Select probe with its fallback +- **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `s3://[/]`); the S3 + endpoint and credentials come from `.env` (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, + optional `S3_REGION`), never from a manifest, never printed. Client: boto3 as an optional extra + (`openultrasast[s3]`); the core install stays PyYAML-only. +- **Tests**: the same contract tests run against `FileStore` and against `S3Store` pointed at an S3-compatible + endpoint (skipped unless `OUSAST_MEMORY_TEST_S3` is set), including the Select probe with its fallback forced, versioned provenance, and metadata filters. The disk-space refusal applies to `FileStore` only. **`remember` task** (`plane/tasks/remember.py`, no Model, budget `{usd: 0, calls: 0}`, one per case). It reads the diff --git a/.kiro/specs/harnessx-removal/tasks.md b/.kiro/specs/harnessx-removal/tasks.md index d2c8ea6..853604b 100644 --- a/.kiro/specs/harnessx-removal/tasks.md +++ b/.kiro/specs/harnessx-removal/tasks.md @@ -13,9 +13,9 @@ and passed its fixture test. Every task is a commit gated on the full suite's ow - _Requirements: 3.1, 5.1_ - [x] 2. Memory store and the `remember` task - - `plane/memory.py`: the `MemoryStore` interface with `FileStore` and `MinioStore` (object metadata/tags, + - `plane/memory.py`: the `MemoryStore` interface with `FileStore` and `S3Store` (object metadata/tags, S3 Select JSON queries probed with a local fallback, versioned provenance, lifecycle, presign stubs; - `OUSAST_MEMORY` selects; MinIO settings from `.env`; `minio` SDK as an optional extra); JSON rows per + `OUSAST_MEMORY` selects; S3 settings from `.env`; boto3 as an optional extra); JSON rows per repository + pin, facts by content hash, `FileStore` ingest refuses below 1 GiB free, `plane/tasks/remember.py` (model-free), `reconciler.mark_done` under the run lock, seed and ingest wiring in `cli._plane`, generator annotations; fact reuse when repository, pin, candidate set and runner image match. diff --git a/.kiro/specs/learned-decision-engine/design.md b/.kiro/specs/learned-decision-engine/design.md index 1753d11..abd0d0e 100644 --- a/.kiro/specs/learned-decision-engine/design.md +++ b/.kiro/specs/learned-decision-engine/design.md @@ -38,7 +38,7 @@ Six facts from the repository and the recorded results shape the design: (`push/policy.py:802-823`: "No shipped declaration is enabled"), so `pre-push` blocks nothing today. The engine's operating point replaces the capability-eligibility reasons, and the structural reasons stay. - **The memory store is the system of record and has the needed shape.** Rows are keyed by repository and pin, - with a closed `KINDS` tuple (`plane/memory.py:52`). There are two backends (FileStore, MinioStore) behind one + with a closed `KINDS` tuple (`plane/memory.py:52`). There are two backends (FileStore, S3Store) behind one interface (`plane/memory.py:176-291`), content-addressed blobs (`put_facts`, `plane/memory.py:249-255`), and a train-on-test `Guard` (`improve/memory.py:95-133`). Features, labels, decisions, model artifacts and experiment outcomes become new row kinds and blob prefixes. No second store is added. @@ -604,7 +604,7 @@ that hits its ceiling records `unfinished` and produces no artifact. `sha256(model id, Model parameters digest, messages, temperature, sample index, json_object)`. Every random choice (compile split, demo draws, tie-breaks, subsets) is seeded from `compile.toml`. Re-running a compile on the same store snapshot with the cache reproduces the artifact byte for byte at $0 (`test_learn_compile.py:: -test_reproducible_from_cache`, on FileStore and on MinioStore gated as today). Without the cache, temperature 0 is +test_reproducible_from_cache`, on FileStore and on S3Store gated as today). Without the cache, temperature 0 is not a determinism guarantee at the provider; the artifact records the cache digest so a re-run can say whether it was replayed or re-asked. @@ -700,7 +700,7 @@ and a top-up. No single run may start whose ceiling exceeds the balance read jus A compiled program is one canonical JSON (sorted keys, floats with `repr`), a content-addressed blob `programs/.json`, with `programs/index.jsonl` (every version) and `programs/adopted.json` (profile -> sha; -a single overwritten object, versioned under MinIO, history in the index on FileStore). On MinIO the object is +a single overwritten object, versioned on S3, history in the index on FileStore). On S3 the object is tagged `kind=program` and `profile`. ``` @@ -1038,7 +1038,7 @@ No test makes a model or embedding call: the chat and embedding clients are scri and stops at its ceiling; the response cache key and replay. - `tests/test_learn_compile.py`: bootstrap keeps only correct, confident, identity-free rationales; instruction candidates scored by the balanced Brier metric with ties to fewer tokens; compile split disjoint from evaluation - folds; `test_reproducible_from_cache` (byte-identical artifact on FileStore, and on MinioStore gated as today); + folds; `test_reproducible_from_cache` (byte-identical artifact on FileStore, and on S3Store gated as today); ceiling -> `unfinished`, no artifact. - `tests/test_learn_folds.py`: no group in both an evaluation fold and the compile split or retrieval pool, for outer, leave-one-source-out and leave-one-framework-out folds; determinism by seed. diff --git a/.kiro/specs/learned-decision-engine/tasks.md b/.kiro/specs/learned-decision-engine/tasks.md index e268a3b..beede23 100644 --- a/.kiro/specs/learned-decision-engine/tasks.md +++ b/.kiro/specs/learned-decision-engine/tasks.md @@ -47,7 +47,7 @@ a model or embedding call. redacted, delta diff); `learn/examples.py` and `ousast learn memory build` (label + feature record + excerpt -> `example` row; refuses `origin: user` and sources outside `sources.toml`; exits 2 when more than 10% of a source has no excerpt); `example` added to `KINDS`; `put_blob`/`get_blob` on both backends. - - Tests: `test_learn_excerpt.py`, example-builder refusals, blob round trip on FileStore (MinIO gated). + - Tests: `test_learn_excerpt.py`, example-builder refusals, blob round trip on FileStore (S3 gated). - _Requirements: 3.2, 3.6, 7.2_ - [x] 6.2 Metered embeddings and the cache - `plane/models/openrouter-embedding.yaml` (`input_per_m: 0.02`), `MeteredEmbeddingClient` in diff --git a/.kiro/steering/overview.md b/.kiro/steering/overview.md index efd3967..02d0fc2 100644 --- a/.kiro/steering/overview.md +++ b/.kiro/steering/overview.md @@ -15,7 +15,7 @@ User-facing documentation: `README.md`, `docs/architecture.md`, `docs/evaluation - **Agentic plane.** google/ax over Agent Substrate on kind is the only executor of model work over whole repositories (`ousast plane run|status|doctor|workspaces|remember|alerts-engine|harvest`). Credentials travel only in the start request through `atenet-router`; egress is per task; budgets are per task; rows go to the - memory store (`OUSAST_MEMORY`: FileStore, or MinioStore on the maintainer's MinIO). + memory store (`OUSAST_MEMORY`: FileStore, or S3Store on an S3-compatible bucket; RustFS is the tested server). - **Removed.** HarnessX (retired 2026-09-30): its config keys fail, a `[harnessx]` section warns. The LLM judge of the verifier and the LLM fusion panels are gone; fusion is deterministic. - **Improvement.** `ousast improve --memory` proposes rule-status edits from plane memory through the unchanged diff --git a/README.md b/README.md index 6dbb282..77fb0dd 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ uv run ousast scan . --mode quick --fail-on verified ``` The core install depends only on PyYAML. Optional extras: `semantic` (tree-sitter grammars -for the overlay) and `minio` (the plane's MinIO memory store). +for the overlay) and `s3` (the plane's memory store on an S3-compatible bucket, boto3). For `standard` and `deep` scans, use the Docker image, which ships Joern (with `php-cli` for the PHP frontend) next to the tool. Source the shell wrapper once and `ousast` runs in the diff --git a/docs/memory.md b/docs/memory.md index e9f4737..7a7a7f1 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -96,17 +96,17 @@ flowchart LR `OUSAST_MEMORY` selects the backend (`open_store`): -| | `FileStore` | `MinioStore` (S3: RustFS or MinIO) | +| | `FileStore` | `S3Store` (any S3-compatible store; RustFS is the tested server) | | --- | --- | --- | -| Selected by | `file:///path`, or nothing (default `$OUSAST_RESULTS/plane/memory`) | `minio://[/]` | -| Needs | a local disk; refuses to write below 1 GiB free | the `minio` extra, `MINIO_*` settings from `.env` or the environment | +| Selected by | `file:///path`, or nothing (default `$OUSAST_RESULTS/plane/memory`) | `s3://[/]`, or `s3://` for the bucket in `S3_BUCKET` | +| Needs | a local disk; refuses to write below 1 GiB free | the `s3` extra (boto3); `S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY` (optional `S3_REGION`, `AWS_SESSION_TOKEN`) from `.env` or the environment | | Filtered reads | read and filter locally | S3 Select pushdown (`SelectObjectContent` over JSON Lines); kind filter by object tag | | Object version cited by provenance | the file's content hash | the bucket's object version id | | Bucket setup | none | done once by an admin, verified at every start ([RustFS setup](rustfs.md)) | ### Verify at startup, no fallback -The S3 store never configures its bucket. Each time a `MinioStore` is opened for a bucket and +The S3 store never configures its bucket. Each time an `S3Store` is opened for a bucket and prefix (once per process), `verify_bucket()` checks, using reads plus one small probe object at `/_probe/select.jsonl`: @@ -125,7 +125,7 @@ messages are listed on [RustFS setup](rustfs.md#what-the-store-verifies-at-start RustFS's Select infers an object's JSON schema from its leading rows, so a `where` field missing there fails with `EvaluatorBindingDoesNotExist` even when a later row has it (measured -2026-10-02; `memory.py`, `MinioStore._select`). On main today the store turns that failure into a +2026-10-02; `memory.py`, `S3Store._select`). On main today the store turns that failure into a `MemoryStoreError` naming the field, never into an empty answer. A fixed set of queryable fields per row kind, so that every filtered field is present in every row of its kind, is in progress and not on main as of this page. diff --git a/docs/rustfs.md b/docs/rustfs.md index 7909d6b..88e3936 100644 --- a/docs/rustfs.md +++ b/docs/rustfs.md @@ -1,9 +1,11 @@ # RustFS setup for the memory store -The plane's memory store can live in an S3-compatible bucket: `OUSAST_MEMORY=minio://sast-memory` -selects `MinioStore` (`src/openultrasast/plane/memory.py`), which works with RustFS and MinIO. The -maintainer's server is RustFS at `files.because-security.com`, bucket `sast-memory`, store at the -bucket root. What the store keeps there is described on [Memory](memory.md). +The plane's memory store can live in an S3-compatible bucket: `OUSAST_MEMORY=s3://sast-memory` +selects `S3Store` (`src/openultrasast/plane/memory.py`), which talks to the server through boto3. +Any S3-compatible store with bucket versioning, lifecycle rules, object tags and S3 Select works; +RustFS is the server the contract tests run against. The maintainer's server is RustFS at +`files.because-security.com`, bucket `sast-memory`, store at the bucket root. What the store keeps +there is described on [Memory](memory.md). **The store never configures its bucket.** An admin sets the bucket up once with admin credentials. The agent account the store runs as can read the bucket's settings but not change @@ -15,7 +17,7 @@ flowchart LR admin["Admin, once: versioning on, runs/ expiry rule"] --> bucket[("Bucket sast-memory")] agent["Agent account: read settings, read and write objects and tags"] --> verify{"verify_bucket at startup"} bucket --> verify - verify -- "all present" --> store["MinioStore serves reads and writes"] + verify -- "all present" --> store["S3Store serves reads and writes"] verify -- "anything missing" --> refuse["MemoryStoreError naming each missing piece and its admin command"] ``` @@ -106,36 +108,43 @@ removing the expiry rule needs the admin. ## The `.env` settings The store reads its endpoint and credentials from `.env` in the working directory or from the -environment (`minio_settings`), never from a manifest, and never prints them. +environment (`s3_settings`), never from a manifest, and never prints them. The names are the ones +the AWS SDKs and CLI use for credentials, so one `.env` serves both the store and `aws s3api`. | Variable | Value for this server | Notes | | --- | --- | --- | -| `OUSAST_MEMORY` | `minio://sast-memory` | `minio://[/]`; unset means the local `FileStore` | -| `MINIO_ENDPOINT` | `files.because-security.com` | host[:port] without a scheme (the `minio` SDK's endpoint); required | -| `MINIO_ACCESS_KEY` | the agent account's access key | required | -| `MINIO_SECRET_KEY` | the agent account's secret key | required | -| `MINIO_SECURE` | `true` | TLS; `0`, `false`, `no` or `off` turn it off; default `true` | -| `MINIO_REGION` | optional | spares a GetBucketLocation call | +| `OUSAST_MEMORY` | `s3://sast-memory` | `s3://[/]`; `s3://` alone uses `S3_BUCKET`; unset means the local `FileStore` | +| `S3_ENDPOINT` | `https://files.because-security.com` | the full URL, scheme included (`https://host[:port]`); required | +| `AWS_ACCESS_KEY_ID` | the agent account's access key | required | +| `AWS_SECRET_ACCESS_KEY` | the agent account's secret key | required | +| `AWS_SESSION_TOKEN` | optional | for temporary credentials | +| `S3_REGION` | optional (`us-east-1` when unset) | the SigV4 signing region; spares a GetBucketLocation call | +| `S3_BUCKET` | `sast-memory` | the bucket `s3://` names when the URL has none; also the real-server tests' default bucket | + +The client signs with SigV4 and uses path-style addressing (`https://host/bucket/key`), which +RustFS needs. The SDK's own checksum trailers are off, because not every S3-compatible server +accepts them. - **Never commit `.env`.** It is gitignored (`.gitignore`: `.env`, `.env.*`, except `.env.example`). `.env.example` lists these names with empty values. - **`.env` never overrides a variable already exported in your shell** (`config.load_dotenv`). - A stale `export MINIO_SECRET_KEY=...` in your profile silently wins over the file; `unset` it - when a change in `.env` seems to have no effect. -- The store needs the `minio` extra (`uv sync --extra minio`); without it opening the store fails - with `OUSAST_MEMORY=minio://... needs the minio SDK: install openultrasast[minio]`. A missing - variable fails with `OUSAST_MEMORY=minio://... needs MINIO_ENDPOINT, MINIO_ACCESS_KEY, - MINIO_SECRET_KEY in .env or the environment` (naming only the missing ones). + A stale `export AWS_SECRET_ACCESS_KEY=...` in your profile silently wins over the file; `unset` + it when a change in `.env` seems to have no effect. +- The store needs the `s3` extra (`uv sync --extra s3`, which installs boto3); without it opening + the store fails with `OUSAST_MEMORY=s3://... needs boto3: install openultrasast[s3]`. A missing + variable fails with `OUSAST_MEMORY=s3://... needs S3_ENDPOINT, AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY in .env or the environment` (naming only the missing ones); an endpoint + without a scheme fails with `S3_ENDPOINT must be a URL with a scheme (https://host[:port])`. ## What the store verifies at startup -`MinioStore.verify_bucket()` runs once per process for each bucket and prefix. It reads the +`S3Store.verify_bucket()` runs once per process for each bucket and prefix. It reads the versioning status and the lifecycle rules, writes a two-line probe object at `_probe/select.jsonl` (under the prefix, if any), queries it with S3 Select and reads its tags. Every missing piece is collected into one error: ```text -memory minio://sast-memory: the bucket is not set up for the store. The store never configures its bucket (an admin sets it once, the store verifies); missing: +memory s3://sast-memory: the bucket is not set up for the store. The store never configures its bucket (an admin sets it once, the store verifies); missing: - ``` @@ -145,16 +154,15 @@ server's error code and the first 200 characters of its message): | Check | Refusal line | | --- | --- | | versioning unreadable | `versioning cannot be read (): the agent needs s3:GetBucketVersioning on arn:aws:s3:::sast-memory, and versioning must be Enabled` | -| versioning off | `versioning is Off, it must be Enabled (provenance cites object versions). Admin: aws s3api put-bucket-versioning --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory --versioning-configuration Status=Enabled` (the actual status, e.g. `Suspended`, replaces `Off` when set) | +| versioning off | `versioning is Off, it must be Enabled (provenance cites object versions). Admin: aws s3api put-bucket-versioning --endpoint-url "$S3_ENDPOINT" --bucket sast-memory --versioning-configuration Status=Enabled` (the actual status, e.g. `Suspended`, replaces `Off` when set) | | lifecycle unreadable | `the lifecycle configuration cannot be read (): the agent needs s3:GetLifecycleConfiguration on arn:aws:s3:::sast-memory, and a rule must expire runs/` | | a rule expires everything | `lifecycle rule(s) expire the whole store (repos/, facts/ and the index would be deleted); limit them to runs/` | -| no `runs/` rule | `no enabled lifecycle rule expires runs/ after a number of days. Admin (merged with any rules the bucket already has): aws s3api put-bucket-lifecycle-configuration --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'` | +| no `runs/` rule | `no enabled lifecycle rule expires runs/ after a number of days. Admin (merged with any rules the bucket already has): aws s3api put-bucket-lifecycle-configuration --endpoint-url "$S3_ENDPOINT" --bucket sast-memory --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'` | | tags unreadable | `object tags cannot be read on _probe/select.jsonl (): the store filters row objects by their kind tag, so the agent needs s3:GetObjectTagging on arn:aws:s3:::sast-memory/*` | -| S3 Select missing | `S3 Select (SelectObjectContent over JSON Lines) does not answer on _probe/select.jsonl (). The store requires it and has no local fallback: use a server with S3 Select (RustFS has it; some MinIO releases removed it) and let the agent s3:PutObject and s3:GetObject` | +| S3 Select missing | `S3 Select (SelectObjectContent over JSON Lines) does not answer on _probe/select.jsonl (). The store requires it and has no local fallback: use a server with S3 Select (RustFS has it; not every S3-compatible server does) and let the agent s3:PutObject and s3:GetObject` | -The admin commands inside the messages use `--endpoint-url "$MINIO_ENDPOINT"`; with the -host-only `MINIO_ENDPOINT` above, pass the full URL (`https://files.because-security.com`) as in -the steps on this page. +The admin commands inside the messages use `--endpoint-url "$S3_ENDPOINT"`, which is the full +URL from `.env`, so they run as written once `.env` is exported. There is **no fallback**: the store does not start in a degraded mode, does not drop to a local store, and does not fetch and filter objects itself. @@ -168,7 +176,7 @@ present at row 5000). The store does not read that as "no rows", since that answ drop matches; it raises: ```text -memory minio://sast-memory: S3 Select on failed (EvaluatorBindingDoesNotExist: ...) (a field of [] is absent from the object's leading rows, which the server reads as its schema); the store has no local fallback +memory s3://sast-memory: S3 Select on failed (EvaluatorBindingDoesNotExist: ...) (a field of [] is absent from the object's leading rows, which the server reads as its schema); the store has no local fallback ``` The rule that follows: a field the store filters on must be present in every row of its kind @@ -178,7 +186,8 @@ per kind in the store is in progress and not on main as of this page (see ## Checks against the real server -The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_MINIO=1` (bucket -`OUSAST_MEMORY_TEST_BUCKET`, else `MINIO_BUCKET`). They verify the bucket at its root and write -only under a fresh `contract-/` prefix; they never configure the bucket. More on the host -setup: [ax on this host](ops/ax/README.md). +The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_S3=1` (bucket +`OUSAST_MEMORY_TEST_BUCKET`, else `S3_BUCKET`). They verify the bucket at its root, write only +under a fresh `contract-/` prefix, and remove every version and delete marker under it +afterwards; they never configure the bucket. More on the host setup: +[ax on this host](ops/ax/README.md). diff --git a/ops/ax/README.md b/ops/ax/README.md index 6d65c9f..c4d35d7 100644 --- a/ops/ax/README.md +++ b/ops/ax/README.md @@ -76,9 +76,9 @@ is skipped by the index. A facts entry is reused for the same repository, pin, c `OUSAST_MEMORY` selects the backend: - `file:///path` (`FileStore`), default `$OUSAST_RESULTS/plane/memory`; it refuses to write below 1 GiB free; -- `minio://[/]` (`MinioStore`, the `minio` extra), used on the maintainer's MinIO. Endpoint and - credentials come from `.env` or the environment (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, - `MINIO_SECURE`), never from a manifest, and are never printed. +- `s3://[/]` (`S3Store`, the `s3` extra: boto3 against any S3-compatible server; RustFS is the + tested one). Endpoint and credentials come from `.env` or the environment (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, + `AWS_SECRET_ACCESS_KEY`), never from a manifest, and are never printed. `ousast improve --memory [STORE]` and the `ousast learn` commands (`--memory`) read the same store. @@ -117,15 +117,16 @@ afterwards (`go clean -cache -modcache`); rerunning the full Substrate install o - A Workspace's `files` reach the actor inline in one environment variable: above ~20 KB of content the template fails with "actor template not found" (an 86 KB and a 31 KB excerpt did; 7 KB ran). -## Memory store on S3 (MinIO or RustFS) +## Memory store on S3 (RustFS is the tested server) -`OUSAST_MEMORY=minio://[/]` puts the plane's memory store (`plane/memory.py`) in an -S3-compatible bucket. MinIO and RustFS both work; the maintainer's server is RustFS. Endpoint and credentials -come from `.env` (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_SECURE`, optional -`MINIO_REGION`, which avoids a GetBucketLocation call) and are never printed. +`OUSAST_MEMORY=s3://[/]` (or `s3://` for the bucket in `S3_BUCKET`) puts the plane's memory store +(`plane/memory.py`) in an S3-compatible bucket through boto3 (the `s3` extra). Any store with versioning, +lifecycle rules, object tags and S3 Select works; RustFS is what the contract tests run against. Endpoint and +credentials come from `.env` (`S3_ENDPOINT` as a full URL, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, optional +`AWS_SESSION_TOKEN` and `S3_REGION`, which avoids a GetBucketLocation call) and are never printed. -**The store never configures its bucket.** An admin sets it up once. Each time a `MinioStore` is opened it -checks the setup (`MinioStore.verify_bucket`), using reads plus one small probe object at +**The store never configures its bucket.** An admin sets it up once. Each time an `S3Store` is opened it +checks the setup (`S3Store.verify_bucket`), using reads plus one small probe object at `/_probe/select.jsonl`. If anything is missing, it refuses to start with a `MemoryStoreError` that lists each missing piece and the admin command that fixes it. Nothing is skipped quietly. The store needs: @@ -134,14 +135,14 @@ each missing piece and the admin command that fixes it. Nothing is skipped quiet default. The rule must be a plain prefix filter, and no rule may expire the whole store (`repos/`, `facts/`, `index.jsonl` and the blobs are kept); - **S3 Select** (`SelectObjectContent` over JSON Lines). Every filtered read is pushed down to the server, and - there is no fetch-and-filter fallback. A server without Select (some MinIO releases removed it) is refused; + there is no fetch-and-filter fallback. A server without Select is refused; - **object tags readable**, because rows are filtered by their `kind` tag. One-time admin setup (admin credentials, bucket `sast-memory`, store at the bucket root): - aws s3api put-bucket-versioning --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory \ + aws s3api put-bucket-versioning --endpoint-url "$S3_ENDPOINT" --bucket sast-memory \ --versioning-configuration Status=Enabled - aws s3api put-bucket-lifecycle-configuration --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory \ + aws s3api put-bucket-lifecycle-configuration --endpoint-url "$S3_ENDPOINT" --bucket sast-memory \ --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled", "Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}' @@ -178,8 +179,8 @@ before any read: a new query declares its field first. Rows written before the r it writes new object versions, and versioning keeps the old ones. Run it when a store moves to RustFS. The store never answers "no rows" for a failed Select; it raises a `MemoryStoreError` naming the object and the cause. -The real-server contract tests (`OUSAST_MEMORY_TEST_MINIO=1`, bucket `OUSAST_MEMORY_TEST_BUCKET`, else -`MINIO_BUCKET`) verify the bucket at its root and write only under a fresh `contract-/` prefix. They never +The real-server contract tests (`OUSAST_MEMORY_TEST_S3=1`, bucket `OUSAST_MEMORY_TEST_BUCKET`, else +`S3_BUCKET`) verify the bucket at its root and write only under a fresh `contract-/` prefix. They never configure the bucket. ## Moving to a separate Kubernetes cluster (planned) @@ -195,7 +196,7 @@ are; these parts assume this laptop and must change first (checked 2026-09-30): | ax's snapshot bucket: `AX_SNAPSHOTS_BUCKET` in ax's `deploy/ax-server.yaml` points at the ax authors' GCS bucket | ax deploy manifest | your own bucket, set before deploying ax | | Egress gateway applied by hand (agentgateway variant, no Rust build) | this README | the Substrate-installed gateway; per-task EgressPolicies work unchanged | | Worker pool of 2 x 1 CPU / 1.5 GiB for the 7 GB host | `workerpool.yaml.tmpl` | sized to the cluster; `--workers` to match | -| Plane memory store and results under `~/ousast-results/` on the host | `reconciler.py` (`OUSAST_RESULTS`), `memory.py` (`OUSAST_MEMORY`) | a persistent volume or bucket shared by the reconciler; the `minio://` store already works against any reachable MinIO | +| Plane memory store and results under `~/ousast-results/` on the host | `reconciler.py` (`OUSAST_RESULTS`), `memory.py` (`OUSAST_MEMORY`) | a persistent volume or bucket shared by the reconciler; the `s3://` store already works against any reachable S3-compatible server | The provider key already travels only in the start request through `atenet-router`, which works the same through a port-forward to any cluster. diff --git a/pyproject.toml b/pyproject.toml index e22909c..fbbc4bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,10 +16,10 @@ docs = [ "mkdocs>=1.6,<2", "mkdocs-material>=9.5,<10", ] -# The plane's memory store on MinIO (OUSAST_MEMORY=minio://, plane/memory.py). Imported lazily; the -# core install and the file store need nothing beyond PyYAML. -minio = [ - "minio>=7.2,<8", +# The plane's memory store on an S3-compatible bucket (OUSAST_MEMORY=s3://, plane/memory.py; RustFS is the +# tested server). Imported lazily; the core install and the file store need nothing beyond PyYAML. +s3 = [ + "boto3>=1.35,<2", ] # Structured parse for overlay (JS/C/C++/Java/Python). Core install stays empty. # Wheels are MIT; import is lazy via openultrasast.semantic.extra. @@ -69,9 +69,10 @@ warn_unused_configs = true module = "tree_sitter" ignore_missing_imports = true -# The minio SDK is an optional extra, absent in the type-check environment (plane/memory.py imports it lazily). +# boto3 is an optional extra without inline types, absent in the type-check environment (plane/memory.py imports +# it lazily). [[tool.mypy.overrides]] -module = ["minio", "minio.*"] +module = ["boto3", "botocore.*"] ignore_missing_imports = true # PyYAML ships no inline types and types-PyYAML is not a dev dependency. diff --git a/src/openultrasast/cli.py b/src/openultrasast/cli.py index c111556..bdf70bc 100644 --- a/src/openultrasast/cli.py +++ b/src/openultrasast/cli.py @@ -218,7 +218,7 @@ def _main(argv: list[str] | None) -> int: const="", default=None, metavar="STORE", - help="also propose rule-status edits from the plane memory store (a directory, file:// or minio:// URL; " + help="also propose rule-status edits from the plane memory store (a directory, file:// or s3:// URL; " "no value: OUSAST_MEMORY or the default store); off by default", ) improve.add_argument( @@ -290,7 +290,7 @@ def _main(argv: list[str] | None) -> int: plane_normalise = plane_sub.add_parser( "memory-normalise", help='rewrite the store\'s rows to the fixed schema (every queryable field, "" where absent); idempotent' ) - plane_normalise.add_argument("--store", help="the store URL (default OUSAST_MEMORY): file:/// or minio://[/]") + plane_normalise.add_argument("--store", help="the store URL (default OUSAST_MEMORY): file:/// or s3://[/]") plane_normalise.add_argument("--dry-run", action="store_true", help="count what would be rewritten, write nothing") plane_engine = plane_sub.add_parser( "alerts-engine", help="a Run's `alerts` for PHP and languages quick mode does not cover, from the engine image on this host" diff --git a/src/openultrasast/improve/memory.py b/src/openultrasast/improve/memory.py index 8ed5331..991e53f 100644 --- a/src/openultrasast/improve/memory.py +++ b/src/openultrasast/improve/memory.py @@ -32,7 +32,7 @@ rule the closed-loop leak broke, where a holdout pair taught the shape that then **Provenance**: each proposal's rationale is ``memory:``, ``proposal_id`` being the sha256 of (rule, edit key, sorted evidence row ids, thresholds). The sidecar ``memory_proposals.jsonl`` next to the journal carries -the evidence rows (id, repo, pin, run, task, object key and version -- MinIO's version id, a file's content hash), +the evidence rows (id, repo, pin, run, task, object key and version -- the bucket's object version id, a file's content hash), the counts that met each threshold, what the guard excluded and the store's index digest. """ diff --git a/src/openultrasast/model/audit.py b/src/openultrasast/model/audit.py index 85cbf22..77678e1 100644 --- a/src/openultrasast/model/audit.py +++ b/src/openultrasast/model/audit.py @@ -63,7 +63,7 @@ SPEC_OWNED = { "plane.tasks.agree": "ai-service-plane Req 5.3/2.1: agreement across two verify passes with per-candidate cost, turns and site match", "plane.reconciler": "ai-service-plane Req 3.1-3.5, 7.3: ax-backed Run execution, status attribution and doctor; `ousast plane`", "plane.generate": "ai-service-plane task 7: an increment's Workspaces, per-case Tasks and Run from a recorded set", - "plane.memory": "harnessx-removal Req 6.1/6.4: the memory store (file, MinIO), ingest and fact reuse; `ousast plane remember`", + "plane.memory": "harnessx-removal Req 6.1/6.4: the memory store (file, S3), ingest and fact reuse; `ousast plane remember`", "plane.tasks.remember": "harnessx-removal Req 6.1: a case's delivered artifacts as memory rows, model-free", "plane.tasks.alerts": "harnessx-removal Req 6.3: quick-mode alerts on a case's vulnerable and fixed pins, model-free", "plane.engine_alerts": "harnessx-removal Req 6.3: engine alerts where quick mode has no rules (PHP), on the host", diff --git a/src/openultrasast/plane/memory.py b/src/openultrasast/plane/memory.py index 809c932..e593e59 100644 --- a/src/openultrasast/plane/memory.py +++ b/src/openultrasast/plane/memory.py @@ -12,11 +12,13 @@ engine's own. Layout, identical for both backends:: by excerpt sha, responses/*.json by request sha, programs/*.json ``OUSAST_MEMORY`` selects the backend: ``file:///path`` (:class:`FileStore`, default ``results_root()/memory``) or -``minio://[/]`` (:class:`MinioStore`, the ``minio`` extra; MinIO or RustFS). The endpoint and -credentials come from ``.env`` (``MINIO_ENDPOINT``, ``MINIO_ACCESS_KEY``, ``MINIO_SECRET_KEY``, ``MINIO_SECURE``, -``MINIO_REGION``), never from a manifest, and are never printed. The store never configures its bucket: an admin -enables versioning and the ``runs/`` expiry rule once, and opening a ``MinioStore`` verifies them and S3 Select -(:meth:`MinioStore.verify_bucket`), refusing to start otherwise. +``s3://[/]`` (:class:`S3Store`, the ``s3`` extra: boto3 against any S3-compatible server with +versioning, lifecycle rules, object tags and S3 Select; RustFS is the server this is tested on). ``s3://`` without a +bucket names ``S3_BUCKET``. The endpoint and credentials come from ``.env`` (``S3_ENDPOINT``, a full URL; +``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``; optional ``AWS_SESSION_TOKEN``, ``S3_REGION``), never from a +manifest, and are never printed. The store never configures its bucket: an admin enables versioning and the +``runs/`` expiry rule once, and opening an ``S3Store`` verifies them and S3 Select (:meth:`S3Store.verify_bucket`), +refusing to start otherwise. Every row carries every queryable field (``QUERY_FIELDS``: strings, ``""`` where one does not apply, never ``null``): the store fills them in on every write, ``rows`` refuses a ``where`` field no kind declared, and ``ousast plane @@ -102,7 +104,7 @@ if tuple(QUERY_FIELDS) != KINDS: # pragma: no cover -- a kind added without its QUERYABLE = tuple(sorted({f for fields in QUERY_FIELDS.values() for f in fields} - set(ROW_FIELDS))) TAG_FIELDS = ("repo", "pin", "kind", "family", "run", "population", "split") MIN_FREE_BYTES = 1 << 30 -MINIO_KEYS = ("MINIO_ENDPOINT", "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY") +S3_KEYS = ("S3_ENDPOINT", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY") _PIN = re.compile(r"[0-9a-f]{40}") _FIELD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") _TAG_VALUE = re.compile(r"[^A-Za-z0-9 +\-=._:/@]") @@ -549,12 +551,12 @@ class FileStore(MemoryStore): return self._path(key).as_uri() -# --- the MinIO backend --------------------------------------------------------------------------------------------- +# --- the S3 backend ------------------------------------------------------------------------------------------------ @dataclass(frozen=True) class ExpiryRule: - """One bucket lifecycle rule as :meth:`MinioStore.verify_bucket` reads it: ``prefix`` is the filter's key prefix + """One bucket lifecycle rule as :meth:`S3Store.verify_bucket` reads it: ``prefix`` is the filter's key prefix (``""`` for a bucket-wide rule), ``tagged`` whether tags also narrow it, ``days`` its current-version expiration in days (None: no expiration by days), ``expires`` whether it expires current versions at all.""" @@ -567,8 +569,8 @@ class ExpiryRule: class ObjectClient(Protocol): - """The object operations :class:`MinioStore` needs; :class:`SdkClient` adapts the ``minio`` SDK, tests fake it. - The bucket-configuration calls only read: the store never configures its bucket.""" + """The object operations :class:`S3Store` needs; :class:`S3Client` adapts boto3, tests fake it. The + bucket-configuration calls only read: the store never configures its bucket.""" def get(self, key: str) -> tuple[bytes, str | None] | None: ... def put(self, key: str, data: bytes, labels: Mapping[str, str]) -> None: ... @@ -589,11 +591,21 @@ RUNS_RULE_ID = "ousast-runs-expiry" RUNS_EXPIRE_DAYS = 30 +def _code(exc: Exception) -> str | None: + """The server's error code: a ``code`` attribute, or botocore's ``response["Error"]["Code"]``.""" + code = getattr(exc, "code", None) + if code is None: + response = getattr(exc, "response", None) + if isinstance(response, Mapping): + code = (response.get("Error") or {}).get("Code") + return None if code is None else str(code) + + def _failure(exc: Exception) -> str: - return f"{getattr(exc, 'code', None) or type(exc).__name__}: {str(exc)[:200]}" + return f"{_code(exc) or type(exc).__name__}: {str(exc)[:200]}" -class MinioStore(MemoryStore): +class S3Store(MemoryStore): """The layout in one bucket (optionally under a prefix): object metadata and tags on every row object, S3 Select pushdown for every filtered read, bucket versioning for provenance, lifecycle expiry for ``runs/``, presigned URLs. An admin configures the bucket once; opening a store verifies it (:meth:`verify_bucket`) and @@ -606,7 +618,7 @@ class MinioStore(MemoryStore): _VERIFIED.add(self.describe()) def describe(self) -> str: - return f"minio://{self.bucket}" + (f"/{self.prefix}" if self.prefix else "") + return f"s3://{self.bucket}" + (f"/{self.prefix}" if self.prefix else "") def _k(self, key: str) -> str: return f"{self.prefix}/{key}" if self.prefix else key @@ -637,7 +649,7 @@ class MinioStore(MemoryStore): over JSON Lines, and the probe's object tags readable. Every missing piece is named with its admin action in one :class:`MemoryStoreError`.""" runs = self._k(RUNS_PREFIX) - endpoint = '--endpoint-url "$MINIO_ENDPOINT"' + endpoint = '--endpoint-url "$S3_ENDPOINT"' missing: list[str] = [] try: status = self.client.versioning() @@ -696,7 +708,7 @@ class MinioStore(MemoryStore): missing.append( f"S3 Select (SelectObjectContent over JSON Lines) does not answer on {self._k(PROBE_KEY)} " f"({select_error}). The store requires it and has no local fallback: use a server with S3 Select " - "(RustFS has it; some MinIO releases removed it) and let the agent s3:PutObject and s3:GetObject" + "(RustFS has it; not every S3-compatible server does) and let the agent s3:PutObject and s3:GetObject" ) if missing: raise MemoryStoreError( @@ -714,7 +726,7 @@ class MinioStore(MemoryStore): return _parse_lines(self.client.select(self._k(key), where_sql(where)), key), version except Exception as exc: hint = "" - if getattr(exc, "code", None) == "EvaluatorBindingDoesNotExist": + if _code(exc) == "EvaluatorBindingDoesNotExist": # RustFS infers an object's JSON schema from its leading rows: a field missing there is unbound even # when later rows carry it, so "no rows" would be a guess. Measured 2026-10-02 (field at row 5000). hint = f" (a field of {sorted(where)} is absent from the object's leading rows, which the server reads as its schema)" @@ -729,130 +741,195 @@ class MinioStore(MemoryStore): return self.client.presign("GET", self._k(key), expires) -class SdkClient: - """:class:`ObjectClient` over ``minio.Minio``; imported lazily so the core install needs no SDK.""" +_MISSING_OBJECT = ("NoSuchKey", "NoSuchObject", "NotFound", "404") - def __init__(self, endpoint: str, access_key: str, secret_key: str, secure: bool, bucket: str, region: str | None = None) -> None: + +class S3Client: + """:class:`ObjectClient` over boto3's S3 client; imported lazily so the core install needs no SDK. Path-style + addressing and SigV4, as RustFS needs; the SDK's own checksum trailers stay off because not every S3-compatible + server accepts them.""" + + def __init__( + self, endpoint: str, access_key: str, secret_key: str, bucket: str, region: str | None = None, session_token: str | None = None + ) -> None: try: - from minio import Minio + import boto3 + from botocore.config import Config except ImportError as exc: - raise MemoryStoreError("OUSAST_MEMORY=minio://... needs the minio SDK: install openultrasast[minio]") from exc - self.sdk = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure, region=region) + raise MemoryStoreError("OUSAST_MEMORY=s3://... needs boto3: install openultrasast[s3]") from exc + options: dict[str, Any] = {"signature_version": "s3v4", "s3": {"addressing_style": "path"}} + try: + config = Config(request_checksum_calculation="when_required", response_checksum_validation="when_required", **options) + except TypeError: # botocore before 1.36 has no checksum options and sends no trailers either + config = Config(**options) + self.sdk = boto3.client( + "s3", endpoint_url=endpoint, aws_access_key_id=access_key, aws_secret_access_key=secret_key, + aws_session_token=session_token, region_name=region or "us-east-1", config=config, + ) # fmt: skip self.bucket = bucket + @staticmethod + def _missing(exc: Exception) -> bool: + return _code(exc) in _MISSING_OBJECT + def get(self, key: str) -> tuple[bytes, str | None] | None: - from minio.error import S3Error + from botocore.exceptions import ClientError try: - response = self.sdk.get_object(self.bucket, key) - except S3Error as exc: - if exc.code in ("NoSuchKey", "NoSuchObject"): + response = self.sdk.get_object(Bucket=self.bucket, Key=key) + except ClientError as exc: + if self._missing(exc): return None raise + body = response["Body"] try: - return response.read(), response.headers.get("x-amz-version-id") + data: bytes = body.read() finally: - response.close() - response.release_conn() + body.close() + version = response.get("VersionId") + return data, None if version in (None, "null") else str(version) def put(self, key: str, data: bytes, labels: Mapping[str, str]) -> None: - import io + from urllib.parse import quote, urlencode - from minio.commonconfig import Tags - - tags = Tags(for_object=True) - for name, value in labels.items(): - tags[name] = value - metadata: dict[str, str | list[str] | tuple[str]] = dict(labels) - self.sdk.put_object( - self.bucket, key, io.BytesIO(data), len(data), content_type="application/x-ndjson", - metadata=metadata or None, tags=tags if labels else None, - ) # fmt: skip + extra: dict[str, Any] = {} + if labels: + extra["Metadata"] = dict(labels) + extra["Tagging"] = urlencode(dict(labels), quote_via=quote) + self.sdk.put_object(Bucket=self.bucket, Key=key, Body=data, ContentType="application/x-ndjson", **extra) def delete(self, key: str) -> None: - self.sdk.remove_object(self.bucket, key) + self.sdk.delete_object(Bucket=self.bucket, Key=key) def keys(self, prefix: str) -> list[str]: - return sorted(str(o.object_name) for o in self.sdk.list_objects(self.bucket, prefix=prefix, recursive=True)) + found: list[str] = [] + for page in self.sdk.get_paginator("list_objects_v2").paginate(Bucket=self.bucket, Prefix=prefix): + found.extend(str(o["Key"]) for o in page.get("Contents") or []) + return sorted(found) + + def versions(self, prefix: str) -> list[tuple[str, str]]: + """Every (key, version id) under ``prefix``, delete markers included.""" + found: list[tuple[str, str]] = [] + for page in self.sdk.get_paginator("list_object_versions").paginate(Bucket=self.bucket, Prefix=prefix): + for entry in [*(page.get("Versions") or []), *(page.get("DeleteMarkers") or [])]: + found.append((str(entry["Key"]), str(entry["VersionId"]))) + return sorted(found) + + def delete_version(self, key: str, version: str) -> None: + self.sdk.delete_object(Bucket=self.bucket, Key=key, VersionId=version) + + def purge(self, prefix: str) -> int: + """Remove every version and delete marker under ``prefix`` (a test prefix, a copy that is not wanted); + returns how many were removed. Nothing of the kind runs on a store's own layout.""" + removed = 0 + for key, version in self.versions(prefix): + self.delete_version(key, version) + removed += 1 + return removed def tags(self, key: str) -> dict[str, str]: - return dict(self.sdk.get_object_tags(self.bucket, key) or {}) + response = self.sdk.get_object_tagging(Bucket=self.bucket, Key=key) + return {str(t["Key"]): str(t["Value"]) for t in response.get("TagSet") or []} def version(self, key: str) -> str | None: - from minio.error import S3Error + from botocore.exceptions import ClientError try: - version = self.sdk.stat_object(self.bucket, key).version_id - return None if version is None else str(version) - except S3Error as exc: - if exc.code in ("NoSuchKey", "NoSuchObject"): + version = self.sdk.head_object(Bucket=self.bucket, Key=key).get("VersionId") + except ClientError as exc: + if self._missing(exc): return None raise + return None if version in (None, "null") else str(version) def select(self, key: str, expression: str) -> bytes: - from minio.select import JSONInputSerialization, JSONOutputSerialization, SelectRequest - - request = SelectRequest(expression, JSONInputSerialization(json_type="LINES"), JSONOutputSerialization(), request_progress=False) - with self.sdk.select_object_content(self.bucket, key, request) as result: - return b"".join(result.stream()) + response = self.sdk.select_object_content( + Bucket=self.bucket, Key=key, ExpressionType="SQL", Expression=expression, + InputSerialization={"JSON": {"Type": "LINES"}}, OutputSerialization={"JSON": {"RecordDelimiter": "\n"}}, + ) # fmt: skip + chunks: list[bytes] = [] + for event in response["Payload"]: # an error frame mid-stream raises botocore's EventStreamError + records = event.get("Records") + if records: + chunks.append(records["Payload"]) + return b"".join(chunks) def presign(self, method: str, key: str, expires: timedelta) -> str: - if method == "PUT": - return str(self.sdk.presigned_put_object(self.bucket, key, expires=expires)) - return str(self.sdk.presigned_get_object(self.bucket, key, expires=expires)) + operation = "put_object" if method == "PUT" else "get_object" + return str( + self.sdk.generate_presigned_url( + operation, Params={"Bucket": self.bucket, "Key": key}, ExpiresIn=int(expires.total_seconds()), HttpMethod=method + ) + ) def versioning(self) -> str: - return str(self.sdk.get_bucket_versioning(self.bucket).status_string) + return str(self.sdk.get_bucket_versioning(Bucket=self.bucket).get("Status") or "Off") def lifecycle(self) -> list[ExpiryRule]: - config = self.sdk.get_bucket_lifecycle(self.bucket) + from botocore.exceptions import ClientError + + try: + config = self.sdk.get_bucket_lifecycle_configuration(Bucket=self.bucket) + except ClientError as exc: + if _code(exc) == "NoSuchLifecycleConfiguration": + return [] + raise rules: list[ExpiryRule] = [] - for rule in config.rules if config else []: - found = rule.rule_filter - joined = found.and_operator if found else None - prefix = (joined.prefix if joined else found.prefix if found else None) or "" - tagged = bool(found and (found.tag or (joined and joined.tags))) - expiration = rule.expiration - expires = bool(expiration and (expiration.days or expiration.date)) - days = expiration.days if expiration else None - rules.append(ExpiryRule(str(rule.rule_id or ""), prefix, days, rule.status == "Enabled", tagged, expires)) + for rule in config.get("Rules") or []: + found = rule.get("Filter") or {} + joined = found.get("And") or {} + prefix = joined.get("Prefix") or found.get("Prefix") or rule.get("Prefix") or "" + tagged = bool(found.get("Tag") or joined.get("Tags")) + expiration = rule.get("Expiration") or {} + expires = bool(expiration.get("Days") or expiration.get("Date")) + days = expiration.get("Days") + rules.append(ExpiryRule(str(rule.get("ID") or ""), prefix, days, rule.get("Status") == "Enabled", tagged, expires)) return rules -def minio_settings(environ: Mapping[str, str] | None = None) -> dict[str, Any]: - """Endpoint, keys and TLS from ``.env``/the environment; a missing key is named, a value never is.""" +def s3_settings(environ: Mapping[str, str] | None = None) -> dict[str, Any]: + """Endpoint, keys, region and session token from ``.env``/the environment; a missing key is named, a value + never is. ``S3_ENDPOINT`` is a full URL (``https://host[:port]``).""" if environ is None: from ..config import load_dotenv load_dotenv() environ = os.environ - missing = [k for k in MINIO_KEYS if not environ.get(k)] + missing = [k for k in S3_KEYS if not environ.get(k)] if missing: - raise MemoryStoreError(f"OUSAST_MEMORY=minio://... needs {', '.join(missing)} in .env or the environment") - secure = (environ.get("MINIO_SECURE") or "true").strip().lower() not in ("0", "false", "no", "off") + raise MemoryStoreError(f"OUSAST_MEMORY=s3://... needs {', '.join(missing)} in .env or the environment") + endpoint = environ["S3_ENDPOINT"].strip() + if urlsplit(endpoint).scheme not in ("http", "https") or not urlsplit(endpoint).netloc: + raise MemoryStoreError("S3_ENDPOINT must be a URL with a scheme (https://host[:port])") settings: dict[str, Any] = { - "endpoint": environ["MINIO_ENDPOINT"], "access_key": environ["MINIO_ACCESS_KEY"], - "secret_key": environ["MINIO_SECRET_KEY"], "secure": secure, + "endpoint": endpoint, "access_key": environ["AWS_ACCESS_KEY_ID"], "secret_key": environ["AWS_SECRET_ACCESS_KEY"], } # fmt: skip - if environ.get("MINIO_REGION"): - settings["region"] = environ["MINIO_REGION"] # spares a GetBucketLocation the agent may not be allowed + if environ.get("S3_REGION"): + settings["region"] = environ["S3_REGION"] # spares a GetBucketLocation the agent may not be allowed + if environ.get("AWS_SESSION_TOKEN"): + settings["session_token"] = environ["AWS_SESSION_TOKEN"] return settings def open_store(spec: str | None = None, environ: Mapping[str, str] | None = None) -> MemoryStore: - """The store ``spec`` (else ``OUSAST_MEMORY``, else ``file:///memory``) names.""" + """The store ``spec`` (else ``OUSAST_MEMORY``, else ``file:///memory``) names. ``s3://`` with no + bucket uses ``S3_BUCKET``.""" from .reconciler import results_root - text = spec or (environ if environ is not None else os.environ).get("OUSAST_MEMORY") or "" + env = environ if environ is not None else os.environ + text = spec or env.get("OUSAST_MEMORY") or "" if not text: return FileStore(results_root() / "memory") parts = urlsplit(text) if parts.scheme == "file" and parts.path: return FileStore(Path(parts.path)) - if parts.scheme == "minio" and parts.netloc: - settings = minio_settings(environ) - return MinioStore(SdkClient(bucket=parts.netloc, **settings), parts.netloc, parts.path) - raise MemoryStoreError(f"OUSAST_MEMORY must be file:/// or minio://[/], got {text!r}") + if parts.scheme == "s3": + bucket = parts.netloc or env.get("S3_BUCKET") or "" + if not bucket: + raise MemoryStoreError("OUSAST_MEMORY=s3:// names no bucket and S3_BUCKET is unset: set one of them") + settings = s3_settings(environ) + return S3Store(S3Client(bucket=bucket, **settings), bucket, parts.path) + raise MemoryStoreError(f"OUSAST_MEMORY must be file:/// or s3://[/], got {text!r}") # --- ingest and seed ----------------------------------------------------------------------------------------------- @@ -950,11 +1027,11 @@ __all__ = [ "IngestResult", "MemoryStore", "MemoryStoreError", - "MinioStore", "ObjectClient", + "S3Client", + "S3Store", "ExpiryRule", "Record", - "SdkClient", "check_query", "counts", "image_digest", @@ -965,6 +1042,7 @@ __all__ = [ "parse_memory_key", "repo_key", "row_id", + "s3_settings", "seed", "validate_row", "where_sql", diff --git a/src/openultrasast/plane/tasks/loop.py b/src/openultrasast/plane/tasks/loop.py index 3e49030..8ea364c 100644 --- a/src/openultrasast/plane/tasks/loop.py +++ b/src/openultrasast/plane/tasks/loop.py @@ -2,7 +2,7 @@ ``command: ["loop", ]``; every step is model-free with budget ``{usd: 0, calls: 0}`` (``budget.py`` refuses any call, so a stray one fails loudly), and each writes ``summary.json`` under the task contract. Records travel as -``{"key", "version", "row"}`` lines: the store object a row came from and its version (MinIO's version id, a file's +``{"key", "version", "row"}`` lines: the store object a row came from and its version (the bucket's object version id, a file's content hash), which a proposal's provenance cites (Req 6.4). - ``snapshot``: never runs in the sandbox, which cannot read the host's store. ``ousast plane run`` seeds it before diff --git a/tests/test_docs_site.py b/tests/test_docs_site.py index 49f033b..e912251 100644 --- a/tests/test_docs_site.py +++ b/tests/test_docs_site.py @@ -119,8 +119,8 @@ def test_rustfs_page_lists_the_complete_policy_and_no_secret() -> None: for needle in ( '"Sid": "BucketSettingsReadOnly"', '"Sid": "ObjectsReadWrite"', '"arn:aws:s3:::sast-memory"', '"arn:aws:s3:::sast-memory/*"', "put-bucket-versioning", "put-bucket-lifecycle-configuration", "--endpoint-url https://files.because-security.com", - "--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "minio://sast-memory", "MINIO_ENDPOINT", - "MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", "MINIO_SECURE", "MINIO_REGION", "EvaluatorBindingDoesNotExist", + "--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "s3://sast-memory", "S3_ENDPOINT", + "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "S3_REGION", "S3_BUCKET", "EvaluatorBindingDoesNotExist", ): # fmt: skip assert needle in text, f"rustfs.md lacks {needle!r}" for shape in SECRET_SHAPES: diff --git a/tests/test_learn_compile.py b/tests/test_learn_compile.py index bd9674f..f5bc9ee 100644 --- a/tests/test_learn_compile.py +++ b/tests/test_learn_compile.py @@ -11,7 +11,7 @@ from pathlib import Path import pytest from learn_fixtures import ScriptedChat, by_id, corpus, default_answer, example -from test_plane_memory import FakeObjects, _real_minio +from test_plane_memory import FakeObjects, _real_s3 from openultrasast.learn import compile as compile_module from openultrasast.learn.compile import ( @@ -34,7 +34,7 @@ from openultrasast.learn.examples import Example from openultrasast.learn.folds import Fold, FoldError from openultrasast.learn.program import Caller, Demo from openultrasast.plane.budget import MeteredClient -from openultrasast.plane.memory import FileStore, MemoryStore, MinioStore +from openultrasast.plane.memory import FileStore, MemoryStore, S3Store PRICES = {"cache_hit_per_m": 0.014, "input_per_m": 0.44, "output_per_m": 1.32} MEMORY = corpus(groups=24, per_group=4) @@ -58,14 +58,14 @@ def answer(messages: Sequence[Mapping[str, object]], temperature: float | None) return json.dumps(data) -@pytest.fixture(params=["file", "fake-minio", "minio"]) +@pytest.fixture(params=["file", "fake-s3-select", "s3"]) def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]: if request.param == "file": yield FileStore(tmp_path / "memory") - elif request.param == "minio": - yield from _real_minio() + elif request.param == "s3": + yield from _real_s3() else: - yield MinioStore(FakeObjects("works"), "bucket", "p") + yield S3Store(FakeObjects("works"), "bucket", "p") def _caller(chat: ScriptedChat, store: MemoryStore | None = None, **budget: object) -> Caller: diff --git a/tests/test_learn_embeddings.py b/tests/test_learn_embeddings.py index 6479c45..bb7cc0c 100644 --- a/tests/test_learn_embeddings.py +++ b/tests/test_learn_embeddings.py @@ -23,7 +23,7 @@ from openultrasast.learn.embeddings import ( from openultrasast.learn.excerpt import excerpt_sha from openultrasast.plane.budget import BudgetExhausted, MeteredEmbeddingClient, UnmeteredUsage from openultrasast.plane.manifests import load_manifests -from openultrasast.plane.memory import FileStore, MemoryStore, MinioStore +from openultrasast.plane.memory import FileStore, MemoryStore, S3Store from openultrasast.provider.openrouter import parse_embedding_payload PRICES = {"cache_hit_per_m": 0, "input_per_m": 0.02, "output_per_m": 0} @@ -43,9 +43,9 @@ class ScriptedEmbedder: return [[float(len(text) % 7 + i) for i in range(self.dims)] for text in inputs] -@pytest.fixture(params=["file", "fake-minio"]) +@pytest.fixture(params=["file", "fake-s3-select"]) def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]: - yield FileStore(tmp_path / "memory") if request.param == "file" else MinioStore(FakeObjects("works"), "bucket", "p") + yield FileStore(tmp_path / "memory") if request.param == "file" else S3Store(FakeObjects("works"), "bucket", "p") def _excerpts(store: MemoryStore, n: int) -> list[str]: diff --git a/tests/test_learn_examples.py b/tests/test_learn_examples.py index e60fe6a..31d81c7 100644 --- a/tests/test_learn_examples.py +++ b/tests/test_learn_examples.py @@ -24,7 +24,7 @@ from openultrasast.learn.examples import ( ) from openultrasast.learn.features import Part, quick_part, record from openultrasast.learn.labels import Source, Sources -from openultrasast.plane.memory import FileStore, MemoryStore, MemoryStoreError, MinioStore +from openultrasast.plane.memory import FileStore, MemoryStore, MemoryStoreError, S3Store PIN = "0123456789abcdef0123456789abcdef01234567" SOURCES = Sources( @@ -39,9 +39,9 @@ CODE = { } -@pytest.fixture(params=["file", "fake-minio"]) +@pytest.fixture(params=["file", "fake-s3-select"]) def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]: - yield FileStore(tmp_path / "memory") if request.param == "file" else MinioStore(FakeObjects("works"), "bucket", "p") + yield FileStore(tmp_path / "memory") if request.param == "file" else S3Store(FakeObjects("works"), "bucket", "p") def label(**kw: Any) -> dict[str, Any]: diff --git a/tests/test_plane_memory.py b/tests/test_plane_memory.py index f56e56b..8e8d002 100644 --- a/tests/test_plane_memory.py +++ b/tests/test_plane_memory.py @@ -20,17 +20,17 @@ import pytest from openultrasast.plane import memory, reconciler from openultrasast.plane.memory import ( - MINIO_KEYS, + S3_KEYS, ExpiryRule, FileStore, MemoryStore, MemoryStoreError, - MinioStore, - SdkClient, + S3Client, + S3Store, memory_key, - minio_settings, open_store, row_id, + s3_settings, seed, where_sql, ) @@ -40,10 +40,10 @@ PIN = "0123456789abcdef0123456789abcdef01234567" OTHER_PIN = "fedcba9876543210fedcba9876543210fedcba98" IMAGE = "sha256:" + "a" * 64 FACTS = json.dumps({"callers": {"a.py::run": []}, "counts": {"files": 1}, "files": {"a.py": {"functions": ["run"]}}}).encode() -MINIO_SKIP = ( - "MinIO/RustFS contract tests need OUSAST_MEMORY_TEST_MINIO=1 plus MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY " - "and a bucket (OUSAST_MEMORY_TEST_BUCKET, else MINIO_BUCKET) exported (optional: MINIO_SECURE, MINIO_REGION) and the " - "minio extra installed; the admin configures that bucket, the tests never do" +S3_SKIP = ( + "real-server contract tests need OUSAST_MEMORY_TEST_S3=1 plus S3_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY " + "and a bucket (OUSAST_MEMORY_TEST_BUCKET, else S3_BUCKET) exported (optional: S3_REGION, AWS_SESSION_TOKEN) and the " + "s3 extra (boto3) installed; the admin configures that bucket, the tests never do" ) @@ -112,13 +112,13 @@ class FakeObjects: leading = rows[: self.schema_rows] typeless = {k for r in leading for k, v in r.items() if v is None and all(x.get(k) is None for x in leading)} if any(r.get(k) is not None for r in rows[self.schema_rows :] for k in typeless): # null-typed, set later - raise type("MinioException", (Exception,), {})("JSONParsingError: An error occurred while parsing the JSON file") + raise type("SelectFailure", (Exception,), {})("JSONParsingError: An error occurred while parsing the JSON file") clauses = re.findall(r"s\.\"(\w+)\" = ('(?:[^']|'')*'|true|false|-?[\d.]+)", expression) want = {n: v[1:-1].replace("''", "'") if v.startswith("'") else json.loads(v) for n, v in clauses} return b"".join(json.dumps(r).encode() + b"\n" for r in rows if all(r.get(k) == v for k, v in want.items())) def presign(self, method: str, key: str, expires: timedelta) -> str: - return f"https://minio.test/{key}?method={method}&X-Amz-Expires={int(expires.total_seconds())}" + return f"https://s3.test/{key}?method={method}&X-Amz-Expires={int(expires.total_seconds())}" def versioning(self) -> str: if self.status == "denied": @@ -136,33 +136,32 @@ def fresh_verification(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.setattr(memory, "_VERIFIED", set()) -def _real_minio() -> Iterator[MemoryStore]: - if os.environ.get("OUSAST_MEMORY_TEST_MINIO") != "1" or any(not os.environ.get(k) for k in MINIO_KEYS): - pytest.skip(MINIO_SKIP) - pytest.importorskip("minio", reason=MINIO_SKIP) - bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("MINIO_BUCKET") +def _real_s3() -> Iterator[MemoryStore]: + if os.environ.get("OUSAST_MEMORY_TEST_S3") != "1" or any(not os.environ.get(k) for k in S3_KEYS): + pytest.skip(S3_SKIP) + pytest.importorskip("boto3", reason=S3_SKIP) + bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("S3_BUCKET") if not bucket: - pytest.skip(MINIO_SKIP) - client = SdkClient(bucket=bucket, **minio_settings(os.environ)) - MinioStore(client, bucket) # verifies the admin's setup at the bucket root; the tests never configure the bucket + pytest.skip(S3_SKIP) + client = S3Client(bucket=bucket, **s3_settings(os.environ)) + S3Store(client, bucket) # verifies the admin's setup at the bucket root; the tests never configure the bucket # The contract rows go under a fresh prefix so every test starts empty; the admin's expiry rule covers the root's # runs/, not this prefix's, so the prefixed store counts as verified by the root's check above. prefix = f"contract-{uuid.uuid4().hex[:12]}" - memory._VERIFIED.add(f"minio://{bucket}/{prefix}") - store = MinioStore(client, bucket, prefix) + memory._VERIFIED.add(f"s3://{bucket}/{prefix}") + store = S3Store(client, bucket, prefix) yield store - for key in client.keys(store.prefix + "/"): - client.delete(key) + client.purge(store.prefix + "/") # every version and delete marker: the bucket keeps nothing of the test -@pytest.fixture(params=["file", "fake-minio", "minio"]) +@pytest.fixture(params=["file", "fake-s3-select", "s3"]) def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]: if request.param == "file": yield FileStore(tmp_path / "memory") - elif request.param == "minio": - yield from _real_minio() + elif request.param == "s3": + yield from _real_s3() else: - yield MinioStore(FakeObjects(), "bucket", "p") + yield S3Store(FakeObjects(), "bucket", "p") # --- the contract -------------------------------------------------------------------------------------------------- @@ -269,7 +268,7 @@ def test_a_declared_field_absent_from_the_leading_rows_is_queryable(store: Memor def test_the_fake_client_mimics_rustfs_and_the_normaliser_repairs_a_pre_rule_object() -> None: client = FakeObjects("works") - store = MinioStore(client, "bucket") + store = S3Store(client, "bucket") alerts, late = _late() key = f"repos/github.com__o__r/{PIN}.jsonl" legacy = sorted([*alerts, late], key=lambda r: str(r["id"])) # written before the rule: no queryable fields @@ -302,9 +301,9 @@ def test_a_where_on_an_undeclared_field_an_unknown_kind_or_null_is_refused_befor assert memory.QUERYABLE == ("candidate", "candidates_digest", "final", "profile") -@pytest.mark.parametrize("backend", ["file", "fake-minio"]) +@pytest.mark.parametrize("backend", ["file", "fake-s3-select"]) def test_the_normaliser_is_idempotent_and_keeps_every_value(backend: str, tmp_path: Path) -> None: - store: MemoryStore = FileStore(tmp_path / "memory") if backend == "file" else MinioStore(FakeObjects(), "bucket", "p") + store: MemoryStore = FileStore(tmp_path / "memory") if backend == "file" else S3Store(FakeObjects(), "bucket", "p") legacy = [verdict("a.py::run", "agreed"), row("unit_cost", "a:a.py", **{"pass": "a", "usd": 0.5, "final": None})] store._put(f"repos/github.com__o__r/{PIN}.jsonl", "".join(json.dumps(r) + "\n" for r in legacy).encode()) current = memory.normalise_row(row("alert", "x", pin=OTHER_PIN)) @@ -336,11 +335,11 @@ def test_the_memory_normalise_command_prints_its_counts(tmp_path: Path, capsys: assert "rewrote 0/1 objects; 0/1 rows filled 0 absent or null fields" in capsys.readouterr().out.splitlines()[-1] -# --- MinIO specifics on the fake client ---------------------------------------------------------------------------- +# --- S3 specifics on the fake client ---------------------------------------------------------------------------- -def _filled(client: FakeObjects) -> MinioStore: - store = MinioStore(client, "bucket") +def _filled(client: FakeObjects) -> S3Store: + store = S3Store(client, "bucket") store.put_rows([verdict("a.py::run", "agreed"), verdict("a.py::x", "disputed"), row("unit_cost", "a:a.py", **{"pass": "a"})]) return store @@ -358,7 +357,7 @@ def test_select_is_pushed_down_when_the_probe_answers(caplog: pytest.LogCaptureF @pytest.mark.parametrize("mode", ["raises", "wrong"]) def test_a_server_without_select_is_refused_at_open(mode: str) -> None: with pytest.raises(MemoryStoreError, match=r"S3 Select .* does not answer on _probe/select.jsonl .*no local fallback") as caught: - MinioStore(FakeObjects(mode), "bucket") + S3Store(FakeObjects(mode), "bucket") assert "versioning" not in str(caught.value) and "lifecycle" not in str(caught.value) @@ -383,20 +382,20 @@ def test_an_unbound_field_on_rustfs_raises_naming_the_schema_inference_never_ret def test_verify_bucket_passes_on_the_admin_setup_and_writes_only_its_probe() -> None: client = FakeObjects() - store = MinioStore(client, "bucket", "p") + store = S3Store(client, "bucket", "p") assert sorted(client.objects) == ["p/_probe/select.jsonl"] and not hasattr(client, "enable_versioning") store.verify_bucket() - MinioStore(client, "bucket", "p") + S3Store(client, "bucket", "p") assert len(client.selects) == 2, "opening a verified store again does not re-verify in the same process" @pytest.mark.parametrize("status", ["Suspended", "Off"]) def test_verify_bucket_refuses_versioning_off_naming_the_admin_command(status: str) -> None: with pytest.raises(MemoryStoreError, match=rf"versioning is {status}, it must be Enabled") as caught: - MinioStore(FakeObjects(versioning=status), "sast-memory") + S3Store(FakeObjects(versioning=status), "sast-memory") message = str(caught.value) assert "never configures its bucket" in message and "lifecycle" not in message and "Select" not in message - assert 'aws s3api put-bucket-versioning --endpoint-url "$MINIO_ENDPOINT" --bucket sast-memory' in message + assert 'aws s3api put-bucket-versioning --endpoint-url "$S3_ENDPOINT" --bucket sast-memory' in message @pytest.mark.parametrize( @@ -412,25 +411,25 @@ def test_verify_bucket_refuses_versioning_off_naming_the_admin_command(status: s ) def test_verify_bucket_refuses_a_missing_runs_expiry_rule(rules: list[ExpiryRule]) -> None: with pytest.raises(MemoryStoreError, match=r"no enabled lifecycle rule expires runs/") as caught: - MinioStore(FakeObjects(rules=rules), "sast-memory") + S3Store(FakeObjects(rules=rules), "sast-memory") assert "put-bucket-lifecycle-configuration" in str(caught.value) assert '"Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}' in str(caught.value) def test_verify_bucket_names_the_prefixed_runs_and_refuses_a_rule_expiring_the_whole_store() -> None: with pytest.raises(MemoryStoreError, match=r"no enabled lifecycle rule expires mem/runs/"): - MinioStore(FakeObjects(rules=[ExpiryRule("r", "runs/", 30, True)]), "bucket", "mem") + S3Store(FakeObjects(rules=[ExpiryRule("r", "runs/", 30, True)]), "bucket", "mem") wide = [ExpiryRule("everything", "", 30, True), ExpiryRule("ousast-runs-expiry", "runs/", 30, True)] with pytest.raises(MemoryStoreError, match=r"lifecycle rule\(s\) everything expire the whole store"): - MinioStore(FakeObjects(rules=wide), "bucket") - MinioStore(FakeObjects(rules=[ExpiryRule("r", "mem/runs/", 14, True)]), "bucket", "mem") + S3Store(FakeObjects(rules=wide), "bucket") + S3Store(FakeObjects(rules=[ExpiryRule("r", "mem/runs/", 14, True)]), "bucket", "mem") def test_verify_bucket_names_every_missing_piece_and_the_permissions_to_read_them() -> None: client = FakeObjects("raises", versioning="denied", rules="denied") client.tags_denied = True with pytest.raises(MemoryStoreError) as caught: - MinioStore(client, "sast-memory") + S3Store(client, "sast-memory") message = str(caught.value) assert "versioning cannot be read (PermissionError: Access Denied)" in message and "s3:GetBucketVersioning" in message assert "lifecycle configuration cannot be read" in message and "s3:GetLifecycleConfiguration" in message @@ -440,7 +439,7 @@ def test_verify_bucket_names_every_missing_piece_and_the_permissions_to_read_the def test_row_objects_carry_tags_and_a_kind_tag_skips_objects_without_that_kind() -> None: client = FakeObjects("works") - store = MinioStore(client, "bucket") + store = S3Store(client, "bucket") store.put_rows([verdict("a.py::run", "agreed"), row("unit_cost", "a:b.py", pin=OTHER_PIN, **{"pass": "a"})]) tags = client.tags(f"repos/github.com__o__r/{PIN}.jsonl") assert tags == {"repo": "github.com/o/r", "pin": PIN, "kind": "verdict", "family": "injection", "run": "r1", @@ -469,32 +468,51 @@ def test_open_store_selects_the_backend(tmp_path: Path, monkeypatch: pytest.Monk assert isinstance(default, FileStore) and default.root == tmp_path / "plane" / "memory" chosen = open_store(environ={"OUSAST_MEMORY": f"file://{tmp_path}/m"}) assert isinstance(chosen, FileStore) and chosen.root == tmp_path / "m" - with pytest.raises(MemoryStoreError, match="file:/// or minio://"): - open_store("s3://bucket") + with pytest.raises(MemoryStoreError, match="file:/// or s3://"): + open_store("gs://bucket") + with pytest.raises(MemoryStoreError, match="names no bucket and S3_BUCKET is unset"): + open_store(environ={"OUSAST_MEMORY": "s3://"}) -def test_minio_settings_name_missing_keys_never_values() -> None: - with pytest.raises(MemoryStoreError, match="MINIO_ACCESS_KEY, MINIO_SECRET_KEY") as caught: - open_store(environ={"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "minio.example:9000"}) - assert "minio.example" not in str(caught.value) - env = {"MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "sk-value", "MINIO_SECURE": "false"} - assert minio_settings(env) == {"endpoint": "h:9000", "access_key": "ak", "secret_key": "sk-value", "secure": False} - assert minio_settings({**env, "MINIO_SECURE": ""})["secure"] is True +def test_s3_settings_name_missing_keys_never_values() -> None: + with pytest.raises(MemoryStoreError, match="AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") as caught: + open_store(environ={"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://files.example:9000"}) + assert "files.example" not in str(caught.value) + env = {"S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak", "AWS_SECRET_ACCESS_KEY": "sk-value"} + assert s3_settings(env) == {"endpoint": "https://h:9000", "access_key": "ak", "secret_key": "sk-value"} + assert s3_settings({**env, "S3_REGION": "eu-1", "AWS_SESSION_TOKEN": "tok"}) == { + **s3_settings(env), "region": "eu-1", "session_token": "tok" + } # fmt: skip + with pytest.raises(MemoryStoreError, match=r"S3_ENDPOINT must be a URL with a scheme") as caught: + s3_settings({**env, "S3_ENDPOINT": "h:9000"}) + assert "h:9000" not in str(caught.value) -def test_minio_without_the_sdk_names_the_extra() -> None: +def test_s3_without_boto3_names_the_extra() -> None: try: - import minio # noqa: F401 + import boto3 # noqa: F401 except ImportError: pass else: - pytest.skip("the minio SDK is installed here") - env = {"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "secret-xyz"} - with pytest.raises(MemoryStoreError, match=r"install openultrasast\[minio\]") as caught: - open_store(environ=env) + pytest.skip("boto3 is installed here") + env = {"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak"} + with pytest.raises(MemoryStoreError, match=r"install openultrasast\[s3\]") as caught: + open_store(environ={**env, "AWS_SECRET_ACCESS_KEY": "secret-xyz"}) assert "secret-xyz" not in str(caught.value) +def test_the_boto3_client_is_built_for_path_style_sigv4_and_no_checksum_trailers() -> None: + pytest.importorskip("boto3") + client = S3Client("https://files.example", "ak", "secret-xyz", "sast-memory", region="eu-1") + meta = client.sdk.meta + assert (meta.endpoint_url, meta.region_name, meta.config.signature_version) == ("https://files.example", "eu-1", "s3v4") + assert meta.config.s3 == {"addressing_style": "path"} + assert getattr(meta.config, "request_checksum_calculation", "when_required") == "when_required" + url = client.presign("PUT", "runs/r1/x/units.jsonl", timedelta(minutes=5)) + assert url.startswith("https://files.example/sast-memory/runs/r1/x/units.jsonl?") and "X-Amz-Expires=300" in url + assert "secret-xyz" not in url + + # --- disk refusal (FileStore) -------------------------------------------------------------------------------------- diff --git a/uv.lock b/uv.lock index 02208fe..de19b60 100644 --- a/uv.lock +++ b/uv.lock @@ -24,65 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" }, ] -[[package]] -name = "argon2-cffi" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi-bindings" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, -] - -[[package]] -name = "argon2-cffi-bindings" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0b/43/bb8b6e8708d49a5ab36781333af092d9f483b198a2710d01281204640055/argon2_cffi_bindings-26.1.0.tar.gz", hash = "sha256:63505c71542a44b68b1e38060450fb006404170da375feb31af153e7f9c6205d", size = 1790807, upload-time = "2026-08-20T07:44:22.492Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d2/0ae991f1b2181e5be49007c574710a800ad36c2978683addb3e67c474e55/argon2_cffi_bindings-26.1.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:21ca0396fe5ec995dd54431c32698189666f9224810acfa752e50d2bd94d9df2", size = 25521, upload-time = "2026-08-20T07:32:43.019Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e4/ad91d8297638aa2258aad4501c306aca99480dfe76ccd638173fa3702db9/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78de2d65e0b9ea7ce9d1b1c3e87297b2d7305a02c266ee2a2d6910daddd7ee69", size = 27177, upload-time = "2026-08-20T07:32:44.158Z" }, - { url = "https://files.pythonhosted.org/packages/6f/86/5363df11b86d02cf3662208e7406496327649cc90eb365bf6f4e8a54a41f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f1821903e2ceadcb88ec2b45ef190897b7682449c772f4d9b53e42c520cf29", size = 26597, upload-time = "2026-08-20T07:32:45.172Z" }, - { url = "https://files.pythonhosted.org/packages/f4/b5/a14dcc592652347dad23ee93b278a4da5d2a25c9ed3ebd10d68eea823a4f/argon2_cffi_bindings-26.1.0-cp310-abi3-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d88e5f7e60f28ae0b0cc6b2f16c43e87cd642a196a86f85e0d8bb6fe016fc16d", size = 27403, upload-time = "2026-08-20T07:32:46.13Z" }, - { url = "https://files.pythonhosted.org/packages/b3/81/b4a20d4902af7f796390bf9245ff83c5217dfa7367efa1d14986956c482b/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:34b7d9c24a4165a2c61cc8ae11d44d48c9ce2830fb536cb7914e11fdd9962728", size = 27132, upload-time = "2026-08-20T07:32:47.13Z" }, - { url = "https://files.pythonhosted.org/packages/7e/1b/c8de358af07b1c490e0fcb863ef98e46ddb486e45567aca5a60bd68d9daa/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:224865cbbcb7a2bd1356741dff12b0134df726b6d44bb7b500df8e303cbd9e81", size = 27588, upload-time = "2026-08-20T07:32:48.087Z" }, - { url = "https://files.pythonhosted.org/packages/48/2f/7ee62a6e79f9309f9d9982d301b22a00010adb580c05c8109b94d7b33de0/argon2_cffi_bindings-26.1.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ffff613aaa9ce6236766e2fc6dc560bb5abde7a2e2416e3db1f9ae395a2b4dd4", size = 26785, upload-time = "2026-08-20T07:32:48.977Z" }, - { url = "https://files.pythonhosted.org/packages/e9/10/960d0ee93d4897741bcaf4799c697dae2d81499f66fd1ed042a7dd54c1f4/argon2_cffi_bindings-26.1.0-cp310-abi3-win32.whl", hash = "sha256:a86c069c91a747a2c4e5c51473590aeb48172fff9b2130d23729a42d98665ecb", size = 23898, upload-time = "2026-08-20T07:32:50.114Z" }, - { url = "https://files.pythonhosted.org/packages/6d/3a/0cc14a05810e6add9bce5e87693334baa2222de5f647fa31781885b6573f/argon2_cffi_bindings-26.1.0-cp310-abi3-win_amd64.whl", hash = "sha256:2c36ff87b5dfaa477d0bd51e9d7f6abdae7c8955d2983c97419085d842154b3e", size = 25730, upload-time = "2026-08-20T07:32:51.091Z" }, - { url = "https://files.pythonhosted.org/packages/4e/db/d83cf2af140547f0b9cdaece05b2dc2dcbf991be4667331d073eff771435/argon2_cffi_bindings-26.1.0-cp310-abi3-win_arm64.whl", hash = "sha256:f9c4420a7a864fe1b86ce35befc95b8e39fb852493b81cf798671ddc265de638", size = 24478, upload-time = "2026-08-20T07:32:52.111Z" }, - { url = "https://files.pythonhosted.org/packages/bb/5f/f652055e18d2627e2eed94c7f31a792127cfe38df786635395d742321674/argon2_cffi_bindings-26.1.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:af11ac37a7c53dc16cb7950a6190851b0870fe218b6c60c0bb7ac355234e3083", size = 15434, upload-time = "2026-08-20T07:32:53.143Z" }, - { url = "https://files.pythonhosted.org/packages/76/38/de696045960f5b846d428c0fb6c130ed3da87aac2af209b05c193815404c/argon2_cffi_bindings-26.1.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:db0fcd827ca61622a01b220aadfbece01939acf53888f2cb98cd93e9b1e2c97e", size = 15449, upload-time = "2026-08-20T07:32:54.075Z" }, - { url = "https://files.pythonhosted.org/packages/91/0a/c25af768f6b75a5a71e31207f87c540656b2808c015260444a22763221ad/argon2_cffi_bindings-26.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:28524438cd3e723f25412f63d4fd516ff5bae9ae5aa56acbe2a1404398a0cf31", size = 25683, upload-time = "2026-08-20T07:32:55.05Z" }, - { url = "https://files.pythonhosted.org/packages/a8/7e/be212c751ab0bcea7f646615f933bf262e8e50b3f7bef32f861d0a2d066b/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac82fc756a446b6ccd7139ce70efa9d8bbe541e7ad579a12dcb52764b7175c5f", size = 27311, upload-time = "2026-08-20T07:32:56.166Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ee/f84b28e4afd13d3cac36c1d8fa8c239d2dc2c51cd978d02ee5d5ad98d9bb/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a4e68eed961a8de6928d1c17ff3dc2a547e0e923c17f8f1cd79fb7bc9502f98", size = 26771, upload-time = "2026-08-20T07:32:57.206Z" }, - { url = "https://files.pythonhosted.org/packages/21/c3/95c07a023691ecd529da9cb6a8f0779e13ebc1bdfaa86d145fdc1c6e7e79/argon2_cffi_bindings-26.1.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:151dfaad9de753f4af2a7854e707e4784f2acc434340ade64239c5b104b2d605", size = 27568, upload-time = "2026-08-20T07:32:58.361Z" }, - { url = "https://files.pythonhosted.org/packages/e6/31/3a18e31406d8694b4d6a31573c3e572fff6bed318bb744453eb653766d22/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:061a6919145bbf282ebf1f9c59d3135d4833c25313c8595c0d68cf7712ddfce2", size = 27280, upload-time = "2026-08-20T07:32:59.343Z" }, - { url = "https://files.pythonhosted.org/packages/0b/39/d4be4577e178b2397aa5b5575c8a309bf0da2afe05fe0c72c8f398662d63/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:62ff20cd130c956c7c9144d5fe35228f98b51c579b2439e988b27ef93e16c02a", size = 27776, upload-time = "2026-08-20T07:33:00.325Z" }, - { url = "https://files.pythonhosted.org/packages/71/47/78f4dd96f7411339f723b96fe24039c1bd5835102b8a5ba71ac4ec712ac7/argon2_cffi_bindings-26.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:19423e5d7ac1cc354baab59eaabf18db2ec04ef6593b5abe5a34f323c4a8f87a", size = 26932, upload-time = "2026-08-20T07:33:01.272Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cd/96bfd37434cc0a848a9066c291d84b28846c4c9ea289ed9866b1164d622b/argon2_cffi_bindings-26.1.0-cp314-cp314t-win32.whl", hash = "sha256:4f84cdd868978d7b7350a566c254042d44216d9e37f241f3a6d3b1dfebeede35", size = 24878, upload-time = "2026-08-20T07:33:02.189Z" }, - { url = "https://files.pythonhosted.org/packages/f1/42/d8b6810abd9b1bd2f47ebbccf460da59c9f32e94888bea4f7b137d998797/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2b741888c93147444fdfc851abd81cc207f37f7f7da42062a00deb3888e57da8", size = 26656, upload-time = "2026-08-20T07:33:03.222Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d1/095d95eaf2ed1d9f77268cf3291bde148c6cd56121f8db2c74c1ba618a0e/argon2_cffi_bindings-26.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6ab674f668d5962a3a4136ae0812519b0f1586874263723a32181d60d64137e1", size = 25378, upload-time = "2026-08-20T07:33:04.332Z" }, - { url = "https://files.pythonhosted.org/packages/66/cb/214092c39c4dbcb72cf98b12234ddac2221f8fe2c0acf29c6a70fa83be53/argon2_cffi_bindings-26.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1d98e33bd8bd67d7206c124e200bf2229c4cfa8c9c19f7b44a897f0fc71837eb", size = 25683, upload-time = "2026-08-20T07:33:05.337Z" }, - { url = "https://files.pythonhosted.org/packages/83/e5/02015b83e9b05ccb85ff2ced424cf6e83a12d3810bc7f66d679a92b69ffb/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ccaf0a46cbb380f1fd102a874e32aa629fd3cb0c0e94f4943fa1f6d5edc5dac6", size = 27310, upload-time = "2026-08-20T07:33:06.344Z" }, - { url = "https://files.pythonhosted.org/packages/c3/4a/85e612787d0796878b3b4f6bd53dcd5484b6fe7b64cc6fc7b6e6a04cf835/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0c3103fcff20183e593459cfea6e012281c0e76ae3ed8b5565ad1b92eac3990", size = 26771, upload-time = "2026-08-20T07:33:07.429Z" }, - { url = "https://files.pythonhosted.org/packages/f6/84/ccb003b6f9969820e87656398f4d49c857def71a85ca1588a0e809afd7ce/argon2_cffi_bindings-26.1.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c49e853a3bef9dd10329f31f702e7fa9b5c58229ff9c2ff6d069efaf09177c08", size = 27569, upload-time = "2026-08-20T07:33:08.598Z" }, - { url = "https://files.pythonhosted.org/packages/88/07/c26b76debf0998ee08fbe947ab2058ac5de37d4b9d46b06c17abaa6c4ce9/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:6376d4b3aca039375ca8bf92f770da0ec424a1ce3a37077a8d3c557411aa56ca", size = 27279, upload-time = "2026-08-20T07:33:09.518Z" }, - { url = "https://files.pythonhosted.org/packages/ee/0d/ead6ddc029f91bc9b9390686dad3c808ab08100d348f6266b5f93f8970ee/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:9bacedc04b0402837586a17f0919e3dfdd95291f441f1f56bd80ec274c2840a1", size = 27774, upload-time = "2026-08-20T07:33:10.728Z" }, - { url = "https://files.pythonhosted.org/packages/7d/47/c108530d9eb86036b78d3af4de28b83b4a2d9a70512bd10ff8e59966aab4/argon2_cffi_bindings-26.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:76ae29acace5d33355344612844d588e19deaaba4639d8bb01601e4b1418ef36", size = 26933, upload-time = "2026-08-20T07:33:11.661Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/0bfc59e781c89acf64c31c388aade9d9d1c1ea38aa1ba1292fe07f607fe9/argon2_cffi_bindings-26.1.0-cp315-cp315t-win32.whl", hash = "sha256:df612391feca41c44d20118f3b88d1b86419465cd1f5496859f715ca60ec2210", size = 24875, upload-time = "2026-08-20T07:33:12.616Z" }, - { url = "https://files.pythonhosted.org/packages/61/c7/c3e46068cddffccecb8ad94d71135e9bf62bbc789589e7dfadc7c6f59214/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:1a0a29ed86960e44eaace7e081bdfab4f08b012fd96ec8edba71e2ad020939e4", size = 26655, upload-time = "2026-08-20T07:33:13.521Z" }, - { url = "https://files.pythonhosted.org/packages/f4/ca/18b9c8c45fecf34b9100ec6d7946057f14a158f2eaa20ea123a3e82351cb/argon2_cffi_bindings-26.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:d157ddfab1e8b21f2f1dedda9c09645d98b5ed0b667b0626be600a345d426440", size = 25376, upload-time = "2026-08-20T07:33:14.491Z" }, - { url = "https://files.pythonhosted.org/packages/a0/b9/97f0370f99611b14efd384918613dd5cbda75f28d9bb1b677aacfeaa17df/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:19b562b1de4b9052ef1214a2821c44b6e6f22945daa102c32ae4eff929d8b6d8", size = 23055, upload-time = "2026-08-20T07:33:19.716Z" }, - { url = "https://files.pythonhosted.org/packages/ae/70/7eb3fe7bf00103cbbb569c51aef150661f22b734a782673a600ff0f52309/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d525938467d52c923a890153c99087c9d5a937d1f6b585dbdba34ec82e397a", size = 24869, upload-time = "2026-08-20T07:33:20.671Z" }, - { url = "https://files.pythonhosted.org/packages/5b/4b/9d5919c6cb1f15df7406af0f99b048bd93936f112e3e8f4c8077bc2a9110/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b0bcac4d490a237e18cf91f57352920c29f77f2fa39efd0813fb81298bf17ba", size = 24216, upload-time = "2026-08-20T07:33:21.653Z" }, - { url = "https://files.pythonhosted.org/packages/a3/34/32109943bace7729233cc4ee78530baa306d8cc3c6501a64ba8cb3b58129/argon2_cffi_bindings-26.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:0cc40f7b4050bb93eb67de95d2d759322fc7ce4930b9d645581ecf4913ec651e", size = 23584, upload-time = "2026-08-20T07:33:22.613Z" }, -] - [[package]] name = "ast-serialize" version = "0.5.0" @@ -172,6 +113,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, ] +[[package]] +name = "boto3" +version = "1.43.107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, + { name = "jmespath" }, + { name = "s3transfer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/c4/c68d22d91482898f1294dab7541ad83f8bcf9df83107e6d8436d6e9dbf01/boto3-1.43.107.tar.gz", hash = "sha256:c4e0f1a0295cbb7103f2950128cf88463c076d220080a7d0f127cf834969fc3a", size = 112718, upload-time = "2026-10-01T19:31:22.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/5c/a865e8902370151daf915e3a072ee57e693144838427e42c9bbd986ecb88/boto3-1.43.107-py3-none-any.whl", hash = "sha256:632a4f8298725b94144c8cd383cbdacb34624daa73221e954c3d33ac6e2528d6", size = 140043, upload-time = "2026-10-01T19:31:20.518Z" }, +] + +[[package]] +name = "botocore" +version = "1.43.107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jmespath" }, + { name = "python-dateutil" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/22/3aed44a1e0b9820485124a9ad25e9bccd5539c547ef12827547c3cbeb25f/botocore-1.43.107.tar.gz", hash = "sha256:4a37fa072a00280c746313532d19b00e2dc53f1993222601df104d71d548d5b6", size = 16306128, upload-time = "2026-10-01T19:31:17.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/39/3a0db1ba57531062cda616dae68b69faef8114939414a2a291b3aac7617b/botocore-1.43.107-py3-none-any.whl", hash = "sha256:23cbe854e815dbaccf097f7fd32b461c9e1d2ed7e0c7dcc5658218704509d840", size = 16002666, upload-time = "2026-10-01T19:31:14.463Z" }, +] + [[package]] name = "cattrs" version = "26.1.0" @@ -194,76 +163,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, ] -[[package]] -name = "cffi" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, - { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, - { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, - { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, - { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, - { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, - { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, - { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, - { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, - { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, - { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, - { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, - { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, - { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, - { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, - { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, - { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, - { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, - { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, - { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, - { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, - { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, - { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, - { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, - { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, - { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, - { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, - { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, - { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, - { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, - { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, - { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, - { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, - { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, - { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, -] - [[package]] name = "cfgv" version = "3.5.0" @@ -563,6 +462,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "jmespath" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, +] + [[package]] name = "librt" version = "0.11.0" @@ -749,22 +657,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, ] -[[package]] -name = "minio" -version = "7.2.20" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "argon2-cffi" }, - { name = "certifi" }, - { name = "pycryptodome" }, - { name = "typing-extensions" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/40/df/6dfc6540f96a74125a11653cce717603fd5b7d0001a8e847b3e54e72d238/minio-7.2.20.tar.gz", hash = "sha256:95898b7a023fbbfde375985aa77e2cd6a0762268db79cf886f002a9ea8e68598", size = 136113, upload-time = "2025-11-27T00:37:15.569Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/9a/b697530a882588a84db616580f2ba5d1d515c815e11c30d219145afeec87/minio-7.2.20-py3-none-any.whl", hash = "sha256:eb33dd2fb80e04c3726a76b13241c6be3c4c46f8d81e1d58e757786f6501897e", size = 93751, upload-time = "2025-11-27T00:37:13.993Z" }, -] - [[package]] name = "mkdocs" version = "1.6.1" @@ -1143,8 +1035,8 @@ docs = [ { name = "mkdocs" }, { name = "mkdocs-material" }, ] -minio = [ - { name = "minio" }, +s3 = [ + { name = "boto3" }, ] semantic = [ { name = "tree-sitter" }, @@ -1167,7 +1059,7 @@ dev = [ [package.metadata] requires-dist = [ - { name = "minio", marker = "extra == 'minio'", specifier = ">=7.2,<8" }, + { name = "boto3", marker = "extra == 's3'", specifier = ">=1.35,<2" }, { name = "mkdocs", marker = "extra == 'docs'", specifier = ">=1.6,<2" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5,<10" }, { name = "pyyaml", specifier = ">=6.0" }, @@ -1179,7 +1071,7 @@ requires-dist = [ { name = "tree-sitter-python", marker = "extra == 'semantic'", specifier = ">=0.23" }, { name = "tree-sitter-typescript", marker = "extra == 'semantic'", specifier = ">=0.23" }, ] -provides-extras = ["docs", "minio", "semantic"] +provides-extras = ["docs", "s3", "semantic"] [package.metadata.requires-dev] dev = [ @@ -1377,45 +1269,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] -[[package]] -name = "pycparser" -version = "3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, -] - -[[package]] -name = "pycryptodome" -version = "3.23.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" }, - { url = "https://files.pythonhosted.org/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" }, - { url = "https://files.pythonhosted.org/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" }, - { url = "https://files.pythonhosted.org/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" }, - { url = "https://files.pythonhosted.org/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" }, - { url = "https://files.pythonhosted.org/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" }, - { url = "https://files.pythonhosted.org/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" }, - { url = "https://files.pythonhosted.org/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" }, - { url = "https://files.pythonhosted.org/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" }, - { url = "https://files.pythonhosted.org/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" }, - { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, - { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, - { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, - { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, - { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, - { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, -] - [[package]] name = "pygments" version = "2.20.0" @@ -1599,6 +1452,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/c7/c53e8dbff9c9dc4b7928773421ae294a5d28fcb8dcda1a089579d3a7e510/ruff-0.15.17-py3-none-win_arm64.whl", hash = "sha256:f3be1fbb34bcdfd146240d8fb92a709d4c2c8191348580a3c044ec60fa0b4456", size = 11355275, upload-time = "2026-06-11T17:54:43.635Z" }, ] +[[package]] +name = "s3transfer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "botocore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/43/35e4d8aa320bffe8287fe8f65f578fa2d2db0a64212f0e710dce58267854/s3transfer-0.19.2.tar.gz", hash = "sha256:ba0309fd86be3c27dbf78cdd813c13c5e1df16e5874b99d2535ebbdfb9892993", size = 165592, upload-time = "2026-07-22T19:30:44.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/e7/5c595c75e9f41a44f30e526eda465ea0b4eec93470e074e4a111b253f13a/s3transfer-0.19.2-py3-none-any.whl", hash = "sha256:d8168eccca828cbb2cd573675333f3bddd254313a9c42494b84c76b539e8ba25", size = 90216, upload-time = "2026-07-22T19:30:43.251Z" }, +] + [[package]] name = "six" version = "1.17.0"