Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions polyapi/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ def {function_name}(
\"""
resp = execute("{function_type}", "{function_id}", {data})
return {api_response_type}(resp.json()) # type: ignore


"""


Expand Down
6 changes: 3 additions & 3 deletions polyapi/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@


class AuthFunctionResponse(TypedDict):
status: int
data: Any
headers: Dict[str, str]
status: int
data: Any
headers: Dict[str, str]


async def getToken(clientId: str, clientSecret: str, scopes: List[str], callback, options: Optional[Dict[str, Any]] = None):
Expand Down
17 changes: 12 additions & 5 deletions polyapi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
from polyapi.utils import print_green

from .config import clear_config, set_api_key_and_url
from .generate import generate, clear, save_rendered_specs
from .generate import generate, clear
from .function_cli import function_add_or_update
from .rendered_spec import get_and_update_rendered_spec


CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "save_rendered_specs"]
CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]

CLIENT_DESC = """Commands
python -m polyapi setup Setup your Poly connection
Expand All @@ -17,7 +18,7 @@
"""


def execute_from_cli():
def execute_from_cli() -> None:
parser = argparse.ArgumentParser(
prog="python -m polyapi", description=CLIENT_DESC, formatter_class=argparse.RawTextHelpFormatter
)
Expand All @@ -42,8 +43,14 @@ def execute_from_cli():
elif command == "setup":
clear_config()
generate()
elif command == "save_rendered_specs":
save_rendered_specs()
elif command == "update_rendered_spec":
assert len(args.subcommands) == 2
updated = get_and_update_rendered_spec(args.subcommands[0], args.subcommands[1])
if updated:
print("Updated rendered spec!")
else:
print("Failed to update rendered spec!")
exit(1)
elif command == "clear":
print("Clearing the generated library...")
clear()
Expand Down
4 changes: 4 additions & 0 deletions polyapi/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
"boolean": "bool",
"array": "List",
"object": "Dict",
"function": "Callable",
"void": "None",
}


Expand All @@ -15,6 +17,8 @@
"bool": "boolean",
"List": "array",
"Dict": "object",
"Callable": "function",
"None": "void",
}

BASIC_PYTHON_TYPES = set(PYTHON_TO_JSONSCHEMA_TYPE_MAP.keys())
Expand Down
19 changes: 0 additions & 19 deletions polyapi/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

from polyapi.auth import render_auth_function
from polyapi.client import render_client_function
from polyapi.execute import execute_post
from polyapi.webhook import render_webhook_handle

from .typedefs import PropertySpecification, SpecificationDto, VariableSpecDto
Expand Down Expand Up @@ -157,24 +156,6 @@ def clear() -> None:
print("Cleared!")


def save_rendered_specs() -> None:
specs = read_cached_specs()
# right now we just support rendered apiFunctions
api_specs = [spec for spec in specs if spec["type"] == "apiFunction"]
for spec in api_specs:
assert spec["function"]
func_str, type_defs = render_spec(spec)
data = {
"language": "python",
"apiFunctionId": spec["id"],
"signature": func_str,
"typedefs": type_defs,
}
resp = execute_post("/functions/rendered-specs", data)
print("adding", spec["context"], spec["name"])
assert resp.status_code == 201, (resp.text, resp.status_code)


def render_spec(spec: SpecificationDto):
function_type = spec["type"]
function_description = spec["description"]
Expand Down
73 changes: 73 additions & 0 deletions polyapi/rendered_spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import os
from typing import Optional

import requests
from polyapi.config import get_api_key_and_url
from polyapi.generate import read_cached_specs, render_spec
from polyapi.typedefs import SpecificationDto


def update_rendered_spec(api_key: str, spec: SpecificationDto):
print("Updating rendered spec...")
func_str, type_defs = render_spec(spec)
data = {
"language": "python",
"signature": func_str,
"typedefs": type_defs,
}
if spec["type"] == "apiFunction":
data["apiFunctionId"] = spec["id"]
elif spec["type"] == "serverFunction":
data["customFunctionId"] = spec["id"]
elif spec["type"] == "clientFunction":
data["customFunctionId"] = spec["id"]
elif spec["type"] == "webhookHandle":
data["webhookHandleId"] = spec["id"]
else:
raise NotImplementedError("todo")

# use super key on develop-k8s here!
_, base_url = get_api_key_and_url()
if not base_url:
base_url = os.environ.get("HOST_URL")

url = f"{base_url}/functions/rendered-specs"
headers = {"Authorization": f"Bearer {api_key}"}
resp = requests.post(url, json=data, headers=headers)
assert resp.status_code == 201, (resp.text, resp.status_code)


def _get_spec(api_key: str, spec_id: str) -> Optional[SpecificationDto]:
_, base_url = get_api_key_and_url()
if not base_url:
base_url = os.environ.get("HOST_URL")

url = f"{base_url}/specs"
headers = {"Authorization": f"Bearer {api_key}"}
resp = requests.get(url, headers=headers)
if resp.status_code == 200:
specs = resp.json()
for spec in specs:
if spec['id'] == spec_id:
return spec
return None
else:
raise NotImplementedError(resp.content)


def get_and_update_rendered_spec(api_key: str, spec_id: str) -> bool:
spec = _get_spec(api_key, spec_id)
if spec:
update_rendered_spec(api_key, spec)
return True
return False


