diff --git a/polyapi/api.py b/polyapi/api.py index 9533b21..fc243ba 100644 --- a/polyapi/api.py +++ b/polyapi/api.py @@ -23,8 +23,16 @@ def {function_name}( Function ID: {function_id} \""" - resp = execute("{function_type}", "{function_id}", {data}) - return {api_response_type}(resp.json()) # type: ignore + if get_direct_execute_config(): + resp = direct_execute("{function_type}", "{function_id}", {data}) + return {api_response_type}({{ + "status": resp.status_code, + "headers": dict(resp.headers), + "data": resp.json() + }}) # type: ignore + else: + resp = execute("{function_type}", "{function_id}", {data}) + return {api_response_type}(resp.json()) # type: ignore """ diff --git a/polyapi/config.py b/polyapi/config.py index 4b0e856..60eb16f 100644 --- a/polyapi/config.py +++ b/polyapi/config.py @@ -8,6 +8,10 @@ # cached values API_KEY = None API_URL = None +API_FUNCTION_DIRECT_EXECUTE = None +MTLS_CERT_PATH = None +MTLS_KEY_PATH = None +MTLS_CA_PATH = None def get_config_file_path() -> str: @@ -45,6 +49,13 @@ def get_api_key_and_url() -> Tuple[str | None, str | None]: API_KEY = key API_URL = url + # Read and cache MTLS and direct execute settings + global API_FUNCTION_DIRECT_EXECUTE, MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH + API_FUNCTION_DIRECT_EXECUTE = config.get("polyapi", "api_function_direct_execute", fallback="false").lower() == "true" + MTLS_CERT_PATH = config.get("polyapi", "mtls_cert_path", fallback=None) + MTLS_KEY_PATH = config.get("polyapi", "mtls_key_path", fallback=None) + MTLS_CA_PATH = config.get("polyapi", "mtls_ca_path", fallback=None) + return key, url @@ -104,4 +115,22 @@ def clear_config(): path = get_config_file_path() if os.path.exists(path): - os.remove(path) \ No newline at end of file + os.remove(path) + + +def get_mtls_config() -> Tuple[bool, str | None, str | None, str | None]: + """Return MTLS configuration settings""" + global MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH + if MTLS_CERT_PATH is None or MTLS_KEY_PATH is None or MTLS_CA_PATH is None: + # Force a config read if values aren't cached + get_api_key_and_url() + return bool(MTLS_CERT_PATH and MTLS_KEY_PATH and MTLS_CA_PATH), MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH + + +def get_direct_execute_config() -> bool: + """Return whether direct execute is enabled""" + global API_FUNCTION_DIRECT_EXECUTE + if API_FUNCTION_DIRECT_EXECUTE is None: + # Force a config read if value isn't cached + get_api_key_and_url() + return bool(API_FUNCTION_DIRECT_EXECUTE) \ No newline at end of file diff --git a/polyapi/execute.py b/polyapi/execute.py index ab529d3..d066574 100644 --- a/polyapi/execute.py +++ b/polyapi/execute.py @@ -1,22 +1,68 @@ -from typing import Dict +from typing import Dict, Optional import requests from requests import Response -from polyapi.config import get_api_key_and_url +from polyapi.config import get_api_key_and_url, get_mtls_config from polyapi.exceptions import PolyApiException +def direct_execute(function_type, function_id, data) -> Response: + """ execute a specific function id/type + """ + api_key, api_url = get_api_key_and_url() + headers = {"Authorization": f"Bearer {api_key}"} + url = f"{api_url}/functions/{function_type}/{function_id}/direct-execute" + + endpoint_info = requests.post(url, json=data, headers=headers) + if endpoint_info.status_code < 200 or endpoint_info.status_code >= 300: + raise PolyApiException(f"{endpoint_info.status_code}: {endpoint_info.content.decode('utf-8', errors='ignore')}") + + endpoint_info_data = endpoint_info.json() + request_params = endpoint_info_data.copy() + request_params.pop("url", None) + + if "maxRedirects" in request_params: + request_params["allow_redirects"] = request_params.pop("maxRedirects") > 0 + + has_mtls, cert_path, key_path, ca_path = get_mtls_config() + + if has_mtls: + resp = requests.request( + url=endpoint_info_data["url"], + cert=(cert_path, key_path), + verify=ca_path, + **request_params + ) + else: + resp = requests.request( + url=endpoint_info_data["url"], + verify=False, + **request_params + ) + + if resp.status_code < 200 or resp.status_code >= 300: + error_content = resp.content.decode("utf-8", errors="ignore") + raise PolyApiException(f"{resp.status_code}: {error_content}") + + return resp def execute(function_type, function_id, data) -> Response: """ execute a specific function id/type """ api_key, api_url = get_api_key_and_url() headers = {"Authorization": f"Bearer {api_key}"} + url = f"{api_url}/functions/{function_type}/{function_id}/execute" - resp = requests.post(url, json=data, headers=headers) - # print(resp.status_code) - # print(resp.headers["content-type"]) + + # Make the request + resp = requests.post( + url, + json=data, + headers=headers, + ) + if resp.status_code < 200 or resp.status_code >= 300: error_content = resp.content.decode("utf-8", errors="ignore") raise PolyApiException(f"{resp.status_code}: {error_content}") + return resp diff --git a/polyapi/generate.py b/polyapi/generate.py index f6aeceb..1558b9e 100644 --- a/polyapi/generate.py +++ b/polyapi/generate.py @@ -14,7 +14,7 @@ from .server import render_server_function from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace from .variables import generate_variables -from .config import get_api_key_and_url +from .config import get_api_key_and_url, get_direct_execute_config SUPPORTED_FUNCTION_TYPES = { "apiFunction", @@ -46,6 +46,10 @@ def get_specs(contexts=Optional[List[str]], no_types: bool = False) -> List: if contexts: params["contexts"] = contexts + # Add apiFunctionDirectExecute parameter if direct execute is enabled + if get_direct_execute_config(): + params["apiFunctionDirectExecute"] = "true" + resp = requests.get(url, headers=headers, params=params) if resp.status_code == 200: return resp.json() diff --git a/polyapi/utils.py b/polyapi/utils.py index 642ec51..2ea11a2 100644 --- a/polyapi/utils.py +++ b/polyapi/utils.py @@ -16,7 +16,7 @@ # this string should be in every __init__ file. # it contains all the imports needed for the function or variable code to run -CODE_IMPORTS = "from typing import List, Dict, Any, Optional, Callable\nfrom typing_extensions import TypedDict, NotRequired\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update\n\n" +CODE_IMPORTS = "from typing import List, Dict, Any, Optional, Callable\nfrom typing_extensions import TypedDict, NotRequired\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url, get_direct_execute_config\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update, direct_execute\n\n" def init_the_init(full_path: str, code_imports="") -> None: diff --git a/pyproject.toml b/pyproject.toml index 6881a0d..61f6eec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ requires = ["setuptools>=61.2", "wheel"] [project] name = "polyapi-python" -version = "0.3.7.dev0" +version = "0.3.7.dev1" description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers" authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }] dependencies = [