From f61c5970df86e8750dd91e92a42d75f0e4916b2a Mon Sep 17 00:00:00 2001 From: phernandez Date: Sun, 19 Jan 2025 20:07:20 -0600 Subject: [PATCH] add resource api route --- src/basic_memory/api/app.py | 14 ++--- src/basic_memory/api/routers/__init__.py | 5 +- .../api/routers/resource_router.py | 34 +++++++++++ src/basic_memory/services/search_service.py | 4 +- tests/api/test_resource_router.py | 58 +++++++++++++++++++ 5 files changed, 103 insertions(+), 12 deletions(-) create mode 100644 src/basic_memory/api/routers/resource_router.py create mode 100644 tests/api/test_resource_router.py diff --git a/src/basic_memory/api/app.py b/src/basic_memory/api/app.py index eb1c174a..d6e5aec3 100644 --- a/src/basic_memory/api/app.py +++ b/src/basic_memory/api/app.py @@ -6,11 +6,7 @@ from fastapi import FastAPI from loguru import logger from basic_memory import db -from .routers import knowledge -from .routers import discovery -from .routers import memory -from .routers.activity_router import router as activity_router -from .routers.search_router import router as search_router +from .routers import knowledge, discovery, search, activity, memory, resource @asynccontextmanager @@ -33,7 +29,7 @@ app = FastAPI( # Include routers app.include_router(knowledge.router) app.include_router(discovery.router) -app.include_router(activity_router) -app.include_router(search_router) - -app.include_router(memory.router) \ No newline at end of file +app.include_router(activity.router) +app.include_router(search.router) +app.include_router(memory.router) +app.include_router(resource.router) \ No newline at end of file diff --git a/src/basic_memory/api/routers/__init__.py b/src/basic_memory/api/routers/__init__.py index 48abda72..273fc811 100644 --- a/src/basic_memory/api/routers/__init__.py +++ b/src/basic_memory/api/routers/__init__.py @@ -3,5 +3,8 @@ from . import knowledge_router as knowledge from . import discovery_router as discovery from . import memory_router as memory +from . import resource_router as resource +from . import activity_router as activity +from . import search_router as search -__all__ = ["knowledge", "discovery", "memory"] +__all__ = ["knowledge", "discovery", "memory", "resource", "activity", "search"] diff --git a/src/basic_memory/api/routers/resource_router.py b/src/basic_memory/api/routers/resource_router.py new file mode 100644 index 00000000..3e03b566 --- /dev/null +++ b/src/basic_memory/api/routers/resource_router.py @@ -0,0 +1,34 @@ +"""Routes for getting entity content.""" + +from pathlib import Path + +from fastapi import APIRouter, HTTPException +from fastapi.responses import FileResponse +from loguru import logger + +from basic_memory.deps import EntityRepositoryDep, ProjectConfigDep + +router = APIRouter(prefix="/resource", tags=["resources"]) + + +@router.get("/{permalink:path}") +async def get_resource_content( + config: ProjectConfigDep, + entity_repository: EntityRepositoryDep, + permalink: str, +) -> FileResponse: + """Get resource content by permalink.""" + logger.debug(f"Getting content for permalink: {permalink}") + + # Find entity by permalink + entity = await entity_repository.get_by_permalink(permalink) + if not entity: + raise HTTPException(status_code=404, detail=f"Entity not found: {permalink}") + + file_path = Path(f"{config.home}/{entity.file_path}") + if not file_path.exists(): + raise HTTPException( + status_code=404, + detail=f"File not found: {file_path}", + ) + return FileResponse(path=file_path) diff --git a/src/basic_memory/services/search_service.py b/src/basic_memory/services/search_service.py index c9024fda..a3f34680 100644 --- a/src/basic_memory/services/search_service.py +++ b/src/basic_memory/services/search_service.py @@ -156,7 +156,7 @@ class SearchService: # We can construct these because observations are always # defined in and owned by a single entity observation_permalink = ( - f"{entity.permalink}/observations/{obs.category}/{generate_permalink(obs.content)}" + generate_permalink(f"{entity.permalink}/observations/{obs.category}/{obs.content}") ) # Index with parent entity's file path since that's where it's defined @@ -186,7 +186,7 @@ class SearchService: # source/relation_type/target # e.g., "specs/search/implements/features/search-ui" relation_permalink = ( - f"{rel.from_entity.permalink}/{rel.relation_type}/{rel.to_entity.permalink}" + generate_permalink(f"{rel.from_entity.permalink}/{rel.relation_type}/{rel.to_entity.permalink}") ) # Create descriptive title showing the relationship diff --git a/tests/api/test_resource_router.py b/tests/api/test_resource_router.py new file mode 100644 index 00000000..a84df3e6 --- /dev/null +++ b/tests/api/test_resource_router.py @@ -0,0 +1,58 @@ +"""Tests for resource router endpoints.""" + +import pytest +from pathlib import Path + + +@pytest.mark.asyncio +async def test_get_resource_content(client, test_config, entity_repository): + """Test getting content by permalink.""" + # Create a test file + content = "# Test Content\n\nThis is a test file." + test_file = Path(test_config.home) / "test" / "test.md" + test_file.parent.mkdir(parents=True, exist_ok=True) + test_file.write_text(content) + + # Create entity referencing the file + entity = await entity_repository.create( + { + "title": "Test Entity", + "entity_type": "test", + "permalink": "test/test", + "file_path": "test/test.md", # Relative to config.home + "content_type": "text/markdown", + } + ) + + # Test getting the content + response = await client.get(f"/resource/{entity.permalink}") + assert response.status_code == 200 + assert response.headers["content-type"] == "text/markdown; charset=utf-8" + assert response.text == content + + +@pytest.mark.asyncio +async def test_get_resource_missing_entity(client): + """Test 404 when entity doesn't exist.""" + response = await client.get("/resource/does/not/exist") + assert response.status_code == 404 + assert "Entity not found" in response.json()["detail"] + + +@pytest.mark.asyncio +async def test_get_resource_missing_file(client, test_config, entity_repository): + """Test 404 when file doesn't exist.""" + # Create entity referencing non-existent file + entity = await entity_repository.create( + { + "title": "Missing File", + "entity_type": "test", + "permalink": "test/missing", + "file_path": "test/missing.md", + "content_type": "text/markdown", + } + ) + + response = await client.get(f"/resource/{entity.permalink}") + assert response.status_code == 404 + assert "File not found" in response.json()["detail"]