From 913875188ae701ca38c7546d041f5ca26d1c5547 Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Thu, 25 Dec 2025 07:36:26 -0800 Subject: [PATCH] oidc --- .env.example | 24 ++- docker-compose.dev.yml | 31 +++- docker-compose.yml | 29 +++- services/raggr/app.py | 7 +- services/raggr/blueprints/users/__init__.py | 158 +++++++++++++++++- services/raggr/blueprints/users/models.py | 9 +- .../raggr/blueprints/users/oidc_service.py | 76 +++++++++ .../models/0_20251025081744_init.py | 63 ------- .../models/0_20251225052005_init.py | 71 ++++++++ .../models/1_20251025091926_update.py | 60 ------- services/raggr/oidc_config.py | 113 +++++++++++++ services/raggr/pyproject.toml | 2 +- .../raggr-frontend/src/api/oidcService.ts | 94 +++++++++++ .../raggr-frontend/src/api/userService.ts | 1 + .../src/components/ChatScreen.tsx | 2 +- .../src/components/LoginScreen.tsx | 122 +++++++------- services/raggr/startup-dev.sh | 26 ++- services/raggr/uv.lock | 130 +++++++++++++- 18 files changed, 799 insertions(+), 219 deletions(-) create mode 100644 services/raggr/blueprints/users/oidc_service.py delete mode 100644 services/raggr/migrations/models/0_20251025081744_init.py create mode 100644 services/raggr/migrations/models/0_20251225052005_init.py delete mode 100644 services/raggr/migrations/models/1_20251025091926_update.py create mode 100644 services/raggr/oidc_config.py create mode 100644 services/raggr/raggr-frontend/src/api/oidcService.ts diff --git a/.env.example b/.env.example index f739df2..dfe55bb 100644 --- a/.env.example +++ b/.env.example @@ -1,8 +1,11 @@ # Database Configuration -# Use DATABASE_PATH for simple relative/absolute paths (e.g., "database/raggr.db" or "dev.db") -# Or use DATABASE_URL for full connection strings (e.g., "sqlite://database/raggr.db") -DATABASE_PATH=database/raggr.db -# DATABASE_URL=sqlite://database/raggr.db +# PostgreSQL is recommended (required for OIDC features) +DATABASE_URL=postgres://raggr:changeme@postgres:5432/raggr + +# PostgreSQL credentials (if using docker-compose postgres service) +POSTGRES_USER=raggr +POSTGRES_PASSWORD=changeme +POSTGRES_DB=raggr # JWT Configuration JWT_SECRET_KEY=your-secret-key-here @@ -26,3 +29,16 @@ IMMICH_URL=http://192.168.1.5:2283 IMMICH_API_KEY=your-immich-api-key SEARCH_QUERY=simba cat DOWNLOAD_DIR=./simba_photos + +# OIDC Configuration (Authelia) +OIDC_ISSUER=https://auth.example.com +OIDC_CLIENT_ID=simbarag +OIDC_CLIENT_SECRET=your-client-secret-here +OIDC_REDIRECT_URI=http://localhost:8080/ +OIDC_USE_DISCOVERY=true + +# Optional: Manual OIDC endpoints (if discovery is disabled) +# OIDC_AUTHORIZATION_ENDPOINT=https://auth.example.com/api/oidc/authorization +# OIDC_TOKEN_ENDPOINT=https://auth.example.com/api/oidc/token +# OIDC_USERINFO_ENDPOINT=https://auth.example.com/api/oidc/userinfo +# OIDC_JWKS_URI=https://auth.example.com/api/oidc/jwks diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f2aa20e..006c674 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,6 +1,22 @@ version: "3.8" services: + postgres: + image: postgres:16-alpine + environment: + - POSTGRES_USER=raggr + - POSTGRES_PASSWORD=raggr_dev_password + - POSTGRES_DB=raggr + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U raggr"] + interval: 5s + timeout: 5s + retries: 5 + raggr-backend: build: context: ./services/raggr @@ -8,14 +24,26 @@ services: image: torrtle/simbarag:dev ports: - "8080:8080" + env_file: + - .env environment: - PAPERLESS_TOKEN=${PAPERLESS_TOKEN} - BASE_URL=${BASE_URL} - OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434} - CHROMADB_PATH=/app/chromadb - OPENAI_API_KEY=${OPENAI_API_KEY} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - OIDC_ISSUER=${OIDC_ISSUER} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} + - OIDC_REDIRECT_URI=${OIDC_REDIRECT_URI} + - OIDC_USE_DISCOVERY=${OIDC_USE_DISCOVERY:-true} + - DATABASE_URL=postgres://raggr:raggr_dev_password@postgres:5432/raggr - FLASK_ENV=development - PYTHONUNBUFFERED=1 + depends_on: + postgres: + condition: service_healthy volumes: # Mount source code for hot reload - ./services/raggr:/app @@ -24,7 +52,6 @@ services: - /app/__pycache__ # Persist data - chromadb_data:/app/chromadb - - database_data:/app/database command: sh -c "chmod +x /app/startup-dev.sh && /app/startup-dev.sh" raggr-frontend: @@ -42,4 +69,4 @@ services: volumes: chromadb_data: - database_data: + postgres_data: diff --git a/docker-compose.yml b/docker-compose.yml index 3090b9f..e35e5f2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,21 @@ version: "3.8" services: + postgres: + image: postgres:16-alpine + environment: + - POSTGRES_USER=${POSTGRES_USER:-raggr} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-changeme} + - POSTGRES_DB=${POSTGRES_DB:-raggr} + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-raggr}"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + raggr: build: context: ./services/raggr @@ -13,10 +28,20 @@ services: - OLLAMA_URL=${OLLAMA_URL:-http://localhost:11434} - CHROMADB_PATH=/app/chromadb - OPENAI_API_KEY=${OPENAI_API_KEY} + - JWT_SECRET_KEY=${JWT_SECRET_KEY} + - OIDC_ISSUER=${OIDC_ISSUER} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} + - OIDC_REDIRECT_URI=${OIDC_REDIRECT_URI} + - OIDC_USE_DISCOVERY=${OIDC_USE_DISCOVERY:-true} + - DATABASE_URL=${DATABASE_URL:-postgres://raggr:changeme@postgres:5432/raggr} + depends_on: + postgres: + condition: service_healthy volumes: - chromadb_data:/app/chromadb - - database_data:/app/database + restart: unless-stopped volumes: chromadb_data: - database_data: + postgres_data: diff --git a/services/raggr/app.py b/services/raggr/app.py index 2425aad..2bfa02c 100644 --- a/services/raggr/app.py +++ b/services/raggr/app.py @@ -25,10 +25,13 @@ app.register_blueprint(blueprints.conversation.conversation_blueprint) # Database configuration with environment variable support -DATABASE_PATH = os.getenv("DATABASE_PATH", "database/raggr.db") +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgres://raggr:raggr_dev_password@localhost:5432/raggr" +) TORTOISE_CONFIG = { - "connections": {"default": f"sqlite://{DATABASE_PATH}"}, + "connections": {"default": DATABASE_URL}, "apps": { "models": { "models": [ diff --git a/services/raggr/blueprints/users/__init__.py b/services/raggr/blueprints/users/__init__.py index 12944f7..e92933d 100644 --- a/services/raggr/blueprints/users/__init__.py +++ b/services/raggr/blueprints/users/__init__.py @@ -6,13 +6,161 @@ from quart_jwt_extended import ( get_jwt_identity, ) from .models import User +from .oidc_service import OIDCUserService +from oidc_config import oidc_config +import secrets +import httpx +from urllib.parse import urlencode +import hashlib +import base64 user_blueprint = Blueprint("user_api", __name__, url_prefix="/api/user") +# In-memory storage for OIDC state/PKCE (production: use Redis or database) +# Format: {state: {"pkce_verifier": str, "redirect_after_login": str}} +_oidc_sessions = {} + +@user_blueprint.route("/oidc/login", methods=["GET"]) +async def oidc_login(): + """ + Initiate OIDC login flow + Generates PKCE parameters and redirects to Authelia + """ + if not oidc_config.validate_config(): + return jsonify({"error": "OIDC not configured"}), 500 + + try: + # Generate PKCE parameters + code_verifier = secrets.token_urlsafe(64) + + # For PKCE, we need code_challenge = BASE64URL(SHA256(code_verifier)) + code_challenge = ( + base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()) + .decode() + .rstrip("=") + ) + + # Generate state for CSRF protection + state = secrets.token_urlsafe(32) + + # Store PKCE verifier and state for callback validation + _oidc_sessions[state] = { + "pkce_verifier": code_verifier, + "redirect_after_login": request.args.get("redirect", "/"), + } + + # Get authorization endpoint from discovery + discovery = await oidc_config.get_discovery_document() + auth_endpoint = discovery.get("authorization_endpoint") + + # Build authorization URL + params = { + "client_id": oidc_config.client_id, + "response_type": "code", + "redirect_uri": oidc_config.redirect_uri, + "scope": "openid email profile", + "state": state, + "code_challenge": code_challenge, + "code_challenge_method": "S256", + } + + auth_url = f"{auth_endpoint}?{urlencode(params)}" + + return jsonify({"auth_url": auth_url}) + except Exception as e: + return jsonify({"error": f"OIDC login failed: {str(e)}"}), 500 + + +@user_blueprint.route("/oidc/callback", methods=["GET"]) +async def oidc_callback(): + """ + Handle OIDC callback from Authelia + Exchanges authorization code for tokens, verifies ID token, and creates/updates user + """ + # Get authorization code and state from callback + code = request.args.get("code") + state = request.args.get("state") + error = request.args.get("error") + + if error: + return jsonify({"error": f"OIDC error: {error}"}), 400 + + if not code or not state: + return jsonify({"error": "Missing code or state"}), 400 + + # Validate state and retrieve PKCE verifier + session = _oidc_sessions.pop(state, None) + if not session: + return jsonify({"error": "Invalid or expired state"}), 400 + + pkce_verifier = session["pkce_verifier"] + + # Exchange authorization code for tokens + discovery = await oidc_config.get_discovery_document() + token_endpoint = discovery.get("token_endpoint") + + token_data = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": oidc_config.redirect_uri, + "client_id": oidc_config.client_id, + "client_secret": oidc_config.client_secret, + "code_verifier": pkce_verifier, + } + + # Use client_secret_post method (credentials in POST body) + async with httpx.AsyncClient() as client: + token_response = await client.post(token_endpoint, data=token_data) + + if token_response.status_code != 200: + return jsonify({"error": f"Failed to exchange code for token: {token_response.text}"}), 400 + + tokens = token_response.json() + + id_token = tokens.get("id_token") + if not id_token: + return jsonify({"error": "No ID token received"}), 400 + + # Verify ID token + try: + claims = await oidc_config.verify_id_token(id_token) + except Exception as e: + return jsonify({"error": f"ID token verification failed: {str(e)}"}), 400 + + # Get or create user from OIDC claims + user = await OIDCUserService.get_or_create_user_from_oidc(claims) + + # Issue backend JWT tokens + access_token = create_access_token(identity=str(user.id)) + refresh_token = create_refresh_token(identity=str(user.id)) + + # Return tokens to frontend + # Frontend will handle storing these and redirecting + return jsonify( + access_token=access_token, + refresh_token=refresh_token, + user={"id": str(user.id), "username": user.username, "email": user.email}, + ) + + +@user_blueprint.route("/refresh", methods=["POST"]) +@jwt_refresh_token_required +async def refresh(): + """Refresh access token (unchanged from original)""" + user_id = get_jwt_identity() + new_token = create_access_token(identity=user_id) + return jsonify(access_token=new_token) + + +# Legacy username/password login - kept for backward compatibility during migration @user_blueprint.route("/login", methods=["POST"]) async def login(): + """ + Legacy username/password login + This can be removed after full OIDC migration is complete + """ data = await request.get_json() username = data.get("username") password = data.get("password") @@ -28,13 +176,5 @@ async def login(): return jsonify( access_token=access_token, refresh_token=refresh_token, - user={"id": user.id, "username": user.username}, + user={"id": str(user.id), "username": user.username}, ) - - -@user_blueprint.route("/refresh", methods=["POST"]) -@jwt_refresh_token_required -async def refresh(): - user_id = get_jwt_identity() - new_token = create_access_token(identity=user_id) - return jsonify(access_token=new_token) diff --git a/services/raggr/blueprints/users/models.py b/services/raggr/blueprints/users/models.py index 43930f0..9b3f6be 100644 --- a/services/raggr/blueprints/users/models.py +++ b/services/raggr/blueprints/users/models.py @@ -8,8 +8,13 @@ import bcrypt class User(Model): id = fields.UUIDField(primary_key=True) username = fields.CharField(max_length=255) - password = fields.BinaryField() # Hashed + password = fields.BinaryField(null=True) # Hashed - nullable for OIDC users email = fields.CharField(max_length=100, unique=True) + + # OIDC fields + oidc_subject = fields.CharField(max_length=255, unique=True, null=True, index=True) # "sub" claim from OIDC + auth_provider = fields.CharField(max_length=50, default="local") # "local" or "oidc" + created_at = fields.DatetimeField(auto_now_add=True) updated_at = fields.DatetimeField(auto_now=True) @@ -23,4 +28,6 @@ class User(Model): ) def verify_password(self, plain_password: str): + if not self.password: + return False return bcrypt.checkpw(plain_password.encode("utf-8"), self.password) diff --git a/services/raggr/blueprints/users/oidc_service.py b/services/raggr/blueprints/users/oidc_service.py new file mode 100644 index 0000000..d01441a --- /dev/null +++ b/services/raggr/blueprints/users/oidc_service.py @@ -0,0 +1,76 @@ +""" +OIDC User Management Service +""" +from typing import Dict, Any, Optional +from uuid import uuid4 +from .models import User + + +class OIDCUserService: + """Service for managing OIDC user authentication and provisioning""" + + @staticmethod + async def get_or_create_user_from_oidc(claims: Dict[str, Any]) -> User: + """ + Get existing user by OIDC subject, or create new user from OIDC claims + + Args: + claims: Decoded OIDC ID token claims + + Returns: + User object (existing or newly created) + """ + oidc_subject = claims.get("sub") + if not oidc_subject: + raise ValueError("No 'sub' claim in ID token") + + # Try to find existing user by OIDC subject + user = await User.filter(oidc_subject=oidc_subject).first() + + if user: + # Update user info from latest claims (optional) + user.email = claims.get("email", user.email) + user.username = ( + claims.get("preferred_username") + or claims.get("name") + or user.username + ) + await user.save() + return user + + # Check if user exists by email (migration case) + email = claims.get("email") + if email: + user = await User.filter(email=email, auth_provider="local").first() + if user: + # Migrate existing local user to OIDC + user.oidc_subject = oidc_subject + user.auth_provider = "oidc" + user.password = None # Clear password + await user.save() + return user + + # Create new user from OIDC claims + username = ( + claims.get("preferred_username") + or claims.get("name") + or claims.get("email", "").split("@")[0] + or f"user_{oidc_subject[:8]}" + ) + + user = await User.create( + id=uuid4(), + username=username, + email=email + or f"{oidc_subject}@oidc.local", # Fallback if no email claim + oidc_subject=oidc_subject, + auth_provider="oidc", + password=None, + ) + + return user + + @staticmethod + async def find_user_by_oidc_subject(oidc_subject: str) -> Optional[User]: + """Find user by OIDC subject ID""" + return await User.filter(oidc_subject=oidc_subject).first() diff --git a/services/raggr/migrations/models/0_20251025081744_init.py b/services/raggr/migrations/models/0_20251025081744_init.py deleted file mode 100644 index 2c09b45..0000000 --- a/services/raggr/migrations/models/0_20251025081744_init.py +++ /dev/null @@ -1,63 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - CREATE TABLE IF NOT EXISTS "conversations" ( - "id" CHAR(36) NOT NULL PRIMARY KEY, - "name" VARCHAR(255) NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE TABLE IF NOT EXISTS "conversation_messages" ( - "id" CHAR(36) NOT NULL PRIMARY KEY, - "text" TEXT NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "speaker" VARCHAR(10) NOT NULL /* USER: user\nSIMBA: simba */, - "conversation_id" CHAR(36) NOT NULL REFERENCES "conversations" ("id") ON DELETE CASCADE -); -CREATE TABLE IF NOT EXISTS "users" ( - "id" CHAR(36) NOT NULL PRIMARY KEY, - "username" VARCHAR(255) NOT NULL, - "password" BLOB NOT NULL, - "email" VARCHAR(100) NOT NULL UNIQUE, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); -CREATE TABLE IF NOT EXISTS "aerich" ( - "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, - "version" VARCHAR(255) NOT NULL, - "app" VARCHAR(100) NOT NULL, - "content" JSON NOT NULL -);""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - """ - - -MODELS_STATE = ( - "eJztmG1v4jgQx79KlFddaa9q2W53VZ1OCpTecrvACcLdPtwqMskAVhMnazvboorvfrbJE4" - "kJpWq3UPGmhRkPtn8ztv/2nRmEHvjsuBWSn0AZ4jgk5oVxZxIUgPig9b82TBRFuVcaOBr7" - "KsAttFQeNGacIpcL5wT5DITJA+ZSHCWdkdj3pTF0RUNMprkpJvhHDA4Pp8BnQIXj23dhxs" - "SDW2Dp1+jamWDwvZVxY0/2rewOn0fKNhp1Lq9US9nd2HFDPw5I3jqa81lIsuZxjL1jGSN9" - "UyBAEQevMA05ymTaqWk5YmHgNIZsqF5u8GCCYl/CMH+fxMSVDAzVk/xz9oe5BR6BWqLFhE" - "sWd4vlrPI5K6spu2p9sAZHb85fqVmGjE+pcioi5kIFIo6WoYprDlL9r6BszRDVo0zbl2CK" - "gT4EY2rIOeY1lIJMAT2MmhmgW8cHMuUz8bXx9m0Nxn+sgSIpWimUoajrZdX3Eldj6ZNIc4" - "QuBTllB/EqyEvh4TgAPczVyBJSLwk9Tj/sKGAxB69P/HmyCGr42p1ue2hb3b/lTALGfvgK" - "kWW3paehrPOS9ei8lIrsR4x/O/YHQ341vvZ77XLtZ+3sr6YcE4p56JDwxkFeYb2m1hTMSm" - "LjyHtgYlcjD4l91sSqwcuTZHJd2AKlYYzc6xtEPWfFUzgdgTE0BVZNfzOJvPo4AD87NkuJ" - "1hyu3eUv7mbGF2kZp9YivLARrqNXdQWNoGxBRMzbS/qWPdXQ2aBQChDvJ1ScYiIPgmWvBQ" - "uHW812bAurHmXafl8ES9022/5sr+ywqSw56lqfX63ssp/6vT/T5gUZ0/rUbx7Uy0s85Krq" - "hUWAroHqxX2bxIHKakfgQMSFSnYL4c+8dMzRsD24MGIG9D8y7HSb1oXBcDBG5gNuAKcn97" - "gAnJ6s1f/SVVpAxYNmu21eE/qYe/6zblYbtviKHtMDrdK8CingKfkI80r9bpZfO02xoruE" - "maKbTEzoykV8EJMEvlzY1rBlXbbNxXpt+5RKbsSUJKpIN2Wv1WpyaR+02f5rM5nHbR+Uij" - "H7otF+waNShBi7CammMpuYIDrXwyxGlWCO53x5/9k9nDX0mlKwFvWWYNbs9KzBF73mTdsX" - "C7f5xW5bJbwQIOxvU6ZZwOPU6OYl/5gVenpyP9VTJ3uquudwcXiZF4fDs+eLSOy2z55PKQ" - "0toNid6cRh4qmVhyhvszP6sEPWvDdp5aHU9KVqTxL2rIeEemr9rXF69u7s/Zvzs/eiiRpJ" - "ZnlXU/2dnr1BDsrLivYOt/6YLYQcxGAGUi6NLSAmzfcT4NNolZBwIJrz7K9hv7f2bSYNKY" - "EcETHBbx52+WvDx4x/302sNRTlrOsfkstvxqXDSP5AU/eK8yuPl8X/Etg7Fw==" -) diff --git a/services/raggr/migrations/models/0_20251225052005_init.py b/services/raggr/migrations/models/0_20251225052005_init.py new file mode 100644 index 0000000..9a8e557 --- /dev/null +++ b/services/raggr/migrations/models/0_20251225052005_init.py @@ -0,0 +1,71 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS "users" ( + "id" UUID NOT NULL PRIMARY KEY, + "username" VARCHAR(255) NOT NULL, + "password" BYTEA, + "email" VARCHAR(100) NOT NULL UNIQUE, + "oidc_subject" VARCHAR(255) UNIQUE, + "auth_provider" VARCHAR(50) NOT NULL DEFAULT 'local', + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS "idx_users_oidc_su_5aec5a" ON "users" ("oidc_subject"); +CREATE TABLE IF NOT EXISTS "conversations" ( + "id" UUID NOT NULL PRIMARY KEY, + "name" VARCHAR(255) NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "user_id" UUID REFERENCES "users" ("id") ON DELETE CASCADE +); +CREATE TABLE IF NOT EXISTS "conversation_messages" ( + "id" UUID NOT NULL PRIMARY KEY, + "text" TEXT NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + "speaker" VARCHAR(10) NOT NULL, + "conversation_id" UUID NOT NULL REFERENCES "conversations" ("id") ON DELETE CASCADE +); +COMMENT ON COLUMN "conversation_messages"."speaker" IS 'USER: user\nSIMBA: simba'; +CREATE TABLE IF NOT EXISTS "aerich" ( + "id" SERIAL NOT NULL PRIMARY KEY, + "version" VARCHAR(255) NOT NULL, + "app" VARCHAR(100) NOT NULL, + "content" JSONB NOT NULL +);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ + + +MODELS_STATE = ( + "eJztmmtP4zgUhv9KlE+MxCLoUGaEViulpex0Z9qO2nR3LjuK3MRtvSROJnYGKsR/X9u5J0" + "56AUqL+gXosU9sPz7OeY/Lveq4FrTJSdvFv6BPAEUuVi+VexUDB7I/pO3Higo8L23lBgom" + "tnAwMz1FC5gQ6gOTssYpsAlkJgsS00deNBgObJsbXZN1RHiWmgKMfgbQoO4M0jn0WcP3H8" + "yMsAXvIIk/ejfGFEHbys0bWXxsYTfowhO28bh7dS168uEmhunagYPT3t6Czl2cdA8CZJ1w" + "H942gxj6gEIrsww+y2jZsSmcMTNQP4DJVK3UYMEpCGwOQ/19GmCTM1DESPzH+R/qGngYao" + "4WYcpZ3D+Eq0rXLKwqH6r9QRsevb14I1bpEjrzRaMgoj4IR0BB6Cq4piDF7xLK9hz4cpRx" + "/wJMNtFNMMaGlGMaQzHIGNBm1FQH3Bk2xDM6Zx8bzWYNxr+1oSDJegmULovrMOr7UVMjbO" + "NIU4SmD/mSDUDLIK9YC0UOlMPMexaQWpHrSfzHjgJma7AG2F5Eh6CGr97tdUa61vvMV+IQ" + "8tMWiDS9w1sawrooWI8uCluRPET5p6t/UPhH5dug3ynGftJP/6byOYGAugZ2bw1gZc5rbI" + "3B5DY28KwNNzbvedjYF93YaPKZfSXQN9bLIBmXR6SRaG5b3MTNkwZPvdMbac7gMMrwrl0f" + "ohn+CBcCYZfNA2BTliwi0TGOHrOr0FJrOgsf3CZqJBsUbHVsTZCG2VMbtbWrjioYToB5cw" + "t8y6iA6UBCwAySMtBW5Hn9cQjtRJrJWWYFXC984m6+VarYClZuw80wytErNzkNp2gBmK3b" + "isbmI9XQWaKCMxBXE8NGdiMPonivRTGFd5KUrzOrHGXcf19EcV0q73zRc1k8lr5HPe3Lm1" + "wm/zTo/xl3z0jl9qdB66CQX6OQKitk4kFwIxMDvIDs4MApSYHc7mbcX/joqONRZ3ip8Iz+" + "Lx51ey3tUiHImQB1tS3OVZlnpysUmWenlTUmbyocoGyiWe81L3F9ynf+nkpYs3Dh9UgpW7" + "w/21mKSzWtJFzW1bbPqeREzSCRbnEtUa3V+NE+aLP912Z8H9e9tMz67ItG28LFpQcIuXV9" + "SWS2EAb+Qg4z61WAOVnQsP7Z1ZJeBq/F9WpWbjFkrW5fG36VS964fzZuW1/1jlagCx2A7H" + "WiNHF4mhBdfuKfMkDPTlcTPXWqpyR7XGSZBgkm/0FTUjlUkyz6bQS0GKTb5fksB55p+bnh" + "+e4vZFWJdjnQkuP23qKq7ZrAfkQaynNtrhKmzeoobZa1+aG4fZ3F7eHrn1exscntcqlIWX" + "Y1X/pfh6e5n99lgbTde3kN+sicq5J6Lmo5rqvoQNpnZ0q6Lq64IpZWdBxzIRiinX9RYSe+" + "HfmtcXb+7vz924vz96yLmElieVfzMuj29SUVHD8I0muXav2RcTnUb6mcY0djHREXdt9PgM" + "9SX7ARKcSS9P7XaNCvvE6NXQogx5gt8LuFTHqs2IjQH7uJtYYiX3X9dz/Fr3kKuZk/oCW7" + "eN3mZeHD/9BpOYI=" +) diff --git a/services/raggr/migrations/models/1_20251025091926_update.py b/services/raggr/migrations/models/1_20251025091926_update.py deleted file mode 100644 index 3194dc7..0000000 --- a/services/raggr/migrations/models/1_20251025091926_update.py +++ /dev/null @@ -1,60 +0,0 @@ -from tortoise import BaseDBAsyncClient - -RUN_IN_TRANSACTION = True - - -async def upgrade(db: BaseDBAsyncClient) -> str: - return """ - -- SQLite doesn't support ADD CONSTRAINT, so we need to recreate the table - CREATE TABLE "conversations_new" ( - "id" CHAR(36) NOT NULL PRIMARY KEY, - "name" VARCHAR(255) NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "user_id" CHAR(36), - FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE - ); - INSERT INTO "conversations_new" ("id", "name", "created_at", "updated_at") - SELECT "id", "name", "created_at", "updated_at" FROM "conversations"; - DROP TABLE "conversations"; - ALTER TABLE "conversations_new" RENAME TO "conversations";""" - - -async def downgrade(db: BaseDBAsyncClient) -> str: - return """ - -- Recreate table without user_id column - CREATE TABLE "conversations_new" ( - "id" CHAR(36) NOT NULL PRIMARY KEY, - "name" VARCHAR(255) NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP - ); - INSERT INTO "conversations_new" ("id", "name", "created_at", "updated_at") - SELECT "id", "name", "created_at", "updated_at" FROM "conversations"; - DROP TABLE "conversations"; - ALTER TABLE "conversations_new" RENAME TO "conversations";""" - - -MODELS_STATE = ( - "eJztmWtP2zAUhv9KlE8gbQg6xhCaJqWlbB20ndp0F9gUuYnbWiROiJ1Bhfjvs91cnMRNKe" - "PSon6B9vic2H5s57w+vdU934Eu2Wn4+C8MCaDIx/qRdqtj4EH2Qdn+RtNBEGSt3EDB0BUB" - "tuQpWsCQ0BDYlDWOgEsgMzmQ2CEK4s5w5Lrc6NvMEeFxZoowuoqgRf0xpBMYsoaLP8yMsA" - "NvIEm+BpfWCEHXyY0bObxvYbfoNBC2waB1fCI8eXdDy/bdyMOZdzClEx+n7lGEnB0ew9vG" - "EMMQUOhI0+CjjKedmGYjZgYaRjAdqpMZHDgCkcth6B9HEbY5A030xP/sf9KXwMNQc7QIU8" - "7i9m42q2zOwqrzrhpfjN7Wu4NtMUuf0HEoGgUR/U4EAgpmoYJrBlL8L6FsTECoRpn4F2Cy" - "gT4EY2LIOGZ7KAGZAHoYNd0DN5YL8ZhO2Nfa+/cVGL8bPUGSeQmUPtvXs13fiZtqszaONE" - "Noh5BP2QK0DPKYtVDkQTXMfGQBqROH7iQfVhQwm4PTxe40PgQVfM1Wu9k3jfY3PhOPkCtX" - "IDLMJm+pCeu0YN06KCxF+hDtR8v8ovGv2nm30yzu/dTPPNf5mEBEfQv71xZwpPOaWBMwuY" - "WNAueBC5uP3Czsiy5sPHhpXQkMreUyiBTyH2kkHtszLuLDkwZPvaNLZc7gMMrwTvwQojE+" - "hVOBsMXGAbCtShax6BjEj1lVaJk1G0UIrlM1Im8KNjs2J0hn2dPoN4zjpi4YDoF9eQ1Cx5" - "oD04OEgDEkZaD1OPLktAfdVJqpWcoCrj174mq+VeaxFaz8mi8xytErN3k1r2gBmM3bifvm" - "PVXQWaCCJYj3E8OWvJAbUbzWopjCG0XKN5lVjTLxXxdRXJXKmz/NXBZPpO9W2/i5ncvkZ9" - "3O58RdksqNs259o5Bfo5AqK2QSQHCpEgP8AtnEkVeSArnVlcJf+Ojog36zd6TxjP4b91vt" - "unGkEeQNgX6/Jc7dMvd273HJ3Nude8fkTYUDJCea5V7zitDHfOevqYS1CwWv/5SyxfrZyl" - "JcqGkV22VZbfuUSk7cGRTSLblLzNdq/GhvtNn6azO+jssWLeWYddFoz1C4DAAh136o2Jl1" - "hEE4VcOUowowh1M6u/+sHs4KenUuWGW9xZjVWx2j90uteRN/eePWf5lNo4AXegC5y2zTNO" - "Bx9ujiI/+YO3Rv936qp0r2lHXP5uLwOi8Om9L6q1jYtHJXEoCLyp6l35Efp/a5VvXkJ615" - "GjBE9kRXaOW4pVItg8xnZeRyC88pvynVMsdc2Azxyr9ozhSV57e1vf0P+4fvDvYPmYsYSW" - "r5UPEyaHXMBeqYHwTllXa+6pBCNto4BcmPxhIQY/f1BPg00s3HFGJFev/a73bmlqqSkALI" - "AWYTvHCQTd9oLiL0z2piraDIZ11dVy+W0Au5mT+gripqPWch5u4f/FVgYA==" -) diff --git a/services/raggr/oidc_config.py b/services/raggr/oidc_config.py new file mode 100644 index 0000000..76b58b2 --- /dev/null +++ b/services/raggr/oidc_config.py @@ -0,0 +1,113 @@ +""" +OIDC Configuration for Authelia Integration +""" +import os +from typing import Dict, Any +from authlib.jose import jwt +from authlib.jose.errors import JoseError +import httpx + + +class OIDCConfig: + """OIDC Configuration Manager""" + + def __init__(self): + # Load from environment variables + self.issuer = os.getenv("OIDC_ISSUER") # e.g., https://auth.example.com + self.client_id = os.getenv("OIDC_CLIENT_ID") + self.client_secret = os.getenv("OIDC_CLIENT_SECRET") + self.redirect_uri = os.getenv( + "OIDC_REDIRECT_URI", "http://localhost:8080/api/user/oidc/callback" + ) + + # OIDC endpoints (can use discovery or manual config) + self.use_discovery = os.getenv("OIDC_USE_DISCOVERY", "true").lower() == "true" + + # Manual endpoint configuration (fallback if discovery fails) + self.authorization_endpoint = os.getenv("OIDC_AUTHORIZATION_ENDPOINT") + self.token_endpoint = os.getenv("OIDC_TOKEN_ENDPOINT") + self.userinfo_endpoint = os.getenv("OIDC_USERINFO_ENDPOINT") + self.jwks_uri = os.getenv("OIDC_JWKS_URI") + + # Cached discovery document and JWKS + self._discovery_doc: Dict[str, Any] | None = None + self._jwks: Dict[str, Any] | None = None + + def validate_config(self) -> bool: + """Validate that required configuration is present""" + if not self.issuer or not self.client_id or not self.client_secret: + return False + return True + + async def get_discovery_document(self) -> Dict[str, Any]: + """Fetch OIDC discovery document from .well-known endpoint""" + if self._discovery_doc: + return self._discovery_doc + + if not self.use_discovery: + # Return manual configuration + return { + "issuer": self.issuer, + "authorization_endpoint": self.authorization_endpoint, + "token_endpoint": self.token_endpoint, + "userinfo_endpoint": self.userinfo_endpoint, + "jwks_uri": self.jwks_uri, + } + + discovery_url = f"{self.issuer.rstrip('/')}/.well-known/openid-configuration" + + async with httpx.AsyncClient() as client: + response = await client.get(discovery_url) + response.raise_for_status() + self._discovery_doc = response.json() + return self._discovery_doc + + async def get_jwks(self) -> Dict[str, Any]: + """Fetch JSON Web Key Set for token verification""" + if self._jwks: + return self._jwks + + discovery = await self.get_discovery_document() + jwks_uri = discovery.get("jwks_uri") + + if not jwks_uri: + raise ValueError("No jwks_uri found in discovery document") + + async with httpx.AsyncClient() as client: + response = await client.get(jwks_uri) + response.raise_for_status() + self._jwks = response.json() + return self._jwks + + async def verify_id_token(self, id_token: str) -> Dict[str, Any]: + """ + Verify and decode ID token from OIDC provider + + Returns the decoded claims if valid + Raises exception if invalid + """ + jwks = await self.get_jwks() + + try: + # Verify token signature and claims + claims = jwt.decode( + id_token, + jwks, + claims_options={ + "iss": {"essential": True, "value": self.issuer}, + "aud": {"essential": True, "value": self.client_id}, + "exp": {"essential": True}, + }, + ) + + # Additional validation + claims.validate() + + return claims + + except JoseError as e: + raise ValueError(f"Invalid ID token: {str(e)}") + + +# Global instance +oidc_config = OIDCConfig() diff --git a/services/raggr/pyproject.toml b/services/raggr/pyproject.toml index 02c15fa..2eab225 100644 --- a/services/raggr/pyproject.toml +++ b/services/raggr/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "Add your description here" readme = "README.md" requires-python = ">=3.13" -dependencies = ["chromadb>=1.1.0", "python-dotenv>=1.0.0", "flask>=3.1.2", "httpx>=0.28.1", "ollama>=0.6.0", "openai>=2.0.1", "pydantic>=2.11.9", "pillow>=10.0.0", "pymupdf>=1.24.0", "black>=25.9.0", "pillow-heif>=1.1.1", "flask-jwt-extended>=4.7.1", "bcrypt>=5.0.0", "pony>=0.7.19", "flask-login>=0.6.3", "quart>=0.20.0", "tortoise-orm>=0.25.1", "quart-jwt-extended>=0.1.0", "pre-commit>=4.3.0", "tortoise-orm-stubs>=1.0.2", "aerich>=0.8.0", "tomlkit>=0.13.3"] +dependencies = ["chromadb>=1.1.0", "python-dotenv>=1.0.0", "flask>=3.1.2", "httpx>=0.28.1", "ollama>=0.6.0", "openai>=2.0.1", "pydantic>=2.11.9", "pillow>=10.0.0", "pymupdf>=1.24.0", "black>=25.9.0", "pillow-heif>=1.1.1", "flask-jwt-extended>=4.7.1", "bcrypt>=5.0.0", "pony>=0.7.19", "flask-login>=0.6.3", "quart>=0.20.0", "tortoise-orm>=0.25.1", "quart-jwt-extended>=0.1.0", "pre-commit>=4.3.0", "tortoise-orm-stubs>=1.0.2", "aerich>=0.8.0", "tomlkit>=0.13.3", "authlib>=1.3.0", "asyncpg>=0.30.0"] [tool.aerich] tortoise_orm = "app.TORTOISE_CONFIG" diff --git a/services/raggr/raggr-frontend/src/api/oidcService.ts b/services/raggr/raggr-frontend/src/api/oidcService.ts new file mode 100644 index 0000000..98df0b3 --- /dev/null +++ b/services/raggr/raggr-frontend/src/api/oidcService.ts @@ -0,0 +1,94 @@ +/** + * OIDC Authentication Service + * Handles OAuth 2.0 Authorization Code flow with PKCE + */ + +interface OIDCLoginResponse { + auth_url: string; +} + +interface OIDCCallbackResponse { + access_token: string; + refresh_token: string; + user: { + id: string; + username: string; + email: string; + }; +} + +class OIDCService { + private baseUrl = "/api/user/oidc"; + + /** + * Initiate OIDC login flow + * Returns authorization URL to redirect user to + */ + async initiateLogin(redirectAfterLogin: string = "/"): Promise { + const response = await fetch( + `${this.baseUrl}/login?redirect=${encodeURIComponent(redirectAfterLogin)}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + } + ); + + if (!response.ok) { + throw new Error("Failed to initiate OIDC login"); + } + + const data: OIDCLoginResponse = await response.json(); + return data.auth_url; + } + + /** + * Handle OIDC callback + * Exchanges authorization code for tokens + */ + async handleCallback( + code: string, + state: string + ): Promise { + const response = await fetch( + `${this.baseUrl}/callback?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + } + ); + + if (!response.ok) { + throw new Error("OIDC callback failed"); + } + + return await response.json(); + } + + /** + * Extract OIDC callback parameters from URL + */ + getCallbackParamsFromURL(): { code: string; state: string } | null { + const params = new URLSearchParams(window.location.search); + const code = params.get("code"); + const state = params.get("state"); + + if (code && state) { + return { code, state }; + } + + return null; + } + + /** + * Clear callback parameters from URL without reload + */ + clearCallbackParams(): void { + const url = new URL(window.location.href); + url.searchParams.delete("code"); + url.searchParams.delete("state"); + url.searchParams.delete("error"); + window.history.replaceState({}, "", url.toString()); + } +} + +export const oidcService = new OIDCService(); diff --git a/services/raggr/raggr-frontend/src/api/userService.ts b/services/raggr/raggr-frontend/src/api/userService.ts index 632a633..7254960 100644 --- a/services/raggr/raggr-frontend/src/api/userService.ts +++ b/services/raggr/raggr-frontend/src/api/userService.ts @@ -4,6 +4,7 @@ interface LoginResponse { user: { id: string; username: string; + email?: string; }; } diff --git a/services/raggr/raggr-frontend/src/components/ChatScreen.tsx b/services/raggr/raggr-frontend/src/components/ChatScreen.tsx index 92e6e44..77fa284 100644 --- a/services/raggr/raggr-frontend/src/components/ChatScreen.tsx +++ b/services/raggr/raggr-frontend/src/components/ChatScreen.tsx @@ -190,7 +190,7 @@ export const ChatScreen = ({ setAuthenticated }: ChatScreenProps) => { className="cursor-pointer hover:opacity-80" onClick={() => setSidebarCollapsed(true)} /> -

