Spaces:
Sleeping
Sleeping
| 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() | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |
| 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)}" | |
| ) | |