feat(library-desk): add wiki models for page operations
- Add WikiPage model for page data - Add WikiPageUpdate model for partial updates - Add field validators for tags and descriptions - Support optional fields for flexible updates Used by wiki_service and entity_linking router
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Pydantic models for Wiki.js operations.
|
||||
|
||||
Models for:
|
||||
- Wiki pages (CRUD operations)
|
||||
- Dossiers (tag-based collections)
|
||||
- Search results
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
# Base models
|
||||
class WikiPageBase(BaseModel):
|
||||
"""Base wiki page fields."""
|
||||
title: str = Field(..., min_length=1, max_length=500, description="Page title")
|
||||
description: Optional[str] = Field(None, max_length=1000, description="Page description")
|
||||
tags: List[str] = Field(default_factory=list, description="Tags (for dossier organization)")
|
||||
is_published: bool = Field(default=True, description="Whether page is published")
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def validate_tags(cls, v: List[str]) -> List[str]:
|
||||
"""Validate and clean tags."""
|
||||
# Remove empty tags and strip whitespace
|
||||
cleaned = [tag.strip() for tag in v if tag.strip()]
|
||||
# Ensure uniqueness
|
||||
return list(set(cleaned))
|
||||
|
||||
|
||||
class WikiPageCreate(WikiPageBase):
|
||||
"""Request model for creating a wiki page."""
|
||||
content: str = Field(..., description="Page content (markdown)")
|
||||
path: str = Field(..., min_length=1, max_length=500, description="Page path (e.g., '/projects/library-desk')")
|
||||
editor: str = Field(default="markdown", description="Editor type")
|
||||
user: Optional[str] = Field(None, description="User identifier (defaults to configured user)")
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_path(cls, v: str) -> str:
|
||||
"""Validate page path."""
|
||||
# Ensure path starts with /
|
||||
if not v.startswith("/"):
|
||||
v = f"/{v}"
|
||||
# Remove trailing slash
|
||||
if v.endswith("/") and v != "/":
|
||||
v = v.rstrip("/")
|
||||
return v
|
||||
|
||||
|
||||
class WikiPageUpdate(BaseModel):
|
||||
"""Request model for updating a wiki page."""
|
||||
content: Optional[str] = Field(None, description="Updated content")
|
||||
title: Optional[str] = Field(None, min_length=1, max_length=500, description="Updated title")
|
||||
description: Optional[str] = Field(None, max_length=1000, description="Updated description")
|
||||
tags: Optional[List[str]] = Field(None, description="Updated tags")
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def validate_tags(cls, v: Optional[List[str]]) -> Optional[List[str]]:
|
||||
"""Validate and clean tags."""
|
||||
if v is None:
|
||||
return None
|
||||
cleaned = [tag.strip() for tag in v if tag.strip()]
|
||||
return list(set(cleaned))
|
||||
|
||||
|
||||
class WikiPage(WikiPageBase):
|
||||
"""Response model for a wiki page."""
|
||||
id: int = Field(..., description="Page ID")
|
||||
path: str = Field(..., description="Page path")
|
||||
content: Optional[str] = Field(None, description="Page content")
|
||||
created_at: Optional[str] = Field(None, description="Creation timestamp")
|
||||
updated_at: Optional[str] = Field(None, description="Last update timestamp")
|
||||
editor: Optional[str] = Field(None, description="Editor type")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class WikiPageSummary(BaseModel):
|
||||
"""Summarized wiki page (for list responses)."""
|
||||
id: int = Field(..., description="Page ID")
|
||||
path: str = Field(..., description="Page path")
|
||||
title: str = Field(..., description="Page title")
|
||||
description: Optional[str] = Field(None, description="Page description")
|
||||
tags: List[str] = Field(default_factory=list, description="Page tags")
|
||||
updated_at: Optional[str] = Field(None, description="Last update timestamp")
|
||||
is_published: bool = Field(..., description="Publication status")
|
||||
|
||||
|
||||
class WikiPageList(BaseModel):
|
||||
"""Response model for list of pages."""
|
||||
pages: List[WikiPageSummary] = Field(..., description="List of pages")
|
||||
total: int = Field(..., description="Total number of pages")
|
||||
filtered_by_tag: Optional[str] = Field(None, description="Tag filter applied")
|
||||
user: str = Field(..., description="User namespace")
|
||||
|
||||
|
||||
# Dossier models
|
||||
class DossierCreate(BaseModel):
|
||||
"""Request model for creating a dossier."""
|
||||
name: str = Field(..., min_length=1, max_length=100, description="Dossier name (becomes a tag)")
|
||||
title: str = Field(..., min_length=1, max_length=200, description="Human-readable title")
|
||||
description: str = Field(..., min_length=1, description="Dossier description")
|
||||
create_index_page: bool = Field(default=True, description="Create an index page for the dossier")
|
||||
user: Optional[str] = Field(None, description="User identifier")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, v: str) -> str:
|
||||
"""Validate dossier name (will be used as tag)."""
|
||||
# Convert to lowercase, replace spaces with hyphens
|
||||
name = v.lower().strip()
|
||||
name = name.replace(" ", "-")
|
||||
# Remove special characters except hyphens and underscores
|
||||
name = "".join(c for c in name if c.isalnum() or c in "-_")
|
||||
if not name:
|
||||
raise ValueError("Dossier name must contain alphanumeric characters")
|
||||
return name
|
||||
|
||||
|
||||
class DossierInfo(BaseModel):
|
||||
"""Response model for dossier information."""
|
||||
name: str = Field(..., description="Dossier name (tag)")
|
||||
title: str = Field(..., description="Dossier title")
|
||||
description: str = Field(..., description="Dossier description")
|
||||
page_count: int = Field(..., description="Number of pages in dossier")
|
||||
index_page_id: Optional[int] = Field(None, description="ID of index page")
|
||||
index_page_path: Optional[str] = Field(None, description="Path to index page")
|
||||
created_at: Optional[str] = Field(None, description="Creation timestamp")
|
||||
|
||||
|
||||
class DossierList(BaseModel):
|
||||
"""Response model for list of dossiers."""
|
||||
dossiers: List[DossierInfo] = Field(..., description="List of dossiers")
|
||||
total: int = Field(..., description="Total number of dossiers")
|
||||
user: str = Field(..., description="User namespace")
|
||||
|
||||
|
||||
# Search models
|
||||
class WikiSearchResult(BaseModel):
|
||||
"""Search result item."""
|
||||
id: int = Field(..., description="Page ID")
|
||||
path: str = Field(..., description="Page path")
|
||||
title: str = Field(..., description="Page title")
|
||||
description: Optional[str] = Field(None, description="Page description")
|
||||
relevance: Optional[float] = Field(None, description="Search relevance score")
|
||||
|
||||
|
||||
class WikiSearchResponse(BaseModel):
|
||||
"""Response model for search results."""
|
||||
results: List[WikiSearchResult] = Field(..., description="Search results")
|
||||
query: str = Field(..., description="Search query")
|
||||
total: int = Field(..., description="Total results found")
|
||||
|
||||
|
||||
# Move/rename models
|
||||
class WikiPageMove(BaseModel):
|
||||
"""Request model for moving/renaming a page."""
|
||||
new_path: str = Field(..., min_length=1, description="New page path")
|
||||
locale: str = Field(default="en", description="Page locale")
|
||||
|
||||
@field_validator("new_path")
|
||||
@classmethod
|
||||
def validate_new_path(cls, v: str) -> str:
|
||||
"""Validate new path."""
|
||||
if not v.startswith("/"):
|
||||
v = f"/{v}"
|
||||
if v.endswith("/") and v != "/":
|
||||
v = v.rstrip("/")
|
||||
return v
|
||||
|
||||
|
||||
# Response models for operations
|
||||
class WikiOperationResponse(BaseModel):
|
||||
"""Generic response for wiki operations."""
|
||||
success: bool = Field(..., description="Whether operation succeeded")
|
||||
message: str = Field(..., description="Operation message")
|
||||
page_id: Optional[int] = Field(None, description="Page ID (if applicable)")
|
||||
page_path: Optional[str] = Field(None, description="Page path (if applicable)")
|
||||
|
||||
|
||||
class DossierOperationResponse(BaseModel):
|
||||
"""Response for dossier operations."""
|
||||
success: bool = Field(..., description="Whether operation succeeded")
|
||||
message: str = Field(..., description="Operation message")
|
||||
dossier_name: str = Field(..., description="Dossier name")
|
||||
index_page_id: Optional[int] = Field(None, description="Index page ID (if created)")
|
||||
index_page_path: Optional[str] = Field(None, description="Index page path (if created)")
|
||||
Reference in New Issue
Block a user