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: 1 addition & 1 deletion polyapi/function_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def function_add_or_update(

headers = get_auth_headers(api_key)
resp = requests.post(url, headers=headers, json=data)
if resp.status_code == 201:
if resp.status_code in [200, 201]:
print_green("DEPLOYED")
function_id = resp.json()["id"]
print(f"Function ID: {function_id}")
Expand Down
133 changes: 105 additions & 28 deletions polyapi/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,24 +129,26 @@ def parse_function_specs(
) -> List[SpecificationDto]:
functions = []
for spec in specs:
if not spec or "function" not in spec:
continue

if not spec["function"]:
continue

if limit_ids and spec["id"] not in limit_ids:
if not spec:
continue

# For no_types mode, we might not have function data, but we still want to include the spec
# if it's a supported function type
if spec["type"] not in SUPPORTED_FUNCTION_TYPES:
continue

if spec["type"] == "customFunction" and spec["language"] != "python":
# poly libraries only support client functions of same language
# Skip if we have a limit and this spec is not in it
if limit_ids and spec.get("id") not in limit_ids:
continue

# For customFunction, check language if we have function data
if spec["type"] == "customFunction":
if spec.get("language") and spec["language"] != "python":
# poly libraries only support client functions of same language
continue

# Functions with serverSideAsync True will always return a Dict with execution ID
if spec.get('serverSideAsync'):
if spec.get('serverSideAsync') and spec.get("function"):
spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'}

functions.append(spec)
Expand Down Expand Up @@ -205,6 +207,63 @@ def remove_old_library():
shutil.rmtree(path)


def create_empty_schemas_module():
"""Create an empty schemas module for no-types mode so user code can still import from polyapi.schemas"""
currdir = os.path.dirname(os.path.abspath(__file__))
schemas_path = os.path.join(currdir, "schemas")

# Create the schemas directory
if not os.path.exists(schemas_path):
os.makedirs(schemas_path)

# Create an __init__.py file with dynamic schema resolution
init_path = os.path.join(schemas_path, "__init__.py")
with open(init_path, "w") as f:
f.write('''"""Empty schemas module for no-types mode"""
from typing import Any, Dict

class _GenericSchema(Dict[str, Any]):
"""Generic schema type that acts like a Dict for no-types mode"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

class _SchemaModule:
"""Dynamic module that returns itself for attribute access, allowing infinite nesting"""

def __getattr__(self, name: str):
# For callable access (like schemas.Response()), return the generic schema class
# For further attribute access (like schemas.random.random2), return self to allow nesting
return _NestedSchemaAccess()

def __call__(self, *args, **kwargs):
# If someone tries to call the module itself, return a generic schema
return _GenericSchema(*args, **kwargs)

def __dir__(self):
# Return common schema names for introspection
return ['Response', 'Request', 'Error', 'Data', 'Result']

class _NestedSchemaAccess:
"""Handles nested attribute access and final callable resolution"""

def __getattr__(self, name: str):
# Continue allowing nested access
return _NestedSchemaAccess()

def __call__(self, *args, **kwargs):
# When finally called, return a generic schema instance
return _GenericSchema(*args, **kwargs)

def __class_getitem__(cls, item):
# Support type annotations like schemas.Response[str]
return _GenericSchema

# Replace this module with our dynamic module
import sys
sys.modules[__name__] = _SchemaModule()
''')


def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> None:
generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
print(generate_msg, end="", flush=True)
Expand All @@ -216,14 +275,23 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
functions = parse_function_specs(specs, limit_ids=limit_ids)

schemas = get_schemas()
schema_index = build_schema_index(schemas)
if schemas:
schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
schemas = replace_poly_refs_in_schemas(schemas, schema_index)
generate_schemas(schemas, limit_ids=schema_limit_ids)

functions = replace_poly_refs_in_functions(functions, schema_index)
# Only process schemas if no_types is False
if not no_types:
schemas = get_schemas()
schema_index = build_schema_index(schemas)
if schemas:
schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
schemas = replace_poly_refs_in_schemas(schemas, schema_index)
generate_schemas(schemas, limit_ids=schema_limit_ids)

functions = replace_poly_refs_in_functions(functions, schema_index)
else:
# When no_types is True, we still need to process functions but without schema resolution
# Use an empty schema index to avoid poly-ref resolution
schema_index = {}

# Create an empty schemas module so user code can still import from polyapi.schemas
create_empty_schemas_module()

if functions:
generate_functions(functions)
Expand All @@ -233,10 +301,11 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
)
exit()

variables = get_variables()
if variables:
generate_variables(variables)

# Only process variables if no_types is False
if not no_types:
variables = get_variables()
if variables:
generate_variables(variables)

# indicator to vscode extension that this is a polyapi-python project
file_path = os.path.join(os.getcwd(), ".polyapi-python")
Expand Down Expand Up @@ -266,11 +335,19 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:

