open-notebook/api/routers/notebooks.py

276 lines
9.6 KiB
Python

from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query
from loguru import logger
from api.models import NotebookCreate, NotebookResponse, NotebookUpdate
from open_notebook.database.repository import ensure_record_id, repo_query
from open_notebook.domain.notebook import Notebook, Source
from open_notebook.exceptions import InvalidInputError
router = APIRouter()
@router.get("/notebooks", response_model=List[NotebookResponse])
async def get_notebooks(
archived: Optional[bool] = Query(None, description="Filter by archived status"),
order_by: str = Query("updated desc", description="Order by field and direction"),
):
"""Get all notebooks with optional filtering and ordering."""
try:
# Build the query with counts
query = f"""
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM notebook
ORDER BY {order_by}
"""
result = await repo_query(query)
# Filter by archived status if specified
if archived is not None:
result = [nb for nb in result if nb.get("archived") == archived]
return [
NotebookResponse(
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
for nb in result
]
except Exception as e:
logger.error(f"Error fetching notebooks: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error fetching notebooks: {str(e)}"
)
@router.post("/notebooks", response_model=NotebookResponse)
async def create_notebook(notebook: NotebookCreate):
"""Create a new notebook."""
try:
new_notebook = Notebook(
name=notebook.name,
description=notebook.description,
)
await new_notebook.save()
return NotebookResponse(
id=new_notebook.id or "",
name=new_notebook.name,
description=new_notebook.description,
archived=new_notebook.archived or False,
created=str(new_notebook.created),
updated=str(new_notebook.updated),
source_count=0, # New notebook has no sources
note_count=0, # New notebook has no notes
)
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error creating notebook: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error creating notebook: {str(e)}"
)
@router.get("/notebooks/{notebook_id}", response_model=NotebookResponse)
async def get_notebook(notebook_id: str):
"""Get a specific notebook by ID."""
try:
# Query with counts for single notebook
query = """
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM $notebook_id
"""
result = await repo_query(query, {"notebook_id": ensure_record_id(notebook_id)})
if not result:
raise HTTPException(status_code=404, detail="Notebook not found")
nb = result[0]
return NotebookResponse(
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error fetching notebook {notebook_id}: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error fetching notebook: {str(e)}"
)
@router.put("/notebooks/{notebook_id}", response_model=NotebookResponse)
async def update_notebook(notebook_id: str, notebook_update: NotebookUpdate):
"""Update a notebook."""
try:
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
# Update only provided fields
if notebook_update.name is not None:
notebook.name = notebook_update.name
if notebook_update.description is not None:
notebook.description = notebook_update.description
if notebook_update.archived is not None:
notebook.archived = notebook_update.archived
await notebook.save()
# Query with counts after update
query = """
SELECT *,
count(<-reference.in) as source_count,
count(<-artifact.in) as note_count
FROM $notebook_id
"""
result = await repo_query(query, {"notebook_id": ensure_record_id(notebook_id)})
if result:
nb = result[0]
return NotebookResponse(
id=str(nb.get("id", "")),
name=nb.get("name", ""),
description=nb.get("description", ""),
archived=nb.get("archived", False),
created=str(nb.get("created", "")),
updated=str(nb.get("updated", "")),
source_count=nb.get("source_count", 0),
note_count=nb.get("note_count", 0),
)
# Fallback if query fails
return NotebookResponse(
id=notebook.id or "",
name=notebook.name,
description=notebook.description,
archived=notebook.archived or False,
created=str(notebook.created),
updated=str(notebook.updated),
source_count=0,
note_count=0,
)
except HTTPException:
raise
except InvalidInputError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
logger.error(f"Error updating notebook {notebook_id}: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error updating notebook: {str(e)}"
)
@router.post("/notebooks/{notebook_id}/sources/{source_id}")
async def add_source_to_notebook(notebook_id: str, source_id: str):
"""Add an existing source to a notebook (create the reference)."""
try:
# Check if notebook exists
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
# Check if source exists
source = await Source.get(source_id)
if not source:
raise HTTPException(status_code=404, detail="Source not found")
# Check if reference already exists (idempotency)
existing_ref = await repo_query(
"SELECT * FROM reference WHERE out = $source_id AND in = $notebook_id",
{
"notebook_id": ensure_record_id(notebook_id),
"source_id": ensure_record_id(source_id),
},
)
# If reference doesn't exist, create it
if not existing_ref:
await repo_query(
"RELATE $source_id->reference->$notebook_id",
{
"notebook_id": ensure_record_id(notebook_id),
"source_id": ensure_record_id(source_id),
},
)
return {"message": "Source linked to notebook successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(
f"Error linking source {source_id} to notebook {notebook_id}: {str(e)}"
)
raise HTTPException(
status_code=500, detail=f"Error linking source to notebook: {str(e)}"
)
@router.delete("/notebooks/{notebook_id}/sources/{source_id}")
async def remove_source_from_notebook(notebook_id: str, source_id: str):
"""Remove a source from a notebook (delete the reference)."""
try:
# Check if notebook exists
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
# Delete the reference record linking source to notebook
await repo_query(
"DELETE FROM reference WHERE out = $notebook_id AND in = $source_id",
{
"notebook_id": ensure_record_id(notebook_id),
"source_id": ensure_record_id(source_id),
},
)
return {"message": "Source removed from notebook successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(
f"Error removing source {source_id} from notebook {notebook_id}: {str(e)}"
)
raise HTTPException(
status_code=500, detail=f"Error removing source from notebook: {str(e)}"
)
@router.delete("/notebooks/{notebook_id}")
async def delete_notebook(notebook_id: str):
"""Delete a notebook."""
try:
notebook = await Notebook.get(notebook_id)
if not notebook:
raise HTTPException(status_code=404, detail="Notebook not found")
await notebook.delete()
return {"message": "Notebook deleted successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting notebook {notebook_id}: {str(e)}")
raise HTTPException(
status_code=500, detail=f"Error deleting notebook: {str(e)}"
)