Paperless API requires field ID (integer) not field name (string) when updating custom fields. Now looks up field ID by name before updating library_indexed custom field. Also includes webhook debugging endpoint for development. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
208 lines
9.1 KiB
Python
208 lines
9.1 KiB
Python
"""
|
|
Document storage models for Library Desk.
|
|
|
|
Models for Paperless-ngx document management, virus scanning,
|
|
and document sync operations.
|
|
"""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import Dict, Any, Optional, List
|
|
from datetime import datetime
|
|
from enum import Enum
|
|
|
|
|
|
class DocumentType(str, Enum):
|
|
"""Types of documents supported in the document store."""
|
|
PDF = "pdf"
|
|
IMAGE = "image"
|
|
VIDEO = "video"
|
|
TEXT = "text"
|
|
ARCHIVE = "archive"
|
|
OTHER = "other"
|
|
|
|
|
|
class SyncStatus(str, Enum):
|
|
"""Status of document sync with Library Desk."""
|
|
PENDING = "pending"
|
|
INDEXED = "indexed"
|
|
FAILED = "failed"
|
|
SKIPPED = "skipped"
|
|
|
|
|
|
# =============================================================================
|
|
# Document Models
|
|
# =============================================================================
|
|
|
|
|
|
class DocumentMetadata(BaseModel):
|
|
"""Metadata for a document in Paperless-ngx."""
|
|
paperless_id: int = Field(..., description="Paperless-ngx document ID")
|
|
title: str = Field(..., description="Document title")
|
|
filename: Optional[str] = Field(None, description="Original filename")
|
|
content: Optional[str] = Field(None, description="Extracted text content")
|
|
created: Optional[datetime] = Field(None, description="Document creation date")
|
|
modified: Optional[datetime] = Field(None, description="Last modification date")
|
|
added: Optional[datetime] = Field(None, description="Date added to Paperless")
|
|
correspondent: Optional[str] = Field(None, description="Correspondent name")
|
|
document_type: Optional[str] = Field(None, description="Document type name")
|
|
tags: List[str] = Field(default_factory=list, description="Tag names")
|
|
custom_fields: Dict[str, Any] = Field(default_factory=dict, description="Custom field values")
|
|
|
|
|
|
class DocumentRecord(BaseModel):
|
|
"""A document record with sync status."""
|
|
metadata: DocumentMetadata = Field(..., description="Document metadata from Paperless")
|
|
sync_status: SyncStatus = Field(default=SyncStatus.PENDING, description="Library Desk sync status")
|
|
indexed_at: Optional[datetime] = Field(None, description="When indexed in Library Desk")
|
|
collection: Optional[str] = Field(None, description="Collection name (e.g., 'fastapi-docs')")
|
|
source_url: Optional[str] = Field(None, description="Original source URL if uploaded via HybridRAG")
|
|
|
|
|
|
# =============================================================================
|
|
# Upload Request/Response Models
|
|
# =============================================================================
|
|
|
|
|
|
class DocumentUploadRequest(BaseModel):
|
|
"""Request to upload a document to Paperless-ngx."""
|
|
url: Optional[str] = Field(None, description="URL to download document from")
|
|
title: Optional[str] = Field(None, description="Document title (derived from filename if not set)")
|
|
collection: Optional[str] = Field(None, description="Collection to add document to")
|
|
tags: List[str] = Field(default_factory=list, description="Tags to apply")
|
|
correspondent: Optional[str] = Field(None, description="Correspondent name")
|
|
document_type: Optional[str] = Field(None, description="Document type name")
|
|
|
|
|
|
class DocumentUploadResponse(BaseModel):
|
|
"""Response from document upload."""
|
|
task_id: str = Field(..., description="Paperless task ID for tracking")
|
|
filename: str = Field(..., description="Uploaded filename")
|
|
message: str = Field(..., description="Status message")
|
|
|
|
|
|
# =============================================================================
|
|
# Webhook Models
|
|
# =============================================================================
|
|
|
|
|
|
class PaperlessWebhookPayload(BaseModel):
|
|
"""
|
|
Payload from Paperless-ngx webhook.
|
|
|
|
Supports Jinja template format:
|
|
- doc_url: Contains document ID in URL path (e.g., http://paperless:8000/documents/123/)
|
|
- title: Document title from {{ doc_title }}
|
|
"""
|
|
doc_url: str = Field(..., description="Paperless document URL containing ID")
|
|
title: Optional[str] = Field(None, description="Document title")
|
|
|
|
class Config:
|
|
extra = "ignore" # Ignore extra fields
|
|
|
|
@property
|
|
def document_id(self) -> int:
|
|
"""Extract document ID from doc_url."""
|
|
import re
|
|
match = re.search(r'/documents/(\d+)/?', self.doc_url)
|
|
if match:
|
|
return int(match.group(1))
|
|
raise ValueError(f"Cannot extract document ID from URL: {self.doc_url}")
|
|
|
|
|
|
class WebhookResponse(BaseModel):
|
|
"""Response to webhook processing."""
|
|
document_id: int = Field(..., description="Processed document ID")
|
|
status: str = Field(..., description="Processing status")
|
|
indexed: bool = Field(..., description="Whether document was indexed")
|
|
message: Optional[str] = Field(None, description="Additional details")
|
|
|
|
|
|
# =============================================================================
|
|
# Sync Models
|
|
# =============================================================================
|
|
|
|
|
|
class SyncRequest(BaseModel):
|
|
"""Request to sync documents from Paperless-ngx."""
|
|
since: Optional[datetime] = Field(None, description="Only sync documents modified after this time")
|
|
collection: Optional[str] = Field(None, description="Only sync documents in this collection")
|
|
limit: int = Field(default=100, ge=1, le=1000, description="Maximum documents to sync")
|
|
force_reindex: bool = Field(default=False, description="Re-index already indexed documents")
|
|
|
|
|
|
class SyncResult(BaseModel):
|
|
"""Result of a sync operation."""
|
|
documents_found: int = Field(..., description="Total documents matching criteria")
|
|
documents_indexed: int = Field(..., description="Successfully indexed")
|
|
documents_skipped: int = Field(..., description="Skipped (already indexed)")
|
|
documents_failed: int = Field(..., description="Failed to index")
|
|
errors: List[str] = Field(default_factory=list, description="Error messages")
|
|
duration_seconds: float = Field(..., description="Sync duration")
|
|
|
|
|
|
# =============================================================================
|
|
# Collection Models
|
|
# =============================================================================
|
|
|
|
|
|
class Collection(BaseModel):
|
|
"""A logical grouping of documents."""
|
|
name: str = Field(..., description="Collection name (e.g., 'fastapi-docs')")
|
|
description: Optional[str] = Field(None, description="Collection description")
|
|
document_count: int = Field(default=0, description="Number of documents")
|
|
source: Optional[str] = Field(None, description="Source (e.g., 'github.com/tiangolo/fastapi')")
|
|
last_sync: Optional[datetime] = Field(None, description="Last sync timestamp")
|
|
wiki_page: Optional[str] = Field(None, description="Wiki catalog page path")
|
|
|
|
|
|
class CollectionListResponse(BaseModel):
|
|
"""Response listing all collections."""
|
|
collections: List[Collection] = Field(..., description="List of collections")
|
|
total_documents: int = Field(..., description="Total documents across all collections")
|
|
|
|
|
|
# =============================================================================
|
|
# Search Models
|
|
# =============================================================================
|
|
|
|
|
|
class DocumentSearchRequest(BaseModel):
|
|
"""Request to search documents."""
|
|
query: str = Field(..., min_length=1, description="Search query")
|
|
collection: Optional[str] = Field(None, description="Limit to collection")
|
|
document_type: Optional[DocumentType] = Field(None, description="Filter by type")
|
|
limit: int = Field(default=10, ge=1, le=50, description="Maximum results")
|
|
include_content: bool = Field(default=False, description="Include full text content")
|
|
|
|
|
|
class DocumentSearchHit(BaseModel):
|
|
"""A document search result."""
|
|
paperless_id: int = Field(..., description="Paperless document ID")
|
|
title: str = Field(..., description="Document title")
|
|
score: float = Field(..., description="Relevance score")
|
|
highlights: Optional[str] = Field(None, description="Highlighted matching text")
|
|
collection: Optional[str] = Field(None, description="Collection name")
|
|
document_type: Optional[str] = Field(None, description="Document type")
|
|
content_preview: Optional[str] = Field(None, description="Content preview if requested")
|
|
|
|
|
|
class DocumentSearchResponse(BaseModel):
|
|
"""Response from document search."""
|
|
query: str = Field(..., description="Original query")
|
|
hits: List[DocumentSearchHit] = Field(..., description="Search results")
|
|
total: int = Field(..., description="Total matching documents")
|
|
duration_ms: int = Field(..., description="Search duration in milliseconds")
|
|
|
|
|
|
# =============================================================================
|
|
# Health Check Models
|
|
# =============================================================================
|
|
|
|
|
|
class DocumentStoreHealth(BaseModel):
|
|
"""Health status of document storage components."""
|
|
paperless_healthy: bool = Field(..., description="Paperless-ngx responding")
|
|
paperless_version: Optional[str] = Field(None, description="Paperless version")
|
|
total_documents: Optional[int] = Field(None, description="Total documents in Paperless")
|
|
indexed_documents: Optional[int] = Field(None, description="Documents indexed in Library Desk")
|