feat(server): Add structured output to all tool/resource output
BREAKING CHANGE
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""Pydantic models for structured MCP server responses."""
|
||||
|
||||
# Base models
|
||||
from .base import (
|
||||
BaseResponse,
|
||||
ErrorResponse,
|
||||
SuccessResponse,
|
||||
PaginatedResponse,
|
||||
IdResponse,
|
||||
StatusResponse,
|
||||
)
|
||||
|
||||
# Notes models
|
||||
from .notes import (
|
||||
Note,
|
||||
NoteSearchResult,
|
||||
NotesSettings,
|
||||
CreateNoteResponse,
|
||||
UpdateNoteResponse,
|
||||
DeleteNoteResponse,
|
||||
AppendContentResponse,
|
||||
SearchNotesResponse,
|
||||
)
|
||||
|
||||
# Calendar models
|
||||
from .calendar import (
|
||||
Calendar,
|
||||
CalendarEvent,
|
||||
CalendarEventSummary,
|
||||
CreateEventResponse,
|
||||
UpdateEventResponse,
|
||||
DeleteEventResponse,
|
||||
ListEventsResponse,
|
||||
ListCalendarsResponse,
|
||||
AvailabilitySlot,
|
||||
FindAvailabilityResponse,
|
||||
BulkOperationResult,
|
||||
BulkOperationResponse,
|
||||
CreateMeetingResponse,
|
||||
UpcomingEventsResponse,
|
||||
ManageCalendarResponse,
|
||||
)
|
||||
|
||||
# Contacts models
|
||||
from .contacts import (
|
||||
AddressBook,
|
||||
Contact,
|
||||
ContactField,
|
||||
ListAddressBooksResponse,
|
||||
ListContactsResponse,
|
||||
CreateContactResponse,
|
||||
UpdateContactResponse,
|
||||
DeleteContactResponse,
|
||||
CreateAddressBookResponse,
|
||||
DeleteAddressBookResponse,
|
||||
)
|
||||
|
||||
# Tables models
|
||||
from .tables import (
|
||||
Table,
|
||||
TableColumn,
|
||||
TableRow,
|
||||
TableView,
|
||||
TableSchema,
|
||||
ListTablesResponse,
|
||||
GetSchemaResponse,
|
||||
ReadTableResponse,
|
||||
CreateRowResponse,
|
||||
UpdateRowResponse,
|
||||
DeleteRowResponse,
|
||||
)
|
||||
|
||||
# WebDAV models
|
||||
from .webdav import (
|
||||
FileInfo,
|
||||
DirectoryListing,
|
||||
ReadFileResponse,
|
||||
WriteFileResponse,
|
||||
CreateDirectoryResponse,
|
||||
DeleteResourceResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base models
|
||||
"BaseResponse",
|
||||
"ErrorResponse",
|
||||
"SuccessResponse",
|
||||
"PaginatedResponse",
|
||||
"IdResponse",
|
||||
"StatusResponse",
|
||||
# Notes models
|
||||
"Note",
|
||||
"NoteSearchResult",
|
||||
"NotesSettings",
|
||||
"CreateNoteResponse",
|
||||
"UpdateNoteResponse",
|
||||
"DeleteNoteResponse",
|
||||
"AppendContentResponse",
|
||||
"SearchNotesResponse",
|
||||
# Calendar models
|
||||
"Calendar",
|
||||
"CalendarEvent",
|
||||
"CalendarEventSummary",
|
||||
"CreateEventResponse",
|
||||
"UpdateEventResponse",
|
||||
"DeleteEventResponse",
|
||||
"ListEventsResponse",
|
||||
"ListCalendarsResponse",
|
||||
"AvailabilitySlot",
|
||||
"FindAvailabilityResponse",
|
||||
"BulkOperationResult",
|
||||
"BulkOperationResponse",
|
||||
"CreateMeetingResponse",
|
||||
"UpcomingEventsResponse",
|
||||
"ManageCalendarResponse",
|
||||
# Contacts models
|
||||
"AddressBook",
|
||||
"Contact",
|
||||
"ContactField",
|
||||
"ListAddressBooksResponse",
|
||||
"ListContactsResponse",
|
||||
"CreateContactResponse",
|
||||
"UpdateContactResponse",
|
||||
"DeleteContactResponse",
|
||||
"CreateAddressBookResponse",
|
||||
"DeleteAddressBookResponse",
|
||||
# Tables models
|
||||
"Table",
|
||||
"TableColumn",
|
||||
"TableRow",
|
||||
"TableView",
|
||||
"TableSchema",
|
||||
"ListTablesResponse",
|
||||
"GetSchemaResponse",
|
||||
"ReadTableResponse",
|
||||
"CreateRowResponse",
|
||||
"UpdateRowResponse",
|
||||
"DeleteRowResponse",
|
||||
# WebDAV models
|
||||
"FileInfo",
|
||||
"DirectoryListing",
|
||||
"ReadFileResponse",
|
||||
"WriteFileResponse",
|
||||
"CreateDirectoryResponse",
|
||||
"DeleteResourceResponse",
|
||||
]
|
||||
@@ -0,0 +1,70 @@
|
||||
"""Base Pydantic models for common response patterns."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Generic, List, Optional, TypeVar, Union
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class BaseResponse(BaseModel):
|
||||
"""Base response model for all MCP tool responses."""
|
||||
|
||||
model_config = {"json_encoders": {datetime: lambda v: v.isoformat()}}
|
||||
|
||||
success: bool = Field(
|
||||
default=True, description="Whether the operation was successful"
|
||||
)
|
||||
timestamp: datetime = Field(
|
||||
default_factory=datetime.now, description="Response timestamp"
|
||||
)
|
||||
|
||||
|
||||
class ErrorResponse(BaseResponse):
|
||||
"""Response model for error cases."""
|
||||
|
||||
success: bool = Field(default=False, description="Always False for error responses")
|
||||
error: str = Field(description="Error message")
|
||||
error_code: Optional[str] = Field(None, description="Optional error code")
|
||||
details: Optional[Dict[str, Any]] = Field(
|
||||
None, description="Additional error details"
|
||||
)
|
||||
|
||||
|
||||
class SuccessResponse(BaseResponse):
|
||||
"""Generic success response."""
|
||||
|
||||
message: Optional[str] = Field(None, description="Optional success message")
|
||||
data: Optional[Dict[str, Any]] = Field(None, description="Optional response data")
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class PaginatedResponse(BaseResponse, Generic[T]):
|
||||
"""Generic paginated response model."""
|
||||
|
||||
items: List[T] = Field(description="List of items")
|
||||
total_count: Optional[int] = Field(
|
||||
None, description="Total number of items available"
|
||||
)
|
||||
page: Optional[int] = Field(None, description="Current page number")
|
||||
page_size: Optional[int] = Field(None, description="Number of items per page")
|
||||
has_more: Optional[bool] = Field(
|
||||
None, description="Whether more items are available"
|
||||
)
|
||||
cursor: Optional[str] = Field(
|
||||
None, description="Cursor for next page (if using cursor-based pagination)"
|
||||
)
|
||||
|
||||
|
||||
class IdResponse(BaseResponse):
|
||||
"""Response model for operations that return a new ID."""
|
||||
|
||||
id: Union[int, str] = Field(description="ID of the created or affected resource")
|
||||
|
||||
|
||||
class StatusResponse(BaseResponse):
|
||||
"""Response model for operations that return just a status."""
|
||||
|
||||
status_code: Optional[int] = Field(None, description="HTTP status code")
|
||||
message: Optional[str] = Field(None, description="Status message")
|
||||
@@ -0,0 +1,182 @@
|
||||
"""Pydantic models for Calendar app responses."""
|
||||
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import BaseResponse, StatusResponse
|
||||
|
||||
|
||||
class Calendar(BaseModel):
|
||||
"""Model for a Nextcloud calendar."""
|
||||
|
||||
name: str = Field(description="Calendar name/ID")
|
||||
display_name: str = Field(description="Calendar display name")
|
||||
description: Optional[str] = Field(None, description="Calendar description")
|
||||
color: Optional[str] = Field(None, description="Calendar color")
|
||||
href: Optional[str] = Field(None, description="Calendar DAV href")
|
||||
timezone: Optional[str] = Field(None, description="Calendar timezone")
|
||||
enabled: bool = Field(default=True, description="Whether calendar is enabled")
|
||||
ctag: Optional[str] = Field(None, description="Calendar tag for synchronization")
|
||||
|
||||
|
||||
class CalendarEventSummary(BaseModel):
|
||||
"""Model for calendar event summary (for lists)."""
|
||||
|
||||
uid: str = Field(description="Event UID")
|
||||
summary: str = Field(description="Event summary/title")
|
||||
start: str = Field(description="Event start datetime (ISO format)")
|
||||
end: Optional[str] = Field(None, description="Event end datetime (ISO format)")
|
||||
all_day: bool = Field(default=False, description="Whether event is all-day")
|
||||
location: Optional[str] = Field(None, description="Event location")
|
||||
description: Optional[str] = Field(None, description="Event description")
|
||||
categories: List[str] = Field(default_factory=list, description="Event categories")
|
||||
status: Optional[str] = Field(
|
||||
None, description="Event status (CONFIRMED, TENTATIVE, CANCELLED)"
|
||||
)
|
||||
|
||||
|
||||
class CalendarEvent(CalendarEventSummary):
|
||||
"""Model for a complete calendar event."""
|
||||
|
||||
created: Optional[str] = Field(None, description="Event creation datetime")
|
||||
last_modified: Optional[str] = Field(None, description="Last modification datetime")
|
||||
recurring: bool = Field(default=False, description="Whether event is recurring")
|
||||
recurrence_rule: Optional[str] = Field(None, description="RFC5545 recurrence rule")
|
||||
recurrence_end: Optional[str] = Field(None, description="Recurrence end date")
|
||||
attendees: List[str] = Field(
|
||||
default_factory=list, description="List of attendee email addresses"
|
||||
)
|
||||
organizer: Optional[str] = Field(None, description="Event organizer")
|
||||
priority: Optional[int] = Field(None, description="Event priority (1-9)")
|
||||
privacy: Optional[str] = Field(None, description="Event privacy level")
|
||||
url: Optional[str] = Field(None, description="Event URL")
|
||||
duration_minutes: Optional[int] = Field(
|
||||
None, description="Event duration in minutes"
|
||||
)
|
||||
reminder_minutes: Optional[int] = Field(
|
||||
None, description="Reminder time in minutes before event"
|
||||
)
|
||||
reminder_email: bool = Field(
|
||||
default=False, description="Whether to send email reminder"
|
||||
)
|
||||
color: Optional[str] = Field(None, description="Event color")
|
||||
etag: Optional[str] = Field(None, description="ETag for versioning")
|
||||
|
||||
|
||||
class CreateEventResponse(BaseResponse):
|
||||
"""Response model for event creation."""
|
||||
|
||||
event: CalendarEvent = Field(description="The created event")
|
||||
calendar_name: str = Field(
|
||||
description="Name of the calendar the event was created in"
|
||||
)
|
||||
|
||||
|
||||
class UpdateEventResponse(BaseResponse):
|
||||
"""Response model for event updates."""
|
||||
|
||||
event: CalendarEvent = Field(description="The updated event")
|
||||
calendar_name: str = Field(description="Name of the calendar the event belongs to")
|
||||
|
||||
|
||||
class DeleteEventResponse(StatusResponse):
|
||||
"""Response model for event deletion."""
|
||||
|
||||
deleted_uid: str = Field(description="UID of the deleted event")
|
||||
calendar_name: str = Field(
|
||||
description="Name of the calendar the event was deleted from"
|
||||
)
|
||||
|
||||
|
||||
class ListEventsResponse(BaseResponse):
|
||||
"""Response model for listing events."""
|
||||
|
||||
events: List[CalendarEventSummary] = Field(description="List of events")
|
||||
calendar_name: Optional[str] = Field(
|
||||
None, description="Calendar name (if filtered to one calendar)"
|
||||
)
|
||||
start_date: Optional[str] = Field(None, description="Start date filter applied")
|
||||
end_date: Optional[str] = Field(None, description="End date filter applied")
|
||||
total_found: int = Field(description="Total number of events found")
|
||||
|
||||
|
||||
class ListCalendarsResponse(BaseResponse):
|
||||
"""Response model for listing calendars."""
|
||||
|
||||
calendars: List[Calendar] = Field(description="List of available calendars")
|
||||
total_count: int = Field(description="Total number of calendars")
|
||||
|
||||
|
||||
class AvailabilitySlot(BaseModel):
|
||||
"""Model for an available time slot."""
|
||||
|
||||
start: str = Field(description="Slot start datetime (ISO format)")
|
||||
end: str = Field(description="Slot end datetime (ISO format)")
|
||||
duration_minutes: int = Field(description="Slot duration in minutes")
|
||||
date: str = Field(description="Date of the slot (YYYY-MM-DD)")
|
||||
|
||||
|
||||
class FindAvailabilityResponse(BaseResponse):
|
||||
"""Response model for finding availability."""
|
||||
|
||||
available_slots: List[AvailabilitySlot] = Field(
|
||||
description="List of available time slots"
|
||||
)
|
||||
duration_requested: int = Field(description="Requested duration in minutes")
|
||||
date_range_start: str = Field(description="Start date of search range")
|
||||
date_range_end: str = Field(description="End date of search range")
|
||||
attendees_checked: List[str] = Field(
|
||||
default_factory=list, description="Attendees checked for availability"
|
||||
)
|
||||
business_hours_only: bool = Field(
|
||||
description="Whether search was limited to business hours"
|
||||
)
|
||||
|
||||
|
||||
class BulkOperationResult(BaseModel):
|
||||
"""Model for bulk operation results."""
|
||||
|
||||
operation: str = Field(description="Operation performed (update, delete, move)")
|
||||
events_processed: int = Field(description="Number of events processed")
|
||||
events_successful: int = Field(
|
||||
description="Number of events successfully processed"
|
||||
)
|
||||
events_failed: int = Field(description="Number of events that failed processing")
|
||||
failed_events: List[str] = Field(
|
||||
default_factory=list, description="UIDs of events that failed"
|
||||
)
|
||||
errors: List[str] = Field(default_factory=list, description="Error messages")
|
||||
|
||||
|
||||
class BulkOperationResponse(BaseResponse):
|
||||
"""Response model for bulk operations."""
|
||||
|
||||
result: BulkOperationResult = Field(description="Bulk operation result")
|
||||
|
||||
|
||||
class CreateMeetingResponse(CreateEventResponse):
|
||||
"""Response model for meeting creation (same as event creation)."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UpcomingEventsResponse(BaseResponse):
|
||||
"""Response model for upcoming events."""
|
||||
|
||||
events: List[CalendarEventSummary] = Field(description="List of upcoming events")
|
||||
days_ahead: int = Field(description="Number of days ahead searched")
|
||||
calendar_name: Optional[str] = Field(
|
||||
None, description="Calendar name (if filtered to one calendar)"
|
||||
)
|
||||
|
||||
|
||||
class ManageCalendarResponse(BaseResponse):
|
||||
"""Response model for calendar management operations."""
|
||||
|
||||
action: str = Field(description="Action performed (create, delete, update, list)")
|
||||
calendar: Optional[Calendar] = Field(None, description="Calendar that was affected")
|
||||
calendars: Optional[List[Calendar]] = Field(
|
||||
None, description="List of calendars (for list action)"
|
||||
)
|
||||
message: str = Field(description="Success message")
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Pydantic models for Contacts app responses."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import BaseResponse, StatusResponse
|
||||
|
||||
|
||||
class AddressBook(BaseModel):
|
||||
"""Model for a Nextcloud address book."""
|
||||
|
||||
uri: str = Field(description="Address book URI")
|
||||
displayname: str = Field(description="Address book display name")
|
||||
description: Optional[str] = Field(None, description="Address book description")
|
||||
ctag: Optional[str] = Field(
|
||||
None, description="Address book tag for synchronization"
|
||||
)
|
||||
|
||||
|
||||
class ContactField(BaseModel):
|
||||
"""Model for a contact field (email, phone, etc.)."""
|
||||
|
||||
type: str = Field(description="Field type (e.g., 'email', 'phone', 'address')")
|
||||
value: str = Field(description="Field value")
|
||||
label: Optional[str] = Field(None, description="Field label (e.g., 'work', 'home')")
|
||||
preferred: bool = Field(
|
||||
default=False, description="Whether this is the preferred field of this type"
|
||||
)
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
"""Model for a Nextcloud contact."""
|
||||
|
||||
uid: str = Field(description="Contact UID")
|
||||
fn: str = Field(description="Full name (formatted name)")
|
||||
given_name: Optional[str] = Field(None, description="Given name")
|
||||
family_name: Optional[str] = Field(None, description="Family name")
|
||||
organization: Optional[str] = Field(None, description="Organization")
|
||||
title: Optional[str] = Field(None, description="Job title")
|
||||
emails: List[ContactField] = Field(
|
||||
default_factory=list, description="Email addresses"
|
||||
)
|
||||
phones: List[ContactField] = Field(
|
||||
default_factory=list, description="Phone numbers"
|
||||
)
|
||||
addresses: List[ContactField] = Field(default_factory=list, description="Addresses")
|
||||
urls: List[ContactField] = Field(default_factory=list, description="URLs")
|
||||
note: Optional[str] = Field(None, description="Notes")
|
||||
photo: Optional[str] = Field(None, description="Photo URL or base64 data")
|
||||
birthday: Optional[str] = Field(None, description="Birthday (ISO date format)")
|
||||
categories: List[str] = Field(
|
||||
default_factory=list, description="Contact categories"
|
||||
)
|
||||
custom_fields: Dict[str, Any] = Field(
|
||||
default_factory=dict, description="Custom fields"
|
||||
)
|
||||
etag: Optional[str] = Field(None, description="ETag for versioning")
|
||||
|
||||
@property
|
||||
def primary_email(self) -> Optional[str]:
|
||||
"""Get the primary email address."""
|
||||
if not self.emails:
|
||||
return None
|
||||
# Return preferred email if available, otherwise first email
|
||||
preferred = next(
|
||||
(email.value for email in self.emails if email.preferred), None
|
||||
)
|
||||
return preferred or self.emails[0].value
|
||||
|
||||
@property
|
||||
def primary_phone(self) -> Optional[str]:
|
||||
"""Get the primary phone number."""
|
||||
if not self.phones:
|
||||
return None
|
||||
# Return preferred phone if available, otherwise first phone
|
||||
preferred = next(
|
||||
(phone.value for phone in self.phones if phone.preferred), None
|
||||
)
|
||||
return preferred or self.phones[0].value
|
||||
|
||||
|
||||
class ListAddressBooksResponse(BaseResponse):
|
||||
"""Response model for listing address books."""
|
||||
|
||||
addressbooks: List[AddressBook] = Field(
|
||||
description="List of available address books"
|
||||
)
|
||||
total_count: int = Field(description="Total number of address books")
|
||||
|
||||
|
||||
class ListContactsResponse(BaseResponse):
|
||||
"""Response model for listing contacts."""
|
||||
|
||||
contacts: List[Contact] = Field(description="List of contacts")
|
||||
addressbook: str = Field(description="Address book name")
|
||||
total_count: int = Field(description="Total number of contacts")
|
||||
|
||||
|
||||
class CreateContactResponse(BaseResponse):
|
||||
"""Response model for contact creation."""
|
||||
|
||||
contact: Contact = Field(description="The created contact")
|
||||
addressbook: str = Field(description="Address book the contact was created in")
|
||||
|
||||
|
||||
class UpdateContactResponse(BaseResponse):
|
||||
"""Response model for contact updates."""
|
||||
|
||||
contact: Contact = Field(description="The updated contact")
|
||||
addressbook: str = Field(description="Address book the contact belongs to")
|
||||
|
||||
|
||||
class DeleteContactResponse(StatusResponse):
|
||||
"""Response model for contact deletion."""
|
||||
|
||||
deleted_uid: str = Field(description="UID of the deleted contact")
|
||||
addressbook: str = Field(description="Address book the contact was deleted from")
|
||||
|
||||
|
||||
class CreateAddressBookResponse(BaseResponse):
|
||||
"""Response model for address book creation."""
|
||||
|
||||
addressbook: AddressBook = Field(description="The created address book")
|
||||
|
||||
|
||||
class DeleteAddressBookResponse(StatusResponse):
|
||||
"""Response model for address book deletion."""
|
||||
|
||||
deleted_name: str = Field(description="Name of the deleted address book")
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Pydantic models for Notes app responses."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import BaseResponse, IdResponse, StatusResponse
|
||||
|
||||
|
||||
class Note(BaseModel):
|
||||
"""Model for a Nextcloud note."""
|
||||
|
||||
id: int = Field(description="Note ID")
|
||||
title: str = Field(description="Note title")
|
||||
content: str = Field(description="Note content in markdown")
|
||||
category: str = Field(default="", description="Note category")
|
||||
modified: int = Field(description="Unix timestamp of last modification")
|
||||
favorite: bool = Field(
|
||||
default=False, description="Whether note is marked as favorite"
|
||||
)
|
||||
etag: Optional[str] = Field(None, description="ETag for versioning")
|
||||
readonly: bool = Field(default=False, description="Whether note is read-only")
|
||||
|
||||
@property
|
||||
def modified_datetime(self) -> datetime:
|
||||
"""Convert Unix timestamp to datetime."""
|
||||
return datetime.fromtimestamp(self.modified)
|
||||
|
||||
|
||||
class NoteSearchResult(BaseModel):
|
||||
"""Model for note search results (limited fields)."""
|
||||
|
||||
id: int = Field(description="Note ID")
|
||||
title: str = Field(description="Note title")
|
||||
category: str = Field(default="", description="Note category")
|
||||
score: Optional[float] = Field(None, description="Search relevance score")
|
||||
|
||||
|
||||
class NotesSettings(BaseModel):
|
||||
"""Model for Notes app settings."""
|
||||
|
||||
notesPath: str = Field(description="Path to notes directory")
|
||||
fileSuffix: str = Field(description="File suffix for notes")
|
||||
noteMode: str = Field(description="Note mode setting")
|
||||
|
||||
|
||||
class CreateNoteResponse(IdResponse):
|
||||
"""Response model for note creation."""
|
||||
|
||||
note: Note = Field(description="The created note")
|
||||
|
||||
|
||||
class UpdateNoteResponse(BaseResponse):
|
||||
"""Response model for note updates."""
|
||||
|
||||
note: Note = Field(description="The updated note")
|
||||
|
||||
|
||||
class DeleteNoteResponse(StatusResponse):
|
||||
"""Response model for note deletion."""
|
||||
|
||||
deleted_id: int = Field(description="ID of the deleted note")
|
||||
|
||||
|
||||
class AppendContentResponse(BaseResponse):
|
||||
"""Response model for appending content to a note."""
|
||||
|
||||
note: Note = Field(description="The updated note after appending content")
|
||||
|
||||
|
||||
class SearchNotesResponse(BaseResponse):
|
||||
"""Response model for note search."""
|
||||
|
||||
results: List[NoteSearchResult] = Field(description="Search results")
|
||||
query: str = Field(description="The search query used")
|
||||
total_found: int = Field(description="Total number of notes found")
|
||||
@@ -0,0 +1,142 @@
|
||||
"""Pydantic models for Tables app responses."""
|
||||
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import BaseResponse, IdResponse, StatusResponse
|
||||
|
||||
|
||||
class TableColumn(BaseModel):
|
||||
"""Model for a table column definition."""
|
||||
|
||||
id: int = Field(description="Column ID")
|
||||
title: str = Field(description="Column title")
|
||||
type: str = Field(description="Column type (text, number, datetime, etc.)")
|
||||
subtype: Optional[str] = Field(None, description="Column subtype")
|
||||
mandatory: bool = Field(default=False, description="Whether column is mandatory")
|
||||
description: Optional[str] = Field(None, description="Column description")
|
||||
text_default: Optional[str] = Field(None, description="Default text value")
|
||||
text_allowed_pattern: Optional[str] = Field(
|
||||
None, description="Allowed text pattern"
|
||||
)
|
||||
text_max_length: Optional[int] = Field(None, description="Maximum text length")
|
||||
number_default: Optional[float] = Field(None, description="Default number value")
|
||||
number_min: Optional[float] = Field(None, description="Minimum number value")
|
||||
number_max: Optional[float] = Field(None, description="Maximum number value")
|
||||
number_decimals: Optional[int] = Field(None, description="Number of decimal places")
|
||||
datetime_default: Optional[str] = Field(None, description="Default datetime value")
|
||||
selection_options: List[str] = Field(
|
||||
default_factory=list, description="Selection options"
|
||||
)
|
||||
selection_default: Optional[str] = Field(
|
||||
None, description="Default selection value"
|
||||
)
|
||||
|
||||
|
||||
class TableRow(BaseModel):
|
||||
"""Model for a table row."""
|
||||
|
||||
id: int = Field(description="Row ID")
|
||||
created_by: Optional[str] = Field(None, description="User who created the row")
|
||||
created_at: Optional[str] = Field(None, description="Row creation timestamp")
|
||||
last_edit_by: Optional[str] = Field(
|
||||
None, description="User who last edited the row"
|
||||
)
|
||||
last_edit_at: Optional[str] = Field(None, description="Last edit timestamp")
|
||||
data: Dict[int, Any] = Field(description="Row data keyed by column ID")
|
||||
|
||||
|
||||
class TableView(BaseModel):
|
||||
"""Model for a table view."""
|
||||
|
||||
id: int = Field(description="View ID")
|
||||
title: str = Field(description="View title")
|
||||
emoji: Optional[str] = Field(None, description="View emoji")
|
||||
description: Optional[str] = Field(None, description="View description")
|
||||
columns: List[int] = Field(
|
||||
default_factory=list, description="List of column IDs in this view"
|
||||
)
|
||||
sort: List[Dict[str, Any]] = Field(
|
||||
default_factory=list, description="Sort configuration"
|
||||
)
|
||||
filter: List[Dict[str, Any]] = Field(
|
||||
default_factory=list, description="Filter configuration"
|
||||
)
|
||||
|
||||
|
||||
class Table(BaseModel):
|
||||
"""Model for a Nextcloud table."""
|
||||
|
||||
id: int = Field(description="Table ID")
|
||||
title: str = Field(description="Table title")
|
||||
emoji: Optional[str] = Field(None, description="Table emoji")
|
||||
ownership: str = Field(description="Table ownership")
|
||||
owner_display_name: str = Field(description="Display name of table owner")
|
||||
created_by: Optional[str] = Field(None, description="User who created the table")
|
||||
created_at: Optional[str] = Field(None, description="Table creation timestamp")
|
||||
last_edit_by: Optional[str] = Field(
|
||||
None, description="User who last edited the table"
|
||||
)
|
||||
last_edit_at: Optional[str] = Field(None, description="Last edit timestamp")
|
||||
row_count: int = Field(default=0, description="Number of rows in the table")
|
||||
has_shares: bool = Field(default=False, description="Whether table is shared")
|
||||
archived: bool = Field(default=False, description="Whether table is archived")
|
||||
is_shared: bool = Field(
|
||||
default=False, description="Whether table is shared with current user"
|
||||
)
|
||||
on_share_permissions: Optional[Dict[str, Any]] = Field(
|
||||
None, description="Share permissions"
|
||||
)
|
||||
|
||||
|
||||
class TableSchema(BaseModel):
|
||||
"""Model for complete table schema including columns and views."""
|
||||
|
||||
table: Table = Field(description="Table information")
|
||||
columns: List[TableColumn] = Field(description="Table columns")
|
||||
views: List[TableView] = Field(description="Table views")
|
||||
|
||||
|
||||
class ListTablesResponse(BaseResponse):
|
||||
"""Response model for listing tables."""
|
||||
|
||||
tables: List[Table] = Field(description="List of available tables")
|
||||
total_count: int = Field(description="Total number of tables")
|
||||
|
||||
|
||||
class GetSchemaResponse(BaseResponse):
|
||||
"""Response model for getting table schema."""
|
||||
|
||||
table_schema: TableSchema = Field(description="Table schema information")
|
||||
|
||||
|
||||
class ReadTableResponse(BaseResponse):
|
||||
"""Response model for reading table rows."""
|
||||
|
||||
rows: List[TableRow] = Field(description="Table rows")
|
||||
table_id: int = Field(description="Table ID")
|
||||
total_count: Optional[int] = Field(
|
||||
None, description="Total number of rows (if known)"
|
||||
)
|
||||
offset: Optional[int] = Field(None, description="Offset used for pagination")
|
||||
limit: Optional[int] = Field(None, description="Limit used for pagination")
|
||||
|
||||
|
||||
class CreateRowResponse(IdResponse):
|
||||
"""Response model for row creation."""
|
||||
|
||||
row: TableRow = Field(description="The created row")
|
||||
table_id: int = Field(description="Table ID the row was created in")
|
||||
|
||||
|
||||
class UpdateRowResponse(BaseResponse):
|
||||
"""Response model for row updates."""
|
||||
|
||||
row: TableRow = Field(description="The updated row")
|
||||
|
||||
|
||||
class DeleteRowResponse(StatusResponse):
|
||||
"""Response model for row deletion."""
|
||||
|
||||
deleted_id: int = Field(description="ID of the deleted row")
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Pydantic models for WebDAV responses."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .base import BaseResponse, StatusResponse
|
||||
|
||||
|
||||
class FileInfo(BaseModel):
|
||||
"""Model for file/directory information."""
|
||||
|
||||
name: str = Field(description="File/directory name")
|
||||
path: str = Field(description="Full path")
|
||||
is_directory: bool = Field(description="Whether this is a directory")
|
||||
size: Optional[int] = Field(
|
||||
None, description="File size in bytes (None for directories)"
|
||||
)
|
||||
content_type: Optional[str] = Field(None, description="MIME content type")
|
||||
last_modified: Optional[str] = Field(
|
||||
None, description="Last modification time (ISO format)"
|
||||
)
|
||||
etag: Optional[str] = Field(None, description="ETag for versioning")
|
||||
|
||||
@property
|
||||
def last_modified_datetime(self) -> Optional[datetime]:
|
||||
"""Convert last modified string to datetime."""
|
||||
if not self.last_modified:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(self.last_modified.replace("Z", "+00:00"))
|
||||
except (ValueError, AttributeError):
|
||||
return None
|
||||
|
||||
|
||||
class DirectoryListing(BaseResponse):
|
||||
"""Response model for directory listings."""
|
||||
|
||||
path: str = Field(description="Directory path")
|
||||
items: List[FileInfo] = Field(description="Files and directories in the path")
|
||||
total_count: int = Field(description="Total number of items")
|
||||
directories_count: int = Field(description="Number of directories")
|
||||
files_count: int = Field(description="Number of files")
|
||||
total_size: int = Field(default=0, description="Total size of all files in bytes")
|
||||
|
||||
|
||||
class ReadFileResponse(BaseResponse):
|
||||
"""Response model for reading file contents."""
|
||||
|
||||
path: str = Field(description="File path")
|
||||
content: str = Field(description="File content (text or base64 for binary)")
|
||||
content_type: str = Field(description="MIME content type")
|
||||
size: int = Field(description="File size in bytes")
|
||||
encoding: Optional[str] = Field(
|
||||
None, description="Encoding used (e.g., 'base64' for binary files)"
|
||||
)
|
||||
etag: Optional[str] = Field(None, description="ETag for versioning")
|
||||
last_modified: Optional[str] = Field(None, description="Last modification time")
|
||||
|
||||
|
||||
class WriteFileResponse(StatusResponse):
|
||||
"""Response model for writing files."""
|
||||
|
||||
path: str = Field(description="File path that was written")
|
||||
size: Optional[int] = Field(None, description="Size of the written file")
|
||||
created: bool = Field(description="Whether a new file was created (vs overwritten)")
|
||||
|
||||
|
||||
class CreateDirectoryResponse(StatusResponse):
|
||||
"""Response model for directory creation."""
|
||||
|
||||
path: str = Field(description="Directory path that was created")
|
||||
created: bool = Field(
|
||||
description="Whether directory was created or already existed"
|
||||
)
|
||||
|
||||
|
||||
class DeleteResourceResponse(StatusResponse):
|
||||
"""Response model for resource deletion."""
|
||||
|
||||
path: str = Field(description="Path that was deleted")
|
||||
was_directory: bool = Field(
|
||||
description="Whether the deleted resource was a directory"
|
||||
)
|
||||
items_deleted: Optional[int] = Field(
|
||||
None, description="Number of items deleted (for directories)"
|
||||
)
|
||||
Reference in New Issue
Block a user