mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-28 02:23:53 +02:00
147 lines
4.6 KiB
Python
147 lines
4.6 KiB
Python
"""
|
|
Obsidian Plugin connector schemas.
|
|
|
|
Wire format spoken between the SurfSense Obsidian plugin
|
|
(``surfsense_obsidian/``) and the FastAPI backend.
|
|
|
|
Stability contract
|
|
------------------
|
|
Every request and response schema sets ``model_config = ConfigDict(extra='ignore')``.
|
|
This is the API stability contract — not just hygiene:
|
|
|
|
- Old plugins talking to a newer backend silently drop any new response fields
|
|
they don't understand instead of failing validation.
|
|
- New plugins talking to an older backend can include forward-looking request
|
|
fields (e.g. attachments metadata) without the older backend rejecting them.
|
|
|
|
Hard breaking changes are reserved for the URL prefix (``/api/v2/...``).
|
|
Additive evolution is signaled via the ``capabilities`` array on
|
|
``HealthResponse`` / ``ConnectResponse`` — older plugins ignore unknown
|
|
capability strings safely.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
_PLUGIN_MODEL_CONFIG = ConfigDict(extra="ignore")
|
|
|
|
|
|
class _PluginBase(BaseModel):
|
|
"""Base class for all plugin payload schemas.
|
|
|
|
Carries the forward-compatibility config so subclasses don't have to
|
|
repeat it.
|
|
"""
|
|
|
|
model_config = _PLUGIN_MODEL_CONFIG
|
|
|
|
|
|
class NotePayload(_PluginBase):
|
|
"""One Obsidian note as pushed by the plugin.
|
|
|
|
The plugin is the source of truth: ``content`` is the post-frontmatter
|
|
body, ``frontmatter``/``tags``/``headings``/etc. are precomputed by the
|
|
plugin via ``app.metadataCache`` so the backend doesn't have to re-parse.
|
|
"""
|
|
|
|
vault_id: str = Field(..., description="Stable plugin-generated UUID for this vault")
|
|
path: str = Field(..., description="Vault-relative path, e.g. 'notes/foo.md'")
|
|
name: str = Field(..., description="File stem (no extension)")
|
|
extension: str = Field(default="md", description="File extension without leading dot")
|
|
content: str = Field(default="", description="Raw markdown body (post-frontmatter)")
|
|
|
|
frontmatter: dict[str, Any] = Field(default_factory=dict)
|
|
tags: list[str] = Field(default_factory=list)
|
|
headings: list[str] = Field(default_factory=list)
|
|
resolved_links: list[str] = Field(default_factory=list)
|
|
unresolved_links: list[str] = Field(default_factory=list)
|
|
embeds: list[str] = Field(default_factory=list)
|
|
aliases: list[str] = Field(default_factory=list)
|
|
|
|
content_hash: str = Field(..., description="Plugin-computed SHA-256 of the raw content")
|
|
mtime: datetime
|
|
ctime: datetime
|
|
|
|
|
|
class SyncBatchRequest(_PluginBase):
|
|
"""Batch upsert. Plugin sends 10-20 notes per request to amortize HTTP overhead."""
|
|
|
|
vault_id: str
|
|
notes: list[NotePayload] = Field(default_factory=list, max_length=100)
|
|
|
|
|
|
class RenameItem(_PluginBase):
|
|
old_path: str
|
|
new_path: str
|
|
|
|
|
|
class RenameBatchRequest(_PluginBase):
|
|
vault_id: str
|
|
renames: list[RenameItem] = Field(default_factory=list, max_length=200)
|
|
|
|
|
|
class DeleteBatchRequest(_PluginBase):
|
|
vault_id: str
|
|
paths: list[str] = Field(default_factory=list, max_length=500)
|
|
|
|
|
|
class ManifestEntry(_PluginBase):
|
|
"""One row of the server-side manifest used by the plugin to reconcile."""
|
|
|
|
hash: str
|
|
mtime: datetime
|
|
|
|
|
|
class ManifestResponse(_PluginBase):
|
|
"""Path-keyed manifest of every non-deleted note for a vault."""
|
|
|
|
vault_id: str
|
|
items: dict[str, ManifestEntry] = Field(default_factory=dict)
|
|
|
|
|
|
class ConnectRequest(_PluginBase):
|
|
"""First-call handshake to register or look up a vault connector row."""
|
|
|
|
vault_id: str
|
|
vault_name: str
|
|
search_space_id: int
|
|
plugin_version: str
|
|
device_id: str
|
|
device_label: str | None = Field(
|
|
default=None,
|
|
description="User-friendly device name shown in the web UI (e.g. 'iPad Pro').",
|
|
)
|
|
|
|
|
|
class ConnectResponse(_PluginBase):
|
|
"""Returned from POST /connect.
|
|
|
|
Carries the same handshake fields as ``HealthResponse`` so the plugin
|
|
learns the contract on its very first call without an extra round-trip
|
|
to ``GET /health``.
|
|
"""
|
|
|
|
connector_id: int
|
|
vault_id: str
|
|
search_space_id: int
|
|
api_version: str
|
|
capabilities: list[str]
|
|
|
|
|
|
class HealthResponse(_PluginBase):
|
|
"""API contract handshake.
|
|
|
|
The plugin calls ``GET /health`` once per ``onload`` and caches the
|
|
result. ``capabilities`` is a forward-extensible string list: future
|
|
additions (``'pat_auth'``, ``'scoped_pat'``, ``'attachments_v2'``,
|
|
``'shared_search_spaces'``...) ship without breaking older plugins
|
|
because they only enable extra behavior, never gate existing endpoints.
|
|
"""
|
|
|
|
api_version: str
|
|
capabilities: list[str]
|
|
server_time_utc: datetime
|