feat(api): implement notebook management CRUD endpoints

Implement Sprint 1: Notebook Management CRUD

- Add NotebookService with full CRUD operations
- Add POST /api/v1/notebooks (create notebook)
- Add GET /api/v1/notebooks (list with pagination)
- Add GET /api/v1/notebooks/{id} (get by ID)
- Add PATCH /api/v1/notebooks/{id} (partial update)
- Add DELETE /api/v1/notebooks/{id} (delete)
- Add Pydantic models for requests/responses
- Add custom exceptions (ValidationError, NotFoundError, NotebookLMError)
- Add comprehensive unit tests (31 tests, 97% coverage)
- Add API integration tests (26 tests)
- Fix router prefix duplication
- Fix JSON serialization in error responses

BREAKING CHANGE: None
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-06 01:13:13 +02:00
commit 4b7a419a98
65 changed files with 10507 additions and 0 deletions

View File

@@ -0,0 +1,420 @@
"""Response models for NotebookLM Agent API.
This module contains Pydantic models for API response serialization.
"""
from datetime import datetime
from typing import Any, Generic, TypeVar
from uuid import UUID
from pydantic import BaseModel, ConfigDict, Field
T = TypeVar("T")
class ErrorDetail(BaseModel):
"""Error detail information.
Attributes:
code: Error code for programmatic handling.
message: Human-readable error message.
details: Additional error details (field errors, etc.).
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [{"field": "title", "error": "Title is too short"}],
}
}
)
code: str = Field(
...,
description="Error code for programmatic handling",
examples=["VALIDATION_ERROR", "NOT_FOUND", "AUTH_ERROR"],
)
message: str = Field(
...,
description="Human-readable error message",
examples=["Invalid input data", "Notebook not found"],
)
details: list[dict[str, Any]] | None = Field(
None,
description="Additional error details",
examples=[[{"field": "title", "error": "Title is too short"}]],
)
class ResponseMeta(BaseModel):
"""Metadata for API responses.
Attributes:
timestamp: ISO 8601 timestamp of the response.
request_id: Unique identifier for the request.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"timestamp": "2026-04-06T10:30:00Z",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
}
}
)
timestamp: datetime = Field(
...,
description="ISO 8601 timestamp of the response",
examples=["2026-04-06T10:30:00Z"],
)
request_id: UUID = Field(
...,
description="Unique identifier for the request",
examples=["550e8400-e29b-41d4-a716-446655440000"],
)
class ApiResponse(BaseModel, Generic[T]):
"""Standard API response wrapper.
This wrapper is used for all API responses to ensure consistency.
Attributes:
success: Whether the request was successful.
data: Response data (None if error).
error: Error details (None if success).
meta: Response metadata.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"success": True,
"data": {"id": "550e8400-e29b-41d4-a716-446655440000"},
"error": None,
"meta": {
"timestamp": "2026-04-06T10:30:00Z",
"request_id": "550e8400-e29b-41d4-a716-446655440000",
},
}
}
)
success: bool = Field(
...,
description="Whether the request was successful",
examples=[True, False],
)
data: T | None = Field(
None,
description="Response data (None if error)",
)
error: ErrorDetail | None = Field(
None,
description="Error details (None if success)",
)
meta: ResponseMeta = Field(
...,
description="Response metadata",
)
class Notebook(BaseModel):
"""Notebook response model.
Attributes:
id: Unique identifier (UUID).
title: Notebook title.
description: Optional notebook description.
created_at: Creation timestamp.
updated_at: Last update timestamp.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "My Research Notebook",
"description": "A collection of AI research papers",
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:30:00Z",
}
}
)
id: UUID = Field(
...,
description="Unique identifier (UUID)",
examples=["550e8400-e29b-41d4-a716-446655440000"],
)
title: str = Field(
...,
description="Notebook title",
examples=["My Research Notebook"],
)
description: str | None = Field(
None,
description="Optional notebook description",
examples=["A collection of AI research papers"],
)
created_at: datetime = Field(
...,
description="Creation timestamp (ISO 8601)",
examples=["2026-04-06T10:00:00Z"],
)
updated_at: datetime = Field(
...,
description="Last update timestamp (ISO 8601)",
examples=["2026-04-06T10:30:00Z"],
)
class NotebookDetail(Notebook):
"""Detailed notebook response with counts.
Extends Notebook with additional statistics.
Attributes:
sources_count: Number of sources in the notebook.
artifacts_count: Number of artifacts in the notebook.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "My Research Notebook",
"description": "A collection of AI research papers",
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:30:00Z",
"sources_count": 5,
"artifacts_count": 2,
}
}
)
sources_count: int = Field(
0,
ge=0,
description="Number of sources in the notebook",
examples=[5, 10, 0],
)
artifacts_count: int = Field(
0,
ge=0,
description="Number of artifacts in the notebook",
examples=[2, 5, 0],
)
class PaginationMeta(BaseModel):
"""Pagination metadata.
Attributes:
total: Total number of items available.
limit: Maximum items per page.
offset: Current offset.
has_more: Whether more items are available.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"total": 100,
"limit": 20,
"offset": 0,
"has_more": True,
}
}
)
total: int = Field(
...,
ge=0,
description="Total number of items available",
examples=[100, 50, 0],
)
limit: int = Field(
...,
ge=1,
description="Maximum items per page",
examples=[20, 50],
)
offset: int = Field(
...,
ge=0,
description="Current offset",
examples=[0, 20, 40],
)
has_more: bool = Field(
...,
description="Whether more items are available",
examples=[True, False],
)
class PaginatedNotebooks(BaseModel):
"""Paginated list of notebooks.
Attributes:
items: List of notebooks.
pagination: Pagination metadata.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"title": "Notebook 1",
"created_at": "2026-04-06T10:00:00Z",
"updated_at": "2026-04-06T10:30:00Z",
}
],
"pagination": {
"total": 100,
"limit": 20,
"offset": 0,
"has_more": True,
},
}
}
)
items: list[Notebook] = Field(
...,
description="List of notebooks",
)
pagination: PaginationMeta = Field(
...,
description="Pagination metadata",
)
class Source(BaseModel):
"""Source response model.
Attributes:
id: Unique identifier (UUID).
notebook_id: Parent notebook ID.
type: Source type (url, file, youtube, drive).
title: Source title.
url: Source URL (if applicable).
status: Processing status.
created_at: Creation timestamp.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"id": "550e8400-e29b-41d4-a716-446655440001",
"notebook_id": "550e8400-e29b-41d4-a716-446655440000",
"type": "url",
"title": "Example Article",
"url": "https://example.com/article",
"status": "ready",
"created_at": "2026-04-06T10:00:00Z",
}
}
)
id: UUID = Field(
...,
description="Unique identifier (UUID)",
examples=["550e8400-e29b-41d4-a716-446655440001"],
)
notebook_id: UUID = Field(
...,
description="Parent notebook ID",
examples=["550e8400-e29b-41d4-a716-446655440000"],
)
type: str = Field(
...,
description="Source type",
examples=["url", "file", "youtube", "drive"],
)
title: str = Field(
...,
description="Source title",
examples=["Example Article"],
)
url: str | None = Field(
None,
description="Source URL (if applicable)",
examples=["https://example.com/article"],
)
status: str = Field(
...,
description="Processing status",
examples=["processing", "ready", "error"],
)
created_at: datetime = Field(
...,
description="Creation timestamp",
examples=["2026-04-06T10:00:00Z"],
)
class PaginatedSources(BaseModel):
"""Paginated list of sources.
Attributes:
items: List of sources.
pagination: Pagination metadata.
"""
items: list[Source] = Field(
...,
description="List of sources",
)
pagination: PaginationMeta = Field(
...,
description="Pagination metadata",
)
class HealthStatus(BaseModel):
"""Health check response.
Attributes:
status: Health status (healthy, degraded, unhealthy).
timestamp: Check timestamp.
version: API version.
service: Service name.
"""
model_config = ConfigDict(
json_schema_extra={
"example": {
"status": "healthy",
"timestamp": "2026-04-06T10:30:00Z",
"version": "0.1.0",
"service": "notebooklm-agent-api",
}
}
)
status: str = Field(
...,
description="Health status",
examples=["healthy", "degraded", "unhealthy"],
)
timestamp: datetime = Field(
...,
description="Check timestamp",
examples=["2026-04-06T10:30:00Z"],
)
version: str = Field(
...,
description="API version",
examples=["0.1.0"],
)
service: str = Field(
...,
description="Service name",
examples=["notebooklm-agent-api"],
)