basic-memory 0.17.1__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.
Files changed (171) hide show
  1. basic_memory/__init__.py +7 -0
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +185 -0
  4. basic_memory/alembic/migrations.py +24 -0
  5. basic_memory/alembic/script.py.mako +26 -0
  6. basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
  7. basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
  8. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  9. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
  10. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
  11. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  12. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  13. basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
  14. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  15. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
  16. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  17. basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
  18. basic_memory/api/__init__.py +5 -0
  19. basic_memory/api/app.py +131 -0
  20. basic_memory/api/routers/__init__.py +11 -0
  21. basic_memory/api/routers/directory_router.py +84 -0
  22. basic_memory/api/routers/importer_router.py +152 -0
  23. basic_memory/api/routers/knowledge_router.py +318 -0
  24. basic_memory/api/routers/management_router.py +80 -0
  25. basic_memory/api/routers/memory_router.py +90 -0
  26. basic_memory/api/routers/project_router.py +448 -0
  27. basic_memory/api/routers/prompt_router.py +260 -0
  28. basic_memory/api/routers/resource_router.py +249 -0
  29. basic_memory/api/routers/search_router.py +36 -0
  30. basic_memory/api/routers/utils.py +169 -0
  31. basic_memory/api/template_loader.py +292 -0
  32. basic_memory/api/v2/__init__.py +35 -0
  33. basic_memory/api/v2/routers/__init__.py +21 -0
  34. basic_memory/api/v2/routers/directory_router.py +93 -0
  35. basic_memory/api/v2/routers/importer_router.py +182 -0
  36. basic_memory/api/v2/routers/knowledge_router.py +413 -0
  37. basic_memory/api/v2/routers/memory_router.py +130 -0
  38. basic_memory/api/v2/routers/project_router.py +342 -0
  39. basic_memory/api/v2/routers/prompt_router.py +270 -0
  40. basic_memory/api/v2/routers/resource_router.py +286 -0
  41. basic_memory/api/v2/routers/search_router.py +73 -0
  42. basic_memory/cli/__init__.py +1 -0
  43. basic_memory/cli/app.py +84 -0
  44. basic_memory/cli/auth.py +277 -0
  45. basic_memory/cli/commands/__init__.py +18 -0
  46. basic_memory/cli/commands/cloud/__init__.py +6 -0
  47. basic_memory/cli/commands/cloud/api_client.py +112 -0
  48. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  49. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  50. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  51. basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
  52. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  53. basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
  54. basic_memory/cli/commands/cloud/upload.py +233 -0
  55. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  56. basic_memory/cli/commands/command_utils.py +77 -0
  57. basic_memory/cli/commands/db.py +44 -0
  58. basic_memory/cli/commands/format.py +198 -0
  59. basic_memory/cli/commands/import_chatgpt.py +84 -0
  60. basic_memory/cli/commands/import_claude_conversations.py +87 -0
  61. basic_memory/cli/commands/import_claude_projects.py +86 -0
  62. basic_memory/cli/commands/import_memory_json.py +87 -0
  63. basic_memory/cli/commands/mcp.py +76 -0
  64. basic_memory/cli/commands/project.py +889 -0
  65. basic_memory/cli/commands/status.py +174 -0
  66. basic_memory/cli/commands/telemetry.py +81 -0
  67. basic_memory/cli/commands/tool.py +341 -0
  68. basic_memory/cli/main.py +28 -0
  69. basic_memory/config.py +616 -0
  70. basic_memory/db.py +394 -0
  71. basic_memory/deps.py +705 -0
  72. basic_memory/file_utils.py +478 -0
  73. basic_memory/ignore_utils.py +297 -0
  74. basic_memory/importers/__init__.py +27 -0
  75. basic_memory/importers/base.py +79 -0
  76. basic_memory/importers/chatgpt_importer.py +232 -0
  77. basic_memory/importers/claude_conversations_importer.py +180 -0
  78. basic_memory/importers/claude_projects_importer.py +148 -0
  79. basic_memory/importers/memory_json_importer.py +108 -0
  80. basic_memory/importers/utils.py +61 -0
  81. basic_memory/markdown/__init__.py +21 -0
  82. basic_memory/markdown/entity_parser.py +279 -0
  83. basic_memory/markdown/markdown_processor.py +160 -0
  84. basic_memory/markdown/plugins.py +242 -0
  85. basic_memory/markdown/schemas.py +70 -0
  86. basic_memory/markdown/utils.py +117 -0
  87. basic_memory/mcp/__init__.py +1 -0
  88. basic_memory/mcp/async_client.py +139 -0
  89. basic_memory/mcp/project_context.py +141 -0
  90. basic_memory/mcp/prompts/__init__.py +19 -0
  91. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  92. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  93. basic_memory/mcp/prompts/recent_activity.py +188 -0
  94. basic_memory/mcp/prompts/search.py +57 -0
  95. basic_memory/mcp/prompts/utils.py +162 -0
  96. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  97. basic_memory/mcp/resources/project_info.py +71 -0
  98. basic_memory/mcp/server.py +81 -0
  99. basic_memory/mcp/tools/__init__.py +48 -0
  100. basic_memory/mcp/tools/build_context.py +120 -0
  101. basic_memory/mcp/tools/canvas.py +152 -0
  102. basic_memory/mcp/tools/chatgpt_tools.py +190 -0
  103. basic_memory/mcp/tools/delete_note.py +242 -0
  104. basic_memory/mcp/tools/edit_note.py +324 -0
  105. basic_memory/mcp/tools/list_directory.py +168 -0
  106. basic_memory/mcp/tools/move_note.py +551 -0
  107. basic_memory/mcp/tools/project_management.py +201 -0
  108. basic_memory/mcp/tools/read_content.py +281 -0
  109. basic_memory/mcp/tools/read_note.py +267 -0
  110. basic_memory/mcp/tools/recent_activity.py +534 -0
  111. basic_memory/mcp/tools/search.py +385 -0
  112. basic_memory/mcp/tools/utils.py +540 -0
  113. basic_memory/mcp/tools/view_note.py +78 -0
  114. basic_memory/mcp/tools/write_note.py +230 -0
  115. basic_memory/models/__init__.py +15 -0
  116. basic_memory/models/base.py +10 -0
  117. basic_memory/models/knowledge.py +226 -0
  118. basic_memory/models/project.py +87 -0
  119. basic_memory/models/search.py +85 -0
  120. basic_memory/repository/__init__.py +11 -0
  121. basic_memory/repository/entity_repository.py +503 -0
  122. basic_memory/repository/observation_repository.py +73 -0
  123. basic_memory/repository/postgres_search_repository.py +379 -0
  124. basic_memory/repository/project_info_repository.py +10 -0
  125. basic_memory/repository/project_repository.py +128 -0
  126. basic_memory/repository/relation_repository.py +146 -0
  127. basic_memory/repository/repository.py +385 -0
  128. basic_memory/repository/search_index_row.py +95 -0
  129. basic_memory/repository/search_repository.py +94 -0
  130. basic_memory/repository/search_repository_base.py +241 -0
  131. basic_memory/repository/sqlite_search_repository.py +439 -0
  132. basic_memory/schemas/__init__.py +86 -0
  133. basic_memory/schemas/base.py +297 -0
  134. basic_memory/schemas/cloud.py +50 -0
  135. basic_memory/schemas/delete.py +37 -0
  136. basic_memory/schemas/directory.py +30 -0
  137. basic_memory/schemas/importer.py +35 -0
  138. basic_memory/schemas/memory.py +285 -0
  139. basic_memory/schemas/project_info.py +212 -0
  140. basic_memory/schemas/prompt.py +90 -0
  141. basic_memory/schemas/request.py +112 -0
  142. basic_memory/schemas/response.py +229 -0
  143. basic_memory/schemas/search.py +117 -0
  144. basic_memory/schemas/sync_report.py +72 -0
  145. basic_memory/schemas/v2/__init__.py +27 -0
  146. basic_memory/schemas/v2/entity.py +129 -0
  147. basic_memory/schemas/v2/resource.py +46 -0
  148. basic_memory/services/__init__.py +8 -0
  149. basic_memory/services/context_service.py +601 -0
  150. basic_memory/services/directory_service.py +308 -0
  151. basic_memory/services/entity_service.py +864 -0
  152. basic_memory/services/exceptions.py +37 -0
  153. basic_memory/services/file_service.py +541 -0
  154. basic_memory/services/initialization.py +216 -0
  155. basic_memory/services/link_resolver.py +121 -0
  156. basic_memory/services/project_service.py +880 -0
  157. basic_memory/services/search_service.py +404 -0
  158. basic_memory/services/service.py +15 -0
  159. basic_memory/sync/__init__.py +6 -0
  160. basic_memory/sync/background_sync.py +26 -0
  161. basic_memory/sync/sync_service.py +1259 -0
  162. basic_memory/sync/watch_service.py +510 -0
  163. basic_memory/telemetry.py +249 -0
  164. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  165. basic_memory/templates/prompts/search.hbs +101 -0
  166. basic_memory/utils.py +468 -0
  167. basic_memory-0.17.1.dist-info/METADATA +617 -0
  168. basic_memory-0.17.1.dist-info/RECORD +171 -0
  169. basic_memory-0.17.1.dist-info/WHEEL +4 -0
  170. basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
  171. basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,448 @@
