""" 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}")