polyapi-python 0.3.2.dev2__py3-none-any.whl → 0.3.3.dev9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polyapi/api.py +2 -0
- polyapi/cli.py +2 -3
- polyapi/config.py +5 -1
- polyapi/deployables.py +4 -4
- polyapi/execute.py +6 -1
- polyapi/function_cli.py +3 -6
- polyapi/generate.py +100 -27
- polyapi/parser.py +15 -13
- polyapi/poly_schemas.py +95 -0
- polyapi/schema.py +35 -2
- polyapi/server.py +9 -5
- polyapi/typedefs.py +14 -0
- polyapi/utils.py +86 -23
- {polyapi_python-0.3.2.dev2.dist-info → polyapi_python-0.3.3.dev9.dist-info}/METADATA +26 -5
- polyapi_python-0.3.3.dev9.dist-info/RECORD +31 -0
- {polyapi_python-0.3.2.dev2.dist-info → polyapi_python-0.3.3.dev9.dist-info}/WHEEL +1 -1
- {polyapi_python-0.3.2.dev2.dist-info → polyapi_python-0.3.3.dev9.dist-info/licenses}/LICENSE +1 -1
- polyapi_python-0.3.2.dev2.dist-info/RECORD +0 -30
- {polyapi_python-0.3.2.dev2.dist-info → polyapi_python-0.3.3.dev9.dist-info}/top_level.txt +0 -0
polyapi/api.py
CHANGED
|
@@ -8,6 +8,7 @@ API_DEFS_TEMPLATE = """
|
|
|
8
8
|
from typing import List, Dict, Any, TypedDict
|
|
9
9
|
{args_def}
|
|
10
10
|
{return_type_def}
|
|
11
|
+
|
|
11
12
|
class {api_response_type}(TypedDict):
|
|
12
13
|
status: int
|
|
13
14
|
headers: Dict
|
|
@@ -41,6 +42,7 @@ def render_api_function(
|
|
|
41
42
|
arg_names = [a["name"] for a in arguments]
|
|
42
43
|
args, args_def = parse_arguments(function_name, arguments)
|
|
43
44
|
return_type_name, return_type_def = get_type_and_def(return_type) # type: ignore
|
|
45
|
+
|
|
44
46
|
data = "{" + ", ".join([f"'{arg}': {rewrite_arg_name(arg)}" for arg in arg_names]) + "}"
|
|
45
47
|
|
|
46
48
|
api_response_type = f"{function_name}Response"
|
polyapi/cli.py
CHANGED
|
@@ -13,6 +13,7 @@ from .sync import sync_deployables
|
|
|
13
13
|
|
|
14
14
|
CLI_COMMANDS = ["setup", "generate", "function", "clear", "help", "update_rendered_spec"]
|
|
15
15
|
|
|
16
|
+
|
|
16
17
|
def execute_from_cli():
|
|
17
18
|
# First we setup all our argument parsing logic
|
|
18
19
|
# Then we parse the arguments (waaay at the bottom)
|
|
@@ -21,7 +22,7 @@ def execute_from_cli():
|
|
|
21
22
|
description="Manage your Poly API configurations and functions",
|
|
22
23
|
formatter_class=argparse.RawTextHelpFormatter
|
|
23
24
|
)
|
|
24
|
-
|
|
25
|
+
|
|
25
26
|
subparsers = parser.add_subparsers(help="Available commands")
|
|
26
27
|
|
|
27
28
|
###########################################################################
|
|
@@ -46,9 +47,7 @@ def execute_from_cli():
|
|
|
46
47
|
|
|
47
48
|
def generate_command(args):
|
|
48
49
|
initialize_config()
|
|
49
|
-
print("Generating Poly functions...", end="")
|
|
50
50
|
generate()
|
|
51
|
-
print_green("DONE")
|
|
52
51
|
|
|
53
52
|
generate_parser.set_defaults(command=generate_command)
|
|
54
53
|
|
polyapi/config.py
CHANGED
|
@@ -55,6 +55,10 @@ def set_api_key_and_url(key: str, url: str):
|
|
|
55
55
|
config.set("polyapi", "poly_api_base_url", url)
|
|
56
56
|
with open(get_config_file_path(), "w") as f:
|
|
57
57
|
config.write(f)
|
|
58
|
+
global API_KEY
|
|
59
|
+
global API_URL
|
|
60
|
+
API_KEY = key
|
|
61
|
+
API_URL = url
|
|
58
62
|
|
|
59
63
|
|
|
60
64
|
def initialize_config(force=False):
|
|
@@ -81,7 +85,7 @@ def initialize_config(force=False):
|
|
|
81
85
|
sys.exit(1)
|
|
82
86
|
|
|
83
87
|
set_api_key_and_url(key, url)
|
|
84
|
-
print_green(
|
|
88
|
+
print_green("Poly setup complete.")
|
|
85
89
|
|
|
86
90
|
if not key or not url:
|
|
87
91
|
print_yellow("Poly API Key and Poly API Base URL are required.")
|
polyapi/deployables.py
CHANGED
|
@@ -261,7 +261,7 @@ def update_deployable_function_comments(file_content: str, deployable: dict, dis
|
|
|
261
261
|
if deployable["docStartIndex"] == deployable["docEndIndex"]:
|
|
262
262
|
# Function doesn't yet have any docstrings so we need to add additional whitespace
|
|
263
263
|
docstring = " " + docstring + "\n"
|
|
264
|
-
|
|
264
|
+
|
|
265
265
|
return f"{file_content[:deployable['docStartIndex']]}{docstring}{file_content[deployable['docEndIndex']:]}"
|
|
266
266
|
return file_content
|
|
267
267
|
|
|
@@ -271,17 +271,17 @@ def write_updated_deployable(deployable: dict, disable_docs: bool = False) -> di
|
|
|
271
271
|
"""
|
|
272
272
|
with open(deployable['file'], 'r', encoding='utf-8') as file:
|
|
273
273
|
file_contents = file.read()
|
|
274
|
-
|
|
274
|
+
|
|
275
275
|
if deployable['type'] in ['client-function', 'server-function']:
|
|
276
276
|
file_contents = update_deployable_function_comments(file_contents, deployable, disable_docs)
|
|
277
277
|
else:
|
|
278
278
|
raise ValueError(f"Unsupported deployable type: '{deployable['type']}'")
|
|
279
279
|
|
|
280
280
|
file_contents = update_deployment_comments(file_contents, deployable)
|
|
281
|
-
|
|
281
|
+
|
|
282
282
|
with open(deployable['file'], 'w', encoding='utf-8') as file:
|
|
283
283
|
file.write(file_contents)
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
deployable['fileRevision'] = get_deployable_file_revision(file_contents)
|
|
286
286
|
return deployable
|
|
287
287
|
|
polyapi/execute.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Dict
|
|
1
2
|
import requests
|
|
2
3
|
from requests import Response
|
|
3
4
|
from polyapi.config import get_api_key_and_url
|
|
@@ -7,10 +8,14 @@ from polyapi.exceptions import PolyApiException
|
|
|
7
8
|
def execute(function_type, function_id, data) -> Response:
|
|
8
9
|
""" execute a specific function id/type
|
|
9
10
|
"""
|
|
11
|
+
data_without_None = data
|
|
12
|
+
if isinstance(data, Dict):
|
|
13
|
+
data_without_None = {k: v for k, v in data.items() if v is not None}
|
|
14
|
+
|
|
10
15
|
api_key, api_url = get_api_key_and_url()
|
|
11
16
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
12
17
|
url = f"{api_url}/functions/{function_type}/{function_id}/execute"
|
|
13
|
-
resp = requests.post(url, json=
|
|
18
|
+
resp = requests.post(url, json=data_without_None, headers=headers)
|
|
14
19
|
# print(resp.status_code)
|
|
15
20
|
# print(resp.headers["content-type"])
|
|
16
21
|
if resp.status_code < 200 or resp.status_code >= 300:
|
polyapi/function_cli.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
from typing import Any, List, Optional
|
|
3
3
|
import requests
|
|
4
|
-
from polyapi.generate import
|
|
4
|
+
from polyapi.generate import generate as generate_library
|
|
5
5
|
from polyapi.config import get_api_key_and_url
|
|
6
6
|
from polyapi.utils import get_auth_headers, print_green, print_red, print_yellow
|
|
7
7
|
from polyapi.parser import parse_function_code, get_jsonschema_type
|
|
@@ -55,7 +55,7 @@ def function_add_or_update(
|
|
|
55
55
|
"code": code,
|
|
56
56
|
"language": "python",
|
|
57
57
|
"returnType": get_jsonschema_type(return_type),
|
|
58
|
-
"arguments": [{**p, "key": p["name"], "type": get_jsonschema_type(p["type"])
|
|
58
|
+
"arguments": [{**p, "key": p["name"], "type": get_jsonschema_type(p["type"])} for p in parsed["types"]["params"]],
|
|
59
59
|
"logsEnabled": logs_enabled,
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -87,10 +87,7 @@ def function_add_or_update(
|
|
|
87
87
|
function_id = resp.json()["id"]
|
|
88
88
|
print(f"Function ID: {function_id}")
|
|
89
89
|
if generate:
|
|
90
|
-
|
|
91
|
-
functions = get_functions_and_parse(limit_ids=[function_id])
|
|
92
|
-
generate_functions(functions)
|
|
93
|
-
print_green("DONE")
|
|
90
|
+
generate_library()
|
|
94
91
|
else:
|
|
95
92
|
print("Error adding function.")
|
|
96
93
|
print(resp.status_code)
|
polyapi/generate.py
CHANGED
|
@@ -2,28 +2,38 @@ import json
|
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, Tuple, cast
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
from
|
|
9
|
-
from
|
|
7
|
+
from .auth import render_auth_function
|
|
8
|
+
from .client import render_client_function
|
|
9
|
+
from .poly_schemas import generate_schemas
|
|
10
|
+
from .webhook import render_webhook_handle
|
|
10
11
|
|
|
11
|
-
from .typedefs import PropertySpecification, SpecificationDto, VariableSpecDto
|
|
12
|
+
from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto
|
|
12
13
|
from .api import render_api_function
|
|
13
14
|
from .server import render_server_function
|
|
14
|
-
from .utils import add_import_to_init, get_auth_headers, init_the_init, to_func_namespace
|
|
15
|
+
from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
|
|
15
16
|
from .variables import generate_variables
|
|
16
17
|
from .config import get_api_key_and_url
|
|
17
18
|
|
|
18
19
|
SUPPORTED_FUNCTION_TYPES = {
|
|
19
20
|
"apiFunction",
|
|
20
21
|
"authFunction",
|
|
21
|
-
"customFunction",
|
|
22
|
+
"customFunction", # client function - this is badly named in /specs atm
|
|
22
23
|
"serverFunction",
|
|
23
24
|
"webhookHandle",
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable"}
|
|
27
|
+
SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
X_POLY_REF_WARNING = '''"""
|
|
31
|
+
x-poly-ref:
|
|
32
|
+
path:'''
|
|
33
|
+
|
|
34
|
+
X_POLY_REF_BETTER_WARNING = '''"""
|
|
35
|
+
Unresolved schema, please add the following schema to complete it:
|
|
36
|
+
path:'''
|
|
27
37
|
|
|
28
38
|
|
|
29
39
|
def get_specs() -> List:
|
|
@@ -38,9 +48,56 @@ def get_specs() -> List:
|
|
|
38
48
|
raise NotImplementedError(resp.content)
|
|
39
49
|
|
|
40
50
|
|
|
51
|
+
def build_schema_index(items):
|
|
52
|
+
index = {}
|
|
53
|
+
for item in items:
|
|
54
|
+
if item.get("type") == "schema" and "contextName" in item:
|
|
55
|
+
index[item["contextName"]] = {**item.get("definition", {}), "name": item.get("name")}
|
|
56
|
+
return index
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def resolve_poly_refs(obj, schema_index):
|
|
60
|
+
if isinstance(obj, dict):
|
|
61
|
+
if "x-poly-ref" in obj:
|
|
62
|
+
ref = obj["x-poly-ref"]
|
|
63
|
+
if isinstance(ref, dict) and "path" in ref:
|
|
64
|
+
path = ref["path"]
|
|
65
|
+
if path in schema_index:
|
|
66
|
+
return resolve_poly_refs(schema_index[path], schema_index)
|
|
67
|
+
else:
|
|
68
|
+
return obj
|
|
69
|
+
return {k: resolve_poly_refs(v, schema_index) for k, v in obj.items()}
|
|
70
|
+
elif isinstance(obj, list):
|
|
71
|
+
return [resolve_poly_refs(item, schema_index) for item in obj]
|
|
72
|
+
else:
|
|
73
|
+
return obj
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def replace_poly_refs_in_functions(specs: List[SpecificationDto], schema_index):
|
|
77
|
+
spec_idxs_to_remove = []
|
|
78
|
+
for idx, spec in enumerate(specs):
|
|
79
|
+
if spec.get("type") in ("apiFunction", "customFunction", "serverFunction"):
|
|
80
|
+
func = spec.get("function")
|
|
81
|
+
if func:
|
|
82
|
+
try:
|
|
83
|
+
spec["function"] = resolve_poly_refs(func, schema_index)
|
|
84
|
+
except Exception:
|
|
85
|
+
# print()
|
|
86
|
+
# print(f"{spec['context']}.{spec['name']} (id: {spec['id']}) failed to resolve poly refs, skipping!")
|
|
87
|
+
spec_idxs_to_remove.append(idx)
|
|
88
|
+
|
|
89
|
+
# reverse the list so we pop off later indexes first
|
|
90
|
+
spec_idxs_to_remove.reverse()
|
|
91
|
+
|
|
92
|
+
for idx in spec_idxs_to_remove:
|
|
93
|
+
specs.pop(idx)
|
|
94
|
+
|
|
95
|
+
return specs
|
|
96
|
+
|
|
97
|
+
|
|
41
98
|
def parse_function_specs(
|
|
42
99
|
specs: List[SpecificationDto],
|
|
43
|
-
limit_ids: List[str] | None, # optional list of ids to limit to
|
|
100
|
+
limit_ids: List[str] | None = None, # optional list of ids to limit to
|
|
44
101
|
) -> List[SpecificationDto]:
|
|
45
102
|
functions = []
|
|
46
103
|
for spec in specs:
|
|
@@ -91,23 +148,14 @@ def read_cached_specs() -> List[SpecificationDto]:
|
|
|
91
148
|
return json.loads(f.read())
|
|
92
149
|
|
|
93
150
|
|
|
94
|
-
def
|
|
95
|
-
specs =
|
|
96
|
-
|
|
97
|
-
return parse_function_specs(specs, limit_ids=limit_ids)
|
|
151
|
+
def get_variables() -> List[VariableSpecDto]:
|
|
152
|
+
specs = read_cached_specs()
|
|
153
|
+
return [cast(VariableSpecDto, spec) for spec in specs if spec["type"] == "serverVariable"]
|
|
98
154
|
|
|
99
155
|
|
|
100
|
-
def
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
# TODO do some caching so this and get_functions just do 1 function call
|
|
104
|
-
url = f"{api_url}/specs"
|
|
105
|
-
resp = requests.get(url, headers=headers)
|
|
106
|
-
if resp.status_code == 200:
|
|
107
|
-
specs = resp.json()
|
|
108
|
-
return [spec for spec in specs if spec["type"] == "serverVariable"]
|
|
109
|
-
else:
|
|
110
|
-
raise NotImplementedError(resp.content)
|
|
156
|
+
def get_schemas() -> List[SchemaSpecDto]:
|
|
157
|
+
specs = read_cached_specs()
|
|
158
|
+
return [cast(SchemaSpecDto, spec) for spec in specs if spec["type"] == "schema"]
|
|
111
159
|
|
|
112
160
|
|
|
113
161
|
def remove_old_library():
|
|
@@ -120,12 +168,28 @@ def remove_old_library():
|
|
|
120
168
|
if os.path.exists(path):
|
|
121
169
|
shutil.rmtree(path)
|
|
122
170
|
|
|
171
|
+
path = os.path.join(currdir, "schemas")
|
|
172
|
+
if os.path.exists(path):
|
|
173
|
+
shutil.rmtree(path)
|
|
123
174
|
|
|
124
|
-
def generate() -> None:
|
|
125
175
|
|
|
176
|
+
def generate() -> None:
|
|
177
|
+
print("Generating Poly Python SDK...", end="", flush=True)
|
|
126
178
|
remove_old_library()
|
|
127
179
|
|
|
128
|
-
|
|
180
|
+
specs = get_specs()
|
|
181
|
+
cache_specs(specs)
|
|
182
|
+
|
|
183
|
+
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
184
|
+
functions = parse_function_specs(specs, limit_ids=limit_ids)
|
|
185
|
+
|
|
186
|
+
schemas = get_schemas()
|
|
187
|
+
if schemas:
|
|
188
|
+
generate_schemas(schemas)
|
|
189
|
+
|
|
190
|
+
schema_index = build_schema_index(schemas)
|
|
191
|
+
functions = replace_poly_refs_in_functions(functions, schema_index)
|
|
192
|
+
|
|
129
193
|
if functions:
|
|
130
194
|
generate_functions(functions)
|
|
131
195
|
else:
|
|
@@ -138,10 +202,13 @@ def generate() -> None:
|
|
|
138
202
|
if variables:
|
|
139
203
|
generate_variables(variables)
|
|
140
204
|
|
|
205
|
+
|
|
141
206
|
# indicator to vscode extension that this is a polyapi-python project
|
|
142
207
|
file_path = os.path.join(os.getcwd(), ".polyapi-python")
|
|
143
208
|
open(file_path, "w").close()
|
|
144
209
|
|
|
210
|
+
print_green("DONE")
|
|
211
|
+
|
|
145
212
|
|
|
146
213
|
def clear() -> None:
|
|
147
214
|
base = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -155,7 +222,7 @@ def clear() -> None:
|
|
|
155
222
|
print("Cleared!")
|
|
156
223
|
|
|
157
224
|
|
|
158
|
-
def render_spec(spec: SpecificationDto):
|
|
225
|
+
def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
|
|
159
226
|
function_type = spec["type"]
|
|
160
227
|
function_description = spec["description"]
|
|
161
228
|
function_name = spec["name"]
|
|
@@ -214,6 +281,12 @@ def render_spec(spec: SpecificationDto):
|
|
|
214
281
|
arguments,
|
|
215
282
|
return_type,
|
|
216
283
|
)
|
|
284
|
+
|
|
285
|
+
if X_POLY_REF_WARNING in func_type_defs:
|
|
286
|
+
# this indicates that jsonschema_gentypes has detected an x-poly-ref
|
|
287
|
+
# let's add a more user friendly error explaining what is going on
|
|
288
|
+
func_type_defs = func_type_defs.replace(X_POLY_REF_WARNING, X_POLY_REF_BETTER_WARNING)
|
|
289
|
+
|
|
217
290
|
return func_str, func_type_defs
|
|
218
291
|
|
|
219
292
|
|
polyapi/parser.py
CHANGED
|
@@ -5,7 +5,7 @@ import sys
|
|
|
5
5
|
import re
|
|
6
6
|
from typing import Dict, List, Mapping, Optional, Tuple, Any
|
|
7
7
|
from typing import _TypedDictMeta as BaseTypedDict # type: ignore
|
|
8
|
-
from typing_extensions import _TypedDictMeta
|
|
8
|
+
from typing_extensions import _TypedDictMeta, cast # type: ignore
|
|
9
9
|
from stdlib_list import stdlib_list
|
|
10
10
|
from pydantic import TypeAdapter
|
|
11
11
|
from importlib.metadata import packages_distributions
|
|
@@ -47,7 +47,7 @@ def _parse_sphinx_docstring(docstring: str) -> Dict[str, Any]:
|
|
|
47
47
|
"type": "Any"
|
|
48
48
|
}
|
|
49
49
|
current_section = None
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
for line in lines:
|
|
52
52
|
stripped_line = line.strip()
|
|
53
53
|
if stripped_line.startswith(":param "):
|
|
@@ -56,7 +56,7 @@ def _parse_sphinx_docstring(docstring: str) -> Dict[str, Any]:
|
|
|
56
56
|
param_name = param_name.strip()
|
|
57
57
|
if param_name in params:
|
|
58
58
|
params[param_name]["description"] = param_desc.strip()
|
|
59
|
-
else:
|
|
59
|
+
else:
|
|
60
60
|
params[param_name] = { "name": param_name, "type": "", "description": param_desc.strip() }
|
|
61
61
|
current_section = param_name
|
|
62
62
|
|
|
@@ -118,7 +118,7 @@ def _parse_google_docstring(docstring: str) -> Dict[str, Any]:
|
|
|
118
118
|
for line in lines:
|
|
119
119
|
line = line.rstrip()
|
|
120
120
|
section_match = section_pattern.match(line)
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
if section_match:
|
|
123
123
|
mode = section_match.group(1).lower()
|
|
124
124
|
continue
|
|
@@ -158,6 +158,7 @@ def _parse_google_docstring(docstring: str) -> Dict[str, Any]:
|
|
|
158
158
|
|
|
159
159
|
return parsed
|
|
160
160
|
|
|
161
|
+
|
|
161
162
|
def _get_schemas(code: str) -> List[Dict]:
|
|
162
163
|
schemas = []
|
|
163
164
|
user_code = types.SimpleNamespace()
|
|
@@ -245,7 +246,7 @@ def _get_type_schema(json_type: str, python_type: str, schemas: List[Dict]):
|
|
|
245
246
|
return schema
|
|
246
247
|
|
|
247
248
|
|
|
248
|
-
def _get_type(expr: ast.expr | None, schemas: List[Dict]) -> Tuple[
|
|
249
|
+
def _get_type(expr: ast.expr | None, schemas: List[Dict]) -> Tuple[Any, Any, Any]:
|
|
249
250
|
if not expr:
|
|
250
251
|
return "any", "Any", None
|
|
251
252
|
python_type = get_python_type_from_ast(expr)
|
|
@@ -317,7 +318,7 @@ def _parse_value(value):
|
|
|
317
318
|
return None
|
|
318
319
|
|
|
319
320
|
|
|
320
|
-
def parse_function_code(code: str, name: Optional[str] = "", context: Optional[str] = ""):
|
|
321
|
+
def parse_function_code(code: str, name: Optional[str] = "", context: Optional[str] = ""): # noqa: C901
|
|
321
322
|
schemas = _get_schemas(code)
|
|
322
323
|
|
|
323
324
|
# the pip name and the import name might be different
|
|
@@ -325,9 +326,9 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
325
326
|
# see https://stackoverflow.com/a/75144378
|
|
326
327
|
pip_name_lookup = packages_distributions()
|
|
327
328
|
|
|
328
|
-
deployable: DeployableRecord = {
|
|
329
|
-
"context": context,
|
|
330
|
-
"name": name,
|
|
329
|
+
deployable: DeployableRecord = { # type: ignore
|
|
330
|
+
"context": context, # type: ignore
|
|
331
|
+
"name": name, # type: ignore
|
|
331
332
|
"description": "",
|
|
332
333
|
"config": {},
|
|
333
334
|
"gitRevision": "",
|
|
@@ -365,7 +366,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
365
366
|
self._line_offsets.append(
|
|
366
367
|
self._line_offsets[i-1] + len(self._lines[i-1])
|
|
367
368
|
)
|
|
368
|
-
|
|
369
|
+
|
|
369
370
|
self._extract_deploy_comments()
|
|
370
371
|
|
|
371
372
|
def visit_AnnAssign(self, node):
|
|
@@ -381,7 +382,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
381
382
|
if node.annotation.id == "PolyServerFunction":
|
|
382
383
|
deployable["type"] = "server-function"
|
|
383
384
|
elif node.annotation.id == "PolyClientFunction":
|
|
384
|
-
deployable["type"] = "
|
|
385
|
+
deployable["type"] = "client-function"
|
|
385
386
|
else:
|
|
386
387
|
print_red("ERROR")
|
|
387
388
|
print(f"Unsupported polyConfig type '${node.annotation.id}'")
|
|
@@ -404,6 +405,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
404
405
|
if type(docstring) is None or (not docstring and '"""' not in self._lines[start_lineno] and "'''" not in self._lines[start_lineno]):
|
|
405
406
|
return None
|
|
406
407
|
|
|
408
|
+
docstring = cast(str, docstring)
|
|
407
409
|
|
|
408
410
|
# Support both types of triple quotation marks
|
|
409
411
|
pattern = '"""'
|
|
@@ -483,7 +485,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
483
485
|
if docstring_params[type_index]["type"] != python_type:
|
|
484
486
|
deployable["dirty"] = True
|
|
485
487
|
except:
|
|
486
|
-
pass
|
|
488
|
+
pass
|
|
487
489
|
else:
|
|
488
490
|
deployable["dirty"] = True
|
|
489
491
|
|
|
@@ -497,7 +499,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
497
499
|
deployable["types"]["returns"]["typeSchema"] = return_type_schema
|
|
498
500
|
else:
|
|
499
501
|
deployable["types"]["returns"]["type"] = "Any"
|
|
500
|
-
|
|
502
|
+
|
|
501
503
|
def generic_visit(self, node):
|
|
502
504
|
if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
|
|
503
505
|
self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
|
polyapi/poly_schemas.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, Tuple
|
|
3
|
+
|
|
4
|
+
from polyapi.schema import wrapped_generate_schema_types
|
|
5
|
+
from polyapi.utils import add_import_to_init, init_the_init, to_func_namespace
|
|
6
|
+
|
|
7
|
+
from .typedefs import SchemaSpecDto
|
|
8
|
+
|
|
9
|
+
SCHEMA_CODE_IMPORTS = """from typing_extensions import TypedDict, NotRequired
|
|
10
|
+
|
|
11
|
+
__all__ = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
|
|
18
|
+
''' unable to generate schema for {name}, defaulting to permissive type '''
|
|
19
|
+
pass
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def generate_schemas(specs: List[SchemaSpecDto]):
|
|
24
|
+
for spec in specs:
|
|
25
|
+
create_schema(spec)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def add_schema_file(
|
|
29
|
+
full_path: str,
|
|
30
|
+
schema_name: str,
|
|
31
|
+
spec: SchemaSpecDto,
|
|
32
|
+
):
|
|
33
|
+
# first lets add the import to the __init__
|
|
34
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
35
|
+
|
|
36
|
+
if not spec["definition"].get("title"):
|
|
37
|
+
# very empty schemas like mews.Unit are possible
|
|
38
|
+
# add a title here to be sure they render
|
|
39
|
+
spec["definition"]["title"] = schema_name
|
|
40
|
+
|
|
41
|
+
schema_defs = render_poly_schema(spec)
|
|
42
|
+
|
|
43
|
+
if schema_defs:
|
|
44
|
+
# add function to init
|
|
45
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
46
|
+
with open(init_path, "a") as f:
|
|
47
|
+
f.write(f"\n\nfrom ._{to_func_namespace(schema_name)} import {schema_name}\n__all__.append('{schema_name}')\n")
|
|
48
|
+
|
|
49
|
+
# add type_defs to underscore file
|
|
50
|
+
file_path = os.path.join(full_path, f"_{to_func_namespace(schema_name)}.py")
|
|
51
|
+
with open(file_path, "w") as f:
|
|
52
|
+
f.write(schema_defs)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def create_schema(
|
|
56
|
+
spec: SchemaSpecDto
|
|
57
|
+
) -> None:
|
|
58
|
+
full_path = os.path.dirname(os.path.abspath(__file__))
|
|
59
|
+
folders = f"schemas.{spec['context']}.{spec['name']}".split(".")
|
|
60
|
+
for idx, folder in enumerate(folders):
|
|
61
|
+
if idx + 1 == len(folders):
|
|
62
|
+
# special handling for final level
|
|
63
|
+
add_schema_file(
|
|
64
|
+
full_path,
|
|
65
|
+
folder,
|
|
66
|
+
spec,
|
|
67
|
+
)
|
|
68
|
+
else:
|
|
69
|
+
full_path = os.path.join(full_path, folder)
|
|
70
|
+
if not os.path.exists(full_path):
|
|
71
|
+
os.makedirs(full_path)
|
|
72
|
+
|
|
73
|
+
# append to __init__.py file if nested folders
|
|
74
|
+
next = folders[idx + 1] if idx + 2 < len(folders) else ""
|
|
75
|
+
if next:
|
|
76
|
+
init_the_init(full_path, SCHEMA_CODE_IMPORTS)
|
|
77
|
+
add_import_to_init(full_path, next)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
|
|
81
|
+
init_the_init(full_path, code_imports="")
|
|
82
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
83
|
+
with open(init_path, "a") as f:
|
|
84
|
+
f.write(render_poly_schema(spec) + "\n\n")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def render_poly_schema(spec: SchemaSpecDto) -> str:
|
|
88
|
+
definition = spec["definition"]
|
|
89
|
+
if not definition.get("type"):
|
|
90
|
+
definition["type"] = "object"
|
|
91
|
+
root, schema_types = wrapped_generate_schema_types(
|
|
92
|
+
definition, root=spec["name"], fallback_type=Dict
|
|
93
|
+
)
|
|
94
|
+
return schema_types
|
|
95
|
+
# return FALLBACK_SPEC_TEMPLATE.format(name=spec["name"])
|
polyapi/schema.py
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
|
+
""" NOTE: this file represents the schema parsing logic for jsonschema_gentypes
|
|
2
|
+
"""
|
|
1
3
|
import logging
|
|
2
4
|
import contextlib
|
|
5
|
+
import re
|
|
3
6
|
from typing import Dict
|
|
4
7
|
from jsonschema_gentypes.cli import process_config
|
|
5
8
|
from jsonschema_gentypes import configuration
|
|
9
|
+
import referencing
|
|
6
10
|
import tempfile
|
|
7
11
|
import json
|
|
8
12
|
|
|
13
|
+
import referencing.exceptions
|
|
14
|
+
|
|
9
15
|
from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
|
|
10
16
|
|
|
11
17
|
|
|
@@ -33,8 +39,16 @@ def _temp_store_input_data(input_data: Dict) -> str:
|
|
|
33
39
|
|
|
34
40
|
|
|
35
41
|
def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
|
|
42
|
+
from polyapi.utils import pascalCase
|
|
36
43
|
if not root:
|
|
37
|
-
root = "
|
|
44
|
+
root = "List" if fallback_type == "List" else "Dict"
|
|
45
|
+
if type_spec.get("x-poly-ref") and type_spec["x-poly-ref"].get("path"):
|
|
46
|
+
# x-poly-ref occurs when we have an unresolved reference
|
|
47
|
+
# lets name the root after the reference for some level of visibility
|
|
48
|
+
root += pascalCase(type_spec["x-poly-ref"]["path"].replace(".", " "))
|
|
49
|
+
else:
|
|
50
|
+
# if we have no root, just add "My"
|
|
51
|
+
root = "My" + root
|
|
38
52
|
|
|
39
53
|
root = clean_title(root)
|
|
40
54
|
|
|
@@ -44,8 +58,13 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
|
|
|
44
58
|
# some schemas are so huge, our library cant handle it
|
|
45
59
|
# TODO identify critical recursion penalty and maybe switch underlying logic to iterative?
|
|
46
60
|
return fallback_type, ""
|
|
61
|
+
except referencing.exceptions.CannotDetermineSpecification:
|
|
62
|
+
# just go with fallback_type here
|
|
63
|
+
# we couldn't match the right $ref earlier in resolve_poly_refs
|
|
64
|
+
# {'$ref': '#/definitions/FinanceAccountListModel'}
|
|
65
|
+
return fallback_type, ""
|
|
47
66
|
except:
|
|
48
|
-
logging.
|
|
67
|
+
logging.error(f"Error when generating schema type: {type_spec}\nusing fallback type '{fallback_type}'")
|
|
49
68
|
return fallback_type, ""
|
|
50
69
|
|
|
51
70
|
|
|
@@ -77,9 +96,23 @@ def generate_schema_types(input_data: Dict, root=None):
|
|
|
77
96
|
with open(tmp_output) as f:
|
|
78
97
|
output = f.read()
|
|
79
98
|
|
|
99
|
+
output = clean_malformed_examples(output)
|
|
100
|
+
|
|
80
101
|
return output
|
|
81
102
|
|
|
82
103
|
|
|
104
|
+
# Regex to match everything between "# example: {\n" and "^}$"
|
|
105
|
+
MALFORMED_EXAMPLES_PATTERN = re.compile(r"# example: \{\n.*?^\}$", flags=re.DOTALL | re.MULTILINE)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def clean_malformed_examples(example: str) -> str:
|
|
109
|
+
""" there is a bug in the `jsonschmea_gentypes` library where if an example from a jsonchema is an object,
|
|
110
|
+
it will break the code because the object won't be properly commented out
|
|
111
|
+
"""
|
|
112
|
+
cleaned_example = MALFORMED_EXAMPLES_PATTERN.sub("", example)
|
|
113
|
+
return cleaned_example
|
|
114
|
+
|
|
115
|
+
|
|
83
116
|
def clean_title(title: str) -> str:
|
|
84
117
|
""" used by library generation, sometimes functions can be added with spaces in the title
|
|
85
118
|
or other nonsense. fix them!
|
polyapi/server.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from typing import Any, Dict, List, Tuple
|
|
1
|
+
from typing import Any, Dict, List, Tuple, cast
|
|
2
2
|
|
|
3
|
-
from polyapi.typedefs import PropertySpecification
|
|
4
|
-
from polyapi.utils import add_type_import_path, parse_arguments, get_type_and_def, rewrite_arg_name
|
|
3
|
+
from polyapi.typedefs import PropertySpecification, PropertyType
|
|
4
|
+
from polyapi.utils import add_type_import_path, parse_arguments, get_type_and_def, return_type_already_defined_in_args, rewrite_arg_name
|
|
5
5
|
|
|
6
6
|
SERVER_DEFS_TEMPLATE = """
|
|
7
7
|
from typing import List, Dict, Any, TypedDict, Callable
|
|
@@ -21,7 +21,7 @@ def {function_name}(
|
|
|
21
21
|
try:
|
|
22
22
|
return {return_action}
|
|
23
23
|
except:
|
|
24
|
-
return resp.text
|
|
24
|
+
return resp.text # type: ignore # fallback for debugging
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
"""
|
|
@@ -37,7 +37,11 @@ def render_server_function(
|
|
|
37
37
|
) -> Tuple[str, str]:
|
|
38
38
|
arg_names = [a["name"] for a in arguments]
|
|
39
39
|
args, args_def = parse_arguments(function_name, arguments)
|
|
40
|
-
return_type_name, return_type_def = get_type_and_def(return_type)
|
|
40
|
+
return_type_name, return_type_def = get_type_and_def(cast(PropertyType, return_type), "ReturnType")
|
|
41
|
+
|
|
42
|
+
if return_type_def and return_type_already_defined_in_args(return_type_name, args_def):
|
|
43
|
+
return_type_def = ""
|
|
44
|
+
|
|
41
45
|
data = "{" + ", ".join([f"'{arg}': {rewrite_arg_name(arg)}" for arg in arg_names]) + "}"
|
|
42
46
|
func_type_defs = SERVER_DEFS_TEMPLATE.format(
|
|
43
47
|
args_def=args_def,
|
polyapi/typedefs.py
CHANGED
|
@@ -55,6 +55,19 @@ class VariableSpecDto(TypedDict):
|
|
|
55
55
|
variable: VariableSpecification
|
|
56
56
|
type: Literal['serverVariable']
|
|
57
57
|
|
|
58
|
+
|
|
59
|
+
class SchemaSpecDto(TypedDict):
|
|
60
|
+
id: str
|
|
61
|
+
context: str
|
|
62
|
+
name: str
|
|
63
|
+
contextName: str
|
|
64
|
+
type: Literal['schema']
|
|
65
|
+
definition: Dict[Any, Any]
|
|
66
|
+
visibilityMetadata: object
|
|
67
|
+
unresolvedPolySchemaRefs: List
|
|
68
|
+
# TODO add more
|
|
69
|
+
|
|
70
|
+
|
|
58
71
|
Visibility = Union[Literal['PUBLIC'], Literal['TENANT'], Literal['ENVIRONMENT']]
|
|
59
72
|
|
|
60
73
|
|
|
@@ -69,6 +82,7 @@ class PolyServerFunction(PolyDeployable):
|
|
|
69
82
|
always_on: NotRequired[bool]
|
|
70
83
|
visibility: NotRequired[Visibility]
|
|
71
84
|
|
|
85
|
+
|
|
72
86
|
class PolyClientFunction(PolyDeployable):
|
|
73
87
|
logs_enabled: NotRequired[bool]
|
|
74
88
|
visibility: NotRequired[Visibility]
|
polyapi/utils.py
CHANGED
|
@@ -6,24 +6,28 @@ from typing import Tuple, List
|
|
|
6
6
|
from colorama import Fore, Style
|
|
7
7
|
from polyapi.constants import BASIC_PYTHON_TYPES
|
|
8
8
|
from polyapi.typedefs import PropertySpecification, PropertyType
|
|
9
|
-
from polyapi.schema import
|
|
9
|
+
from polyapi.schema import (
|
|
10
|
+
wrapped_generate_schema_types,
|
|
11
|
+
clean_title,
|
|
12
|
+
map_primitive_types,
|
|
13
|
+
)
|
|
10
14
|
|
|
11
15
|
|
|
12
16
|
# this string should be in every __init__ file.
|
|
13
17
|
# it contains all the imports needed for the function or variable code to run
|
|
14
|
-
CODE_IMPORTS = "from typing import List, Dict, Any,
|
|
15
|
-
FALLBACK_TYPES = {"Dict", "List"}
|
|
18
|
+
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"
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
def init_the_init(full_path: str) -> None:
|
|
21
|
+
def init_the_init(full_path: str, code_imports="") -> None:
|
|
19
22
|
init_path = os.path.join(full_path, "__init__.py")
|
|
20
23
|
if not os.path.exists(init_path):
|
|
24
|
+
code_imports = code_imports or CODE_IMPORTS
|
|
21
25
|
with open(init_path, "w") as f:
|
|
22
|
-
f.write(
|
|
26
|
+
f.write(code_imports)
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
def add_import_to_init(full_path: str, next: str) -> None:
|
|
26
|
-
init_the_init(full_path)
|
|
29
|
+
def add_import_to_init(full_path: str, next: str, code_imports="") -> None:
|
|
30
|
+
init_the_init(full_path, code_imports=code_imports)
|
|
27
31
|
|
|
28
32
|
init_path = os.path.join(full_path, "__init__.py")
|
|
29
33
|
with open(init_path, "a+") as f:
|
|
@@ -38,16 +42,20 @@ def get_auth_headers(api_key: str):
|
|
|
38
42
|
return {"Authorization": f"Bearer {api_key}"}
|
|
39
43
|
|
|
40
44
|
|
|
41
|
-
def camelCase(s):
|
|
45
|
+
def camelCase(s: str) -> str:
|
|
42
46
|
s = s.strip()
|
|
43
47
|
if " " in s or "-" in s:
|
|
44
48
|
s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "")
|
|
45
|
-
return
|
|
49
|
+
return "".join([s[0].lower(), s[1:]])
|
|
46
50
|
else:
|
|
47
51
|
# s is already in camelcase as best as we can tell, just move on!
|
|
48
52
|
return s
|
|
49
53
|
|
|
50
54
|
|
|
55
|
+
def pascalCase(s) -> str:
|
|
56
|
+
return re.sub(r"(^|_)([a-z])", lambda match: match.group(2).upper(), s)
|
|
57
|
+
|
|
58
|
+
|
|
51
59
|
def print_green(s: str):
|
|
52
60
|
print(Fore.GREEN + s + Style.RESET_ALL)
|
|
53
61
|
|
|
@@ -61,8 +69,7 @@ def print_red(s: str):
|
|
|
61
69
|
|
|
62
70
|
|
|
63
71
|
def add_type_import_path(function_name: str, arg: str) -> str:
|
|
64
|
-
"""
|
|
65
|
-
"""
|
|
72
|
+
"""if not basic type, coerce to camelCase and add the import path"""
|
|
66
73
|
# for now, just treat Callables as basic types
|
|
67
74
|
if arg.startswith("Callable"):
|
|
68
75
|
return arg
|
|
@@ -79,12 +86,16 @@ def add_type_import_path(function_name: str, arg: str) -> str:
|
|
|
79
86
|
sub = sub.replace('"', "")
|
|
80
87
|
return f'List["{to_func_namespace(function_name)}.{camelCase(sub)}"]'
|
|
81
88
|
else:
|
|
82
|
-
return f
|
|
89
|
+
return f"List[{to_func_namespace(function_name)}.{camelCase(sub)}]"
|
|
83
90
|
|
|
84
|
-
return f
|
|
91
|
+
return f"{to_func_namespace(function_name)}.{camelCase(arg)}"
|
|
85
92
|
|
|
86
93
|
|
|
87
|
-
def get_type_and_def(
|
|
94
|
+
def get_type_and_def(
|
|
95
|
+
type_spec: PropertyType, title_fallback: str = ""
|
|
96
|
+
) -> Tuple[str, str]:
|
|
97
|
+
""" returns type and type definition for a given PropertyType
|
|
98
|
+
"""
|
|
88
99
|
if type_spec["kind"] == "plain":
|
|
89
100
|
value = type_spec["value"]
|
|
90
101
|
if value.endswith("[]"):
|
|
@@ -111,18 +122,27 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
111
122
|
elif type_spec["kind"] == "object":
|
|
112
123
|
if type_spec.get("schema"):
|
|
113
124
|
schema = type_spec["schema"]
|
|
114
|
-
title = schema.get("title", "")
|
|
115
|
-
if title:
|
|
125
|
+
title = schema.get("title", schema.get("name", title_fallback))
|
|
126
|
+
if title and schema.get("type") == "array":
|
|
127
|
+
# TODO fix me
|
|
128
|
+
# we don't use ReturnType as name for the list type here, we use _ReturnTypeItem
|
|
129
|
+
return "List", ""
|
|
130
|
+
elif title:
|
|
116
131
|
assert isinstance(title, str)
|
|
117
132
|
return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
|
|
118
|
-
|
|
133
|
+
elif schema.get("allOf") and len(schema["allOf"]):
|
|
134
|
+
# we are in a case of a single allOf, lets strip off the allOf and move on!
|
|
135
|
+
# our library doesn't handle allOf well yet
|
|
136
|
+
allOf = schema["allOf"][0]
|
|
137
|
+
title = allOf.get("title", allOf.get("name", title_fallback))
|
|
138
|
+
return wrapped_generate_schema_types(allOf, title, "Dict")
|
|
119
139
|
elif schema.get("items"):
|
|
120
140
|
# fallback to schema $ref name if no explicit title
|
|
121
141
|
items = schema.get("items") # type: ignore
|
|
122
|
-
title = items.get("title"
|
|
142
|
+
title = items.get("title") # type: ignore
|
|
123
143
|
if not title:
|
|
124
144
|
# title is actually a reference to another schema
|
|
125
|
-
title = items.get("$ref",
|
|
145
|
+
title = items.get("$ref", title_fallback) # type: ignore
|
|
126
146
|
|
|
127
147
|
title = title.rsplit("/", 1)[-1]
|
|
128
148
|
if not title:
|
|
@@ -144,12 +164,18 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
144
164
|
return_type = "Any"
|
|
145
165
|
|
|
146
166
|
for argument in type_spec["spec"]["arguments"]:
|
|
167
|
+
# do NOT add this fallback here
|
|
168
|
+
# callable arguments don't understand the imports yet
|
|
169
|
+
# if it's not a basic type here, we'll just do Any
|
|
170
|
+
# _maybe_add_fallback_schema_name(argument)
|
|
147
171
|
arg_type, arg_def = get_type_and_def(argument["type"])
|
|
148
172
|
arg_types.append(arg_type)
|
|
149
173
|
if arg_def:
|
|
150
174
|
arg_defs.append(arg_def)
|
|
151
175
|
|
|
152
|
-
final_arg_type = "Callable[[{}], {}]".format(
|
|
176
|
+
final_arg_type = "Callable[[{}], {}]".format(
|
|
177
|
+
", ".join(arg_types), return_type
|
|
178
|
+
)
|
|
153
179
|
return final_arg_type, "\n".join(arg_defs)
|
|
154
180
|
else:
|
|
155
181
|
return "Callable", ""
|
|
@@ -159,15 +185,30 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
159
185
|
return "Any", ""
|
|
160
186
|
|
|
161
187
|
|
|
162
|
-
def
|
|
188
|
+
def _maybe_add_fallback_schema_name(a: PropertySpecification):
|
|
189
|
+
if a["type"]["kind"] == "object" and a["type"].get("schema"):
|
|
190
|
+
schema = a["type"].get("schema", {})
|
|
191
|
+
if not schema.get("title") and not schema.get("name") and a["name"]:
|
|
192
|
+
schema["title"] = a["name"].title()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def parse_arguments(
|
|
196
|
+
function_name: str, arguments: List[PropertySpecification]
|
|
197
|
+
) -> Tuple[str, str]:
|
|
163
198
|
args_def = []
|
|
164
199
|
arg_string = ""
|
|
165
200
|
for idx, a in enumerate(arguments):
|
|
201
|
+
_maybe_add_fallback_schema_name(a)
|
|
166
202
|
arg_type, arg_def = get_type_and_def(a["type"])
|
|
167
203
|
if arg_def:
|
|
168
204
|
args_def.append(arg_def)
|
|
169
205
|
a["name"] = rewrite_arg_name(a["name"])
|
|
170
|
-
arg_string +=
|
|
206
|
+
arg_string += (
|
|
207
|
+
f" {a['name']}: {add_type_import_path(function_name, arg_type)}"
|
|
208
|
+
)
|
|
209
|
+
if not a["required"]:
|
|
210
|
+
arg_string += " = None"
|
|
211
|
+
|
|
171
212
|
description = a.get("description", "")
|
|
172
213
|
description = description.replace("\n", " ")
|
|
173
214
|
if description:
|
|
@@ -193,7 +234,7 @@ RESERVED_WORDS = {"List", "Dict", "Any", "Optional", "Callable"} | set(keyword.k
|
|
|
193
234
|
|
|
194
235
|
|
|
195
236
|
def to_func_namespace(s: str) -> str:
|
|
196
|
-
"""
|
|
237
|
+
"""convert a function name to some function namespace
|
|
197
238
|
by default it is
|
|
198
239
|
"""
|
|
199
240
|
rv = s[0].upper() + s[1:]
|
|
@@ -212,6 +253,10 @@ def rewrite_arg_name(s: str):
|
|
|
212
253
|
return rewrite_reserved(camelCase(s))
|
|
213
254
|
|
|
214
255
|
|
|
256
|
+
# def get_return_type_name(function_name: str) -> str:
|
|
257
|
+
# return function_name[0].upper() + function_name[1:] + "ReturnType"
|
|
258
|
+
|
|
259
|
+
|
|
215
260
|
valid_subdomains = ["na[1-2]", "eu[1-2]", "dev"]
|
|
216
261
|
|
|
217
262
|
|
|
@@ -229,3 +274,21 @@ def is_valid_uuid(uuid_string, version=4):
|
|
|
229
274
|
return False
|
|
230
275
|
|
|
231
276
|
return str(uuid_obj) == uuid_string
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def return_type_already_defined_in_args(return_type_name: str, args_def: str) -> bool:
|
|
280
|
+
"""
|
|
281
|
+
Checks if the return_type_name preceded optionally by 'class ' and followed by ' =' exists in args_def.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
return_type_name (str): The name of the return type to check.
|
|
285
|
+
args_def (str): The string containing argument definitions.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
bool: True if the pattern exists, False otherwise.
|
|
289
|
+
"""
|
|
290
|
+
basic_pattern = rf"^{re.escape(return_type_name)}\s="
|
|
291
|
+
basic_match = bool(re.search(basic_pattern, args_def, re.MULTILINE))
|
|
292
|
+
class_pattern = rf"^class {re.escape(return_type_name)}\(TypedDict"
|
|
293
|
+
class_match = bool(re.search(class_pattern, args_def, re.MULTILINE))
|
|
294
|
+
return basic_match or class_match
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3.dev9
|
|
4
4
|
Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
|
|
5
5
|
Author-email: Dan Fellin <dan@polyapi.io>
|
|
6
6
|
License: MIT License
|
|
7
7
|
|
|
8
|
-
Copyright (c)
|
|
8
|
+
Copyright (c) 2025 PolyAPI Inc.
|
|
9
9
|
|
|
10
10
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
11
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -28,14 +28,15 @@ Project-URL: Homepage, https://github.com/polyapi/polyapi-python
|
|
|
28
28
|
Requires-Python: >=3.10
|
|
29
29
|
Description-Content-Type: text/markdown
|
|
30
30
|
License-File: LICENSE
|
|
31
|
-
Requires-Dist: requests
|
|
32
|
-
Requires-Dist: typing_extensions>=4.
|
|
31
|
+
Requires-Dist: requests>=2.32.3
|
|
32
|
+
Requires-Dist: typing_extensions>=4.12.2
|
|
33
33
|
Requires-Dist: jsonschema-gentypes==2.6.0
|
|
34
34
|
Requires-Dist: pydantic==2.6.4
|
|
35
35
|
Requires-Dist: stdlib_list==0.10.0
|
|
36
36
|
Requires-Dist: colorama==0.4.4
|
|
37
37
|
Requires-Dist: python-socketio[asyncio_client]==5.11.1
|
|
38
38
|
Requires-Dist: truststore==0.8.0
|
|
39
|
+
Dynamic: license-file
|
|
39
40
|
|
|
40
41
|
# PolyAPI Python Library
|
|
41
42
|
|
|
@@ -182,6 +183,26 @@ To run this library's unit tests, please clone the repo then run:
|
|
|
182
183
|
python -m unittest discover
|
|
183
184
|
```
|
|
184
185
|
|
|
186
|
+
## Linting
|
|
187
|
+
|
|
188
|
+
The flake8 config is at the root of this repo at `.flake8`.
|
|
189
|
+
|
|
190
|
+
When hacking on this library, please enable flake8 and add this line to your flake8 args (e.g., in your VSCode Workspace Settings):
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
--config=.flake8
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Mypy Type Improvements
|
|
197
|
+
|
|
198
|
+
This script is handy for checking for any mypy types:
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
./check_mypy.sh
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Please ignore \[name-defined\] errors for now. This is a known bug we are working to fix!
|
|
205
|
+
|
|
185
206
|
## Support
|
|
186
207
|
|
|
187
208
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
polyapi/__init__.py,sha256=a1Poy1kaTncYnUg6nWRcTjVm-R1CUQk12UX7VYQ9d5k,616
|
|
2
|
+
polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
|
|
3
|
+
polyapi/api.py,sha256=f1037HFJF7DtQSSypM4PE5AmmxWxjd0JiW6ARZqrgac,1879
|
|
4
|
+
polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
|
|
5
|
+
polyapi/cli.py,sha256=AKsWVHZPKGnypOdnzIpoZOsTuwcAuDGQajXhLe9OQKI,8239
|
|
6
|
+
polyapi/client.py,sha256=CoFDYvyKsqL4wPQbUDIr0Qb8Q5eD92xN4OEEcJEVuGQ,1296
|
|
7
|
+
polyapi/config.py,sha256=Vgc_q9FYXWGCOTr13EbpD0AwHks0Nflimy1NtZxgynA,3088
|
|
8
|
+
polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
|
|
9
|
+
polyapi/deployables.py,sha256=WVcNNB6W5ZW_-ukf_kK3moRcnwIkC-O4te6vLepjcco,11936
|
|
10
|
+
polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
|
|
11
|
+
polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
12
|
+
polyapi/execute.py,sha256=T9lXtiOz-JZTJgBKvJptA5_mz31qvYa6-O4NzM52mq4,2118
|
|
13
|
+
polyapi/function_cli.py,sha256=htgmcx_dPmw4_5NKRgIivcwS7D8bkOsxCTOrJhzV3pU,3989
|
|
14
|
+
polyapi/generate.py,sha256=IIbU4Kc8Ut-N3cPI1qzgV3M4r_GHi39dgU0ngpUY86Q,10473
|
|
15
|
+
polyapi/parser.py,sha256=mdoh4pNq8pyiHE0-i6Coqj8frEXfBLRk6itpAXMrrgI,20373
|
|
16
|
+
polyapi/poly_schemas.py,sha256=KFVmpB047pWQaTkiCJ3A9sUTNplTS8JETon1Sm2lnQs,2969
|
|
17
|
+
polyapi/prepare.py,sha256=Q8CWV4kmZ2dbXYVsud34AgJkj5ymcQ_IcYhLuikc9yk,6659
|
|
18
|
+
polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
+
polyapi/rendered_spec.py,sha256=uaNzBhP4cX7iGfKwzZv0dxMagWzsGeDr0cQYx_AyIhQ,2153
|
|
20
|
+
polyapi/schema.py,sha256=ZSzeUjpqigLvE4tFKB7y4AaZG-W5N5Z9wMH-F-vjMBU,4616
|
|
21
|
+
polyapi/server.py,sha256=YXWxhYBx-hluwDQ8Jvfpy2s8ogz0GsNTMcZVNcP5ca8,2147
|
|
22
|
+
polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
|
|
23
|
+
polyapi/typedefs.py,sha256=KniVl7vwcDOhgAJmHSgTJKkP0rKWvSLIPOGsWuf9jRU,2239
|
|
24
|
+
polyapi/utils.py,sha256=K6QMKEf2fgmh3AswyNBADfv53sIOSAbXmGx2MaW5vy8,10261
|
|
25
|
+
polyapi/variables.py,sha256=d36-trnfTL_8m2NkorMiImb4O3UrJbiFV38CHxV5i0A,4200
|
|
26
|
+
polyapi/webhook.py,sha256=LWv28c2MLz_OKBI_Nn7WR4C-gs1SWgbdXsoxIIf-9UI,4886
|
|
27
|
+
polyapi_python-0.3.3.dev9.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
|
|
28
|
+
polyapi_python-0.3.3.dev9.dist-info/METADATA,sha256=fd2KtyjZKHie7x1Ua2p-QshQIE4Rc8gb5CMczxqGqgg,5782
|
|
29
|
+
polyapi_python-0.3.3.dev9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
30
|
+
polyapi_python-0.3.3.dev9.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
31
|
+
polyapi_python-0.3.3.dev9.dist-info/RECORD,,
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
polyapi/__init__.py,sha256=a1Poy1kaTncYnUg6nWRcTjVm-R1CUQk12UX7VYQ9d5k,616
|
|
2
|
-
polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
|
|
3
|
-
polyapi/api.py,sha256=e-8nOzq6SXD7_YPVBlW82_9wVxDCc8XD9ioMv_QvnH0,1877
|
|
4
|
-
polyapi/auth.py,sha256=zrIGatjba5GwUTNjKj1GHQWTEDP9B-HrSzCKbLFoqvc,5336
|
|
5
|
-
polyapi/cli.py,sha256=w0SOiRHR7gJRoEwkfyGC1sdBT76sgx8MUcF8_GnGuJU,8324
|
|
6
|
-
polyapi/client.py,sha256=CoFDYvyKsqL4wPQbUDIr0Qb8Q5eD92xN4OEEcJEVuGQ,1296
|
|
7
|
-
polyapi/config.py,sha256=uvEvOfWYZTLmBmZX-5jJxCzWPpwzVmEOIiQIdi98P4Y,3015
|
|
8
|
-
polyapi/constants.py,sha256=sc-FnS0SngBLvSu1ZWMs0UCf9EYD1u1Yhfr-sZXGLns,607
|
|
9
|
-
polyapi/deployables.py,sha256=qKoyuPUv46yZoLBvSOmk6JsBWqomwJAkadhItp8Lx40,11956
|
|
10
|
-
polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
|
|
11
|
-
polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
|
|
12
|
-
polyapi/execute.py,sha256=kXnvlNQ7nz9cRlV2_5gXH09UCmyiDP5zi3wiAw0uDuk,1943
|
|
13
|
-
polyapi/function_cli.py,sha256=-Fjv_DECZB5LK8rT222B62GPw4vksAi7KsIqT-ss5FM,4185
|
|
14
|
-
polyapi/generate.py,sha256=SIRfN7RF3Z7eQ8kSNg4H70LeT7Hmh-Mn5maibvM7kAM,7969
|
|
15
|
-
polyapi/parser.py,sha256=noi3Tjf1vVNm5gjTzgvOlJ6IMaoZ9DheklibVWfPPhk,20311
|
|
16
|
-
polyapi/prepare.py,sha256=Q8CWV4kmZ2dbXYVsud34AgJkj5ymcQ_IcYhLuikc9yk,6659
|
|
17
|
-
polyapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
-
polyapi/rendered_spec.py,sha256=uaNzBhP4cX7iGfKwzZv0dxMagWzsGeDr0cQYx_AyIhQ,2153
|
|
19
|
-
polyapi/schema.py,sha256=VVMHAT5yU47cRC7xF44BrmUSemlk5oIKSxH2HTVPaQ8,3169
|
|
20
|
-
polyapi/server.py,sha256=NzQCZFSAJK7XiRw1kiU_i9uMvgYK7i8qh7UX2xjytJU,1908
|
|
21
|
-
polyapi/sync.py,sha256=PGdC0feBBjEVrF3d9EluW_OAxbWuzSrfh84czma8kWg,6476
|
|
22
|
-
polyapi/typedefs.py,sha256=U0i30Y9CgoUBWeMUbpM28S8eVOEU9NfdckK1VAop3A0,1994
|
|
23
|
-
polyapi/utils.py,sha256=jzCh-ivKMcgp5fIXynhYmP9UyzsISr9bGGEzdPP8n3w,7644
|
|
24
|
-
polyapi/variables.py,sha256=d36-trnfTL_8m2NkorMiImb4O3UrJbiFV38CHxV5i0A,4200
|
|
25
|
-
polyapi/webhook.py,sha256=LWv28c2MLz_OKBI_Nn7WR4C-gs1SWgbdXsoxIIf-9UI,4886
|
|
26
|
-
polyapi_python-0.3.2.dev2.dist-info/LICENSE,sha256=Hi0kDr56Dsy0uYIwNt4r9G7tI8x8miXRTlyvbeplCP8,1068
|
|
27
|
-
polyapi_python-0.3.2.dev2.dist-info/METADATA,sha256=YKwIhWvsJJ3qnsGIa1LqH7fu86jCs2a9pA5lrBtzfF8,5326
|
|
28
|
-
polyapi_python-0.3.2.dev2.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
29
|
-
polyapi_python-0.3.2.dev2.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
|
|
30
|
-
polyapi_python-0.3.2.dev2.dist-info/RECORD,,
|
|
File without changes
|