From cdcd6c55c2282b417ef738acc4c9450c6c4e434b Mon Sep 17 00:00:00 2001 From: Arkadiusz Kwasigroch Date: Tue, 25 Aug 2026 16:55:26 +0200 Subject: [PATCH] refactor(backend): extract tool and type lookup crud Continues the incremental split of the crud monolith. Tool and TypeLookup move to their own modules; callers switch to direct submodule imports. Pure move -- no logic changes. --- .../src/rhesis/backend/app/crud/__init__.py | 141 ------------------ .../src/rhesis/backend/app/crud/tool.py | 84 +++++++++++ .../rhesis/backend/app/crud/type_lookup.py | 95 ++++++++++++ .../src/rhesis/backend/app/routers/tools.py | 28 ++-- .../rhesis/backend/app/routers/type_lookup.py | 13 +- .../src/rhesis/backend/app/services/source.py | 11 +- .../backend/app/services/task_notification.py | 6 +- .../backend/app/services/tool/actions.py | 7 +- .../backend/app/services/tool/mcp/config.py | 7 +- .../backend/app/services/tool/rest/config.py | 4 +- .../backend/app/services/tool/rest/health.py | 9 +- .../backend/app/services/tool/rest/jira.py | 5 +- tests/backend/services/test_jira_rest.py | 12 +- tests/backend/services/test_mcp_service.py | 77 +++++----- tests/backend/services/test_rest_config.py | 36 ++--- tests/backend/services/test_rest_health.py | 28 ++-- 16 files changed, 312 insertions(+), 251 deletions(-) create mode 100644 apps/backend/src/rhesis/backend/app/crud/tool.py create mode 100644 apps/backend/src/rhesis/backend/app/crud/type_lookup.py diff --git a/apps/backend/src/rhesis/backend/app/crud/__init__.py b/apps/backend/src/rhesis/backend/app/crud/__init__.py index 1f928b2363..ddee4a03f7 100644 --- a/apps/backend/src/rhesis/backend/app/crud/__init__.py +++ b/apps/backend/src/rhesis/backend/app/crud/__init__.py @@ -619,144 +619,3 @@ def _recompute_affected_test_sets(deleted_ids: List[uuid.UUID]) -> None: user_id=user_id, on_deleted=_recompute_affected_test_sets, ) - - -# TypeLookup CRUD -def get_type_lookup( - db: Session, type_lookup_id: uuid.UUID, organization_id: str = None, user_id: str = None -) -> Optional[models.TypeLookup]: - """Get type_lookup.""" - return get_item(db, models.TypeLookup, type_lookup_id, organization_id, user_id) - - -def get_type_lookups( - db: Session, - skip: int = 0, - limit: int = 10, - sort_by: str = "created_at", - sort_order: str = "desc", - filter: str | None = None, - organization_id: str = None, - user_id: str = None, -) -> List[models.TypeLookup]: - return get_items( - db, - models.TypeLookup, - skip, - limit, - sort_by, - sort_order, - filter, - organization_id=organization_id, - user_id=user_id, - ) - - -def create_type_lookup( - db: Session, - type_lookup: schemas.TypeLookupCreate, - organization_id: str = None, - user_id: str = None, -) -> models.TypeLookup: - """Create type_lookup.""" - return create_item(db, models.TypeLookup, type_lookup, organization_id, user_id) - - -def update_type_lookup( - db: Session, - type_lookup_id: uuid.UUID, - type_lookup: schemas.TypeLookupUpdate, - organization_id: str = None, - user_id: str = None, -) -> Optional[models.TypeLookup]: - """Update type_lookup.""" - return update_item(db, models.TypeLookup, type_lookup_id, type_lookup, organization_id, user_id) - - -def delete_type_lookup( - db: Session, type_lookup_id: uuid.UUID, organization_id: str, user_id: str -) -> Optional[models.TypeLookup]: - """Delete type lookup.""" - return delete_item(db, models.TypeLookup, type_lookup_id, organization_id, user_id) - - -def get_type_lookup_by_name_and_value( - db: Session, type_name: str, type_value: str, organization_id: str, user_id: str = None -) -> Optional[models.TypeLookup]: - """Get a type lookup by its type_name and type_value""" - return ( - QueryBuilder(db, models.TypeLookup) - .with_organization_filter(organization_id) - .with_custom_filter( - lambda q: q.filter( - models.TypeLookup.type_name == type_name, models.TypeLookup.type_value == type_value - ) - ) - .first() - ) - - -# Tool CRUD -_TOOL_RELATED_FIELDS = (include(models.Tool.tool_provider_type),) - - -def get_tool( - db: Session, tool_id: uuid.UUID, organization_id: str, user_id: str = None -) -> Optional[models.Tool]: - """Get a specific tool by ID with relationships loaded""" - return get_item_detail( - db, - models.Tool, - tool_id, - organization_id=organization_id, - user_id=user_id, - related_fields=_TOOL_RELATED_FIELDS, - ) - - -def get_tools( - db: Session, - skip: int = 0, - limit: int = 10, - sort_by: str = "created_at", - sort_order: str = "desc", - filter: str | None = None, - organization_id: str = None, - user_id: str = None, -) -> List[models.Tool]: - """Get all tools for an organization with filtering and pagination""" - return ( - QueryBuilder(db, models.Tool) - .with_related(*_TOOL_RELATED_FIELDS) - .with_organization_filter(organization_id) - .with_visibility_filter(user_id) - .with_odata_filter(filter) - .with_sorting(sort_by, sort_order) - .with_pagination(skip, limit) - .all() - ) - - -def create_tool( - db: Session, tool: schemas.ToolCreate, organization_id: str, user_id: str = None -) -> models.Tool: - """Create a new tool""" - return create_item(db, models.Tool, tool, organization_id, user_id) - - -def update_tool( - db: Session, - tool_id: uuid.UUID, - tool: schemas.ToolUpdate, - organization_id: str, - user_id: str = None, -) -> Optional[models.Tool]: - """Update a tool""" - return update_item(db, models.Tool, tool_id, tool, organization_id, user_id) - - -def delete_tool( - db: Session, tool_id: uuid.UUID, organization_id: str, user_id: str = None -) -> Optional[models.Tool]: - """Delete a tool (soft delete)""" - return delete_item(db, models.Tool, tool_id, organization_id=organization_id, user_id=user_id) diff --git a/apps/backend/src/rhesis/backend/app/crud/tool.py b/apps/backend/src/rhesis/backend/app/crud/tool.py new file mode 100644 index 0000000000..6a2a00525d --- /dev/null +++ b/apps/backend/src/rhesis/backend/app/crud/tool.py @@ -0,0 +1,84 @@ +"""CRUD operations for tools. + +Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds +the bulk of the functions, and per-entity modules like this one take over as the code +around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. +""" + +import uuid +from typing import List, Optional + +from sqlalchemy.orm import Session + +from rhesis.backend.app import models, schemas +from rhesis.backend.app.utils.crud_utils import ( + create_item, + delete_item, + get_item_detail, + update_item, +) +from rhesis.backend.app.utils.query_utils import QueryBuilder, include + +_TOOL_RELATED_FIELDS = (include(models.Tool.tool_provider_type),) + + +def get_tool( + db: Session, tool_id: uuid.UUID, organization_id: str, user_id: str = None +) -> Optional[models.Tool]: + """Get a specific tool by ID with relationships loaded""" + return get_item_detail( + db, + models.Tool, + tool_id, + organization_id=organization_id, + user_id=user_id, + related_fields=_TOOL_RELATED_FIELDS, + ) + + +def get_tools( + db: Session, + skip: int = 0, + limit: int = 10, + sort_by: str = "created_at", + sort_order: str = "desc", + filter: str | None = None, + organization_id: str = None, + user_id: str = None, +) -> List[models.Tool]: + """Get all tools for an organization with filtering and pagination""" + return ( + QueryBuilder(db, models.Tool) + .with_related(*_TOOL_RELATED_FIELDS) + .with_organization_filter(organization_id) + .with_visibility_filter(user_id) + .with_odata_filter(filter) + .with_sorting(sort_by, sort_order) + .with_pagination(skip, limit) + .all() + ) + + +def create_tool( + db: Session, tool: schemas.ToolCreate, organization_id: str, user_id: str = None +) -> models.Tool: + """Create a new tool""" + return create_item(db, models.Tool, tool, organization_id, user_id) + + +def update_tool( + db: Session, + tool_id: uuid.UUID, + tool: schemas.ToolUpdate, + organization_id: str, + user_id: str = None, +) -> Optional[models.Tool]: + """Update a tool""" + return update_item(db, models.Tool, tool_id, tool, organization_id, user_id) + + +def delete_tool( + db: Session, tool_id: uuid.UUID, organization_id: str, user_id: str = None +) -> Optional[models.Tool]: + """Delete a tool (soft delete)""" + return delete_item(db, models.Tool, tool_id, organization_id=organization_id, user_id=user_id) diff --git a/apps/backend/src/rhesis/backend/app/crud/type_lookup.py b/apps/backend/src/rhesis/backend/app/crud/type_lookup.py new file mode 100644 index 0000000000..d39a16fe2f --- /dev/null +++ b/apps/backend/src/rhesis/backend/app/crud/type_lookup.py @@ -0,0 +1,95 @@ +"""CRUD operations for type lookups. + +Part of the incremental split of the ``crud`` monolith: ``crud/__init__.py`` still holds +the bulk of the functions, and per-entity modules like this one take over as the code +around them is touched -- see ``apps/backend/AGENTS.md``'s crud-layout rule. +""" + +import uuid +from typing import List, Optional + +from sqlalchemy.orm import Session + +from rhesis.backend.app import models, schemas +from rhesis.backend.app.utils.crud_utils import ( + create_item, + delete_item, + get_item, + get_items, + update_item, +) +from rhesis.backend.app.utils.query_utils import QueryBuilder + + +def get_type_lookup( + db: Session, type_lookup_id: uuid.UUID, organization_id: str = None, user_id: str = None +) -> Optional[models.TypeLookup]: + """Get type_lookup.""" + return get_item(db, models.TypeLookup, type_lookup_id, organization_id, user_id) + + +def get_type_lookups( + db: Session, + skip: int = 0, + limit: int = 10, + sort_by: str = "created_at", + sort_order: str = "desc", + filter: str | None = None, + organization_id: str = None, + user_id: str = None, +) -> List[models.TypeLookup]: + return get_items( + db, + models.TypeLookup, + skip, + limit, + sort_by, + sort_order, + filter, + organization_id=organization_id, + user_id=user_id, + ) + + +def create_type_lookup( + db: Session, + type_lookup: schemas.TypeLookupCreate, + organization_id: str = None, + user_id: str = None, +) -> models.TypeLookup: + """Create type_lookup.""" + return create_item(db, models.TypeLookup, type_lookup, organization_id, user_id) + + +def update_type_lookup( + db: Session, + type_lookup_id: uuid.UUID, + type_lookup: schemas.TypeLookupUpdate, + organization_id: str = None, + user_id: str = None, +) -> Optional[models.TypeLookup]: + """Update type_lookup.""" + return update_item(db, models.TypeLookup, type_lookup_id, type_lookup, organization_id, user_id) + + +def delete_type_lookup( + db: Session, type_lookup_id: uuid.UUID, organization_id: str, user_id: str +) -> Optional[models.TypeLookup]: + """Delete type lookup.""" + return delete_item(db, models.TypeLookup, type_lookup_id, organization_id, user_id) + + +def get_type_lookup_by_name_and_value( + db: Session, type_name: str, type_value: str, organization_id: str, user_id: str = None +) -> Optional[models.TypeLookup]: + """Get a type lookup by its type_name and type_value""" + return ( + QueryBuilder(db, models.TypeLookup) + .with_organization_filter(organization_id) + .with_custom_filter( + lambda q: q.filter( + models.TypeLookup.type_name == type_name, models.TypeLookup.type_value == type_value + ) + ) + .first() + ) diff --git a/apps/backend/src/rhesis/backend/app/routers/tools.py b/apps/backend/src/rhesis/backend/app/routers/tools.py index af01aa6186..71ec962bd4 100644 --- a/apps/backend/src/rhesis/backend/app/routers/tools.py +++ b/apps/backend/src/rhesis/backend/app/routers/tools.py @@ -7,8 +7,10 @@ from rhesis.backend.app.routers.base import RhesisRouter from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.user_utils import require_current_user_or_token +from rhesis.backend.app.crud import tool as tool_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.dependencies import ( get_project_context, get_tenant_context, @@ -262,7 +264,9 @@ def create_tool( """ organization_id, user_id = tenant_context - provider_type = crud.get_type_lookup(db, tool.tool_provider_type_id, organization_id, user_id) + provider_type = type_lookup_crud.get_type_lookup( + db, tool.tool_provider_type_id, organization_id, user_id + ) if provider_type: if provider_type.type_value == "jira": if not tool.tool_metadata or "space_key" not in tool.tool_metadata: @@ -288,7 +292,7 @@ def create_tool( _validate_azure_devops_project(tool.tool_metadata) tool = tool.model_copy(update={"credentials": prepared_credentials}) - return crud.create_tool(db=db, tool=tool, organization_id=organization_id, user_id=user_id) + return tool_crud.create_tool(db=db, tool=tool, organization_id=organization_id, user_id=user_id) @router.get("/", response_model=List[schemas.ToolDetail]) @@ -310,7 +314,7 @@ def read_tools( Note: credentials is excluded from the response for security. """ organization_id, user_id = tenant_context - tools = crud.get_tools( + tools = tool_crud.get_tools( db=db, skip=skip, limit=limit, @@ -336,7 +340,9 @@ def read_tool( Note: credentials is excluded from the response for security. """ organization_id, user_id = tenant_context - tool = crud.get_tool(db=db, tool_id=tool_id, organization_id=organization_id, user_id=user_id) + tool = tool_crud.get_tool( + db=db, tool_id=tool_id, organization_id=organization_id, user_id=user_id + ) if tool is None: raise HTTPException(status_code=404, detail="Tool not found") return tool @@ -358,7 +364,7 @@ def update_tool( """ organization_id, user_id = tenant_context - existing_tool = crud.get_tool( + existing_tool = tool_crud.get_tool( db=db, tool_id=tool_id, organization_id=organization_id, user_id=user_id ) if not existing_tool: @@ -369,7 +375,9 @@ def update_tool( if tool.tool_provider_type_id is not None else existing_tool.tool_provider_type_id ) - provider_type = crud.get_type_lookup(db, effective_provider_type_id, organization_id, user_id) + provider_type = type_lookup_crud.get_type_lookup( + db, effective_provider_type_id, organization_id, user_id + ) _validate_provider_type_switch(existing_tool, tool, provider_type) @@ -423,7 +431,7 @@ def update_tool( _validate_azure_devops_credentials(merged_credentials) tool = tool.model_copy(update={"credentials": merged_credentials}) - db_tool = crud.update_tool( + db_tool = tool_crud.update_tool( db=db, tool_id=tool_id, tool=tool, organization_id=organization_id, user_id=user_id ) if db_tool is None: @@ -512,7 +520,7 @@ async def test_tool_connection( effective_metadata = request.tool_metadata if request.tool_id and request.credentials is not None: - existing_tool = crud.get_tool( + existing_tool = tool_crud.get_tool( db=db, tool_id=uuid.UUID(request.tool_id), organization_id=organization_id, @@ -631,7 +639,7 @@ def delete_tool( Delete a tool (soft delete). """ organization_id, user_id = tenant_context - db_tool = crud.delete_tool( + db_tool = tool_crud.delete_tool( db=db, tool_id=tool_id, organization_id=organization_id, user_id=user_id ) if db_tool is None: diff --git a/apps/backend/src/rhesis/backend/app/routers/type_lookup.py b/apps/backend/src/rhesis/backend/app/routers/type_lookup.py index e90d208fe4..1f91609940 100644 --- a/apps/backend/src/rhesis/backend/app/routers/type_lookup.py +++ b/apps/backend/src/rhesis/backend/app/routers/type_lookup.py @@ -4,8 +4,9 @@ from rhesis.backend.app.routers.base import RhesisRouter from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.auth.user_utils import require_current_user_or_token +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.dependencies import ( get_tenant_context, get_tenant_db_session, @@ -35,7 +36,7 @@ def create_type_lookup( ): """Create a new type lookup.""" organization_id, user_id = tenant_context - return crud.create_type_lookup( + return type_lookup_crud.create_type_lookup( db=db, type_lookup=type_lookup, organization_id=organization_id, user_id=user_id ) @@ -55,7 +56,7 @@ def read_type_lookups( ): """Get all type lookups with their related objects""" organization_id, user_id = tenant_context - return crud.get_type_lookups( + return type_lookup_crud.get_type_lookups( db=db, skip=skip, limit=limit, @@ -76,7 +77,7 @@ def read_type_lookup( ): """Get a type lookup by ID.""" organization_id, user_id = tenant_context - db_type_lookup = crud.get_type_lookup( + db_type_lookup = type_lookup_crud.get_type_lookup( db, type_lookup_id=type_lookup_id, organization_id=organization_id, user_id=user_id ) if db_type_lookup is None: @@ -93,7 +94,7 @@ def delete_type_lookup( ): """Delete a type lookup by ID.""" organization_id, user_id = tenant_context - db_type_lookup = crud.delete_type_lookup( + db_type_lookup = type_lookup_crud.delete_type_lookup( db, type_lookup_id=type_lookup_id, organization_id=organization_id, user_id=user_id ) if db_type_lookup is None: @@ -111,7 +112,7 @@ def update_type_lookup( ): """Update a type lookup by ID.""" organization_id, user_id = tenant_context - db_type_lookup = crud.update_type_lookup( + db_type_lookup = type_lookup_crud.update_type_lookup( db, type_lookup_id=type_lookup_id, type_lookup=type_lookup, diff --git a/apps/backend/src/rhesis/backend/app/services/source.py b/apps/backend/src/rhesis/backend/app/services/source.py index a98c6ccc68..c4bd12e33f 100644 --- a/apps/backend/src/rhesis/backend/app/services/source.py +++ b/apps/backend/src/rhesis/backend/app/services/source.py @@ -6,8 +6,9 @@ from fastapi import UploadFile from sqlalchemy.orm import Session -from rhesis.backend.app import crud, models, schemas +from rhesis.backend.app import models, schemas from rhesis.backend.app.crud import source as source_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.services.chunking import auto_chunk_source from rhesis.backend.app.services.handlers import get_source_handler @@ -30,7 +31,7 @@ def get_source_type_by_value( Returns: TypeLookup: The source type, or None if not found """ - return crud.get_type_lookup_by_name_and_value( + return type_lookup_crud.get_type_lookup_by_name_and_value( db, type_name="SourceType", type_value=source_type_value, organization_id=organization_id ) @@ -178,7 +179,7 @@ def validate_source_for_extraction( raise ValueError("Source has no type specified") # Get source type details - source_type = crud.get_type_lookup( + source_type = type_lookup_crud.get_type_lookup( db, db_source.source_type_id, organization_id=organization_id, user_id=user_id ) if not source_type: @@ -214,7 +215,7 @@ async def extract_source_content( db_source, file_path = validate_source_for_extraction(db, source_id, organization_id, user_id) # Get source type to determine handler - source_type = crud.get_type_lookup( + source_type = type_lookup_crud.get_type_lookup( db, db_source.source_type_id, organization_id=organization_id, user_id=user_id ) if not source_type: @@ -268,7 +269,7 @@ async def get_source_file_content( db_source, file_path = validate_source_for_extraction(db, source_id, organization_id, user_id) # Get source type to determine handler - source_type = crud.get_type_lookup( + source_type = type_lookup_crud.get_type_lookup( db, db_source.source_type_id, organization_id=organization_id, user_id=user_id ) if not source_type: diff --git a/apps/backend/src/rhesis/backend/app/services/task_notification.py b/apps/backend/src/rhesis/backend/app/services/task_notification.py index 124c33134a..36700e8cfd 100644 --- a/apps/backend/src/rhesis/backend/app/services/task_notification.py +++ b/apps/backend/src/rhesis/backend/app/services/task_notification.py @@ -8,8 +8,8 @@ from sqlalchemy.orm import Session from rhesis.backend.app import models -from rhesis.backend.app.crud import get_type_lookup from rhesis.backend.app.crud import status as status_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.crud import user as user_crud from rhesis.backend.app.models.enums import NotificationEventType from rhesis.backend.app.services.notification import RenderedNotification, notify @@ -49,7 +49,9 @@ def send_task_assignment_notification( status = status_crud.get_status(db, task.status_id) if task.status_id else None # Get priority details - priority = get_type_lookup(db, task.priority_id) if task.priority_id else None + priority = ( + type_lookup_crud.get_type_lookup(db, task.priority_id) if task.priority_id else None + ) # Get entity name if entity_type and entity_id are provided entity_name = None diff --git a/apps/backend/src/rhesis/backend/app/services/tool/actions.py b/apps/backend/src/rhesis/backend/app/services/tool/actions.py index ac05f14f0c..607758119d 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/actions.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/actions.py @@ -18,7 +18,8 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import tool as tool_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.services.tool.exceptions import ToolConfigurationError from rhesis.backend.app.utils.database_exceptions import ItemDeletedException @@ -91,7 +92,7 @@ def resolve_provider( """ if tool_id is not None: try: - tool = crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) + tool = tool_crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) except ItemDeletedException: raise ToolConfigurationError( f"Tool '{tool_id}' has been deleted. Please re-import the source." @@ -100,7 +101,7 @@ def resolve_provider( raise ToolConfigurationError(f"Tool '{tool_id}' not found.") return tool.tool_provider_type.type_value - provider_type = crud.get_type_lookup(db, provider_type_id, organization_id, user_id) + provider_type = type_lookup_crud.get_type_lookup(db, provider_type_id, organization_id, user_id) if not provider_type: raise ToolConfigurationError(f"Provider type '{provider_type_id}' not found.") return provider_type.type_value diff --git a/apps/backend/src/rhesis/backend/app/services/tool/mcp/config.py b/apps/backend/src/rhesis/backend/app/services/tool/mcp/config.py index b5347e093b..a926f3b346 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/mcp/config.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/mcp/config.py @@ -6,7 +6,8 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import tool as tool_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.services.tool.exceptions import ToolConfigurationError from rhesis.backend.app.utils.database_exceptions import ItemDeletedException from rhesis.sdk.agents.mcp import MCPClientFactory @@ -61,7 +62,7 @@ def _get_mcp_tool_config( ) -> Tuple[Any, str, Optional[Dict[str, str]]]: """Return MCP client, provider name, and optional provider scope context.""" try: - tool = crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) + tool = tool_crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) except ItemDeletedException: raise ToolConfigurationError( f"Tool '{tool_id}' has been deleted. Please re-import the source." @@ -99,7 +100,7 @@ def _get_mcp_client_from_params( tool_metadata: Optional[Dict[str, Any]] = None, ) -> Tuple[Any, str, Optional[Dict[str, str]]]: """Build an MCP client from unsaved credentials (connection test before save).""" - provider_type = crud.get_type_lookup(db, provider_type_id, organization_id, user_id) + provider_type = type_lookup_crud.get_type_lookup(db, provider_type_id, organization_id, user_id) if not provider_type: raise ValueError( diff --git a/apps/backend/src/rhesis/backend/app/services/tool/rest/config.py b/apps/backend/src/rhesis/backend/app/services/tool/rest/config.py index 07d0190f4e..8eb8860413 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/rest/config.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/rest/config.py @@ -9,7 +9,7 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import tool as tool_crud from rhesis.backend.app.services.tool.exceptions import ToolConfigurationError from rhesis.backend.app.utils.database_exceptions import ItemDeletedException @@ -114,7 +114,7 @@ def get_rest_client( ToolConfigurationError: If tool not found, deleted, or provider unsupported. """ try: - tool = crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) + tool = tool_crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) except ItemDeletedException: raise ToolConfigurationError( f"Tool '{tool_id}' has been deleted. Please re-import the source." diff --git a/apps/backend/src/rhesis/backend/app/services/tool/rest/health.py b/apps/backend/src/rhesis/backend/app/services/tool/rest/health.py index bfc5ef98eb..ec144f6aaf 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/rest/health.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/rest/health.py @@ -6,7 +6,8 @@ from sqlalchemy.orm import Session -from rhesis.backend.app import crud +from rhesis.backend.app.crud import tool as tool_crud +from rhesis.backend.app.crud import type_lookup as type_lookup_crud from rhesis.backend.app.services.tool.exceptions import ToolConfigurationError from rhesis.backend.app.utils.database_exceptions import ItemDeletedException @@ -30,7 +31,7 @@ async def run_rest_health_check( metadata = tool_metadata if tool_id is not None: try: - tool = crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) + tool = tool_crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) except ItemDeletedException: raise ToolConfigurationError(f"Tool '{tool_id}' has been deleted.") if not tool: @@ -44,7 +45,9 @@ async def run_rest_health_check( if metadata is None: metadata = tool.tool_metadata else: - provider_type = crud.get_type_lookup(db, provider_type_id, organization_id, user_id) + provider_type = type_lookup_crud.get_type_lookup( + db, provider_type_id, organization_id, user_id + ) if not provider_type: raise ToolConfigurationError(f"Provider type '{provider_type_id}' not found.") provider = provider_type.type_value diff --git a/apps/backend/src/rhesis/backend/app/services/tool/rest/jira.py b/apps/backend/src/rhesis/backend/app/services/tool/rest/jira.py index 8297f04678..67fb691322 100644 --- a/apps/backend/src/rhesis/backend/app/services/tool/rest/jira.py +++ b/apps/backend/src/rhesis/backend/app/services/tool/rest/jira.py @@ -8,8 +8,9 @@ import httpx from sqlalchemy.orm import Session -from rhesis.backend.app import crud, schemas +from rhesis.backend.app import schemas from rhesis.backend.app.crud import task as task_crud +from rhesis.backend.app.crud import tool as tool_crud from rhesis.backend.app.services.tool.rest.config import validate_base_url logger = logging.getLogger(__name__) @@ -164,7 +165,7 @@ async def create_jira_ticket_from_task( if not isinstance(client, JiraRestClient): raise ValueError(f"Tool '{tool_id}' is not a Jira integration") - tool = crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) + tool = tool_crud.get_tool(db, uuid.UUID(tool_id), organization_id, user_id) if not tool.tool_metadata or "space_key" not in tool.tool_metadata: raise ValueError("Jira tool is not configured with a space_key") diff --git a/tests/backend/services/test_jira_rest.py b/tests/backend/services/test_jira_rest.py index 1b9c5ae9c3..052690c4b3 100644 --- a/tests/backend/services/test_jira_rest.py +++ b/tests/backend/services/test_jira_rest.py @@ -42,14 +42,14 @@ async def test_create_jira_ticket_success(self): with ( patch("rhesis.backend.app.services.tool.rest.jira.task_crud") as mock_task_crud, - patch("rhesis.backend.app.services.tool.rest.jira.crud") as mock_crud, + patch("rhesis.backend.app.services.tool.rest.jira.tool_crud") as mock_tool_crud, patch( "rhesis.backend.app.services.tool.rest.config.get_rest_client", return_value=mock_jira_client, ), ): mock_task_crud.get_task.return_value = mock_task - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool result = await create_jira_ticket_from_task(task_id, tool_id, db, "org", "user") @@ -103,14 +103,14 @@ async def test_create_jira_ticket_missing_space_key(self): with ( patch("rhesis.backend.app.services.tool.rest.jira.task_crud") as mock_task_crud, - patch("rhesis.backend.app.services.tool.rest.jira.crud") as mock_crud, + patch("rhesis.backend.app.services.tool.rest.jira.tool_crud") as mock_tool_crud, patch( "rhesis.backend.app.services.tool.rest.config.get_rest_client", return_value=Mock(spec=JiraRestClient), ), ): mock_task_crud.get_task.return_value = mock_task - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool with pytest.raises(ValueError, match="not configured with a space_key"): await create_jira_ticket_from_task(task_id, tool_id, db, "org", "user") @@ -127,14 +127,14 @@ async def test_create_jira_ticket_null_metadata(self): with ( patch("rhesis.backend.app.services.tool.rest.jira.task_crud") as mock_task_crud, - patch("rhesis.backend.app.services.tool.rest.jira.crud") as mock_crud, + patch("rhesis.backend.app.services.tool.rest.jira.tool_crud") as mock_tool_crud, patch( "rhesis.backend.app.services.tool.rest.config.get_rest_client", return_value=Mock(spec=JiraRestClient), ), ): mock_task_crud.get_task.return_value = mock_task - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool with pytest.raises(ValueError, match="not configured with a space_key"): await create_jira_ticket_from_task(task_id, tool_id, db, "org", "user") diff --git a/tests/backend/services/test_mcp_service.py b/tests/backend/services/test_mcp_service.py index af92240a2e..e92d0deb1f 100644 --- a/tests/backend/services/test_mcp_service.py +++ b/tests/backend/services/test_mcp_service.py @@ -175,8 +175,8 @@ class TestGetMCPClientByToolId: """Test client creation from tool ID""" @patch("rhesis.backend.app.services.tool.mcp.config.MCPClientFactory") - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_create_client_standard_provider(self, mock_crud, mock_factory): + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + def test_create_client_standard_provider(self, mock_tool_crud, mock_factory): """Test successfully create client for standard provider""" # Setup mocks tool_id = str(uuid.uuid4()) @@ -188,7 +188,7 @@ def test_create_client_standard_provider(self, mock_crud, mock_factory): mock_tool.credentials = '{"NOTION_TOKEN": "test_token"}' mock_tool.tool_metadata = None - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool mock_factory_instance = Mock() mock_client = Mock() @@ -197,35 +197,33 @@ def test_create_client_standard_provider(self, mock_crud, mock_factory): # Execute db = Mock() - client, provider_name, project_context = _get_mcp_tool_config( - db, tool_id, org_id, user_id - ) + client, provider_name, project_context = _get_mcp_tool_config(db, tool_id, org_id, user_id) # Assert assert client == mock_client assert provider_name == "notion" assert project_context is None - mock_crud.get_tool.assert_called_once_with(db, uuid.UUID(tool_id), org_id, user_id) + mock_tool_crud.get_tool.assert_called_once_with(db, uuid.UUID(tool_id), org_id, user_id) mock_factory.from_provider.assert_called_once_with( provider="notion", credentials={"NOTION_TOKEN": "test_token"} ) mock_factory_instance.create_client.assert_called_once_with("notion") - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_raises_when_tool_not_found(self, mock_crud): + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + def test_raises_when_tool_not_found(self, mock_tool_crud): """Test raises ToolConfigurationError when tool not found""" tool_id = str(uuid.uuid4()) org_id = str(uuid.uuid4()) - mock_crud.get_tool.return_value = None + mock_tool_crud.get_tool.return_value = None with pytest.raises(ToolConfigurationError) as exc_info: _get_mcp_tool_config(Mock(), tool_id, org_id) assert "not found" in str(exc_info.value).lower() - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_raises_when_credentials_invalid_json(self, mock_crud): + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + def test_raises_when_credentials_invalid_json(self, mock_tool_crud): """Test raises ToolConfigurationError when credentials JSON is invalid""" tool_id = str(uuid.uuid4()) org_id = str(uuid.uuid4()) @@ -234,7 +232,7 @@ def test_raises_when_credentials_invalid_json(self, mock_crud): mock_tool.tool_provider_type.type_value = "notion" mock_tool.credentials = "invalid json{" - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool with pytest.raises(ToolConfigurationError) as exc_info: _get_mcp_tool_config(Mock(), tool_id, org_id) @@ -242,8 +240,8 @@ def test_raises_when_credentials_invalid_json(self, mock_crud): assert "Invalid credentials format" in str(exc_info.value) @patch("rhesis.backend.app.services.tool.mcp.config.MCPClientFactory") - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_create_client_gitlab_returns_project_context(self, mock_crud, mock_factory): + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + def test_create_client_gitlab_returns_project_context(self, mock_tool_crud, mock_factory): """GitLab tools return project scope context from metadata.""" tool_id = str(uuid.uuid4()) org_id = str(uuid.uuid4()) @@ -252,16 +250,14 @@ def test_create_client_gitlab_returns_project_context(self, mock_crud, mock_fact mock_tool.tool_provider_type.type_value = "gitlab" mock_tool.credentials = '{"GITLAB_PERSONAL_ACCESS_TOKEN": "test_token"}' mock_tool.tool_metadata = {"project": {"namespace": "group/project"}} - mock_crud.get_tool.return_value = mock_tool + mock_tool_crud.get_tool.return_value = mock_tool mock_factory_instance = Mock() mock_client = Mock() mock_factory_instance.create_client.return_value = mock_client mock_factory.from_provider.return_value = mock_factory_instance - client, provider_name, project_context = _get_mcp_tool_config( - Mock(), tool_id, org_id - ) + client, provider_name, project_context = _get_mcp_tool_config(Mock(), tool_id, org_id) assert client == mock_client assert provider_name == "gitlab" @@ -274,8 +270,8 @@ class TestGetMCPClientFromParams: """Test client creation from parameters""" @patch("rhesis.backend.app.services.tool.mcp.config.MCPClientFactory") - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_create_client_standard_provider(self, mock_crud, mock_factory): + @patch("rhesis.backend.app.services.tool.mcp.config.type_lookup_crud") + def test_create_client_standard_provider(self, mock_type_lookup_crud, mock_factory): """Test successfully create client for standard provider""" provider_type_id = uuid.uuid4() org_id = str(uuid.uuid4()) @@ -283,7 +279,7 @@ def test_create_client_standard_provider(self, mock_crud, mock_factory): mock_provider_type = Mock() mock_provider_type.type_value = "notion" - mock_crud.get_type_lookup.return_value = mock_provider_type + mock_type_lookup_crud.get_type_lookup.return_value = mock_provider_type mock_factory_instance = Mock() mock_client = Mock() @@ -304,18 +300,20 @@ def test_create_client_standard_provider(self, mock_crud, mock_factory): assert client == mock_client assert provider_name == "notion" assert project_context is None - mock_crud.get_type_lookup.assert_called_once_with(db, provider_type_id, org_id, None) + mock_type_lookup_crud.get_type_lookup.assert_called_once_with( + db, provider_type_id, org_id, None + ) mock_factory.from_provider.assert_called_once_with( provider="notion", credentials=credentials ) - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - def test_raises_when_provider_type_not_found(self, mock_crud): + @patch("rhesis.backend.app.services.tool.mcp.config.type_lookup_crud") + def test_raises_when_provider_type_not_found(self, mock_type_lookup_crud): """Test raises ValueError when provider type not found""" provider_type_id = uuid.uuid4() org_id = str(uuid.uuid4()) - mock_crud.get_type_lookup.return_value = None + mock_type_lookup_crud.get_type_lookup.return_value = None with pytest.raises(ValueError) as exc_info: _get_mcp_client_from_params(provider_type_id, {}, Mock(), org_id) @@ -329,14 +327,15 @@ def test_raises_when_provider_type_not_found(self, mock_crud): class TestQueryMCP: """Test query_mcp function""" - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - @patch("rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", - return_value=[]) + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + @patch( + "rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", return_value=[] + ) @patch("rhesis.backend.app.services.tool.mcp.operations.MCPAgent") @patch("rhesis.backend.app.services.tool.mcp.operations._get_mcp_tool_config") @patch("rhesis.backend.app.services.tool.mcp.operations.jinja_env") async def test_query_with_default_prompt( - self, mock_jinja_env, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_crud + self, mock_jinja_env, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_tool_crud ): """Test successfully execute query with default prompt""" tool_id = "test-tool-id" @@ -378,13 +377,14 @@ async def test_query_with_default_prompt( event_handlers=[], ) - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - @patch("rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", - return_value=[]) + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + @patch( + "rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", return_value=[] + ) @patch("rhesis.backend.app.services.tool.mcp.operations.MCPAgent") @patch("rhesis.backend.app.services.tool.mcp.operations._get_mcp_tool_config") async def test_query_with_custom_prompt( - self, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_crud + self, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_tool_crud ): """Test successfully execute query with custom system_prompt""" tool_id = "test-tool-id" @@ -420,14 +420,15 @@ async def test_query_with_custom_prompt( event_handlers=[], ) - @patch("rhesis.backend.app.services.tool.mcp.config.crud") - @patch("rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", - return_value=[]) + @patch("rhesis.backend.app.services.tool.mcp.config.tool_crud") + @patch( + "rhesis.backend.app.services.tool.mcp.operations.get_agent_event_handlers", return_value=[] + ) @patch("rhesis.backend.app.services.tool.mcp.operations.MCPAgent") @patch("rhesis.backend.app.services.tool.mcp.operations._get_mcp_tool_config") @patch("rhesis.backend.app.services.tool.mcp.operations.jinja_env") async def test_query_with_custom_max_iterations( - self, mock_jinja_env, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_crud + self, mock_jinja_env, mock_get_client, mock_mcp_agent, mock_get_handlers, mock_tool_crud ): """Test successfully execute query with custom max_iterations""" tool_id = "test-tool-id" diff --git a/tests/backend/services/test_rest_config.py b/tests/backend/services/test_rest_config.py index 4267faa7da..39528fa051 100644 --- a/tests/backend/services/test_rest_config.py +++ b/tests/backend/services/test_rest_config.py @@ -35,7 +35,11 @@ def test_jira(self): ): client = build_client( "jira", - {"JIRA_URL": "https://example.atlassian.net", "JIRA_USERNAME": "n", "JIRA_API_TOKEN": "t"}, + { + "JIRA_URL": "https://example.atlassian.net", + "JIRA_USERNAME": "n", + "JIRA_API_TOKEN": "t", + }, ) assert isinstance(client, JiraRestClient) @@ -46,7 +50,11 @@ def test_confluence(self): ): client = build_client( "confluence", - {"CONFLUENCE_URL": "https://example.atlassian.net", "CONFLUENCE_USERNAME": "n", "CONFLUENCE_API_TOKEN": "t"}, + { + "CONFLUENCE_URL": "https://example.atlassian.net", + "CONFLUENCE_USERNAME": "n", + "CONFLUENCE_API_TOKEN": "t", + }, ) assert isinstance(client, ConfluenceRestClient) @@ -70,36 +78,28 @@ def test_resolves_client(self): db = Mock(spec=Session) org_id = str(uuid.uuid4()) tool_id = str(uuid.uuid4()) - with patch( - "rhesis.backend.app.services.tool.rest.config.crud" - ) as mock_crud: - mock_crud.get_tool.return_value = self._tool("github", '{"X": "y"}') + with patch("rhesis.backend.app.services.tool.rest.config.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.return_value = self._tool("github", '{"X": "y"}') client = get_rest_client(db, tool_id, org_id) assert isinstance(client, GitHubRestClient) def test_tool_not_found_raises(self): db = Mock(spec=Session) - with patch( - "rhesis.backend.app.services.tool.rest.config.crud" - ) as mock_crud: - mock_crud.get_tool.return_value = None + with patch("rhesis.backend.app.services.tool.rest.config.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.return_value = None with pytest.raises(ToolConfigurationError, match="not found"): get_rest_client(db, str(uuid.uuid4()), str(uuid.uuid4())) def test_deleted_tool_raises(self): db = Mock(spec=Session) - with patch( - "rhesis.backend.app.services.tool.rest.config.crud" - ) as mock_crud: - mock_crud.get_tool.side_effect = ItemDeletedException("Tool", "gone") + with patch("rhesis.backend.app.services.tool.rest.config.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.side_effect = ItemDeletedException("Tool", "gone") with pytest.raises(ToolConfigurationError, match="has been deleted"): get_rest_client(db, str(uuid.uuid4()), str(uuid.uuid4())) def test_invalid_credentials_json_raises(self): db = Mock(spec=Session) - with patch( - "rhesis.backend.app.services.tool.rest.config.crud" - ) as mock_crud: - mock_crud.get_tool.return_value = self._tool(credentials="not json{") + with patch("rhesis.backend.app.services.tool.rest.config.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.return_value = self._tool(credentials="not json{") with pytest.raises(ToolConfigurationError, match="Invalid credentials"): get_rest_client(db, str(uuid.uuid4()), str(uuid.uuid4())) diff --git a/tests/backend/services/test_rest_health.py b/tests/backend/services/test_rest_health.py index 5e1fdc2039..555186b75a 100644 --- a/tests/backend/services/test_rest_health.py +++ b/tests/backend/services/test_rest_health.py @@ -29,13 +29,13 @@ async def test_existing_tool_path(self): mock_client.health_check = AsyncMock(return_value=_OK) with ( - patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud, + patch("rhesis.backend.app.services.tool.rest.health.tool_crud") as mock_tool_crud, patch( "rhesis.backend.app.services.tool.rest.health.build_client", return_value=mock_client, ) as mock_build, ): - mock_crud.get_tool.return_value = tool + mock_tool_crud.get_tool.return_value = tool result = await run_rest_health_check( db, "org", tool_id=str(uuid.uuid4()), user_id="user" ) @@ -52,13 +52,15 @@ async def test_unsaved_provider_path(self): mock_client.health_check = AsyncMock(return_value=_OK) with ( - patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud, + patch( + "rhesis.backend.app.services.tool.rest.health.type_lookup_crud" + ) as mock_type_lookup_crud, patch( "rhesis.backend.app.services.tool.rest.health.build_client", return_value=mock_client, ) as mock_build, ): - mock_crud.get_type_lookup.return_value = provider_type + mock_type_lookup_crud.get_type_lookup.return_value = provider_type result = await run_rest_health_check( db, "org", @@ -82,13 +84,13 @@ async def test_existing_tool_passes_scope_metadata(self): scope_metadata = {"repository": {"owner": "o", "repo": "r2"}} with ( - patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud, + patch("rhesis.backend.app.services.tool.rest.health.tool_crud") as mock_tool_crud, patch( "rhesis.backend.app.services.tool.rest.health.build_client", return_value=mock_client, ), ): - mock_crud.get_tool.return_value = tool + mock_tool_crud.get_tool.return_value = tool await run_rest_health_check( db, "org", @@ -101,22 +103,24 @@ async def test_existing_tool_passes_scope_metadata(self): async def test_tool_not_found_raises(self): db = Mock(spec=Session) - with patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud: - mock_crud.get_tool.return_value = None + with patch("rhesis.backend.app.services.tool.rest.health.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.return_value = None with pytest.raises(ToolConfigurationError, match="not found"): await run_rest_health_check(db, "org", tool_id=str(uuid.uuid4())) async def test_deleted_tool_raises(self): db = Mock(spec=Session) - with patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud: - mock_crud.get_tool.side_effect = ItemDeletedException("Tool", "gone") + with patch("rhesis.backend.app.services.tool.rest.health.tool_crud") as mock_tool_crud: + mock_tool_crud.get_tool.side_effect = ItemDeletedException("Tool", "gone") with pytest.raises(ToolConfigurationError, match="has been deleted"): await run_rest_health_check(db, "org", tool_id=str(uuid.uuid4())) async def test_unknown_provider_type_raises(self): db = Mock(spec=Session) - with patch("rhesis.backend.app.services.tool.rest.health.crud") as mock_crud: - mock_crud.get_type_lookup.return_value = None + with patch( + "rhesis.backend.app.services.tool.rest.health.type_lookup_crud" + ) as mock_type_lookup_crud: + mock_type_lookup_crud.get_type_lookup.return_value = None with pytest.raises(ToolConfigurationError, match="not found"): await run_rest_health_check( db, "org", provider_type_id=uuid.uuid4(), credentials={}