Files
wehub-resource-sync 6ede33ccdb
Build and Push Docker Images / create_manifest (web, surfsense-web, , cpu) (push) Has been cancelled
Build and Push Docker Images / finalize_release (push) Has been cancelled
Obsidian Plugin Lint / lint (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_web, cpu, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64, , runner, false, cpu) (push) Has been cancelled
Build and Push Docker Images / compute_version (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cpu, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, , production, false, cpu) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu126, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda126, production, true, cuda126) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / build (./surfsense_backend, cu128, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64, -cuda, production, true, cuda) (push) Has been cancelled
Build and Push Docker Images / verify_digests (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, , cpu) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda, cuda) (push) Has been cancelled
Build and Push Docker Images / create_manifest (backend, surfsense-backend, -cuda126, cuda126) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:33:44 +08:00

126 lines
4.4 KiB
Python

"""Authenticated transport to a SurfSense backend's REST API.
Sends requests to fully-formed paths, returns parsed JSON, and turns any
transport or HTTP failure into a readable ``ToolError``.
"""
from __future__ import annotations
from typing import Any
import httpx
from .auth.identity import current_api_key
from .errors import ToolError
_FAILURE_HINTS: dict[int, str] = {
401: "Authentication failed — the SurfSense API key is invalid or expired.",
402: "The workspace is out of credits for this operation.",
403: (
"Access denied — the token lacks permission, or API access is disabled "
"for this workspace (enable it in SurfSense workspace settings)."
),
404: "The requested resource was not found.",
429: "Rate limited by the backend — retry after a short pause.",
}
class SurfSenseClient:
"""Issues authenticated requests against ``{base_url}{api_prefix}``."""
def __init__(
self, *, api_base: str, timeout: float, fallback_api_key: str | None = None
) -> None:
self._api_base = api_base
# Resolved per request, so no key is baked into the shared client. The
# fallback is the env key used under stdio, where there is no header.
self._fallback_api_key = fallback_api_key
self._http = httpx.AsyncClient(
base_url=api_base,
headers={"Accept": "application/json"},
timeout=timeout,
)
def _auth_headers(self) -> dict[str, str]:
"""Resolve the caller's key: the per-request header, else the env key."""
api_key = current_api_key() or self._fallback_api_key
if not api_key:
raise ToolError(
"No SurfSense API key supplied. Send it as an 'Authorization: "
"Bearer ss_pat_...' header (remote server), or set the "
"SURFSENSE_API_KEY environment variable (stdio)."
)
return {"Authorization": f"Bearer {api_key}"}
async def request(
self,
method: str,
path: str,
*,
params: dict[str, Any] | None = None,
json: Any | None = None,
data: dict[str, Any] | None = None,
files: Any | None = None,
) -> Any:
"""Send a request and return the parsed body, or raise ``ToolError``."""
# Omit unset query params: sending them empty makes the API parse ""
# as a value (e.g. int("") on folder_id) and fail.
if params is not None:
params = {key: value for key, value in params.items() if value is not None}
headers = self._auth_headers()
try:
response = await self._http.request(
method,
path,
params=params,
json=json,
data=data,
files=files,
headers=headers,
)
except httpx.RequestError as exc:
raise ToolError(
f"Could not reach SurfSense at {self._api_base}: {exc}. "
"Confirm the backend is running and SURFSENSE_BASE_URL is correct."
) from exc
if response.is_success:
return self._parse_body(response)
raise ToolError(self._explain_failure(response))
async def aclose(self) -> None:
await self._http.aclose()
@staticmethod
def _parse_body(response: httpx.Response) -> Any:
if not response.content:
return None
try:
return response.json()
except ValueError:
return response.text
@classmethod
def _explain_failure(cls, response: httpx.Response) -> str:
"""Turn an error response into one actionable sentence for the model."""
detail = cls._extract_detail(response)
hint = _FAILURE_HINTS.get(response.status_code)
if detail and hint:
return f"{hint} (server said: {detail})"
if detail:
return f"SurfSense returned {response.status_code}: {detail}"
return hint or f"SurfSense returned HTTP {response.status_code}."
@staticmethod
def _extract_detail(response: httpx.Response) -> str | None:
try:
body = response.json()
except ValueError:
return response.text.strip() or None
if isinstance(body, dict):
detail = body.get("detail", body)
if isinstance(detail, dict):
return detail.get("message") or str(detail)
return str(detail)
return str(body)