Spaces:
Sleeping
Sleeping
Download app/db.py from stevafernandes/CURAS: direct link, hf CLI and curl.
- Browser
- Download file 5.55 kB
-
https://huggingface.co/spaces/stevafernandes/CURAS/resolve/main/app/db.py
- Command line
-
hf download hf://spaces/stevafernandes/CURAS/app/db.py
-
curl -L -o db.py https://huggingface.co/spaces/stevafernandes/CURAS/resolve/main/app/db.py
5.55 kB
| """Thin client for the Supabase REST API (PostgREST). | |
| Only the server holds the service_role key. Filter values passed here are | |
| always server-generated (validated UUIDs or fixed status words), never raw | |
| user input; searching and filtering of listings happens in Python. | |
| """ | |
| from __future__ import annotations | |
| import logging | |
| import uuid | |
| from typing import Any, Optional | |
| import httpx | |
| log = logging.getLogger("curas.db") | |
| class DbError(Exception): | |
| def __init__(self, message: str, status: Optional[int] = None): | |
| super().__init__(message) | |
| self.status = status | |
| class DbUnavailable(DbError): | |
| pass | |
| def check_uuid(value: str) -> str: | |
| """Return the canonical form of a UUID string or raise ValueError.""" | |
| return str(uuid.UUID(str(value))) | |
| class Db: | |
| def __init__(self, url: str, key: str, rest_path: str = "/rest/v1", timeout: float = 10.0): | |
| if not url or not key: | |
| raise DbUnavailable("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are not configured") | |
| self.base = url.rstrip("/") + (rest_path if rest_path.startswith("/") or rest_path == "" else "/" + rest_path) | |
| # New-style Supabase secret keys (sb_secret_...) are sent only on the | |
| # apikey header; legacy service_role JWTs are sent on both. | |
| self._headers = {"apikey": key, "Accept": "application/json"} | |
| if not key.startswith("sb_"): | |
| self._headers["Authorization"] = f"Bearer {key}" | |
| self._client = httpx.Client(timeout=timeout, headers=self._headers) | |
| def close(self) -> None: | |
| self._client.close() | |
| # -- low level --------------------------------------------------------- | |
| def _request(self, method: str, table: str, *, params: Optional[dict] = None, | |
| json: Any = None, prefer: Optional[str] = None) -> Any: | |
| headers = {} | |
| if prefer: | |
| headers["Prefer"] = prefer | |
| if json is not None: | |
| headers["Content-Type"] = "application/json" | |
| try: | |
| resp = self._send(method, table, params, json, headers) | |
| except httpx.HTTPError as exc: | |
| log.error("database request failed: %s %s: %s", method, table, exc) | |
| raise DbUnavailable(f"database request failed: {exc.__class__.__name__}") from exc | |
| if resp.status_code >= 400: | |
| detail = resp.text[:500] | |
| log.error("database error %s on %s %s: %s", resp.status_code, method, table, detail) | |
| raise DbError(f"database error {resp.status_code}", resp.status_code) | |
| if resp.status_code == 204 or not resp.content: | |
| return None | |
| return resp.json() | |
| def _send(self, method: str, table: str, params, json, headers): | |
| """Send a request; retry a read once if a pooled connection was dropped.""" | |
| try: | |
| return self._client.request(method, f"{self.base}/{table}", params=params, | |
| json=json, headers=headers) | |
| except (httpx.RemoteProtocolError, httpx.ConnectError): | |
| if method not in ("GET", "HEAD"): | |
| raise | |
| return self._client.request(method, f"{self.base}/{table}", params=params, | |
| json=json, headers=headers) | |
| # -- helpers ----------------------------------------------------------- | |
| def select(self, table: str, filters: Optional[dict] = None, *, order: Optional[str] = None, | |
| columns: str = "*", limit: Optional[int] = None) -> list[dict]: | |
| params: dict[str, str] = {"select": columns} | |
| params.update(filters or {}) | |
| if order: | |
| params["order"] = order | |
| if limit is not None: | |
| params["limit"] = str(limit) | |
| return self._request("GET", table, params=params) or [] | |
| def get_one(self, table: str, id_: str) -> Optional[dict]: | |
| rows = self.select(table, {"id": f"eq.{check_uuid(id_)}"}, limit=1) | |
| return rows[0] if rows else None | |
| def insert(self, table: str, row: dict) -> dict: | |
| rows = self._request("POST", table, json=row, prefer="return=representation") | |
| if not rows: | |
| raise DbError("insert returned no row") | |
| return rows[0] | |
| def insert_many(self, table: str, rows: list[dict]) -> list[dict]: | |
| if not rows: | |
| return [] | |
| return self._request("POST", table, json=rows, prefer="return=representation") or [] | |
| def update(self, table: str, filters: dict, patch: dict) -> list[dict]: | |
| """PATCH rows matching filters. Returns the updated rows (may be empty).""" | |
| return self._request("PATCH", table, params=filters, json=patch, | |
| prefer="return=representation") or [] | |
| def count(self, table: str, filters: Optional[dict] = None) -> int: | |
| """Exact row count via a zero-width range request (keeps the pooled | |
| connection reusable, unlike HEAD).""" | |
| params: dict[str, str] = {"select": "id"} | |
| params.update(filters or {}) | |
| headers = {"Prefer": "count=exact", "Range-Unit": "items", "Range": "0-0"} | |
| try: | |
| resp = self._send("GET", table, params, None, headers) | |
| except httpx.HTTPError as exc: | |
| raise DbUnavailable(f"database request failed: {exc.__class__.__name__}") from exc | |
| if resp.status_code >= 400: | |
| raise DbError(f"database error {resp.status_code}", resp.status_code) | |
| rng = resp.headers.get("content-range", "") | |
| try: | |
| return int(rng.split("/")[-1]) | |
| except ValueError: | |
| return 0 | |