From fe07f33e9d7dc0ff481d3e954002a80cd26ae795 Mon Sep 17 00:00:00 2001 From: elroy-bot Date: Sun, 3 Aug 2025 15:16:32 -0700 Subject: [PATCH] wip --- elroy/db/db_models.py | 2 + .../alembic/versions/add_user_auth_fields.py | 35 +++++ .../alembic/versions/add_user_auth_fields.py | 35 +++++ elroy/utils/utils.py | 8 ++ elroy/web_api/auth.py | 59 ++++++++ elroy/web_api/main.py | 17 ++- elroy/web_api/routes/auth.py | 132 ++++++++++++++++++ pyproject.toml | 3 + test_auth.py | 78 +++++++++++ uv.lock | 73 ++++++++++ 10 files changed, 437 insertions(+), 5 deletions(-) create mode 100644 elroy/db/postgres/alembic/versions/add_user_auth_fields.py create mode 100644 elroy/db/sqlite/alembic/versions/add_user_auth_fields.py create mode 100644 elroy/web_api/auth.py create mode 100644 elroy/web_api/routes/auth.py create mode 100644 test_auth.py diff --git a/elroy/db/db_models.py b/elroy/db/db_models.py index 1550c084..19ec4bf3 100644 --- a/elroy/db/db_models.py +++ b/elroy/db/db_models.py @@ -100,6 +100,8 @@ class User(SQLModel, table=True): __table_args__ = {"extend_existing": True} id: Optional[int] = Field(default=None, primary_key=True) token: str = Field(..., description="The unique token for the user") + email: Optional[str] = Field(None, description="User email address", unique=True) + password_hash: Optional[str] = Field(None, description="Hashed password for authentication") created_at: datetime = Field(default_factory=utc_now, nullable=False) updated_at: datetime = Field(default_factory=utc_now, nullable=False) # noqa F841 diff --git a/elroy/db/postgres/alembic/versions/add_user_auth_fields.py b/elroy/db/postgres/alembic/versions/add_user_auth_fields.py new file mode 100644 index 00000000..71be5c8e --- /dev/null +++ b/elroy/db/postgres/alembic/versions/add_user_auth_fields.py @@ -0,0 +1,35 @@ +"""add user authentication fields + +Revision ID: add_user_auth_fields +Revises: b360a1f1b06e +Create Date: 2025-08-03 12:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlmodel.sql.sqltypes import AutoString + +# revision identifiers, used by Alembic. +revision: str = "add_user_auth_fields" +down_revision: Union[str, None] = "b360a1f1b06e" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add email and password_hash columns + op.add_column("user", sa.Column("email", AutoString(), nullable=True)) + op.add_column("user", sa.Column("password_hash", AutoString(), nullable=True)) + + # Create unique index on email + op.create_index("ix_user_email", "user", ["email"], unique=True) + + +def downgrade() -> None: + # Drop the columns + op.drop_index("ix_user_email", "user") + op.drop_column("user", "password_hash") + op.drop_column("user", "email") diff --git a/elroy/db/sqlite/alembic/versions/add_user_auth_fields.py b/elroy/db/sqlite/alembic/versions/add_user_auth_fields.py new file mode 100644 index 00000000..b1caa00e --- /dev/null +++ b/elroy/db/sqlite/alembic/versions/add_user_auth_fields.py @@ -0,0 +1,35 @@ +"""add user authentication fields + +Revision ID: add_user_auth_fields +Revises: f880962b9187 +Create Date: 2025-08-03 12:00:00.000000 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlmodel.sql.sqltypes import AutoString + +# revision identifiers, used by Alembic. +revision: str = "add_user_auth_fields" +down_revision: Union[str, None] = "f880962b9187" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add email and password_hash columns + op.add_column("user", sa.Column("email", AutoString(), nullable=True)) + op.add_column("user", sa.Column("password_hash", AutoString(), nullable=True)) + + # Create unique index on email + op.create_index("ix_user_email", "user", ["email"], unique=True) + + +def downgrade() -> None: + # Drop the columns + op.drop_index("ix_user_email", "user") + op.drop_column("user", "password_hash") + op.drop_column("user", "email") diff --git a/elroy/utils/utils.py b/elroy/utils/utils.py index fd6a3b30..285ba00b 100644 --- a/elroy/utils/utils.py +++ b/elroy/utils/utils.py @@ -1,4 +1,6 @@ import asyncio +import secrets +import string import threading from concurrent.futures import ThreadPoolExecutor from datetime import datetime @@ -27,6 +29,12 @@ def run_async(thread_pool: ThreadPoolExecutor, coro): return thread_pool.submit(asyncio.run, coro).result() +def generate_random_string(length: int = 32) -> str: + """Generate a cryptographically secure random string.""" + alphabet = string.ascii_letters + string.digits + return "".join(secrets.choice(alphabet) for _ in range(length)) + + def is_blank(input: Optional[str]) -> bool: assert isinstance(input, (str, type(None))) return not input or not input.strip() diff --git a/elroy/web_api/auth.py b/elroy/web_api/auth.py new file mode 100644 index 00000000..a2ed7cce --- /dev/null +++ b/elroy/web_api/auth.py @@ -0,0 +1,59 @@ +import os +from datetime import datetime, timedelta, timezone +from typing import Optional + +import jwt +from passlib.context import CryptContext + +from ..db.db_models import User + +# Password hashing configuration +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT configuration +SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash a password.""" + return pwd_context.hash(password) + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create a JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.now(timezone.utc) + expires_delta + else: + expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def verify_token(token: str) -> Optional[dict]: + """Verify and decode a JWT token.""" + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + return None + except jwt.JWTError: + return None + + +def authenticate_user(email: str, password: str, db_session) -> Optional[User]: + """Authenticate a user by email and password.""" + user = db_session.query(User).filter(User.email == email).first() + if not user or not user.password_hash: + return None + if not verify_password(password, user.password_hash): + return None + return user diff --git a/elroy/web_api/main.py b/elroy/web_api/main.py index 1323492d..90e7919b 100644 --- a/elroy/web_api/main.py +++ b/elroy/web_api/main.py @@ -1,13 +1,20 @@ from typing import List -from fastapi import FastAPI +from fastapi import Depends, FastAPI from pydantic import BaseModel from elroy.api import Elroy from elroy.repository.memories.models import MemoryResponse +from ..db.db_models import User +from .routes.auth import get_current_user +from .routes.auth import router as auth_router + app = FastAPI(title="Elroy API", version="1.0.0", log_level="info") +# Include authentication routes +app.include_router(auth_router) + # Style note: do not catch and reraise errors, outside of specific error handling, let regular errors propagate. @@ -39,7 +46,7 @@ class ApiResponse(BaseModel): @app.get("/get_current_messages", response_model=List[MessageResponse]) -async def get_current_messages(): +async def get_current_messages(current_user: User = Depends(get_current_user)): """Return a list of current messages in the conversation context.""" elroy = Elroy() elroy.ctx @@ -52,14 +59,14 @@ async def get_current_messages(): @app.post("/create_augmented_memory", response_model=ApiResponse) -async def create_augmented_memory(request: MemoryRequest): +async def create_augmented_memory(request: MemoryRequest, current_user: User = Depends(get_current_user)): elroy = Elroy() result = elroy.create_augmented_memory(request.text) return ApiResponse(result=result) @app.get("/get_current_memories", response_model=List[MemoryResponse]) -async def get_current_memories(): +async def get_current_memories(current_user: User = Depends(get_current_user)): """Return a list of memories for the current user.""" elroy = Elroy() elroy.ctx @@ -72,7 +79,7 @@ async def get_current_memories(): @app.post("/chat", response_model=ChatResponse) -async def chat(request: ChatRequest): +async def chat(request: ChatRequest, current_user: User = Depends(get_current_user)): """Process a user message and return the updated conversation.""" elroy = Elroy() elroy.message(request.message) diff --git a/elroy/web_api/routes/auth.py b/elroy/web_api/routes/auth.py new file mode 100644 index 00000000..9cb92288 --- /dev/null +++ b/elroy/web_api/routes/auth.py @@ -0,0 +1,132 @@ +from datetime import timedelta + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from pydantic import BaseModel, EmailStr + +from ...core.ctx import ElroyContext +from ...db.db_models import User +from ...utils.utils import generate_random_string +from ..auth import ( + ACCESS_TOKEN_EXPIRE_MINUTES, + authenticate_user, + create_access_token, + get_password_hash, + verify_token, +) + +router = APIRouter(prefix="/auth", tags=["authentication"]) +security = HTTPBearer() + + +class UserRegistration(BaseModel): + email: EmailStr + password: str + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class Token(BaseModel): + access_token: str + token_type: str + + +class UserResponse(BaseModel): + id: int + email: str + token: str + + +def get_db_session(): + """Get database session dependency.""" + ctx = ElroyContext() + return ctx.db.get_session() + + +def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> User: + """Get current authenticated user from JWT token.""" + db_session = get_db_session() + + payload = verify_token(credentials.credentials) + if payload is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + email: str = payload.get("sub") + if email is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + user = db_session.query(User).filter(User.email == email).first() + if user is None: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + return user + + +@router.post("/register", response_model=UserResponse) +async def register(user_data: UserRegistration): + """Register a new user.""" + db_session = get_db_session() + + # Check if user already exists + existing_user = db_session.query(User).filter(User.email == user_data.email).first() + if existing_user: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered") + + # Create new user + user = User( + email=user_data.email, + password_hash=get_password_hash(user_data.password), + token=generate_random_string(32), # Generate unique token + ) + + db_session.add(user) + db_session.commit() + db_session.refresh(user) + + return UserResponse(id=user.id, email=user.email, token=user.token) + + +@router.post("/login", response_model=Token) +async def login(user_data: UserLogin): + """Login user and return JWT token.""" + db_session = get_db_session() + + user = authenticate_user(user_data.email, user_data.password, db_session) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = create_access_token(data={"sub": user.email}, expires_delta=access_token_expires) + + return {"access_token": access_token, "token_type": "bearer"} + + +@router.post("/logout") +async def logout(current_user: User = Depends(get_current_user)): + """Logout user (token invalidation handled client-side).""" + return {"message": "Successfully logged out"} + + +@router.get("/me", response_model=UserResponse) +async def get_current_user_info(current_user: User = Depends(get_current_user)): + """Get current user information.""" + return UserResponse(id=current_user.id, email=current_user.email, token=current_user.token) diff --git a/pyproject.toml b/pyproject.toml index 5a576a7e..4c526e25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ dependencies = [ "apscheduler>=3.11.0", "fastapi>=0.104.0", "uvicorn>=0.24.0", + "PyJWT>=2.8.0", + "passlib[bcrypt]>=1.7.4", + "python-multipart>=0.0.6", ] [project.optional-dependencies] diff --git a/test_auth.py b/test_auth.py new file mode 100644 index 00000000..eff4cffa --- /dev/null +++ b/test_auth.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Test script for authentication endpoints""" + +import requests + +BASE_URL = "http://localhost:8000" + + +def test_auth_flow(): + # Test data + test_email = "test@example.com" + test_password = "testpassword123" + + print("Testing authentication flow...") + + # 1. Test registration + print("\n1. Testing user registration...") + register_data = {"email": test_email, "password": test_password} + + response = requests.post(f"{BASE_URL}/auth/register", json=register_data) + if response.status_code == 200: + user_data = response.json() + print(f"✓ Registration successful: {user_data}") + else: + print(f"✗ Registration failed: {response.status_code} - {response.text}") + return + + # 2. Test login + print("\n2. Testing user login...") + login_data = {"email": test_email, "password": test_password} + + response = requests.post(f"{BASE_URL}/auth/login", json=login_data) + if response.status_code == 200: + token_data = response.json() + access_token = token_data["access_token"] + print(f"✓ Login successful: {token_data}") + else: + print(f"✗ Login failed: {response.status_code} - {response.text}") + return + + # 3. Test accessing protected endpoint + print("\n3. Testing protected endpoint access...") + headers = {"Authorization": f"Bearer {access_token}"} + + response = requests.get(f"{BASE_URL}/auth/me", headers=headers) + if response.status_code == 200: + user_info = response.json() + print(f"✓ Protected endpoint access successful: {user_info}") + else: + print(f"✗ Protected endpoint access failed: {response.status_code} - {response.text}") + + # 4. Test accessing protected API endpoint + print("\n4. Testing protected API endpoint...") + response = requests.get(f"{BASE_URL}/get_current_memories", headers=headers) + if response.status_code == 200: + memories = response.json() + print(f"✓ Protected API endpoint access successful: {len(memories)} memories") + else: + print(f"✗ Protected API endpoint access failed: {response.status_code} - {response.text}") + + # 5. Test logout + print("\n5. Testing logout...") + response = requests.post(f"{BASE_URL}/auth/logout", headers=headers) + if response.status_code == 200: + print(f"✓ Logout successful: {response.json()}") + else: + print(f"✗ Logout failed: {response.status_code} - {response.text}") + + print("\n✅ Authentication flow test completed!") + + +if __name__ == "__main__": + try: + test_auth_flow() + except requests.exceptions.ConnectionError: + print("❌ Could not connect to the server. Make sure the API is running on http://localhost:8000") + except Exception as e: + print(f"❌ Test failed with error: {e}") diff --git a/uv.lock b/uv.lock index 0220009c..9eab89c4 100644 --- a/uv.lock +++ b/uv.lock @@ -323,6 +323,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, ] +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, + { url = "https://files.pythonhosted.org/packages/55/2d/0c7e5ab0524bf1a443e34cdd3926ec6f5879889b2f3c32b2f5074e99ed53/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1", size = 275367, upload-time = "2025-02-28T01:23:54.578Z" }, + { url = "https://files.pythonhosted.org/packages/10/4f/f77509f08bdff8806ecc4dc472b6e187c946c730565a7470db772d25df70/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d", size = 280644, upload-time = "2025-02-28T01:23:56.547Z" }, + { url = "https://files.pythonhosted.org/packages/35/18/7d9dc16a3a4d530d0a9b845160e9e5d8eb4f00483e05d44bb4116a1861da/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492", size = 274881, upload-time = "2025-02-28T01:23:57.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/c4/ae6921088adf1e37f2a3a6a688e72e7d9e45fdd3ae5e0bc931870c1ebbda/bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90", size = 280203, upload-time = "2025-02-28T01:23:59.331Z" }, + { url = "https://files.pythonhosted.org/packages/4c/b1/1289e21d710496b88340369137cc4c5f6ee036401190ea116a7b4ae6d32a/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a", size = 275103, upload-time = "2025-02-28T01:24:00.764Z" }, + { url = "https://files.pythonhosted.org/packages/94/41/19be9fe17e4ffc5d10b7b67f10e459fc4eee6ffe9056a88de511920cfd8d/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce", size = 280513, upload-time = "2025-02-28T01:24:02.243Z" }, + { url = "https://files.pythonhosted.org/packages/aa/73/05687a9ef89edebdd8ad7474c16d8af685eb4591c3c38300bb6aad4f0076/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8", size = 274685, upload-time = "2025-02-28T01:24:04.512Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/47bba97924ebe86a62ef83dc75b7c8a881d53c535f83e2c54c4bd701e05c/bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938", size = 280110, upload-time = "2025-02-28T01:24:05.896Z" }, +] + [[package]] name = "black" version = "24.10.0" @@ -665,10 +709,13 @@ dependencies = [ { name = "litellm" }, { name = "lock" }, { name = "mcp" }, + { name = "passlib", extra = ["bcrypt"] }, { name = "pgvector" }, { name = "prompt-toolkit" }, { name = "psycopg2-binary" }, { name = "pygments" }, + { name = "pyjwt" }, + { name = "python-multipart" }, { name = "pytz" }, { name = "pyyaml" }, { name = "requests" }, @@ -749,17 +796,20 @@ requires-dist = [ { name = "openinference-instrumentation", marker = "extra == 'tracing'" }, { name = "openinference-instrumentation-litellm", marker = "extra == 'tracing'" }, { name = "openinference-semantic-conventions", marker = "extra == 'tracing'" }, + { name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" }, { name = "pgvector", specifier = ">=0.3.6" }, { name = "prompt-toolkit", specifier = ">=3.0.47" }, { name = "psycopg2-binary", specifier = ">=2.9.9" }, { name = "pydantic", marker = "extra == 'dev'", specifier = ">=2.10.3" }, { name = "pygments", specifier = ">=2.18.0" }, + { name = "pyjwt", specifier = ">=2.8.0" }, { name = "pylint", marker = "extra == 'dev'", specifier = ">=3.3.1" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.350" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.1.1" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.14.0" }, { name = "pytest-rerunfailures", marker = "extra == 'dev'" }, + { name = "python-multipart", specifier = ">=0.0.6" }, { name = "pytz", specifier = ">=2024.1" }, { name = "pyyaml", specifier = ">=6.0.1" }, { name = "requests", specifier = ">=2.32.2" }, @@ -1948,6 +1998,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/d4/1244ab8edf173a10fd601f7e13b9566c1b525c4f365d6bee918e68381889/pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13", size = 11504248, upload-time = "2024-09-20T13:09:23.137Z" }, ] +[[package]] +name = "passlib" +version = "1.7.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -2296,6 +2360,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + [[package]] name = "pylint" version = "3.3.3"