asksimba!

+

asksimba!

void; }; export const LoginScreen = ({ setAuthenticated }: LoginScreenProps) => { - const [username, setUsername] = useState(""); - const [password, setPassword] = useState(""); const [error, setError] = useState(""); const [isChecking, setIsChecking] = useState(true); + const [isLoggingIn, setIsLoggingIn] = useState(false); useEffect(() => { - // Check if user is already authenticated - const checkAuth = async () => { + const initAuth = async () => { + // First, check for OIDC callback parameters + const callbackParams = oidcService.getCallbackParamsFromURL(); + + if (callbackParams) { + // Handle OIDC callback + try { + setIsLoggingIn(true); + const result = await oidcService.handleCallback( + callbackParams.code, + callbackParams.state + ); + + // Store tokens + localStorage.setItem("access_token", result.access_token); + localStorage.setItem("refresh_token", result.refresh_token); + + // Clear URL parameters + oidcService.clearCallbackParams(); + + setAuthenticated(true); + setIsChecking(false); + return; + } catch (err) { + console.error("OIDC callback error:", err); + setError("Login failed. Please try again."); + oidcService.clearCallbackParams(); + setIsLoggingIn(false); + setIsChecking(false); + return; + } + } + + // Check if user is already authenticated const isValid = await userService.validateToken(); if (isValid) { setAuthenticated(true); } setIsChecking(false); }; - checkAuth(); + + initAuth(); }, [setAuthenticated]); - const handleLogin = async (e?: React.FormEvent) => { - e?.preventDefault(); - - if (!username || !password) { - setError("Please enter username and password"); - return; - } - + const handleOIDCLogin = async () => { try { - const result = await userService.login(username, password); - localStorage.setItem("access_token", result.access_token); - localStorage.setItem("refresh_token", result.refresh_token); - setAuthenticated(true); + setIsLoggingIn(true); setError(""); + + // Get authorization URL from backend + const authUrl = await oidcService.initiateLogin(); + + // Redirect to Authelia + window.location.href = authUrl; } catch (err) { - setError("Login failed. Please check your credentials."); - console.error("Login error:", err); + setError("Failed to initiate login. Please try again."); + console.error("OIDC login error:", err); + setIsLoggingIn(false); } }; - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleLogin(); - } - }; - - // Show loading state while checking authentication - if (isChecking) { + // Show loading state while checking authentication or processing callback + if (isChecking || isLoggingIn) { return (
-

Checking authentication...

+

+ {isLoggingIn ? "Logging in..." : "Checking authentication..."} +

@@ -67,7 +93,7 @@ export const LoginScreen = ({ setAuthenticated }: LoginScreenProps) => {
-
+

I AM LOOKING FOR A DESIGNER. THIS APP WILL REMAIN UGLY UNTIL A @@ -77,42 +103,24 @@ export const LoginScreen = ({ setAuthenticated }: LoginScreenProps) => {

ask simba!

- - setUsername(e.target.value)} - onKeyPress={handleKeyPress} - className="border border-s-slate-950 p-3 rounded-md min-h-[44px]" - /> - - setPassword(e.target.value)} - onKeyPress={handleKeyPress} - className="border border-s-slate-950 p-3 rounded-md min-h-[44px]" - /> + {error && ( -
+
{error}
)} + +
+ Click below to login with Authelia +

diff --git a/services/raggr/startup-dev.sh b/services/raggr/startup-dev.sh index b8719cf..6a06318 100755 --- a/services/raggr/startup-dev.sh +++ b/services/raggr/startup-dev.sh @@ -1,8 +1,8 @@ #!/bin/bash set -e -echo "Initializing database directories..." -mkdir -p /app/chromadb /app/database +echo "Initializing directories..." +mkdir -p /app/chromadb echo "Waiting for frontend to build..." while [ ! -f /app/raggr-frontend/dist/index.html ]; do @@ -10,19 +10,17 @@ while [ ! -f /app/raggr-frontend/dist/index.html ]; do done echo "Frontend built successfully!" -echo "Running database migrations..." -aerich upgrade +echo "Setting up database..." +# Give PostgreSQL a moment to be ready (healthcheck in docker-compose handles this) +sleep 3 -echo "Initializing visited.db with indexed_documents table..." -python3 -c " -import sqlite3 -conn = sqlite3.connect('database/visited.db') -c = conn.cursor() -c.execute('CREATE TABLE IF NOT EXISTS indexed_documents (id INTEGER PRIMARY KEY AUTOINCREMENT, paperless_id INTEGER)') -conn.commit() -conn.close() -print('Database initialized successfully') -" +if ls migrations/models/0_*.py 1> /dev/null 2>&1; then + echo "Running database migrations..." + aerich upgrade +else + echo "No migrations found, initializing database..." + aerich init-db +fi echo "Starting reindex process..." python main.py "" --reindex || echo "Reindex failed, continuing anyway..." diff --git a/services/raggr/uv.lock b/services/raggr/uv.lock index 98c6b20..8fb2531 100644 --- a/services/raggr/uv.lock +++ b/services/raggr/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -81,6 +81,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "authlib" +version = "1.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -218,6 +230,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "cfgv" version = "3.4.0" @@ -333,6 +390,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "cryptography" +version = "46.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/33/c00162f49c0e2fe8064a62cb92b93e50c74a72bc370ab92f86112b33ff62/cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1", size = 749258, upload-time = "2025-10-15T23:18:31.74Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/42/9c391dd801d6cf0d561b5890549d4b27bafcc53b39c31a817e69d87c625b/cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a", size = 7225004, upload-time = "2025-10-15T23:16:52.239Z" }, + { url = "https://files.pythonhosted.org/packages/1c/67/38769ca6b65f07461eb200e85fc1639b438bdc667be02cf7f2cd6a64601c/cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc", size = 4296667, upload-time = "2025-10-15T23:16:54.369Z" }, + { url = "https://files.pythonhosted.org/packages/5c/49/498c86566a1d80e978b42f0d702795f69887005548c041636df6ae1ca64c/cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d", size = 4450807, upload-time = "2025-10-15T23:16:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0a/863a3604112174c8624a2ac3c038662d9e59970c7f926acdcfaed8d61142/cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb", size = 4299615, upload-time = "2025-10-15T23:16:58.442Z" }, + { url = "https://files.pythonhosted.org/packages/64/02/b73a533f6b64a69f3cd3872acb6ebc12aef924d8d103133bb3ea750dc703/cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849", size = 4016800, upload-time = "2025-10-15T23:17:00.378Z" }, + { url = "https://files.pythonhosted.org/packages/25/d5/16e41afbfa450cde85a3b7ec599bebefaef16b5c6ba4ec49a3532336ed72/cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8", size = 4984707, upload-time = "2025-10-15T23:17:01.98Z" }, + { url = "https://files.pythonhosted.org/packages/c9/56/e7e69b427c3878352c2fb9b450bd0e19ed552753491d39d7d0a2f5226d41/cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec", size = 4482541, upload-time = "2025-10-15T23:17:04.078Z" }, + { url = "https://files.pythonhosted.org/packages/78/f6/50736d40d97e8483172f1bb6e698895b92a223dba513b0ca6f06b2365339/cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91", size = 4299464, upload-time = "2025-10-15T23:17:05.483Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/d8e26b1a855f19d9994a19c702fa2e93b0456beccbcfe437eda00e0701f2/cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e", size = 4950838, upload-time = "2025-10-15T23:17:07.425Z" }, + { url = "https://files.pythonhosted.org/packages/8f/29/798fc4ec461a1c9e9f735f2fc58741b0daae30688f41b2497dcbc9ed1355/cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926", size = 4481596, upload-time = "2025-10-15T23:17:09.343Z" }, + { url = "https://files.pythonhosted.org/packages/15/8d/03cd48b20a573adfff7652b76271078e3045b9f49387920e7f1f631d125e/cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71", size = 4426782, upload-time = "2025-10-15T23:17:11.22Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/ebacbfe53317d55cf33165bda24c86523497a6881f339f9aae5c2e13e57b/cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac", size = 4698381, upload-time = "2025-10-15T23:17:12.829Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/8a6a9525893325fc057a01f654d7efc2c64b9de90413adcf605a85744ff4/cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018", size = 3055988, upload-time = "2025-10-15T23:17:14.65Z" }, + { url = "https://files.pythonhosted.org/packages/7e/bf/80fbf45253ea585a1e492a6a17efcb93467701fa79e71550a430c5e60df0/cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb", size = 3514451, upload-time = "2025-10-15T23:17:16.142Z" }, + { url = "https://files.pythonhosted.org/packages/2e/af/9b302da4c87b0beb9db4e756386a7c6c5b8003cd0e742277888d352ae91d/cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c", size = 2928007, upload-time = "2025-10-15T23:17:18.04Z" }, + { url = "https://files.pythonhosted.org/packages/f5/e2/a510aa736755bffa9d2f75029c229111a1d02f8ecd5de03078f4c18d91a3/cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217", size = 7158012, upload-time = "2025-10-15T23:17:19.982Z" }, + { url = "https://files.pythonhosted.org/packages/73/dc/9aa866fbdbb95b02e7f9d086f1fccfeebf8953509b87e3f28fff927ff8a0/cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5", size = 4288728, upload-time = "2025-10-15T23:17:21.527Z" }, + { url = "https://files.pythonhosted.org/packages/c5/fd/bc1daf8230eaa075184cbbf5f8cd00ba9db4fd32d63fb83da4671b72ed8a/cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715", size = 4435078, upload-time = "2025-10-15T23:17:23.042Z" }, + { url = "https://files.pythonhosted.org/packages/82/98/d3bd5407ce4c60017f8ff9e63ffee4200ab3e23fe05b765cab805a7db008/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54", size = 4293460, upload-time = "2025-10-15T23:17:24.885Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/e23e7900983c2b8af7a08098db406cf989d7f09caea7897e347598d4cd5b/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459", size = 3995237, upload-time = "2025-10-15T23:17:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/af68c509d4a138cfe299d0d7ddb14afba15233223ebd933b4bbdbc7155d3/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422", size = 4967344, upload-time = "2025-10-15T23:17:28.06Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e3/8643d077c53868b681af077edf6b3cb58288b5423610f21c62aadcbe99f4/cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7", size = 4466564, upload-time = "2025-10-15T23:17:29.665Z" }, + { url = "https://files.pythonhosted.org/packages/0e/43/c1e8726fa59c236ff477ff2b5dc071e54b21e5a1e51aa2cee1676f1c986f/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044", size = 4292415, upload-time = "2025-10-15T23:17:31.686Z" }, + { url = "https://files.pythonhosted.org/packages/42/f9/2f8fefdb1aee8a8e3256a0568cffc4e6d517b256a2fe97a029b3f1b9fe7e/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665", size = 4931457, upload-time = "2025-10-15T23:17:33.478Z" }, + { url = "https://files.pythonhosted.org/packages/79/30/9b54127a9a778ccd6d27c3da7563e9f2d341826075ceab89ae3b41bf5be2/cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3", size = 4466074, upload-time = "2025-10-15T23:17:35.158Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/b4f4a10928e26c941b1b6a179143af9f4d27d88fe84a6a3c53592d2e76bf/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20", size = 4420569, upload-time = "2025-10-15T23:17:37.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/3746dab4c0d1979888f125226357d3262a6dd40e114ac29e3d2abdf1ec55/cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de", size = 4681941, upload-time = "2025-10-15T23:17:39.236Z" }, + { url = "https://files.pythonhosted.org/packages/fd/30/27654c1dbaf7e4a3531fa1fc77986d04aefa4d6d78259a62c9dc13d7ad36/cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914", size = 3022339, upload-time = "2025-10-15T23:17:40.888Z" }, + { url = "https://files.pythonhosted.org/packages/f6/30/640f34ccd4d2a1bc88367b54b926b781b5a018d65f404d409aba76a84b1c/cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db", size = 3494315, upload-time = "2025-10-15T23:17:42.769Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/88cc7e3bd0a8e7b861f26981f7b820e1f46aa9d26cc482d0feba0ecb4919/cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21", size = 2919331, upload-time = "2025-10-15T23:17:44.468Z" }, + { url = "https://files.pythonhosted.org/packages/fd/23/45fe7f376a7df8daf6da3556603b36f53475a99ce4faacb6ba2cf3d82021/cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936", size = 7218248, upload-time = "2025-10-15T23:17:46.294Z" }, + { url = "https://files.pythonhosted.org/packages/27/32/b68d27471372737054cbd34c84981f9edbc24fe67ca225d389799614e27f/cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683", size = 4294089, upload-time = "2025-10-15T23:17:48.269Z" }, + { url = "https://files.pythonhosted.org/packages/26/42/fa8389d4478368743e24e61eea78846a0006caffaf72ea24a15159215a14/cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d", size = 4440029, upload-time = "2025-10-15T23:17:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/f483db0ec5ac040824f269e93dd2bd8a21ecd1027e77ad7bdf6914f2fd80/cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0", size = 4297222, upload-time = "2025-10-15T23:17:51.357Z" }, + { url = "https://files.pythonhosted.org/packages/fd/cf/da9502c4e1912cb1da3807ea3618a6829bee8207456fbbeebc361ec38ba3/cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc", size = 4012280, upload-time = "2025-10-15T23:17:52.964Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8f/9adb86b93330e0df8b3dcf03eae67c33ba89958fc2e03862ef1ac2b42465/cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3", size = 4978958, upload-time = "2025-10-15T23:17:54.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/5fa77988289c34bdb9f913f5606ecc9ada1adb5ae870bd0d1054a7021cc4/cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971", size = 4473714, upload-time = "2025-10-15T23:17:56.754Z" }, + { url = "https://files.pythonhosted.org/packages/14/e5/fc82d72a58d41c393697aa18c9abe5ae1214ff6f2a5c18ac470f92777895/cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac", size = 4296970, upload-time = "2025-10-15T23:17:58.588Z" }, + { url = "https://files.pythonhosted.org/packages/78/06/5663ed35438d0b09056973994f1aec467492b33bd31da36e468b01ec1097/cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04", size = 4940236, upload-time = "2025-10-15T23:18:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/fc/59/873633f3f2dcd8a053b8dd1d38f783043b5fce589c0f6988bf55ef57e43e/cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506", size = 4472642, upload-time = "2025-10-15T23:18:02.749Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/8e71f3930e40f6877737d6f69248cf74d4e34b886a3967d32f919cc50d3b/cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963", size = 4423126, upload-time = "2025-10-15T23:18:04.85Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c7/f65027c2810e14c3e7268353b1681932b87e5a48e65505d8cc17c99e36ae/cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4", size = 4686573, upload-time = "2025-10-15T23:18:06.908Z" }, + { url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" }, + { url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" }, +] + [[package]] name = "dictdiffer" version = "0.9.0" @@ -1482,6 +1595,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/52/5600104ef7b85f89fb8ec54f73504ead3f6f0294027e08d281f3cafb5c1a/pybase64-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f25140496b02db0e7401567cd869fb13b4c8118bf5c2428592ec339987146d8b", size = 31600, upload-time = "2025-07-27T13:05:52.24Z" }, ] +[[package]] +name = "pycparser" +version = "2.23" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" }, +] + [[package]] name = "pydantic" version = "2.11.9" @@ -1706,6 +1828,7 @@ version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "aerich" }, + { name = "authlib" }, { name = "bcrypt" }, { name = "black" }, { name = "chromadb" }, @@ -1732,6 +1855,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "aerich", specifier = ">=0.8.0" }, + { name = "authlib", specifier = ">=1.3.0" }, { name = "bcrypt", specifier = ">=5.0.0" }, { name = "black", specifier = ">=25.9.0" }, { name = "chromadb", specifier = ">=1.1.0" }, @@ -1975,8 +2099,8 @@ version = "0.25.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiosqlite" }, - { name = "iso8601", marker = "python_full_version < '4.0'" }, - { name = "pypika-tortoise", marker = "python_full_version < '4.0'" }, + { name = "iso8601", marker = "python_full_version < '4'" }, + { name = "pypika-tortoise", marker = "python_full_version < '4'" }, { name = "pytz" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d7/9b/de966810021fa773fead258efd8deea2bb73bb12479e27f288bd8ceb8763/tortoise_orm-0.25.1.tar.gz", hash = "sha256:4d5bfd13d5750935ffe636a6b25597c5c8f51c47e5b72d7509d712eda1a239fe", size = 128341, upload-time = "2025-06-05T10:43:31.058Z" }