Skip to content

Commit 6c720da

Browse files
phernandezclaude
andcommitted
refactor: consolidate normalize_project_path into shared utils module
Extract normalize_project_path() from 3 duplicate locations into basic_memory/utils.py to eliminate code duplication and improve maintainability. Changes: - Add normalize_project_path() to utils.py with comprehensive docstring - Remove duplicate implementations from: - api/routers/project_router.py - cli/commands/project.py - cli/commands/cloud/rclone_commands.py (inline version) - Update all imports to use shared function from basic_memory.utils Benefits: - Single source of truth for path normalization logic - Easier to maintain and test - Consistent behavior across API and CLI layers Addresses code review feedback for PR #405. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 0c29884 commit 6c720da

7 files changed

Lines changed: 46 additions & 43 deletions

File tree

src/basic_memory/api/routers/project_router.py

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ProjectInfoRequest,
1919
ProjectStatusResponse,
2020
)
21+
from basic_memory.utils import normalize_project_path
2122

2223
# Router for resources in a specific project
2324
# The ProjectPathDep is used in the path as a prefix, so the request path is like /{project}/project/info
@@ -27,24 +28,6 @@
2728
project_resource_router = APIRouter(prefix="/projects", tags=["project_management"])
2829

2930

30-
def normalize_project_path(path: str) -> str:
31-
"""Normalize project path by stripping mount point prefix.
32-
33-
In cloud deployments, the S3 bucket is mounted at /app/data. We strip this
34-
prefix from project paths to avoid leaking implementation details and to
35-
ensure paths match the actual S3 bucket structure.
36-
37-
Args:
38-
path: Project path (e.g., "/app/data/basic-memory-llc")
39-
40-
Returns:
41-
Normalized path (e.g., "/basic-memory-llc")
42-
"""
43-
if path.startswith("/app/data/"):
44-
return path.removeprefix("/app/data")
45-
return path
46-
47-
4831
@project_router.get("/info", response_model=ProjectInfoResponse)
4932
async def get_project_info(
5033
project_service: ProjectServiceDep,
@@ -127,7 +110,9 @@ async def sync_project(
127110
background_tasks: BackgroundTasks,
128111
sync_service: SyncServiceDep,
129112
project_config: ProjectConfigDep,
130-
force_full: bool = Query(False, description="Force full scan, bypassing watermark optimization"),
113+
force_full: bool = Query(
114+
False, description="Force full scan, bypassing watermark optimization"
115+
),
131116
):
132117
"""Force project filesystem sync to database.
133118
@@ -146,8 +131,7 @@ async def sync_project(
146131
sync_service.sync, project_config.home, project_config.name, force_full=force_full
147132
)
148133
logger.info(
149-
f"Filesystem sync initiated for project: {project_config.name} "
150-
f"(force_full={force_full})"
134+
f"Filesystem sync initiated for project: {project_config.name} (force_full={force_full})"
151135
)
152136

153137
return {

src/basic_memory/cli/commands/cloud/rclone_commands.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
from rich.console import Console
1818

19+
from basic_memory.utils import normalize_project_path
20+
1921
console = Console()
2022

2123

@@ -97,10 +99,8 @@ def get_project_remote(project: SyncProject, bucket_name: str) -> str:
9799
is mounted at /app/data on the fly machine. We need to strip the /app/data/
98100
prefix to get the actual S3 path within the bucket.
99101
"""
100-
# Strip /app/data/ prefix from cloud path (mount point on fly machine)
101-
cloud_path = project.path.lstrip("/")
102-
if cloud_path.startswith("app/data/"):
103-
cloud_path = cloud_path.removeprefix("app/data/")
102+
# Normalize path to strip /app/data/ mount point prefix
103+
cloud_path = normalize_project_path(project.path).lstrip("/")
104104
return f"basic-memory-cloud:{bucket_name}/{cloud_path}"
105105

106106

src/basic_memory/cli/commands/project.py

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
from basic_memory.schemas.project_info import ProjectStatusResponse
2323
from basic_memory.mcp.tools.utils import call_delete
2424
from basic_memory.mcp.tools.utils import call_put
25-
from basic_memory.utils import generate_permalink
25+
from basic_memory.utils import generate_permalink, normalize_project_path
2626
from basic_memory.mcp.tools.utils import call_patch
2727

2828
# Import rclone commands for project sync
@@ -43,23 +43,6 @@
4343
app.add_typer(project_app, name="project")
4444

4545

46-
def normalize_project_path(path: str) -> str:
47-
"""Normalize project path by stripping mount point prefix.
48-
49-
In cloud deployments, the S3 bucket is mounted at /app/data. We strip this
50-
prefix to get the actual S3 bucket path and avoid leaking implementation details.
51-
52-
Args:
53-
path: Project path (e.g., "/app/data/basic-memory-llc")
54-
55-
Returns:
56-
Normalized path (e.g., "/basic-memory-llc")
57-
"""
58-
if path.startswith("/app/data/"):
59-
return path.removeprefix("/app/data")
60-
return path
61-
62-
6346
def format_path(path: str) -> str:
6447
"""Format a path for display, using ~ for home directory."""
6548
home = str(Path.home())

src/basic_memory/utils.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,39 @@
1313
from unidecode import unidecode
1414

1515

16+
def normalize_project_path(path: str) -> str:
17+
"""Normalize project path by stripping mount point prefix.
18+
19+
In cloud deployments, the S3 bucket is mounted at /app/data. We strip this
20+
prefix from project paths to avoid leaking implementation details and to
21+
ensure paths match the actual S3 bucket structure.
22+
23+
Args:
24+
path: Project path (e.g., "/app/data/basic-memory-llc")
25+
26+
Returns:
27+
Normalized path (e.g., "/basic-memory-llc")
28+
29+
Examples:
30+
>>> normalize_project_path("/app/data/my-project")
31+
'/my-project'
32+
>>> normalize_project_path("/my-project")
33+
'/my-project'
34+
>>> normalize_project_path("app/data/my-project")
35+
'/my-project'
36+
"""
37+
# Handle both absolute and relative paths
38+
normalized = path.lstrip("/")
39+
if normalized.startswith("app/data/"):
40+
normalized = normalized.removeprefix("app/data/")
41+
42+
# Ensure leading slash for absolute paths
43+
if not normalized.startswith("/"):
44+
normalized = "/" + normalized
45+
46+
return normalized
47+
48+
1649
@runtime_checkable
1750
class PathLike(Protocol):
1851
"""Protocol for objects that can be used as paths."""

test2/foo.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

test2/foot.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo

test3/test.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
test content

0 commit comments

Comments
 (0)