"""Additional Pydantic response models for the ObsiGate REST API. These models enrich the auto-generated OpenAPI 3.1 schema (#72). They are intentionally permissive (``extra="allow"``) so that adding a new field to an existing endpoint never breaks response validation — the model documents the stable, public shape while still accepting internal additions. Models whose endpoints are already typed in :mod:`backend.main` (e.g. ``FileContentResponse``) live there; this module only holds the ones used to fill the gaps. """ from typing import Any from pydantic import BaseModel, ConfigDict, Field # --------------------------------------------------------------------------- # Generic helpers # --------------------------------------------------------------------------- class StatusResponse(BaseModel): """Generic ``{"status": "..."}`` acknowledgement.""" model_config = ConfigDict(extra="allow") status: str = Field(default="ok", description="Operation status") class ErrorResponse(BaseModel): """Standard FastAPI error envelope.""" detail: str | list[dict[str, Any]] | None = Field( default=None, description="Human-readable error message, or a list of validation errors", examples=["Vault 'Notes' not found"], ) # --------------------------------------------------------------------------- # Recent files & bookmarks # --------------------------------------------------------------------------- class RecentFileItem(BaseModel): """A recently opened or recently modified file.""" path: str = Field(description="Relative path within the vault") title: str = Field(description="File title") vault: str = Field(description="Vault name") mtime: float | int | str | None = Field(default=None, description="Modification timestamp") mtime_human: str | None = Field(default=None, description="Human-readable modification time") mtime_iso: str | None = Field(default=None, description="ISO-8601 modification time") size_bytes: int | None = Field(default=None, description="File size in bytes") tags: list[str] = Field(default_factory=list, description="Up to 5 leading tags") preview: str | None = Field(default=None, description="Short content preview") bookmarked: bool | None = Field(default=None, description="Whether the file is bookmarked") class RecentResponse(BaseModel): """Response for ``GET /api/recent``.""" files: list[RecentFileItem] total: int = Field(description="Total number of files in the selected mode") limit: int = Field(description="Applied limit") mode: str = Field(description="'opened' or 'modified'") model_config = ConfigDict( json_schema_extra={ "example": { "files": [ { "path": "notes/Accueil.md", "title": "Accueil", "vault": "TestVault", "mtime": 1750000000.0, "mtime_human": "il y a 2 h", "size_bytes": 1024, "tags": ["#accueil"], "preview": "# Bienvenue…", "bookmarked": False, } ], "total": 1, "limit": 20, "mode": "opened", } } ) class BookmarkFileItem(BaseModel): """A bookmarked file.""" path: str = Field(description="Relative path within the vault") title: str = Field(description="File title") vault: str = Field(description="Vault name") mtime: float | int | str | None = Field(default=None, description="Bookmark timestamp") mtime_human: str | None = Field(default=None, description="Human-readable bookmark time") size_bytes: int | None = Field(default=None, description="File size in bytes") tags: list[str] = Field(default_factory=list, description="Up to 5 leading tags") bookmarked: bool | None = Field(default=True, description="Always true for this endpoint") class BookmarksResponse(BaseModel): """Response for ``GET /api/bookmarks``.""" files: list[BookmarkFileItem] total: int = Field(description="Number of bookmarked files") class BookmarkToggleResponse(BaseModel): """Response for ``POST /api/bookmarks/toggle``.""" bookmarked: bool = Field(description="New bookmark state") class SavedSearch(BaseModel): """A persisted search definition.""" model_config = ConfigDict(extra="allow") id: str = Field(description="Unique identifier (millisecond timestamp)") query: str = Field(description="Search query text") vault: str = Field(default="all", description="Vault filter") case_sensitive: bool = Field(default=False) whole_word: bool = Field(default=False) regex: bool = Field(default=False) include_paths: str = Field(default="") exclude_paths: str = Field(default="") created_at: float = Field(description="Creation timestamp (epoch seconds)") # --------------------------------------------------------------------------- # Backups & diffs # --------------------------------------------------------------------------- class BackupsResponse(BaseModel): """Response for ``GET /api/file/{vault}/backups``.""" vault: str path: str backups: list[dict[str, Any]] = Field(description="Backups, newest first") class BacklinksResponse(BaseModel): """Response for ``GET /api/file/{vault}/backlinks``.""" vault: str path: str backlinks: list[dict[str, Any]] = Field(description="Files linking to the target") total: int class BackupsListResponse(BaseModel): """Response for ``GET /api/backups``.""" backups: list[dict[str, Any]] = Field(description="All backups across vaults") total: int total_size_bytes: int class BackupsDeletedResponse(BaseModel): """Response for backup deletion / purge endpoints.""" deleted: int = Field(description="Number of backup files deleted") class BackupContentResponse(BaseModel): """Response for ``GET /api/backups/content``.""" content: str = Field(description="Backup file content (truncated to 100 KB)") name: str = Field(description="Backup file name") size: int = Field(description="Returned content size in characters") class BackupsCompressResponse(BaseModel): """Response for ``POST /api/backups/compress``.""" compressed: int = Field(description="Number of backups processed") saved_bytes: int = Field(description="Bytes saved by gzip compression") dry_run: bool class BackupsAutoResponse(BaseModel): """Response for ``POST /api/backups/auto``.""" backed_up: int = Field(description="Number of files backed up") since_hours: int | float = Field(description="Look-back window in hours") class DiffResponse(BaseModel): """Response containing a unified diff between two file versions (#85 — extrait de backend.main, inchangé).""" vault: str = Field(description="Vault name") path: str = Field(description="Relative file path") version: int = Field(description="Backup version timestamp (left/old side)") compare_with: int | None = Field(default=None, description="Other backup version or null for current file (right/new side)") diff: str = Field(description="Unified diff (empty if no changes)") class RestoreRequest(BaseModel): """Request to restore a file from a backup (#85 — extrait de backend.main, inchangé).""" version: int = Field(description="Timestamp of the backup version to restore") class RestoreResponse(BaseModel): """Response after restoring a file from backup (#85 — extrait de backend.main, inchangé).""" success: bool = Field(description="Whether restore succeeded") vault: str = Field(description="Vault name") path: str = Field(description="Relative file path") restored_from: int = Field(description="Timestamp of the backup used") current_backed_up: int | None = Field(default=None, description="Timestamp of the backup created from the current version before restore, if any") class BackupEntry(BaseModel): """A single backup version of a file (#85 — extrait de backend.main, inchangé).""" timestamp: int = Field(description="Unix timestamp of when the backup was created") datetime: str = Field(description="ISO 8601 datetime string") size: int = Field(description="File size in bytes") filename: str = Field(description="Backup filename on disk") class BackupListResponse(BaseModel): """Response listing all available backups for a file (#85 — extrait de backend.main, inchangé).""" vault: str = Field(description="Vault name") path: str = Field(description="Relative file path") backups: list[BackupEntry] = Field(description="Available backups, newest first") class DiffRequest(BaseModel): """Request parameters for generating a diff (#85 — extrait de backend.main, inchangé).""" version: int = Field(description="Timestamp of the backup version to compare") compare_with: int | None = Field(default=None, description="Timestamp of another backup version. If omitted, compares with the current file.") # --------------------------------------------------------------------------- # Files — browse / read (#85 — extrait de backend.main, inchangé) # --------------------------------------------------------------------------- class BrowseItem(BaseModel): """A single entry (file or directory) returned by the browse endpoint.""" name: str = Field(description="File or directory name") path: str = Field(description="Relative path within vault") type: str = Field(description="'file' or 'directory'") children_count: int | None = Field(default=None, description="Number of children (directories only)") size: int | None = Field(default=None, description="File size in bytes") extension: str | None = Field(default=None, description="File extension") class BrowseResponse(BaseModel): """Paginated directory listing for a vault.""" vault: str path: str items: list[BrowseItem] class FileContentResponse(BaseModel): """Rendered file content with metadata.""" vault: str = Field(description="Vault name") path: str = Field(description="Relative file path within the vault") title: str = Field(description="File title (from frontmatter or filename)") tags: list[str] = Field(description="Extracted tags from frontmatter and inline #tags") frontmatter: dict[str, Any] = Field(description="YAML frontmatter as key-value dict") html: str = Field(description="Rendered HTML content") raw_length: int = Field(description="Length of raw file content in characters") extension: str = Field(description="File extension (e.g. .md, .txt)") is_markdown: bool = Field(description="Whether the file is markdown") unsupported: bool | None = Field(default=False, description="True for binary/unsupported files") size_bytes: int | None = Field(default=None, description="File size in bytes (for unsupported files)") is_pdf: bool | None = Field(default=None, description="True for PDF files") is_image: bool | None = Field(default=None, description="True for image files") is_audio: bool | None = Field(default=None, description="True for audio files (HTML5