diff --git a/.env.example b/.env.example index 7ad0363e..cb643ae2 100644 --- a/.env.example +++ b/.env.example @@ -24,7 +24,6 @@ BIGRAG_MASTER_KEY= # Override only when the backing services are not running on local defaults. # BIGRAG_DATABASE_URL=postgres://bigrag:bigrag@localhost:5432/bigrag?sslmode=disable -# BIGRAG_QDRANT_URL=http://localhost:6333 # BIGRAG_REDIS_URL=redis://localhost:6379/0 # Optional Redis password for docker-compose production runs. diff --git a/AGENTS.md b/AGENTS.md index 3ac68271..16f1e4b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,12 @@ ## Project Structure -- `api/` — Python/FastAPI backend (Docling ingestion + Qdrant/Turbopuffer vector DB) +- `api/` — Python/FastAPI backend (Docling ingestion + Turbopuffer vector search) - `sdks/typescript/` — TypeScript SDK (`@bigrag/client`) - `sdks/python/` — Python SDK (`bigrag`) - `app/` — admin UI (Vite + TanStack Router + Tailwind v4 + Base UI, `@bigrag/app`) - `website/` — Documentation site (Next.js + Fumadocs, content in `website/content/docs/`) -- `e2e/` — pytest + vitest end-to-end suites against a fake-OpenAI + real Postgres/Redis/Qdrant stack +- `e2e/` — pytest + vitest end-to-end suites against fake OpenAI/Turbopuffer services plus real Postgres/Redis ## Style Guide @@ -33,8 +33,8 @@ When adding new code, prefer the smallest meaningful module instead of dropping ## Tech Stack -- **Backend**: Python 3.12+, FastAPI, SQLAlchemy 2 (async) + asyncpg, Alembic, qdrant-client, docling, openai, cohere, cryptography (Fernet for at-rest encryption of provider secrets), dramatiq (Redis broker) -- **Vector DB**: Qdrant default; turbopuffer alternative (selected per collection) +- **Backend**: Python 3.12+, FastAPI, SQLAlchemy 2 (async) + asyncpg, Alembic, docling, openai, cohere, cryptography (Fernet for at-rest encryption of provider secrets), dramatiq (Redis broker) +- **Vector DB**: Turbopuffer - **Metadata DB**: PostgreSQL 17 - **Ingestion**: Docling (PDF, DOCX, PPTX, XLSX, HTML, Markdown, images) - **Embedding**: OpenAI, Cohere, Voyage, and OpenAI-compatible providers @@ -124,7 +124,7 @@ If a feature is removed, remove it from the docs too. Never leave stale referenc ```bash ./dev.sh # starts infra + backend + worker -./dev.sh --infra # postgres + redis + qdrant only +./dev.sh --infra # postgres + redis only ./dev.sh --website # docs site only pnpm dev:app # admin UI on localhost:3000 ``` @@ -133,7 +133,6 @@ pnpm dev:app # admin UI on localhost:3000 - Backend API: http://localhost:4000 (Swagger docs at /docs) - Postgres: localhost:5432 - Redis: localhost:6379 -- Qdrant: localhost:6333 `dev.sh` only tears down the docker stack that it started — if you ran `docker compose up` separately before invoking `dev.sh`, it leaves that running on exit. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 980cfb0b..cbdda1be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Thank you for your interest in contributing to bigRAG. This guide will help you - **Python 3.12+** with [uv](https://docs.astral.sh/uv/) - **Node.js 20+** with [pnpm](https://pnpm.io/) (via corepack) -- **Docker** and **Docker Compose** — for Postgres, Redis, Qdrant +- **Docker** and **Docker Compose** — for Postgres and Redis ### Development Setup @@ -31,7 +31,7 @@ Or manually: ```bash # Start infrastructure -docker compose up postgres redis qdrant -d +docker compose up postgres redis -d # Set up the Python backend cd api @@ -60,7 +60,7 @@ bigrag/ ├── sdks/python/ # Python SDK (bigrag) ├── app/ # Admin UI (TanStack Router + React) ├── website/ # Docs site (Next.js + Fumadocs) -├── docker-compose.yml # Full stack (Postgres, Redis, Qdrant, API) +├── docker-compose.yml # Full stack (Postgres, Redis, API) ├── biome.jsonc # Biome linting config for TypeScript ├── pnpm-workspace.yaml # pnpm workspace config ├── dev.sh # One-command dev setup diff --git a/README.md b/README.md index df85e5a9..e269dbf3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Open-source, self-hostable RAG platform. Upload documents, auto-chunk, embed, an - **Document ingestion** — PDF, DOCX, PPTX, HTML, Markdown, images, and more via [Docling](https://github.com/DS4SD/docling) - **Embedding providers** — OpenAI, OpenAI-compatible gateways, Cohere, and Voyage - **Embedding presets** — save named provider/model configs once, reuse across collections -- **Vector search** — semantic, keyword, and hybrid search modes via [Qdrant](https://qdrant.io) or [turbopuffer](https://turbopuffer.com) +- **Vector search** — semantic, keyword, and hybrid search modes via [Turbopuffer](https://turbopuffer.com) - **Reranking** — Cohere reranking for improved result relevance - **Multi-collection queries** — search across collections in a single request - **Generated chat** — stateless backend-grounded playground chat with streaming and citations @@ -31,7 +31,7 @@ Open-source, self-hostable RAG platform. Upload documents, auto-chunk, embed, an docker compose up -d ``` -This starts bigRAG API, worker, admin UI, Postgres, Redis, and Qdrant (or turbopuffer per collection). Open http://localhost:3000 for the admin UI or http://localhost:4000/docs for the interactive API docs. +This starts bigRAG API, worker, admin UI, Postgres, and Redis. Configure Turbopuffer before ingesting or querying collections. Open http://localhost:3000 for the admin UI or http://localhost:4000/docs for the interactive API docs. ```bash # Create a collection @@ -52,7 +52,7 @@ curl -X POST http://localhost:4000/v1/collections/docs/query \ ### Development ```bash -./dev.sh # starts Postgres, Redis, Qdrant, the API with hot reload, and the worker +./dev.sh # starts Postgres, Redis, the API with hot reload, and the worker ``` ### Docker Images @@ -90,7 +90,7 @@ graph TD Worker -->|parse| Docling[Docling
PDF, DOCX, HTML, Images] Worker -->|embed| Embedding[Embedding provider
OpenAI / compatible / Cohere / Voyage] - Worker -->|store vectors| Vectors[(Qdrant
or turbopuffer)] + Worker -->|store vectors| Vectors[(Turbopuffer)] Query -->|search| Vectors Query -->|embed query| Embedding @@ -288,8 +288,8 @@ Full-workspace keys expose 8 tools — `list_collections`, `get_collection`, `ge ## Configuration -Most settings use the `BIGRAG_` prefix as environment variables, or configure via `bigrag.toml`. -Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_LOG_LEVEL=info` and `BIGRAG_LOG_FORMAT=json` for production log collection. +Bootstrap settings use the `BIGRAG_` prefix as environment variables, or configure via `bigrag.toml`. +Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_LOG_LEVEL=info` and `BIGRAG_LOG_FORMAT=json` for production log collection. Configure Turbopuffer from the admin UI; it is stored in Postgres with the other instance settings. | Variable | Description | Default | |----------|-------------|---------| @@ -303,13 +303,6 @@ Backend logging defaults to `debug` / `text` for local development. Use `BIGRAG_ | `BIGRAG_DB_POOL_MIN` | Min Postgres pool size | `5` | | `BIGRAG_DB_POOL_MAX` | Max Postgres pool size | `50` | | `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration check timeout (`0` disables the timeout) | `60` | -| `BIGRAG_QDRANT_URL` | Qdrant URL | `http://localhost:6333` | -| `BIGRAG_QDRANT_CONNECT_TIMEOUT_SECONDS` | Qdrant startup connection timeout (`0` disables the timeout) | `10` | -| `BIGRAG_QDRANT_REQUIRED` | Fail API startup if Qdrant cannot be reached | `false` | -| `BIGRAG_QDRANT_SEARCH_EF` | Optional Qdrant HNSW search recall/latency tuning | — | -| `BIGRAG_TURBOPUFFER_API_KEY` | turbopuffer API key (only required for collections using turbopuffer) | — | -| `BIGRAG_TURBOPUFFER_REGION` | turbopuffer region | `aws-us-east-1` | -| `BIGRAG_TURBOPUFFER_NAMESPACE_PREFIX` | Prefix prepended to turbopuffer namespace names | `bigrag_` | | `BIGRAG_REDIS_URL` | Redis URL | `redis://localhost:6379/0` | | `BIGRAG_ENV` | `dev` or `prod` (prod enables startup safety checks) | `dev` | | `BIGRAG_TRUSTED_PROXIES` | JSON array of trusted proxy CIDRs used to honor `X-Forwarded-For` for audit and access logs | `[]` | diff --git a/api/alembic/versions/0001_initial_schema.py b/api/alembic/versions/0001_initial_schema.py index 3658d651..fff94d0d 100644 --- a/api/alembic/versions/0001_initial_schema.py +++ b/api/alembic/versions/0001_initial_schema.py @@ -260,7 +260,6 @@ def upgrade() -> None: sa.Column("embedding_api_key", bigrag.services.crypto.EncryptedString(), nullable=True), sa.Column("embedding_base_url", sa.Text(), nullable=True), sa.Column("embedding_preset_id", sa.Uuid(), nullable=True), - sa.Column("vector_store_provider", sa.Text(), server_default="qdrant", nullable=False), sa.Column("dimension", sa.Integer(), server_default=sa.text("1536"), nullable=False), sa.Column("chunk_size", sa.Integer(), server_default=sa.text("512"), nullable=False), sa.Column("chunk_overlap", sa.Integer(), server_default=sa.text("50"), nullable=False), @@ -283,7 +282,6 @@ def upgrade() -> None: server_default=sa.text("false"), nullable=False, ), - sa.Column("index_type", sa.Text(), server_default="HNSW", nullable=False), sa.Column("tenant_field", sa.Text(), nullable=True), sa.Column("metadata_schema", postgresql.JSONB(astext_type=sa.Text()), nullable=True), sa.Column( @@ -317,66 +315,6 @@ def upgrade() -> None: unique=False, ) op.create_index("idx_collections_name", "collections", ["name"], unique=False) - op.create_table( - "vector_migration_jobs", - sa.Column("id", sa.Uuid(), nullable=False), - sa.Column("collection_id", sa.Uuid(), nullable=True), - sa.Column("collection_name", sa.Text(), nullable=False), - sa.Column("source_provider", sa.Text(), nullable=False), - sa.Column("target_provider", sa.Text(), nullable=False), - sa.Column("status", sa.Text(), server_default="pending", nullable=False), - sa.Column("phase", sa.Text(), server_default="queued", nullable=False), - sa.Column("progress", sa.Double(), server_default=sa.text("0"), nullable=False), - sa.Column("copied_points", sa.Integer(), server_default=sa.text("0"), nullable=False), - sa.Column("total_points", sa.Integer(), nullable=True), - sa.Column( - "details", - postgresql.JSONB(astext_type=sa.Text()), - server_default=sa.text("'{}'::jsonb"), - nullable=False, - ), - sa.Column("error_message", sa.Text(), nullable=True), - sa.Column("created_by", sa.Uuid(), nullable=True), - sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.Column( - "created_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.Column( - "updated_at", - sa.DateTime(timezone=True), - server_default=sa.text("now()"), - nullable=False, - ), - sa.CheckConstraint( - "status IN ('pending', 'running', 'canceling', 'succeeded', 'failed')", - name="vector_migration_jobs_status_check", - ), - sa.ForeignKeyConstraint(["collection_id"], ["collections.id"], ondelete="SET NULL"), - sa.ForeignKeyConstraint(["created_by"], ["users.id"], ondelete="SET NULL"), - sa.PrimaryKeyConstraint("id"), - ) - op.create_index( - "idx_vector_migration_jobs_collection", - "vector_migration_jobs", - ["collection_name"], - unique=False, - ) - op.create_index( - "idx_vector_migration_jobs_created_at_id", - "vector_migration_jobs", - [sa.literal_column("created_at DESC"), sa.literal_column("id DESC")], - unique=False, - ) - op.create_index( - "idx_vector_migration_jobs_status", - "vector_migration_jobs", - ["status"], - unique=False, - ) op.create_table( "connector_accounts", sa.Column("id", sa.Uuid(), nullable=False), @@ -1170,11 +1108,6 @@ def upgrade() -> None: "webhook_deliveries", "status IN ('pending', 'delivered', 'failed')", ) - op.create_check_constraint( - "collections_vector_store_provider_check", - "collections", - "vector_store_provider IN ('qdrant', 'turbopuffer')", - ) op.create_check_constraint( "embedding_presets_provider_check", "embedding_presets", @@ -1266,10 +1199,6 @@ def downgrade() -> None: op.drop_table("connector_accounts") op.drop_index("idx_collections_name", table_name="collections") op.drop_index("idx_collections_created_at_id", table_name="collections") - op.drop_index("idx_vector_migration_jobs_status", table_name="vector_migration_jobs") - op.drop_index("idx_vector_migration_jobs_created_at_id", table_name="vector_migration_jobs") - op.drop_index("idx_vector_migration_jobs_collection", table_name="vector_migration_jobs") - op.drop_table("vector_migration_jobs") op.drop_table("collections") op.drop_index("idx_backup_jobs_status", table_name="backup_jobs") op.drop_index("idx_backup_jobs_created_at_id", table_name="backup_jobs") diff --git a/api/bigrag/app_factory/lifespan.py b/api/bigrag/app_factory/lifespan.py index 12034bf1..0c31aa69 100644 --- a/api/bigrag/app_factory/lifespan.py +++ b/api/bigrag/app_factory/lifespan.py @@ -52,21 +52,16 @@ async def lifespan(app: FastAPI): runtime = await runtime_settings.get_values( [ "ingestion_workers", - "qdrant_connect_timeout_seconds", - "qdrant_required", - "qdrant_search_ef", - "qdrant_url", "turbopuffer_api_key", + "turbopuffer_base_url", "turbopuffer_namespace_prefix", "turbopuffer_region", ] ) vector_store.configure( - qdrant_url=runtime["qdrant_url"], - connect_timeout_seconds=runtime["qdrant_connect_timeout_seconds"], - search_ef=runtime["qdrant_search_ef"], turbopuffer_api_key=runtime["turbopuffer_api_key"], + turbopuffer_base_url=runtime["turbopuffer_base_url"], turbopuffer_region=runtime["turbopuffer_region"], turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"], ) @@ -76,12 +71,10 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.warning( "Vector store startup connection failed; API will start degraded", - provider=vector_store.provider, + provider="turbopuffer", error_type=exc.__class__.__name__, error=str(exc), ) - if runtime["qdrant_required"]: - raise app.state.vector_store = vector_store storage = await init_storage_from_runtime(upload_dir=s.upload_dir) diff --git a/api/bigrag/app_factory/routers.py b/api/bigrag/app_factory/routers.py index d1dba1aa..dc2033fc 100644 --- a/api/bigrag/app_factory/routers.py +++ b/api/bigrag/app_factory/routers.py @@ -15,7 +15,6 @@ def include_all_routers(app: FastAPI) -> None: from bigrag.routers.admin_realtime import router as admin_realtime_router from bigrag.routers.admin_settings import router as admin_settings_router from bigrag.routers.admin_users import router as admin_users_router - from bigrag.routers.admin_vector_migrations import router as admin_vector_migrations_router from bigrag.routers.admin_vector_storage import router as admin_vector_storage_router from bigrag.routers.analytics import router as analytics_router from bigrag.routers.auth import router as auth_router @@ -51,7 +50,6 @@ def include_all_routers(app: FastAPI) -> None: app.include_router(admin_settings_router) app.include_router(admin_access_router) app.include_router(admin_vector_storage_router) - app.include_router(admin_vector_migrations_router) app.include_router(admin_realtime_router) app.include_router(mcp_servers_router) app.include_router(admin_audit_router) diff --git a/api/bigrag/config.py b/api/bigrag/config.py index 9eacab94..b8c81c3f 100644 --- a/api/bigrag/config.py +++ b/api/bigrag/config.py @@ -27,15 +27,6 @@ class Settings(BaseSettings): db_pool_max: int = 50 migration_timeout_seconds: int = 60 - qdrant_url: str = "http://localhost:6333" - qdrant_connect_timeout_seconds: int = 10 - qdrant_required: bool = False - qdrant_prefer_grpc: bool = False - qdrant_grpc_port: int = 6334 - turbopuffer_api_key: str | None = None - turbopuffer_region: str = "aws-us-east-1" - turbopuffer_namespace_prefix: str = "bigrag_" - redis_url: str = "redis://localhost:6379/0" master_key: str | None = None diff --git a/api/bigrag/db/models/__init__.py b/api/bigrag/db/models/__init__.py index b9a89dce..1b41b899 100644 --- a/api/bigrag/db/models/__init__.py +++ b/api/bigrag/db/models/__init__.py @@ -19,7 +19,6 @@ from bigrag.db.models.instance import InstanceSetting, MaintenanceLock from bigrag.db.models.observability import AccessLog, AuditLog, BackupJob, QueryLog from bigrag.db.models.preference import UserPreference -from bigrag.db.models.vector_migration import VectorMigrationJob from bigrag.db.models.webhook import Webhook, WebhookDelivery __all__ = [ @@ -46,7 +45,6 @@ "User", "UserSession", "UserPreference", - "VectorMigrationJob", "Webhook", "WebhookDelivery", ] diff --git a/api/bigrag/db/models/collection.py b/api/bigrag/db/models/collection.py index 4fda5d81..b6bac6a6 100644 --- a/api/bigrag/db/models/collection.py +++ b/api/bigrag/db/models/collection.py @@ -14,10 +14,6 @@ class Collection(Base): __tablename__ = "collections" __table_args__ = ( - sa.CheckConstraint( - "vector_store_provider IN ('qdrant', 'turbopuffer')", - name="collections_vector_store_provider_check", - ), sa.Index("idx_collections_name", "name"), sa.Index("idx_collections_created_at_id", sa.desc("created_at"), sa.desc("id")), ) @@ -37,11 +33,6 @@ class Collection(Base): sa.ForeignKey("embedding_presets.id", ondelete="RESTRICT"), nullable=True, ) - vector_store_provider: Mapped[str] = mapped_column( - sa.Text, - nullable=False, - server_default="qdrant", - ) dimension: Mapped[int] = mapped_column( sa.Integer, nullable=False, server_default=sa.text("1536") ) @@ -75,7 +66,6 @@ class Collection(Base): multimodal_enrichment_enabled: Mapped[bool] = mapped_column( sa.Boolean, nullable=False, server_default=sa.false() ) - index_type: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="HNSW") tenant_field: Mapped[str | None] = mapped_column(sa.Text) metadata_schema: Mapped[dict | None] = mapped_column(JSONB) meta: Mapped[dict] = mapped_column( diff --git a/api/bigrag/db/models/vector_migration.py b/api/bigrag/db/models/vector_migration.py deleted file mode 100644 index d9f402c0..00000000 --- a/api/bigrag/db/models/vector_migration.py +++ /dev/null @@ -1,51 +0,0 @@ -from __future__ import annotations - -from datetime import datetime -from uuid import UUID - -import sqlalchemy as sa -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import Mapped, mapped_column - -from bigrag.db.base import TS, Base, TSupd, UUIDpk - - -class VectorMigrationJob(Base): - __tablename__ = "vector_migration_jobs" - __table_args__ = ( - sa.CheckConstraint( - "status IN ('pending', 'running', 'canceling', 'succeeded', 'failed')", - name="vector_migration_jobs_status_check", - ), - sa.Index("idx_vector_migration_jobs_collection", "collection_name"), - sa.Index("idx_vector_migration_jobs_status", "status"), - sa.Index( - "idx_vector_migration_jobs_created_at_id", - sa.desc("created_at"), - sa.desc("id"), - ), - ) - - id: Mapped[UUIDpk] - collection_id: Mapped[UUID | None] = mapped_column( - sa.ForeignKey("collections.id", ondelete="SET NULL") - ) - collection_name: Mapped[str] = mapped_column(sa.Text, nullable=False) - source_provider: Mapped[str] = mapped_column(sa.Text, nullable=False) - target_provider: Mapped[str] = mapped_column(sa.Text, nullable=False) - status: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="pending") - phase: Mapped[str] = mapped_column(sa.Text, nullable=False, server_default="queued") - progress: Mapped[float] = mapped_column(sa.Double, nullable=False, server_default=sa.text("0")) - copied_points: Mapped[int] = mapped_column( - sa.Integer, nullable=False, server_default=sa.text("0") - ) - total_points: Mapped[int | None] = mapped_column(sa.Integer) - details: Mapped[dict] = mapped_column( - JSONB, nullable=False, server_default=sa.text("'{}'::jsonb") - ) - error_message: Mapped[str | None] = mapped_column(sa.Text) - created_by: Mapped[UUID | None] = mapped_column(sa.ForeignKey("users.id", ondelete="SET NULL")) - started_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True)) - completed_at: Mapped[datetime | None] = mapped_column(sa.DateTime(timezone=True)) - created_at: Mapped[TS] - updated_at: Mapped[TSupd] diff --git a/api/bigrag/logging.py b/api/bigrag/logging.py index a0a0dad4..03e7d6a1 100644 --- a/api/bigrag/logging.py +++ b/api/bigrag/logging.py @@ -255,7 +255,6 @@ def configure_logging(log_level: str = "debug", log_format: str = "text") -> Non "hpack", "httpcore", "httpx", - "qdrant_client", "uvicorn.access", ): logging.getLogger(name).setLevel(logging.WARNING) diff --git a/api/bigrag/main.py b/api/bigrag/main.py index e4d31f52..b8adf430 100644 --- a/api/bigrag/main.py +++ b/api/bigrag/main.py @@ -47,7 +47,7 @@ def create_app(settings_override: Settings | None = None) -> FastAPI: app = FastAPI( title="bigRAG", - description="Self-hostable RAG platform with Docling + Qdrant", + description="Self-hostable RAG platform with Docling + turbopuffer", version=__version__, lifespan=lifespan, default_response_class=ORJSONResponse, @@ -79,7 +79,6 @@ def cli() -> None: parser.add_argument("--host", help="Server host") parser.add_argument("--port", type=int, help="Server port") parser.add_argument("--database-url", help="Postgres connection URL") - parser.add_argument("--qdrant-url", help="Qdrant connection URL") parser.add_argument("--redis-url", help="Redis connection URL") args = parser.parse_args() @@ -92,8 +91,6 @@ def cli() -> None: overrides["port"] = args.port if args.database_url is not None: overrides["database_url"] = args.database_url - if args.qdrant_url is not None: - overrides["qdrant_url"] = args.qdrant_url if args.redis_url is not None: overrides["redis_url"] = args.redis_url for key, value in overrides.items(): diff --git a/api/bigrag/middleware/maintenance.py b/api/bigrag/middleware/maintenance.py index 4a5e134b..9310c02a 100644 --- a/api/bigrag/middleware/maintenance.py +++ b/api/bigrag/middleware/maintenance.py @@ -7,7 +7,7 @@ from bigrag.services.maintenance import active_lock SAFE_METHODS = {"GET", "HEAD", "OPTIONS"} -CONTROL_PATH_PREFIXES = ("/v1/admin/backups", "/v1/admin/vector-storage/migrations") +CONTROL_PATH_PREFIXES = ("/v1/admin/backups",) def _is_control_path(path: str) -> bool: diff --git a/api/bigrag/models/collection.py b/api/bigrag/models/collection.py index 4754965d..cee4226f 100644 --- a/api/bigrag/models/collection.py +++ b/api/bigrag/models/collection.py @@ -4,14 +4,11 @@ from pydantic import BaseModel, Field, model_validator -from bigrag.services.vector_store.base import VectorStoreProvider - class CreateCollectionRequest(BaseModel): name: str = Field(min_length=1, max_length=128, pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$") description: str = "" embedding_preset_id: str | None = None - vector_store_provider: VectorStoreProvider = "qdrant" embedding_provider: str | None = None embedding_model: str | None = None embedding_api_key: str | None = None @@ -24,16 +21,11 @@ class CreateCollectionRequest(BaseModel): pattern=r"^(paragraph|recursive)$", description="Chunking algorithm: paragraph (default) or recursive.", ) - index_type: str = Field( - default="HNSW", - pattern=r"^HNSW$", - description="Vector index preference. Qdrant uses HNSW for dense-vector search.", - ) tenant_field: str | None = Field( default=None, max_length=64, description=( - "Optional metadata field name to index for tenant-aware Qdrant " + "Optional metadata field name to index for tenant-aware " "payload filtering in multi-tenant deployments." ), ) @@ -92,12 +84,10 @@ class CollectionResponse(BaseModel): description: str embedding_provider: str embedding_model: str - vector_store_provider: VectorStoreProvider dimension: int chunk_size: int chunk_overlap: int chunk_strategy: str = "paragraph" - index_type: str = "HNSW" tenant_field: str | None = None has_metadata_schema: bool = False document_count: int diff --git a/api/bigrag/models/vector_migration.py b/api/bigrag/models/vector_migration.py deleted file mode 100644 index 71954ee9..00000000 --- a/api/bigrag/models/vector_migration.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations - -from datetime import datetime - -from pydantic import BaseModel, Field - -from bigrag.services.vector_store.base import VectorStoreProvider - - -class VectorMigrationCreateRequest(BaseModel): - collection: str = Field(min_length=1, max_length=128) - target_provider: VectorStoreProvider - - -class VectorMigrationJobResponse(BaseModel): - id: str - collection_id: str | None - collection_name: str - source_provider: VectorStoreProvider - target_provider: VectorStoreProvider - status: str - phase: str - progress: float - copied_points: int - total_points: int | None - details: dict - error_message: str | None = None - created_by: str | None = None - started_at: datetime | None = None - completed_at: datetime | None = None - created_at: datetime - updated_at: datetime - - -class VectorMigrationJobListResponse(BaseModel): - jobs: list[VectorMigrationJobResponse] - total: int | None = None - next_cursor: str | None = None diff --git a/api/bigrag/routers/admin_realtime.py b/api/bigrag/routers/admin_realtime.py index 61e4184d..1b7aa76d 100644 --- a/api/bigrag/routers/admin_realtime.py +++ b/api/bigrag/routers/admin_realtime.py @@ -11,7 +11,6 @@ from bigrag.routers.admin_access import access_overview, list_access_logs from bigrag.routers.admin_audit import list_audit_log from bigrag.routers.admin_backups import list_backup_jobs -from bigrag.routers.admin_vector_migrations import list_vector_migration_jobs from bigrag.routers.collections import get_collection_stats from bigrag.routers.connectors import connector_sources, connector_sync_jobs from bigrag.routers.documents import get_document, list_documents @@ -37,7 +36,6 @@ ACTIVE_SYNC_JOB_STATUSES = {"pending", "running"} ACTIVE_BACKUP_JOB_STATUSES = {"pending", "running"} -ACTIVE_VECTOR_MIGRATION_STATUSES = {"pending", "running", "canceling"} def _parse_document_ids(document_ids: list[str]) -> list[str]: @@ -84,12 +82,6 @@ def _backup_jobs_interval(payload: Any | None) -> float: return 2.0 if active else 15.0 -def _vector_migration_jobs_interval(payload: Any | None) -> float: - jobs = getattr(payload, "jobs", []) if payload is not None else [] - active = any(getattr(job, "status", None) in ACTIVE_VECTOR_MIGRATION_STATUSES for job in jobs) - return 2.0 if active else 15.0 - - @router.get("/collections/{collection_name}/documents", response_class=StreamingResponse) async def collection_documents_stream( collection_name: str, @@ -283,31 +275,6 @@ async def load(): return _interval_response(topic, load, _backup_jobs_interval) -@router.get("/vector-migrations", response_class=StreamingResponse) -async def vector_migration_jobs_stream( - collection: str | None = Query(default=None, max_length=128), - limit: int = Query(default=20, ge=1, le=100), - offset: int = Query(default=0, ge=0), - user: dict = Depends(require_admin_session), -): - topic = f"vector-migrations:{collection or 'all'}:{limit}:{offset}" - - async def load(): - return await _with_session( - lambda session: list_vector_migration_jobs( - collection=collection, - limit=limit, - offset=offset, - cursor=None, - include_total=False, - _=user, - session=session, - ) - ) - - return _interval_response(topic, load, _vector_migration_jobs_interval) - - @router.get("/access/overview", response_class=StreamingResponse) async def access_overview_stream( window_days: int = Query(default=7, ge=1, le=90), diff --git a/api/bigrag/routers/admin_vector_migrations.py b/api/bigrag/routers/admin_vector_migrations.py deleted file mode 100644 index aa623fec..00000000 --- a/api/bigrag/routers/admin_vector_migrations.py +++ /dev/null @@ -1,177 +0,0 @@ -from __future__ import annotations - -from uuid import UUID - -import sqlalchemy as sa -from fastapi import APIRouter, Depends, HTTPException, Query, Request -from sqlalchemy.ext.asyncio import AsyncSession - -from bigrag.db.models import VectorMigrationJob -from bigrag.db.session import get_session -from bigrag.middleware.auth import require_admin_session -from bigrag.models import StatusResponse -from bigrag.models.vector_migration import ( - VectorMigrationCreateRequest, - VectorMigrationJobListResponse, - VectorMigrationJobResponse, -) -from bigrag.routers import uuid_or_404 -from bigrag.services import audit -from bigrag.services.error_sanitize import sanitize_message_text -from bigrag.services.jobs.actors import enqueue_vector_migration_job -from bigrag.services.pagination import apply_cursor, build_response_cursor, decode_cursor_or_400 -from bigrag.services.vector_migration import ( - VectorMigrationConflictError, - VectorMigrationError, - create_vector_migration_job, - delete_vector_migration_job, -) - -router = APIRouter( - prefix="/v1/admin/vector-storage/migrations", - tags=["admin:vector-storage"], -) - - -def vector_migration_job_response(job: VectorMigrationJob) -> VectorMigrationJobResponse: - return VectorMigrationJobResponse( - id=str(job.id), - collection_id=str(job.collection_id) if job.collection_id else None, - collection_name=job.collection_name, - source_provider=job.source_provider, - target_provider=job.target_provider, - status=job.status, - phase=job.phase, - progress=job.progress, - copied_points=job.copied_points, - total_points=job.total_points, - details=job.details or {}, - error_message=job.error_message, - created_by=str(job.created_by) if job.created_by else None, - started_at=job.started_at, - completed_at=job.completed_at, - created_at=job.created_at, - updated_at=job.updated_at, - ) - - -@router.get("", response_model=VectorMigrationJobListResponse) -async def list_vector_migration_jobs( - collection: str | None = Query(default=None, max_length=128), - limit: int = Query(default=20, ge=1, le=100), - offset: int = Query(default=0, ge=0), - cursor: str | None = Query(default=None), - include_total: bool = Query(default=False), - _: dict = Depends(require_admin_session), - session: AsyncSession = Depends(get_session), -) -> VectorMigrationJobListResponse: - cursor_tuple = decode_cursor_or_400(cursor) - stmt = sa.select(VectorMigrationJob).order_by( - VectorMigrationJob.created_at.desc(), - VectorMigrationJob.id.desc(), - ) - count_stmt = sa.select(sa.func.count()).select_from(VectorMigrationJob) - if collection: - stmt = stmt.where(VectorMigrationJob.collection_name == collection) - count_stmt = count_stmt.where(VectorMigrationJob.collection_name == collection) - - if cursor_tuple is not None: - stmt = apply_cursor( - stmt, - VectorMigrationJob.created_at, - VectorMigrationJob.id, - cursor_tuple, - ).limit(limit + 1) - else: - stmt = stmt.limit(limit + 1).offset(offset) - - rows = (await session.scalars(stmt)).all() - page, next_cursor = build_response_cursor(list(rows), "created_at", "id", limit) - - total: int | None = None - if include_total: - total = (await session.scalar(count_stmt)) or 0 - - return VectorMigrationJobListResponse( - jobs=[vector_migration_job_response(job) for job in page], - total=total, - next_cursor=next_cursor, - ) - - -@router.get("/{migration_id}", response_model=VectorMigrationJobResponse) -async def get_vector_migration_job( - migration_id: str, - _: dict = Depends(require_admin_session), - session: AsyncSession = Depends(get_session), -) -> VectorMigrationJobResponse: - try: - target_id = UUID(migration_id) - except ValueError as exc: - raise HTTPException(status_code=400, detail="Invalid migration_id") from exc - job = await session.get(VectorMigrationJob, target_id) - if job is None: - raise HTTPException(status_code=404, detail="Vector migration job not found") - return vector_migration_job_response(job) - - -@router.post("", response_model=VectorMigrationJobResponse, status_code=201) -async def start_vector_migration_job( - body: VectorMigrationCreateRequest, - request: Request, - admin: dict = Depends(require_admin_session), -) -> VectorMigrationJobResponse: - user_id = UUID(admin["id"]) if admin.get("id") else None - try: - job = await create_vector_migration_job( - collection_name=body.collection, - target_provider=body.target_provider, - created_by=user_id, - ) - except VectorMigrationConflictError as exc: - raise HTTPException( - status_code=409, - detail=sanitize_message_text(str(exc)) or "Vector migration cannot be started.", - ) from exc - except VectorMigrationError as exc: - raise HTTPException( - status_code=400, - detail=sanitize_message_text(str(exc)) or "Vector migration cannot be started.", - ) from exc - audit.record( - request, - user=admin, - action="vector_migration.requested", - resource_type="vector_migration_job", - resource_id=str(job.id), - metadata={ - "collection": job.collection_name, - "source_provider": job.source_provider, - "target_provider": job.target_provider, - }, - ) - enqueue_vector_migration_job(str(job.id)) - return vector_migration_job_response(job) - - -@router.delete("/{migration_id}", response_model=StatusResponse) -async def delete_vector_migration_job_route( - migration_id: str, - request: Request, - admin: dict = Depends(require_admin_session), -) -> StatusResponse: - target_id = uuid_or_404(migration_id, "Vector migration job") - result = await delete_vector_migration_job(target_id) - if result is None: - raise HTTPException(status_code=404, detail="Vector migration job not found") - audit.record( - request, - user=admin, - action="vector_migration.delete", - resource_type="vector_migration_job", - resource_id=migration_id, - metadata={"result": result}, - ) - if result == "stop_requested": - return StatusResponse(status="ok", message="Vector migration stop requested") - return StatusResponse(status="ok", message="Vector migration deleted") diff --git a/api/bigrag/routers/admin_vector_storage.py b/api/bigrag/routers/admin_vector_storage.py index 0fffd917..a4e4114f 100644 --- a/api/bigrag/routers/admin_vector_storage.py +++ b/api/bigrag/routers/admin_vector_storage.py @@ -7,6 +7,7 @@ from bigrag.db.models import Collection, Document from bigrag.db.session import get_session from bigrag.middleware.auth import require_admin_session +from bigrag.services.health import categorize_dependency_error from bigrag.services.vector_store import vector_store router = APIRouter(prefix="/v1/admin/vector-storage", tags=["admin:vector-storage"]) @@ -17,12 +18,15 @@ async def vector_storage_overview( _: dict = Depends(require_admin_session), session: AsyncSession = Depends(get_session), ) -> dict[str, object]: - provider_health = await vector_store.provider_health() + health: dict[str, object] = {"status": "ok", "error": None} + try: + await vector_store.health_check() + except Exception as exc: + health = {"status": "error", "error": categorize_dependency_error(exc)} rows = ( await session.execute( sa.select( Collection.name, - Collection.vector_store_provider, sa.func.count(Document.id).label("documents"), sa.func.coalesce(sa.func.sum(Document.chunk_count), 0).label("chunks"), sa.func.coalesce(sa.func.sum(Document.file_size), 0).label("bytes"), @@ -35,12 +39,11 @@ async def vector_storage_overview( collections = [ { "name": name, - "provider": provider, "documents": int(documents or 0), "chunks": int(chunks or 0), "bytes": int(bytes_ or 0), } - for name, provider, documents, chunks, bytes_ in rows + for name, documents, chunks, bytes_ in rows ] totals = { "collections": len(collections), @@ -49,9 +52,8 @@ async def vector_storage_overview( "bytes": sum(item["bytes"] for item in collections), } return { - "fallback_provider": vector_store.provider, - "configured_providers": list(vector_store.configured_providers), - "provider_health": provider_health, + "provider": "turbopuffer", + "health": health, "collections": collections, "totals": totals, } diff --git a/api/bigrag/routers/collections.py b/api/bigrag/routers/collections.py index 087ea0d4..381a0663 100644 --- a/api/bigrag/routers/collections.py +++ b/api/bigrag/routers/collections.py @@ -49,12 +49,10 @@ def _collection_response(c: Collection) -> CollectionResponse: description=c.description, embedding_provider=c.embedding_provider, embedding_model=c.embedding_model, - vector_store_provider=c.vector_store_provider, dimension=c.dimension, chunk_size=c.chunk_size, chunk_overlap=c.chunk_overlap, chunk_strategy=c.chunk_strategy, - index_type=c.index_type, tenant_field=c.tenant_field, has_metadata_schema=bool(c.metadata_schema), document_count=c.document_count, @@ -125,7 +123,6 @@ async def create_collection( logger.info( "create collection", name=body.name, - vector_store_provider=body.vector_store_provider, provider=body.embedding_provider, model=body.embedding_model, ) @@ -232,14 +229,12 @@ async def create_collection( collection = Collection( name=body.name, description=body.description, - vector_store_provider=body.vector_store_provider, embedding_provider=provider, embedding_model=model, dimension=dimension, chunk_size=body.chunk_size, chunk_overlap=body.chunk_overlap, chunk_strategy=body.chunk_strategy, - index_type=body.index_type, tenant_field=body.tenant_field, meta=body.metadata, metadata_schema=body.metadata_schema, @@ -260,11 +255,11 @@ async def create_collection( await session.commit() except IntegrityError as e: await session.rollback() - await vector_store.delete_collection(body.name, provider=body.vector_store_provider) + await vector_store.delete_collection(body.name) raise HTTPException(status_code=409, detail="Collection already exists") from e except Exception: await session.rollback() - await vector_store.delete_collection(body.name, provider=body.vector_store_provider) + await vector_store.delete_collection(body.name) raise await session.refresh(collection) await collection_cache.invalidate(body.name) @@ -285,7 +280,6 @@ async def create_collection( metadata={ "name": body.name, "provider": provider, - "vector_store_provider": body.vector_store_provider, "model": model, "dimension": dimension, }, diff --git a/api/bigrag/routers/collections_embedding.py b/api/bigrag/routers/collections_embedding.py index 396a5451..76bcc6de 100644 --- a/api/bigrag/routers/collections_embedding.py +++ b/api/bigrag/routers/collections_embedding.py @@ -14,6 +14,7 @@ from bigrag.services.ingestion_job import create_ingestion_job from bigrag.services.queue import ingestion_queue from bigrag.services.retrieval import invalidate_collection_query_cache +from bigrag.services.vector_store import vector_store logger = get_logger("bigrag.routers.collections_embedding") @@ -47,7 +48,6 @@ async def reembed_collection( "chunk_size": collection.chunk_size, "chunk_overlap": collection.chunk_overlap, "chunk_strategy": collection.chunk_strategy or "paragraph", - "vector_store_provider": collection.vector_store_provider, "tenant_field": collection.tenant_field, } jobs = [ @@ -61,6 +61,8 @@ async def reembed_collection( ] doc_ids = [doc_id for doc_id, _ in docs] + for doc_id in doc_ids: + await vector_store.delete_by_document(name, str(doc_id)) await session.execute( sa.update(Document) .where(Document.id.in_(doc_ids)) diff --git a/api/bigrag/routers/documents.py b/api/bigrag/routers/documents.py index 3c14e52f..19ac88c3 100644 --- a/api/bigrag/routers/documents.py +++ b/api/bigrag/routers/documents.py @@ -286,7 +286,6 @@ async def delete_document( await vector_store.delete_by_document( collection_name, document_id, - provider=collection.get("vector_store_provider"), ) storage = get_storage() await storage.delete(file_path) @@ -333,7 +332,6 @@ async def reprocess_document( await vector_store.delete_by_document( collection_name, document_id, - provider=collection.get("vector_store_provider"), ) doc.status = "pending" @@ -451,7 +449,6 @@ async def get_document_chunks( document_id, limit=limit, offset=offset, - provider=collection.get("vector_store_provider"), ) return {"chunks": chunks, "total": total} diff --git a/api/bigrag/routers/documents_batch.py b/api/bigrag/routers/documents_batch.py index 5c52d16d..8a18bac9 100644 --- a/api/bigrag/routers/documents_batch.py +++ b/api/bigrag/routers/documents_batch.py @@ -241,7 +241,6 @@ async def _delete_one(doc_id: str, doc: Document) -> bool: await vector_store.delete_by_document( collection_name, doc_id, - provider=collection.get("vector_store_provider"), ) storage = get_storage() await storage.delete(doc.file_path) diff --git a/api/bigrag/routers/documents_global.py b/api/bigrag/routers/documents_global.py index 881f8c5d..8c72c566 100644 --- a/api/bigrag/routers/documents_global.py +++ b/api/bigrag/routers/documents_global.py @@ -60,6 +60,5 @@ async def get_document_chunks_global( document_id, limit=limit, offset=offset, - provider=collection.get("vector_store_provider"), ) return {"chunks": chunks, "total": total} diff --git a/api/bigrag/routers/evaluation.py b/api/bigrag/routers/evaluation.py index 9eba0e5e..38b3384e 100644 --- a/api/bigrag/routers/evaluation.py +++ b/api/bigrag/routers/evaluation.py @@ -122,7 +122,6 @@ async def run_evaluation( search_mode=body.search_mode, filters=case_filters, reranking_config=get_reranking_config(collection), - vector_store_provider=collection.get("vector_store_provider"), ) hit_ids = [r.get("document_id") or r.get("id") for r in outcome.results] expected = set(case.relevant_ids) diff --git a/api/bigrag/routers/query.py b/api/bigrag/routers/query.py index 9ab04c50..811073e7 100644 --- a/api/bigrag/routers/query.py +++ b/api/bigrag/routers/query.py @@ -95,7 +95,6 @@ async def query_collection( search_mode=search_mode, reranking_config=get_reranking_config(collection), rerank_override=body.rerank, - vector_store_provider=collection.get("vector_store_provider"), ) logger.info( @@ -214,7 +213,6 @@ async def multi_collection_query( embedding_models = {} reranking_configs = {} - vector_store_providers = {} resolved_collections = await asyncio.gather( *[get_collection_or_404(col_name) for col_name in body.collections] ) @@ -225,7 +223,6 @@ async def multi_collection_query( except (ImportError, ValueError) as e: raise HTTPException(status_code=400, detail=f"Collection '{col_name}': {e}") from e reranking_configs[col_name] = get_reranking_config(collection) - vector_store_providers[col_name] = collection.get("vector_store_provider") or "qdrant" include_multimodal_by_collection = { col_name: bool(body.multimodal and collection.get("multimodal_enabled")) for col_name, collection in zip(body.collections, resolved_collections, strict=True) @@ -241,7 +238,6 @@ async def multi_collection_query( search_mode=body.search_mode, reranking_configs=reranking_configs, rerank_override=body.rerank, - vector_store_providers=vector_store_providers, ) logger.info("multi-query complete", collections=body.collections, results=len(results)) @@ -314,7 +310,6 @@ async def run_one(item: BatchQueryItem) -> tuple[BatchQueryItem, list[dict], int search_mode=item.search_mode, reranking_config=get_reranking_config(collection), rerank_override=item.rerank, - vector_store_provider=collection.get("vector_store_provider"), ) include_multimodal = bool(item.multimodal and collection.get("multimodal_enabled")) diff --git a/api/bigrag/routers/vectors.py b/api/bigrag/routers/vectors.py index b2c27e2c..f673299e 100644 --- a/api/bigrag/routers/vectors.py +++ b/api/bigrag/routers/vectors.py @@ -90,7 +90,6 @@ async def upsert_vectors( embeddings=embeddings, texts=texts, metadata=metadata, - provider=collection.get("vector_store_provider"), ) await invalidate_collection_query_cache(collection_name) logger.info("vector upsert complete", collection=collection_name, upserted=count) @@ -123,12 +122,11 @@ async def delete_vectors( status_code=413, detail=f"Too many vector IDs. Max: {limits['max_vector_delete_count']}", ) - collection = await get_collection_or_404(collection_name) + await get_collection_or_404(collection_name) logger.info("vector delete", collection=collection_name, ids=len(body.ids)) await vector_store.delete_by_ids( collection_name, body.ids, - provider=collection.get("vector_store_provider"), ) await invalidate_collection_query_cache(collection_name) access_log.set_context(request, metadata={"deleted": len(body.ids)}) diff --git a/api/bigrag/services/backup/exporters.py b/api/bigrag/services/backup/exporters.py index 4ff39bb2..bcbdbbf1 100644 --- a/api/bigrag/services/backup/exporters.py +++ b/api/bigrag/services/backup/exporters.py @@ -77,7 +77,6 @@ async def _export_vector_store(temp_dir: Path) -> dict[str, int]: async for point in vector_store.iter_collection_points( collection.name, with_vectors=False, - provider=collection.vector_store_provider, ): f.write(orjson.dumps(_point_payload(point)) + b"\n") count += 1 @@ -88,7 +87,6 @@ async def _export_vector_store(temp_dir: Path) -> dict[str, int]: collections_meta.append( { "collection": collection.name, - "provider": collection.vector_store_provider, "vector_store_collection": collection.name, "exists": exists, "points": count, diff --git a/api/bigrag/services/backup/jobs.py b/api/bigrag/services/backup/jobs.py index e2332b03..085630ff 100644 --- a/api/bigrag/services/backup/jobs.py +++ b/api/bigrag/services/backup/jobs.py @@ -12,7 +12,7 @@ import sqlalchemy as sa from bigrag.db.engine import session_factory -from bigrag.db.models import AuditLog, BackupJob, ConnectorSyncJob, VectorMigrationJob +from bigrag.db.models import AuditLog, BackupJob, ConnectorSyncJob from bigrag.logging import get_logger from bigrag.services.maintenance import acquire_backup_lock, active_lock, release_backup_lock from bigrag.services.queue import ingestion_queue @@ -25,8 +25,6 @@ logger = get_logger("bigrag.backup") -ACTIVE_VECTOR_MIGRATION_STATUSES = ("pending", "running", "canceling") - async def create_backup_job(*, label: str, created_by: uuid.UUID | None) -> BackupJob: lock = await active_lock() @@ -43,17 +41,6 @@ async def create_backup_job(*, label: str, created_by: uuid.UUID | None) -> Back ) if active is not None: raise BackupConfigError("A backup is already pending or running") - active_migration = await session.scalar( - sa.select(VectorMigrationJob) - .where(VectorMigrationJob.status.in_(ACTIVE_VECTOR_MIGRATION_STATUSES)) - .order_by(VectorMigrationJob.created_at.desc()) - .limit(1) - .with_for_update() - ) - if active_migration is not None: - raise BackupConfigError( - "A vector migration is already pending, running, or canceling" - ) job = BackupJob(label=label.strip(), created_by=created_by) session.add(job) await session.refresh(job) diff --git a/api/bigrag/services/chat/questions/api.py b/api/bigrag/services/chat/questions/api.py index 03a4bb9a..1fe2cbbf 100644 --- a/api/bigrag/services/chat/questions/api.py +++ b/api/bigrag/services/chat/questions/api.py @@ -192,7 +192,6 @@ async def _sample_chunks( str(document.id), limit=CHUNK_LIMIT, offset=offset, - provider=collection.get("vector_store_provider"), ) for chunk in chunks: item = dict(chunk) diff --git a/api/bigrag/services/chat/turn/prepare.py b/api/bigrag/services/chat/turn/prepare.py index f8484d2b..617a17fe 100644 --- a/api/bigrag/services/chat/turn/prepare.py +++ b/api/bigrag/services/chat/turn/prepare.py @@ -90,7 +90,6 @@ async def _prepare_chat_turn( search_mode=search_mode, reranking_config=get_reranking_config(collection), rerank_override=rerank, - vector_store_provider=collection.get("vector_store_provider"), ) sources = await _sources_from_results(session, outcome.results) timings = ChatTimings( diff --git a/api/bigrag/services/collection_cache.py b/api/bigrag/services/collection_cache.py index de44635b..6d13f7d7 100644 --- a/api/bigrag/services/collection_cache.py +++ b/api/bigrag/services/collection_cache.py @@ -34,7 +34,6 @@ def _serialize(c: Collection, preset: EmbeddingPreset | None = None) -> dict: "embedding_preset_id": str(c.embedding_preset_id) if c.embedding_preset_id else None, "embedding_preset_api_key": (preset.api_key if preset else None) if crypto_ready else None, "embedding_preset_base_url": preset.base_url if preset else None, - "vector_store_provider": c.vector_store_provider, "dimension": c.dimension, "chunk_size": c.chunk_size, "chunk_overlap": c.chunk_overlap, @@ -48,7 +47,6 @@ def _serialize(c: Collection, preset: EmbeddingPreset | None = None) -> dict: "reranking_api_key": c.reranking_api_key if crypto_ready else None, "multimodal_enabled": c.multimodal_enabled, "multimodal_enrichment_enabled": c.multimodal_enrichment_enabled, - "index_type": c.index_type, "tenant_field": c.tenant_field, "metadata_schema": c.metadata_schema, "metadata": c.meta or {}, diff --git a/api/bigrag/services/collection_provision.py b/api/bigrag/services/collection_provision.py index 4dca1db4..8c23088e 100644 --- a/api/bigrag/services/collection_provision.py +++ b/api/bigrag/services/collection_provision.py @@ -36,21 +36,19 @@ async def verify_embedding_credentials( ) from exc -def vector_store_unavailable_detail(provider: str) -> str: - if provider == "turbopuffer": - return ( - "turbopuffer is not configured. Save a turbopuffer API key in Vector Storage " - "before creating a turbopuffer collection." - ) - return f"{provider} vector store is not configured." +def vector_store_unavailable_detail() -> str: + return ( + "Turbopuffer is not configured. Save a turbopuffer API key in Settings " + "before creating a collection." + ) -def ensure_vector_store_provider_available(provider: str) -> None: - if provider in vector_store.configured_providers: +def ensure_vector_store_available() -> None: + if vector_store.is_configured(): return raise HTTPException( status_code=400, - detail=vector_store_unavailable_detail(provider), + detail=vector_store_unavailable_detail(), ) @@ -58,48 +56,38 @@ async def create_vector_store_collection( body: CreateCollectionRequest, dimension: int, ) -> None: - ensure_vector_store_provider_available(body.vector_store_provider) + ensure_vector_store_available() try: await vector_store.create_collection( body.name, dimension, - index_type=body.index_type, tenant_field=body.tenant_field, - provider=body.vector_store_provider, ) except RuntimeError as e: message = str(e) if "API key is not configured" in message or "client is not connected" in message: raise HTTPException( status_code=400, - detail=vector_store_unavailable_detail(body.vector_store_provider), + detail=vector_store_unavailable_detail(), ) from e logger.warning( "vector collection create failed", collection=body.name, - vector_store_provider=body.vector_store_provider, error_type=e.__class__.__name__, error=message, ) raise HTTPException( status_code=502, - detail=( - f"Unable to create {body.vector_store_provider} vector collection. " - "Check Vector Storage settings." - ), + detail="Unable to create vector collection. Check Turbopuffer settings.", ) from e except httpx.HTTPError as e: logger.warning( "vector collection create failed", collection=body.name, - vector_store_provider=body.vector_store_provider, error_type=e.__class__.__name__, error=str(e), ) raise HTTPException( status_code=502, - detail=( - f"Unable to create {body.vector_store_provider} vector collection. " - "Check Vector Storage settings." - ), + detail="Unable to create vector collection. Check Turbopuffer settings.", ) from e diff --git a/api/bigrag/services/collections.py b/api/bigrag/services/collections.py index cf7d72d5..a6a7533a 100644 --- a/api/bigrag/services/collections.py +++ b/api/bigrag/services/collections.py @@ -25,14 +25,13 @@ async def delete_collection(session: AsyncSession, name: str) -> str: logger.info("delete collection jobs cancelled", collection=name, flushed=flushed) deleted_id = str(collection.id) - vector_store_provider = collection.vector_store_provider await session.delete(collection) await session.commit() await collection_cache.invalidate(name) await invalidate_collection_query_cache(name) logger.info("delete collection database records removed", collection=name) - await vector_store.delete_collection(name, provider=vector_store_provider) + await vector_store.delete_collection(name) logger.info("delete collection vectors dropped", collection=name) deleted = await get_storage().delete_prefix(f"{name}/") @@ -51,7 +50,6 @@ async def truncate_collection(session: AsyncSession, name: str) -> str: logger.info("truncate collection jobs cancelled", collection=name, flushed=flushed) collection_id = str(collection.id) - vector_store_provider = collection.vector_store_provider await session.execute(sa.delete(Document).where(Document.collection_id == collection.id)) await session.execute( sa.update(Collection).where(Collection.id == collection.id).values(document_count=0) @@ -61,7 +59,7 @@ async def truncate_collection(session: AsyncSession, name: str) -> str: await invalidate_collection_query_cache(name) logger.info("truncate collection documents removed", collection=name) - await vector_store.delete_collection(name, provider=vector_store_provider) + await vector_store.delete_collection(name) logger.info("truncate collection vectors cleared", collection=name) deleted = await get_storage().delete_prefix(f"{name}/") diff --git a/api/bigrag/services/connectors/documents.py b/api/bigrag/services/connectors/documents.py index 19844259..72c5e9f0 100644 --- a/api/bigrag/services/connectors/documents.py +++ b/api/bigrag/services/connectors/documents.py @@ -104,7 +104,6 @@ async def _put_downloaded(storage_key: str) -> None: await vector_store.delete_by_document( source.collection_name, str(doc.id), - provider=collection.vector_store_provider, ) old_path = doc.file_path storage_key = f"{source.collection_name}/{doc.id}{downloaded.file_ext}" @@ -156,7 +155,6 @@ async def delete_synced_document( await vector_store.delete_by_document( source.collection_name, str(doc.id), - provider=collection.vector_store_provider, ) storage = get_storage() await storage.delete(doc.file_path) diff --git a/api/bigrag/services/connectors/manifest.py b/api/bigrag/services/connectors/manifest.py index e4358686..6b869b59 100644 --- a/api/bigrag/services/connectors/manifest.py +++ b/api/bigrag/services/connectors/manifest.py @@ -26,7 +26,6 @@ def collection_dict_for_sync(collection: Collection) -> dict[str, Any]: "chunk_size": collection.chunk_size, "chunk_overlap": collection.chunk_overlap, "chunk_strategy": collection.chunk_strategy or "paragraph", - "vector_store_provider": collection.vector_store_provider, "tenant_field": collection.tenant_field, "metadata_schema": collection.metadata_schema, } diff --git a/api/bigrag/services/health.py b/api/bigrag/services/health.py index cacd6952..89aa6232 100644 --- a/api/bigrag/services/health.py +++ b/api/bigrag/services/health.py @@ -211,11 +211,6 @@ async def _check_redis(): healthy = False else: checks[name] = True - checks["vector_store_provider"] = "per_collection" - checks["qdrant"] = ( - checks["vector_store"] if "qdrant" in getattr(vs, "configured_providers", ()) else None - ) - embedding_result = await check_embedding_provider() checks.update(embedding_result) if not embedding_result.get("embedding"): diff --git a/api/bigrag/services/ingestion_job.py b/api/bigrag/services/ingestion_job.py index 3bad2281..00e928d3 100644 --- a/api/bigrag/services/ingestion_job.py +++ b/api/bigrag/services/ingestion_job.py @@ -17,7 +17,6 @@ class IngestionJob: chunk_size: int chunk_overlap: int chunk_strategy: str = "paragraph" - vector_store_provider: str = "qdrant" tenant_field: str | None = None embedding_base_url: str | None = None multimodal_enabled: bool = False @@ -45,7 +44,6 @@ def serialize(self) -> bytes: "chunk_size": self.chunk_size, "chunk_overlap": self.chunk_overlap, "chunk_strategy": self.chunk_strategy, - "vector_store_provider": self.vector_store_provider, "tenant_field": self.tenant_field, "attempt": self.attempt, "max_attempts": self.max_attempts, @@ -79,7 +77,6 @@ def create_ingestion_job( chunk_size=collection["chunk_size"], chunk_overlap=collection["chunk_overlap"], chunk_strategy=collection.get("chunk_strategy") or "paragraph", - vector_store_provider=collection.get("vector_store_provider") or "qdrant", tenant_field=collection.get("tenant_field"), multimodal_enabled=bool(collection.get("multimodal_enabled")), multimodal_enrichment_enabled=bool(collection.get("multimodal_enrichment_enabled")), diff --git a/api/bigrag/services/jobs/actors.py b/api/bigrag/services/jobs/actors.py index c4dfd69b..f24204ad 100644 --- a/api/bigrag/services/jobs/actors.py +++ b/api/bigrag/services/jobs/actors.py @@ -71,10 +71,6 @@ def enqueue_backup_job(job_id: str) -> None: run_backup.send(job_id) -def enqueue_vector_migration_job(job_id: str) -> None: - run_vector_migration.send(job_id) - - def seed_periodic_jobs(enabled_queues: set[str] | None = None) -> None: if enabled_queues is None or MAINTENANCE_QUEUE in enabled_queues: _schedule_sync( @@ -227,18 +223,6 @@ async def _run_backup(job_id: str) -> None: await run_backup_job(job_id) -@dramatiq.actor(queue_name=MAINTENANCE_QUEUE, max_retries=0, broker=broker) -def run_vector_migration(job_id: str) -> None: - _run(_run_vector_migration, job_id) - - -async def _run_vector_migration(job_id: str) -> None: - await ensure_worker_runtime() - from bigrag.services.vector_migration import run_vector_migration_job - - await run_vector_migration_job(job_id) - - @dramatiq.actor(queue_name=MAINTENANCE_QUEUE, max_retries=0, broker=broker) def run_cleanup() -> None: try: diff --git a/api/bigrag/services/jobs/runtime.py b/api/bigrag/services/jobs/runtime.py index 6fab5c0a..e569385d 100644 --- a/api/bigrag/services/jobs/runtime.py +++ b/api/bigrag/services/jobs/runtime.py @@ -3,6 +3,7 @@ import asyncio import os from datetime import UTC, datetime +from typing import Any from bigrag import __version__ from bigrag import config as config_module @@ -31,8 +32,16 @@ _initialized = False _storage = None _heartbeat_task: asyncio.Task | None = None +_vector_store_settings: tuple[object, ...] | None = None _HEARTBEAT_SECONDS = 30 _HEARTBEAT_TTL_SECONDS = 120 +_RUNTIME_SETTING_KEYS = ( + "ingestion_workers", + "turbopuffer_api_key", + "turbopuffer_base_url", + "turbopuffer_namespace_prefix", + "turbopuffer_region", +) _DEFAULT_QUEUES = { INGESTION_QUEUE, CONNECTORS_QUEUE, @@ -46,6 +55,7 @@ async def ensure_worker_runtime() -> None: global _initialized, _storage async with _lock: if _initialized: + await _sync_runtime_settings() await start_worker_heartbeat() return settings = config_module.settings @@ -61,30 +71,9 @@ async def ensure_worker_runtime() -> None: await run_migrations() configure_logging(log_level=settings.log_level, log_format=settings.log_format) logger.info("worker migrations complete") - runtime = await runtime_settings.get_values( - [ - "ingestion_workers", - "qdrant_connect_timeout_seconds", - "qdrant_required", - "qdrant_search_ef", - "qdrant_url", - "turbopuffer_api_key", - "turbopuffer_namespace_prefix", - "turbopuffer_region", - ] - ) + runtime = await runtime_settings.get_values(list(_RUNTIME_SETTING_KEYS)) logger.info("worker runtime settings loaded") - vector_store.configure( - qdrant_url=runtime["qdrant_url"], - connect_timeout_seconds=runtime["qdrant_connect_timeout_seconds"], - search_ef=runtime["qdrant_search_ef"], - turbopuffer_api_key=runtime["turbopuffer_api_key"], - turbopuffer_region=runtime["turbopuffer_region"], - turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"], - ) - vector_store.connect() - await vector_store.health_check() - logger.info("worker vector store ready") + await _sync_vector_store(runtime) _storage = await init_storage_from_runtime(upload_dir=settings.upload_dir) await redis_cache.connect(settings.redis_url) await event_bus.connect(settings.redis_url) @@ -99,6 +88,48 @@ async def ensure_worker_runtime() -> None: logger.info("worker ready") +async def _sync_runtime_settings() -> None: + runtime = await runtime_settings.get_values(list(_RUNTIME_SETTING_KEYS)) + ingestion_queue._num_workers = runtime["ingestion_workers"] + await _sync_vector_store(runtime) + + +async def _sync_vector_store(runtime: dict[str, Any]) -> None: + global _vector_store_settings + settings = ( + runtime["turbopuffer_api_key"], + runtime["turbopuffer_base_url"], + runtime["turbopuffer_namespace_prefix"], + runtime["turbopuffer_region"], + ) + if settings == _vector_store_settings: + return + if _vector_store_settings is not None: + await vector_store.close() + vector_store.configure( + turbopuffer_api_key=runtime["turbopuffer_api_key"], + turbopuffer_base_url=runtime["turbopuffer_base_url"], + turbopuffer_region=runtime["turbopuffer_region"], + turbopuffer_namespace_prefix=runtime["turbopuffer_namespace_prefix"], + ) + if runtime["turbopuffer_api_key"]: + try: + vector_store.connect() + await vector_store.health_check() + logger.info("worker vector store ready") + except Exception as exc: + logger.warning( + "worker vector store connection failed; worker will continue degraded", + provider="turbopuffer", + error_type=exc.__class__.__name__, + error=str(exc), + ) + else: + logger.warning("worker vector store not configured", provider="turbopuffer") + _vector_store_settings = settings + ingestion_queue.bind_vector_store(vector_store) + + async def record_worker_heartbeat() -> None: redis = ingestion_queue.redis if redis is not None: diff --git a/api/bigrag/services/maintenance.py b/api/bigrag/services/maintenance.py index e5a57a58..08e6fb55 100644 --- a/api/bigrag/services/maintenance.py +++ b/api/bigrag/services/maintenance.py @@ -11,7 +11,6 @@ MAINTENANCE_LOCK_NAME = "maintenance" BACKUP_LOCK_NAME = MAINTENANCE_LOCK_NAME -VECTOR_MIGRATION_LOCK_NAME = MAINTENANCE_LOCK_NAME class MaintenanceActiveError(RuntimeError): diff --git a/api/bigrag/services/queue_embedding/insert.py b/api/bigrag/services/queue_embedding/insert.py index 82242a67..8479caab 100644 --- a/api/bigrag/services/queue_embedding/insert.py +++ b/api/bigrag/services/queue_embedding/insert.py @@ -107,7 +107,6 @@ async def chunk_and_embed( job.collection_name, job.embedding_dimension, tenant_field=getattr(job, "tenant_field", None), - provider=job.vector_store_provider, ) await ensure_job_current(job) @@ -246,7 +245,6 @@ async def _embed_one_bounded(bn, bs, be, bc): texts=batch_texts, embeddings=embeddings, metadata=metadata, - provider=job.vector_store_provider, ) try: await ensure_job_current(job) @@ -254,7 +252,6 @@ async def _embed_one_bounded(bn, bs, be, bc): await vector_store.delete_by_document( job.collection_name, doc, - provider=job.vector_store_provider, ) raise insert_elapsed = time.monotonic() - t1 @@ -266,7 +263,6 @@ async def _embed_one_bounded(bn, bs, be, bc): await vector_store.delete_by_ids( job.collection_name, [f"{doc}_{i}" for i in range(batch_start, batch_end)], - provider=job.vector_store_provider, ) except Exception as cleanup_exc: logger.warning( diff --git a/api/bigrag/services/retrieval/modes.py b/api/bigrag/services/retrieval/modes.py index a0e5d3c1..8104db3f 100644 --- a/api/bigrag/services/retrieval/modes.py +++ b/api/bigrag/services/retrieval/modes.py @@ -8,7 +8,7 @@ from bigrag.services.embedding import EmbeddingModel from bigrag.services.retrieval.cache import embed_query_with_cache from bigrag.services.retrieval.fusion import keyword_patterns, keyword_score, reciprocal_rank_fusion -from bigrag.services.vector_store import VectorStoreFeatureError, VectorStoreProvider, vector_store +from bigrag.services.vector_store import vector_store async def keyword_search( @@ -17,7 +17,6 @@ async def keyword_search( query_terms: list[str], top_k: int, filter_expr: FilterExpression | None, - vector_store_provider: VectorStoreProvider | None, timings: dict[str, float], ) -> list[dict]: t0 = time.monotonic() @@ -27,9 +26,8 @@ async def keyword_search( query_terms=query_terms, top_k=top_k, filters=filter_expr, - provider=vector_store_provider, ) - except VectorStoreFeatureError as exc: + except RuntimeError as exc: raise ValidationError(str(exc)) from exc timings["search_ms"] = (time.monotonic() - t0) * 1000 @@ -53,7 +51,6 @@ async def hybrid_search( top_k: int, max_top_k: int, filter_expr: FilterExpression | None, - vector_store_provider: VectorStoreProvider | None, timings: dict[str, float], ) -> tuple[list[dict], list[float] | None]: t0 = time.monotonic() @@ -67,18 +64,16 @@ async def hybrid_search( query_embedding=query_embedding, top_k=fusion_pool, filters=filter_expr, - provider=vector_store_provider, ) keyword_task = vector_store.text_search( collection=collection_name, query_terms=query_terms, top_k=fusion_pool, filters=filter_expr, - provider=vector_store_provider, ) try: semantic_results, keyword_raw = await asyncio.gather(semantic_task, keyword_task) - except VectorStoreFeatureError as exc: + except RuntimeError as exc: raise ValidationError(str(exc)) from exc timings["search_ms"] = (time.monotonic() - t0) * 1000 @@ -102,7 +97,6 @@ async def semantic_search( embedding_model: EmbeddingModel, top_k: int, filter_expr: FilterExpression | None, - vector_store_provider: VectorStoreProvider | None, timings: dict[str, float], ) -> tuple[list[dict], list[float]]: t0 = time.monotonic() @@ -115,7 +109,6 @@ async def semantic_search( query_embedding=query_embedding, top_k=top_k, filters=filter_expr, - provider=vector_store_provider, ) timings["search_ms"] = (time.monotonic() - t0) * 1000 return results, query_embedding diff --git a/api/bigrag/services/retrieval/orchestrate.py b/api/bigrag/services/retrieval/orchestrate.py index 6ed442b1..55006a8d 100644 --- a/api/bigrag/services/retrieval/orchestrate.py +++ b/api/bigrag/services/retrieval/orchestrate.py @@ -19,7 +19,6 @@ from bigrag.services.retrieval.modes import hybrid_search, keyword_search, semantic_search from bigrag.services.retrieval.rerank import rerank_results from bigrag.services.runtime_settings import get_values -from bigrag.services.vector_store import VectorStoreProvider, vector_store logger = get_logger("bigrag.retrieval") @@ -40,10 +39,6 @@ def _on_done(t: asyncio.Task) -> None: return task -def _supports_text_search(provider: VectorStoreProvider | None) -> bool: - return vector_store.supports_text_search_for(provider) - - async def retrieve( collection_name: str, query: str, @@ -54,7 +49,6 @@ async def retrieve( search_mode: str = "semantic", reranking_config: dict | None = None, rerank_override: bool | None = None, - vector_store_provider: VectorStoreProvider | None = None, ) -> RetrievalOutcome: if top_k > MAX_TOP_K: raise ValidationError(f"top_k {top_k} exceeds maximum {MAX_TOP_K}") @@ -75,9 +69,6 @@ async def retrieve( filter_expr = build_filter(filters) if filters else None except ValueError as exc: raise ValidationError(str(exc)) from exc - if search_mode in {"keyword", "hybrid"} and not _supports_text_search(vector_store_provider): - provider_label = vector_store_provider or vector_store.provider - raise ValidationError(f"{provider_label} does not support {search_mode} search in v1") query_terms = tokenize_query(query) result_cache_key: str | None = None @@ -126,7 +117,6 @@ async def retrieve( query_terms=query_terms, top_k=top_k, filter_expr=filter_expr, - vector_store_provider=vector_store_provider, timings=timings, ) elif search_mode == "hybrid": @@ -138,7 +128,6 @@ async def retrieve( top_k=top_k, max_top_k=MAX_TOP_K, filter_expr=filter_expr, - vector_store_provider=vector_store_provider, timings=timings, ) else: @@ -148,7 +137,6 @@ async def retrieve( embedding_model=embedding_model, top_k=top_k, filter_expr=filter_expr, - vector_store_provider=vector_store_provider, timings=timings, ) @@ -225,7 +213,6 @@ async def retrieve_multi( search_mode: str = "semantic", reranking_configs: dict[str, dict] | None = None, rerank_override: bool | None = None, - vector_store_providers: dict[str, VectorStoreProvider] | None = None, ) -> list[dict]: if top_k > MAX_TOP_K: raise ValidationError(f"top_k {top_k} exceeds maximum {MAX_TOP_K}") @@ -242,7 +229,6 @@ async def search_one(col_name: str) -> list[dict]: search_mode=search_mode, reranking_config=col_reranking, rerank_override=rerank_override, - vector_store_provider=(vector_store_providers or {}).get(col_name), ) for r in outcome.results: r["collection"] = col_name diff --git a/api/bigrag/services/runtime_setting_specs/vector_store.py b/api/bigrag/services/runtime_setting_specs/vector_store.py index 1080ec7f..c7f51aa4 100644 --- a/api/bigrag/services/runtime_setting_specs/vector_store.py +++ b/api/bigrag/services/runtime_setting_specs/vector_store.py @@ -3,32 +3,6 @@ from bigrag.services.runtime_setting_specs._spec import SettingSpec VECTOR_STORE_SPECS: tuple[SettingSpec, ...] = ( - SettingSpec( - key="qdrant_url", - group="vector_store", - label="Qdrant URL", - kind="string", - default="http://localhost:6333", - description="Qdrant connection URL.", - ), - SettingSpec( - key="qdrant_connect_timeout_seconds", - group="vector_store", - label="Qdrant connect timeout", - kind="int", - default=10, - description="Qdrant startup connection timeout in seconds.", - min=0, - max=300, - ), - SettingSpec( - key="qdrant_required", - group="vector_store", - label="Require vector store", - kind="bool", - default=False, - description="Fail startup if configured vector-store clients cannot be reached.", - ), SettingSpec( key="turbopuffer_api_key", group="vector_store", @@ -38,6 +12,14 @@ description="turbopuffer API key.", secret=True, ), + SettingSpec( + key="turbopuffer_base_url", + group="vector_store", + label="turbopuffer base URL", + kind="string", + default=None, + description="Optional turbopuffer API base URL.", + ), SettingSpec( key="turbopuffer_region", group="vector_store", @@ -54,14 +36,4 @@ default="bigrag_", description="Prefix prepended to turbopuffer namespace names.", ), - SettingSpec( - key="qdrant_search_ef", - group="vector_store", - label="Qdrant search ef", - kind="int", - default=None, - description="Optional Qdrant HNSW search ef override.", - min=1, - max=10000, - ), ) diff --git a/api/bigrag/services/runtime_settings_apply.py b/api/bigrag/services/runtime_settings_apply.py index 711d5549..f9784f4c 100644 --- a/api/bigrag/services/runtime_settings_apply.py +++ b/api/bigrag/services/runtime_settings_apply.py @@ -42,10 +42,8 @@ } VECTOR_CONFIG_KEYS = { - "qdrant_connect_timeout_seconds", - "qdrant_search_ef", - "qdrant_url", "turbopuffer_api_key", + "turbopuffer_base_url", "turbopuffer_namespace_prefix", "turbopuffer_region", } @@ -155,8 +153,9 @@ async def _prepare_vector_backend(values: dict[str, Any]) -> VectorStore: store = VectorStore() _configure_vector_store(store, values) try: - store.connect() - await store.health_check() + if values.get("turbopuffer_api_key"): + store.connect() + await store.health_check() return store except Exception: await store.close() @@ -165,10 +164,8 @@ async def _prepare_vector_backend(values: dict[str, Any]) -> VectorStore: def _configure_vector_store(store: VectorStore, values: dict[str, Any]) -> None: store.configure( - qdrant_url=values["qdrant_url"], - connect_timeout_seconds=values["qdrant_connect_timeout_seconds"], - search_ef=values["qdrant_search_ef"], turbopuffer_api_key=values["turbopuffer_api_key"], + turbopuffer_base_url=values["turbopuffer_base_url"], turbopuffer_region=values["turbopuffer_region"], turbopuffer_namespace_prefix=values["turbopuffer_namespace_prefix"], ) diff --git a/api/bigrag/services/vector_migration/__init__.py b/api/bigrag/services/vector_migration/__init__.py deleted file mode 100644 index 28c5b5a8..00000000 --- a/api/bigrag/services/vector_migration/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from __future__ import annotations - -from bigrag.services.vector_migration.jobs import ( - VectorMigrationConflictError, - VectorMigrationError, - create_vector_migration_job, - delete_vector_migration_job, - run_vector_migration_job, -) - -__all__ = [ - "VectorMigrationConflictError", - "VectorMigrationError", - "create_vector_migration_job", - "delete_vector_migration_job", - "run_vector_migration_job", -] diff --git a/api/bigrag/services/vector_migration/jobs.py b/api/bigrag/services/vector_migration/jobs.py deleted file mode 100644 index b4cf092c..00000000 --- a/api/bigrag/services/vector_migration/jobs.py +++ /dev/null @@ -1,522 +0,0 @@ -from __future__ import annotations - -import asyncio -import uuid -from datetime import UTC, datetime -from typing import Any - -import sqlalchemy as sa - -from bigrag.db.engine import session_factory -from bigrag.db.models import AuditLog, BackupJob, Collection, ConnectorSyncJob, VectorMigrationJob -from bigrag.logging import get_logger -from bigrag.services import collection_cache -from bigrag.services.error_sanitize import sanitize_message_text -from bigrag.services.maintenance import ( - acquire_maintenance_lock, - active_lock, - release_maintenance_lock, -) -from bigrag.services.queue import ingestion_queue -from bigrag.services.retrieval import invalidate_collection_query_cache -from bigrag.services.runtime_settings import get_value -from bigrag.services.vector_store import vector_store -from bigrag.services.vector_store._util import validate_provider -from bigrag.services.vector_store.base import _FIXED_PAYLOAD_FIELDS - -logger = get_logger("bigrag.vector_migration") - -ACTIVE_STATUSES = ("pending", "running", "canceling") -ACTIVE_BACKUP_STATUSES = ("pending", "running") - - -class VectorMigrationError(RuntimeError): - pass - - -class VectorMigrationConflictError(VectorMigrationError): - pass - - -class VectorMigrationCanceledError(VectorMigrationError): - pass - - -async def create_vector_migration_job( - *, - collection_name: str, - target_provider: str, - created_by: uuid.UUID | None, -) -> VectorMigrationJob: - target = validate_provider(target_provider) - lock = await active_lock() - if lock is not None: - raise VectorMigrationConflictError(f"Instance maintenance active: {lock.reason}") - if target not in vector_store.configured_providers: - raise VectorMigrationError(f"{target} vector store is not configured") - async with session_factory()() as session: - async with session.begin(): - active_backup = await session.scalar( - sa.select(BackupJob) - .where(BackupJob.status.in_(ACTIVE_BACKUP_STATUSES)) - .order_by(BackupJob.created_at.desc()) - .limit(1) - .with_for_update() - ) - if active_backup is not None: - raise VectorMigrationConflictError("A backup is already pending or running") - active = await session.scalar( - sa.select(VectorMigrationJob) - .where(VectorMigrationJob.status.in_(ACTIVE_STATUSES)) - .order_by(VectorMigrationJob.created_at.desc()) - .limit(1) - .with_for_update() - ) - if active is not None: - raise VectorMigrationConflictError( - "A vector migration is already pending, running, or canceling" - ) - collection = await session.scalar( - sa.select(Collection).where(Collection.name == collection_name).with_for_update() - ) - if collection is None: - raise VectorMigrationError("Collection not found") - source = validate_provider(collection.vector_store_provider) - if source == target: - raise VectorMigrationConflictError("Collection already uses that vector provider") - job = VectorMigrationJob( - collection_id=collection.id, - collection_name=collection.name, - source_provider=source, - target_provider=target, - created_by=created_by, - ) - session.add(job) - await session.refresh(job) - return job - - -async def delete_vector_migration_job(job_id: uuid.UUID) -> str | None: - async with session_factory()() as session: - async with session.begin(): - job = await session.scalar( - sa.select(VectorMigrationJob) - .where(VectorMigrationJob.id == job_id) - .with_for_update() - ) - if job is None: - return None - if job.status == "pending" or job.status in {"succeeded", "failed"}: - await session.delete(job) - return "deleted" - details = dict(job.details or {}) - details["delete_requested"] = True - job.details = details - job.status = "canceling" - job.phase = "canceling" - job.updated_at = datetime.now(UTC) - return "stop_requested" - - -async def run_vector_migration_job(job_id: str) -> None: - owner_id = uuid.UUID(job_id) - locked = False - try: - job = await _get_job(owner_id) - if job is None: - return - if job.status != "pending": - return - locked = await acquire_maintenance_lock( - owner_id, - reason=f"vector migration for {job.collection_name}", - metadata={ - "collection": job.collection_name, - "source_provider": job.source_provider, - "target_provider": job.target_provider, - }, - ) - if not locked: - await _fail_job(owner_id, "Another maintenance lock is active") - return - if not await _mark_running(owner_id): - return - await _raise_if_delete_requested(owner_id) - await _wait_for_connector_sync_drain(owner_id) - await _wait_for_ingestion_drain(owner_id) - await _run_locked_migration(owner_id) - except VectorMigrationCanceledError: - await _delete_job(owner_id, "vector_migration.deleted", {"reason": "canceled"}) - except Exception as exc: - logger.exception("vector migration failed", job_id=job_id, error=str(exc)) - await _fail_job(owner_id, sanitize_message_text(str(exc)) or "Vector migration failed") - finally: - if locked: - await release_maintenance_lock(owner_id) - - -async def _run_locked_migration(job_id: uuid.UUID) -> None: - job, collection = await _load_job_and_collection(job_id) - if job is None: - return - if collection is None: - await _fail_job(job_id, "Collection not found") - return - source = validate_provider(job.source_provider) - target = validate_provider(job.target_provider) - cutover_done = False - copied = 0 - try: - if collection.vector_store_provider != source: - raise VectorMigrationError( - "Collection vector provider changed before migration started" - ) - await _raise_if_delete_requested(job_id) - await _update_job(job_id, phase="provisioning", progress=0.2) - await vector_store.delete_collection(collection.name, provider=target) - await _raise_if_delete_requested(job_id) - await vector_store.create_collection( - collection.name, - collection.dimension, - index_type=collection.index_type, - tenant_field=collection.tenant_field, - provider=target, - ) - await _raise_if_delete_requested(job_id) - copied = await _copy_points(job_id, collection.name, source, target) - await _raise_if_delete_requested(job_id) - await _update_job( - job_id, - phase="verifying", - progress=0.86, - copied_points=copied, - total_points=copied, - ) - target_count = 0 if copied == 0 else await _count_points(collection.name, target) - if target_count != copied: - raise VectorMigrationError( - f"Target point count mismatch: copied {copied}, target has {target_count}" - ) - await _raise_if_delete_requested(job_id) - await _cutover_collection(job_id, collection.id, collection.name, source, target) - cutover_done = True - await _update_job( - job_id, - phase="cleanup", - progress=0.94, - copied_points=copied, - total_points=copied, - ) - await vector_store.delete_collection(collection.name, provider=source) - await _complete_job(job_id, copied) - except VectorMigrationCanceledError: - if not cutover_done: - try: - await vector_store.delete_collection(collection.name, provider=target) - except Exception as cleanup_exc: - logger.warning( - "canceled vector migration cleanup failed", - collection=collection.name, - target_provider=target, - error=str(cleanup_exc), - ) - await _delete_job(job_id, "vector_migration.deleted", {"reason": "canceled"}) - except Exception as exc: - message = sanitize_message_text(str(exc)) or "Vector migration failed" - if not cutover_done: - try: - await vector_store.delete_collection(collection.name, provider=target) - except Exception as cleanup_exc: - logger.warning( - "partial vector migration cleanup failed", - collection=collection.name, - target_provider=target, - error=str(cleanup_exc), - ) - await _fail_job( - job_id, - message, - phase="cleanup_failed" if cutover_done else "failed", - copied_points=copied, - total_points=copied or None, - ) - - -async def _copy_points( - job_id: uuid.UUID, - collection: str, - source: str, - target: str, -) -> int: - batch_size = max(1, min(int(await get_value("ingestion_batch_size")), 1000)) - copied = 0 - batch: list[dict[str, Any]] = [] - await _raise_if_delete_requested(job_id) - await _update_job(job_id, phase="copying", progress=0.32) - async for point in vector_store.iter_collection_points( - collection, - with_vectors=True, - provider=source, - ): - await _raise_if_delete_requested(job_id) - batch.append(_normalise_point(point)) - if len(batch) >= batch_size: - copied += await _insert_batch(collection, target, batch) - batch.clear() - await _update_job( - job_id, - copied_points=copied, - progress=min(0.84, 0.34 + copied / (copied + batch_size) * 0.48), - ) - if batch: - await _raise_if_delete_requested(job_id) - copied += await _insert_batch(collection, target, batch) - await _update_job(job_id, copied_points=copied, progress=0.84) - return copied - - -def _normalise_point(point: dict[str, Any]) -> dict[str, Any]: - payload = dict(point.get("payload") or {}) - vector = point.get("vector") - if vector is None: - raise VectorMigrationError("Source point is missing its vector") - public_id = str(payload.get("id") or point.get("id") or "") - if not public_id: - raise VectorMigrationError("Source point is missing its id") - return { - "id": public_id, - "document_id": str(payload.get("document_id") or ""), - "chunk_index": int(payload.get("chunk_index") or 0), - "text": str(payload.get("text") or ""), - "vector": vector, - "metadata": { - k: v for k, v in payload.items() if k not in _FIXED_PAYLOAD_FIELDS and v is not None - }, - } - - -async def _insert_batch( - collection: str, - target: str, - batch: list[dict[str, Any]], -) -> int: - return await vector_store.insert( - collection=collection, - ids=[item["id"] for item in batch], - document_ids=[item["document_id"] for item in batch], - chunk_indices=[item["chunk_index"] for item in batch], - texts=[item["text"] for item in batch], - embeddings=[item["vector"] for item in batch], - metadata=[item["metadata"] for item in batch], - provider=target, - ) - - -async def _count_points(collection: str, provider: str) -> int: - count = 0 - async for _point in vector_store.iter_collection_points( - collection, - with_vectors=False, - provider=provider, - ): - count += 1 - return count - - -async def _cutover_collection( - job_id: uuid.UUID, - collection_id: uuid.UUID, - collection_name: str, - source: str, - target: str, -) -> None: - await _update_job(job_id, phase="cutover", progress=0.9) - async with session_factory()() as session: - async with session.begin(): - collection = await session.scalar( - sa.select(Collection).where(Collection.id == collection_id).with_for_update() - ) - if collection is None: - raise VectorMigrationError("Collection not found during cutover") - if collection.vector_store_provider != source: - raise VectorMigrationError("Collection vector provider changed during migration") - collection.vector_store_provider = target - await collection_cache.invalidate(collection_name) - await invalidate_collection_query_cache(collection_name) - - -async def _wait_for_ingestion_drain(job_id: uuid.UUID, max_wait_seconds: int = 1800) -> None: - deadline = asyncio.get_event_loop().time() + max_wait_seconds - while True: - await _raise_if_delete_requested(job_id) - stats = await ingestion_queue.stats - processing = int(stats.get("processing") or 0) - if processing <= 0: - return - if asyncio.get_event_loop().time() >= deadline: - raise VectorMigrationError( - f"Timed out waiting for ingestion drain after {max_wait_seconds}s" - ) - await _update_job(job_id, phase="draining", progress=0.12) - await asyncio.sleep(1) - - -async def _wait_for_connector_sync_drain(job_id: uuid.UUID, max_wait_seconds: int = 1800) -> None: - deadline = asyncio.get_event_loop().time() + max_wait_seconds - while True: - await _raise_if_delete_requested(job_id) - async with session_factory()() as session: - running = await session.scalar( - sa.select(sa.func.count()) - .select_from(ConnectorSyncJob) - .where(ConnectorSyncJob.status == "running") - ) - if int(running or 0) <= 0: - return - if asyncio.get_event_loop().time() >= deadline: - raise VectorMigrationError( - f"Timed out waiting for connector sync drain after {max_wait_seconds}s" - ) - await _update_job(job_id, phase="draining", progress=0.08) - await asyncio.sleep(1) - - -async def _load_job_and_collection( - job_id: uuid.UUID, -) -> tuple[VectorMigrationJob | None, Collection | None]: - async with session_factory()() as session: - job = await session.get(VectorMigrationJob, job_id) - if job is None: - return None, None - collection = await session.scalar( - sa.select(Collection).where(Collection.name == job.collection_name) - ) - return job, collection - - -async def _get_job(job_id: uuid.UUID) -> VectorMigrationJob | None: - async with session_factory()() as session: - return await session.get(VectorMigrationJob, job_id) - - -async def _mark_running(job_id: uuid.UUID) -> bool: - updated = await _update_job( - job_id, - status="running", - phase="draining", - progress=0.04, - started_at=datetime.now(UTC), - ) - if updated <= 0: - return False - await _insert_audit(job_id, "vector_migration.start", {}) - return True - - -async def _complete_job(job_id: uuid.UUID, copied_points: int) -> None: - await _update_job( - job_id, - status="succeeded", - phase="complete", - progress=1.0, - copied_points=copied_points, - total_points=copied_points, - completed_at=datetime.now(UTC), - ) - await _insert_audit(job_id, "vector_migration.succeeded", {"copied_points": copied_points}) - if await _delete_requested(job_id): - await _delete_job( - job_id, - "vector_migration.deleted", - {"reason": "completed_after_delete_request"}, - ) - - -async def _fail_job( - job_id: uuid.UUID, - message: str, - *, - phase: str = "failed", - copied_points: int | None = None, - total_points: int | None = None, -) -> None: - values: dict[str, Any] = { - "status": "failed", - "phase": phase, - "error_message": sanitize_message_text(message), - "completed_at": datetime.now(UTC), - } - if copied_points is not None: - values["copied_points"] = copied_points - if total_points is not None: - values["total_points"] = total_points - await _update_job(job_id, **values) - await _insert_audit(job_id, "vector_migration.failed", {"error": values["error_message"]}) - if await _delete_requested(job_id): - await _delete_job( - job_id, - "vector_migration.deleted", - {"reason": "failed_after_delete_request"}, - ) - - -async def _update_job(job_id: uuid.UUID, **values: Any) -> int: - async with session_factory()() as session: - values["updated_at"] = sa.func.now() - result = await session.execute( - sa.update(VectorMigrationJob).where(VectorMigrationJob.id == job_id).values(**values) - ) - await session.commit() - return result.rowcount or 0 - - -async def _insert_audit(job_id: uuid.UUID, action: str, metadata: dict[str, Any]) -> None: - async with session_factory()() as session: - job = await session.get(VectorMigrationJob, job_id) - session.add( - AuditLog( - actor_id=job.created_by if job else None, - actor_email=None, - api_key_id=None, - action=action, - resource_type="vector_migration_job", - resource_id=str(job_id), - meta=metadata, - ip=None, - user_agent=None, - ) - ) - await session.commit() - - -async def _delete_requested(job_id: uuid.UUID) -> bool: - job = await _get_job(job_id) - if job is None: - return False - return job.status == "canceling" or bool((job.details or {}).get("delete_requested")) - - -async def _raise_if_delete_requested(job_id: uuid.UUID) -> None: - if await _delete_requested(job_id): - raise VectorMigrationCanceledError("Vector migration deletion requested") - - -async def _delete_job(job_id: uuid.UUID, action: str, metadata: dict[str, Any]) -> None: - async with session_factory()() as session: - job = await session.get(VectorMigrationJob, job_id) - session.add( - AuditLog( - actor_id=job.created_by if job else None, - actor_email=None, - api_key_id=None, - action=action, - resource_type="vector_migration_job", - resource_id=str(job_id), - meta=metadata, - ip=None, - user_agent=None, - ) - ) - if job is not None: - await session.delete(job) - await session.commit() diff --git a/api/bigrag/services/vector_store/__init__.py b/api/bigrag/services/vector_store/__init__.py index dd050983..a171a352 100644 --- a/api/bigrag/services/vector_store/__init__.py +++ b/api/bigrag/services/vector_store/__init__.py @@ -1,16 +1,10 @@ from __future__ import annotations -from bigrag.services.vector_store.base import ( - VectorStoreBackend, - VectorStoreFeatureError, - VectorStoreProvider, -) +from bigrag.services.vector_store.base import VectorStoreBackend from bigrag.services.vector_store.facade import VectorStore, vector_store __all__ = [ "VectorStore", "VectorStoreBackend", - "VectorStoreFeatureError", - "VectorStoreProvider", "vector_store", ] diff --git a/api/bigrag/services/vector_store/_util.py b/api/bigrag/services/vector_store/_util.py deleted file mode 100644 index 4b847419..00000000 --- a/api/bigrag/services/vector_store/_util.py +++ /dev/null @@ -1,36 +0,0 @@ -from __future__ import annotations - -from typing import cast - -from bigrag.logging import get_logger -from bigrag.services.vector_store.base import VectorStoreBackend, VectorStoreProvider - -logger = get_logger("bigrag.vector_store") - -PROVIDERS: tuple[VectorStoreProvider, ...] = ("qdrant", "turbopuffer") - - -def validate_provider(value: str) -> VectorStoreProvider: - if value not in PROVIDERS: - raise ValueError(f"Unsupported vector store provider: {value}") - return cast(VectorStoreProvider, value) - - -async def close_backends( - backends: dict[VectorStoreProvider, VectorStoreBackend], - *, - log_errors: bool = False, -) -> None: - seen: set[int] = set() - for backend in backends.values(): - ident = id(backend) - if ident in seen: - continue - seen.add(ident) - try: - await backend.close() - except Exception as exc: - if log_errors: - logger.warning("old vector store close failed", error=str(exc)) - else: - raise diff --git a/api/bigrag/services/vector_store/base.py b/api/bigrag/services/vector_store/base.py index 6992ef0f..ffbda9af 100644 --- a/api/bigrag/services/vector_store/base.py +++ b/api/bigrag/services/vector_store/base.py @@ -2,12 +2,10 @@ import uuid from collections.abc import AsyncIterator -from typing import Literal, Protocol +from typing import Protocol from bigrag.services._retrieval_filters import FilterExpression -VectorStoreProvider = Literal["qdrant", "turbopuffer"] - _POINT_NAMESPACE = uuid.UUID("1b04f7ca-0c3b-5d76-a5bb-6e4b4a40f61d") _FIXED_PAYLOAD_FIELDS = {"id", "text", "document_id", "chunk_index", "embedding"} DEFAULT_SEARCH_PAYLOAD_FIELDS: list[str] = [ @@ -23,14 +21,7 @@ ] -class VectorStoreFeatureError(RuntimeError): - pass - - class VectorStoreBackend(Protocol): - provider: VectorStoreProvider - supports_text_search: bool - def connect(self) -> None: ... async def close(self) -> None: ... @@ -41,7 +32,6 @@ async def create_collection( self, name: str, dimension: int, - index_type: str = "HNSW", tenant_field: str | None = None, ) -> None: ... diff --git a/api/bigrag/services/vector_store/facade.py b/api/bigrag/services/vector_store/facade.py index 956a9139..055e5fa3 100644 --- a/api/bigrag/services/vector_store/facade.py +++ b/api/bigrag/services/vector_store/facade.py @@ -5,99 +5,50 @@ from contextlib import asynccontextmanager from typing import Any -from bigrag.config import settings as _app_settings -from bigrag.logging import get_logger from bigrag.services._retrieval_filters import FilterExpression -from bigrag.services.error_sanitize import sanitize_message_text -from bigrag.services.vector_store._util import PROVIDERS, close_backends, validate_provider -from bigrag.services.vector_store.base import VectorStoreBackend, VectorStoreProvider -from bigrag.services.vector_store.qdrant import QdrantVectorStore +from bigrag.services.vector_store.base import VectorStoreBackend from bigrag.services.vector_store.turbopuffer import TurbopufferVectorStore -logger = get_logger("bigrag.vector_store") - class VectorStore: def __init__(self) -> None: - self.provider: str = "collection" - self.backends: dict[VectorStoreProvider, VectorStoreBackend] = {} - self._configured_providers: set[VectorStoreProvider] = {"qdrant"} - self._fallback_provider: VectorStoreProvider = "qdrant" - self.backend = QdrantVectorStore() + self._backend_instance: VectorStoreBackend = TurbopufferVectorStore() self.client: Any | None = None self._condition = asyncio.Condition() self._active = 0 self._swapping = False + def is_configured(self) -> bool: + return bool(getattr(self.backend, "api_key", None)) + @property def backend(self) -> VectorStoreBackend: - return self.backends[self._fallback_provider] + return self._backend_instance @backend.setter def backend(self, value: VectorStoreBackend) -> None: - provider = validate_provider(getattr(value, "provider", self._fallback_provider)) - self.backends[provider] = value - self._fallback_provider = provider - self._configured_providers.add(provider) + self._backend_instance = value self._sync_client() def configure( self, url: str | None = None, *, - provider: VectorStoreProvider | None = None, - connect_timeout_seconds: int | float | None = 10, - search_ef: int | None = None, - qdrant_url: str | None = None, - qdrant_prefer_grpc: bool | None = None, - qdrant_grpc_port: int | None = None, turbopuffer_api_key: str | None = None, turbopuffer_region: str = "aws-us-east-1", turbopuffer_namespace_prefix: str = "bigrag_", + turbopuffer_base_url: str | None = None, + **_: Any, ) -> None: - if provider is not None: - validate_provider(provider) - prefer_grpc = ( - qdrant_prefer_grpc - if qdrant_prefer_grpc is not None - else _app_settings.qdrant_prefer_grpc + self.backend = TurbopufferVectorStore( + api_key=turbopuffer_api_key, + region=turbopuffer_region or "aws-us-east-1", + namespace_prefix=turbopuffer_namespace_prefix, + base_url=turbopuffer_base_url, ) - grpc_port = ( - qdrant_grpc_port if qdrant_grpc_port is not None else _app_settings.qdrant_grpc_port - ) - self.backends = { - "qdrant": QdrantVectorStore( - qdrant_url or url or "http://localhost:6333", - connect_timeout_seconds=connect_timeout_seconds, - search_ef=search_ef, - prefer_grpc=prefer_grpc, - grpc_port=grpc_port, - ), - "turbopuffer": TurbopufferVectorStore( - api_key=turbopuffer_api_key, - region=turbopuffer_region, - namespace_prefix=turbopuffer_namespace_prefix, - ), - } - self._configured_providers = {"qdrant"} - if turbopuffer_api_key or provider == "turbopuffer": - self._configured_providers.add("turbopuffer") - self._fallback_provider = provider or "qdrant" - self.provider = provider or "collection" - self._sync_client() - - def supports_text_search_for(self, provider: VectorStoreProvider | None = None) -> bool: - return self.backends[ - validate_provider(provider or self._fallback_provider) - ].supports_text_search - - @property - def configured_providers(self) -> tuple[VectorStoreProvider, ...]: - return tuple(provider for provider in PROVIDERS if provider in self._configured_providers) def connect(self) -> None: - for provider in self.configured_providers: - self.backends[provider].connect() + self.backend.connect() self._sync_client() async def close(self) -> None: @@ -106,7 +57,7 @@ async def close(self) -> None: try: while self._active: await self._condition.wait() - await close_backends(self.backends) + await self.backend.close() self._sync_client() finally: self._swapping = False @@ -117,23 +68,23 @@ async def replace_with(self, other: VectorStore) -> None: self._swapping = True while self._active: await self._condition.wait() - old_backends = dict(self.backends) - self.provider = other.provider - self.backends = dict(other.backends) - self._configured_providers = set(other._configured_providers) - self._fallback_provider = other._fallback_provider + old_backend = self.backend + self.backend = other.backend self._sync_client() - await close_backends(old_backends, log_errors=True) + try: + await old_backend.close() + except Exception: + pass self._swapping = False self._condition.notify_all() @asynccontextmanager - async def _backend(self, provider: VectorStoreProvider) -> AsyncIterator[VectorStoreBackend]: + async def _backend(self) -> AsyncIterator[VectorStoreBackend]: async with self._condition: while self._swapping: await self._condition.wait() self._active += 1 - backend = self.backends[provider] + backend = self.backend try: yield backend finally: @@ -143,102 +94,33 @@ async def _backend(self, provider: VectorStoreProvider) -> AsyncIterator[VectorS self._condition.notify_all() async def health_check(self) -> None: - errors: list[str] = [] - for provider in self.configured_providers: - try: - async with self._backend(provider) as backend: - await backend.health_check() - except Exception as exc: - logger.warning( - "vector store provider unhealthy", - provider=provider, - error_type=type(exc).__name__, - ) - errors.append(f"{provider}: {type(exc).__name__}") - if errors: - raise RuntimeError("; ".join(errors)) - - async def provider_health(self) -> dict[str, dict[str, object]]: - results: dict[str, dict[str, object]] = {} - for provider in PROVIDERS: - configured = provider in self._configured_providers - if not configured: - results[provider] = {"configured": False, "status": "not_configured", "error": None} - continue - try: - async with self._backend(provider) as backend: - await backend.health_check() - results[provider] = {"configured": True, "status": "ok", "error": None} - except Exception as exc: - logger.warning( - "vector store provider_health error", - provider=provider, - error_type=type(exc).__name__, - ) - results[provider] = { - "configured": True, - "status": "error", - "error": sanitize_message_text(type(exc).__name__), - } - return results + async with self._backend() as backend: + await backend.health_check() - def _client(self, provider: VectorStoreProvider | None = None) -> Any: - backend = self.backends[validate_provider(provider or self._fallback_provider)] - if isinstance(backend, QdrantVectorStore): - return backend._client() - client = getattr(backend, "client", None) + def _client(self) -> Any: + client = getattr(self.backend, "client", None) if client is None: - backend.connect() - client = getattr(backend, "client", None) + self.backend.connect() + client = getattr(self.backend, "client", None) return client - async def _provider_for( - self, - collection: str, - provider: VectorStoreProvider | None, - ) -> VectorStoreProvider: - if provider is not None: - return validate_provider(provider) - try: - from bigrag.services.collection_cache import get_or_404 - - value = (await get_or_404(collection)).get("vector_store_provider") - if value: - return validate_provider(str(value)) - except Exception: - return self._fallback_provider - return self._fallback_provider - def _sync_client(self) -> None: - fallback = self.backends.get(self._fallback_provider) - self.client = getattr(fallback, "client", None) - if self.client is not None: - return - for provider in self.configured_providers: - client = getattr(self.backends[provider], "client", None) - if client is not None: - self.client = client - return + self.client = getattr(self.backend, "client", None) async def create_collection( self, name: str, dimension: int, - index_type: str = "HNSW", tenant_field: str | None = None, - provider: VectorStoreProvider | None = None, ) -> None: - selected_provider = await self._provider_for(name, provider) - async with self._backend(selected_provider) as backend: - await backend.create_collection(name, dimension, index_type, tenant_field) + async with self._backend() as backend: + await backend.create_collection(name, dimension, tenant_field) async def delete_collection( self, name: str, - provider: VectorStoreProvider | None = None, ) -> None: - selected_provider = await self._provider_for(name, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: await backend.delete_collection(name) async def insert( @@ -250,10 +132,8 @@ async def insert( texts: list[str], embeddings: list[list[float]], metadata: list[dict] | None = None, - provider: VectorStoreProvider | None = None, ) -> int: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.insert( collection, ids, @@ -270,11 +150,9 @@ async def search( query_embedding: list[float], top_k: int = 10, filters: FilterExpression | None = None, - provider: VectorStoreProvider | None = None, payload_fields: list[str] | None = None, ) -> list[dict]: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.search( collection, query_embedding, @@ -289,30 +167,24 @@ async def get_chunks( document_id: str, limit: int = 10000, offset: int = 0, - provider: VectorStoreProvider | None = None, ) -> tuple[list[dict], int]: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.get_chunks(collection, document_id, limit, offset) async def delete_by_document( self, collection: str, document_id: str, - provider: VectorStoreProvider | None = None, ) -> None: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: await backend.delete_by_document(collection, document_id) async def delete_by_ids( self, collection: str, ids: list[str], - provider: VectorStoreProvider | None = None, ) -> None: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: await backend.delete_by_ids(collection, ids) async def text_search( @@ -321,10 +193,8 @@ async def text_search( query_terms: list[str], top_k: int = 10, filters: FilterExpression | None = None, - provider: VectorStoreProvider | None = None, ) -> list[dict]: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.text_search(collection, query_terms, top_k, filters) async def upsert( @@ -334,10 +204,8 @@ async def upsert( embeddings: list[list[float]], texts: list[str], metadata: list[dict] | None = None, - provider: VectorStoreProvider | None = None, ) -> int: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.upsert(collection, ids, embeddings, texts, metadata) async def export_collection_points( @@ -345,10 +213,8 @@ async def export_collection_points( collection: str, *, with_vectors: bool = True, - provider: VectorStoreProvider | None = None, ) -> list[dict]: - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: return await backend.export_collection_points(collection, with_vectors=with_vectors) async def iter_collection_points( @@ -356,10 +222,9 @@ async def iter_collection_points( collection: str, *, with_vectors: bool = True, - provider: VectorStoreProvider | None = None, + **_: Any, ): - selected_provider = await self._provider_for(collection, provider) - async with self._backend(selected_provider) as backend: + async with self._backend() as backend: async for point in backend.iter_collection_points( collection, with_vectors=with_vectors, diff --git a/api/bigrag/services/vector_store/qdrant.py b/api/bigrag/services/vector_store/qdrant.py deleted file mode 100644 index 3c72ba31..00000000 --- a/api/bigrag/services/vector_store/qdrant.py +++ /dev/null @@ -1,499 +0,0 @@ -from __future__ import annotations - -import asyncio -from collections.abc import Awaitable, Callable -from typing import Any - -import httpx -from qdrant_client import AsyncQdrantClient, models - -from bigrag.logging import get_logger -from bigrag.services._retrieval_filters import FilterExpression -from bigrag.services.vector_store.base import ( - VectorStoreProvider, - _backend_name, - _build_payload, - _chunk_rows_from_payloads, - _point_id, - _row_from_payload, -) -from bigrag.services.vector_store.qdrant_filter import combine_filters, to_qdrant_filter - -logger = get_logger("bigrag.vector_store") - -_TRANSIENT_ERRORS = ( - ConnectionError, - TimeoutError, - OSError, - httpx.HTTPError, -) - - -class QdrantVectorStore: - provider: VectorStoreProvider = "qdrant" - supports_text_search = True - - def __init__( - self, - url: str = "http://localhost:6333", - *, - connect_timeout_seconds: int | float | None = 10, - search_ef: int | None = None, - prefix: str = "bigrag_", - prefer_grpc: bool = False, - grpc_port: int = 6334, - ) -> None: - self.url = url - self.client: AsyncQdrantClient | None = None - self._max_retries: int = 2 - self._connect_timeout_seconds = ( - None - if connect_timeout_seconds is None or connect_timeout_seconds <= 0 - else float(connect_timeout_seconds) - ) - self._search_ef = search_ef if search_ef and search_ef > 0 else None - self.prefix = prefix - self._prefer_grpc = bool(prefer_grpc) - self._grpc_port = int(grpc_port) if grpc_port else 6334 - - def connect(self) -> None: - self.client = AsyncQdrantClient( - url=self.url, - timeout=self._connect_timeout_seconds, - prefer_grpc=self._prefer_grpc, - grpc_port=self._grpc_port, - ) - logger.info( - "connected to qdrant", - url=self.url, - prefer_grpc=self._prefer_grpc, - grpc_port=self._grpc_port, - ) - - async def reconnect(self) -> None: - logger.warning("reconnecting to qdrant", url=self.url) - await self.close() - self.connect() - logger.info("reconnected to qdrant", url=self.url) - - async def _run_with_retry( - self, - fn: Callable[..., Awaitable[Any]], - *args: Any, - **kwargs: Any, - ) -> Any: - last_error = None - for attempt in range(self._max_retries + 1): - try: - return await fn(*args, **kwargs) - except _TRANSIENT_ERRORS as e: - last_error = e - if attempt < self._max_retries: - logger.warning( - "qdrant transient error", - attempt=attempt + 1, - max_attempts=self._max_retries + 1, - error=repr(e), - ) - await self.reconnect() - else: - raise - except Exception as e: - err_str = str(e).lower() - if any(kw in err_str for kw in ("connect", "timeout", "unavailable", "reset")): - last_error = e - if attempt < self._max_retries: - logger.warning( - "qdrant likely transient error", - attempt=attempt + 1, - max_attempts=self._max_retries + 1, - error=repr(e), - ) - await self.reconnect() - else: - raise - else: - raise - raise last_error - - async def close(self) -> None: - if self.client: - await self.client.close() - self.client = None - logger.info("qdrant connection closed") - - async def health_check(self) -> None: - client = self._client() - await self._run_with_retry(client.get_collections) - - def _client(self) -> AsyncQdrantClient: - if self.client is None: - self.connect() - if self.client is None: - raise RuntimeError("Qdrant client is not connected") - return self.client - - def _col(self, name: str) -> str: - return _backend_name(self.prefix, name) - - async def _create_payload_index( - self, - collection_name: str, - field_name: str, - schema: Any, - ) -> None: - client = self._client() - try: - await self._run_with_retry( - client.create_payload_index, - collection_name=collection_name, - field_name=field_name, - field_schema=schema, - wait=True, - ) - except Exception as exc: - if "already exists" in str(exc).lower() or "exists" in str(exc).lower(): - return - logger.warning( - "vector_store: payload index creation failed", - collection=collection_name, - field=field_name, - error=str(exc), - ) - - async def _ensure_payload_indexes( - self, - collection_name: str, - tenant_field: str | None = None, - ) -> None: - text_schema = models.TextIndexParams( - type=models.TextIndexType.TEXT, - tokenizer=models.TokenizerType.WORD, - min_token_len=2, - lowercase=True, - ) - indexes: list[tuple[str, Any]] = [ - ("id", "keyword"), - ("document_id", "keyword"), - ("chunk_index", "integer"), - ("char_start", "integer"), - ("char_end", "integer"), - ("page_no", "integer"), - ("text", text_schema), - ] - if tenant_field: - indexes.append((tenant_field, "keyword")) - - await asyncio.gather( - *[ - self._create_payload_index(collection_name, field_name, schema) - for field_name, schema in indexes - ] - ) - - async def create_collection( - self, - name: str, - dimension: int, - index_type: str = "HNSW", - tenant_field: str | None = None, - ) -> None: - col = self._col(name) - client = self._client() - - if not await self._run_with_retry(client.collection_exists, col): - await self._run_with_retry( - client.create_collection, - collection_name=col, - vectors_config=models.VectorParams( - size=dimension, - distance=models.Distance.COSINE, - ), - ) - logger.info( - "created qdrant collection", - collection=col, - dimension=dimension, - index=index_type, - ) - - await self._ensure_payload_indexes(col, tenant_field=tenant_field) - - async def delete_collection(self, name: str) -> None: - col = self._col(name) - client = self._client() - if await self._run_with_retry(client.collection_exists, col): - await self._run_with_retry(client.delete_collection, col) - logger.info("dropped qdrant collection", collection=col) - - async def insert( - self, - collection: str, - ids: list[str], - document_ids: list[str], - chunk_indices: list[int], - texts: list[str], - embeddings: list[list[float]], - metadata: list[dict] | None = None, - ) -> int: - col = self._col(collection) - points = [] - for i in range(len(ids)): - points.append( - models.PointStruct( - id=_point_id(col, ids[i]), - vector=embeddings[i], - payload=_build_payload( - id_=ids[i], - document_id=document_ids[i], - chunk_index=chunk_indices[i], - text=texts[i], - metadata=metadata[i] if metadata else None, - ), - ) - ) - - client = self._client() - await self._run_with_retry(client.upsert, collection_name=col, points=points, wait=True) - logger.info("inserted vectors", collection=col, count=len(points)) - return len(points) - - def _search_params(self) -> models.SearchParams | None: - if self._search_ef is None: - return None - return models.SearchParams(hnsw_ef=self._search_ef) - - @staticmethod - def _row_from_qdrant(point: Any) -> dict: - payload = dict(getattr(point, "payload", None) or {}) - point_id = str(getattr(point, "id", "")) - return _row_from_payload(point_id, getattr(point, "score", 0.0), payload) - - async def search( - self, - collection: str, - query_embedding: list[float], - top_k: int = 10, - filters: FilterExpression | None = None, - payload_fields: list[str] | None = None, - ) -> list[dict]: - col = self._col(collection) - - with_payload: Any = ( - models.PayloadSelectorInclude(include=list(payload_fields)) if payload_fields else True - ) - - client = self._client() - results = await self._run_with_retry( - client.query_points, - collection_name=col, - query=query_embedding, - limit=top_k, - query_filter=to_qdrant_filter(filters), - search_params=self._search_params(), - with_payload=with_payload, - with_vectors=False, - ) - - hits = [self._row_from_qdrant(point) for point in results.points] - logger.info("vector search", collection=col, top_k=top_k, hits=len(hits), filters=filters) - return hits - - async def get_chunks( - self, - collection: str, - document_id: str, - limit: int = 10000, - offset: int = 0, - ) -> tuple[list[dict], int]: - col = self._col(collection) - client = self._client() - if not await self._run_with_retry(client.collection_exists, col): - return [], 0 - - doc_filter = models.Filter( - must=[ - models.FieldCondition( - key="document_id", - match=models.MatchValue(value=document_id), - ) - ] - ) - - try: - count_resp = await self._run_with_retry( - client.count, - collection_name=col, - count_filter=doc_filter, - exact=True, - ) - total = int(getattr(count_resp, "count", 0)) - except Exception: - total = 0 - - needed = offset + max(limit, 0) - if needed <= 0: - return [], total - - results = [] - next_offset = None - page_size = min(max(needed, 256), 10000) - while True: - batch, next_offset = await self._run_with_retry( - client.scroll, - collection_name=col, - scroll_filter=doc_filter, - with_payload=True, - with_vectors=False, - limit=page_size, - offset=next_offset, - ) - results.extend(batch) - if next_offset is None: - break - if total and len(results) >= total: - break - payloads = [r.payload or {} for r in results] - rows, computed_total = _chunk_rows_from_payloads(payloads, limit, offset) - return rows, total or computed_total - - async def delete_by_document(self, collection: str, document_id: str) -> None: - col = self._col(collection) - client = self._client() - if not await self._run_with_retry(client.collection_exists, col): - return - await self._run_with_retry( - client.delete, - collection_name=col, - points_selector=models.Filter( - must=[ - models.FieldCondition( - key="document_id", - match=models.MatchValue(value=document_id), - ) - ] - ), - wait=True, - ) - logger.info("delete vectors by document", collection=col, document_id=document_id) - - async def delete_by_ids(self, collection: str, ids: list[str]) -> None: - col = self._col(collection) - client = self._client() - point_ids = [_point_id(col, id_) for id_ in ids] - await self._run_with_retry( - client.delete, - collection_name=col, - points_selector=point_ids, - wait=True, - ) - logger.info("delete vectors by ids", collection=col, count=len(ids)) - - async def text_search( - self, - collection: str, - query_terms: list[str], - top_k: int = 10, - filters: FilterExpression | None = None, - ) -> list[dict]: - col = self._col(collection) - terms = [term for term in query_terms if term] - if not terms: - return [] - - text_filter = models.Filter( - should=[ - models.FieldCondition(key="text", match=models.MatchText(text=term)) - for term in terms - ] - ) - combined_filter = combine_filters(to_qdrant_filter(filters), text_filter) - - try: - client = self._client() - results, _next_offset = await self._run_with_retry( - client.scroll, - collection_name=col, - scroll_filter=combined_filter, - with_payload=True, - with_vectors=False, - limit=top_k * 10, - ) - except _TRANSIENT_ERRORS: - raise - except Exception as exc: - logger.warning("text search query failed", collection=col, error=repr(exc)) - return [] - - logger.info("text search", collection=col, terms=len(terms), hits=len(results)) - return [self._row_from_qdrant(point) for point in results] - - async def upsert( - self, - collection: str, - ids: list[str], - embeddings: list[list[float]], - texts: list[str], - metadata: list[dict] | None = None, - ) -> int: - col = self._col(collection) - points = [] - for i in range(len(ids)): - points.append( - models.PointStruct( - id=_point_id(col, ids[i]), - vector=embeddings[i], - payload=_build_payload( - id_=ids[i], - document_id="", - chunk_index=0, - text=texts[i], - metadata=metadata[i] if metadata else None, - ), - ) - ) - - client = self._client() - await self._run_with_retry(client.upsert, collection_name=col, points=points, wait=True) - logger.info("upserted vectors", collection=col, count=len(points)) - return len(points) - - async def export_collection_points( - self, - collection: str, - *, - with_vectors: bool = True, - ) -> list[dict]: - return [ - point - async for point in self.iter_collection_points(collection, with_vectors=with_vectors) - ] - - async def iter_collection_points( - self, - collection: str, - *, - with_vectors: bool = True, - ): - col = self._col(collection) - client = self._client() - if not await self._run_with_retry(client.collection_exists, col): - return - offset = None - while True: - points, offset = await self._run_with_retry( - client.scroll, - collection_name=col, - limit=256, - offset=offset, - with_payload=True, - with_vectors=with_vectors, - ) - for point in points: - yield { - "id": str(getattr(point, "id", "")), - "payload": getattr(point, "payload", {}) or {}, - "vector": getattr(point, "vector", None) if with_vectors else None, - } - if offset is None: - break diff --git a/api/bigrag/services/vector_store/qdrant_filter.py b/api/bigrag/services/vector_store/qdrant_filter.py deleted file mode 100644 index ae1c44c0..00000000 --- a/api/bigrag/services/vector_store/qdrant_filter.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -from qdrant_client import models - -from bigrag.services._retrieval_filters import FilterExpression - - -def to_qdrant_filter(filters: FilterExpression | None) -> models.Filter | None: - if filters is None: - return None - must: list[models.Condition] = [] - must_not: list[models.Condition] = [] - for condition in filters.conditions: - if condition.operator == "eq": - must.append( - models.FieldCondition( - key=condition.field, - match=models.MatchValue(value=condition.value), - ) - ) - elif condition.operator == "ne": - must_not.append( - models.FieldCondition( - key=condition.field, - match=models.MatchValue(value=condition.value), - ) - ) - elif condition.operator == "in": - must.append( - models.FieldCondition( - key=condition.field, - match=models.MatchAny(any=condition.value), - ) - ) - else: - must.append( - models.FieldCondition( - key=condition.field, - range=models.Range( - gt=condition.value if condition.operator == "gt" else None, - gte=condition.value if condition.operator == "gte" else None, - lt=condition.value if condition.operator == "lt" else None, - lte=condition.value if condition.operator == "lte" else None, - ), - ) - ) - if not must and not must_not: - return None - return models.Filter(must=must or None, must_not=must_not or None) - - -def combine_filters( - *filters: models.Filter | None, -) -> models.Filter | None: - active = [f for f in filters if f is not None] - if not active: - return None - if len(active) == 1: - return active[0] - return models.Filter(must=active) diff --git a/api/bigrag/services/vector_store/turbopuffer.py b/api/bigrag/services/vector_store/turbopuffer.py index 45d15492..0cfefd63 100644 --- a/api/bigrag/services/vector_store/turbopuffer.py +++ b/api/bigrag/services/vector_store/turbopuffer.py @@ -2,13 +2,11 @@ from typing import Any -import httpx +from turbopuffer import AsyncTurbopuffer, NotFoundError from bigrag.logging import get_logger from bigrag.services._retrieval_filters import FilterCondition, FilterExpression from bigrag.services.vector_store.base import ( - VectorStoreFeatureError, - VectorStoreProvider, _backend_name, _build_payload, _chunk_rows_from_payloads, @@ -22,7 +20,7 @@ _EXPORT_PAGE_SIZE = 10000 -def _to_turbopuffer_filter(filters: FilterExpression | None) -> list | None: +def _to_turbopuffer_filter(filters: FilterExpression | None) -> tuple | None: if filters is None: return None clauses = [_to_turbopuffer_condition(condition) for condition in filters.conditions] @@ -30,10 +28,10 @@ def _to_turbopuffer_filter(filters: FilterExpression | None) -> list | None: return None if len(clauses) == 1: return clauses[0] - return ["And", clauses] + return ("And", tuple(clauses)) -def _to_turbopuffer_condition(condition: FilterCondition) -> list: +def _to_turbopuffer_condition(condition: FilterCondition) -> tuple: field = _PUBLIC_ID_FIELD if condition.field == "id" else condition.field op = { "eq": "Eq", @@ -44,7 +42,7 @@ def _to_turbopuffer_condition(condition: FilterCondition) -> list: "lte": "Lte", "in": "In", }[condition.operator] - return [field, op, condition.value] + return (field, op, condition.value) def _schema(dimension: int) -> dict: @@ -53,7 +51,7 @@ def _schema(dimension: int) -> dict: _PUBLIC_ID_FIELD: {"type": "string"}, "document_id": {"type": "string"}, "chunk_index": {"type": "int"}, - "text": {"type": "string", "filterable": False}, + "text": {"type": "string", "full_text_search": True}, } @@ -67,40 +65,58 @@ def _row_payload(row: dict) -> dict: return payload -class TurbopufferVectorStore: - provider: VectorStoreProvider = "turbopuffer" - supports_text_search = False +def _response_row(row: Any) -> dict: + if isinstance(row, dict): + return row + if hasattr(row, "to_dict"): + return row.to_dict() + if hasattr(row, "model_dump"): + return row.model_dump(mode="json", by_alias=True) + return dict(row) + +class TurbopufferVectorStore: def __init__( self, *, - api_key: str | None, - region: str, + api_key: str | None = None, + region: str = "aws-us-east-1", namespace_prefix: str = "bigrag_", + base_url: str | None = None, ) -> None: self.api_key = api_key self.region = region self.prefix = namespace_prefix or "bigrag_" - self.client: httpx.AsyncClient | None = None + self.base_url = base_url.rstrip("/") if base_url else None + self.client: AsyncTurbopuffer | None = None def connect(self) -> None: if not self.api_key: raise RuntimeError("turbopuffer API key is not configured") - base_url = f"https://{self.region}.turbopuffer.com" - self.client = httpx.AsyncClient( - base_url=base_url, - headers={"Authorization": f"Bearer {self.api_key}"}, + kwargs: dict[str, Any] = { + "api_key": self.api_key, + } + if self.base_url: + kwargs["base_url"] = self.base_url + else: + kwargs["region"] = self.region + self.client = AsyncTurbopuffer( + **kwargs, + max_retries=2, timeout=30, ) - logger.info("connected to turbopuffer", region=self.region) + logger.info("connected to turbopuffer", region=self.region, base_url=self.base_url) - def _client(self) -> httpx.AsyncClient: + def _client(self) -> AsyncTurbopuffer: if self.client is None: self.connect() if self.client is None: raise RuntimeError("turbopuffer client is not connected") return self.client + def _namespace_client(self, name: str): + return self._client().namespace(self._namespace(name)) + def _namespace(self, name: str) -> str: return _backend_name(self.prefix, name) @@ -109,29 +125,26 @@ def _point_id(self, collection: str, value: str) -> str: async def close(self) -> None: if self.client is not None: - await self.client.aclose() + await self.client.close() self.client = None async def health_check(self) -> None: - client = self._client() - response = await client.get("/v1/namespaces") - response.raise_for_status() + async for _ in self._client().namespaces(prefix=self.prefix, page_size=1): + break async def create_collection( self, name: str, dimension: int, - index_type: str = "HNSW", tenant_field: str | None = None, ) -> None: await self.health_check() async def delete_collection(self, name: str) -> None: - client = self._client() - response = await client.delete(f"/v2/namespaces/{self._namespace(name)}") - if response.status_code == 404: + try: + await self._namespace_client(name).delete_all() + except NotFoundError: return - response.raise_for_status() async def insert( self, @@ -172,10 +185,10 @@ async def insert( return len(rows) async def _write(self, collection: str, payload: dict) -> dict: - client = self._client() - response = await client.post(f"/v2/namespaces/{self._namespace(collection)}", json=payload) - response.raise_for_status() - return response.json() if response.content else {} + response = await self._namespace_client(collection).write(**payload) + if hasattr(response, "to_dict"): + return response.to_dict() + return {} async def search( self, @@ -197,14 +210,8 @@ async def search( turbo_filter = _to_turbopuffer_filter(filters) if turbo_filter: payload["filters"] = turbo_filter - client = self._client() - response = await client.post( - f"/v2/namespaces/{self._namespace(collection)}/query", - json=payload, - ) - response.raise_for_status() rows = [] - for row in response.json().get("rows", []): + for row in await self._query_rows(collection, payload): point_id = str(row.get("id", "")) distance = float(row.get("$dist", 0.0)) rows.append(_row_from_payload(point_id, max(0.0, 1.0 - distance), _row_payload(row))) @@ -240,13 +247,8 @@ async def get_chunks( return _chunk_rows_from_payloads([_row_payload(row) for row in rows], limit, offset) async def _query_rows(self, collection: str, payload: dict) -> list[dict]: - client = self._client() - response = await client.post( - f"/v2/namespaces/{self._namespace(collection)}/query", - json=payload, - ) - response.raise_for_status() - return response.json().get("rows", []) + response = await self._namespace_client(collection).query(**payload) + return [_response_row(row) for row in response.rows] async def delete_by_document(self, collection: str, document_id: str) -> None: await self._write(collection, {"delete_by_filter": ["document_id", "Eq", document_id]}) @@ -261,7 +263,26 @@ async def text_search( top_k: int = 10, filters: FilterExpression | None = None, ) -> list[dict]: - raise VectorStoreFeatureError("turbopuffer does not support keyword or hybrid search in v1") + query = " ".join(term for term in query_terms if term).strip() + if not query: + return [] + payload: dict[str, Any] = { + "rank_by": ["text", "BM25", query], + "top_k": top_k, + "exclude_attributes": ["vector"], + } + turbo_filter = _to_turbopuffer_filter(filters) + if turbo_filter: + payload["filters"] = turbo_filter + rows = await self._query_rows(collection, payload) + results = [] + for row in rows: + point_id = str(row.get("id", "")) + score = row.get("$score") + if score is None: + score = max(0.0, 1.0 - float(row.get("$dist", 0.0))) + results.append(_row_from_payload(point_id, float(score), _row_payload(row))) + return results async def upsert( self, diff --git a/api/pyproject.toml b/api/pyproject.toml index 98a0454c..f07daee3 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bigrag" version = "2026.4.30" -description = "Self-hostable RAG platform with Docling ingestion and pluggable vector storage" +description = "Self-hostable RAG platform with Docling ingestion and Turbopuffer vector storage" license = "MIT" requires-python = ">=3.12" dependencies = [ @@ -11,7 +11,6 @@ dependencies = [ "asyncpg>=0.30.0,<1", "sqlalchemy[asyncio]>=2.0.36,<3", "alembic>=1.14.0,<2", - "qdrant-client>=1.17.0,<2", "docling>=2.25.0,<3", "pypdfium2>=5.7.0,<6", "huggingface-hub>=0.36.0,<2", @@ -30,6 +29,7 @@ dependencies = [ "mcp>=1.12.0,<2", "tiktoken>=0.8.0,<1", "dramatiq[redis]>=2.1.0,<3", + "turbopuffer>=2.1.0,<3", ] [dependency-groups] diff --git a/api/uv.lock b/api/uv.lock index ed566c8f..f0d0d161 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -31,6 +31,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" }, ] +[[package]] +name = "aiohappyeyeballs" +version = "2.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/c6/61a2d7b7572279226bb2e7f61d7a19ca7c90da0329c93fa0d560cbf288d8/aiohappyeyeballs-2.6.2.tar.gz", hash = "sha256:e202810ee718bd01fc6ef49e8ea53d023d5cb6b581076d7925aa499fa55dbe64", size = 22591, upload-time = "2026-05-20T15:12:24.631Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/fc/a7bf5b6e4e617b45f90f2d9d2a68519c249c81dd4fc2658c7a2a61c4f4b7/aiohappyeyeballs-2.6.2-py3-none-any.whl", hash = "sha256:4708045e2d7a6c6bdf8aafa8ed39649eaf926a4543b54560659129e3365953c4", size = 15062, upload-time = "2026-05-20T15:12:23.328Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + [[package]] name = "alembic" version = "1.18.4" @@ -210,12 +317,12 @@ dependencies = [ { name = "pydantic-settings" }, { name = "pypdfium2" }, { name = "python-multipart" }, - { name = "qdrant-client" }, { name = "redis", extra = ["hiredis"] }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "starlette" }, { name = "structlog" }, { name = "tiktoken" }, + { name = "turbopuffer" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -244,12 +351,12 @@ requires-dist = [ { name = "pydantic-settings", specifier = ">=2.7.0,<3" }, { name = "pypdfium2", specifier = ">=5.7.0,<6" }, { name = "python-multipart", specifier = ">=0.0.18,<1" }, - { name = "qdrant-client", specifier = ">=1.17.0,<2" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0,<8" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.36,<3" }, { name = "starlette", specifier = ">=1.0.0,<2" }, { name = "structlog", specifier = ">=24.4.0,<26" }, { name = "tiktoken", specifier = ">=0.8.0,<1" }, + { name = "turbopuffer", specifier = ">=2.1.0,<3" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0,<1" }, ] @@ -875,6 +982,95 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/79/1b8fa1bb3568781e84c9200f951c735f3f157429f44be0495da55894d620/filetype-1.2.0-py2.py3-none-any.whl", hash = "sha256:7ce71b6880181241cf7ac8697a2f1eb6a8bd9b429f7ad6d27b8db9ba5f1c2d25", size = 19970, upload-time = "2022-11-02T17:34:01.425Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + [[package]] name = "fsspec" version = "2026.4.0" @@ -931,47 +1127,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, ] -[[package]] -name = "grpcio" -version = "1.80.0" -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, 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, 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" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/7c3c25789e3f069e581dc342e03613c5b1cb012c4e8c7d9d5cf960a75856/grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad", size = 6017243, upload-time = "2026-03-30T08:47:40.075Z" }, - { url = "https://files.pythonhosted.org/packages/04/19/21a9806eb8240e174fd1ab0cd5b9aa948bb0e05c2f2f55f9d5d7405e6d08/grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0", size = 12010840, upload-time = "2026-03-30T08:47:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/18/3a/23347d35f76f639e807fb7a36fad3068aed100996849a33809591f26eca6/grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f", size = 6567644, upload-time = "2026-03-30T08:47:46.806Z" }, - { url = "https://files.pythonhosted.org/packages/ff/40/96e07ecb604a6a67ae6ab151e3e35b132875d98bc68ec65f3e5ab3e781d7/grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6", size = 7277830, upload-time = "2026-03-30T08:47:49.643Z" }, - { url = "https://files.pythonhosted.org/packages/9b/e2/da1506ecea1f34a5e365964644b35edef53803052b763ca214ba3870c856/grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140", size = 6783216, upload-time = "2026-03-30T08:47:52.817Z" }, - { url = "https://files.pythonhosted.org/packages/44/83/3b20ff58d0c3b7f6caaa3af9a4174d4023701df40a3f39f7f1c8e7c48f9d/grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d", size = 7385866, upload-time = "2026-03-30T08:47:55.687Z" }, - { url = "https://files.pythonhosted.org/packages/47/45/55c507599c5520416de5eefecc927d6a0d7af55e91cfffb2e410607e5744/grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7", size = 8391602, upload-time = "2026-03-30T08:47:58.303Z" }, - { url = "https://files.pythonhosted.org/packages/10/bb/dd06f4c24c01db9cf11341b547d0a016b2c90ed7dbbb086a5710df7dd1d7/grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7", size = 7826752, upload-time = "2026-03-30T08:48:01.311Z" }, - { url = "https://files.pythonhosted.org/packages/f9/1e/9d67992ba23371fd63d4527096eb8c6b76d74d52b500df992a3343fd7251/grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294", size = 4142310, upload-time = "2026-03-30T08:48:04.594Z" }, - { url = "https://files.pythonhosted.org/packages/cf/e6/283326a27da9e2c3038bc93eeea36fb118ce0b2d03922a9cda6688f53c5b/grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50", size = 4882833, upload-time = "2026-03-30T08:48:07.363Z" }, - { url = "https://files.pythonhosted.org/packages/c5/6d/e65307ce20f5a09244ba9e9d8476e99fb039de7154f37fb85f26978b59c3/grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e", size = 6017376, upload-time = "2026-03-30T08:48:10.005Z" }, - { url = "https://files.pythonhosted.org/packages/69/10/9cef5d9650c72625a699c549940f0abb3c4bfdb5ed45a5ce431f92f31806/grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f", size = 12018133, upload-time = "2026-03-30T08:48:12.927Z" }, - { url = "https://files.pythonhosted.org/packages/04/82/983aabaad82ba26113caceeb9091706a0696b25da004fe3defb5b346e15b/grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9", size = 6574748, upload-time = "2026-03-30T08:48:16.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/d7/031666ef155aa0bf399ed7e19439656c38bbd143779ae0861b038ce82abd/grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14", size = 7277711, upload-time = "2026-03-30T08:48:19.627Z" }, - { url = "https://files.pythonhosted.org/packages/e8/43/f437a78f7f4f1d311804189e8f11fb311a01049b2e08557c1068d470cb2e/grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05", size = 6785372, upload-time = "2026-03-30T08:48:22.373Z" }, - { url = "https://files.pythonhosted.org/packages/93/3d/f6558e9c6296cb4227faa5c43c54a34c68d32654b829f53288313d16a86e/grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1", size = 7395268, upload-time = "2026-03-30T08:48:25.638Z" }, - { url = "https://files.pythonhosted.org/packages/06/21/0fdd77e84720b08843c371a2efa6f2e19dbebf56adc72df73d891f5506f0/grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f", size = 8392000, upload-time = "2026-03-30T08:48:28.974Z" }, - { url = "https://files.pythonhosted.org/packages/f5/68/67f4947ed55d2e69f2cc199ab9fd85e0a0034d813bbeef84df6d2ba4d4b7/grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e", size = 7828477, upload-time = "2026-03-30T08:48:32.054Z" }, - { url = "https://files.pythonhosted.org/packages/44/b6/8d4096691b2e385e8271911a0de4f35f0a6c7d05aff7098e296c3de86939/grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae", size = 4218563, upload-time = "2026-03-30T08:48:34.538Z" }, - { url = "https://files.pythonhosted.org/packages/e5/8c/bbe6baf2557262834f2070cf668515fa308b2d38a4bbf771f8f7872a7036/grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f", size = 5019457, upload-time = "2026-03-30T08:48:37.308Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -981,19 +1136,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "h2" -version = "4.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, -] - [[package]] name = "hf-xet" version = "1.5.0" @@ -1086,15 +1228,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/d6/191e6741addc97bcf5e755661f8c82f0fd0aa35f07ece56e858da689b57e/hiredis-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:ab1f646ff531d70bfd25f01e60708dfa3d105eb458b7dedd9fe9a443039fd809", size = 23811, upload-time = "2026-03-16T15:20:34.292Z" }, ] -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, -] - [[package]] name = "httpcore" version = "1.0.9" @@ -1152,11 +1285,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] -[package.optional-dependencies] -http2 = [ - { name = "h2" }, -] - [[package]] name = "httpx-sse" version = "0.4.3" @@ -1186,15 +1314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/11/0b64cc9024329b76d7547c19a67604a61d21d3ba678a69d1b220c29d5112/huggingface_hub-1.15.0-py3-none-any.whl", hash = "sha256:a4a59af04cbc41a3fe3fec429b171ef994ef8c971eda10136746f408dd4e3744", size = 663602, upload-time = "2026-05-15T11:42:50.487Z" }, ] -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, -] - [[package]] name = "idna" version = "3.15" @@ -1377,6 +1496,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/65/d1/bc0ed2427bf609f2ee10da303a6a226f9c8bce94f945dc29a32ce55de6e4/lxml-6.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aa366a1e55b8ebfe8ca8ddc3cfe75c8ebade181aeb0f661d0cb05986b647f72a", size = 5260995, upload-time = "2026-05-18T19:18:37.091Z" }, { url = "https://files.pythonhosted.org/packages/69/8b/6772e1a4b513fc50a8d931f19edde0e13ae6918510a1e13ff67864f3e5ed/lxml-6.1.1-cp312-cp312-win32.whl", hash = "sha256:126c93f7f56f0eda92f6d8c619edc463a4f23d9252f1c9d0405a76f25fa9f11a", size = 3596382, upload-time = "2026-05-18T19:17:18.37Z" }, { url = "https://files.pythonhosted.org/packages/1b/89/45198e9624762af2dfd2cb8782598477ceb29f6e59caab560388ae1f4ec1/lxml-6.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:26e6eda8d38c1fcab1090dd196ee87cbd13788e531937610e2589085de074e77", size = 3997255, upload-time = "2026-05-18T19:17:56.781Z" }, + { url = "https://files.pythonhosted.org/packages/90/a9/7a54b6834088d9ae528a7b780584ba6a39a9457b0ac330479f20ffbc9449/lxml-6.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:6540377fbd53fe1b629172288c464fb18db11ce1fa7dc15891da10aa9dcc3e7f", size = 3659610, upload-time = "2026-05-19T19:22:50.843Z" }, { url = "https://files.pythonhosted.org/packages/a5/eb/7e6f37c5584ccbb2ff267f56fd0339016938c1c8684cfefab9b33ffc2f36/lxml-6.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:68a9198d0fc122d14bb76837de9aa80cf84caed990b5b237f532ed87d3706736", size = 8559780, upload-time = "2026-05-18T19:17:57.661Z" }, { url = "https://files.pythonhosted.org/packages/a1/36/587c2521cf23a2cd6c9c22108aa7528f683a1f195ed7ccd23a4b1786ad36/lxml-6.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7d47866cb32fb503450b6edc9df355d10dc49836af2e89901bd6ac6b0896d9d9", size = 4618006, upload-time = "2026-05-18T19:18:04.452Z" }, { url = "https://files.pythonhosted.org/packages/6e/ca/ab7bfe2bf4c972af5e7878262845ead3a24a929a9b04bc11c7c1ece6c82a/lxml-6.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb7c9811bfaa8b1ed5ed319f5d370dfbcaa59d52ea64be2a5a85e18195930354", size = 4924139, upload-time = "2026-05-18T19:19:04.873Z" }, @@ -1394,6 +1514,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/f8/f6a5e8185bcb28c2befae3d31f8e3df3b811cb0f47746517a81279fcafe1/lxml-6.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47402e62c52ff5988c1e8c6c63177f5708bccf48e366dea4e3dcf1e645e04947", size = 5250276, upload-time = "2026-05-18T19:19:03.834Z" }, { url = "https://files.pythonhosted.org/packages/c7/f2/1a2b9f1b7a49d45495369be7ef9ad05b262930f2eab3e3145706fca8083f/lxml-6.1.1-cp313-cp313-win32.whl", hash = "sha256:3483644525531e1d5762b0c44a8e18b6efba321b6dcf8a8952de10b037618bca", size = 3596903, upload-time = "2026-05-18T19:17:29.863Z" }, { url = "https://files.pythonhosted.org/packages/e6/99/f4ffb024f238eec2131aaa09f3278fb6129cf892741bf68e1fc1afb8c100/lxml-6.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:a10bd2fd62e8ce916ececb342f348f190724a098c1faa056fdfb2a22ad5e8660", size = 3995869, upload-time = "2026-05-18T19:18:02.596Z" }, + { url = "https://files.pythonhosted.org/packages/d1/53/70eb8c5c6037f27448f1e3c54ebede9545a801ae63f0a7254afca4fe8e45/lxml-6.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:424aa57aca0897eb922aef34395bd1289b3b6f04e6bae20ea123c0c7e333cffc", size = 3658490, upload-time = "2026-05-19T19:22:53.846Z" }, { url = "https://files.pythonhosted.org/packages/13/e2/2e325795566de01d0d7c3bb57d3c370616b2d07b01214e84eec5d3b10963/lxml-6.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:19b7ab10b210b0b3ad7985d9ac4eb66ab09a90b20fe6e2f7ba55d01a234345d0", size = 8577146, upload-time = "2026-05-18T19:18:17.765Z" }, { url = "https://files.pythonhosted.org/packages/93/cf/5630b5e4be7d2e6bee8efe83865c925221103cf0221303b104ce134b01e2/lxml-6.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c08e5c694306507275f2290073350c4f32e383db15213b2c69e7ff39c1193840", size = 4623866, upload-time = "2026-05-18T19:18:30.669Z" }, { url = "https://files.pythonhosted.org/packages/d2/51/3904907c063451cf8d4a5c9fe0cad95fa1f4ec57f4e3884fa0731bd7a305/lxml-6.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:74a9717fd0d82effef5c2854f0d917231d5324b5a3eb7275c43ac9fa32f97a14", size = 4950022, upload-time = "2026-05-18T19:19:31.958Z" }, @@ -1411,6 +1532,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2f/5d/b329acbbedc0b619ebc2be6cf7ee9ed07e80892c88d4dfd612c33805789a/lxml-6.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63876be28efefa04a1df615b46770e82042cce445cfdce55160522f57b231ccb", size = 5264191, upload-time = "2026-05-18T19:19:21.118Z" }, { url = "https://files.pythonhosted.org/packages/d6/85/be36fb1425b30db3c3f9df75fe86343ebffb79e6320bd7f588e25bfeac39/lxml-6.1.1-cp314-cp314-win32.whl", hash = "sha256:7f7a92e8583f06b1fd49d01158143b8461cfcd135dcb10ec807270a3051bd603", size = 3657202, upload-time = "2026-05-18T19:17:39.509Z" }, { url = "https://files.pythonhosted.org/packages/b8/ce/3cf9a827342269f54d405a6202397de63f07c69cbd6ce7d183a3f0cba1e9/lxml-6.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:b2d444f2e66624d68e9c6b211e28a76e22fff5fcabcfff4deac18b529b7d4137", size = 4064497, upload-time = "2026-05-18T19:18:14.662Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3e/1a957bde8f0760039e627f94699f82caa782c9d838d86c3d28245ee67212/lxml-6.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:3fd9728a2735fda14f4e8235830c86b539e9661e849665bf926d3f867943b4bf", size = 3741991, upload-time = "2026-05-19T19:22:59.111Z" }, { url = "https://files.pythonhosted.org/packages/78/b2/00ed55b3a2efa4658fb795c38d1090ec9b3e8a6c3683d4441fa517f09c3b/lxml-6.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:787b2496d0dbe8cd180984e8d29e3a6f76e7ea34db781cb3bd55e4ba1ef8b4ee", size = 8827545, upload-time = "2026-05-18T19:18:41.193Z" }, { url = "https://files.pythonhosted.org/packages/c0/73/74573db19baa618d5f266f2407898b087ff6927115b00b71e5fc1b700847/lxml-6.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2c8daa471358dc2d6fcf02165e80ec68f77871a286df95bc5cc3816153b0fd2c", size = 4735736, upload-time = "2026-05-18T19:18:46.761Z" }, { url = "https://files.pythonhosted.org/packages/16/02/6f7061f4f95f51e545d48e87647c54791d204a4e881be4156e7a26ba5338/lxml-6.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:acd7d70b64c0aae0c7922cca83d288a16f5f6da523637697872253415269baef", size = 4970291, upload-time = "2026-05-18T19:19:56.215Z" }, @@ -1428,6 +1550,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/2d/2dafd8149e94b05bb070690efd5bb2680720681e03ff03fc57d2b70a1105/lxml-6.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9e36f163528fc50cbef305f02a5fd66d404edf7049cdaff211dbc2cba5a7013e", size = 5247845, upload-time = "2026-05-18T19:19:36.649Z" }, { url = "https://files.pythonhosted.org/packages/ce/68/b30e913340c380ddac9580c6e6230991fc37240ec4f64704833e4f3e2769/lxml-6.1.1-cp314-cp314t-win32.whl", hash = "sha256:649dda677cf3bd6ac9ae14007ba0c824ded8ce5808b53fc7431d9140399118c1", size = 3897345, upload-time = "2026-05-18T19:17:33.562Z" }, { url = "https://files.pythonhosted.org/packages/3c/4e/9eb2af5335545f9fbcd7af57bcf87c6025d31eaa31b14ec184a6c8675328/lxml-6.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:793033d6c5cdf33a573f910d9bea14ef8f5771820411d118da8e1182edb53d5e", size = 4393350, upload-time = "2026-05-18T19:18:10.076Z" }, + { url = "https://files.pythonhosted.org/packages/7f/2c/0f1e93c636720e8a3eb59af2bfda99d98b55891e1c53bc30c2e0e865f01b/lxml-6.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:58bb955caba94e467d2a96da17660d2d704e0675894cba21ab8a775b8621fd1c", size = 3817223, upload-time = "2026-05-19T19:22:56.823Z" }, ] [[package]] @@ -1588,6 +1711,105 @@ wheels = [ { 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 = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + [[package]] name = "multiprocess" version = "0.70.19" @@ -2095,30 +2317,97 @@ wheels = [ ] [[package]] -name = "portalocker" -version = "3.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pywin32", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, -] - -[[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, 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, 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" }, +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] @@ -2149,6 +2438,124 @@ wheels = [ { 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 = "pybase64" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/b8/4ed5c7ad5ec15b08d35cc79ace6145d5c1ae426e46435f4987379439dfea/pybase64-1.4.3.tar.gz", hash = "sha256:c2ed274c9e0ba9c8f9c4083cfe265e66dd679126cd9c2027965d807352f3f053", size = 137272, upload-time = "2025-12-06T13:27:04.013Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/a7/efcaa564f091a2af7f18a83c1c4875b1437db56ba39540451dc85d56f653/pybase64-1.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:18d85e5ab8b986bb32d8446aca6258ed80d1bafe3603c437690b352c648f5967", size = 38167, upload-time = "2025-12-06T13:23:16.821Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/c7ad35adff2d272bf2930132db2b3eea8c44bb1b1f64eb9b2b8e57cde7b4/pybase64-1.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f5791a3491d116d0deaf4d83268f48792998519698f8751efb191eac84320e9", size = 31673, upload-time = "2025-12-06T13:23:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/43/1b/9a8cab0042b464e9a876d5c65fe5127445a2436da36fda64899b119b1a1b/pybase64-1.4.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f0b3f200c3e06316f6bebabd458b4e4bcd4c2ca26af7c0c766614d91968dee27", size = 68210, upload-time = "2025-12-06T13:23:18.813Z" }, + { url = "https://files.pythonhosted.org/packages/62/f7/965b79ff391ad208b50e412b5d3205ccce372a2d27b7218ae86d5295b105/pybase64-1.4.3-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb632edfd132b3eaf90c39c89aa314beec4e946e210099b57d40311f704e11d4", size = 71599, upload-time = "2025-12-06T13:23:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/03/4b/a3b5175130b3810bbb8ccfa1edaadbd3afddb9992d877c8a1e2f274b476e/pybase64-1.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:356ef1d74648ce997f5a777cf8f1aefecc1c0b4fe6201e0ef3ec8a08170e1b54", size = 59922, upload-time = "2025-12-06T13:23:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/da/5d/c38d1572027fc601b62d7a407721688b04b4d065d60ca489912d6893e6cf/pybase64-1.4.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:c48361f90db32bacaa5518419d4eb9066ba558013aaf0c7781620279ecddaeb9", size = 56712, upload-time = "2025-12-06T13:23:22.77Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d4/4e04472fef485caa8f561d904d4d69210a8f8fc1608ea15ebd9012b92655/pybase64-1.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:702bcaa16ae02139d881aeaef5b1c8ffb4a3fae062fe601d1e3835e10310a517", size = 59300, upload-time = "2025-12-06T13:23:24.543Z" }, + { url = "https://files.pythonhosted.org/packages/86/e7/16e29721b86734b881d09b7e23dfd7c8408ad01a4f4c7525f3b1088e25ec/pybase64-1.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:53d0ffe1847b16b647c6413d34d1de08942b7724273dd57e67dcbdb10c574045", size = 60278, upload-time = "2025-12-06T13:23:25.608Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/18515f211d7c046be32070709a8efeeef8a0203de4fd7521e6b56404731b/pybase64-1.4.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9a1792e8b830a92736dae58f0c386062eb038dfe8004fb03ba33b6083d89cd43", size = 54817, upload-time = "2025-12-06T13:23:26.633Z" }, + { url = "https://files.pythonhosted.org/packages/e7/be/14e29d8e1a481dbff151324c96dd7b5d2688194bb65dc8a00ca0e1ad1e86/pybase64-1.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d468b1b1ac5ad84875a46eaa458663c3721e8be5f155ade356406848d3701f6", size = 58611, upload-time = "2025-12-06T13:23:27.684Z" }, + { url = "https://files.pythonhosted.org/packages/b4/8a/a2588dfe24e1bbd742a554553778ab0d65fdf3d1c9a06d10b77047d142aa/pybase64-1.4.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e97b7bdbd62e71898cd542a6a9e320d9da754ff3ebd02cb802d69087ee94d468", size = 52404, upload-time = "2025-12-06T13:23:28.714Z" }, + { url = "https://files.pythonhosted.org/packages/27/fc/afcda7445bebe0cbc38cafdd7813234cdd4fc5573ff067f1abf317bb0cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b33aeaa780caaa08ffda87fc584d5eab61e3d3bbb5d86ead02161dc0c20d04bc", size = 68817, upload-time = "2025-12-06T13:23:30.079Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3a/87c3201e555ed71f73e961a787241a2438c2bbb2ca8809c29ddf938a3157/pybase64-1.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c0efcf78f11cf866bed49caa7b97552bc4855a892f9cc2372abcd3ed0056f0d", size = 57854, upload-time = "2025-12-06T13:23:31.17Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7d/931c2539b31a7b375e7d595b88401eeb5bd6c5ce1059c9123f9b608aaa14/pybase64-1.4.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:66e3791f2ed725a46593f8bd2761ff37d01e2cdad065b1dceb89066f476e50c6", size = 54333, upload-time = "2025-12-06T13:23:32.422Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/537601e02cc01f27e9d75f440f1a6095b8df44fc28b1eef2cd739aea8cec/pybase64-1.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:72bb0b6bddadab26e1b069bb78e83092711a111a80a0d6b9edcb08199ad7299b", size = 56492, upload-time = "2025-12-06T13:23:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/96/97/2a2e57acf8f5c9258d22aba52e71f8050e167b29ed2ee1113677c1b600c1/pybase64-1.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5b3365dbcbcdb0a294f0f50af0c0a16b27a232eddeeb0bceeefd844ef30d2a23", size = 70974, upload-time = "2025-12-06T13:23:36.27Z" }, + { url = "https://files.pythonhosted.org/packages/75/2e/a9e28941c6dab6f06e6d3f6783d3373044be9b0f9a9d3492c3d8d2260ac0/pybase64-1.4.3-cp312-cp312-win32.whl", hash = "sha256:7bca1ed3a5df53305c629ca94276966272eda33c0d71f862d2d3d043f1e1b91a", size = 33686, upload-time = "2025-12-06T13:23:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/83/e3/507ab649d8c3512c258819c51d25c45d6e29d9ca33992593059e7b646a33/pybase64-1.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:9f2da8f56d9b891b18b4daf463a0640eae45a80af548ce435be86aa6eff3603b", size = 35833, upload-time = "2025-12-06T13:23:38.877Z" }, + { url = "https://files.pythonhosted.org/packages/bc/8a/6eba66cd549a2fc74bb4425fd61b839ba0ab3022d3c401b8a8dc2cc00c7a/pybase64-1.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:0631d8a2d035de03aa9bded029b9513e1fee8ed80b7ddef6b8e9389ffc445da0", size = 31185, upload-time = "2025-12-06T13:23:39.908Z" }, + { url = "https://files.pythonhosted.org/packages/3a/50/b7170cb2c631944388fe2519507fe3835a4054a6a12a43f43781dae82be1/pybase64-1.4.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:ea4b785b0607d11950b66ce7c328f452614aefc9c6d3c9c28bae795dc7f072e1", size = 33901, upload-time = "2025-12-06T13:23:40.951Z" }, + { url = "https://files.pythonhosted.org/packages/48/8b/69f50578e49c25e0a26e3ee72c39884ff56363344b79fc3967f5af420ed6/pybase64-1.4.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:6a10b6330188c3026a8b9c10e6b9b3f2e445779cf16a4c453d51a072241c65a2", size = 40807, upload-time = "2025-12-06T13:23:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8d/20b68f11adfc4c22230e034b65c71392e3e338b413bf713c8945bd2ccfb3/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:27fdff227a0c0e182e0ba37a99109645188978b920dfb20d8b9c17eeee370d0d", size = 30932, upload-time = "2025-12-06T13:23:43.348Z" }, + { url = "https://files.pythonhosted.org/packages/f7/79/b1b550ac6bff51a4880bf6e089008b2e1ca16f2c98db5e039a08ac3ad157/pybase64-1.4.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2a8204f1fdfec5aa4184249b51296c0de95445869920c88123978304aad42df1", size = 31394, upload-time = "2025-12-06T13:23:44.317Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/b5d7c5932bf64ee1ec5da859fbac981930b6a55d432a603986c7f509c838/pybase64-1.4.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:874fc2a3777de6baf6aa921a7aa73b3be98295794bea31bd80568a963be30767", size = 38078, upload-time = "2025-12-06T13:23:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/e66fe373bce717c6858427670736d54297938dad61c5907517ab4106bd90/pybase64-1.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2dc64a94a9d936b8e3449c66afabbaa521d3cc1a563d6bbaaa6ffa4535222e4b", size = 38158, upload-time = "2025-12-06T13:23:46.872Z" }, + { url = "https://files.pythonhosted.org/packages/80/a9/b806ed1dcc7aed2ea3dd4952286319e6f3a8b48615c8118f453948e01999/pybase64-1.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e48f86de1c145116ccf369a6e11720ce696c2ec02d285f440dfb57ceaa0a6cb4", size = 31672, upload-time = "2025-12-06T13:23:47.88Z" }, + { url = "https://files.pythonhosted.org/packages/1c/c9/24b3b905cf75e23a9a4deaf203b35ffcb9f473ac0e6d8257f91a05dfce62/pybase64-1.4.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:1d45c8fe8fe82b65c36b227bb4a2cf623d9ada16bed602ce2d3e18c35285b72a", size = 68244, upload-time = "2025-12-06T13:23:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/f8/cd/d15b0c3e25e5859fab0416dc5b96d34d6bd2603c1c96a07bb2202b68ab92/pybase64-1.4.3-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ad70c26ba091d8f5167e9d4e1e86a0483a5414805cdb598a813db635bd3be8b8", size = 71620, upload-time = "2025-12-06T13:23:50.081Z" }, + { url = "https://files.pythonhosted.org/packages/0d/31/4ca953cc3dcde2b3711d6bfd70a6f4ad2ca95a483c9698076ba605f1520f/pybase64-1.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e98310b7c43145221e7194ac9fa7fffc84763c87bfc5e2f59f9f92363475bdc1", size = 59930, upload-time = "2025-12-06T13:23:51.68Z" }, + { url = "https://files.pythonhosted.org/packages/60/55/e7f7bdcd0fd66e61dda08db158ffda5c89a306bbdaaf5a062fbe4e48f4a1/pybase64-1.4.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:398685a76034e91485a28aeebcb49e64cd663212fd697b2497ac6dfc1df5e671", size = 56425, upload-time = "2025-12-06T13:23:52.732Z" }, + { url = "https://files.pythonhosted.org/packages/cb/65/b592c7f921e51ca1aca3af5b0d201a98666d0a36b930ebb67e7c2ed27395/pybase64-1.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7e46400a6461187ccb52ed75b0045d937529e801a53a9cd770b350509f9e4d50", size = 59327, upload-time = "2025-12-06T13:23:53.856Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/1613d2fb82dbb1548595ad4179f04e9a8451bfa18635efce18b631eabe3f/pybase64-1.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1b62b9f2f291d94f5e0b76ab499790b7dcc78a009d4ceea0b0428770267484b6", size = 60294, upload-time = "2025-12-06T13:23:54.937Z" }, + { url = "https://files.pythonhosted.org/packages/9d/73/40431f37f7d1b3eab4673e7946ff1e8f5d6bd425ec257e834dae8a6fc7b0/pybase64-1.4.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:f30ceb5fa4327809dede614be586efcbc55404406d71e1f902a6fdcf322b93b2", size = 54858, upload-time = "2025-12-06T13:23:56.031Z" }, + { url = "https://files.pythonhosted.org/packages/a7/84/f6368bcaf9f743732e002a9858646fd7a54f428490d427dd6847c5cfe89e/pybase64-1.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0d5f18ed53dfa1d4cf8b39ee542fdda8e66d365940e11f1710989b3cf4a2ed66", size = 58629, upload-time = "2025-12-06T13:23:57.12Z" }, + { url = "https://files.pythonhosted.org/packages/43/75/359532f9adb49c6b546cafc65c46ed75e2ccc220d514ba81c686fbd83965/pybase64-1.4.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:119d31aa4b58b85a8ebd12b63c07681a138c08dfc2fe5383459d42238665d3eb", size = 52448, upload-time = "2025-12-06T13:23:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/92/6c/ade2ba244c3f33ed920a7ed572ad772eb0b5f14480b72d629d0c9e739a40/pybase64-1.4.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3cf0218b0e2f7988cf7d738a73b6a1d14f3be6ce249d7c0f606e768366df2cce", size = 68841, upload-time = "2025-12-06T13:23:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/51/b345139cd236be382f2d4d4453c21ee6299e14d2f759b668e23080f8663f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:12f4ee5e988bc5c0c1106b0d8fc37fb0508f12dab76bac1b098cb500d148da9d", size = 57910, upload-time = "2025-12-06T13:24:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b8/9f84bdc4f1c4f0052489396403c04be2f9266a66b70c776001eaf0d78c1f/pybase64-1.4.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:937826bc7b6b95b594a45180e81dd4d99bd4dd4814a443170e399163f7ff3fb6", size = 54335, upload-time = "2025-12-06T13:24:02.046Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c7/be63b617d284de46578a366da77ede39c8f8e815ed0d82c7c2acca560fab/pybase64-1.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:88995d1460971ef80b13e3e007afbe4b27c62db0508bc7250a2ab0a0b4b91362", size = 56486, upload-time = "2025-12-06T13:24:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/5e/96/f252c8f9abd6ded3ef1ccd3cdbb8393a33798007f761b23df8de1a2480e6/pybase64-1.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:72326fe163385ed3e1e806dd579d47fde5d8a59e51297a60fc4e6cbc1b4fc4ed", size = 70978, upload-time = "2025-12-06T13:24:04.221Z" }, + { url = "https://files.pythonhosted.org/packages/af/51/0f5714af7aeef96e30f968e4371d75ad60558aaed3579d7c6c8f1c43c18a/pybase64-1.4.3-cp313-cp313-win32.whl", hash = "sha256:b1623730c7892cf5ed0d6355e375416be6ef8d53ab9b284f50890443175c0ac3", size = 33684, upload-time = "2025-12-06T13:24:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ad/0cea830a654eb08563fb8214150ef57546ece1cc421c09035f0e6b0b5ea9/pybase64-1.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:8369887590f1646a5182ca2fb29252509da7ae31d4923dbb55d3e09da8cc4749", size = 35832, upload-time = "2025-12-06T13:24:06.35Z" }, + { url = "https://files.pythonhosted.org/packages/b4/0d/eec2a8214989c751bc7b4cad1860eb2c6abf466e76b77508c0f488c96a37/pybase64-1.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:860b86bca71e5f0237e2ab8b2d9c4c56681f3513b1bf3e2117290c1963488390", size = 31175, upload-time = "2025-12-06T13:24:07.419Z" }, + { url = "https://files.pythonhosted.org/packages/db/c9/e23463c1a2913686803ef76b1a5ae7e6fac868249a66e48253d17ad7232c/pybase64-1.4.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:eb51db4a9c93215135dccd1895dca078e8785c357fabd983c9f9a769f08989a9", size = 38497, upload-time = "2025-12-06T13:24:08.873Z" }, + { url = "https://files.pythonhosted.org/packages/71/83/343f446b4b7a7579bf6937d2d013d82f1a63057cf05558e391ab6039d7db/pybase64-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a03ef3f529d85fd46b89971dfb00c634d53598d20ad8908fb7482955c710329d", size = 32076, upload-time = "2025-12-06T13:24:09.975Z" }, + { url = "https://files.pythonhosted.org/packages/46/fc/cb64964c3b29b432f54d1bce5e7691d693e33bbf780555151969ffd95178/pybase64-1.4.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2e745f2ce760c6cf04d8a72198ef892015ddb89f6ceba489e383518ecbdb13ab", size = 72317, upload-time = "2025-12-06T13:24:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b7/fab2240da6f4e1ad46f71fa56ec577613cf5df9dce2d5b4cfaa4edd0e365/pybase64-1.4.3-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fac217cd9de8581a854b0ac734c50fd1fa4b8d912396c1fc2fce7c230efe3a7", size = 75534, upload-time = "2025-12-06T13:24:12.433Z" }, + { url = "https://files.pythonhosted.org/packages/91/3b/3e2f2b6e68e3d83ddb9fa799f3548fb7449765daec9bbd005a9fbe296d7f/pybase64-1.4.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:da1ee8fa04b283873de2d6e8fa5653e827f55b86bdf1a929c5367aaeb8d26f8a", size = 65399, upload-time = "2025-12-06T13:24:13.928Z" }, + { url = "https://files.pythonhosted.org/packages/6b/08/476ac5914c3b32e0274a2524fc74f01cbf4f4af4513d054e41574eb018f6/pybase64-1.4.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:b0bf8e884ee822ca7b1448eeb97fa131628fe0ff42f60cae9962789bd562727f", size = 60487, upload-time = "2025-12-06T13:24:15.177Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b8/618a92915330cc9cba7880299b546a1d9dab1a21fd6c0292ee44a4fe608c/pybase64-1.4.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1bf749300382a6fd1f4f255b183146ef58f8e9cb2f44a077b3a9200dfb473a77", size = 63959, upload-time = "2025-12-06T13:24:16.854Z" }, + { url = "https://files.pythonhosted.org/packages/a5/52/af9d8d051652c3051862c442ec3861259c5cdb3fc69774bc701470bd2a59/pybase64-1.4.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:153a0e42329b92337664cfc356f2065248e6c9a1bd651bbcd6dcaf15145d3f06", size = 64874, upload-time = "2025-12-06T13:24:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/5381a7adf1f381bd184d33203692d3c57cf8ae9f250f380c3fecbdbe554b/pybase64-1.4.3-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:86ee56ac7f2184ca10217ed1c655c1a060273e233e692e9086da29d1ae1768db", size = 58572, upload-time = "2025-12-06T13:24:19.417Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f0/578ee4ffce5818017de4fdf544e066c225bc435e73eb4793cde28a689d0b/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:0e71a4db76726bf830b47477e7d830a75c01b2e9b01842e787a0836b0ba741e3", size = 63636, upload-time = "2025-12-06T13:24:20.497Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ad/8ae94814bf20159ea06310b742433e53d5820aa564c9fdf65bf2d79f8799/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2ba7799ec88540acd9861b10551d24656ca3c2888ecf4dba2ee0a71544a8923f", size = 56193, upload-time = "2025-12-06T13:24:21.559Z" }, + { url = "https://files.pythonhosted.org/packages/d1/31/6438cfcc3d3f0fa84d229fa125c243d5094e72628e525dfefadf3bcc6761/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2860299e4c74315f5951f0cf3e72ba0f201c3356c8a68f95a3ab4e620baf44e9", size = 72655, upload-time = "2025-12-06T13:24:22.673Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0d/2bbc9e9c3fc12ba8a6e261482f03a544aca524f92eae0b4908c0a10ba481/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:bb06015db9151f0c66c10aae8e3603adab6b6cd7d1f7335a858161d92fc29618", size = 62471, upload-time = "2025-12-06T13:24:23.8Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0b/34d491e7f49c1dbdb322ea8da6adecda7c7cd70b6644557c6e4ca5c6f7c7/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:242512a070817272865d37c8909059f43003b81da31f616bb0c391ceadffe067", size = 58119, upload-time = "2025-12-06T13:24:24.994Z" }, + { url = "https://files.pythonhosted.org/packages/ce/17/c21d0cde2a6c766923ae388fc1f78291e1564b0d38c814b5ea8a0e5e081c/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:5d8277554a12d3e3eed6180ebda62786bf9fc8d7bb1ee00244258f4a87ca8d20", size = 60791, upload-time = "2025-12-06T13:24:26.046Z" }, + { url = "https://files.pythonhosted.org/packages/92/b2/eaa67038916a48de12b16f4c384bcc1b84b7ec731b23613cb05f27673294/pybase64-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f40b7ddd698fc1e13a4b64fbe405e4e0e1279e8197e37050e24154655f5f7c4e", size = 74701, upload-time = "2025-12-06T13:24:27.466Z" }, + { url = "https://files.pythonhosted.org/packages/42/10/abb7757c330bb869ebb95dab0c57edf5961ffbd6c095c8209cbbf75d117d/pybase64-1.4.3-cp313-cp313t-win32.whl", hash = "sha256:46d75c9387f354c5172582a9eaae153b53a53afeb9c19fcf764ea7038be3bd8b", size = 33965, upload-time = "2025-12-06T13:24:28.548Z" }, + { url = "https://files.pythonhosted.org/packages/63/a0/2d4e5a59188e9e6aed0903d580541aaea72dcbbab7bf50fb8b83b490b6c3/pybase64-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:d7344625591d281bec54e85cbfdab9e970f6219cac1570f2aa140b8c942ccb81", size = 36207, upload-time = "2025-12-06T13:24:29.646Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/95b902e8f567b4d4b41df768ccc438af618f8d111e54deaf57d2df46bd76/pybase64-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:28a3c60c55138e0028313f2eccd321fec3c4a0be75e57a8d3eb883730b1b0880", size = 31505, upload-time = "2025-12-06T13:24:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/e4/80/4bd3dff423e5a91f667ca41982dc0b79495b90ec0c0f5d59aca513e50f8c/pybase64-1.4.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:015bb586a1ea1467f69d57427abe587469392215f59db14f1f5c39b52fdafaf5", size = 33835, upload-time = "2025-12-06T13:24:31.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/60/a94d94cc1e3057f602e0b483c9ebdaef40911d84a232647a2fe593ab77bb/pybase64-1.4.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d101e3a516f837c3dcc0e5a0b7db09582ebf99ed670865223123fb2e5839c6c0", size = 40673, upload-time = "2025-12-06T13:24:32.82Z" }, + { url = "https://files.pythonhosted.org/packages/e3/71/cf62b261d431857e8e054537a5c3c24caafa331de30daede7b2c6c558501/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8f183ac925a48046abe047360fe3a1b28327afb35309892132fe1915d62fb282", size = 30939, upload-time = "2025-12-06T13:24:34.001Z" }, + { url = "https://files.pythonhosted.org/packages/24/3e/d12f92a3c1f7c6ab5d53c155bff9f1084ba997a37a39a4f781ccba9455f3/pybase64-1.4.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30bf3558e24dcce4da5248dcf6d73792adfcf4f504246967e9db155be4c439ad", size = 31401, upload-time = "2025-12-06T13:24:35.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3d/9c27440031fea0d05146f8b70a460feb95d8b4e3d9ca8f45c972efb4c3d3/pybase64-1.4.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a674b419de318d2ce54387dd62646731efa32b4b590907800f0bd40675c1771d", size = 38075, upload-time = "2025-12-06T13:24:36.53Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d4/6c0e0cf0efd53c254173fbcd84a3d8fcbf5e0f66622473da425becec32a5/pybase64-1.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:720104fd7303d07bac302be0ff8f7f9f126f2f45c1edb4f48fdb0ff267e69fe1", size = 38257, upload-time = "2025-12-06T13:24:38.049Z" }, + { url = "https://files.pythonhosted.org/packages/50/eb/27cb0b610d5cd70f5ad0d66c14ad21c04b8db930f7139818e8fbdc14df4d/pybase64-1.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:83f1067f73fa5afbc3efc0565cecc6ed53260eccddef2ebe43a8ce2b99ea0e0a", size = 31685, upload-time = "2025-12-06T13:24:40.327Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/b136a4b65e5c94ff06217f7726478df3f31ab1c777c2c02cf698e748183f/pybase64-1.4.3-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b51204d349a4b208287a8aa5b5422be3baa88abf6cc8ff97ccbda34919bbc857", size = 68460, upload-time = "2025-12-06T13:24:41.735Z" }, + { url = "https://files.pythonhosted.org/packages/68/6d/84ce50e7ee1ae79984d689e05a9937b2460d4efa1e5b202b46762fb9036c/pybase64-1.4.3-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30f2fd53efecbdde4bdca73a872a68dcb0d1bf8a4560c70a3e7746df973e1ef3", size = 71688, upload-time = "2025-12-06T13:24:42.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/6743e420416c3ff1b004041c85eb0ebd9c50e9cf05624664bfa1dc8b5625/pybase64-1.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0932b0c5cfa617091fd74f17d24549ce5de3628791998c94ba57be808078eeaf", size = 60040, upload-time = "2025-12-06T13:24:44.37Z" }, + { url = "https://files.pythonhosted.org/packages/3b/68/733324e28068a89119af2921ce548e1c607cc5c17d354690fc51c302e326/pybase64-1.4.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:acb61f5ab72bec808eb0d4ce8b87ec9f38d7d750cb89b1371c35eb8052a29f11", size = 56478, upload-time = "2025-12-06T13:24:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9e/f3f4aa8cfe3357a3cdb0535b78eb032b671519d3ecc08c58c4c6b72b5a91/pybase64-1.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:2bc2d5bc15168f5c04c53bdfe5a1e543b2155f456ed1e16d7edce9ce73842021", size = 59463, upload-time = "2025-12-06T13:24:46.938Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d1/53286038e1f0df1cf58abcf4a4a91b0f74ab44539c2547b6c31001ddd054/pybase64-1.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:8a7bc3cd23880bdca59758bcdd6f4ef0674f2393782763910a7466fab35ccb98", size = 60360, upload-time = "2025-12-06T13:24:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/00/9a/5cc6ce95db2383d27ff4d790b8f8b46704d360d701ab77c4f655bcfaa6a7/pybase64-1.4.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:ad15acf618880d99792d71e3905b0e2508e6e331b76a1b34212fa0f11e01ad28", size = 54999, upload-time = "2025-12-06T13:24:49.547Z" }, + { url = "https://files.pythonhosted.org/packages/64/e7/c3c1d09c3d7ae79e3aa1358c6d912d6b85f29281e47aa94fc0122a415a2f/pybase64-1.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448158d417139cb4851200e5fee62677ae51f56a865d50cda9e0d61bda91b116", size = 58736, upload-time = "2025-12-06T13:24:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/0baa08e3d8119b15b588c39f0d39fd10472f0372e3c54ca44649cbefa256/pybase64-1.4.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9058c49b5a2f3e691b9db21d37eb349e62540f9f5fc4beabf8cbe3c732bead86", size = 52298, upload-time = "2025-12-06T13:24:51.791Z" }, + { url = "https://files.pythonhosted.org/packages/00/87/fc6f11474a1de7e27cd2acbb8d0d7508bda3efa73dfe91c63f968728b2a3/pybase64-1.4.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ce561724f6522907a66303aca27dce252d363fcd85884972d348f4403ba3011a", size = 69049, upload-time = "2025-12-06T13:24:53.253Z" }, + { url = "https://files.pythonhosted.org/packages/69/9d/7fb5566f669ac18b40aa5fc1c438e24df52b843c1bdc5da47d46d4c1c630/pybase64-1.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:63316560a94ac449fe86cb8b9e0a13714c659417e92e26a5cbf085cd0a0c838d", size = 57952, upload-time = "2025-12-06T13:24:54.342Z" }, + { url = "https://files.pythonhosted.org/packages/de/cc/ceb949232dbbd3ec4ee0190d1df4361296beceee9840390a63df8bc31784/pybase64-1.4.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ecd796f2ac0be7b73e7e4e232b8c16422014de3295d43e71d2b19fd4a4f5368", size = 54484, upload-time = "2025-12-06T13:24:55.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/69/659f3c8e6a5d7b753b9c42a4bd9c42892a0f10044e9c7351a4148d413a33/pybase64-1.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d01e102a12fb2e1ed3dc11611c2818448626637857ec3994a9cf4809dfd23477", size = 56542, upload-time = "2025-12-06T13:24:57Z" }, + { url = "https://files.pythonhosted.org/packages/85/2c/29c9e6c9c82b72025f9676f9e82eb1fd2339ad038cbcbf8b9e2ac02798fc/pybase64-1.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ebff797a93c2345f22183f454fd8607a34d75eca5a3a4a969c1c75b304cee39d", size = 71045, upload-time = "2025-12-06T13:24:58.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/84/5a3dce8d7a0040a5c0c14f0fe1311cd8db872913fa04438071b26b0dac04/pybase64-1.4.3-cp314-cp314-win32.whl", hash = "sha256:28b2a1bb0828c0595dc1ea3336305cd97ff85b01c00d81cfce4f92a95fb88f56", size = 34200, upload-time = "2025-12-06T13:24:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/57/bc/ce7427c12384adee115b347b287f8f3cf65860b824d74fe2c43e37e81c1f/pybase64-1.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:33338d3888700ff68c3dedfcd49f99bfc3b887570206130926791e26b316b029", size = 36323, upload-time = "2025-12-06T13:25:01.708Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1b/2b8ffbe9a96eef7e3f6a5a7be75995eebfb6faaedc85b6da6b233e50c778/pybase64-1.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:62725669feb5acb186458da2f9353e88ae28ef66bb9c4c8d1568b12a790dfa94", size = 31584, upload-time = "2025-12-06T13:25:02.801Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d8/6824c2e6fb45b8fa4e7d92e3c6805432d5edc7b855e3e8e1eedaaf6efb7c/pybase64-1.4.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:153fe29be038948d9372c3e77ae7d1cab44e4ba7d9aaf6f064dbeea36e45b092", size = 38601, upload-time = "2025-12-06T13:25:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/ea/e5/10d2b3a4ad3a4850be2704a2f70cd9c0cf55725c8885679872d3bc846c67/pybase64-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f7fe3decaa7c4a9e162327ec7bd81ce183d2b16f23c6d53b606649c6e0203e9e", size = 32078, upload-time = "2025-12-06T13:25:05.362Z" }, + { url = "https://files.pythonhosted.org/packages/43/04/8b15c34d3c2282f1c1b0850f1113a249401b618a382646a895170bc9b5e7/pybase64-1.4.3-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a5ae04ea114c86eb1da1f6e18d75f19e3b5ae39cb1d8d3cd87c29751a6a22780", size = 72474, upload-time = "2025-12-06T13:25:06.434Z" }, + { url = "https://files.pythonhosted.org/packages/42/00/f34b4d11278f8fdc68bc38f694a91492aa318f7c6f1bd7396197ac0f8b12/pybase64-1.4.3-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1755b3dce3a2a5c7d17ff6d4115e8bee4a1d5aeae74469db02e47c8f477147da", size = 75706, upload-time = "2025-12-06T13:25:07.636Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5d/71747d4ad7fe16df4c4c852bdbdeb1f2cf35677b48d7c34d3011a7a6ad3a/pybase64-1.4.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb852f900e27ffc4ec1896817535a0fa19610ef8875a096b59f21d0aa42ff172", size = 65589, upload-time = "2025-12-06T13:25:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/49/b1/d1e82bd58805bb5a3a662864800bab83a83a36ba56e7e3b1706c708002a5/pybase64-1.4.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:9cf21ea8c70c61eddab3421fbfce061fac4f2fb21f7031383005a1efdb13d0b9", size = 60670, upload-time = "2025-12-06T13:25:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/15/67/16c609b7a13d1d9fc87eca12ba2dce5e67f949eeaab61a41bddff843cbb0/pybase64-1.4.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:afff11b331fdc27692fc75e85ae083340a35105cea1a3c4552139e2f0e0d174f", size = 64194, upload-time = "2025-12-06T13:25:11.48Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/37bc724e42960f0106c2d33dc957dcec8f760c91a908cc6c0df7718bc1a8/pybase64-1.4.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9a5143df542c1ce5c1f423874b948c4d689b3f05ec571f8792286197a39ba02", size = 64984, upload-time = "2025-12-06T13:25:12.645Z" }, + { url = "https://files.pythonhosted.org/packages/6e/66/b2b962a6a480dd5dae3029becf03ea1a650d326e39bf1c44ea3db78bb010/pybase64-1.4.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:d62e9861019ad63624b4a7914dff155af1cc5d6d79df3be14edcaedb5fdad6f9", size = 58750, upload-time = "2025-12-06T13:25:13.848Z" }, + { url = "https://files.pythonhosted.org/packages/2b/15/9b6d711035e29b18b2e1c03d47f41396d803d06ef15b6c97f45b75f73f04/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:84cfd4d92668ef5766cc42a9c9474b88960ac2b860767e6e7be255c6fddbd34a", size = 63816, upload-time = "2025-12-06T13:25:15.356Z" }, + { url = "https://files.pythonhosted.org/packages/b4/21/e2901381ed0df62e2308380f30d9c4d87d6b74e33a84faed3478d33a7197/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:60fc025437f9a7c2cc45e0c19ed68ed08ba672be2c5575fd9d98bdd8f01dd61f", size = 56348, upload-time = "2025-12-06T13:25:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/c4/16/3d788388a178a0407aa814b976fe61bfa4af6760d9aac566e59da6e4a8b4/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:edc8446196f04b71d3af76c0bd1fe0a45066ac5bffecca88adb9626ee28c266f", size = 72842, upload-time = "2025-12-06T13:25:18.055Z" }, + { url = "https://files.pythonhosted.org/packages/a6/63/c15b1f8bd47ea48a5a2d52a4ec61f037062932ea6434ab916107b58e861e/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e99f6fa6509c037794da57f906ade271f52276c956d00f748e5b118462021d48", size = 62651, upload-time = "2025-12-06T13:25:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/bd/b8/f544a2e37c778d59208966d4ef19742a0be37c12fc8149ff34483c176616/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d94020ef09f624d841aa9a3a6029df8cf65d60d7a6d5c8687579fa68bd679b65", size = 58295, upload-time = "2025-12-06T13:25:20.822Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/1fae8a3b7ac181e36f6e7864a62d42d5b1f4fa7edf408c6711e28fba6b4d/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f64ce70d89942a23602dee910dec9b48e5edf94351e1b378186b74fcc00d7f66", size = 60960, upload-time = "2025-12-06T13:25:22.099Z" }, + { url = "https://files.pythonhosted.org/packages/9d/9e/cd4c727742345ad8384569a4466f1a1428f4e5cc94d9c2ab2f53d30be3fe/pybase64-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8ea99f56e45c469818b9781903be86ba4153769f007ba0655fa3b46dc332803d", size = 74863, upload-time = "2025-12-06T13:25:23.442Z" }, + { url = "https://files.pythonhosted.org/packages/28/86/a236ecfc5b494e1e922da149689f690abc84248c7c1358f5605b8c9fdd60/pybase64-1.4.3-cp314-cp314t-win32.whl", hash = "sha256:343b1901103cc72362fd1f842524e3bb24978e31aea7ff11e033af7f373f66ab", size = 34513, upload-time = "2025-12-06T13:25:24.592Z" }, + { url = "https://files.pythonhosted.org/packages/56/ce/ca8675f8d1352e245eb012bfc75429ee9cf1f21c3256b98d9a329d44bf0f/pybase64-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:57aff6f7f9dea6705afac9d706432049642de5b01080d3718acc23af87c5af76", size = 36702, upload-time = "2025-12-06T13:25:25.72Z" }, + { url = "https://files.pythonhosted.org/packages/3b/30/4a675864877397179b09b720ee5fcb1cf772cf7bebc831989aff0a5f79c1/pybase64-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e906aa08d4331e799400829e0f5e4177e76a3281e8a4bc82ba114c6b30e405c9", size = 31904, upload-time = "2025-12-06T13:25:26.826Z" }, + { url = "https://files.pythonhosted.org/packages/17/45/92322aec1b6979e789b5710f73c59f2172bc37c8ce835305434796824b7b/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:2baaa092f3475f3a9c87ac5198023918ea8b6c125f4c930752ab2cbe3cd1d520", size = 38746, upload-time = "2025-12-06T13:26:25.869Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/f1a07402870388fdfc2ecec0c718111189732f7d0f2d7fe1386e19e8fad0/pybase64-1.4.3-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:cde13c0764b1af07a631729f26df019070dad759981d6975527b7e8ecb465b6c", size = 32573, upload-time = "2025-12-06T13:26:27.792Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8f/43c3bb11ca9bacf81cb0b7a71500bb65b2eda6d5fe07433c09b543de97f3/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5c29a582b0ea3936d02bd6fe9bf674ab6059e6e45ab71c78404ab2c913224414", size = 43461, upload-time = "2025-12-06T13:26:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/2d/4c/2a5258329200be57497d3972b5308558c6de42e3749c6cc2aa1cbe34b25a/pybase64-1.4.3-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6b664758c804fa919b4f1257aa8cf68e95db76fc331de5f70bfc3a34655afe1", size = 36058, upload-time = "2025-12-06T13:26:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/41faa414cde66ec023b0ca8402a8f11cb61731c3dc27c082909cbbd1f929/pybase64-1.4.3-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:f7537fa22ae56a0bf51e4b0ffc075926ad91c618e1416330939f7ef366b58e3b", size = 36231, upload-time = "2025-12-06T13:26:31.656Z" }, +] + [[package]] name = "pyclipper" version = "1.4.0" @@ -2466,24 +2873,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] -[[package]] -name = "qdrant-client" -version = "1.18.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "grpcio" }, - { name = "httpx", extra = ["http2"] }, - { name = "numpy" }, - { name = "portalocker" }, - { name = "protobuf" }, - { name = "pydantic" }, - { name = "urllib3" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/45/5b1bdd15a3c7730eefb9c113600829e20d689b82b5a23f9e07d107094004/qdrant_client-1.18.0.tar.gz", hash = "sha256:52e8ece1a7d40519801bf0b70713bfa0f6b7ae28c7275bbe0b0286fbed7f6db4", size = 352580, upload-time = "2026-05-11T14:12:38.702Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/10/c437bd2ac41ef30d3019063e6ce537dc111e9214473b337ee88f7fa6359a/qdrant_client-1.18.0-py3-none-any.whl", hash = "sha256:093aa8cf8a420ee3ad2a68b007e1378d7992b2600e0b53c193fc172674f659cd", size = 398126, upload-time = "2026-05-11T14:12:36.998Z" }, -] - [[package]] name = "rapidocr" version = "3.8.1" @@ -3379,6 +3768,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/68/fa86e5a39608000f645535b2c124920126327ab731f8c4fafd5b07ff8d4b/triton-3.7.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce061073102714b725f3660ec6939d94a1da7984b3aa99c921417cae273672f5", size = 201546766, upload-time = "2026-05-07T18:46:42.088Z" }, ] +[[package]] +name = "turbopuffer" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "orjson" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/6c/00fb1c2bc74e2c61e7185649f1439aadb43f28362907d4f2a7eea4030559/turbopuffer-2.1.0.tar.gz", hash = "sha256:c9b9447cc30ec54838b390ae07dd584d77d54c7fbf4df4482559279a767eb044", size = 345993, upload-time = "2026-05-17T21:00:56.658Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/fb/ca540afa9134e69b2280d4d52947874d271c10397006f4c06f07542b9d97/turbopuffer-2.1.0-py3-none-any.whl", hash = "sha256:63edcf7ef5d4a2ad0283cd0fbbf374732ff073107c26ae63ec2dd882f5abb422", size = 130463, upload-time = "2026-05-17T21:00:54.958Z" }, +] + [[package]] name = "typer" version = "0.21.2" @@ -3640,3 +4049,85 @@ sdist = { url = "https://files.pythonhosted.org/packages/46/2c/c06ef49dc36e7954e wheels = [ { url = "https://files.pythonhosted.org/packages/3a/0c/3662f4a66880196a590b202f0db82d919dd2f89e99a27fadef91c4a33d41/xlsxwriter-3.2.9-py3-none-any.whl", hash = "sha256:9a5db42bc5dff014806c58a20b9eae7322a134abb6fce3c92c181bfb275ec5b3", size = 175315, upload-time = "2025-09-16T00:16:20.108Z" }, ] + +[[package]] +name = "yarl" +version = "1.24.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/12/1e8f37460ea0f7eb59c221fdaf0ed75e7ac43e97f8093b9c6f411df50a78/yarl-1.24.2.tar.gz", hash = "sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8", size = 210798, upload-time = "2026-05-19T21:31:05.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/da/866bcb01076ba49d2b42b309867bed3826421f1c479655eb7a607b44f20b/yarl-1.24.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b975866c184564c827e0877380f0dae57dcca7e52782128381b72feff6dfceb8", size = 129957, upload-time = "2026-05-19T21:28:51.695Z" }, + { url = "https://files.pythonhosted.org/packages/bf/1d/fcefb70922ea2268a8971d8e5874d9a8218644200fb8465f1dcad55e6851/yarl-1.24.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3b075301a2836a0e297b1b658cb6d6135df535d62efefdd60366bd589c2c82f2", size = 92164, upload-time = "2026-05-19T21:28:53.242Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/170e2b8d4e3bc30e6bfdcca53556537f5bf595e938632dfcb059311f3ff6/yarl-1.24.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ae44649b00947634ab0dab2a374a638f52923a6e67083f2c156cd5cbd1a881d", size = 91688, upload-time = "2026-05-19T21:28:54.865Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a5/c9f655d5553ea0b99fdac9d6a99ad3f9b3e73b8e5758bb46f58c9831f74c/yarl-1.24.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:507cc19f0b45454e2d6dcd62ff7d062b9f77a2812404e62dbdaec05b50faa035", size = 102902, upload-time = "2026-05-19T21:28:56.963Z" }, + { url = "https://files.pythonhosted.org/packages/5d/bc/6b9664d815d79af4ee553337f9d606c56bbf269186ada9172de45f1b5f60/yarl-1.24.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4c17bad5a530912d2111825d3f05e89bab2dd376aaa8cbc77e449e6db63e576", size = 97931, upload-time = "2026-05-19T21:28:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/98/ec/32ba48acae30fecd60928f5791188b80a9d6ee3840507ffda29fecd37b71/yarl-1.24.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5f0cbb112838a4a293985b6ed73948a547dadcc1ba6d2089938e7abdedceef8", size = 111030, upload-time = "2026-05-19T21:29:00.148Z" }, + { url = "https://files.pythonhosted.org/packages/82/5a/6f4cd081e5f4934d2ae3a8ef4abe3afacc010d26f0035ee91b35cd7d7c37/yarl-1.24.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ec8356b8a6afcf81fc7aeeef13b1ff7a49dec00f313394bbb9e83830d32ccd7", size = 110392, upload-time = "2026-05-19T21:29:02.155Z" }, + { url = "https://files.pythonhosted.org/packages/7a/da/323a01c349bd5fb01bb6652e314d9bb218cee630a736bdb810ad50e4013f/yarl-1.24.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e7ebcdef69dec6c6451e616f32b622a6d4a2e92b445c992f7c8e5274a6bbc4c", size = 105612, upload-time = "2026-05-19T21:29:04.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/80/264ab684f181e1a876389374519ff05d10248725535ae2ac4e8ac4e563d6/yarl-1.24.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:47a55d6cf6db2f401017a9e96e5288844e5051911fb4e0c8311a3980f5e59a7d", size = 104487, upload-time = "2026-05-19T21:29:06.491Z" }, + { url = "https://files.pythonhosted.org/packages/41/07/efabe5df87e96d7ad5959760b888344be48cd6884db127b407c6b5503adc/yarl-1.24.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3065657c80a2321225e804048597ad55658a7e76b32d6f5ee4074d04c50401db", size = 102333, upload-time = "2026-05-19T21:29:08.267Z" }, + { url = "https://files.pythonhosted.org/packages/44/0c/bcf7c42603e1009295f586d8890f2ba032c8b53310e815adf0a202c73d9f/yarl-1.24.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cb84b80d88e19ede158619b80813968713d8d008b0e2497a576e6a0557d50712", size = 99025, upload-time = "2026-05-19T21:29:10.682Z" }, + { url = "https://files.pythonhosted.org/packages/4f/82/84482ab1a57a0f21a08afe6a7004c61d741f8f2ecc3b05c321577c612164/yarl-1.24.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:990de4f680b1c217e77ff0d6aa0029f9eb79889c11fb3e9a3942c7eba29c1996", size = 110507, upload-time = "2026-05-19T21:29:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8d/a546ba1dfe1b0f290e05fef145cd07614c0f15df1a707195e512d1e39d1d/yarl-1.24.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:abb8ec0323b80161e3802da3150ef660b41d0e9be2048b76a363d93eee992c2b", size = 103719, upload-time = "2026-05-19T21:29:14.893Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b6/267f2a09213138473adfce6b8a6e17791d7fee70bd4d9003218e4dec58b0/yarl-1.24.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e7977781f83638a4c73e0f88425563d70173e0dfd90ac006a45c65036293ee3c", size = 110438, upload-time = "2026-05-19T21:29:16.485Z" }, + { url = "https://files.pythonhosted.org/packages/48/2d/1c8d89c7c5f9cad9fb2902445d94e2ab1d7aa35de029afbb8ae95c42d00f/yarl-1.24.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e30dd55825dc554ec5b66a94953b8eda8745926514c5089dfcacecb9c99b5bd1", size = 105719, upload-time = "2026-05-19T21:29:18.367Z" }, + { url = "https://files.pythonhosted.org/packages/a7/25/722e3b93bd687009afb2d59a35e13d30ddd8f80571445bb0c4e4ce26ec66/yarl-1.24.2-cp312-cp312-win_amd64.whl", hash = "sha256:7dafe10c12ddd4d120d528c4b5599c953bd7b12845347d507b95451195bb6cad", size = 92901, upload-time = "2026-05-19T21:29:20.014Z" }, + { url = "https://files.pythonhosted.org/packages/39/47/4486ccfb674c04854a1ef8aa77868b6a6f765feaf69633409d7ca4f02cb8/yarl-1.24.2-cp312-cp312-win_arm64.whl", hash = "sha256:044a09d8401fcf8681977faef6d286b8ade1e2d2e9dceda175d1cfa5ca496f30", size = 87229, upload-time = "2026-05-19T21:29:22.1Z" }, + { url = "https://files.pythonhosted.org/packages/82/62/fcf0ce677f17e5c471c06311dd25964be38a4c586993632910d2e75278bc/yarl-1.24.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536", size = 128978, upload-time = "2026-05-19T21:29:23.83Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/8e63299bb71ed61a834121d9d3fe6c9fcf2a6a5d09754ff4f20f2d20baf5/yarl-1.24.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607", size = 91733, upload-time = "2026-05-19T21:29:25.375Z" }, + { url = "https://files.pythonhosted.org/packages/c1/24/16748d5dab6daec8b0ed81ccec639a1cded0f18dcc62a4f696b4fe366c37/yarl-1.24.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1", size = 91113, upload-time = "2026-05-19T21:29:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/b63fff7b71211e866624b21432d5943cbb633eb0c2872d9ee3070648f22c/yarl-1.24.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986", size = 103899, upload-time = "2026-05-19T21:29:28.842Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/ba1974b8533909636f7733fe86cf677e3619527c3c2fa913e0ea89c48757/yarl-1.24.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488", size = 97862, upload-time = "2026-05-19T21:29:31.086Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/123ac993b5c2ba6f554a140305620cb8f150fa543711bbc49be3ec0a65a4/yarl-1.24.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b", size = 111060, upload-time = "2026-05-19T21:29:32.657Z" }, + { url = "https://files.pythonhosted.org/packages/23/37/c472d3af3509688392134a88a825276770a187f1daa4de3f6dc0a327a751/yarl-1.24.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592", size = 110613, upload-time = "2026-05-19T21:29:34.379Z" }, + { url = "https://files.pythonhosted.org/packages/df/88/09c28dad91e662ccfaa1b78f1c57badde74fc9d0b23e74aef644750ecd73/yarl-1.24.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617", size = 107012, upload-time = "2026-05-19T21:29:36.216Z" }, + { url = "https://files.pythonhosted.org/packages/07/ab/9d4f69d571a94f4d112fa7e2e007200f5a54d319f58c82ac7b7baa61f5c6/yarl-1.24.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92", size = 105887, upload-time = "2026-05-19T21:29:38.746Z" }, + { url = "https://files.pythonhosted.org/packages/8e/9a/000b2b66c0d772a499fc531d21dab92dfeb73b640a12eed6ba89f49bb2d0/yarl-1.24.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a", size = 103620, upload-time = "2026-05-19T21:29:40.368Z" }, + { url = "https://files.pythonhosted.org/packages/41/7c/7c1050f73450fbdaa3f0c72017059f00ce5e13366692f3dba25275a1083d/yarl-1.24.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44", size = 100599, upload-time = "2026-05-19T21:29:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b1/29e5756b3926705f5f6089bd5b9f50a56eaac550da6e260bf713ead44d04/yarl-1.24.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a", size = 110604, upload-time = "2026-05-19T21:29:44.632Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4b/8415bc96e9b150cde942fbac9a8182985e58f40ce5c54c34ed015407d3ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf", size = 105161, upload-time = "2026-05-19T21:29:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d4/cde059abfa229553b7298a2eadde2752e723d50aeedaef86ce59da2718ee/yarl-1.24.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056", size = 110619, upload-time = "2026-05-19T21:29:48.972Z" }, + { url = "https://files.pythonhosted.org/packages/e7/2c/d6a6c9a61549f7b6c7e6dc6937d195bcf069582b47b7200dcd0e7b256acf/yarl-1.24.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992", size = 107362, upload-time = "2026-05-19T21:29:51Z" }, + { url = "https://files.pythonhosted.org/packages/92/dd/3ae5fe417e9d1c353a548553326eb9935e76b6b727161563b424cc296df3/yarl-1.24.2-cp313-cp313-win_amd64.whl", hash = "sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656", size = 92667, upload-time = "2026-05-19T21:29:52.743Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/a7beb239f78f27fca1b053c8e8595e4179c02e62249b4687ec218c370c50/yarl-1.24.2-cp313-cp313-win_arm64.whl", hash = "sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461", size = 87069, upload-time = "2026-05-19T21:29:54.442Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/e08087695fc12789263821c5dc0f8dc52b5b17efd0887cacf419f8a43ba3/yarl-1.24.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2", size = 129670, upload-time = "2026-05-19T21:29:56.631Z" }, + { url = "https://files.pythonhosted.org/packages/3a/98/ab4b5ed1b1b5cd973c8a3eb994c3a6aefb6ce6d399e21bb5f0316c33815c/yarl-1.24.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630", size = 91916, upload-time = "2026-05-19T21:29:58.645Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b1/5297bb6a7df4782f7605bffc43b31f5044070935fbbcaa6c705a07e6ac65/yarl-1.24.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8", size = 91625, upload-time = "2026-05-19T21:30:00.412Z" }, + { url = "https://files.pythonhosted.org/packages/02/a7/45baabfff76829264e623b185cff0c340d7e11bf3e1cd9ea37e7d17934bd/yarl-1.24.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14", size = 104574, upload-time = "2026-05-19T21:30:02.544Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/3a5ab144d3d650ca37d4f4b57e56169be8af3ca34c448793e064b30baaed/yarl-1.24.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535", size = 97534, upload-time = "2026-05-19T21:30:04.319Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b5/5658fef3681fb5776b4513b052bec750009f47b3a592251c705d75375798/yarl-1.24.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14", size = 111481, upload-time = "2026-05-19T21:30:05.988Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/fdcd7dde037f00866dce123ed4ba23dba94beb56fc4cf561668d27be37f2/yarl-1.24.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3", size = 111529, upload-time = "2026-05-19T21:30:07.738Z" }, + { url = "https://files.pythonhosted.org/packages/c2/53/d81269aaafccea0d33396c03035de997b743f11e648e6e27a0df99c72980/yarl-1.24.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208", size = 107338, upload-time = "2026-05-19T21:30:09.713Z" }, + { url = "https://files.pythonhosted.org/packages/ae/04/23049463f729bd899df203a7960505a75333edd499cda8aa1d5a82b64df5/yarl-1.24.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50", size = 106147, upload-time = "2026-05-19T21:30:11.365Z" }, + { url = "https://files.pythonhosted.org/packages/14/18/04a4b5830b43ed5e4c5015b40e9f6241ad91487d71611061b4e111d6ac80/yarl-1.24.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd", size = 104272, upload-time = "2026-05-19T21:30:12.978Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f7/8cffdf319aee7a7c1dbd07b61d91c3e3fda460c7a93b5f93e445f3806c4c/yarl-1.24.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67", size = 99962, upload-time = "2026-05-19T21:30:15.001Z" }, + { url = "https://files.pythonhosted.org/packages/d7/39/b3cce3b7dbef64ac700ad4cea156a207d01bede0f507587616c364b5468e/yarl-1.24.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1", size = 111063, upload-time = "2026-05-19T21:30:16.683Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ea/100818505e7ebf165c7242ff17fdf7d9fee79e27234aeca871c1082920d7/yarl-1.24.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1", size = 105438, upload-time = "2026-05-19T21:30:18.769Z" }, + { url = "https://files.pythonhosted.org/packages/8f/d2/e075a0b32aa6625087de9e653087df0759fed5de4a435fef594181102a77/yarl-1.24.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b", size = 111458, upload-time = "2026-05-19T21:30:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5c/ceea7ba98b65c8eb8d947fdc52f9bedfcd43c6a57c9e3c90c17be8f324a3/yarl-1.24.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8", size = 107589, upload-time = "2026-05-19T21:30:23.412Z" }, + { url = "https://files.pythonhosted.org/packages/fa/d9/5582d57e2b2db9b85eb6663a22efdd78e08805f3f5389566e9fcad254d1b/yarl-1.24.2-cp314-cp314-win_amd64.whl", hash = "sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0", size = 94424, upload-time = "2026-05-19T21:30:25.425Z" }, + { url = "https://files.pythonhosted.org/packages/92/10/7dc07a0e22806a9280f42a57361395506e800c64e22737cd7b0886feab42/yarl-1.24.2-cp314-cp314-win_arm64.whl", hash = "sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57", size = 88690, upload-time = "2026-05-19T21:30:27.623Z" }, + { url = "https://files.pythonhosted.org/packages/9e/13/d5b8e2c8667db955bcb3de233f18798fefe7edf1d7429c2c9d4f9c401114/yarl-1.24.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b", size = 136248, upload-time = "2026-05-19T21:30:29.297Z" }, + { url = "https://files.pythonhosted.org/packages/de/46/a4a97c05c9c9b8fd266bb2a0df12992c7fbd02391eb9640583411b6dab32/yarl-1.24.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761", size = 95084, upload-time = "2026-05-19T21:30:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/95/b2/845cf2074a015e6fe0d0808cf1a2d9e868386c4220d657ebd8302b199043/yarl-1.24.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8", size = 95272, upload-time = "2026-05-19T21:30:33.062Z" }, + { url = "https://files.pythonhosted.org/packages/fe/16/e69d4aa244aef45235ddfebc0e04036a6829842bc5a6a795aedc6c998d23/yarl-1.24.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed", size = 101497, upload-time = "2026-05-19T21:30:34.842Z" }, + { url = "https://files.pythonhosted.org/packages/15/94/c07107715d621076863ee88b3ddf183fa5e9d4aba5769623c9979828410a/yarl-1.24.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543", size = 94002, upload-time = "2026-05-19T21:30:37.724Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/fc1bbdd895b5e4010b8fdd037f7ed3aa289d3863e08231b30231ca9a0815/yarl-1.24.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0", size = 106524, upload-time = "2026-05-19T21:30:40.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/32b66d0a4ba47c296cf86d03e2c67bff58399fe6d6d84d5205c04c66cc6d/yarl-1.24.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024", size = 106165, upload-time = "2026-05-19T21:30:41.888Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/37cb5ff50c5e825d4d38e81bb04d1b7e96bf960f7ab89f9850b162f3f114/yarl-1.24.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf", size = 103010, upload-time = "2026-05-19T21:30:43.985Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d2/4597912315096f7bb359e46e13bf8b60994fcbb2db29b804c0902ef4eff5/yarl-1.24.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc", size = 101128, upload-time = "2026-05-19T21:30:46.291Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d5/c8e86e120521e646013d02a8e3b8884392e28494be8f392366e50d208efc/yarl-1.24.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb", size = 101382, upload-time = "2026-05-19T21:30:48.085Z" }, + { url = "https://files.pythonhosted.org/packages/fa/98/70b229236118f89dbeb739b76f10225bbf53b5497725502594c9a01d699a/yarl-1.24.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420", size = 95964, upload-time = "2026-05-19T21:30:49.785Z" }, + { url = "https://files.pythonhosted.org/packages/87/f8/56c386981e3c8648d279fdef2397ffec577e8320fd5649745e34d54faeb7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f", size = 106204, upload-time = "2026-05-19T21:30:51.862Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1e/765afe97811ca35933e2a7de70ac57b1997ea2e4ee895719ee7a231fb7e5/yarl-1.24.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa", size = 101510, upload-time = "2026-05-19T21:30:53.62Z" }, + { url = "https://files.pythonhosted.org/packages/ee/78/393913f4b9039e1edd09ae8a9bbb9d539be909a8abf6d8a2084585bed4b7/yarl-1.24.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe", size = 105584, upload-time = "2026-05-19T21:30:55.962Z" }, + { url = "https://files.pythonhosted.org/packages/78/87/deb17b7049bbe74ea11a713b86f8f27800cc1c8648b0b797243ebb4830ba/yarl-1.24.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd", size = 103410, upload-time = "2026-05-19T21:30:57.962Z" }, + { url = "https://files.pythonhosted.org/packages/8f/be/f9f7594e23b5b93affff0318e4593c1920331bcaefda326cabcad94296a1/yarl-1.24.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215", size = 102980, upload-time = "2026-05-19T21:30:59.735Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/ba80dccd3593ff1f01051a818694d07b58cb8232677ee9a22a5a1f93a9fc/yarl-1.24.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d", size = 91219, upload-time = "2026-05-19T21:31:01.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/4b880086bd0d3e034d25647be1d830afc3e3f610e98c4ab3490af6b1b6d5/yarl-1.24.2-py3-none-any.whl", hash = "sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9", size = 53576, upload-time = "2026-05-19T21:31:03.909Z" }, +] diff --git a/app/src/components/navigation/sidebar.tsx b/app/src/components/navigation/sidebar.tsx index 0cf14854..f9f1430e 100644 --- a/app/src/components/navigation/sidebar.tsx +++ b/app/src/components/navigation/sidebar.tsx @@ -7,7 +7,6 @@ import { BookOpen, Cloud, Cpu, - Database, FlaskConical, HardDrive, KeyRound, @@ -68,7 +67,6 @@ const NAV_GROUPS: readonly NavGroup[] = [ items: [ { admin: true, href: "/backups", icon: Archive, label: "Backups" }, { admin: true, href: "/data-storage", icon: HardDrive, label: "Data Storage" }, - { admin: true, href: "/vector-storage", icon: Database, label: "Vector Storage" }, { admin: true, href: "/settings", icon: Settings, label: "Settings" }, ], }, diff --git a/app/src/features/collections/collection-form-state.ts b/app/src/features/collections/collection-form-state.ts index 7ab2e28a..6cdb5d66 100644 --- a/app/src/features/collections/collection-form-state.ts +++ b/app/src/features/collections/collection-form-state.ts @@ -10,7 +10,6 @@ export type CreateCollectionFormValues = { presetId: string; tenantGuardEnabled: boolean; tenantField: string; - vectorStoreProvider: "qdrant" | "turbopuffer"; }; export type CollectionSearchMode = "semantic" | "keyword" | "hybrid"; @@ -34,7 +33,6 @@ export const defaultCreateCollectionFormValues = (): CreateCollectionFormValues presetId: "", tenantGuardEnabled: false, tenantField: "", - vectorStoreProvider: "qdrant", }); export const defaultCollectionSearchFormValues = (): CollectionSearchFormValues => ({ @@ -118,7 +116,6 @@ export const createCollectionBodyFromValues = ({ presetId, tenantGuardEnabled, tenantField, - vectorStoreProvider, }: CreateCollectionFormValues) => ({ chunk_overlap: chunkOverlap, chunk_size: chunkSize, @@ -129,7 +126,6 @@ export const createCollectionBodyFromValues = ({ multimodal_enrichment_enabled: multimodalEnrichmentEnabled, name: normalizeCollectionName(name), tenant_field: tenantGuardEnabled ? tenantField.trim() || null : null, - vector_store_provider: vectorStoreProvider, }); export const collectionSearchBodyFromValues = ({ diff --git a/app/src/features/collections/create-collection-modal.tsx b/app/src/features/collections/create-collection-modal.tsx index 1d46fe0d..dbfa9233 100644 --- a/app/src/features/collections/create-collection-modal.tsx +++ b/app/src/features/collections/create-collection-modal.tsx @@ -147,18 +147,6 @@ export const CreateCollectionModal = ({ open, onClose }: Props) => { /> )} - - {(field) => ( - patchDraft({ apiKey: event.target.value })} + placeholder={complete ? "Saved" : "tpuf_..."} + type="password" + value={draft.apiKey} + /> + patchDraft({ region: event.target.value })} + placeholder="aws-us-east-1" + value={draft.region} + /> + patchDraft({ namespacePrefix: event.target.value })} + placeholder="bigrag_" + value={draft.namespacePrefix} + /> + patchDraft({ baseUrl: event.target.value })} + placeholder="https://api.turbopuffer.com" + value={draft.baseUrl} + /> + +
+
+ {complete + ? "Turbopuffer is configured for this instance." + : skipped + ? "Skipped for now. System health will keep reporting vector readiness." + : "Save a working vector store now, or skip and configure it later."} +
+
+ {!complete && ( + + )} + +
+
+ + ); +}; diff --git a/app/src/features/overview/overview-page.tsx b/app/src/features/overview/overview-page.tsx index 369799cd..493cedaa 100644 --- a/app/src/features/overview/overview-page.tsx +++ b/app/src/features/overview/overview-page.tsx @@ -70,12 +70,7 @@ export const OverviewPage = () => { const services = useMemo( () => [ { label: "Postgres", ok: readiness?.postgres }, - { - label: readiness?.vector_store_provider - ? `Vector store (${readiness.vector_store_provider.replace("_", " ")})` - : "Vector store", - ok: readiness?.vector_store, - }, + { label: "Vector store", ok: readiness?.vector_store }, { label: "Redis", ok: readiness?.redis }, { detail: readiness?.embedding_error, label: "Embeddings", ok: readiness?.embedding }, { diff --git a/app/src/features/settings/instance-settings-helpers.ts b/app/src/features/settings/instance-settings-helpers.ts index b3da822c..db431005 100644 --- a/app/src/features/settings/instance-settings-helpers.ts +++ b/app/src/features/settings/instance-settings-helpers.ts @@ -13,7 +13,6 @@ const examplePlaceholders: Record = { chat_base_url: "https://api.openai.com/v1", embedding_api_key: "Paste API key", embedding_base_url: "https://api.openai.com/v1", - qdrant_search_ef: "Optional, e.g. 128", storage_s3_access_key_id: "Access key ID", storage_s3_bucket: "bigrag-documents", storage_s3_endpoint_url: "https://account-id.r2.cloudflarestorage.com", diff --git a/app/src/features/settings/settings-layout.ts b/app/src/features/settings/settings-layout.ts index f20ce364..a806a65a 100644 --- a/app/src/features/settings/settings-layout.ts +++ b/app/src/features/settings/settings-layout.ts @@ -141,20 +141,12 @@ const SETTINGS_GROUP_LAYOUTS: Record title: "File storage", }, vector_store: { - commonKeys: [ - "qdrant_url", - "qdrant_connect_timeout_seconds", - "qdrant_required", - "qdrant_search_ef", - "turbopuffer_api_key", - "turbopuffer_region", - "turbopuffer_namespace_prefix", - ], - description: "Vector backend connection details and provider credentials.", + commonKeys: ["turbopuffer_api_key", "turbopuffer_region", "turbopuffer_namespace_prefix"], + description: "Turbopuffer connection details and provider credentials.", emptyState: "Vector storage settings are not available from this API.", eyebrow: "Indexes", group: "vector_store", - recommendedAction: "Keep connection details ready for the providers collections can select.", + recommendedAction: "Keep the API key, region, and namespace prefix aligned with deployment.", title: "Vector storage", }, webhooks: { diff --git a/app/src/features/settings/tabs/server-tab.tsx b/app/src/features/settings/tabs/server-tab.tsx index 814e9be7..234df441 100644 --- a/app/src/features/settings/tabs/server-tab.tsx +++ b/app/src/features/settings/tabs/server-tab.tsx @@ -114,11 +114,7 @@ export const ServerTab = () => { diff --git a/app/src/features/vector-storage/vector-migration-panel.tsx b/app/src/features/vector-storage/vector-migration-panel.tsx deleted file mode 100644 index c73e5f19..00000000 --- a/app/src/features/vector-storage/vector-migration-panel.tsx +++ /dev/null @@ -1,370 +0,0 @@ -import { useQueryClient } from "@tanstack/react-query"; -import { ArrowRightLeft, CircleStop, Cloud, Database, Trash2 } from "lucide-react"; -import { useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { ConfirmDialog } from "@/components/ui/confirm-dialog"; -import { Empty } from "@/components/ui/empty"; -import { Modal } from "@/components/ui/modal"; -import { - useDeleteVectorMigration, - useStartVectorMigration, - useVectorMigrations, - useVectorStorageOverview, -} from "@/hooks/use-vector-migrations"; -import { formatNumber, formatRelative } from "@/lib/format"; -import { queryKeys } from "@/lib/query-keys"; -import type { Collection, VectorMigrationJob, VectorMigrationProvider } from "@/types/bigrag"; - -type VectorMigrationPanelProps = { - readonly collection?: Collection; -}; - -type MigrationTarget = { - readonly collection: string; - readonly source: VectorMigrationProvider; - readonly target: VectorMigrationProvider; -}; - -const PROVIDER_META: Record = { - qdrant: { label: "Qdrant", icon: Database }, - turbopuffer: { label: "turbopuffer", icon: Cloud }, -}; - -const targetProvider = (provider: VectorMigrationProvider): VectorMigrationProvider => - provider === "qdrant" ? "turbopuffer" : "qdrant"; - -const providerLabel = (provider: VectorMigrationProvider) => PROVIDER_META[provider].label; - -const activeStatuses = new Set(["pending", "running", "canceling"]); -const migrationDescription = - "Move a collection between vector providers. Writes are paused during the job, then old source vectors are deleted after cutover."; - -export const VectorMigrationPanel = ({ collection }: VectorMigrationPanelProps) => { - return ( - - - - - Vector migration - - {migrationDescription} - - - - - - ); -}; - -export const VectorMigrationModal = ({ - collection, - onClose, - open, -}: VectorMigrationPanelProps & { - readonly onClose: () => void; - readonly open: boolean; -}) => ( - -
-

{migrationDescription}

- -
-
-); - -const VectorMigrationContent = ({ collection }: VectorMigrationPanelProps) => { - const queryClient = useQueryClient(); - const overview = useVectorStorageOverview(); - const migrations = useVectorMigrations({ collection: collection?.name }); - const startMigration = useStartVectorMigration(); - const deleteMigration = useDeleteVectorMigration(); - const [target, setTarget] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); - - const configuredProviders = overview.data?.configured_providers ?? []; - const rows = useMemo(() => { - if (collection) { - return [ - { - name: collection.name, - provider: collection.vector_store_provider, - documents: collection.document_count, - chunks: null, - }, - ]; - } - return ( - overview.data?.collections.map((item) => ({ - name: item.name, - provider: item.provider, - documents: item.documents, - chunks: item.chunks, - })) ?? [] - ); - }, [collection, overview.data?.collections]); - const jobs = migrations.data?.jobs ?? []; - const activeJob = jobs.find((job) => activeStatuses.has(job.status)); - const completionKey = jobs - .filter((job) => !activeStatuses.has(job.status)) - .map((job) => `${job.id}:${job.status}:${job.updated_at}`) - .join("|"); - - useEffect(() => { - if (!completionKey) return; - queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() }); - queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() }); - if (collection) { - queryClient.invalidateQueries({ - queryKey: queryKeys.collections.one({ name: collection.name }), - }); - } - }, [collection, completionKey, queryClient]); - - return ( - <> -
- {rows.length ? ( -
-
- Collection - Current - Action -
-
- {rows.map((row) => { - const nextProvider = targetProvider(row.provider); - const ready = configuredProviders.includes(nextProvider); - return ( -
-
-
{row.name}
-
- {formatNumber(row.documents)} documents - {row.chunks === null ? "" : ` · ${formatNumber(row.chunks)} chunks`} -
-
- - -
- ); - })} -
-
- ) : ( - } - title="No collections" - description="Create a collection before starting a vector migration." - bordered={false} - className="rounded-md border border-dashed border-border bg-muted/40" - /> - )} - -
- setTarget(null)} - title={target ? `Migrate ${target.collection}?` : "Migrate collection?"} - description={ - target - ? `bigRAG will pause writes, copy vectors from ${providerLabel(target.source)} to ${providerLabel(target.target)}, switch the collection, and delete the old ${providerLabel( - target.source, - )} vectors.` - : "" - } - confirmLabel="Start migration" - confirmationLabel={target ? `Type ${target.collection} to start migration` : undefined} - confirmationText={target?.collection} - loading={startMigration.isPending} - onConfirm={async () => { - if (!target) return; - await startMigration.mutateAsync({ - collection: target.collection, - target_provider: target.target, - }); - setTarget(null); - }} - /> - setDeleteTarget(null)} - title={deleteTarget ? deleteTitle(deleteTarget) : "Delete migration?"} - description={deleteTarget ? deleteDescription(deleteTarget) : ""} - confirmLabel={ - deleteTarget && activeStatuses.has(deleteTarget.status) ? "Stop and delete" : "Delete" - } - confirmationLabel={ - deleteTarget - ? `Type ${deleteTarget.collection_name} to ${activeStatuses.has(deleteTarget.status) ? "stop and delete" : "delete"} migration` - : undefined - } - confirmationText={deleteTarget?.collection_name} - loading={deleteMigration.isPending} - onConfirm={async () => { - if (!deleteTarget) return; - try { - await deleteMigration.mutateAsync(deleteTarget); - setDeleteTarget(null); - } catch (err) { - toast.error(err instanceof Error ? err.message : "Failed"); - } - }} - /> - - ); -}; - -const ProviderBadge = ({ provider }: { readonly provider: VectorMigrationProvider }) => { - const Icon = PROVIDER_META[provider].icon; - return ( - - - {providerLabel(provider)} - - ); -}; - -const MigrationJobs = ({ - deleting, - jobs, - onDelete, -}: { - readonly deleting: boolean; - readonly jobs: VectorMigrationJob[]; - readonly onDelete: (job: VectorMigrationJob) => void; -}) => { - if (!jobs.length) { - return ( - } - title="No migration jobs" - description="Completed and failed migrations appear here." - bordered={false} - className="rounded-md border border-dashed border-border bg-muted/40" - /> - ); - } - return ( -
-
- Migration - Copied - Status - Action -
-
- {jobs.map((job) => ( - - ))} -
-
- ); -}; - -const MigrationJobRow = ({ - deleting, - job, - onDelete, -}: { - readonly deleting: boolean; - readonly job: VectorMigrationJob; - readonly onDelete: (job: VectorMigrationJob) => void; -}) => ( -
-
-
-
{job.collection_name}
- {formatRelative(job.created_at)} -
-
- {providerLabel(job.source_provider)} to {providerLabel(job.target_provider)} · {job.phase} -
- {job.error_message && ( -
{job.error_message}
- )} - {activeStatuses.has(job.status) && ( -
-
-
- )} -
-
- {formatNumber(job.copied_points)} - {job.total_points === null ? "" : ` / ${formatNumber(job.total_points)}`} -
- - {job.status} - - -
-); - -const statusVariant = (status: VectorMigrationJob["status"]) => { - if (status === "succeeded") return "success"; - if (status === "failed") return "error"; - if (status === "running") return "primary"; - if (status === "canceling") return "error"; - return "neutral"; -}; - -const deleteTitle = (job: VectorMigrationJob) => - activeStatuses.has(job.status) ? "Stop and delete migration?" : "Delete migration?"; - -const deleteDescription = (job: VectorMigrationJob) => - activeStatuses.has(job.status) - ? `Stop the ${providerLabel(job.source_provider)} to ${providerLabel(job.target_provider)} migration for ${job.collection_name} and remove it from the migration list. If cutover already started, bigRAG will finish cleanup before removing it.` - : `Delete the ${providerLabel(job.source_provider)} to ${providerLabel(job.target_provider)} migration record for ${job.collection_name}.`; diff --git a/app/src/features/vector-storage/vector-storage-page.tsx b/app/src/features/vector-storage/vector-storage-page.tsx deleted file mode 100644 index 50a52155..00000000 --- a/app/src/features/vector-storage/vector-storage-page.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { Cloud, Database } from "lucide-react"; -import { Page } from "@/components/ui/page"; -import { Tabs } from "@/components/ui/tabs"; -import { InstanceSettingsTab } from "@/features/settings/tabs/instance-settings-tab"; -import { VectorMigrationPanel } from "@/features/vector-storage/vector-migration-panel"; - -export type VectorStorageProvider = "qdrant" | "turbopuffer"; - -type VectorStoragePageProps = { - readonly provider?: string; - readonly onProviderChange?: (provider: VectorStorageProvider) => void; -}; - -const VECTOR_STORAGE_TABS = [ - { icon: Database, label: "Qdrant", value: "qdrant" }, - { icon: Cloud, label: "turbopuffer", value: "turbopuffer" }, -]; - -const VECTOR_PROVIDER_SETTINGS: Record< - VectorStorageProvider, - { - readonly description: string; - readonly emptyState: string; - readonly eyebrow: string; - readonly keys: readonly string[]; - readonly recommendedAction: string; - readonly title: string; - } -> = { - qdrant: { - description: "Qdrant connection, readiness policy, and HNSW search tuning.", - emptyState: "Qdrant settings are not available from this API.", - eyebrow: "Self-hosted index", - keys: ["qdrant_url", "qdrant_connect_timeout_seconds", "qdrant_required", "qdrant_search_ef"], - recommendedAction: "Save Qdrant when the URL and readiness policy match this deployment.", - title: "Qdrant", - }, - turbopuffer: { - description: "turbopuffer credentials, region, and namespace routing.", - emptyState: "turbopuffer settings are not available from this API.", - eyebrow: "Managed index", - keys: ["turbopuffer_api_key", "turbopuffer_region", "turbopuffer_namespace_prefix"], - recommendedAction: "Save turbopuffer after the API key, region, and namespace prefix are set.", - title: "turbopuffer", - }, -}; - -const getVectorStorageProvider = (value: unknown): VectorStorageProvider | undefined => - value === "qdrant" || value === "turbopuffer" ? value : undefined; - -export const VectorStoragePage = ({ provider, onProviderChange }: VectorStoragePageProps) => { - const activeProvider = getVectorStorageProvider(provider) ?? "qdrant"; - const settings = VECTOR_PROVIDER_SETTINGS[activeProvider]; - - return ( - - - { - const nextProvider = getVectorStorageProvider(value); - if (nextProvider) onProviderChange?.(nextProvider); - }} - tabs={VECTOR_STORAGE_TABS} - value={activeProvider} - /> - - - - ); -}; diff --git a/app/src/features/vector-storage/vector-storage-route.tsx b/app/src/features/vector-storage/vector-storage-route.tsx deleted file mode 100644 index 9d9eb9a0..00000000 --- a/app/src/features/vector-storage/vector-storage-route.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { getRouteApi, useNavigate } from "@tanstack/react-router"; -import { - VectorStoragePage, - type VectorStorageProvider, -} from "@/features/vector-storage/vector-storage-page"; - -const routeApi = getRouteApi("/_dashboard/vector-storage"); - -export const VectorStorageRoute = () => { - const navigate = useNavigate(); - const search = routeApi.useSearch(); - - const setProvider = (provider: VectorStorageProvider) => - navigate({ - to: "/vector-storage", - search: { provider }, - replace: true, - }); - - return ; -}; diff --git a/app/src/hooks/use-collections.ts b/app/src/hooks/use-collections.ts index f05d4694..e884937a 100644 --- a/app/src/hooks/use-collections.ts +++ b/app/src/hooks/use-collections.ts @@ -49,7 +49,6 @@ export const useCollectionStats = (name: string) => { type CreateCollectionBody = { name: string; description?: string; - vector_store_provider?: "qdrant" | "turbopuffer"; embedding_preset_id?: string | null; embedding_provider?: "openai" | "openai_compatible" | "cohere" | "voyage"; embedding_model?: string; diff --git a/app/src/hooks/use-vector-migrations.ts b/app/src/hooks/use-vector-migrations.ts deleted file mode 100644 index 21691e36..00000000 --- a/app/src/hooks/use-vector-migrations.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useMemo } from "react"; -import { toast } from "sonner"; -import { useSseSnapshotQuery } from "@/hooks/use-sse-snapshot-query"; -import { apiClient } from "@/lib/api"; -import { errorToast } from "@/lib/mutation-toast"; -import { queryKeys } from "@/lib/query-keys"; -import type { - VectorMigrationJob, - VectorMigrationJobListResponse, - VectorMigrationProvider, -} from "@/types/bigrag"; - -type VectorMigrationListOptions = { - readonly collection?: string; -}; - -type StatusResponse = { - readonly status: string; - readonly message?: string; -}; - -type VectorStorageOverview = { - fallback_provider: string; - configured_providers: VectorMigrationProvider[]; - provider_health: Record; - collections: { - name: string; - provider: VectorMigrationProvider; - documents: number; - chunks: number; - bytes: number; - }[]; - totals: { - collections: number; - documents: number; - chunks: number; - bytes: number; - }; -}; - -export const useVectorStorageOverview = () => - useQuery({ - queryKey: queryKeys.vectorStorageOverview(), - queryFn: () => apiClient.get("v1/admin/vector-storage/overview"), - staleTime: 15_000, - }); - -export const useVectorMigrations = ({ collection }: VectorMigrationListOptions = {}) => { - const queryKey = useMemo(() => queryKeys.vectorMigrations({ collection }), [collection]); - const searchParams = collection ? { collection } : undefined; - const path = collection - ? `v1/admin/realtime/vector-migrations?collection=${encodeURIComponent(collection)}` - : "v1/admin/realtime/vector-migrations"; - - return useSseSnapshotQuery({ - queryKey, - queryFn: () => - apiClient.get( - "v1/admin/vector-storage/migrations", - searchParams, - ), - path, - }); -}; - -export const useStartVectorMigration = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (body: { collection: string; target_provider: VectorMigrationProvider }) => - apiClient.post("v1/admin/vector-storage/migrations", body), - onSuccess: (job) => { - queryClient.invalidateQueries({ queryKey: queryKeys.vectorMigrations({}) }); - queryClient.invalidateQueries({ - queryKey: queryKeys.vectorMigrations({ collection: job.collection_name }), - }); - queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() }); - queryClient.invalidateQueries({ - queryKey: queryKeys.collections.one({ name: job.collection_name }), - }); - queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() }); - toast.success("Vector migration started"); - }, - onError: errorToast("Failed to start migration"), - }); -}; - -export const useDeleteVectorMigration = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: (job: VectorMigrationJob) => - apiClient.delete( - `v1/admin/vector-storage/migrations/${encodeURIComponent(job.id)}`, - ), - onSuccess: (response, job) => { - queryClient.invalidateQueries({ queryKey: queryKeys.vectorMigrations({}) }); - queryClient.invalidateQueries({ - queryKey: queryKeys.vectorMigrations({ collection: job.collection_name }), - }); - queryClient.invalidateQueries({ queryKey: queryKeys.collections.all() }); - queryClient.invalidateQueries({ - queryKey: queryKeys.collections.one({ name: job.collection_name }), - }); - queryClient.invalidateQueries({ queryKey: queryKeys.vectorStorageOverview() }); - toast.success(response.message ?? "Vector migration deleted"); - }, - onError: errorToast("Failed to delete migration"), - }); -}; diff --git a/app/src/lib/query-keys.ts b/app/src/lib/query-keys.ts index 0c4f39f4..5f17e48c 100644 --- a/app/src/lib/query-keys.ts +++ b/app/src/lib/query-keys.ts @@ -52,10 +52,6 @@ type AuditListParams = { readonly resourceType?: string; }; -type VectorMigrationsParams = { - readonly collection?: string; -}; - export const queryKeys = { auth: { all: () => ["auth"] as const, @@ -64,9 +60,6 @@ export const queryKeys = { }, apiKeys: () => ["api-keys"] as const, backups: () => ["backups"] as const, - vectorStorageOverview: () => ["vector-storage", "overview"] as const, - vectorMigrations: ({ collection }: VectorMigrationsParams = {}) => - ["vector-migrations", { collection: collection ?? "all" }] as const, access: { logs: (filters: Record) => ["access", "logs", filters] as const, overview: ({ windowDays }: WindowDaysParams) => ["access", "overview", { windowDays }] as const, diff --git a/app/src/routeTree.gen.ts b/app/src/routeTree.gen.ts index 4dc40d53..d755d516 100644 --- a/app/src/routeTree.gen.ts +++ b/app/src/routeTree.gen.ts @@ -9,10 +9,10 @@ import { Route as DashboardRouteImport } from "./routes/_dashboard"; import { Route as AuthRouteImport } from "./routes/_auth"; import { Route as IndexRouteImport } from "./routes/index"; import { Route as DashboardWebhooksRouteImport } from "./routes/_dashboard.webhooks"; -import { Route as DashboardVectorStorageRouteImport } from "./routes/_dashboard.vector-storage"; import { Route as DashboardUsageRouteImport } from "./routes/_dashboard.usage"; import { Route as DashboardSettingsRouteImport } from "./routes/_dashboard.settings"; import { Route as DashboardOverviewRouteImport } from "./routes/_dashboard.overview"; +import { Route as DashboardOnboardingRouteImport } from "./routes/_dashboard.onboarding"; import { Route as DashboardModelsRouteImport } from "./routes/_dashboard.models"; import { Route as DashboardMcpRouteImport } from "./routes/_dashboard.mcp"; import { Route as DashboardEvalsRouteImport } from "./routes/_dashboard.evals"; @@ -54,11 +54,6 @@ const DashboardWebhooksRoute = DashboardWebhooksRouteImport.update({ path: "/webhooks", getParentRoute: () => DashboardRoute, } as any); -const DashboardVectorStorageRoute = DashboardVectorStorageRouteImport.update({ - id: "/vector-storage", - path: "/vector-storage", - getParentRoute: () => DashboardRoute, -} as any); const DashboardUsageRoute = DashboardUsageRouteImport.update({ id: "/usage", path: "/usage", @@ -74,6 +69,11 @@ const DashboardOverviewRoute = DashboardOverviewRouteImport.update({ path: "/overview", getParentRoute: () => DashboardRoute, } as any); +const DashboardOnboardingRoute = DashboardOnboardingRouteImport.update({ + id: "/onboarding", + path: "/onboarding", + getParentRoute: () => DashboardRoute, +} as any); const DashboardModelsRoute = DashboardModelsRouteImport.update({ id: "/models", path: "/models", @@ -209,10 +209,10 @@ export interface FileRoutesByFullPath { "/evals": typeof DashboardEvalsRoute; "/mcp": typeof DashboardMcpRoute; "/models": typeof DashboardModelsRoute; + "/onboarding": typeof DashboardOnboardingRoute; "/overview": typeof DashboardOverviewRoute; "/settings": typeof DashboardSettingsRoute; "/usage": typeof DashboardUsageRoute; - "/vector-storage": typeof DashboardVectorStorageRoute; "/webhooks": typeof DashboardWebhooksRoute; "/collections/$name": typeof DashboardCollectionsNameRouteWithChildren; "/collections/": typeof DashboardCollectionsIndexRoute; @@ -239,10 +239,10 @@ export interface FileRoutesByTo { "/evals": typeof DashboardEvalsRoute; "/mcp": typeof DashboardMcpRoute; "/models": typeof DashboardModelsRoute; + "/onboarding": typeof DashboardOnboardingRoute; "/overview": typeof DashboardOverviewRoute; "/settings": typeof DashboardSettingsRoute; "/usage": typeof DashboardUsageRoute; - "/vector-storage": typeof DashboardVectorStorageRoute; "/webhooks": typeof DashboardWebhooksRoute; "/collections": typeof DashboardCollectionsIndexRoute; "/collections/$name/search": typeof DashboardCollectionsNameSearchRoute; @@ -270,10 +270,10 @@ export interface FileRoutesById { "/_dashboard/evals": typeof DashboardEvalsRoute; "/_dashboard/mcp": typeof DashboardMcpRoute; "/_dashboard/models": typeof DashboardModelsRoute; + "/_dashboard/onboarding": typeof DashboardOnboardingRoute; "/_dashboard/overview": typeof DashboardOverviewRoute; "/_dashboard/settings": typeof DashboardSettingsRoute; "/_dashboard/usage": typeof DashboardUsageRoute; - "/_dashboard/vector-storage": typeof DashboardVectorStorageRoute; "/_dashboard/webhooks": typeof DashboardWebhooksRoute; "/_dashboard/collections/$name": typeof DashboardCollectionsNameRouteWithChildren; "/_dashboard/collections/": typeof DashboardCollectionsIndexRoute; @@ -302,10 +302,10 @@ export interface FileRouteTypes { | "/evals" | "/mcp" | "/models" + | "/onboarding" | "/overview" | "/settings" | "/usage" - | "/vector-storage" | "/webhooks" | "/collections/$name" | "/collections/" @@ -332,10 +332,10 @@ export interface FileRouteTypes { | "/evals" | "/mcp" | "/models" + | "/onboarding" | "/overview" | "/settings" | "/usage" - | "/vector-storage" | "/webhooks" | "/collections" | "/collections/$name/search" @@ -362,10 +362,10 @@ export interface FileRouteTypes { | "/_dashboard/evals" | "/_dashboard/mcp" | "/_dashboard/models" + | "/_dashboard/onboarding" | "/_dashboard/overview" | "/_dashboard/settings" | "/_dashboard/usage" - | "/_dashboard/vector-storage" | "/_dashboard/webhooks" | "/_dashboard/collections/$name" | "/_dashboard/collections/" @@ -415,13 +415,6 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof DashboardWebhooksRouteImport; parentRoute: typeof DashboardRoute; }; - "/_dashboard/vector-storage": { - id: "/_dashboard/vector-storage"; - path: "/vector-storage"; - fullPath: "/vector-storage"; - preLoaderRoute: typeof DashboardVectorStorageRouteImport; - parentRoute: typeof DashboardRoute; - }; "/_dashboard/usage": { id: "/_dashboard/usage"; path: "/usage"; @@ -443,6 +436,13 @@ declare module "@tanstack/react-router" { preLoaderRoute: typeof DashboardOverviewRouteImport; parentRoute: typeof DashboardRoute; }; + "/_dashboard/onboarding": { + id: "/_dashboard/onboarding"; + path: "/onboarding"; + fullPath: "/onboarding"; + preLoaderRoute: typeof DashboardOnboardingRouteImport; + parentRoute: typeof DashboardRoute; + }; "/_dashboard/models": { id: "/_dashboard/models"; path: "/models"; @@ -669,10 +669,10 @@ interface DashboardRouteChildren { DashboardEvalsRoute: typeof DashboardEvalsRoute; DashboardMcpRoute: typeof DashboardMcpRoute; DashboardModelsRoute: typeof DashboardModelsRoute; + DashboardOnboardingRoute: typeof DashboardOnboardingRoute; DashboardOverviewRoute: typeof DashboardOverviewRoute; DashboardSettingsRoute: typeof DashboardSettingsRoute; DashboardUsageRoute: typeof DashboardUsageRoute; - DashboardVectorStorageRoute: typeof DashboardVectorStorageRoute; DashboardWebhooksRoute: typeof DashboardWebhooksRoute; DashboardCollectionsNameRoute: typeof DashboardCollectionsNameRouteWithChildren; DashboardCollectionsIndexRoute: typeof DashboardCollectionsIndexRoute; @@ -689,10 +689,10 @@ const DashboardRouteChildren: DashboardRouteChildren = { DashboardEvalsRoute: DashboardEvalsRoute, DashboardMcpRoute: DashboardMcpRoute, DashboardModelsRoute: DashboardModelsRoute, + DashboardOnboardingRoute: DashboardOnboardingRoute, DashboardOverviewRoute: DashboardOverviewRoute, DashboardSettingsRoute: DashboardSettingsRoute, DashboardUsageRoute: DashboardUsageRoute, - DashboardVectorStorageRoute: DashboardVectorStorageRoute, DashboardWebhooksRoute: DashboardWebhooksRoute, DashboardCollectionsNameRoute: DashboardCollectionsNameRouteWithChildren, DashboardCollectionsIndexRoute: DashboardCollectionsIndexRoute, diff --git a/app/src/routes/_auth.setup.tsx b/app/src/routes/_auth.setup.tsx index 4c4bbf46..88f38e07 100644 --- a/app/src/routes/_auth.setup.tsx +++ b/app/src/routes/_auth.setup.tsx @@ -30,7 +30,7 @@ const SetupPage = () => { try { await setup.mutateAsync(setupBodyFromValues(value)); toast.success("Admin account created"); - navigate({ to: "/overview", replace: true }); + navigate({ to: "/onboarding", replace: true }); } catch (err) { toast.error(err instanceof Error ? err.message : "Setup failed"); } diff --git a/app/src/routes/_dashboard.collections.$name.index.tsx b/app/src/routes/_dashboard.collections.$name.index.tsx index 764cd11b..72bc04fc 100644 --- a/app/src/routes/_dashboard.collections.$name.index.tsx +++ b/app/src/routes/_dashboard.collections.$name.index.tsx @@ -40,10 +40,6 @@ const CollectionIndex = () => { - diff --git a/app/src/routes/_dashboard.collections.$name.settings.tsx b/app/src/routes/_dashboard.collections.$name.settings.tsx index 95a4965d..7c263cf9 100644 --- a/app/src/routes/_dashboard.collections.$name.settings.tsx +++ b/app/src/routes/_dashboard.collections.$name.settings.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; -import { ArrowRightLeft, KeyRound, Trash2, TriangleAlert } from "lucide-react"; +import { KeyRound, Trash2, TriangleAlert } from "lucide-react"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; @@ -12,7 +12,6 @@ import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; import { decodeCollectionName } from "@/features/collections/use-collection-name"; -import { VectorMigrationModal } from "@/features/vector-storage/vector-migration-panel"; import { useCollection, useDeleteCollection, @@ -57,7 +56,6 @@ const CollectionSettings = () => { const [allowedTypes, setAllowedTypes] = useState>(new Set(ALL_FILE_TYPES)); const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false); const [confirmTruncateOpen, setConfirmTruncateOpen] = useState(false); - const [migrationOpen, setMigrationOpen] = useState(false); useCollectionSettingsDraft(collection, { setAllowedTypes, @@ -378,25 +376,6 @@ const CollectionSettings = () => { - - - - - Vector migration - - - Move this collection to another configured vector provider. Source vectors are deleted - after a successful cutover. - - - - - - - @@ -417,12 +396,6 @@ const CollectionSettings = () => { - setMigrationOpen(false)} - open={migrationOpen} - /> - setConfirmTruncateOpen(false)} diff --git a/app/src/routes/_dashboard.collections.$name.tsx b/app/src/routes/_dashboard.collections.$name.tsx index ed20a519..6e03af3d 100644 --- a/app/src/routes/_dashboard.collections.$name.tsx +++ b/app/src/routes/_dashboard.collections.$name.tsx @@ -51,10 +51,6 @@ const CollectionLayout = () => { collection && (
Model {collection.embedding_model} - - Storage{" "} - {collection.vector_store_provider === "turbopuffer" ? "Turbopuffer" : "Qdrant"} - Dimensions {collection.dimension}d {collection.reranking_enabled && ( rerank: {collection.reranking_model} diff --git a/app/src/routes/_dashboard.collections.index.tsx b/app/src/routes/_dashboard.collections.index.tsx index 1775399b..30940996 100644 --- a/app/src/routes/_dashboard.collections.index.tsx +++ b/app/src/routes/_dashboard.collections.index.tsx @@ -57,20 +57,6 @@ const CollectionsPage = () => { ), }, - { - header: "Provider", - key: "provider", - render: (c) => ( - - {c.embedding_provider} - - ), - }, - { - header: "Storage", - key: "storage", - render: (c) => {storageLabel(c.vector_store_provider)}, - }, { header: "Model", key: "model", @@ -184,6 +170,3 @@ const CollectionsPage = () => { ); }; - -const storageLabel = (provider: Collection["vector_store_provider"]) => - provider === "turbopuffer" ? "Turbopuffer" : "Qdrant"; diff --git a/app/src/routes/_dashboard.onboarding.tsx b/app/src/routes/_dashboard.onboarding.tsx new file mode 100644 index 00000000..cfcb9af1 --- /dev/null +++ b/app/src/routes/_dashboard.onboarding.tsx @@ -0,0 +1,6 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { OnboardingPage } from "@/features/onboarding/onboarding-page"; + +export const Route = createFileRoute("/_dashboard/onboarding")({ + component: () => , +}); diff --git a/app/src/routes/_dashboard.vector-storage.tsx b/app/src/routes/_dashboard.vector-storage.tsx deleted file mode 100644 index 37375356..00000000 --- a/app/src/routes/_dashboard.vector-storage.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { createFileRoute, lazyRouteComponent } from "@tanstack/react-router"; - -type VectorStorageSearch = { - provider?: string; -}; - -export const Route = createFileRoute("/_dashboard/vector-storage")({ - validateSearch: (search: Record): VectorStorageSearch => ({ - provider: typeof search.provider === "string" ? search.provider : undefined, - }), - component: lazyRouteComponent(() => - import("@/features/vector-storage/vector-storage-route").then((m) => ({ - default: m.VectorStorageRoute, - })), - ), -}); diff --git a/app/src/types/bigrag-api/admin.ts b/app/src/types/bigrag-api/admin.ts index 2b431b42..da02029e 100644 --- a/app/src/types/bigrag-api/admin.ts +++ b/app/src/types/bigrag-api/admin.ts @@ -101,10 +101,8 @@ export type ReadinessReport = { version: string; postgres: boolean; postgres_error?: string; - qdrant: boolean | null; vector_store: boolean; vector_store_error?: string; - vector_store_provider: "per_collection"; redis: boolean; redis_error?: string; embedding: boolean; diff --git a/app/src/types/bigrag-api/settings.ts b/app/src/types/bigrag-api/settings.ts index 2ce268f4..adf2737c 100644 --- a/app/src/types/bigrag-api/settings.ts +++ b/app/src/types/bigrag-api/settings.ts @@ -69,31 +69,3 @@ export type BackupJobListResponse = { total: number | null; next_cursor: string | null; }; - -export type VectorMigrationProvider = "qdrant" | "turbopuffer"; - -export type VectorMigrationJob = { - id: string; - collection_id: string | null; - collection_name: string; - source_provider: VectorMigrationProvider; - target_provider: VectorMigrationProvider; - status: "pending" | "running" | "canceling" | "succeeded" | "failed"; - phase: string; - progress: number; - copied_points: number; - total_points: number | null; - details: Record; - error_message: string | null; - created_by: string | null; - started_at: string | null; - completed_at: string | null; - created_at: string; - updated_at: string; -}; - -export type VectorMigrationJobListResponse = { - jobs: VectorMigrationJob[]; - total: number | null; - next_cursor: string | null; -}; diff --git a/bigrag.toml b/bigrag.toml index 249d3a8f..0db76471 100644 --- a/bigrag.toml +++ b/bigrag.toml @@ -21,14 +21,6 @@ # db_pool_max = 50 # migration_timeout_seconds = 60 -# qdrant_url = "http://localhost:6333" -# qdrant_connect_timeout_seconds = 10 -# qdrant_required = false - -# turbopuffer_api_key = "" # required only for collections using turbopuffer -# turbopuffer_region = "aws-us-east-1" -# turbopuffer_namespace_prefix = "bigrag_" - # redis_url = "redis://localhost:6379/0" # Encryption key for provider secrets at rest. Required in env=prod. diff --git a/dev.sh b/dev.sh index 8bac5a9b..9be28a5b 100755 --- a/dev.sh +++ b/dev.sh @@ -122,19 +122,17 @@ if [ "$START_WEBSITE" = true ]; then fi if [ "$START_INFRA" = true ]; then - echo -e "${CYAN}Starting Docker services (Postgres, Redis, Qdrant)...${NC}" - if ! docker compose -f "$ROOT_DIR/docker-compose.yml" ps --status running --quiet postgres redis qdrant | grep -q .; then + echo -e "${CYAN}Starting Docker services (Postgres, Redis)...${NC}" + if ! docker compose -f "$ROOT_DIR/docker-compose.yml" ps --status running --quiet postgres redis | grep -q .; then STARTED_INFRA=true fi - docker compose -f "$ROOT_DIR/docker-compose.yml" up postgres redis qdrant -d + docker compose -f "$ROOT_DIR/docker-compose.yml" up postgres redis -d wait_for "Postgres" "docker exec bigrag-postgres pg_isready -U bigrag" 30 wait_for "Redis" "docker exec bigrag-redis redis-cli ping" 15 - wait_for "Qdrant" "curl -sf http://localhost:6333/healthz" 60 fi DATABASE_URL="postgres://bigrag:bigrag@localhost:5432/bigrag?sslmode=disable" -QDRANT_URL="http://localhost:6333" REDIS_URL="redis://localhost:6379/0" if [ "$START_BACKEND" = true ]; then @@ -148,7 +146,6 @@ if [ "$START_BACKEND" = true ]; then DEV_MASTER_KEY="${BIGRAG_MASTER_KEY:-Zm5VZ4vO8r0y3rVsT0xz7nxV_wP7u6-n5tB1GAlHZIw=}" export BIGRAG_DATABASE_URL="$DATABASE_URL" - export BIGRAG_QDRANT_URL="$QDRANT_URL" export BIGRAG_REDIS_URL="$REDIS_URL" export BIGRAG_MASTER_KEY="$DEV_MASTER_KEY" export BIGRAG_CORS_ORIGINS="${BIGRAG_CORS_ORIGINS:-[\"http://localhost:3000\"]}" @@ -186,7 +183,6 @@ echo -e "\n${GREEN}Services started:${NC}" [ "$START_BACKEND" = true ] && echo -e " API Docs → http://localhost:4000/docs" [ "$START_INFRA" = true ] && echo -e " Postgres → localhost:5432" [ "$START_INFRA" = true ] && echo -e " Redis → localhost:6379" -[ "$START_INFRA" = true ] && echo -e " Qdrant → localhost:6333" [ "$START_WEBSITE" = true ] && echo -e " Website → http://localhost:3100" echo -e "\n${YELLOW}Press Ctrl+C to stop all services.${NC}" diff --git a/docker-compose.yml b/docker-compose.yml index 1cd904a0..ff8801dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,6 @@ x-bigrag-env: &bigrag-env BIGRAG_ENV: "${BIGRAG_ENV:-dev}" BIGRAG_DATABASE_URL: "postgres://${POSTGRES_USER:-bigrag}:${POSTGRES_PASSWORD:-bigrag}@postgres:5432/${POSTGRES_DB:-bigrag}?sslmode=disable" - BIGRAG_QDRANT_URL: http://qdrant:6333 BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}" BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}" BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}' @@ -38,8 +37,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4000/health"] interval: 30s @@ -66,8 +63,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: [ @@ -145,22 +140,7 @@ services: timeout: 5s retries: 5 - qdrant: - image: qdrant/qdrant:v1.17.1 - container_name: bigrag-qdrant - restart: unless-stopped - ports: - - "127.0.0.1:6333:6333" - - "127.0.0.1:6334:6334" - volumes: - - qdrant_data:/qdrant/storage - deploy: - resources: - limits: - memory: 2g - volumes: bigrag_data: postgres_data: redis_data: - qdrant_data: diff --git a/e2e/Makefile b/e2e/Makefile index d43960cb..a86036b5 100644 --- a/e2e/Makefile +++ b/e2e/Makefile @@ -3,21 +3,22 @@ E2E_PROJECT ?= bigrag-e2e COMPOSE = docker compose -p $(E2E_PROJECT) -f ../docker-compose.yml -f docker-compose.e2e.yml API_BASE ?= http://localhost:4000 -E2E_SERVICES = bigrag-api bigrag-worker fake-openai webhook-sink minio +API_LIVENESS ?= $(API_BASE)/health +E2E_SERVICES = bigrag-api bigrag-worker fake-openai fake-turbopuffer webhook-sink minio up: $(COMPOSE) up -d --build $(E2E_SERVICES) $(MAKE) wait-ready wait-ready: - @echo "Waiting for $(API_BASE)/health/ready ..." + @echo "Waiting for $(API_LIVENESS) ..." @for i in $$(seq 1 60); do \ - if curl -fsS $(API_BASE)/health/ready >/dev/null 2>&1; then \ + if curl -fsS $(API_LIVENESS) >/dev/null 2>&1; then \ echo "ready"; exit 0; \ fi; \ sleep 2; \ done; \ - echo "timed out waiting for /health/ready"; exit 1 + echo "timed out waiting for $(API_LIVENESS)"; exit 1 down: $(COMPOSE) down -v @@ -41,13 +42,20 @@ test-sdk-ts: test: test-api test-sdk-py test-sdk-ts e2e: - $(MAKE) up + @if ! $(MAKE) up; then \ + echo "=== bigrag-api logs (last 1000 lines) ==="; \ + $(COMPOSE) logs --tail=1000 bigrag-api || true; \ + echo "=== bigrag-worker logs (last 1000 lines) ==="; \ + $(COMPOSE) logs --tail=1000 bigrag-worker || true; \ + $(COMPOSE) down -v || true; \ + exit 1; \ + fi @if ! $(MAKE) test; then \ echo "=== bigrag-api logs (last 1000 lines) ==="; \ $(COMPOSE) logs --tail=1000 bigrag-api || true; \ echo "=== bigrag-worker logs (last 1000 lines) ==="; \ $(COMPOSE) logs --tail=1000 bigrag-worker || true; \ - $(MAKE) down; \ + $(COMPOSE) down -v || true; \ exit 1; \ fi $(MAKE) down diff --git a/e2e/README.md b/e2e/README.md index 95d5ee52..13a4881c 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -31,7 +31,7 @@ make e2e # up, test, down | Target | What it does | |--------------------|--------------------------------------------------------------------| -| `make up` | Brings up the API/SDK e2e compose stack and waits for `/health/ready` | +| `make up` | Brings up the API/SDK e2e compose stack and waits for `/health` | | `make down` | Tears down the stack and removes volumes | | `make logs` | Tails compose logs | | `make install` | `uv sync` + `pnpm install` | @@ -45,6 +45,11 @@ The Makefile uses the `bigrag-e2e` Docker Compose project. Its `down` target removes e2e volumes only and does not remove the default `bigrag` dev volumes used by `./dev.sh`. +`make up` waits for API liveness on `/health`. The pytest admin fixture then +creates the first admin when needed and seeds e2e runtime settings for +`fake-openai`, `fake-turbopuffer`, and `webhook-sink` before tests that need +configured providers run. + ## Architecture ``` @@ -54,15 +59,15 @@ used by `./dev.sh`. | host:4000 v host:9001/9002/9003 +-------------+--------+ +-----------------------------+ - | bigrag-api | | fake-openai | fake-gdrive | | + | bigrag-api | | fake-openai | fake-turbo | | | bigrag-worker | | webhook-sink | +--+---------+---------+ +-----------------------------+ | | ^ ^ ^ v v | | | - postgres redis qdrant +------------+---------+----+ + postgres redis turbopuffer +--------+---------+----+ (bigrag-api routes embeddings, chat, - Google Drive connector, and webhook - deliveries at these local fakes) + vectors, and webhook deliveries at + these local fakes) ``` The fakes live in `stubs/`: @@ -70,8 +75,8 @@ The fakes live in `stubs/`: - `fake-openai` — OpenAI-compatible `/v1/embeddings`, `/v1/chat/completions` (non-stream + SSE), `/v1/models`. Deterministic embeddings via sha256-seeded numpy RNG; canned chat responses. -- `fake-gdrive` — Mock OAuth (`/o/oauth2/*`) + Drive (`/drive/v3/*`) for the - Google Drive connector tests. +- `fake-turbopuffer` — Turbopuffer-compatible namespace writes, vector query, + fake BM25 keyword query, and hybrid-friendly rows. - `webhook-sink` — Records every incoming webhook delivery; tests poll `/received?label=...` to assert. @@ -113,8 +118,8 @@ All Python suites share fixtures from `e2e/conftest.py`: See `e2e/conftest.py` and `e2e/tests/_helpers.py` for the full contract. -Collection names are short — `e2e_<8 hex>` — so they fit Qdrant's collection -name limit. Each fixture cleans up in teardown so the suite is safe under +Collection names are short — `e2e_<8 hex>` — so generated namespace names stay +compact. Each fixture cleans up in teardown so the suite is safe under `pytest-xdist -n auto`. ## Coverage diff --git a/e2e/conftest.py b/e2e/conftest.py index c10435e3..84d25213 100644 --- a/e2e/conftest.py +++ b/e2e/conftest.py @@ -35,9 +35,11 @@ API_BASE = os.environ.get("BIGRAG_E2E_API_BASE", "http://localhost:4000") FAKE_OPENAI_BASE = os.environ.get("BIGRAG_E2E_FAKE_OPENAI", "http://localhost:9001") +FAKE_TURBOPUFFER_BASE = os.environ.get("BIGRAG_E2E_FAKE_TURBOPUFFER", "http://localhost:9002") WEBHOOK_SINK_BASE = os.environ.get("BIGRAG_E2E_WEBHOOK_SINK", "http://localhost:9003") FAKE_OPENAI_INTERNAL_BASE = "http://fake-openai:9001" +FAKE_TURBOPUFFER_INTERNAL_BASE = "http://fake-turbopuffer:9002" WEBHOOK_SINK_INTERNAL_BASE = "http://webhook-sink:9003" ADMIN_EMAIL = "e2e-admin@example.com" @@ -64,6 +66,11 @@ def fake_openai_base() -> str: return FAKE_OPENAI_INTERNAL_BASE +@pytest.fixture(scope="session") +def fake_turbopuffer_base() -> str: + return FAKE_TURBOPUFFER_INTERNAL_BASE + + # --------------------------------------------------------------------------- # Unauthenticated client (function-scoped) # --------------------------------------------------------------------------- @@ -137,15 +144,26 @@ async def _bootstrap_e2e_runtime_settings() -> None: ) as client: for k, v in cookies.items(): client.cookies.set(k, v) + settings_resp = await client.get("/v1/admin/settings") + if settings_resp.status_code != 200: + return + spec_keys = {spec["key"] for spec in settings_resp.json().get("specs", [])} + values: dict[str, Any] = { + "allow_private_embedding_base_urls": True, + "allow_private_chat_base_urls": True, + "allow_local_webhooks": True, + } + if "turbopuffer_api_key" in spec_keys: + values["turbopuffer_api_key"] = "e2e-fake-turbopuffer-key" + if "turbopuffer_region" in spec_keys: + values["turbopuffer_region"] = "e2e" + if "turbopuffer_namespace_prefix" in spec_keys: + values["turbopuffer_namespace_prefix"] = "e2e_" + if "turbopuffer_base_url" in spec_keys: + values["turbopuffer_base_url"] = FAKE_TURBOPUFFER_INTERNAL_BASE resp = await client.put( "/v1/admin/settings", - json={ - "values": { - "allow_private_embedding_base_urls": True, - "allow_private_chat_base_urls": True, - "allow_local_webhooks": True, - } - }, + json={"values": values}, ) if resp.status_code not in (200, 204): return @@ -389,9 +407,7 @@ async def collection( The instance is configured with embedding credentials at deploy time (``BIGRAG_EMBEDDING_*`` env vars point at fake-openai), so we only need to supply the bare minimum here. To override, pass any - ``CreateCollectionRequest`` field as a kwarg:: - - coll = await collection(dimension=384, vector_store_provider="qdrant") + ``CreateCollectionRequest`` field as a kwarg. """ created_names: list[str] = [] @@ -400,7 +416,6 @@ async def _create( name: str | None = None, description: str = "e2e fixture collection", dimension: int | None = 1536, - vector_store_provider: str = "qdrant", chunk_size: int = 512, chunk_overlap: int = 50, chunk_strategy: str = "paragraph", @@ -423,7 +438,6 @@ async def _create( body: dict[str, Any] = { "name": name or unique_name("e2e"), "description": description, - "vector_store_provider": vector_store_provider, "chunk_size": chunk_size, "chunk_overlap": chunk_overlap, "chunk_strategy": chunk_strategy, diff --git a/e2e/docker-compose.e2e.yml b/e2e/docker-compose.e2e.yml index 67633052..006c9628 100644 --- a/e2e/docker-compose.e2e.yml +++ b/e2e/docker-compose.e2e.yml @@ -36,10 +36,10 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started fake-openai: condition: service_healthy + fake-turbopuffer: + condition: service_healthy bigrag-worker: command: @@ -72,10 +72,10 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started fake-openai: condition: service_healthy + fake-turbopuffer: + condition: service_healthy fake-openai: build: ./e2e/stubs/fake_openai @@ -89,6 +89,18 @@ services: timeout: 3s retries: 10 + fake-turbopuffer: + build: ./e2e/stubs/fake_turbopuffer + container_name: bigrag-e2e-fake-turbopuffer + restart: unless-stopped + ports: + - "127.0.0.1:9002:9002" + healthcheck: + test: ["CMD-SHELL", "python -c 'import urllib.request,sys; sys.exit(0 if urllib.request.urlopen(\"http://localhost:9002/health\").status==200 else 1)'"] + interval: 5s + timeout: 3s + retries: 10 + webhook-sink: build: ./e2e/stubs/webhook_sink container_name: bigrag-e2e-webhook-sink diff --git a/e2e/fixtures/documents/sample.md b/e2e/fixtures/documents/sample.md index 6bf98a75..b7797c9d 100644 --- a/e2e/fixtures/documents/sample.md +++ b/e2e/fixtures/documents/sample.md @@ -15,7 +15,7 @@ through CSRF, scope, and audit middleware. 2. Conversion service (Docling) extracts text from PDF/DOCX/HTML/PNG 3. Chunker splits the document by `chunk_strategy` 4. Embedding worker batches chunks and embeds via the configured provider -5. Vectors and payloads are upserted into Qdrant or Turbopuffer +5. Vectors and payloads are upserted into Turbopuffer ## Retrieval diff --git a/e2e/fixtures/documents/sample.txt b/e2e/fixtures/documents/sample.txt index 78431b58..a5f7b574 100644 --- a/e2e/fixtures/documents/sample.txt +++ b/e2e/fixtures/documents/sample.txt @@ -2,6 +2,6 @@ Acme Corp Overview Acme Corp was founded in 2017 in Singapore by a team of three engineers focused on shipping reliable RAG infrastructure for builders. The company now employs 47 people across Singapore, Bangalore, and Berlin, and reported $12.4 million in annual recurring revenue in fiscal year 2026. -The flagship product is the Acme Knowledge Platform, a self-hostable retrieval-augmented-generation stack that ingests documents from Google Drive, Notion, S3, and arbitrary HTTP sources, chunks them with configurable strategies, and indexes them in Qdrant or Turbopuffer. Customers include teams at fictional firms Globex, Initech, and Soylent. +The flagship product is the Acme Knowledge Platform, a self-hostable retrieval-augmented-generation stack that ingests documents from Google Drive, Notion, S3, and arbitrary HTTP sources, chunks them with configurable strategies, and indexes them in Turbopuffer. Customers include teams at fictional firms Globex, Initech, and Soylent. The Acme support guarantee promises a four-hour response time for production incidents and a 99.95% uptime SLA on the managed control plane. The CEO is Jordan Park; the CTO is Priya Iyer; the head of customer success is Marco Vidal. The company's office is located at 1 Marina Boulevard, Singapore 018989. diff --git a/e2e/stubs/fake_openai/app/main.py b/e2e/stubs/fake_openai/app/main.py index e67e9729..5843f87c 100644 --- a/e2e/stubs/fake_openai/app/main.py +++ b/e2e/stubs/fake_openai/app/main.py @@ -128,9 +128,7 @@ async def event_stream(): "object": "chat.completion.chunk", "created": created, "model": model, - "choices": [ - {"index": 0, "delta": {"role": "assistant"}, "finish_reason": None} - ], + "choices": [{"index": 0, "delta": {"role": "assistant"}, "finish_reason": None}], } yield _sse_chunk(first) @@ -140,9 +138,7 @@ async def event_stream(): "object": "chat.completion.chunk", "created": created, "model": model, - "choices": [ - {"index": 0, "delta": {"content": piece}, "finish_reason": None} - ], + "choices": [{"index": 0, "delta": {"content": piece}, "finish_reason": None}], } yield _sse_chunk(chunk) diff --git a/e2e/stubs/fake_turbopuffer/Dockerfile b/e2e/stubs/fake_turbopuffer/Dockerfile new file mode 100644 index 00000000..3b814558 --- /dev/null +++ b/e2e/stubs/fake_turbopuffer/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /srv + +COPY requirements.txt /srv/requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY app /srv/app + +EXPOSE 9002 + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "9002"] diff --git a/e2e/stubs/fake_turbopuffer/app/__init__.py b/e2e/stubs/fake_turbopuffer/app/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/e2e/stubs/fake_turbopuffer/app/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/e2e/stubs/fake_turbopuffer/app/main.py b/e2e/stubs/fake_turbopuffer/app/main.py new file mode 100644 index 00000000..29018e62 --- /dev/null +++ b/e2e/stubs/fake_turbopuffer/app/main.py @@ -0,0 +1,184 @@ +from __future__ import annotations + +import math +import re +from copy import deepcopy +from typing import Any + +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse + +app = FastAPI(title="fake-turbopuffer", version="0.1.0") + +_namespaces: dict[str, dict[str, dict[str, Any]]] = {} + + +@app.get("/health") +async def health() -> dict[str, str]: + return {"status": "ok"} + + +@app.get("/v1/namespaces") +async def list_namespaces() -> dict[str, list[dict[str, str]]]: + return {"namespaces": [{"id": name, "name": name} for name in sorted(_namespaces)]} + + +@app.delete("/v2/namespaces/{namespace}") +async def delete_namespace(namespace: str) -> JSONResponse: + if namespace not in _namespaces: + return JSONResponse({"error": "not found"}, status_code=404) + del _namespaces[namespace] + return JSONResponse({"status": "ok"}) + + +@app.post("/v2/namespaces/{namespace}") +async def write_namespace(namespace: str, request: Request) -> dict[str, Any]: + body = await request.json() + rows = _namespaces.setdefault(namespace, {}) + affected = 0 + for row in body.get("upsert_rows") or []: + row_id = str(row["id"]) + rows[row_id] = deepcopy(row) + affected += 1 + for row_id in body.get("deletes") or []: + if rows.pop(str(row_id), None) is not None: + affected += 1 + delete_filter = body.get("delete_by_filter") + if delete_filter is not None: + delete_ids = [row_id for row_id, row in rows.items() if _matches(row, delete_filter)] + for row_id in delete_ids: + rows.pop(row_id, None) + affected += len(delete_ids) + return {"status": "ok", "rows_affected": affected} + + +@app.post("/v2/namespaces/{namespace}/query") +async def query_namespace(namespace: str, request: Request) -> dict[str, list[dict[str, Any]]]: + body = await request.json() + rows = [ + deepcopy(row) + for row in _namespaces.get(namespace, {}).values() + if _matches(row, body.get("filters")) + ] + ranked = _rank(rows, body.get("rank_by")) + total = _limit(body, default=len(ranked)) + return {"rows": [_project(row, body) for row in ranked[:total]]} + + +def _rank(rows: list[dict[str, Any]], rank_by: Any) -> list[dict[str, Any]]: + if not isinstance(rank_by, list) or len(rank_by) < 2: + return sorted(rows, key=lambda row: str(row.get("id", ""))) + mode = str(rank_by[1]).lower() + field = str(rank_by[0]) + if field == "id" and mode == "asc": + return sorted(rows, key=lambda row: str(row.get("id", ""))) + if mode == "ann": + query_vector = rank_by[2] if len(rank_by) > 2 else [] + for row in rows: + score = _cosine(row.get(field) or [], query_vector) + row["$dist"] = max(0.0, 1.0 - score) + row["$score"] = score + return sorted(rows, key=lambda row: float(row.get("$dist", 1.0))) + if mode in {"bm25", "hybrid"}: + query = str(rank_by[2] if len(rank_by) > 2 else "") + for row in rows: + score = _text_score(str(row.get(field, "")), query) + if mode == "hybrid": + score += max(0.0, _cosine(row.get("vector") or [], _vector_hint(rank_by))) * 0.25 + row["$score"] = score + row["$dist"] = max(0.0, 1.0 - min(score, 1.0)) + return sorted(rows, key=lambda row: float(row.get("$score", 0.0)), reverse=True) + return sorted(rows, key=lambda row: str(row.get("id", ""))) + + +def _vector_hint(rank_by: list[Any]) -> list[float]: + for item in rank_by: + if isinstance(item, list) and all(isinstance(value, (int, float)) for value in item): + return [float(value) for value in item] + return [] + + +def _text_score(text: str, query: str) -> float: + words = re.findall(r"[a-z0-9]+", text.lower()) + terms = re.findall(r"[a-z0-9]+", query.lower()) + if not words or not terms: + return 0.0 + counts = {word: words.count(word) for word in set(words)} + hits = sum(counts.get(term, 0) for term in terms) + coverage = len({term for term in terms if term in counts}) / max(1, len(set(terms))) + return round(hits / len(words) + coverage, 6) + + +def _cosine(left: list[Any], right: list[Any]) -> float: + if not left or not right: + return 0.0 + size = min(len(left), len(right)) + a = [float(value) for value in left[:size]] + b = [float(value) for value in right[:size]] + dot = sum(x * y for x, y in zip(a, b, strict=False)) + norm_a = math.sqrt(sum(x * x for x in a)) + norm_b = math.sqrt(sum(y * y for y in b)) + if norm_a == 0.0 or norm_b == 0.0: + return 0.0 + return dot / (norm_a * norm_b) + + +def _limit(body: dict[str, Any], *, default: int) -> int: + if isinstance(body.get("top_k"), int): + return max(0, int(body["top_k"])) + limit = body.get("limit") + if isinstance(limit, dict) and isinstance(limit.get("total"), int): + return max(0, int(limit["total"])) + if isinstance(limit, int): + return max(0, int(limit)) + return default + + +def _project(row: dict[str, Any], body: dict[str, Any]) -> dict[str, Any]: + include = body.get("include_attributes") + exclude = set(body.get("exclude_attributes") or []) + if include is True or include is None: + projected = deepcopy(row) + elif isinstance(include, list): + projected = {"id": row.get("id")} + for key in include: + if key in row: + projected[str(key)] = deepcopy(row[key]) + else: + projected = deepcopy(row) + for key in exclude: + projected.pop(str(key), None) + return projected + + +def _matches(row: dict[str, Any], filter_expr: Any) -> bool: + if filter_expr is None: + return True + if not isinstance(filter_expr, list) or not filter_expr: + return True + head = filter_expr[0] + if head == "And": + clauses = filter_expr[1] if len(filter_expr) > 1 else [] + return all(_matches(row, clause) for clause in clauses) + if head == "Or": + clauses = filter_expr[1] if len(filter_expr) > 1 else [] + return any(_matches(row, clause) for clause in clauses) + if len(filter_expr) < 3: + return True + field, operator, expected = filter_expr[0], str(filter_expr[1]), filter_expr[2] + actual = row.get(str(field)) + if operator == "Eq": + return actual == expected + if operator == "NotEq": + return actual != expected + if operator == "In": + return actual in set(expected if isinstance(expected, list) else [expected]) + if operator == "Gt": + return actual is not None and actual > expected + if operator == "Gte": + return actual is not None and actual >= expected + if operator == "Lt": + return actual is not None and actual < expected + if operator == "Lte": + return actual is not None and actual <= expected + return True diff --git a/e2e/stubs/fake_turbopuffer/requirements.txt b/e2e/stubs/fake_turbopuffer/requirements.txt new file mode 100644 index 00000000..2081c2ae --- /dev/null +++ b/e2e/stubs/fake_turbopuffer/requirements.txt @@ -0,0 +1,2 @@ +fastapi>=0.136.1 +uvicorn[standard]>=0.47.0 diff --git a/e2e/tests/_helpers.py b/e2e/tests/_helpers.py index 84d8df0f..24f5a202 100644 --- a/e2e/tests/_helpers.py +++ b/e2e/tests/_helpers.py @@ -25,11 +25,7 @@ def unique_name(prefix: str = "e2e") -> str: - """Return a unique, Qdrant-safe collection name. - - Qdrant collection names are limited; we keep the prefix tiny and use - 8 hex characters of a UUID4 to stay well under any provider limit. - """ + """Return a compact unique collection name.""" return f"{prefix}_{uuid.uuid4().hex[:8]}" diff --git a/e2e/tests/api/test_access_logs.py b/e2e/tests/api/test_access_logs.py index 5eb4614f..81855653 100644 --- a/e2e/tests/api/test_access_logs.py +++ b/e2e/tests/api/test_access_logs.py @@ -33,9 +33,7 @@ async def _wait_for_action( timeout: float = 10.0, ) -> list[dict]: async def _fetch() -> list[dict]: - r = await admin_client.get( - "/v1/admin/access/logs", params={"action": action, "limit": 50} - ) + r = await admin_client.get("/v1/admin/access/logs", params={"action": action, "limit": 50}) r.raise_for_status() return r.json().get("entries", []) @@ -175,9 +173,7 @@ async def test_logs_filter_by_method( await _seed_query(admin_client, coll["name"]) await _wait_for_action(admin_client, "query.run") - resp = await admin_client.get( - "/v1/admin/access/logs", params={"method": "post", "limit": 20} - ) + resp = await admin_client.get("/v1/admin/access/logs", params={"method": "post", "limit": 20}) body = assert_envelope(resp, 200) for entry in body["entries"]: assert entry["method"] == "POST" @@ -193,9 +189,7 @@ async def test_logs_filter_by_path_substring( await _seed_query(admin_client, coll["name"]) await _wait_for_action(admin_client, "query.run") - resp = await admin_client.get( - "/v1/admin/access/logs", params={"path": "/query", "limit": 20} - ) + resp = await admin_client.get("/v1/admin/access/logs", params={"path": "/query", "limit": 20}) body = assert_envelope(resp, 200) for entry in body["entries"]: assert "/query" in entry["path"] @@ -229,9 +223,7 @@ async def test_logs_filter_by_success( await _seed_query(admin_client, coll["name"]) await _wait_for_action(admin_client, "query.run") - resp = await admin_client.get( - "/v1/admin/access/logs", params={"success": "true", "limit": 20} - ) + resp = await admin_client.get("/v1/admin/access/logs", params={"success": "true", "limit": 20}) body = assert_envelope(resp, 200) for entry in body["entries"]: assert entry["success"] is True @@ -240,18 +232,14 @@ async def test_logs_filter_by_success( async def test_logs_invalid_actor_id_returns_400( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/admin/access/logs", params={"actor_id": "not-a-uuid"} - ) + resp = await admin_client.get("/v1/admin/access/logs", params={"actor_id": "not-a-uuid"}) assert resp.status_code == 400 async def test_logs_invalid_status_family_returns_422( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/admin/access/logs", params={"status_family": "9xx"} - ) + resp = await admin_client.get("/v1/admin/access/logs", params={"status_family": "9xx"}) assert resp.status_code == 422 @@ -298,9 +286,7 @@ async def test_overview_shape_and_window( await _seed_query(admin_client, coll["name"]) await _wait_for_action(admin_client, "query.run") - resp = await admin_client.get( - "/v1/admin/access/overview", params={"window_days": 7} - ) + resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 7}) body = assert_envelope(resp, 200) for key in ( "window_days", @@ -332,12 +318,8 @@ async def test_overview_default_window(admin_client: httpx.AsyncClient) -> None: async def test_overview_invalid_window_rejected( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/admin/access/overview", params={"window_days": 0} - ) + resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 0}) assert resp.status_code == 422 - resp = await admin_client.get( - "/v1/admin/access/overview", params={"window_days": 1000} - ) + resp = await admin_client.get("/v1/admin/access/overview", params={"window_days": 1000}) assert resp.status_code == 422 diff --git a/e2e/tests/api/test_api_keys.py b/e2e/tests/api/test_api_keys.py index cb3e1d4b..1e4347f4 100644 --- a/e2e/tests/api/test_api_keys.py +++ b/e2e/tests/api/test_api_keys.py @@ -197,7 +197,5 @@ async def test_patch_api_key_invalid_id_returns_404( async def test_delete_api_key_unknown_id_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.delete( - "/v1/admin/api-keys/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.delete("/v1/admin/api-keys/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text diff --git a/e2e/tests/api/test_audit.py b/e2e/tests/api/test_audit.py index 46b629f8..2e34c0ce 100644 --- a/e2e/tests/api/test_audit.py +++ b/e2e/tests/api/test_audit.py @@ -100,9 +100,7 @@ async def test_audit_filter_by_actor_id( async def test_audit_filter_invalid_actor_id(admin_client: httpx.AsyncClient) -> None: - resp = await admin_client.get( - "/v1/admin/audit", params={"actor_id": "not-a-uuid"} - ) + resp = await admin_client.get("/v1/admin/audit", params={"actor_id": "not-a-uuid"}) assert resp.status_code == 400 @@ -113,16 +111,12 @@ async def test_audit_pagination( for _ in range(3): await api_key(name=unique_name("audit")) - resp_first = await admin_client.get( - "/v1/admin/audit", params={"limit": 2, "offset": 0} - ) + resp_first = await admin_client.get("/v1/admin/audit", params={"limit": 2, "offset": 0}) first_page = assert_envelope(resp_first, 200) assert len(first_page["entries"]) <= 2 assert "total" in first_page - resp_second = await admin_client.get( - "/v1/admin/audit", params={"limit": 2, "offset": 2} - ) + resp_second = await admin_client.get("/v1/admin/audit", params={"limit": 2, "offset": 2}) second_page = assert_envelope(resp_second, 200) first_ids = {e["id"] for e in first_page["entries"]} second_ids = {e["id"] for e in second_page["entries"]} @@ -173,9 +167,7 @@ async def _fetch(action: str) -> dict: # audit.record uses safe_create_task → polling absorbs the race. await poll_until( lambda: _fetch("webhook.create"), - predicate=lambda b: any( - e["resource_id"] == created["id"] for e in b["entries"] - ), + predicate=lambda b: any(e["resource_id"] == created["id"] for e in b["entries"]), timeout=10.0, interval=0.3, description=f"audit webhook.create entry for {created['id']}", @@ -185,9 +177,7 @@ async def _fetch(action: str) -> dict: await poll_until( lambda: _fetch("webhook.delete"), - predicate=lambda b: any( - e["resource_id"] == created["id"] for e in b["entries"] - ), + predicate=lambda b: any(e["resource_id"] == created["id"] for e in b["entries"]), timeout=10.0, interval=0.3, description=f"audit webhook.delete entry for {created['id']}", diff --git a/e2e/tests/api/test_auth.py b/e2e/tests/api/test_auth.py index 7a399fd4..42846f8e 100644 --- a/e2e/tests/api/test_auth.py +++ b/e2e/tests/api/test_auth.py @@ -24,9 +24,7 @@ API_BASE_HEADERS = {"Origin": "http://localhost:4000"} -async def _login( - client: httpx.AsyncClient, email: str, password: str -) -> httpx.Response: +async def _login(client: httpx.AsyncClient, email: str, password: str) -> httpx.Response: return await client.post( "/v1/auth/login", json={"email": email, "password": password}, @@ -114,9 +112,7 @@ async def test_logout_all_invalidates_other_sessions( admin_client: httpx.AsyncClient, unauth_client: httpx.AsyncClient, ) -> None: - second_login = await _login( - unauth_client, admin_setup["email"], admin_setup["password"] - ) + second_login = await _login(unauth_client, admin_setup["email"], admin_setup["password"]) assert second_login.status_code == 200, second_login.text other_me = await unauth_client.get("/v1/auth/me") assert other_me.status_code == 200 diff --git a/e2e/tests/api/test_backups.py b/e2e/tests/api/test_backups.py index 31ab31ca..0f7b9f43 100644 --- a/e2e/tests/api/test_backups.py +++ b/e2e/tests/api/test_backups.py @@ -42,7 +42,10 @@ def ensure_backup_bucket() -> None: try: import boto3 # type: ignore[import-untyped] from botocore.client import Config # type: ignore[import-untyped] - from botocore.exceptions import ClientError, EndpointConnectionError # type: ignore[import-untyped] + from botocore.exceptions import ( # type: ignore[import-untyped] + ClientError, + EndpointConnectionError, + ) except ImportError as exc: pytest.skip(f"boto3 not available: {exc}") @@ -119,9 +122,7 @@ async def test_list_backups_returns_shape(admin_client: httpx.AsyncClient) -> No async def test_get_backup_unknown_id_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/admin/backups/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.get("/v1/admin/backups/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text @@ -174,17 +175,14 @@ async def _fetch() -> dict: ) if final["status"] == "failed": pytest.skip( - "backup job failed (likely environment-specific); " - f"error={final.get('error_message')!r}" + f"backup job failed (likely environment-specific); error={final.get('error_message')!r}" ) assert final["progress"] == 1.0 assert final["object_count"] >= 1 assert final["destination_prefix"] # And it must show up in the list. - list_resp = await admin_client.get( - "/v1/admin/backups", params={"limit": 50} - ) + list_resp = await admin_client.get("/v1/admin/backups", params={"limit": 50}) list_body = assert_envelope(list_resp, 200) assert any(j["id"] == job_id for j in list_body["jobs"]) diff --git a/e2e/tests/api/test_batch.py b/e2e/tests/api/test_batch.py index f2e8c38d..22382c72 100644 --- a/e2e/tests/api/test_batch.py +++ b/e2e/tests/api/test_batch.py @@ -28,7 +28,6 @@ from tests._helpers import assert_envelope, read_fixture - # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/e2e/tests/api/test_collections.py b/e2e/tests/api/test_collections.py index 1a2db2f9..a2a77aa5 100644 --- a/e2e/tests/api/test_collections.py +++ b/e2e/tests/api/test_collections.py @@ -29,7 +29,6 @@ from tests._helpers import assert_envelope, unique_name - # --------------------------------------------------------------------------- # Create # --------------------------------------------------------------------------- @@ -43,7 +42,6 @@ async def test_create_collection_returns_201_with_defaults( assert coll["dimension"] == 1536 assert coll["embedding_model"] == "text-embedding-3-small" assert coll["embedding_provider"] == "openai_compatible" - assert coll["vector_store_provider"] == "qdrant" assert coll["chunk_size"] == 512 assert coll["chunk_overlap"] == 50 assert coll["chunk_strategy"] == "paragraph" @@ -246,9 +244,7 @@ async def test_delete_collection_also_removes_documents( resp = await admin_client.delete(f"/v1/collections/{coll['name']}") assert resp.status_code == 200, resp.text - follow_up = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}" - ) + follow_up = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}") assert follow_up.status_code == 404, follow_up.text @@ -329,9 +325,7 @@ async def test_reembed_kicks_off_for_existing_collection( async def test_reembed_unknown_collection_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - f"/v1/collections/{unique_name('missing')}/reembed" - ) + resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/reembed") assert resp.status_code == 404, resp.text @@ -355,9 +349,7 @@ async def test_truncate_removes_documents_but_keeps_collection( async def test_truncate_unknown_collection_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - f"/v1/collections/{unique_name('missing')}/truncate" - ) + resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/truncate") assert resp.status_code == 404, resp.text @@ -371,9 +363,7 @@ async def test_event_token_returns_short_lived_token( collection: Callable[..., Awaitable[dict[str, Any]]], ) -> None: coll = await collection() - resp = await admin_client.post( - f"/v1/collections/{coll['name']}/events/token" - ) + resp = await admin_client.post(f"/v1/collections/{coll['name']}/events/token") body = assert_envelope(resp, 200) assert isinstance(body["token"], str) and len(body["token"]) > 0 assert isinstance(body["expires_in"], int) and body["expires_in"] > 0 @@ -382,9 +372,7 @@ async def test_event_token_returns_short_lived_token( async def test_event_token_unknown_collection_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - f"/v1/collections/{unique_name('missing')}/events/token" - ) + resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/events/token") assert resp.status_code == 404, resp.text @@ -394,9 +382,7 @@ async def test_collection_events_unknown_collection_returns_404( """Hit the SSE GET endpoint just enough to verify it 404s for unknown collections without consuming the stream (full SSE flow is covered by test_sse_events.py).""" - resp = await admin_client.get( - f"/v1/collections/{unique_name('missing')}/events" - ) + resp = await admin_client.get(f"/v1/collections/{unique_name('missing')}/events") assert resp.status_code == 404, resp.text diff --git a/e2e/tests/api/test_documents.py b/e2e/tests/api/test_documents.py index e80ffa09..2c68ad4e 100644 --- a/e2e/tests/api/test_documents.py +++ b/e2e/tests/api/test_documents.py @@ -31,7 +31,6 @@ from tests._helpers import assert_envelope, read_fixture, unique_name - # --------------------------------------------------------------------------- # Upload: happy path across formats # --------------------------------------------------------------------------- @@ -138,9 +137,7 @@ async def test_upload_broken_pdf_ends_failed_with_message( from tests._helpers import poll_until async def fetch(): - r = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{body['id']}" - ) + r = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{body['id']}") r.raise_for_status() return r.json() @@ -315,9 +312,7 @@ async def test_get_document_returns_full_payload( ) -> None: coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}") body = assert_envelope(resp, 200) assert body["id"] == doc["id"] assert body["filename"] == doc["filename"] @@ -340,9 +335,7 @@ async def test_get_document_invalid_uuid_returns_404( collection: Callable[..., Awaitable[dict[str, Any]]], ) -> None: coll = await collection() - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/not-a-uuid" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/not-a-uuid") assert resp.status_code == 404, resp.text @@ -353,9 +346,7 @@ async def test_get_document_chunks_shape( ) -> None: coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}/chunks" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}/chunks") body = assert_envelope(resp, 200) assert "chunks" in body assert "total" in body @@ -385,9 +376,7 @@ async def test_download_document_file_matches_content_type( ) -> None: coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}/file" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}/file") assert resp.status_code == 200, resp.text assert resp.headers["content-type"].startswith("text/plain") assert b"Acme" in resp.content @@ -399,8 +388,7 @@ async def test_download_unknown_document_returns_404( ) -> None: coll = await collection() resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/" - "00000000-0000-0000-0000-000000000000/file" + f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000/file" ) assert resp.status_code == 404, resp.text @@ -430,8 +418,7 @@ async def test_reprocess_unknown_document_returns_404( ) -> None: coll = await collection() resp = await admin_client.post( - f"/v1/collections/{coll['name']}/documents/" - "00000000-0000-0000-0000-000000000000/reprocess" + f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000/reprocess" ) assert resp.status_code == 404, resp.text @@ -443,14 +430,10 @@ async def test_delete_document_removes_it( ) -> None: coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - resp = await admin_client.delete( - f"/v1/collections/{coll['name']}/documents/{doc['id']}" - ) + resp = await admin_client.delete(f"/v1/collections/{coll['name']}/documents/{doc['id']}") assert resp.status_code == 200, resp.text - follow_up = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}" - ) + follow_up = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}") assert follow_up.status_code == 404, follow_up.text @@ -460,8 +443,7 @@ async def test_delete_unknown_document_returns_404( ) -> None: coll = await collection() resp = await admin_client.delete( - f"/v1/collections/{coll['name']}/documents/" - "00000000-0000-0000-0000-000000000000" + f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000" ) assert resp.status_code == 404, resp.text @@ -486,9 +468,7 @@ async def test_get_document_cross_collection( async def test_get_document_cross_collection_unknown_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/documents/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.get("/v1/documents/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text diff --git a/e2e/tests/api/test_documents_progress.py b/e2e/tests/api/test_documents_progress.py index 8d772e2a..3389151f 100644 --- a/e2e/tests/api/test_documents_progress.py +++ b/e2e/tests/api/test_documents_progress.py @@ -39,7 +39,6 @@ from tests._helpers import assert_envelope - PROGRESS_KEYS = { "document_id", "collection_name", @@ -87,9 +86,7 @@ async def test_get_document_includes_progress_field( ) -> None: coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/{doc['id']}" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/documents/{doc['id']}") body = assert_envelope(resp, 200) _assert_progress_shape(body["progress"], doc_id=doc["id"]) diff --git a/e2e/tests/api/test_embedding_presets.py b/e2e/tests/api/test_embedding_presets.py index cdef372f..a105b64b 100644 --- a/e2e/tests/api/test_embedding_presets.py +++ b/e2e/tests/api/test_embedding_presets.py @@ -19,7 +19,6 @@ from tests._helpers import assert_envelope, unique_name - PresetFactory = Callable[..., Awaitable[dict[str, Any]]] @@ -103,18 +102,14 @@ async def test_list_presets_pagination( ) -> None: a = await preset() b = await preset() - resp = await admin_client.get( - "/v1/admin/embedding-presets", params={"limit": 1, "offset": 0} - ) + resp = await admin_client.get("/v1/admin/embedding-presets", params={"limit": 1, "offset": 0}) body = assert_envelope(resp, 200) assert isinstance(body["presets"], list) assert len(body["presets"]) == 1 assert body["total"] >= 2 all_ids = {a["id"], b["id"]} page1_id = body["presets"][0]["id"] - resp2 = await admin_client.get( - "/v1/admin/embedding-presets", params={"limit": 1, "offset": 1} - ) + resp2 = await admin_client.get("/v1/admin/embedding-presets", params={"limit": 1, "offset": 1}) body2 = assert_envelope(resp2, 200) assert len(body2["presets"]) == 1 page2_id = body2["presets"][0]["id"] @@ -143,9 +138,7 @@ async def test_test_saved_preset_against_fake_openai( preset: PresetFactory, ) -> None: created = await preset() - resp = await admin_client.post( - f"/v1/admin/embedding-presets/{created['id']}/test" - ) + resp = await admin_client.post(f"/v1/admin/embedding-presets/{created['id']}/test") body = assert_envelope(resp, 200) assert body["status"] == "ok" @@ -153,9 +146,7 @@ async def test_test_saved_preset_against_fake_openai( async def test_test_saved_preset_unknown_id_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - "/v1/admin/embedding-presets/not-a-uuid/test" - ) + resp = await admin_client.post("/v1/admin/embedding-presets/not-a-uuid/test") assert resp.status_code == 404, resp.text resp2 = await admin_client.post( @@ -242,9 +233,10 @@ async def test_collection_can_use_preset( ) -> None: created = await preset() coll = await collection(embedding_preset_id=created["id"]) - assert coll.get("embedding_preset_id") == created["id"] or coll.get("embedding_preset") is not None or coll.get("embedding_provider") in ( - "openai_compatible", - "openai", + assert ( + coll.get("embedding_preset_id") == created["id"] + or coll.get("embedding_preset") is not None + or coll.get("embedding_provider") in ("openai_compatible", "openai") ) @@ -253,16 +245,12 @@ async def test_delete_preset_makes_it_unfindable( preset: PresetFactory, ) -> None: created = await preset() - resp = await admin_client.delete( - f"/v1/admin/embedding-presets/{created['id']}" - ) + resp = await admin_client.delete(f"/v1/admin/embedding-presets/{created['id']}") body = assert_envelope(resp, 200) assert body["status"] == "ok" # Subsequent lookups should 404. - test_resp = await admin_client.post( - f"/v1/admin/embedding-presets/{created['id']}/test" - ) + test_resp = await admin_client.post(f"/v1/admin/embedding-presets/{created['id']}/test") assert test_resp.status_code == 404, test_resp.text patch_resp = await admin_client.patch( @@ -284,9 +272,7 @@ async def test_delete_preset_in_use_returns_409( created = await preset() coll = await collection(embedding_preset_id=created["id"]) try: - resp = await admin_client.delete( - f"/v1/admin/embedding-presets/{created['id']}" - ) + resp = await admin_client.delete(f"/v1/admin/embedding-presets/{created['id']}") assert resp.status_code == 409, resp.text finally: # collection factory tears the collection down; force-delete it @@ -297,9 +283,7 @@ async def test_delete_preset_in_use_returns_409( async def test_delete_preset_unknown_id_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.delete( - "/v1/admin/embedding-presets/not-a-uuid" - ) + resp = await admin_client.delete("/v1/admin/embedding-presets/not-a-uuid") assert resp.status_code == 404, resp.text resp2 = await admin_client.delete( diff --git a/e2e/tests/api/test_errors.py b/e2e/tests/api/test_errors.py index 43bc6927..59b98632 100644 --- a/e2e/tests/api/test_errors.py +++ b/e2e/tests/api/test_errors.py @@ -26,7 +26,6 @@ from tests._helpers import unique_name - # --------------------------------------------------------------------------- # 401 — missing authentication on a protected endpoint. # --------------------------------------------------------------------------- @@ -93,23 +92,18 @@ async def test_404_unknown_document( ) -> None: coll = await collection() resp = await admin_client.get( - f"/v1/collections/{coll['name']}/documents/" - "00000000-0000-0000-0000-000000000000" + f"/v1/collections/{coll['name']}/documents/00000000-0000-0000-0000-000000000000" ) assert resp.status_code == 404, resp.text async def test_404_unknown_user(admin_client: httpx.AsyncClient) -> None: - resp = await admin_client.delete( - "/v1/admin/users/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.delete("/v1/admin/users/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text async def test_404_unknown_webhook(admin_client: httpx.AsyncClient) -> None: - resp = await admin_client.get( - "/v1/admin/webhooks/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.get("/v1/admin/webhooks/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text @@ -128,7 +122,6 @@ async def test_409_duplicate_collection_name( json={ "name": first["name"], "description": "dup attempt", - "vector_store_provider": "qdrant", "chunk_size": 512, "chunk_overlap": 50, "chunk_strategy": "paragraph", @@ -195,9 +188,8 @@ async def test_422_bad_enum_on_create_collection( "/v1/collections", json={ "name": unique_name("enum"), - "vector_store_provider": "not-a-real-provider", "chunk_strategy": "paragraph", - "default_search_mode": "semantic", + "default_search_mode": "not-a-real-mode", }, ) assert resp.status_code == 422, resp.text @@ -224,9 +216,7 @@ async def test_422_wrong_type_on_create_collection( async def test_422_out_of_range_pagination( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - "/v1/admin/api-keys", params={"limit": 0} - ) + resp = await admin_client.get("/v1/admin/api-keys", params={"limit": 0}) assert resp.status_code == 422, resp.text diff --git a/e2e/tests/api/test_evaluation.py b/e2e/tests/api/test_evaluation.py index 550b94ac..9a33685f 100644 --- a/e2e/tests/api/test_evaluation.py +++ b/e2e/tests/api/test_evaluation.py @@ -28,9 +28,7 @@ def _load_golden_set() -> dict[str, Any]: return json.loads((GOLDEN_SETS_DIR / "acme.json").read_text()) -def _substitute_ids( - golden: dict[str, Any], id_map: dict[str, str] -) -> list[dict[str, Any]]: +def _substitute_ids(golden: dict[str, Any], id_map: dict[str, str]) -> list[dict[str, Any]]: cases: list[dict[str, Any]] = [] for entry in golden["queries"]: expected = [id_map.get(raw, raw) for raw in entry["expected_doc_ids"]] diff --git a/e2e/tests/api/test_health.py b/e2e/tests/api/test_health.py index 0be70d3f..6cbb6711 100644 --- a/e2e/tests/api/test_health.py +++ b/e2e/tests/api/test_health.py @@ -31,8 +31,6 @@ async def test_health_readiness_reports_each_dependency( for dep in ("postgres", "redis", "vector_store", "embedding"): assert dep in body, f"missing dependency key {dep!r} in {body!r}" assert isinstance(body[dep], bool) - assert "vector_store_provider" in body - assert "qdrant" in body async def test_health_readiness_status_matches_http_code( diff --git a/e2e/tests/api/test_mcp_servers.py b/e2e/tests/api/test_mcp_servers.py index 1469b7ca..cbc5d9ca 100644 --- a/e2e/tests/api/test_mcp_servers.py +++ b/e2e/tests/api/test_mcp_servers.py @@ -282,9 +282,7 @@ async def test_patch_duplicate_server_name_409( async def test_rotate_returns_new_key(admin_client: httpx.AsyncClient) -> None: created = await _create(admin_client) try: - resp = await admin_client.post( - f"/v1/admin/mcp-servers/{created['id']}/rotate" - ) + resp = await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate") body = assert_envelope(resp, 200) assert body["api_key"].startswith("bigrag_sk_") assert body["api_key"] != created["api_key"] @@ -309,9 +307,7 @@ async def test_rotated_old_key_no_longer_authenticates( resp = await old_client.get("/v1/collections") assert resp.status_code == 200 - rotated = ( - await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate") - ).json() + rotated = (await admin_client.post(f"/v1/admin/mcp-servers/{created['id']}/rotate")).json() async with httpx.AsyncClient( base_url=str(admin_client.base_url), diff --git a/e2e/tests/api/test_query.py b/e2e/tests/api/test_query.py index ee6a2863..4ac751aa 100644 --- a/e2e/tests/api/test_query.py +++ b/e2e/tests/api/test_query.py @@ -36,9 +36,7 @@ async def test_query_collection_returns_results_with_timings( document: DocumentFactory, ) -> None: coll = await seed_collection(collection, document) - await wait_until_searchable( - admin_client, coll["name"], "Acme Corp founded Singapore", top_k=3 - ) + await wait_until_searchable(admin_client, coll["name"], "Acme Corp founded Singapore", top_k=3) resp = await admin_client.post( f"/v1/collections/{coll['name']}/query", json={"query": "Acme Corp founded Singapore", "top_k": 5}, @@ -176,15 +174,11 @@ async def test_query_cache_hit_on_second_call( await wait_until_searchable(admin_client, coll["name"], "cache probe Acme") payload = {"query": "cache probe Acme", "top_k": 5} - first = await admin_client.post( - f"/v1/collections/{coll['name']}/query", json=payload - ) + first = await admin_client.post(f"/v1/collections/{coll['name']}/query", json=payload) first_body = assert_envelope(first, 200) assert "cache_hit" in first_body["timings"] - second = await admin_client.post( - f"/v1/collections/{coll['name']}/query", json=payload - ) + second = await admin_client.post(f"/v1/collections/{coll['name']}/query", json=payload) second_body = assert_envelope(second, 200) # Cache may or may not hit on the immediate second call depending on # whether result caching is enabled by the runtime config; just assert diff --git a/e2e/tests/api/test_realtime_sse.py b/e2e/tests/api/test_realtime_sse.py index c09e6934..9314bf51 100644 --- a/e2e/tests/api/test_realtime_sse.py +++ b/e2e/tests/api/test_realtime_sse.py @@ -9,7 +9,6 @@ - GET /v1/admin/realtime/{provider_slug}/sources - GET /v1/admin/realtime/{provider_slug}/sync-jobs - GET /v1/admin/realtime/backups -- GET /v1/admin/realtime/vector-migrations - GET /v1/admin/realtime/access/overview - GET /v1/admin/realtime/access/logs - GET /v1/admin/realtime/audit @@ -33,7 +32,6 @@ import pytest from conftest import sse_events -from tests._helpers import unique_name SSE_TIMEOUT_SECONDS = 10.0 @@ -52,9 +50,7 @@ async def _read() -> dict[str, Any]: if event.event == "snapshot": return json.loads(event.data) if event.event == "error": - raise AssertionError( - f"SSE error event for {path!r}: {event.data}" - ) + raise AssertionError(f"SSE error event for {path!r}: {event.data}") raise AssertionError(f"no snapshot event received from {path!r}") return await asyncio.wait_for(_read(), timeout=timeout) @@ -65,7 +61,9 @@ async def _assert_unauth_blocked( path: str, ) -> None: resp = await unauth_client.get(path) - assert resp.status_code == 401, f"{path}: expected 401 unauth, got {resp.status_code} {resp.text}" + assert resp.status_code == 401, ( + f"{path}: expected 401 unauth, got {resp.status_code} {resp.text}" + ) async def _assert_api_key_blocked( @@ -150,9 +148,7 @@ async def test_realtime_collection_documents_batch_status_requires_ids( collection: Callable[..., Awaitable[dict[str, Any]]], ) -> None: coll = await collection() - path = ( - f"/v1/admin/realtime/collections/{coll['name']}/documents/batch-status" - ) + path = f"/v1/admin/realtime/collections/{coll['name']}/documents/batch-status" resp = await admin_client.get(path) assert resp.status_code == 400, resp.text @@ -176,20 +172,6 @@ async def test_realtime_backups_stream( await _assert_api_key_blocked(api_key_client, path) -async def test_realtime_vector_migrations_stream( - admin_client: httpx.AsyncClient, - unauth_client: httpx.AsyncClient, - api_key_client: Callable[..., Awaitable[httpx.AsyncClient]], -) -> None: - path = "/v1/admin/realtime/vector-migrations" - snapshot = await _first_snapshot(admin_client, path) - assert snapshot["topic"].startswith("vector-migrations:") - assert "payload" in snapshot - - await _assert_unauth_blocked(unauth_client, path) - await _assert_api_key_blocked(api_key_client, path) - - async def test_realtime_usage_stream( admin_client: httpx.AsyncClient, unauth_client: httpx.AsyncClient, @@ -303,8 +285,7 @@ async def test_realtime_upload_session_unauth( unauth_client: httpx.AsyncClient, ) -> None: path = ( - "/v1/admin/realtime/collections/nope/upload-sessions/" - "00000000-0000-0000-0000-000000000000" + "/v1/admin/realtime/collections/nope/upload-sessions/00000000-0000-0000-0000-000000000000" ) resp = await unauth_client.get(path) assert resp.status_code == 401, resp.text diff --git a/e2e/tests/api/test_settings.py b/e2e/tests/api/test_settings.py index 785060e3..3dc3038a 100644 --- a/e2e/tests/api/test_settings.py +++ b/e2e/tests/api/test_settings.py @@ -75,7 +75,15 @@ async def test_settings_get_returns_specs_and_values( assert "values" in body and isinstance(body["values"], dict) spec_keys = {spec["key"] for spec in body["specs"]} - for sample in ("chat_temperature", "embedding_api_key", "max_upload_size_mb"): + for sample in ( + "chat_temperature", + "embedding_api_key", + "max_upload_size_mb", + "turbopuffer_api_key", + "turbopuffer_base_url", + "turbopuffer_namespace_prefix", + "turbopuffer_region", + ): assert sample in spec_keys, f"expected setting key {sample!r} in spec list" assert sample in body["values"], f"missing value entry for {sample!r}" @@ -246,6 +254,29 @@ async def test_settings_test_validates_chat_provider_settings( assert set(body["checked"]) >= {"chat_base_url", "chat_model", "chat_temperature"} +async def test_settings_test_validates_turbopuffer_settings( + admin_client: httpx.AsyncClient, + fake_turbopuffer_base: str, +) -> None: + resp = await admin_client.post( + "/v1/admin/settings/test", + json={ + "values": { + "turbopuffer_api_key": "e2e-fake-turbopuffer-key", + "turbopuffer_base_url": fake_turbopuffer_base, + "turbopuffer_region": "e2e", + }, + }, + ) + body = assert_envelope(resp, 200) + assert body["status"] == "ok" + assert set(body["checked"]) >= { + "turbopuffer_api_key", + "turbopuffer_base_url", + "turbopuffer_region", + } + + async def test_settings_test_rejects_broken_base_url( admin_client: httpx.AsyncClient, ) -> None: diff --git a/e2e/tests/api/test_sse_events.py b/e2e/tests/api/test_sse_events.py index 903985a0..9e32c350 100644 --- a/e2e/tests/api/test_sse_events.py +++ b/e2e/tests/api/test_sse_events.py @@ -70,7 +70,7 @@ async def _read() -> None: for trigger in triggers: await trigger() await asyncio.wait_for(reader, timeout=timeout) - except asyncio.TimeoutError as exc: + except TimeoutError as exc: reader.cancel() raise TimeoutError( f"SSE stream {path!r} did not satisfy predicate within {timeout}s; " @@ -109,9 +109,7 @@ async def test_events_token_requires_auth( async def test_events_token_unknown_collection_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - f"/v1/collections/{unique_name('missing')}/events/token" - ) + resp = await admin_client.post(f"/v1/collections/{unique_name('missing')}/events/token") assert resp.status_code == 404, resp.text @@ -173,18 +171,14 @@ async def test_events_stream_requires_auth_without_token( collection: CollectionFactory, ) -> None: coll = await collection() - resp = await unauth_client.get( - f"/v1/collections/{coll['name']}/events" - ) + resp = await unauth_client.get(f"/v1/collections/{coll['name']}/events") assert resp.status_code == 401, resp.text async def test_events_stream_unknown_collection_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.get( - f"/v1/collections/{unique_name('missing')}/events" - ) + resp = await admin_client.get(f"/v1/collections/{unique_name('missing')}/events") assert resp.status_code == 404, resp.text @@ -194,9 +188,7 @@ async def test_events_stream_token_grants_anonymous_access( collection: CollectionFactory, ) -> None: coll = await collection() - token_resp = await admin_client.post( - f"/v1/collections/{coll['name']}/events/token" - ) + token_resp = await admin_client.post(f"/v1/collections/{coll['name']}/events/token") token_body = assert_envelope(token_resp, 200) token = token_body["token"] diff --git a/e2e/tests/api/test_upload_sessions.py b/e2e/tests/api/test_upload_sessions.py index dc76c1f9..9931eb55 100644 --- a/e2e/tests/api/test_upload_sessions.py +++ b/e2e/tests/api/test_upload_sessions.py @@ -27,7 +27,6 @@ from tests._helpers import assert_envelope, poll_until, unique_name - # --------------------------------------------------------------------------- # Create / Get # --------------------------------------------------------------------------- @@ -74,9 +73,7 @@ async def test_get_upload_session_returns_payload( body = assert_envelope(create, 201) session_id = body["id"] - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/upload-sessions/{session_id}" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/{session_id}") got = assert_envelope(resp, 200) assert got["id"] == session_id assert got["total_files"] == 1 @@ -88,8 +85,7 @@ async def test_get_unknown_upload_session_returns_404( ) -> None: coll = await collection() resp = await admin_client.get( - f"/v1/collections/{coll['name']}/upload-sessions/" - "00000000-0000-0000-0000-000000000000" + f"/v1/collections/{coll['name']}/upload-sessions/00000000-0000-0000-0000-000000000000" ) assert resp.status_code == 404, resp.text @@ -99,9 +95,7 @@ async def test_get_invalid_session_uuid_returns_404( collection: Callable[..., Awaitable[dict[str, Any]]], ) -> None: coll = await collection() - resp = await admin_client.get( - f"/v1/collections/{coll['name']}/upload-sessions/not-a-uuid" - ) + resp = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/not-a-uuid") assert resp.status_code == 404, resp.text @@ -148,9 +142,7 @@ async def test_full_lifecycle_two_files_end_up_in_collection( assert complete_body["uploaded_files"] == 2 async def fetch_session() -> dict[str, Any]: - r = await admin_client.get( - f"/v1/collections/{coll['name']}/upload-sessions/{session_id}" - ) + r = await admin_client.get(f"/v1/collections/{coll['name']}/upload-sessions/{session_id}") r.raise_for_status() return r.json() diff --git a/e2e/tests/api/test_users.py b/e2e/tests/api/test_users.py index c511183d..1101e34e 100644 --- a/e2e/tests/api/test_users.py +++ b/e2e/tests/api/test_users.py @@ -152,9 +152,7 @@ async def test_patch_user_unknown_id_returns_404( async def test_delete_user_unknown_id_returns_404( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.delete( - "/v1/admin/users/00000000-0000-0000-0000-000000000000" - ) + resp = await admin_client.delete("/v1/admin/users/00000000-0000-0000-0000-000000000000") assert resp.status_code == 404, resp.text diff --git a/e2e/tests/api/test_vector_storage.py b/e2e/tests/api/test_vector_storage.py index a6aa5962..e9b2d40c 100644 --- a/e2e/tests/api/test_vector_storage.py +++ b/e2e/tests/api/test_vector_storage.py @@ -10,7 +10,6 @@ from typing import Any import httpx -import pytest from tests._helpers import assert_envelope @@ -36,25 +35,11 @@ async def test_vector_storage_overview_returns_provider_shape( resp = await admin_client.get("/v1/admin/vector-storage/overview") body = assert_envelope(resp, 200) - for key in ( - "fallback_provider", - "configured_providers", - "provider_health", - "collections", - "totals", - ): - assert key in body, f"missing top-level key {key!r} in {body!r}" - - assert isinstance(body["fallback_provider"], str) and body["fallback_provider"] - assert isinstance(body["configured_providers"], list) - # bigRAG can return a fallback_provider label distinct from the listed - # configured providers (e.g., "collection" when collection-level provider - # overrides are in effect). Just assert the field is non-empty here. - assert isinstance(body["provider_health"], dict) - for provider in body["configured_providers"]: - assert provider in body["provider_health"], ( - f"provider_health missing entry for {provider!r}" - ) + assert set(body) == {"provider", "health", "collections", "totals"} + + assert body["provider"] == "turbopuffer" + assert isinstance(body["health"], dict) + assert body["health"].get("status") in {"ok", "error"} assert isinstance(body["collections"], list) totals = body["totals"] @@ -84,74 +69,8 @@ async def test_vector_storage_overview_reflects_collection_changes( assert after_body["totals"]["collections"] >= before_count + 1 entry = next(c for c in after_body["collections"] if c["name"] == new_coll["name"]) - for key in ("name", "provider", "documents", "chunks", "bytes"): + for key in ("name", "documents", "chunks", "bytes"): assert key in entry + assert "provider" not in entry assert entry["documents"] == 0 assert entry["chunks"] == 0 - - -async def test_vector_migrations_requires_admin_session( - unauth_client: httpx.AsyncClient, -) -> None: - resp = await unauth_client.get("/v1/admin/vector-storage/migrations") - assert resp.status_code == 401, resp.text - - -async def test_vector_migrations_rejects_api_key( - api_key_client: Callable[..., Awaitable[httpx.AsyncClient]], -) -> None: - client = await api_key_client() - resp = await client.get("/v1/admin/vector-storage/migrations") - assert resp.status_code in (401, 403), resp.text - - -async def test_vector_migrations_list_returns_shape( - admin_client: httpx.AsyncClient, -) -> None: - resp = await admin_client.get( - "/v1/admin/vector-storage/migrations", - params={"include_total": "true", "limit": 1}, - ) - body = assert_envelope(resp, 200) - assert "jobs" in body - assert "total" in body - assert "next_cursor" in body - assert isinstance(body["jobs"], list) - assert isinstance(body["total"], int) - - -async def test_vector_migration_unknown_collection_returns_400( - admin_client: httpx.AsyncClient, -) -> None: - resp = await admin_client.post( - "/v1/admin/vector-storage/migrations", - json={"collection": "missing_collection", "target_provider": "turbopuffer"}, - ) - assert resp.status_code in (400, 409), resp.text - - -async def test_vector_migration_same_provider_returns_409( - admin_client: httpx.AsyncClient, - collection: Callable[..., Awaitable[dict[str, Any]]], -) -> None: - coll = await collection() - resp = await admin_client.post( - "/v1/admin/vector-storage/migrations", - json={"collection": coll["name"], "target_provider": coll["vector_store_provider"]}, - ) - assert resp.status_code == 409, resp.text - - -async def test_vector_migration_unconfigured_target_returns_400( - admin_client: httpx.AsyncClient, - collection: Callable[..., Awaitable[dict[str, Any]]], -) -> None: - overview = assert_envelope(await admin_client.get("/v1/admin/vector-storage/overview"), 200) - if "turbopuffer" in overview["configured_providers"]: - pytest.skip("turbopuffer is configured in this e2e environment") - coll = await collection() - resp = await admin_client.post( - "/v1/admin/vector-storage/migrations", - json={"collection": coll["name"], "target_provider": "turbopuffer"}, - ) - assert resp.status_code == 400, resp.text diff --git a/e2e/tests/api/test_webhooks.py b/e2e/tests/api/test_webhooks.py index 599964c9..2dd4ef9c 100644 --- a/e2e/tests/api/test_webhooks.py +++ b/e2e/tests/api/test_webhooks.py @@ -198,9 +198,7 @@ async def test_test_delivery_reaches_sink_with_valid_signature( async def test_test_delivery_404_for_missing_webhook( admin_client: httpx.AsyncClient, ) -> None: - resp = await admin_client.post( - "/v1/admin/webhooks/00000000-0000-0000-0000-000000000000/test" - ) + resp = await admin_client.post("/v1/admin/webhooks/00000000-0000-0000-0000-000000000000/test") assert resp.status_code == 404 @@ -255,9 +253,7 @@ async def test_replay_delivery_redelivers( assert_envelope(resp, 200) await webhook_sink["wait"](label, count=1) - body = ( - await admin_client.get(f"/v1/admin/webhooks/{created['id']}/deliveries") - ).json() + body = (await admin_client.get(f"/v1/admin/webhooks/{created['id']}/deliveries")).json() if not body.get("deliveries"): pytest.skip("No persisted delivery to replay (test endpoint does not persist)") diff --git a/e2e/tests/sdk_python/conftest.py b/e2e/tests/sdk_python/conftest.py index 00c83559..86650d8d 100644 --- a/e2e/tests/sdk_python/conftest.py +++ b/e2e/tests/sdk_python/conftest.py @@ -17,7 +17,6 @@ import httpx import pytest_asyncio - from bigrag import BigRAG SdkClientFactory = Callable[..., Awaitable[BigRAG]] @@ -97,9 +96,7 @@ async def session_sdk_client( base_url=api_base_url, http_client=http_client, ) - await client.auth.login( - {"email": admin_setup["email"], "password": admin_setup["password"]} - ) + await client.auth.login({"email": admin_setup["email"], "password": admin_setup["password"]}) try: yield client finally: diff --git a/e2e/tests/sdk_python/test_auth.py b/e2e/tests/sdk_python/test_auth.py index 70a3f1e9..ac89eadd 100644 --- a/e2e/tests/sdk_python/test_auth.py +++ b/e2e/tests/sdk_python/test_auth.py @@ -18,8 +18,8 @@ import httpx import pytest - from bigrag import APIError, AuthenticationError, BigRAG, NotFoundError + from tests._helpers import unique_name diff --git a/e2e/tests/sdk_python/test_chat_stream.py b/e2e/tests/sdk_python/test_chat_stream.py index a24fed52..cad17de8 100644 --- a/e2e/tests/sdk_python/test_chat_stream.py +++ b/e2e/tests/sdk_python/test_chat_stream.py @@ -18,7 +18,6 @@ from typing import Any import pytest - from bigrag import BigRAG, NotFoundError @@ -129,15 +128,11 @@ async def test_chat_client_aliases_create_and_stream( coll = await collection() await document(coll["name"], fixture="sample.txt") - non_stream = await client.chat_create( - {"collection": coll["name"], "message": "hello"} - ) + non_stream = await client.chat_create({"collection": coll["name"], "message": "hello"}) assert non_stream["assistant_message"]["role"] == "assistant" saw_event = False - async for event in client.chat_stream( - {"collection": coll["name"], "message": "hello stream"} - ): + async for event in client.chat_stream({"collection": coll["name"], "message": "hello stream"}): saw_event = True if event["event"] == "done": break diff --git a/e2e/tests/sdk_python/test_collections.py b/e2e/tests/sdk_python/test_collections.py index 45a7971a..7711514c 100644 --- a/e2e/tests/sdk_python/test_collections.py +++ b/e2e/tests/sdk_python/test_collections.py @@ -18,8 +18,8 @@ from typing import Any import pytest - from bigrag import BigRAG, NotFoundError + from tests._helpers import unique_name @@ -32,7 +32,6 @@ async def test_collections_create_returns_full_collection( { "name": name, "description": "sdk create", - "vector_store_provider": "qdrant", "dimension": 1536, "chunk_size": 512, "chunk_overlap": 50, @@ -48,7 +47,6 @@ async def test_collections_create_returns_full_collection( "description", "embedding_provider", "embedding_model", - "vector_store_provider", "dimension", "chunk_size", "chunk_overlap", diff --git a/e2e/tests/sdk_python/test_documents.py b/e2e/tests/sdk_python/test_documents.py index e52c4bfa..fb6c32af 100644 --- a/e2e/tests/sdk_python/test_documents.py +++ b/e2e/tests/sdk_python/test_documents.py @@ -18,8 +18,8 @@ from typing import Any import pytest - from bigrag import BigRAG, NotFoundError + from tests._helpers import poll_until, read_fixture, unique_name @@ -83,9 +83,7 @@ async def test_documents_list_and_pagination( coll = await collection() doc = await document(coll["name"], fixture="sample.txt") - listing = await client.documents.list( - coll["name"], limit=10, offset=0, include_total=True - ) + listing = await client.documents.list(coll["name"], limit=10, offset=0, include_total=True) assert listing["total"] >= 1 ids = [d["id"] for d in listing["documents"]] assert doc["id"] in ids diff --git a/e2e/tests/sdk_python/test_errors.py b/e2e/tests/sdk_python/test_errors.py index 71da2610..6c3ee883 100644 --- a/e2e/tests/sdk_python/test_errors.py +++ b/e2e/tests/sdk_python/test_errors.py @@ -22,7 +22,6 @@ from typing import Any import pytest - from bigrag import ( APIConnectionError, APIError, @@ -36,6 +35,7 @@ RateLimitError, error_for_status, ) + from tests._helpers import unique_name @@ -43,9 +43,7 @@ async def test_invalid_api_key_raises_authentication_error( api_base_url: str, ) -> None: """An obviously-bogus bearer token returns 401.""" - async with BigRAG( - api_key="bigrag_sk_not_a_valid_key_at_all", base_url=api_base_url - ) as client: + async with BigRAG(api_key="bigrag_sk_not_a_valid_key_at_all", base_url=api_base_url) as client: with pytest.raises(AuthenticationError) as excinfo: await client.collections.list() assert excinfo.value.status == 401 @@ -84,9 +82,7 @@ async def test_unknown_webhook_raises_not_found( ) -> None: # Webhook admin endpoints require session auth, not API key. with pytest.raises(NotFoundError): - await session_sdk_client.webhooks.get( - "00000000-0000-0000-0000-000000000000" - ) + await session_sdk_client.webhooks.get("00000000-0000-0000-0000-000000000000") async def test_validation_error_surfaces_as_api_error( diff --git a/e2e/tests/sdk_python/test_query.py b/e2e/tests/sdk_python/test_query.py index 5e790d44..8d5ba8d3 100644 --- a/e2e/tests/sdk_python/test_query.py +++ b/e2e/tests/sdk_python/test_query.py @@ -15,8 +15,8 @@ from typing import Any import pytest - from bigrag import BigRAG, NotFoundError + from tests._helpers import unique_name @@ -60,9 +60,7 @@ async def test_query_top_k_limits_result_count( coll = await collection() await document(coll["name"], fixture="sample.txt") - response = await client.queries.query( - coll["name"], {"query": "sample", "top_k": 1} - ) + response = await client.queries.query(coll["name"], {"query": "sample", "top_k": 1}) assert len(response["results"]) <= 1 @@ -135,9 +133,7 @@ async def test_query_empty_collection_returns_empty_results( client = await sdk_client() coll = await collection() - response = await client.queries.query( - coll["name"], {"query": "anything", "top_k": 5} - ) + response = await client.queries.query(coll["name"], {"query": "anything", "top_k": 5}) assert response["results"] == [] assert response["total"] == 0 @@ -203,9 +199,7 @@ async def test_query_unknown_collection_raises_not_found( ) -> None: client = await sdk_client() with pytest.raises(NotFoundError): - await client.queries.query( - unique_name("missing"), {"query": "x", "top_k": 1} - ) + await client.queries.query(unique_name("missing"), {"query": "x", "top_k": 1}) async def test_collection_client_query_wrapper( diff --git a/e2e/tests/sdk_typescript/auth.test.ts b/e2e/tests/sdk_typescript/auth.test.ts index c04f9b6e..a1d5673a 100644 --- a/e2e/tests/sdk_typescript/auth.test.ts +++ b/e2e/tests/sdk_typescript/auth.test.ts @@ -64,7 +64,6 @@ describe("AuthResource", () => { const body: Parameters[0] = { name: collName, description: "auth pin test", - vector_store_provider: "qdrant", dimension: 1536, chunk_size: 512, chunk_overlap: 50, diff --git a/e2e/tests/sdk_typescript/collections.test.ts b/e2e/tests/sdk_typescript/collections.test.ts index 1e4301c1..3d4a7e38 100644 --- a/e2e/tests/sdk_typescript/collections.test.ts +++ b/e2e/tests/sdk_typescript/collections.test.ts @@ -19,7 +19,6 @@ async function createTempCollection(): Promise { const body: Parameters[0] = { name: uniqueName("sdkcoll"), description: "sdk collections.test", - vector_store_provider: "qdrant", dimension: 1536, chunk_size: 512, chunk_overlap: 50, @@ -38,7 +37,6 @@ describe("CollectionsResource", () => { expect(typeof created.id).toBe("string"); expect(created.id.length).toBeGreaterThan(0); expect(typeof created.name).toBe("string"); - expect(created.vector_store_provider).toBe("qdrant"); expect(typeof created.dimension).toBe("number"); expect(created.dimension).toBeGreaterThan(0); expect(created.document_count).toBe(0); diff --git a/e2e/tests/sdk_typescript/helpers.ts b/e2e/tests/sdk_typescript/helpers.ts index f7f01b63..9d88ab42 100644 --- a/e2e/tests/sdk_typescript/helpers.ts +++ b/e2e/tests/sdk_typescript/helpers.ts @@ -256,9 +256,6 @@ export async function createCollection( const body: CreateCollectionBody = { name: uniqueName("e2e"), description: "sdk e2e collection", - vector_store_provider: "qdrant", - // bigRAG requires `dimension` for openai_compatible providers (the - // default in e2e). text-embedding-3-small uses 1536. dimension: 1536, chunk_size: 512, chunk_overlap: 50, diff --git a/sdks/python/src/bigrag/resources/admin/__init__.py b/sdks/python/src/bigrag/resources/admin/__init__.py index 0d4c6472..da0090f5 100644 --- a/sdks/python/src/bigrag/resources/admin/__init__.py +++ b/sdks/python/src/bigrag/resources/admin/__init__.py @@ -15,7 +15,6 @@ from bigrag.resources.admin.realtime import AdminRealtimeResource from bigrag.resources.admin.settings import AdminSettingsResource from bigrag.resources.admin.users import AdminUsersResource -from bigrag.resources.admin.vector_migrations import AdminVectorMigrationsResource if TYPE_CHECKING: from bigrag._core import BigRAGCore @@ -32,7 +31,6 @@ class AdminResource: mcp_servers: AdminMcpServersResource realtime: AdminRealtimeResource settings: AdminSettingsResource - vector_migrations: AdminVectorMigrationsResource def __init__(self, client: BigRAGCore) -> None: self.users = AdminUsersResource(client) @@ -45,7 +43,6 @@ def __init__(self, client: BigRAGCore) -> None: self.mcp_servers = AdminMcpServersResource(client) self.realtime = AdminRealtimeResource(client) self.settings = AdminSettingsResource(client) - self.vector_migrations = AdminVectorMigrationsResource(client) __all__ = [ @@ -61,5 +58,4 @@ def __init__(self, client: BigRAGCore) -> None: "AdminResource", "AdminSettingsResource", "AdminUsersResource", - "AdminVectorMigrationsResource", ] diff --git a/sdks/python/src/bigrag/resources/admin/realtime.py b/sdks/python/src/bigrag/resources/admin/realtime.py index b9cec978..91740531 100644 --- a/sdks/python/src/bigrag/resources/admin/realtime.py +++ b/sdks/python/src/bigrag/resources/admin/realtime.py @@ -88,18 +88,6 @@ def backups( "/v1/admin/realtime/backups", {"limit": limit, "offset": offset} ) - def vector_migrations( - self, - *, - collection: str | None = None, - limit: int | None = None, - offset: int | None = None, - ) -> AsyncGenerator[AdminRealtimeEvent, None]: - return self._stream( - "/v1/admin/realtime/vector-migrations", - {"collection": collection, "limit": limit, "offset": offset}, - ) - def access_overview( self, *, window_days: int | None = None ) -> AsyncGenerator[AdminRealtimeEvent, None]: diff --git a/sdks/python/src/bigrag/resources/admin/vector_migrations.py b/sdks/python/src/bigrag/resources/admin/vector_migrations.py deleted file mode 100644 index bf8e3715..00000000 --- a/sdks/python/src/bigrag/resources/admin/vector_migrations.py +++ /dev/null @@ -1,56 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING -from urllib.parse import quote - -from bigrag.resources.admin._shared import _pagination -from bigrag.types.admin import ( - VectorMigrationCreateBody, - VectorMigrationJob, - VectorMigrationJobListResponse, -) -from bigrag.types.common import StatusResponse - -if TYPE_CHECKING: - from bigrag._core import BigRAGCore - - -class AdminVectorMigrationsResource: - def __init__(self, client: BigRAGCore) -> None: - self._client = client - - async def list( - self, - *, - collection: str | None = None, - cursor: str | None = None, - include_total: bool | None = None, - limit: int | None = None, - offset: int | None = None, - ) -> VectorMigrationJobListResponse: - params = _pagination(limit=limit, offset=offset) - if collection is not None: - params["collection"] = collection - if cursor is not None: - params["cursor"] = cursor - if include_total is not None: - params["include_total"] = "true" if include_total else "false" - return await self._client._request( - "GET", "/v1/admin/vector-storage/migrations", params=params - ) - - async def get(self, migration_id: str) -> VectorMigrationJob: - return await self._client._request( - "GET", f"/v1/admin/vector-storage/migrations/{quote(migration_id, safe='')}" - ) - - async def create(self, body: VectorMigrationCreateBody) -> VectorMigrationJob: - return await self._client._request( - "POST", "/v1/admin/vector-storage/migrations", json=body - ) - - async def delete(self, migration_id: str) -> StatusResponse: - return await self._client._request( - "DELETE", - f"/v1/admin/vector-storage/migrations/{quote(migration_id, safe='')}", - ) diff --git a/sdks/python/src/bigrag/types/__init__.py b/sdks/python/src/bigrag/types/__init__.py index 97e9ec28..1ccbad5e 100644 --- a/sdks/python/src/bigrag/types/__init__.py +++ b/sdks/python/src/bigrag/types/__init__.py @@ -38,11 +38,6 @@ UpdateMcpServerBody, UpdateUserBody, UserListResponse, - VectorMigrationCreateBody, - VectorMigrationJob, - VectorMigrationJobListResponse, - VectorMigrationJobStatus, - VectorMigrationProvider, ) from bigrag.types.analytics import AnalyticsResponse, PeriodStats, TopQuery from bigrag.types.auth import ( @@ -134,6 +129,7 @@ QueryResponse, QueryResult, QueryTimings, + SearchMode, ) from bigrag.types.sse import ProgressEvent from bigrag.types.usage import CollectionUsage, UsageResponse @@ -201,11 +197,6 @@ "BackupCreateBody", "BackupJob", "BackupJobListResponse", - "VectorMigrationProvider", - "VectorMigrationJobStatus", - "VectorMigrationCreateBody", - "VectorMigrationJob", - "VectorMigrationJobListResponse", "AdminRealtimeEvent", "AccessLogEntry", "AccessLogListResponse", @@ -258,6 +249,7 @@ "QueryBody", "QueryResult", "QueryTimings", + "SearchMode", "QueryResponse", "MultiQueryBody", "MultiQueryResult", diff --git a/sdks/python/src/bigrag/types/admin.py b/sdks/python/src/bigrag/types/admin.py index 06bd6fb7..ed34879e 100644 --- a/sdks/python/src/bigrag/types/admin.py +++ b/sdks/python/src/bigrag/types/admin.py @@ -100,43 +100,6 @@ class BackupJobListResponse(TypedDict): total: int -VectorMigrationProvider = Literal["qdrant", "turbopuffer"] -VectorMigrationJobStatus = Literal[ - "pending", "running", "canceling", "succeeded", "failed" -] - - -class VectorMigrationCreateBody(TypedDict): - collection: str - target_provider: VectorMigrationProvider - - -class VectorMigrationJob(TypedDict): - id: str - collection_id: str | None - collection_name: str - source_provider: VectorMigrationProvider - target_provider: VectorMigrationProvider - status: VectorMigrationJobStatus - phase: str - progress: float - copied_points: int - total_points: int | None - details: dict[str, Any] - error_message: str | None - created_by: str | None - started_at: str | None - completed_at: str | None - created_at: str - updated_at: str - - -class VectorMigrationJobListResponse(TypedDict): - jobs: list[VectorMigrationJob] - total: int | None - next_cursor: str | None - - class AdminRealtimeEvent(TypedDict): event: str data: Any diff --git a/sdks/python/src/bigrag/types/chat.py b/sdks/python/src/bigrag/types/chat.py index 38ee5865..b329fc34 100644 --- a/sdks/python/src/bigrag/types/chat.py +++ b/sdks/python/src/bigrag/types/chat.py @@ -3,7 +3,7 @@ from typing import Any, NotRequired, TypedDict from .documents import MultimodalElementRef -from .query import QueryTimings +from .query import QueryTimings, SearchMode class ChatBody(TypedDict): @@ -14,7 +14,7 @@ class ChatBody(TypedDict): model: NotRequired[str] temperature: NotRequired[float] top_k: NotRequired[int] - search_mode: NotRequired[str] + search_mode: NotRequired[SearchMode] min_score: NotRequired[float | None] rerank: NotRequired[bool | None] filters: NotRequired[dict[str, Any] | None] diff --git a/sdks/python/src/bigrag/types/collections.py b/sdks/python/src/bigrag/types/collections.py index 2cb07b64..93132b9d 100644 --- a/sdks/python/src/bigrag/types/collections.py +++ b/sdks/python/src/bigrag/types/collections.py @@ -2,19 +2,18 @@ from typing import Any, NotRequired, TypedDict +from .query import SearchMode + class Collection(TypedDict): id: str name: str description: str - embedding_provider: str embedding_model: str - vector_store_provider: str dimension: int chunk_size: int chunk_overlap: int chunk_strategy: str - index_type: str tenant_field: str | None has_metadata_schema: bool document_count: int @@ -27,7 +26,7 @@ class Collection(TypedDict): multimodal_enrichment_enabled: bool default_top_k: int default_min_score: float | None - default_search_mode: str + default_search_mode: SearchMode metadata: dict[str, Any] created_at: str updated_at: str @@ -50,9 +49,7 @@ class CollectionStatsResponse(TypedDict): class CreateCollectionBody(TypedDict): name: str description: NotRequired[str] - vector_store_provider: NotRequired[str] embedding_preset_id: NotRequired[str] - embedding_provider: NotRequired[str] embedding_model: NotRequired[str] embedding_api_key: NotRequired[str] embedding_base_url: NotRequired[str] @@ -60,7 +57,6 @@ class CreateCollectionBody(TypedDict): chunk_size: NotRequired[int] chunk_overlap: NotRequired[int] chunk_strategy: NotRequired[str] - index_type: NotRequired[str] tenant_field: NotRequired[str] metadata_schema: NotRequired[dict[str, Any]] metadata: NotRequired[dict[str, Any]] @@ -71,7 +67,7 @@ class CreateCollectionBody(TypedDict): multimodal_enrichment_enabled: NotRequired[bool] default_top_k: NotRequired[int] default_min_score: NotRequired[float] - default_search_mode: NotRequired[str] + default_search_mode: NotRequired[SearchMode] class UpdateCollectionBody(TypedDict, total=False): @@ -85,6 +81,6 @@ class UpdateCollectionBody(TypedDict, total=False): multimodal_enrichment_enabled: bool default_top_k: int default_min_score: float - default_search_mode: str + default_search_mode: SearchMode chunk_strategy: str metadata_schema: dict[str, Any] diff --git a/sdks/python/src/bigrag/types/common.py b/sdks/python/src/bigrag/types/common.py index 40a21054..58a19631 100644 --- a/sdks/python/src/bigrag/types/common.py +++ b/sdks/python/src/bigrag/types/common.py @@ -18,8 +18,6 @@ class ReadinessResponse(TypedDict): version: str postgres: bool vector_store: bool - vector_store_provider: NotRequired[str] - qdrant: NotRequired[bool | None] redis: bool embedding: NotRequired[bool] embedding_source: NotRequired[str] diff --git a/sdks/python/src/bigrag/types/evaluations.py b/sdks/python/src/bigrag/types/evaluations.py index 655e41ca..d7cbe1b1 100644 --- a/sdks/python/src/bigrag/types/evaluations.py +++ b/sdks/python/src/bigrag/types/evaluations.py @@ -2,6 +2,8 @@ from typing import NotRequired, TypedDict +from .query import SearchMode + class EvalCase(TypedDict): query: str @@ -13,7 +15,7 @@ class EvalBody(TypedDict): collection: str cases: list[EvalCase] top_k: NotRequired[int] - search_mode: NotRequired[str] + search_mode: NotRequired[SearchMode] class EvalPerCase(TypedDict): diff --git a/sdks/python/src/bigrag/types/query.py b/sdks/python/src/bigrag/types/query.py index f707838a..e13e13c4 100644 --- a/sdks/python/src/bigrag/types/query.py +++ b/sdks/python/src/bigrag/types/query.py @@ -1,16 +1,18 @@ from __future__ import annotations -from typing import Any, NotRequired, TypedDict +from typing import Any, Literal, NotRequired, TypedDict from .documents import MultimodalElementRef +SearchMode = Literal["semantic", "keyword", "hybrid"] + class QueryBody(TypedDict): query: str top_k: NotRequired[int] filters: NotRequired[dict[str, Any]] min_score: NotRequired[float] - search_mode: NotRequired[str] + search_mode: NotRequired[SearchMode] rerank: NotRequired[bool] multimodal: NotRequired[bool] @@ -52,7 +54,7 @@ class MultiQueryBody(TypedDict): top_k: NotRequired[int] filters: NotRequired[dict[str, Any]] min_score: NotRequired[float] - search_mode: NotRequired[str] + search_mode: NotRequired[SearchMode] rerank: NotRequired[bool] multimodal: NotRequired[bool] @@ -82,7 +84,7 @@ class BatchQueryItem(TypedDict): top_k: NotRequired[int] filters: NotRequired[dict[str, Any]] min_score: NotRequired[float] - search_mode: NotRequired[str] + search_mode: NotRequired[SearchMode] rerank: NotRequired[bool] multimodal: NotRequired[bool] diff --git a/sdks/typescript/src/resources/admin/index.ts b/sdks/typescript/src/resources/admin/index.ts index 3abcd8e8..0195cf4b 100644 --- a/sdks/typescript/src/resources/admin/index.ts +++ b/sdks/typescript/src/resources/admin/index.ts @@ -9,7 +9,6 @@ import { AdminMcpServersResource } from "./mcp_servers.js"; import { AdminRealtimeResource } from "./realtime.js"; import { AdminSettingsResource } from "./settings.js"; import { AdminUsersResource } from "./users.js"; -import { AdminVectorMigrationsResource } from "./vector_migrations.js"; export { AdminAccessResource } from "./access.js"; export { AdminApiKeysResource } from "./api_keys.js"; @@ -21,7 +20,6 @@ export { AdminMcpServersResource } from "./mcp_servers.js"; export { AdminRealtimeResource } from "./realtime.js"; export { AdminSettingsResource } from "./settings.js"; export { AdminUsersResource } from "./users.js"; -export { AdminVectorMigrationsResource } from "./vector_migrations.js"; export class AdminResource { readonly users: AdminUsersResource; @@ -34,7 +32,6 @@ export class AdminResource { readonly mcpServers: AdminMcpServersResource; readonly realtime: AdminRealtimeResource; readonly settings: AdminSettingsResource; - readonly vectorMigrations: AdminVectorMigrationsResource; constructor(client: RequestClient) { this.users = new AdminUsersResource(client); @@ -47,6 +44,5 @@ export class AdminResource { this.mcpServers = new AdminMcpServersResource(client); this.realtime = new AdminRealtimeResource(client); this.settings = new AdminSettingsResource(client); - this.vectorMigrations = new AdminVectorMigrationsResource(client); } } diff --git a/sdks/typescript/src/resources/admin/realtime.ts b/sdks/typescript/src/resources/admin/realtime.ts index f90cdedd..457c4170 100644 --- a/sdks/typescript/src/resources/admin/realtime.ts +++ b/sdks/typescript/src/resources/admin/realtime.ts @@ -17,7 +17,6 @@ import type { ReadinessResponse, UploadSession, UsageResponse, - VectorMigrationJobListResponse, } from "../../types/index.js"; export class AdminRealtimeResource { @@ -102,16 +101,6 @@ export class AdminRealtimeResource { return this._stream("/v1/admin/realtime/backups", options); } - vectorMigrations( - options: { collection?: string; limit?: number; offset?: number } = {}, - ): AsyncGenerator> { - return this._stream("/v1/admin/realtime/vector-migrations", { - collection: options.collection, - limit: options.limit, - offset: options.offset, - }); - } - accessOverview( options: { windowDays?: number } = {}, ): AsyncGenerator> { diff --git a/sdks/typescript/src/resources/admin/vector_migrations.ts b/sdks/typescript/src/resources/admin/vector_migrations.ts deleted file mode 100644 index 7fdda41a..00000000 --- a/sdks/typescript/src/resources/admin/vector_migrations.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { RequestClient } from "../../core.js"; -import type { - StatusResponse, - VectorMigrationCreateBody, - VectorMigrationJob, - VectorMigrationJobListResponse, -} from "../../types/index.js"; -import { pagination } from "./_shared.js"; - -type VectorMigrationListOptions = { - collection?: string; - cursor?: string; - includeTotal?: boolean; - limit?: number; - offset?: number; -}; - -export class AdminVectorMigrationsResource { - constructor(private readonly _client: RequestClient) {} - - list(options: VectorMigrationListOptions = {}): Promise { - return this._client._request("GET", "/v1/admin/vector-storage/migrations", { - params: { - ...pagination(options), - ...(options.collection ? { collection: options.collection } : {}), - ...(options.cursor ? { cursor: options.cursor } : {}), - ...(options.includeTotal === undefined - ? {} - : { include_total: String(options.includeTotal) }), - }, - }); - } - - get(migrationId: string): Promise { - return this._client._request( - "GET", - `/v1/admin/vector-storage/migrations/${encodeURIComponent(migrationId)}`, - ); - } - - create(body: VectorMigrationCreateBody): Promise { - return this._client._request("POST", "/v1/admin/vector-storage/migrations", { - json: body, - }); - } - - delete(migrationId: string): Promise { - return this._client._request( - "DELETE", - `/v1/admin/vector-storage/migrations/${encodeURIComponent(migrationId)}`, - ); - } -} diff --git a/sdks/typescript/src/types/admin.ts b/sdks/typescript/src/types/admin.ts index 3fba3dfb..1014ca28 100644 --- a/sdks/typescript/src/types/admin.ts +++ b/sdks/typescript/src/types/admin.ts @@ -93,40 +93,6 @@ export interface BackupJobListResponse { total: number; } -export type VectorMigrationProvider = "qdrant" | "turbopuffer"; -export type VectorMigrationJobStatus = "pending" | "running" | "canceling" | "succeeded" | "failed"; - -export interface VectorMigrationCreateBody { - collection: string; - target_provider: VectorMigrationProvider; -} - -export interface VectorMigrationJob { - id: string; - collection_id: string | null; - collection_name: string; - source_provider: VectorMigrationProvider; - target_provider: VectorMigrationProvider; - status: VectorMigrationJobStatus; - phase: string; - progress: number; - copied_points: number; - total_points: number | null; - details: Record; - error_message: string | null; - created_by: string | null; - started_at: string | null; - completed_at: string | null; - created_at: string; - updated_at: string; -} - -export interface VectorMigrationJobListResponse { - jobs: VectorMigrationJob[]; - total: number | null; - next_cursor: string | null; -} - export interface AdminRealtimeSnapshot { event: "snapshot"; data: { diff --git a/sdks/typescript/src/types/collections.ts b/sdks/typescript/src/types/collections.ts index b488d5f0..af05fd2f 100644 --- a/sdks/typescript/src/types/collections.ts +++ b/sdks/typescript/src/types/collections.ts @@ -2,14 +2,11 @@ export interface Collection { id: string; name: string; description: string; - embedding_provider: string; embedding_model: string; - vector_store_provider: "qdrant" | "turbopuffer"; dimension: number; chunk_size: number; chunk_overlap: number; chunk_strategy: string; - index_type: string; tenant_field: string | null; has_metadata_schema: boolean; document_count: number; @@ -52,9 +49,7 @@ export interface CollectionStatsResponse { export interface CreateCollectionBody { name: string; description?: string; - vector_store_provider?: "qdrant" | "turbopuffer"; embedding_preset_id?: string; - embedding_provider?: string; embedding_model?: string; embedding_api_key?: string; embedding_base_url?: string; @@ -62,7 +57,6 @@ export interface CreateCollectionBody { chunk_size?: number; chunk_overlap?: number; chunk_strategy?: "paragraph" | "recursive"; - index_type?: "HNSW"; tenant_field?: string; metadata_schema?: Record; metadata?: Record; diff --git a/sdks/typescript/src/types/common.ts b/sdks/typescript/src/types/common.ts index 4793f26e..14e42601 100644 --- a/sdks/typescript/src/types/common.ts +++ b/sdks/typescript/src/types/common.ts @@ -13,8 +13,6 @@ export interface ReadinessResponse { version: string; postgres: boolean; vector_store: boolean; - vector_store_provider?: string; - qdrant?: boolean | null; redis: boolean; embedding?: boolean; embedding_source?: string; diff --git a/website/components/home/code-section.tsx b/website/components/home/code-section.tsx index c6fa7207..9f28b862 100644 --- a/website/components/home/code-section.tsx +++ b/website/components/home/code-section.tsx @@ -39,7 +39,7 @@ export const CodeSection = () => (
- Qdrant vector database with HNSW search and cosine similarity + Turbopuffer vector search with semantic, keyword, and hybrid modes
diff --git a/website/content/docs/admin-ui.mdx b/website/content/docs/admin-ui.mdx index 48061fdf..93ddf89a 100644 --- a/website/content/docs/admin-ui.mdx +++ b/website/content/docs/admin-ui.mdx @@ -1,6 +1,6 @@ --- title: Admin UI -description: Setup, collections, chat, connectors, API keys, usage, audit, webhooks, data storage, vector storage, backups, and settings. +description: Setup, collections, chat, connectors, API keys, usage, audit, webhooks, data storage, backups, and settings. --- import { Callout } from "fumadocs-ui/components/callout"; @@ -30,6 +30,7 @@ For cross-site production deployments, enable secure session cookies and usually | Path | Purpose | |------|---------| | `/setup` | One-time page to create the first admin account. Disappears once `needs_setup: false`. | +| `/onboarding` | Authenticated first-run provider setup. Requires one verified embedding preset, prompts for Turbopuffer, and then continues to `/overview`. | | `/login` | Email + password login for admins and members. | | `/overview` | Platform-wide stats — collection count, document states, stored token count, queue depth, readiness, and worker heartbeat. | | `/collections` | List, create, delete, and search collections. | @@ -39,7 +40,7 @@ For cross-site production deployments, enable secure session cookies and usually | `/collections/[name]/connectors` | Redirects to the first available collection connector from the provider catalog. | | `/collections/[name]/connectors/google-drive` | Browse Google Drive, sync selected files/folders, monitor Drive sync progress, trigger manual resync, and manage sync schedules. Source creation and manual sync are disabled when the worker is offline. | | `/collections/[name]/search` | Query tester scoped to the collection — bounded requests, cancel/retry states, result metadata, document filenames, and chunk deep links. | -| `/collections/[name]/settings` | Edit description, metadata, metadata schema, reranking, chunking, default query params, and collection vector migration. | +| `/collections/[name]/settings` | Edit description, metadata, metadata schema, reranking, chunking, and default query params. | | `/chat` | Browser-local playground chat UI; retrieve from one collection, stream cited answers, render safe markdown, and keep local sessions across reloads. | | `/models` | Manage [embedding presets](/docs/api-reference/embedding-presets), including provider connection tests and OpenAI-compatible base URLs, plus fallback embedding, retrieval, and chat model settings. | | `/mcp` | Mint, scope, rotate, and revoke [MCP server](/docs/api-reference/mcp-servers) credentials for Claude Desktop / Cursor / custom MCP clients. | @@ -51,13 +52,12 @@ For cross-site production deployments, enable secure session cookies and usually | `/connectors` | Browse the provider catalog, configure connector credentials, and review account state. | | `/backups` | Configure readable backup storage, start full-instance exports, and monitor backup history. Backup starts are disabled when the worker heartbeat is offline or no bucket is configured. | | `/data-storage` | Configure uploaded source-file storage for local disk or S3-compatible object stores. | -| `/vector-storage` | Review vector provider health and per-collection routing/stats, configure Qdrant and turbopuffer connection settings, and migrate collections between configured providers. | | `/settings` | Operator-console settings workspace for account, platform, and data sections. | | `/evals` | Run golden-set retrieval evaluations. | Unknown admin routes render the built-in 404 screen with links back to overview and collections. Unexpected route render failures use the admin 500 screen with a retry action, overview fallback, and a readable error-details panel showing the boundary error message, cause, and digest when available. -The sidebar is grouped by operator task: Workspace for day-to-day retrieval surfaces, Interfaces for client and integration entry points, Observability for logs and spend, and Administration for backup, data storage, vector storage, and instance settings. Member accounts only see groups that still contain at least one available destination. Standard admin pages share one centered dashboard body width so page headers, tabs, and primary panels align across routes; shared tabs rely on the page stack gap so spacing above and below the tab row stays even. The desktop sidebar keeps matching top and bottom insets so it reads as a floating surface, while page scroll areas still keep their own bottom edge available so footer actions are not clipped by a reserved app gutter. The footer account menu opens almost as wide as the sidebar footer trigger, with a small horizontal inset. +The sidebar is grouped by operator task: Workspace for day-to-day retrieval surfaces, Interfaces for client and integration entry points, Observability for logs and spend, and Administration for backup, data storage, and instance settings. Member accounts only see groups that still contain at least one available destination. Standard admin pages share one centered dashboard body width so page headers, tabs, and primary panels align across routes; shared tabs rely on the page stack gap so spacing above and below the tab row stays even. The desktop sidebar keeps matching top and bottom insets so it reads as a floating surface, while page scroll areas still keep their own bottom edge available so footer actions are not clipped by a reserved app gutter. The footer account menu opens almost as wide as the sidebar footer trigger, with a small horizontal inset. ## Settings sections @@ -65,22 +65,20 @@ The `/models` page owns reusable embedding presets and runtime model settings. ` The `/connectors` page renders provider tabs above the selected provider setup panel. It owns provider credentials and account state through `GET /v1/admin/connectors/{provider}`, `GET /v1/connectors/{provider}/account`, `PUT /v1/admin/connectors/{provider}`, and `POST /v1/connectors/{provider}/disconnect`. The collection connector pages read from the same provider catalog while staying scoped to browsing, source selection, sync schedules, and sync-job progress. -The `/backups` page renders backup destination settings directly above export controls backed by `GET /v1/admin/backups`, `GET /v1/admin/realtime/backups`, and `POST /v1/admin/backups` so backup operations stay out of the general Settings workspace. Backup starts are rejected while a vector migration is pending, running, or canceling. Usage, audit, data storage, and vector storage also live as first-class sidebar pages at `/usage`, `/audit`, `/data-storage`, and `/vector-storage`. +The `/backups` page renders backup destination settings directly above export controls backed by `GET /v1/admin/backups`, `GET /v1/admin/realtime/backups`, and `POST /v1/admin/backups` so backup operations stay out of the general Settings workspace. Usage, audit, and data storage also live as first-class sidebar pages at `/usage`, `/audit`, and `/data-storage`. The `/data-storage` page is a standalone runtime settings surface for uploaded source-file storage. It uses `GET /v1/admin/settings` and `PUT /v1/admin/settings` for local disk and S3-compatible object-store settings. -The `/vector-storage` page is a standalone runtime settings surface for vector provider connection details. It separates Qdrant and turbopuffer into provider tabs and uses `GET /v1/admin/settings` and `PUT /v1/admin/settings`; vector migrations use `POST /v1/admin/vector-storage/migrations`, `GET /v1/admin/vector-storage/migrations`, `DELETE /v1/admin/vector-storage/migrations/{migration_id}`, and `GET /v1/admin/realtime/vector-migrations`. A migration pauses writes, copies existing vectors, switches the collection, and deletes old source vectors after cutover. Stop-and-delete remains available while the migration maintenance lock is active. - The settings page is a compact task-oriented workspace: - **Top tab control** for Account, Health, Security, and Data — no nested settings sidebar; each tab opens directly into its area without a repeated section heading. - **Stacked layout** — settings content is stacked sequentially rather than split into desktop columns. Common controls appear first; raw setting keys and source metadata stay inside Advanced controls; each runtime panel keeps its guidance plus Save action together in the panel footer. - **Security tab** focuses on trusted-proxy handling, outbound URL policy, and embedding-cache posture. CORS origins and session-cookie deployment controls stay in deployment config and the instance settings API. -- **Data tab** covers ingestion, queue, retention, and webhook runtime settings after file storage moved to `/data-storage`. +- **Data tab** covers Turbopuffer, ingestion, queue, retention, and webhook runtime settings after file storage moved to `/data-storage`. - **Placeholders** — empty controls show default or example values so unset settings remain readable. -- **Validation** — saving probes applicable storage, backup, and vector targets before persistence. +- **Validation** — saving probes applicable storage, backup, and Turbopuffer targets before persistence. -Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`. Backups, usage, audit, connectors, models, evals, data storage, and vector storage are standalone routes rather than Settings deep-link redirects. +Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`. Backups, usage, audit, connectors, models, evals, and data storage are standalone routes rather than Settings deep-link redirects. | Group | Section | Reads from | Writes to | |-------|---------|-----------|-----------| @@ -126,6 +124,6 @@ The admin UI uses session cookies exclusively. To automate anything the admin UI ## When to skip the Admin UI -- **Pure backend deployment** — no admin UI process, everything via API keys and SDKs. Set `BIGRAG_ENV=prod` and use [`POST /v1/auth/setup`](/docs/api-reference/authentication) from an admin workstation to bootstrap. +- **Pure backend deployment** — no admin UI process, everything via API keys and SDKs. Set `BIGRAG_ENV=prod`, use [`POST /v1/auth/setup`](/docs/api-reference/authentication) from an admin workstation to bootstrap, then create at least one embedding preset through the admin API before indexing documents. - **Embedded experiences** — if you're surfacing search inside your own app, skip admin UI and build against the [query API](/docs/api-reference/query). - **Read-only operators** — create a `member` account; they get `/overview`, `/collections` (read), and `/chat` but can't mutate. diff --git a/website/content/docs/api-reference/admin-realtime.mdx b/website/content/docs/api-reference/admin-realtime.mdx index a7ee45cc..9bb38781 100644 --- a/website/content/docs/api-reference/admin-realtime.mdx +++ b/website/content/docs/api-reference/admin-realtime.mdx @@ -48,14 +48,13 @@ Connector source streams accept `collection`. Sync-job streams accept `collectio | `GET /v1/admin/realtime/access/logs` | `GET /v1/admin/access/logs` | | `GET /v1/admin/realtime/audit` | `GET /v1/admin/audit` | | `GET /v1/admin/realtime/backups` | `GET /v1/admin/backups` | -| `GET /v1/admin/realtime/vector-migrations` | `GET /v1/admin/vector-storage/migrations` | | `GET /v1/admin/realtime/usage` | `GET /v1/usage` | | `GET /v1/admin/realtime/platform/stats` | `GET /v1/stats` | | `GET /v1/admin/realtime/platform/readiness` | `GET /health/ready` | These streams support the same query parameters as their REST counterparts. Where no domain event source exists yet, the backend emits full snapshots from a server-side refresh tick so browsers do not need to poll. -Backup streams refresh quickly while jobs are pending or running. Vector migration streams refresh quickly while jobs are pending, running, or canceling so stop-and-delete progress is visible. +Backup streams refresh quickly while jobs are pending or running. ## API Client Fallback diff --git a/website/content/docs/api-reference/authentication.mdx b/website/content/docs/api-reference/authentication.mdx index 4f0e95b4..c70bc6fe 100644 --- a/website/content/docs/api-reference/authentication.mdx +++ b/website/content/docs/api-reference/authentication.mdx @@ -32,7 +32,7 @@ curl -X POST http://localhost:4000/v1/auth/setup \ Once at least one admin exists, `setup-status` returns `{"needs_setup": false}` and `/v1/auth/setup` returns `409`. -The admin UI exposes the same flow at `http://localhost:3000/setup`. +The admin UI exposes the same flow at `http://localhost:3000/setup`. After the first admin is created, the UI redirects the signed-in admin to `/onboarding` to create a verified embedding preset and optionally save Turbopuffer settings before continuing to `/overview`. The setup endpoint itself still only creates the first admin and session. ## Session auth (admin UI, browser) diff --git a/website/content/docs/api-reference/backups.mdx b/website/content/docs/api-reference/backups.mdx index 8aab7f02..ccb416c3 100644 --- a/website/content/docs/api-reference/backups.mdx +++ b/website/content/docs/api-reference/backups.mdx @@ -70,15 +70,15 @@ Content-Type: application/json Creates a backup job and enqueues it on the Dramatiq backup worker. Only one backup can be pending or running at a time. -While the job runs, bigRAG acquires a maintenance lock. Mutating API requests are rejected with `423`, new ingestion jobs are rejected, Dramatiq ingestion and connector actors delay themselves, and scheduled Google Drive syncs do not start. Backups and vector migrations share the same maintenance lane, so a pending or running backup blocks new vector migrations and a pending, running, or canceling vector migration blocks new backups. +While the job runs, bigRAG acquires a maintenance lock. Mutating API requests are rejected with `423`, new ingestion jobs are rejected, Dramatiq ingestion and connector actors delay themselves, and scheduled Google Drive syncs do not start. The backup fails if: - The backup bucket settings are incomplete or invalid. -- A configured vector-store client cannot be reached. +- Turbopuffer cannot be reached. - A stored upload file referenced by a document row is missing. -Starting a backup returns `409` when another backup, vector migration, or maintenance lock is active. +Starting a backup returns `409` when another backup or maintenance lock is active. ## Realtime stream diff --git a/website/content/docs/api-reference/collections.mdx b/website/content/docs/api-reference/collections.mdx index e78ae42c..84b1dac3 100644 --- a/website/content/docs/api-reference/collections.mdx +++ b/website/content/docs/api-reference/collections.mdx @@ -32,12 +32,10 @@ GET /v1/collections "description": "Academic research papers", "embedding_provider": "openai", "embedding_model": "text-embedding-3-small", - "vector_store_provider": "qdrant", "dimension": 1536, "chunk_size": 512, "chunk_overlap": 50, "chunk_strategy": "paragraph", - "index_type": "HNSW", "tenant_field": null, "has_metadata_schema": false, "document_count": 15, @@ -74,12 +72,10 @@ POST /v1/collections "description": "Academic research papers", "embedding_provider": "openai", "embedding_model": "text-embedding-3-small", - "vector_store_provider": "qdrant", "dimension": 1536, "chunk_size": 512, "chunk_overlap": 50, "chunk_strategy": "paragraph", - "index_type": "HNSW", "tenant_field": "tenant_id", "metadata_schema": { "type": "object", @@ -102,7 +98,6 @@ POST /v1/collections | `description` | string | no | `""` | — | | `embedding_provider` | string | no | Server default | `openai`, `cohere`, `voyage`, `openai_compatible` | | `embedding_model` | string | no | Server default | Model name | -| `vector_store_provider` | string | no | `"qdrant"` | `qdrant` or `turbopuffer`; changed only through a vector migration job. `turbopuffer` requires a saved turbopuffer API key in Vector Storage before creation or migration | | `embedding_preset_id` | string | no | — | Link the collection to a saved embedding preset. The collection inherits the preset's provider, model, dimension, base URL, and key live | | `embedding_api_key` | string | no | — | Required when `embedding_preset_id` is not provided; validated against the provider before the collection is created. Use any non-empty value for local OpenAI-compatible gateways without auth. Ignored when a preset is supplied — the collection inherits the preset's key live | | `embedding_base_url` | string | no | — | Required for `openai_compatible` (Ollama, vLLM, TEI, LiteLLM, Azure, Bedrock) | @@ -110,8 +105,7 @@ POST /v1/collections | `chunk_size` | integer | no | `512` | 64–10,000 | | `chunk_overlap` | integer | no | `50` | 0–5,000, must be `< chunk_size` | | `chunk_strategy` | string | no | `"paragraph"` | `paragraph` (split on blank lines) or `recursive` (hierarchical) | -| `index_type` | string | no | `"HNSW"` | Dense-vector index preference | -| `tenant_field` | string | no | — | Metadata key indexed by supported vector stores and required on uploads, raw vector upserts, query filters, and chat filters | +| `tenant_field` | string | no | — | Metadata key required on uploads, raw vector upserts, query filters, and chat filters | | `metadata_schema` | object | no | — | JSON Schema validated on every upload / upsert | | `default_top_k` | integer | no | `10` | 1–1,000 | | `default_min_score` | float | no | `null` | Minimum similarity score | @@ -127,8 +121,8 @@ POST /v1/collections **Errors:** -- `400` — Invalid name, chunk config, dimension mismatch, or unconfigured vector provider -- `502` — Selected vector provider rejected collection provisioning +- `400` — Invalid name, chunk config, or dimension mismatch +- `502` — Vector store rejected collection provisioning - `409` — Collection name already exists ## Get Collection @@ -166,7 +160,7 @@ Lightweight endpoint returning document and chunk counts. PUT /v1/collections/{name} ``` -Only the fields below are mutable. Embedding provider / model / dimension, chunk size / overlap, `index_type`, and `tenant_field` are fixed at creation. Use `POST /v1/admin/vector-storage/migrations` to change a collection's vector store provider. +Only the fields below are mutable. Embedding provider / model / dimension, chunk size / overlap, and `tenant_field` are fixed at creation. | Field | Type | Notes | |-------|------|-------| diff --git a/website/content/docs/api-reference/health.mdx b/website/content/docs/api-reference/health.mdx index f196c1df..6324606c 100644 --- a/website/content/docs/api-reference/health.mdx +++ b/website/content/docs/api-reference/health.mdx @@ -21,7 +21,7 @@ No authentication required. Returns 200 if the server is running. GET /health/ready ``` -No authentication required. Tests connectivity to Postgres, configured vector-store clients, Redis, and the configured embedding provider. +No authentication required. Tests connectivity to Postgres, Turbopuffer, Redis, and the configured embedding provider. **Response** `200`: @@ -31,8 +31,6 @@ No authentication required. Tests connectivity to Postgres, configured vector-st "version": "2026.4.30", "postgres": true, "vector_store": true, - "vector_store_provider": "per_collection", - "qdrant": true, "redis": true, "embedding": true, "embedding_source": "settings" @@ -47,8 +45,6 @@ When a dependency is unreachable or misconfigured: "version": "2026.4.30", "postgres": true, "vector_store": true, - "vector_store_provider": "per_collection", - "qdrant": true, "redis": false, "redis_error": "unreachable", "embedding": false, diff --git a/website/content/docs/api-reference/instance-settings.mdx b/website/content/docs/api-reference/instance-settings.mdx index 409bdf37..0a987635 100644 --- a/website/content/docs/api-reference/instance-settings.mdx +++ b/website/content/docs/api-reference/instance-settings.mdx @@ -16,7 +16,7 @@ The Settings UI is optimized for day-to-day operators. It does not show every re | `/settings → Security` | Trusted proxy ranges, outbound provider URL policy, private-network escape hatches, local webhook policy, and embedding-cache posture. | | `/data-storage` | Uploaded source-file storage for local disk and S3-compatible object stores. | | Deployment config or this API | CORS origins and session-cookie flags: `cors_origins`, `session_cookie_secure`, `session_cookie_samesite`, and `session_cookie_domain`. These can lock admins out when changed incorrectly, so they are best handled deliberately during deployment. | -| Other Settings pages | Ingestion, queue, retention, webhooks, model defaults, vector storage, and backups. | +| Other Settings pages | Ingestion, queue, retention, webhooks, model defaults, Turbopuffer vector storage, and backups. | ## List Settings @@ -101,9 +101,9 @@ Deployment-managed security keys use the same endpoint when you intentionally au } ``` -Security hardening settings include `allow_public_bind_in_prod`. Ingestion settings include raw vector API request caps (`max_vector_upsert_count`, `max_vector_delete_count`, `max_vector_text_chars`, `max_vector_metadata_bytes`). UI-visible settings can be changed from the admin UI. Registry keys that are not rendered in the UI remain available through this API and the SDK admin settings resources. +Security hardening settings include `allow_public_bind_in_prod`. Ingestion settings include raw vector API request caps (`max_vector_upsert_count`, `max_vector_delete_count`, `max_vector_text_chars`, `max_vector_metadata_bytes`). Turbopuffer settings include `turbopuffer_api_key`, `turbopuffer_base_url`, `turbopuffer_region`, and `turbopuffer_namespace_prefix`. UI-visible settings can be changed from the admin UI. Registry keys that are not rendered in the UI remain available through this API and the SDK admin settings resources. -Storage and vector-store connection changes are validated before they are saved. Existing collections keep their current provider until an admin starts a vector migration job. +Storage and Turbopuffer connection changes are validated before they are saved and apply to the running API. ## Test Settings @@ -111,7 +111,7 @@ Storage and vector-store connection changes are validated before they are saved. POST /v1/admin/settings/test ``` -Validates the submitted values without saving them. Storage settings run a lightweight backend construction/probe when S3 is selected. Backup settings run a lightweight S3-compatible bucket probe. Save operations use the same validation path for changed keys; this endpoint remains useful for SDKs and automation that want a dry run. +Validates the submitted values without saving them. Storage settings run a lightweight backend construction/probe when S3 is selected. Backup settings run a lightweight S3-compatible bucket probe. Turbopuffer settings run the same connectivity validation used by save operations. This endpoint remains useful for SDKs and automation that want a dry run. ```json { "values": { "storage_backend": "s3", "storage_s3_bucket": "bigrag-documents" } } @@ -121,6 +121,10 @@ Validates the submitted values without saving them. Storage settings run a light { "values": { "backup_s3_bucket": "bigrag-backups", "backup_s3_endpoint_url": "https://ACCOUNT.r2.cloudflarestorage.com", "backup_s3_region": "auto" } } ``` +```json +{ "values": { "turbopuffer_api_key": "tpuf_...", "turbopuffer_region": "aws-us-east-1", "turbopuffer_namespace_prefix": "prod_" } } +``` + ## Reset Settings ``` diff --git a/website/content/docs/api-reference/vectors.mdx b/website/content/docs/api-reference/vectors.mdx index 2f397295..6bb26936 100644 --- a/website/content/docs/api-reference/vectors.mdx +++ b/website/content/docs/api-reference/vectors.mdx @@ -15,19 +15,19 @@ Scoped API keys need `vector:write` for upsert and `vector:delete` for delete. ` GET /v1/admin/vector-storage/overview ``` -Session-only. Returns configured vector providers, provider health, and per-collection routing/stats for the admin UI `/vector-storage` page. +Session-only. Returns Turbopuffer health plus collection and vector totals for admin diagnostics. ```json { - "fallback_provider": "collection", - "configured_providers": ["qdrant", "turbopuffer"], "provider_health": { - "qdrant": { "configured": true, "status": "ok", "error": null } + "name": "turbopuffer", + "configured": true, + "status": "ok", + "error": null }, "collections": [ { "name": "docs", - "provider": "qdrant", "documents": 12, "chunks": 480, "bytes": 1048576 @@ -37,66 +37,6 @@ Session-only. Returns configured vector providers, provider health, and per-coll } ``` -## Admin Vector Migrations - -``` -GET /v1/admin/vector-storage/migrations -POST /v1/admin/vector-storage/migrations -GET /v1/admin/vector-storage/migrations/{migration_id} -DELETE /v1/admin/vector-storage/migrations/{migration_id} -GET /v1/admin/realtime/vector-migrations -``` - -Session-only. Migrates one collection in place from its current vector provider to a configured target provider. - -`GET /v1/admin/vector-storage/migrations` accepts: - -| Name | Type | Default | Notes | -|------|------|---------|-------| -| `collection` | string | — | Filter jobs to one collection | -| `limit` | integer | `20` | 1-100 | -| `offset` | integer | `0` | Offset pagination when `cursor` is omitted | -| `cursor` | string | — | Cursor returned as `next_cursor` | -| `include_total` | boolean | `false` | Include the total job count for the filter | - -`POST /v1/admin/vector-storage/migrations` starts a background maintenance job: - -```json -{ - "collection": "docs", - "target_provider": "turbopuffer" -} -``` - -The job pauses writes with the maintenance lock, waits for active ingestion and connector sync work to drain, copies provider-neutral vector points, switches `vector_store_provider`, invalidates collection/search caches, and deletes the old source provider collection after a successful cutover. Backups and vector migrations share the same maintenance lane, so a pending or running backup blocks new vector migrations and a pending, running, or canceling vector migration blocks new backups. - -`DELETE /v1/admin/vector-storage/migrations/{migration_id}` deletes completed or failed migration records. If the job is pending, running, or canceling, it requests a stop first and removes the record after the worker has cleaned up any partial target collection. A migration that has already entered cutover finishes cleanup before removal. This endpoint remains available while the maintenance lock is active so operators can stop a running migration. - -**Response** `201`: - -```json -{ - "id": "0196f3c9-8f45-7d39-9ad3-cfc3f34bd23d", - "collection_name": "docs", - "source_provider": "qdrant", - "target_provider": "turbopuffer", - "status": "pending", - "phase": "queued", - "progress": 0, - "copied_points": 0, - "total_points": null, - "error_message": null -} -``` - -`GET /v1/admin/realtime/vector-migrations` is an SSE stream whose `snapshot` payload matches the list response. Add `?collection=docs` to scope the stream to one collection. Pending, running, and canceling jobs keep the stream on the short refresh interval. - -**Errors:** - -- `400` — Collection missing or target provider not configured -- `409` — The collection already uses the target provider, another migration or backup is active, or another maintenance lock is active -- `404` — Migration job not found - ## Upsert Vectors ``` diff --git a/website/content/docs/comparison.mdx b/website/content/docs/comparison.mdx index 723c4980..93018ba2 100644 --- a/website/content/docs/comparison.mdx +++ b/website/content/docs/comparison.mdx @@ -22,7 +22,7 @@ There are many ways to build a RAG pipeline. This page compares bigRAG to popula | **Python SDK** | Yes | Yes | No | No | No | Yes | Yes | Yes | Yes | | **Web UI** | Yes (admin UI) | Yes | Yes | Yes | Yes | No | No | No | Yes | | **LLM generation** | Yes (chat API) | Yes | Yes | Yes | Yes | Via code | Via code | Via code | Yes | -| **Vector DB** | Qdrant default, turbopuffer | OpenSearch | Elasticsearch | 9 options | Qdrant or pgvector | Integrations | Integrations | Integrations | Proprietary | +| **Vector DB** | Turbopuffer | OpenSearch | Elasticsearch | 9 options | Local vector DB or pgvector | Integrations | Integrations | Integrations | Proprietary | | **Multi-collection** | Yes | No | Yes | Yes | No | N/A | N/A | N/A | Yes | | **Setup complexity** | Low | Low | Medium | Low | Medium | High | High | High | None | @@ -68,7 +68,7 @@ These are the closest alternatives to bigRAG — self-hostable platforms that ha **Where bigRAG is a better fit:** - Lighter footprint — RAGFlow requires 4+ cores and 16 GB RAM minimum - API-first design for developers building integrations, not end-user apps -- Purpose-built vector storage: Qdrant by default, with turbopuffer as a managed cloud backend +- Purpose-built Turbopuffer storage for semantic, keyword, and hybrid search - Webhook-driven architecture for event-based workflows - Simpler deployment and configuration diff --git a/website/content/docs/concepts/architecture.mdx b/website/content/docs/concepts/architecture.mdx index 3536cd83..c89eef45 100644 --- a/website/content/docs/concepts/architecture.mdx +++ b/website/content/docs/concepts/architecture.mdx @@ -10,9 +10,8 @@ import { Callout } from "fumadocs-ui/components/callout"; bigRAG intentionally keeps four distinct stores instead of shoving everything into Postgres with pgvector. Each one earns its keep: -- **Vector store** — Qdrant by default, with turbopuffer available as an - instance-level managed backend. The vector store owns dense embeddings, - payload metadata, and filtered semantic search. +- **Vector store** — Turbopuffer owns dense embeddings, chunk text, + payload metadata, BM25 full-text search, and filtered semantic search. - **Postgres** — metadata and control plane. Collections, documents, users, API keys, webhooks, query logs, audit trail, embedding cache. Transactions keep ingestion consistent across stores. @@ -26,11 +25,15 @@ everything into Postgres with pgvector. Each one earns its keep: re-uploading. -bigRAG can run entirely from `docker compose up` on a single machine -with Qdrant. Turbopuffer moves vector operations out of the local -deployment when you prefer a managed vector service. +bigRAG runs Postgres and Redis locally with `docker compose up`, while +Turbopuffer provides the vector and full-text search backend. +The backend talks to Turbopuffer through the official Python client for +namespace writes, deletes, health listing, vector queries, and BM25 +queries. The local e2e stack points that same client at the fake +Turbopuffer service through `turbopuffer_base_url`. + The admin UI leans on TanStack Router's built-in route code splitting instead of wrapping dashboard pages in extra `React.lazy` boundaries, so each route transition pays for one route chunk, not a route chunk @@ -56,8 +59,8 @@ identifiers such as Google Drive file IDs, caller-provided vector IDs, collection names, settings keys, idempotency keys, OAuth states, session tokens, API keys, and webhook secrets remain opaque strings. Vector-store point IDs are backend-safe deterministic UUIDv5 values derived from the -caller-facing vector or chunk ID, so Qdrant and turbopuffer can use stable -point IDs without changing API-visible IDs. +caller-facing vector or chunk ID, so Turbopuffer writes, deletes, and +exports can use stable point IDs without changing API-visible IDs. ## Request flow — `POST /v1/collections/{name}/documents` @@ -99,7 +102,7 @@ Client → FastAPI │ 1. auth → scope check (query:read) │ 2. embed query when search mode needs vectors │ 3. vector search (with metadata filters) - │ 4. optional hybrid fusion — Qdrant keyword search + reciprocal rank fusion + │ 4. optional hybrid fusion — Turbopuffer BM25 + reciprocal rank fusion │ 5. optional rerank — Cohere Rerank v3.5 │ 6. attach timings (embed/search/rerank/cache/total) ▼ @@ -107,9 +110,9 @@ Response JSON with results and timings ``` Keyword mode skips embeddings. Semantic and hybrid mode use the -collection's configured embedding model. In v1, keyword and hybrid -search are available on Qdrant; turbopuffer supports semantic search -and returns a clear error for keyword or hybrid mode. +collection's configured embedding model. Keyword search runs BM25 over +Turbopuffer full-text chunk data, and hybrid search fuses Turbopuffer +ANN results with BM25 results using reciprocal rank fusion. Query-result cache hits return the cached chunks but report the current cache lookup latency, not the original uncached retrieval timings. @@ -128,25 +131,24 @@ images, and exposes layout provenance we can thread into citation metadata (`page_no`, `bbox`). PDFs with embedded text use a faster direct extractor; local OCR for scanned PDFs is enabled by default. -## Why Qdrant by default +## Why Turbopuffer -pgvector is tempting — one less store, one less deploy. But at scale -the gap widens: +pgvector is tempting — one less external service. But at scale the gap +widens: -- **Vector engine**: Qdrant gives bigRAG a dedicated HNSW vector - store with payload-aware filtering and operational knobs such as - search `ef`. pgvector can work well, but it shares capacity and - tuning with the transactional database. -- **Payload indexes**: Qdrant can index tenant IDs, document IDs, - chunk offsets, pages, and text payloads alongside vectors. pgvector - deployments usually need extra Postgres indexes and careful query - planning to keep filtered ANN search predictable. -- **Dedicated memory management**: Qdrant won't blow out Postgres's - shared buffers during a large ANN scan. +- **Managed vector engine**: Turbopuffer keeps ANN search and payload + filtering outside the transactional database, so Postgres remains the + metadata and control plane. +- **Full-text plus vectors**: Turbopuffer stores chunk text with + full-text search enabled, which lets keyword search run as BM25 and + hybrid search fuse BM25 with semantic results. +- **Namespace isolation**: each collection maps to a namespace, keeping + collection deletion, truncation, export, and raw vector writes scoped + to one backend object. For single-tenant deployments under ~1M vectors, pgvector would work -— and the `VectorStore` abstraction in bigRAG leaves a door open for -a future pgvector backend. +well. bigRAG keeps Postgres focused on durable metadata and uses +Turbopuffer for retrieval-specific storage and query execution. ## Scaling notes @@ -164,11 +166,9 @@ a future pgvector backend. seeded after boot through the same Redis scheduler guards used by the actors, so restarts and replicas do not create duplicate delayed messages. -- **Vector backend choice**: use local Qdrant for self-hosted installs - or turbopuffer for a managed vector search service. Turbopuffer - namespaces are created on first write and store bigRAG vector IDs as - payload attributes while using backend-safe IDs internally for writes, - deletes, and exports. +- **Vector backend**: Turbopuffer namespaces are created on first write + and store bigRAG vector IDs as payload attributes while using + backend-safe IDs internally for writes, deletes, and exports. - **Postgres replication**: the control plane is read-light; a warm-standby suffices for failover. - **Redis persistence**: enable `appendonly yes` (already set in the diff --git a/website/content/docs/concepts/collections.mdx b/website/content/docs/concepts/collections.mdx index 8cec8b13..0c96b0f8 100644 --- a/website/content/docs/concepts/collections.mdx +++ b/website/content/docs/concepts/collections.mdx @@ -5,14 +5,13 @@ description: Collections are logical groupings of documents that share the same import { Callout } from "fumadocs-ui/components/callout"; -A **collection** is a logical grouping of documents that share the same embedding and vector-storage configuration. Each collection maps to one backend object in its selected vector store. +A **collection** is a logical grouping of documents that share the same embedding configuration. Each collection maps to one Turbopuffer namespace for vector, metadata, and full-text search. ## What is a Collection? When you create a collection, you define: - **Embedding provider and model** — how documents will be embedded (e.g., OpenAI `text-embedding-3-small`) -- **Vector storage provider** — where vectors for this collection live (`qdrant` or `turbopuffer`) - **Chunk size and overlap** — how documents are split into searchable segments - **Default query settings** — `top_k`, `min_score`, and `search_mode` defaults - **Multimodal element handling** — whether ingestion stores headings, tables, equations, images, page bounds, and optional VLM summaries @@ -30,7 +29,6 @@ curl -X POST http://localhost:4000/v1/collections \ "description": "Academic research papers", "embedding_provider": "openai", "embedding_model": "text-embedding-3-small", - "vector_store_provider": "qdrant", "embedding_api_key": "sk-...", "dimension": 1536, "chunk_size": 512, @@ -46,7 +44,6 @@ curl -X POST http://localhost:4000/v1/collections \ | `description` | string | no | `""` | Human-readable description | | `embedding_provider` | string | no | Server default | `openai`, `openai_compatible`, `cohere`, or `voyage` | | `embedding_model` | string | no | Server default | Model name for the provider | -| `vector_store_provider` | string | no | `"qdrant"` | `qdrant` or `turbopuffer`; changed only through a vector migration job. `turbopuffer` requires a saved turbopuffer API key in Vector Storage before creation or migration | | `embedding_api_key` | string | no | — | API key for the embedding provider; use any non-empty value for local OpenAI-compatible gateways without auth | | `embedding_base_url` | string | no | — | Required for `openai_compatible` providers | | `dimension` | integer | no | Server default | Embedding vector dimension | @@ -62,9 +59,7 @@ curl -X POST http://localhost:4000/v1/collections \ | `multimodal_enrichment_enabled` | boolean | no | `false` | Queue VLM summaries for image, table, and equation elements; requires `multimodal_enabled` | | `metadata` | object | no | `{}` | Arbitrary key-value pairs | -For turbopuffer collections, creation validates the saved turbopuffer connection, then the provider namespace is created on the first document or vector write. - -To move an existing collection between Qdrant and turbopuffer, start a vector migration from `/vector-storage` or `POST /v1/admin/vector-storage/migrations`. The migration pauses writes, copies existing vectors to the configured target provider, switches the collection, and deletes the old source vectors after cutover. Pending, running, or canceling migrations can be stopped and removed from `/vector-storage` or `DELETE /v1/admin/vector-storage/migrations/{migration_id}`. Backups and vector migrations share the same maintenance lane, so only one of them can be pending or running at a time. +Collection creation validates the saved Turbopuffer connection. The namespace is created on the first document or vector write, and chunk text is stored with full-text search enabled so semantic, keyword, and hybrid retrieval use the same backend. ## Collection Stats @@ -160,7 +155,7 @@ curl -X PUT http://localhost:4000/v1/collections/research_papers \ All fields are optional — only provided fields are updated. - Embedding configuration (provider, model, dimension) and chunk settings cannot be changed after creation. Vector storage provider changes must use a vector migration job. + Embedding configuration (provider, model, dimension) and chunk settings cannot be changed after creation. ## Deleting a Collection diff --git a/website/content/docs/concepts/search.mdx b/website/content/docs/concepts/search.mdx index b5871430..b255c7cc 100644 --- a/website/content/docs/concepts/search.mdx +++ b/website/content/docs/concepts/search.mdx @@ -10,7 +10,7 @@ bigRAG exposes three search modes and an optional rerank pass. Keep query-time c | Mode | Description | |------|-------------| | `semantic` | Default. Cosine similarity against stored vectors. | -| `keyword` | Text-based matching using term frequency. No embedding involved. | +| `keyword` | Turbopuffer BM25 full-text matching over stored chunk text. No embedding involved. | | `hybrid` | Runs semantic and keyword in parallel, then merges with reciprocal rank fusion. | ### Semantic @@ -85,9 +85,9 @@ Pass a plain value for exact match, or use operators for more control. } ``` -Multiple filters are combined with AND. When a collection was created with `tenant_field`, bigRAG configures that field for backend filtering where supported and requires the tenant field in every query and chat filter. Missing tenant filters return `400`. +Multiple filters are combined with AND. When a collection was created with `tenant_field`, bigRAG configures that field for backend filtering and requires the tenant field in every query and chat filter. Missing tenant filters return `400`. -Keyword and hybrid search require the default Qdrant backend in v1. Turbopuffer supports semantic search and returns a clear error for keyword or hybrid mode. +Keyword search uses Turbopuffer BM25 over the chunk `text` field. Hybrid search runs Turbopuffer ANN and BM25 queries, then merges the two result sets with reciprocal rank fusion before optional reranking. ## Reranking diff --git a/website/content/docs/cookbook/multi-tenant-saas.mdx b/website/content/docs/cookbook/multi-tenant-saas.mdx index cdbd25f6..a621662a 100644 --- a/website/content/docs/cookbook/multi-tenant-saas.mdx +++ b/website/content/docs/cookbook/multi-tenant-saas.mdx @@ -54,15 +54,15 @@ The middleware enforces both the scopes and the collection pin, so a leaked tena Collections-per-tenant is simple but creates operational overhead as tenant count grows. Above small/medium tenant counts, move to Pattern B -so one Qdrant collection can use indexed tenant payload filters. +so one Turbopuffer namespace can use tenant metadata filters. ## Pattern B — shared collection with tenant filters One collection, tenant isolation via a required metadata filter. Setting -`tenant_field` tells bigRAG to create a Qdrant payload index for that -field and reject uploads, raw vector upserts, queries, and chat calls that -omit the tenant field. +`tenant_field` tells bigRAG to configure that field for backend filtering +and reject uploads, raw vector upserts, queries, and chat calls that omit +the tenant field. ```python await client.collections.create({ diff --git a/website/content/docs/deployment/docker.mdx b/website/content/docs/deployment/docker.mdx index ce5655e4..726b2fd8 100644 --- a/website/content/docs/deployment/docker.mdx +++ b/website/content/docs/deployment/docker.mdx @@ -27,7 +27,7 @@ BIGRAG_UI_IMAGE=yoginth/bigrag-ui:2026.4.30 \ docker compose up -d --no-build ``` -This starts bigRAG API, worker, admin UI, Postgres, Redis, and Qdrant. +This starts bigRAG API, worker, admin UI, Postgres, and Redis. Turbopuffer runs as the managed vector and full-text search backend. ### Verify @@ -40,7 +40,7 @@ curl http://localhost:4000/health ## Docker Compose -The default `docker-compose.yml` runs Qdrant as a single service. Qdrant does not require etcd for this standalone setup. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`. +The default `docker-compose.yml` runs the API, worker, admin UI, Postgres, and Redis. Turbopuffer is configured from the admin UI and stored in Postgres; it does not run as a local Compose service. When hacking from a checkout, plain `docker compose up -d` builds local API and UI images. When deploying published images, set the image variables shown above and use `--no-build`. ```yaml services: @@ -53,7 +53,6 @@ services: environment: BIGRAG_ENV: "${BIGRAG_ENV:-dev}" BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable - BIGRAG_QDRANT_URL: http://qdrant:6333 BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}" BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}" BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}' @@ -70,8 +69,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4000/health"] interval: 30s @@ -86,7 +83,6 @@ services: environment: BIGRAG_ENV: "${BIGRAG_ENV:-dev}" BIGRAG_DATABASE_URL: postgres://bigrag:bigrag@postgres:5432/bigrag?sslmode=disable - BIGRAG_QDRANT_URL: http://qdrant:6333 BIGRAG_REDIS_URL: "${BIGRAG_REDIS_URL:-redis://redis:6379/0}" BIGRAG_UPLOAD_DIR: /data/uploads BIGRAG_MASTER_KEY: "${BIGRAG_MASTER_KEY:-}" @@ -103,8 +99,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: [ @@ -140,14 +134,6 @@ services: timeout: 5s retries: 5 - qdrant: - image: qdrant/qdrant:v1.17.1 - ports: - - "6333:6333" - - "6334:6334" - volumes: - - qdrant_data:/qdrant/storage - redis: image: redis:7-alpine environment: @@ -171,7 +157,6 @@ services: volumes: bigrag_data: postgres_data: - qdrant_data: redis_data: ``` @@ -208,8 +193,6 @@ Scale the worker with more `bigrag-worker` containers from the `bigrag-api` imag |---------|-------------| | bigRAG API | 4000 | | PostgreSQL | 5432 | -| Qdrant HTTP | 6333 | -| Qdrant gRPC | 6334 | | Redis | 6379 | ## Health Checks @@ -219,7 +202,7 @@ bigRAG exposes two health endpoints: | Endpoint | Purpose | Notes | |----------|---------|-------| | `/health` | Liveness check | Always returns `200` if the server is running | -| `/health/ready` | Readiness check | Returns `503` if Postgres, Redis, configured vector-store clients, or the embedding provider is unreachable | +| `/health/ready` | Readiness check | Returns `503` if Postgres, Redis, Turbopuffer, or the embedding provider is unreachable | ```bash # Quick liveness check @@ -230,7 +213,6 @@ curl http://localhost:4000/health/ready # Infrastructure services docker exec bigrag-postgres pg_isready -U bigrag -curl -f http://localhost:6333/healthz docker exec bigrag-redis redis-cli ping ``` diff --git a/website/content/docs/deployment/encryption.mdx b/website/content/docs/deployment/encryption.mdx index d3fb9bb8..439139f9 100644 --- a/website/content/docs/deployment/encryption.mdx +++ b/website/content/docs/deployment/encryption.mdx @@ -8,7 +8,7 @@ import { Callout } from "fumadocs-ui/components/callout"; bigRAG treats "at-rest encryption" as a split responsibility: - **App-layer envelope encryption** (handled by bigRAG) — sensitive credential columns, persistent embedding-cache rows, and Redis cache payloads are Fernet-encrypted before they ever touch disk. -- **Storage-layer encryption** (handled by the operator) — document chunks, Qdrant vectors and payloads, uploaded files, Redis persistence, and backups still rely on the disk or object-store encrypting its own data. +- **Storage-layer encryption** (handled by the operator) — document chunks, Turbopuffer vectors and payload attributes, uploaded files, Redis persistence, and backups still rely on the disk, managed service, or object-store encrypting its own data. Both layers are required for a defensible "encrypted at rest" posture. @@ -57,10 +57,10 @@ bigRAG delegates the bulk of at-rest protection to your infrastructure. For a pr - **RDS / Aurora**: enable **Storage encryption** with a customer-managed KMS key. Enable the same on every read replica and snapshot target. - **Backups**: pipe `pg_dump` into a GPG or `age`-encrypted file, or store it in an encrypted backup system. -### Qdrant +### Turbopuffer -- Qdrant stores vectors, payloads, and indexes under `/qdrant/storage` in the Docker deployment. Put that volume on encrypted storage. -- For Qdrant Cloud, enable the provider's encryption controls and restrict API keys to the bigRAG service. +- Turbopuffer stores vectors, full-text fields, payload attributes, and indexes in its managed service. Select the production region deliberately and keep the API key scoped to the bigRAG service. +- Save the Turbopuffer API key in the admin UI so it is encrypted in the `instance_settings` table with `BIGRAG_MASTER_KEY`. Rotate it on the same cadence as other provider credentials, and restrict outbound network access where your platform allows it. ### Redis @@ -74,7 +74,7 @@ The upload directory (default `./data/uploads`) must live on an encrypted volume ### Readable backups -Readable backups created from `/backups` are intentionally not client-side encrypted. They keep logs, Qdrant payloads, and raw uploaded files inspectable, but redact provider keys, OAuth tokens, webhook secrets, S3 credentials, API key hashes, session hashes, and embedding-cache vectors. Put the backup bucket behind strict IAM/R2 token policy, bucket encryption, private networking where available, lifecycle retention, and access logging. +Readable backups created from `/backups` are intentionally not client-side encrypted. They keep logs, Turbopuffer payload exports, and raw uploaded files inspectable, but redact provider keys, OAuth tokens, webhook secrets, S3 credentials, API key hashes, session hashes, and embedding-cache vectors. Put the backup bucket behind strict IAM/R2 token policy, bucket encryption, private networking where available, lifecycle retention, and access logging. ## Rotation @@ -94,7 +94,7 @@ A built-in `bigrag crypto rotate` command is not available yet; use a controlled | Data | Why | |------|-----| | Document chunks (`document_chunks.content`) | Per-query decrypt kills search latency; rely on disk encryption. | -| Vectors and text payloads in Qdrant | Search needs raw floats and filterable payloads, so use Qdrant auth, tenant filters, network isolation, and encrypted storage. | +| Vectors and text payloads in Turbopuffer | Search needs raw floats and filterable payloads, so use Turbopuffer API-key controls, tenant filters, network isolation, and managed-service encryption. | | Audit log rows | Append-only. Store encrypted at the disk layer; querying encrypted metadata is prohibitive. | | User display names / emails | PII, not credentials. Disk-layer + column-level access control is the right split. | diff --git a/website/content/docs/deployment/production.mdx b/website/content/docs/deployment/production.mdx index 3c2c3d2d..c9c97ec5 100644 --- a/website/content/docs/deployment/production.mdx +++ b/website/content/docs/deployment/production.mdx @@ -22,11 +22,11 @@ Tick these off before pointing real traffic at a bigRAG deployment: proxies if bigRAG sits behind nginx, Caddy, Traefik, or a load balancer. - [ ] Redis with `requirepass` set and `appendonly yes` persisted to a mounted volume. -- [ ] Qdrant cluster mode or turbopuffer if you expect >10M vectors or need managed HA. +- [ ] Turbopuffer API key and region saved from the admin UI for managed vector and full-text search. - [ ] Postgres warm-standby replica for failover. - [ ] Backup strategy: configure readable S3/R2 backups from `/backups`, protect that bucket as sensitive, and separately keep encrypted infrastructure - snapshots for Postgres, vector-store backends, Redis, and uploads. + snapshots for Postgres, Turbopuffer exports, Redis, and uploads. - [ ] Set `metadata_schema` on collections that accept untrusted metadata so uploads with invalid shape are rejected at the edge. - [ ] Wire `GET /v1/admin/audit` into your SIEM / log pipeline. @@ -62,7 +62,6 @@ services: environment: BIGRAG_ENV: prod BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag - BIGRAG_QDRANT_URL: http://qdrant:6333 BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL} BIGRAG_HOST: 0.0.0.0 BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY} @@ -86,8 +85,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: ["CMD", "curl", "-f", "http://localhost:4000/health"] interval: 30s @@ -102,7 +99,6 @@ services: environment: BIGRAG_ENV: prod BIGRAG_DATABASE_URL: postgres://bigrag:strongpassword@postgres:5432/bigrag - BIGRAG_QDRANT_URL: http://qdrant:6333 BIGRAG_REDIS_URL: ${BIGRAG_REDIS_URL} BIGRAG_MASTER_KEY: ${BIGRAG_MASTER_KEY} BIGRAG_MASTER_KEY_PREVIOUS: '${BIGRAG_MASTER_KEY_PREVIOUS:-[]}' @@ -120,8 +116,6 @@ services: condition: service_healthy redis: condition: service_healthy - qdrant: - condition: service_started healthcheck: test: [ @@ -161,18 +155,6 @@ services: timeout: 5s retries: 5 - qdrant: - image: qdrant/qdrant:v1.17.1 - ports: - - "127.0.0.1:6333:6333" - - "127.0.0.1:6334:6334" - volumes: - - qdrant_data:/qdrant/storage - deploy: - resources: - limits: - memory: 2g - redis: image: redis:7-alpine environment: @@ -197,7 +179,6 @@ services: volumes: bigrag_data: postgres_data: - qdrant_data: redis_data: ``` @@ -236,7 +217,6 @@ BIGRAG_WORKERS=8 # Match CPU cores BIGRAG_INGESTION_WORKERS=8 # UI/runtime concurrency target for ingestion BIGRAG_DB_POOL_MAX=100 # Increase for high concurrency BIGRAG_EMBEDDING_CONCURRENCY=16 # Parallel embedding requests -BIGRAG_QDRANT_SEARCH_EF=128 # Optional HNSW recall/latency tuning # Worker command bigrag-worker --processes 1 --threads 8 @@ -279,7 +259,7 @@ Text logs are colored by default with fixed columns for time, level, logger, event, and fields. Text rendering escapes control characters in event and field values before writing to stdout, so request paths and headers cannot create extra terminal lines or emit terminal control sequences. Dependency loggers such -as Dramatiq, Alembic, Uvicorn access, HTTPX, and Qdrant are kept at warning level +as Dramatiq, Alembic, Uvicorn access, HTTPX, and Turbopuffer are kept at warning level so local terminals show bigRAG actions instead of library startup chatter. Every API request logs `request_start`, `response_start`, and `request_complete`; unhandled failures log @@ -322,7 +302,6 @@ Ensure all infrastructure services are running and healthy: ```bash docker exec bigrag-postgres pg_isready -U bigrag -curl -f http://localhost:6333/healthz docker exec bigrag-redis redis-cli ping ``` @@ -352,7 +331,7 @@ File exceeds the max upload size. Increase `BIGRAG_MAX_UPLOAD_SIZE_MB`. ### `/health/ready` returns 503 but services are running -The readiness endpoint also checks the embedding provider. If no collection has an `embedding_api_key` configured, it reports degraded status. Check `/health` first to confirm the server itself is running, then create a collection with a valid embedding key. +The readiness endpoint also checks Turbopuffer and the embedding provider. Check `/health` first to confirm the server itself is running, then save Turbopuffer settings in the admin UI and create a collection with a valid embedding key. ### Document uploads fail with embedding errors diff --git a/website/content/docs/deployment/railway.mdx b/website/content/docs/deployment/railway.mdx index 0d7e4b6b..86dc59c1 100644 --- a/website/content/docs/deployment/railway.mdx +++ b/website/content/docs/deployment/railway.mdx @@ -1,11 +1,11 @@ --- title: Railway -description: Deploy bigRAG to Railway with managed Postgres and Redis plus self-hosted Qdrant. +description: Deploy bigRAG to Railway with managed Postgres, Redis, and Turbopuffer search. --- import { Callout } from "fumadocs-ui/components/callout"; -This guide deploys the API, Dramatiq worker, admin UI, Postgres, Redis, and Qdrant in one Railway project. Qdrant runs as a single Docker-image service and does not need etcd in this setup. +This guide deploys the API, Dramatiq worker, admin UI, Postgres, and Redis in one Railway project. Turbopuffer provides managed vector, keyword, and hybrid search. ## What you get @@ -14,11 +14,12 @@ This guide deploys the API, Dramatiq worker, admin UI, Postgres, Redis, and Qdra - **App** — admin UI, public domain. - **Postgres 17** — Railway plugin, exposes `DATABASE_URL`. - **Redis 7** — Railway plugin, exposes `REDIS_URL`. -- **Qdrant** — Qdrant Docker image, internal-only. +- **Turbopuffer** — managed vector and full-text search, configured from the admin UI. ## Prerequisites -- Railway Pro plan for comfortable API and vector-store memory limits. +- Railway Pro plan for comfortable API and worker memory limits. +- A Turbopuffer API key. - An OpenAI, Cohere, Voyage, or OpenAI-compatible embedding provider key. - A region close to your users and embedding provider. @@ -28,21 +29,9 @@ Provision in this order so API variables can reference private service domains: 1. Postgres plugin. 2. Redis plugin. -3. `Qdrant` Docker image. -4. `App` from the repository root. -5. `API` from the repo root directory `/api`. -6. `Worker` from the repo root directory `/api`. - -## Qdrant - -| Setting | Value | -|---|---| -| Source | Docker image `qdrant/qdrant:v1.17.1` | -| Start command | leave empty | -| Volume | 10 GB at `/qdrant/storage` | -| Public domain | none | - -Qdrant exposes HTTP on `6333` and gRPC on `6334`. The API uses HTTP. +3. `App` from the repository root. +4. `API` from the repo root directory `/api`. +5. `Worker` from the repo root directory `/api`. ## App @@ -79,7 +68,6 @@ BIGRAG_LOG_LEVEL=info BIGRAG_LOG_FORMAT=json BIGRAG_DATABASE_URL=${{Postgres.DATABASE_URL}} BIGRAG_REDIS_URL=${{Redis.REDIS_URL}} -BIGRAG_QDRANT_URL=http://${{Qdrant.RAILWAY_PRIVATE_DOMAIN}}:6333 BIGRAG_EMBEDDING_API_KEY= BIGRAG_MASTER_KEY= BIGRAG_ALLOW_PUBLIC_BIND_IN_PROD=true @@ -89,7 +77,7 @@ BIGRAG_CORS_ORIGINS=["https://${{App.RAILWAY_PUBLIC_DOMAIN}}"] BIGRAG_UPLOAD_DIR=/data/uploads ``` -Use the Railway Qdrant service URL for `BIGRAG_QDRANT_URL`. +Save the Turbopuffer API key and region from the admin UI after setup. ## Worker @@ -102,7 +90,7 @@ Use the Railway Qdrant service URL for `BIGRAG_QDRANT_URL`. | Volume | same mounted upload volume path as API | | Public domain | no | -Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_QDRANT_URL`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`. +Use the same environment as `API`, including `BIGRAG_DATABASE_URL`, `BIGRAG_REDIS_URL`, `BIGRAG_MASTER_KEY`, and `BIGRAG_UPLOAD_DIR`. ## Generate the master key @@ -116,15 +104,16 @@ Back the master key up immediately. It encrypts provider secrets, webhook secret ## First Run -1. Wait for Postgres, Redis, Qdrant, API, Worker, and app services to deploy. +1. Wait for Postgres, Redis, API, Worker, and app services to deploy. 2. Open the admin UI public URL. 3. Complete `/setup` to create the first admin account. -4. Create a collection and upload a small document. -5. Check `/health/ready`; it should report `vector_store: true`. +4. Save the Turbopuffer API key and region in the admin UI. +5. Create a collection and upload a small document. +6. Check `/health/ready`; it should report `vector_store: true`. ## Troubleshooting -**API deploy logs stop while connecting to Qdrant.** Keep the default optional Qdrant readiness behavior so the API can boot and `/health` can respond while `/health/ready` reports the degraded vector-store dependency. Fix the Qdrant service URL or credentials, then redeploy. +**`/health/ready` reports a degraded vector store.** Check the Turbopuffer settings in the admin UI and any outbound network restrictions. **API exits with `ImportError: Can't find Python file .../site-packages/alembic/env.py`.** Rebuild from the latest repo revision and clear Railway's build cache if it keeps reusing an old image. The API image bundles bigRAG's Alembic migration environment into the installed package; this error means the deployed image was built without that migration environment. diff --git a/website/content/docs/development/testing.mdx b/website/content/docs/development/testing.mdx index fa3c3c3f..14cf850d 100644 --- a/website/content/docs/development/testing.mdx +++ b/website/content/docs/development/testing.mdx @@ -37,4 +37,4 @@ The E2E workflow uses the local fake OpenAI service only. There is no maintained The `e2e/` workspace contains API pytest coverage, Python SDK contract tests, and TypeScript SDK Vitest coverage. It intentionally does not include Playwright or other UI browser tests. -The local `e2e/Makefile` runs Docker Compose under the `bigrag-e2e` project name. `make down` removes that isolated e2e stack and its volumes without deleting the default `bigrag` dev volumes used by `./dev.sh`. +The local `e2e/Makefile` runs Docker Compose under the `bigrag-e2e` project name. `make up` waits for API liveness on `/health`; pytest then creates the first admin when needed and seeds fake-provider runtime settings before provider-dependent tests run. `make down` removes that isolated e2e stack and its volumes without deleting the default `bigrag` dev volumes used by `./dev.sh`. diff --git a/website/content/docs/getting-started/configuration.mdx b/website/content/docs/getting-started/configuration.mdx index a311b762..6a906ea6 100644 --- a/website/content/docs/getting-started/configuration.mdx +++ b/website/content/docs/getting-started/configuration.mdx @@ -5,7 +5,7 @@ description: Bootstrap bigRAG, then manage runtime settings from the admin UI. import { Callout } from "fumadocs-ui/components/callout"; -bigRAG uses a UI-first configuration model. Operators provide only the wiring needed before the API can read Postgres, then admins manage product/runtime settings from `/settings`, `/data-storage`, `/models`, and `/vector-storage`. +bigRAG uses a UI-first configuration model. Operators provide only the wiring needed before the API can read Postgres, then admins manage product/runtime settings from `/settings`, `/data-storage`, and `/models`. Auth is admin accounts + session cookies and minted `bigrag_sk_…` API keys — see [Authentication](/docs/api-reference/authentication). @@ -26,9 +26,6 @@ Bootstrap config is the small set of values the API needs before it can connect | `BIGRAG_DB_POOL_MAX` | Maximum Postgres pool size. | `50` | | `BIGRAG_MIGRATION_TIMEOUT_SECONDS` | Startup migration timeout. Set `0` to disable the timeout. | `60` | | `BIGRAG_REDIS_URL` | Redis connection URL. | `redis://localhost:6379/0` | -| `BIGRAG_QDRANT_URL` | Qdrant connection URL. | `http://localhost:6333` | -| `BIGRAG_QDRANT_CONNECT_TIMEOUT_SECONDS` | Qdrant startup connection timeout. | `10` | -| `BIGRAG_QDRANT_REQUIRED` | Fail startup if configured vector-store clients cannot be reached. | `false` | | `BIGRAG_MASTER_KEY` | Fernet key for encrypted provider secrets, instance-setting secrets, embedding-cache rows, and Redis cache payloads. Required for production. | — | | `BIGRAG_MASTER_KEY_PREVIOUS` | JSON array of old Fernet keys during staged key rotation. | `[]` | | `BIGRAG_UPLOAD_DIR` | Local document upload directory when storage backend is `local`. | `./data/uploads` | @@ -55,7 +52,6 @@ workers = 1 database_url = "postgres://bigrag:change-me@postgres:5432/bigrag?sslmode=disable" redis_url = "redis://redis:6379/0" -qdrant_url = "http://qdrant:6333" master_key = "generate-a-fernet-key" master_key_previous = [] @@ -67,7 +63,7 @@ Environment variables override TOML values. CLI flags passed to `python -m bigra ## Runtime settings -After the first admin account exists, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, `/models` to manage model presets and model runtime settings, and `/vector-storage` to manage vector backend connection details. New collections choose their vector store during creation. Runtime settings are stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read. +After the first admin account exists, the admin UI sends first-run operators to `/onboarding` to create one verified embedding preset and optionally save Turbopuffer settings. Later, go to `/settings` to manage platform runtime settings, `/data-storage` to manage uploaded source-file storage, and `/models` to manage model presets and model runtime settings. Turbopuffer is configured from the UI, is the vector backend for every collection, and is stored in Postgres in the `instance_settings` table. Secret settings are encrypted with `BIGRAG_MASTER_KEY` and redacted on read. The same surface is available through [`/v1/admin/settings`](/docs/api-reference/instance-settings). @@ -77,17 +73,17 @@ The same surface is available through [`/v1/admin/settings`](/docs/api-reference | Data | Upload limits, OCR, ingestion workers, queue depth, webhook delivery limits, and retention windows. | | Data Storage (`/data-storage`) | Storage backend, S3-compatible bucket, endpoint, region, prefix, credentials, and path-style behavior. | | Models (`/models`) | Default embedding provider, model, dimension, key, base URL, cache TTLs, chat provider, chat model, temperature, history count, and context budget. | -| Vector Storage (`/vector-storage`) | Provider tabs for Qdrant URL, readiness behavior, and HNSW tuning or turbopuffer API key, region, and namespace prefix. | +| Vector search | Turbopuffer API key, region, namespace prefix, and optional compatible base URL. | | Backups | S3-compatible readable backup bucket, endpoint, region, prefix, and credentials for S3, R2, or MinIO. | `BIGRAG_CHAT_API_KEY` can seed a default instance chat credential for OpenAI's standard API endpoint. For non-default `chat_base_url` values, configure a saved chat key in the admin UI or pass `provider_api_key` on chat requests so the instance fallback key is never sent to a custom provider. The UI uses the same compact top tab control and shared dashboard body width as the other admin pages for Account, Health, Security, and Data, then opens directly into the selected Settings area without a repeated section heading. Each area is arranged sequentially from top to bottom. Each runtime panel shows common controls first, keeps raw setting keys, source metadata, and rarely changed limits in Advanced controls, keeps Save in the panel footer, and uses default or example placeholders for empty controls. Settings only accepts current tab values: `/settings?tab=account`, `/settings?tab=health`, `/settings?tab=security`, and `/settings?tab=data`; file storage lives at `/data-storage`. -Database-backed admin settings apply immediately on save. Storage, backup, and vector-store connection changes validate the new target before saving, then swap the runtime client in place where applicable. +Database-backed admin settings apply immediately on save. Storage, backup, and Turbopuffer connection changes validate the new target before saving, then swap the runtime client in place where applicable. -Legacy runtime environment variables are still read as initial defaults for compatibility, but enterprise installs should treat `/settings` as the control plane after bootstrap. +Turbopuffer settings are not bootstrap environment variables. Save them from the admin UI so the API and worker read the same database-backed instance settings. ## CLI flags @@ -99,6 +95,5 @@ Legacy runtime environment variables are still read as initial defaults for comp --host HOST Override server host --port PORT Override server port --database-url URL Override Postgres URL ---qdrant-url URL Override Qdrant URL --redis-url URL Override Redis URL ``` diff --git a/website/content/docs/getting-started/installation.mdx b/website/content/docs/getting-started/installation.mdx index bda4a77d..c5d6a3f4 100644 --- a/website/content/docs/getting-started/installation.mdx +++ b/website/content/docs/getting-started/installation.mdx @@ -12,6 +12,7 @@ import { Callout } from "fumadocs-ui/components/callout"; - **Docker** and **Docker Compose** (recommended) - **Python 3.12+** and [**uv**](https://docs.astral.sh/uv/) (for running the backend from source) - An **OpenAI** or **Cohere** API key for embeddings +- A **Turbopuffer** API key for vector search ## Install @@ -27,7 +28,6 @@ docker compose up -d This starts: - **bigRAG API** on port `4000` (Swagger docs at `/docs`) -- **Qdrant** vector database on port `6333` - **PostgreSQL** for metadata on port `5432` - **Redis** for the ingestion queue on port `6379` @@ -48,7 +48,7 @@ Release artifacts use CalVer (`YYYY.M.D`). Docker also publishes `latest` for co ### Start infrastructure services ```bash -docker compose up postgres redis qdrant -d +docker compose up postgres redis -d ``` @@ -86,7 +86,7 @@ This script: 1. Kills stale processes on port 4000 2. Validates required commands (`docker`, `curl`, `uv`) -3. Starts Docker services (Postgres, Redis, Qdrant) and waits for readiness +3. Starts Docker services (Postgres and Redis) and waits for readiness 4. Installs Python dependencies with `uv` 5. Starts the backend with auto-reload 6. Starts `bigrag-worker` with Dramatiq on Redis diff --git a/website/content/docs/getting-started/quickstart.mdx b/website/content/docs/getting-started/quickstart.mdx index 97fef456..dbc9f72f 100644 --- a/website/content/docs/getting-started/quickstart.mdx +++ b/website/content/docs/getting-started/quickstart.mdx @@ -42,7 +42,13 @@ curl -X POST $BASE/v1/auth/setup \ -c cookies.txt ``` -The response sets a `bigrag_session` cookie. The admin UI exposes the same flow at `http://localhost:3000/setup`. +The response sets a `bigrag_session` cookie. The admin UI exposes the same flow at `http://localhost:3000/setup`, then redirects to `/onboarding` where you create one verified embedding preset and can optionally save Turbopuffer settings before landing on `/overview`. + + + +### Complete provider onboarding + +In the admin UI, finish `/onboarding` by adding one embedding preset. Turbopuffer can be saved there too, or skipped and configured later from the runtime settings API. diff --git a/website/content/docs/index.mdx b/website/content/docs/index.mdx index 4a31c766..e75a810d 100644 --- a/website/content/docs/index.mdx +++ b/website/content/docs/index.mdx @@ -15,11 +15,11 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi - **Any document format** — PDF, DOCX, PPTX, XLSX, HTML, Markdown, CSV/TSV/XML/JSON, and scanned images (OCR) via [Docling](https://github.com/DS4SD/docling) - **Any embedding model** — OpenAI, Cohere, Voyage, or `openai_compatible` gateways (Ollama, vLLM, TEI, LiteLLM, Azure, Bedrock) configured per collection - **Three search modes** — semantic, keyword, and hybrid, with optional Cohere reranking -- **Qdrant vector database** — HNSW vector search with tenant-aware payload indexes +- **Turbopuffer vector search** — semantic, keyword, and hybrid retrieval with namespace isolation - **First-class auth** — admin accounts, session cookies, scoped `bigrag_sk_…` API keys, and full audit/access logs - **Operational features** — batch ingestion, Google Drive sync, webhooks with HMAC signatures, metadata-schema validation, an evaluation runner, and per-collection query analytics - **Admin UI** — first-run setup, collection browser, stateless chat, connector management, access logs, MCP credentials, and API-key minting -- **Self-hostable** — Docker Compose, no external dependencies +- **Self-hostable control plane** — Docker Compose for the API, admin UI, Postgres, and Redis - **MIT licensed** — run it anywhere, forever free ## Architecture @@ -37,10 +37,10 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi Redis --> Worker["Ingestion worker"] Worker --> Docling["Docling conversion"] Docling --> Embeddings["Embedding providers"] - Embeddings --> Qdrant[("Qdrant vectors")] + Embeddings --> Turbopuffer[("Turbopuffer vectors")] Retrieval --> Embeddings - Retrieval --> Qdrant + Retrieval --> Turbopuffer Retrieval --> LLM["Chat provider"] LLM --> Answer["Retrieved answer"] @@ -55,7 +55,7 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi class API api class Control,Ingestion,Retrieval plane class Redis,Worker,Docling pipeline - class Postgres,Files,Embeddings,Qdrant,LLM store + class Postgres,Files,Embeddings,Turbopuffer,LLM store class Answer output' /> | Component | Purpose | Default Address | @@ -63,7 +63,7 @@ bigRAG is an open-source RAG (Retrieval-Augmented Generation) platform. It provi | **bigRAG API** | REST API server (FastAPI) | `http://localhost:4000` | | **Admin UI** | Admin web app | `http://localhost:3000` | | **PostgreSQL** | Users, sessions, API keys, metadata, audit | `localhost:5432` | -| **Qdrant** | Vector storage and search | `localhost:6333` | +| **Turbopuffer** | Vector storage and semantic, keyword, and hybrid search | Managed service | | **Redis** | Ingestion + event bus | `localhost:6379` | ## Explore diff --git a/website/content/docs/migration/from-pinecone.mdx b/website/content/docs/migration/from-pinecone.mdx index 6fc971d1..f450c3f0 100644 --- a/website/content/docs/migration/from-pinecone.mdx +++ b/website/content/docs/migration/from-pinecone.mdx @@ -90,15 +90,15 @@ Recommended if you're in the middle of a chunking rethink anyway. ## API differences to watch - **Filters stay as JSON objects**. Pass a dict like - `{"department": "sales"}` and bigRAG translates it into Qdrant - payload filters. + `{"department": "sales"}` and bigRAG translates it into Turbopuffer + filters. - **Namespaces → metadata filters**. Pinecone's `namespace="docs"` becomes `metadata.namespace = "docs"`. Set `tenant_field: "namespace"` on the collection when that field should - get a Qdrant payload index and become required on uploads, raw vector - upserts, queries, and chat requests. + become required on uploads, raw vector upserts, queries, and chat + requests. - **Hybrid search is built-in**. `search_mode: "hybrid"` runs keyword - + vector in parallel and fuses with reciprocal rank fusion. + BM25 + vector search in parallel and fuses with reciprocal rank fusion. No separate "sparse" vectors to maintain. ## Cut-over diff --git a/website/content/docs/sdks/python.mdx b/website/content/docs/sdks/python.mdx index e59ed9a8..ebee5fa6 100644 --- a/website/content/docs/sdks/python.mdx +++ b/website/content/docs/sdks/python.mdx @@ -72,7 +72,7 @@ The SDK follows a resource namespace pattern: | `client.vectors` | Raw vector upsert and delete | | `client.webhooks` | Webhook management | | `client.auth` | Setup, login, logout, identity, password, and preferences | -| `client.admin` | Users, API keys, access logs, audit logs, runtime settings, backups, vector migrations, admin realtime streams, connector config, embedding presets, and MCP server keys | +| `client.admin` | Users, API keys, access logs, audit logs, runtime settings, backups, admin realtime streams, connector config, embedding presets, and MCP server keys | | `client.connectors.google` | Google Drive account, file browsing, sources, and sync jobs | | `client.evaluations` | Golden-set retrieval evaluation runs | @@ -87,7 +87,6 @@ result = await client.collections.list(name="prefix", limit=10) # Create collection = await client.collections.create({ "name": "docs", - "vector_store_provider": "qdrant", "embedding_provider": "openai", "embedding_model": "text-embedding-3-small", }) @@ -328,34 +327,19 @@ except APITimeoutError: ```python settings = await client.admin.settings.list() -await client.admin.settings.test({"values": {"qdrant_url": "http://qdrant:6333"}}) +await client.admin.settings.test({"values": {"turbopuffer_region": "aws-us-west-2"}}) await client.admin.settings.update({"values": {"trusted_proxies": ["10.0.0.0/8"]}}) await client.admin.settings.reset({"keys": ["trusted_proxies"]}) await client.admin.settings.purge_embedding_cache() backups = await client.admin.backups.list(limit=20) -backup = await client.admin.backups.create({"label": "before migration"}) +backup = await client.admin.backups.create({"label": "nightly export"}) backup = await client.admin.backups.get(backup["id"]) -vector_migrations = await client.admin.vector_migrations.list( - collection="docs", - include_total=True, -) -vector_migration = await client.admin.vector_migrations.create({ - "collection": "docs", - "target_provider": "turbopuffer", -}) -vector_migration = await client.admin.vector_migrations.get(vector_migration["id"]) -await client.admin.vector_migrations.delete(vector_migration["id"]) - async for event in client.admin.realtime.backups(limit=20): if event["event"] == "snapshot": print(event["data"]["payload"]["jobs"]) -async for event in client.admin.realtime.vector_migrations(collection="docs"): - if event["event"] == "snapshot": - print(event["data"]["payload"]["jobs"]) - async for event in client.admin.realtime.platform_readiness(): print(event["data"]) ``` @@ -366,7 +350,7 @@ async for event in client.admin.realtime.platform_readiness(): # Health check health = await client.health() -# Readiness (checks Postgres, Qdrant, Redis, embedding provider) +# Readiness (checks Postgres, Redis, Turbopuffer, embedding provider) ready = await client.readiness() # Platform stats diff --git a/website/content/docs/sdks/typescript.mdx b/website/content/docs/sdks/typescript.mdx index 8635e64a..570b93b5 100644 --- a/website/content/docs/sdks/typescript.mdx +++ b/website/content/docs/sdks/typescript.mdx @@ -70,7 +70,6 @@ client.collections.listAll({ name?, limit? }) // AsyncGenerator that pages thro client.collections.create({ name, description?, - vector_store_provider?, // "qdrant" | "turbopuffer" embedding_preset_id?, embedding_provider?, // "openai" | "cohere" | "voyage" | "openai_compatible" embedding_model?, @@ -78,7 +77,6 @@ client.collections.create({ embedding_base_url?, dimension?, chunk_size?, chunk_overlap?, chunk_strategy?, // "paragraph" | "recursive" - index_type?, // "HNSW" tenant_field?, // metadata key required for tenant filtering metadata_schema?, // JSON Schema reranking_enabled?, reranking_model?, reranking_api_key?, @@ -238,11 +236,6 @@ client.admin.backups.list({ limit?, offset? }) client.admin.backups.get(backupId) client.admin.backups.create({ label? }) -client.admin.vectorMigrations.list({ collection?, cursor?, includeTotal?, limit?, offset? }) -client.admin.vectorMigrations.get(migrationId) -client.admin.vectorMigrations.create({ collection, target_provider }) -client.admin.vectorMigrations.delete(migrationId) - client.admin.connectors.google.get() client.admin.connectors.google.update({ enabled?, client_id?, client_secret? }) @@ -267,12 +260,6 @@ for await (const event of client.admin.realtime.backups({ limit: 20 })) { } } -for await (const event of client.admin.realtime.vectorMigrations({ collection: "docs" })) { - if (event.event === "snapshot") { - console.log(event.data.payload.jobs) - } -} - for await (const event of client.admin.realtime.platformReadiness()) { console.log(event.data) }