From 31904cba843e7ee5d84c00a6f6e564d057d97464 Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 19 Jul 2026 23:48:31 -0500 Subject: [PATCH] refactor(api): collapse dead v1/_v2 DI provider tiers The DI layer carried three providers per service, threading three project-resolution strategies: by name (v1), by integer id (_v2), and by external UUID (_v2_external). The v1 routers are long gone and every live v2 router consumes only the _v2_external tier, so the v1 and _v2 tiers were dead weight - most of it marked '# pragma: no cover'. Delete the v1 and _v2 provider tiers across deps/projects.py, deps/repositories.py, deps/services.py, and deps/importers.py, along with the name-based and integer-id project resolution they existed to thread. One provider per service remains. Test updates: test_deps.py now exercises the live validate_project_external_id resolver; the mcp/cli/test-int conftests drop their vestigial get_project_config overrides (no route resolves it anymore); the embedding-provider reuse test calls the surviving search repository provider. Part of #1109 (ConfigManager injection is the remaining half) Co-Authored-By: Claude Fable 5 Signed-off-by: phernandez --- src/basic_memory/deps/__init__.py | 146 ---------- src/basic_memory/deps/importers.py | 152 +---------- src/basic_memory/deps/projects.py | 165 +---------- src/basic_memory/deps/repositories.py | 99 +------ src/basic_memory/deps/services.py | 253 ----------------- test-int/conftest.py | 3 +- .../semantic/test_embedding_provider_reuse.py | 6 +- tests/cli/conftest.py | 3 +- tests/mcp/conftest.py | 3 +- tests/test_deps.py | 258 +----------------- 10 files changed, 31 insertions(+), 1057 deletions(-) diff --git a/src/basic_memory/deps/__init__.py b/src/basic_memory/deps/__init__.py index c849c7011..82dbe7cbd 100644 --- a/src/basic_memory/deps/__init__.py +++ b/src/basic_memory/deps/__init__.py @@ -32,15 +32,6 @@ from basic_memory.deps.projects import ( get_project_repository, ProjectRepositoryDep, - ProjectPathDep, - get_project_id, - ProjectIdDep, - get_project_config, - ProjectConfigDep, - validate_project_id, - ProjectIdPathDep, - get_project_config_v2, - ProjectConfigV2Dep, validate_project_external_id, ProjectExternalIdPathDep, get_project_config_v2_external, @@ -48,49 +39,21 @@ ) from basic_memory.deps.repositories import ( - get_entity_repository, - EntityRepositoryDep, - get_entity_repository_v2, - EntityRepositoryV2Dep, get_entity_repository_v2_external, EntityRepositoryV2ExternalDep, - get_observation_repository, - ObservationRepositoryDep, - get_observation_repository_v2, - ObservationRepositoryV2Dep, get_observation_repository_v2_external, ObservationRepositoryV2ExternalDep, - get_relation_repository, - RelationRepositoryDep, - get_relation_repository_v2, - RelationRepositoryV2Dep, get_relation_repository_v2_external, RelationRepositoryV2ExternalDep, - get_search_repository, - SearchRepositoryDep, - get_search_repository_v2, - SearchRepositoryV2Dep, get_search_repository_v2_external, SearchRepositoryV2ExternalDep, ) from basic_memory.deps.services import ( - get_entity_parser, - EntityParserDep, - get_entity_parser_v2, - EntityParserV2Dep, get_entity_parser_v2_external, EntityParserV2ExternalDep, - get_markdown_processor, - MarkdownProcessorDep, - get_markdown_processor_v2, - MarkdownProcessorV2Dep, get_markdown_processor_v2_external, MarkdownProcessorV2ExternalDep, - get_file_service, - FileServiceDep, - get_file_service_v2, - FileServiceV2Dep, get_file_service_v2_external, FileServiceV2ExternalDep, get_entity_vector_sync_scheduler, @@ -103,10 +66,6 @@ ProjectIndexCommandDep, get_search_reindex_scheduler, SearchReindexSchedulerDep, - get_search_service, - SearchServiceDep, - get_search_service_v2, - SearchServiceV2Dep, get_search_service_v2_external, SearchServiceV2ExternalDep, get_note_content_query_service, @@ -117,22 +76,10 @@ NoteContentMaterializationProviderDep, get_directory_delete_service, DirectoryDeleteServiceDep, - get_link_resolver, - LinkResolverDep, - get_link_resolver_v2, - LinkResolverV2Dep, get_link_resolver_v2_external, LinkResolverV2ExternalDep, - get_entity_service, - EntityServiceDep, - get_entity_service_v2, - EntityServiceV2Dep, get_entity_service_v2_external, EntityServiceV2ExternalDep, - get_context_service, - ContextServiceDep, - get_context_service_v2, - ContextServiceV2Dep, get_context_service_v2_external, ContextServiceV2ExternalDep, get_index_file_executor_v2_external, @@ -143,37 +90,17 @@ ProjectIndexObserverDep, get_project_service, ProjectServiceDep, - get_directory_service, - DirectoryServiceDep, - get_directory_service_v2, - DirectoryServiceV2Dep, get_directory_service_v2_external, DirectoryServiceV2ExternalDep, ) from basic_memory.deps.importers import ( - get_chatgpt_importer, - ChatGPTImporterDep, - get_chatgpt_importer_v2, - ChatGPTImporterV2Dep, get_chatgpt_importer_v2_external, ChatGPTImporterV2ExternalDep, - get_claude_conversations_importer, - ClaudeConversationsImporterDep, - get_claude_conversations_importer_v2, - ClaudeConversationsImporterV2Dep, get_claude_conversations_importer_v2_external, ClaudeConversationsImporterV2ExternalDep, - get_claude_projects_importer, - ClaudeProjectsImporterDep, - get_claude_projects_importer_v2, - ClaudeProjectsImporterV2Dep, get_claude_projects_importer_v2_external, ClaudeProjectsImporterV2ExternalDep, - get_memory_json_importer, - MemoryJsonImporterDep, - get_memory_json_importer_v2, - MemoryJsonImporterV2Dep, get_memory_json_importer_v2_external, MemoryJsonImporterV2ExternalDep, ) @@ -192,61 +119,24 @@ # Projects "get_project_repository", "ProjectRepositoryDep", - "ProjectPathDep", - "get_project_id", - "ProjectIdDep", - "get_project_config", - "ProjectConfigDep", - "validate_project_id", - "ProjectIdPathDep", - "get_project_config_v2", - "ProjectConfigV2Dep", "validate_project_external_id", "ProjectExternalIdPathDep", "get_project_config_v2_external", "ProjectConfigV2ExternalDep", # Repositories - "get_entity_repository", - "EntityRepositoryDep", - "get_entity_repository_v2", - "EntityRepositoryV2Dep", "get_entity_repository_v2_external", "EntityRepositoryV2ExternalDep", - "get_observation_repository", - "ObservationRepositoryDep", - "get_observation_repository_v2", - "ObservationRepositoryV2Dep", "get_observation_repository_v2_external", "ObservationRepositoryV2ExternalDep", - "get_relation_repository", - "RelationRepositoryDep", - "get_relation_repository_v2", - "RelationRepositoryV2Dep", "get_relation_repository_v2_external", "RelationRepositoryV2ExternalDep", - "get_search_repository", - "SearchRepositoryDep", - "get_search_repository_v2", - "SearchRepositoryV2Dep", "get_search_repository_v2_external", "SearchRepositoryV2ExternalDep", # Services - "get_entity_parser", - "EntityParserDep", - "get_entity_parser_v2", - "EntityParserV2Dep", "get_entity_parser_v2_external", "EntityParserV2ExternalDep", - "get_markdown_processor", - "MarkdownProcessorDep", - "get_markdown_processor_v2", - "MarkdownProcessorV2Dep", "get_markdown_processor_v2_external", "MarkdownProcessorV2ExternalDep", - "get_file_service", - "FileServiceDep", - "get_file_service_v2", - "FileServiceV2Dep", "get_file_service_v2_external", "FileServiceV2ExternalDep", "get_entity_vector_sync_scheduler", @@ -259,10 +149,6 @@ "ProjectIndexCommandDep", "get_search_reindex_scheduler", "SearchReindexSchedulerDep", - "get_search_service", - "SearchServiceDep", - "get_search_service_v2", - "SearchServiceV2Dep", "get_search_service_v2_external", "SearchServiceV2ExternalDep", "get_note_content_query_service", @@ -273,22 +159,10 @@ "NoteContentMaterializationProviderDep", "get_directory_delete_service", "DirectoryDeleteServiceDep", - "get_link_resolver", - "LinkResolverDep", - "get_link_resolver_v2", - "LinkResolverV2Dep", "get_link_resolver_v2_external", "LinkResolverV2ExternalDep", - "get_entity_service", - "EntityServiceDep", - "get_entity_service_v2", - "EntityServiceV2Dep", "get_entity_service_v2_external", "EntityServiceV2ExternalDep", - "get_context_service", - "ContextServiceDep", - "get_context_service_v2", - "ContextServiceV2Dep", "get_context_service_v2_external", "ContextServiceV2ExternalDep", "get_index_file_executor_v2_external", @@ -299,35 +173,15 @@ "ProjectIndexObserverDep", "get_project_service", "ProjectServiceDep", - "get_directory_service", - "DirectoryServiceDep", - "get_directory_service_v2", - "DirectoryServiceV2Dep", "get_directory_service_v2_external", "DirectoryServiceV2ExternalDep", # Importers - "get_chatgpt_importer", - "ChatGPTImporterDep", - "get_chatgpt_importer_v2", - "ChatGPTImporterV2Dep", "get_chatgpt_importer_v2_external", "ChatGPTImporterV2ExternalDep", - "get_claude_conversations_importer", - "ClaudeConversationsImporterDep", - "get_claude_conversations_importer_v2", - "ClaudeConversationsImporterV2Dep", "get_claude_conversations_importer_v2_external", "ClaudeConversationsImporterV2ExternalDep", - "get_claude_projects_importer", - "ClaudeProjectsImporterDep", - "get_claude_projects_importer_v2", - "ClaudeProjectsImporterV2Dep", "get_claude_projects_importer_v2_external", "ClaudeProjectsImporterV2ExternalDep", - "get_memory_json_importer", - "MemoryJsonImporterDep", - "get_memory_json_importer_v2", - "MemoryJsonImporterV2Dep", "get_memory_json_importer_v2_external", "MemoryJsonImporterV2ExternalDep", ] diff --git a/src/basic_memory/deps/importers.py b/src/basic_memory/deps/importers.py index bddeff78f..fc1c013bb 100644 --- a/src/basic_memory/deps/importers.py +++ b/src/basic_memory/deps/importers.py @@ -11,17 +11,9 @@ from fastapi import Depends -from basic_memory.deps.projects import ( - ProjectConfigDep, - ProjectConfigV2Dep, - ProjectConfigV2ExternalDep, -) +from basic_memory.deps.projects import ProjectConfigV2ExternalDep from basic_memory.deps.services import ( - FileServiceDep, - FileServiceV2Dep, FileServiceV2ExternalDep, - MarkdownProcessorDep, - MarkdownProcessorV2Dep, MarkdownProcessorV2ExternalDep, ) from basic_memory.importers import ( @@ -35,40 +27,6 @@ # --- ChatGPT Importer --- -async def get_chatgpt_importer( - project_config: ProjectConfigDep, - markdown_processor: MarkdownProcessorDep, - file_service: FileServiceDep, -) -> ChatGPTImporter: - """Create ChatGPTImporter with dependencies.""" - return ChatGPTImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)] - - -async def get_chatgpt_importer_v2( # pragma: no cover - project_config: ProjectConfigV2Dep, - markdown_processor: MarkdownProcessorV2Dep, - file_service: FileServiceV2Dep, -) -> ChatGPTImporter: - """Create ChatGPTImporter with v2 dependencies.""" - return ChatGPTImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ChatGPTImporterV2Dep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer_v2)] - - async def get_chatgpt_importer_v2_external( project_config: ProjectConfigV2ExternalDep, markdown_processor: MarkdownProcessorV2ExternalDep, @@ -89,44 +47,6 @@ async def get_chatgpt_importer_v2_external( # --- Claude Conversations Importer --- -async def get_claude_conversations_importer( - project_config: ProjectConfigDep, - markdown_processor: MarkdownProcessorDep, - file_service: FileServiceDep, -) -> ClaudeConversationsImporter: - """Create ClaudeConversationsImporter with dependencies.""" - return ClaudeConversationsImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ClaudeConversationsImporterDep = Annotated[ - ClaudeConversationsImporter, Depends(get_claude_conversations_importer) -] - - -async def get_claude_conversations_importer_v2( # pragma: no cover - project_config: ProjectConfigV2Dep, - markdown_processor: MarkdownProcessorV2Dep, - file_service: FileServiceV2Dep, -) -> ClaudeConversationsImporter: - """Create ClaudeConversationsImporter with v2 dependencies.""" - return ClaudeConversationsImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ClaudeConversationsImporterV2Dep = Annotated[ - ClaudeConversationsImporter, Depends(get_claude_conversations_importer_v2) -] - - async def get_claude_conversations_importer_v2_external( project_config: ProjectConfigV2ExternalDep, markdown_processor: MarkdownProcessorV2ExternalDep, @@ -149,42 +69,6 @@ async def get_claude_conversations_importer_v2_external( # --- Claude Projects Importer --- -async def get_claude_projects_importer( - project_config: ProjectConfigDep, - markdown_processor: MarkdownProcessorDep, - file_service: FileServiceDep, -) -> ClaudeProjectsImporter: - """Create ClaudeProjectsImporter with dependencies.""" - return ClaudeProjectsImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)] - - -async def get_claude_projects_importer_v2( # pragma: no cover - project_config: ProjectConfigV2Dep, - markdown_processor: MarkdownProcessorV2Dep, - file_service: FileServiceV2Dep, -) -> ClaudeProjectsImporter: - """Create ClaudeProjectsImporter with v2 dependencies.""" - return ClaudeProjectsImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -ClaudeProjectsImporterV2Dep = Annotated[ - ClaudeProjectsImporter, Depends(get_claude_projects_importer_v2) -] - - async def get_claude_projects_importer_v2_external( project_config: ProjectConfigV2ExternalDep, markdown_processor: MarkdownProcessorV2ExternalDep, @@ -207,40 +91,6 @@ async def get_claude_projects_importer_v2_external( # --- Memory JSON Importer --- -async def get_memory_json_importer( - project_config: ProjectConfigDep, - markdown_processor: MarkdownProcessorDep, - file_service: FileServiceDep, -) -> MemoryJsonImporter: - """Create MemoryJsonImporter with dependencies.""" - return MemoryJsonImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)] - - -async def get_memory_json_importer_v2( # pragma: no cover - project_config: ProjectConfigV2Dep, - markdown_processor: MarkdownProcessorV2Dep, - file_service: FileServiceV2Dep, -) -> MemoryJsonImporter: - """Create MemoryJsonImporter with v2 dependencies.""" - return MemoryJsonImporter( - project_config.home, - markdown_processor, - file_service, - project_name=project_config.name, - ) - - -MemoryJsonImporterV2Dep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer_v2)] - - async def get_memory_json_importer_v2_external( project_config: ProjectConfigV2ExternalDep, markdown_processor: MarkdownProcessorV2ExternalDep, diff --git a/src/basic_memory/deps/projects.py b/src/basic_memory/deps/projects.py index f39d10247..5edda2539 100644 --- a/src/basic_memory/deps/projects.py +++ b/src/basic_memory/deps/projects.py @@ -1,22 +1,24 @@ """Project dependency injection for basic-memory. This module provides project-related dependencies: -- Project path extraction from URL +- Project resolution from the external UUID in the URL path - Project config resolution -- Project ID validation - Project repository + +The v2 API is the only public surface, and it addresses projects exclusively by +external UUID; the name-based (v1) and integer-id resolution tiers were removed +with the v1 routers (#1109). """ import pathlib from typing import Annotated -from fastapi import Depends, HTTPException, Path, status +from fastapi import Depends, HTTPException, status from basic_memory import db from basic_memory.config import ProjectConfig from basic_memory.deps.db import SessionMakerDep from basic_memory.repository.project_repository import ProjectRepository -from basic_memory.utils import generate_permalink # --- Project Repository --- @@ -30,161 +32,6 @@ async def get_project_repository() -> ProjectRepository: ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)] -# --- Path Extraction --- - -# V1 API: Project name from URL path -ProjectPathDep = Annotated[str, Path()] - - -# --- Project ID Resolution (V1 API) --- - - -async def get_project_id( - session_maker: SessionMakerDep, - project_repository: ProjectRepositoryDep, - project: ProjectPathDep, -) -> int: - """Get the current project ID from request state. - - When using sub-applications with /{project} mounting, the project value - is stored in request.state by middleware. - - Args: - project_repository: Repository for project operations - project: The project name from URL path - - Returns: - The resolved project ID - - Raises: - HTTPException: If project is not found - """ - # Convert project name to permalink for lookup - project_permalink = generate_permalink(str(project)) - async with db.scoped_session(session_maker) as session: - project_obj = await project_repository.get_by_permalink(session, project_permalink) - if project_obj: - return project_obj.id - - # Try by name if permalink lookup fails - project_obj = await project_repository.get_by_name( - session, str(project) - ) # pragma: no cover - if project_obj: # pragma: no cover - return project_obj.id - - # Not found - raise HTTPException( # pragma: no cover - status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found." - ) - - -ProjectIdDep = Annotated[int, Depends(get_project_id)] - - -# --- Project Config Resolution (V1 API) --- - - -async def get_project_config( - session_maker: SessionMakerDep, - project: ProjectPathDep, - project_repository: ProjectRepositoryDep, -) -> ProjectConfig: # pragma: no cover - """Get the current project referenced from request state. - - Args: - project: The project name from URL path - project_repository: Repository for project operations - - Returns: - The resolved project config - - Raises: - HTTPException: If project is not found - """ - # Convert project name to permalink for lookup - project_permalink = generate_permalink(str(project)) - async with db.scoped_session(session_maker) as session: - project_obj = await project_repository.get_by_permalink(session, project_permalink) - if project_obj: - return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path)) - - # Not found - raise HTTPException( # pragma: no cover - status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found." - ) - - -ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] - - -# --- V2 API: Integer Project ID from Path --- - - -async def validate_project_id( - session_maker: SessionMakerDep, - project_id: int, - project_repository: ProjectRepositoryDep, -) -> int: - """Validate that a numeric project ID exists in the database. - - This is used for v2 API endpoints that take project IDs as integers in the path. - The project_id parameter will be automatically extracted from the URL path by FastAPI. - - Args: - project_id: The numeric project ID from the URL path - project_repository: Repository for project operations - - Returns: - The validated project ID - - Raises: - HTTPException: If project with that ID is not found - """ - async with db.scoped_session(session_maker) as session: - project_obj = await project_repository.get_by_id(session, project_id) - if not project_obj: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail=f"Project with ID {project_id} not found.", - ) - return project_id - - -ProjectIdPathDep = Annotated[int, Depends(validate_project_id)] - - -async def get_project_config_v2( - session_maker: SessionMakerDep, - project_id: ProjectIdPathDep, - project_repository: ProjectRepositoryDep, -) -> ProjectConfig: # pragma: no cover - """Get the project config for v2 API (uses integer project_id from path). - - Args: - project_id: The validated numeric project ID from the URL path - project_repository: Repository for project operations - - Returns: - The resolved project config - - Raises: - HTTPException: If project is not found - """ - async with db.scoped_session(session_maker) as session: - project_obj = await project_repository.get_by_id(session, project_id) - if project_obj: - return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path)) - - # Not found (this should not happen since ProjectIdPathDep already validates existence) - raise HTTPException( # pragma: no cover - status_code=status.HTTP_404_NOT_FOUND, detail=f"Project with ID {project_id} not found." - ) - - -ProjectConfigV2Dep = Annotated[ProjectConfig, Depends(get_project_config_v2)] - - # --- V2 API: External UUID Project ID from Path --- diff --git a/src/basic_memory/deps/repositories.py b/src/basic_memory/deps/repositories.py index 9670d41be..ccc325880 100644 --- a/src/basic_memory/deps/repositories.py +++ b/src/basic_memory/deps/repositories.py @@ -6,7 +6,8 @@ - RelationRepository - SearchRepository -Each repository is scoped to a project ID from the request. +Each repository is scoped to the project resolved from the external UUID in the +request path (the only resolution tier since the v1 routers were removed, #1109). """ from typing import Annotated @@ -15,11 +16,7 @@ from basic_memory.deps.config import AppConfigDep from basic_memory.deps.db import SessionMakerDep -from basic_memory.deps.projects import ( - ProjectIdDep, - ProjectIdPathDep, - ProjectExternalIdPathDep, -) +from basic_memory.deps.projects import ProjectExternalIdPathDep from basic_memory.repository.entity_repository import EntityRepository from basic_memory.repository.observation_repository import ObservationRepository from basic_memory.repository.relation_repository import RelationRepository @@ -29,26 +26,6 @@ # --- Entity Repository --- -async def get_entity_repository( - project_id: ProjectIdDep, -) -> EntityRepository: - """Create an EntityRepository instance for the current project.""" - return EntityRepository(project_id=project_id) - - -EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)] - - -async def get_entity_repository_v2( # pragma: no cover - project_id: ProjectIdPathDep, -) -> EntityRepository: - """Create an EntityRepository instance for v2 API (uses integer project_id from path).""" - return EntityRepository(project_id=project_id) - - -EntityRepositoryV2Dep = Annotated[EntityRepository, Depends(get_entity_repository_v2)] - - async def get_entity_repository_v2_external( project_id: ProjectExternalIdPathDep, ) -> EntityRepository: @@ -64,28 +41,6 @@ async def get_entity_repository_v2_external( # --- Observation Repository --- -async def get_observation_repository( - project_id: ProjectIdDep, -) -> ObservationRepository: - """Create an ObservationRepository instance for the current project.""" - return ObservationRepository(project_id=project_id) - - -ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)] - - -async def get_observation_repository_v2( # pragma: no cover - project_id: ProjectIdPathDep, -) -> ObservationRepository: - """Create an ObservationRepository instance for v2 API.""" - return ObservationRepository(project_id=project_id) - - -ObservationRepositoryV2Dep = Annotated[ - ObservationRepository, Depends(get_observation_repository_v2) -] - - async def get_observation_repository_v2_external( project_id: ProjectExternalIdPathDep, ) -> ObservationRepository: @@ -101,26 +56,6 @@ async def get_observation_repository_v2_external( # --- Relation Repository --- -async def get_relation_repository( - project_id: ProjectIdDep, -) -> RelationRepository: - """Create a RelationRepository instance for the current project.""" - return RelationRepository(project_id=project_id) - - -RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)] - - -async def get_relation_repository_v2( # pragma: no cover - project_id: ProjectIdPathDep, -) -> RelationRepository: - """Create a RelationRepository instance for v2 API.""" - return RelationRepository(project_id=project_id) - - -RelationRepositoryV2Dep = Annotated[RelationRepository, Depends(get_relation_repository_v2)] - - async def get_relation_repository_v2_external( project_id: ProjectExternalIdPathDep, ) -> RelationRepository: @@ -136,9 +71,9 @@ async def get_relation_repository_v2_external( # --- Search Repository --- -async def get_search_repository( +async def get_search_repository_v2_external( session_maker: SessionMakerDep, - project_id: ProjectIdDep, + project_id: ProjectExternalIdPathDep, app_config: AppConfigDep, ) -> SearchRepository: """Create a backend-specific SearchRepository instance for the current project. @@ -149,30 +84,6 @@ async def get_search_repository( return create_search_repository(session_maker, project_id=project_id, app_config=app_config) -SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)] - - -async def get_search_repository_v2( # pragma: no cover - session_maker: SessionMakerDep, - project_id: ProjectIdPathDep, - app_config: AppConfigDep, -) -> SearchRepository: - """Create a SearchRepository instance for v2 API.""" - return create_search_repository(session_maker, project_id=project_id, app_config=app_config) - - -SearchRepositoryV2Dep = Annotated[SearchRepository, Depends(get_search_repository_v2)] - - -async def get_search_repository_v2_external( - session_maker: SessionMakerDep, - project_id: ProjectExternalIdPathDep, - app_config: AppConfigDep, -) -> SearchRepository: - """Create a SearchRepository instance for v2 API (uses external_id).""" - return create_search_repository(session_maker, project_id=project_id, app_config=app_config) - - SearchRepositoryV2ExternalDep = Annotated[ SearchRepository, Depends(get_search_repository_v2_external) ] diff --git a/src/basic_memory/deps/services.py b/src/basic_memory/deps/services.py index 18d07bfdb..311872992 100644 --- a/src/basic_memory/deps/services.py +++ b/src/basic_memory/deps/services.py @@ -20,23 +20,13 @@ from basic_memory.deps.config import AppConfigDep from basic_memory.deps.db import SessionMakerDep from basic_memory.deps.projects import ( - ProjectConfigDep, - ProjectConfigV2Dep, ProjectConfigV2ExternalDep, ProjectRepositoryDep, ) from basic_memory.deps.repositories import ( - EntityRepositoryDep, - EntityRepositoryV2Dep, EntityRepositoryV2ExternalDep, - ObservationRepositoryDep, - ObservationRepositoryV2Dep, ObservationRepositoryV2ExternalDep, - RelationRepositoryDep, - RelationRepositoryV2Dep, RelationRepositoryV2ExternalDep, - SearchRepositoryDep, - SearchRepositoryV2Dep, SearchRepositoryV2ExternalDep, ) from basic_memory.indexing.relation_resolution import RepositoryRelationResolutionRuntime @@ -97,22 +87,6 @@ # --- Entity Parser --- -async def get_entity_parser(project_config: ProjectConfigDep) -> EntityParser: - return EntityParser(project_config.home) - - -EntityParserDep = Annotated["EntityParser", Depends(get_entity_parser)] - - -async def get_entity_parser_v2( - project_config: ProjectConfigV2Dep, -) -> EntityParser: # pragma: no cover - return EntityParser(project_config.home) - - -EntityParserV2Dep = Annotated["EntityParser", Depends(get_entity_parser_v2)] - - async def get_entity_parser_v2_external(project_config: ProjectConfigV2ExternalDep) -> EntityParser: return EntityParser(project_config.home) @@ -123,24 +97,6 @@ async def get_entity_parser_v2_external(project_config: ProjectConfigV2ExternalD # --- Markdown Processor --- -async def get_markdown_processor( - entity_parser: EntityParserDep, app_config: AppConfigDep -) -> MarkdownProcessor: - return MarkdownProcessor(entity_parser, app_config=app_config) - - -MarkdownProcessorDep = Annotated[MarkdownProcessor, Depends(get_markdown_processor)] - - -async def get_markdown_processor_v2( # pragma: no cover - entity_parser: EntityParserV2Dep, app_config: AppConfigDep -) -> MarkdownProcessor: - return MarkdownProcessor(entity_parser, app_config=app_config) - - -MarkdownProcessorV2Dep = Annotated[MarkdownProcessor, Depends(get_markdown_processor_v2)] - - async def get_markdown_processor_v2_external( entity_parser: EntityParserV2ExternalDep, app_config: AppConfigDep ) -> MarkdownProcessor: @@ -155,36 +111,6 @@ async def get_markdown_processor_v2_external( # --- File Service --- -async def get_file_service( - project_config: ProjectConfigDep, - markdown_processor: MarkdownProcessorDep, - app_config: AppConfigDep, -) -> FileService: - file_service = FileService(project_config.home, markdown_processor, app_config=app_config) - logger.debug( - f"Created FileService for project: {project_config.name}, base_path: {project_config.home} " - ) - return file_service - - -FileServiceDep = Annotated[FileService, Depends(get_file_service)] - - -async def get_file_service_v2( # pragma: no cover - project_config: ProjectConfigV2Dep, - markdown_processor: MarkdownProcessorV2Dep, - app_config: AppConfigDep, -) -> FileService: - file_service = FileService(project_config.home, markdown_processor, app_config=app_config) - logger.debug( - f"Created FileService for project: {project_config.name}, base_path: {project_config.home}" - ) - return file_service - - -FileServiceV2Dep = Annotated[FileService, Depends(get_file_service_v2)] - - async def get_file_service_v2_external( project_config: ProjectConfigV2ExternalDep, markdown_processor: MarkdownProcessorV2ExternalDep, @@ -203,32 +129,6 @@ async def get_file_service_v2_external( # --- Search Service --- -async def get_search_service( - search_repository: SearchRepositoryDep, - entity_repository: EntityRepositoryDep, - file_service: FileServiceDep, - session_maker: SessionMakerDep, -) -> SearchService: - """Create SearchService with dependencies.""" - return SearchService(search_repository, entity_repository, file_service, session_maker) - - -SearchServiceDep = Annotated[SearchService, Depends(get_search_service)] - - -async def get_search_service_v2( # pragma: no cover - search_repository: SearchRepositoryV2Dep, - entity_repository: EntityRepositoryV2Dep, - file_service: FileServiceV2Dep, - session_maker: SessionMakerDep, -) -> SearchService: - """Create SearchService for v2 API.""" - return SearchService(search_repository, entity_repository, file_service, session_maker) - - -SearchServiceV2Dep = Annotated[SearchService, Depends(get_search_service_v2)] - - async def get_search_service_v2_external( search_repository: SearchRepositoryV2ExternalDep, entity_repository: EntityRepositoryV2ExternalDep, @@ -287,36 +187,6 @@ async def get_directory_delete_service( # --- Link Resolver --- -async def get_link_resolver( - entity_repository: EntityRepositoryDep, - search_service: SearchServiceDep, - session_maker: SessionMakerDep, -) -> LinkResolver: - return LinkResolver( - entity_repository=entity_repository, - search_service=search_service, - session_maker=session_maker, - ) - - -LinkResolverDep = Annotated[LinkResolver, Depends(get_link_resolver)] - - -async def get_link_resolver_v2( # pragma: no cover - entity_repository: EntityRepositoryV2Dep, - search_service: SearchServiceV2Dep, - session_maker: SessionMakerDep, -) -> LinkResolver: - return LinkResolver( - entity_repository=entity_repository, - search_service=search_service, - session_maker=session_maker, - ) - - -LinkResolverV2Dep = Annotated[LinkResolver, Depends(get_link_resolver_v2)] - - async def get_link_resolver_v2_external( entity_repository: EntityRepositoryV2ExternalDep, search_service: SearchServiceV2ExternalDep, @@ -335,62 +205,6 @@ async def get_link_resolver_v2_external( # --- Entity Service --- -async def get_entity_service( - entity_repository: EntityRepositoryDep, - observation_repository: ObservationRepositoryDep, - relation_repository: RelationRepositoryDep, - entity_parser: EntityParserDep, - file_service: FileServiceDep, - link_resolver: LinkResolverDep, - search_service: SearchServiceDep, - session_maker: SessionMakerDep, - app_config: AppConfigDep, -) -> EntityService: - """Create EntityService with repository.""" - return EntityService( - entity_repository=entity_repository, - observation_repository=observation_repository, - relation_repository=relation_repository, - entity_parser=entity_parser, - file_service=file_service, - link_resolver=link_resolver, - session_maker=session_maker, - search_service=search_service, - app_config=app_config, - ) - - -EntityServiceDep = Annotated[EntityService, Depends(get_entity_service)] - - -async def get_entity_service_v2( # pragma: no cover - entity_repository: EntityRepositoryV2Dep, - observation_repository: ObservationRepositoryV2Dep, - relation_repository: RelationRepositoryV2Dep, - entity_parser: EntityParserV2Dep, - file_service: FileServiceV2Dep, - link_resolver: LinkResolverV2Dep, - search_service: SearchServiceV2Dep, - session_maker: SessionMakerDep, - app_config: AppConfigDep, -) -> EntityService: - """Create EntityService for v2 API.""" - return EntityService( - entity_repository=entity_repository, - observation_repository=observation_repository, - relation_repository=relation_repository, - entity_parser=entity_parser, - file_service=file_service, - link_resolver=link_resolver, - session_maker=session_maker, - search_service=search_service, - app_config=app_config, - ) - - -EntityServiceV2Dep = Annotated[EntityService, Depends(get_entity_service_v2)] - - async def get_entity_service_v2_external( entity_repository: EntityRepositoryV2ExternalDep, observation_repository: ObservationRepositoryV2ExternalDep, @@ -422,45 +236,6 @@ async def get_entity_service_v2_external( # --- Context Service --- -async def get_context_service( - search_repository: SearchRepositoryDep, - entity_repository: EntityRepositoryDep, - observation_repository: ObservationRepositoryDep, - link_resolver: LinkResolverDep, - session_maker: SessionMakerDep, -) -> ContextService: - return ContextService( - search_repository=search_repository, - entity_repository=entity_repository, - observation_repository=observation_repository, - link_resolver=link_resolver, - session_maker=session_maker, - ) - - -ContextServiceDep = Annotated[ContextService, Depends(get_context_service)] - - -async def get_context_service_v2( # pragma: no cover - search_repository: SearchRepositoryV2Dep, - entity_repository: EntityRepositoryV2Dep, - observation_repository: ObservationRepositoryV2Dep, - link_resolver: LinkResolverV2Dep, - session_maker: SessionMakerDep, -) -> ContextService: - """Create ContextService for v2 API.""" - return ContextService( - search_repository=search_repository, - entity_repository=entity_repository, - observation_repository=observation_repository, - link_resolver=link_resolver, - session_maker=session_maker, - ) - - -ContextServiceV2Dep = Annotated[ContextService, Depends(get_context_service_v2)] - - async def get_context_service_v2_external( search_repository: SearchRepositoryV2ExternalDep, entity_repository: EntityRepositoryV2ExternalDep, @@ -752,34 +527,6 @@ async def get_project_service( # --- Directory Service --- -async def get_directory_service( - entity_repository: EntityRepositoryDep, - session_maker: SessionMakerDep, -) -> DirectoryService: - """Create DirectoryService with dependencies.""" - return DirectoryService( - entity_repository=entity_repository, - session_maker=session_maker, - ) - - -DirectoryServiceDep = Annotated[DirectoryService, Depends(get_directory_service)] - - -async def get_directory_service_v2( # pragma: no cover - entity_repository: EntityRepositoryV2Dep, - session_maker: SessionMakerDep, -) -> DirectoryService: - """Create DirectoryService for v2 API (uses integer project_id from path).""" - return DirectoryService( - entity_repository=entity_repository, - session_maker=session_maker, - ) - - -DirectoryServiceV2Dep = Annotated[DirectoryService, Depends(get_directory_service_v2)] - - async def get_directory_service_v2_external( entity_repository: EntityRepositoryV2ExternalDep, session_maker: SessionMakerDep, diff --git a/test-int/conftest.py b/test-int/conftest.py index fa031b4bb..aa1166c41 100644 --- a/test-int/conftest.py +++ b/test-int/conftest.py @@ -84,7 +84,7 @@ async def test_my_mcp_tool(mcp_server, app): from basic_memory.repository.project_repository import ProjectRepository from fastapi import FastAPI -from basic_memory.deps import get_project_config, get_engine_factory, get_app_config +from basic_memory.deps import get_engine_factory, get_app_config # Import MCP tools so they're available for testing @@ -465,7 +465,6 @@ def app( app = fastapi_app previous_overrides = dict(app.dependency_overrides) - app.dependency_overrides[get_project_config] = lambda: project_config app.dependency_overrides[get_engine_factory] = lambda: engine_factory app.dependency_overrides[get_app_config] = lambda: app_config try: diff --git a/test-int/semantic/test_embedding_provider_reuse.py b/test-int/semantic/test_embedding_provider_reuse.py index d1d22ada5..45a398db1 100644 --- a/test-int/semantic/test_embedding_provider_reuse.py +++ b/test-int/semantic/test_embedding_provider_reuse.py @@ -8,7 +8,7 @@ These tests use the *real* composition paths — ``create_embedding_provider``, the ``create_search_repository`` factory, and the FastAPI deps function -``get_search_repository`` — with a real FastEmbed provider. FastEmbed loads the +``get_search_repository_v2_external`` — with a real FastEmbed provider. FastEmbed loads the ONNX model lazily on first embed, so constructing providers/repositories here is cheap and never touches the native model. @@ -28,7 +28,7 @@ import pytest from basic_memory.config import BasicMemoryConfig, DatabaseBackend, ProjectEntry -from basic_memory.deps.repositories import get_search_repository +from basic_memory.deps.repositories import get_search_repository_v2_external from basic_memory.repository.embedding_provider_factory import ( create_embedding_provider, reset_embedding_provider_cache, @@ -93,7 +93,7 @@ async def test_deps_path_reuses_cached_provider(tmp_path, sqlite_engine_factory) repo = cast( SQLiteSearchRepository, - await get_search_repository( + await get_search_repository_v2_external( session_maker=session_maker, project_id=1, app_config=config, diff --git a/tests/cli/conftest.py b/tests/cli/conftest.py index 5f982ec8a..6f8bce19c 100644 --- a/tests/cli/conftest.py +++ b/tests/cli/conftest.py @@ -8,7 +8,7 @@ from httpx import AsyncClient, ASGITransport from basic_memory.api.app import app as fastapi_app -from basic_memory.deps import get_project_config, get_engine_factory, get_app_config +from basic_memory.deps import get_engine_factory, get_app_config @pytest.fixture(autouse=True) @@ -45,7 +45,6 @@ async def app( app = fastapi_app previous_overrides = dict(app.dependency_overrides) app.dependency_overrides[get_app_config] = lambda: app_config - app.dependency_overrides[get_project_config] = lambda: project_config app.dependency_overrides[get_engine_factory] = lambda: engine_factory try: yield app diff --git a/tests/mcp/conftest.py b/tests/mcp/conftest.py index 67de63912..44d7e4dcf 100644 --- a/tests/mcp/conftest.py +++ b/tests/mcp/conftest.py @@ -10,7 +10,7 @@ from mcp.server import FastMCP from basic_memory.api.app import app as fastapi_app -from basic_memory.deps import get_project_config, get_engine_factory, get_app_config +from basic_memory.deps import get_engine_factory, get_app_config from basic_memory.services.search_service import SearchService from basic_memory.mcp.server import mcp as mcp_server @@ -53,7 +53,6 @@ def app( app = fastapi_app previous_overrides = dict(app.dependency_overrides) app.dependency_overrides[get_app_config] = lambda: app_config - app.dependency_overrides[get_project_config] = lambda: project_config app.dependency_overrides[get_engine_factory] = lambda: engine_factory try: yield app diff --git a/tests/test_deps.py b/tests/test_deps.py index 6b0535b2a..6336e7555 100644 --- a/tests/test_deps.py +++ b/tests/test_deps.py @@ -1,271 +1,39 @@ -"""Tests for dependency injection functions in deps.py.""" - -from datetime import datetime, timezone -from pathlib import Path +"""Tests for dependency injection functions in the deps package.""" import pytest -import pytest_asyncio from fastapi import HTTPException -from basic_memory import db -from basic_memory.deps import get_project_config, get_project_id -from basic_memory.deps.projects import validate_project_id +from basic_memory.deps import validate_project_external_id from basic_memory.models.project import Project from basic_memory.repository.project_repository import ProjectRepository -@pytest_asyncio.fixture -async def project_with_spaces(project_repository: ProjectRepository, session_maker) -> Project: - """Create a project with spaces in the name for testing permalink normalization.""" - project_data = { - "name": "My Test Project", - "description": "A project with spaces in the name", - "path": "/my/test/project", - "is_active": True, - "is_default": False, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), - } - async with db.scoped_session(session_maker) as session: - return await project_repository.create(session, project_data) - - -@pytest_asyncio.fixture -async def project_with_special_chars( - project_repository: ProjectRepository, session_maker -) -> Project: - """Create a project with special characters for testing permalink normalization.""" - project_data = { - "name": "Project: Test & Development!", - "description": "A project with special characters", - "path": "/project/test/dev", - "is_active": True, - "is_default": False, - "created_at": datetime.now(timezone.utc), - "updated_at": datetime.now(timezone.utc), - } - async with db.scoped_session(session_maker) as session: - return await project_repository.create(session, project_data) - - -@pytest.mark.asyncio -async def test_get_project_config_with_spaces( - project_repository: ProjectRepository, project_with_spaces: Project, session_maker -): - """Test that get_project_config normalizes project names with spaces.""" - # The project name has spaces: "My Test Project" - # The permalink should be: "my-test-project" - assert project_with_spaces.name == "My Test Project" - assert project_with_spaces.permalink == "my-test-project" - - # Call get_project_config with the project name (not permalink) - # This simulates what happens when the project name comes from URL path - config = await get_project_config( - session_maker=session_maker, - project="My Test Project", - project_repository=project_repository, - ) - - # Verify we got the correct project config - assert config.name == "My Test Project" - assert config.home == Path("/my/test/project") - - -@pytest.mark.asyncio -async def test_get_project_config_with_permalink( - project_repository: ProjectRepository, project_with_spaces: Project, session_maker -): - """Test that get_project_config works when already given a permalink.""" - # Call with the permalink directly - config = await get_project_config( - session_maker=session_maker, - project="my-test-project", - project_repository=project_repository, - ) - - # Verify we got the correct project config - assert config.name == "My Test Project" - assert config.home == Path("/my/test/project") - - -@pytest.mark.asyncio -async def test_get_project_config_with_special_chars( - project_repository: ProjectRepository, project_with_special_chars: Project, session_maker -): - """Test that get_project_config normalizes project names with special characters.""" - # The project name has special chars: "Project: Test & Development!" - # The permalink should be: "project-test-development" - assert project_with_special_chars.name == "Project: Test & Development!" - assert project_with_special_chars.permalink == "project-test-development" - - # Call get_project_config with the project name - config = await get_project_config( - session_maker=session_maker, - project="Project: Test & Development!", - project_repository=project_repository, - ) - - # Verify we got the correct project config - assert config.name == "Project: Test & Development!" - assert config.home == Path("/project/test/dev") - - -@pytest.mark.asyncio -async def test_get_project_config_not_found(project_repository: ProjectRepository, session_maker): - """Test that get_project_config raises HTTPException when project not found.""" - with pytest.raises(HTTPException) as exc_info: - await get_project_config( - session_maker=session_maker, - project="Nonexistent Project", - project_repository=project_repository, - ) - - assert exc_info.value.status_code == 404 - assert "Project 'Nonexistent Project' not found" in exc_info.value.detail - - @pytest.mark.asyncio -async def test_get_project_id_with_spaces( - project_repository: ProjectRepository, project_with_spaces: Project, session_maker -): - """Test that get_project_id normalizes project names with spaces.""" - # Call get_project_id with the project name (not permalink) - project_id = await get_project_id( - session_maker=session_maker, - project_repository=project_repository, - project="My Test Project", - ) - - # Verify we got the correct project ID - assert project_id == project_with_spaces.id - - -@pytest.mark.asyncio -async def test_get_project_id_with_permalink( - project_repository: ProjectRepository, project_with_spaces: Project, session_maker -): - """Test that get_project_id works when already given a permalink.""" - # Call with the permalink directly - project_id = await get_project_id( - session_maker=session_maker, - project_repository=project_repository, - project="my-test-project", - ) - - # Verify we got the correct project ID - assert project_id == project_with_spaces.id - - -@pytest.mark.asyncio -async def test_get_project_id_with_special_chars( - project_repository: ProjectRepository, project_with_special_chars: Project, session_maker -): - """Test that get_project_id normalizes project names with special characters.""" - # Call get_project_id with the project name - project_id = await get_project_id( - session_maker=session_maker, - project_repository=project_repository, - project="Project: Test & Development!", - ) - - # Verify we got the correct project ID - assert project_id == project_with_special_chars.id - - -@pytest.mark.asyncio -async def test_get_project_id_not_found(project_repository: ProjectRepository, session_maker): - """Test that get_project_id raises HTTPException when project not found.""" - with pytest.raises(HTTPException) as exc_info: - await get_project_id( - session_maker=session_maker, - project_repository=project_repository, - project="Nonexistent Project", - ) - - assert exc_info.value.status_code == 404 - assert "Project 'Nonexistent Project' not found" in exc_info.value.detail - - -@pytest.mark.asyncio -async def test_get_project_id_fallback_to_name( +async def test_validate_project_external_id_success( project_repository: ProjectRepository, test_project: Project, session_maker ): - """Test that get_project_id falls back to name lookup if permalink lookup fails. - - This test verifies the fallback behavior in get_project_id where it tries - get_by_name if get_by_permalink returns None. - """ - # The test_project fixture has name "test-project" and permalink "test-project" - # Since both are the same, we can't easily test the fallback with existing fixtures - # So this test just verifies the normal path works with test_project - project_id = await get_project_id( + """validate_project_external_id resolves the internal id from the external UUID.""" + project_id = await validate_project_external_id( session_maker=session_maker, + project_id=test_project.external_id, project_repository=project_repository, - project="test-project", ) assert project_id == test_project.id @pytest.mark.asyncio -async def test_get_project_config_case_sensitivity( - project_repository: ProjectRepository, project_with_spaces: Project, session_maker -): - """Test that get_project_config handles case variations correctly. - - Permalink normalization should convert to lowercase, so different case - variations of the same name should resolve to the same project. - """ - # Create project with mixed case: "My Test Project" -> permalink "my-test-project" - - # Try with different case variations - config1 = await get_project_config( - session_maker=session_maker, - project="My Test Project", - project_repository=project_repository, - ) - config2 = await get_project_config( - session_maker=session_maker, - project="my test project", - project_repository=project_repository, - ) - config3 = await get_project_config( - session_maker=session_maker, - project="MY TEST PROJECT", - project_repository=project_repository, - ) - - # All should resolve to the same project - assert config1.name == config2.name == config3.name == "My Test Project" - assert config1.home == config2.home == config3.home == Path("/my/test/project") - - -# --- Tests for validate_project_id (v2 API) --- - - -@pytest.mark.asyncio -async def test_validate_project_id_success( - project_repository: ProjectRepository, test_project: Project, session_maker +async def test_validate_project_external_id_not_found( + project_repository: ProjectRepository, session_maker ): - """Test that validate_project_id returns project_id when project exists.""" - project_id = await validate_project_id( - session_maker=session_maker, - project_id=test_project.id, - project_repository=project_repository, - ) - - assert project_id == test_project.id - - -@pytest.mark.asyncio -async def test_validate_project_id_not_found(project_repository: ProjectRepository, session_maker): - """Test that validate_project_id raises HTTPException when project not found.""" + """validate_project_external_id raises HTTPException when no project matches.""" + fake_uuid = "00000000-0000-0000-0000-000000000000" with pytest.raises(HTTPException) as exc_info: - await validate_project_id( + await validate_project_external_id( session_maker=session_maker, - project_id=99999, + project_id=fake_uuid, project_repository=project_repository, ) assert exc_info.value.status_code == 404 - assert "Project with ID 99999 not found" in exc_info.value.detail + assert f"Project with external_id '{fake_uuid}' not found" in exc_info.value.detail