1
+ """Router for project management."""
2
+
3
+ import os
4
+ from fastapi import APIRouter, HTTPException, Path, Body, BackgroundTasks, Response, Query
5
+ from typing import Optional
6
+ from loguru import logger
7
+
8
+ from basic_memory.deps import (
9
+ ProjectConfigDep,
10
+ ProjectServiceDep,
11
+ ProjectPathDep,
12
+ SyncServiceDep,
13
+ )
14
+ from basic_memory.schemas import ProjectInfoResponse, SyncReportResponse
15
+ from basic_memory.schemas.project_info import (
16
+ ProjectList,
17
+ ProjectItem,
18
+ ProjectInfoRequest,
19
+ ProjectStatusResponse,
20
+ )
21
+ from basic_memory.utils import normalize_project_path
22
+
23
+ # Router for resources in a specific project
24
+ # The ProjectPathDep is used in the path as a prefix, so the request path is like /{project}/project/info
25
+ project_router = APIRouter(prefix="/project", tags=["project"])
26
+
27
+ # Router for managing project resources
28
+ project_resource_router = APIRouter(prefix="/projects", tags=["project_management"])
29
+
30
+
31
+ @project_router.get("/info", response_model=ProjectInfoResponse)
32
+ async def get_project_info(
33
+ project_service: ProjectServiceDep,
34
+ project: ProjectPathDep,
35
+ ) -> ProjectInfoResponse:
36
+ """Get comprehensive information about the specified Basic Memory project."""
37
+ return await project_service.get_project_info(project)
38
+
39
+
40
+ @project_router.get("/item", response_model=ProjectItem)
41
+ async def get_project(
42
+ project_service: ProjectServiceDep,
43
+ project: ProjectPathDep,
44
+ ) -> ProjectItem:
45
+ """Get bassic info about the specified Basic Memory project."""
46
+ found_project = await project_service.get_project(project)
47
+ if not found_project:
48
+ raise HTTPException(
49
+ status_code=404, detail=f"Project: '{project}' does not exist"
50
+ ) # pragma: no cover
51
+
52
+ return ProjectItem(
53
+ id=found_project.id,
54
+ name=found_project.name,
55
+ path=normalize_project_path(found_project.path),
56
+ is_default=found_project.is_default or False,
57
+ )
58
+
59
+
60
+ # Update a project
61
+ @project_router.patch("/{name}", response_model=ProjectStatusResponse)
62
+ async def update_project(
63
+ project_service: ProjectServiceDep,
64
+ name: str = Path(..., description="Name of the project to update"),
65
+ path: Optional[str] = Body(None, description="New absolute path for the project"),
66
+ is_active: Optional[bool] = Body(None, description="Status of the project (active/inactive)"),
67
+ ) -> ProjectStatusResponse:
68
+ """Update a project's information in configuration and database.
69
+
70
+ Args:
71
+ name: The name of the project to update
72
+ path: Optional new absolute path for the project
73
+ is_active: Optional status update for the project
74
+
75
+ Returns:
76
+ Response confirming the project was updated
77
+ """
78
+ try:
79
+ # Validate that path is absolute if provided
80
+ if path and not os.path.isabs(path):
81
+ raise HTTPException(status_code=400, detail="Path must be absolute")
82
+
83
+ # Get original project info for the response
84
+ old_project = await project_service.get_project(name)
85
+ if not old_project:
86
+ raise HTTPException(
87
+ status_code=400, detail=f"Project '{name}' not found in configuration"
88
+ )
89
+
90
+ old_project_info = ProjectItem(
91
+ id=old_project.id,
92
+ name=old_project.name,
93
+ path=old_project.path,
94
+ is_default=old_project.is_default or False,
95
+ )
96
+
97
+ if path:
98
+ await project_service.move_project(name, path)
99
+ elif is_active is not None:
100
+ await project_service.update_project(name, is_active=is_active)
101
+
102
+ # Get updated project info
103
+ updated_project = await project_service.get_project(name)
104
+ if not updated_project:
105
+ raise HTTPException(status_code=404, detail=f"Project '{name}' not found after update")
106
+
107
+ return ProjectStatusResponse(
108
+ message=f"Project '{name}' updated successfully",
109
+ status="success",
110
+ default=(name == project_service.default_project),
111
+ old_project=old_project_info,
112
+ new_project=ProjectItem(
113
+ id=updated_project.id,
114
+ name=updated_project.name,
115
+ path=updated_project.path,
116
+ is_default=updated_project.is_default or False,
117
+ ),
118
+ )
119
+ except ValueError as e:
120
+ raise HTTPException(status_code=400, detail=str(e))
121
+
122
+
123
+ # Sync project filesystem
124
+ @project_router.post("/sync")
125
+ async def sync_project(
126
+ background_tasks: BackgroundTasks,
127
+ sync_service: SyncServiceDep,
128
+ project_config: ProjectConfigDep,
129
+ force_full: bool = Query(
130
+ False, description="Force full scan, bypassing watermark optimization"
131
+ ),
132
+ run_in_background: bool = Query(True, description="Run in background"),
133
+ ):
134
+ """Force project filesystem sync to database.
135
+
136
+ Scans the project directory and updates the database with any new or modified files.
137
+
138
+ Args:
139
+ background_tasks: FastAPI background tasks
140
+ sync_service: Sync service for this project
141
+ project_config: Project configuration
142
+ force_full: If True, force a full scan even if watermark exists
143
+ run_in_background: If True, run sync in background and return immediately
144
+
145
+ Returns:
146
+ Response confirming sync was initiated (background) or SyncReportResponse (foreground)
147
+ """
148
+ if run_in_background:
149
+ background_tasks.add_task(
150
+ sync_service.sync, project_config.home, project_config.name, force_full=force_full
151
+ )
152
+ logger.info(
153
+ f"Filesystem sync initiated for project: {project_config.name} (force_full={force_full})"
154
+ )
155
+
156
+ return {
157
+ "status": "sync_started",
158
+ "message": f"Filesystem sync initiated for project '{project_config.name}'",
159
+ }
160
+ else:
161
+ report = await sync_service.sync(
162
+ project_config.home, project_config.name, force_full=force_full
163
+ )
164
+ logger.info(
165
+ f"Filesystem sync completed for project: {project_config.name} (force_full={force_full})"
166
+ )
167
+ return SyncReportResponse.from_sync_report(report)
168
+
169
+
170
+ @project_router.post("/status", response_model=SyncReportResponse)
171
+ async def project_sync_status(
172
+ sync_service: SyncServiceDep,
173
+ project_config: ProjectConfigDep,
174
+ ) -> SyncReportResponse:
175
+ """Scan directory for changes compared to database state.
176
+
177
+ Args:
178
+ sync_service: Sync service for this project
179
+ project_config: Project configuration
180
+
181
+ Returns:
182
+ Scan report with details on files that need syncing
183
+ """
184
+ logger.info(f"Scanning filesystem for project: {project_config.name}")
185
+ sync_report = await sync_service.scan(project_config.home)
186
+
187
+ return SyncReportResponse.from_sync_report(sync_report)
188
+
189
+
190
+ # List all available projects
191
+ @project_resource_router.get("/projects", response_model=ProjectList)
192
+ async def list_projects(
193
+ project_service: ProjectServiceDep,
194
+ ) -> ProjectList:
195
+ """List all configured projects.
196
+
197
+ Returns:
198
+ A list of all projects with metadata
199
+ """
200
+ projects = await project_service.list_projects()
201
+ default_project = project_service.default_project
202
+
203
+ project_items = [
204
+ ProjectItem(
205
+ id=project.id,
206
+ name=project.name,
207
+ path=normalize_project_path(project.path),
208
+ is_default=project.is_default or False,
209
+ )
210
+ for project in projects
211
+ ]
212
+
213
+ return ProjectList(
214
+ projects=project_items,
215
+ default_project=default_project,
216
+ )
217
+
218
+
219
+ # Add a new project
220
+ @project_resource_router.post("/projects", response_model=ProjectStatusResponse, status_code=201)
221
+ async def add_project(
222
+ response: Response,
223
+ project_data: ProjectInfoRequest,
224
+ project_service: ProjectServiceDep,
225
+ ) -> ProjectStatusResponse:
226
+ """Add a new project to configuration and database.
227
+
228
+ Args:
229
+ project_data: The project name and path, with option to set as default
230
+
231
+ Returns:
232
+ Response confirming the project was added
233
+ """
234
+ # Check if project already exists before attempting to add
235
+ existing_project = await project_service.get_project(project_data.name)
236
+ if existing_project:
237
+ # Project exists - check if paths match for true idempotency
238
+ # Normalize paths for comparison (resolve symlinks, etc.)
239
+ from pathlib import Path
240
+
241
+ requested_path = Path(project_data.path).resolve()
242
+ existing_path = Path(existing_project.path).resolve()
243
+
244
+ if requested_path == existing_path:
245
+ # Same name, same path - return 200 OK (idempotent)
246
+ response.status_code = 200
247
+ return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
248
+ message=f"Project '{project_data.name}' already exists",
249
+ status="success",
250
+ default=existing_project.is_default or False,
251
+ new_project=ProjectItem(
252
+ id=existing_project.id,
253
+ name=existing_project.name,
254
+ path=existing_project.path,
255
+ is_default=existing_project.is_default or False,
256
+ ),
257
+ )
258
+ else:
259
+ # Same name, different path - this is an error
260
+ raise HTTPException(
261
+ status_code=400,
262
+ detail=f"Project '{project_data.name}' already exists with different path. Existing: {existing_project.path}, Requested: {project_data.path}",
263
+ )
264
+
265
+ try: # pragma: no cover
266
+ # The service layer now handles cloud mode validation and path sanitization
267
+ await project_service.add_project(
268
+ project_data.name, project_data.path, set_default=project_data.set_default
269
+ )
270
+
271
+ # Fetch the newly created project to get its ID
272
+ new_project = await project_service.get_project(project_data.name)
273
+ if not new_project:
274
+ raise HTTPException(status_code=500, detail="Failed to retrieve newly created project")
275
+
276
+ return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
277
+ message=f"Project '{new_project.name}' added successfully",
278
+ status="success",
279
+ default=project_data.set_default,
280
+ new_project=ProjectItem(
281
+ id=new_project.id,
282
+ name=new_project.name,
283
+ path=new_project.path,
284
+ is_default=new_project.is_default or False,
285
+ ),
286
+ )
287
+ except ValueError as e: # pragma: no cover
288
+ raise HTTPException(status_code=400, detail=str(e))
289
+
290
+
291
+ # Remove a project
292
+ @project_resource_router.delete("/{name}", response_model=ProjectStatusResponse)
293
+ async def remove_project(
294
+ project_service: ProjectServiceDep,
295
+ name: str = Path(..., description="Name of the project to remove"),
296
+ delete_notes: bool = Query(
297
+ False, description="If True, delete project directory from filesystem"
298
+ ),
299
+ ) -> ProjectStatusResponse:
300
+ """Remove a project from configuration and database.
301
+
302
+ Args:
303
+ name: The name of the project to remove
304
+ delete_notes: If True, delete the project directory from the filesystem
305
+
306
+ Returns:
307
+ Response confirming the project was removed
308
+ """
309
+ try:
310
+ old_project = await project_service.get_project(name)
311
+ if not old_project: # pragma: no cover
312
+ raise HTTPException(
313
+ status_code=404, detail=f"Project: '{name}' does not exist"
314
+ ) # pragma: no cover
315
+
316
+ # Check if trying to delete the default project
317
+ if name == project_service.default_project:
318
+ available_projects = await project_service.list_projects()
319
+ other_projects = [p.name for p in available_projects if p.name != name]
320
+ detail = f"Cannot delete default project '{name}'. "
321
+ if other_projects:
322
+ detail += (
323
+ f"Set another project as default first. Available: {', '.join(other_projects)}"
324
+ )
325
+ else:
326
+ detail += "This is the only project in your configuration."
327
+ raise HTTPException(status_code=400, detail=detail)
328
+
329
+ await project_service.remove_project(name, delete_notes=delete_notes)
330
+
331
+ return ProjectStatusResponse(
332
+ message=f"Project '{old_project.name}' removed successfully",
333
+ status="success",
334
+ default=False,
335
+ old_project=ProjectItem(
336
+ id=old_project.id,
337
+ name=old_project.name,
338
+ path=old_project.path,
339
+ is_default=old_project.is_default or False,
340
+ ),
341
+ new_project=None,
342
+ )
343
+ except ValueError as e: # pragma: no cover
344
+ raise HTTPException(status_code=400, detail=str(e))
345
+
346
+
347
+ # Set a project as default
348
+ @project_resource_router.put("/{name}/default", response_model=ProjectStatusResponse)
349
+ async def set_default_project(
350
+ project_service: ProjectServiceDep,
351
+ name: str = Path(..., description="Name of the project to set as default"),
352
+ ) -> ProjectStatusResponse:
353
+ """Set a project as the default project.
354
+
355
+ Args:
356
+ name: The name of the project to set as default
357
+
358
+ Returns:
359
+ Response confirming the project was set as default
360
+ """
361
+ try:
362
+ # Get the old default project
363
+ default_name = project_service.default_project
364
+ default_project = await project_service.get_project(default_name)
365
+ if not default_project: # pragma: no cover
366
+ raise HTTPException( # pragma: no cover
367
+ status_code=404, detail=f"Default Project: '{default_name}' does not exist"
368
+ )
369
+
370
+ # get the new project
371
+ new_default_project = await project_service.get_project(name)
372
+ if not new_default_project: # pragma: no cover
373
+ raise HTTPException(
374
+ status_code=404, detail=f"Project: '{name}' does not exist"
375
+ ) # pragma: no cover
376
+
377
+ await project_service.set_default_project(name)
378
+
379
+ return ProjectStatusResponse(
380
+ message=f"Project '{name}' set as default successfully",
381
+ status="success",
382
+ default=True,
383
+ old_project=ProjectItem(
384
+ id=default_project.id,
385
+ name=default_name,
386
+ path=default_project.path,
387
+ is_default=False,
388
+ ),
389
+ new_project=ProjectItem(
390
+ id=new_default_project.id,
391
+ name=name,
392
+ path=new_default_project.path,
393
+ is_default=True,
394
+ ),
395
+ )
396
+ except ValueError as e: # pragma: no cover
397
+ raise HTTPException(status_code=400, detail=str(e))
398
+
399
+
400
+ # Get the default project
401
+ @project_resource_router.get("/default", response_model=ProjectItem)
402
+ async def get_default_project(
403
+ project_service: ProjectServiceDep,
404
+ ) -> ProjectItem:
405
+ """Get the default project.
406
+
407
+ Returns:
408
+ Response with project default information
409
+ """
410
+ # Get the old default project
411
+ default_name = project_service.default_project
412
+ default_project = await project_service.get_project(default_name)
413
+ if not default_project: # pragma: no cover
414
+ raise HTTPException( # pragma: no cover
415
+ status_code=404, detail=f"Default Project: '{default_name}' does not exist"
416
+ )
417
+
418
+ return ProjectItem(
419
+ id=default_project.id,
420
+ name=default_project.name,
421
+ path=default_project.path,
422
+ is_default=True,
423
+ )
424
+
425
+
426
+ # Synchronize projects between config and database
427
+ @project_resource_router.post("/config/sync", response_model=ProjectStatusResponse)
428
+ async def synchronize_projects(
429
+ project_service: ProjectServiceDep,
430
+ ) -> ProjectStatusResponse:
431
+ """Synchronize projects between configuration file and database.
432
+
433
+ Ensures that all projects in the configuration file exist in the database
434
+ and vice versa.
435
+
436
+ Returns:
437
+ Response confirming synchronization was completed
438
+ """
439
+ try: # pragma: no cover
440
+ await project_service.synchronize_projects()
441
+
442
+ return ProjectStatusResponse( # pyright: ignore [reportCallIssue]
443
+ message="Projects synchronized successfully between configuration and database",
444
+ status="success",
445
+ default=False,
446
+ )
447
+ except ValueError as e: # pragma: no cover
448
+ raise HTTPException(status_code=400, detail=str(e))