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://<bucket>[/<prefix>]` (`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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-10-02 10:15:19 +02:00
co-authored by Claude Fable 5.1
parent 285b6a367f
commit 51d064e381
22 changed files with 410 additions and 437 deletions
+11 -10
View File
@@ -11,13 +11,14 @@ OPENROUTER_API_KEY=
OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small OPENROUTER_EMBEDDING_MODEL=openai/text-embedding-3-small
# Plane memory store. Default is a local FileStore under ~/ousast-results/plane/memory. # Plane memory store. Default is a local FileStore under ~/ousast-results/plane/memory.
# For an S3-compatible server (MinIO, RustFS): OUSAST_MEMORY=minio://<bucket> # For an S3-compatible server (RustFS is the tested one): OUSAST_MEMORY=s3://<bucket>[/<prefix>], or
# OUSAST_MEMORY=minio://sast-memory # OUSAST_MEMORY=s3:// to use S3_BUCKET. Needs the s3 extra (boto3). Example for a RustFS server:
# MINIO_ENDPOINT=host[:port] # OUSAST_MEMORY=s3://sast-memory
# MINIO_ACCESS_KEY= # S3_ENDPOINT=https://files.example.com
# MINIO_SECRET_KEY= # S3_REGION=us-east-1
# MINIO_SECURE=true # S3_BUCKET=sast-memory
# MINIO_REGION=us-east-1 # AWS_ACCESS_KEY_ID=
# MINIO_BUCKET=sast-memory # AWS_SECRET_ACCESS_KEY=
# The bucket must have versioning enabled and a lifecycle rule expiring runs/ (see ops/ax/README.md); # AWS_SESSION_TOKEN=
# the store verifies both at startup and refuses to run otherwise. # 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.
+11 -11
View File
@@ -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 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. the store path in the error.
**Storage backends (maintainer, 2026-09-30: "think of using minio features, including its json support. i have a **Storage backends (maintainer, 2026-09-30: asked to use the object-store features of the S3 server he runs,
server").** The store is written against one interface, `MemoryStore` (`put_row`, `put_facts`, `get_facts`, 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: `rows(repo=..., pin=..., kind=..., where=...)`, `index`), with two backends of identical behaviour:
- `FileStore`: the layout above, for this laptop and the unit tests. - `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: 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`, - **Object metadata and tags** on every row object: `repo`, `pin`, `kind`, `family`, `run`, `population`,
`split`. Rules list by prefix (`repos/<repo>/`) and filter by tag instead of scanning the bucket. `split`. Rules list by prefix (`repos/<repo>/`) and filter by tag instead of scanning the bucket.
- **Server-side JSON queries**: `rows(where=...)` pushes the filter down with S3 Select - **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 (`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 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 fallback once, and returns the same rows. A query never silently returns nothing because a feature is
missing. missing.
- **Versioning** on the bucket: each proposal's provenance records the object key and version id of every - **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. `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 - **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 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. `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 - **Bucket notifications** (follow-on): an event on new `repos/` rows can trigger the loop Run
(event-driven loop engineering). (event-driven loop engineering).
- **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `minio://<bucket>`); the MinIO - **Configuration**: `OUSAST_MEMORY` selects the backend (`file:///path` or `s3://<bucket>[/<prefix>]`); the S3
endpoint and credentials come from `.env` (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, endpoint and credentials come from `.env` (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`,
`MINIO_SECURE`), never from a manifest, never printed. Client: the `minio` Python SDK as an optional extra optional `S3_REGION`), never from a manifest, never printed. Client: boto3 as an optional extra
(`openultrasast[minio]`); the core install stays PyYAML-only. (`openultrasast[s3]`); the core install stays PyYAML-only.
- **Tests**: the same contract tests run against `FileStore` and against `MinioStore` pointed at a MinIO - **Tests**: the same contract tests run against `FileStore` and against `S3Store` pointed at an S3-compatible
endpoint (skipped unless `OUSAST_MEMORY_TEST_MINIO` is set), including the Select probe with its fallback 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. 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 **`remember` task** (`plane/tasks/remember.py`, no Model, budget `{usd: 0, calls: 0}`, one per case). It reads the
+2 -2
View File
@@ -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_ - _Requirements: 3.1, 5.1_
- [x] 2. Memory store and the `remember` task - [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; 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, 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 `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. `cli._plane`, generator annotations; fact reuse when repository, pin, candidate set and runner image match.
@@ -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 (`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. 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, - **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 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 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. 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 `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 (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:: 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 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. 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 A compiled program is one canonical JSON (sorted keys, floats with `repr`), a content-addressed blob
`programs/<sha256>.json`, with `programs/index.jsonl` (every version) and `programs/adopted.json` (profile -> sha; `programs/<sha256>.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`. 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. 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 - `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 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. ceiling -> `unfinished`, no artifact.
- `tests/test_learn_folds.py`: no group in both an evaluation fold and the compile split or retrieval pool, for - `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. outer, leave-one-source-out and leave-one-framework-out folds; determinism by seed.
+1 -1
View File
@@ -47,7 +47,7 @@ a model or embedding call.
redacted, delta diff); `learn/examples.py` and `ousast learn memory build` (label + feature record + 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 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. 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_ - _Requirements: 3.2, 3.6, 7.2_
- [x] 6.2 Metered embeddings and the cache - [x] 6.2 Metered embeddings and the cache
- `plane/models/openrouter-embedding.yaml` (`input_per_m: 0.02`), `MeteredEmbeddingClient` in - `plane/models/openrouter-embedding.yaml` (`input_per_m: 0.02`), `MeteredEmbeddingClient` in
+1 -1
View File
@@ -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 - **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 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 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 - **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. 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 - **Improvement.** `ousast improve --memory` proposes rule-status edits from plane memory through the unchanged
+1 -1
View File
@@ -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 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 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 the PHP frontend) next to the tool. Source the shell wrapper once and `ousast` runs in the
+5 -5
View File
@@ -96,17 +96,17 @@ flowchart LR
`OUSAST_MEMORY` selects the backend (`open_store`): `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://<bucket>[/<prefix>]` | | Selected by | `file:///path`, or nothing (default `$OUSAST_RESULTS/plane/memory`) | `s3://<bucket>[/<prefix>]`, or `s3://` for the bucket in `S3_BUCKET` |
| Needs | a local disk; refuses to write below 1 GiB free | the `minio` extra, `MINIO_*` settings from `.env` or the environment | | 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 | | 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 | | 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)) | | Bucket setup | none | done once by an admin, verified at every start ([RustFS setup](rustfs.md)) |
### Verify at startup, no fallback ### 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 prefix (once per process), `verify_bucket()` checks, using reads plus one small probe object at
`<prefix>/_probe/select.jsonl`: `<prefix>/_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 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 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 `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 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. and not on main as of this page.
+40 -31
View File
@@ -1,9 +1,11 @@
# RustFS setup for the memory store # RustFS setup for the memory store
The plane's memory store can live in an S3-compatible bucket: `OUSAST_MEMORY=minio://sast-memory` The plane's memory store can live in an S3-compatible bucket: `OUSAST_MEMORY=s3://sast-memory`
selects `MinioStore` (`src/openultrasast/plane/memory.py`), which works with RustFS and MinIO. The selects `S3Store` (`src/openultrasast/plane/memory.py`), which talks to the server through boto3.
maintainer's server is RustFS at `files.because-security.com`, bucket `sast-memory`, store at the Any S3-compatible store with bucket versioning, lifecycle rules, object tags and S3 Select works;
bucket root. What the store keeps there is described on [Memory](memory.md). 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 **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 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")] 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"} agent["Agent account: read settings, read and write objects and tags"] --> verify{"verify_bucket at startup"}
bucket --> verify 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"] 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 `.env` settings
The store reads its endpoint and credentials from `.env` in the working directory or from the 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 | | Variable | Value for this server | Notes |
| --- | --- | --- | | --- | --- | --- |
| `OUSAST_MEMORY` | `minio://sast-memory` | `minio://<bucket>[/<prefix>]`; unset means the local `FileStore` | | `OUSAST_MEMORY` | `s3://sast-memory` | `s3://<bucket>[/<prefix>]`; `s3://` alone uses `S3_BUCKET`; unset means the local `FileStore` |
| `MINIO_ENDPOINT` | `files.because-security.com` | host[:port] without a scheme (the `minio` SDK's endpoint); required | | `S3_ENDPOINT` | `https://files.because-security.com` | the full URL, scheme included (`https://host[:port]`); required |
| `MINIO_ACCESS_KEY` | the agent account's access key | required | | `AWS_ACCESS_KEY_ID` | the agent account's access key | required |
| `MINIO_SECRET_KEY` | the agent account's secret key | required | | `AWS_SECRET_ACCESS_KEY` | the agent account's secret key | required |
| `MINIO_SECURE` | `true` | TLS; `0`, `false`, `no` or `off` turn it off; default `true` | | `AWS_SESSION_TOKEN` | optional | for temporary credentials |
| `MINIO_REGION` | optional | spares a GetBucketLocation call | | `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 - **Never commit `.env`.** It is gitignored (`.gitignore`: `.env`, `.env.*`, except
`.env.example`). `.env.example` lists these names with empty values. `.env.example`). `.env.example` lists these names with empty values.
- **`.env` never overrides a variable already exported in your shell** (`config.load_dotenv`). - **`.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 A stale `export AWS_SECRET_ACCESS_KEY=...` in your profile silently wins over the file; `unset`
when a change in `.env` seems to have no effect. 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 - The store needs the `s3` extra (`uv sync --extra s3`, which installs boto3); without it opening
with `OUSAST_MEMORY=minio://... needs the minio SDK: install openultrasast[minio]`. A missing the store fails with `OUSAST_MEMORY=s3://... needs boto3: install openultrasast[s3]`. A missing
variable fails with `OUSAST_MEMORY=minio://... needs MINIO_ENDPOINT, MINIO_ACCESS_KEY, variable fails with `OUSAST_MEMORY=s3://... needs S3_ENDPOINT, AWS_ACCESS_KEY_ID,
MINIO_SECRET_KEY in .env or the environment` (naming only the missing ones). 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 ## 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 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. `_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: Every missing piece is collected into one error:
```text ```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:
- <one line per missing piece> - <one line per missing piece>
``` ```
@@ -145,16 +154,15 @@ server's error code and the first 200 characters of its message):
| Check | Refusal line | | Check | Refusal line |
| --- | --- | | --- | --- |
| versioning unreadable | `versioning cannot be read (<failure>): the agent needs s3:GetBucketVersioning on arn:aws:s3:::sast-memory, and versioning must be Enabled` | | versioning unreadable | `versioning cannot be read (<failure>): 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 (<failure>): the agent needs s3:GetLifecycleConfiguration on arn:aws:s3:::sast-memory, and a rule must expire runs/` | | lifecycle unreadable | `the lifecycle configuration cannot be read (<failure>): the agent needs s3:GetLifecycleConfiguration on arn:aws:s3:::sast-memory, and a rule must expire runs/` |
| a rule expires everything | `lifecycle rule(s) <rule ids> expire the whole store (repos/, facts/ and the index would be deleted); limit them to runs/` | | a rule expires everything | `lifecycle rule(s) <rule ids> 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 (<failure>): the store filters row objects by their kind tag, so the agent needs s3:GetObjectTagging on arn:aws:s3:::sast-memory/*` | | tags unreadable | `object tags cannot be read on _probe/select.jsonl (<failure>): 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 (<failure>). 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 (<failure>). 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 The admin commands inside the messages use `--endpoint-url "$S3_ENDPOINT"`, which is the full
host-only `MINIO_ENDPOINT` above, pass the full URL (`https://files.because-security.com`) as in URL from `.env`, so they run as written once `.env` is exported.
the steps on this page.
There is **no fallback**: the store does not start in a degraded mode, does not drop to a local 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. 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: drop matches; it raises:
```text ```text
memory minio://sast-memory: S3 Select on <key> failed (EvaluatorBindingDoesNotExist: ...) (a field of [<fields>] 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 <key> failed (EvaluatorBindingDoesNotExist: ...) (a field of [<fields>] 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 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 ## Checks against the real server
The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_MINIO=1` (bucket The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_S3=1` (bucket
`OUSAST_MEMORY_TEST_BUCKET`, else `MINIO_BUCKET`). They verify the bucket at its root and write `OUSAST_MEMORY_TEST_BUCKET`, else `S3_BUCKET`). They verify the bucket at its root, write only
only under a fresh `contract-<id>/` prefix; they never configure the bucket. More on the host under a fresh `contract-<id>/` prefix, and remove every version and delete marker under it
setup: [ax on this host](ops/ax/README.md). afterwards; they never configure the bucket. More on the host setup:
[ax on this host](ops/ax/README.md).
+17 -16
View File
@@ -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: `OUSAST_MEMORY` selects the backend:
- `file:///path` (`FileStore`), default `$OUSAST_RESULTS/plane/memory`; it refuses to write below 1 GiB free; - `file:///path` (`FileStore`), default `$OUSAST_RESULTS/plane/memory`; it refuses to write below 1 GiB free;
- `minio://<bucket>[/<prefix>]` (`MinioStore`, the `minio` extra), used on the maintainer's MinIO. Endpoint and - `s3://<bucket>[/<prefix>]` (`S3Store`, the `s3` extra: boto3 against any S3-compatible server; RustFS is the
credentials come from `.env` or the environment (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, tested one). Endpoint and credentials come from `.env` or the environment (`S3_ENDPOINT`, `AWS_ACCESS_KEY_ID`,
`MINIO_SECURE`), never from a manifest, and are never printed. `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. `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 - 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). 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://<bucket>[/<prefix>]` puts the plane's memory store (`plane/memory.py`) in an `OUSAST_MEMORY=s3://<bucket>[/<prefix>]` (or `s3://` for the bucket in `S3_BUCKET`) puts the plane's memory store
S3-compatible bucket. MinIO and RustFS both work; the maintainer's server is RustFS. Endpoint and credentials (`plane/memory.py`) in an S3-compatible bucket through boto3 (the `s3` extra). Any store with versioning,
come from `.env` (`MINIO_ENDPOINT`, `MINIO_ACCESS_KEY`, `MINIO_SECRET_KEY`, `MINIO_SECURE`, optional lifecycle rules, object tags and S3 Select works; RustFS is what the contract tests run against. Endpoint and
`MINIO_REGION`, which avoids a GetBucketLocation call) and are never printed. 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 **The store never configures its bucket.** An admin sets it up once. Each time an `S3Store` is opened it
checks the setup (`MinioStore.verify_bucket`), using reads plus one small probe object at checks the setup (`S3Store.verify_bucket`), using reads plus one small probe object at
`<prefix>/_probe/select.jsonl`. If anything is missing, it refuses to start with a `MemoryStoreError` that lists `<prefix>/_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: 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/`, 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); `index.jsonl` and the blobs are kept);
- **S3 Select** (`SelectObjectContent` over JSON Lines). Every filtered read is pushed down to the server, and - **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. - **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): 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 --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", --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled",
"Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}' "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 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. 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 The real-server contract tests (`OUSAST_MEMORY_TEST_S3=1`, bucket `OUSAST_MEMORY_TEST_BUCKET`, else
`MINIO_BUCKET`) verify the bucket at its root and write only under a fresh `contract-<id>/` prefix. They never `S3_BUCKET`) verify the bucket at its root and write only under a fresh `contract-<id>/` prefix. They never
configure the bucket. configure the bucket.
## Moving to a separate Kubernetes cluster (planned) ## 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 | | 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 | | 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 | | 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 The provider key already travels only in the start request through `atenet-router`, which works the same through
a port-forward to any cluster. a port-forward to any cluster.
+7 -6
View File
@@ -16,10 +16,10 @@ docs = [
"mkdocs>=1.6,<2", "mkdocs>=1.6,<2",
"mkdocs-material>=9.5,<10", "mkdocs-material>=9.5,<10",
] ]
# The plane's memory store on MinIO (OUSAST_MEMORY=minio://<bucket>, plane/memory.py). Imported lazily; the # The plane's memory store on an S3-compatible bucket (OUSAST_MEMORY=s3://<bucket>, plane/memory.py; RustFS is the
# core install and the file store need nothing beyond PyYAML. # tested server). Imported lazily; the core install and the file store need nothing beyond PyYAML.
minio = [ s3 = [
"minio>=7.2,<8", "boto3>=1.35,<2",
] ]
# Structured parse for overlay (JS/C/C++/Java/Python). Core install stays empty. # Structured parse for overlay (JS/C/C++/Java/Python). Core install stays empty.
# Wheels are MIT; import is lazy via openultrasast.semantic.extra. # Wheels are MIT; import is lazy via openultrasast.semantic.extra.
@@ -69,9 +69,10 @@ warn_unused_configs = true
module = "tree_sitter" module = "tree_sitter"
ignore_missing_imports = true 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]] [[tool.mypy.overrides]]
module = ["minio", "minio.*"] module = ["boto3", "botocore.*"]
ignore_missing_imports = true ignore_missing_imports = true
# PyYAML ships no inline types and types-PyYAML is not a dev dependency. # PyYAML ships no inline types and types-PyYAML is not a dev dependency.
+2 -2
View File
@@ -218,7 +218,7 @@ def _main(argv: list[str] | None) -> int:
const="", const="",
default=None, default=None,
metavar="STORE", 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", "no value: OUSAST_MEMORY or the default store); off by default",
) )
improve.add_argument( improve.add_argument(
@@ -290,7 +290,7 @@ def _main(argv: list[str] | None) -> int:
plane_normalise = plane_sub.add_parser( plane_normalise = plane_sub.add_parser(
"memory-normalise", help='rewrite the store\'s rows to the fixed schema (every queryable field, "" where absent); idempotent' "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:///<path> or minio://<bucket>[/<prefix>]") plane_normalise.add_argument("--store", help="the store URL (default OUSAST_MEMORY): file:///<path> or s3://<bucket>[/<prefix>]")
plane_normalise.add_argument("--dry-run", action="store_true", help="count what would be rewritten, write nothing") plane_normalise.add_argument("--dry-run", action="store_true", help="count what would be rewritten, write nothing")
plane_engine = plane_sub.add_parser( 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" "alerts-engine", help="a Run's `alerts` for PHP and languages quick mode does not cover, from the engine image on this host"
+1 -1
View File
@@ -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>``, ``proposal_id`` being the sha256 of (rule, **Provenance**: each proposal's rationale is ``memory:<proposal_id>``, ``proposal_id`` being the sha256 of (rule,
edit key, sorted evidence row ids, thresholds). The sidecar ``memory_proposals.jsonl`` next to the journal carries 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. the counts that met each threshold, what the guard excluded and the store's index digest.
""" """
+1 -1
View File
@@ -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.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.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.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.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.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", "plane.engine_alerts": "harnessx-removal Req 6.3: engine alerts where quick mode has no rules (PHP), on the host",
+162 -84
View File
@@ -12,11 +12,13 @@ engine's own. Layout, identical for both backends::
by excerpt sha, responses/*.json by request sha, programs/*.json by excerpt sha, responses/*.json by request sha, programs/*.json
``OUSAST_MEMORY`` selects the backend: ``file:///path`` (:class:`FileStore`, default ``results_root()/memory``) or ``OUSAST_MEMORY`` selects the backend: ``file:///path`` (:class:`FileStore`, default ``results_root()/memory``) or
``minio://<bucket>[/<prefix>]`` (:class:`MinioStore`, the ``minio`` extra; MinIO or RustFS). The endpoint and ``s3://<bucket>[/<prefix>]`` (:class:`S3Store`, the ``s3`` extra: boto3 against any S3-compatible server with
credentials come from ``.env`` (``MINIO_ENDPOINT``, ``MINIO_ACCESS_KEY``, ``MINIO_SECRET_KEY``, ``MINIO_SECURE``, versioning, lifecycle rules, object tags and S3 Select; RustFS is the server this is tested on). ``s3://`` without a
``MINIO_REGION``), never from a manifest, and are never printed. The store never configures its bucket: an admin bucket names ``S3_BUCKET``. The endpoint and credentials come from ``.env`` (``S3_ENDPOINT``, a full URL;
enables versioning and the ``runs/`` expiry rule once, and opening a ``MinioStore`` verifies them and S3 Select ``AWS_ACCESS_KEY_ID``, ``AWS_SECRET_ACCESS_KEY``; optional ``AWS_SESSION_TOKEN``, ``S3_REGION``), never from a
(:meth:`MinioStore.verify_bucket`), refusing to start otherwise. 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``): 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 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))) 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") TAG_FIELDS = ("repo", "pin", "kind", "family", "run", "population", "split")
MIN_FREE_BYTES = 1 << 30 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}") _PIN = re.compile(r"[0-9a-f]{40}")
_FIELD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") _FIELD = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
_TAG_VALUE = re.compile(r"[^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() return self._path(key).as_uri()
# --- the MinIO backend --------------------------------------------------------------------------------------------- # --- the S3 backend ------------------------------------------------------------------------------------------------
@dataclass(frozen=True) @dataclass(frozen=True)
class ExpiryRule: 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 (``""`` 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.""" 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): class ObjectClient(Protocol):
"""The object operations :class:`MinioStore` needs; :class:`SdkClient` adapts the ``minio`` SDK, tests fake it. """The object operations :class:`S3Store` needs; :class:`S3Client` adapts boto3, tests fake it. The
The bucket-configuration calls only read: the store never configures its bucket.""" bucket-configuration calls only read: the store never configures its bucket."""
def get(self, key: str) -> tuple[bytes, str | None] | None: ... def get(self, key: str) -> tuple[bytes, str | None] | None: ...
def put(self, key: str, data: bytes, labels: Mapping[str, str]) -> 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 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: 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 """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/``, 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 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()) _VERIFIED.add(self.describe())
def describe(self) -> str: 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: def _k(self, key: str) -> str:
return f"{self.prefix}/{key}" if self.prefix else key 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 over JSON Lines, and the probe's object tags readable. Every missing piece is named with its admin action
in one :class:`MemoryStoreError`.""" in one :class:`MemoryStoreError`."""
runs = self._k(RUNS_PREFIX) runs = self._k(RUNS_PREFIX)
endpoint = '--endpoint-url "$MINIO_ENDPOINT"' endpoint = '--endpoint-url "$S3_ENDPOINT"'
missing: list[str] = [] missing: list[str] = []
try: try:
status = self.client.versioning() status = self.client.versioning()
@@ -696,7 +708,7 @@ class MinioStore(MemoryStore):
missing.append( missing.append(
f"S3 Select (SelectObjectContent over JSON Lines) does not answer on {self._k(PROBE_KEY)} " 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 " 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: if missing:
raise MemoryStoreError( raise MemoryStoreError(
@@ -714,7 +726,7 @@ class MinioStore(MemoryStore):
return _parse_lines(self.client.select(self._k(key), where_sql(where)), key), version return _parse_lines(self.client.select(self._k(key), where_sql(where)), key), version
except Exception as exc: except Exception as exc:
hint = "" 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 # 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). # 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)" 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) return self.client.presign("GET", self._k(key), expires)
class SdkClient: _MISSING_OBJECT = ("NoSuchKey", "NoSuchObject", "NotFound", "404")
""":class:`ObjectClient` over ``minio.Minio``; imported lazily so the core install needs no SDK."""
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: try:
from minio import Minio import boto3
from botocore.config import Config
except ImportError as exc: except ImportError as exc:
raise MemoryStoreError("OUSAST_MEMORY=minio://... needs the minio SDK: install openultrasast[minio]") from exc raise MemoryStoreError("OUSAST_MEMORY=s3://... needs boto3: install openultrasast[s3]") from exc
self.sdk = Minio(endpoint, access_key=access_key, secret_key=secret_key, secure=secure, region=region) 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 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: def get(self, key: str) -> tuple[bytes, str | None] | None:
from minio.error import S3Error from botocore.exceptions import ClientError
try: try:
response = self.sdk.get_object(self.bucket, key) response = self.sdk.get_object(Bucket=self.bucket, Key=key)
except S3Error as exc: except ClientError as exc:
if exc.code in ("NoSuchKey", "NoSuchObject"): if self._missing(exc):
return None return None
raise raise
body = response["Body"]
try: try:
return response.read(), response.headers.get("x-amz-version-id") data: bytes = body.read()
finally: finally:
response.close() body.close()
response.release_conn() 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: 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 extra: dict[str, Any] = {}
if labels:
tags = Tags(for_object=True) extra["Metadata"] = dict(labels)
for name, value in labels.items(): extra["Tagging"] = urlencode(dict(labels), quote_via=quote)
tags[name] = value self.sdk.put_object(Bucket=self.bucket, Key=key, Body=data, ContentType="application/x-ndjson", **extra)
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
def delete(self, key: str) -> None: 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]: 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]: 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: def version(self, key: str) -> str | None:
from minio.error import S3Error from botocore.exceptions import ClientError
try: try:
version = self.sdk.stat_object(self.bucket, key).version_id version = self.sdk.head_object(Bucket=self.bucket, Key=key).get("VersionId")
return None if version is None else str(version) except ClientError as exc:
except S3Error as exc: if self._missing(exc):
if exc.code in ("NoSuchKey", "NoSuchObject"):
return None return None
raise raise
return None if version in (None, "null") else str(version)
def select(self, key: str, expression: str) -> bytes: def select(self, key: str, expression: str) -> bytes:
from minio.select import JSONInputSerialization, JSONOutputSerialization, SelectRequest response = self.sdk.select_object_content(
Bucket=self.bucket, Key=key, ExpressionType="SQL", Expression=expression,
request = SelectRequest(expression, JSONInputSerialization(json_type="LINES"), JSONOutputSerialization(), request_progress=False) InputSerialization={"JSON": {"Type": "LINES"}}, OutputSerialization={"JSON": {"RecordDelimiter": "\n"}},
with self.sdk.select_object_content(self.bucket, key, request) as result: ) # fmt: skip
return b"".join(result.stream()) 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: def presign(self, method: str, key: str, expires: timedelta) -> str:
if method == "PUT": operation = "put_object" if method == "PUT" else "get_object"
return str(self.sdk.presigned_put_object(self.bucket, key, expires=expires)) return str(
return str(self.sdk.presigned_get_object(self.bucket, key, expires=expires)) self.sdk.generate_presigned_url(
operation, Params={"Bucket": self.bucket, "Key": key}, ExpiresIn=int(expires.total_seconds()), HttpMethod=method
)
)
def versioning(self) -> str: 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]: 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] = [] rules: list[ExpiryRule] = []
for rule in config.rules if config else []: for rule in config.get("Rules") or []:
found = rule.rule_filter found = rule.get("Filter") or {}
joined = found.and_operator if found else None joined = found.get("And") or {}
prefix = (joined.prefix if joined else found.prefix if found else None) or "" prefix = joined.get("Prefix") or found.get("Prefix") or rule.get("Prefix") or ""
tagged = bool(found and (found.tag or (joined and joined.tags))) tagged = bool(found.get("Tag") or joined.get("Tags"))
expiration = rule.expiration expiration = rule.get("Expiration") or {}
expires = bool(expiration and (expiration.days or expiration.date)) expires = bool(expiration.get("Days") or expiration.get("Date"))
days = expiration.days if expiration else None days = expiration.get("Days")
rules.append(ExpiryRule(str(rule.rule_id or ""), prefix, days, rule.status == "Enabled", tagged, expires)) rules.append(ExpiryRule(str(rule.get("ID") or ""), prefix, days, rule.get("Status") == "Enabled", tagged, expires))
return rules return rules
def minio_settings(environ: Mapping[str, str] | None = None) -> dict[str, Any]: def s3_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.""" """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: if environ is None:
from ..config import load_dotenv from ..config import load_dotenv
load_dotenv() load_dotenv()
environ = os.environ 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: if missing:
raise MemoryStoreError(f"OUSAST_MEMORY=minio://... needs {', '.join(missing)} in .env or the environment") raise MemoryStoreError(f"OUSAST_MEMORY=s3://... needs {', '.join(missing)} in .env or the environment")
secure = (environ.get("MINIO_SECURE") or "true").strip().lower() not in ("0", "false", "no", "off") 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] = { settings: dict[str, Any] = {
"endpoint": environ["MINIO_ENDPOINT"], "access_key": environ["MINIO_ACCESS_KEY"], "endpoint": endpoint, "access_key": environ["AWS_ACCESS_KEY_ID"], "secret_key": environ["AWS_SECRET_ACCESS_KEY"],
"secret_key": environ["MINIO_SECRET_KEY"], "secure": secure,
} # fmt: skip } # fmt: skip
if environ.get("MINIO_REGION"): if environ.get("S3_REGION"):
settings["region"] = environ["MINIO_REGION"] # spares a GetBucketLocation the agent may not be allowed 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 return settings
def open_store(spec: str | None = None, environ: Mapping[str, str] | None = None) -> MemoryStore: def open_store(spec: str | None = None, environ: Mapping[str, str] | None = None) -> MemoryStore:
"""The store ``spec`` (else ``OUSAST_MEMORY``, else ``file://<results_root()>/memory``) names.""" """The store ``spec`` (else ``OUSAST_MEMORY``, else ``file://<results_root()>/memory``) names. ``s3://`` with no
bucket uses ``S3_BUCKET``."""
from .reconciler import results_root 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: if not text:
return FileStore(results_root() / "memory") return FileStore(results_root() / "memory")
parts = urlsplit(text) parts = urlsplit(text)
if parts.scheme == "file" and parts.path: if parts.scheme == "file" and parts.path:
return FileStore(Path(parts.path)) return FileStore(Path(parts.path))
if parts.scheme == "minio" and parts.netloc: if parts.scheme == "s3":
settings = minio_settings(environ) bucket = parts.netloc or env.get("S3_BUCKET") or ""
return MinioStore(SdkClient(bucket=parts.netloc, **settings), parts.netloc, parts.path) if not bucket:
raise MemoryStoreError(f"OUSAST_MEMORY must be file:///<path> or minio://<bucket>[/<prefix>], got {text!r}") 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:///<path> or s3://<bucket>[/<prefix>], got {text!r}")
# --- ingest and seed ----------------------------------------------------------------------------------------------- # --- ingest and seed -----------------------------------------------------------------------------------------------
@@ -950,11 +1027,11 @@ __all__ = [
"IngestResult", "IngestResult",
"MemoryStore", "MemoryStore",
"MemoryStoreError", "MemoryStoreError",
"MinioStore",
"ObjectClient", "ObjectClient",
"S3Client",
"S3Store",
"ExpiryRule", "ExpiryRule",
"Record", "Record",
"SdkClient",
"check_query", "check_query",
"counts", "counts",
"image_digest", "image_digest",
@@ -965,6 +1042,7 @@ __all__ = [
"parse_memory_key", "parse_memory_key",
"repo_key", "repo_key",
"row_id", "row_id",
"s3_settings",
"seed", "seed",
"validate_row", "validate_row",
"where_sql", "where_sql",
+1 -1
View File
@@ -2,7 +2,7 @@
``command: ["loop", <step>]``; every step is model-free with budget ``{usd: 0, calls: 0}`` (``budget.py`` refuses any ``command: ["loop", <step>]``; 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 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). 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 - ``snapshot``: never runs in the sandbox, which cannot read the host's store. ``ousast plane run`` seeds it before
+2 -2
View File
@@ -119,8 +119,8 @@ def test_rustfs_page_lists_the_complete_policy_and_no_secret() -> None:
for needle in ( for needle in (
'"Sid": "BucketSettingsReadOnly"', '"Sid": "ObjectsReadWrite"', '"arn:aws:s3:::sast-memory"', '"arn:aws:s3:::sast-memory/*"', '"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", "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", "--bucket sast-memory", '"Prefix":"runs/"', '"Days":30', "OUSAST_MEMORY", "s3://sast-memory", "S3_ENDPOINT",
"MINIO_ACCESS_KEY", "MINIO_SECRET_KEY", "MINIO_SECURE", "MINIO_REGION", "EvaluatorBindingDoesNotExist", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "S3_REGION", "S3_BUCKET", "EvaluatorBindingDoesNotExist",
): # fmt: skip ): # fmt: skip
assert needle in text, f"rustfs.md lacks {needle!r}" assert needle in text, f"rustfs.md lacks {needle!r}"
for shape in SECRET_SHAPES: for shape in SECRET_SHAPES:
+6 -6
View File
@@ -11,7 +11,7 @@ from pathlib import Path
import pytest import pytest
from learn_fixtures import ScriptedChat, by_id, corpus, default_answer, example 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 import compile as compile_module
from openultrasast.learn.compile import ( 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.folds import Fold, FoldError
from openultrasast.learn.program import Caller, Demo from openultrasast.learn.program import Caller, Demo
from openultrasast.plane.budget import MeteredClient 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} PRICES = {"cache_hit_per_m": 0.014, "input_per_m": 0.44, "output_per_m": 1.32}
MEMORY = corpus(groups=24, per_group=4) 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) 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]: def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
if request.param == "file": if request.param == "file":
yield FileStore(tmp_path / "memory") yield FileStore(tmp_path / "memory")
elif request.param == "minio": elif request.param == "s3":
yield from _real_minio() yield from _real_s3()
else: else:
yield MinioStore(FakeObjects("works"), "bucket", "p") yield S3Store(FakeObjects("works"), "bucket", "p")
def _caller(chat: ScriptedChat, store: MemoryStore | None = None, **budget: object) -> Caller: def _caller(chat: ScriptedChat, store: MemoryStore | None = None, **budget: object) -> Caller:
+3 -3
View File
@@ -23,7 +23,7 @@ from openultrasast.learn.embeddings import (
from openultrasast.learn.excerpt import excerpt_sha from openultrasast.learn.excerpt import excerpt_sha
from openultrasast.plane.budget import BudgetExhausted, MeteredEmbeddingClient, UnmeteredUsage from openultrasast.plane.budget import BudgetExhausted, MeteredEmbeddingClient, UnmeteredUsage
from openultrasast.plane.manifests import load_manifests 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 from openultrasast.provider.openrouter import parse_embedding_payload
PRICES = {"cache_hit_per_m": 0, "input_per_m": 0.02, "output_per_m": 0} 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] 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]: 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]: def _excerpts(store: MemoryStore, n: int) -> list[str]:
+3 -3
View File
@@ -24,7 +24,7 @@ from openultrasast.learn.examples import (
) )
from openultrasast.learn.features import Part, quick_part, record from openultrasast.learn.features import Part, quick_part, record
from openultrasast.learn.labels import Source, Sources 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" PIN = "0123456789abcdef0123456789abcdef01234567"
SOURCES = Sources( 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]: 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]: def label(**kw: Any) -> dict[str, Any]:
+76 -58
View File
@@ -20,17 +20,17 @@ import pytest
from openultrasast.plane import memory, reconciler from openultrasast.plane import memory, reconciler
from openultrasast.plane.memory import ( from openultrasast.plane.memory import (
MINIO_KEYS, S3_KEYS,
ExpiryRule, ExpiryRule,
FileStore, FileStore,
MemoryStore, MemoryStore,
MemoryStoreError, MemoryStoreError,
MinioStore, S3Client,
SdkClient, S3Store,
memory_key, memory_key,
minio_settings,
open_store, open_store,
row_id, row_id,
s3_settings,
seed, seed,
where_sql, where_sql,
) )
@@ -40,10 +40,10 @@ PIN = "0123456789abcdef0123456789abcdef01234567"
OTHER_PIN = "fedcba9876543210fedcba9876543210fedcba98" OTHER_PIN = "fedcba9876543210fedcba9876543210fedcba98"
IMAGE = "sha256:" + "a" * 64 IMAGE = "sha256:" + "a" * 64
FACTS = json.dumps({"callers": {"a.py::run": []}, "counts": {"files": 1}, "files": {"a.py": {"functions": ["run"]}}}).encode() FACTS = json.dumps({"callers": {"a.py::run": []}, "counts": {"files": 1}, "files": {"a.py": {"functions": ["run"]}}}).encode()
MINIO_SKIP = ( S3_SKIP = (
"MinIO/RustFS contract tests need OUSAST_MEMORY_TEST_MINIO=1 plus MINIO_ENDPOINT, MINIO_ACCESS_KEY, MINIO_SECRET_KEY " "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 MINIO_BUCKET) exported (optional: MINIO_SECURE, MINIO_REGION) and the " "and a bucket (OUSAST_MEMORY_TEST_BUCKET, else S3_BUCKET) exported (optional: S3_REGION, AWS_SESSION_TOKEN) and the "
"minio extra installed; the admin configures that bucket, the tests never do" "s3 extra (boto3) installed; the admin configures that bucket, the tests never do"
) )
@@ -112,13 +112,13 @@ class FakeObjects:
leading = rows[: self.schema_rows] 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)} 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 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) 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} 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())) 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: 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: def versioning(self) -> str:
if self.status == "denied": if self.status == "denied":
@@ -136,33 +136,32 @@ def fresh_verification(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(memory, "_VERIFIED", set()) monkeypatch.setattr(memory, "_VERIFIED", set())
def _real_minio() -> Iterator[MemoryStore]: def _real_s3() -> Iterator[MemoryStore]:
if os.environ.get("OUSAST_MEMORY_TEST_MINIO") != "1" or any(not os.environ.get(k) for k in MINIO_KEYS): if os.environ.get("OUSAST_MEMORY_TEST_S3") != "1" or any(not os.environ.get(k) for k in S3_KEYS):
pytest.skip(MINIO_SKIP) pytest.skip(S3_SKIP)
pytest.importorskip("minio", reason=MINIO_SKIP) pytest.importorskip("boto3", reason=S3_SKIP)
bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("MINIO_BUCKET") bucket = os.environ.get("OUSAST_MEMORY_TEST_BUCKET") or os.environ.get("S3_BUCKET")
if not bucket: if not bucket:
pytest.skip(MINIO_SKIP) pytest.skip(S3_SKIP)
client = SdkClient(bucket=bucket, **minio_settings(os.environ)) client = S3Client(bucket=bucket, **s3_settings(os.environ))
MinioStore(client, bucket) # verifies the admin's setup at the bucket root; the tests never configure the bucket 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 # 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. # 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]}" prefix = f"contract-{uuid.uuid4().hex[:12]}"
memory._VERIFIED.add(f"minio://{bucket}/{prefix}") memory._VERIFIED.add(f"s3://{bucket}/{prefix}")
store = MinioStore(client, bucket, prefix) store = S3Store(client, bucket, prefix)
yield store yield store
for key in client.keys(store.prefix + "/"): client.purge(store.prefix + "/") # every version and delete marker: the bucket keeps nothing of the test
client.delete(key)
@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]: def store(request: pytest.FixtureRequest, tmp_path: Path) -> Iterator[MemoryStore]:
if request.param == "file": if request.param == "file":
yield FileStore(tmp_path / "memory") yield FileStore(tmp_path / "memory")
elif request.param == "minio": elif request.param == "s3":
yield from _real_minio() yield from _real_s3()
else: else:
yield MinioStore(FakeObjects(), "bucket", "p") yield S3Store(FakeObjects(), "bucket", "p")
# --- the contract -------------------------------------------------------------------------------------------------- # --- 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: def test_the_fake_client_mimics_rustfs_and_the_normaliser_repairs_a_pre_rule_object() -> None:
client = FakeObjects("works") client = FakeObjects("works")
store = MinioStore(client, "bucket") store = S3Store(client, "bucket")
alerts, late = _late() alerts, late = _late()
key = f"repos/github.com__o__r/{PIN}.jsonl" 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 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") 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: 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})] 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()) 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)) 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] 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: def _filled(client: FakeObjects) -> S3Store:
store = MinioStore(client, "bucket") 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"})]) store.put_rows([verdict("a.py::run", "agreed"), verdict("a.py::x", "disputed"), row("unit_cost", "a:a.py", **{"pass": "a"})])
return store 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"]) @pytest.mark.parametrize("mode", ["raises", "wrong"])
def test_a_server_without_select_is_refused_at_open(mode: str) -> None: 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: 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) 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: def test_verify_bucket_passes_on_the_admin_setup_and_writes_only_its_probe() -> None:
client = FakeObjects() 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") assert sorted(client.objects) == ["p/_probe/select.jsonl"] and not hasattr(client, "enable_versioning")
store.verify_bucket() 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" assert len(client.selects) == 2, "opening a verified store again does not re-verify in the same process"
@pytest.mark.parametrize("status", ["Suspended", "Off"]) @pytest.mark.parametrize("status", ["Suspended", "Off"])
def test_verify_bucket_refuses_versioning_off_naming_the_admin_command(status: str) -> None: 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: 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) message = str(caught.value)
assert "never configures its bucket" in message and "lifecycle" not in message and "Select" not in message 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( @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: 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: 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 "put-bucket-lifecycle-configuration" in str(caught.value)
assert '"Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}' 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: 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/"): 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)] 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"): with pytest.raises(MemoryStoreError, match=r"lifecycle rule\(s\) everything expire the whole store"):
MinioStore(FakeObjects(rules=wide), "bucket") S3Store(FakeObjects(rules=wide), "bucket")
MinioStore(FakeObjects(rules=[ExpiryRule("r", "mem/runs/", 14, True)]), "bucket", "mem") 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: def test_verify_bucket_names_every_missing_piece_and_the_permissions_to_read_them() -> None:
client = FakeObjects("raises", versioning="denied", rules="denied") client = FakeObjects("raises", versioning="denied", rules="denied")
client.tags_denied = True client.tags_denied = True
with pytest.raises(MemoryStoreError) as caught: with pytest.raises(MemoryStoreError) as caught:
MinioStore(client, "sast-memory") S3Store(client, "sast-memory")
message = str(caught.value) message = str(caught.value)
assert "versioning cannot be read (PermissionError: Access Denied)" in message and "s3:GetBucketVersioning" in message 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 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: def test_row_objects_carry_tags_and_a_kind_tag_skips_objects_without_that_kind() -> None:
client = FakeObjects("works") 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"})]) 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") 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", 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" assert isinstance(default, FileStore) and default.root == tmp_path / "plane" / "memory"
chosen = open_store(environ={"OUSAST_MEMORY": f"file://{tmp_path}/m"}) chosen = open_store(environ={"OUSAST_MEMORY": f"file://{tmp_path}/m"})
assert isinstance(chosen, FileStore) and chosen.root == tmp_path / "m" assert isinstance(chosen, FileStore) and chosen.root == tmp_path / "m"
with pytest.raises(MemoryStoreError, match="file:///<path> or minio://<bucket>"): with pytest.raises(MemoryStoreError, match="file:///<path> or s3://<bucket>"):
open_store("s3://bucket") 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: def test_s3_settings_name_missing_keys_never_values() -> None:
with pytest.raises(MemoryStoreError, match="MINIO_ACCESS_KEY, MINIO_SECRET_KEY") as caught: with pytest.raises(MemoryStoreError, match="AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") as caught:
open_store(environ={"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "minio.example:9000"}) open_store(environ={"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://files.example:9000"})
assert "minio.example" not in str(caught.value) assert "files.example" not in str(caught.value)
env = {"MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "sk-value", "MINIO_SECURE": "false"} env = {"S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak", "AWS_SECRET_ACCESS_KEY": "sk-value"}
assert minio_settings(env) == {"endpoint": "h:9000", "access_key": "ak", "secret_key": "sk-value", "secure": False} assert s3_settings(env) == {"endpoint": "https://h:9000", "access_key": "ak", "secret_key": "sk-value"}
assert minio_settings({**env, "MINIO_SECURE": ""})["secure"] is True 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: try:
import minio # noqa: F401 import boto3 # noqa: F401
except ImportError: except ImportError:
pass pass
else: else:
pytest.skip("the minio SDK is installed here") pytest.skip("boto3 is installed here")
env = {"OUSAST_MEMORY": "minio://bucket", "MINIO_ENDPOINT": "h:9000", "MINIO_ACCESS_KEY": "ak", "MINIO_SECRET_KEY": "secret-xyz"} env = {"OUSAST_MEMORY": "s3://bucket", "S3_ENDPOINT": "https://h:9000", "AWS_ACCESS_KEY_ID": "ak"}
with pytest.raises(MemoryStoreError, match=r"install openultrasast\[minio\]") as caught: with pytest.raises(MemoryStoreError, match=r"install openultrasast\[s3\]") as caught:
open_store(environ=env) open_store(environ={**env, "AWS_SECRET_ACCESS_KEY": "secret-xyz"})
assert "secret-xyz" not in str(caught.value) 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) -------------------------------------------------------------------------------------- # --- disk refusal (FileStore) --------------------------------------------------------------------------------------
Generated
+53 -188
View File
@@ -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" }, { 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]] [[package]]
name = "ast-serialize" name = "ast-serialize"
version = "0.5.0" 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" }, { 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]] [[package]]
name = "cattrs" name = "cattrs"
version = "26.1.0" 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" }, { 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]] [[package]]
name = "cfgv" name = "cfgv"
version = "3.5.0" 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" }, { 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]] [[package]]
name = "librt" name = "librt"
version = "0.11.0" 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" }, { 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]] [[package]]
name = "mkdocs" name = "mkdocs"
version = "1.6.1" version = "1.6.1"
@@ -1143,8 +1035,8 @@ docs = [
{ name = "mkdocs" }, { name = "mkdocs" },
{ name = "mkdocs-material" }, { name = "mkdocs-material" },
] ]
minio = [ s3 = [
{ name = "minio" }, { name = "boto3" },
] ]
semantic = [ semantic = [
{ name = "tree-sitter" }, { name = "tree-sitter" },
@@ -1167,7 +1059,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ 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", marker = "extra == 'docs'", specifier = ">=1.6,<2" },
{ name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5,<10" }, { name = "mkdocs-material", marker = "extra == 'docs'", specifier = ">=9.5,<10" },
{ name = "pyyaml", specifier = ">=6.0" }, { name = "pyyaml", specifier = ">=6.0" },
@@ -1179,7 +1071,7 @@ requires-dist = [
{ name = "tree-sitter-python", marker = "extra == 'semantic'", specifier = ">=0.23" }, { name = "tree-sitter-python", marker = "extra == 'semantic'", specifier = ">=0.23" },
{ name = "tree-sitter-typescript", 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] [package.metadata.requires-dev]
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" }, { 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]] [[package]]
name = "pygments" name = "pygments"
version = "2.20.0" 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" }, { 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]] [[package]]
name = "six" name = "six"
version = "1.17.0" version = "1.17.0"