diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/supafunc')
8 files changed, 404 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/supafunc/__init__.py b/.venv/lib/python3.12/site-packages/supafunc/__init__.py new file mode 100644 index 00000000..7b819695 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/__init__.py @@ -0,0 +1,34 @@ +from __future__ import annotations + +from typing import Literal, Optional, Union, overload + +from ._async.functions_client import AsyncFunctionsClient +from ._sync.functions_client import SyncFunctionsClient +from .utils import FunctionRegion + +__all__ = ["create_client"] + + +@overload +def create_client( + url: str, headers: dict[str, str], *, is_async: Literal[True], verify: bool +) -> AsyncFunctionsClient: ... + + +@overload +def create_client( + url: str, headers: dict[str, str], *, is_async: Literal[False], verify: bool +) -> SyncFunctionsClient: ... + + +def create_client( + url: str, + headers: dict[str, str], + *, + is_async: bool, + verify: bool = True, +) -> Union[AsyncFunctionsClient, SyncFunctionsClient]: + if is_async: + return AsyncFunctionsClient(url, headers, verify) + else: + return SyncFunctionsClient(url, headers, verify) diff --git a/.venv/lib/python3.12/site-packages/supafunc/_async/__init__.py b/.venv/lib/python3.12/site-packages/supafunc/_async/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/_async/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/.venv/lib/python3.12/site-packages/supafunc/_async/functions_client.py b/.venv/lib/python3.12/site-packages/supafunc/_async/functions_client.py new file mode 100644 index 00000000..42d8d207 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/_async/functions_client.py @@ -0,0 +1,127 @@ +from typing import Any, Dict, Literal, Optional, Union +from warnings import warn + +from httpx import HTTPError, Response + +from ..errors import FunctionsHttpError, FunctionsRelayError +from ..utils import ( + AsyncClient, + FunctionRegion, + is_http_url, + is_valid_jwt, + is_valid_str_arg, +) +from ..version import __version__ + + +class AsyncFunctionsClient: + def __init__( + self, + url: str, + headers: Dict, + timeout: int, + verify: bool = True, + proxy: Optional[str] = None, + ): + if not is_http_url(url): + raise ValueError("url must be a valid HTTP URL string") + self.url = url + self.headers = { + "User-Agent": f"supabase-py/functions-py v{__version__}", + **headers, + } + self._client = AsyncClient( + base_url=self.url, + headers=self.headers, + verify=bool(verify), + timeout=int(abs(timeout)), + proxy=proxy, + follow_redirects=True, + http2=True, + ) + + async def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + url: str, + headers: Optional[Dict[str, str]] = None, + json: Optional[Dict[Any, Any]] = None, + ) -> Response: + + user_data = {"data": json} if isinstance(json, str) else {"json": json} + response = await self._client.request(method, url, **user_data, headers=headers) + + try: + response.raise_for_status() + except HTTPError as exc: + raise FunctionsHttpError( + response.json().get("error") + or f"An error occurred while requesting your edge function at {exc.request.url!r}." + ) from exc + + return response + + def set_auth(self, token: str) -> None: + """Updates the authorization header + + Parameters + ---------- + token : str + the new jwt token sent in the authorization header + """ + + if not is_valid_jwt(token): + raise ValueError("token must be a valid JWT authorization token string.") + + self.headers["Authorization"] = f"Bearer {token}" + + async def invoke( + self, function_name: str, invoke_options: Optional[Dict] = None + ) -> Union[Dict, bytes]: + """Invokes a function + + Parameters + ---------- + function_name : the name of the function to invoke + invoke_options : object with the following properties + `headers`: object representing the headers to send with the request + `body`: the body of the request + `responseType`: how the response should be parsed. The default is `json` + """ + if not is_valid_str_arg(function_name): + raise ValueError("function_name must a valid string value.") + headers = self.headers + body = None + response_type = "text/plain" + if invoke_options is not None: + headers.update(invoke_options.get("headers", {})) + response_type = invoke_options.get("responseType", "text/plain") + + region = invoke_options.get("region") + if region: + if not isinstance(region, FunctionRegion): + warn(f"Use FunctionRegion({region})") + region = FunctionRegion(region) + + if region.value != "any": + headers["x-region"] = region.value + + body = invoke_options.get("body") + if isinstance(body, str): + headers["Content-Type"] = "text/plain" + elif isinstance(body, dict): + headers["Content-Type"] = "application/json" + + response = await self._request( + "POST", f"{self.url}/{function_name}", headers=headers, json=body + ) + is_relay_error = response.headers.get("x-relay-header") + + if is_relay_error and is_relay_error == "true": + raise FunctionsRelayError(response.json().get("error")) + + if response_type == "json": + data = response.json() + else: + data = response.content + return data diff --git a/.venv/lib/python3.12/site-packages/supafunc/_sync/__init__.py b/.venv/lib/python3.12/site-packages/supafunc/_sync/__init__.py new file mode 100644 index 00000000..9d48db4f --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/_sync/__init__.py @@ -0,0 +1 @@ +from __future__ import annotations diff --git a/.venv/lib/python3.12/site-packages/supafunc/_sync/functions_client.py b/.venv/lib/python3.12/site-packages/supafunc/_sync/functions_client.py new file mode 100644 index 00000000..99780eb7 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/_sync/functions_client.py @@ -0,0 +1,127 @@ +from typing import Any, Dict, Literal, Optional, Union +from warnings import warn + +from httpx import HTTPError, Response + +from ..errors import FunctionsHttpError, FunctionsRelayError +from ..utils import ( + FunctionRegion, + SyncClient, + is_http_url, + is_valid_jwt, + is_valid_str_arg, +) +from ..version import __version__ + + +class SyncFunctionsClient: + def __init__( + self, + url: str, + headers: Dict, + timeout: int, + verify: bool = True, + proxy: Optional[str] = None, + ): + if not is_http_url(url): + raise ValueError("url must be a valid HTTP URL string") + self.url = url + self.headers = { + "User-Agent": f"supabase-py/functions-py v{__version__}", + **headers, + } + self._client = SyncClient( + base_url=self.url, + headers=self.headers, + verify=bool(verify), + timeout=int(abs(timeout)), + proxy=proxy, + follow_redirects=True, + http2=True, + ) + + def _request( + self, + method: Literal["GET", "OPTIONS", "HEAD", "POST", "PUT", "PATCH", "DELETE"], + url: str, + headers: Optional[Dict[str, str]] = None, + json: Optional[Dict[Any, Any]] = None, + ) -> Response: + + user_data = {"data": json} if isinstance(json, str) else {"json": json} + response = self._client.request(method, url, **user_data, headers=headers) + + try: + response.raise_for_status() + except HTTPError as exc: + raise FunctionsHttpError( + response.json().get("error") + or f"An error occurred while requesting your edge function at {exc.request.url!r}." + ) from exc + + return response + + def set_auth(self, token: str) -> None: + """Updates the authorization header + + Parameters + ---------- + token : str + the new jwt token sent in the authorization header + """ + + if not is_valid_jwt(token): + raise ValueError("token must be a valid JWT authorization token string.") + + self.headers["Authorization"] = f"Bearer {token}" + + def invoke( + self, function_name: str, invoke_options: Optional[Dict] = None + ) -> Union[Dict, bytes]: + """Invokes a function + + Parameters + ---------- + function_name : the name of the function to invoke + invoke_options : object with the following properties + `headers`: object representing the headers to send with the request + `body`: the body of the request + `responseType`: how the response should be parsed. The default is `json` + """ + if not is_valid_str_arg(function_name): + raise ValueError("function_name must a valid string value.") + headers = self.headers + body = None + response_type = "text/plain" + if invoke_options is not None: + headers.update(invoke_options.get("headers", {})) + response_type = invoke_options.get("responseType", "text/plain") + + region = invoke_options.get("region") + if region: + if not isinstance(region, FunctionRegion): + warn(f"Use FunctionRegion({region})") + region = FunctionRegion(region) + + if region.value != "any": + headers["x-region"] = region.value + + body = invoke_options.get("body") + if isinstance(body, str): + headers["Content-Type"] = "text/plain" + elif isinstance(body, dict): + headers["Content-Type"] = "application/json" + + response = self._request( + "POST", f"{self.url}/{function_name}", headers=headers, json=body + ) + is_relay_error = response.headers.get("x-relay-header") + + if is_relay_error and is_relay_error == "true": + raise FunctionsRelayError(response.json().get("error")) + + if response_type == "json": + data = response.json() + else: + data = response.content + return data diff --git a/.venv/lib/python3.12/site-packages/supafunc/errors.py b/.venv/lib/python3.12/site-packages/supafunc/errors.py new file mode 100644 index 00000000..ced5f313 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/errors.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from typing import TypedDict + + +class FunctionsApiErrorDict(TypedDict): + name: str + message: str + status: int + + +class FunctionsError(Exception): + def __init__(self, message: str, name: str, status: int) -> None: + super().__init__(message) + self.message = message + self.name = name + self.status = status + + def to_dict(self) -> FunctionsApiErrorDict: + return { + "name": self.name, + "message": self.message, + "status": self.status, + } + + +class FunctionsHttpError(FunctionsError): + def __init__(self, message: str) -> None: + super().__init__( + message, + "FunctionsHttpError", + 400, + ) + + +class FunctionsRelayError(FunctionsError): + """Base exception for relay errors.""" + + def __init__(self, message: str) -> None: + super().__init__( + message, + "FunctionsRelayError", + 400, + ) diff --git a/.venv/lib/python3.12/site-packages/supafunc/utils.py b/.venv/lib/python3.12/site-packages/supafunc/utils.py new file mode 100644 index 00000000..e7bf6453 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/utils.py @@ -0,0 +1,69 @@ +import re +import sys +from urllib.parse import urlparse + +from httpx import AsyncClient as AsyncClient # noqa: F401 +from httpx import Client as BaseClient + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + from strenum import StrEnum + + +DEFAULT_FUNCTION_CLIENT_TIMEOUT = 5 +BASE64URL_REGEX = r"^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)$" + + +class FunctionRegion(StrEnum): + Any = "any" + ApNortheast1 = "ap-northeast-1" + ApNortheast2 = "ap-northeast-2" + ApSouth1 = "ap-south-1" + ApSoutheast1 = "ap-southeast-1" + ApSoutheast2 = "ap-southeast-2" + CaCentral1 = "ca-central-1" + EuCentral1 = "eu-central-1" + EuWest1 = "eu-west-1" + EuWest2 = "eu-west-2" + EuWest3 = "eu-west-3" + SaEast1 = "sa-east-1" + UsEast1 = "us-east-1" + UsWest1 = "us-west-1" + UsWest2 = "us-west-2" + + +class SyncClient(BaseClient): + def aclose(self) -> None: + self.close() + + +def is_valid_str_arg(target: str) -> bool: + return isinstance(target, str) and len(target.strip()) > 0 + + +def is_http_url(url: str) -> bool: + return urlparse(url).scheme in {"https", "http"} + + +def is_valid_jwt(value: str) -> bool: + """Checks if value looks like a JWT, does not do any extra parsing.""" + if not isinstance(value, str): + return False + + # Remove trailing whitespaces if any. + value = value.strip() + + # Remove "Bearer " prefix if any. + if value.startswith("Bearer "): + value = value[7:] + + # Valid JWT must have 2 dots (Header.Paylod.Signature) + if value.count(".") != 2: + return False + + for part in value.split("."): + if not re.search(BASE64URL_REGEX, part, re.IGNORECASE): + return False + + return True diff --git a/.venv/lib/python3.12/site-packages/supafunc/version.py b/.venv/lib/python3.12/site-packages/supafunc/version.py new file mode 100644 index 00000000..be3f964d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/supafunc/version.py @@ -0,0 +1 @@ +__version__ = "0.9.3" # {x-release-please-version} |