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.
- basic_memory/__init__.py +7 -0
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +185 -0
- basic_memory/alembic/migrations.py +24 -0
- basic_memory/alembic/script.py.mako +26 -0
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/3dae7c7b1564_initial_schema.py +93 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/api/__init__.py +5 -0
- basic_memory/api/app.py +131 -0
- basic_memory/api/routers/__init__.py +11 -0
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +318 -0
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +90 -0
- basic_memory/api/routers/project_router.py +448 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +249 -0
- basic_memory/api/routers/search_router.py +36 -0
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +182 -0
- basic_memory/api/v2/routers/knowledge_router.py +413 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +342 -0
- basic_memory/api/v2/routers/prompt_router.py +270 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/__init__.py +1 -0
- basic_memory/cli/app.py +84 -0
- basic_memory/cli/auth.py +277 -0
- basic_memory/cli/commands/__init__.py +18 -0
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +112 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +371 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +233 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +77 -0
- basic_memory/cli/commands/db.py +44 -0
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +84 -0
- basic_memory/cli/commands/import_claude_conversations.py +87 -0
- basic_memory/cli/commands/import_claude_projects.py +86 -0
- basic_memory/cli/commands/import_memory_json.py +87 -0
- basic_memory/cli/commands/mcp.py +76 -0
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +174 -0
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/main.py +28 -0
- basic_memory/config.py +616 -0
- basic_memory/db.py +394 -0
- basic_memory/deps.py +705 -0
- basic_memory/file_utils.py +478 -0
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +79 -0
- basic_memory/importers/chatgpt_importer.py +232 -0
- basic_memory/importers/claude_conversations_importer.py +180 -0
- basic_memory/importers/claude_projects_importer.py +148 -0
- basic_memory/importers/memory_json_importer.py +108 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/__init__.py +21 -0
- basic_memory/markdown/entity_parser.py +279 -0
- basic_memory/markdown/markdown_processor.py +160 -0
- basic_memory/markdown/plugins.py +242 -0
- basic_memory/markdown/schemas.py +70 -0
- basic_memory/markdown/utils.py +117 -0
- basic_memory/mcp/__init__.py +1 -0
- basic_memory/mcp/async_client.py +139 -0
- basic_memory/mcp/project_context.py +141 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +81 -0
- basic_memory/mcp/tools/__init__.py +48 -0
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +242 -0
- basic_memory/mcp/tools/edit_note.py +324 -0
- basic_memory/mcp/tools/list_directory.py +168 -0
- basic_memory/mcp/tools/move_note.py +551 -0
- basic_memory/mcp/tools/project_management.py +201 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +267 -0
- basic_memory/mcp/tools/recent_activity.py +534 -0
- basic_memory/mcp/tools/search.py +385 -0
- basic_memory/mcp/tools/utils.py +540 -0
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +15 -0
- basic_memory/models/base.py +10 -0
- basic_memory/models/knowledge.py +226 -0
- basic_memory/models/project.py +87 -0
- basic_memory/models/search.py +85 -0
- basic_memory/repository/__init__.py +11 -0
- basic_memory/repository/entity_repository.py +503 -0
- basic_memory/repository/observation_repository.py +73 -0
- basic_memory/repository/postgres_search_repository.py +379 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +128 -0
- basic_memory/repository/relation_repository.py +146 -0
- basic_memory/repository/repository.py +385 -0
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +94 -0
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +439 -0
- basic_memory/schemas/__init__.py +86 -0
- basic_memory/schemas/base.py +297 -0
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/delete.py +37 -0
- basic_memory/schemas/directory.py +30 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +285 -0
- basic_memory/schemas/project_info.py +212 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +112 -0
- basic_memory/schemas/response.py +229 -0
- basic_memory/schemas/search.py +117 -0
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +129 -0
- basic_memory/schemas/v2/resource.py +46 -0
- basic_memory/services/__init__.py +8 -0
- basic_memory/services/context_service.py +601 -0
- basic_memory/services/directory_service.py +308 -0
- basic_memory/services/entity_service.py +864 -0
- basic_memory/services/exceptions.py +37 -0
- basic_memory/services/file_service.py +541 -0
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +121 -0
- basic_memory/services/project_service.py +880 -0
- basic_memory/services/search_service.py +404 -0
- basic_memory/services/service.py +15 -0
- basic_memory/sync/__init__.py +6 -0
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/sync_service.py +1259 -0
- basic_memory/sync/watch_service.py +510 -0
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +468 -0
- basic_memory-0.17.1.dist-info/METADATA +617 -0
- basic_memory-0.17.1.dist-info/RECORD +171 -0
- basic_memory-0.17.1.dist-info/WHEEL +4 -0
- basic_memory-0.17.1.dist-info/entry_points.txt +3 -0
- basic_memory-0.17.1.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""V2 routes for memory:// URI operations.
|
|
2
|
+
|
|
3
|
+
This router uses integer project IDs for stable, efficient routing.
|
|
4
|
+
V1 uses string-based project names which are less efficient and less stable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Annotated, Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, Query
|
|
10
|
+
from loguru import logger
|
|
11
|
+
|
|
12
|
+
from basic_memory.deps import ContextServiceV2Dep, EntityRepositoryV2Dep, ProjectIdPathDep
|
|
13
|
+
from basic_memory.schemas.base import TimeFrame, parse_timeframe
|
|
14
|
+
from basic_memory.schemas.memory import (
|
|
15
|
+
GraphContext,
|
|
16
|
+
normalize_memory_url,
|
|
17
|
+
)
|
|
18
|
+
from basic_memory.schemas.search import SearchItemType
|
|
19
|
+
from basic_memory.api.routers.utils import to_graph_context
|
|
20
|
+
|
|
21
|
+
# Note: No prefix here - it's added during registration as /v2/{project_id}/memory
|
|
22
|
+
router = APIRouter(tags=["memory"])
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@router.get("/memory/recent", response_model=GraphContext)
|
|
26
|
+
async def recent(
|
|
27
|
+
project_id: ProjectIdPathDep,
|
|
28
|
+
context_service: ContextServiceV2Dep,
|
|
29
|
+
entity_repository: EntityRepositoryV2Dep,
|
|
30
|
+
type: Annotated[list[SearchItemType] | None, Query()] = None,
|
|
31
|
+
depth: int = 1,
|
|
32
|
+
timeframe: TimeFrame = "7d",
|
|
33
|
+
page: int = 1,
|
|
34
|
+
page_size: int = 10,
|
|
35
|
+
max_related: int = 10,
|
|
36
|
+
) -> GraphContext:
|
|
37
|
+
"""Get recent activity context for a project.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
project_id: Validated numeric project ID from URL path
|
|
41
|
+
context_service: Context service scoped to project
|
|
42
|
+
entity_repository: Entity repository scoped to project
|
|
43
|
+
type: Types of items to include (entities, relations, observations)
|
|
44
|
+
depth: How many levels of related entities to include
|
|
45
|
+
timeframe: Time window for recent activity (e.g., "7d", "1 week")
|
|
46
|
+
page: Page number for pagination
|
|
47
|
+
page_size: Number of items per page
|
|
48
|
+
max_related: Maximum related entities to include per item
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
GraphContext with recent activity and related entities
|
|
52
|
+
"""
|
|
53
|
+
# return all types by default
|
|
54
|
+
types = (
|
|
55
|
+
[SearchItemType.ENTITY, SearchItemType.RELATION, SearchItemType.OBSERVATION]
|
|
56
|
+
if not type
|
|
57
|
+
else type
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logger.debug(
|
|
61
|
+
f"V2 Getting recent context for project {project_id}: `{types}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
|
|
62
|
+
)
|
|
63
|
+
# Parse timeframe
|
|
64
|
+
since = parse_timeframe(timeframe)
|
|
65
|
+
limit = page_size
|
|
66
|
+
offset = (page - 1) * page_size
|
|
67
|
+
|
|
68
|
+
# Build context
|
|
69
|
+
context = await context_service.build_context(
|
|
70
|
+
types=types, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
|
|
71
|
+
)
|
|
72
|
+
recent_context = await to_graph_context(
|
|
73
|
+
context, entity_repository=entity_repository, page=page, page_size=page_size
|
|
74
|
+
)
|
|
75
|
+
logger.debug(f"V2 Recent context: {recent_context.model_dump_json()}")
|
|
76
|
+
return recent_context
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# get_memory_context needs to be declared last so other paths can match
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@router.get("/memory/{uri:path}", response_model=GraphContext)
|
|
83
|
+
async def get_memory_context(
|
|
84
|
+
project_id: ProjectIdPathDep,
|
|
85
|
+
context_service: ContextServiceV2Dep,
|
|
86
|
+
entity_repository: EntityRepositoryV2Dep,
|
|
87
|
+
uri: str,
|
|
88
|
+
depth: int = 1,
|
|
89
|
+
timeframe: Optional[TimeFrame] = None,
|
|
90
|
+
page: int = 1,
|
|
91
|
+
page_size: int = 10,
|
|
92
|
+
max_related: int = 10,
|
|
93
|
+
) -> GraphContext:
|
|
94
|
+
"""Get rich context from memory:// URI.
|
|
95
|
+
|
|
96
|
+
V2 supports both legacy path-based URIs and new ID-based URIs:
|
|
97
|
+
- Legacy: memory://path/to/note
|
|
98
|
+
- ID-based: memory://id/123 or memory://123
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
project_id: Validated numeric project ID from URL path
|
|
102
|
+
context_service: Context service scoped to project
|
|
103
|
+
entity_repository: Entity repository scoped to project
|
|
104
|
+
uri: Memory URI path (e.g., "id/123", "123", or "path/to/note")
|
|
105
|
+
depth: How many levels of related entities to include
|
|
106
|
+
timeframe: Optional time window for filtering related content
|
|
107
|
+
page: Page number for pagination
|
|
108
|
+
page_size: Number of items per page
|
|
109
|
+
max_related: Maximum related entities to include
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
GraphContext with the entity and its related context
|
|
113
|
+
"""
|
|
114
|
+
logger.debug(
|
|
115
|
+
f"V2 Getting context for project {project_id}, URI: `{uri}` depth: `{depth}` timeframe: `{timeframe}` page: `{page}` page_size: `{page_size}` max_related: `{max_related}`"
|
|
116
|
+
)
|
|
117
|
+
memory_url = normalize_memory_url(uri)
|
|
118
|
+
|
|
119
|
+
# Parse timeframe
|
|
120
|
+
since = parse_timeframe(timeframe) if timeframe else None
|
|
121
|
+
limit = page_size
|
|
122
|
+
offset = (page - 1) * page_size
|
|
123
|
+
|
|
124
|
+
# Build context
|
|
125
|
+
context = await context_service.build_context(
|
|
126
|
+
memory_url, depth=depth, since=since, limit=limit, offset=offset, max_related=max_related
|
|
127
|
+
)
|
|
128
|
+
return await to_graph_context(
|
|
129
|
+
context, entity_repository=entity_repository, page=page, page_size=page_size
|
|
130
|
+
)
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""V2 Project Router - ID-based project management operations.
|
|
2
|
+
|
|
3
|
+
This router provides ID-based CRUD operations for projects, replacing the
|
|
4
|
+
name-based identifiers used in v1 with direct integer ID lookups.
|
|
5
|
+
|
|
6
|
+
Key improvements:
|
|
7
|
+
- Direct database lookups via integer primary keys
|
|
8
|
+
- Stable references that don't change with project renames
|
|
9
|
+
- Better performance through indexed queries
|
|
10
|
+
- Consistent with v2 entity operations
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from fastapi import APIRouter, HTTPException, Body, Query
|
|
17
|
+
from loguru import logger
|
|
18
|
+
|
|
19
|
+
from basic_memory.deps import (
|
|
20
|
+
ProjectServiceDep,
|
|
21
|
+
ProjectRepositoryDep,
|
|
22
|
+
ProjectIdPathDep,
|
|
23
|
+
)
|
|
24
|
+
from basic_memory.schemas.project_info import (
|
|
25
|
+
ProjectItem,
|
|
26
|
+
ProjectStatusResponse,
|
|
27
|
+
)
|
|
28
|
+
from basic_memory.schemas.v2 import ProjectResolveRequest, ProjectResolveResponse
|
|
29
|
+
from basic_memory.utils import normalize_project_path, generate_permalink
|
|
30
|
+
|
|
31
|
+
router = APIRouter(prefix="/projects", tags=["project_management-v2"])
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.post("/resolve", response_model=ProjectResolveResponse)
|
|
35
|
+
async def resolve_project_identifier(
|
|
36
|
+
data: ProjectResolveRequest,
|
|
37
|
+
project_repository: ProjectRepositoryDep,
|
|
38
|
+
) -> ProjectResolveResponse:
|
|
39
|
+
"""Resolve a project identifier (name or permalink) to a project ID.
|
|
40
|
+
|
|
41
|
+
This endpoint provides efficient lookup of projects by name without
|
|
42
|
+
needing to fetch the entire project list. Supports case-insensitive
|
|
43
|
+
matching on both name and permalink.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
data: Request containing the identifier to resolve
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Project information including the numeric ID
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
HTTPException: 404 if project not found
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
POST /v2/projects/resolve
|
|
56
|
+
{"identifier": "my-project"}
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
{
|
|
60
|
+
"project_id": 1,
|
|
61
|
+
"name": "my-project",
|
|
62
|
+
"permalink": "my-project",
|
|
63
|
+
"path": "/path/to/project",
|
|
64
|
+
"is_active": true,
|
|
65
|
+
"is_default": false,
|
|
66
|
+
"resolution_method": "name"
|
|
67
|
+
}
|
|
68
|
+
"""
|
|
69
|
+
logger.info(f"API v2 request: resolve_project_identifier for '{data.identifier}'")
|
|
70
|
+
|
|
71
|
+
# Generate permalink for comparison
|
|
72
|
+
identifier_permalink = generate_permalink(data.identifier)
|
|
73
|
+
|
|
74
|
+
# Try to find project by ID first (if identifier is numeric)
|
|
75
|
+
resolution_method = "name"
|
|
76
|
+
project = None
|
|
77
|
+
|
|
78
|
+
if data.identifier.isdigit():
|
|
79
|
+
project_id = int(data.identifier)
|
|
80
|
+
project = await project_repository.get_by_id(project_id)
|
|
81
|
+
if project:
|
|
82
|
+
resolution_method = "id"
|
|
83
|
+
|
|
84
|
+
# If not found by ID, try by permalink first (exact match)
|
|
85
|
+
if not project:
|
|
86
|
+
project = await project_repository.get_by_permalink(identifier_permalink)
|
|
87
|
+
if project:
|
|
88
|
+
resolution_method = "permalink"
|
|
89
|
+
|
|
90
|
+
# If not found by permalink, try case-insensitive name search
|
|
91
|
+
# Uses efficient database query instead of fetching all projects
|
|
92
|
+
if not project:
|
|
93
|
+
project = await project_repository.get_by_name_case_insensitive(data.identifier)
|
|
94
|
+
if project:
|
|
95
|
+
resolution_method = "name"
|
|
96
|
+
|
|
97
|
+
if not project:
|
|
98
|
+
raise HTTPException(status_code=404, detail=f"Project not found: '{data.identifier}'")
|
|
99
|
+
|
|
100
|
+
return ProjectResolveResponse(
|
|
101
|
+
project_id=project.id,
|
|
102
|
+
name=project.name,
|
|
103
|
+
permalink=generate_permalink(project.name),
|
|
104
|
+
path=normalize_project_path(project.path),
|
|
105
|
+
is_active=project.is_active if hasattr(project, "is_active") else True,
|
|
106
|
+
is_default=project.is_default or False,
|
|
107
|
+
resolution_method=resolution_method,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@router.get("/{project_id}", response_model=ProjectItem)
|
|
112
|
+
async def get_project_by_id(
|
|
113
|
+
project_id: ProjectIdPathDep,
|
|
114
|
+
project_repository: ProjectRepositoryDep,
|
|
115
|
+
) -> ProjectItem:
|
|
116
|
+
"""Get project by its numeric ID.
|
|
117
|
+
|
|
118
|
+
This is the primary project retrieval method in v2, using direct database
|
|
119
|
+
lookups for maximum performance.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
project_id: Numeric project ID
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Project information
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
HTTPException: 404 if project not found
|
|
129
|
+
|
|
130
|
+
Example:
|
|
131
|
+
GET /v2/projects/3
|
|
132
|
+
"""
|
|
133
|
+
logger.info(f"API v2 request: get_project_by_id for project_id={project_id}")
|
|
134
|
+
|
|
135
|
+
project = await project_repository.get_by_id(project_id)
|
|
136
|
+
if not project:
|
|
137
|
+
raise HTTPException(status_code=404, detail=f"Project with ID {project_id} not found")
|
|
138
|
+
|
|
139
|
+
return ProjectItem(
|
|
140
|
+
id=project.id,
|
|
141
|
+
name=project.name,
|
|
142
|
+
path=normalize_project_path(project.path),
|
|
143
|
+
is_default=project.is_default or False,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@router.patch("/{project_id}", response_model=ProjectStatusResponse)
|
|
148
|
+
async def update_project_by_id(
|
|
149
|
+
project_id: ProjectIdPathDep,
|
|
150
|
+
project_service: ProjectServiceDep,
|
|
151
|
+
project_repository: ProjectRepositoryDep,
|
|
152
|
+
path: Optional[str] = Body(None, description="New absolute path for the project"),
|
|
153
|
+
is_active: Optional[bool] = Body(None, description="Status of the project (active/inactive)"),
|
|
154
|
+
) -> ProjectStatusResponse:
|
|
155
|
+
"""Update a project's information by ID.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
project_id: Numeric project ID
|
|
159
|
+
path: Optional new absolute path for the project
|
|
160
|
+
is_active: Optional status update for the project
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
Response confirming the project was updated
|
|
164
|
+
|
|
165
|
+
Raises:
|
|
166
|
+
HTTPException: 400 if validation fails, 404 if project not found
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
PATCH /v2/projects/3
|
|
170
|
+
{"path": "/new/path"}
|
|
171
|
+
"""
|
|
172
|
+
logger.info(f"API v2 request: update_project_by_id for project_id={project_id}")
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
# Validate that path is absolute if provided
|
|
176
|
+
if path and not os.path.isabs(path):
|
|
177
|
+
raise HTTPException(status_code=400, detail="Path must be absolute")
|
|
178
|
+
|
|
179
|
+
# Get original project info for the response
|
|
180
|
+
old_project = await project_repository.get_by_id(project_id)
|
|
181
|
+
if not old_project:
|
|
182
|
+
raise HTTPException(status_code=404, detail=f"Project with ID {project_id} not found")
|
|
183
|
+
|
|
184
|
+
old_project_info = ProjectItem(
|
|
185
|
+
id=old_project.id,
|
|
186
|
+
name=old_project.name,
|
|
187
|
+
path=old_project.path,
|
|
188
|
+
is_default=old_project.is_default or False,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Update using project name (service layer still uses names internally)
|
|
192
|
+
if path:
|
|
193
|
+
await project_service.move_project(old_project.name, path)
|
|
194
|
+
elif is_active is not None:
|
|
195
|
+
await project_service.update_project(old_project.name, is_active=is_active)
|
|
196
|
+
|
|
197
|
+
# Get updated project info
|
|
198
|
+
updated_project = await project_repository.get_by_id(project_id)
|
|
199
|
+
if not updated_project:
|
|
200
|
+
raise HTTPException(
|
|
201
|
+
status_code=404, detail=f"Project with ID {project_id} not found after update"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
return ProjectStatusResponse(
|
|
205
|
+
message=f"Project '{updated_project.name}' updated successfully",
|
|
206
|
+
status="success",
|
|
207
|
+
default=(old_project.name == project_service.default_project),
|
|
208
|
+
old_project=old_project_info,
|
|
209
|
+
new_project=ProjectItem(
|
|
210
|
+
id=updated_project.id,
|
|
211
|
+
name=updated_project.name,
|
|
212
|
+
path=updated_project.path,
|
|
213
|
+
is_default=updated_project.is_default or False,
|
|
214
|
+
),
|
|
215
|
+
)
|
|
216
|
+
except ValueError as e:
|
|
217
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@router.delete("/{project_id}", response_model=ProjectStatusResponse)
|
|
221
|
+
async def delete_project_by_id(
|
|
222
|
+
project_id: ProjectIdPathDep,
|
|
223
|
+
project_service: ProjectServiceDep,
|
|
224
|
+
project_repository: ProjectRepositoryDep,
|
|
225
|
+
delete_notes: bool = Query(
|
|
226
|
+
False, description="If True, delete project directory from filesystem"
|
|
227
|
+
),
|
|
228
|
+
) -> ProjectStatusResponse:
|
|
229
|
+
"""Delete a project by ID.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
project_id: Numeric project ID
|
|
233
|
+
delete_notes: If True, delete the project directory from the filesystem
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Response confirming the project was deleted
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
HTTPException: 400 if trying to delete default project, 404 if not found
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
DELETE /v2/projects/3?delete_notes=false
|
|
243
|
+
"""
|
|
244
|
+
logger.info(
|
|
245
|
+
f"API v2 request: delete_project_by_id for project_id={project_id}, delete_notes={delete_notes}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
old_project = await project_repository.get_by_id(project_id)
|
|
250
|
+
if not old_project:
|
|
251
|
+
raise HTTPException(status_code=404, detail=f"Project with ID {project_id} not found")
|
|
252
|
+
|
|
253
|
+
# Check if trying to delete the default project
|
|
254
|
+
if old_project.name == project_service.default_project:
|
|
255
|
+
available_projects = await project_service.list_projects()
|
|
256
|
+
other_projects = [p.name for p in available_projects if p.id != project_id]
|
|
257
|
+
detail = f"Cannot delete default project '{old_project.name}'. "
|
|
258
|
+
if other_projects:
|
|
259
|
+
detail += (
|
|
260
|
+
f"Set another project as default first. Available: {', '.join(other_projects)}"
|
|
261
|
+
)
|
|
262
|
+
else:
|
|
263
|
+
detail += "This is the only project in your configuration."
|
|
264
|
+
raise HTTPException(status_code=400, detail=detail)
|
|
265
|
+
|
|
266
|
+
# Delete using project name (service layer still uses names internally)
|
|
267
|
+
await project_service.remove_project(old_project.name, delete_notes=delete_notes)
|
|
268
|
+
|
|
269
|
+
return ProjectStatusResponse(
|
|
270
|
+
message=f"Project '{old_project.name}' removed successfully",
|
|
271
|
+
status="success",
|
|
272
|
+
default=False,
|
|
273
|
+
old_project=ProjectItem(
|
|
274
|
+
id=old_project.id,
|
|
275
|
+
name=old_project.name,
|
|
276
|
+
path=old_project.path,
|
|
277
|
+
is_default=old_project.is_default or False,
|
|
278
|
+
),
|
|
279
|
+
new_project=None,
|
|
280
|
+
)
|
|
281
|
+
except ValueError as e:
|
|
282
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@router.put("/{project_id}/default", response_model=ProjectStatusResponse)
|
|
286
|
+
async def set_default_project_by_id(
|
|
287
|
+
project_id: ProjectIdPathDep,
|
|
288
|
+
project_service: ProjectServiceDep,
|
|
289
|
+
project_repository: ProjectRepositoryDep,
|
|
290
|
+
) -> ProjectStatusResponse:
|
|
291
|
+
"""Set a project as the default project by ID.
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
project_id: Numeric project ID to set as default
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Response confirming the project was set as default
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
HTTPException: 404 if project not found
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
PUT /v2/projects/3/default
|
|
304
|
+
"""
|
|
305
|
+
logger.info(f"API v2 request: set_default_project_by_id for project_id={project_id}")
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# Get the old default project
|
|
309
|
+
default_name = project_service.default_project
|
|
310
|
+
default_project = await project_service.get_project(default_name)
|
|
311
|
+
if not default_project:
|
|
312
|
+
raise HTTPException(
|
|
313
|
+
status_code=404, detail=f"Default Project: '{default_name}' does not exist"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Get the new default project
|
|
317
|
+
new_default_project = await project_repository.get_by_id(project_id)
|
|
318
|
+
if not new_default_project:
|
|
319
|
+
raise HTTPException(status_code=404, detail=f"Project with ID {project_id} not found")
|
|
320
|
+
|
|
321
|
+
# Set as default using project name (service layer still uses names internally)
|
|
322
|
+
await project_service.set_default_project(new_default_project.name)
|
|
323
|
+
|
|
324
|
+
return ProjectStatusResponse(
|
|
325
|
+
message=f"Project '{new_default_project.name}' set as default successfully",
|
|
326
|
+
status="success",
|
|
327
|
+
default=True,
|
|
328
|
+
old_project=ProjectItem(
|
|
329
|
+
id=default_project.id,
|
|
330
|
+
name=default_name,
|
|
331
|
+
path=default_project.path,
|
|
332
|
+
is_default=False,
|
|
333
|
+
),
|
|
334
|
+
new_project=ProjectItem(
|
|
335
|
+
id=new_default_project.id,
|
|
336
|
+
name=new_default_project.name,
|
|
337
|
+
path=new_default_project.path,
|
|
338
|
+
is_default=True,
|
|
339
|
+
),
|
|
340
|
+
)
|
|
341
|
+
except ValueError as e:
|
|
342
|
+
raise HTTPException(status_code=400, detail=str(e))
|