def save_rendered_specs() -> None:
specs = read_cached_specs()
# right now we just support rendered apiFunctions
api_specs = [spec for spec in specs if spec["type"] == "apiFunction"]
for spec in api_specs:
assert spec["function"]
print("adding", spec["context"], spec["name"])
update_rendered_spec("FIXME", spec)
27 changes: 22 additions & 5 deletions polyapi/schema.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import logging
import contextlib
from typing import Dict
from jsonschema_gentypes.cli import process_config
Expand All @@ -18,10 +19,8 @@ def _cleanup_input_for_gentypes(input_data: Dict):
# jsonschema_gentypes doesn't like double quotes in enums
# TODO fix this upstream
for idx, enum in enumerate(v):
assert isinstance(enum, str)
v[idx] = enum.replace('"', "'")


if isinstance(enum, str):
v[idx] = enum.replace('"', "'")


def _temp_store_input_data(input_data: Dict) -> str:
Expand All @@ -33,6 +32,23 @@ def _temp_store_input_data(input_data: Dict) -> str:
return temp_file.name


def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
if not root:
root = "MyList" if fallback_type == "List" else "MyDict"

root = clean_title(root)

try:
return root, generate_schema_types(type_spec, root=root)
except RecursionError:
# some schemas are so huge, our library cant handle it
# TODO identify critical recursion penalty and maybe switch underlying logic to iterative?
return fallback_type, ""
except:
logging.exception(f"Error when generating schema type: {type_spec}")
return fallback_type, ""


def generate_schema_types(input_data: Dict, root=None):
"""takes in a Dict representing a schema as input then appends the resulting python code to the output file"""
_cleanup_input_for_gentypes(input_data)
Expand All @@ -48,14 +64,15 @@ def generate_schema_types(input_data: Dict, root=None):
"source": tmp_input,
"destination": tmp_output,
"root_name": root,
"api_arguments": {"get_name_properties": "UpperFirst"},
}
],
}

# jsonschema_gentypes prints source to stdout
# no option to surpress so we do this
with contextlib.redirect_stdout(None):
process_config(config)
process_config(config, [tmp_input])

with open(tmp_output) as f:
output = f.read()
Expand Down
4 changes: 3 additions & 1 deletion polyapi/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from polyapi.utils import camelCase, add_type_import_path, parse_arguments, get_type_and_def

SERVER_DEFS_TEMPLATE = """
from typing import List, Dict, Any, TypedDict
from typing import List, Dict, Any, TypedDict, Callable
{args_def}
{return_type_def}
"""
Expand All @@ -22,6 +22,8 @@ def {function_name}(
return {return_action}
except:
return resp.text


"""


Expand Down
1 change: 1 addition & 0 deletions polyapi/typedefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class PropertySpecification(TypedDict):

class PropertyType(TypedDict):
kind: Literal['void', 'primitive', 'array', 'object', 'function', 'plain']
spec: NotRequired[Dict]
name: NotRequired[str]
type: NotRequired[str]
items: NotRequired['PropertyType']
Expand Down
49 changes: 29 additions & 20 deletions polyapi/utils.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import re
import os
import logging
from typing import Tuple, List
from colorama import Fore, Style
from polyapi.constants import BASIC_PYTHON_TYPES
from polyapi.typedefs import PropertySpecification, PropertyType
from polyapi.schema import generate_schema_types, clean_title, map_primitive_types
from polyapi.schema import wrapped_generate_schema_types, clean_title, map_primitive_types


# 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, TypedDict, Optional\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, TypedDict, Optional, Callable\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"
FALLBACK_TYPES = {"Dict", "List"}


def init_the_init(full_path: str) -> None:
Expand Down Expand Up @@ -61,6 +61,10 @@ def print_red(s: str):
def add_type_import_path(function_name: str, arg: str) -> str:
""" if not basic type, coerce to camelCase and add the import path
"""
# for now, just treat Callables as basic types
if arg.startswith("Callable"):
return arg

if arg in BASIC_PYTHON_TYPES:
return arg

Expand Down Expand Up @@ -92,11 +96,7 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
if type_spec.get("items"):
items = type_spec["items"]
if items.get("$ref"):
try:
return "ResponseType", generate_schema_types(type_spec, root="ResponseType") # type: ignore
except:
logging.exception(f"Error when generating schema type: {type_spec}")
return "Dict", ""
return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
else:
item_type, _ = get_type_and_def(items)
title = f"List[{item_type}]"
Expand All @@ -112,12 +112,7 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
title = schema.get("title", "")
if title:
assert isinstance(title, str)
title = clean_title(title)
try:
return title, generate_schema_types(schema, root=title) # type: ignore
except:
logging.exception(f"Error when generating schema type: {schema}")
return "Dict", ""
return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore

elif schema.get("items"):
# fallback to schema $ref name if no explicit title
Expand All @@ -128,20 +123,34 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
title = items.get("$ref", "") # type: ignore

title = title.rsplit("/", 1)[-1]
title = clean_title(title)
if not title:
return "List", ""

title = f"List[{title}]"
try:
return title, generate_schema_types(schema, root=title)
except:
logging.exception(f"Error when generating schema type: {schema}")
return "List", ""
return wrapped_generate_schema_types(schema, title, "List")
else:
return "Any", ""
else:
return "Dict", ""
elif type_spec["kind"] == "function":
arg_types = []
arg_defs = []
if "spec" in type_spec:
return_type, _ = get_type_and_def(type_spec["spec"]["returnType"])
if return_type not in BASIC_PYTHON_TYPES:
# for now only Python only supports basic types as return types
return_type = "Any"

for argument in type_spec["spec"]["arguments"]:
arg_type, arg_def = get_type_and_def(argument["type"])
arg_types.append(arg_type)
if arg_def:
arg_defs.append(arg_def)

final_arg_type = "Callable[[{}], {}]".format(", ".join(arg_types), return_type)
return final_arg_type, "\n".join(arg_defs)
else:
return "Callable", ""
elif type_spec["kind"] == "any":
return "Any", ""
else:
Expand Down
Loading