arguments: List[PropertySpecification] = []
return_type = {}
if spec["function"]:
arguments = [
arg for arg in spec["function"]["arguments"]
]
return_type = spec["function"]["returnType"]
if spec.get("function"):
# Handle cases where arguments might be missing or None
if spec["function"].get("arguments"):
arguments = [
arg for arg in spec["function"]["arguments"]
]

# Handle cases where returnType might be missing or None
if spec["function"].get("returnType"):
return_type = spec["function"]["returnType"]
else:
# Provide a fallback return type when missing
return_type = {"kind": "any"}

if function_type == "apiFunction":
func_str, func_type_defs = render_api_function(
Expand All @@ -284,7 +361,7 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
elif function_type == "customFunction":
func_str, func_type_defs = render_client_function(
function_name,
spec["code"],
spec.get("code", ""),
arguments,
return_type,
)
Expand Down
57 changes: 46 additions & 11 deletions polyapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,32 @@ def get_type_and_def(
) -> Tuple[str, str]:
""" returns type and type definition for a given PropertyType
"""
# Handle cases where type_spec might be None or empty
if not type_spec:
return "Any", ""

# Handle cases where kind might be missing
if "kind" not in type_spec:
return "Any", ""

if type_spec["kind"] == "plain":
value = type_spec["value"]
value = type_spec.get("value", "")
if value.endswith("[]"):
primitive = map_primitive_types(value[:-2])
return f"List[{primitive}]", ""
else:
return map_primitive_types(value), ""
elif type_spec["kind"] == "primitive":
return map_primitive_types(type_spec["type"]), ""
return map_primitive_types(type_spec.get("type", "any")), ""
elif type_spec["kind"] == "array":
if type_spec.get("items"):
items = type_spec["items"]
if items.get("$ref"):
return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
# For no-types mode, avoid complex schema generation
try:
return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
except:
return "List[Dict]", ""
else:
item_type, _ = get_type_and_def(items)
title = f"List[{item_type}]"
Expand All @@ -130,13 +142,20 @@ def get_type_and_def(
return "List", ""
elif title:
assert isinstance(title, str)
return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
# For no-types mode, avoid complex schema generation
try:
return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
except:
return "Dict", ""
elif schema.get("allOf") and len(schema["allOf"]):
# we are in a case of a single allOf, lets strip off the allOf and move on!
# our library doesn't handle allOf well yet
allOf = schema["allOf"][0]
title = allOf.get("title", allOf.get("name", title_fallback))
return wrapped_generate_schema_types(allOf, title, "Dict")
try:
return wrapped_generate_schema_types(allOf, title, "Dict")
except:
return "Dict", ""
elif schema.get("items"):
# fallback to schema $ref name if no explicit title
items = schema.get("items") # type: ignore
Expand All @@ -150,7 +169,10 @@ def get_type_and_def(
return "List", ""

title = f"List[{title}]"
return wrapped_generate_schema_types(schema, title, "List")
try:
return wrapped_generate_schema_types(schema, title, "List")
except:
return "List[Dict]", ""
elif schema.get("properties"):
result = wrapped_generate_schema_types(schema, "ResponseType", "Dict") # type: ignore
return result
Expand Down Expand Up @@ -190,9 +212,13 @@ def get_type_and_def(


def _maybe_add_fallback_schema_name(a: PropertySpecification):
if a["type"]["kind"] == "object" and a["type"].get("schema"):
# Handle cases where type might be missing
if not a.get("type"):
return

if a["type"].get("kind") == "object" and a["type"].get("schema"):
schema = a["type"].get("schema", {})
if not schema.get("title") and not schema.get("name") and a["name"]:
if not schema.get("title") and not schema.get("name") and a.get("name"):
schema["title"] = a["name"].title()


Expand All @@ -203,14 +229,23 @@ def parse_arguments(
arg_string = ""
for idx, a in enumerate(arguments):
_maybe_add_fallback_schema_name(a)
arg_type, arg_def = get_type_and_def(a["type"])

# Handle cases where type might be missing
arg_type_spec = a.get("type", {"kind": "any"})
arg_type, arg_def = get_type_and_def(arg_type_spec)
if arg_def:
args_def.append(arg_def)
a["name"] = rewrite_arg_name(a["name"])

# Handle cases where name might be missing
arg_name = a.get("name", f"arg{idx}")
a["name"] = rewrite_arg_name(arg_name)

arg_string += (
f" {a['name']}: {add_type_import_path(function_name, arg_type)}"
)
if not a["required"]:

# Handle cases where required might be missing
if not a.get("required", True):
arg_string += " = None"

description = a.get("description", "")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["setuptools>=61.2", "wheel"]

[project]
name = "polyapi-python"
version = "0.3.7.dev2"
version = "0.3.7.dev3"
description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
dependencies = [
Expand Down
Loading