basic-memory 0.16.1__py3-none-any.whl → 0.17.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (143) hide show
  1. basic_memory/__init__.py +1 -1
  2. basic_memory/alembic/env.py +112 -26
  3. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  4. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +15 -3
  5. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +44 -36
  6. basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
  7. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  8. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +13 -0
  9. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  10. basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
  11. basic_memory/api/app.py +45 -24
  12. basic_memory/api/container.py +133 -0
  13. basic_memory/api/routers/knowledge_router.py +17 -5
  14. basic_memory/api/routers/project_router.py +68 -14
  15. basic_memory/api/routers/resource_router.py +37 -27
  16. basic_memory/api/routers/utils.py +53 -14
  17. basic_memory/api/v2/__init__.py +35 -0
  18. basic_memory/api/v2/routers/__init__.py +21 -0
  19. basic_memory/api/v2/routers/directory_router.py +93 -0
  20. basic_memory/api/v2/routers/importer_router.py +181 -0
  21. basic_memory/api/v2/routers/knowledge_router.py +427 -0
  22. basic_memory/api/v2/routers/memory_router.py +130 -0
  23. basic_memory/api/v2/routers/project_router.py +359 -0
  24. basic_memory/api/v2/routers/prompt_router.py +269 -0
  25. basic_memory/api/v2/routers/resource_router.py +286 -0
  26. basic_memory/api/v2/routers/search_router.py +73 -0
  27. basic_memory/cli/app.py +43 -7
  28. basic_memory/cli/auth.py +27 -4
  29. basic_memory/cli/commands/__init__.py +3 -1
  30. basic_memory/cli/commands/cloud/api_client.py +20 -5
  31. basic_memory/cli/commands/cloud/cloud_utils.py +13 -6
  32. basic_memory/cli/commands/cloud/rclone_commands.py +110 -14
  33. basic_memory/cli/commands/cloud/rclone_installer.py +18 -4
  34. basic_memory/cli/commands/cloud/upload.py +10 -3
  35. basic_memory/cli/commands/command_utils.py +52 -4
  36. basic_memory/cli/commands/db.py +78 -19
  37. basic_memory/cli/commands/format.py +198 -0
  38. basic_memory/cli/commands/import_chatgpt.py +12 -8
  39. basic_memory/cli/commands/import_claude_conversations.py +12 -8
  40. basic_memory/cli/commands/import_claude_projects.py +12 -8
  41. basic_memory/cli/commands/import_memory_json.py +12 -8
  42. basic_memory/cli/commands/mcp.py +8 -26
  43. basic_memory/cli/commands/project.py +22 -9
  44. basic_memory/cli/commands/status.py +3 -2
  45. basic_memory/cli/commands/telemetry.py +81 -0
  46. basic_memory/cli/container.py +84 -0
  47. basic_memory/cli/main.py +7 -0
  48. basic_memory/config.py +177 -77
  49. basic_memory/db.py +183 -77
  50. basic_memory/deps/__init__.py +293 -0
  51. basic_memory/deps/config.py +26 -0
  52. basic_memory/deps/db.py +56 -0
  53. basic_memory/deps/importers.py +200 -0
  54. basic_memory/deps/projects.py +238 -0
  55. basic_memory/deps/repositories.py +179 -0
  56. basic_memory/deps/services.py +480 -0
  57. basic_memory/deps.py +14 -409
  58. basic_memory/file_utils.py +212 -3
  59. basic_memory/ignore_utils.py +5 -5
  60. basic_memory/importers/base.py +40 -19
  61. basic_memory/importers/chatgpt_importer.py +17 -4
  62. basic_memory/importers/claude_conversations_importer.py +27 -12
  63. basic_memory/importers/claude_projects_importer.py +50 -14
  64. basic_memory/importers/memory_json_importer.py +36 -16
  65. basic_memory/importers/utils.py +5 -2
  66. basic_memory/markdown/entity_parser.py +62 -23
  67. basic_memory/markdown/markdown_processor.py +67 -4
  68. basic_memory/markdown/plugins.py +4 -2
  69. basic_memory/markdown/utils.py +10 -1
  70. basic_memory/mcp/async_client.py +1 -0
  71. basic_memory/mcp/clients/__init__.py +28 -0
  72. basic_memory/mcp/clients/directory.py +70 -0
  73. basic_memory/mcp/clients/knowledge.py +176 -0
  74. basic_memory/mcp/clients/memory.py +120 -0
  75. basic_memory/mcp/clients/project.py +89 -0
  76. basic_memory/mcp/clients/resource.py +71 -0
  77. basic_memory/mcp/clients/search.py +65 -0
  78. basic_memory/mcp/container.py +110 -0
  79. basic_memory/mcp/project_context.py +47 -33
  80. basic_memory/mcp/prompts/ai_assistant_guide.py +2 -2
  81. basic_memory/mcp/prompts/recent_activity.py +2 -2
  82. basic_memory/mcp/prompts/utils.py +3 -3
  83. basic_memory/mcp/server.py +58 -0
  84. basic_memory/mcp/tools/build_context.py +14 -14
  85. basic_memory/mcp/tools/canvas.py +34 -12
  86. basic_memory/mcp/tools/chatgpt_tools.py +4 -1
  87. basic_memory/mcp/tools/delete_note.py +31 -7
  88. basic_memory/mcp/tools/edit_note.py +14 -9
  89. basic_memory/mcp/tools/list_directory.py +7 -17
  90. basic_memory/mcp/tools/move_note.py +35 -31
  91. basic_memory/mcp/tools/project_management.py +29 -25
  92. basic_memory/mcp/tools/read_content.py +13 -3
  93. basic_memory/mcp/tools/read_note.py +24 -14
  94. basic_memory/mcp/tools/recent_activity.py +32 -38
  95. basic_memory/mcp/tools/search.py +17 -10
  96. basic_memory/mcp/tools/utils.py +28 -0
  97. basic_memory/mcp/tools/view_note.py +2 -1
  98. basic_memory/mcp/tools/write_note.py +37 -14
  99. basic_memory/models/knowledge.py +15 -2
  100. basic_memory/models/project.py +7 -1
  101. basic_memory/models/search.py +58 -2
  102. basic_memory/project_resolver.py +222 -0
  103. basic_memory/repository/entity_repository.py +210 -3
  104. basic_memory/repository/observation_repository.py +1 -0
  105. basic_memory/repository/postgres_search_repository.py +451 -0
  106. basic_memory/repository/project_repository.py +38 -1
  107. basic_memory/repository/relation_repository.py +58 -2
  108. basic_memory/repository/repository.py +1 -0
  109. basic_memory/repository/search_index_row.py +95 -0
  110. basic_memory/repository/search_repository.py +77 -615
  111. basic_memory/repository/search_repository_base.py +241 -0
  112. basic_memory/repository/sqlite_search_repository.py +437 -0
  113. basic_memory/runtime.py +61 -0
  114. basic_memory/schemas/base.py +36 -6
  115. basic_memory/schemas/directory.py +2 -1
  116. basic_memory/schemas/memory.py +9 -2
  117. basic_memory/schemas/project_info.py +2 -0
  118. basic_memory/schemas/response.py +84 -27
  119. basic_memory/schemas/search.py +5 -0
  120. basic_memory/schemas/sync_report.py +1 -1
  121. basic_memory/schemas/v2/__init__.py +27 -0
  122. basic_memory/schemas/v2/entity.py +133 -0
  123. basic_memory/schemas/v2/resource.py +47 -0
  124. basic_memory/services/context_service.py +219 -43
  125. basic_memory/services/directory_service.py +26 -11
  126. basic_memory/services/entity_service.py +68 -33
  127. basic_memory/services/file_service.py +131 -16
  128. basic_memory/services/initialization.py +51 -26
  129. basic_memory/services/link_resolver.py +1 -0
  130. basic_memory/services/project_service.py +68 -43
  131. basic_memory/services/search_service.py +75 -16
  132. basic_memory/sync/__init__.py +2 -1
  133. basic_memory/sync/coordinator.py +160 -0
  134. basic_memory/sync/sync_service.py +135 -115
  135. basic_memory/sync/watch_service.py +32 -12
  136. basic_memory/telemetry.py +249 -0
  137. basic_memory/utils.py +96 -75
  138. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/METADATA +129 -5
  139. basic_memory-0.17.4.dist-info/RECORD +193 -0
  140. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
  141. basic_memory-0.16.1.dist-info/RECORD +0 -148
  142. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +0 -0
  143. {basic_memory-0.16.1.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,56 @@
1
+ """Database dependency injection for basic-memory.
2
+
3
+ This module provides database-related dependencies:
4
+ - Engine and session maker factories
5
+ - Session dependencies for request handling
6
+ """
7
+
8
+ from typing import Annotated
9
+
10
+ from fastapi import Depends, Request
11
+ from loguru import logger
12
+ from sqlalchemy.ext.asyncio import (
13
+ AsyncEngine,
14
+ AsyncSession,
15
+ async_sessionmaker,
16
+ )
17
+
18
+ from basic_memory import db
19
+ from basic_memory.deps.config import get_app_config
20
+
21
+
22
+ async def get_engine_factory(
23
+ request: Request,
24
+ ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
25
+ """Get cached engine and session maker from app state.
26
+
27
+ For API requests, returns cached connections from app.state for optimal performance.
28
+ For non-API contexts (CLI), falls back to direct database connection.
29
+ """
30
+ # Try to get cached connections from app state (API context)
31
+ if (
32
+ hasattr(request, "app")
33
+ and hasattr(request.app.state, "engine")
34
+ and hasattr(request.app.state, "session_maker")
35
+ ):
36
+ return request.app.state.engine, request.app.state.session_maker
37
+
38
+ # Fallback for non-API contexts (CLI)
39
+ logger.debug("Using fallback database connection for non-API context")
40
+ app_config = get_app_config()
41
+ engine, session_maker = await db.get_or_create_db(app_config.database_path)
42
+ return engine, session_maker
43
+
44
+
45
+ EngineFactoryDep = Annotated[
46
+ tuple[AsyncEngine, async_sessionmaker[AsyncSession]], Depends(get_engine_factory)
47
+ ]
48
+
49
+
50
+ async def get_session_maker(engine_factory: EngineFactoryDep) -> async_sessionmaker[AsyncSession]:
51
+ """Get session maker."""
52
+ _, session_maker = engine_factory
53
+ return session_maker
54
+
55
+
56
+ SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
@@ -0,0 +1,200 @@
1
+ """Importer dependency injection for basic-memory.
2
+
3
+ This module provides importer dependencies:
4
+ - ChatGPTImporter
5
+ - ClaudeConversationsImporter
6
+ - ClaudeProjectsImporter
7
+ - MemoryJsonImporter
8
+ """
9
+
10
+ from typing import Annotated
11
+
12
+ from fastapi import Depends
13
+
14
+ from basic_memory.deps.projects import (
15
+ ProjectConfigDep,
16
+ ProjectConfigV2Dep,
17
+ ProjectConfigV2ExternalDep,
18
+ )
19
+ from basic_memory.deps.services import (
20
+ FileServiceDep,
21
+ FileServiceV2Dep,
22
+ FileServiceV2ExternalDep,
23
+ MarkdownProcessorDep,
24
+ MarkdownProcessorV2Dep,
25
+ MarkdownProcessorV2ExternalDep,
26
+ )
27
+ from basic_memory.importers import (
28
+ ChatGPTImporter,
29
+ ClaudeConversationsImporter,
30
+ ClaudeProjectsImporter,
31
+ MemoryJsonImporter,
32
+ )
33
+
34
+
35
+ # --- ChatGPT Importer ---
36
+
37
+
38
+ async def get_chatgpt_importer(
39
+ project_config: ProjectConfigDep,
40
+ markdown_processor: MarkdownProcessorDep,
41
+ file_service: FileServiceDep,
42
+ ) -> ChatGPTImporter:
43
+ """Create ChatGPTImporter with dependencies."""
44
+ return ChatGPTImporter(project_config.home, markdown_processor, file_service)
45
+
46
+
47
+ ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)]
48
+
49
+
50
+ async def get_chatgpt_importer_v2( # pragma: no cover
51
+ project_config: ProjectConfigV2Dep,
52
+ markdown_processor: MarkdownProcessorV2Dep,
53
+ file_service: FileServiceV2Dep,
54
+ ) -> ChatGPTImporter:
55
+ """Create ChatGPTImporter with v2 dependencies."""
56
+ return ChatGPTImporter(project_config.home, markdown_processor, file_service)
57
+
58
+
59
+ ChatGPTImporterV2Dep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer_v2)]
60
+
61
+
62
+ async def get_chatgpt_importer_v2_external(
63
+ project_config: ProjectConfigV2ExternalDep,
64
+ markdown_processor: MarkdownProcessorV2ExternalDep,
65
+ file_service: FileServiceV2ExternalDep,
66
+ ) -> ChatGPTImporter:
67
+ """Create ChatGPTImporter with v2 external_id dependencies."""
68
+ return ChatGPTImporter(project_config.home, markdown_processor, file_service)
69
+
70
+
71
+ ChatGPTImporterV2ExternalDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer_v2_external)]
72
+
73
+
74
+ # --- Claude Conversations Importer ---
75
+
76
+
77
+ async def get_claude_conversations_importer(
78
+ project_config: ProjectConfigDep,
79
+ markdown_processor: MarkdownProcessorDep,
80
+ file_service: FileServiceDep,
81
+ ) -> ClaudeConversationsImporter:
82
+ """Create ClaudeConversationsImporter with dependencies."""
83
+ return ClaudeConversationsImporter(project_config.home, markdown_processor, file_service)
84
+
85
+
86
+ ClaudeConversationsImporterDep = Annotated[
87
+ ClaudeConversationsImporter, Depends(get_claude_conversations_importer)
88
+ ]
89
+
90
+
91
+ async def get_claude_conversations_importer_v2( # pragma: no cover
92
+ project_config: ProjectConfigV2Dep,
93
+ markdown_processor: MarkdownProcessorV2Dep,
94
+ file_service: FileServiceV2Dep,
95
+ ) -> ClaudeConversationsImporter:
96
+ """Create ClaudeConversationsImporter with v2 dependencies."""
97
+ return ClaudeConversationsImporter(project_config.home, markdown_processor, file_service)
98
+
99
+
100
+ ClaudeConversationsImporterV2Dep = Annotated[
101
+ ClaudeConversationsImporter, Depends(get_claude_conversations_importer_v2)
102
+ ]
103
+
104
+
105
+ async def get_claude_conversations_importer_v2_external(
106
+ project_config: ProjectConfigV2ExternalDep,
107
+ markdown_processor: MarkdownProcessorV2ExternalDep,
108
+ file_service: FileServiceV2ExternalDep,
109
+ ) -> ClaudeConversationsImporter:
110
+ """Create ClaudeConversationsImporter with v2 external_id dependencies."""
111
+ return ClaudeConversationsImporter(project_config.home, markdown_processor, file_service)
112
+
113
+
114
+ ClaudeConversationsImporterV2ExternalDep = Annotated[
115
+ ClaudeConversationsImporter, Depends(get_claude_conversations_importer_v2_external)
116
+ ]
117
+
118
+
119
+ # --- Claude Projects Importer ---
120
+
121
+
122
+ async def get_claude_projects_importer(
123
+ project_config: ProjectConfigDep,
124
+ markdown_processor: MarkdownProcessorDep,
125
+ file_service: FileServiceDep,
126
+ ) -> ClaudeProjectsImporter:
127
+ """Create ClaudeProjectsImporter with dependencies."""
128
+ return ClaudeProjectsImporter(project_config.home, markdown_processor, file_service)
129
+
130
+
131
+ ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)]
132
+
133
+
134
+ async def get_claude_projects_importer_v2( # pragma: no cover
135
+ project_config: ProjectConfigV2Dep,
136
+ markdown_processor: MarkdownProcessorV2Dep,
137
+ file_service: FileServiceV2Dep,
138
+ ) -> ClaudeProjectsImporter:
139
+ """Create ClaudeProjectsImporter with v2 dependencies."""
140
+ return ClaudeProjectsImporter(project_config.home, markdown_processor, file_service)
141
+
142
+
143
+ ClaudeProjectsImporterV2Dep = Annotated[
144
+ ClaudeProjectsImporter, Depends(get_claude_projects_importer_v2)
145
+ ]
146
+
147
+
148
+ async def get_claude_projects_importer_v2_external(
149
+ project_config: ProjectConfigV2ExternalDep,
150
+ markdown_processor: MarkdownProcessorV2ExternalDep,
151
+ file_service: FileServiceV2ExternalDep,
152
+ ) -> ClaudeProjectsImporter:
153
+ """Create ClaudeProjectsImporter with v2 external_id dependencies."""
154
+ return ClaudeProjectsImporter(project_config.home, markdown_processor, file_service)
155
+
156
+
157
+ ClaudeProjectsImporterV2ExternalDep = Annotated[
158
+ ClaudeProjectsImporter, Depends(get_claude_projects_importer_v2_external)
159
+ ]
160
+
161
+
162
+ # --- Memory JSON Importer ---
163
+
164
+
165
+ async def get_memory_json_importer(
166
+ project_config: ProjectConfigDep,
167
+ markdown_processor: MarkdownProcessorDep,
168
+ file_service: FileServiceDep,
169
+ ) -> MemoryJsonImporter:
170
+ """Create MemoryJsonImporter with dependencies."""
171
+ return MemoryJsonImporter(project_config.home, markdown_processor, file_service)
172
+
173
+
174
+ MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]
175
+
176
+
177
+ async def get_memory_json_importer_v2( # pragma: no cover
178
+ project_config: ProjectConfigV2Dep,
179
+ markdown_processor: MarkdownProcessorV2Dep,
180
+ file_service: FileServiceV2Dep,
181
+ ) -> MemoryJsonImporter:
182
+ """Create MemoryJsonImporter with v2 dependencies."""
183
+ return MemoryJsonImporter(project_config.home, markdown_processor, file_service)
184
+
185
+
186
+ MemoryJsonImporterV2Dep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer_v2)]
187
+
188
+
189
+ async def get_memory_json_importer_v2_external(
190
+ project_config: ProjectConfigV2ExternalDep,
191
+ markdown_processor: MarkdownProcessorV2ExternalDep,
192
+ file_service: FileServiceV2ExternalDep,
193
+ ) -> MemoryJsonImporter:
194
+ """Create MemoryJsonImporter with v2 external_id dependencies."""
195
+ return MemoryJsonImporter(project_config.home, markdown_processor, file_service)
196
+
197
+
198
+ MemoryJsonImporterV2ExternalDep = Annotated[
199
+ MemoryJsonImporter, Depends(get_memory_json_importer_v2_external)
200
+ ]
@@ -0,0 +1,238 @@
1
+ """Project dependency injection for basic-memory.
2
+
3
+ This module provides project-related dependencies:
4
+ - Project path extraction from URL
5
+ - Project config resolution
6
+ - Project ID validation
7
+ - Project repository
8
+ """
9
+
10
+ import pathlib
11
+ from typing import Annotated
12
+
13
+ from fastapi import Depends, HTTPException, Path, status
14
+
15
+ from basic_memory.config import ProjectConfig
16
+ from basic_memory.deps.db import SessionMakerDep
17
+ from basic_memory.repository.project_repository import ProjectRepository
18
+ from basic_memory.utils import generate_permalink
19
+
20
+
21
+ # --- Project Repository ---
22
+
23
+
24
+ async def get_project_repository(
25
+ session_maker: SessionMakerDep,
26
+ ) -> ProjectRepository:
27
+ """Get the project repository."""
28
+ return ProjectRepository(session_maker)
29
+
30
+
31
+ ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)]
32
+
33
+
34
+ # --- Path Extraction ---
35
+
36
+ # V1 API: Project name from URL path
37
+ ProjectPathDep = Annotated[str, Path()]
38
+
39
+
40
+ # --- Project ID Resolution (V1 API) ---
41
+
42
+
43
+ async def get_project_id(
44
+ project_repository: ProjectRepositoryDep,
45
+ project: ProjectPathDep,
46
+ ) -> int:
47
+ """Get the current project ID from request state.
48
+
49
+ When using sub-applications with /{project} mounting, the project value
50
+ is stored in request.state by middleware.
51
+
52
+ Args:
53
+ project_repository: Repository for project operations
54
+ project: The project name from URL path
55
+
56
+ Returns:
57
+ The resolved project ID
58
+
59
+ Raises:
60
+ HTTPException: If project is not found
61
+ """
62
+ # Convert project name to permalink for lookup
63
+ project_permalink = generate_permalink(str(project))
64
+ project_obj = await project_repository.get_by_permalink(project_permalink)
65
+ if project_obj:
66
+ return project_obj.id
67
+
68
+ # Try by name if permalink lookup fails
69
+ project_obj = await project_repository.get_by_name(str(project)) # pragma: no cover
70
+ if project_obj: # pragma: no cover
71
+ return project_obj.id
72
+
73
+ # Not found
74
+ raise HTTPException( # pragma: no cover
75
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
76
+ )
77
+
78
+
79
+ ProjectIdDep = Annotated[int, Depends(get_project_id)]
80
+
81
+
82
+ # --- Project Config Resolution (V1 API) ---
83
+
84
+
85
+ async def get_project_config(
86
+ project: ProjectPathDep, project_repository: ProjectRepositoryDep
87
+ ) -> ProjectConfig: # pragma: no cover
88
+ """Get the current project referenced from request state.
89
+
90
+ Args:
91
+ project: The project name from URL path
92
+ project_repository: Repository for project operations
93
+
94
+ Returns:
95
+ The resolved project config
96
+
97
+ Raises:
98
+ HTTPException: If project is not found
99
+ """
100
+ # Convert project name to permalink for lookup
101
+ project_permalink = generate_permalink(str(project))
102
+ project_obj = await project_repository.get_by_permalink(project_permalink)
103
+ if project_obj:
104
+ return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
105
+
106
+ # Not found
107
+ raise HTTPException( # pragma: no cover
108
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
109
+ )
110
+
111
+
112
+ ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)]
113
+
114
+
115
+ # --- V2 API: Integer Project ID from Path ---
116
+
117
+
118
+ async def validate_project_id(
119
+ project_id: int,
120
+ project_repository: ProjectRepositoryDep,
121
+ ) -> int:
122
+ """Validate that a numeric project ID exists in the database.
123
+
124
+ This is used for v2 API endpoints that take project IDs as integers in the path.
125
+ The project_id parameter will be automatically extracted from the URL path by FastAPI.
126
+
127
+ Args:
128
+ project_id: The numeric project ID from the URL path
129
+ project_repository: Repository for project operations
130
+
131
+ Returns:
132
+ The validated project ID
133
+
134
+ Raises:
135
+ HTTPException: If project with that ID is not found
136
+ """
137
+ project_obj = await project_repository.get_by_id(project_id)
138
+ if not project_obj:
139
+ raise HTTPException(
140
+ status_code=status.HTTP_404_NOT_FOUND,
141
+ detail=f"Project with ID {project_id} not found.",
142
+ )
143
+ return project_id
144
+
145
+
146
+ ProjectIdPathDep = Annotated[int, Depends(validate_project_id)]
147
+
148
+
149
+ async def get_project_config_v2(
150
+ project_id: ProjectIdPathDep, project_repository: ProjectRepositoryDep
151
+ ) -> ProjectConfig: # pragma: no cover
152
+ """Get the project config for v2 API (uses integer project_id from path).
153
+
154
+ Args:
155
+ project_id: The validated numeric project ID from the URL path
156
+ project_repository: Repository for project operations
157
+
158
+ Returns:
159
+ The resolved project config
160
+
161
+ Raises:
162
+ HTTPException: If project is not found
163
+ """
164
+ project_obj = await project_repository.get_by_id(project_id)
165
+ if project_obj:
166
+ return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
167
+
168
+ # Not found (this should not happen since ProjectIdPathDep already validates existence)
169
+ raise HTTPException( # pragma: no cover
170
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project with ID {project_id} not found."
171
+ )
172
+
173
+
174
+ ProjectConfigV2Dep = Annotated[ProjectConfig, Depends(get_project_config_v2)]
175
+
176
+
177
+ # --- V2 API: External UUID Project ID from Path ---
178
+
179
+
180
+ async def validate_project_external_id(
181
+ project_id: str,
182
+ project_repository: ProjectRepositoryDep,
183
+ ) -> int:
184
+ """Validate that a project external_id (UUID) exists in the database.
185
+
186
+ This is used for v2 API endpoints that take project external_ids as strings in the path.
187
+ The project_id parameter will be automatically extracted from the URL path by FastAPI.
188
+
189
+ Args:
190
+ project_id: The external UUID from the URL path (named project_id for URL consistency)
191
+ project_repository: Repository for project operations
192
+
193
+ Returns:
194
+ The internal numeric project ID (for use by repositories)
195
+
196
+ Raises:
197
+ HTTPException: If project with that external_id is not found
198
+ """
199
+ project_obj = await project_repository.get_by_external_id(project_id)
200
+ if not project_obj:
201
+ raise HTTPException(
202
+ status_code=status.HTTP_404_NOT_FOUND,
203
+ detail=f"Project with external_id '{project_id}' not found.",
204
+ )
205
+ return project_obj.id
206
+
207
+
208
+ ProjectExternalIdPathDep = Annotated[int, Depends(validate_project_external_id)]
209
+
210
+
211
+ async def get_project_config_v2_external(
212
+ project_id: ProjectExternalIdPathDep, project_repository: ProjectRepositoryDep
213
+ ) -> ProjectConfig: # pragma: no cover
214
+ """Get the project config for v2 API (uses external_id UUID from path).
215
+
216
+ Args:
217
+ project_id: The internal project ID resolved from external_id
218
+ project_repository: Repository for project operations
219
+
220
+ Returns:
221
+ The resolved project config
222
+
223
+ Raises:
224
+ HTTPException: If project is not found
225
+ """
226
+ project_obj = await project_repository.get_by_id(project_id)
227
+ if project_obj:
228
+ return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
229
+
230
+ # Not found (this should not happen since ProjectExternalIdPathDep already validates)
231
+ raise HTTPException( # pragma: no cover
232
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project with ID {project_id} not found."
233
+ )
234
+
235
+
236
+ ProjectConfigV2ExternalDep = Annotated[
237
+ ProjectConfig, Depends(get_project_config_v2_external)
238
+ ]
@@ -0,0 +1,179 @@
1
+ """Repository dependency injection for basic-memory.
2
+
3
+ This module provides repository dependencies:
4
+ - EntityRepository
5
+ - ObservationRepository
6
+ - RelationRepository
7
+ - SearchRepository
8
+
9
+ Each repository is scoped to a project ID from the request.
10
+ """
11
+
12
+ from typing import Annotated
13
+
14
+ from fastapi import Depends
15
+
16
+ from basic_memory.deps.db import SessionMakerDep
17
+ from basic_memory.deps.projects import (
18
+ ProjectIdDep,
19
+ ProjectIdPathDep,
20
+ ProjectExternalIdPathDep,
21
+ )
22
+ from basic_memory.repository.entity_repository import EntityRepository
23
+ from basic_memory.repository.observation_repository import ObservationRepository
24
+ from basic_memory.repository.relation_repository import RelationRepository
25
+ from basic_memory.repository.search_repository import SearchRepository, create_search_repository
26
+
27
+
28
+ # --- Entity Repository ---
29
+
30
+
31
+ async def get_entity_repository(
32
+ session_maker: SessionMakerDep,
33
+ project_id: ProjectIdDep,
34
+ ) -> EntityRepository:
35
+ """Create an EntityRepository instance for the current project."""
36
+ return EntityRepository(session_maker, project_id=project_id)
37
+
38
+
39
+ EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)]
40
+
41
+
42
+ async def get_entity_repository_v2( # pragma: no cover
43
+ session_maker: SessionMakerDep,
44
+ project_id: ProjectIdPathDep,
45
+ ) -> EntityRepository:
46
+ """Create an EntityRepository instance for v2 API (uses integer project_id from path)."""
47
+ return EntityRepository(session_maker, project_id=project_id)
48
+
49
+
50
+ EntityRepositoryV2Dep = Annotated[EntityRepository, Depends(get_entity_repository_v2)]
51
+
52
+
53
+ async def get_entity_repository_v2_external(
54
+ session_maker: SessionMakerDep,
55
+ project_id: ProjectExternalIdPathDep,
56
+ ) -> EntityRepository:
57
+ """Create an EntityRepository instance for v2 API (uses external_id from path)."""
58
+ return EntityRepository(session_maker, project_id=project_id)
59
+
60
+
61
+ EntityRepositoryV2ExternalDep = Annotated[EntityRepository, Depends(get_entity_repository_v2_external)]
62
+
63
+
64
+ # --- Observation Repository ---
65
+
66
+
67
+ async def get_observation_repository(
68
+ session_maker: SessionMakerDep,
69
+ project_id: ProjectIdDep,
70
+ ) -> ObservationRepository:
71
+ """Create an ObservationRepository instance for the current project."""
72
+ return ObservationRepository(session_maker, project_id=project_id)
73
+
74
+
75
+ ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)]
76
+
77
+
78
+ async def get_observation_repository_v2( # pragma: no cover
79
+ session_maker: SessionMakerDep,
80
+ project_id: ProjectIdPathDep,
81
+ ) -> ObservationRepository:
82
+ """Create an ObservationRepository instance for v2 API."""
83
+ return ObservationRepository(session_maker, project_id=project_id)
84
+
85
+
86
+ ObservationRepositoryV2Dep = Annotated[
87
+ ObservationRepository, Depends(get_observation_repository_v2)
88
+ ]
89
+
90
+
91
+ async def get_observation_repository_v2_external(
92
+ session_maker: SessionMakerDep,
93
+ project_id: ProjectExternalIdPathDep,
94
+ ) -> ObservationRepository:
95
+ """Create an ObservationRepository instance for v2 API (uses external_id)."""
96
+ return ObservationRepository(session_maker, project_id=project_id)
97
+
98
+
99
+ ObservationRepositoryV2ExternalDep = Annotated[
100
+ ObservationRepository, Depends(get_observation_repository_v2_external)
101
+ ]
102
+
103
+
104
+ # --- Relation Repository ---
105
+
106
+
107
+ async def get_relation_repository(
108
+ session_maker: SessionMakerDep,
109
+ project_id: ProjectIdDep,
110
+ ) -> RelationRepository:
111
+ """Create a RelationRepository instance for the current project."""
112
+ return RelationRepository(session_maker, project_id=project_id)
113
+
114
+
115
+ RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
116
+
117
+
118
+ async def get_relation_repository_v2( # pragma: no cover
119
+ session_maker: SessionMakerDep,
120
+ project_id: ProjectIdPathDep,
121
+ ) -> RelationRepository:
122
+ """Create a RelationRepository instance for v2 API."""
123
+ return RelationRepository(session_maker, project_id=project_id)
124
+
125
+
126
+ RelationRepositoryV2Dep = Annotated[RelationRepository, Depends(get_relation_repository_v2)]
127
+
128
+
129
+ async def get_relation_repository_v2_external(
130
+ session_maker: SessionMakerDep,
131
+ project_id: ProjectExternalIdPathDep,
132
+ ) -> RelationRepository:
133
+ """Create a RelationRepository instance for v2 API (uses external_id)."""
134
+ return RelationRepository(session_maker, project_id=project_id)
135
+
136
+
137
+ RelationRepositoryV2ExternalDep = Annotated[
138
+ RelationRepository, Depends(get_relation_repository_v2_external)
139
+ ]
140
+
141
+
142
+ # --- Search Repository ---
143
+
144
+
145
+ async def get_search_repository(
146
+ session_maker: SessionMakerDep,
147
+ project_id: ProjectIdDep,
148
+ ) -> SearchRepository:
149
+ """Create a backend-specific SearchRepository instance for the current project.
150
+
151
+ Uses factory function to return SQLiteSearchRepository or PostgresSearchRepository
152
+ based on database backend configuration.
153
+ """
154
+ return create_search_repository(session_maker, project_id=project_id)
155
+
156
+
157
+ SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
158
+
159
+
160
+ async def get_search_repository_v2( # pragma: no cover
161
+ session_maker: SessionMakerDep,
162
+ project_id: ProjectIdPathDep,
163
+ ) -> SearchRepository:
164
+ """Create a SearchRepository instance for v2 API."""
165
+ return create_search_repository(session_maker, project_id=project_id)
166
+
167
+
168
+ SearchRepositoryV2Dep = Annotated[SearchRepository, Depends(get_search_repository_v2)]
169
+
170
+
171
+ async def get_search_repository_v2_external(
172
+ session_maker: SessionMakerDep,
173
+ project_id: ProjectExternalIdPathDep,
174
+ ) -> SearchRepository:
175
+ """Create a SearchRepository instance for v2 API (uses external_id)."""
176
+ return create_search_repository(session_maker, project_id=project_id)
177
+
178
+
179
+ SearchRepositoryV2ExternalDep = Annotated[SearchRepository, Depends(get_search_repository_v2_external)]