about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/supafunc
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/supafunc')
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/__init__.py34
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/_async/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/_async/functions_client.py127
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/_sync/__init__.py1
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/_sync/functions_client.py127
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/errors.py44
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/utils.py69
-rw-r--r--.venv/lib/python3.12/site-packages/supafunc/version.py1
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}