From 10a8bb16e320da98bb02e9a63879d50435cf5475 Mon Sep 17 00:00:00 2001 From: Charles Shorb Date: Mon, 6 Apr 2026 15:20:55 -0400 Subject: [PATCH 1/4] Modular VectorDB Benchmark --- .gitignore | 1 + vdb_benchmark/vdbbench/benchmark/.env.example | 37 + vdb_benchmark/vdbbench/benchmark/README.md | 557 +++++++++++ vdb_benchmark/vdbbench/benchmark/__init__.py | 46 + vdb_benchmark/vdbbench/benchmark/__main__.py | 7 + .../vdbbench/benchmark/backends/README.md | 567 +++++++++++ .../vdbbench/benchmark/backends/__init__.py | 183 ++++ .../vdbbench/benchmark/backends/_env.py | 151 +++ .../vdbbench/benchmark/backends/_help.py | 141 +++ .../vdbbench/benchmark/backends/base.py | 487 ++++++++++ .../backends/elasticsearch/README.md | 210 +++++ .../backends/elasticsearch/__init__.py | 103 ++ .../backends/elasticsearch/backend.py | 343 +++++++ .../benchmark/backends/milvus/README.md | 186 ++++ .../benchmark/backends/milvus/__init__.py | 144 +++ .../benchmark/backends/milvus/backend.py | 314 +++++++ .../benchmark/backends/pgvector/README.md | 182 ++++ .../benchmark/backends/pgvector/__init__.py | 124 +++ .../benchmark/backends/pgvector/backend.py | 439 +++++++++ .../vdbbench/benchmark/collection_admin.py | 884 ++++++++++++++++++ .../benchmark/configs/1m_diskann.yaml | 45 + .../vdbbench/benchmark/configs/1m_hnsw.yaml | 45 + .../configs/elasticsearch_1m_hnsw.yaml | 46 + .../benchmark/configs/pgvector_1m_hnsw.yaml | 48 + vdb_benchmark/vdbbench/benchmark/generator.py | 169 ++++ .../vdbbench/benchmark/ground_truth.py | 241 +++++ .../vdbbench/benchmark/orchestrator.py | 566 +++++++++++ .../vdbbench/benchmark/run_benchmark.py | 581 ++++++++++++ .../vdbbench/benchmark/search_runner.py | 463 +++++++++ 29 files changed, 7310 insertions(+) create mode 100644 vdb_benchmark/vdbbench/benchmark/.env.example create mode 100644 vdb_benchmark/vdbbench/benchmark/README.md create mode 100644 vdb_benchmark/vdbbench/benchmark/__init__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/__main__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/README.md create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/__init__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/_env.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/_help.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/base.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/README.md create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/__init__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/backend.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/milvus/README.md create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/milvus/__init__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/milvus/backend.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/pgvector/README.md create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/pgvector/__init__.py create mode 100644 vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py create mode 100755 vdb_benchmark/vdbbench/benchmark/collection_admin.py create mode 100644 vdb_benchmark/vdbbench/benchmark/configs/1m_diskann.yaml create mode 100644 vdb_benchmark/vdbbench/benchmark/configs/1m_hnsw.yaml create mode 100644 vdb_benchmark/vdbbench/benchmark/configs/elasticsearch_1m_hnsw.yaml create mode 100644 vdb_benchmark/vdbbench/benchmark/configs/pgvector_1m_hnsw.yaml create mode 100644 vdb_benchmark/vdbbench/benchmark/generator.py create mode 100644 vdb_benchmark/vdbbench/benchmark/ground_truth.py create mode 100644 vdb_benchmark/vdbbench/benchmark/orchestrator.py create mode 100755 vdb_benchmark/vdbbench/benchmark/run_benchmark.py create mode 100644 vdb_benchmark/vdbbench/benchmark/search_runner.py diff --git a/.gitignore b/.gitignore index 41c7ff58..c3d0228a 100755 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,7 @@ env-* .coverage htmlcov/ *.html +vdb_benchmark/vdbbench/benchmark/results/* # OS files .DS_Store diff --git a/vdb_benchmark/vdbbench/benchmark/.env.example b/vdb_benchmark/vdbbench/benchmark/.env.example new file mode 100644 index 00000000..2e4d658f --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/.env.example @@ -0,0 +1,37 @@ +# VDB Benchmark -- backend connection parameters +# ================================================ +# +# Copy this file to .env and uncomment / edit the values you need. +# The benchmark CLI loads this file automatically (requires python-dotenv). +# +# Naming convention: +# {BACKEND}__{PARAM} +# +# Both parts are UPPER-CASED and separated by a double underscore (__). +# The PARAM name matches the backend's connection_params (see --help). +# +# Precedence (highest wins): +# CLI flags > environment / .env > YAML config > built-in defaults +# +# To verify which source each parameter comes from, run: +# python -m vdbbench.benchmark --backend milvus --config ... --what-if + + +# ── Milvus ──────────────────────────────────────────────────────── +# MILVUS__HOST=127.0.0.1 +# MILVUS__PORT=19530 +# MILVUS__MAX_MESSAGE_LENGTH=514983574 + + +# ── pgvector (PostgreSQL) ───────────────────────────────────────── +# PGVECTOR__HOST=127.0.0.1 +# PGVECTOR__PORT=5432 +# PGVECTOR__DBNAME=postgres +# PGVECTOR__USER=postgres +# PGVECTOR__PASSWORD= + + +# ── Elasticsearch ───────────────────────────────────────────────── +# ELASTICSEARCH__HOST=http://localhost:9200 +# ELASTICSEARCH__API_KEY= +# ELASTICSEARCH__CLOUD_ID= diff --git a/vdb_benchmark/vdbbench/benchmark/README.md b/vdb_benchmark/vdbbench/benchmark/README.md new file mode 100644 index 00000000..e355feb9 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/README.md @@ -0,0 +1,557 @@ +# VDB Benchmark Framework + +A modular, backend-agnostic benchmarking framework for vector databases. It +generates synthetic vectors, ingests them into a pluggable database backend, +computes brute-force ground truth, and runs ANN search benchmarks that report +QPS, recall, and latency percentiles. + +## Supported Backends + +| Backend | `--backend` | Supported Indexes | Supported Metrics | Required Packages | +|---------|-------------|-------------------|-------------------|-------------------| +| Milvus | `milvus` | HNSW, DISKANN, AISAQ, FLAT | COSINE, L2, IP | `pymilvus` | +| pgvector (PostgreSQL) | `pgvector` | HNSW, IVFFLAT, FLAT | COSINE, L2, IP | `psycopg2-binary`, `pgvector` | +| Elasticsearch | `elasticsearch` | HNSW, FLAT | COSINE, L2, IP | `elasticsearch` | + +All backends implement the same abstract interface (`VectorDBBackend`), so +the benchmark orchestrator, data generation, ground-truth computation, and +search pipeline are completely database-agnostic. + +## Directory Layout + +``` +benchmark/ +├── __init__.py # Public API exports +├── __main__.py # python -m vdbbench.benchmark entry point +├── run_benchmark.py # CLI: argument parsing, config resolution +├── orchestrator.py # BenchmarkOrchestrator + BenchmarkConfig +├── generator.py # VectorGenerator (producer thread) +├── ground_truth.py # GroundTruthBuilder (brute-force exact NN) +├── search_runner.py # SearchRunner (latency / recall measurement) +├── collection_admin.py # CLI: collection admin + interactive manager +├── .env.example # Template for backend connection env vars +├── backends/ # Pluggable database adapters +│ ├── __init__.py # BackendRegistry + auto-discovery +│ ├── base.py # Abstract VectorDBBackend + descriptors +│ ├── _env.py # Environment variable loading +│ ├── _help.py # CLI help formatting +│ ├── elasticsearch/ # Elasticsearch adapter +│ ├── milvus/ # Milvus adapter +│ └── pgvector/ # PostgreSQL + pgvector adapter +└── configs/ # Example YAML configuration files + ├── 1m_diskann.yaml + ├── 1m_hnsw.yaml + ├── elasticsearch_1m_hnsw.yaml + └── pgvector_1m_hnsw.yaml +``` + +## Modular Backend Interface + +### Abstract Base Class + +Every database adapter subclasses `VectorDBBackend` (defined in +`backends/base.py`) and implements the following abstract methods: + +#### Lifecycle + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `connect` | `(**kwargs) -> None` | Open a connection using params from the backend descriptor. | +| `disconnect` | `() -> None` | Close the connection and release resources. | + +#### Collection Management + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `create_collection` | `(name, dimension, metric_type, index_type, index_params, num_shards, force) -> CollectionInfo` | Create a collection and its index. Drops first when `force=True`. | +| `collection_exists` | `(name) -> bool` | Check whether a collection exists. | +| `drop_collection` | `(name) -> None` | Drop a collection if it exists. | + +#### Data Ingestion + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `insert_batch` | `(name, ids, vectors) -> int` | Insert vectors. `ids` is `(n,)` int64, `vectors` is `(n, dim)` float32. | +| `flush` | `(name) -> None` | Commit pending writes to durable storage. | + +#### Search + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `search` | `(name, query_vectors, top_k, search_params) -> List[List[int]]` | ANN or exact search. Returns `top_k` IDs per query, closest-first. | + +#### Status / Info + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `row_count` | `(name) -> int` | Number of vectors in the collection. | +| `get_index_progress` | `(name) -> IndexProgress` | Point-in-time index build snapshot. | + +#### Administration / Introspection + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `list_collections` | `() -> List[str]` | All collection names on the server. | +| `get_collection_info` | `(name) -> Dict` | Detailed metadata (rows, dimension, metric, index, schema). | +| `list_indexes` | `(name) -> List[Dict]` | All indexes on a collection. | +| `drop_index` | `(name, index_name=None) -> None` | Drop an index. Default raises `NotImplementedError`. | +| `get_collection_stats` | `(name) -> Dict` | Operational stats. Default returns row count + index progress. | + +#### Concrete Methods (provided by base class) + +| Method | Purpose | +|--------|---------| +| `wait_for_index(name, interval, timeout, compacted)` | Polls `get_index_progress()` with unified progress logging, rates, and ETA. | +| `compact(name)` | Trigger segment compaction. Default is a no-op. | + +### Descriptor System + +Each backend exposes a `BackendDescriptor` that declares its capabilities. +This drives CLI help, argument validation, and execution planning. + +```python +@dataclass +class BackendDescriptor: + name: str # "milvus" -- used in --backend + display_name: str # "Milvus" -- shown in help + description: str # one-paragraph overview + backend_class: Type[VectorDBBackend] + supported_metrics: List[str] # ["COSINE", "L2", "IP"] + supported_indexes: List[IndexDescriptor] + connection_params: List[ParamDescriptor] + active: bool = True # False hides from CLI/registry +``` + +Supporting dataclasses: + +```python +@dataclass +class ParamDescriptor: + name: str # e.g. "M", "host" + description: str # shown in --help + type: str = "int" # "int" | "float" | "str" | "bool" + default: Any = None + required: bool = False + +@dataclass +class IndexDescriptor: + name: str # e.g. "HNSW" + description: str + build_params: List[ParamDescriptor] + search_params: List[ParamDescriptor] +``` + +### Auto-Discovery + +Backend packages are discovered automatically when the `backends` package is +imported: + +1. Walk every sub-directory of `backends/` that is a Python package. +2. Import the package and look for a `backend_descriptor` attribute. +3. If callable, call it; otherwise use it directly. +4. If the result is a `BackendDescriptor`, register it in the global `registry`. +5. If import fails (missing dependency), log a warning and skip. + +No manual wiring is needed. Drop a new package into `backends/` and it will be +picked up on the next import. + +### Backend Registry + +The `registry` singleton (`backends/__init__.py`) provides: + +| Method | Returns | Description | +|--------|---------|-------------| +| `registry.names()` | `List[str]` | Active backend names, sorted. | +| `registry.list_backends()` | `List[BackendDescriptor]` | Active descriptors, sorted. | +| `registry.get(name)` | `BackendDescriptor` or `None` | Look up by name. | +| `registry.create_backend(name)` | `VectorDBBackend` | Instantiate (disconnected). | +| `get_backend(name)` | `VectorDBBackend` | Module-level shortcut. | + +## Environment Variable Configuration + +Connection parameters can be set via environment variables or a `.env` file +using the naming convention: + +``` +{BACKEND}__{PARAM} +``` + +Both parts are upper-cased, separated by a double underscore. Examples: + +```bash +MILVUS__HOST=10.0.0.5 +MILVUS__PORT=19530 +PGVECTOR__PASSWORD=s3cret +ELASTICSEARCH__API_KEY=abc123 +``` + +Precedence (highest wins): + +``` +CLI flags > environment variables / .env > YAML config > built-in defaults +``` + +See `.env.example` for a full template. + +## Collection Admin CLI + +`collection_admin.py` provides both non-interactive commands and an interactive +menu-driven mode for managing collections across any registered backend. + +### Non-Interactive Commands + +Require `--backend` to specify which database to operate on: + +```bash +# List all collections +collection-admin --backend milvus list + +# Detailed collection metadata +collection-admin --backend milvus info my_collection + +# List indexes +collection-admin --backend pgvector indexes my_collection + +# Collection statistics +collection-admin --backend elasticsearch stats my_collection + +# Drop a collection (requires --yes) +collection-admin --backend milvus drop my_collection --yes + +# Drop an index +collection-admin --backend pgvector drop-index my_collection --yes + +# JSON output +collection-admin --backend milvus --json list +collection-admin --backend milvus --json info my_collection + +# Override connection parameters +collection-admin --backend milvus --param host=10.0.0.5 --param port=19530 list +``` + +### Interactive Mode + +Discovers all active backends, health-checks each one, and presents +menu-driven navigation: + +```bash +# Enter interactive mode (either form works) +collection-admin interactive +collection-admin # defaults to interactive when no command given +``` + +Interactive mode flow: + +1. **Backend discovery** -- probes every active backend from the registry. + For each, loads connection params from `.env` / environment variables, + falls back to descriptor defaults, and attempts a `connect()` / + `disconnect()` health-check ping. + +2. **Backend picker** -- displays a table of all backends with health status: + ``` + | Idx | Backend | Configured | Status | Details | + |-----|----------------------|------------|-------------|-----------------------| + | 0 | Milvus | Yes | Healthy | host=10.0.0.5, port=… | + | 1 | pgvector (PostgreSQL) | defaults | Unreachable | connection refused | + | 2 | Elasticsearch | Yes | Healthy | host=http://local… | + ``` + Only healthy backends are selectable. Passwords are hidden. + +3. **Collection picker** -- lists collections on the selected backend with + row count, dimension, index type, and metric: + ``` + | Idx | Collection | Rows | Dim | Index | Metric | + |-----|------------|---------|------|---------|--------| + | 0 | bench_1m | 1,000,000 | 1536 | HNSW | COSINE | + | 1 | test_100k | 100,000 | 768 | FLAT | L2 | + ``` + +4. **Operations menu** -- run commands against the selected collection: + - `i` -- info (detailed schema, partitions) + - `s` -- stats (row count, index progress) + - `x` -- indexes (list all indexes) + - `c` -- compact (trigger compaction) + - `di` -- drop-index (with confirmation) + - `d` -- delete/drop collection (with confirmation) + - `b` -- back to collection list + - `q` -- quit + +Navigation: `b` goes back one level (operations -> collections -> backends), +`q` exits at any point. + +## Architecture Overview + +``` + BenchmarkOrchestrator + ┌──────────────────────────────────────────────┐ + │ │ + YAML / CLI ──────────>│ BenchmarkConfig (all tunables) │ + │ │ + │ ┌── LOAD PHASE ──────────────────────────┐ │ + │ │ │ │ + │ │ VectorGenerator (background thread) │ │ + │ │ │ │ │ + │ │ │ queue.Queue[VectorBlock] │ │ + │ │ │ │ │ + │ │ ├──> backend.insert_batch() │ │ + │ │ └──> GroundTruthBuilder.update() │ │ + │ │ │ │ + │ │ backend.flush() │ │ + │ │ backend.compact() (optional) │ │ + │ │ backend.get_index_progress() → wait │ │ + │ │ gt_builder.build() → truth_table │ │ + │ └────────────────────────────────────────┘ │ + │ │ + │ ┌── SEARCH PHASE ────────────────────────┐ │ + │ │ │ │ + │ │ SearchRunner │ │ + │ │ for each round x each batch: │ │ + │ │ backend.search() [timed] │ │ + │ │ compute recall vs truth_table │ │ + │ │ record latency │ │ + │ │ → SearchResult (QPS, recall, P50…) │ │ + │ └────────────────────────────────────────┘ │ + │ │ + │ save(output_dir) → artifacts on disk │ + └──────────────────────────────────────────────┘ +``` + +### Key Components + +| Component | File | Responsibility | +|-----------|------|----------------| +| **BenchmarkConfig** | `orchestrator.py` | Dataclass holding every tunable. Built from YAML + CLI. | +| **BenchmarkOrchestrator** | `orchestrator.py` | Top-level coordinator for load and search phases. | +| **VectorGenerator** | `generator.py` | Background thread producing L2-normalized `VectorBlock` objects. | +| **GroundTruthBuilder** | `ground_truth.py` | Incrementally computes exact nearest neighbors as blocks arrive. | +| **SearchRunner** | `search_runner.py` | Sends queries, measures latency, computes recall against truth table. | +| **VectorDBBackend** | `backends/base.py` | Abstract interface every database adapter implements. | +| **BackendRegistry** | `backends/__init__.py` | Auto-discovers and registers backend packages. | +| **collection_admin** | `collection_admin.py` | CLI for collection management (non-interactive + interactive). | + +## Metrics & Measurement + +### Load Phase Timings + +Every stage of the load phase is timed independently with `time.time()` and +stored in `benchmark_meta.json` under the `timings` key: + +| Metric | What is timed | +|--------|---------------| +| `query_gen_sec` | Generating random query vectors (CPU only). | +| `create_collection_sec` | Creating the collection and its primary index on the server. | +| `pipeline_sec` | The entire insert pipeline -- consuming vector blocks from the generator thread and calling `backend.insert_batch()` for each batch. Ground-truth computation runs in parallel on a background thread and does **not** inflate this number. | +| `flush_sec` | `backend.flush()` -- committing pending writes to durable storage. | +| `compact_sec` | `backend.compact()` -- merging small segments (optional, backend-dependent). | +| `index_build_sec` | Polling `backend.get_index_progress()` until the ANN index is fully built. | +| `truth_build_sec` | Finalising the brute-force ground-truth table. | + +Per-block insert and ground-truth timings are logged during the run but are +not persisted as aggregate statistics. + +### Search Phase Metrics + +Each query batch is timed with `time.perf_counter()` (high-resolution, +monotonic). Recall is computed **after** timing stops so it does not inflate +latency numbers. + +Final metrics (written to `search_results.json`): + +| Metric | Description | +|--------|-------------| +| `qps` | Queries per second -- `total_queries / wall_elapsed`. | +| `recall_at_k` | Fraction of true nearest neighbors returned, averaged across all queries. | +| `latency_p50_ms` | 50th-percentile per-query latency (ms). | +| `latency_p90_ms` | 90th-percentile per-query latency (ms). | +| `latency_p99_ms` | 99th-percentile per-query latency (ms). | +| `latency_mean_ms` | Mean per-query latency (ms). | +| `total_queries` | Total number of queries executed across all rounds. | +| `total_wall_sec` | Wall-clock duration of the search phase. | +| `intervals` | Per-interval snapshots (every `log_interval` queries) of all the above, plus `qps_interval` for the most recent window. | + +### What "I/O" Includes + +The benchmark measures **end-to-end I/O latency** including network +round-trips to the database server, not isolated disk I/O: + +| Timing | What is in the measurement | +|--------|----------------------------| +| Insert (`pipeline_sec`) | Network send + server-side WAL writes. | +| Flush (`flush_sec`) | Durable commit to storage. | +| Compact (`compact_sec`) | Server-side segment merges. | +| Index build (`index_build_sec`) | Server-side index construction. | +| Search (`latency_*_ms`) | Network query + server-side ANN search + result transfer. | + +CPU-only work -- vector generation, ground-truth computation, recall +calculation -- is either executed on a separate thread or measured outside +the timing window, so it does not contaminate I/O numbers. + +### Concurrency During Measurement + +The load phase uses a three-way producer-consumer pipeline: + +1. **VectorGenerator** (background thread) -- produces `VectorBlock` objects + into a bounded queue. +2. **Main thread** -- consumes blocks, calls `backend.insert_batch()` (network + I/O that releases the GIL). +3. **GroundTruthBuilder** (background thread via `ThreadPoolExecutor`) -- + computes brute-force nearest neighbors for each block (BLAS matmul, + also releases the GIL). + +The search phase is single-threaded: one query batch at a time, timed +individually. + +## Modes + +| Mode | What it does | Required inputs | +|------|-------------|-----------------| +| **load** (default) | Generate vectors, ingest, build ground truth, save artifacts | `collection_name`, `dimension`, `num_vectors` | +| **search** | Load artifacts from a prior run, benchmark ANN queries | `collection_name`, `artifacts_dir` | +| **both** | Run load then search in a single invocation | Same as load | + +## Configuration + +The benchmark is config-driven. All parameters live in a YAML file. The CLI +provides operational flags (`--config`, `--backend`, `--mode`, `--force`, +`--output-dir`, `--artifacts-dir`) plus introspection (`--what-if`, `--plan`). + +### YAML Structure + +```yaml +backend: milvus +mode: both + +database: + host: 127.0.0.1 + port: 19530 + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + distribution: uniform + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +query: + num_query_vectors: 10_000 + query_seed: 99 + +ground_truth: + truth_k: 100 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + M: 64 + efConstruction: 200 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 1 + search_batch_size: 1 + search_params: + ef: 128 + +workflow: + force: false + compact: true + monitor_interval: 5 +``` + +### CLI Examples + +```bash +# Load and search (backend set in YAML) +python -m vdbbench.benchmark --config configs/1m_hnsw.yaml + +# Override mode +python -m vdbbench.benchmark --config configs/1m_hnsw.yaml --mode load + +# Search using artifacts from a prior run +python -m vdbbench.benchmark \ + --config configs/1m_diskann.yaml \ + --mode search \ + --artifacts-dir results/bench_1m_diskann_20250120_143022 + +# Override backend +python -m vdbbench.benchmark \ + --config configs/pgvector_1m_hnsw.yaml --backend pgvector + +# Preview execution plan +python -m vdbbench.benchmark --config configs/1m_hnsw.yaml --plan + +# Dump resolved config (shows env-var sources) +python -m vdbbench.benchmark --config configs/1m_diskann.yaml --what-if +``` + +### CLI Flags + +| Flag | Description | +|------|-------------| +| `--config PATH` | YAML configuration file (required) | +| `--backend NAME` | Override backend from config | +| `--mode {load,search,both}` | Override runtime mode | +| `--force` | Drop existing collection before load | +| `--output-dir PATH` | Directory for output artifacts | +| `--artifacts-dir PATH` | Directory with prior load artifacts (search mode) | +| `--what-if` | Print resolved config and exit | +| `--plan` | Print execution plan and exit | +| `--debug` | Enable DEBUG logging | + +## Output Artifacts + +| File | Content | When | +|------|---------|------| +| `query_vectors.npy` | Query vectors `(nq, dim)` float32 | load / both | +| `ground_truth.npz` | `truth_table` `(nq, truth_k)` int64 | load / both | +| `search_results.json` | QPS, recall, latencies, intervals | search / both | +| `benchmark_meta.json` | Full config + per-phase timing | always | + +## Adding a New Backend + +1. Create `backends/mydb/__init__.py` and `backends/mydb/backend.py`. +2. Subclass `VectorDBBackend` and implement all abstract methods. +3. Write a `backend_descriptor()` function returning a `BackendDescriptor`. +4. That's it -- auto-discovery registers it on the next import. + +See `backends/README.md` for a complete walkthrough with code examples. + +## Programmatic Usage + +```python +from vdbbench.benchmark import ( + BenchmarkConfig, + BenchmarkOrchestrator, + get_backend, +) + +backend = get_backend("milvus") +backend.connect(host="127.0.0.1", port="19530") + +cfg = BenchmarkConfig( + mode="both", + num_vectors=100_000, + dimension=768, + collection_name="my_bench", + index_type="HNSW", + metric_type="COSINE", + index_params={"M": 32, "efConstruction": 128}, + search_k=10, + search_params={"ef": 64}, + num_search_rounds=3, + force=True, +) + +orch = BenchmarkOrchestrator(config=cfg, backend=backend) +summary = orch.run() +paths = orch.save("./results/my_run") + +backend.disconnect() + +print(f"QPS: {summary['search_qps']:.1f}") +print(f"Recall@10: {summary['search_recall_at_k']:.4f}") +``` diff --git a/vdb_benchmark/vdbbench/benchmark/__init__.py b/vdb_benchmark/vdbbench/benchmark/__init__.py new file mode 100644 index 00000000..c88606ad --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/__init__.py @@ -0,0 +1,46 @@ +"""Producer-consumer vector-DB benchmark framework. + +Key entry points: + +* :class:`BenchmarkOrchestrator` -- runs the full pipeline. +* :class:`BenchmarkConfig` -- all tunables. +* :mod:`backends` -- pluggable, auto-discovered database adapters. +""" + +from .backends import ( + BackendDescriptor, + BackendRegistry, + CollectionInfo, + IndexDescriptor, + ParamDescriptor, + VectorDBBackend, + get_backend, + registry, +) +from .generator import VectorBlock, VectorGenerator, generate_query_vectors +from .ground_truth import GroundTruthBuilder +from .orchestrator import BenchmarkConfig, BenchmarkOrchestrator +from .search_runner import SearchResult, SearchRunner + +__all__ = [ + # Config & orchestration + "BenchmarkConfig", + "BenchmarkOrchestrator", + # Backend framework + "BackendDescriptor", + "BackendRegistry", + "CollectionInfo", + "IndexDescriptor", + "ParamDescriptor", + "VectorDBBackend", + "get_backend", + "registry", + # Data pipeline + "GroundTruthBuilder", + "VectorBlock", + "VectorGenerator", + "generate_query_vectors", + # Search benchmark + "SearchResult", + "SearchRunner", +] diff --git a/vdb_benchmark/vdbbench/benchmark/__main__.py b/vdb_benchmark/vdbbench/benchmark/__main__.py new file mode 100644 index 00000000..84738da6 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/__main__.py @@ -0,0 +1,7 @@ +"""Allow running the benchmark as ``python -m vdbbench.benchmark``.""" + +import sys + +from .run_benchmark import main + +sys.exit(main()) diff --git a/vdb_benchmark/vdbbench/benchmark/backends/README.md b/vdb_benchmark/vdbbench/benchmark/backends/README.md new file mode 100644 index 00000000..2318a7f2 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/README.md @@ -0,0 +1,567 @@ +# Vector Database Backends + +This package provides a **pluggable backend system** for the VDB benchmark +framework. Every database adapter implements the same abstract interface +(`VectorDBBackend`), and the framework discovers and registers backends +automatically at import time -- no manual wiring required. + +## Directory Layout + +``` +backends/ +├── __init__.py # BackendRegistry + auto-discovery +├── base.py # Abstract VectorDBBackend + descriptor dataclasses +├── _env.py # Environment variable loading for connection params +├── _help.py # CLI help formatting utilities +├── elasticsearch/ # Elasticsearch adapter +│ ├── __init__.py # backend_descriptor() + exports +│ ├── backend.py # ElasticsearchBackend implementation +│ └── README.md # Elasticsearch-specific documentation +├── milvus/ # Milvus / Zilliz Cloud adapter +│ ├── __init__.py # backend_descriptor() + exports +│ ├── backend.py # MilvusBackend implementation +│ └── README.md # Milvus-specific documentation +└── pgvector/ # PostgreSQL + pgvector adapter + ├── __init__.py # backend_descriptor() + exports + ├── backend.py # PGVectorBackend implementation + └── README.md # pgvector-specific documentation +``` + +## Abstract Interface + +`VectorDBBackend` (defined in `base.py`) is the contract that every adapter +must satisfy. The benchmark orchestrator only calls methods on this interface, +so adding a new database requires **zero changes** to the generation, +ground-truth, or search pipelines. + +### Method Reference + +#### Lifecycle + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `connect` | `connect(self, **kwargs) -> None` | Open a connection. Keyword arguments come from the backend's `connection_params`. | +| `disconnect` | `disconnect(self) -> None` | Close the connection and release resources. | + +#### Collection Management + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `create_collection` | `create_collection(self, name, dimension, metric_type="COSINE", index_type="HNSW", index_params=None, num_shards=1, force=False) -> CollectionInfo` | Create a collection (or drop + recreate when `force=True`) and build its index. | +| `collection_exists` | `collection_exists(self, name: str) -> bool` | Check whether a collection already exists. | +| `drop_collection` | `drop_collection(self, name: str) -> None` | Drop a collection if it exists. | + +#### Data Ingestion + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `insert_batch` | `insert_batch(self, name, ids: np.ndarray, vectors: np.ndarray) -> int` | Insert a batch of vectors. `ids` is `(n,)` int64; `vectors` is `(n, dim)` float32. Returns the number of vectors inserted. | +| `flush` | `flush(self, name: str) -> None` | Commit pending writes to durable storage. | + +#### Search + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `search` | `search(self, name, query_vectors: np.ndarray, top_k: int, search_params=None) -> List[List[int]]` | Run an ANN (or exact) search. Returns a list of `top_k` primary-key IDs per query, ordered closest-first. | + +#### Status / Info + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `row_count` | `row_count(self, name: str) -> int` | Return the number of vectors currently in the collection. | +| `get_index_progress` | `get_index_progress(self, name: str) -> IndexProgress` | **(Abstract)** Return a point-in-time snapshot of the index build. Each backend fills in whatever fields it can (see `IndexProgress` below). | + +#### Concrete (provided by base class) + +| Method | Signature | Purpose | +|--------|-----------|---------| +| `wait_for_index` | `wait_for_index(self, name, interval=5.0, timeout=0, compacted=False) -> None` | Polls `get_index_progress()` in a loop with unified progress logging. When row counts are available (e.g. Milvus) it logs percentage, overall/recent rates, and ETA; otherwise it logs a simpler status line. Raises `TimeoutError` if `timeout > 0` is exceeded. **Do not override** -- implement `get_index_progress()` instead. | +| `compact` | `compact(self, name: str) -> None` | Trigger segment compaction. Default is a no-op; override if your backend needs it (e.g. Milvus). | + +## Descriptor System + +Every backend exposes a `BackendDescriptor` that tells the framework what the +backend supports. This descriptor drives: + +- CLI `--help` output and argument validation +- Index type and metric validation before a run starts +- The `--plan` execution planner + +### Descriptor Dataclasses + +```python +@dataclass +class ParamDescriptor: + name: str # e.g. "M", "ef", "host" + description: str # shown in --help + type: str = "int" # "int" | "float" | "str" | "bool" + default: Any = None + required: bool = False + +@dataclass +class IndexDescriptor: + name: str # e.g. "HNSW", "DISKANN" + description: str + build_params: List[ParamDescriptor] # used during create_collection + search_params: List[ParamDescriptor] # used during search + +@dataclass +class BackendDescriptor: + name: str # short key used in --backend flag + display_name: str # human-readable name + description: str # one-paragraph overview + backend_class: Type[VectorDBBackend] + supported_metrics: List[str] # e.g. ["COSINE", "L2", "IP"] + supported_indexes: List[IndexDescriptor] + connection_params: List[ParamDescriptor] + active: bool = True # set False to hide from CLI / help + +@dataclass +class CollectionInfo: + name: str + dimension: int + metric_type: str + index_type: str + row_count: int = 0 + extra: Dict[str, Any] = field(default_factory=dict) + +@dataclass +class IndexProgress: + """Snapshot of index-build progress returned by get_index_progress().""" + is_ready: bool = False # True when the build is complete + total_rows: int = 0 # total rows to index (0 if unknown) + indexed_rows: int = 0 # rows indexed so far + pending_rows: int = 0 # rows waiting to be indexed + status: str = "" # free-form backend status (e.g. "yellow") +``` + +When `total_rows > 0` the base-class `wait_for_index()` logs detailed +progress: + +``` +Building index: 55.17% complete... (551,660/1,000,000 rows) | Pending rows: 681,000 | Overall rate: 227.28 rows/sec | Recent rate: 4065.85 rows/sec | ETA: 2026-03-31 17:45:23 | Est. remaining: 0:32:52 +``` + +When only `status` is available (e.g. Elasticsearch health), a simpler +line is shown: + +``` +Waiting for index on 'my_collection' ... (status: yellow) [5s elapsed] +``` + +## Auto-Discovery + +Backend packages are discovered automatically when the `backends` package is +imported. The mechanism (in `__init__.py`) works as follows: + +1. Walk every sub-directory of `backends/` that is a Python package. +2. Import the package. +3. Look for a module-level `backend_descriptor` attribute. +4. If it is callable, call it; otherwise use it directly. +5. If the result is a `BackendDescriptor`, register it in the global + `registry`. +6. If import fails (missing dependency, etc.), log a warning and skip. + +This means installing a new backend is as simple as dropping a package into +`backends/` -- the framework will pick it up on the next import. + +## Existing Backends + +| Backend | `--backend` name | Supported Indexes | Supported Metrics | Active | Required packages | +|---------|-------------------|-------------------|-------------------|--------|-------------------| +| Milvus | `milvus` | HNSW, DISKANN, AISAQ, FLAT | COSINE, L2, IP | Yes | `pymilvus` | +| pgvector | `pgvector` | HNSW, IVFFLAT, FLAT | COSINE, L2, IP | Yes | `psycopg2-binary`, `pgvector` | +| Elasticsearch | `elasticsearch` | HNSW, FLAT | COSINE, L2, IP | Yes | `elasticsearch` | + +### Active vs Inactive Backends + +A backend can be present in the source tree but hidden from users by setting +`active=False` in its `BackendDescriptor`. Inactive backends: + +- Are **not** listed in `--help` or `help backends` output. +- Are **not** returned by `registry.names()`, `registry.list_backends()`, + or `registry.get()`. +- **Cannot** be selected via `--backend` (the CLI will report "unknown + backend"). +- **Are** still registered internally and can be inspected via + `registry.all_backends(include_inactive=True)`. + +This is useful for backends that are under development or not yet ready for +general use. To activate a backend, simply change `active=False` to +`active=True` in its `backend_descriptor()` function. + +## Environment Variable Configuration + +Backend connection parameters can be set via **environment variables** or a +**`.env` file** instead of (or in addition to) CLI flags and YAML configs. + +### Naming Convention + +``` +{BACKEND}__{PARAM} +``` + +Both parts are **upper-cased** and separated by a **double underscore** (`__`). +`PARAM` matches the `name` field of the backend's `connection_params` +descriptors. + +| Backend | Example variables | +|---------|-------------------| +| Milvus | `MILVUS__HOST`, `MILVUS__PORT`, `MILVUS__MAX_MESSAGE_LENGTH` | +| pgvector | `PGVECTOR__HOST`, `PGVECTOR__PORT`, `PGVECTOR__DBNAME`, `PGVECTOR__USER`, `PGVECTOR__PASSWORD` | +| Elasticsearch | `ELASTICSEARCH__HOST`, `ELASTICSEARCH__API_KEY`, `ELASTICSEARCH__CLOUD_ID` | + +### .env File + +If the [`python-dotenv`](https://pypi.org/project/python-dotenv/) package +is installed, the benchmark CLI automatically loads a `.env` file from the +current working directory on startup. See `.env.example` in the benchmark +directory for a template. + +```bash +pip install python-dotenv # optional; enables .env file support +cp benchmark/.env.example .env +# edit .env with your values +``` + +When `python-dotenv` is not installed, only real shell environment variables +are read. + +### Precedence + +Connection parameters are resolved with the following precedence (highest +wins): + +``` +CLI flags > environment variables / .env > YAML config > built-in defaults +``` + +For example, if `MILVUS__HOST=10.0.0.5` is set in `.env` and +`host: 127.0.0.1` is in the YAML config, the env value `10.0.0.5` wins. +But `--host 192.168.1.1` on the CLI overrides both. + +### Debugging + +Use `--what-if` to see where each connection parameter came from: + +```bash +python -m vdbbench.benchmark \ + --backend milvus --config configs/1m_hnsw.yaml --what-if +``` + +Output includes a "Connection parameters (source)" section showing each +parameter's resolved value and whether it came from CLI, env, YAML, or +default. + +### Type Coercion + +Environment variables are always strings. The framework automatically +coerces them to the type declared in `ParamDescriptor.type`: + +| `type` | Conversion | +|--------|-----------| +| `"str"` | Used as-is | +| `"int"` | `int(value)` | +| `"float"` | `float(value)` | +| `"bool"` | `true` / `1` / `yes` / `on` → `True`; everything else → `False` | + +Invalid conversions (e.g. `MILVUS__PORT=abc`) are logged as warnings and +skipped. + +--- + +## Creating a New Backend + +Follow these steps to add support for a new vector database. + +### 1. Create the package directory + +``` +backends/ +└── mydb/ + ├── __init__.py + └── backend.py +``` + +### 2. Implement the backend class (`backend.py`) + +Subclass `VectorDBBackend` and implement every abstract method: + +```python +"""MyDB backend implementation.""" + +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +import numpy as np + +from ..base import CollectionInfo, IndexProgress, VectorDBBackend + +logger = logging.getLogger(__name__) + + +class MyDBBackend(VectorDBBackend): + """Concrete backend for MyDB.""" + + def __init__(self) -> None: + self._client = None + + # -- Lifecycle -------------------------------------------------------- + + def connect(self, host: str = "127.0.0.1", port: str = "6333", **kwargs) -> None: + from mydb_client import Client # import here to keep it optional + self._client = Client(host=host, port=int(port)) + logger.info("Connected to MyDB at %s:%s", host, port) + + def disconnect(self) -> None: + if self._client is not None: + self._client.close() + self._client = None + logger.info("Disconnected from MyDB") + + # -- Collection management -------------------------------------------- + + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "HNSW", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + if self.collection_exists(name): + if force: + self.drop_collection(name) + else: + raise ValueError(f"Collection '{name}' already exists") + + params = index_params or {} + # ... create the collection and index using your DB client ... + + return CollectionInfo( + name=name, + dimension=dimension, + metric_type=metric_type, + index_type=index_type, + row_count=0, + extra={"index_params": params}, + ) + + def collection_exists(self, name: str) -> bool: + return self._client.has_collection(name) + + def drop_collection(self, name: str) -> None: + if self.collection_exists(name): + self._client.delete_collection(name) + logger.info("Dropped collection '%s'", name) + + # -- Data ingestion --------------------------------------------------- + + def insert_batch(self, name: str, ids: np.ndarray, vectors: np.ndarray) -> int: + # ids: (n,) int64, vectors: (n, dim) float32 + self._client.upsert( + collection=name, + ids=ids.tolist(), + vectors=vectors.tolist(), + ) + return len(ids) + + def flush(self, name: str) -> None: + self._client.flush(collection=name) + logger.info("Flushed '%s'", name) + + # -- Search ----------------------------------------------------------- + + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + results = [] + for qvec in query_vectors: + hits = self._client.search( + collection=name, + vector=qvec.tolist(), + limit=top_k, + **(search_params or {}), + ) + results.append([hit.id for hit in hits]) + return results + + # -- Status ----------------------------------------------------------- + + def row_count(self, name: str) -> int: + return self._client.count(collection=name) + + def get_index_progress(self, name: str) -> IndexProgress: + info = self._client.index_status(collection=name) + return IndexProgress( + is_ready=info.get("ready", False), + total_rows=info.get("total", 0), + indexed_rows=info.get("indexed", 0), + pending_rows=info.get("pending", 0), + status=info.get("state", ""), + ) + + # -- Optional overrides ----------------------------------------------- + + def load_collection(self, name: str) -> None: + """Load collection into memory (if your DB requires it).""" + self._client.load(collection=name) + logger.info("Loaded collection '%s' into memory", name) +``` + +**Guidelines:** + +- Import your database client library **inside** `connect()` (not at + module level). This keeps the dependency optional -- the framework can + still import the package and show help text even when the client library + is not installed. +- Always accept `**kwargs` in `connect()` so the framework can pass + connection parameters defined in your descriptor. +- `search()` must return results sorted **closest-first**. +- `insert_batch()` receives NumPy arrays. Convert to lists or native types + as needed by your client library. +- Implement `get_index_progress()` -- **not** `wait_for_index()`. The + base class owns the polling loop and all progress logging. Your method + just returns a single `IndexProgress` snapshot. If your database has a + synchronous index build (like pgvector), simply return + `IndexProgress(is_ready=True)` once the index exists. + +### 3. Write the descriptor (`__init__.py`) + +The `__init__.py` must expose a `backend_descriptor` attribute -- either a +callable (function) that returns a `BackendDescriptor`, or a +`BackendDescriptor` instance directly. + +```python +"""MyDB backend package.""" + +from ..base import BackendDescriptor, IndexDescriptor, ParamDescriptor +from .backend import MyDBBackend + +__all__ = ["MyDBBackend", "backend_descriptor"] + + +def backend_descriptor() -> BackendDescriptor: + """Return the capability descriptor for the MyDB backend.""" + return BackendDescriptor( + name="mydb", # used in --backend mydb + display_name="MyDB", # shown in CLI help + description=( + "A scalable vector database with support for HNSW " + "and brute-force search. Requires the mydb-client " + "Python package." + ), + backend_class=MyDBBackend, + supported_metrics=["COSINE", "L2", "IP"], + supported_indexes=[ + IndexDescriptor( + name="HNSW", + description="Graph-based approximate search.", + build_params=[ + ParamDescriptor( + name="M", + description="Max connections per node.", + type="int", + default=16, + ), + ParamDescriptor( + name="efConstruction", + description="Build-time search width.", + type="int", + default=200, + ), + ], + search_params=[ + ParamDescriptor( + name="ef", + description="Query-time search width.", + type="int", + default=128, + ), + ], + ), + IndexDescriptor( + name="FLAT", + description="Brute-force exact search.", + build_params=[], + search_params=[], + ), + ], + connection_params=[ + ParamDescriptor( + name="host", + description="Server hostname or IP.", + type="str", + default="127.0.0.1", + ), + ParamDescriptor( + name="port", + description="Server port.", + type="str", + default="6333", + ), + ], + ) +``` + +**Key rules for the descriptor:** + +- `name` must be a unique, lower-case identifier. This is used as the + `--backend` CLI value. +- `supported_indexes` must list every index algorithm your backend + supports. `build_params` describe the parameters passed to + `create_collection(index_params=...)`. `search_params` describe the + parameters passed to `search(search_params=...)`. +- `connection_params` should list every keyword accepted by your + `connect()` method so the framework can generate the correct CLI flags. +- Set `active=False` to keep the backend in the tree but hidden from + users. This is useful during development. Omit the field or set + `active=True` (the default) to make it available. + +### 4. Verify + +No manual registration code is needed. Simply restart Python and the +auto-discovery will find your package: + +```bash +# Confirm the backend is discovered +python -c " +from vdbbench.benchmark.backends import registry +print(registry.names()) # should include 'mydb' +print(registry.get('mydb')) # should show your BackendDescriptor +" + +# Check CLI help +python -m vdbbench.benchmark help backend mydb + +# Run a benchmark +python -m vdbbench.benchmark \ + --backend mydb \ + --config configs/1m_hnsw.yaml \ + --mode both +``` + +### 5. Checklist + +- [ ] `backend.py` subclasses `VectorDBBackend` and implements all abstract + methods. +- [ ] `__init__.py` exposes a `backend_descriptor` callable returning a + `BackendDescriptor`. +- [ ] Client library imported inside `connect()`, not at module top level. +- [ ] `connect()` accepts `**kwargs`. +- [ ] `create_collection()` respects the `force` flag (drop + recreate). +- [ ] `search()` returns IDs sorted closest-first. +- [ ] `get_index_progress()` returns an `IndexProgress` snapshot. + `wait_for_index()` is provided by the base class -- do **not** + override it. +- [ ] `supported_indexes` lists every index type the backend handles. +- [ ] `connection_params` matches the keyword arguments of `connect()`. +- [ ] The backend appears in `registry.names()` after import. diff --git a/vdb_benchmark/vdbbench/benchmark/backends/__init__.py b/vdb_benchmark/vdbbench/benchmark/backends/__init__.py new file mode 100644 index 00000000..7a0af32d --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/__init__.py @@ -0,0 +1,183 @@ +"""Backend registry -- auto-discovers backend packages at import time. + +Every sub-directory of ``backends/`` that contains an ``__init__.py`` +with a module-level ``backend_descriptor`` attribute (a callable +returning :class:`BackendDescriptor`) is loaded and registered +automatically. + +Public API consumed by the rest of the benchmark: + +* ``registry`` -- the singleton :class:`BackendRegistry`. +* ``get_backend(name)`` -- shortcut to instantiate a backend by name. +""" + +from __future__ import annotations + +import importlib +import logging +import os +import pkgutil +from typing import Dict, List, Optional, Type + +from .base import ( + BackendDescriptor, + CollectionInfo, + IndexDescriptor, + IndexProgress, + ParamDescriptor, + VectorDBBackend, +) + +logger = logging.getLogger(__name__) + +__all__ = [ + # Data model + "BackendDescriptor", + "CollectionInfo", + "IndexDescriptor", + "IndexProgress", + "ParamDescriptor", + "VectorDBBackend", + # Registry + "BackendRegistry", + "registry", + "get_backend", +] + + +class BackendRegistry: + """Collects :class:`BackendDescriptor` instances from backend packages. + + Only **active** backends (``descriptor.active is True``) are visible + through the public query methods (``get``, ``names``, + ``list_backends``, ``create_backend``). Inactive backends are still + stored internally so they can be reactivated at runtime if needed. + """ + + def __init__(self) -> None: + self._backends: Dict[str, BackendDescriptor] = {} + + # ------------------------------------------------------------------ + # Registration + # ------------------------------------------------------------------ + def register(self, descriptor: BackendDescriptor) -> None: + """Register a backend descriptor (idempotent for the same name).""" + key = descriptor.name.lower() + if key in self._backends: + logger.debug("Backend '%s' already registered; skipping.", key) + return + self._backends[key] = descriptor + status = "active" if descriptor.active else "inactive" + logger.debug("Registered backend: %s (%s)", key, status) + + # ------------------------------------------------------------------ + # Querying (only active backends) + # ------------------------------------------------------------------ + def get(self, name: str) -> Optional[BackendDescriptor]: + """Return the descriptor for *name*, or ``None``. + + Returns ``None`` for inactive backends. + """ + desc = self._backends.get(name.lower()) + if desc is not None and not desc.active: + return None + return desc + + def list_backends(self) -> List[BackendDescriptor]: + """Return all **active** registered descriptors, sorted by name.""" + return sorted( + (d for d in self._backends.values() if d.active), + key=lambda d: d.name, + ) + + def names(self) -> List[str]: + """Return **active** registered backend names, sorted.""" + return sorted(k for k, d in self._backends.items() if d.active) + + def __contains__(self, name: str) -> bool: + desc = self._backends.get(name.lower()) + return desc is not None and desc.active + + # ------------------------------------------------------------------ + # Convenience + # ------------------------------------------------------------------ + def create_backend(self, name: str) -> VectorDBBackend: + """Instantiate and return a (disconnected) backend by name. + + Raises :class:`ValueError` for unknown or inactive backends. + """ + desc = self.get(name) + if desc is None: + available = ", ".join(self.names()) or "(none)" + raise ValueError( + f"Unknown backend '{name}'. Available: {available}" + ) + return desc.backend_class() + + # ------------------------------------------------------------------ + # Introspection (includes inactive) + # ------------------------------------------------------------------ + def all_backends(self, include_inactive: bool = True) -> List[BackendDescriptor]: + """Return every registered descriptor, optionally including inactive ones.""" + return sorted( + (d for d in self._backends.values() if include_inactive or d.active), + key=lambda d: d.name, + ) + + +# Singleton used by the rest of the package. +registry = BackendRegistry() + + +def get_backend(name: str) -> VectorDBBackend: + """Convenience: instantiate a backend by name from the global registry.""" + return registry.create_backend(name) + + +# ------------------------------------------------------------------ +# Auto-discovery +# ------------------------------------------------------------------ + +def _discover_backends() -> None: + """Walk sub-packages of this directory and register any that expose + a ``backend_descriptor`` callable. + """ + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + for finder, subpkg_name, is_pkg in pkgutil.iter_modules([pkg_dir]): + if not is_pkg: + continue # skip plain .py files like base.py + fqn = f"{__name__}.{subpkg_name}" + try: + mod = importlib.import_module(fqn) + except Exception: + logger.warning( + "Failed to import backend package '%s'; skipping.", + fqn, exc_info=True, + ) + continue + + descriptor_fn = getattr(mod, "backend_descriptor", None) + if descriptor_fn is None: + logger.debug( + "Package '%s' has no backend_descriptor(); skipping.", fqn + ) + continue + + try: + desc = descriptor_fn() if callable(descriptor_fn) else descriptor_fn + if isinstance(desc, BackendDescriptor): + registry.register(desc) + else: + logger.warning( + "backend_descriptor in '%s' did not return a " + "BackendDescriptor; got %s", + fqn, type(desc).__name__, + ) + except Exception: + logger.warning( + "Error calling backend_descriptor() in '%s'; skipping.", + fqn, exc_info=True, + ) + + +_discover_backends() diff --git a/vdb_benchmark/vdbbench/benchmark/backends/_env.py b/vdb_benchmark/vdbbench/benchmark/backends/_env.py new file mode 100644 index 00000000..8852e2eb --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/_env.py @@ -0,0 +1,151 @@ +"""Load backend connection parameters from environment variables and ``.env`` files. + +Variable naming convention:: + + {BACKEND_NAME}__{PARAM_NAME} + +Both parts are **upper-cased** and separated by a **double underscore**. +The ``PARAM_NAME`` corresponds to a ``ParamDescriptor.name`` from the +backend's ``connection_params``, also upper-cased. + +Examples:: + + MILVUS__HOST=10.0.0.5 + MILVUS__PORT=19530 + PGVECTOR__PASSWORD=s3cret + ELASTICSEARCH__API_KEY=abc123 + +If the `python-dotenv`_ package is installed, a ``.env`` file in the +current working directory (or the path given to :func:`load_env_file`) is +loaded automatically so that the variables are available via +``os.environ``. When ``python-dotenv`` is not installed the module +falls back to reading ``os.environ`` directly (i.e. only real shell +environment variables are considered). + +.. _python-dotenv: https://pypi.org/project/python-dotenv/ +""" + +from __future__ import annotations + +import logging +import os +from typing import Any, Dict, Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from .base import BackendDescriptor + +logger = logging.getLogger(__name__) + +# Double underscore separates the backend name from the parameter name. +_SEP = "__" + + +# ------------------------------------------------------------------ +# .env file loading +# ------------------------------------------------------------------ + +def load_env_file(path: Optional[str] = None) -> bool: + """Load a ``.env`` file into ``os.environ``. + + Parameters + ---------- + path : str, optional + Explicit path to the ``.env`` file. When *None*, ``python-dotenv`` + searches upward from the current working directory. + + Returns + ------- + bool + ``True`` if a ``.env`` file was loaded, ``False`` otherwise + (including when ``python-dotenv`` is not installed). + """ + try: + from dotenv import load_dotenv, find_dotenv # type: ignore[import-untyped] + except ImportError: + logger.debug( + "python-dotenv is not installed; skipping .env file loading. " + "Install it with: pip install python-dotenv" + ) + return False + + dotenv_path = path or find_dotenv(usecwd=True) + if not dotenv_path or not os.path.isfile(dotenv_path): + logger.debug("No .env file found") + return False + + load_dotenv(dotenv_path, override=False) + logger.info("Loaded .env file: %s", dotenv_path) + return True + + +# ------------------------------------------------------------------ +# Type coercion +# ------------------------------------------------------------------ + +def _coerce(value: str, type_hint: str) -> Any: + """Convert a string *value* to the Python type indicated by *type_hint*. + + Supported hints (matching ``ParamDescriptor.type``): + ``"int"``, ``"float"``, ``"str"``, ``"bool"``. + """ + type_hint = type_hint.lower() + if type_hint == "int": + return int(value) + if type_hint == "float": + return float(value) + if type_hint == "bool": + return value.lower() in ("1", "true", "yes", "on") + return value # "str" or anything else + + +# ------------------------------------------------------------------ +# Read env vars for a backend +# ------------------------------------------------------------------ + +def env_for_backend( + backend_name: str, + desc: "BackendDescriptor", +) -> Dict[str, Any]: + """Return a dict of connection parameters sourced from the environment. + + For each ``ParamDescriptor`` in *desc.connection_params*, the function + looks for an environment variable named + ``{BACKEND_NAME}__{PARAM_NAME}`` (both upper-cased, separated by a + double underscore). + + Values are coerced to the type declared in ``ParamDescriptor.type``. + Variables that are not set in the environment are omitted from the + returned dict. + + Parameters + ---------- + backend_name : str + Short backend key (e.g. ``"milvus"``). + desc : BackendDescriptor + The backend's descriptor (used to enumerate connection params and + their types). + + Returns + ------- + dict[str, Any] + Mapping of ``param_name -> coerced_value`` for every env var that + was found. + """ + prefix = backend_name.upper() + _SEP + result: Dict[str, Any] = {} + + for param in desc.connection_params: + env_key = prefix + param.name.upper() + raw = os.environ.get(env_key) + if raw is None: + continue + try: + result[param.name] = _coerce(raw, param.type) + logger.debug("Env var %s -> %s = %r", env_key, param.name, result[param.name]) + except (ValueError, TypeError) as exc: + logger.warning( + "Ignoring env var %s: could not coerce %r to %s: %s", + env_key, raw, param.type, exc, + ) + + return result diff --git a/vdb_benchmark/vdbbench/benchmark/backends/_help.py b/vdb_benchmark/vdbbench/benchmark/backends/_help.py new file mode 100644 index 00000000..6d69f74a --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/_help.py @@ -0,0 +1,141 @@ +"""Human-readable help formatter for backend capabilities. + +Usage from CLI:: + + help backends -- list all registered backends + help backend milvus -- detailed info for one backend + +Usage from Python:: + + from benchmark.backends._help import format_backend_help, format_backends_list + print(format_backends_list(registry)) + print(format_backend_help(registry, "milvus")) +""" + +from __future__ import annotations + +import textwrap +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from . import BackendRegistry + from .base import BackendDescriptor, IndexDescriptor + + +def format_backends_list(reg: "BackendRegistry") -> str: + """One-line summary of every registered backend.""" + backends = reg.list_backends() + if not backends: + return "No backends registered." + + lines = ["Registered vector-database backends:", ""] + name_width = max(len(d.display_name) for d in backends) + for desc in backends: + first_line = desc.description.split(".")[0].strip() + "." + metrics = ", ".join(desc.supported_metrics) + indexes = ", ".join(desc.index_names()) + lines.append( + f" {desc.display_name:<{name_width}} " + f"(name: {desc.name})" + ) + lines.append( + f" {'':<{name_width}} " + f"metrics: {metrics}" + ) + lines.append( + f" {'':<{name_width}} " + f"indexes: {indexes}" + ) + lines.append("") + + lines.append( + "Use 'help backend ' for detailed parameters. " + "Example: help backend milvus" + ) + return "\n".join(lines) + + +def format_backend_help(reg: "BackendRegistry", name: str) -> str: + """Detailed help for one backend, including every parameter.""" + desc = reg.get(name) + if desc is None: + available = ", ".join(reg.names()) or "(none)" + return f"Unknown backend '{name}'. Available: {available}" + return _render_descriptor(desc) + + +# ------------------------------------------------------------------ +# Internal renderers +# ------------------------------------------------------------------ + +_SEPARATOR = "-" * 64 + + +def _render_descriptor(desc: "BackendDescriptor") -> str: + parts: list[str] = [] + + # Header + parts.append("=" * 64) + parts.append(f"Backend: {desc.display_name} (--backend {desc.name})") + parts.append("=" * 64) + parts.append("") + parts.append(textwrap.fill(desc.description, width=64)) + parts.append("") + + # Metrics + parts.append("Supported distance metrics:") + for m in desc.supported_metrics: + parts.append(f" - {m}") + parts.append("") + + # Connection params + if desc.connection_params: + parts.append(_SEPARATOR) + parts.append("Connection parameters:") + parts.append(_SEPARATOR) + parts.append("") + for p in desc.connection_params: + parts.append(_render_param(p)) + parts.append("") + + # Index types + if desc.supported_indexes: + parts.append(_SEPARATOR) + parts.append("Index types:") + parts.append(_SEPARATOR) + for idx in desc.supported_indexes: + parts.append("") + parts.extend(_render_index(idx)) + + return "\n".join(parts) + + +def _render_index(idx: "IndexDescriptor") -> list[str]: + lines: list[str] = [] + lines.append(f" [{idx.name}]") + lines.append(f" {idx.description}") + lines.append("") + + if idx.build_params: + lines.append(" Build parameters:") + for p in idx.build_params: + lines.append(" " + _render_param(p)) + else: + lines.append(" Build parameters: (none)") + + lines.append("") + + if idx.search_params: + lines.append(" Search parameters:") + for p in idx.search_params: + lines.append(" " + _render_param(p)) + else: + lines.append(" Search parameters: (none)") + + return lines + + +def _render_param(p) -> str: + req = " (required)" if p.required else "" + default = f" [default: {p.default}]" if p.default is not None else "" + return f" --{p.name} <{p.type}>{req}{default}\n {p.description}" diff --git a/vdb_benchmark/vdbbench/benchmark/backends/base.py b/vdb_benchmark/vdbbench/benchmark/backends/base.py new file mode 100644 index 00000000..27139d32 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/base.py @@ -0,0 +1,487 @@ +"""Abstract base class for vector database backends. + +Every concrete backend (Milvus, Qdrant, Weaviate, ...) must subclass +``VectorDBBackend`` and implement the abstract methods below. The +benchmark orchestrator only talks through this interface, so swapping +databases requires zero changes to the generation / ground-truth pipeline. + +Each backend lives in its own sub-package (e.g. ``backends/milvus/``) +and exposes a :func:`backend_descriptor` function that returns a +:class:`BackendDescriptor`. The registry discovers these packages +automatically at import time. +""" + +from __future__ import annotations + +import abc +import logging +import time +from dataclasses import dataclass, field +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Type + +import numpy as np + +logger = logging.getLogger(__name__) + + +# ===================================================================== +# Capability / descriptor data model +# ===================================================================== + +@dataclass +class ParamDescriptor: + """One tunable parameter for an index or a connection.""" + name: str + description: str + type: str = "int" # "int", "float", "str", "bool" + default: Any = None + required: bool = False + + +@dataclass +class IndexDescriptor: + """Everything the benchmark needs to know about one index algorithm.""" + name: str # e.g. "HNSW" + description: str + build_params: List[ParamDescriptor] = field(default_factory=list) + search_params: List[ParamDescriptor] = field(default_factory=list) + + +@dataclass +class BackendDescriptor: + """Self-description returned by every backend package. + + The registry collects these and uses them for CLI help, validation, + and dynamic argument generation. + + Set *active* to ``False`` to keep a backend in the tree without + exposing it to users (it will be hidden from ``--help``, CLI + validation, and ``registry.names()``). + """ + name: str # short, lower-case key ("milvus") + display_name: str # human-readable ("Milvus") + description: str # one-paragraph overview + backend_class: Type["VectorDBBackend"] + supported_metrics: List[str] = field(default_factory=list) + supported_indexes: List[IndexDescriptor] = field(default_factory=list) + connection_params: List[ParamDescriptor] = field(default_factory=list) + active: bool = True + + # ------------------------------------------------------------------ + # Convenience look-ups + # ------------------------------------------------------------------ + def index_names(self) -> List[str]: + """Return the list of supported index algorithm names.""" + return [idx.name for idx in self.supported_indexes] + + def get_index(self, name: str) -> Optional[IndexDescriptor]: + """Return the :class:`IndexDescriptor` for *name*, or ``None``.""" + for idx in self.supported_indexes: + if idx.name.upper() == name.upper(): + return idx + return None + + +# ===================================================================== +# Collection metadata (unchanged) +# ===================================================================== + +@dataclass +class CollectionInfo: + """Metadata returned after a collection is created or connected to.""" + name: str + dimension: int + metric_type: str + index_type: str + row_count: int = 0 + extra: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class IndexProgress: + """Snapshot of index-build progress returned by backends. + + Backends fill in as much as they know: + + * **Milvus** – has ``total_rows``, ``indexed_rows``, and ``pending_rows``. + * **pgvector** – ``CREATE INDEX`` is synchronous; simply sets ``is_ready``. + * **Elasticsearch** – sets ``status`` (red/yellow/green) and ``is_ready``. + + The base-class ``wait_for_index`` handles all logging, adapting + the detail level to whatever fields the backend provides. + """ + is_ready: bool = False + total_rows: int = 0 + indexed_rows: int = 0 + pending_rows: int = 0 + status: str = "" # free-form backend status (e.g. "yellow") + + +class VectorDBBackend(abc.ABC): + """Thin, storage-only contract that every vector DB must satisfy.""" + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + @abc.abstractmethod + def connect(self, **kwargs) -> None: + """Establish a connection to the database server.""" + + @abc.abstractmethod + def disconnect(self) -> None: + """Cleanly disconnect from the server.""" + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + @abc.abstractmethod + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "HNSW", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + """Create (or re-create if *force*) a collection and its index. + + Parameters + ---------- + name : str + Collection / table / index name. + dimension : int + Dimensionality of the vectors. + metric_type : str + Distance metric (``COSINE``, ``L2``, ``IP``). + index_type : str + Index algorithm (``HNSW``, ``DISKANN``, ``FLAT``, ...). + index_params : dict, optional + Backend-specific index build parameters (e.g. ``M``, + ``efConstruction`` for HNSW). + num_shards : int + Number of shards / partitions. + force : bool + If *True*, drop any existing collection with the same name first. + + Returns + ------- + CollectionInfo + """ + + @abc.abstractmethod + def collection_exists(self, name: str) -> bool: + """Return *True* if the collection already exists.""" + + @abc.abstractmethod + def drop_collection(self, name: str) -> None: + """Drop a collection if it exists.""" + + # ------------------------------------------------------------------ + # Data ingestion + # ------------------------------------------------------------------ + @abc.abstractmethod + def insert_batch( + self, + name: str, + ids: np.ndarray, + vectors: np.ndarray, + ) -> int: + """Insert a batch of vectors. + + Parameters + ---------- + name : str + Target collection name. + ids : np.ndarray + 1-D array of integer primary keys (int64). + vectors : np.ndarray + 2-D float32 array of shape ``(n, dim)``. + + Returns + ------- + int + Number of vectors successfully inserted. + """ + + @abc.abstractmethod + def flush(self, name: str) -> None: + """Flush / commit pending writes for the collection.""" + + def compact(self, name: str) -> None: + """Trigger segment compaction and wait for it to finish. + + Compaction merges many small segments into fewer large ones so + the index builder can process them efficiently. The default + implementation is a no-op (not every backend needs compaction). + """ + + # ------------------------------------------------------------------ + # Search + # ------------------------------------------------------------------ + @abc.abstractmethod + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + """Run an ANN (or exact) search. + + Parameters + ---------- + name : str + Collection to search. + query_vectors : np.ndarray + 2-D float32 array of shape ``(nq, dim)``. + top_k : int + Number of nearest neighbors to return per query. + search_params : dict, optional + Backend-specific search parameters (e.g. ``ef`` for HNSW). + + Returns + ------- + list[list[int]] + For each query vector, a list of ``top_k`` primary-key IDs + ordered by distance (closest first). + """ + + # ------------------------------------------------------------------ + # Status / info + # ------------------------------------------------------------------ + @abc.abstractmethod + def row_count(self, name: str) -> int: + """Return the current number of vectors in the collection.""" + + @abc.abstractmethod + def get_index_progress(self, name: str) -> IndexProgress: + """Return a point-in-time snapshot of the index build. + + Each backend fills in whatever it can. Milvus can report row + counts; pgvector simply returns ``is_ready=True`` once the + synchronous ``CREATE INDEX`` finishes; Elasticsearch checks + cluster health status. + + The base class ``wait_for_index`` calls this in a loop and + handles all progress logging. + """ + + # ------------------------------------------------------------------ + # Administration / introspection + # ------------------------------------------------------------------ + @abc.abstractmethod + def list_collections(self) -> List[str]: + """Return names of all collections (tables / indexes) on the server.""" + + @abc.abstractmethod + def get_collection_info(self, name: str) -> Dict[str, Any]: + """Return detailed metadata about a single collection. + + The returned dict should include at least: + + * ``name`` (str) + * ``row_count`` (int) + * ``dimension`` (int or None) + * ``metric_type`` (str or None) + * ``index_type`` (str or None) + * ``schema`` (list[dict] -- one entry per field/column) + + Backends may add extra keys. + """ + + @abc.abstractmethod + def list_indexes(self, name: str) -> List[Dict[str, Any]]: + """Return info about every index on *name*. + + Each dict should include at least ``index_name``, + ``index_type``, and ``params``. + """ + + def drop_index(self, name: str, index_name: Optional[str] = None) -> None: + """Drop an index from the collection. + + Parameters + ---------- + name : str + Collection name. + index_name : str, optional + Specific index to drop. When *None* the backend drops the + primary / only vector index. + + The default implementation raises :class:`NotImplementedError`. + """ + raise NotImplementedError( + f"{type(self).__name__} does not implement drop_index" + ) + + def get_collection_stats(self, name: str) -> Dict[str, Any]: + """Return operational statistics for a collection. + + The default implementation returns the row count and index + progress; backends may override to add richer metrics. + """ + prog = self.get_index_progress(name) + return { + "name": name, + "row_count": self.row_count(name), + "index_ready": prog.is_ready, + "index_status": prog.status, + "indexed_rows": prog.indexed_rows, + "total_rows": prog.total_rows, + "pending_rows": prog.pending_rows, + } + + # ------------------------------------------------------------------ + # Unified index-wait with progress logging + # ------------------------------------------------------------------ + _STALL_LOG_EVERY: int = 6 # stall reminder every N unchanged polls + + def wait_for_index( + self, + name: str, + interval: float = 5.0, + timeout: float = 0, + compacted: bool = False, + ) -> None: + """Block until the index build finishes. + + Polls :meth:`get_index_progress` every *interval* seconds and + emits unified progress logs. When the backend provides row + counts the output includes overall/recent rates and an ETA; + otherwise a simpler status line is shown. + + Parameters + ---------- + interval : float + Polling interval in seconds. + timeout : float + Maximum seconds to wait (0 = forever). + compacted : bool + Hint from the orchestrator — used only in stall warnings. + """ + start = time.time() + prev_indexed = -1 + prev_time = start + stall_polls = 0 + eta_deadline = float("inf") + warned = False + + while True: + try: + prog = self.get_index_progress(name) + now = time.time() + elapsed = now - start + + # ---------- done? ---------- + if prog.is_ready: + if prog.total_rows: + logger.info( + "Index build complete for '%s' " + "(%s rows in %.1fs)", + name, f"{prog.total_rows:,}", elapsed, + ) + else: + msg = f"Index ready for '{name}'" + if prog.status: + msg += f" (status: {prog.status})" + msg += f" [{elapsed:.1f}s]" + logger.info(msg) + return + + # ---------- row-level progress (Milvus-style) ---------- + if prog.total_rows > 0: + pct = prog.indexed_rows / prog.total_rows * 100 + + if prog.indexed_rows != prev_indexed: + delta = prog.indexed_rows - max(prev_indexed, 0) + dt = now - prev_time + recent_rate = delta / dt if dt > 0 else 0 + overall_rate = ( + prog.indexed_rows / elapsed if elapsed > 0 else 0 + ) + remaining = prog.total_rows - prog.indexed_rows + eta_secs = ( + remaining / recent_rate if recent_rate > 0 else 0 + ) + eta_deadline = now + eta_secs + eta_dt = datetime.now() + timedelta(seconds=eta_secs) + remaining_td = str(timedelta(seconds=int(eta_secs))) + logger.info( + "Building index: %.2f%% complete... " + "(%s/%s rows) | Pending rows: %s | " + "Overall rate: %.2f rows/sec | " + "Recent rate: %.2f rows/sec | " + "ETA: %s | Est. remaining: %s", + pct, + f"{prog.indexed_rows:,}", + f"{prog.total_rows:,}", + f"{prog.pending_rows:,}", + overall_rate, + recent_rate, + eta_dt.strftime("%Y-%m-%d %H:%M:%S"), + remaining_td, + ) + stall_polls = 0 + warned = False + prev_indexed = prog.indexed_rows + prev_time = now + else: + stall_polls += 1 + if not warned and now > eta_deadline: + warned = True + if compacted: + logger.warning( + "Index build has exceeded ETA by " + "%.0fs (compaction was already " + "performed). This may be normal " + "for large indexes -- waiting. " + "[%.0fs elapsed]", + now - eta_deadline, elapsed, + ) + else: + logger.warning( + "Index build has exceeded ETA by " + "%.0fs. Set 'compact: true' in " + "your config so small segments " + "are merged before index build. " + "[%.0fs elapsed]", + now - eta_deadline, elapsed, + ) + elif stall_polls % self._STALL_LOG_EVERY == 0: + overall_rate = ( + prog.indexed_rows / elapsed + if elapsed > 0 else 0 + ) + logger.info( + "Building index: %.2f%% complete... " + "(%s/%s rows) | Pending rows: %s | " + "Overall rate: %.2f rows/sec | " + "No progress for %.0fs " + "[%.0fs elapsed]", + pct, + f"{prog.indexed_rows:,}", + f"{prog.total_rows:,}", + f"{prog.pending_rows:,}", + overall_rate, + stall_polls * interval, + elapsed, + ) + # ---------- status-only (ES / pgvector-style) ---------- + else: + status_str = prog.status or "waiting" + logger.info( + "Waiting for index on '%s' … (status: %s) " + "[%.0fs elapsed]", + name, status_str, elapsed, + ) + except Exception as exc: + logger.warning("Index progress check failed: %s", exc) + + if timeout > 0 and (time.time() - start) > timeout: + raise TimeoutError( + f"Index build did not finish within {timeout}s" + ) + time.sleep(interval) diff --git a/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/README.md b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/README.md new file mode 100644 index 00000000..df947b17 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/README.md @@ -0,0 +1,210 @@ +# Elasticsearch Backend + +Adapter for [Elasticsearch](https://www.elastic.co/elasticsearch/) 8.x+ +with native dense-vector kNN search. + +## Requirements + +```bash +pip install elasticsearch +``` + +A running Elasticsearch 8.x cluster is required. The backend uses the +[kNN search API](https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html) +introduced in Elasticsearch 8.0. + +## Connection + +| Parameter | Env Variable | Default | Description | +|-----------|-------------|---------|-------------| +| `host` | `ELASTICSEARCH__HOST` | `http://localhost:9200` | Elasticsearch server URL | +| `api_key` | `ELASTICSEARCH__API_KEY` | *(none)* | API key for authentication (optional) | +| `cloud_id` | `ELASTICSEARCH__CLOUD_ID` | *(none)* | Elastic Cloud deployment ID (optional, alternative to `host`) | + +Connection precedence: +1. If `cloud_id` is set, connect via Elastic Cloud with optional `api_key`. +2. If only `api_key` is set, connect to `host` with API key authentication. +3. Otherwise, connect to `host` without authentication. + +## Supported Indexes + +### HNSW + +Default dense-vector index type in Elasticsearch 8.x. Segments are built +during refresh/merge operations. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `m` | int | 16 | Max connections per node. Higher values improve recall at the cost of memory | +| `ef_construction` | int | 100 | Search width during index construction | + +| Search Parameter | Type | Default | Description | +|-----------------|------|---------|-------------| +| `num_candidates` | int | 100 | Candidate vectors to consider per shard during kNN search | + +### FLAT + +Brute-force exact search via Elasticsearch's flat index type. Perfect +recall but O(n) per query. No build or search parameters. + +## Supported Metrics + +| Metric | ES Similarity | Notes | +|--------|--------------|-------| +| `COSINE` | `cosine` | Default | +| `L2` | `l2_norm` | Euclidean distance | +| `IP` | `dot_product` | Inner product | + +## Class Structure + +``` +ElasticsearchBackend(VectorDBBackend) +│ +│ # Lifecycle +├── connect(host, **kwargs) +├── disconnect() +│ +│ # Collection (index) management +├── create_collection(name, dimension, metric_type, index_type, +│ index_params, num_shards, force) +├── collection_exists(name) -> bool +├── drop_collection(name) +│ +│ # Data ingestion +├── insert_batch(name, ids, vectors) -> int +├── flush(name) # triggers ES refresh +│ +│ # Search +├── search(name, query_vectors, top_k, search_params) +│ +│ # Status (implements abstract) +├── row_count(name) -> int +├── get_index_progress(name) -> IndexProgress +│ +│ # Optional +└── load_collection(name) # no-op +``` + +### Index Mapping + +Each Elasticsearch index is created with a single `dense_vector` field: + +```json +{ + "mappings": { + "properties": { + "vector": { + "type": "dense_vector", + "dims": 1536, + "similarity": "cosine", + "index": true, + "index_options": { + "type": "hnsw", + "m": 16, + "ef_construction": 200 + } + } + } + }, + "settings": { + "number_of_shards": 1, + "number_of_replicas": 0 + } +} +``` + +Document IDs are stored as the Elasticsearch `_id` field (string +representation of the int64 primary key). + +### Data Ingestion + +`insert_batch()` uses the Elasticsearch +[Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) +with `refresh=False` for maximum throughput. Partial failures are logged +as warnings and the count of successfully inserted documents is returned. + +### Flush / Refresh + +`flush()` calls `indices.refresh()` which forces Elasticsearch to make +all recently indexed documents searchable. This is distinct from the +Elasticsearch "flush" API (which syncs the translog to disk). + +### Index Progress + +Elasticsearch builds HNSW segments during refresh/merge, so there is no +separate "index build" phase to monitor. `get_index_progress()` checks +cluster health for the index: + +- **yellow** or **green** = ready (`IndexProgress(is_ready=True)`) +- **red** = not ready, the base-class `wait_for_index()` continues polling + +The base-class progress log shows the simpler status-only format: + +``` +Waiting for index on 'bench_1m_hnsw' ... (status: yellow) [5s elapsed] +``` + +### Search + +Each query is sent individually via the kNN search API: + +```python +client.search( + index=name, + knn={ + "field": "vector", + "query_vector": [...], + "k": top_k, + "num_candidates": 100, # from search_params + }, + size=top_k, + _source=False, +) +``` + +The `num_candidates` parameter controls the per-shard candidate pool +size. Higher values improve recall at the cost of latency. + +### Load Collection + +`load_collection()` is a no-op. Elasticsearch indexes are always +queryable once refreshed -- there is no separate "load into memory" step. + +## Example YAML Config + +```yaml +backend: elasticsearch +mode: both + +database: + host: http://localhost:9200 + # api_key: "" # set via ELASTICSEARCH__API_KEY env var + # cloud_id: "" # set via ELASTICSEARCH__CLOUD_ID env var + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + m: 16 + ef_construction: 200 + +search: + search_k: 10 + search_params: + num_candidates: 128 +``` + +## Files + +| File | Purpose | +|------|---------| +| `__init__.py` | `backend_descriptor()` -- registers the backend with supported indexes, metrics, and connection params | +| `backend.py` | `ElasticsearchBackend` -- full implementation of `VectorDBBackend` | diff --git a/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/__init__.py b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/__init__.py new file mode 100644 index 00000000..3badd5af --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/__init__.py @@ -0,0 +1,103 @@ +"""Elasticsearch backend package. + +Exposes :class:`ElasticsearchBackend` and :func:`backend_descriptor` for +automatic registration by the backend registry. + +Requires the ``elasticsearch`` Python package:: + + pip install elasticsearch +""" + +from ..base import BackendDescriptor, IndexDescriptor, ParamDescriptor +from .backend import ElasticsearchBackend + +__all__ = ["ElasticsearchBackend", "backend_descriptor"] + + +def backend_descriptor() -> BackendDescriptor: + """Return the capability descriptor for the Elasticsearch backend.""" + return BackendDescriptor( + name="elasticsearch", + display_name="Elasticsearch", + description=( + "Elasticsearch with dense vector support for approximate and " + "exact k-nearest-neighbor search. Uses the kNN search API " + "introduced in Elasticsearch 8.x with HNSW and brute-force " + "(exact) retrieval. Requires a running Elasticsearch cluster " + "and the elasticsearch-py Python package." + ), + backend_class=ElasticsearchBackend, + supported_metrics=["COSINE", "L2", "IP"], + supported_indexes=[ + IndexDescriptor( + name="HNSW", + description=( + "Hierarchical Navigable Small World graph index. " + "Default dense-vector index type in Elasticsearch 8.x." + ), + build_params=[ + ParamDescriptor( + name="m", + description=( + "Max number of connections per node. Higher " + "values improve recall at the cost of memory." + ), + type="int", + default=16, + ), + ParamDescriptor( + name="ef_construction", + description=( + "Search width during index construction. " + "Higher values improve recall at the cost of " + "build time." + ), + type="int", + default=100, + ), + ], + search_params=[ + ParamDescriptor( + name="num_candidates", + description=( + "Number of candidate vectors to consider per " + "shard during kNN search. Higher values improve " + "recall at the cost of latency." + ), + type="int", + default=100, + ), + ], + ), + IndexDescriptor( + name="FLAT", + description=( + "Brute-force exact search via script_score queries. " + "Perfect recall but O(n) per query." + ), + build_params=[], + search_params=[], + ), + ], + connection_params=[ + ParamDescriptor( + name="host", + description="Elasticsearch server URL (e.g. http://localhost:9200).", + type="str", + default="http://localhost:9200", + ), + ParamDescriptor( + name="api_key", + description="API key for authentication (optional).", + type="str", + default=None, + ), + ParamDescriptor( + name="cloud_id", + description="Elastic Cloud deployment ID (optional, alternative to host).", + type="str", + default=None, + ), + ], + active=True, + ) diff --git a/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/backend.py b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/backend.py new file mode 100644 index 00000000..dc1012a0 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/elasticsearch/backend.py @@ -0,0 +1,343 @@ +"""Elasticsearch implementation of :class:`VectorDBBackend`. + +This wraps the ``elasticsearch`` Python client behind the abstract backend +interface. The implementation targets Elasticsearch 8.x dense-vector +fields with native kNN search. + +Requirements:: + + pip install elasticsearch +""" + +from __future__ import annotations + +import logging +import time +from typing import Any, Dict, List, Optional + +import numpy as np + +from ..base import CollectionInfo, IndexProgress, VectorDBBackend + +logger = logging.getLogger(__name__) + +# Elasticsearch similarity names mapped from our canonical metric names. +_METRIC_TO_ES_SIMILARITY: Dict[str, str] = { + "COSINE": "cosine", + "L2": "l2_norm", + "IP": "dot_product", +} + + +class ElasticsearchBackend(VectorDBBackend): + """Concrete backend for Elasticsearch (8.x+ with dense vectors).""" + + def __init__(self) -> None: + self._client = None # type: Any # elasticsearch.Elasticsearch + self._index_meta: Dict[str, Dict[str, Any]] = {} # name -> {metric, dim, …} + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + def connect( + self, + host: str = "http://localhost:9200", + **kwargs, + ) -> None: + from elasticsearch import Elasticsearch + + api_key = kwargs.get("api_key") + cloud_id = kwargs.get("cloud_id") + + if cloud_id: + self._client = Elasticsearch(cloud_id=cloud_id, api_key=api_key) + elif api_key: + self._client = Elasticsearch(host, api_key=api_key) + else: + self._client = Elasticsearch(host) + + info = self._client.info() + logger.info( + "Connected to Elasticsearch %s at %s", + info["version"]["number"], + host, + ) + + def disconnect(self) -> None: + if self._client is not None: + self._client.close() + self._client = None + self._index_meta.clear() + logger.info("Disconnected from Elasticsearch") + + # ------------------------------------------------------------------ + # Collection (index) management + # ------------------------------------------------------------------ + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "HNSW", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + if self.collection_exists(name): + if force: + self.drop_collection(name) + else: + raise ValueError( + f"Index '{name}' already exists. Use force=True to drop it." + ) + + params = index_params or {} + similarity = _METRIC_TO_ES_SIMILARITY.get(metric_type.upper(), "cosine") + + # Build the dense_vector mapping + vector_field: Dict[str, Any] = { + "type": "dense_vector", + "dims": dimension, + "similarity": similarity, + } + + if index_type.upper() == "HNSW": + vector_field["index"] = True + vector_field["index_options"] = { + "type": "hnsw", + "m": params.get("m", 16), + "ef_construction": params.get("ef_construction", 100), + } + elif index_type.upper() == "FLAT": + vector_field["index"] = True + vector_field["index_options"] = { + "type": "flat", + } + else: + # Default to HNSW for unknown types + logger.warning( + "Unknown index type '%s'; falling back to HNSW", index_type + ) + vector_field["index"] = True + vector_field["index_options"] = {"type": "hnsw"} + + mappings = { + "properties": { + "vector": vector_field, + } + } + settings = { + "number_of_shards": num_shards, + "number_of_replicas": 0, + } + + self._client.indices.create( + index=name, + mappings=mappings, + settings=settings, + ) + logger.info( + "Created index '%s' (%d-d, %s, %s, %d shards)", + name, dimension, similarity, index_type, num_shards, + ) + + self._index_meta[name] = { + "dimension": dimension, + "metric_type": metric_type, + "index_type": index_type, + "similarity": similarity, + } + + return CollectionInfo( + name=name, + dimension=dimension, + metric_type=metric_type, + index_type=index_type, + row_count=0, + extra={"index_params": params, "similarity": similarity}, + ) + + def collection_exists(self, name: str) -> bool: + return self._client.indices.exists(index=name).body + + def drop_collection(self, name: str) -> None: + if self.collection_exists(name): + self._client.indices.delete(index=name) + self._index_meta.pop(name, None) + logger.info("Deleted index: %s", name) + + # ------------------------------------------------------------------ + # Data ingestion + # ------------------------------------------------------------------ + def insert_batch( + self, + name: str, + ids: np.ndarray, + vectors: np.ndarray, + ) -> int: + actions = [] + for i in range(len(ids)): + actions.append({"index": {"_index": name, "_id": str(int(ids[i]))}}) + actions.append({"vector": vectors[i].tolist()}) + + resp = self._client.bulk(operations=actions, refresh=False) + if resp.get("errors"): + failed = sum( + 1 for item in resp["items"] + if item.get("index", {}).get("error") + ) + logger.warning("Bulk insert had %s errors", f"{failed:,}") + return len(ids) - failed + return len(ids) + + def flush(self, name: str) -> None: + t0 = time.time() + self._client.indices.refresh(index=name) + logger.info("Refresh completed in %.2f s", time.time() - t0) + + # ------------------------------------------------------------------ + # Search + # ------------------------------------------------------------------ + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + params = search_params or {} + num_candidates = params.get("num_candidates", 100) + + results: List[List[int]] = [] + for qvec in query_vectors: + resp = self._client.search( + index=name, + knn={ + "field": "vector", + "query_vector": qvec.tolist(), + "k": top_k, + "num_candidates": num_candidates, + }, + size=top_k, + _source=False, + ) + ids = [int(hit["_id"]) for hit in resp["hits"]["hits"]] + results.append(ids) + + return results + + # ------------------------------------------------------------------ + # Status / info + # ------------------------------------------------------------------ + def row_count(self, name: str) -> int: + self._client.indices.refresh(index=name) + resp = self._client.count(index=name) + return resp["count"] + + def get_index_progress(self, name: str) -> IndexProgress: + """Check Elasticsearch cluster health for this index. + + Elasticsearch builds HNSW segments during refresh/merge, so + after a bulk ingest + refresh the index is queryable. Health + status of *yellow* or *green* means the index is ready. + """ + health = self._client.cluster.health( + index=name, wait_for_status="yellow", timeout="5s" + ) + status = health["status"] + is_ready = status in ("yellow", "green") + return IndexProgress(is_ready=is_ready, status=status) + + # ------------------------------------------------------------------ + # Optional: load_collection (no-op for Elasticsearch) + # ------------------------------------------------------------------ + def load_collection(self, name: str) -> None: + """No-op -- Elasticsearch indexes are always queryable once refreshed.""" + logger.debug("load_collection is a no-op for Elasticsearch") + + # ------------------------------------------------------------------ + # Administration / introspection + # ------------------------------------------------------------------ + def list_collections(self) -> List[str]: + resp = self._client.cat.indices(format="json") + return sorted( + entry["index"] + for entry in resp + if not entry["index"].startswith(".") + ) + + def get_collection_info(self, name: str) -> Dict[str, Any]: + mapping = self._client.indices.get_mapping(index=name) + props = mapping[name]["mappings"].get("properties", {}) + + # Parse vector field + dimension = None + metric_type = None + index_type = None + schema: List[Dict[str, Any]] = [] + for field_name, field_def in props.items(): + entry: Dict[str, Any] = { + "name": field_name, + "dtype": field_def.get("type", "unknown"), + } + if field_def.get("type") == "dense_vector": + dimension = field_def.get("dims") + entry["dim"] = dimension + # Reverse-map similarity back to our canonical metric + sim = field_def.get("similarity", "") + for canonical, es_sim in _METRIC_TO_ES_SIMILARITY.items(): + if es_sim == sim: + metric_type = canonical + break + idx_opts = field_def.get("index_options", {}) + index_type = idx_opts.get("type", "hnsw").upper() + schema.append(entry) + + row_count = self.row_count(name) + + return { + "name": name, + "row_count": row_count, + "dimension": dimension, + "metric_type": metric_type, + "index_type": index_type, + "schema": schema, + } + + def list_indexes(self, name: str) -> List[Dict[str, Any]]: + mapping = self._client.indices.get_mapping(index=name) + props = mapping[name]["mappings"].get("properties", {}) + + results: List[Dict[str, Any]] = [] + for field_name, field_def in props.items(): + if field_def.get("type") != "dense_vector": + continue + idx_opts = field_def.get("index_options", {}) + results.append({ + "index_name": field_name, + "field_name": field_name, + "index_type": idx_opts.get("type", "hnsw").upper(), + "similarity": field_def.get("similarity", ""), + "params": { + k: v for k, v in idx_opts.items() if k != "type" + }, + }) + return results + + def get_collection_stats(self, name: str) -> Dict[str, Any]: + stats = self._client.indices.stats(index=name) + idx_stats = stats["indices"].get(name, {}).get("primaries", {}) + docs = idx_stats.get("docs", {}) + store = idx_stats.get("store", {}) + health = self._client.cluster.health(index=name) + return { + "name": name, + "row_count": docs.get("count", 0), + "deleted_docs": docs.get("deleted", 0), + "store_size_bytes": store.get("size_in_bytes", 0), + "index_ready": health["status"] in ("yellow", "green"), + "index_status": health["status"], + "indexed_rows": 0, + "total_rows": 0, + "pending_rows": 0, + } diff --git a/vdb_benchmark/vdbbench/benchmark/backends/milvus/README.md b/vdb_benchmark/vdbbench/benchmark/backends/milvus/README.md new file mode 100644 index 00000000..11ac7455 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/milvus/README.md @@ -0,0 +1,186 @@ +# Milvus Backend + +Adapter for [Milvus](https://milvus.io/) / [Zilliz Cloud](https://zilliz.com/) +-- an open-source vector database built for scalable similarity search. + +## Requirements + +```bash +pip install pymilvus +``` + +A running Milvus server (standalone or cluster) is required. See the +[Milvus quickstart](https://milvus.io/docs/install_standalone-docker.md) +for Docker-based setup. + +## Connection + +| Parameter | Env Variable | Default | Description | +|-----------|-------------|---------|-------------| +| `host` | `MILVUS__HOST` | `127.0.0.1` | Milvus server hostname or IP | +| `port` | `MILVUS__PORT` | `19530` | Milvus gRPC port | +| `max_message_length` | `MILVUS__MAX_MESSAGE_LENGTH` | `514983574` | Max gRPC message size in bytes (~491 MB) | + +Connection uses the `pymilvus.connections.connect()` API with the +`"default"` alias. The `max_message_length` parameter controls both +`max_receive_message_length` and `max_send_message_length` on the gRPC +channel. + +## Supported Indexes + +### HNSW + +Hierarchical Navigable Small World graph index. Good general-purpose choice +balancing recall and speed. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `M` | int | 16 | Max connections per node | +| `efConstruction` | int | 200 | Search width during index construction | + +| Search Parameter | Type | Default | Description | +|-----------------|------|---------|-------------| +| `ef` | int | 128 | Search width at query time (higher = better recall) | + +### DiskANN + +Microsoft DiskANN -- SSD-friendly graph index for large-scale datasets +that exceed RAM. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `MaxDegree` | int | 64 | Maximum out-degree of each graph node | +| `SearchListSize` | int | 200 | Candidate-list size during index build | + +| Search Parameter | Type | Default | Description | +|-----------------|------|---------|-------------| +| `search_list` | int | 200 | Candidate-list size at query time | + +### AISAQ + +Approximate Inference with Scalar and Additive Quantization -- a +compressed index format. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `inline_pq` | int | 16 | Product-quantization sub-vector count | +| `max_degree` | int | 32 | Maximum out-degree of each graph node | +| `search_list_size` | int | 100 | Candidate-list size during build | + +No search-time parameters. + +### FLAT + +Brute-force exact search. Perfect recall but O(n) per query. No +build or search parameters. + +## Supported Metrics + +`COSINE`, `L2`, `IP` + +## Class Structure + +``` +MilvusBackend(VectorDBBackend) +│ +│ # Lifecycle +├── connect(host, port, **kwargs) +├── disconnect() +│ +│ # Collection management +├── create_collection(name, dimension, metric_type, index_type, +│ index_params, num_shards, force) +├── collection_exists(name) -> bool +├── drop_collection(name) +│ +│ # Data ingestion +├── insert_batch(name, ids, vectors) -> int +├── flush(name) +├── compact(name) # overrides base no-op +│ +│ # Search +├── search(name, query_vectors, top_k, search_params) +│ +│ # Status (implements abstract) +├── row_count(name) -> int +├── get_index_progress(name) -> IndexProgress +│ +│ # Internal helpers +├── _get_collection(name) -> Collection # lazy pymilvus Collection cache +└── _build_index_params(index_type, metric_type, params) -> dict +``` + +### Schema + +Every collection uses a fixed two-field schema: + +| Field | Type | Notes | +|-------|------|-------| +| `id` | `INT64` | Primary key, not auto-generated | +| `vector` | `FLOAT_VECTOR` | Dimensionality set at creation | + +### Compaction + +Milvus is the only backend that overrides `compact()`. After batch +inserts, Milvus may have many small segments that slow down index +building. `compact()` calls `Collection.compact()` followed by +`Collection.wait_for_compaction_completed()` to merge segments before +the index build begins. + +### Index Progress + +`get_index_progress()` calls `pymilvus.utility.index_building_progress()` +which returns `total_rows`, `indexed_rows`, and `pending_index_rows`. +These feed into the base-class `wait_for_index()` progress logging with +percentage, rates, and ETA. + +### Search Parameter Handling + +The `search()` method accepts `search_params` in two formats: + +1. **Raw keys** (preferred from YAML configs): `{"ef": 128}` -- wrapped + automatically into the `{"metric_type": ..., "params": {...}}` structure + that `pymilvus` expects. +2. **pymilvus format**: `{"metric_type": "COSINE", "params": {"ef": 128}}` + -- passed through as-is. + +## Example YAML Config + +```yaml +backend: milvus +mode: both + +database: + host: 127.0.0.1 + port: 19530 + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + M: 64 + efConstruction: 200 + +search: + search_k: 10 + search_params: + ef: 128 + +workflow: + compact: true +``` + +## Files + +| File | Purpose | +|------|---------| +| `__init__.py` | `backend_descriptor()` -- registers the backend with supported indexes, metrics, and connection params | +| `backend.py` | `MilvusBackend` -- full implementation of `VectorDBBackend` | diff --git a/vdb_benchmark/vdbbench/benchmark/backends/milvus/__init__.py b/vdb_benchmark/vdbbench/benchmark/backends/milvus/__init__.py new file mode 100644 index 00000000..da6b53e9 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/milvus/__init__.py @@ -0,0 +1,144 @@ +"""Milvus backend package. + +Exposes :class:`MilvusBackend` and :func:`backend_descriptor` for +automatic registration by the backend registry. +""" + +from ..base import BackendDescriptor, IndexDescriptor, ParamDescriptor +from .backend import MilvusBackend + +__all__ = ["MilvusBackend", "backend_descriptor"] + + +def backend_descriptor() -> BackendDescriptor: + """Return the capability descriptor for the Milvus backend.""" + return BackendDescriptor( + name="milvus", + display_name="Milvus", + description=( + "Open-source vector database built for scalable similarity " + "search. Supports HNSW, DiskANN, AISAQ, and FLAT index types " + "with COSINE, L2, and IP distance metrics. Requires a running " + "Milvus server (standalone or cluster) and the pymilvus Python " + "package." + ), + backend_class=MilvusBackend, + supported_metrics=["COSINE", "L2", "IP"], + supported_indexes=[ + IndexDescriptor( + name="HNSW", + description=( + "Hierarchical Navigable Small World graph index. " + "Good general-purpose choice balancing recall and speed." + ), + build_params=[ + ParamDescriptor( + name="M", + description="Max number of connections per node.", + type="int", + default=16, + ), + ParamDescriptor( + name="efConstruction", + description="Search width during index construction.", + type="int", + default=200, + ), + ], + search_params=[ + ParamDescriptor( + name="ef", + description="Search width at query time (higher = better recall).", + type="int", + default=128, + ), + ], + ), + IndexDescriptor( + name="DISKANN", + description=( + "Microsoft DiskANN -- SSD-friendly graph index for " + "large-scale datasets that exceed RAM." + ), + build_params=[ + ParamDescriptor( + name="MaxDegree", + description="Maximum out-degree of each graph node.", + type="int", + default=64, + ), + ParamDescriptor( + name="SearchListSize", + description="Candidate-list size during index build.", + type="int", + default=200, + ), + ], + search_params=[ + ParamDescriptor( + name="search_list", + description="Candidate-list size at query time.", + type="int", + default=200, + ), + ], + ), + IndexDescriptor( + name="AISAQ", + description=( + "Approximate Inference with Scalar and Additive " + "Quantization -- a compressed index format." + ), + build_params=[ + ParamDescriptor( + name="inline_pq", + description="Product-quantization sub-vector count.", + type="int", + default=16, + ), + ParamDescriptor( + name="max_degree", + description="Maximum out-degree of each graph node.", + type="int", + default=32, + ), + ParamDescriptor( + name="search_list_size", + description="Candidate-list size during build.", + type="int", + default=100, + ), + ], + search_params=[], + ), + IndexDescriptor( + name="FLAT", + description=( + "Brute-force exact search (no indexing). " + "Perfect recall but O(n) per query." + ), + build_params=[], + search_params=[], + ), + ], + connection_params=[ + ParamDescriptor( + name="host", + description="Milvus server hostname or IP.", + type="str", + default="127.0.0.1", + ), + ParamDescriptor( + name="port", + description="Milvus gRPC port.", + type="str", + default="19530", + ), + ParamDescriptor( + name="max_message_length", + description="Max gRPC message size in bytes.", + type="int", + default=514_983_574, + ), + ], + ) diff --git a/vdb_benchmark/vdbbench/benchmark/backends/milvus/backend.py b/vdb_benchmark/vdbbench/benchmark/backends/milvus/backend.py new file mode 100644 index 00000000..f21a7aaf --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/milvus/backend.py @@ -0,0 +1,314 @@ +"""Milvus implementation of :class:`VectorDBBackend`. + +This wraps ``pymilvus`` behind the abstract backend interface so the +benchmark pipeline is completely database-agnostic. The implementation +mirrors the conventions used by the existing ``load_vdb.py`` script +(schema, index params, connection options). +""" + +from __future__ import annotations + +import logging +import time +from typing import Any, Dict, List, Optional + +import numpy as np +from pymilvus import ( + Collection, + CollectionSchema, + DataType, + FieldSchema, + connections, + utility, +) + +from ..base import CollectionInfo, IndexProgress, VectorDBBackend + +logger = logging.getLogger(__name__) + + +class MilvusBackend(VectorDBBackend): + """Concrete backend for Milvus / Zilliz Cloud.""" + + def __init__(self) -> None: + self._collections: Dict[str, Collection] = {} + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + def connect( + self, + host: str = "127.0.0.1", + port: str = "19530", + **kwargs, + ) -> None: + max_msg = kwargs.get("max_message_length", 514_983_574) + connections.connect( + "default", + host=host, + port=port, + max_receive_message_length=max_msg, + max_send_message_length=max_msg, + ) + logger.info("Connected to Milvus at %s:%s", host, port) + + def disconnect(self) -> None: + connections.disconnect("default") + self._collections.clear() + logger.info("Disconnected from Milvus") + + # ------------------------------------------------------------------ + # Collection helpers + # ------------------------------------------------------------------ + def _get_collection(self, name: str) -> Collection: + if name not in self._collections: + self._collections[name] = Collection(name=name) + return self._collections[name] + + @staticmethod + def _build_index_params( + index_type: str, + metric_type: str, + params: Optional[Dict[str, Any]], + ) -> Dict[str, Any]: + params = params or {} + ip: Dict[str, Any] = { + "index_type": index_type, + "metric_type": metric_type, + "params": {}, + } + if index_type == "HNSW": + ip["params"] = { + "M": params.get("M", 16), + "efConstruction": params.get("efConstruction", 200), + } + elif index_type == "DISKANN": + ip["params"] = { + "MaxDegree": params.get("MaxDegree", 64), + "SearchListSize": params.get("SearchListSize", 200), + } + elif index_type == "AISAQ": + ip["params"] = { + "inline_pq": params.get("inline_pq", 16), + "max_degree": params.get("max_degree", 32), + "search_list_size": params.get("search_list_size", 100), + } + elif index_type == "FLAT": + pass # no extra params + else: + ip["params"] = params + return ip + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "HNSW", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + if utility.has_collection(name): + if force: + Collection(name=name).drop() + logger.info("Dropped existing collection: %s", name) + else: + raise ValueError( + f"Collection '{name}' already exists. Use force=True to drop it." + ) + + fields = [ + FieldSchema(name="id", dtype=DataType.INT64, + is_primary=True, auto_id=False), + FieldSchema(name="vector", dtype=DataType.FLOAT_VECTOR, dim=dimension), + ] + schema = CollectionSchema(fields, description="Benchmark Collection") + col = Collection(name=name, schema=schema, num_shards=num_shards) + logger.info("Created collection '%s' (%s-d, %s shards)", name, f"{dimension:,}", num_shards) + + ip = self._build_index_params(index_type, metric_type, index_params) + col.create_index("vector", ip) + logger.info("Index created: %s / %s", index_type, metric_type) + + self._collections[name] = col + return CollectionInfo( + name=name, + dimension=dimension, + metric_type=metric_type, + index_type=index_type, + row_count=0, + extra={"index_params": ip}, + ) + + def collection_exists(self, name: str) -> bool: + return utility.has_collection(name) + + def drop_collection(self, name: str) -> None: + if utility.has_collection(name): + Collection(name=name).drop() + self._collections.pop(name, None) + logger.info("Dropped collection: %s", name) + + # ------------------------------------------------------------------ + # Data ingestion + # ------------------------------------------------------------------ + def insert_batch( + self, + name: str, + ids: np.ndarray, + vectors: np.ndarray, + ) -> int: + col = self._get_collection(name) + col.insert([ids.tolist(), vectors]) + return len(ids) + + def flush(self, name: str) -> None: + col = self._get_collection(name) + t0 = time.time() + col.flush() + logger.info("Flush completed in %.2f s", time.time() - t0) + + def compact(self, name: str) -> None: + """Trigger Milvus segment compaction and block until done.""" + col = self._get_collection(name) + logger.info("Triggering compaction for '%s' ...", name) + t0 = time.time() + col.compact() + col.wait_for_compaction_completed() + elapsed = time.time() - t0 + logger.info("Compaction completed in %.2f s", elapsed) + + # ------------------------------------------------------------------ + # Search + # ------------------------------------------------------------------ + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + col = self._get_collection(name) + col.load() + raw = search_params or {} + if "params" in raw: + # Already in pymilvus format (has metric_type + params wrapper) + sp = raw + else: + # Wrap raw keys into the structure pymilvus expects + sp = { + "metric_type": raw.get("metric_type", "COSINE"), + "params": {k: v for k, v in raw.items() + if k != "metric_type"}, + } + results = col.search( + data=query_vectors.tolist(), + anns_field="vector", + param=sp, + limit=top_k, + ) + return [[hit.id for hit in hits] for hits in results] + + # ------------------------------------------------------------------ + # Status / info + # ------------------------------------------------------------------ + def row_count(self, name: str) -> int: + col = self._get_collection(name) + col.flush() + return col.num_entities + + def get_index_progress(self, name: str) -> IndexProgress: + """Query Milvus ``index_building_progress`` and return a snapshot.""" + progress = utility.index_building_progress(name) + total = progress.get("total_rows", 0) + indexed = progress.get("indexed_rows", 0) + pending = progress.get("pending_index_rows", 0) + is_ready = total > 0 and indexed >= total and pending == 0 + return IndexProgress( + is_ready=is_ready, + total_rows=total, + indexed_rows=indexed, + pending_rows=pending, + ) + + # ------------------------------------------------------------------ + # Administration / introspection + # ------------------------------------------------------------------ + def list_collections(self) -> List[str]: + return utility.list_collections() + + def get_collection_info(self, name: str) -> Dict[str, Any]: + col = self._get_collection(name) + col.flush() + + # Extract schema fields + schema = [] + dimension = None + for field in col.schema.fields: + entry: Dict[str, Any] = { + "name": field.name, + "dtype": field.dtype.name if hasattr(field.dtype, "name") else str(field.dtype), + "is_primary": field.is_primary, + } + if field.params.get("dim"): + entry["dim"] = field.params["dim"] + dimension = field.params["dim"] + schema.append(entry) + + # Extract index info + index_type = None + metric_type = None + if col.indexes: + idx = col.indexes[0] + index_type = idx.params.get("index_type") + metric_type = idx.params.get("metric_type") + + return { + "name": name, + "row_count": col.num_entities, + "dimension": dimension, + "metric_type": metric_type, + "index_type": index_type, + "schema": schema, + "num_partitions": len(col.partitions), + "partitions": [p.name for p in col.partitions], + } + + def list_indexes(self, name: str) -> List[Dict[str, Any]]: + col = self._get_collection(name) + results: List[Dict[str, Any]] = [] + for idx in col.indexes: + results.append({ + "index_name": idx.field_name, + "field_name": idx.field_name, + "index_type": idx.params.get("index_type", "UNKNOWN"), + "metric_type": idx.params.get("metric_type", "UNKNOWN"), + "params": idx.params.get("params", {}), + }) + return results + + def drop_index(self, name: str, index_name: Optional[str] = None) -> None: + col = self._get_collection(name) + field = index_name or "vector" + col.drop_index(field_name=field) + logger.info("Dropped index on field '%s' from '%s'", field, name) + + def get_collection_stats(self, name: str) -> Dict[str, Any]: + col = self._get_collection(name) + col.flush() + prog = self.get_index_progress(name) + stats: Dict[str, Any] = { + "name": name, + "row_count": col.num_entities, + "index_ready": prog.is_ready, + "index_status": prog.status, + "indexed_rows": prog.indexed_rows, + "total_rows": prog.total_rows, + "pending_rows": prog.pending_rows, + "num_partitions": len(col.partitions), + } + return stats diff --git a/vdb_benchmark/vdbbench/benchmark/backends/pgvector/README.md b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/README.md new file mode 100644 index 00000000..f50c2a4a --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/README.md @@ -0,0 +1,182 @@ +# pgvector Backend + +Adapter for [pgvector](https://github.com/pgvector/pgvector) -- a PostgreSQL +extension for vector similarity search using standard SQL. + +## Requirements + +```bash +pip install psycopg2-binary pgvector +``` + +The target PostgreSQL server must have the `vector` extension installed: + +```sql +CREATE EXTENSION IF NOT EXISTS vector; +``` + +The backend runs this command automatically on `connect()`. + +## Connection + +| Parameter | Env Variable | Default | Description | +|-----------|-------------|---------|-------------| +| `host` | `PGVECTOR__HOST` | `127.0.0.1` | PostgreSQL server hostname or IP | +| `port` | `PGVECTOR__PORT` | `5432` | PostgreSQL server port | +| `dbname` | `PGVECTOR__DBNAME` | `postgres` | Database name | +| `user` | `PGVECTOR__USER` | `postgres` | Database user | +| `password` | `PGVECTOR__PASSWORD` | `""` | Database password | + +Connection uses `psycopg2.connect()` with `autocommit = True`. The +`pgvector.psycopg2.register_vector()` call enables transparent +NumPy-to-vector conversion. + +## Supported Indexes + +### HNSW + +Hierarchical Navigable Small World graph index. Built-in to +pgvector >= 0.5.0. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `M` (or `m`) | int | 16 | Max connections per node | +| `efConstruction` (or `ef_construction`) | int | 200 | Search width during index construction | + +| Search Parameter | Type | Default | Description | +|-----------------|------|---------|-------------| +| `ef_search` | int | 40 | Search width at query time. Set via `SET LOCAL hnsw.ef_search` | + +### IVFFLAT + +Inverted-file flat index. Partitions vectors into lists and searches a +subset. Lower build time than HNSW but typically lower recall at the same +speed. + +| Build Parameter | Type | Default | Description | +|----------------|------|---------|-------------| +| `lists` (or `nlist`) | int | 100 | Number of inverted-file lists (clusters) | + +| Search Parameter | Type | Default | Description | +|-----------------|------|---------|-------------| +| `probes` | int | 10 | Number of lists to probe at query time. Set via `SET LOCAL ivfflat.probes` | + +### FLAT + +No index -- exact brute-force sequential scan via PostgreSQL `ORDER BY`. +Perfect recall but O(n) per query. No build or search parameters. Selected +by setting `index_type: FLAT` (or `NONE`) in the config. + +## Supported Metrics + +| Metric | pgvector Operator | Operator Class | +|--------|-------------------|---------------| +| `COSINE` | `<=>` | `vector_cosine_ops` | +| `L2` | `<->` | `vector_l2_ops` | +| `IP` | `<#>` | `vector_ip_ops` | + +## Class Structure + +``` +PGVectorBackend(VectorDBBackend) +│ +│ # Lifecycle +├── connect(host, port, dbname, user, password, **kwargs) +├── disconnect() +│ +│ # Collection management +├── create_collection(name, dimension, metric_type, index_type, +│ index_params, num_shards, force) +├── collection_exists(name) -> bool +├── drop_collection(name) +│ +│ # Data ingestion +├── insert_batch(name, ids, vectors) -> int +├── flush(name) # no-op (autocommit) +│ +│ # Search +├── search(name, query_vectors, top_k, search_params) +│ +│ # Status (implements abstract) +├── row_count(name) -> int +├── get_index_progress(name) -> IndexProgress +│ +│ # Internal helpers +├── _cur() -> cursor # new cursor with connection check +├── _table(name) -> str # SQL-safe identifier quoting +├── _index_name(table, suffix) -> str # deterministic index name +└── _create_index(name, dim, metric, type, params) +``` + +### Schema + +Every table uses a fixed two-column schema: + +| Column | Type | Notes | +|--------|------|-------| +| `id` | `BIGINT PRIMARY KEY` | Not auto-generated | +| `vector` | `vector(dim)` | pgvector `vector` type with fixed dimensionality | + +### Synchronous Index Build + +Unlike Milvus, `CREATE INDEX` in PostgreSQL is **synchronous** -- the +call blocks until the index is fully built. As a result: + +- `get_index_progress()` simply checks `pg_indexes` for the table and + returns `IndexProgress(is_ready=True)` once an index exists. +- The base-class `wait_for_index()` typically completes on the first + poll since the index is already built by the time inserts finish. + +### Search Parameter Handling + +Search-time GUCs (`hnsw.ef_search`, `ivfflat.probes`) require a +transaction block. The `search()` method temporarily exits `autocommit` +mode, runs `SET LOCAL` inside a transaction, executes all queries, then +commits and restores `autocommit`. When no search-time parameters are +set, queries run directly without a transaction wrapper. + +### Flush + +`flush()` is a no-op because the connection runs in `autocommit = True` +mode -- every `INSERT` is committed immediately. + +## Example YAML Config + +```yaml +backend: pgvector +mode: both + +database: + host: 127.0.0.1 + port: 5432 + dbname: postgres + user: postgres + password: "" + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + m: 64 + ef_construction: 200 + +search: + search_k: 10 + search_params: + ef_search: 128 +``` + +## Files + +| File | Purpose | +|------|---------| +| `__init__.py` | `backend_descriptor()` -- registers the backend with supported indexes, metrics, and connection params | +| `backend.py` | `PGVectorBackend` -- full implementation of `VectorDBBackend` | diff --git a/vdb_benchmark/vdbbench/benchmark/backends/pgvector/__init__.py b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/__init__.py new file mode 100644 index 00000000..b759ab78 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/__init__.py @@ -0,0 +1,124 @@ +"""pgvector backend package. + +Exposes :class:`PGVectorBackend` and :func:`backend_descriptor` for +automatic registration by the backend registry. +""" + +from ..base import BackendDescriptor, IndexDescriptor, ParamDescriptor +from .backend import PGVectorBackend + +__all__ = ["PGVectorBackend", "backend_descriptor"] + + +def backend_descriptor() -> BackendDescriptor: + """Return the capability descriptor for the pgvector backend.""" + return BackendDescriptor( + name="pgvector", + display_name="pgvector (PostgreSQL)", + description=( + "PostgreSQL extension for vector similarity search. Uses " + "standard SQL with the pgvector extension for HNSW and IVFFlat " + "indexes. Supports COSINE, L2, and IP distance metrics. " + "Requires a PostgreSQL server with the vector extension " + "installed and the psycopg2-binary + pgvector Python packages." + ), + backend_class=PGVectorBackend, + supported_metrics=["COSINE", "L2", "IP"], + supported_indexes=[ + IndexDescriptor( + name="HNSW", + description=( + "Hierarchical Navigable Small World graph index. " + "Built-in to pgvector >= 0.5.0. Good general-purpose " + "choice balancing recall and speed." + ), + build_params=[ + ParamDescriptor( + name="M", + description="Max number of connections per node.", + type="int", + default=16, + ), + ParamDescriptor( + name="efConstruction", + description="Search width during index construction.", + type="int", + default=200, + ), + ], + search_params=[ + ParamDescriptor( + name="ef_search", + description="Search width at query time (higher = better recall).", + type="int", + default=40, + ), + ], + ), + IndexDescriptor( + name="IVFFLAT", + description=( + "Inverted-file flat index. Partitions vectors into " + "lists and searches a subset. Lower build time than " + "HNSW but typically lower recall at the same speed." + ), + build_params=[ + ParamDescriptor( + name="lists", + description="Number of inverted-file lists (clusters).", + type="int", + default=100, + ), + ], + search_params=[ + ParamDescriptor( + name="probes", + description="Number of lists to probe at query time.", + type="int", + default=10, + ), + ], + ), + IndexDescriptor( + name="FLAT", + description=( + "No index -- exact brute-force sequential scan. " + "Perfect recall but O(n) per query." + ), + build_params=[], + search_params=[], + ), + ], + connection_params=[ + ParamDescriptor( + name="host", + description="PostgreSQL server hostname or IP.", + type="str", + default="127.0.0.1", + ), + ParamDescriptor( + name="port", + description="PostgreSQL server port.", + type="str", + default="5432", + ), + ParamDescriptor( + name="dbname", + description="Database name to connect to.", + type="str", + default="postgres", + ), + ParamDescriptor( + name="user", + description="Database user.", + type="str", + default="postgres", + ), + ParamDescriptor( + name="password", + description="Database password.", + type="str", + default="", + ), + ], + ) diff --git a/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py new file mode 100644 index 00000000..c2c9d4b0 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py @@ -0,0 +1,439 @@ +"""pgvector (PostgreSQL) implementation of :class:`VectorDBBackend`. + +This wraps ``psycopg2`` and the ``pgvector`` extension behind the abstract +backend interface so the benchmark pipeline is completely database-agnostic. + +Requirements:: + + pip install psycopg2-binary pgvector + +The target PostgreSQL server must have the ``vector`` extension installed:: + + CREATE EXTENSION IF NOT EXISTS vector; +""" + +from __future__ import annotations + +import logging +from typing import Any, Dict, List, Optional + +import numpy as np + +from ..base import CollectionInfo, IndexProgress, VectorDBBackend + +logger = logging.getLogger(__name__) + +# Mapping from the generic metric names used by the benchmark framework +# to the pgvector operator classes required by each index type. +_METRIC_TO_HNSW_OPS: Dict[str, str] = { + "L2": "vector_l2_ops", + "COSINE": "vector_cosine_ops", + "IP": "vector_ip_ops", +} + +_METRIC_TO_IVFFLAT_OPS: Dict[str, str] = { + "L2": "vector_l2_ops", + "COSINE": "vector_cosine_ops", + "IP": "vector_ip_ops", +} + +# The SQL distance operator used at query time for each metric. +_METRIC_TO_OPERATOR: Dict[str, str] = { + "L2": "<->", + "COSINE": "<=>", + "IP": "<#>", +} + + +class PGVectorBackend(VectorDBBackend): + """Concrete backend for PostgreSQL + pgvector.""" + + def __init__(self) -> None: + self._conn = None # type: Any # psycopg2 connection + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + def connect( + self, + host: str = "127.0.0.1", + port: str = "5432", + dbname: str = "postgres", + user: str = "postgres", + password: str = "", + **kwargs, + ) -> None: + import psycopg2 + from pgvector.psycopg2 import register_vector + + self._conn = psycopg2.connect( + host=host, + port=port, + dbname=dbname, + user=user, + password=password, + ) + self._conn.autocommit = True + register_vector(self._conn) + + # Ensure the vector extension exists. + with self._conn.cursor() as cur: + cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + logger.info("Connected to PostgreSQL at %s:%s (db=%s)", host, port, dbname) + + def disconnect(self) -> None: + if self._conn and not self._conn.closed: + self._conn.close() + self._conn = None + logger.info("Disconnected from PostgreSQL") + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + def _cur(self): + """Return a new cursor, raising if not connected.""" + if self._conn is None or self._conn.closed: + raise RuntimeError("Not connected to PostgreSQL") + return self._conn.cursor() + + @staticmethod + def _table(name: str) -> str: + """Sanitize a collection name for use as a SQL identifier.""" + import psycopg2.extensions + return psycopg2.extensions.quote_ident(name) if hasattr( + psycopg2.extensions, "quote_ident" + ) else f'"{name}"' + + @staticmethod + def _index_name(table: str, suffix: str = "vec_idx") -> str: + return f"{table}_{suffix}" + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "HNSW", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + table = self._table(name) + idx_name = self._index_name(name) + + if self.collection_exists(name): + if force: + self.drop_collection(name) + else: + raise ValueError( + f"Table '{name}' already exists. Use force=True to drop it." + ) + + with self._cur() as cur: + cur.execute( + f"CREATE TABLE {table} (" + f" id BIGINT PRIMARY KEY," + f" vector vector({dimension})" + f")" + ) + logger.info("Created table '%s' (%s-d)", name, f"{dimension:,}") + + # Build the index (unless FLAT / no index requested). + index_params = index_params or {} + if index_type.upper() not in ("FLAT", "NONE"): + self._create_index( + name, dimension, metric_type, index_type, index_params + ) + + return CollectionInfo( + name=name, + dimension=dimension, + metric_type=metric_type, + index_type=index_type, + row_count=0, + extra={"index_params": index_params}, + ) + + def _create_index( + self, + name: str, + dimension: int, + metric_type: str, + index_type: str, + index_params: Dict[str, Any], + ) -> None: + table = self._table(name) + idx_name = self._index_name(name) + upper = index_type.upper() + + if upper == "HNSW": + ops = _METRIC_TO_HNSW_OPS.get(metric_type.upper(), "vector_cosine_ops") + m = index_params.get("M", index_params.get("m", 16)) + ef_construction = index_params.get( + "efConstruction", + index_params.get("ef_construction", 200), + ) + with_clause = f"(m = {m}, ef_construction = {ef_construction})" + sql = ( + f"CREATE INDEX {idx_name} ON {table} " + f"USING hnsw (vector {ops}) WITH {with_clause}" + ) + elif upper == "IVFFLAT": + ops = _METRIC_TO_IVFFLAT_OPS.get(metric_type.upper(), "vector_cosine_ops") + nlist = index_params.get("nlist", index_params.get("lists", 100)) + with_clause = f"(lists = {nlist})" + sql = ( + f"CREATE INDEX {idx_name} ON {table} " + f"USING ivfflat (vector {ops}) WITH {with_clause}" + ) + else: + logger.warning( + "Unknown index type '%s' for pgvector; skipping index creation.", + index_type, + ) + return + + logger.info("Creating index: %s", sql) + with self._cur() as cur: + cur.execute(sql) + logger.info("Index '%s' created (%s / %s)", idx_name, index_type, metric_type) + + def collection_exists(self, name: str) -> bool: + with self._cur() as cur: + cur.execute( + "SELECT EXISTS (" + " SELECT 1 FROM information_schema.tables" + " WHERE table_name = %s" + ")", + (name,), + ) + return cur.fetchone()[0] + + def drop_collection(self, name: str) -> None: + table = self._table(name) + with self._cur() as cur: + cur.execute(f"DROP TABLE IF EXISTS {table} CASCADE") + logger.info("Dropped table: %s", name) + + # ------------------------------------------------------------------ + # Data ingestion + # ------------------------------------------------------------------ + def insert_batch( + self, + name: str, + ids: np.ndarray, + vectors: np.ndarray, + ) -> int: + import psycopg2.extras + + table = self._table(name) + n = len(ids) + # Build a list of tuples for execute_values. + rows = [(int(ids[i]), vectors[i].tolist()) for i in range(n)] + with self._cur() as cur: + psycopg2.extras.execute_values( + cur, + f"INSERT INTO {table} (id, vector) VALUES %s " + f"ON CONFLICT (id) DO NOTHING", + rows, + template="(%s, %s::vector)", + page_size=1000, + ) + return n + + def flush(self, name: str) -> None: + # With autocommit = True every statement is already committed. + logger.info("Flush (no-op with autocommit) for table '%s'", name) + + # ------------------------------------------------------------------ + # Search + # ------------------------------------------------------------------ + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + table = self._table(name) + search_params = search_params or {} + + # Determine distance operator from metric_type in search_params. + metric = search_params.get("metric_type", "COSINE").upper() + op = _METRIC_TO_OPERATOR.get(metric, "<=>") + + # Apply runtime search params (e.g. ef_search for HNSW, probes for IVFFlat). + ef_search = search_params.get("ef_search", search_params.get("ef")) + probes = search_params.get("probes") + + results: List[List[int]] = [] + + # SET LOCAL requires a transaction block, so temporarily leave + # autocommit mode when we need to apply search-time GUCs. + need_txn = ef_search is not None or probes is not None + if need_txn: + self._conn.autocommit = False + + try: + with self._cur() as cur: + if ef_search is not None: + cur.execute( + f"SET LOCAL hnsw.ef_search = {int(ef_search)}" + ) + if probes is not None: + cur.execute( + f"SET LOCAL ivfflat.probes = {int(probes)}" + ) + + for qvec in query_vectors: + vec_literal = "[" + ",".join(str(float(v)) for v in qvec) + "]" + cur.execute( + f"SELECT id FROM {table} " + f"ORDER BY vector {op} %s::vector " + f"LIMIT %s", + (vec_literal, top_k), + ) + results.append([row[0] for row in cur.fetchall()]) + + if need_txn: + self._conn.commit() + except Exception: + if need_txn: + self._conn.rollback() + raise + finally: + if need_txn: + self._conn.autocommit = True + + return results + + # ------------------------------------------------------------------ + # Status / info + # ------------------------------------------------------------------ + def row_count(self, name: str) -> int: + table = self._table(name) + with self._cur() as cur: + cur.execute(f"SELECT COUNT(*) FROM {table}") + return cur.fetchone()[0] + + def get_index_progress(self, name: str) -> IndexProgress: + """In PostgreSQL ``CREATE INDEX`` is synchronous, so by the time + control returns the index is already built. This simply checks + whether any index exists on the table. + """ + with self._cur() as cur: + cur.execute( + "SELECT indexname FROM pg_indexes WHERE tablename = %s", + (name,), + ) + indexes = [row[0] for row in cur.fetchall()] + if indexes: + return IndexProgress( + is_ready=True, + status=", ".join(indexes), + ) + return IndexProgress(is_ready=False, status="waiting") + + # ------------------------------------------------------------------ + # Administration / introspection + # ------------------------------------------------------------------ + def list_collections(self) -> List[str]: + with self._cur() as cur: + cur.execute( + "SELECT table_name FROM information_schema.tables " + "WHERE table_schema = 'public' " + "AND table_type = 'BASE TABLE' " + "ORDER BY table_name" + ) + return [row[0] for row in cur.fetchall()] + + def get_collection_info(self, name: str) -> Dict[str, Any]: + table = self._table(name) + + # Columns + schema: List[Dict[str, Any]] = [] + dimension = None + with self._cur() as cur: + cur.execute( + "SELECT column_name, data_type, udt_name " + "FROM information_schema.columns " + "WHERE table_name = %s ORDER BY ordinal_position", + (name,), + ) + for col_name, data_type, udt_name in cur.fetchall(): + entry: Dict[str, Any] = { + "name": col_name, + "dtype": udt_name if udt_name != data_type else data_type, + } + if udt_name == "vector": + # Retrieve dimension from atttypmod + cur.execute( + "SELECT atttypmod FROM pg_attribute " + "WHERE attrelid = %s::regclass AND attname = %s", + (name, col_name), + ) + row = cur.fetchone() + if row and row[0] > 0: + dimension = row[0] + entry["dim"] = dimension + schema.append(entry) + + # Index info + indexes = self.list_indexes(name) + index_type = indexes[0]["index_type"] if indexes else None + + # Metric type from operator class + metric_type = None + if indexes: + ops = indexes[0].get("params", {}).get("opclass", "") + for metric, op_cls in _METRIC_TO_HNSW_OPS.items(): + if op_cls == ops: + metric_type = metric + break + + row_count = self.row_count(name) + + return { + "name": name, + "row_count": row_count, + "dimension": dimension, + "metric_type": metric_type, + "index_type": index_type, + "schema": schema, + } + + def list_indexes(self, name: str) -> List[Dict[str, Any]]: + results: List[Dict[str, Any]] = [] + with self._cur() as cur: + cur.execute( + "SELECT indexname, indexdef FROM pg_indexes " + "WHERE tablename = %s", + (name,), + ) + for idx_name, idx_def in cur.fetchall(): + # Skip primary-key indexes + if "_pkey" in idx_name: + continue + idx_type = "UNKNOWN" + idx_def_upper = idx_def.upper() + if "USING HNSW" in idx_def_upper: + idx_type = "HNSW" + elif "USING IVFFLAT" in idx_def_upper: + idx_type = "IVFFLAT" + results.append({ + "index_name": idx_name, + "index_type": idx_type, + "definition": idx_def, + "params": {}, + }) + return results + + def drop_index(self, name: str, index_name: Optional[str] = None) -> None: + if index_name is None: + index_name = self._index_name(name) + with self._cur() as cur: + cur.execute(f"DROP INDEX IF EXISTS {index_name}") + logger.info("Dropped index '%s' from table '%s'", index_name, name) diff --git a/vdb_benchmark/vdbbench/benchmark/collection_admin.py b/vdb_benchmark/vdbbench/benchmark/collection_admin.py new file mode 100755 index 00000000..52a9dd37 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/collection_admin.py @@ -0,0 +1,884 @@ +#!/usr/bin/env python3 +"""Backend-agnostic collection administration CLI. + +Provides subcommands for inspecting and managing collections across +any registered vector-database backend (Milvus, pgvector, Elasticsearch, +etc.) All heavy lifting delegates to the :class:`VectorDBBackend` +admin methods so behaviour is consistent across databases. + +Usage examples:: + + # Interactive mode -- discover backends, pick one, browse collections + collection-admin interactive + + # List all collections on a Milvus server + collection-admin --backend milvus list + + # Detailed info for one collection + collection-admin --backend milvus info my_collection + + # Show indexes + collection-admin --backend pgvector indexes my_collection + + # Collection statistics + collection-admin --backend elasticsearch stats my_collection + + # Drop a collection (requires --yes for safety) + collection-admin --backend milvus drop my_collection --yes + + # Drop an index + collection-admin --backend pgvector drop-index my_collection + +Connection parameters are sourced from environment variables using the +``{BACKEND}__{PARAM}`` convention (see ``_env.py``), from a ``.env`` +file, or from ``--param key=value`` CLI flags. +""" + +from __future__ import annotations + +import sys + +# ------------------------------------------------------------------ +# Direct-execution bootstrap (same pattern as run_benchmark.py) +# ------------------------------------------------------------------ +if __name__ == "__main__": + import importlib + import pathlib + + _this = pathlib.Path(__file__).resolve() + _pkg_root = str(_this.parent.parent.parent) + if _pkg_root not in sys.path: + sys.path.insert(0, _pkg_root) + + _mod = importlib.import_module("vdbbench.benchmark.collection_admin") + raise SystemExit(_mod.main()) + +# ------------------------------------------------------------------ +# Normal imports (only reached when loaded as a package member). +# ------------------------------------------------------------------ + +import argparse +import json +import logging +import os +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional + +from tabulate import tabulate as _tabulate + +from .backends import registry, get_backend +from .backends._env import load_env_file, env_for_backend +from .backends.base import BackendDescriptor, VectorDBBackend + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(name)s %(message)s", +) +logger = logging.getLogger(__name__) + + +# ===================================================================== +# Output formatting helpers +# ===================================================================== + +def _json_out(data: Any) -> None: + """Print *data* as indented JSON to stdout.""" + print(json.dumps(data, indent=2, default=str)) + + +def _table_out(rows: List[Dict[str, Any]], keys: Optional[List[str]] = None) -> None: + """Print rows as a simple aligned table.""" + if not rows: + print("(no results)") + return + + keys = keys or list(rows[0].keys()) + # Column widths + widths = {k: len(k) for k in keys} + for row in rows: + for k in keys: + widths[k] = max(widths[k], len(str(row.get(k, "")))) + + header = " ".join(k.ljust(widths[k]) for k in keys) + sep = " ".join("-" * widths[k] for k in keys) + print(header) + print(sep) + for row in rows: + print(" ".join(str(row.get(k, "")).ljust(widths[k]) for k in keys)) + + +# ===================================================================== +# Backend connection helper +# ===================================================================== + +def _connect_backend( + backend_name: str, + extra_params: Optional[Dict[str, str]] = None, +) -> VectorDBBackend: + """Instantiate, connect, and return a backend. + + Connection parameters come from (highest-precedence-first): + 1. ``--param key=value`` CLI flags (*extra_params*). + 2. Environment variables (``{BACKEND}__{PARAM}``). + 3. Defaults from the backend descriptor. + """ + load_env_file() + + desc = registry.get(backend_name) + if desc is None: + available = ", ".join(registry.names()) or "(none)" + print(f"Unknown backend '{backend_name}'. Available: {available}", + file=sys.stderr) + sys.exit(1) + + # Merge env + CLI overrides + conn = env_for_backend(backend_name, desc) + if extra_params: + conn.update(extra_params) + + backend = desc.backend_class() + backend.connect(**conn) + return backend + + +# ===================================================================== +# Non-interactive subcommand handlers +# ===================================================================== + +def _cmd_list(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``list`` -- show all collections.""" + names = backend.list_collections() + if args.json: + _json_out(names) + return + if not names: + print("(no collections found)") + return + for n in sorted(names): + print(n) + + +def _cmd_info(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``info`` -- detailed metadata for one collection.""" + info = backend.get_collection_info(args.collection) + if args.json: + _json_out(info) + return + + print(f"\nCollection: {info['name']}") + print(f" Rows: {info.get('row_count', '?'):,}") + print(f" Dimension: {info.get('dimension') or '?'}") + print(f" Metric: {info.get('metric_type') or '?'}") + print(f" Index type: {info.get('index_type') or '?'}") + + schema = info.get("schema", []) + if schema: + print("\n Schema:") + for fld in schema: + extras = [] + if fld.get("dim"): + extras.append(f"dim={fld['dim']}") + if fld.get("is_primary"): + extras.append("PK") + suffix = f" ({', '.join(extras)})" if extras else "" + print(f" - {fld['name']}: {fld.get('dtype', '?')}{suffix}") + + for key in ("num_partitions", "partitions"): + if key in info: + print(f" {key}: {info[key]}") + print() + + +def _cmd_indexes(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``indexes`` -- list indexes on a collection.""" + indexes = backend.list_indexes(args.collection) + if args.json: + _json_out(indexes) + return + if not indexes: + print(f"No indexes found on '{args.collection}'") + return + _table_out(indexes) + + +def _cmd_stats(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``stats`` -- operational statistics for a collection.""" + stats = backend.get_collection_stats(args.collection) + if args.json: + _json_out(stats) + return + for k, v in stats.items(): + label = k.replace("_", " ").title() + if isinstance(v, int) and v > 999: + print(f" {label}: {v:,}") + else: + print(f" {label}: {v}") + + +def _cmd_drop(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``drop`` -- drop a collection (destructive!).""" + name = args.collection + if not backend.collection_exists(name): + print(f"Collection '{name}' does not exist.", file=sys.stderr) + sys.exit(1) + + if not args.yes: + try: + answer = input(f"Really DROP collection '{name}'? (yes/[no]) > ").strip() + except (EOFError, KeyboardInterrupt): + answer = "" + if answer.lower() != "yes": + print("Aborted.") + return + + backend.drop_collection(name) + print(f"Dropped: {name}") + + +def _cmd_drop_index(backend: VectorDBBackend, args: argparse.Namespace) -> None: + """``drop-index`` -- drop an index from a collection.""" + name = args.collection + idx = getattr(args, "index_name", None) + + if not args.yes: + target = f"index '{idx}'" if idx else "the vector index" + try: + answer = input( + f"Really DROP {target} on '{name}'? (yes/[no]) > " + ).strip() + except (EOFError, KeyboardInterrupt): + answer = "" + if answer.lower() != "yes": + print("Aborted.") + return + + backend.drop_index(name, index_name=idx) + print(f"Dropped index on '{name}'") + + +# ===================================================================== +# Interactive mode -- backend discovery, health-check, menus +# ===================================================================== + +@dataclass +class BackendStatus: + """Result of probing one backend.""" + name: str + display_name: str + configured: bool = False + healthy: bool = False + error: str = "" + conn_params: Dict[str, Any] = field(default_factory=dict) + descriptor: Optional[BackendDescriptor] = None + + +def discover_backends(env_path: Optional[str] = None) -> List[BackendStatus]: + """Probe every active backend and return their status. + + For each active backend registered in the global registry: + + 1. Load connection params from ``.env`` / environment variables. + 2. If at least one connection parameter is configured, attempt + ``connect()`` followed by ``disconnect()`` as a health check. + 3. If no env vars are set, fall back to the defaults declared in the + backend descriptor and try to connect anyway -- but mark it as + *not explicitly configured*. + """ + load_env_file(env_path) + + results: List[BackendStatus] = [] + for desc in registry.list_backends(): + status = BackendStatus( + name=desc.name, + display_name=desc.display_name, + descriptor=desc, + ) + + # Gather connection params from env + env_params = env_for_backend(desc.name, desc) + status.configured = bool(env_params) + + # Build full param set: defaults + env overrides + conn: Dict[str, Any] = {} + for p in desc.connection_params: + if p.default is not None: + conn[p.name] = p.default + conn.update(env_params) + status.conn_params = conn + + # Attempt ping + try: + backend = desc.backend_class() + backend.connect(**conn) + backend.disconnect() + status.healthy = True + except Exception as exc: + status.healthy = False + status.error = str(exc) + + results.append(status) + + return results + + +def _sep(text: str) -> str: + """Return a ``─`` line matching the widest line in *text*.""" + width = max((len(l) for l in text.splitlines()), default=0) + return "─" * width + + +def pick_backend(statuses: List[BackendStatus]) -> Optional[BackendStatus]: + """Display a table of backends and let the user choose one. + + Only healthy backends are selectable. Returns ``None`` if the user + cancels or no healthy backends exist. + """ + headers = ["Idx", "Backend", "Configured", "Status", "Details"] + rows = [] + for i, s in enumerate(statuses): + configured = "Yes" if s.configured else "defaults" + if s.healthy: + status_str = "Healthy" + detail = ", ".join(f"{k}={v}" for k, v in s.conn_params.items() + if v is not None and k != "password") + else: + status_str = "Unreachable" + detail = s.error[:60] if s.error else "" + rows.append([i, s.display_name, configured, status_str, detail]) + + table = _tabulate(rows, headers=headers, tablefmt="github") + sep = _sep(table) + print(f"\n{sep}") + print(table) + print(sep) + + healthy_ids = [i for i, s in enumerate(statuses) if s.healthy] + if not healthy_ids: + print("\nNo healthy backends found. Check your .env configuration.") + return None + + print(f"\nHealthy backends: {', '.join(str(i) for i in healthy_ids)}") + while True: + try: + choice = input("Select backend idx (or q to quit) > ").strip() + except (EOFError, KeyboardInterrupt): + return None + if choice.lower() == "q": + return None + try: + idx = int(choice) + except ValueError: + print(f"Invalid input '{choice}'. Enter a backend idx or q to quit.") + continue + if idx < 0 or idx >= len(statuses): + print(f"Index {idx} out of range. Select an idx between 0 and {len(statuses) - 1}.") + continue + if not statuses[idx].healthy: + print(f"Backend '{statuses[idx].display_name}' is not healthy. Select a healthy idx.") + continue + return statuses[idx] + + +def _connect_from_status(status: BackendStatus) -> VectorDBBackend: + """Instantiate and connect a backend from its discovered status.""" + backend = status.descriptor.backend_class() + backend.connect(**status.conn_params) + return backend + + +def pick_collection( + backend: VectorDBBackend, + backend_name: str, +) -> Optional[str]: + """List collections on the backend and let the user choose one. + + Returns the collection *name* or ``None`` if cancelled. + """ + try: + names = backend.list_collections() + except Exception as exc: + print(f"Failed to list collections: {exc}") + return None + + if not names: + print(f"\nNo collections found on '{backend_name}'.") + return None + + headers = ["Idx", "Collection", "Rows", "Dim", "Index", "Metric"] + rows = [] + for i, name in enumerate(sorted(names)): + try: + info = backend.get_collection_info(name) + row_count = (f"{info.get('row_count', '?'):,}" + if isinstance(info.get('row_count'), int) else "?") + dim = info.get("dimension") or "?" + idx_type = info.get("index_type") or "?" + metric = info.get("metric_type") or "?" + except Exception: + row_count = "?" + dim = "?" + idx_type = "?" + metric = "?" + rows.append([i, name, row_count, dim, idx_type, metric]) + + table = _tabulate(rows, headers=headers, tablefmt="github") + sep = _sep(table) + print(f"\n{sep}") + print(table) + print(sep) + + while True: + try: + choice = input("\nSelect collection idx (or b=back, q=quit) > ").strip() + except (EOFError, KeyboardInterrupt): + return None + if choice.lower() == "b": + return None + if choice.lower() == "q": + print("Bye.") + sys.exit(0) + try: + idx = int(choice) + except ValueError: + print(f"Invalid input '{choice}'. Enter a collection idx, b, or q.") + continue + if idx < 0 or idx >= len(rows): + print(f"Index {idx} out of range. Select an idx between 0 and {len(rows) - 1}.") + continue + return rows[idx][1] # collection name + + +# ── Interactive operation helpers ────────────────────────────────── + +def _iop_info(backend: VectorDBBackend, collection: str) -> None: + """Display detailed collection info.""" + try: + info = backend.get_collection_info(collection) + except Exception as exc: + print(f"Failed to get info: {exc}") + return + + print(f"\n{'='*70}") + print(f"Collection: {info['name']}") + print(f"{'='*70}") + row_count = info.get("row_count", "?") + if isinstance(row_count, int): + print(f"Rows: {row_count:,}") + else: + print(f"Rows: {row_count}") + print(f"Dimension: {info.get('dimension') or '?'}") + print(f"Metric: {info.get('metric_type') or '?'}") + print(f"Index type: {info.get('index_type') or '?'}") + + schema = info.get("schema", []) + if schema: + print("\nSchema:") + for fld in schema: + extras = [] + if fld.get("dim"): + extras.append(f"dim={fld['dim']}") + if fld.get("is_primary"): + extras.append("PK") + suffix = f" ({', '.join(extras)})" if extras else "" + print(f" - {fld['name']}: {fld.get('dtype', '?')}{suffix}") + + if "num_partitions" in info: + print(f"\nPartitions: {info['num_partitions']}") + for p in info.get("partitions", []): + print(f" - {p}") + print(f"{'='*70}\n") + + +def _iop_stats(backend: VectorDBBackend, collection: str) -> None: + """Display operational statistics.""" + try: + stats = backend.get_collection_stats(collection) + except Exception as exc: + print(f"Failed to get stats: {exc}") + return + + print(f"\nStats for '{collection}':") + for k, v in stats.items(): + label = k.replace("_", " ").title() + if isinstance(v, int) and v > 999: + print(f" {label}: {v:,}") + else: + print(f" {label}: {v}") + print() + + +def _iop_indexes(backend: VectorDBBackend, collection: str) -> None: + """List indexes on a collection.""" + try: + indexes = backend.list_indexes(collection) + except Exception as exc: + print(f"Failed to list indexes: {exc}") + return + + if not indexes: + print(f"No indexes on '{collection}'.") + return + + print(f"\nIndexes on '{collection}':") + print(_tabulate( + [{k: v for k, v in idx.items()} for idx in indexes], + headers="keys", + tablefmt="github", + )) + print() + + +def _iop_compact(backend: VectorDBBackend, collection: str) -> None: + """Trigger compaction (if supported).""" + try: + print(f"Starting compaction on '{collection}'...") + backend.compact(collection) + print("Compaction completed.") + except NotImplementedError: + print("Compaction is not supported by this backend.") + except Exception as exc: + print(f"Compact failed: {exc}") + + +def _iop_drop_index(backend: VectorDBBackend, collection: str) -> None: + """Drop the vector index from a collection.""" + try: + confirm = input( + f"Really DROP the index on '{collection}'? (yes/[no]) > " + ).strip() + except (EOFError, KeyboardInterrupt): + confirm = "" + if confirm.lower() != "yes": + print("Aborted.") + return + + try: + backend.drop_index(collection) + print(f"Index dropped on '{collection}'.") + except NotImplementedError: + print("drop_index is not supported by this backend.") + except Exception as exc: + print(f"Drop index failed: {exc}") + + +def _iop_delete(backend: VectorDBBackend, collection: str) -> None: + """Drop (delete) a collection entirely.""" + try: + confirm = input( + f"Really DROP collection '{collection}'? " + "This is irreversible. (yes/[no]) > " + ).strip() + except (EOFError, KeyboardInterrupt): + confirm = "" + if confirm.lower() != "yes": + print("Aborted; collection kept.") + return + + try: + backend.drop_collection(collection) + print(f"Collection '{collection}' dropped.") + except Exception as exc: + print(f"Delete failed: {exc}") + + +_INTERACTIVE_OPS = { + "i": ("info", "Detailed collection info", _iop_info), + "s": ("stats", "Operational statistics", _iop_stats), + "x": ("indexes", "List indexes", _iop_indexes), + "c": ("compact", "Trigger compaction", _iop_compact), + "di": ("drop-index", "Drop the vector index", _iop_drop_index), + "d": ("delete", "Drop the collection", _iop_delete), + "b": ("back", "Back to collection list", None), + "q": ("quit", "Exit", None), +} + + +def operations_menu( + backend: VectorDBBackend, + collection: str, + backend_name: str, +) -> bool: + """Run the operations loop for a single collection. + + Returns ``True`` to go back to the collection picker, + ``False`` to exit. + """ + while True: + header = f" [{backend_name}] Collection: '{collection}'" + cmd_lines = [f" {key:<4} {name:<12} {desc}" + for key, (name, desc, _) in _INTERACTIVE_OPS.items()] + body = "\n".join([header, " Available commands:"] + cmd_lines) + sep = _sep(body) + print(f"\n{sep}") + print(body) + print(sep) + + try: + choice = input("Enter command > ").strip().lower() + except (EOFError, KeyboardInterrupt): + return False + + if choice == "q": + print("Bye.") + sys.exit(0) + + if choice == "b": + return True + + entry = _INTERACTIVE_OPS.get(choice) + if entry is None: + print(f"Unknown command '{choice}'. Enter one of: " + f"{', '.join(_INTERACTIVE_OPS.keys())}") + continue + + _, _, handler = entry + if handler is not None: + handler(backend, collection) + + # If the collection was deleted, return to the picker + if choice == "d": + return True + + +def _cmd_interactive(args: argparse.Namespace) -> int: + """``interactive`` -- menu-driven backend and collection manager.""" + env_path = getattr(args, "env_file", None) + + print("Discovering backends...") + statuses = discover_backends(env_path=env_path) + + if not statuses: + print("No backends registered. Is the benchmark package installed?") + return 1 + + backend: Optional[VectorDBBackend] = None + current_status: Optional[BackendStatus] = None + + while True: + # ── backend picker ──────────────────────────────────────── + if backend is not None: + print(f"\nCurrently connected to: {current_status.display_name}") + try: + switch = input("Switch backend? (y/[n]) > ").strip().lower() + except (EOFError, KeyboardInterrupt): + break + if switch == "y": + try: + backend.disconnect() + except Exception: + pass + backend = None + + if backend is None: + chosen = pick_backend(statuses) + if chosen is None: + print("Bye.") + break + try: + backend = _connect_from_status(chosen) + current_status = chosen + print(f"\nConnected to {chosen.display_name}.") + except Exception as exc: + print(f"Connection failed: {exc}") + continue + + # ── collection picker ───────────────────────────────────── + col_name = pick_collection(backend, current_status.display_name) + if col_name is None: + try: + backend.disconnect() + except Exception: + pass + backend = None + continue + + # ── operations menu ─────────────────────────────────────── + go_back = operations_menu(backend, col_name, current_status.display_name) + if not go_back: + break + + # Cleanup + if backend is not None: + try: + backend.disconnect() + except Exception: + pass + + return 0 + + +# ===================================================================== +# Argument parser +# ===================================================================== + +_EPILOG = """\ +concepts: + collection The data container that holds vectors and their metadata + (IDs, dimensions, schema). Mapped to a Milvus Collection, + a PostgreSQL table (pgvector), or an Elasticsearch index. + Dropping a collection permanently destroys all stored data. + + index A search-acceleration structure (e.g. HNSW, IVF_FLAT, + DISKANN) built on a collection's vector field. Enables + fast approximate nearest-neighbor (ANN) queries. Created + automatically with the collection. Dropping an index + removes only the search structure -- the underlying data + remains intact and can be re-indexed. +""" + + +def _build_parser() -> argparse.ArgumentParser: + """Build the argparse parser with subcommands.""" + parser = argparse.ArgumentParser( + prog="collection_admin", + description="Backend-agnostic vector-DB collection administration.", + epilog=_EPILOG, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--backend", "-b", + default=None, + help="Backend name (e.g. milvus, pgvector, elasticsearch). " + "Required for non-interactive commands.", + ) + parser.add_argument( + "--param", "-p", + action="append", + default=[], + metavar="KEY=VALUE", + help="Extra connection parameter (repeatable).", + ) + parser.add_argument( + "--json", "-j", + action="store_true", + default=False, + help="Output results as JSON.", + ) + + sub = parser.add_subparsers(dest="command") + + # -- interactive -- + p_ia = sub.add_parser( + "interactive", + help="Menu-driven interactive mode: discover backends, browse " + "collections, run operations.", + ) + p_ia.add_argument( + "--env-file", + default=None, + help="Path to .env file (default: auto-detect).", + ) + + # -- list -- + sub.add_parser("list", help="List all collections on the server.") + + # -- info -- + p_info = sub.add_parser("info", help="Show detailed collection metadata.") + p_info.add_argument("collection", help="Collection name.") + + # -- indexes -- + p_idx = sub.add_parser("indexes", help="List indexes on a collection.") + p_idx.add_argument("collection", help="Collection name.") + + # -- stats -- + p_stats = sub.add_parser("stats", help="Show collection statistics.") + p_stats.add_argument("collection", help="Collection name.") + + # -- drop -- + p_drop = sub.add_parser( + "drop", + help="Drop a collection -- permanently deletes all data and indexes.", + ) + p_drop.add_argument("collection", help="Collection name.") + p_drop.add_argument( + "--yes", "-y", + action="store_true", + default=False, + help="Skip confirmation prompt.", + ) + + # -- drop-index -- + p_di = sub.add_parser( + "drop-index", + help="Drop an index from a collection -- data is kept and can be re-indexed.", + ) + p_di.add_argument("collection", help="Collection name.") + p_di.add_argument( + "--index-name", "-i", + default=None, + help="Specific index to drop (default: primary vector index).", + ) + p_di.add_argument( + "--yes", "-y", + action="store_true", + default=False, + help="Skip confirmation prompt.", + ) + + return parser + + +def _parse_params(raw: List[str]) -> Dict[str, str]: + """Parse ``--param KEY=VALUE`` arguments into a dict.""" + result: Dict[str, str] = {} + for item in raw: + if "=" not in item: + print(f"Invalid --param format (expected KEY=VALUE): {item}", + file=sys.stderr) + sys.exit(1) + key, _, value = item.partition("=") + result[key.strip()] = value.strip() + return result + + +# ===================================================================== +# Main entry point +# ===================================================================== + +_DISPATCH = { + "list": _cmd_list, + "info": _cmd_info, + "indexes": _cmd_indexes, + "stats": _cmd_stats, + "drop": _cmd_drop, + "drop-index": _cmd_drop_index, +} + + +def main(argv: Optional[List[str]] = None) -> int: + """Parse arguments, connect to the backend, and dispatch.""" + parser = _build_parser() + args = parser.parse_args(argv) + + # Default to interactive when no subcommand given + if not args.command: + args.command = "interactive" + + # ── Interactive mode (no --backend required) ────────────────── + if args.command == "interactive": + return _cmd_interactive(args) + + # ── Non-interactive commands require --backend ──────────────── + if not args.backend: + parser.error("--backend/-b is required for non-interactive commands.") + + extra = _parse_params(args.param) + backend = _connect_backend(args.backend, extra) + + try: + handler = _DISPATCH[args.command] + handler(backend, args) + except NotImplementedError as exc: + print(f"Not supported: {exc}", file=sys.stderr) + return 1 + except Exception as exc: + logger.error("Error: %s", exc, exc_info=True) + return 1 + finally: + backend.disconnect() + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/vdb_benchmark/vdbbench/benchmark/configs/1m_diskann.yaml b/vdb_benchmark/vdbbench/benchmark/configs/1m_diskann.yaml new file mode 100644 index 00000000..fbe3db27 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/configs/1m_diskann.yaml @@ -0,0 +1,45 @@ +# --------------------------------------------------------------- +# 1M-vector DiskANN benchmark (Milvus, producer-consumer pipeline) +# --------------------------------------------------------------- +backend: milvus +mode: both + +database: + host: 127.0.0.1 + port: 19530 + +dataset: + collection_name: bench_1m_diskann + num_vectors: 1_000_000 + dimension: 1536 + distribution: uniform + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +query: + num_query_vectors: 10_000 + query_seed: 99 + +ground_truth: + truth_k: 100 + +index: + index_type: DISKANN + metric_type: COSINE + index_params: + MaxDegree: 64 + SearchListSize: 200 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 1 + search_batch_size: 1 + search_params: + search_list: 128 + +workflow: + force: false + compact: true + monitor_interval: 5 diff --git a/vdb_benchmark/vdbbench/benchmark/configs/1m_hnsw.yaml b/vdb_benchmark/vdbbench/benchmark/configs/1m_hnsw.yaml new file mode 100644 index 00000000..24d9ea6e --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/configs/1m_hnsw.yaml @@ -0,0 +1,45 @@ +# --------------------------------------------------------------- +# 1M-vector HNSW benchmark (Milvus, producer-consumer pipeline) +# --------------------------------------------------------------- +backend: milvus +mode: both + +database: + host: 127.0.0.1 + port: 19530 + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + distribution: uniform + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +query: + num_query_vectors: 10_000 + query_seed: 99 + +ground_truth: + truth_k: 100 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + M: 64 + efConstruction: 200 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 1 + search_batch_size: 1 + search_params: + ef: 128 + +workflow: + force: false + compact: true + monitor_interval: 5 diff --git a/vdb_benchmark/vdbbench/benchmark/configs/elasticsearch_1m_hnsw.yaml b/vdb_benchmark/vdbbench/benchmark/configs/elasticsearch_1m_hnsw.yaml new file mode 100644 index 00000000..6568ebed --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/configs/elasticsearch_1m_hnsw.yaml @@ -0,0 +1,46 @@ +# --------------------------------------------------------------- +# 1M-vector HNSW benchmark (Elasticsearch) +# --------------------------------------------------------------- +backend: elasticsearch +mode: both + +database: + host: http://localhost:9200 + # api_key: "" # set via ELASTICSEARCH__API_KEY env var + # cloud_id: "" # set via ELASTICSEARCH__CLOUD_ID env var + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + distribution: uniform + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +query: + num_query_vectors: 10_000 + query_seed: 99 + +ground_truth: + truth_k: 100 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + m: 16 + ef_construction: 200 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 1 + search_batch_size: 1 + search_params: + num_candidates: 128 + +workflow: + force: false + compact: true + monitor_interval: 5 diff --git a/vdb_benchmark/vdbbench/benchmark/configs/pgvector_1m_hnsw.yaml b/vdb_benchmark/vdbbench/benchmark/configs/pgvector_1m_hnsw.yaml new file mode 100644 index 00000000..cc3095ba --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/configs/pgvector_1m_hnsw.yaml @@ -0,0 +1,48 @@ +# --------------------------------------------------------------- +# 1M-vector HNSW benchmark (pgvector / PostgreSQL) +# --------------------------------------------------------------- +backend: pgvector +mode: both + +database: + host: 127.0.0.1 + port: 5432 + dbname: postgres + user: postgres + password: "" + +dataset: + collection_name: bench_1m_hnsw + num_vectors: 1_000_000 + dimension: 1536 + distribution: uniform + block_size: 100_000 + batch_size: 10_000 + seed: 42 + +query: + num_query_vectors: 10_000 + query_seed: 99 + +ground_truth: + truth_k: 100 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + m: 64 + ef_construction: 200 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 1 + search_batch_size: 1 + search_params: + ef_search: 128 + +workflow: + force: false + compact: true + monitor_interval: 5 diff --git a/vdb_benchmark/vdbbench/benchmark/generator.py b/vdb_benchmark/vdbbench/benchmark/generator.py new file mode 100644 index 00000000..b9e5fe72 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/generator.py @@ -0,0 +1,169 @@ +"""Vector generator -- the *producer* side of the pipeline. + +Generates random vectors in configurable blocks and pushes them onto a +:class:`queue.Queue`. Each block is a :class:`VectorBlock` containing: + +* ``ids`` -- int64 primary keys (globally unique, monotonically increasing) +* ``vectors`` -- float32 array of shape ``(block_size, dimension)`` + +The generator also produces a separate set of **query vectors** that are +held aside for benchmarking and ground-truth computation. + +Supported distributions: ``uniform``, ``normal``. +All vectors are L2-normalized so that COSINE distance is meaningful. +""" + +from __future__ import annotations + +import logging +import queue +import threading +from dataclasses import dataclass +from typing import Optional + +import numpy as np + +logger = logging.getLogger(__name__) + +# Sentinel pushed onto the queue after the last block. +_DONE = None + + +@dataclass +class VectorBlock: + """A batch of vectors ready for consumption.""" + ids: np.ndarray # shape (n,), dtype int64 + vectors: np.ndarray # shape (n, dim), dtype float32 + block_index: int # ordinal of this block (0-based) + + +def _generate_block( + num_vectors: int, + dimension: int, + distribution: str, + rng: np.random.RandomState, +) -> np.ndarray: + """Return a normalized float32 array of shape ``(num_vectors, dimension)``.""" + if distribution == "normal": + vectors = rng.normal(0, 1, (num_vectors, dimension)).astype(np.float32) + else: # uniform (default) + vectors = rng.random((num_vectors, dimension)).astype(np.float32) + + norms = np.linalg.norm(vectors, axis=1, keepdims=True) + norms[norms == 0] = 1.0 # avoid division by zero + vectors /= norms + return vectors + + +def generate_query_vectors( + num_queries: int, + dimension: int, + distribution: str = "uniform", + seed: int = 99, +) -> np.ndarray: + """Deterministically generate a set of query vectors. + + Uses a *separate* seed from the database vectors so that the query + set is independent of the dataset. + + Returns + ------- + np.ndarray + Shape ``(num_queries, dimension)``, dtype float32, L2-normalized. + """ + rng = np.random.RandomState(seed) + return _generate_block(num_queries, dimension, distribution, rng) + + +class VectorGenerator: + """Producer that feeds vector blocks into a queue. + + Parameters + ---------- + total_vectors : int + How many database vectors to produce in total. + dimension : int + Dimensionality of each vector. + block_size : int + Vectors per block (the last block may be smaller). + distribution : str + ``"uniform"`` or ``"normal"``. + seed : int + Random seed for reproducibility. + max_queue_depth : int + Backpressure limit -- producer blocks when queue is this full. + """ + + def __init__( + self, + total_vectors: int, + dimension: int, + block_size: int = 100_000, + distribution: str = "uniform", + seed: int = 42, + max_queue_depth: int = 4, + ) -> None: + self.total_vectors = total_vectors + self.dimension = dimension + self.block_size = block_size + self.distribution = distribution + self.seed = seed + self.queue: queue.Queue[Optional[VectorBlock]] = queue.Queue( + maxsize=max_queue_depth + ) + self._thread: Optional[threading.Thread] = None + self._error: Optional[Exception] = None + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + def start(self) -> None: + """Spawn the producer thread. Non-blocking.""" + self._thread = threading.Thread(target=self._run, daemon=True) + self._thread.start() + + def join(self) -> None: + """Wait for the producer to finish. Raises if it errored.""" + if self._thread is not None: + self._thread.join() + if self._error is not None: + raise self._error + + @property + def num_blocks(self) -> int: + return (self.total_vectors + self.block_size - 1) // self.block_size + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + def _run(self) -> None: + try: + rng = np.random.RandomState(self.seed) + remaining = self.total_vectors + block_idx = 0 + next_id = 0 + + while remaining > 0: + n = min(self.block_size, remaining) + vectors = _generate_block(n, self.dimension, self.distribution, rng) + ids = np.arange(next_id, next_id + n, dtype=np.int64) + + block = VectorBlock( + ids=ids, vectors=vectors, block_index=block_idx + ) + self.queue.put(block) + logger.info( + "Producer: block %d (%s vectors, ids %s..%s)", + block_idx, f"{n:,}", f"{next_id:,}", f"{next_id + n - 1:,}", + ) + + next_id += n + remaining -= n + block_idx += 1 + + # Sentinel signals consumers that production is done. + self.queue.put(_DONE) + except Exception as exc: + logger.exception("Producer thread failed") + self._error = exc + self.queue.put(_DONE) diff --git a/vdb_benchmark/vdbbench/benchmark/ground_truth.py b/vdb_benchmark/vdbbench/benchmark/ground_truth.py new file mode 100644 index 00000000..66f86f45 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/ground_truth.py @@ -0,0 +1,241 @@ +"""Ground-truth builder -- incremental nearest-neighbor tracking. + +As each :class:`VectorBlock` arrives from the producer, this module +computes the distances between the **query vectors** and the new block, +then merges those distances into a running top-K table. + +At the end of ingestion the result is a truth table:: + + query_index -> [id_1, id_2, ..., id_K] + +where *id_1* is the nearest database vector to that query, *id_2* the +second-nearest, etc. This is computed entirely in NumPy using +brute-force inner product / cosine distance -- no database calls needed. + +The approach is streaming-friendly: memory usage is O(num_queries * K) +for the truth table plus O(num_queries * block_size) transiently per +block. For 10 000 queries, K=100, and block_size=100 000 this is very +manageable. + +Performance notes +----------------- +* The dominant cost is the matrix multiply (BLAS ``sgemm``), which is + O(Q * B * D) per block and cannot be reduced without approximate + methods. +* Because all vectors are L2-normalized, inner-product ranking is + equivalent to L2 and cosine ranking. We therefore use a single + "higher is better" code path for every metric, which also avoids + allocating a second (Q, B) distance matrix for L2. +* The matmul is **sub-blocked** along the database-vector dimension so + that the transient similarity matrix stays within a configurable + memory budget (default 512 MiB) instead of growing to Q * B * 4 bytes + (3.8 GiB at the default config). Because the smaller tiles fit in L3 + cache, this is also marginally faster than the single large ``sgemm``. +* After the first sub-block, a per-query **threshold filter** is applied + before the expensive ``argpartition``: ``flatnonzero(row > thresh)`` + is a simple comparison+gather (~30 us / 100 K floats) vs introselect + (~230 us). Only the few candidates that beat the current worst in the + top-K need to be partially sorted, giving a ~4x merge speedup on + subsequent sub-blocks. +* The final merge (running top-K + block top-K -> new top-K) is a + single vectorized ``argpartition`` over the small ``(Q, 2K)`` matrix. +""" + +from __future__ import annotations + +import logging +from typing import Optional + +import numpy as np + +from .generator import VectorBlock + +logger = logging.getLogger(__name__) + +# Target memory budget for the transient (Q, sub_B) similarity matrix. +# The actual sub-block size is: sub_B = budget // (num_queries * 4). +# 512 MiB ⇒ sub_B ≈ 13 000 for Q = 10 000. +_SIMS_MEM_BUDGET: int = 512 << 20 # 512 MiB + + +class GroundTruthBuilder: + """Incrementally build a nearest-neighbor truth table. + + Parameters + ---------- + query_vectors : np.ndarray + Shape ``(num_queries, dimension)``, dtype float32, L2-normalized. + k : int + Number of nearest neighbors to track per query. + metric : str + ``"COSINE"`` (or ``"IP"``). Both reduce to inner-product on + L2-normalized vectors. ``"L2"`` is also supported. + """ + + def __init__( + self, + query_vectors: np.ndarray, + k: int = 100, + metric: str = "COSINE", + ) -> None: + self.query_vectors = np.ascontiguousarray(query_vectors, dtype=np.float32) + self.num_queries, self.dimension = self.query_vectors.shape + self.k = k + self.metric = metric.upper() + + # Running top-K state -- always "higher is better" internally. + # + # For L2-normalized vectors the inner product (IP) preserves the + # ranking of all three supported metrics: + # COSINE = IP (identical by definition for unit vecs) + # L2^2 = 2 - 2 * IP (monotone decreasing transform of IP) + # + # So we store IP similarities and use a single merge path. + self._top_ids: np.ndarray = np.full( + (self.num_queries, k), -1, dtype=np.int64 + ) + self._top_dist: np.ndarray = np.full( + (self.num_queries, k), -np.inf, dtype=np.float32 + ) + + self._blocks_processed = 0 + self._topk_initialized = False + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + def update(self, block: VectorBlock) -> None: + """Incorporate a new block of database vectors. + + For each query vector *q*, compute the similarity to every + vector in *block*, then merge the best results into the running + top-K. The matmul is sub-blocked along the database-vector axis + to keep the transient similarity matrix within + ``_SIMS_MEM_BUDGET``. + """ + db_vecs = np.ascontiguousarray(block.vectors, dtype=np.float32) + db_ids = block.ids # shape (n,) + B = len(db_ids) + + # Sub-block size: keep the (Q, sub_b) similarity matrix under budget. + sub_b = max(1, _SIMS_MEM_BUDGET // (self.num_queries * 4)) + + for sb in range(0, B, sub_b): + se = min(sb + sub_b, B) + # Inner product: higher = more similar = closer for all + # metrics on L2-normalized vectors. + sub_sims = self.query_vectors @ db_vecs[sb:se].T # (Q, se-sb) + sub_ids = db_ids[sb:se] + + if not self._topk_initialized: + self._merge_first_block(sub_sims, sub_ids) + self._topk_initialized = True + else: + self._merge_with_threshold(sub_sims, sub_ids) + + self._blocks_processed += 1 + logger.debug( + "GroundTruth: processed block %d (%d vectors, %d sub-blocks)", + block.block_index, B, (B + sub_b - 1) // sub_b, + ) + + def build(self) -> np.ndarray: + """Return the final truth table. + + Returns + ------- + np.ndarray + Shape ``(num_queries, k)``, dtype int64. + ``result[q]`` contains the IDs of the *k* nearest database + vectors to query *q*, ordered closest-first. + """ + # Descending similarity -- highest (closest) first. + order = np.argsort(-self._top_dist, axis=1) + sorted_ids = np.take_along_axis(self._top_ids, order, axis=1) + return sorted_ids + + # ------------------------------------------------------------------ + # Internals + # ------------------------------------------------------------------ + def _merge_first_block( + self, sims: np.ndarray, db_ids: np.ndarray, + ) -> None: + """Merge the very first sub-block (no useful threshold yet). + + Uses per-row ``argpartition`` on the full sub-block, which is + the fastest NumPy path when there is no threshold to exploit. + """ + k = self.k + Q, B = sims.shape + + if B <= k: + block_top_sims = sims + block_top_ids = np.broadcast_to(db_ids, sims.shape).copy() + else: + block_top_sims = np.empty((Q, k), dtype=np.float32) + block_top_ids = np.empty((Q, k), dtype=np.int64) + for q in range(Q): + idx = np.argpartition(sims[q], -k)[-k:] + block_top_sims[q] = sims[q, idx] + block_top_ids[q] = db_ids[idx] + + self._vectorized_merge(block_top_sims, block_top_ids) + + def _merge_with_threshold( + self, sims: np.ndarray, db_ids: np.ndarray, + ) -> None: + """Merge a sub-block using per-query threshold filtering. + + For each query, only the entries whose similarity exceeds the + current worst score in the running top-K are considered. With + high-dimensional random vectors this typically reduces the + candidate set from *B* to ~0.1--1 % of *B*, making the per-row + ``argpartition`` (and even the need for one) much cheaper. + """ + k = self.k + Q, B = sims.shape + + # Per-query threshold: worst similarity currently in the top-K. + thresh = self._top_dist.min(axis=1) # (Q,) + + block_top_sims = np.full((Q, k), -np.inf, dtype=np.float32) + block_top_ids = np.full((Q, k), -1, dtype=np.int64) + + for q in range(Q): + cand_idx = np.flatnonzero(sims[q] > thresh[q]) + nc = len(cand_idx) + if nc == 0: + continue + if nc <= k: + block_top_sims[q, :nc] = sims[q, cand_idx] + block_top_ids[q, :nc] = db_ids[cand_idx] + else: + vals = sims[q, cand_idx] + sub = np.argpartition(vals, -k)[-k:] + block_top_sims[q] = vals[sub] + block_top_ids[q] = db_ids[cand_idx[sub]] + + self._vectorized_merge(block_top_sims, block_top_ids) + + def _vectorized_merge( + self, + block_top_sims: np.ndarray, + block_top_ids: np.ndarray, + ) -> None: + """Merge block top-K into running top-K (single vectorized op). + + Concatenates ``(Q, K)`` running state with ``(Q, K_block)`` + block candidates, then selects the overall top-K via a + single ``argpartition`` along ``axis=1``. + """ + k = self.k + cand_sims = np.concatenate( + [self._top_dist, block_top_sims], axis=1, + ) + cand_ids = np.concatenate( + [self._top_ids, block_top_ids], axis=1, + ) + + best = np.argpartition(cand_sims, -k, axis=1)[:, -k:] + self._top_dist = np.take_along_axis(cand_sims, best, axis=1) + self._top_ids = np.take_along_axis(cand_ids, best, axis=1) diff --git a/vdb_benchmark/vdbbench/benchmark/orchestrator.py b/vdb_benchmark/vdbbench/benchmark/orchestrator.py new file mode 100644 index 00000000..35da4041 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/orchestrator.py @@ -0,0 +1,566 @@ +"""Benchmark orchestrator -- producer / consumer pipeline. + +Coordinates three concerns during the **load** phase: + +1. **Producer** (:class:`VectorGenerator`) -- generates random vectors in + blocks on a background thread. +2. **VDB consumer** (:class:`VectorDBBackend`) -- inserts each block into + the target database (main thread, network I/O). +3. **Ground-truth consumer** (:class:`GroundTruthBuilder`) -- computes + brute-force nearest neighbors for each block against the query set + (background thread, runs in parallel with insert). + +And during the **search** phase: + +4. **SearchRunner** -- queries the VDB in batches, computes recall + against the truth table, and logs QPS / latency percentiles. + +Three runtime modes are supported via ``BenchmarkConfig.mode``: + +* ``load`` -- generate vectors, ingest, compute ground truth. +* ``search`` -- run search queries against an already-loaded collection. +* ``both`` -- load then search. + +After all blocks have been processed the orchestrator writes artifacts +to ``output_dir``: + +* **Vectors in the database** -- already stored by the VDB consumer. +* **query_vectors.npy** -- the query-vector matrix. +* **ground_truth.npz** -- the truth table (``truth_table``) and the + query vectors (``query_vectors``). ``truth_table[q]`` is a length-K + array of database IDs ordered closest-first to query *q*. +* **search_results.json** -- search benchmark results (search/both modes). + +Usage:: + + from benchmark.orchestrator import BenchmarkOrchestrator + + orch = BenchmarkOrchestrator(config, backend) + orch.run() # blocking -- runs load, search, or both + orch.save(output_dir) # write artifacts +""" + +from __future__ import annotations + +import json +import logging +import os +import time +from concurrent.futures import ThreadPoolExecutor +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, Optional + +import numpy as np + +from .backends.base import VectorDBBackend +from .generator import VectorBlock, VectorGenerator, generate_query_vectors +from .ground_truth import GroundTruthBuilder +from .search_runner import ( + SearchResult, + SearchRunner, + build_truth_from_flat, + ensure_flat_collection, +) + +logger = logging.getLogger(__name__) + +# Valid mode values +MODES = ("load", "search", "both") +# Valid truth_mode values +TRUTH_MODES = ("precomputed", "flat_index") + + +@dataclass +class BenchmarkConfig: + """All tunables for a single benchmark run.""" + + # Run mode + mode: str = "load" # "load", "search", or "both" + + # Database vectors + num_vectors: int = 1_000_000 + dimension: int = 1536 + distribution: str = "uniform" + seed: int = 42 + block_size: int = 100_000 + batch_size: int = 10_000 + + # Query vectors + num_query_vectors: int = 10_000 + query_seed: int = 99 + + # Ground truth + truth_k: int = 100 + truth_mode: str = "precomputed" # "precomputed" or "flat_index" + + # Index + collection_name: str = "bench_vectors" + metric_type: str = "COSINE" + index_type: str = "HNSW" + index_params: Dict[str, Any] = field(default_factory=dict) + num_shards: int = 1 + force: bool = False + + # Connection (used by Milvus backend) + host: str = "127.0.0.1" + port: str = "19530" + + # Pipeline tuning + max_queue_depth: int = 4 + + # Post-load + compact: bool = False + monitor_interval: int = 5 + + # Search benchmark + search_k: int = 10 + search_params: Dict[str, Any] = field(default_factory=dict) + num_search_rounds: int = 1 + search_batch_size: int = 1 + log_interval: int = 1000 + + # Artifacts directory (for search mode -- where to load from) + artifacts_dir: str = "" + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + @classmethod + def from_dict(cls, d: Dict[str, Any]) -> "BenchmarkConfig": + """Build from a flat or sectioned dict (like the YAML configs). + + Nested dicts that correspond to known dict-typed fields + (e.g. ``search_params``, ``index_params``) are preserved as-is. + Other nested dicts (YAML sections like ``database``, ``dataset``) + are flattened into the top level. + """ + known = {f.name for f in cls.__dataclass_fields__.values()} + # Fields that are Dict-typed and should stay as dicts + dict_fields = { + f.name for f in cls.__dataclass_fields__.values() + if f.default_factory is dict # type: ignore[comparison-overlap] + } + flat: Dict[str, Any] = {} + for key, val in d.items(): + if isinstance(val, dict) and key not in dict_fields: + # YAML section -- flatten its contents + flat.update(val) + else: + flat[key] = val + return cls(**{k: v for k, v in flat.items() if k in known}) + + +class BenchmarkOrchestrator: + """Wire everything together and drive the pipeline. + + Parameters + ---------- + config : BenchmarkConfig + Benchmark tunables. + backend : VectorDBBackend + A connected backend instance (``connect()`` already called). + """ + + def __init__( + self, + config: BenchmarkConfig, + backend: VectorDBBackend, + ) -> None: + self.cfg = config + self.backend = backend + + self.query_vectors: Optional[np.ndarray] = None + self.truth_table: Optional[np.ndarray] = None + self.search_result: Optional[SearchResult] = None + + # Timing bookkeeping + self._timings: Dict[str, float] = {} + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + def run(self) -> Dict[str, Any]: + """Execute the benchmark in the configured mode. + + Returns a summary dict with timings and counts. + """ + mode = self.cfg.mode.lower() + if mode not in MODES: + raise ValueError( + f"Invalid mode '{mode}'. Must be one of {MODES}" + ) + + summary: Dict[str, Any] = {} + + if mode in ("load", "both"): + summary.update(self._run_load()) + + if mode in ("search", "both"): + summary.update(self._run_search()) + + logger.info("Pipeline complete (%s mode). Summary: %s", mode, summary) + return summary + + def save(self, output_dir: str) -> Dict[str, str]: + """Persist artifacts to *output_dir*. + + Returns a dict mapping artifact name to file path. + """ + os.makedirs(output_dir, exist_ok=True) + paths: Dict[str, str] = {} + + # Query vectors + if self.query_vectors is not None: + qpath = os.path.join(output_dir, "query_vectors.npy") + np.save(qpath, self.query_vectors) + paths["query_vectors"] = qpath + + # Ground-truth table + if self.truth_table is not None: + gtpath = os.path.join(output_dir, "ground_truth.npz") + np.savez_compressed( + gtpath, + truth_table=self.truth_table, + query_vectors=self.query_vectors, + ) + paths["ground_truth"] = gtpath + + # Search results + if self.search_result is not None: + spath = os.path.join(output_dir, "search_results.json") + with open(spath, "w") as f: + json.dump(self.search_result.to_dict(), f, indent=2, default=str) + paths["search_results"] = spath + + # Config + timings + meta = { + "config": self.cfg.to_dict(), + "timings": self._timings, + } + mpath = os.path.join(output_dir, "benchmark_meta.json") + with open(mpath, "w") as f: + json.dump(meta, f, indent=2, default=str) + paths["meta"] = mpath + + logger.info("Artifacts saved to %s", output_dir) + for name, p in paths.items(): + logger.info(" %s -> %s", name, p) + return paths + + # ------------------------------------------------------------------ + # Load phase + # ------------------------------------------------------------------ + def _run_load(self) -> Dict[str, Any]: + """Execute the full load pipeline (blocking).""" + cfg = self.cfg + + # ---- 1. Generate query vectors --------------------------------- + logger.info( + "Generating %s query vectors (%s-d, seed=%d) ...", + f"{cfg.num_query_vectors:,}", f"{cfg.dimension:,}", cfg.query_seed, + ) + t0 = time.time() + self.query_vectors = generate_query_vectors( + num_queries=cfg.num_query_vectors, + dimension=cfg.dimension, + distribution=cfg.distribution, + seed=cfg.query_seed, + ) + self._timings["query_gen_sec"] = time.time() - t0 + logger.info( + "%s query vectors generated in %.2f s", + f"{cfg.num_query_vectors:,}", self._timings["query_gen_sec"], + ) + + # ---- 2. Create the collection ---------------------------------- + logger.info( + "Creating collection '%s' (%s / %s) ...", + cfg.collection_name, cfg.index_type, cfg.metric_type, + ) + t0 = time.time() + self.backend.create_collection( + name=cfg.collection_name, + dimension=cfg.dimension, + metric_type=cfg.metric_type, + index_type=cfg.index_type, + index_params=cfg.index_params, + num_shards=cfg.num_shards, + force=cfg.force, + ) + self._timings["create_collection_sec"] = time.time() - t0 + + # ---- 2b. Create FLAT companion (if flat_index truth mode) ------ + flat_name = f"{cfg.collection_name}_flat" + if cfg.truth_mode == "flat_index": + ensure_flat_collection( + backend=self.backend, + source_name=cfg.collection_name, + flat_name=flat_name, + dimension=cfg.dimension, + metric_type=cfg.metric_type, + ) + + # ---- 3. Set up producer and ground-truth builder --------------- + generator = VectorGenerator( + total_vectors=cfg.num_vectors, + dimension=cfg.dimension, + block_size=cfg.block_size, + distribution=cfg.distribution, + seed=cfg.seed, + max_queue_depth=cfg.max_queue_depth, + ) + # Only build brute-force GT when in precomputed mode + gt_builder: Optional[GroundTruthBuilder] = None + if cfg.truth_mode == "precomputed": + gt_builder = GroundTruthBuilder( + query_vectors=self.query_vectors, + k=cfg.truth_k, + metric=cfg.metric_type, + ) + + # ---- 4. Run the pipeline --------------------------------------- + # Insert (network I/O) and GT update (BLAS matmul) both release + # the GIL, so they run truly in parallel when overlapped. + logger.info( + "Starting pipeline: %s vectors, block_size=%s, batch_size=%s", + f"{cfg.num_vectors:,}", f"{cfg.block_size:,}", f"{cfg.batch_size:,}", + ) + t_pipeline = time.time() + total_inserted = 0 + blocks_consumed = 0 + + def _timed_gt_update(builder, blk): + """Run GT update and return its wall-clock time.""" + t0 = time.time() + builder.update(blk) + return time.time() - t0 + + generator.start() + + with ThreadPoolExecutor(max_workers=1, + thread_name_prefix="gt") as gt_pool: + while True: + block: Optional[VectorBlock] = generator.queue.get() + if block is None: + break # sentinel + + n = len(block.ids) + t_wall = time.time() + + # -- kick off GT in background thread -------------------- + gt_future = None + if gt_builder is not None: + gt_future = gt_pool.submit( + _timed_gt_update, gt_builder, block, + ) + + # -- consumer 1: insert into VDB (main thread) ----------- + t_insert = time.time() + for off in range(0, n, cfg.batch_size): + end = min(off + cfg.batch_size, n) + self.backend.insert_batch( + name=cfg.collection_name, + ids=block.ids[off:end], + vectors=block.vectors[off:end], + ) + insert_elapsed = time.time() - t_insert + total_inserted += n + + # -- consumer 1b: mirror into FLAT collection ------------ + if cfg.truth_mode == "flat_index": + for off in range(0, n, cfg.batch_size): + end = min(off + cfg.batch_size, n) + self.backend.insert_batch( + name=flat_name, + ids=block.ids[off:end], + vectors=block.vectors[off:end], + ) + + # -- wait for GT to finish ------------------------------- + gt_elapsed = gt_future.result() if gt_future else 0.0 + wall_elapsed = time.time() - t_wall + + blocks_consumed += 1 + logger.info( + "Block %d/%d consumed: %s vectors " + "(insert=%.2fs | GT=%.2fs | wall=%.2fs). " + "Total: %s / %s", + blocks_consumed, generator.num_blocks, f"{n:,}", + insert_elapsed, gt_elapsed, wall_elapsed, + f"{total_inserted:,}", f"{cfg.num_vectors:,}", + ) + + generator.join() # propagate any producer error + + self._timings["pipeline_sec"] = time.time() - t_pipeline + logger.info( + "%s vectors inserted in %.2f s", + f"{total_inserted:,}", self._timings["pipeline_sec"], + ) + + # ---- 5. Flush + optional compaction + wait for index -------------- + logger.info("Flushing collection ...") + t0 = time.time() + self.backend.flush(cfg.collection_name) + if cfg.truth_mode == "flat_index": + self.backend.flush(flat_name) + self._timings["flush_sec"] = time.time() - t0 + logger.info("Flush completed in %.2f s", self._timings["flush_sec"]) + + if cfg.compact: + logger.info("Compacting segments ...") + t0 = time.time() + self.backend.compact(cfg.collection_name) + self.backend.flush(cfg.collection_name) + self._timings["compact_sec"] = time.time() - t0 + logger.info("Compaction completed in %.2f s", self._timings["compact_sec"]) + + logger.info("Waiting for index build ...") + t0 = time.time() + self.backend.wait_for_index( + cfg.collection_name, interval=cfg.monitor_interval, + compacted=cfg.compact, + ) + self._timings["index_build_sec"] = time.time() - t0 + + # ---- 7. Finalize ground truth ---------------------------------- + if gt_builder is not None: + logger.info("Building final truth table (k=%d) ...", cfg.truth_k) + t0 = time.time() + self.truth_table = gt_builder.build() + self._timings["truth_build_sec"] = time.time() - t0 + logger.info( + "Ground truth built in %.2f s (%s queries x k=%s)", + self._timings["truth_build_sec"], + f"{cfg.num_query_vectors:,}", f"{cfg.truth_k:,}", + ) + elif cfg.truth_mode == "flat_index": + logger.info( + "Building truth table from FLAT collection (k=%d) ...", + cfg.truth_k, + ) + t0 = time.time() + self.truth_table = build_truth_from_flat( + backend=self.backend, + flat_collection_name=flat_name, + query_vectors=self.query_vectors, + truth_k=cfg.truth_k, + metric_type=cfg.metric_type, + ) + self._timings["truth_build_sec"] = time.time() - t0 + logger.info( + "Ground truth (FLAT) built in %.2f s (%s queries x k=%s)", + self._timings["truth_build_sec"], + f"{cfg.num_query_vectors:,}", f"{cfg.truth_k:,}", + ) + + return self._load_summary(total_inserted, blocks_consumed) + + # ------------------------------------------------------------------ + # Search phase + # ------------------------------------------------------------------ + def _run_search(self) -> Dict[str, Any]: + """Execute the search benchmark (blocking).""" + cfg = self.cfg + + # ---- 1. Load query vectors + truth table ----------------------- + if self.query_vectors is None or self.truth_table is None: + self._load_artifacts() + + # ---- 2. Build search params ------------------------------------ + search_params = cfg.search_params + if not search_params: + search_params = { + "metric_type": cfg.metric_type, + "params": {}, + } + + # ---- 3. Run the search benchmark ------------------------------- + runner = SearchRunner( + backend=self.backend, + collection_name=cfg.collection_name, + query_vectors=self.query_vectors, + truth_table=self.truth_table, + search_k=cfg.search_k, + search_params=search_params, + metric_type=cfg.metric_type, + num_rounds=cfg.num_search_rounds, + batch_size=cfg.search_batch_size, + log_interval=cfg.log_interval, + ) + + t0 = time.time() + self.search_result = runner.run() + self._timings["search_sec"] = time.time() - t0 + + return self._search_summary() + + def _load_artifacts(self) -> None: + """Load query vectors and truth table from a previous run.""" + d = self.cfg.artifacts_dir + if not d: + raise ValueError( + "In 'search' mode, either run 'load' first (mode=both) " + "or provide --artifacts-dir pointing to a previous run." + ) + qpath = os.path.join(d, "query_vectors.npy") + gtpath = os.path.join(d, "ground_truth.npz") + + if not os.path.isfile(qpath) or not os.path.isfile(gtpath): + raise FileNotFoundError( + f"Expected artifacts not found in '{d}'. " + f"Looking for query_vectors.npy and ground_truth.npz" + ) + + self.query_vectors = np.load(qpath) + gt = np.load(gtpath) + self.truth_table = gt["truth_table"] + + logger.info( + "Loaded artifacts from '%s': queries=%s, truth=%s", + d, self.query_vectors.shape, self.truth_table.shape, + ) + + # If truth_mode is flat_index and we don't have precomputed truth, + # build it on-the-fly + if (self.cfg.truth_mode == "flat_index" + and self.truth_table is None): + flat_name = f"{self.cfg.collection_name}_flat" + self.truth_table = build_truth_from_flat( + backend=self.backend, + flat_collection_name=flat_name, + query_vectors=self.query_vectors, + truth_k=self.cfg.truth_k, + metric_type=self.cfg.metric_type, + ) + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + def _load_summary(self, total_inserted: int, blocks: int) -> Dict[str, Any]: + return { + "total_vectors_inserted": total_inserted, + "blocks_processed": blocks, + "num_query_vectors": self.cfg.num_query_vectors, + "truth_k": self.cfg.truth_k, + "truth_table_shape": list(self.truth_table.shape) + if self.truth_table is not None + else None, + "timings": dict(self._timings), + } + + def _search_summary(self) -> Dict[str, Any]: + r = self.search_result + if r is None: + return {} + return { + "search_total_queries": r.total_queries, + "search_qps": r.qps, + "search_recall_at_k": r.recall_at_k, + "search_latency_p50_ms": r.latency_p50_ms, + "search_latency_p90_ms": r.latency_p90_ms, + "search_latency_p99_ms": r.latency_p99_ms, + "search_latency_mean_ms": r.latency_mean_ms, + "search_wall_sec": r.total_wall_sec, + "timings": dict(self._timings), + } diff --git a/vdb_benchmark/vdbbench/benchmark/run_benchmark.py b/vdb_benchmark/vdbbench/benchmark/run_benchmark.py new file mode 100755 index 00000000..e9a463ab --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/run_benchmark.py @@ -0,0 +1,581 @@ +#!/usr/bin/env python3 +"""CLI entry point for the producer-consumer vector-DB benchmark. + +Usage examples:: + + # List available backends + python -m vdbbench.benchmark.run_benchmark help backends + + # Show detailed help for a specific backend + python -m vdbbench.benchmark.run_benchmark help backend milvus + + # Run a benchmark (config-driven) + python -m vdbbench.benchmark.run_benchmark --config configs/1m_hnsw.yaml + + # Override mode or backend on the CLI + python -m vdbbench.benchmark.run_benchmark --config configs/1m_hnsw.yaml --mode both + python -m vdbbench.benchmark.run_benchmark --config configs/1m_hnsw.yaml --backend pgvector + + # Dry-run (print resolved config and exit) + python -m vdbbench.benchmark.run_benchmark --config configs/1m_hnsw.yaml --what-if + + # Direct script execution also works: + python benchmark/run_benchmark.py help backend milvus + +All dataset, index, search, and connection parameters are set in the YAML +config file. The CLI is intentionally minimal -- only operational switches +(``--mode``, ``--backend``, ``--force``, ``--output-dir``, etc.) may be +given on the command line. +""" + +from __future__ import annotations + +import sys + +# ------------------------------------------------------------------ +# Direct-execution bootstrap. When someone runs this file as a script +# (``python run_benchmark.py …``), Python sets __name__ = "__main__" +# and relative imports are impossible. We detect that case *before* +# any relative imports, fix sys.path, re-import ourselves as a proper +# package member, and delegate to main(). +# ------------------------------------------------------------------ +if __name__ == "__main__": + import importlib + import pathlib + + _this = pathlib.Path(__file__).resolve() + # …/vdb_benchmark/vdbbench/benchmark/run_benchmark.py + # parent.parent.parent → …/vdb_benchmark (contains vdbbench/) + _pkg_root = str(_this.parent.parent.parent) + if _pkg_root not in sys.path: + sys.path.insert(0, _pkg_root) + + _mod = importlib.import_module("vdbbench.benchmark.run_benchmark") + raise SystemExit(_mod.main()) + +# ------------------------------------------------------------------ +# Normal imports (only reached when loaded as a package member). +# ------------------------------------------------------------------ + +import argparse +import json +import logging +import math +import os +import sys +import time +from datetime import datetime + +import yaml + +from .backends import registry, get_backend +from .backends._env import load_env_file, env_for_backend +from .backends._help import format_backend_help, format_backends_list +from .orchestrator import BenchmarkConfig, BenchmarkOrchestrator, MODES, TRUTH_MODES + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s %(levelname)-8s %(name)s %(message)s", +) +logger = logging.getLogger(__name__) + +# ------------------------------------------------------------------ +# YAML helpers (mirrors existing config_loader.py pattern) +# ------------------------------------------------------------------ + +def _load_yaml(path: str) -> dict: + """Try *path* directly, then under ``configs/``.""" + for candidate in [path, os.path.join("configs", path)]: + if os.path.isfile(candidate): + with open(candidate) as fh: + cfg = yaml.safe_load(fh) + logger.info("Loaded config from %s", candidate) + return cfg or {} + # Also try relative to this file's directory + pkg_dir = os.path.dirname(os.path.abspath(__file__)) + candidate = os.path.join(pkg_dir, "configs", path) + if os.path.isfile(candidate): + with open(candidate) as fh: + cfg = yaml.safe_load(fh) + logger.info("Loaded config from %s", candidate) + return cfg or {} + logger.error("Config file not found: %s", path) + return {} + +# ------------------------------------------------------------------ +# Help sub-commands +# ------------------------------------------------------------------ + +def _handle_help(argv: list[str]) -> bool: + """If *argv* starts with ``help ...``, print the requested info + and return ``True`` (meaning: handled, exit). Otherwise return + ``False``. + """ + if not argv or argv[0].lower() != "help": + return False + + rest = [a.lower() for a in argv[1:]] + + # help backends + if rest == ["backends"]: + print(format_backends_list(registry)) + return True + + # help backend + if len(rest) == 2 and rest[0] == "backend": + print(format_backend_help(registry, rest[1])) + return True + + # Bare "help" or unknown + print("Usage:") + print(" help backends -- list all registered backends") + print(" help backend -- show parameters for a backend") + print() + print(format_backends_list(registry)) + return True + +# ------------------------------------------------------------------ +# CLI +# ------------------------------------------------------------------ + +def _build_parser() -> argparse.ArgumentParser: + available = ", ".join(registry.names()) or "(none)" + p = argparse.ArgumentParser( + description="Vector-DB benchmark: generate, ingest, build ground truth, and search", + epilog=( + "All dataset, index, search, and connection parameters live in " + "the YAML config file. Run 'help backends' or " + "'help backend ' for backend-specific details." + ), + ) + + # Config file (the primary input) + p.add_argument("--config", type=str, required=False, + help="Path to YAML config file (required for benchmark runs)") + + # Operational overrides (take precedence over YAML values) + p.add_argument( + "--mode", type=str, dest="mode", + choices=list(MODES), + help="Override runtime mode: 'load', 'search', or 'both'", + ) + p.add_argument( + "--backend", type=str, dest="backend", + help=f"Override backend ({available})", + ) + p.add_argument("--force", action="store_true", default=None, + help="Drop collection if it already exists") + p.add_argument("--output-dir", type=str, dest="output_dir", + help="Directory for artifacts (default: auto-timestamped)") + p.add_argument("--artifacts-dir", type=str, dest="artifacts_dir", + help="Load query/truth artifacts from this directory " + "(required for --mode search without prior load)") + + # Introspection + p.add_argument("--what-if", action="store_true", + help="Print resolved config and exit") + p.add_argument("--plan", action="store_true", + help="Show the full execution plan (steps, sizes, " + "estimates) without running anything") + p.add_argument("--debug", action="store_true", + help="Enable DEBUG logging") + + return p + + +def _merge_cli_over_yaml(yaml_cfg: dict, cli_ns: argparse.Namespace) -> dict: + """Flatten YAML sections and overlay non-None CLI values.""" + flat: dict = {} + for key, val in yaml_cfg.items(): + if isinstance(val, dict): + flat.update(val) + else: + flat[key] = val + + skip = {"config", "what_if", "plan", "debug", "output_dir", "artifacts_dir"} + for key, val in vars(cli_ns).items(): + if key in skip: + continue + if val is not None: + flat[key] = val + + return flat + + +def _collect_index_params(flat: dict) -> dict: + """Pull index-specific keys into the nested ``index_params`` dict.""" + ip = flat.get("index_params", {}) + if isinstance(ip, dict): + ip = dict(ip) + else: + ip = {} + for k in ("M", "efConstruction", "MaxDegree", "SearchListSize", + "inline_pq", "max_degree", "search_list_size", + "lists", "ef_search", "probes"): + if k in flat and flat[k] is not None: + ip[k] = flat[k] + flat["index_params"] = ip + return flat + + +def _resolve_backend_name(flat: dict, cli_ns: argparse.Namespace) -> str: + """Determine which backend to use. + + Precedence: ``--backend`` CLI flag > ``backend`` key in YAML config + > ``"milvus"`` (default). + """ + if cli_ns.backend: + return cli_ns.backend.lower() + if "backend" in flat: + return str(flat["backend"]).lower() + return "milvus" + + +# ------------------------------------------------------------------ +# Plan formatter +# ------------------------------------------------------------------ + +def _sizeof_fmt(num_bytes: float) -> str: + """Human-readable byte size (e.g. ``5.86 GB``).""" + for unit in ("B", "KB", "MB", "GB", "TB"): + if abs(num_bytes) < 1024: + return f"{num_bytes:.2f} {unit}" + num_bytes /= 1024 + return f"{num_bytes:.2f} PB" + + +def _format_plan(cfg: BenchmarkConfig, desc) -> str: + """Build a human-readable execution plan from *cfg* and the backend + *desc* (:class:`BackendDescriptor`). No database connection needed. + """ + W = 64 + SEP = "-" * W + lines: list[str] = [] + + def heading(title: str) -> None: + lines.append("") + lines.append("=" * W) + lines.append(f" {title}") + lines.append("=" * W) + + def step(num: int, title: str) -> None: + lines.append("") + lines.append(SEP) + lines.append(f" Step {num}: {title}") + lines.append(SEP) + + def kv(key: str, val, indent: int = 4) -> None: + pad = " " * indent + lines.append(f"{pad}{key:<32s}: {val}") + + # -- Sizes ----------------------------------------------------------- + bytes_per_vector = cfg.dimension * 4 # float32 + db_vector_bytes = cfg.num_vectors * bytes_per_vector + query_vector_bytes = cfg.num_query_vectors * bytes_per_vector + # truth table: int64 ids per query + truth_bytes = cfg.num_query_vectors * cfg.truth_k * 8 + num_blocks = math.ceil(cfg.num_vectors / cfg.block_size) + inserts_per_block = math.ceil(cfg.block_size / cfg.batch_size) + total_inserts = num_blocks * inserts_per_block + + # Ground-truth working memory: the builder keeps a running top-K + # matrix of shape (num_queries, K) for IDs and distances (both float64). + gt_working_bytes = cfg.num_query_vectors * cfg.truth_k * 8 * 2 + + # Per-block GT compute: cosine/IP needs (num_queries x block_size) + # distance matrix in float32. + gt_block_bytes = cfg.num_query_vectors * cfg.block_size * 4 + + # -- Header ---------------------------------------------------------- + heading("BENCHMARK EXECUTION PLAN") + lines.append("") + kv("Backend", f"{desc.display_name} (--backend {desc.name})") + kv("Mode", cfg.mode) + kv("Collection", cfg.collection_name) + kv("Force recreate", "yes" if cfg.force else "no") + + # -- Step 1: Query vector generation --------------------------------- + step(1, "Generate query vectors") + kv("Num query vectors", f"{cfg.num_query_vectors:,}") + kv("Dimension", f"{cfg.dimension:,}") + kv("Distribution", cfg.distribution) + kv("Query seed", cfg.query_seed) + kv("Memory", _sizeof_fmt(query_vector_bytes)) + kv("Output", "held in memory (saved to query_vectors.npy later)") + + # -- Step 2: Create collection + index ------------------------------- + step(2, "Create collection and index") + kv("Index type", cfg.index_type) + kv("Metric type", cfg.metric_type) + kv("Num shards", cfg.num_shards) + idx_desc = desc.get_index(cfg.index_type) + if idx_desc and cfg.index_params: + for p in idx_desc.build_params: + val = cfg.index_params.get(p.name, p.default) + kv(f" {p.name}", val) + elif idx_desc: + for p in idx_desc.build_params: + kv(f" {p.name}", f"{p.default} (default)") + + # -- Step 3: Vector generation + ingestion + GT ---------------------- + step(3, "Generate, ingest, and compute ground truth") + lines.append("") + lines.append(" Producer (background thread):") + kv("Total database vectors", f"{cfg.num_vectors:,}") + kv("Dimension", f"{cfg.dimension:,}") + kv("Distribution", cfg.distribution) + kv("Vector seed", cfg.seed) + kv("Block size", f"{cfg.block_size:,} vectors") + kv("Num blocks", f"{num_blocks:,}") + kv("Queue depth", f"{cfg.max_queue_depth} blocks") + kv("Per-block memory", _sizeof_fmt(cfg.block_size * bytes_per_vector)) + kv("Total vector data", _sizeof_fmt(db_vector_bytes)) + + lines.append("") + lines.append(" Consumer 1 -- Database ingestion:") + kv("Batch size", f"{cfg.batch_size:,} vectors/insert") + kv("Inserts per block", f"{inserts_per_block:,}") + kv("Total insert calls", f"{total_inserts:,}") + + lines.append("") + lines.append(" Consumer 2 -- Ground-truth builder:") + kv("Query vectors", f"{cfg.num_query_vectors:,}") + kv("K (neighbors)", f"{cfg.truth_k:,}") + kv("Metric", cfg.metric_type) + kv("Per-block distance matrix", _sizeof_fmt(gt_block_bytes)) + kv("Running top-K memory", _sizeof_fmt(gt_working_bytes)) + + # -- Step 4: Flush --------------------------------------------------- + step(4, "Flush collection") + kv("Action", "commit pending writes to storage") + + # -- Step 5: Optional compaction ------------------------------------- + if cfg.compact: + step(5, "Compact collection") + kv("Action", "merge small segments before index build") + else: + lines.append("") + lines.append(f" (Step 5: Compact -- skipped, compact not set)") + + # -- Step 6: Wait for index build ------------------------------------ + step(6, "Wait for index build") + kv("Poll interval", f"{cfg.monitor_interval}s") + + # -- Step 7: Finalize ground truth ----------------------------------- + step(7, "Finalize ground truth") + kv("Truth table shape", f"({cfg.num_query_vectors:,}, {cfg.truth_k:,})") + kv("Truth table size", _sizeof_fmt(truth_bytes)) + + # -- Step 8: Save artifacts ------------------------------------------ + step(8, "Save artifacts") + kv("query_vectors.npy", _sizeof_fmt(query_vector_bytes)) + kv("ground_truth.npz", f"~{_sizeof_fmt(truth_bytes + query_vector_bytes)}" + " (compressed)") + kv("benchmark_meta.json", "config + timings") + + # -- Search steps (when mode is 'search' or 'both') ------------------ + mode = cfg.mode.lower() + if mode in ("search", "both"): + step(9, "Load collection into memory") + kv("Collection", cfg.collection_name) + kv("Action", "ensure collection is loaded for search") + + step(10, "Run search benchmark") + kv("Search K (top-K)", cfg.search_k) + kv("Query vectors", f"{cfg.num_query_vectors:,}") + kv("Rounds", cfg.num_search_rounds) + kv("Batch size", cfg.search_batch_size) + kv("Log interval", f"every {cfg.log_interval} queries") + kv("Truth K", cfg.truth_k) + kv("Search params", cfg.search_params or "(backend defaults)") + kv("Total queries", f"{cfg.num_query_vectors * cfg.num_search_rounds:,}") + + # -- Summary --------------------------------------------------------- + heading("RESOURCE ESTIMATES") + lines.append("") + peak_mem = ( + query_vector_bytes # query vectors + + cfg.max_queue_depth * cfg.block_size * bytes_per_vector # queue + + gt_working_bytes # GT top-K state + + gt_block_bytes # GT distance matrix + ) + kv("Peak memory (estimate)", _sizeof_fmt(peak_mem)) + kv("Total vector data generated", _sizeof_fmt(db_vector_bytes)) + kv("Disk artifacts (approx)", _sizeof_fmt( + query_vector_bytes + truth_bytes + query_vector_bytes + 4096)) + lines.append("") + + return "\n".join(lines) +# Main +# ------------------------------------------------------------------ + +def main(argv: list[str] | None = None) -> int: + raw_argv = argv if argv is not None else sys.argv[1:] + + # No arguments at all → show usage and exit. + if not raw_argv: + _build_parser().print_help() + print() + print(format_backends_list(registry)) + return 0 + + # Intercept "help" sub-commands before argparse runs. + if _handle_help(raw_argv): + return 0 + + parser = _build_parser() + args = parser.parse_args(raw_argv) + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + # A config file is required for any real work. + if not args.config and not (args.what_if or args.plan): + parser.error("--config is required (or use --what-if / --plan)") + + # Load .env file (if python-dotenv is installed and .env exists) + load_env_file() + + # Build resolved config: defaults <- YAML <- CLI overrides + yaml_cfg = _load_yaml(args.config) if args.config else {} + flat = _merge_cli_over_yaml(yaml_cfg, args) + flat = _collect_index_params(flat) + + # Inject CLI-only overrides that are not part of YAML sections + if args.artifacts_dir is not None: + flat["artifacts_dir"] = args.artifacts_dir + + # Resolve backend + backend_name = _resolve_backend_name(flat, args) + desc = registry.get(backend_name) + if desc is None: + available = ", ".join(registry.names()) or "(none)" + parser.error( + f"Unknown backend '{backend_name}'. Available: {available}" + ) + + cfg = BenchmarkConfig.from_dict(flat) + + # --what-if: show config and exit + if args.what_if: + print(f"\nBackend: {desc.display_name} (--backend {desc.name})") + print("\nResolved benchmark configuration:") + print("=" * 60) + display = {k: v for k, v in cfg.to_dict().items() + if not (k == "compact" and v)} + print(json.dumps(display, indent=2, default=str)) + print("=" * 60) + + # Show resolved connection parameters with sources + _env = env_for_backend(backend_name, desc) + if desc.connection_params: + print("\nConnection parameters (source):") + for p in desc.connection_params: + k = p.name + env_val = _env.get(k) + yaml_val = flat.get(k) + if env_val is not None: + print(f" {k}: {env_val!r} (env: {backend_name.upper()}__{k.upper()})") + elif yaml_val is not None: + print(f" {k}: {yaml_val!r} (config)") + else: + print(f" {k}: {p.default!r} (default)") + return 0 + + # --plan: show step-by-step execution plan and exit + if args.plan: + print(_format_plan(cfg, desc)) + return 0 + + # Validate essentials + mode = cfg.mode.lower() + if mode in ("load", "both"): + if not cfg.collection_name or not cfg.dimension or not cfg.num_vectors: + parser.error( + "collection_name, dimension, and num_vectors are required " + "for load/both modes (set them in the config file)." + ) + elif mode == "search": + if not cfg.collection_name: + parser.error( + "collection_name is required for search mode " + "(set it in the config file)." + ) + if not cfg.artifacts_dir: + parser.error( + "--artifacts-dir is required for search mode to load " + "query vectors and ground truth." + ) + + # Validate index type against backend capabilities + if cfg.index_type and cfg.index_type.upper() not in ( + n.upper() for n in desc.index_names() + ): + parser.error( + f"Backend '{desc.name}' does not support index type " + f"'{cfg.index_type}'. Supported: {', '.join(desc.index_names())}" + ) + + # Output directory + output_dir = args.output_dir or os.path.join( + "results", + f"{cfg.collection_name}_{datetime.now():%Y%m%d_%H%M%S}", + ) + + # Connect backend. + # Precedence: environment variables (.env / shell) > YAML config > defaults + backend = desc.backend_class() + env_kwargs = env_for_backend(backend_name, desc) + conn_kwargs: dict = {} + for p in desc.connection_params: + k = p.name + env_val = env_kwargs.get(k) # env var / .env file + yaml_val = flat.get(k) # YAML config + if env_val is not None: + conn_kwargs[k] = env_val + elif yaml_val is not None: + conn_kwargs[k] = yaml_val + # else: omitted → backend.connect() uses its own default + backend.connect(**conn_kwargs) + + try: + orch = BenchmarkOrchestrator(config=cfg, backend=backend) + summary = orch.run() + paths = orch.save(output_dir) + + mode = cfg.mode.lower() + + print("\n" + "=" * 60) + print(f"BENCHMARK COMPLETE (backend: {desc.display_name}, mode: {mode})") + print("=" * 60) + + if mode in ("load", "both"): + print(f" Vectors inserted : {summary.get('total_vectors_inserted', 'N/A'):,}") + print(f" Query vectors : {cfg.num_query_vectors:,}") + print(f" Truth table : {summary.get('truth_table_shape', 'N/A')}") + print(f" Truth mode : {cfg.truth_mode}") + + if mode in ("search", "both"): + print(f"\n --- Search Results ---") + print(f" Total queries : {summary.get('search_total_queries', 'N/A'):,}") + print(f" QPS : {summary.get('search_qps', 0):.1f}") + print(f" Recall@{cfg.search_k:<9d}: {summary.get('search_recall_at_k', 0):.4f}") + print(f" Latency P50 : {summary.get('search_latency_p50_ms', 0):.2f} ms") + print(f" Latency P90 : {summary.get('search_latency_p90_ms', 0):.2f} ms") + print(f" Latency P99 : {summary.get('search_latency_p99_ms', 0):.2f} ms") + print(f" Latency mean : {summary.get('search_latency_mean_ms', 0):.2f} ms") + print(f" Wall time : {summary.get('search_wall_sec', 0):.2f} s") + + print(f"\n Output dir : {output_dir}") + for name, p in paths.items(): + print(f" {name:20s} -> {p}") + print("=" * 60) + print("\nTimings:") + for k, v in summary.get("timings", {}).items(): + print(f" {k:30s} : {v:>10.2f} s") + print() + + finally: + backend.disconnect() + + return 0 diff --git a/vdb_benchmark/vdbbench/benchmark/search_runner.py b/vdb_benchmark/vdbbench/benchmark/search_runner.py new file mode 100644 index 00000000..016b9f69 --- /dev/null +++ b/vdb_benchmark/vdbbench/benchmark/search_runner.py @@ -0,0 +1,463 @@ +"""Search benchmark runner -- query the VDB and measure performance. + +Sends query vectors to the vector database in batches, measures +latency per batch, computes recall against a ground-truth table, +and periodically logs aggregate statistics. + +Two ground-truth modes are supported: + +* **precomputed** -- a truth table (``num_queries × K`` array of IDs) + is provided up-front (e.g. from the load phase). +* **flat_index** -- a second collection with a ``FLAT`` index is + queried at the start of the run to build the truth table on-the-fly. + +Usage:: + + runner = SearchRunner(cfg, backend, query_vectors, truth_table) + result = runner.run() + runner.save(output_dir) +""" + +from __future__ import annotations + +import json +import logging +import os +import time +from dataclasses import asdict, dataclass, field +from typing import Any, Dict, List, Optional + +import numpy as np + +from .backends.base import VectorDBBackend + +logger = logging.getLogger(__name__) + + +# ===================================================================== +# Result data model +# ===================================================================== + +@dataclass +class IntervalStats: + """Stats captured every *log_interval* queries.""" + interval_index: int + wall_clock_sec: float + total_queries: int + interval_queries: int + qps_cumulative: float + qps_interval: float + recall_at_k: float + latency_p50_ms: float + latency_p90_ms: float + latency_p99_ms: float + latency_mean_ms: float + + +@dataclass +class SearchResult: + """Final result of a search benchmark run.""" + total_queries: int + total_wall_sec: float + qps: float + recall_at_k: float + search_k: int + truth_k: int + + # Aggregate latency (all queries) + latency_p50_ms: float + latency_p90_ms: float + latency_p99_ms: float + latency_mean_ms: float + + # Per-interval snapshots + intervals: List[Dict[str, Any]] = field(default_factory=list) + + def to_dict(self) -> Dict[str, Any]: + return asdict(self) + + +# ===================================================================== +# Recall helpers +# ===================================================================== + +def _recall_at_k( + predicted_ids: np.ndarray, + truth_ids: np.ndarray, + k: int, +) -> float: + """Compute mean recall@k across all queries. + + Parameters + ---------- + predicted_ids : np.ndarray + Shape ``(nq, pred_k)`` -- IDs returned by ANN search. + truth_ids : np.ndarray + Shape ``(nq, truth_k)`` -- ground-truth nearest IDs. + k : int + Evaluate recall using the top-*k* of the truth table. + + Returns + ------- + float + Mean recall in [0, 1]. + """ + nq = predicted_ids.shape[0] + truth_top_k = truth_ids[:, :k] + hits = 0 + for q in range(nq): + gt_set = set(truth_top_k[q].tolist()) + pred_set = set(predicted_ids[q].tolist()) + hits += len(gt_set & pred_set) + return hits / (nq * k) + + +# ===================================================================== +# Ground-truth via FLAT index +# ===================================================================== + +def build_truth_from_flat( + backend: VectorDBBackend, + flat_collection_name: str, + query_vectors: np.ndarray, + truth_k: int, + metric_type: str = "COSINE", +) -> np.ndarray: + """Query a FLAT-index collection to produce a truth table. + + Parameters + ---------- + backend : + Connected backend instance. + flat_collection_name : + Name of a collection that already has a FLAT index and + contains the same vectors as the ANN collection. + query_vectors : + Shape ``(nq, dim)``, dtype float32. + truth_k : + Number of neighbors per query. + metric_type : + Distance metric used by the collection. + + Returns + ------- + np.ndarray + Shape ``(nq, truth_k)``, dtype int64. + """ + logger.info( + "Building truth table from FLAT collection '%s' (k=%d) ...", + flat_collection_name, truth_k, + ) + t0 = time.time() + + # Search in small batches to avoid overwhelming the server + batch = 100 + nq = query_vectors.shape[0] + all_ids: list[list[int]] = [] + + search_params = { + "metric_type": metric_type, + "params": {}, + } + + for start in range(0, nq, batch): + end = min(start + batch, nq) + batch_results = backend.search( + name=flat_collection_name, + query_vectors=query_vectors[start:end], + top_k=truth_k, + search_params=search_params, + ) + all_ids.extend(batch_results) + + truth = np.array(all_ids, dtype=np.int64) + elapsed = time.time() - t0 + logger.info( + "Truth table built from FLAT index in %.2f s (shape %s)", + elapsed, truth.shape, + ) + return truth + + +def ensure_flat_collection( + backend: VectorDBBackend, + source_name: str, + flat_name: str, + dimension: int, + metric_type: str, +) -> bool: + """Create the FLAT companion collection if it does not exist. + + Returns True if the collection already exists, False if it must + be populated by the caller (e.g. during the load phase). + """ + if backend.collection_exists(flat_name): + logger.info("FLAT collection '%s' already exists", flat_name) + return True + + logger.info("Creating FLAT collection '%s' ...", flat_name) + backend.create_collection( + name=flat_name, + dimension=dimension, + metric_type=metric_type, + index_type="FLAT", + index_params={}, + num_shards=1, + force=False, + ) + return False + + +# ===================================================================== +# Search runner +# ===================================================================== + +class SearchRunner: + """Execute a search benchmark against a loaded VDB collection. + + Parameters + ---------- + backend : + Connected backend (collection must already be loaded with data). + collection_name : + Name of the ANN collection to search. + query_vectors : + Shape ``(nq, dim)``, dtype float32. + truth_table : + Shape ``(nq, truth_k)``, dtype int64 -- ground-truth IDs. + search_k : + Number of neighbors to retrieve per query. + search_params : + Backend-specific search parameters (e.g. ``ef`` for HNSW). + metric_type : + Distance metric (for ``search_params`` wrapper). + num_rounds : + How many times to cycle through the full query set. + batch_size : + Number of query vectors per ``backend.search()`` call. + log_interval : + Log aggregate stats every *log_interval* queries. + """ + + def __init__( + self, + backend: VectorDBBackend, + collection_name: str, + query_vectors: np.ndarray, + truth_table: np.ndarray, + search_k: int = 10, + search_params: Optional[Dict[str, Any]] = None, + metric_type: str = "COSINE", + num_rounds: int = 1, + batch_size: int = 1, + log_interval: int = 1000, + ) -> None: + self.backend = backend + self.collection_name = collection_name + self.query_vectors = np.ascontiguousarray(query_vectors, dtype=np.float32) + self.truth_table = truth_table + self.search_k = search_k + self.metric_type = metric_type + self.num_rounds = num_rounds + self.batch_size = batch_size + self.log_interval = log_interval + + # Build search params in the format backends expect + if search_params is not None: + self.search_params = search_params + else: + self.search_params = { + "metric_type": metric_type, + "params": {}, + } + + self.result: Optional[SearchResult] = None + + def run(self) -> SearchResult: + """Run the search benchmark. + + Returns + ------- + SearchResult + Aggregate and per-interval statistics. + """ + nq = self.query_vectors.shape[0] + total_queries_planned = nq * self.num_rounds + k = self.search_k + + logger.info( + "Starting search benchmark: %s queries x %d rounds = %s total, " + "k=%d, batch_size=%d, log every %s queries", + f"{nq:,}", self.num_rounds, f"{total_queries_planned:,}", + k, self.batch_size, f"{self.log_interval:,}", + ) + + all_latencies: list[float] = [] + all_predicted: list[np.ndarray] = [] + all_truth: list[np.ndarray] = [] + intervals: list[IntervalStats] = [] + + # Latencies for the current logging interval + interval_latencies: list[float] = [] + interval_predicted: list[np.ndarray] = [] + interval_truth: list[np.ndarray] = [] + interval_idx = 0 + + total_queries = 0 + wall_start = time.time() + interval_start = wall_start + + for round_num in range(self.num_rounds): + # Shuffle query order each round (except the first) for + # realistic cache behavior + if round_num == 0: + order = np.arange(nq) + else: + order = np.random.permutation(nq) + + for batch_start in range(0, nq, self.batch_size): + batch_end = min(batch_start + self.batch_size, nq) + batch_idx = order[batch_start:batch_end] + batch_queries = self.query_vectors[batch_idx] + batch_truth = self.truth_table[batch_idx] + + # Timed search + t0 = time.perf_counter() + result_ids = self.backend.search( + name=self.collection_name, + query_vectors=batch_queries, + top_k=k, + search_params=self.search_params, + ) + elapsed_ms = (time.perf_counter() - t0) * 1000.0 + + batch_n = batch_end - batch_start + per_query_ms = elapsed_ms / batch_n + + # Record per-query latency + for _ in range(batch_n): + all_latencies.append(per_query_ms) + interval_latencies.append(per_query_ms) + + predicted_arr = np.array(result_ids, dtype=np.int64) + all_predicted.append(predicted_arr) + all_truth.append(batch_truth) + interval_predicted.append(predicted_arr) + interval_truth.append(batch_truth) + + total_queries += batch_n + + # Check if we should log an interval + if total_queries >= (interval_idx + 1) * self.log_interval: + stats = self._compute_interval( + interval_idx=interval_idx, + wall_start=wall_start, + interval_start=interval_start, + total_queries=total_queries, + interval_latencies=interval_latencies, + interval_predicted=interval_predicted, + interval_truth=interval_truth, + ) + intervals.append(stats) + self._log_stats(stats) + + # Reset interval accumulators + interval_latencies = [] + interval_predicted = [] + interval_truth = [] + interval_start = time.time() + interval_idx += 1 + + wall_elapsed = time.time() - wall_start + + # Final stats across all queries + lat_arr = np.array(all_latencies) + pred_all = np.concatenate(all_predicted, axis=0) + truth_all = np.concatenate(all_truth, axis=0) + recall = _recall_at_k(pred_all, truth_all, k) + + self.result = SearchResult( + total_queries=total_queries, + total_wall_sec=wall_elapsed, + qps=total_queries / wall_elapsed if wall_elapsed > 0 else 0, + recall_at_k=recall, + search_k=k, + truth_k=self.truth_table.shape[1], + latency_p50_ms=float(np.percentile(lat_arr, 50)), + latency_p90_ms=float(np.percentile(lat_arr, 90)), + latency_p99_ms=float(np.percentile(lat_arr, 99)), + latency_mean_ms=float(np.mean(lat_arr)), + intervals=[asdict(s) for s in intervals], + ) + + logger.info( + "Search benchmark complete: %s queries in %.2f s " + "(%.1f QPS, recall@%d=%.4f)", + f"{total_queries:,}", wall_elapsed, self.result.qps, + k, recall, + ) + return self.result + + def save(self, output_dir: str) -> str: + """Save search results to *output_dir*. + + Returns the path to the JSON results file. + """ + os.makedirs(output_dir, exist_ok=True) + path = os.path.join(output_dir, "search_results.json") + with open(path, "w") as f: + json.dump(self.result.to_dict(), f, indent=2, default=str) + logger.info("Search results saved to %s", path) + return path + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + def _compute_interval( + self, + interval_idx: int, + wall_start: float, + interval_start: float, + total_queries: int, + interval_latencies: list[float], + interval_predicted: list[np.ndarray], + interval_truth: list[np.ndarray], + ) -> IntervalStats: + now = time.time() + wall_elapsed = now - wall_start + interval_elapsed = now - interval_start + + lat_arr = np.array(interval_latencies) + pred = np.concatenate(interval_predicted, axis=0) + truth = np.concatenate(interval_truth, axis=0) + recall = _recall_at_k(pred, truth, self.search_k) + iq = len(interval_latencies) + + return IntervalStats( + interval_index=interval_idx, + wall_clock_sec=wall_elapsed, + total_queries=total_queries, + interval_queries=iq, + qps_cumulative=total_queries / wall_elapsed if wall_elapsed > 0 else 0, + qps_interval=iq / interval_elapsed if interval_elapsed > 0 else 0, + recall_at_k=recall, + latency_p50_ms=float(np.percentile(lat_arr, 50)), + latency_p90_ms=float(np.percentile(lat_arr, 90)), + latency_p99_ms=float(np.percentile(lat_arr, 99)), + latency_mean_ms=float(np.mean(lat_arr)), + ) + + @staticmethod + def _log_stats(stats: IntervalStats) -> None: + logger.info( + "[Interval %d] queries=%s cumQPS=%.1f intQPS=%.1f " + "recall@k=%.4f P50=%.2fms P90=%.2fms P99=%.2fms", + stats.interval_index, + f"{stats.total_queries:,}", + stats.qps_cumulative, + stats.qps_interval, + stats.recall_at_k, + stats.latency_p50_ms, + stats.latency_p90_ms, + stats.latency_p99_ms, + ) From 69164d5f876477db053a48f3246fba9e5c0983c3 Mon Sep 17 00:00:00 2001 From: Charles Date: Mon, 6 Apr 2026 15:28:14 -0400 Subject: [PATCH 2/4] Update README.md --- vdb_benchmark/README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/vdb_benchmark/README.md b/vdb_benchmark/README.md index 880430d9..e1128b04 100644 --- a/vdb_benchmark/README.md +++ b/vdb_benchmark/README.md @@ -25,16 +25,18 @@ pip3 install -e ./ ## Deploying a Standalone Milvus Instance Stand-alone instances are available via Docker containers in the stacks directory. -> stacks -> └── milvus -> ├── cluster -> └── standalone -> ├── minio -> │   ├── .env.example -> │   └── docker-compose.yml -> └── s3 -> ├── .env.example -> └── docker-compose-s3.yml +``` + stacks + └── milvus + ├── cluster + └── standalone + ├── minio + │   ├── .env.example + │   └── docker-compose.yml + └── s3 + ├── .env.example + └── docker-compose-s3.yml +``` For each specific instance, copy the `.env.example` file to `.env` and update the values as needed. ```bash From 18f487aae062490d7687a7979998d09d45950cfe Mon Sep 17 00:00:00 2001 From: Devasena Inupakutika Date: Tue, 19 May 2026 15:46:50 +0000 Subject: [PATCH 3/4] added tests, pgvector backend fixes, pyproject.toml additions for standalone vdb modular workflow for different DB backends --- pyproject.toml | 37 +- tests/configs/elasticsearch_5k_hnsw.yaml | 43 + tests/configs/milvus_10k_hnsw.yaml | 42 + tests/configs/pgvector_5k_hnsw.yaml | 47 + tests/configs/vectordb_readme.md | 21 + tests/run_elasticsearch_5k_hnsw.sh | 90 ++ tests/run_milvus_10k_hnsw.sh | 78 ++ tests/run_pgvector_5k_hnsw.sh | 84 ++ tests/unit/test_vdb_modular_fake_backend.py | 243 +++++ uv.lock | 959 ++++++++++-------- vdb_benchmark/pyproject.toml | 28 + .../benchmark/backends/pgvector/backend.py | 501 ++++++--- 12 files changed, 1608 insertions(+), 565 deletions(-) create mode 100644 tests/configs/elasticsearch_5k_hnsw.yaml create mode 100644 tests/configs/milvus_10k_hnsw.yaml create mode 100644 tests/configs/pgvector_5k_hnsw.yaml create mode 100644 tests/configs/vectordb_readme.md create mode 100755 tests/run_elasticsearch_5k_hnsw.sh create mode 100755 tests/run_milvus_10k_hnsw.sh create mode 100755 tests/run_pgvector_5k_hnsw.sh create mode 100644 tests/unit/test_vdb_modular_fake_backend.py diff --git a/pyproject.toml b/pyproject.toml index 80545fdd..2d1514c9 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,10 +35,39 @@ full = [ "dlio-benchmark", ] vectordb = [ - "pymilvus>=2.4.0", - "numpy>=1.24", - "pandas>=2.0", - "tabulate>=0.9", + "pymilvus>=2.4.0", + "psycopg2-binary>=2.9", + "pgvector>=0.2", + "elasticsearch>=8.0", + "numpy>=1.24", + "pandas>=2.0", + "pyyaml>=6.0", + "tabulate>=0.9", +] + +vectordb-milvus = [ + "pymilvus>=2.4.0", + "numpy>=1.24", + "pandas>=2.0", + "pyyaml>=6.0", + "tabulate>=0.9", +] + +vectordb-pgvector = [ + "psycopg2-binary>=2.9", + "pgvector>=0.2", + "numpy>=1.24", + "pandas>=2.0", + "pyyaml>=6.0", + "tabulate>=0.9", +] + +vectordb-elasticsearch = [ + "elasticsearch>=8.0", + "numpy>=1.24", + "pandas>=2.0", + "pyyaml>=6.0", + "tabulate>=0.9", ] [project.urls] diff --git a/tests/configs/elasticsearch_5k_hnsw.yaml b/tests/configs/elasticsearch_5k_hnsw.yaml new file mode 100644 index 00000000..07f0a72e --- /dev/null +++ b/tests/configs/elasticsearch_5k_hnsw.yaml @@ -0,0 +1,43 @@ +backend: elasticsearch +mode: both + +database: + host: http://localhost:9200 + +dataset: + collection_name: pr316_es_hnsw_5k + num_vectors: 5000 + dimension: 64 + distribution: uniform + block_size: 1000 + batch_size: 500 + seed: 42 + +query: + num_query_vectors: 100 + query_seed: 99 + +ground_truth: + truth_k: 20 + truth_mode: precomputed + +index: + index_type: HNSW + metric_type: COSINE + index_params: + m: 16 + ef_construction: 64 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 2 + search_batch_size: 5 + log_interval: 50 + search_params: + num_candidates: 100 + +workflow: + force: true + compact: false + monitor_interval: 2 diff --git a/tests/configs/milvus_10k_hnsw.yaml b/tests/configs/milvus_10k_hnsw.yaml new file mode 100644 index 00000000..421dd7ba --- /dev/null +++ b/tests/configs/milvus_10k_hnsw.yaml @@ -0,0 +1,42 @@ +backend: milvus +mode: both + +database: + host: 127.0.0.1 + port: 19530 + +dataset: + collection_name: pr316_milvus_hnsw_10k + num_vectors: 10000 + dimension: 128 + distribution: uniform + block_size: 2000 + batch_size: 500 + seed: 42 + +query: + num_query_vectors: 200 + query_seed: 99 + +ground_truth: + truth_k: 50 + +index: + index_type: HNSW + metric_type: COSINE + index_params: + M: 16 + efConstruction: 100 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 2 + search_batch_size: 10 + search_params: + ef: 64 + +workflow: + force: true + compact: true + monitor_interval: 2 diff --git a/tests/configs/pgvector_5k_hnsw.yaml b/tests/configs/pgvector_5k_hnsw.yaml new file mode 100644 index 00000000..4dcd66ba --- /dev/null +++ b/tests/configs/pgvector_5k_hnsw.yaml @@ -0,0 +1,47 @@ +backend: pgvector +mode: both + +database: + host: 127.0.0.1 + port: 5432 + dbname: postgres + user: postgres + password: postgres + +dataset: + collection_name: pr316_pgvector_hnsw_5k + num_vectors: 5000 + dimension: 64 + distribution: uniform + block_size: 1000 + batch_size: 500 + seed: 42 + +query: + num_query_vectors: 100 + query_seed: 99 + +ground_truth: + truth_k: 20 + truth_mode: precomputed + +index: + index_type: HNSW + metric_type: COSINE + index_params: + M: 16 + efConstruction: 64 + num_shards: 1 + +search: + search_k: 10 + num_search_rounds: 2 + search_batch_size: 5 + log_interval: 50 + search_params: + ef_search: 40 + +workflow: + force: true + compact: false + monitor_interval: 2 diff --git a/tests/configs/vectordb_readme.md b/tests/configs/vectordb_readme.md new file mode 100644 index 00000000..7c01114a --- /dev/null +++ b/tests/configs/vectordb_readme.md @@ -0,0 +1,21 @@ +# VDB modular runner smoke tests + +These smoke tests validate the modular backend-agnostic VDB runner added under `vdb_benchmark/vdbbench/benchmark`. + +They are intentionally small and are meant for PR validation, not official MLPerf Storage result generation. + +## What these tests cover + +| Script | Backend | Size | Index | +|---|---:|---:|---| +| `run_milvus_10k_hnsw.sh` | Milvus | 10,000 vectors | HNSW | +| `run_pgvector_5k_hnsw.sh` | PostgreSQL + pgvector | 5,000 vectors | HNSW | +| `run_elasticsearch_5k_hnsw.sh` | Elasticsearch | 5,000 vectors | HNSW | + +Each script verifies that the modular runner writes: + +```text +query_vectors.npy +ground_truth.npz +search_results.json +benchmark_meta.json diff --git a/tests/run_elasticsearch_5k_hnsw.sh b/tests/run_elasticsearch_5k_hnsw.sh new file mode 100755 index 00000000..3e68e1cc --- /dev/null +++ b/tests/run_elasticsearch_5k_hnsw.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Prefer git repo root when available. +ROOT_DIR="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null || true)" + +# Fallback: walk upward until pyproject.toml is found. +if [[ -z "${ROOT_DIR}" ]]; then + SEARCH_DIR="${SCRIPT_DIR}" + while [[ "${SEARCH_DIR}" != "/" && ! -f "${SEARCH_DIR}/pyproject.toml" ]]; do + SEARCH_DIR="$(dirname "${SEARCH_DIR}")" + done + ROOT_DIR="${SEARCH_DIR}" +fi + +cd "${ROOT_DIR}" + +if [[ ! -f "pyproject.toml" ]]; then + echo "ERROR: pyproject.toml not found." + echo "Current directory: $(pwd)" + echo "Set ROOT_DIR manually or run this script from inside the repo." + exit 1 +fi + + +CONFIG="${CONFIG:-tests/configs/elasticsearch_5k_hnsw.yaml}" +OUT_DIR="${OUT_DIR:-/tmp/pr316_elasticsearch_hnsw_5k}" + +ELASTICSEARCH_HOST="${ELASTICSEARCH_HOST:-http://localhost:9200}" +ELASTICSEARCH_API_KEY="${ELASTICSEARCH_API_KEY:-}" +ELASTICSEARCH_CLOUD_ID="${ELASTICSEARCH_CLOUD_ID:-}" + +echo "Running Elasticsearch modular VDB smoke test" +echo "Config: ${CONFIG}" +echo "Output: ${OUT_DIR}" +echo "Elasticsearch host: ${ELASTICSEARCH_HOST}" + +uv sync --extra vectordb-elasticsearch +uv pip install -e ./vdb_benchmark + +rm -rf "${OUT_DIR}" + +if [[ -n "${ELASTICSEARCH_API_KEY}" || -n "${ELASTICSEARCH_CLOUD_ID}" ]]; then + ELASTICSEARCH__HOST="${ELASTICSEARCH_HOST}" \ + ELASTICSEARCH__API_KEY="${ELASTICSEARCH_API_KEY}" \ + ELASTICSEARCH__CLOUD_ID="${ELASTICSEARCH_CLOUD_ID}" \ + uv run python -m vdbbench.benchmark \ + --config "${CONFIG}" \ + --backend elasticsearch \ + --mode both \ + --force \ + --output-dir "${OUT_DIR}" +else + ELASTICSEARCH__HOST="${ELASTICSEARCH_HOST}" \ + uv run python -m vdbbench.benchmark \ + --config "${CONFIG}" \ + --backend elasticsearch \ + --mode both \ + --force \ + --output-dir "${OUT_DIR}" +fi + +test -f "${OUT_DIR}/query_vectors.npy" +test -f "${OUT_DIR}/ground_truth.npz" +test -f "${OUT_DIR}/search_results.json" +test -f "${OUT_DIR}/benchmark_meta.json" + +uv run python - < 0, results +assert 0 <= results["recall_at_k"] <= 1, results + +print("Elasticsearch smoke test passed") +print(json.dumps({ + "total_queries": results["total_queries"], + "qps": results["qps"], + "recall_at_k": results["recall_at_k"], + "latency_p50_ms": results["latency_p50_ms"], + "latency_p99_ms": results["latency_p99_ms"], +}, indent=2)) +PY diff --git a/tests/run_milvus_10k_hnsw.sh b/tests/run_milvus_10k_hnsw.sh new file mode 100755 index 00000000..a9936248 --- /dev/null +++ b/tests/run_milvus_10k_hnsw.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Prefer git repo root when available. +ROOT_DIR="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null || true)" + +# Fallback: walk upward until pyproject.toml is found. +if [[ -z "${ROOT_DIR}" ]]; then + SEARCH_DIR="${SCRIPT_DIR}" + while [[ "${SEARCH_DIR}" != "/" && ! -f "${SEARCH_DIR}/pyproject.toml" ]]; do + SEARCH_DIR="$(dirname "${SEARCH_DIR}")" + done + ROOT_DIR="${SEARCH_DIR}" +fi + +cd "${ROOT_DIR}" + +if [[ ! -f "pyproject.toml" ]]; then + echo "ERROR: pyproject.toml not found." + echo "Current directory: $(pwd)" + echo "Set ROOT_DIR manually or run this script from inside the repo." + exit 1 +fi + +CONFIG="${CONFIG:-tests/configs/milvus_10k_hnsw.yaml}" +OUT_DIR="${OUT_DIR:-/tmp/pr316_milvus_hnsw_10k}" + +MILVUS_HOST="${MILVUS_HOST:-127.0.0.1}" +MILVUS_PORT="${MILVUS_PORT:-19530}" + +echo "Running Milvus modular VDB smoke test" +echo "Repo root: $(pwd)" +echo "Config: ${CONFIG}" +echo "Output: ${OUT_DIR}" +echo "Milvus: ${MILVUS_HOST}:${MILVUS_PORT}" + +uv sync --extra vectordb-milvus +uv pip install -e ./vdb_benchmark + +rm -rf "${OUT_DIR}" + +MILVUS__HOST="${MILVUS_HOST}" \ +MILVUS__PORT="${MILVUS_PORT}" \ +uv run python -m vdbbench.benchmark \ + --config "${CONFIG}" \ + --backend milvus \ + --mode both \ + --force \ + --output-dir "${OUT_DIR}" + +test -f "${OUT_DIR}/query_vectors.npy" +test -f "${OUT_DIR}/ground_truth.npz" +test -f "${OUT_DIR}/search_results.json" +test -f "${OUT_DIR}/benchmark_meta.json" + +uv run python - < 0, results +assert 0 <= results["recall_at_k"] <= 1, results + +print("Milvus smoke test passed") +print(json.dumps({ + "total_queries": results["total_queries"], + "qps": results["qps"], + "recall_at_k": results["recall_at_k"], + "latency_p50_ms": results.get("latency_p50_ms"), + "latency_p99_ms": results.get("latency_p99_ms"), +}, indent=2)) +PY diff --git a/tests/run_pgvector_5k_hnsw.sh b/tests/run_pgvector_5k_hnsw.sh new file mode 100755 index 00000000..ef22fe6f --- /dev/null +++ b/tests/run_pgvector_5k_hnsw.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Prefer git repo root when available. +ROOT_DIR="$(git -C "${SCRIPT_DIR}" rev-parse --show-toplevel 2>/dev/null || true)" + +# Fallback: walk upward until pyproject.toml is found. +if [[ -z "${ROOT_DIR}" ]]; then + SEARCH_DIR="${SCRIPT_DIR}" + while [[ "${SEARCH_DIR}" != "/" && ! -f "${SEARCH_DIR}/pyproject.toml" ]]; do + SEARCH_DIR="$(dirname "${SEARCH_DIR}")" + done + ROOT_DIR="${SEARCH_DIR}" +fi + +cd "${ROOT_DIR}" + +if [[ ! -f "pyproject.toml" ]]; then + echo "ERROR: pyproject.toml not found." + echo "Current directory: $(pwd)" + echo "Set ROOT_DIR manually or run this script from inside the repo." + exit 1 +fi + + +CONFIG="${CONFIG:-tests/configs/pgvector_5k_hnsw.yaml}" +OUT_DIR="${OUT_DIR:-/tmp/pr316_pgvector_hnsw_5k}" + +PGVECTOR_HOST="${PGVECTOR_HOST:-127.0.0.1}" +PGVECTOR_PORT="${PGVECTOR_PORT:-5432}" +PGVECTOR_DBNAME="${PGVECTOR_DBNAME:-postgres}" +PGVECTOR_USER="${PGVECTOR_USER:-postgres}" +PGVECTOR_PASSWORD="${PGVECTOR_PASSWORD:-postgres}" + +echo "Running pgvector modular VDB smoke test" +echo "Config: ${CONFIG}" +echo "Output: ${OUT_DIR}" +echo "pgvector: ${PGVECTOR_USER}@${PGVECTOR_HOST}:${PGVECTOR_PORT}/${PGVECTOR_DBNAME}" + +uv sync --extra vectordb-pgvector +uv pip install -e ./vdb_benchmark + +rm -rf "${OUT_DIR}" + +PGVECTOR__HOST="${PGVECTOR_HOST}" \ +PGVECTOR__PORT="${PGVECTOR_PORT}" \ +PGVECTOR__DBNAME="${PGVECTOR_DBNAME}" \ +PGVECTOR__USER="${PGVECTOR_USER}" \ +PGVECTOR__PASSWORD="${PGVECTOR_PASSWORD}" \ +uv run python -m vdbbench.benchmark \ + --config "${CONFIG}" \ + --backend pgvector \ + --mode both \ + --force \ + --output-dir "${OUT_DIR}" + +test -f "${OUT_DIR}/query_vectors.npy" +test -f "${OUT_DIR}/ground_truth.npz" +test -f "${OUT_DIR}/search_results.json" +test -f "${OUT_DIR}/benchmark_meta.json" + +uv run python - < 0, results +assert 0 <= results["recall_at_k"] <= 1, results + +print("pgvector smoke test passed") +print(json.dumps({ + "total_queries": results["total_queries"], + "qps": results["qps"], + "recall_at_k": results["recall_at_k"], + "latency_p50_ms": results["latency_p50_ms"], + "latency_p99_ms": results["latency_p99_ms"], +}, indent=2)) +PY diff --git a/tests/unit/test_vdb_modular_fake_backend.py b/tests/unit/test_vdb_modular_fake_backend.py new file mode 100644 index 00000000..6a41a3bb --- /dev/null +++ b/tests/unit/test_vdb_modular_fake_backend.py @@ -0,0 +1,243 @@ +"""Unit smoke test for the modular VDB benchmark runner. + +This test uses an in-memory exact backend so CI can exercise the modular +orchestrator without requiring Milvus, PostgreSQL/pgvector, or Elasticsearch. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[2] +VDB_BENCHMARK_DIR = ROOT_DIR / "vdb_benchmark" + +if str(VDB_BENCHMARK_DIR) not in sys.path: + sys.path.insert(0, str(VDB_BENCHMARK_DIR)) + +import json +from typing import Any, Dict, List, Optional + +import numpy as np + +from vdbbench.benchmark.backends.base import ( + CollectionInfo, + IndexProgress, + VectorDBBackend, +) +from vdbbench.benchmark.orchestrator import ( + BenchmarkConfig, + BenchmarkOrchestrator, +) + + +class FakeExactBackend(VectorDBBackend): + """Minimal in-memory backend implementing the modular VDB contract.""" + + def __init__(self) -> None: + self.connected = False + self.collections: Dict[str, Dict[str, Any]] = {} + + def connect(self, **kwargs: Any) -> None: + self.connected = True + + def disconnect(self) -> None: + self.connected = False + + def create_collection( + self, + name: str, + dimension: int, + metric_type: str = "COSINE", + index_type: str = "FLAT", + index_params: Optional[Dict[str, Any]] = None, + num_shards: int = 1, + force: bool = False, + ) -> CollectionInfo: + if force or name not in self.collections: + self.collections[name] = { + "dimension": dimension, + "metric_type": metric_type, + "index_type": index_type, + "index_params": index_params or {}, + "num_shards": num_shards, + "ids": [], + "vectors": [], + } + + return CollectionInfo( + name=name, + dimension=dimension, + metric_type=metric_type, + index_type=index_type, + row_count=self.row_count(name), + ) + + def collection_exists(self, name: str) -> bool: + return name in self.collections + + def drop_collection(self, name: str) -> None: + self.collections.pop(name, None) + + def insert_batch( + self, + name: str, + ids: np.ndarray, + vectors: np.ndarray, + ) -> int: + collection = self.collections[name] + ids = np.asarray(ids, dtype=np.int64) + vectors = np.asarray(vectors, dtype=np.float32) + + collection["ids"].extend(ids.tolist()) + collection["vectors"].append(vectors) + + return int(len(ids)) + + def flush(self, name: str) -> None: + # In-memory backend has no pending writes. + return None + + def compact(self, name: str) -> None: + # In-memory backend has no compaction step. + return None + + def search( + self, + name: str, + query_vectors: np.ndarray, + top_k: int, + search_params: Optional[Dict[str, Any]] = None, + ) -> List[List[int]]: + collection = self.collections[name] + + ids = np.asarray(collection["ids"], dtype=np.int64) + vectors = np.vstack(collection["vectors"]).astype(np.float32) + query_vectors = np.asarray(query_vectors, dtype=np.float32) + + # The generator normalizes vectors. The ground-truth path ranks by + # inner product, which is equivalent for normalized COSINE/IP/L2 cases. + scores = query_vectors @ vectors.T + order = np.argsort(-scores, axis=1)[:, :top_k] + + return ids[order].tolist() + + def row_count(self, name: str) -> int: + if name not in self.collections: + return 0 + return len(self.collections[name]["ids"]) + + def get_index_progress(self, name: str) -> IndexProgress: + return IndexProgress( + is_ready=True, + total_rows=self.row_count(name), + indexed_rows=self.row_count(name), + pending_rows=0, + status="ready", + ) + + def list_collections(self) -> List[str]: + return sorted(self.collections.keys()) + + def get_collection_info(self, name: str) -> Dict[str, Any]: + collection = self.collections[name] + return { + "name": name, + "row_count": self.row_count(name), + "dimension": collection["dimension"], + "metric_type": collection["metric_type"], + "index_type": collection["index_type"], + "schema": [ + {"name": "id", "type": "BIGINT", "primary_key": True}, + { + "name": "vector", + "type": f"VECTOR({collection['dimension']})", + "primary_key": False, + }, + ], + } + + def list_indexes(self, name: str) -> List[Dict[str, Any]]: + collection = self.collections[name] + return [ + { + "index_name": f"{name}_fake_exact_idx", + "index_type": collection["index_type"], + "params": collection["index_params"], + } + ] + + def drop_index( + self, + name: str, + index_name: Optional[str] = None, + ) -> None: + # No-op for in-memory backend. + return None + + +def test_modular_orchestrator_with_fake_exact_backend(tmp_path): + cfg = BenchmarkConfig( + mode="both", + collection_name="ci_fake_exact", + num_vectors=1000, + dimension=32, + distribution="uniform", + seed=42, + block_size=250, + batch_size=100, + num_query_vectors=50, + query_seed=99, + truth_k=10, + search_k=10, + num_search_rounds=1, + search_batch_size=5, + log_interval=25, + force=True, + index_type="FLAT", + metric_type="COSINE", + ) + + backend = FakeExactBackend() + backend.connect() + + orchestrator = BenchmarkOrchestrator(cfg, backend) + summary = orchestrator.run() + paths = orchestrator.save(str(tmp_path)) + + assert summary["total_vectors_inserted"] == 1000 + assert summary["blocks_processed"] == 4 + assert summary["num_query_vectors"] == 50 + assert summary["truth_k"] == 10 + assert summary["truth_table_shape"] == [50, 10] + + assert summary["search_total_queries"] == 50 + assert summary["search_qps"] > 0 + assert summary["search_recall_at_k"] == 1.0 + assert summary["search_latency_mean_ms"] >= 0 + + assert "query_vectors" in paths + assert "ground_truth" in paths + assert "search_results" in paths + assert "meta" in paths + + query_vectors = np.load(paths["query_vectors"]) + assert query_vectors.shape == (50, 32) + + ground_truth = np.load(paths["ground_truth"]) + assert ground_truth["truth_table"].shape == (50, 10) + assert ground_truth["query_vectors"].shape == (50, 32) + + with open(paths["search_results"], "r", encoding="utf-8") as f: + search_results = json.load(f) + + assert search_results["total_queries"] == 50 + assert search_results["qps"] > 0 + assert search_results["recall_at_k"] == 1.0 + + with open(paths["meta"], "r", encoding="utf-8") as f: + meta = json.load(f) + + assert meta["config"]["collection_name"] == "ci_fake_exact" + assert meta["config"]["num_vectors"] == 1000 + assert meta["config"]["dimension"] == 32 diff --git a/uv.lock b/uv.lock index adc1a57e..928e85ce 100755 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 3 requires-python = "==3.12.*" resolution-markers = [ "sys_platform == 'win32'", @@ -10,16 +11,29 @@ resolution-markers = [ name = "absl-py" version = "2.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543 } +sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750 }, + { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" }, ] [[package]] name = "antlr4-python3-runtime" version = "4.9.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034 } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] [[package]] name = "argon2-cffi" @@ -28,9 +42,9 @@ 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 } +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 }, + { 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]] @@ -40,18 +54,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441 } +sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/db8af0df73c1cf454f71b2bbe5e356b8c1f8041c979f505b3d3186e520a9/argon2_cffi_bindings-25.1.0.tar.gz", hash = "sha256:b957f3e6ea4d55d820e40ff76f450952807013d361a65d7f28acc0acbf29229d", size = 1783441, upload-time = "2025-07-30T10:02:05.147Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121 }, - { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177 }, - { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090 }, - { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246 }, - { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126 }, - { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343 }, - { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777 }, - { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180 }, - { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715 }, - { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149 }, + { url = "https://files.pythonhosted.org/packages/1d/57/96b8b9f93166147826da5f90376e784a10582dd39a393c99bb62cfcf52f0/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:aecba1723ae35330a008418a91ea6cfcedf6d31e5fbaa056a166462ff066d500", size = 54121, upload-time = "2025-07-30T10:01:50.815Z" }, + { url = "https://files.pythonhosted.org/packages/0a/08/a9bebdb2e0e602dde230bdde8021b29f71f7841bd54801bcfd514acb5dcf/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2630b6240b495dfab90aebe159ff784d08ea999aa4b0d17efa734055a07d2f44", size = 29177, upload-time = "2025-07-30T10:01:51.681Z" }, + { url = "https://files.pythonhosted.org/packages/b6/02/d297943bcacf05e4f2a94ab6f462831dc20158614e5d067c35d4e63b9acb/argon2_cffi_bindings-25.1.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:7aef0c91e2c0fbca6fc68e7555aa60ef7008a739cbe045541e438373bc54d2b0", size = 31090, upload-time = "2025-07-30T10:01:53.184Z" }, + { url = "https://files.pythonhosted.org/packages/c1/93/44365f3d75053e53893ec6d733e4a5e3147502663554b4d864587c7828a7/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e021e87faa76ae0d413b619fe2b65ab9a037f24c60a1e6cc43457ae20de6dc6", size = 81246, upload-time = "2025-07-30T10:01:54.145Z" }, + { url = "https://files.pythonhosted.org/packages/09/52/94108adfdd6e2ddf58be64f959a0b9c7d4ef2fa71086c38356d22dc501ea/argon2_cffi_bindings-25.1.0-cp39-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d3e924cfc503018a714f94a49a149fdc0b644eaead5d1f089330399134fa028a", size = 87126, upload-time = "2025-07-30T10:01:55.074Z" }, + { url = "https://files.pythonhosted.org/packages/72/70/7a2993a12b0ffa2a9271259b79cc616e2389ed1a4d93842fac5a1f923ffd/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87b72589133f0346a1cb8d5ecca4b933e3c9b64656c9d175270a000e73b288d", size = 80343, upload-time = "2025-07-30T10:01:56.007Z" }, + { url = "https://files.pythonhosted.org/packages/78/9a/4e5157d893ffc712b74dbd868c7f62365618266982b64accab26bab01edc/argon2_cffi_bindings-25.1.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1db89609c06afa1a214a69a462ea741cf735b29a57530478c06eb81dd403de99", size = 86777, upload-time = "2025-07-30T10:01:56.943Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/15777dfde1c29d96de7f18edf4cc94c385646852e7c7b0320aa91ccca583/argon2_cffi_bindings-25.1.0-cp39-abi3-win32.whl", hash = "sha256:473bcb5f82924b1becbb637b63303ec8d10e84c8d241119419897a26116515d2", size = 27180, upload-time = "2025-07-30T10:01:57.759Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c6/a759ece8f1829d1f162261226fbfd2c6832b3ff7657384045286d2afa384/argon2_cffi_bindings-25.1.0-cp39-abi3-win_amd64.whl", hash = "sha256:a98cd7d17e9f7ce244c0803cad3c23a7d379c301ba618a5fa76a67d116618b98", size = 31715, upload-time = "2025-07-30T10:01:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, ] [[package]] @@ -62,27 +76,27 @@ dependencies = [ { name = "six" }, { name = "wheel" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290 } +sdist = { url = "https://files.pythonhosted.org/packages/f3/af/4182184d3c338792894f34a62672919db7ca008c89abee9b564dd34d8029/astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872", size = 18290, upload-time = "2019-12-22T18:12:13.129Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732 }, + { url = "https://files.pythonhosted.org/packages/2b/03/13dde6512ad7b4557eb792fbcf0c653af6076b81e5941d36ec61f7ce6028/astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8", size = 12732, upload-time = "2019-12-22T18:12:11.297Z" }, ] [[package]] name = "cachetools" version = "7.0.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526 } +sdist = { url = "https://files.pythonhosted.org/packages/76/7b/1755ed2c6bfabd1d98b37ae73152f8dcf94aa40fee119d163c19ed484704/cachetools-7.0.6.tar.gz", hash = "sha256:e5d524d36d65703a87243a26ff08ad84f73352adbeafb1cde81e207b456aaf24", size = 37526, upload-time = "2026-04-20T19:02:23.289Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976 }, + { url = "https://files.pythonhosted.org/packages/fe/c4/cf76242a5da1410917107ff14551764aa405a5fd10cd10cf9a5ca8fa77f4/cachetools-7.0.6-py3-none-any.whl", hash = "sha256:4e94956cfdd3086f12042cdd29318f5ced3893014f7d0d059bf3ead3f85b7f8b", size = 13976, upload-time = "2026-04-20T19:02:21.187Z" }, ] [[package]] name = "certifi" version = "2026.2.25" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029 } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684 }, + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] [[package]] @@ -92,78 +106,78 @@ 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 } +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/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932 }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557 }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762 }, + { 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" }, ] [[package]] name = "charset-normalizer" version = "3.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363 } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154 }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191 }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674 }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259 }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276 }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161 }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452 }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272 }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622 }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056 }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751 }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563 }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265 }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229 }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277 }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817 }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455 }, + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] [[package]] name = "colorama" version = "0.4.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] [[package]] name = "coverage" version = "7.13.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554 }, - { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908 }, - { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419 }, - { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159 }, - { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270 }, - { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538 }, - { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821 }, - { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191 }, - { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337 }, - { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404 }, - { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903 }, - { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780 }, - { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093 }, - { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900 }, - { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515 }, - { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346 }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] [[package]] @@ -174,8 +188,8 @@ dependencies = [ { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404 }, - { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619 }, + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, ] [[package]] @@ -183,7 +197,7 @@ name = "cuda-pathfinder" version = "1.5.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/66/0c02bd330e7d976f83fa68583d6198d76f23581bcbb5c0e98a6148f326e5/cuda_pathfinder-1.5.0-py3-none-any.whl", hash = "sha256:498f90a9e9de36044a7924742aecce11c50c49f735f1bc53e05aa46de9ea4110", size = 49739 }, + { url = "https://files.pythonhosted.org/packages/93/66/0c02bd330e7d976f83fa68583d6198d76f23581bcbb5c0e98a6148f326e5/cuda_pathfinder-1.5.0-py3-none-any.whl", hash = "sha256:498f90a9e9de36044a7924742aecce11c50c49f735f1bc53e05aa46de9ea4110", size = 49739, upload-time = "2026-03-24T21:14:30.869Z" }, ] [[package]] @@ -191,7 +205,7 @@ name = "cuda-toolkit" version = "13.0.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364 }, + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, ] [package.optional-dependencies] @@ -236,9 +250,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/9f/e04c2c79bd91937593d79bb480c83c67141922da26ba39cff6d5f38e1673/dgen_py-0.2.3.tar.gz", hash = "sha256:fbebb1fc6b24f77abc78baaec82218c6377c1a84d8caf2f055899c1cee050ecd", size = 208444 } +sdist = { url = "https://files.pythonhosted.org/packages/ad/9f/e04c2c79bd91937593d79bb480c83c67141922da26ba39cff6d5f38e1673/dgen_py-0.2.3.tar.gz", hash = "sha256:fbebb1fc6b24f77abc78baaec82218c6377c1a84d8caf2f055899c1cee050ecd", size = 208444, upload-time = "2026-04-16T02:44:30.045Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/42/b24dd7f7794b3a999290fa461d745caf9e1bad07643caf912f575b833b10/dgen_py-0.2.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:44eb5b802cf5cb721c76e30d1e94cbf86cc9d64dab44caef127f82fe6f253d6d", size = 392290 }, + { url = "https://files.pythonhosted.org/packages/55/42/b24dd7f7794b3a999290fa461d745caf9e1bad07643caf912f575b833b10/dgen_py-0.2.3-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:44eb5b802cf5cb721c76e30d1e94cbf86cc9d64dab44caef127f82fe6f253d6d", size = 392290, upload-time = "2026-04-16T02:40:16.161Z" }, ] [[package]] @@ -264,13 +278,43 @@ dependencies = [ { name = "typing-extensions" }, ] +[[package]] +name = "elastic-transport" +version = "9.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "sniffio" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/3f/076b7cdc93ff4c7953a76e8e64830e6ff0352cd713d338abc601f142b75f/elastic_transport-9.4.0.tar.gz", hash = "sha256:4eff263c8011dd950451b72be567a2484b814a89c70081053d6ae6addeab52e2", size = 77819, upload-time = "2026-05-05T14:40:54.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/17/8d6ee5cd6218f03b195cc017e9e8e09b3df48ebc307903d09cf9f04ff0fe/elastic_transport-9.4.0-py3-none-any.whl", hash = "sha256:2dbb907ededa14e6ff5be058f8737bbba3926bd1b1a40dbc98a471285fa2cb3c", size = 65358, upload-time = "2026-05-05T14:40:52.545Z" }, +] + +[[package]] +name = "elasticsearch" +version = "9.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "elastic-transport" }, + { name = "python-dateutil" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/2d/413775b172c6983c7cdbbdc3ac2be71eb00679510a536081801b387a2c8c/elasticsearch-9.4.0.tar.gz", hash = "sha256:95e38e130b1d01438b19343dfa0458e1857a7df8e2e30cbf23a72182b03f05ff", size = 907191, upload-time = "2026-05-06T11:12:51.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/a1/193be12421e0db8e786c1592b449f9cadef240970d391bf94b7bb07ca195/elasticsearch-9.4.0-py3-none-any.whl", hash = "sha256:e20095ba40229f4562f7cc951883c7c62a017435f94dbe0c21526f58ba411885", size = 992735, upload-time = "2026-05-06T11:12:48.267Z" }, +] + [[package]] name = "filelock" version = "3.25.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480 } +sdist = { url = "https://files.pythonhosted.org/packages/94/b8/00651a0f559862f3bb7d6f7477b192afe3f583cc5e26403b44e59a55ab34/filelock-3.25.2.tar.gz", hash = "sha256:b64ece2b38f4ca29dd3e810287aa8c48182bbecd1ae6e9ae126c9b35f1382694", size = 40480, upload-time = "2026-03-11T20:45:38.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759 }, + { url = "https://files.pythonhosted.org/packages/a4/a5/842ae8f0c08b61d6484b52f99a03510a3a72d23141942d216ebe81fefbce/filelock-3.25.2-py3-none-any.whl", hash = "sha256:ca8afb0da15f229774c9ad1b455ed96e85a81373065fb10446672f64444ddf70", size = 26759, upload-time = "2026-03-11T20:45:37.437Z" }, ] [[package]] @@ -278,25 +322,25 @@ name = "flatbuffers" version = "25.12.19" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661 }, + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, ] [[package]] name = "fsspec" version = "2026.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547 } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595 }, + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, ] [[package]] name = "gast" version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630 } +sdist = { url = "https://files.pythonhosted.org/packages/91/f6/e73969782a2ecec280f8a176f2476149dd9dba69d5f8779ec6108a7721e6/gast-0.7.0.tar.gz", hash = "sha256:0bb14cd1b806722e91ddbab6fb86bba148c22b40e7ff11e248974e04c8adfdae", size = 33630, upload-time = "2025-11-29T15:30:05.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966 }, + { url = "https://files.pythonhosted.org/packages/1d/33/f1c6a276de27b7d7339a34749cc33fa87f077f921969c47185d34a887ae2/gast-0.7.0-py3-none-any.whl", hash = "sha256:99cbf1365633a74099f69c59bd650476b96baa5ef196fec88032b00b31ba36f7", size = 22966, upload-time = "2025-11-29T15:30:03.983Z" }, ] [[package]] @@ -306,9 +350,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430 } +sdist = { url = "https://files.pythonhosted.org/packages/35/4a/0bd53b36ff0323d10d5f24ebd67af2de10a1117f5cf4d7add90df92756f1/google-pasta-0.2.0.tar.gz", hash = "sha256:c9f2c8dfc8f96d0d5808299920721be30c9eec37f2389f28904f454565c8a16e", size = 40430, upload-time = "2020-03-13T18:57:50.34Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471 }, + { url = "https://files.pythonhosted.org/packages/a3/de/c648ef6835192e6e2cc03f40b19eeda4382c49b5bafb43d88b931c4c74ac/google_pasta-0.2.0-py3-none-any.whl", hash = "sha256:b32482794a366b5366a32c92a9a9201b107821889935a02b3e51f6b432ea84ed", size = 57471, upload-time = "2020-03-13T18:57:48.872Z" }, ] [[package]] @@ -318,18 +362,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905 } +sdist = { url = "https://files.pythonhosted.org/packages/b7/48/af6173dbca4454f4637a4678b67f52ca7e0c1ed7d5894d89d434fecede05/grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257", size = 12978905, upload-time = "2026-03-30T08:49:10.502Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616 }, - { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204 }, - { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866 }, - { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060 }, - { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121 }, - { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811 }, - { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860 }, - { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132 }, - { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904 }, - { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944 }, + { url = "https://files.pythonhosted.org/packages/5c/e8/a2b749265eb3415abc94f2e619bbd9e9707bebdda787e61c593004ec927a/grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0", size = 6015616, upload-time = "2026-03-30T08:47:13.428Z" }, + { url = "https://files.pythonhosted.org/packages/3e/97/b1282161a15d699d1e90c360df18d19165a045ce1c343c7f313f5e8a0b77/grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2", size = 12014204, upload-time = "2026-03-30T08:47:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/d319c6e997b50c155ac5a8cb12f5173d5b42677510e886d250d50264949d/grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de", size = 6563866, upload-time = "2026-03-30T08:47:18.588Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f6/fdd975a2cb4d78eb67769a7b3b3830970bfa2e919f1decf724ae4445f42c/grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921", size = 7273060, upload-time = "2026-03-30T08:47:21.113Z" }, + { url = "https://files.pythonhosted.org/packages/db/f0/a3deb5feba60d9538a962913e37bd2e69a195f1c3376a3dd44fe0427e996/grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411", size = 6782121, upload-time = "2026-03-30T08:47:23.827Z" }, + { url = "https://files.pythonhosted.org/packages/ca/84/36c6dcfddc093e108141f757c407902a05085e0c328007cb090d56646cdf/grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd", size = 7383811, upload-time = "2026-03-30T08:47:26.517Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ef/f3a77e3dc5b471a0ec86c564c98d6adfa3510d38f8ee99010410858d591e/grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f", size = 8393860, upload-time = "2026-03-30T08:47:29.439Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/9d4d27ed7f33d109c50d6b5ce578a9914aa68edab75d65869a17e630a8d1/grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f", size = 7830132, upload-time = "2026-03-30T08:47:33.254Z" }, + { url = "https://files.pythonhosted.org/packages/14/e4/9990b41c6d7a44e1e9dee8ac11d7a9802ba1378b40d77468a7761d1ad288/grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193", size = 4140904, upload-time = "2026-03-30T08:47:35.319Z" }, + { url = "https://files.pythonhosted.org/packages/2f/2c/296f6138caca1f4b92a31ace4ae1b87dab692fc16a7a3417af3bb3c805bf/grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff", size = 4880944, upload-time = "2026-03-30T08:47:37.831Z" }, ] [[package]] @@ -339,16 +383,16 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526 } +sdist = { url = "https://files.pythonhosted.org/packages/db/33/acd0ce6863b6c0d7735007df01815403f5589a21ff8c2e1ee2587a38f548/h5py-3.16.0.tar.gz", hash = "sha256:a0dbaad796840ccaa67a4c144a0d0c8080073c34c76d5a6941d6818678ef2738", size = 446526, upload-time = "2026-03-06T13:49:08.07Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604 }, - { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940 }, - { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852 }, - { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250 }, - { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108 }, - { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216 }, - { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868 }, - { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286 }, + { url = "https://files.pythonhosted.org/packages/c8/c0/5d4119dba94093bbafede500d3defd2f5eab7897732998c04b54021e530b/h5py-3.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5313566f4643121a78503a473f0fb1e6dcc541d5115c44f05e037609c565c4d", size = 3685604, upload-time = "2026-03-06T13:48:04.198Z" }, + { url = "https://files.pythonhosted.org/packages/b0/42/c84efcc1d4caebafb1ecd8be4643f39c85c47a80fe254d92b8b43b1eadaf/h5py-3.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:42b012933a83e1a558c673176676a10ce2fd3759976a0fedee1e672d1e04fc9d", size = 3061940, upload-time = "2026-03-06T13:48:05.783Z" }, + { url = "https://files.pythonhosted.org/packages/89/84/06281c82d4d1686fde1ac6b0f307c50918f1c0151062445ab3b6fa5a921d/h5py-3.16.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:ff24039e2573297787c3063df64b60aab0591980ac898329a08b0320e0cf2527", size = 5198852, upload-time = "2026-03-06T13:48:07.482Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/1a19e42cd43cc1365e127db6aae85e1c671da1d9a5d746f4d34a50edb577/h5py-3.16.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:dfc21898ff025f1e8e67e194965a95a8d4754f452f83454538f98f8a3fcb207e", size = 5405250, upload-time = "2026-03-06T13:48:09.628Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/9790c1655eabeb85b92b1ecab7d7e62a2069e53baefd58c98f0909c7a948/h5py-3.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:698dd69291272642ffda44a0ecd6cd3bda5faf9621452d255f57ce91487b9794", size = 5190108, upload-time = "2026-03-06T13:48:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/51/d7/ab693274f1bd7e8c5f9fdd6c7003a88d59bedeaf8752716a55f532924fbb/h5py-3.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b2c02b0a160faed5fb33f1ba8a264a37ee240b22e049ecc827345d0d9043074", size = 5419216, upload-time = "2026-03-06T13:48:13.322Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/0976b235cf29ead553e22f2fb6385a8252b533715e00d0ae52ed7b900582/h5py-3.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:96b422019a1c8975c2d5dadcf61d4ba6f01c31f92bbde6e4649607885fe502d6", size = 3182868, upload-time = "2026-03-06T13:48:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/14/d9/866b7e570b39070f92d47b0ff1800f0f8239b6f9e45f02363d7112336c1f/h5py-3.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:39c2838fb1e8d97bcf1755e60ad1f3dd76a7b2a475928dc321672752678b96db", size = 2653286, upload-time = "2026-03-06T13:48:17.279Z" }, ] [[package]] @@ -360,27 +404,27 @@ dependencies = [ { name = "omegaconf" }, { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494 } +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547 }, + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, ] [[package]] name = "idna" version = "3.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] [[package]] name = "iniconfig" version = "2.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] [[package]] @@ -390,9 +434,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115 } +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899 }, + { 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]] @@ -409,35 +453,35 @@ dependencies = [ { name = "packaging" }, { name = "rich" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/e9/400582e5f3dbd815d2a373f7de7717dd1bc8349274e9ac1b9ac47410b123/keras-3.13.2.tar.gz", hash = "sha256:62f0123488ac87c929c988617e14f293f7bc993811837d08bb37eff77adc85a9", size = 1155875 } +sdist = { url = "https://files.pythonhosted.org/packages/09/e9/400582e5f3dbd815d2a373f7de7717dd1bc8349274e9ac1b9ac47410b123/keras-3.13.2.tar.gz", hash = "sha256:62f0123488ac87c929c988617e14f293f7bc993811837d08bb37eff77adc85a9", size = 1155875, upload-time = "2026-01-30T00:35:13.796Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/b5/ea85873abc99dc64a7a27ff1a8dbfdc7dbb57d4e5d1a423abc11217af4f1/keras-3.13.2-py3-none-any.whl", hash = "sha256:14b2afc0f9c636cc295d28efc36aae42fc52e7b892c950eec33f3befe4d22fb5", size = 1513769 }, + { url = "https://files.pythonhosted.org/packages/28/b5/ea85873abc99dc64a7a27ff1a8dbfdc7dbb57d4e5d1a423abc11217af4f1/keras-3.13.2-py3-none-any.whl", hash = "sha256:14b2afc0f9c636cc295d28efc36aae42fc52e7b892c950eec33f3befe4d22fb5", size = 1513769, upload-time = "2026-01-30T00:35:09.664Z" }, ] [[package]] name = "libclang" version = "18.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612 } +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/ca35e19a4f142adffa27e3d652196b7362fa612243e2b916845d801454fc/libclang-18.1.1.tar.gz", hash = "sha256:a1214966d08d73d971287fc3ead8dfaf82eb07fb197680d8b3859dbbbbf78250", size = 39612, upload-time = "2024-03-17T16:04:37.434Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045 }, - { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641 }, - { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207 }, - { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943 }, - { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972 }, - { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606 }, - { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494 }, - { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083 }, - { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112 }, + { url = "https://files.pythonhosted.org/packages/4b/49/f5e3e7e1419872b69f6f5e82ba56e33955a74bd537d8a1f5f1eff2f3668a/libclang-18.1.1-1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:0b2e143f0fac830156feb56f9231ff8338c20aecfe72b4ffe96f19e5a1dbb69a", size = 25836045, upload-time = "2024-06-30T17:40:31.646Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e5/fc61bbded91a8830ccce94c5294ecd6e88e496cc85f6704bf350c0634b70/libclang-18.1.1-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:6f14c3f194704e5d09769108f03185fce7acaf1d1ae4bbb2f30a72c2400cb7c5", size = 26502641, upload-time = "2024-03-18T15:52:26.722Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/1df62b44db2583375f6a8a5e2ca5432bbdc3edb477942b9b7c848c720055/libclang-18.1.1-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:83ce5045d101b669ac38e6da8e58765f12da2d3aafb3b9b98d88b286a60964d8", size = 26420207, upload-time = "2024-03-17T15:00:26.63Z" }, + { url = "https://files.pythonhosted.org/packages/1d/fc/716c1e62e512ef1c160e7984a73a5fc7df45166f2ff3f254e71c58076f7c/libclang-18.1.1-py2.py3-none-manylinux2010_x86_64.whl", hash = "sha256:c533091d8a3bbf7460a00cb6c1a71da93bffe148f172c7d03b1c31fbf8aa2a0b", size = 24515943, upload-time = "2024-03-17T16:03:45.942Z" }, + { url = "https://files.pythonhosted.org/packages/3c/3d/f0ac1150280d8d20d059608cf2d5ff61b7c3b7f7bcf9c0f425ab92df769a/libclang-18.1.1-py2.py3-none-manylinux2014_aarch64.whl", hash = "sha256:54dda940a4a0491a9d1532bf071ea3ef26e6dbaf03b5000ed94dd7174e8f9592", size = 23784972, upload-time = "2024-03-17T16:12:47.677Z" }, + { url = "https://files.pythonhosted.org/packages/fe/2f/d920822c2b1ce9326a4c78c0c2b4aa3fde610c7ee9f631b600acb5376c26/libclang-18.1.1-py2.py3-none-manylinux2014_armv7l.whl", hash = "sha256:cf4a99b05376513717ab5d82a0db832c56ccea4fd61a69dbb7bccf2dfb207dbe", size = 20259606, upload-time = "2024-03-17T16:17:42.437Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/de1db8c6d413597076a4259cea409b83459b2db997c003578affdd32bf66/libclang-18.1.1-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:69f8eb8f65c279e765ffd28aaa7e9e364c776c17618af8bff22a8df58677ff4f", size = 24921494, upload-time = "2024-03-17T16:14:20.132Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2d/3f480b1e1d31eb3d6de5e3ef641954e5c67430d5ac93b7fa7e07589576c7/libclang-18.1.1-py2.py3-none-win_amd64.whl", hash = "sha256:4dd2d3b82fab35e2bf9ca717d7b63ac990a3519c7e312f19fa8e86dcc712f7fb", size = 26415083, upload-time = "2024-03-17T16:42:21.703Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/e01dc4cc79779cd82d77888a88ae2fa424d93b445ad4f6c02bfc18335b70/libclang-18.1.1-py2.py3-none-win_arm64.whl", hash = "sha256:3f0e1f49f04d3cd198985fea0511576b0aee16f9ff0e0f0cad7f9c57ec3c20e8", size = 22361112, upload-time = "2024-03-17T16:42:59.565Z" }, ] [[package]] name = "markdown" version = "3.10.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805 } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180 }, + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, ] [[package]] @@ -447,37 +491,37 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070 } +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321 }, + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, ] [[package]] name = "markupsafe" version = "3.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313 } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615 }, - { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020 }, - { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332 }, - { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947 }, - { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962 }, - { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760 }, - { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529 }, - { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015 }, - { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540 }, - { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105 }, - { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906 }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, ] [[package]] name = "mdurl" version = "0.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] [[package]] @@ -491,9 +535,9 @@ dependencies = [ { 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 } +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 }, + { 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]] @@ -503,13 +547,13 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314 } +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927 }, - { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464 }, - { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002 }, - { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222 }, - { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793 }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, ] [[package]] @@ -539,9 +583,35 @@ test = [ { name = "pytest-mock" }, ] vectordb = [ + { name = "elasticsearch" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pgvector" }, + { name = "psycopg2-binary" }, + { name = "pymilvus" }, + { name = "pyyaml" }, + { name = "tabulate" }, +] +vectordb-elasticsearch = [ + { name = "elasticsearch" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "pyyaml" }, + { name = "tabulate" }, +] +vectordb-milvus = [ { name = "numpy" }, { name = "pandas" }, { name = "pymilvus" }, + { name = "pyyaml" }, + { name = "tabulate" }, +] +vectordb-pgvector = [ + { name = "numpy" }, + { name = "pandas" }, + { name = "pgvector" }, + { name = "psycopg2-binary" }, + { name = "pyyaml" }, { name = "tabulate" }, ] @@ -549,86 +619,107 @@ vectordb = [ requires-dist = [ { name = "dlio-benchmark", git = "https://github.com/russfellows/dlio_benchmark.git?branch=feat%2Fparquet-dgen-streaming" }, { name = "dlio-benchmark", marker = "extra == 'full'", git = "https://github.com/russfellows/dlio_benchmark.git?branch=feat%2Fparquet-dgen-streaming" }, + { name = "elasticsearch", marker = "extra == 'vectordb'", specifier = ">=8.0" }, + { name = "elasticsearch", marker = "extra == 'vectordb-elasticsearch'", specifier = ">=8.0" }, { name = "minio", specifier = ">=7.2.20" }, { name = "numpy", marker = "extra == 'vectordb'", specifier = ">=1.24" }, + { name = "numpy", marker = "extra == 'vectordb-elasticsearch'", specifier = ">=1.24" }, + { name = "numpy", marker = "extra == 'vectordb-milvus'", specifier = ">=1.24" }, + { name = "numpy", marker = "extra == 'vectordb-pgvector'", specifier = ">=1.24" }, { name = "packaging", specifier = ">=21.0" }, { name = "pandas", marker = "extra == 'vectordb'", specifier = ">=2.0" }, + { name = "pandas", marker = "extra == 'vectordb-elasticsearch'", specifier = ">=2.0" }, + { name = "pandas", marker = "extra == 'vectordb-milvus'", specifier = ">=2.0" }, + { name = "pandas", marker = "extra == 'vectordb-pgvector'", specifier = ">=2.0" }, + { name = "pgvector", marker = "extra == 'vectordb'", specifier = ">=0.2" }, + { name = "pgvector", marker = "extra == 'vectordb-pgvector'", specifier = ">=0.2" }, { name = "psutil", specifier = ">=5.9" }, + { name = "psycopg2-binary", marker = "extra == 'vectordb'", specifier = ">=2.9" }, + { name = "psycopg2-binary", marker = "extra == 'vectordb-pgvector'", specifier = ">=2.9" }, { name = "pyarrow" }, { name = "pymilvus", marker = "extra == 'vectordb'", specifier = ">=2.4.0" }, + { name = "pymilvus", marker = "extra == 'vectordb-milvus'", specifier = ">=2.4.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=7.0" }, { name = "pytest-cov", marker = "extra == 'test'", specifier = ">=4.0" }, { name = "pytest-mock", marker = "extra == 'test'", specifier = ">=3.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "pyyaml", specifier = ">=6.0" }, + { name = "pyyaml", marker = "extra == 'vectordb'", specifier = ">=6.0" }, + { name = "pyyaml", marker = "extra == 'vectordb-elasticsearch'", specifier = ">=6.0" }, + { name = "pyyaml", marker = "extra == 'vectordb-milvus'", specifier = ">=6.0" }, + { name = "pyyaml", marker = "extra == 'vectordb-pgvector'", specifier = ">=6.0" }, { name = "rich", specifier = ">=13.0" }, { name = "s3dlio", specifier = ">=0.9.95" }, { name = "s3torchconnector", specifier = ">=1.5.0" }, { name = "tabulate", marker = "extra == 'vectordb'", specifier = ">=0.9" }, + { name = "tabulate", marker = "extra == 'vectordb-elasticsearch'", specifier = ">=0.9" }, + { name = "tabulate", marker = "extra == 'vectordb-milvus'", specifier = ">=0.9" }, + { name = "tabulate", marker = "extra == 'vectordb-pgvector'", specifier = ">=0.9" }, ] +provides-extras = ["test", "full", "vectordb", "vectordb-milvus", "vectordb-pgvector", "vectordb-elasticsearch"] [[package]] name = "mpi4py" version = "4.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/62/74/28ea85b0b949cad827ea50720e00e814e88c8fd536c27c3c491e4f025724/mpi4py-4.1.1.tar.gz", hash = "sha256:eb2c8489bdbc47fdc6b26ca7576e927a11b070b6de196a443132766b3d0a2a22", size = 500518 } +sdist = { url = "https://files.pythonhosted.org/packages/62/74/28ea85b0b949cad827ea50720e00e814e88c8fd536c27c3c491e4f025724/mpi4py-4.1.1.tar.gz", hash = "sha256:eb2c8489bdbc47fdc6b26ca7576e927a11b070b6de196a443132766b3d0a2a22", size = 500518, upload-time = "2025-10-10T13:55:20.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/b3/2e7df40608f2188dca16e38f8030add1071f06b1cd94dd8a4e16b9acbd84/mpi4py-4.1.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1586f5d1557abed9cba7e984d18f32e787b353be0986e599974db177ae36329a", size = 1422849 }, - { url = "https://files.pythonhosted.org/packages/6d/ed/970bd3edc0e614eccc726fa406255b88f728a8bc059e81f96f28d6ede0af/mpi4py-4.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ba85e4778d63c750226de95115c92b709f38d7e661be660a275da4f0992ee197", size = 1326982 }, - { url = "https://files.pythonhosted.org/packages/5d/c3/f9a5d1f9ba52ac6386bf3d3550027f42a6b102b0432113cc43294420feb2/mpi4py-4.1.1-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0a8332884626994d9ef48da233dc7a0355f4868dd7ff59f078d5813a2935b930", size = 1373127 }, - { url = "https://files.pythonhosted.org/packages/84/d1/1fe75025df801d817ed49371c719559f742f3f263323442d34dbe3366af3/mpi4py-4.1.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e0352860f0b3e18bc0dcb47e42e583ccb9472f89752d711a6fca46a38670554", size = 1225134 }, - { url = "https://files.pythonhosted.org/packages/40/44/d653fec0e4ca8181645da4bfb2763017625e5b3f151b208fadd932cb1766/mpi4py-4.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:0f46dfe666a599e4bd2641116b2b4852a3ed9d37915edf98fae471d666663128", size = 1478863 }, - { url = "https://files.pythonhosted.org/packages/ff/2c/e201cd4828555f10306a5439875cbd0ecfba766ace01ff5c6df43f795650/mpi4py-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4403a7cec985be9963efc626193e6df3f63f5ada0c26373c28e640e623e56c3", size = 1669517 }, - { url = "https://files.pythonhosted.org/packages/7b/53/18d978c3a19deecf38217ce54319e6c9162fec3569c4256c039b66eac2f4/mpi4py-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a2ffccc9f3a8c7c957403faad594d650c60234ac08cbedf45beaa96602debe9", size = 1454721 }, - { url = "https://files.pythonhosted.org/packages/ee/15/b908d1d23a4bd2bd7b2e98de5df23b26e43145119fe294728bf89211b935/mpi4py-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3d9b619bf197a290f7fd67eb61b1c2a5c204afd9621651a50dc0b1c1280d45", size = 1448977 }, - { url = "https://files.pythonhosted.org/packages/5d/19/088a2d37e80e0feb7851853b2a71cbe6f9b18bdf0eab680977864ea83aab/mpi4py-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0699c194db5d95fc2085711e4e0013083bd7ae9a88438e1fd64ddb67e9b0cf9e", size = 1318737 }, - { url = "https://files.pythonhosted.org/packages/97/3a/526261f39bf096e5ff396d18b76740a58d872425612ff84113dd85c2c08e/mpi4py-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:0abf5490c3d49c30542b461bfc5ad88dd7d147a4bdb456b7163640577fdfef88", size = 1725676 }, + { url = "https://files.pythonhosted.org/packages/36/b3/2e7df40608f2188dca16e38f8030add1071f06b1cd94dd8a4e16b9acbd84/mpi4py-4.1.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1586f5d1557abed9cba7e984d18f32e787b353be0986e599974db177ae36329a", size = 1422849, upload-time = "2025-10-10T13:53:40.082Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/970bd3edc0e614eccc726fa406255b88f728a8bc059e81f96f28d6ede0af/mpi4py-4.1.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ba85e4778d63c750226de95115c92b709f38d7e661be660a275da4f0992ee197", size = 1326982, upload-time = "2025-10-10T13:53:42.32Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c3/f9a5d1f9ba52ac6386bf3d3550027f42a6b102b0432113cc43294420feb2/mpi4py-4.1.1-cp310-abi3-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0a8332884626994d9ef48da233dc7a0355f4868dd7ff59f078d5813a2935b930", size = 1373127, upload-time = "2025-10-10T13:53:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/84/d1/1fe75025df801d817ed49371c719559f742f3f263323442d34dbe3366af3/mpi4py-4.1.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e0352860f0b3e18bc0dcb47e42e583ccb9472f89752d711a6fca46a38670554", size = 1225134, upload-time = "2025-10-10T13:53:45.583Z" }, + { url = "https://files.pythonhosted.org/packages/40/44/d653fec0e4ca8181645da4bfb2763017625e5b3f151b208fadd932cb1766/mpi4py-4.1.1-cp310-abi3-win_amd64.whl", hash = "sha256:0f46dfe666a599e4bd2641116b2b4852a3ed9d37915edf98fae471d666663128", size = 1478863, upload-time = "2025-10-10T13:53:47.178Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2c/e201cd4828555f10306a5439875cbd0ecfba766ace01ff5c6df43f795650/mpi4py-4.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4403a7cec985be9963efc626193e6df3f63f5ada0c26373c28e640e623e56c3", size = 1669517, upload-time = "2025-10-10T13:54:08.404Z" }, + { url = "https://files.pythonhosted.org/packages/7b/53/18d978c3a19deecf38217ce54319e6c9162fec3569c4256c039b66eac2f4/mpi4py-4.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8a2ffccc9f3a8c7c957403faad594d650c60234ac08cbedf45beaa96602debe9", size = 1454721, upload-time = "2025-10-10T13:54:09.977Z" }, + { url = "https://files.pythonhosted.org/packages/ee/15/b908d1d23a4bd2bd7b2e98de5df23b26e43145119fe294728bf89211b935/mpi4py-4.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ed3d9b619bf197a290f7fd67eb61b1c2a5c204afd9621651a50dc0b1c1280d45", size = 1448977, upload-time = "2025-10-10T13:54:11.65Z" }, + { url = "https://files.pythonhosted.org/packages/5d/19/088a2d37e80e0feb7851853b2a71cbe6f9b18bdf0eab680977864ea83aab/mpi4py-4.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0699c194db5d95fc2085711e4e0013083bd7ae9a88438e1fd64ddb67e9b0cf9e", size = 1318737, upload-time = "2025-10-10T13:54:13.075Z" }, + { url = "https://files.pythonhosted.org/packages/97/3a/526261f39bf096e5ff396d18b76740a58d872425612ff84113dd85c2c08e/mpi4py-4.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:0abf5490c3d49c30542b461bfc5ad88dd7d147a4bdb456b7163640577fdfef88", size = 1725676, upload-time = "2025-10-10T13:54:14.681Z" }, ] [[package]] name = "mpmath" version = "1.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, ] [[package]] name = "namex" version = "0.1.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649 } +sdist = { url = "https://files.pythonhosted.org/packages/0c/c0/ee95b28f029c73f8d49d8f52edaed02a1d4a9acb8b69355737fdb1faa191/namex-0.1.0.tar.gz", hash = "sha256:117f03ccd302cc48e3f5c58a296838f6b89c83455ab8683a1e85f2a430aa4306", size = 6649, upload-time = "2025-05-26T23:17:38.918Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905 }, + { url = "https://files.pythonhosted.org/packages/b2/bc/465daf1de06409cdd4532082806770ee0d8d7df434da79c76564d0f69741/namex-0.1.0-py3-none-any.whl", hash = "sha256:e2012a474502f1e2251267062aae3114611f07df4224b6e06334c57b0f2ce87c", size = 5905, upload-time = "2025-05-26T23:17:37.695Z" }, ] [[package]] name = "networkx" version = "3.6.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504 }, + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, ] [[package]] name = "numpy" version = "2.4.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587 } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272 }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573 }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782 }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038 }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666 }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480 }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036 }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643 }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117 }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584 }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450 }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, ] [[package]] @@ -636,8 +727,8 @@ name = "nvidia-cublas" version = "13.1.0.3" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226 }, - { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236 }, + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, ] [[package]] @@ -645,8 +736,8 @@ name = "nvidia-cuda-cupti" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827 }, - { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597 }, + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, ] [[package]] @@ -654,8 +745,8 @@ name = "nvidia-cuda-nvrtc" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200 }, - { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449 }, + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, ] [[package]] @@ -663,8 +754,8 @@ name = "nvidia-cuda-runtime" version = "13.0.96" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060 }, - { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632 }, + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, ] [[package]] @@ -675,8 +766,8 @@ dependencies = [ { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201 }, - { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321 }, + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, ] [[package]] @@ -687,8 +778,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554 }, - { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489 }, + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, ] [[package]] @@ -696,8 +787,8 @@ name = "nvidia-cufile" version = "1.15.1.6" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672 }, - { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992 }, + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, ] [[package]] @@ -705,8 +796,8 @@ name = "nvidia-curand" version = "10.4.0.35" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106 }, - { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258 }, + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, ] [[package]] @@ -719,8 +810,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760 }, - { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980 }, + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, ] [[package]] @@ -731,8 +822,8 @@ dependencies = [ { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568 }, - { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937 }, + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, ] [[package]] @@ -740,8 +831,8 @@ name = "nvidia-cusparselt-cu13" version = "0.8.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277 }, - { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119 }, + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, ] [[package]] @@ -749,8 +840,8 @@ name = "nvidia-nccl-cu13" version = "2.28.9" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677 }, - { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177 }, + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, ] [[package]] @@ -758,8 +849,8 @@ name = "nvidia-nvjitlink" version = "13.0.88" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933 }, - { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748 }, + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, ] [[package]] @@ -767,8 +858,8 @@ name = "nvidia-nvshmem-cu13" version = "3.4.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947 }, - { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546 }, + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, ] [[package]] @@ -776,8 +867,8 @@ name = "nvidia-nvtx" version = "13.0.85" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047 }, - { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878 }, + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, ] [[package]] @@ -788,18 +879,18 @@ dependencies = [ { name = "antlr4-python3-runtime" }, { name = "pyyaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120 } +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500 }, + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, ] [[package]] name = "opt-einsum" version = "3.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004 } +sdist = { url = "https://files.pythonhosted.org/packages/8c/b9/2ac072041e899a52f20cf9510850ff58295003aa75525e58343591b0cbfb/opt_einsum-3.4.0.tar.gz", hash = "sha256:96ca72f1b886d148241348783498194c577fa30a8faac108586b14f1ba4473ac", size = 63004, upload-time = "2024-09-26T14:33:24.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932 }, + { url = "https://files.pythonhosted.org/packages/23/cd/066e86230ae37ed0be70aae89aabf03ca8d9f39c8aea0dec8029455b5540/opt_einsum-3.4.0-py3-none-any.whl", hash = "sha256:69bb92469f86a1565195ece4ac0323943e83477171b91d24c35afe028a90d7cd", size = 71932, upload-time = "2024-09-26T14:33:23.039Z" }, ] [[package]] @@ -809,51 +900,51 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/63/7b078bc36d5a206c21b03565a818ede38ff0fbf014e92085ec467ef10adb/optree-0.19.0.tar.gz", hash = "sha256:bc1991a948590756409e76be4e29efd4a487a185056d35db6c67619c19ea27a1", size = 175199 } +sdist = { url = "https://files.pythonhosted.org/packages/3d/63/7b078bc36d5a206c21b03565a818ede38ff0fbf014e92085ec467ef10adb/optree-0.19.0.tar.gz", hash = "sha256:bc1991a948590756409e76be4e29efd4a487a185056d35db6c67619c19ea27a1", size = 175199, upload-time = "2026-02-23T01:56:37.752Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2d/bf/5cbbf61a27f94797c3d9786f6230223023a943b60f5e893d52368f10b8b1/optree-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ec4b2ce49622c6be2c8634712b6c63cc274835bac89a56e3ab2ca863a32ff4b", size = 418100 }, - { url = "https://files.pythonhosted.org/packages/00/9e/65899e6470f5df289ccdbe9e228fb0cd0ae45ccda8e32c92d6efae1530ef/optree-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0978603623b4b1f794f05f6bbed0645cb7e219f4a5a349b2a2bd4514d84ac82", size = 388582 }, - { url = "https://files.pythonhosted.org/packages/d1/dc/f4826835be660181f1b4444ac92b51dda96d4634d3c2271e14598da7bf2a/optree-0.19.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c9e52c50ed3f3f8b1cf4e47a20a7c5e77175b4f84b2ecf390a76f0d1dd91da6", size = 407457 }, - { url = "https://files.pythonhosted.org/packages/ce/b0/89283ac1dd1ead3aa3d7a6b45a26846f457bded79a83b6828fc1ed9a6db3/optree-0.19.0-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:3fe3e5f7a30a7d08ddba0a34e48f5483f6c4d7bb710375434ad3633170c73c48", size = 471230 }, - { url = "https://files.pythonhosted.org/packages/2a/a2/47f620f87b0544b2e0eb0b3c661682bd0ea1c79f6e38f9147bc0f835c973/optree-0.19.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8315527e1f14a91173fe6871847da7b949048ec61ff8b3e507fc286e75b0aa3c", size = 469442 }, - { url = "https://files.pythonhosted.org/packages/84/e9/b9ae18404135de53809fb994b754ac0eac838d8c4dfa8a10a811d8dec91d/optree-0.19.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:938fb15d140ab65148f4e6975048facbef83a9210353fbedd471ac39e7544339", size = 468840 }, - { url = "https://files.pythonhosted.org/packages/0a/e5/a77df15a62b37bb14c81b5757e2a0573f57e7c06d125a410ad2cd7cefb72/optree-0.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b8209570340135a7e586c90f393f3c6359e8a49c40d783196721cc487e51d9c", size = 451408 }, - { url = "https://files.pythonhosted.org/packages/8c/43/1aa431cee19cd98c4229e468767021f9a92195d9431857e28198a3a3ce2f/optree-0.19.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1397dc925026917531a43fda32054ae1e77e5ed9bf8284bcae6354c19c26e14a", size = 412544 }, - { url = "https://files.pythonhosted.org/packages/5b/b9/b94fd3a116b80951d692a82f4135ae84b3d78bd1b092250aff76a3366138/optree-0.19.0-cp312-cp312-win32.whl", hash = "sha256:68f58e8f8b75c76c51e61e3dc2d9e94609bafb0e1a6459e6d525ced905cd9a74", size = 312033 }, - { url = "https://files.pythonhosted.org/packages/9e/7f/31fa1b2311038bfc355ad6e4e4e63d028719cb67fb3ebe6fb76ff2124105/optree-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5c44ca0f579ed3e0ca777a5711d4a6c1b374feacf1bb4fe9cfe85297b0c8d237", size = 335374 }, - { url = "https://files.pythonhosted.org/packages/09/86/863bc3f42f83113f5c6a5beaf4fec3c3481a76872f3244d0e64fb9ebd3b0/optree-0.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0461f796b4ade3fab519d821b0fa521f07e2af70206b76aac75fcfdc2e051fca", size = 345868 }, + { url = "https://files.pythonhosted.org/packages/2d/bf/5cbbf61a27f94797c3d9786f6230223023a943b60f5e893d52368f10b8b1/optree-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7ec4b2ce49622c6be2c8634712b6c63cc274835bac89a56e3ab2ca863a32ff4b", size = 418100, upload-time = "2026-02-23T01:55:05.282Z" }, + { url = "https://files.pythonhosted.org/packages/00/9e/65899e6470f5df289ccdbe9e228fb0cd0ae45ccda8e32c92d6efae1530ef/optree-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f0978603623b4b1f794f05f6bbed0645cb7e219f4a5a349b2a2bd4514d84ac82", size = 388582, upload-time = "2026-02-23T01:55:06.628Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dc/f4826835be660181f1b4444ac92b51dda96d4634d3c2271e14598da7bf2a/optree-0.19.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c9e52c50ed3f3f8b1cf4e47a20a7c5e77175b4f84b2ecf390a76f0d1dd91da6", size = 407457, upload-time = "2026-02-23T01:55:07.713Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b0/89283ac1dd1ead3aa3d7a6b45a26846f457bded79a83b6828fc1ed9a6db3/optree-0.19.0-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:3fe3e5f7a30a7d08ddba0a34e48f5483f6c4d7bb710375434ad3633170c73c48", size = 471230, upload-time = "2026-02-23T01:55:09.244Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a2/47f620f87b0544b2e0eb0b3c661682bd0ea1c79f6e38f9147bc0f835c973/optree-0.19.0-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8315527e1f14a91173fe6871847da7b949048ec61ff8b3e507fc286e75b0aa3c", size = 469442, upload-time = "2026-02-23T01:55:10.387Z" }, + { url = "https://files.pythonhosted.org/packages/84/e9/b9ae18404135de53809fb994b754ac0eac838d8c4dfa8a10a811d8dec91d/optree-0.19.0-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:938fb15d140ab65148f4e6975048facbef83a9210353fbedd471ac39e7544339", size = 468840, upload-time = "2026-02-23T01:55:11.419Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e5/a77df15a62b37bb14c81b5757e2a0573f57e7c06d125a410ad2cd7cefb72/optree-0.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b8209570340135a7e586c90f393f3c6359e8a49c40d783196721cc487e51d9c", size = 451408, upload-time = "2026-02-23T01:55:12.501Z" }, + { url = "https://files.pythonhosted.org/packages/8c/43/1aa431cee19cd98c4229e468767021f9a92195d9431857e28198a3a3ce2f/optree-0.19.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1397dc925026917531a43fda32054ae1e77e5ed9bf8284bcae6354c19c26e14a", size = 412544, upload-time = "2026-02-23T01:55:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/b94fd3a116b80951d692a82f4135ae84b3d78bd1b092250aff76a3366138/optree-0.19.0-cp312-cp312-win32.whl", hash = "sha256:68f58e8f8b75c76c51e61e3dc2d9e94609bafb0e1a6459e6d525ced905cd9a74", size = 312033, upload-time = "2026-02-23T01:55:15.101Z" }, + { url = "https://files.pythonhosted.org/packages/9e/7f/31fa1b2311038bfc355ad6e4e4e63d028719cb67fb3ebe6fb76ff2124105/optree-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:5c44ca0f579ed3e0ca777a5711d4a6c1b374feacf1bb4fe9cfe85297b0c8d237", size = 335374, upload-time = "2026-02-23T01:55:16.094Z" }, + { url = "https://files.pythonhosted.org/packages/09/86/863bc3f42f83113f5c6a5beaf4fec3c3481a76872f3244d0e64fb9ebd3b0/optree-0.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:0461f796b4ade3fab519d821b0fa521f07e2af70206b76aac75fcfdc2e051fca", size = 345868, upload-time = "2026-02-23T01:55:18.006Z" }, ] [[package]] name = "orjson" version = "3.11.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832 } +sdist = { url = "https://files.pythonhosted.org/packages/9d/1b/2024d06792d0779f9dbc51531b61c24f76c75b9f4ce05e6f3377a1814cea/orjson-3.11.8.tar.gz", hash = "sha256:96163d9cdc5a202703e9ad1b9ae757d5f0ca62f4fa0cc93d1f27b0e180cc404e", size = 5603832, upload-time = "2026-03-31T16:16:27.878Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233 }, - { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772 }, - { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946 }, - { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368 }, - { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540 }, - { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877 }, - { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837 }, - { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624 }, - { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904 }, - { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742 }, - { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806 }, - { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485 }, - { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966 }, - { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441 }, - { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364 }, + { url = "https://files.pythonhosted.org/packages/01/f6/8d58b32ab32d9215973a1688aebd098252ee8af1766c0e4e36e7831f0295/orjson-3.11.8-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1cd0b77e77c95758f8e1100139844e99f3ccc87e71e6fc8e1c027e55807c549f", size = 229233, upload-time = "2026-03-31T16:15:12.762Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/2ffe35e71f6b92622e8ea4607bf33ecf7dfb51b3619dcfabfd36cbe2d0a5/orjson-3.11.8-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:6a3d159d5ffa0e3961f353c4b036540996bf8b9697ccc38261c0eac1fd3347a6", size = 128772, upload-time = "2026-03-31T16:15:14.237Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/1f8682ae50d5c6897a563cb96bc106da8c9cb5b7b6e81a52e4cc086679b9/orjson-3.11.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76070a76e9c5ae661e2d9848f216980d8d533e0f8143e6ed462807b242e3c5e8", size = 131946, upload-time = "2026-03-31T16:15:15.607Z" }, + { url = "https://files.pythonhosted.org/packages/52/4b/5500f76f0eece84226e0689cb48dcde081104c2fa6e2483d17ca13685ffb/orjson-3.11.8-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54153d21520a71a4c82a0dbb4523e468941d549d221dc173de0f019678cf3813", size = 130368, upload-time = "2026-03-31T16:15:17.066Z" }, + { url = "https://files.pythonhosted.org/packages/da/4e/58b927e08fbe9840e6c920d9e299b051ea667463b1f39a56e668669f8508/orjson-3.11.8-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:469ac2125611b7c5741a0b3798cd9e5786cbad6345f9f400c77212be89563bec", size = 135540, upload-time = "2026-03-31T16:15:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/56/7c/ba7cb871cba1bcd5cd02ee34f98d894c6cea96353ad87466e5aef2429c60/orjson-3.11.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14778ffd0f6896aa613951a7fbf4690229aa7a543cb2bfbe9f358e08aafa9546", size = 146877, upload-time = "2026-03-31T16:15:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/0b/5d/eb9c25fc1386696c6a342cd361c306452c75e0b55e86ad602dd4827a7fd7/orjson-3.11.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea56a955056a6d6c550cf18b3348656a9d9a4f02e2d0c02cabf3c73f1055d506", size = 132837, upload-time = "2026-03-31T16:15:21.282Z" }, + { url = "https://files.pythonhosted.org/packages/37/87/5ddeb7fc1fbd9004aeccab08426f34c81a5b4c25c7061281862b015fce2b/orjson-3.11.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53a0f57e59a530d18a142f4d4ba6dfc708dc5fdedce45e98ff06b44930a2a48f", size = 133624, upload-time = "2026-03-31T16:15:22.641Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/90048793db94ee4b2fcec4ac8e5ddb077367637d6650be896b3494b79bb7/orjson-3.11.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9b48e274f8824567d74e2158199e269597edf00823a1b12b63d48462bbf5123e", size = 141904, upload-time = "2026-03-31T16:15:24.435Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cf/eb284847487821a5d415e54149a6449ba9bfc5872ce63ab7be41b8ec401c/orjson-3.11.8-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3f262401086a3960586af06c054609365e98407151f5ea24a62893a40d80dbbb", size = 423742, upload-time = "2026-03-31T16:15:26.155Z" }, + { url = "https://files.pythonhosted.org/packages/44/09/e12423d327071c851c13e76936f144a96adacfc037394dec35ac3fc8d1e8/orjson-3.11.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8e8c6218b614badf8e229b697865df4301afa74b791b6c9ade01d19a9953a942", size = 147806, upload-time = "2026-03-31T16:15:27.909Z" }, + { url = "https://files.pythonhosted.org/packages/b3/6d/37c2589ba864e582ffe7611643314785c6afb1f83c701654ef05daa8fcc7/orjson-3.11.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:093d489fa039ddade2db541097dbb484999fcc65fc2b0ff9819141e2ab364f25", size = 136485, upload-time = "2026-03-31T16:15:29.749Z" }, + { url = "https://files.pythonhosted.org/packages/be/c9/135194a02ab76b04ed9a10f68624b7ebd238bbe55548878b11ff15a0f352/orjson-3.11.8-cp312-cp312-win32.whl", hash = "sha256:e0950ed1bcb9893f4293fd5c5a7ee10934fbf82c4101c70be360db23ce24b7d2", size = 131966, upload-time = "2026-03-31T16:15:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/ed/9a/9796f8fbe3cf30ce9cb696748dbb535e5c87be4bf4fe2e9ca498ef1fa8cf/orjson-3.11.8-cp312-cp312-win_amd64.whl", hash = "sha256:3cf17c141617b88ced4536b2135c552490f07799f6ad565948ea07bef0dcb9a6", size = 127441, upload-time = "2026-03-31T16:15:33.333Z" }, + { url = "https://files.pythonhosted.org/packages/cc/47/5aaf54524a7a4a0dd09dd778f3fa65dd2108290615b652e23d944152bc8e/orjson-3.11.8-cp312-cp312-win_arm64.whl", hash = "sha256:48854463b0572cc87dac7d981aa72ed8bf6deedc0511853dc76b8bbd5482d36d", size = 127364, upload-time = "2026-03-31T16:15:34.748Z" }, ] [[package]] name = "packaging" version = "25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, ] [[package]] @@ -865,136 +956,167 @@ dependencies = [ { name = "python-dateutil" }, { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855 } +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921 }, - { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127 }, - { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577 }, - { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030 }, - { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468 }, - { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381 }, - { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993 }, - { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118 }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, +] + +[[package]] +name = "pgvector" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/6c/6d8b4b03b958c02fa8687ec6063c49d952a189f8c91ebbe51e877dfab8f7/pgvector-0.4.2.tar.gz", hash = "sha256:322cac0c1dc5d41c9ecf782bd9991b7966685dee3a00bc873631391ed949513a", size = 31354, upload-time = "2025-12-05T01:07:17.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" }, ] [[package]] name = "pillow" version = "12.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264 } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803 }, - { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601 }, - { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995 }, - { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012 }, - { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638 }, - { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540 }, - { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613 }, - { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745 }, - { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823 }, - { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367 }, - { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811 }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, ] [[package]] name = "pluggy" version = "1.6.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] [[package]] name = "protobuf" version = "7.34.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708 } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247 }, - { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753 }, - { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198 }, - { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267 }, - { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628 }, - { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901 }, - { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715 }, + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, ] [[package]] name = "psutil" version = "7.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740 } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090 }, - { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859 }, - { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560 }, - { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997 }, - { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972 }, - { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266 }, - { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737 }, - { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617 }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/60/a3624f79acea344c16fbef3a94d28b89a8042ddfb8f3e4ca83f538671409/psycopg2_binary-2.9.12.tar.gz", hash = "sha256:5ac9444edc768c02a6b6a591f070b8aae28ff3a99be57560ac996001580f294c", size = 379686, upload-time = "2026-04-21T09:40:34.304Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/9f/ef4ef3c8e15083df90ca35265cfd1a081a2f0cc07bb229c6314c6af817f4/psycopg2_binary-2.9.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5cdc05117180c5fa9c40eea8ea559ce64d73824c39d928b7da9fb5f6a9392433", size = 3712459, upload-time = "2026-04-20T23:34:30.549Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/3dd14e46ba48c1e1a6ec58ee599fa1b5efa00c246d5046cd903d0eeb1af1/psycopg2_binary-2.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3227a3bc228c10d21011a99245edca923e4e8bf461857e869a507d9a41fe9f6", size = 3822936, upload-time = "2026-04-20T23:34:32.77Z" }, + { url = "https://files.pythonhosted.org/packages/a6/f7/0640e4901119d8a9f7a1784b927f494e2198e213ceb593753d1f2c8b1b30/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:995ce929eede89db6254b50827e2b7fd61e50d11f0b116b29fffe4a2e53c4580", size = 4578676, upload-time = "2026-04-20T23:34:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/b0/55/44df3965b5f297c50cc0b1b594a31c67d6127a9d133045b8a66611b14dfb/psycopg2_binary-2.9.12-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9fe06d93e72f1c048e731a2e3e7854a5bfaa58fc736068df90b352cefe66f03f", size = 4274917, upload-time = "2026-04-20T23:34:37.982Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4b/74535248b1eac0c9336862e8617c765ac94dac76f9e25d7c4a79588c8907/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40e7b28b63aaf737cb3a1edc3a9bbc9a9f4ad3dcb7152e8c1130e4050eddcb7d", size = 5894843, upload-time = "2026-04-20T23:34:40.856Z" }, + { url = "https://files.pythonhosted.org/packages/f2/ba/f1bf8d2ae71868ad800b661099086ee52bc0f8d9f05be1acd8ebb06757cc/psycopg2_binary-2.9.12-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:89d19a9f7899e8eb0656a2b3a08e0da04c720a06db6e0033eab5928aabe60fa9", size = 4110556, upload-time = "2026-04-20T23:34:44.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/46/c15706c338403b7c420bcc0c2905aad116cc064545686d8bf85f1999ea00/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:612b965daee295ae2da8f8218ce1d274645dc76ef3f1abf6a0a94fd57eff876d", size = 3655714, upload-time = "2026-04-20T23:34:46.233Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7c/a2d5dc09b64a4564db242a0fe418fde7d33f6f8259dd2c5b9d7def00fb5a/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b9a339b79d37c1b45f3235265f07cdeb0cb5ad7acd2ac7720a5920989c17c24e", size = 3301154, upload-time = "2026-04-20T23:34:49.528Z" }, + { url = "https://files.pythonhosted.org/packages/c0/e8/cc8c9a4ce71461f9ec548d38cadc41dc184b34c73e6455450775a9334ccd/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:3471336e1acfd9c7fe507b8bad5af9317b6a89294f9eb37bd9a030bb7bebcdc6", size = 3048882, upload-time = "2026-04-20T23:34:51.86Z" }, + { url = "https://files.pythonhosted.org/packages/19/6a/31e2296bc0787c5ab75d3d118e40b239db8151b5192b90b77c72bc9256e9/psycopg2_binary-2.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7af18183109e23502c8b2ae7f6926c0882766f35b5175a4cd737ad825e4d7a1b", size = 3351298, upload-time = "2026-04-20T23:34:54.124Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/75f4e3e11203b590150abed2cf7794b9c9c9f7eceddae955191138b44dde/psycopg2_binary-2.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:398fcd4db988c7d7d3713e2b8e18939776fd3fb447052daae4f24fa39daede4c", size = 2757230, upload-time = "2026-04-20T23:34:56.242Z" }, ] [[package]] name = "pyarrow" version = "23.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336 } +sdist = { url = "https://files.pythonhosted.org/packages/88/22/134986a4cc224d593c1afde5494d18ff629393d74cc2eddb176669f234a4/pyarrow-23.0.1.tar.gz", hash = "sha256:b8c5873e33440b2bc2f4a79d2b47017a89c5a24116c055625e6f2ee50523f019", size = 1167336, upload-time = "2026-02-16T10:14:12.39Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575 }, - { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540 }, - { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940 }, - { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063 }, - { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045 }, - { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741 }, - { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678 }, + { url = "https://files.pythonhosted.org/packages/9a/4b/4166bb5abbfe6f750fc60ad337c43ecf61340fa52ab386da6e8dbf9e63c4/pyarrow-23.0.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f4b0dbfa124c0bb161f8b5ebb40f1a680b70279aa0c9901d44a2b5a20806039f", size = 34214575, upload-time = "2026-02-16T10:09:56.225Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/3f941e3734ac8088ea588b53e860baeddac8323ea40ce22e3d0baa865cc9/pyarrow-23.0.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:7707d2b6673f7de054e2e83d59f9e805939038eebe1763fe811ee8fa5c0cd1a7", size = 35832540, upload-time = "2026-02-16T10:10:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/88/7c/3d841c366620e906d54430817531b877ba646310296df42ef697308c2705/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:86ff03fb9f1a320266e0de855dee4b17da6794c595d207f89bba40d16b5c78b9", size = 44470940, upload-time = "2026-02-16T10:10:10.704Z" }, + { url = "https://files.pythonhosted.org/packages/2c/a5/da83046273d990f256cb79796a190bbf7ec999269705ddc609403f8c6b06/pyarrow-23.0.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:813d99f31275919c383aab17f0f455a04f5a429c261cc411b1e9a8f5e4aaaa05", size = 47586063, upload-time = "2026-02-16T10:10:17.95Z" }, + { url = "https://files.pythonhosted.org/packages/5b/3c/b7d2ebcff47a514f47f9da1e74b7949138c58cfeb108cdd4ee62f43f0cf3/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bf5842f960cddd2ef757d486041d57c96483efc295a8c4a0e20e704cbbf39c67", size = 48173045, upload-time = "2026-02-16T10:10:25.363Z" }, + { url = "https://files.pythonhosted.org/packages/43/b2/b40961262213beaba6acfc88698eb773dfce32ecdf34d19291db94c2bd73/pyarrow-23.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564baf97c858ecc03ec01a41062e8f4698abc3e6e2acd79c01c2e97880a19730", size = 50621741, upload-time = "2026-02-16T10:10:33.477Z" }, + { url = "https://files.pythonhosted.org/packages/f6/70/1fdda42d65b28b078e93d75d371b2185a61da89dda4def8ba6ba41ebdeb4/pyarrow-23.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:07deae7783782ac7250989a7b2ecde9b3c343a643f82e8a4df03d93b633006f0", size = 27620678, upload-time = "2026-02-16T10:10:39.31Z" }, ] [[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 } +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 }, + { 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 } +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/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { 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 }, - { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484 }, - { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636 }, - { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675 }, + { url = "https://files.pythonhosted.org/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" }, + { url = "https://files.pythonhosted.org/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" }, + { url = "https://files.pythonhosted.org/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" }, + { url = "https://files.pythonhosted.org/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" }, + { url = "https://files.pythonhosted.org/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" }, + { url = "https://files.pythonhosted.org/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" }, + { url = "https://files.pythonhosted.org/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" }, ] [[package]] name = "pydftracer" version = "2.0.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/12/b7f0bfb3888d569e630c110d977b00f0fa010e51ffc667524d7ecf0affea/pydftracer-2.0.2.tar.gz", hash = "sha256:3a2d92e17206e5a69f8e890b00b087943372680755c5e6c5e6e2b7b0814f5e92", size = 45448 } +sdist = { url = "https://files.pythonhosted.org/packages/a0/12/b7f0bfb3888d569e630c110d977b00f0fa010e51ffc667524d7ecf0affea/pydftracer-2.0.2.tar.gz", hash = "sha256:3a2d92e17206e5a69f8e890b00b087943372680755c5e6c5e6e2b7b0814f5e92", size = 45448, upload-time = "2025-10-20T06:09:20.566Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/8e/4c9cde902dbac10227dff0975e6d8ce6eab70358f4db38862fce2939d1c3/pydftracer-2.0.2-py3-none-any.whl", hash = "sha256:29962597d301387698be901137c62c4569635b05975e982904df63e19197df93", size = 18683 }, + { url = "https://files.pythonhosted.org/packages/c6/8e/4c9cde902dbac10227dff0975e6d8ce6eab70358f4db38862fce2939d1c3/pydftracer-2.0.2-py3-none-any.whl", hash = "sha256:29962597d301387698be901137c62c4569635b05975e982904df63e19197df93", size = 18683, upload-time = "2025-10-20T06:09:19.651Z" }, ] [[package]] name = "pygments" version = "2.20.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991 } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151 }, + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] [[package]] @@ -1011,9 +1133,9 @@ dependencies = [ { name = "requests" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2c/d7/c5d1381248a33975ccc864a0f980f93270ecc35354de8646c8a16443cccb/pymilvus-2.6.12.tar.gz", hash = "sha256:8323e990dc305e607fef525498eb779e42940a69e0691dde009cd02d48845f7a", size = 1584521 } +sdist = { url = "https://files.pythonhosted.org/packages/2c/d7/c5d1381248a33975ccc864a0f980f93270ecc35354de8646c8a16443cccb/pymilvus-2.6.12.tar.gz", hash = "sha256:8323e990dc305e607fef525498eb779e42940a69e0691dde009cd02d48845f7a", size = 1584521, upload-time = "2026-04-09T07:49:11.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ce/5d/44b0fa94c91503381e6f12298277f84f8e7b0bb00715ab89fc273c4d681e/pymilvus-2.6.12-py3-none-any.whl", hash = "sha256:69051b8b62712f157b2b50aeb7bde7fd7cdb5940aac0122094eb3cd58bc20f0d", size = 315183 }, + { url = "https://files.pythonhosted.org/packages/ce/5d/44b0fa94c91503381e6f12298277f84f8e7b0bb00715ab89fc273c4d681e/pymilvus-2.6.12-py3-none-any.whl", hash = "sha256:69051b8b62712f157b2b50aeb7bde7fd7cdb5940aac0122094eb3cd58bc20f0d", size = 315183, upload-time = "2026-04-09T07:49:09.013Z" }, ] [[package]] @@ -1027,9 +1149,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] [[package]] @@ -1041,9 +1163,9 @@ dependencies = [ { name = "pluggy" }, { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592 } +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876 }, + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, ] [[package]] @@ -1053,9 +1175,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036 } +sdist = { url = "https://files.pythonhosted.org/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095 }, + { url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, ] [[package]] @@ -1065,36 +1187,36 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "six" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] [[package]] name = "python-dotenv" version = "1.2.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135 } +sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101 }, + { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] [[package]] name = "pyyaml" version = "6.0.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960 } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063 }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973 }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116 }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011 }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870 }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089 }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181 }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658 }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003 }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344 }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, ] [[package]] @@ -1107,9 +1229,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947 }, + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] [[package]] @@ -1120,9 +1242,9 @@ dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582 } +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458 }, + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] [[package]] @@ -1132,10 +1254,10 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/bf/b17bf94e1fd7c58b2f93d53192b61271f14538b847d98fd40ef2cc652d61/s3dlio-0.9.95.tar.gz", hash = "sha256:55f79071d244cccf7a49714c33c024639a24723dd88c7cac629c63daa89d0d96", size = 1481201 } +sdist = { url = "https://files.pythonhosted.org/packages/13/bf/b17bf94e1fd7c58b2f93d53192b61271f14538b847d98fd40ef2cc652d61/s3dlio-0.9.95.tar.gz", hash = "sha256:55f79071d244cccf7a49714c33c024639a24723dd88c7cac629c63daa89d0d96", size = 1481201, upload-time = "2026-04-27T18:11:27.506Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/c3/502a898baa514cf796f11572508f3a78a93574d45ce7d36bcd34e2e7fe40/s3dlio-0.9.95-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d4f6d929e743a74428d4a6e944fbb85bd6a9cfffbdc36d6635e89f0919a5ba", size = 10258346 }, - { url = "https://files.pythonhosted.org/packages/91/4f/d394679708a4fb7c0f362076b7f92a0933201d258a90b6b28f0529dacf98/s3dlio-0.9.95-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dd5f1d71c3655346a879a5c3e49142c3d916a6df3505a823f983b0b1abb5bd5", size = 10613865 }, + { url = "https://files.pythonhosted.org/packages/7c/c3/502a898baa514cf796f11572508f3a78a93574d45ce7d36bcd34e2e7fe40/s3dlio-0.9.95-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93d4f6d929e743a74428d4a6e944fbb85bd6a9cfffbdc36d6635e89f0919a5ba", size = 10258346, upload-time = "2026-04-27T18:11:18.678Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/d394679708a4fb7c0f362076b7f92a0933201d258a90b6b28f0529dacf98/s3dlio-0.9.95-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9dd5f1d71c3655346a879a5c3e49142c3d916a6df3505a823f983b0b1abb5bd5", size = 10613865, upload-time = "2026-04-27T18:11:20.909Z" }, ] [[package]] @@ -1146,36 +1268,45 @@ dependencies = [ { name = "s3torchconnectorclient" }, { name = "torch" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0f/24/a3422bc7e3d8f2a55a64250a6d5a07416c49d6f5695879445ff72c695612/s3torchconnector-1.5.0.tar.gz", hash = "sha256:44167d8e7bc0fce6d97627fc10aa7e215f4b58e0bb7037e87858c41eefd5b5af", size = 103050 } +sdist = { url = "https://files.pythonhosted.org/packages/0f/24/a3422bc7e3d8f2a55a64250a6d5a07416c49d6f5695879445ff72c695612/s3torchconnector-1.5.0.tar.gz", hash = "sha256:44167d8e7bc0fce6d97627fc10aa7e215f4b58e0bb7037e87858c41eefd5b5af", size = 103050, upload-time = "2026-02-20T13:05:41.437Z" } [[package]] name = "s3torchconnectorclient" version = "1.5.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/e04febe3e7ff7c91bc4678a16bec1c87674fc9c160c75a8f8745e516e563/s3torchconnectorclient-1.5.0.tar.gz", hash = "sha256:09ffceca1fd025abd8a4a4cbd94b3f70a7c8ccfbf3e0f76337e180f95ce58e61", size = 85516 } +sdist = { url = "https://files.pythonhosted.org/packages/a5/8d/e04febe3e7ff7c91bc4678a16bec1c87674fc9c160c75a8f8745e516e563/s3torchconnectorclient-1.5.0.tar.gz", hash = "sha256:09ffceca1fd025abd8a4a4cbd94b3f70a7c8ccfbf3e0f76337e180f95ce58e61", size = 85516, upload-time = "2026-02-20T13:05:42.435Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/ca/65c66f2b4cc331f3d8fb92961f90edf8e9964fa6890ef7f335fbf9d7989f/s3torchconnectorclient-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83ae3c096da011af6e57947d2530814a4f78935bf1336117547984da34e1cdec", size = 2124261 }, - { url = "https://files.pythonhosted.org/packages/e6/20/629141bf19c24fedda41f9c710e55439d6303784cc1ca8e367367a51e08b/s3torchconnectorclient-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1eba5cfc67d7e2bd3cd51400105288a979096cfb293c604d19cdd880f960c396", size = 2019312 }, - { url = "https://files.pythonhosted.org/packages/7d/51/288b8857991cffa36b833c7128897766fb84f3a4a60a5cc3dfe6e2546f8a/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7c0d11b4da0271414ffa370718bbbfb5454dac2ad546d89c7c6c49831e2eb7e5", size = 3594664 }, - { url = "https://files.pythonhosted.org/packages/35/d3/9354e5620c3839393ff9afe2435f5e42bb63eb829edd93395cb0a3b1aa39/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f5277d76b4d1e12cd6f96823cf5911c51a7a614acbabb4ee4133d8caa332df1", size = 3747379 }, + { url = "https://files.pythonhosted.org/packages/ca/ca/65c66f2b4cc331f3d8fb92961f90edf8e9964fa6890ef7f335fbf9d7989f/s3torchconnectorclient-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83ae3c096da011af6e57947d2530814a4f78935bf1336117547984da34e1cdec", size = 2124261, upload-time = "2026-02-20T13:05:13.172Z" }, + { url = "https://files.pythonhosted.org/packages/e6/20/629141bf19c24fedda41f9c710e55439d6303784cc1ca8e367367a51e08b/s3torchconnectorclient-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1eba5cfc67d7e2bd3cd51400105288a979096cfb293c604d19cdd880f960c396", size = 2019312, upload-time = "2026-02-20T13:05:14.478Z" }, + { url = "https://files.pythonhosted.org/packages/7d/51/288b8857991cffa36b833c7128897766fb84f3a4a60a5cc3dfe6e2546f8a/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7c0d11b4da0271414ffa370718bbbfb5454dac2ad546d89c7c6c49831e2eb7e5", size = 3594664, upload-time = "2026-02-20T13:05:15.708Z" }, + { url = "https://files.pythonhosted.org/packages/35/d3/9354e5620c3839393ff9afe2435f5e42bb63eb829edd93395cb0a3b1aa39/s3torchconnectorclient-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f5277d76b4d1e12cd6f96823cf5911c51a7a614acbabb4ee4133d8caa332df1", size = 3747379, upload-time = "2026-02-20T13:05:17.76Z" }, ] [[package]] name = "setuptools" version = "81.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299 } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021 }, + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, ] [[package]] name = "six" version = "1.17.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] [[package]] @@ -1185,18 +1316,18 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mpmath" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 } +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 }, + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] [[package]] name = "tabulate" version = "0.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754 } +sdist = { url = "https://files.pythonhosted.org/packages/46/58/8c37dea7bbf769b20d58e7ace7e5edfe65b849442b00ffcdd56be88697c6/tabulate-0.10.0.tar.gz", hash = "sha256:e2cfde8f79420f6deeffdeda9aaec3b6bc5abce947655d17ac662b126e48a60d", size = 91754, upload-time = "2026-03-04T18:55:34.402Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814 }, + { url = "https://files.pythonhosted.org/packages/99/55/db07de81b5c630da5cbf5c7df646580ca26dfaefa593667fc6f2fe016d2e/tabulate-0.10.0-py3-none-any.whl", hash = "sha256:f0b0622e567335c8fabaaa659f1b33bcb6ddfe2e496071b743aa113f8774f2d3", size = 39814, upload-time = "2026-03-04T18:55:31.284Z" }, ] [[package]] @@ -1216,7 +1347,7 @@ dependencies = [ { name = "werkzeug" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680 }, + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, ] [[package]] @@ -1224,9 +1355,9 @@ name = "tensorboard-data-server" version = "0.7.2" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356 }, - { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598 }, - { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363 }, + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, ] [[package]] @@ -1257,19 +1388,19 @@ dependencies = [ { name = "wrapt" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/35/31/47712f425c09cc8b8dba39c6c45aee939c4636a6feb8c81376a4eae653e0/tensorflow-2.20.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:52b122f0232fd7ab10f28d537ce08470d0b6dcac7fff9685432daac7f8a06c8f", size = 200540302 }, - { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109 }, - { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547 }, - { url = "https://files.pythonhosted.org/packages/f9/37/b97abb360b551fbf5870a0ee07e39ff9c655e6e3e2f839bc88be81361842/tensorflow-2.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:1590cbf87b6bcbd34d8e9ad70d0c696135e0aa71be31803b27358cf7ed63f8fc", size = 331887041 }, + { url = "https://files.pythonhosted.org/packages/35/31/47712f425c09cc8b8dba39c6c45aee939c4636a6feb8c81376a4eae653e0/tensorflow-2.20.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:52b122f0232fd7ab10f28d537ce08470d0b6dcac7fff9685432daac7f8a06c8f", size = 200540302, upload-time = "2025-08-13T16:52:22.146Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b4/f028a5de27d0fda10ba6145bc76e40c37ff6d2d1e95b601adb5ae17d635e/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bfbfb3dd0e22bffc45fe1e922390d27753e99261fab8a882e802cf98a0e078f", size = 259533109, upload-time = "2025-08-13T16:52:31.513Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d1/6aa15085d672056d5f08b5f28b1c7ce01c4e12149a23b0c98e3c79d04441/tensorflow-2.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25265b0bc527e0d54b1e9cc60c44a24f44a809fe27666b905f0466471f9c52ec", size = 620682547, upload-time = "2025-08-13T16:52:46.396Z" }, + { url = "https://files.pythonhosted.org/packages/f9/37/b97abb360b551fbf5870a0ee07e39ff9c655e6e3e2f839bc88be81361842/tensorflow-2.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:1590cbf87b6bcbd34d8e9ad70d0c696135e0aa71be31803b27358cf7ed63f8fc", size = 331887041, upload-time = "2025-08-13T16:53:05.532Z" }, ] [[package]] name = "termcolor" version = "3.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434 } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734 }, + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, ] [[package]] @@ -1293,10 +1424,10 @@ dependencies = [ { name = "typing-extensions" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338 }, - { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115 }, - { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279 }, - { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047 }, + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, ] [[package]] @@ -1304,35 +1435,35 @@ name = "triton" version = "3.6.0" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243 }, - { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850 }, + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, ] [[package]] name = "typing-extensions" version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] name = "tzdata" version = "2025.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 }, + { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] [[package]] name = "urllib3" version = "2.6.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, ] [[package]] @@ -1342,9 +1473,9 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markupsafe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700 } +sdist = { url = "https://files.pythonhosted.org/packages/b5/43/76ded108b296a49f52de6bac5192ca1c4be84e886f9b5c9ba8427d9694fd/werkzeug-3.1.7.tar.gz", hash = "sha256:fb8c01fe6ab13b9b7cdb46892b99b1d66754e1d7ab8e542e865ec13f526b5351", size = 875700, upload-time = "2026-03-24T01:08:07.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295 }, + { url = "https://files.pythonhosted.org/packages/7f/b2/0bba9bbb4596d2d2f285a16c2ab04118f6b957d8441566e1abb892e6a6b2/werkzeug-3.1.7-py3-none-any.whl", hash = "sha256:4b314d81163a3e1a169b6a0be2a000a0e204e8873c5de6586f453c55688d422f", size = 226295, upload-time = "2026-03-24T01:08:06.133Z" }, ] [[package]] @@ -1354,52 +1485,52 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605 } +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557 }, + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, ] [[package]] name = "wrapt" version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678 } +sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255 }, - { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848 }, - { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433 }, - { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013 }, - { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326 }, - { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444 }, - { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237 }, - { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563 }, - { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198 }, - { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441 }, - { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836 }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993 }, + { url = "https://files.pythonhosted.org/packages/4c/b6/1db817582c49c7fcbb7df6809d0f515af29d7c2fbf57eb44c36e98fb1492/wrapt-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ff2aad9c4cda28a8f0653fc2d487596458c2a3f475e56ba02909e950a9efa6a9", size = 61255, upload-time = "2026-03-06T02:52:45.663Z" }, + { url = "https://files.pythonhosted.org/packages/a2/16/9b02a6b99c09227c93cd4b73acc3678114154ec38da53043c0ddc1fba0dc/wrapt-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6433ea84e1cfacf32021d2a4ee909554ade7fd392caa6f7c13f1f4bf7b8e8748", size = 61848, upload-time = "2026-03-06T02:53:48.728Z" }, + { url = "https://files.pythonhosted.org/packages/af/aa/ead46a88f9ec3a432a4832dfedb84092fc35af2d0ba40cd04aea3889f247/wrapt-2.1.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c20b757c268d30d6215916a5fa8461048d023865d888e437fab451139cad6c8e", size = 121433, upload-time = "2026-03-06T02:54:40.328Z" }, + { url = "https://files.pythonhosted.org/packages/3a/9f/742c7c7cdf58b59085a1ee4b6c37b013f66ac33673a7ef4aaed5e992bc33/wrapt-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79847b83eb38e70d93dc392c7c5b587efe65b3e7afcc167aa8abd5d60e8761c8", size = 123013, upload-time = "2026-03-06T02:53:26.58Z" }, + { url = "https://files.pythonhosted.org/packages/e8/44/2c3dd45d53236b7ed7c646fcf212251dc19e48e599debd3926b52310fafb/wrapt-2.1.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8fba1bae256186a83d1875b2b1f4e2d1242e8fac0f58ec0d7e41b26967b965c", size = 117326, upload-time = "2026-03-06T02:53:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/74/e2/b17d66abc26bd96f89dec0ecd0ef03da4a1286e6ff793839ec431b9fae57/wrapt-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3d3b35eedcf5f7d022291ecd7533321c4775f7b9cd0050a31a68499ba45757c", size = 121444, upload-time = "2026-03-06T02:54:09.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/62/e2977843fdf9f03daf1586a0ff49060b1b2fc7ff85a7ea82b6217c1ae36e/wrapt-2.1.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:6f2c5390460de57fa9582bc8a1b7a6c86e1a41dfad74c5225fc07044c15cc8d1", size = 116237, upload-time = "2026-03-06T02:54:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/27fc67914e68d740bce512f11734aec08696e6b17641fef8867c00c949fc/wrapt-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7dfa9f2cf65d027b951d05c662cc99ee3bd01f6e4691ed39848a7a5fffc902b2", size = 120563, upload-time = "2026-03-06T02:53:20.412Z" }, + { url = "https://files.pythonhosted.org/packages/ec/9f/b750b3692ed2ef4705cb305bd68858e73010492b80e43d2a4faa5573cbe7/wrapt-2.1.2-cp312-cp312-win32.whl", hash = "sha256:eba8155747eb2cae4a0b913d9ebd12a1db4d860fc4c829d7578c7b989bd3f2f0", size = 58198, upload-time = "2026-03-06T02:53:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b2/feecfe29f28483d888d76a48f03c4c4d8afea944dbee2b0cd3380f9df032/wrapt-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1c51c738d7d9faa0b3601708e7e2eda9bf779e1b601dce6c77411f2a1b324a63", size = 60441, upload-time = "2026-03-06T02:52:47.138Z" }, + { url = "https://files.pythonhosted.org/packages/44/e1/e328f605d6e208547ea9fd120804fcdec68536ac748987a68c47c606eea8/wrapt-2.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:c8e46ae8e4032792eb2f677dbd0d557170a8e5524d22acc55199f43efedd39bf", size = 58836, upload-time = "2026-03-06T02:53:22.053Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, ] [[package]] name = "zstandard" version = "0.25.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738 }, - { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436 }, - { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019 }, - { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012 }, - { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148 }, - { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652 }, - { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993 }, - { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806 }, - { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659 }, - { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933 }, - { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008 }, - { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517 }, - { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292 }, - { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237 }, - { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922 }, - { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276 }, - { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679 }, +sdist = { url = "https://files.pythonhosted.org/packages/fd/aa/3e0508d5a5dd96529cdc5a97011299056e14c6505b678fd58938792794b1/zstandard-0.25.0.tar.gz", hash = "sha256:7713e1179d162cf5c7906da876ec2ccb9c3a9dcbdffef0cc7f70c3667a205f0b", size = 711513, upload-time = "2025-09-14T22:15:54.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/fc/f26eb6ef91ae723a03e16eddb198abcfce2bc5a42e224d44cc8b6765e57e/zstandard-0.25.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b3c3a3ab9daa3eed242d6ecceead93aebbb8f5f84318d82cee643e019c4b73b", size = 795738, upload-time = "2025-09-14T22:16:56.237Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1c/d920d64b22f8dd028a8b90e2d756e431a5d86194caa78e3819c7bf53b4b3/zstandard-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:913cbd31a400febff93b564a23e17c3ed2d56c064006f54efec210d586171c00", size = 640436, upload-time = "2025-09-14T22:16:57.774Z" }, + { url = "https://files.pythonhosted.org/packages/53/6c/288c3f0bd9fcfe9ca41e2c2fbfd17b2097f6af57b62a81161941f09afa76/zstandard-0.25.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64", size = 5343019, upload-time = "2025-09-14T22:16:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/1e/15/efef5a2f204a64bdb5571e6161d49f7ef0fffdbca953a615efbec045f60f/zstandard-0.25.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dffecc361d079bb48d7caef5d673c88c8988d3d33fb74ab95b7ee6da42652ea", size = 5063012, upload-time = "2025-09-14T22:17:01.156Z" }, + { url = "https://files.pythonhosted.org/packages/b7/37/a6ce629ffdb43959e92e87ebdaeebb5ac81c944b6a75c9c47e300f85abdf/zstandard-0.25.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7149623bba7fdf7e7f24312953bcf73cae103db8cae49f8154dd1eadc8a29ecb", size = 5394148, upload-time = "2025-09-14T22:17:03.091Z" }, + { url = "https://files.pythonhosted.org/packages/e3/79/2bf870b3abeb5c070fe2d670a5a8d1057a8270f125ef7676d29ea900f496/zstandard-0.25.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6a573a35693e03cf1d67799fd01b50ff578515a8aeadd4595d2a7fa9f3ec002a", size = 5451652, upload-time = "2025-09-14T22:17:04.979Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/7be26e610767316c028a2cbedb9a3beabdbe33e2182c373f71a1c0b88f36/zstandard-0.25.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5a56ba0db2d244117ed744dfa8f6f5b366e14148e00de44723413b2f3938a902", size = 5546993, upload-time = "2025-09-14T22:17:06.781Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/3483ad9ff0662623f3648479b0380d2de5510abf00990468c286c6b04017/zstandard-0.25.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10ef2a79ab8e2974e2075fb984e5b9806c64134810fac21576f0668e7ea19f8f", size = 5046806, upload-time = "2025-09-14T22:17:08.415Z" }, + { url = "https://files.pythonhosted.org/packages/08/b3/206883dd25b8d1591a1caa44b54c2aad84badccf2f1de9e2d60a446f9a25/zstandard-0.25.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aaf21ba8fb76d102b696781bddaa0954b782536446083ae3fdaa6f16b25a1c4b", size = 5576659, upload-time = "2025-09-14T22:17:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/76c0779101453e6c117b0ff22565865c54f48f8bd807df2b00c2c404b8e0/zstandard-0.25.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1869da9571d5e94a85a5e8d57e4e8807b175c9e4a6294e3b66fa4efb074d90f6", size = 4953933, upload-time = "2025-09-14T22:17:11.857Z" }, + { url = "https://files.pythonhosted.org/packages/18/e1/97680c664a1bf9a247a280a053d98e251424af51f1b196c6d52f117c9720/zstandard-0.25.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:809c5bcb2c67cd0ed81e9229d227d4ca28f82d0f778fc5fea624a9def3963f91", size = 5268008, upload-time = "2025-09-14T22:17:13.627Z" }, + { url = "https://files.pythonhosted.org/packages/1e/73/316e4010de585ac798e154e88fd81bb16afc5c5cb1a72eeb16dd37e8024a/zstandard-0.25.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f27662e4f7dbf9f9c12391cb37b4c4c3cb90ffbd3b1fb9284dadbbb8935fa708", size = 5433517, upload-time = "2025-09-14T22:17:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/5b/60/dd0f8cfa8129c5a0ce3ea6b7f70be5b33d2618013a161e1ff26c2b39787c/zstandard-0.25.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99c0c846e6e61718715a3c9437ccc625de26593fea60189567f0118dc9db7512", size = 5814292, upload-time = "2025-09-14T22:17:17.827Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5f/75aafd4b9d11b5407b641b8e41a57864097663699f23e9ad4dbb91dc6bfe/zstandard-0.25.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:474d2596a2dbc241a556e965fb76002c1ce655445e4e3bf38e5477d413165ffa", size = 5360237, upload-time = "2025-09-14T22:17:19.954Z" }, + { url = "https://files.pythonhosted.org/packages/ff/8d/0309daffea4fcac7981021dbf21cdb2e3427a9e76bafbcdbdf5392ff99a4/zstandard-0.25.0-cp312-cp312-win32.whl", hash = "sha256:23ebc8f17a03133b4426bcc04aabd68f8236eb78c3760f12783385171b0fd8bd", size = 436922, upload-time = "2025-09-14T22:17:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/79/3b/fa54d9015f945330510cb5d0b0501e8253c127cca7ebe8ba46a965df18c5/zstandard-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffef5a74088f1e09947aecf91011136665152e0b4b359c42be3373897fb39b01", size = 506276, upload-time = "2025-09-14T22:17:21.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6b/8b51697e5319b1f9ac71087b0af9a40d8a6288ff8025c36486e0c12abcc4/zstandard-0.25.0-cp312-cp312-win_arm64.whl", hash = "sha256:181eb40e0b6a29b3cd2849f825e0fa34397f649170673d385f3598ae17cca2e9", size = 462679, upload-time = "2025-09-14T22:17:23.147Z" }, ] diff --git a/vdb_benchmark/pyproject.toml b/vdb_benchmark/pyproject.toml index 156c3acc..a3b30da8 100644 --- a/vdb_benchmark/pyproject.toml +++ b/vdb_benchmark/pyproject.toml @@ -20,6 +20,32 @@ dependencies = [ "tabulate" ] +[project.optional-dependencies] +milvus = [ + "pymilvus>=2.4.0", +] + +pgvector = [ + "psycopg2-binary>=2.9", + "pgvector>=0.2", +] + +elasticsearch = [ + "elasticsearch>=8.0", +] + +dotenv = [ + "python-dotenv>=1.0.0", +] + +all = [ + "pymilvus>=2.4.0", + "psycopg2-binary>=2.9", + "pgvector>=0.2", + "elasticsearch>=8.0", + "python-dotenv>=1.0.0", +] + [project.urls] "Homepage" = "https://github.com/mlcommons/storage/tree/main/vdb_benchmark" "Bug Tracker" = "https://github.com/mlcommons/storage/issues" @@ -29,6 +55,8 @@ compact-and-watch = "vdbbench.compact_and_watch:main" load-vdb = "vdbbench.load_vdb:main" vdbbench = "vdbbench.simple_bench:main" enhanced-bench = "vdbbench.enhanced_bench:main" +vdbbench-modular = "vdbbench.benchmark.run_benchmark:main" +vdbbench-collections = "vdbbench.benchmark.collection_admin:main" [tool.setuptools] packages = {find = {}} diff --git a/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py index c2c9d4b0..be50c4db 100644 --- a/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py +++ b/vdb_benchmark/vdbbench/benchmark/backends/pgvector/backend.py @@ -1,7 +1,7 @@ """pgvector (PostgreSQL) implementation of :class:`VectorDBBackend`. This wraps ``psycopg2`` and the ``pgvector`` extension behind the abstract -backend interface so the benchmark pipeline is completely database-agnostic. +backend interface so the benchmark pipeline is database-agnostic. Requirements:: @@ -15,14 +15,17 @@ from __future__ import annotations import logging +import re from typing import Any, Dict, List, Optional import numpy as np +from psycopg2 import sql from ..base import CollectionInfo, IndexProgress, VectorDBBackend logger = logging.getLogger(__name__) + # Mapping from the generic metric names used by the benchmark framework # to the pgvector operator classes required by each index type. _METRIC_TO_HNSW_OPS: Dict[str, str] = { @@ -37,23 +40,26 @@ "IP": "vector_ip_ops", } -# The SQL distance operator used at query time for each metric. +# SQL distance operator used at query time for each metric. _METRIC_TO_OPERATOR: Dict[str, str] = { "L2": "<->", "COSINE": "<=>", "IP": "<#>", } +_VECTOR_TYPE_RE = re.compile(r"vector\((\d+)\)", re.IGNORECASE) + class PGVectorBackend(VectorDBBackend): """Concrete backend for PostgreSQL + pgvector.""" def __init__(self) -> None: - self._conn = None # type: Any # psycopg2 connection + self._conn = None # type: Any # psycopg2 connection # ------------------------------------------------------------------ # Lifecycle # ------------------------------------------------------------------ + def connect( self, host: str = "127.0.0.1", @@ -61,35 +67,61 @@ def connect( dbname: str = "postgres", user: str = "postgres", password: str = "", - **kwargs, + **kwargs: Any, ) -> None: + """Connect to PostgreSQL and ensure pgvector is available.""" import psycopg2 from pgvector.psycopg2 import register_vector - self._conn = psycopg2.connect( - host=host, - port=port, - dbname=dbname, - user=user, - password=password, - ) + connect_timeout = kwargs.pop("connect_timeout", kwargs.pop("timeout", 10)) + + conn_params: Dict[str, Any] = { + "host": host, + "port": port, + "dbname": dbname, + "user": user, + "password": password, + } + + if connect_timeout is not None: + conn_params["connect_timeout"] = int(connect_timeout) + + # Support common optional psycopg2 connection parameters without + # passing arbitrary benchmark config keys into psycopg2.connect(). + for optional_key in ( + "sslmode", + "sslrootcert", + "sslcert", + "sslkey", + "application_name", + ): + value = kwargs.get(optional_key) + if value is not None: + conn_params[optional_key] = value + + self._conn = psycopg2.connect(**conn_params) self._conn.autocommit = True + register_vector(self._conn) # Ensure the vector extension exists. with self._conn.cursor() as cur: cur.execute("CREATE EXTENSION IF NOT EXISTS vector") + logger.info("Connected to PostgreSQL at %s:%s (db=%s)", host, port, dbname) def disconnect(self) -> None: - if self._conn and not self._conn.closed: + """Disconnect from PostgreSQL.""" + if self._conn is not None and not self._conn.closed: self._conn.close() + self._conn = None logger.info("Disconnected from PostgreSQL") # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ + def _cur(self): """Return a new cursor, raising if not connected.""" if self._conn is None or self._conn.closed: @@ -97,20 +129,39 @@ def _cur(self): return self._conn.cursor() @staticmethod - def _table(name: str) -> str: - """Sanitize a collection name for use as a SQL identifier.""" - import psycopg2.extensions - return psycopg2.extensions.quote_ident(name) if hasattr( - psycopg2.extensions, "quote_ident" - ) else f'"{name}"' + def _ident(name: str) -> sql.Identifier: + """Return a safely quoted SQL identifier.""" + return sql.Identifier(name) @staticmethod def _index_name(table: str, suffix: str = "vec_idx") -> str: + """Return the default vector index name for a table.""" return f"{table}_{suffix}" + @staticmethod + def _vector_literal(vector: np.ndarray) -> str: + """Convert one vector into pgvector's text input format. + + The result is always passed as a bound parameter and cast to ``vector``. + """ + return "[" + ",".join(str(float(v)) for v in vector) + "]" + + @staticmethod + def _metric(metric_type: str) -> str: + """Normalize metric names to the benchmark's canonical spelling.""" + metric = (metric_type or "COSINE").upper() + if metric not in _METRIC_TO_OPERATOR: + logger.warning( + "Unknown pgvector metric '%s'; defaulting to COSINE.", + metric_type, + ) + metric = "COSINE" + return metric + # ------------------------------------------------------------------ # Collection management # ------------------------------------------------------------------ + def create_collection( self, name: str, @@ -121,40 +172,52 @@ def create_collection( num_shards: int = 1, force: bool = False, ) -> CollectionInfo: - table = self._table(name) - idx_name = self._index_name(name) - + """Create a PostgreSQL table with an ``id`` primary key and vector column.""" if self.collection_exists(name): if force: self.drop_collection(name) else: - raise ValueError( - f"Table '{name}' already exists. Use force=True to drop it." - ) + raise ValueError(f"Table '{name}' already exists. Use force=True to drop it.") + + dimension = int(dimension) + metric = self._metric(metric_type) + index_params = index_params or {} with self._cur() as cur: cur.execute( - f"CREATE TABLE {table} (" - f" id BIGINT PRIMARY KEY," - f" vector vector({dimension})" - f")" + sql.SQL( + "CREATE TABLE {} (" + " id BIGINT PRIMARY KEY," + " vector vector({})" + ")" + ).format( + self._ident(name), + sql.SQL(str(dimension)), + ) ) - logger.info("Created table '%s' (%s-d)", name, f"{dimension:,}") - # Build the index (unless FLAT / no index requested). - index_params = index_params or {} + logger.info("Created table '%s' (%s-d)", name, f"{dimension:,}") + + # Build the index unless FLAT / NONE was requested. if index_type.upper() not in ("FLAT", "NONE"): self._create_index( - name, dimension, metric_type, index_type, index_params + name=name, + dimension=dimension, + metric_type=metric, + index_type=index_type, + index_params=index_params, ) return CollectionInfo( name=name, dimension=dimension, - metric_type=metric_type, + metric_type=metric, index_type=index_type, row_count=0, - extra={"index_params": index_params}, + extra={ + "index_params": index_params, + "num_shards": num_shards, + }, ) def _create_index( @@ -165,30 +228,50 @@ def _create_index( index_type: str, index_params: Dict[str, Any], ) -> None: - table = self._table(name) + """Create a pgvector HNSW or IVFFLAT index.""" + del dimension # Kept in signature for interface symmetry / future use. + idx_name = self._index_name(name) upper = index_type.upper() + metric = self._metric(metric_type) if upper == "HNSW": - ops = _METRIC_TO_HNSW_OPS.get(metric_type.upper(), "vector_cosine_ops") - m = index_params.get("M", index_params.get("m", 16)) - ef_construction = index_params.get( - "efConstruction", - index_params.get("ef_construction", 200), + ops = _METRIC_TO_HNSW_OPS.get(metric, "vector_cosine_ops") + m = int(index_params.get("M", index_params.get("m", 16))) + ef_construction = int( + index_params.get( + "efConstruction", + index_params.get("ef_construction", 200), + ) ) - with_clause = f"(m = {m}, ef_construction = {ef_construction})" - sql = ( - f"CREATE INDEX {idx_name} ON {table} " - f"USING hnsw (vector {ops}) WITH {with_clause}" + + stmt = sql.SQL( + "CREATE INDEX {} ON {} " + "USING hnsw (vector {}) " + "WITH (m = {}, ef_construction = {})" + ).format( + self._ident(idx_name), + self._ident(name), + sql.SQL(ops), # safe: selected from whitelist above + sql.Literal(m), + sql.Literal(ef_construction), ) + elif upper == "IVFFLAT": - ops = _METRIC_TO_IVFFLAT_OPS.get(metric_type.upper(), "vector_cosine_ops") - nlist = index_params.get("nlist", index_params.get("lists", 100)) - with_clause = f"(lists = {nlist})" - sql = ( - f"CREATE INDEX {idx_name} ON {table} " - f"USING ivfflat (vector {ops}) WITH {with_clause}" + ops = _METRIC_TO_IVFFLAT_OPS.get(metric, "vector_cosine_ops") + lists = int(index_params.get("nlist", index_params.get("lists", 100))) + + stmt = sql.SQL( + "CREATE INDEX {} ON {} " + "USING ivfflat (vector {}) " + "WITH (lists = {})" + ).format( + self._ident(idx_name), + self._ident(name), + sql.SQL(ops), # safe: selected from whitelist above + sql.Literal(lists), ) + else: logger.warning( "Unknown index type '%s' for pgvector; skipping index creation.", @@ -196,61 +279,94 @@ def _create_index( ) return - logger.info("Creating index: %s", sql) + logger.info("Creating index: %s", stmt.as_string(self._conn)) + with self._cur() as cur: - cur.execute(sql) - logger.info("Index '%s' created (%s / %s)", idx_name, index_type, metric_type) + cur.execute(stmt) + + logger.info("Index '%s' created (%s / %s)", idx_name, index_type, metric) def collection_exists(self, name: str) -> bool: + """Return True if a public table with this name exists.""" with self._cur() as cur: cur.execute( - "SELECT EXISTS (" - " SELECT 1 FROM information_schema.tables" - " WHERE table_name = %s" - ")", + """ + SELECT EXISTS ( + SELECT 1 + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + AND table_name = %s + ) + """, (name,), ) - return cur.fetchone()[0] + return bool(cur.fetchone()[0]) def drop_collection(self, name: str) -> None: - table = self._table(name) + """Drop a PostgreSQL table if it exists.""" with self._cur() as cur: - cur.execute(f"DROP TABLE IF EXISTS {table} CASCADE") + cur.execute( + sql.SQL("DROP TABLE IF EXISTS {} CASCADE").format( + self._ident(name) + ) + ) + logger.info("Dropped table: %s", name) # ------------------------------------------------------------------ # Data ingestion # ------------------------------------------------------------------ + def insert_batch( self, name: str, ids: np.ndarray, vectors: np.ndarray, ) -> int: + """Insert a batch of vectors into the target table.""" import psycopg2.extras - table = self._table(name) - n = len(ids) - # Build a list of tuples for execute_values. - rows = [(int(ids[i]), vectors[i].tolist()) for i in range(n)] + ids = np.asarray(ids, dtype=np.int64) + vectors = np.asarray(vectors, dtype=np.float32) + + n = int(len(ids)) + rows = [ + ( + int(ids[i]), + self._vector_literal(vectors[i]), + ) + for i in range(n) + ] + + stmt = sql.SQL( + "INSERT INTO {} (id, vector) VALUES %s " + "ON CONFLICT (id) DO NOTHING" + ).format(self._ident(name)) + with self._cur() as cur: psycopg2.extras.execute_values( cur, - f"INSERT INTO {table} (id, vector) VALUES %s " - f"ON CONFLICT (id) DO NOTHING", + stmt.as_string(self._conn), rows, template="(%s, %s::vector)", page_size=1000, ) + return n def flush(self, name: str) -> None: - # With autocommit = True every statement is already committed. - logger.info("Flush (no-op with autocommit) for table '%s'", name) + """No-op because this backend uses autocommit.""" + logger.info("Flush no-op with autocommit for table '%s'", name) + + def compact(self, name: str) -> None: + """No-op for PostgreSQL / pgvector.""" + logger.info("Compaction no-op for pgvector table '%s'", name) # ------------------------------------------------------------------ # Search # ------------------------------------------------------------------ + def search( self, name: str, @@ -258,22 +374,25 @@ def search( top_k: int, search_params: Optional[Dict[str, Any]] = None, ) -> List[List[int]]: - table = self._table(name) + """Run pgvector nearest-neighbor search.""" search_params = search_params or {} - # Determine distance operator from metric_type in search_params. - metric = search_params.get("metric_type", "COSINE").upper() - op = _METRIC_TO_OPERATOR.get(metric, "<=>") + metric = self._metric(search_params.get("metric_type", "COSINE")) + op = _METRIC_TO_OPERATOR.get(metric, "<=>") # safe whitelist lookup - # Apply runtime search params (e.g. ef_search for HNSW, probes for IVFFlat). ef_search = search_params.get("ef_search", search_params.get("ef")) probes = search_params.get("probes") + query_vectors = np.atleast_2d(np.asarray(query_vectors, dtype=np.float32)) + top_k = int(top_k) + results: List[List[int]] = [] - # SET LOCAL requires a transaction block, so temporarily leave - # autocommit mode when we need to apply search-time GUCs. + # SET LOCAL requires a transaction block. The connection normally runs + # in autocommit mode, so temporarily disable it when GUCs are requested. need_txn = ef_search is not None or probes is not None + original_autocommit = self._conn.autocommit + if need_txn: self._conn.autocommit = False @@ -281,124 +400,175 @@ def search( with self._cur() as cur: if ef_search is not None: cur.execute( - f"SET LOCAL hnsw.ef_search = {int(ef_search)}" + sql.SQL("SET LOCAL hnsw.ef_search = {}").format( + sql.Literal(int(ef_search)) + ) ) + if probes is not None: cur.execute( - f"SET LOCAL ivfflat.probes = {int(probes)}" + sql.SQL("SET LOCAL ivfflat.probes = {}").format( + sql.Literal(int(probes)) + ) ) + stmt = sql.SQL( + "SELECT id FROM {} " + "ORDER BY vector {} %s::vector " + "LIMIT %s" + ).format( + self._ident(name), + sql.SQL(op), # safe: selected from whitelist above + ) + for qvec in query_vectors: - vec_literal = "[" + ",".join(str(float(v)) for v in qvec) + "]" - cur.execute( - f"SELECT id FROM {table} " - f"ORDER BY vector {op} %s::vector " - f"LIMIT %s", - (vec_literal, top_k), - ) - results.append([row[0] for row in cur.fetchall()]) + vec_literal = self._vector_literal(qvec) + cur.execute(stmt, (vec_literal, top_k)) + results.append([int(row[0]) for row in cur.fetchall()]) if need_txn: self._conn.commit() + except Exception: if need_txn: self._conn.rollback() raise + finally: if need_txn: - self._conn.autocommit = True + self._conn.autocommit = original_autocommit return results # ------------------------------------------------------------------ # Status / info # ------------------------------------------------------------------ + def row_count(self, name: str) -> int: - table = self._table(name) + """Return the number of rows in the table.""" with self._cur() as cur: - cur.execute(f"SELECT COUNT(*) FROM {table}") - return cur.fetchone()[0] + cur.execute( + sql.SQL("SELECT COUNT(*) FROM {}").format(self._ident(name)) + ) + return int(cur.fetchone()[0]) def get_index_progress(self, name: str) -> IndexProgress: - """In PostgreSQL ``CREATE INDEX`` is synchronous, so by the time - control returns the index is already built. This simply checks - whether any index exists on the table. + """Return index readiness. + + PostgreSQL ``CREATE INDEX`` is synchronous in this backend, so once + control returns from index creation the index is ready. For FLAT/NONE, + no vector index is required, so the collection is also ready. """ - with self._cur() as cur: - cur.execute( - "SELECT indexname FROM pg_indexes WHERE tablename = %s", - (name,), - ) - indexes = [row[0] for row in cur.fetchall()] + if not self.collection_exists(name): + return IndexProgress(is_ready=False, status="table does not exist") + + indexes = self.list_indexes(name) + if indexes: return IndexProgress( is_ready=True, - status=", ".join(indexes), + total_rows=self.row_count(name), + indexed_rows=self.row_count(name), + pending_rows=0, + status=", ".join(idx["index_name"] for idx in indexes), ) - return IndexProgress(is_ready=False, status="waiting") + + return IndexProgress( + is_ready=True, + total_rows=self.row_count(name), + indexed_rows=self.row_count(name), + pending_rows=0, + status="flat/no vector index", + ) # ------------------------------------------------------------------ # Administration / introspection # ------------------------------------------------------------------ + def list_collections(self) -> List[str]: + """List all public base tables.""" with self._cur() as cur: cur.execute( - "SELECT table_name FROM information_schema.tables " - "WHERE table_schema = 'public' " - "AND table_type = 'BASE TABLE' " - "ORDER BY table_name" + """ + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + AND table_type = 'BASE TABLE' + ORDER BY table_name + """ ) return [row[0] for row in cur.fetchall()] def get_collection_info(self, name: str) -> Dict[str, Any]: - table = self._table(name) - - # Columns + """Return schema, row count, index type, and metric metadata.""" schema: List[Dict[str, Any]] = [] - dimension = None + dimension: Optional[int] = None + with self._cur() as cur: cur.execute( - "SELECT column_name, data_type, udt_name " - "FROM information_schema.columns " - "WHERE table_name = %s ORDER BY ordinal_position", + """ + SELECT + a.attname, + format_type(a.atttypid, a.atttypmod) AS formatted_type, + a.attnotnull, + EXISTS ( + SELECT 1 + FROM pg_index i + WHERE i.indrelid = a.attrelid + AND i.indisprimary + AND a.attnum = ANY(i.indkey) + ) AS is_primary_key + FROM pg_attribute a + JOIN pg_class c + ON c.oid = a.attrelid + JOIN pg_namespace n + ON n.oid = c.relnamespace + WHERE n.nspname = 'public' + AND c.relname = %s + AND a.attnum > 0 + AND NOT a.attisdropped + ORDER BY a.attnum + """, (name,), ) - for col_name, data_type, udt_name in cur.fetchall(): + + for col_name, formatted_type, not_null, is_primary_key in cur.fetchall(): entry: Dict[str, Any] = { "name": col_name, - "dtype": udt_name if udt_name != data_type else data_type, + "dtype": formatted_type, + "nullable": not bool(not_null), + "primary_key": bool(is_primary_key), } - if udt_name == "vector": - # Retrieve dimension from atttypmod - cur.execute( - "SELECT atttypmod FROM pg_attribute " - "WHERE attrelid = %s::regclass AND attname = %s", - (name, col_name), - ) - row = cur.fetchone() - if row and row[0] > 0: - dimension = row[0] - entry["dim"] = dimension + + match = _VECTOR_TYPE_RE.search(formatted_type or "") + if match: + dimension = int(match.group(1)) + entry["dim"] = dimension + schema.append(entry) - # Index info indexes = self.list_indexes(name) - index_type = indexes[0]["index_type"] if indexes else None - # Metric type from operator class - metric_type = None + index_type: Optional[str] + metric_type: Optional[str] + if indexes: - ops = indexes[0].get("params", {}).get("opclass", "") - for metric, op_cls in _METRIC_TO_HNSW_OPS.items(): - if op_cls == ops: + index_type = indexes[0].get("index_type") + opclass = indexes[0].get("params", {}).get("opclass", "") + + metric_type = None + for metric, hnsw_opclass in _METRIC_TO_HNSW_OPS.items(): + ivfflat_opclass = _METRIC_TO_IVFFLAT_OPS.get(metric) + if opclass in (hnsw_opclass, ivfflat_opclass): metric_type = metric break - - row_count = self.row_count(name) + else: + index_type = "FLAT" + metric_type = None return { "name": name, - "row_count": row_count, + "row_count": self.row_count(name), "dimension": dimension, "metric_type": metric_type, "index_type": index_type, @@ -406,34 +576,71 @@ def get_collection_info(self, name: str) -> Dict[str, Any]: } def list_indexes(self, name: str) -> List[Dict[str, Any]]: + """Return non-primary-key indexes on the table.""" results: List[Dict[str, Any]] = [] + with self._cur() as cur: cur.execute( - "SELECT indexname, indexdef FROM pg_indexes " - "WHERE tablename = %s", + """ + SELECT indexname, indexdef + FROM pg_indexes + WHERE schemaname = 'public' + AND tablename = %s + ORDER BY indexname + """, (name,), ) + for idx_name, idx_def in cur.fetchall(): - # Skip primary-key indexes - if "_pkey" in idx_name: + # Skip the primary-key btree index. + if idx_name.endswith("_pkey"): continue - idx_type = "UNKNOWN" + idx_def_upper = idx_def.upper() + if "USING HNSW" in idx_def_upper: idx_type = "HNSW" elif "USING IVFFLAT" in idx_def_upper: idx_type = "IVFFLAT" - results.append({ - "index_name": idx_name, - "index_type": idx_type, - "definition": idx_def, - "params": {}, - }) + else: + idx_type = "UNKNOWN" + + opclass = None + for candidate in set( + list(_METRIC_TO_HNSW_OPS.values()) + + list(_METRIC_TO_IVFFLAT_OPS.values()) + ): + if candidate in idx_def: + opclass = candidate + break + + results.append( + { + "index_name": idx_name, + "index_type": idx_type, + "definition": idx_def, + "params": { + "opclass": opclass, + }, + } + ) + return results - def drop_index(self, name: str, index_name: Optional[str] = None) -> None: + def drop_index( + self, + name: str, + index_name: Optional[str] = None, + ) -> None: + """Drop a pgvector index from the table.""" if index_name is None: index_name = self._index_name(name) + with self._cur() as cur: - cur.execute(f"DROP INDEX IF EXISTS {index_name}") + cur.execute( + sql.SQL("DROP INDEX IF EXISTS {}").format( + self._ident(index_name) + ) + ) + logger.info("Dropped index '%s' from table '%s'", index_name, name) From daa99ec669cc6569c77c05cfd9a23676a000b16b Mon Sep 17 00:00:00 2001 From: Devasena Inupakutika Date: Tue, 19 May 2026 15:48:01 +0000 Subject: [PATCH 4/4] updated vdb readme with a note --- vdb_benchmark/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vdb_benchmark/README.md b/vdb_benchmark/README.md index b7f14df4..ff8135bc 100644 --- a/vdb_benchmark/README.md +++ b/vdb_benchmark/README.md @@ -2,6 +2,10 @@ This tool benchmarks and compares vector database performance, with current support for Milvus (DiskANN, HNSW, AISAQ indexing). +> The modular backend-agnostic runner is currently a standalone preview. +> It is invoked with `python -m vdbbench.benchmark`. +> The existing `./mlpstorage vectordb` command continues to use the Milvus-oriented scripts until the modular runner is integrated. + ## Installation ### Clone the repository