84 lines
3.2 KiB
Python
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>")
|