Files
nocobase-mcp/nocobase_crm_mcp/nocobase_http.py
T

84 lines
3.2 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import Any, Sequence
import httpx
class NocoBaseHttpError(RuntimeError):
def __init__(self, status_code: int, message: str) -> None:
self.status_code = status_code
super().__init__(message)
class NocoBaseHttpClient:
"""Small synchronous client for the fixed NocoBase attachment endpoints."""
_ATTACHMENT_FIELD = "t_projects.f_projects_attachments"
def __init__(
self,
api_base_url: str,
jwt: str,
timeout: float = 30.0,
transport: httpx.BaseTransport | None = None,
) -> None:
self.api_base_url = api_base_url.strip().rstrip("/")
self.jwt = jwt.strip()
if not self.api_base_url:
raise ValueError("NocoBase API base URL must not be blank")
if not self.jwt:
raise ValueError("NocoBase JWT must not be blank")
self._client = httpx.Client(
base_url=self.api_base_url,
headers={"Authorization": f"Bearer {self.jwt}"},
timeout=timeout,
transport=transport,
)
def upload_attachment(self, path: Path, filename: str, mimetype: str | None) -> dict[str, Any]:
with Path(path).open("rb") as handle:
response = self._client.post(
"/attachments:create",
params={"attachmentField": self._ATTACHMENT_FIELD},
files={"file": (filename, handle, mimetype or "application/octet-stream")},
)
payload = self._json_response(response)
data = payload.get("data")
if not isinstance(data, dict):
raise NocoBaseHttpError(response.status_code, "NocoBase response did not contain an attachment object")
return data
def add_attachment_ids(self, project_id: Any, attachment_ids: Sequence[Any]) -> None:
self._post_attachment_ids(project_id, attachment_ids, "add")
def remove_attachment_ids(self, project_id: Any, attachment_ids: Sequence[Any]) -> None:
self._post_attachment_ids(project_id, attachment_ids, "remove")
def _post_attachment_ids(self, project_id: Any, attachment_ids: Sequence[Any], action: str) -> None:
response = self._client.post(
f"/t_projects/{project_id}/f_projects_attachments:{action}",
json={"tk": list(attachment_ids)},
)
self._raise_for_error(response)
def _json_response(self, response: httpx.Response) -> dict[str, Any]:
self._raise_for_error(response)
try:
payload = response.json()
except ValueError as error:
raise NocoBaseHttpError(response.status_code, "NocoBase response was not valid JSON") from error
if not isinstance(payload, dict):
raise NocoBaseHttpError(response.status_code, "NocoBase response was not a JSON object")
return payload
def _raise_for_error(self, response: httpx.Response) -> None:
if response.is_success:
return
message = response.text.strip() or response.reason_phrase or "NocoBase request failed"
raise NocoBaseHttpError(response.status_code, self._redact(message))
def _redact(self, value: str) -> str:
return value.replace(self.jwt, "<redacted>")