From 4cdf7222d2573cda008e24a6c8110b392e3bb56b Mon Sep 17 00:00:00 2001 From: Vineeth Voruganti <13438633+VVoruganti@users.noreply.github.com> Date: Wed, 2 Apr 2025 12:02:33 -0400 Subject: [PATCH] chore: linter fixes --- pyproject.toml | 3 +- src/crud.py | 275 ++++++++++++++++------------ src/deriver/consumer.py | 29 ++- src/deriver/queue.py | 17 +- src/deriver/tom/__init__.py | 57 ++++-- src/exceptions.py | 11 +- src/main.py | 2 +- src/models.py | 14 +- src/routers/documents.py | 33 ++-- src/routers/metamessages.py | 14 +- src/schemas.py | 12 +- tests/conftest.py | 2 +- tests/routes/test_apps.py | 14 +- tests/routes/test_documents.py | 6 +- tests/routes/test_messages.py | 19 +- tests/routes/test_metamessages.py | 8 +- tests/routes/test_validation_api.py | 121 ++++++------ tests/test_schema_validations.py | 13 +- uv.lock | 27 +++ 19 files changed, 396 insertions(+), 281 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4814ca19..a0c38012 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dev-dependencies = [ "coverage>=7.6.0", "interrogate>=1.7.0", "py-spy>=0.3.14", + "ruff>=0.11.2", ] [tool.ruff.lint] @@ -52,7 +53,7 @@ select = [ ] ignore = ["E501"] -[tool.ruff.flake8-bugbear] +[tool.ruff.lint.flake8-bugbear] extend-immutable-calls = ["fastapi.Depends"] [tool.pytest.ini_options] diff --git a/src/crud.py b/src/crud.py index 5f81662f..492dbf00 100644 --- a/src/crud.py +++ b/src/crud.py @@ -1,5 +1,5 @@ -from collections.abc import Sequence import logging +from collections.abc import Sequence from typing import Optional from dotenv import load_dotenv @@ -12,9 +12,9 @@ from sqlalchemy.types import BigInteger from . import models, schemas from .exceptions import ( + ConflictException, ResourceNotFoundException, ValidationException, - ConflictException, ) logger = logging.getLogger(__name__) @@ -31,14 +31,14 @@ openai_client = OpenAI() async def get_app(db: AsyncSession, app_id: str) -> models.App: """ Get an app by its ID. - + Args: db: Database session app_id: Public ID of the app - + Returns: The app if found - + Raises: ResourceNotFoundException: If the app does not exist """ @@ -54,14 +54,14 @@ async def get_app(db: AsyncSession, app_id: str) -> models.App: async def get_app_by_name(db: AsyncSession, name: str) -> models.App: """ Get an app by its name. - + Args: db: Database session name: Name of the app - + Returns: The app if found - + Raises: ResourceNotFoundException: If the app does not exist """ @@ -81,14 +81,14 @@ async def get_app_by_name(db: AsyncSession, name: str) -> models.App: async def create_app(db: AsyncSession, app: schemas.AppCreate) -> models.App: """ Create a new app. - + Args: db: Database session app: App creation schema - + Returns: The created app - + Raises: ConflictException: If an app with the same name already exists """ @@ -109,21 +109,21 @@ async def update_app( ) -> models.App: """ Update an app. - + Args: db: Database session app_id: Public ID of the app app: App update schema - + Returns: The updated app - + Raises: ResourceNotFoundException: If the app does not exist """ try: honcho_app = await get_app(db, app_id) - + if app.name is not None: honcho_app.name = app.name if app.metadata is not None: @@ -135,7 +135,9 @@ async def update_app( except IntegrityError as e: await db.rollback() logger.error(f"IntegrityError updating app {app_id}: {str(e)}") - raise ConflictException("App update failed - unique constraint violation") from e + raise ConflictException( + "App update failed - unique constraint violation" + ) from e # def delete_app(db: AsyncSession, app_id: str) -> bool: @@ -157,15 +159,15 @@ async def create_user( ) -> models.User: """ Create a new user. - + Args: db: Database session app_id: Public ID of the app user: User creation schema - + Returns: The created user - + Raises: ConflictException: If a user with the same name already exists in this app """ @@ -185,20 +187,18 @@ async def create_user( raise ConflictException("User with this name already exists") from e -async def get_user( - db: AsyncSession, app_id: str, user_id: str -) -> models.User: +async def get_user(db: AsyncSession, app_id: str, user_id: str) -> models.User: """ Get a user by app ID and user ID. - + Args: db: Database session app_id: Public ID of the app user_id: Public ID of the user - + Returns: The user if found - + Raises: ResourceNotFoundException: If the user does not exist """ @@ -215,20 +215,18 @@ async def get_user( return user -async def get_user_by_name( - db: AsyncSession, app_id: str, name: str -) -> models.User: +async def get_user_by_name(db: AsyncSession, app_id: str, name: str) -> models.User: """ Get a user by app ID and name. - + Args: db: Database session app_id: Public ID of the app name: Name of the user - + Returns: The user if found - + Raises: ResourceNotFoundException: If the user does not exist """ @@ -269,16 +267,16 @@ async def update_user( ) -> models.User: """ Update a user. - + Args: db: Database session app_id: Public ID of the app user_id: Public ID of the user user: User update schema - + Returns: The updated user - + Raises: ResourceNotFoundException: If the user does not exist ValidationException: If the update data is invalid @@ -287,7 +285,7 @@ async def update_user( try: # get_user will raise ResourceNotFoundException if not found honcho_user = await get_user(db, app_id, user_id) - + if user.name is not None: honcho_user.name = user.name if user.metadata is not None: @@ -299,7 +297,9 @@ async def update_user( except IntegrityError as e: await db.rollback() logger.warning(f"User update failed due to integrity error: {str(e)}") - raise ConflictException("User update failed - unique constraint violation") from e + raise ConflictException( + "User update failed - unique constraint violation" + ) from e # def delete_user(db: AsyncSession, app_id: str, user_id: str) -> bool: @@ -371,23 +371,23 @@ async def create_session( ) -> models.Session: """ Create a new session for a user. - + Args: db: Database session session: Session creation schema app_id: ID of the app user_id: ID of the user - + Returns: The created session - + Raises: ResourceNotFoundException: If the user does not exist """ try: # This will raise ResourceNotFoundException if user not found honcho_user = await get_user(db, app_id=app_id, user_id=user_id) - + honcho_session = models.Session( user_id=user_id, h_metadata=session.metadata, @@ -411,17 +411,17 @@ async def update_session( ) -> models.Session: """ Update a session. - + Args: db: Database session session: Session update schema app_id: ID of the app user_id: ID of the user session_id: ID of the session - + Returns: The updated session - + Raises: ResourceNotFoundException: If the session does not exist or doesn't belong to the user """ @@ -431,10 +431,12 @@ async def update_session( if honcho_session is None: logger.warning(f"Session {session_id} not found for user {user_id}") raise ResourceNotFoundException("Session not found or does not belong to user") - - if session.metadata is not None: # Need to explicitly be there won't make it empty by default + + if ( + session.metadata is not None + ): # Need to explicitly be there won't make it empty by default honcho_session.h_metadata = session.metadata - + await db.commit() logger.info(f"Session {session_id} updated successfully") return honcho_session @@ -445,16 +447,16 @@ async def delete_session( ) -> bool: """ Mark a session as inactive (soft delete). - + Args: db: Database session app_id: ID of the app user_id: ID of the user session_id: ID of the session - + Returns: True if the session was deleted successfully - + Raises: ResourceNotFoundException: If the session does not exist or doesn't belong to the user """ @@ -467,11 +469,11 @@ async def delete_session( ) result = await db.execute(stmt) honcho_session = result.scalar_one_or_none() - + if honcho_session is None: logger.warning(f"Session {session_id} not found for user {user_id}") raise ResourceNotFoundException("Session not found or does not belong to user") - + honcho_session.is_active = False await db.commit() logger.info(f"Session {session_id} marked as inactive") @@ -750,8 +752,10 @@ async def create_metamessage( # Validate user exists user = await get_user(db, app_id=app_id, user_id=metamessage.user_id) if user is None: - raise ResourceNotFoundException(f"User with ID '{metamessage.user_id}' not found") - + raise ResourceNotFoundException( + f"User with ID '{metamessage.user_id}' not found" + ) + # Initialize metamessage data metamessage_data = { "user_id": metamessage.user_id, @@ -759,16 +763,21 @@ async def create_metamessage( "content": metamessage.content, "h_metadata": metamessage.metadata, } - + # Validate session_id if provided if metamessage.session_id is not None: session = await get_session( - db, app_id=app_id, user_id=metamessage.user_id, session_id=metamessage.session_id + db, + app_id=app_id, + user_id=metamessage.user_id, + session_id=metamessage.session_id, ) if session is None: - raise ResourceNotFoundException(f"Session not found or does not belong to user") + raise ResourceNotFoundException( + "Session not found or does not belong to user" + ) metamessage_data["session_id"] = metamessage.session_id - + # Validate message_id if provided if metamessage.message_id is not None: message = await get_message( @@ -779,12 +788,14 @@ async def create_metamessage( message_id=metamessage.message_id, ) if message is None: - raise ResourceNotFoundException(f"Message not found or does not belong to session") + raise ResourceNotFoundException( + "Message not found or does not belong to session" + ) metamessage_data["message_id"] = metamessage.message_id elif metamessage.message_id is not None: # If message_id provided but no session_id, that's an error raise ValidationException("Cannot specify message_id without session_id") - + # Create metamessage honcho_metamessage = models.Metamessage(**metamessage_data) db.add(honcho_metamessage) @@ -853,15 +864,15 @@ async def get_metamessage( .where(models.User.public_id == user_id) .where(models.Metamessage.public_id == metamessage_id) ) - + # Add session filter if provided if session_id is not None: stmt = stmt.where(models.Metamessage.session_id == session_id) - + # Add message filter if provided if message_id is not None: stmt = stmt.where(models.Metamessage.message_id == message_id) - + result = await db.execute(stmt) return result.scalar_one_or_none() @@ -881,10 +892,12 @@ async def update_metamessage( session_id=metamessage.session_id, message_id=metamessage.message_id, ) - + if honcho_metamessage is None: - raise ResourceNotFoundException("Metamessage not found or does not belong to user") - + raise ResourceNotFoundException( + "Metamessage not found or does not belong to user" + ) + # Validate the consistency of relationships if they're being changed # If we're setting message_id, we must have a session_id if metamessage.message_id is not None and metamessage.session_id is None: @@ -892,7 +905,7 @@ async def update_metamessage( metamessage.session_id = honcho_metamessage.session_id if metamessage.session_id is None: raise ValidationException("Cannot specify message_id without session_id") - + # If we're updating session_id and message_id, validate they belong together if metamessage.session_id is not None and metamessage.message_id is not None: message = await get_message( @@ -903,21 +916,23 @@ async def update_metamessage( message_id=metamessage.message_id, ) if message is None: - raise ResourceNotFoundException("Message not found or doesn't belong to session") - + raise ResourceNotFoundException( + "Message not found or doesn't belong to session" + ) + # Update fields if metamessage.session_id is not None: honcho_metamessage.session_id = metamessage.session_id - + if metamessage.message_id is not None: honcho_metamessage.message_id = metamessage.message_id - + if metamessage.metadata is not None: honcho_metamessage.h_metadata = metamessage.metadata - + if metamessage.metamessage_type is not None: honcho_metamessage.metamessage_type = metamessage.metamessage_type - + await db.commit() return honcho_metamessage @@ -960,16 +975,16 @@ async def get_collection_by_id( ) -> models.Collection: """ Get a collection by ID for a specific user and app. - + Args: db: Database session app_id: Public ID of the app user_id: Public ID of the user collection_id: Public ID of the collection - + Returns: The collection if found - + Raises: ResourceNotFoundException: If the collection does not exist """ @@ -983,8 +998,12 @@ async def get_collection_by_id( result = await db.execute(stmt) collection = result.scalar_one_or_none() if collection is None: - logger.warning(f"Collection with ID '{collection_id}' not found for user {user_id}") - raise ResourceNotFoundException(f"Collection not found or does not belong to user") + logger.warning( + f"Collection with ID '{collection_id}' not found for user {user_id}" + ) + raise ResourceNotFoundException( + "Collection not found or does not belong to user" + ) return collection @@ -993,16 +1012,16 @@ async def get_collection_by_name( ) -> models.Collection: """ Get a collection by name for a specific user and app. - + Args: db: Database session app_id: Public ID of the app user_id: Public ID of the user name: Name of the collection - + Returns: The collection if found - + Raises: ResourceNotFoundException: If the collection does not exist """ @@ -1029,16 +1048,16 @@ async def create_collection( ) -> models.Collection: """ Create a new collection for a user. - + Args: db: Database session collection: Collection creation schema app_id: ID of the app user_id: ID of the user - + Returns: The created collection - + Raises: ConflictException: If a collection with the same name already exists for this user ValidationException: If the collection configuration is invalid @@ -1047,12 +1066,16 @@ async def create_collection( try: # This will raise ResourceNotFoundException if user not found await get_user(db, app_id=app_id, user_id=user_id) - + # Check for reserved names if collection.name == "honcho": - logger.warning(f"Attempted to create collection with reserved name 'honcho' for user {user_id}") - raise ValidationException("Invalid collection configuration - 'honcho' is a reserved name") - + logger.warning( + f"Attempted to create collection with reserved name 'honcho' for user {user_id}" + ) + raise ValidationException( + "Invalid collection configuration - 'honcho' is a reserved name" + ) + honcho_collection = models.Collection( user_id=user_id, name=collection.name, @@ -1060,12 +1083,16 @@ async def create_collection( ) db.add(honcho_collection) await db.commit() - logger.info(f"Collection '{collection.name}' created successfully for user {user_id}") + logger.info( + f"Collection '{collection.name}' created successfully for user {user_id}" + ) return honcho_collection except IntegrityError as e: await db.rollback() logger.warning(f"Failed to create collection - integrity error: {str(e)}") - raise ConflictException(f"Collection with name '{collection.name}' already exists") from e + raise ConflictException( + f"Collection with name '{collection.name}' already exists" + ) from e async def update_collection( @@ -1077,17 +1104,17 @@ async def update_collection( ) -> models.Collection: """ Update a collection. - + Args: db: Database session collection: Collection update schema app_id: ID of the app user_id: ID of the user collection_id: ID of the collection - + Returns: The updated collection - + Raises: ResourceNotFoundException: If the collection does not exist ValidationException: If the update data is invalid @@ -1096,25 +1123,33 @@ async def update_collection( try: # Validate input if collection.name is None and collection.metadata is None: - logger.warning(f"Collection update attempted with no fields provided for collection {collection_id}") - raise ValidationException("Invalid collection configuration - at least one field must be provided") - + logger.warning( + f"Collection update attempted with no fields provided for collection {collection_id}" + ) + raise ValidationException( + "Invalid collection configuration - at least one field must be provided" + ) + # This will raise ResourceNotFoundException if not found honcho_collection = await get_collection_by_id( db, app_id=app_id, user_id=user_id, collection_id=collection_id ) - + # Check for reserved names if name is being updated if collection.name == "honcho": - logger.warning(f"Attempted to rename collection to reserved name 'honcho' for user {user_id}") - raise ValidationException("Invalid collection configuration - 'honcho' is a reserved name") - + logger.warning( + f"Attempted to rename collection to reserved name 'honcho' for user {user_id}" + ) + raise ValidationException( + "Invalid collection configuration - 'honcho' is a reserved name" + ) + if collection.metadata is not None: honcho_collection.h_metadata = collection.metadata - + if collection.name is not None: honcho_collection.name = collection.name - + await db.commit() logger.info(f"Collection {collection_id} updated successfully") return honcho_collection @@ -1130,16 +1165,16 @@ async def delete_collection( """ Delete a Collection and all documents associated with it. Takes advantage of the orm cascade feature. - + Args: db: Database session app_id: ID of the app user_id: ID of the user collection_id: ID of the collection - + Returns: True if the collection was deleted successfully - + Raises: ResourceNotFoundException: If the collection does not exist """ @@ -1148,7 +1183,7 @@ async def delete_collection( honcho_collection = await get_collection_by_id( db, app_id=app_id, user_id=user_id, collection_id=collection_id ) - + await db.delete(honcho_collection) await db.commit() logger.info(f"Collection {collection_id} deleted successfully") @@ -1206,17 +1241,17 @@ async def get_document( ) -> models.Document: """ Get a document by ID. - + Args: db: Database session app_id: Public ID of the app user_id: Public ID of the user collection_id: Public ID of the collection document_id: Public ID of the document - + Returns: The document if found - + Raises: ResourceNotFoundException: If the document does not exist """ @@ -1236,7 +1271,9 @@ async def get_document( result = await db.execute(stmt) document = result.scalar_one_or_none() if document is None: - logger.warning(f"Document with ID '{document_id}' not found in collection {collection_id}") + logger.warning( + f"Document with ID '{document_id}' not found in collection {collection_id}" + ) raise ResourceNotFoundException(f"Document with ID '{document_id}' not found") return document @@ -1284,17 +1321,17 @@ async def create_document( ) -> models.Document: """ Embed text as a vector and create a document. - + Args: db: Database session document: Document creation schema app_id: ID of the app user_id: ID of the user collection_id: ID of the collection - + Returns: The created document - + Raises: ResourceNotFoundException: If the collection does not exist ValidationException: If the document data is invalid @@ -1304,9 +1341,11 @@ async def create_document( await get_collection_by_id( db, app_id=app_id, collection_id=collection_id, user_id=user_id ) - + if not document.content: - logger.warning(f"Attempted to create document with empty content in collection {collection_id}") + logger.warning( + f"Attempted to create document with empty content in collection {collection_id}" + ) raise ValidationException("Document content cannot be empty") response = openai_client.embeddings.create( @@ -1326,9 +1365,13 @@ async def create_document( logger.info(f"Document created successfully in collection {collection_id}") return honcho_document except Exception as e: - if not isinstance(e, ResourceNotFoundException) and not isinstance(e, ValidationException): + if not isinstance(e, ResourceNotFoundException) and not isinstance( + e, ValidationException + ): await db.rollback() - logger.error(f"Error creating document in collection {collection_id}: {str(e)}") + logger.error( + f"Error creating document in collection {collection_id}: {str(e)}" + ) raise diff --git a/src/deriver/consumer.py b/src/deriver/consumer.py index 04910cb5..73cfb13a 100644 --- a/src/deriver/consumer.py +++ b/src/deriver/consumer.py @@ -9,7 +9,7 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from .. import models -from ..exceptions import ResourceNotFoundException, ValidationException +from ..exceptions import ValidationException from .tom import get_tom_inference, get_user_representation # Configure logging @@ -67,22 +67,29 @@ async def get_chat_history(db, session_id, message_id) -> str: async def process_item(db: AsyncSession, payload: dict): """ Process a queue item based on whether it's a user or AI message. - + Args: db: Database session payload: Message payload from the queue - + Raises: ValidationException: If the payload is missing required fields """ try: # Validate required fields - required_fields = ["content", "app_id", "user_id", "session_id", "message_id", "is_user"] + required_fields = [ + "content", + "app_id", + "user_id", + "session_id", + "message_id", + "is_user", + ] for field in required_fields: if field not in payload: logger.error(f"Missing required field in payload: {field}") raise ValidationException(f"Missing required field in payload: {field}") - + processing_args = [ payload["content"], payload["app_id"], @@ -91,16 +98,18 @@ async def process_item(db: AsyncSession, payload: dict): payload["message_id"], db, ] - + if payload["is_user"]: logger.info(f"Processing user message: {payload['message_id']}") await process_user_message(*processing_args) else: logger.info(f"Processing AI message: {payload['message_id']}") await process_ai_message(*processing_args) - + except Exception as e: - logger.error(f"Error processing message {payload.get('message_id', 'unknown')}: {str(e)}") + logger.error( + f"Error processing message {payload.get('message_id', 'unknown')}: {str(e)}" + ) if os.getenv("SENTRY_ENABLED", "False").lower() == "true": sentry_sdk.capture_exception(e) raise @@ -183,7 +192,9 @@ async def process_user_message( existing_representation.content if existing_representation else "None" ) logger.info(f"User {user_id}: Existing Representation retrieved") - logger.debug(f"User {user_id}: Existing Representation: {existing_representation_content}") + logger.debug( + f"User {user_id}: Existing Representation: {existing_representation_content}" + ) langfuse_context.update_current_trace( session_id=session_id, diff --git a/src/deriver/queue.py b/src/deriver/queue.py index ef0ee2fd..7b224f14 100644 --- a/src/deriver/queue.py +++ b/src/deriver/queue.py @@ -75,7 +75,9 @@ class QueueManager: self.shutdown_event.set() if self.active_tasks: - logger.info(f"Waiting for {len(self.active_tasks)} active tasks to complete...") + logger.info( + f"Waiting for {len(self.active_tasks)} active tasks to complete..." + ) await asyncio.gather(*self.active_tasks, return_exceptions=True) async def cleanup(self): @@ -86,7 +88,9 @@ class QueueManager: async with SessionLocal() as db: await db.execute( delete(models.ActiveQueueSession).where( - models.ActiveQueueSession.session_id.in_(self.owned_sessions) + models.ActiveQueueSession.session_id.in_( + self.owned_sessions + ) ) ) await db.commit() @@ -193,11 +197,16 @@ class QueueManager: if not message: break try: - logger.info(f"Processing message {message.id} from session {session_id}") + logger.info( + f"Processing message {message.id} from session {session_id}" + ) await process_item(db, payload=message.payload) logger.info(f"Successfully processed message {message.id}") except Exception as e: - logger.error(f"Error processing message {message.id}: {str(e)}", exc_info=True) + logger.error( + f"Error processing message {message.id}: {str(e)}", + exc_info=True, + ) if os.getenv("SENTRY_ENABLED", "False").lower() == "true": sentry_sdk.capture_exception(e) finally: diff --git a/src/deriver/tom/__init__.py b/src/deriver/tom/__init__.py index 477ab531..e2d6d8cb 100644 --- a/src/deriver/tom/__init__.py +++ b/src/deriver/tom/__init__.py @@ -1,30 +1,47 @@ -from .conversational import get_tom_inference_conversational, get_user_representation_conversational -from .single_prompt import get_tom_inference_single_prompt, get_user_representation_single_prompt +from .conversational import ( + get_tom_inference_conversational, + get_user_representation_conversational, +) +from .single_prompt import ( + get_tom_inference_single_prompt, + get_user_representation_single_prompt, +) -async def get_tom_inference(chat_history: str, - session_id: str, - user_representation: str = "None", - method: str = "conversational", - **kwargs - ) -> str: + +async def get_tom_inference( + chat_history: str, + session_id: str, + user_representation: str = "None", + method: str = "conversational", + **kwargs, +) -> str: if method == "conversational": - return await get_tom_inference_conversational(chat_history, session_id, user_representation, **kwargs) + return await get_tom_inference_conversational( + chat_history, session_id, user_representation, **kwargs + ) elif method == "single_prompt": - return await get_tom_inference_single_prompt(chat_history, session_id, user_representation, **kwargs) + return await get_tom_inference_single_prompt( + chat_history, session_id, user_representation, **kwargs + ) else: raise ValueError(f"Invalid method: {method}") -async def get_user_representation(chat_history: str, - session_id: str, - user_representation: str = "None", - tom_inference: str = "None", - method: str = "conversational", - **kwargs - ) -> str: +async def get_user_representation( + chat_history: str, + session_id: str, + user_representation: str = "None", + tom_inference: str = "None", + method: str = "conversational", + **kwargs, +) -> str: if method == "conversational": - return await get_user_representation_conversational(chat_history, session_id, user_representation, tom_inference, **kwargs) + return await get_user_representation_conversational( + chat_history, session_id, user_representation, tom_inference, **kwargs + ) elif method == "single_prompt": - return await get_user_representation_single_prompt(chat_history, session_id, user_representation, tom_inference, **kwargs) + return await get_user_representation_single_prompt( + chat_history, session_id, user_representation, tom_inference, **kwargs + ) else: - raise ValueError(f"Invalid method: {method}") \ No newline at end of file + raise ValueError(f"Invalid method: {method}") diff --git a/src/exceptions.py b/src/exceptions.py index 1b034a33..c62bb9e9 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -1,14 +1,16 @@ """ Custom exceptions for the Honcho application. """ + from typing import Optional class HonchoException(Exception): """Base exception for all Honcho-specific errors.""" + status_code = 500 detail = "An unexpected error occurred" - + def __init__(self, detail: Optional[str] = None, status_code: Optional[int] = None): self.detail = detail or self.detail self.status_code = status_code or self.status_code @@ -17,29 +19,34 @@ class HonchoException(Exception): class ResourceNotFoundException(HonchoException): """Exception raised when a requested resource is not found.""" + status_code = 404 detail = "Resource not found" class ValidationException(HonchoException): """Exception raised when validation fails.""" + status_code = 422 detail = "Validation error" class ConflictException(HonchoException): """Exception raised when there's a resource conflict.""" + status_code = 409 detail = "Resource conflict" class AuthenticationException(HonchoException): """Exception raised when authentication fails.""" + status_code = 401 detail = "Authentication failed" class AuthorizationException(HonchoException): """Exception raised when authorization fails.""" + status_code = 403 - detail = "Not authorized to access this resource" \ No newline at end of file + detail = "Not authorized to access this resource" diff --git a/src/main.py b/src/main.py index f4dbc036..e7f75271 100644 --- a/src/main.py +++ b/src/main.py @@ -21,7 +21,7 @@ from src.routers import ( users, ) -from .db import engine, scaffold_db +from .db import engine def get_log_level(env_var="LOG_LEVEL", default="INFO"): diff --git a/src/models.py b/src/models.py index 9c81abff..2f62b37f 100644 --- a/src/models.py +++ b/src/models.py @@ -151,11 +151,9 @@ class Metamessage(Base): ) metamessage_type: Mapped[str] = mapped_column(TEXT, index=True) content: Mapped[str] = mapped_column(TEXT) - + # Foreign keys - message_id is now optional - user_id: Mapped[str] = mapped_column( - ForeignKey("users.public_id"), index=True - ) + user_id: Mapped[str] = mapped_column(ForeignKey("users.public_id"), index=True) session_id: Mapped[str | None] = mapped_column( ForeignKey("sessions.public_id"), index=True, nullable=True ) @@ -167,7 +165,7 @@ class Metamessage(Base): user = relationship("User", back_populates="metamessages") session = relationship("Session", back_populates="metamessages") message = relationship("Message", back_populates="metamessages") - + created_at: Mapped[datetime.datetime] = mapped_column( DateTime(timezone=True), index=True, default=func.now() ) @@ -182,8 +180,8 @@ class Metamessage(Base): ), # Added constraints to ensure consistency CheckConstraint( - "(message_id IS NULL) OR (session_id IS NOT NULL)", - name="message_requires_session" + "(message_id IS NULL) OR (session_id IS NOT NULL)", + name="message_requires_session", ), # Keep existing index Index( @@ -192,7 +190,7 @@ class Metamessage(Base): text("id DESC"), postgresql_include=["public_id", "message_id", "created_at"], ), - # Add new indices for user, session, and message lookups + # Indices for user, session, and message lookups Index( "idx_metamessages_user_lookup", "user_id", diff --git a/src/routers/documents.py b/src/routers/documents.py index a3f7e242..fa11fcba 100644 --- a/src/routers/documents.py +++ b/src/routers/documents.py @@ -1,4 +1,3 @@ -import json import logging from collections.abc import Sequence from typing import Optional @@ -40,11 +39,15 @@ async def get_documents( filter=options.filter, reverse=reverse, ) - + return await paginate(db, documents_query) except ValueError as e: - logger.warning(f"Failed to get documents for collection {collection_id}: {str(e)}") - raise ResourceNotFoundException("Collection not found or does not belong to user") from e + logger.warning( + f"Failed to get documents for collection {collection_id}: {str(e)}" + ) + raise ResourceNotFoundException( + "Collection not found or does not belong to user" + ) from e @router.get( @@ -84,7 +87,7 @@ async def query_documents( filter = options.filter if options.filter == {}: filter = None - + documents = await crud.query_documents( db=db, app_id=app_id, @@ -94,11 +97,13 @@ async def query_documents( filter=filter, top_k=top_k, ) - + logger.info(f"Query documents successful for collection {collection_id}") return documents except ValueError as e: - logger.error(f"Error querying documents in collection {collection_id}: {str(e)}") + logger.error( + f"Error querying documents in collection {collection_id}: {str(e)}" + ) raise ValidationException("Error querying documents") from e @@ -122,8 +127,12 @@ async def create_document( logger.info(f"Document created successfully in collection {collection_id}") return document_obj except ValueError as e: - logger.warning(f"Failed to create document in collection {collection_id}: {str(e)}") - raise ResourceNotFoundException("Collection not found or does not belong to user") from e + logger.warning( + f"Failed to create document in collection {collection_id}: {str(e)}" + ) + raise ResourceNotFoundException( + "Collection not found or does not belong to user" + ) from e @router.put( @@ -140,9 +149,11 @@ async def update_document( ): """Update the content and/or the metadata of a Document""" if document.content is None and document.metadata is None: - logger.warning(f"Document update attempted with empty content and metadata for document {document_id}") + logger.warning( + f"Document update attempted with empty content and metadata for document {document_id}" + ) raise ValidationException("Content and metadata cannot both be None") - + try: updated_document = await crud.update_document( db, diff --git a/src/routers/metamessages.py b/src/routers/metamessages.py index 7007e5e0..d541ae01 100644 --- a/src/routers/metamessages.py +++ b/src/routers/metamessages.py @@ -33,7 +33,7 @@ async def create_metamessage( try: # Set the user_id from the URL parameters metamessage.user_id = user_id - + metamessage_obj = await crud.create_metamessage( db, metamessage=metamessage, @@ -56,9 +56,9 @@ async def get_metamessages( ): """ Get metamessages with flexible filtering. - + - Filter by user only: No additional parameters needed - - Filter by session: Provide session_id + - Filter by session: Provide session_id - Filter by message: Provide message_id (and session_id) - Filter by type: Provide metamessage_type - Filter by metadata: Provide filter object @@ -75,7 +75,7 @@ async def get_metamessages( filter=options.filter, reverse=reverse, ) - + return await paginate(db, metamessages_query) except (ResourceNotFoundException, ValidationException) as e: logger.warning(f"Failed to get metamessages: {str(e)}") @@ -101,7 +101,9 @@ async def get_metamessage( ) if honcho_metamessage is None: logger.warning(f"Metamessage {metamessage_id} not found") - raise ResourceNotFoundException(f"Metamessage with ID {metamessage_id} not found") + raise ResourceNotFoundException( + f"Metamessage with ID {metamessage_id} not found" + ) return honcho_metamessage @@ -119,7 +121,7 @@ async def update_metamessage( """Update a metamessage's metadata, type, or relationships""" # Ensure user_id from URL path is used metamessage.user_id = user_id - + try: updated_metamessage = await crud.update_metamessage( db, diff --git a/src/schemas.py b/src/schemas.py index 63b23904..c4786431 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -188,7 +188,7 @@ class MetamessageGet(MetamessageBase): class MetamessageUpdate(MetamessageBase): user_id: str | None = None # Will be set from URL parameter in endpoint - session_id: str | None = None + session_id: str | None = None message_id: str | None = None metamessage_type: str | None = None metadata: dict | None = None @@ -323,24 +323,26 @@ class Document(DocumentBase): class AgentQuery(BaseModel): queries: str | list[str] - @field_validator('queries') + @field_validator("queries") def validate_queries(cls, v): MAX_STRING_LENGTH = 10000 MAX_LIST_LENGTH = 25 if isinstance(v, str): if len(v) > MAX_STRING_LENGTH: - raise ValueError('Query too long') + raise ValueError("Query too long") elif isinstance(v, list): if len(v) > MAX_LIST_LENGTH: - raise ValueError('Too many queries') + raise ValueError("Too many queries") if any(len(q) > MAX_STRING_LENGTH for q in v): - raise ValueError('One or more queries too long') + raise ValueError("One or more queries too long") return v + class AgentChat(BaseModel): content: str class MessageBatchCreate(BaseModel): """Schema for batch message creation with a max of 100 messages""" + messages: list[MessageCreate] = Field(..., max_length=100) diff --git a/tests/conftest.py b/tests/conftest.py index b9f5dc1d..e0eea897 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ from fastapi import Request from fastapi.responses import JSONResponse from fastapi.testclient import TestClient from sqlalchemy import text -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine, AsyncSession +from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError, ProgrammingError from sqlalchemy_utils import create_database, database_exists, drop_database diff --git a/tests/routes/test_apps.py b/tests/routes/test_apps.py index 5365a1e0..1f5463fa 100644 --- a/tests/routes/test_apps.py +++ b/tests/routes/test_apps.py @@ -37,7 +37,7 @@ def test_get_or_create_app(client): response = client.get(f"/v1/apps/name/{name}") assert response.status_code == 404 assert "detail" in response.json() - + # This should create the app response = client.get(f"/v1/apps/get_or_create/{name}") assert response.status_code == 200 @@ -48,23 +48,23 @@ def test_get_or_create_app(client): def test_get_or_create_existing_app(client): name = str(generate_nanoid()) - + # App doesn't exist yet response = client.get(f"/v1/apps/name/{name}") assert response.status_code == 404 - + # Create the app response = client.post( "/v1/apps", json={"name": name, "metadata": {"key": "value"}} ) assert response.status_code == 200 app1 = response.json() - + # Now get_or_create should find the existing app response = client.get(f"/v1/apps/get_or_create/{name}") assert response.status_code == 200 app2 = response.json() - + # Both should be the same app assert app1["name"] == app2["name"] assert app1["id"] == app2["id"] @@ -107,10 +107,10 @@ def test_create_duplicate_app_name(client): name = str(generate_nanoid()) response = client.post("/v1/apps", json={"name": name}) assert response.status_code == 200 - + # Try to create another app with the same name response = client.post("/v1/apps", json={"name": name}) - + # Should get a ConflictException with 409 status assert response.status_code == 409 data = response.json() diff --git a/tests/routes/test_documents.py b/tests/routes/test_documents.py index 9c90d23b..fc45ffc4 100644 --- a/tests/routes/test_documents.py +++ b/tests/routes/test_documents.py @@ -10,7 +10,7 @@ def test_update_document_validation_error(client, sample_data): ) assert response.status_code == 200 collection = response.json() - + # Create a document response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection['id']}/documents", @@ -18,13 +18,13 @@ def test_update_document_validation_error(client, sample_data): ) assert response.status_code == 200 document = response.json() - + # Try to update the document with empty content and metadata response = client.put( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection['id']}/documents/{document['id']}", json={"content": None, "metadata": None}, ) - + # Should get a ValidationException with 422 status assert response.status_code == 422 data = response.json() diff --git a/tests/routes/test_messages.py b/tests/routes/test_messages.py index d4bff1c8..0c7d99df 100644 --- a/tests/routes/test_messages.py +++ b/tests/routes/test_messages.py @@ -111,6 +111,7 @@ async def test_update_message(client, db_session, sample_data): data = response.json() assert data["metadata"] == {"new_key": "new_value"} + @pytest.mark.asyncio async def test_update_message_empty_metadata(client, db_session, sample_data): test_app, test_user = sample_data @@ -148,8 +149,9 @@ async def test_create_batch_messages(client, db_session, sample_data): { "content": f"Test message {i}", "is_user": i % 2 == 0, # Alternating user/non-user messages - "metadata": {"batch_index": i} - } for i in range(3) + "metadata": {"batch_index": i}, + } + for i in range(3) ] } @@ -157,13 +159,13 @@ async def test_create_batch_messages(client, db_session, sample_data): f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/batch", json=test_messages, ) - + assert response.status_code == 200 data = response.json() - + # Verify the response contains all messages assert len(data) == 3 - + # Verify messages are in the correct order and have correct content for i, message in enumerate(data): assert message["content"] == f"Test message {i}" @@ -195,8 +197,9 @@ async def test_create_batch_messages_limit(client, db_session, sample_data): { "content": f"Test message {i}", "is_user": i % 2 == 0, - "metadata": {"batch_index": i} - } for i in range(101) # 101 messages + "metadata": {"batch_index": i}, + } + for i in range(101) # 101 messages ] } @@ -204,7 +207,7 @@ async def test_create_batch_messages_limit(client, db_session, sample_data): f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{test_session.public_id}/messages/batch", json=test_messages, ) - + assert response.status_code == 422 # Validation error data = response.json() assert "messages" in data["detail"][0]["loc"] # Error should mention messages field diff --git a/tests/routes/test_metamessages.py b/tests/routes/test_metamessages.py index e2d5381f..d0f563a5 100644 --- a/tests/routes/test_metamessages.py +++ b/tests/routes/test_metamessages.py @@ -84,7 +84,7 @@ async def test_get_metamessages_by_session(client, db_session, sample_data): ) db_session.add(test_message) await db_session.commit() - + # Create metamessages for the same session test_metamessage_1 = models.Metamessage( user_id=test_user.public_id, @@ -129,7 +129,7 @@ async def test_get_metamessages_by_session(client, db_session, sample_data): f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/metamessages/list", json={ "session_id": str(test_session.public_id), - "metamessage_type": "test_type" + "metamessage_type": "test_type", }, ) @@ -233,7 +233,7 @@ async def test_get_metamessage_by_user(client, db_session, sample_data): @pytest.mark.asyncio async def test_create_user_level_metamessage(client, db_session, sample_data): test_app, test_user = sample_data - + # Create a user-level metamessage (no session or message) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/metamessages", @@ -281,7 +281,7 @@ async def test_update_metamessage(client, db_session, sample_data): json={ "user_id": str(test_user.public_id), "metadata": {"new_key": "new_value"}, - "metamessage_type": "updated_type" + "metamessage_type": "updated_type", }, ) assert response.status_code == 200 diff --git a/tests/routes/test_validation_api.py b/tests/routes/test_validation_api.py index e5aeccb3..8b0b515e 100644 --- a/tests/routes/test_validation_api.py +++ b/tests/routes/test_validation_api.py @@ -1,4 +1,3 @@ -import pytest from nanoid import generate as generate_nanoid @@ -32,8 +31,7 @@ def test_user_validations_api(client, sample_data): # Test name too short response = client.post( - f"/v1/apps/{test_app.public_id}/users", - json={"name": "", "metadata": {}} + f"/v1/apps/{test_app.public_id}/users", json={"name": "", "metadata": {}} ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -43,8 +41,7 @@ def test_user_validations_api(client, sample_data): # Test name too long response = client.post( - f"/v1/apps/{test_app.public_id}/users", - json={"name": "a" * 101, "metadata": {}} + f"/v1/apps/{test_app.public_id}/users", json={"name": "a" * 101, "metadata": {}} ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -58,18 +55,14 @@ def test_message_validations_api(client, sample_data): # Create a test session first session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test content too long response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages", - json={ - "content": "a" * 50001, - "is_user": True, - "metadata": {} - } + json={"content": "a" * 50001, "is_user": True, "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -80,11 +73,7 @@ def test_message_validations_api(client, sample_data): # Test invalid is_user type response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages", - json={ - "content": "test", - "is_user": "not a bool", - "metadata": {} - } + json={"content": "test", "is_user": "not a bool", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -98,7 +87,7 @@ def test_collection_validations_api(client, sample_data): # Test name too short response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": "", "metadata": {}} + json={"name": "", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -109,7 +98,7 @@ def test_collection_validations_api(client, sample_data): # Test name too long response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": "a" * 101, "metadata": {}} + json={"name": "a" * 101, "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -120,7 +109,7 @@ def test_collection_validations_api(client, sample_data): # Test 'honcho' name restriction response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": "honcho", "metadata": {}} + json={"name": "honcho", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -134,14 +123,14 @@ def test_document_validations_api(client, sample_data): # Create a collection first collection_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": str(generate_nanoid()), "metadata": {}} + json={"name": str(generate_nanoid()), "metadata": {}}, ) collection_id = collection_response.json()["id"] # Test content too short response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents", - json={"content": "", "metadata": {}} + json={"content": "", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -152,7 +141,7 @@ def test_document_validations_api(client, sample_data): # Test content too long response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents", - json={"content": "a" * 100001, "metadata": {}} + json={"content": "a" * 100001, "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -166,14 +155,14 @@ def test_document_query_validations_api(client, sample_data): # Create a collection first collection_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": str(generate_nanoid()), "metadata": {}} + json={"name": str(generate_nanoid()), "metadata": {}}, ) collection_id = collection_response.json()["id"] # Test query too short response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query", - json={"query": "", "top_k": 5} + json={"query": "", "top_k": 5}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -184,7 +173,7 @@ def test_document_query_validations_api(client, sample_data): # Test query too long response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query", - json={"query": "a" * 1001, "top_k": 5} + json={"query": "a" * 1001, "top_k": 5}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -195,7 +184,7 @@ def test_document_query_validations_api(client, sample_data): # Test top_k too small response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query", - json={"query": "test", "top_k": 0} + json={"query": "test", "top_k": 0}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -206,7 +195,7 @@ def test_document_query_validations_api(client, sample_data): # Test top_k too large response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/query", - json={"query": "test", "top_k": 51} + json={"query": "test", "top_k": 51}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -220,23 +209,19 @@ def test_message_batch_validations_api(client, sample_data): # Create a test session first session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test batch too large messages = [ - { - "content": f"test message {i}", - "is_user": True, - "metadata": {} - } + {"content": f"test message {i}", "is_user": True, "metadata": {}} for i in range(101) # Create 101 messages ] - + response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages/batch", - json={"messages": messages} + json={"messages": messages}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -250,13 +235,13 @@ def test_metamessage_validations_api(client, sample_data): # Create session and message first session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] - + message_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages", - json={"content": "test message", "is_user": True, "metadata": {}} + json={"content": "test message", "is_user": True, "metadata": {}}, ) message_id = message_response.json()["id"] @@ -268,8 +253,8 @@ def test_metamessage_validations_api(client, sample_data): "content": "test content", "session_id": session_id, "message_id": message_id, - "metadata": {} - } + "metadata": {}, + }, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -285,8 +270,8 @@ def test_metamessage_validations_api(client, sample_data): "content": "test content", "session_id": session_id, "message_id": message_id, - "metadata": {} - } + "metadata": {}, + }, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -301,8 +286,8 @@ def test_metamessage_validations_api(client, sample_data): "metamessage_type": "test_type", "content": "a" * 50001, "message_id": message_id, - "metadata": {} - } + "metadata": {}, + }, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -316,14 +301,14 @@ def test_collection_update_validations_api(client, sample_data): # Create a collection first collection_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": str(generate_nanoid()), "metadata": {}} + json={"name": str(generate_nanoid()), "metadata": {}}, ) collection_id = collection_response.json()["id"] # Test honcho name in update response = client.put( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}", - json={"name": "honcho", "metadata": {}} + json={"name": "honcho", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -337,26 +322,26 @@ def test_document_update_validations_api(client, sample_data): # Create collection and document first collection_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"name": str(generate_nanoid()), "metadata": {}} + json={"name": str(generate_nanoid()), "metadata": {}}, ) collection_id = collection_response.json()["id"] - + document_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents", - json={"content": "test content", "metadata": {}} + json={"content": "test content", "metadata": {}}, ) document_id = document_response.json()["id"] # Test content too long in update response = client.put( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/{collection_id}/documents/{document_id}", - json={"content": "a" * 100001, "metadata": {}} + json={"content": "a" * 100001, "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] assert error["loc"] == ["body", "content"] assert error["msg"] == "String should have at most 100000 characters" - assert error["type"] == "string_too_long" + assert error["type"] == "string_too_long" def test_session_validations_api(client, sample_data): @@ -364,14 +349,14 @@ def test_session_validations_api(client, sample_data): # Create a test session first session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test invalid metadata type response = client.put( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}", - json={"metadata": "not a dict"} + json={"metadata": "not a dict"}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -381,7 +366,7 @@ def test_session_validations_api(client, sample_data): # Test empty update response = client.put( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}", - json={} + json={}, ) assert response.status_code == 422 @@ -391,21 +376,21 @@ def test_agent_query_validations_api(client, sample_data): # Create a session first since agent queries are likely session-based session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test valid string query (under 10000 chars) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": "a" * 9999} + json={"queries": "a" * 9999}, ) assert response.status_code == 200 # Test string query too long (over 10000 chars) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": "a" * 10001} + json={"queries": "a" * 10001}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -416,14 +401,14 @@ def test_agent_query_validations_api(client, sample_data): # Test valid list query (under 25 items, each under 10000 chars) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": ["a" * 9999 for _ in range(25)]} + json={"queries": ["a" * 9999 for _ in range(25)]}, ) assert response.status_code == 200 # Test list too long (over 25 items) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": ["test" for _ in range(26)]} + json={"queries": ["test" for _ in range(26)]}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -433,7 +418,7 @@ def test_agent_query_validations_api(client, sample_data): # Test list item too long (item over 10000 chars) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": ["a" * 10001]} + json={"queries": ["a" * 10001]}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -444,7 +429,7 @@ def test_agent_query_validations_api(client, sample_data): # Test that strings over 20 chars are allowed response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/chat", - json={"queries": "a" * 100} # 100 chars should be fine + json={"queries": "a" * 100}, # 100 chars should be fine ) assert response.status_code == 200 @@ -453,14 +438,14 @@ def test_required_field_validations_api(client, sample_data): test_app, test_user = sample_data session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test missing required content in message response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages", - json={"is_user": True, "metadata": {}} + json={"is_user": True, "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -470,7 +455,7 @@ def test_required_field_validations_api(client, sample_data): # Test missing required is_user in message response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages", - json={"content": "test", "metadata": {}} + json={"content": "test", "metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -480,7 +465,7 @@ def test_required_field_validations_api(client, sample_data): # Test missing required name in collection response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections", - json={"metadata": {}} + json={"metadata": {}}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -493,14 +478,14 @@ def test_filter_validations_api(client, sample_data): # Create a session first session_response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions", - json={"metadata": {}} + json={"metadata": {}}, ) session_id = session_response.json()["id"] # Test invalid filter type in message list (at session level) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/sessions/{session_id}/messages/list", - json={"filter": "not a dict"} + json={"filter": "not a dict"}, ) assert response.status_code == 422 error = response.json()["detail"][0] @@ -510,7 +495,7 @@ def test_filter_validations_api(client, sample_data): # Test invalid filter type in collection list (at user level) response = client.post( f"/v1/apps/{test_app.public_id}/users/{test_user.public_id}/collections/list", - json={"filter": "not a dict"} + json={"filter": "not a dict"}, ) assert response.status_code == 422 error = response.json()["detail"][0] diff --git a/tests/test_schema_validations.py b/tests/test_schema_validations.py index 39ab3586..77d6f28b 100644 --- a/tests/test_schema_validations.py +++ b/tests/test_schema_validations.py @@ -1,14 +1,15 @@ import pytest from pydantic import ValidationError + from src.schemas import ( AppCreate, - UserCreate, - MessageCreate, - MetamessageCreate, CollectionCreate, DocumentCreate, DocumentQuery, MessageBatchCreate, + MessageCreate, + MetamessageCreate, + UserCreate, ) @@ -200,9 +201,7 @@ class TestDocumentQueryValidations: class TestMessageBatchValidations: def test_valid_message_batch(self): batch = MessageBatchCreate( - messages=[ - MessageCreate(content="test", is_user=True, metadata={}) - ] + messages=[MessageCreate(content="test", is_user=True, metadata={})] ) assert len(batch.messages) == 1 @@ -215,4 +214,4 @@ class TestMessageBatchValidations: ] ) error_dict = exc_info.value.errors()[0] - assert error_dict["type"] == "too_long" \ No newline at end of file + assert error_dict["type"] == "too_long" diff --git a/uv.lock b/uv.lock index 3f2d47c7..c0001b04 100644 --- a/uv.lock +++ b/uv.lock @@ -443,6 +443,7 @@ dev = [ { name = "py-spy" }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "ruff" }, { name = "sqlalchemy-utils" }, ] @@ -472,6 +473,7 @@ dev = [ { name = "py-spy", specifier = ">=0.3.14" }, { name = "pytest", specifier = ">=8.2.2" }, { name = "pytest-asyncio", specifier = ">=0.23.7" }, + { name = "ruff", specifier = ">=0.11.2" }, { name = "sqlalchemy-utils", specifier = ">=0.41.2" }, ] @@ -1260,6 +1262,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ac/3c/3b66696fc8a6c980674851108d7d57fbcbfedbefb3d8b61a64166dc9b18e/rich_toolkit-0.12.0-py3-none-any.whl", hash = "sha256:a2da4416384410ae871e890db7edf8623e1f5e983341dbbc8cc03603ce24f0ab", size = 13012 }, ] +[[package]] +name = "ruff" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/61/fb87430f040e4e577e784e325351186976516faef17d6fcd921fe28edfd7/ruff-0.11.2.tar.gz", hash = "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", size = 3857511 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/99/102578506f0f5fa29fd7e0df0a273864f79af044757aef73d1cae0afe6ad/ruff-0.11.2-py3-none-linux_armv6l.whl", hash = "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", size = 10113146 }, + { url = "https://files.pythonhosted.org/packages/74/ad/5cd4ba58ab602a579997a8494b96f10f316e874d7c435bcc1a92e6da1b12/ruff-0.11.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", size = 10867092 }, + { url = "https://files.pythonhosted.org/packages/fc/3e/d3f13619e1d152c7b600a38c1a035e833e794c6625c9a6cea6f63dbf3af4/ruff-0.11.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9", size = 10224082 }, + { url = "https://files.pythonhosted.org/packages/90/06/f77b3d790d24a93f38e3806216f263974909888fd1e826717c3ec956bbcd/ruff-0.11.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", size = 10394818 }, + { url = "https://files.pythonhosted.org/packages/99/7f/78aa431d3ddebfc2418cd95b786642557ba8b3cb578c075239da9ce97ff9/ruff-0.11.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", size = 9952251 }, + { url = "https://files.pythonhosted.org/packages/30/3e/f11186d1ddfaca438c3bbff73c6a2fdb5b60e6450cc466129c694b0ab7a2/ruff-0.11.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", size = 11563566 }, + { url = "https://files.pythonhosted.org/packages/22/6c/6ca91befbc0a6539ee133d9a9ce60b1a354db12c3c5d11cfdbf77140f851/ruff-0.11.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", size = 12208721 }, + { url = "https://files.pythonhosted.org/packages/19/b0/24516a3b850d55b17c03fc399b681c6a549d06ce665915721dc5d6458a5c/ruff-0.11.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", size = 11662274 }, + { url = "https://files.pythonhosted.org/packages/d7/65/76be06d28ecb7c6070280cef2bcb20c98fbf99ff60b1c57d2fb9b8771348/ruff-0.11.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", size = 13792284 }, + { url = "https://files.pythonhosted.org/packages/ce/d2/4ceed7147e05852876f3b5f3fdc23f878ce2b7e0b90dd6e698bda3d20787/ruff-0.11.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", size = 11327861 }, + { url = "https://files.pythonhosted.org/packages/c4/78/4935ecba13706fd60ebe0e3dc50371f2bdc3d9bc80e68adc32ff93914534/ruff-0.11.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", size = 10276560 }, + { url = "https://files.pythonhosted.org/packages/81/7f/1b2435c3f5245d410bb5dc80f13ec796454c21fbda12b77d7588d5cf4e29/ruff-0.11.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", size = 9945091 }, + { url = "https://files.pythonhosted.org/packages/39/c4/692284c07e6bf2b31d82bb8c32f8840f9d0627d92983edaac991a2b66c0a/ruff-0.11.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", size = 10977133 }, + { url = "https://files.pythonhosted.org/packages/94/cf/8ab81cb7dd7a3b0a3960c2769825038f3adcd75faf46dd6376086df8b128/ruff-0.11.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", size = 11378514 }, + { url = "https://files.pythonhosted.org/packages/d9/3a/a647fa4f316482dacf2fd68e8a386327a33d6eabd8eb2f9a0c3d291ec549/ruff-0.11.2-py3-none-win32.whl", hash = "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", size = 10319835 }, + { url = "https://files.pythonhosted.org/packages/86/54/3c12d3af58012a5e2cd7ebdbe9983f4834af3f8cbea0e8a8c74fa1e23b2b/ruff-0.11.2-py3-none-win_amd64.whl", hash = "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", size = 11373713 }, + { url = "https://files.pythonhosted.org/packages/d6/d4/dd813703af8a1e2ac33bf3feb27e8a5ad514c9f219df80c64d69807e7f71/ruff-0.11.2-py3-none-win_arm64.whl", hash = "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", size = 10441990 }, +] + [[package]] name = "sentry-sdk" version = "2.22.0"