From 0de07eea3f832f30e9cdc46af2859fe208188066 Mon Sep 17 00:00:00 2001 From: Evan Doyle Date: Tue, 23 Jul 2024 15:03:09 -0700 Subject: [PATCH] Allow invoking decorated endpoints from sdk --- gauge/cli/deploy.py | 24 +++++++++++++++--------- gauge/errors.py | 6 ++++++ gauge/sdk/main.py | 40 +++++++++++++++++++++++++++++++++++++--- gauge/settings.py | 10 ++++++++++ pyproject.toml | 30 ++++++++---------------------- 5 files changed, 76 insertions(+), 34 deletions(-) create mode 100644 gauge/errors.py create mode 100644 gauge/settings.py diff --git a/gauge/cli/deploy.py b/gauge/cli/deploy.py index e6c39a0..2f4aa3f 100644 --- a/gauge/cli/deploy.py +++ b/gauge/cli/deploy.py @@ -13,10 +13,9 @@ import requests from rich.console import Console +from gauge import settings from gauge.cli.console import log_error, log_task -API_URL = os.environ.get("GAUGE_API_URL", "http://localhost:8000") - class DeployType(TypedDict): module: str | None @@ -30,8 +29,19 @@ class DeployType(TypedDict): class DeployHandler: - def __init__(self, file_paths: list[str]) -> None: + def __init__( + self, + file_paths: list[str], + api_url: str | None = None, + client_secret: str | None = None + ) -> None: self.file_paths = {Path(file_path) for file_path in file_paths} + self.deploy_url = (api_url or settings.GAUGE_API_URL) + "/deploy" + self.client_secret = client_secret or settings.CLIENT_SECRET + + @property + def headers(self) -> dict[str, str]: + return {"X-Client-Secret": self.client_secret} def validate_file_paths(self) -> None: errored = False @@ -59,21 +69,17 @@ def bundle(self, temp_dir: str) -> Path: return zip_path def upload(self, zip_path: Path, deployments: DeployConfigType) -> None: - gauge_client_id = os.environ.get("GAUGE_CLIENT_ID") or input( - "Input your GAUGE_CLIENT_ID: " - ) with log_task( start_message="Uploading bundle...", end_message="Bundle uploaded" ): with open(zip_path, "rb") as zip_file: files = {"file": zip_file, "json_data": (None, json.dumps(deployments))} resp = requests.post( - API_URL + "/v0.1/deploy/", - headers={"X-Client-Secret": gauge_client_id}, + self.deploy_url, + headers=self.headers, files=files, ) if resp.status_code != 200: - print(resp.status_code, resp.content) log_error("Failed to trigger the deploy") sys.exit(1) diff --git a/gauge/errors.py b/gauge/errors.py new file mode 100644 index 0000000..2147ffa --- /dev/null +++ b/gauge/errors.py @@ -0,0 +1,6 @@ +class GaugeError(Exception): + ... + + +class GaugeInvokeError(GaugeError): + ... diff --git a/gauge/sdk/main.py b/gauge/sdk/main.py index d424053..4d15915 100644 --- a/gauge/sdk/main.py +++ b/gauge/sdk/main.py @@ -1,7 +1,35 @@ from __future__ import annotations +from dataclasses import dataclass, field, asdict +import json + +import requests from typing import Any, Callable +from gauge import settings, errors + + +@dataclass +class RemoteInvocationArguments: + args: list[Any] = field(default_factory=list) + kwargs: dict[Any, Any] = field(default_factory=dict) + + +def invoke_endpoint(function_name: str, arguments: RemoteInvocationArguments) -> Any: + try: + response = requests.post( + f"{settings.GAUGE_API_URL}/invoke/{function_name}/", + headers={"X-Client-Secret": settings.CLIENT_SECRET}, + json=json.dumps(asdict(arguments)) + ) + response.raise_for_status() + return response.json() + except requests.HTTPError as e: + raise errors.GaugeInvokeError(f"Function invocation for '{function_name}' failed with status: {e.response.status_code}") + except Exception as e: + raise errors.GaugeInvokeError(f"Could not invoke function: '{function_name}' due to error:\n{e}") + + def endpoint( name: str, python_version: str = "3.12", dependencies: list[str] = [] @@ -18,7 +46,7 @@ def _gauge_register() -> tuple[str, dict[str, str | list[str]]]: function._gauge_register = _gauge_register # pyright: ignore[reportFunctionMemberAccess] - def as_lambda_function_url_handler() -> Callable[[Any, Any], Any]: + def _as_lambda_handler() -> Callable[[Any, Any], Any]: def _lambda_handler(event: Any, context: Any) -> Any: if not isinstance(event, dict): return { @@ -26,13 +54,19 @@ def _lambda_handler(event: Any, context: Any) -> Any: "detail": "Could not parse incoming data. The request body must be JSON.", } try: - return {"status": 200, "result": function(**event)} + return {"status": 200, "result": function(*event["args"], **event["kwargs"])} except Exception as e: return {"status": 500, "detail": str(e)} return _lambda_handler - function.as_lambda_function_url_handler = as_lambda_function_url_handler # pyright: ignore[reportFunctionMemberAccess] + function.as_lambda_function_url_handler = _as_lambda_handler # pyright: ignore[reportFunctionMemberAccess] + + def _invoke_fn(*args, **kwargs) -> Callable[..., Any]: # type: ignore + return invoke_endpoint(name, RemoteInvocationArguments(args=args, kwargs=kwargs)) # type: ignore + + function.invoke = _invoke_fn # pyright: ignore[reportFunctionMemberAccess] + return function return endpoint_decorator diff --git a/gauge/settings.py b/gauge/settings.py new file mode 100644 index 0000000..8f71f0b --- /dev/null +++ b/gauge/settings.py @@ -0,0 +1,10 @@ +# type: ignore +from __future__ import annotations + +from environs import Env + +env = Env() +env.read_env() + +GAUGE_API_URL: str = env.str("GAUGE_API_URL", "http://localhost:8000/v0.1") +CLIENT_SECRET: str = env.str("CLIENT_SECRET", "") diff --git a/pyproject.toml b/pyproject.toml index 7518835..0745b2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,20 +30,17 @@ dependencies = [ "pydantic~=2.0", "stdlib-list>=0.10.0; python_version < '3.10'", "eval-type-backport>=0.2.0; python_version < '3.10'", - "boto3~=1.34.145", "requests~=2.32.3", - + "environs~=11.0", ] keywords = ['python', 'lambda', 'aws', 'serverless', 'fastapi'] [project.optional-dependencies] dev = [ # Core deps (pinned) "pyyaml==6.0.1", - "pydantic==2.8.2; python_version > '3.7'", + "pydantic==2.8.2", "rich==13.7.1", "fastapi==0.111.1", - "boto3-stubs-lite==1.34.145", - "boto3-stubs==1.34.145", "tach==0.8.1", # Setup "pip==24.0", @@ -51,24 +48,13 @@ dev = [ "pyright==1.1.372", "ruff==0.5.2", # Build/Release - "setuptools==69.5.1; python_version > '3.7'", - "twine==5.1.1; python_version > '3.7'", - "build==1.2.1; python_version > '3.7'", + "setuptools==69.5.1", + "twine==5.1.1", + "build==1.2.1", # Tests - "pytest==8.2.2; python_version > '3.7'", - "pytest-mock==3.14.0; python_version > '3.7'", - "coverage==7.6.0; python_version > '3.7'", - # python version 3.7 pinned dependencies - "botocore==1.33.13; python_version == '3.7'", - "boto3-stubs-lite==1.33.13; python_version == '3.7'", - "boto3-stubs==1.33.13; python_version == '3.7'", - "pydantic==2.5.3; python_version == '3.7'", - "setuptools==47.1.0; python_version == '3.7'", - "twine==4.0.2; python_version == '3.7'", - "build==1.1.1; python_version == '3.7'", - "pytest==7.4.4; python_version == '3.7'", - "pytest-mock==3.11.1; python_version == '3.7'", - "coverage==7.2.7; python_version == '3.7'", + "pytest==8.2.2", + "pytest-mock==3.14.0", + "coverage==7.6.0", ]