"""
Directus REST API Client (Self-Hosted)
Handles all HTTP communication with Directus Self-Hosted APIs.
Uses static token authentication via Authorization: Bearer header.
APIs:
- Items (/items/) - CRUD for any collection
- Collections (/collections/) - Schema management
- Fields (/fields/) - Field definitions
- Relations (/relations/) - Relationships
- Files (/files/) - Asset management
- Folders (/folders/) - Folder management
- Users (/users/) - User management
- Roles (/roles/) - Role management
- Permissions (/permissions/) - Permission rules
- Policies (/policies/) - Access policies
- Flows (/flows/) - Automation flows
- Operations (/operations/) - Flow operations
- Webhooks (/webhooks/) - Webhook management
- Activity (/activity/) - Activity log
- Revisions (/revisions/) - Content revisions
- Versions (/versions/) - Content versions
- Comments (/comments/) - Item comments
- Dashboards (/dashboards/) - Insights dashboards
- Panels (/panels/) - Dashboard panels
- Settings (/settings/) - System settings
- Server (/server/) - Server info & health
- Schema (/schema/) - Schema management
- Presets (/presets/) - User presets
- Shares (/shares/) - Content shares
- Notifications (/notifications/) - User notifications
- Translations (/translations/) - Content translations
"""
import base64
import json
import logging
from typing import Any
import aiohttp
def _ensure_list(value: Any) -> list[str]:
"""Ensure value is a list. If string, wrap in list."""
if value is None:
return []
if isinstance(value, list):
return value
if isinstance(value, str):
return [value]
return [str(value)]
class DirectusClient:
"""
Directus Self-Hosted API client.
Uses static token authentication for server-to-server communication.
All requests include Authorization: Bearer {token} header.
"""
def __init__(self, base_url: str, token: str):
"""
Initialize Directus API client.
Args:
base_url: Directus instance URL (e.g., https://directus.example.com)
token: Static admin token
"""
self.base_url = base_url.rstrip("/")
self.token = token
# Initialize logger
self.logger = logging.getLogger(f"DirectusClient.{base_url}")
def _get_headers(self, additional_headers: dict | None = None) -> dict[str, str]:
"""
Get request headers with token authentication.
Args:
additional_headers: Additional headers to include
Returns:
Dict: Headers with authentication
"""
headers = {
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": f"Bearer {self.token}",
}
if additional_headers:
headers.update(additional_headers)
return headers
async def request(
self,
method: str,
endpoint: str,
params: dict | None = None,
json_data: dict | list | None = None,
data: bytes | None = None,
headers_override: dict | None = None,
) -> Any:
"""
Make authenticated request to Directus API.
Args:
method: HTTP method
endpoint: API endpoint (with leading /)
params: Query parameters
json_data: JSON body data
data: Raw binary data (for file uploads)
headers_override: Override/add headers
Returns:
API response
Raises:
Exception: On API errors
"""
url = f"{self.base_url}{endpoint}"
headers = self._get_headers(headers_override)
# Remove Content-Type for binary data (multipart)
if data is not None:
headers.pop("Content-Type", None)
# Clean params - remove None values
if params:
params = {k: v for k, v in params.items() if v is not None}
async with aiohttp.ClientSession() as session:
kwargs = {
"method": method,
"url": url,
"headers": headers,
}
if params:
kwargs["params"] = params
if json_data is not None:
kwargs["json"] = json_data
if data:
kwargs["data"] = data
async with session.request(**kwargs) as response:
self.logger.debug(f"Response status: {response.status}")
# Handle 204 No Content
if response.status == 204:
return {"success": True}
# Handle binary responses (file downloads)
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type and response.status < 400:
# Return binary data as base64
binary_data = await response.read()
return {
"data": base64.b64encode(binary_data).decode(),
"content_type": content_type,
"size": len(binary_data),
}
# Parse JSON response
try:
response_data = await response.json()
except Exception:
response_text = await response.text()
if response.status >= 400:
raise Exception(
f"Directus API error (status {response.status}): {response_text}"
)
return {"success": True, "message": response_text}
# Check for errors
if response.status >= 400:
error_msg = self._extract_error_message(response_data)
raise Exception(f"Directus API error (status {response.status}): {error_msg}")
return response_data
def _extract_error_message(self, response_data: Any) -> str:
"""Extract error message from Directus error response."""
if isinstance(response_data, dict):
errors = response_data.get("errors", [])
if errors and isinstance(errors, list) and len(errors) > 0:
first_error = errors[0]
if isinstance(first_error, dict):
return first_error.get("message", str(first_error))
if "message" in response_data:
return response_data["message"]
if "error" in response_data:
return response_data["error"]
return str(response_data)
# =====================
# ITEMS
# =====================
async def list_items(
self,
collection: str,
fields: list[str] | None = None,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
search: str | None = None,
deep: dict | None = None,
aggregate: dict | None = None,
) -> dict[str, Any]:
"""List items from a collection."""
params = {
"limit": limit,
"offset": offset,
}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
if search:
params["search"] = search
if deep:
params["deep"] = json.dumps(deep)
if aggregate:
params["aggregate"] = json.dumps(aggregate)
return await self.request("GET", f"/items/{collection}", params=params)
async def get_item(
self, collection: str, id: str, fields: list[str] | None = None, deep: dict | None = None
) -> dict[str, Any]:
"""Get item by ID."""
params = {}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
if deep:
params["deep"] = json.dumps(deep)
return await self.request("GET", f"/items/{collection}/{id}", params=params)
async def create_item(
self, collection: str, data: dict[str, Any], fields: list[str] | None = None
) -> dict[str, Any]:
"""Create a new item."""
params = {}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
return await self.request("POST", f"/items/{collection}", params=params, json_data=data)
async def create_items(
self, collection: str, data: list[dict[str, Any]], fields: list[str] | None = None
) -> dict[str, Any]:
"""Create multiple items."""
params = {}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
return await self.request("POST", f"/items/{collection}", params=params, json_data=data)
async def update_item(
self, collection: str, id: str, data: dict[str, Any], fields: list[str] | None = None
) -> dict[str, Any]:
"""Update an item."""
params = {}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
return await self.request(
"PATCH", f"/items/{collection}/{id}", params=params, json_data=data
)
async def update_items(
self,
collection: str,
keys: list[str],
data: dict[str, Any],
fields: list[str] | None = None,
) -> dict[str, Any]:
"""Update multiple items."""
params = {}
if fields:
params["fields"] = ",".join(_ensure_list(fields))
body = {"keys": keys, "data": data}
return await self.request("PATCH", f"/items/{collection}", params=params, json_data=body)
async def delete_item(self, collection: str, id: str) -> dict[str, Any]:
"""Delete an item."""
return await self.request("DELETE", f"/items/{collection}/{id}")
async def delete_items(self, collection: str, keys: list[str]) -> dict[str, Any]:
"""Delete multiple items."""
return await self.request("DELETE", f"/items/{collection}", json_data=keys)
# =====================
# COLLECTIONS
# =====================
async def list_collections(self) -> dict[str, Any]:
"""List all collections."""
return await self.request("GET", "/collections")
async def get_collection(self, collection: str) -> dict[str, Any]:
"""Get collection by name."""
return await self.request("GET", f"/collections/{collection}")
async def create_collection(
self,
collection: str,
meta: dict | None = None,
schema: dict | None = None,
fields: list[dict] | None = None,
) -> dict[str, Any]:
"""Create a new collection."""
data = {"collection": collection}
if meta:
data["meta"] = meta
if schema:
data["schema"] = schema
if fields:
data["fields"] = fields
return await self.request("POST", "/collections", json_data=data)
async def update_collection(self, collection: str, meta: dict) -> dict[str, Any]:
"""Update collection meta."""
return await self.request("PATCH", f"/collections/{collection}", json_data={"meta": meta})
async def delete_collection(self, collection: str) -> dict[str, Any]:
"""Delete a collection."""
return await self.request("DELETE", f"/collections/{collection}")
# =====================
# FIELDS
# =====================
async def list_fields(self, collection: str | None = None) -> dict[str, Any]:
"""List fields, optionally filtered by collection."""
if collection:
return await self.request("GET", f"/fields/{collection}")
return await self.request("GET", "/fields")
async def get_field(self, collection: str, field: str) -> dict[str, Any]:
"""Get field by name."""
return await self.request("GET", f"/fields/{collection}/{field}")
async def create_field(
self,
collection: str,
field: str,
type: str,
meta: dict | None = None,
schema: dict | None = None,
) -> dict[str, Any]:
"""Create a new field."""
data = {"field": field, "type": type}
if meta:
data["meta"] = meta
if schema:
data["schema"] = schema
return await self.request("POST", f"/fields/{collection}", json_data=data)
async def update_field(
self, collection: str, field: str, meta: dict | None = None, schema: dict | None = None
) -> dict[str, Any]:
"""Update a field."""
data = {}
if meta:
data["meta"] = meta
if schema:
data["schema"] = schema
return await self.request("PATCH", f"/fields/{collection}/{field}", json_data=data)
async def delete_field(self, collection: str, field: str) -> dict[str, Any]:
"""Delete a field."""
return await self.request("DELETE", f"/fields/{collection}/{field}")
# =====================
# RELATIONS
# =====================
async def list_relations(self, collection: str | None = None) -> dict[str, Any]:
"""List relations."""
if collection:
return await self.request("GET", f"/relations/{collection}")
return await self.request("GET", "/relations")
async def get_relation(self, collection: str, field: str) -> dict[str, Any]:
"""Get relation by collection and field."""
return await self.request("GET", f"/relations/{collection}/{field}")
async def create_relation(
self,
collection: str,
field: str,
related_collection: str,
meta: dict | None = None,
schema: dict | None = None,
) -> dict[str, Any]:
"""Create a new relation."""
data = {"collection": collection, "field": field, "related_collection": related_collection}
if meta:
data["meta"] = meta
if schema:
data["schema"] = schema
return await self.request("POST", "/relations", json_data=data)
async def update_relation(
self, collection: str, field: str, meta: dict | None = None, schema: dict | None = None
) -> dict[str, Any]:
"""Update a relation."""
data = {}
if meta:
data["meta"] = meta
if schema:
data["schema"] = schema
return await self.request("PATCH", f"/relations/{collection}/{field}", json_data=data)
async def delete_relation(self, collection: str, field: str) -> dict[str, Any]:
"""Delete a relation."""
return await self.request("DELETE", f"/relations/{collection}/{field}")
# =====================
# FILES
# =====================
async def list_files(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
search: str | None = None,
) -> dict[str, Any]:
"""List files."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
if search:
params["search"] = search
return await self.request("GET", "/files", params=params)
async def get_file(self, id: str) -> dict[str, Any]:
"""Get file by ID."""
return await self.request("GET", f"/files/{id}")
async def update_file(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update file metadata."""
return await self.request("PATCH", f"/files/{id}", json_data=data)
async def delete_file(self, id: str) -> dict[str, Any]:
"""Delete a file."""
return await self.request("DELETE", f"/files/{id}")
async def delete_files(self, ids: list[str]) -> dict[str, Any]:
"""Delete multiple files."""
return await self.request("DELETE", "/files", json_data=ids)
async def import_file_url(self, url: str, data: dict | None = None) -> dict[str, Any]:
"""Import file from URL."""
body = {"url": url}
if data:
body["data"] = data
return await self.request("POST", "/files/import", json_data=body)
# =====================
# FOLDERS
# =====================
async def list_folders(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
search: str | None = None,
) -> dict[str, Any]:
"""List folders."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
if search:
params["search"] = search
return await self.request("GET", "/folders", params=params)
async def get_folder(self, id: str) -> dict[str, Any]:
"""Get folder by ID."""
return await self.request("GET", f"/folders/{id}")
async def create_folder(self, name: str, parent: str | None = None) -> dict[str, Any]:
"""Create a folder."""
data = {"name": name}
if parent:
data["parent"] = parent
return await self.request("POST", "/folders", json_data=data)
async def update_folder(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update folder."""
return await self.request("PATCH", f"/folders/{id}", json_data=data)
async def delete_folder(self, id: str) -> dict[str, Any]:
"""Delete a folder."""
return await self.request("DELETE", f"/folders/{id}")
# =====================
# USERS
# =====================
async def list_users(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
search: str | None = None,
) -> dict[str, Any]:
"""List users."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
if search:
params["search"] = search
return await self.request("GET", "/users", params=params)
async def get_user(self, id: str) -> dict[str, Any]:
"""Get user by ID."""
return await self.request("GET", f"/users/{id}")
async def get_current_user(self) -> dict[str, Any]:
"""Get current authenticated user."""
return await self.request("GET", "/users/me")
async def create_user(
self,
email: str,
password: str,
role: str,
first_name: str | None = None,
last_name: str | None = None,
status: str = "active",
**kwargs,
) -> dict[str, Any]:
"""Create a new user."""
data = {"email": email, "password": password, "role": role, "status": status}
if first_name:
data["first_name"] = first_name
if last_name:
data["last_name"] = last_name
data.update(kwargs)
return await self.request("POST", "/users", json_data=data)
async def update_user(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a user."""
return await self.request("PATCH", f"/users/{id}", json_data=data)
async def delete_user(self, id: str) -> dict[str, Any]:
"""Delete a user."""
return await self.request("DELETE", f"/users/{id}")
async def delete_users(self, ids: list[str]) -> dict[str, Any]:
"""Delete multiple users."""
return await self.request("DELETE", "/users", json_data=ids)
async def invite_user(
self, email: str, role: str, invite_url: str | None = None
) -> dict[str, Any]:
"""Invite a user."""
data = {"email": email, "role": role}
if invite_url:
data["invite_url"] = invite_url
return await self.request("POST", "/users/invite", json_data=data)
# =====================
# ROLES
# =====================
async def list_roles(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List roles."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/roles", params=params)
async def get_role(self, id: str) -> dict[str, Any]:
"""Get role by ID."""
return await self.request("GET", f"/roles/{id}")
async def create_role(
self,
name: str,
icon: str = "supervised_user_circle",
description: str | None = None,
admin_access: bool = False,
app_access: bool = True,
**kwargs,
) -> dict[str, Any]:
"""Create a new role."""
data = {"name": name, "icon": icon, "admin_access": admin_access, "app_access": app_access}
if description:
data["description"] = description
data.update(kwargs)
return await self.request("POST", "/roles", json_data=data)
async def update_role(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a role."""
return await self.request("PATCH", f"/roles/{id}", json_data=data)
async def delete_role(self, id: str) -> dict[str, Any]:
"""Delete a role."""
return await self.request("DELETE", f"/roles/{id}")
# =====================
# PERMISSIONS
# =====================
async def list_permissions(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List permissions."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/permissions", params=params)
async def get_permission(self, id: str) -> dict[str, Any]:
"""Get permission by ID."""
return await self.request("GET", f"/permissions/{id}")
async def get_my_permissions(self) -> dict[str, Any]:
"""Get current user's permissions."""
return await self.request("GET", "/permissions/me")
async def create_permission(
self,
collection: str,
action: str,
policy: str,
role: str | None = None,
permissions: dict | None = None,
validation: dict | None = None,
presets: dict | None = None,
fields: list[str] | None = None,
) -> dict[str, Any]:
"""Create a new permission. Requires 'policy' in Directus v10+."""
data = {
"collection": collection,
"action": action,
"policy": policy, # Required in Directus v10+
}
if role:
data["role"] = role
if permissions:
data["permissions"] = permissions
if validation:
data["validation"] = validation
if presets:
data["presets"] = presets
if fields:
data["fields"] = fields
return await self.request("POST", "/permissions", json_data=data)
async def update_permission(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a permission."""
return await self.request("PATCH", f"/permissions/{id}", json_data=data)
async def delete_permission(self, id: str) -> dict[str, Any]:
"""Delete a permission."""
return await self.request("DELETE", f"/permissions/{id}")
# =====================
# POLICIES
# =====================
async def list_policies(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List policies."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/policies", params=params)
async def get_policy(self, id: str) -> dict[str, Any]:
"""Get policy by ID."""
return await self.request("GET", f"/policies/{id}")
# =====================
# FLOWS
# =====================
async def list_flows(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List flows."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/flows", params=params)
async def get_flow(self, id: str) -> dict[str, Any]:
"""Get flow by ID."""
return await self.request("GET", f"/flows/{id}")
async def create_flow(
self,
name: str,
trigger: str,
status: str = "active",
icon: str = "bolt",
options: dict | None = None,
accountability: str | None = None,
description: str | None = None,
) -> dict[str, Any]:
"""Create a new flow."""
data = {"name": name, "trigger": trigger, "status": status, "icon": icon}
if options:
data["options"] = options
if accountability:
data["accountability"] = accountability
if description:
data["description"] = description
return await self.request("POST", "/flows", json_data=data)
async def update_flow(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a flow."""
return await self.request("PATCH", f"/flows/{id}", json_data=data)
async def delete_flow(self, id: str) -> dict[str, Any]:
"""Delete a flow."""
return await self.request("DELETE", f"/flows/{id}")
async def trigger_flow(self, id: str, data: dict | None = None) -> dict[str, Any]:
"""Trigger a manual flow."""
return await self.request("POST", f"/flows/trigger/{id}", json_data=data or {})
# =====================
# OPERATIONS
# =====================
async def list_operations(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List operations."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/operations", params=params)
async def get_operation(self, id: str) -> dict[str, Any]:
"""Get operation by ID."""
return await self.request("GET", f"/operations/{id}")
async def create_operation(
self,
flow: str,
type: str,
name: str | None = None,
key: str | None = None,
options: dict | None = None,
position_x: int = 0,
position_y: int = 0,
resolve: str | None = None,
reject: str | None = None,
) -> dict[str, Any]:
"""Create a new operation."""
data = {"flow": flow, "type": type, "position_x": position_x, "position_y": position_y}
if name:
data["name"] = name
if key:
data["key"] = key
if options:
data["options"] = options
if resolve:
data["resolve"] = resolve
if reject:
data["reject"] = reject
return await self.request("POST", "/operations", json_data=data)
async def update_operation(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update an operation."""
return await self.request("PATCH", f"/operations/{id}", json_data=data)
async def delete_operation(self, id: str) -> dict[str, Any]:
"""Delete an operation."""
return await self.request("DELETE", f"/operations/{id}")
# =====================
# WEBHOOKS
# =====================
async def list_webhooks(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List webhooks."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/webhooks", params=params)
async def get_webhook(self, id: str) -> dict[str, Any]:
"""Get webhook by ID."""
return await self.request("GET", f"/webhooks/{id}")
async def create_webhook(
self,
name: str,
url: str,
actions: list[str],
collections: list[str],
method: str = "POST",
status: str = "active",
headers: dict | None = None,
) -> dict[str, Any]:
"""Create a new webhook."""
data = {
"name": name,
"url": url,
"actions": _ensure_list(actions),
"collections": _ensure_list(collections),
"method": method,
"status": status,
}
if headers:
data["headers"] = headers
return await self.request("POST", "/webhooks", json_data=data)
async def update_webhook(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a webhook."""
return await self.request("PATCH", f"/webhooks/{id}", json_data=data)
async def delete_webhook(self, id: str) -> dict[str, Any]:
"""Delete a webhook."""
return await self.request("DELETE", f"/webhooks/{id}")
# =====================
# ACTIVITY
# =====================
async def list_activity(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List activity."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/activity", params=params)
async def get_activity(self, id: str) -> dict[str, Any]:
"""Get activity by ID."""
return await self.request("GET", f"/activity/{id}")
# =====================
# REVISIONS
# =====================
async def list_revisions(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List revisions."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/revisions", params=params)
async def get_revision(self, id: str) -> dict[str, Any]:
"""Get revision by ID."""
return await self.request("GET", f"/revisions/{id}")
# =====================
# VERSIONS
# =====================
async def list_versions(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List content versions."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/versions", params=params)
async def get_version(self, id: str) -> dict[str, Any]:
"""Get version by ID."""
return await self.request("GET", f"/versions/{id}")
async def create_version(
self, name: str, collection: str, item: str, key: str
) -> dict[str, Any]:
"""Create a new version."""
data = {"name": name, "collection": collection, "item": item, "key": key}
return await self.request("POST", "/versions", json_data=data)
async def update_version(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a version."""
return await self.request("PATCH", f"/versions/{id}", json_data=data)
async def delete_version(self, id: str) -> dict[str, Any]:
"""Delete a version."""
return await self.request("DELETE", f"/versions/{id}")
async def promote_version(self, id: str, mainHash: str | None = None) -> dict[str, Any]:
"""Promote a version to main."""
data = {}
if mainHash:
data["mainHash"] = mainHash
return await self.request("POST", f"/versions/{id}/promote", json_data=data)
# =====================
# COMMENTS
# =====================
async def list_comments(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List comments."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/comments", params=params)
async def get_comment(self, id: str) -> dict[str, Any]:
"""Get comment by ID."""
return await self.request("GET", f"/comments/{id}")
async def create_comment(self, collection: str, item: str, comment: str) -> dict[str, Any]:
"""Create a new comment."""
data = {"collection": collection, "item": item, "comment": comment}
return await self.request("POST", "/comments", json_data=data)
async def update_comment(self, id: str, comment: str) -> dict[str, Any]:
"""Update a comment."""
return await self.request("PATCH", f"/comments/{id}", json_data={"comment": comment})
async def delete_comment(self, id: str) -> dict[str, Any]:
"""Delete a comment."""
return await self.request("DELETE", f"/comments/{id}")
# =====================
# DASHBOARDS
# =====================
async def list_dashboards(
self,
filter: dict | None = None,
sort: list[str] | None = None,
limit: int = 100,
offset: int = 0,
) -> dict[str, Any]:
"""List dashboards."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
if sort:
params["sort"] = ",".join(_ensure_list(sort))
return await self.request("GET", "/dashboards", params=params)
async def get_dashboard(self, id: str) -> dict[str, Any]:
"""Get dashboard by ID."""
return await self.request("GET", f"/dashboards/{id}")
async def create_dashboard(
self, name: str, icon: str = "dashboard", note: str | None = None, color: str | None = None
) -> dict[str, Any]:
"""Create a new dashboard."""
data = {"name": name, "icon": icon}
if note:
data["note"] = note
if color:
data["color"] = color
return await self.request("POST", "/dashboards", json_data=data)
async def update_dashboard(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a dashboard."""
return await self.request("PATCH", f"/dashboards/{id}", json_data=data)
async def delete_dashboard(self, id: str) -> dict[str, Any]:
"""Delete a dashboard."""
return await self.request("DELETE", f"/dashboards/{id}")
# =====================
# PANELS
# =====================
async def list_panels(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List panels."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/panels", params=params)
async def get_panel(self, id: str) -> dict[str, Any]:
"""Get panel by ID."""
return await self.request("GET", f"/panels/{id}")
async def create_panel(
self,
dashboard: str,
type: str,
name: str | None = None,
icon: str | None = None,
color: str | None = None,
note: str | None = None,
width: int = 12,
height: int = 6,
position_x: int = 0,
position_y: int = 0,
options: dict | None = None,
) -> dict[str, Any]:
"""Create a new panel."""
data = {
"dashboard": dashboard,
"type": type,
"width": width,
"height": height,
"position_x": position_x,
"position_y": position_y,
}
if name:
data["name"] = name
if icon:
data["icon"] = icon
if color:
data["color"] = color
if note:
data["note"] = note
if options:
data["options"] = options
return await self.request("POST", "/panels", json_data=data)
async def update_panel(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a panel."""
return await self.request("PATCH", f"/panels/{id}", json_data=data)
async def delete_panel(self, id: str) -> dict[str, Any]:
"""Delete a panel."""
return await self.request("DELETE", f"/panels/{id}")
# =====================
# SETTINGS
# =====================
async def get_settings(self) -> dict[str, Any]:
"""Get system settings."""
return await self.request("GET", "/settings")
async def update_settings(self, data: dict[str, Any]) -> dict[str, Any]:
"""Update system settings."""
return await self.request("PATCH", "/settings", json_data=data)
# =====================
# SERVER
# =====================
async def get_server_info(self) -> dict[str, Any]:
"""Get server info."""
return await self.request("GET", "/server/info")
async def health_check(self) -> dict[str, Any]:
"""Check server health."""
return await self.request("GET", "/server/health")
async def get_graphql_sdl(self) -> dict[str, Any]:
"""Get GraphQL SDL schema."""
return await self.request("GET", "/server/specs/graphql")
async def get_openapi_spec(self) -> dict[str, Any]:
"""Get OpenAPI specification."""
return await self.request("GET", "/server/specs/oas")
# =====================
# SCHEMA
# =====================
async def get_schema_snapshot(self) -> dict[str, Any]:
"""Get schema snapshot."""
return await self.request("GET", "/schema/snapshot")
async def schema_diff(self, snapshot: dict[str, Any]) -> dict[str, Any]:
"""Get diff between current schema and snapshot."""
return await self.request("POST", "/schema/diff", json_data=snapshot)
async def schema_apply(self, diff: dict[str, Any]) -> dict[str, Any]:
"""Apply schema diff."""
return await self.request("POST", "/schema/apply", json_data=diff)
# =====================
# PRESETS
# =====================
async def list_presets(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List presets."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/presets", params=params)
async def get_preset(self, id: str) -> dict[str, Any]:
"""Get preset by ID."""
return await self.request("GET", f"/presets/{id}")
async def create_preset(
self,
collection: str,
layout: str | None = None,
layout_query: dict | None = None,
layout_options: dict | None = None,
filter: dict | None = None,
search: str | None = None,
bookmark: str | None = None,
user: str | None = None,
role: str | None = None,
) -> dict[str, Any]:
"""Create a new preset."""
data = {"collection": collection}
if layout:
data["layout"] = layout
if layout_query:
data["layout_query"] = layout_query
if layout_options:
data["layout_options"] = layout_options
if filter:
data["filter"] = filter
if search:
data["search"] = search
if bookmark:
data["bookmark"] = bookmark
if user:
data["user"] = user
if role:
data["role"] = role
return await self.request("POST", "/presets", json_data=data)
async def update_preset(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a preset."""
return await self.request("PATCH", f"/presets/{id}", json_data=data)
async def delete_preset(self, id: str) -> dict[str, Any]:
"""Delete a preset."""
return await self.request("DELETE", f"/presets/{id}")
# =====================
# SHARES
# =====================
async def list_shares(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List shares."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/shares", params=params)
async def get_share(self, id: str) -> dict[str, Any]:
"""Get share by ID."""
return await self.request("GET", f"/shares/{id}")
async def create_share(
self,
collection: str,
item: str,
name: str | None = None,
role: str | None = None,
password: str | None = None,
date_start: str | None = None,
date_end: str | None = None,
max_uses: int | None = None,
) -> dict[str, Any]:
"""Create a new share."""
data = {"collection": collection, "item": item}
if name:
data["name"] = name
if role:
data["role"] = role
if password:
data["password"] = password
if date_start:
data["date_start"] = date_start
if date_end:
data["date_end"] = date_end
if max_uses:
data["max_uses"] = max_uses
return await self.request("POST", "/shares", json_data=data)
async def update_share(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a share."""
return await self.request("PATCH", f"/shares/{id}", json_data=data)
async def delete_share(self, id: str) -> dict[str, Any]:
"""Delete a share."""
return await self.request("DELETE", f"/shares/{id}")
# =====================
# NOTIFICATIONS
# =====================
async def list_notifications(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List notifications."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/notifications", params=params)
async def get_notification(self, id: str) -> dict[str, Any]:
"""Get notification by ID."""
return await self.request("GET", f"/notifications/{id}")
async def update_notification(self, id: str, status: str) -> dict[str, Any]:
"""Update notification (mark as read/archived)."""
return await self.request("PATCH", f"/notifications/{id}", json_data={"status": status})
async def delete_notification(self, id: str) -> dict[str, Any]:
"""Delete a notification."""
return await self.request("DELETE", f"/notifications/{id}")
# =====================
# TRANSLATIONS
# =====================
async def list_translations(
self, filter: dict | None = None, limit: int = 100, offset: int = 0
) -> dict[str, Any]:
"""List translations."""
params = {"limit": limit, "offset": offset}
if filter:
params["filter"] = json.dumps(filter)
return await self.request("GET", "/translations", params=params)
async def get_translation(self, id: str) -> dict[str, Any]:
"""Get translation by ID."""
return await self.request("GET", f"/translations/{id}")
async def create_translation(self, key: str, language: str, value: str) -> dict[str, Any]:
"""Create a new translation."""
data = {"key": key, "language": language, "value": value}
return await self.request("POST", "/translations", json_data=data)
async def update_translation(self, id: str, data: dict[str, Any]) -> dict[str, Any]:
"""Update a translation."""
return await self.request("PATCH", f"/translations/{id}", json_data=data)
async def delete_translation(self, id: str) -> dict[str, Any]:
"""Delete a translation."""
return await self.request("DELETE", f"/translations/{id}")