polyapi-python 0.3.2.dev2__tar.gz → 0.3.3.dev1__tar.gz
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_python-0.3.2.dev2/polyapi_python.egg-info → polyapi_python-0.3.3.dev1}/PKG-INFO +15 -4
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/README.md +10 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/api.py +1 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/cli.py +3 -2
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/deployables.py +4 -4
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/function_cli.py +6 -3
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/generate.py +93 -21
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/parser.py +6 -6
- polyapi_python-0.3.3.dev1/polyapi/poly_schemas.py +61 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/schema.py +24 -2
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/typedefs.py +14 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/utils.py +19 -10
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1/polyapi_python.egg-info}/PKG-INFO +15 -4
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/SOURCES.txt +2 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/requires.txt +2 -2
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/pyproject.toml +3 -3
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_deployables.py +3 -4
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_parser.py +1 -1
- polyapi_python-0.3.3.dev1/tests/test_utils.py +14 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/LICENSE +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/__init__.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/__main__.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/auth.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/client.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/config.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/constants.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/error_handler.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/exceptions.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/execute.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/prepare.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/py.typed +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/rendered_spec.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/server.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/sync.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/variables.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi/webhook.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/setup.cfg +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_api.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_auth.py +0 -0
- /polyapi_python-0.3.2.dev2/tests/test_utils.py → /polyapi_python-0.3.3.dev1/tests/test_generate.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_rendered_spec.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_schema.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_server.py +0 -0
- {polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/tests/test_variables.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3.dev1
|
|
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
|
|
@@ -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,16 @@ 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
|
+
|
|
185
196
|
## Support
|
|
186
197
|
|
|
187
198
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -143,6 +143,16 @@ To run this library's unit tests, please clone the repo then run:
|
|
|
143
143
|
python -m unittest discover
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
## Linting
|
|
147
|
+
|
|
148
|
+
The flake8 config is at the root of this repo at `.flake8`.
|
|
149
|
+
|
|
150
|
+
When hacking on this library, please enable flake8 and add this line to your flake8 args (e.g., in your VSCode Workspace Settings):
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
--config=.flake8
|
|
154
|
+
```
|
|
155
|
+
|
|
146
156
|
## Support
|
|
147
157
|
|
|
148
158
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -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,7 +47,7 @@ def execute_from_cli():
|
|
|
46
47
|
|
|
47
48
|
def generate_command(args):
|
|
48
49
|
initialize_config()
|
|
49
|
-
print("Generating Poly
|
|
50
|
+
print("Generating Poly Python SDK...", end="")
|
|
50
51
|
generate()
|
|
51
52
|
print_green("DONE")
|
|
52
53
|
|
|
@@ -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
|
|
|
@@ -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 cache_specs, generate_functions, get_specs, parse_function_specs
|
|
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
|
|
|
@@ -88,7 +88,10 @@ def function_add_or_update(
|
|
|
88
88
|
print(f"Function ID: {function_id}")
|
|
89
89
|
if generate:
|
|
90
90
|
print("Generating new custom function...", end="")
|
|
91
|
-
|
|
91
|
+
# TODO do something more efficient here rather than regetting ALL the specs again
|
|
92
|
+
specs = get_specs()
|
|
93
|
+
cache_specs(specs)
|
|
94
|
+
functions = parse_function_specs(specs)
|
|
92
95
|
generate_functions(functions)
|
|
93
96
|
print_green("DONE")
|
|
94
97
|
else:
|
|
@@ -2,13 +2,15 @@ import json
|
|
|
2
2
|
import requests
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
|
-
from typing import List
|
|
5
|
+
from typing import List, cast
|
|
6
6
|
|
|
7
|
+
from polyapi import schema
|
|
7
8
|
from polyapi.auth import render_auth_function
|
|
8
9
|
from polyapi.client import render_client_function
|
|
10
|
+
from polyapi.poly_schemas import generate_schemas
|
|
9
11
|
from polyapi.webhook import render_webhook_handle
|
|
10
12
|
|
|
11
|
-
from .typedefs import PropertySpecification, SpecificationDto, VariableSpecDto
|
|
13
|
+
from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto
|
|
12
14
|
from .api import render_api_function
|
|
13
15
|
from .server import render_server_function
|
|
14
16
|
from .utils import add_import_to_init, get_auth_headers, init_the_init, to_func_namespace
|
|
@@ -18,12 +20,21 @@ from .config import get_api_key_and_url
|
|
|
18
20
|
SUPPORTED_FUNCTION_TYPES = {
|
|
19
21
|
"apiFunction",
|
|
20
22
|
"authFunction",
|
|
21
|
-
"customFunction",
|
|
23
|
+
"customFunction", # client function - this is badly named in /specs atm
|
|
22
24
|
"serverFunction",
|
|
23
25
|
"webhookHandle",
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable"}
|
|
28
|
+
SUPPORTED_TYPES = SUPPORTED_FUNCTION_TYPES | {"serverVariable", "schema", "snippet"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
X_POLY_REF_WARNING = '''"""
|
|
32
|
+
x-poly-ref:
|
|
33
|
+
path:'''
|
|
34
|
+
|
|
35
|
+
X_POLY_REF_BETTER_WARNING = '''"""
|
|
36
|
+
Unresolved schema, please add the following schema to complete it:
|
|
37
|
+
path:'''
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
def get_specs() -> List:
|
|
@@ -38,9 +49,56 @@ def get_specs() -> List:
|
|
|
38
49
|
raise NotImplementedError(resp.content)
|
|
39
50
|
|
|
40
51
|
|
|
52
|
+
def build_schema_index(items):
|
|
53
|
+
index = {}
|
|
54
|
+
for item in items:
|
|
55
|
+
if item.get("type") == "schema" and "contextName" in item:
|
|
56
|
+
index[item["contextName"]] = {**item.get("definition", {}), "name": item.get("name")}
|
|
57
|
+
return index
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def resolve_poly_refs(obj, schema_index):
|
|
61
|
+
if isinstance(obj, dict):
|
|
62
|
+
if "x-poly-ref" in obj:
|
|
63
|
+
ref = obj["x-poly-ref"]
|
|
64
|
+
if isinstance(ref, dict) and "path" in ref:
|
|
65
|
+
path = ref["path"]
|
|
66
|
+
if path in schema_index:
|
|
67
|
+
return resolve_poly_refs(schema_index[path], schema_index)
|
|
68
|
+
else:
|
|
69
|
+
return obj
|
|
70
|
+
return {k: resolve_poly_refs(v, schema_index) for k, v in obj.items()}
|
|
71
|
+
elif isinstance(obj, list):
|
|
72
|
+
return [resolve_poly_refs(item, schema_index) for item in obj]
|
|
73
|
+
else:
|
|
74
|
+
return obj
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def replace_poly_refs_in_functions(specs: List[SpecificationDto], schema_index):
|
|
78
|
+
spec_idxs_to_remove = []
|
|
79
|
+
for idx, spec in enumerate(specs):
|
|
80
|
+
if spec.get("type") in ("apiFunction", "customFunction", "serverFunction"):
|
|
81
|
+
func = spec.get("function")
|
|
82
|
+
if func:
|
|
83
|
+
try:
|
|
84
|
+
spec["function"] = resolve_poly_refs(func, schema_index)
|
|
85
|
+
except Exception:
|
|
86
|
+
print()
|
|
87
|
+
print(f"{spec['context']}.{spec['name']} (id: {spec['id']}) failed to resolve poly refs, skipping!")
|
|
88
|
+
spec_idxs_to_remove.append(idx)
|
|
89
|
+
|
|
90
|
+
# reverse the list so we pop off later indexes first
|
|
91
|
+
spec_idxs_to_remove.reverse()
|
|
92
|
+
|
|
93
|
+
for idx in spec_idxs_to_remove:
|
|
94
|
+
specs.pop(idx)
|
|
95
|
+
|
|
96
|
+
return specs
|
|
97
|
+
|
|
98
|
+
|
|
41
99
|
def parse_function_specs(
|
|
42
100
|
specs: List[SpecificationDto],
|
|
43
|
-
limit_ids: List[str] | None, # optional list of ids to limit to
|
|
101
|
+
limit_ids: List[str] | None = None, # optional list of ids to limit to
|
|
44
102
|
) -> List[SpecificationDto]:
|
|
45
103
|
functions = []
|
|
46
104
|
for spec in specs:
|
|
@@ -91,23 +149,14 @@ def read_cached_specs() -> List[SpecificationDto]:
|
|
|
91
149
|
return json.loads(f.read())
|
|
92
150
|
|
|
93
151
|
|
|
94
|
-
def
|
|
95
|
-
specs =
|
|
96
|
-
|
|
97
|
-
return parse_function_specs(specs, limit_ids=limit_ids)
|
|
152
|
+
def get_variables() -> List[VariableSpecDto]:
|
|
153
|
+
specs = read_cached_specs()
|
|
154
|
+
return [cast(VariableSpecDto, spec) for spec in specs if spec["type"] == "serverVariable"]
|
|
98
155
|
|
|
99
156
|
|
|
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)
|
|
157
|
+
def get_schemas() -> List[SchemaSpecDto]:
|
|
158
|
+
specs = read_cached_specs()
|
|
159
|
+
return [cast(SchemaSpecDto, spec) for spec in specs if spec["type"] == "schema"]
|
|
111
160
|
|
|
112
161
|
|
|
113
162
|
def remove_old_library():
|
|
@@ -120,12 +169,28 @@ def remove_old_library():
|
|
|
120
169
|
if os.path.exists(path):
|
|
121
170
|
shutil.rmtree(path)
|
|
122
171
|
|
|
172
|
+
path = os.path.join(currdir, "schemas")
|
|
173
|
+
if os.path.exists(path):
|
|
174
|
+
shutil.rmtree(path)
|
|
175
|
+
|
|
123
176
|
|
|
124
177
|
def generate() -> None:
|
|
125
178
|
|
|
126
179
|
remove_old_library()
|
|
127
180
|
|
|
128
|
-
|
|
181
|
+
limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
|
|
182
|
+
|
|
183
|
+
specs = get_specs()
|
|
184
|
+
cache_specs(specs)
|
|
185
|
+
functions = parse_function_specs(specs, limit_ids=limit_ids)
|
|
186
|
+
|
|
187
|
+
schemas = get_schemas()
|
|
188
|
+
if schemas:
|
|
189
|
+
generate_schemas(schemas)
|
|
190
|
+
|
|
191
|
+
schema_index = build_schema_index(schemas)
|
|
192
|
+
functions = replace_poly_refs_in_functions(functions, schema_index)
|
|
193
|
+
|
|
129
194
|
if functions:
|
|
130
195
|
generate_functions(functions)
|
|
131
196
|
else:
|
|
@@ -138,6 +203,7 @@ def generate() -> None:
|
|
|
138
203
|
if variables:
|
|
139
204
|
generate_variables(variables)
|
|
140
205
|
|
|
206
|
+
|
|
141
207
|
# indicator to vscode extension that this is a polyapi-python project
|
|
142
208
|
file_path = os.path.join(os.getcwd(), ".polyapi-python")
|
|
143
209
|
open(file_path, "w").close()
|
|
@@ -214,6 +280,12 @@ def render_spec(spec: SpecificationDto):
|
|
|
214
280
|
arguments,
|
|
215
281
|
return_type,
|
|
216
282
|
)
|
|
283
|
+
|
|
284
|
+
if X_POLY_REF_WARNING in func_type_defs:
|
|
285
|
+
# this indicates that jsonschema_gentypes has detected an x-poly-ref
|
|
286
|
+
# let's add a more user friendly error explaining what is going on
|
|
287
|
+
func_type_defs = func_type_defs.replace(X_POLY_REF_WARNING, X_POLY_REF_BETTER_WARNING)
|
|
288
|
+
|
|
217
289
|
return func_str, func_type_defs
|
|
218
290
|
|
|
219
291
|
|
|
@@ -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
|
|
@@ -365,7 +365,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
365
365
|
self._line_offsets.append(
|
|
366
366
|
self._line_offsets[i-1] + len(self._lines[i-1])
|
|
367
367
|
)
|
|
368
|
-
|
|
368
|
+
|
|
369
369
|
self._extract_deploy_comments()
|
|
370
370
|
|
|
371
371
|
def visit_AnnAssign(self, node):
|
|
@@ -483,7 +483,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
483
483
|
if docstring_params[type_index]["type"] != python_type:
|
|
484
484
|
deployable["dirty"] = True
|
|
485
485
|
except:
|
|
486
|
-
pass
|
|
486
|
+
pass
|
|
487
487
|
else:
|
|
488
488
|
deployable["dirty"] = True
|
|
489
489
|
|
|
@@ -497,7 +497,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
|
|
|
497
497
|
deployable["types"]["returns"]["typeSchema"] = return_type_schema
|
|
498
498
|
else:
|
|
499
499
|
deployable["types"]["returns"]["type"] = "Any"
|
|
500
|
-
|
|
500
|
+
|
|
501
501
|
def generic_visit(self, node):
|
|
502
502
|
if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
|
|
503
503
|
self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
|
6
|
+
from tests.test_schema import SCHEMA
|
|
7
|
+
|
|
8
|
+
from .typedefs import SchemaSpecDto
|
|
9
|
+
|
|
10
|
+
SCHEMA_CODE_IMPORTS = """from typing_extensions import TypedDict, NotRequired
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
|
|
17
|
+
''' unable to generate schema for {name}, defaulting to permissive type '''
|
|
18
|
+
pass
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def generate_schemas(specs: List[SchemaSpecDto]):
|
|
23
|
+
for spec in specs:
|
|
24
|
+
create_schema(spec)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_schema(spec: SchemaSpecDto) -> None:
|
|
28
|
+
folders = ["schemas"]
|
|
29
|
+
if spec["context"]:
|
|
30
|
+
folders += [s for s in spec["context"].split(".")]
|
|
31
|
+
|
|
32
|
+
# build up the full_path by adding all the folders
|
|
33
|
+
full_path = os.path.join(os.path.dirname(os.path.abspath(__file__)))
|
|
34
|
+
|
|
35
|
+
for idx, folder in enumerate(folders):
|
|
36
|
+
full_path = os.path.join(full_path, folder)
|
|
37
|
+
if not os.path.exists(full_path):
|
|
38
|
+
os.makedirs(full_path)
|
|
39
|
+
next = folders[idx + 1] if idx + 1 < len(folders) else None
|
|
40
|
+
if next:
|
|
41
|
+
add_import_to_init(full_path, next, code_imports=SCHEMA_CODE_IMPORTS)
|
|
42
|
+
|
|
43
|
+
add_schema_to_init(full_path, spec)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
|
|
47
|
+
init_the_init(full_path, code_imports="")
|
|
48
|
+
init_path = os.path.join(full_path, "__init__.py")
|
|
49
|
+
with open(init_path, "a") as f:
|
|
50
|
+
f.write(render_poly_schema(spec) + "\n\n")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def render_poly_schema(spec: SchemaSpecDto) -> str:
|
|
54
|
+
definition = spec["definition"]
|
|
55
|
+
if not definition.get("type"):
|
|
56
|
+
definition["type"] = "object"
|
|
57
|
+
root, schema_types = wrapped_generate_schema_types(
|
|
58
|
+
definition, root=spec["name"], fallback_type=Dict
|
|
59
|
+
)
|
|
60
|
+
return schema_types
|
|
61
|
+
# return FALLBACK_SPEC_TEMPLATE.format(name=spec["name"])
|
|
@@ -1,11 +1,18 @@
|
|
|
1
|
+
""" NOTE: this file represents the schema parsing logic for jsonschema_gentypes
|
|
2
|
+
"""
|
|
3
|
+
import random
|
|
4
|
+
import string
|
|
1
5
|
import logging
|
|
2
6
|
import contextlib
|
|
3
7
|
from typing import Dict
|
|
4
8
|
from jsonschema_gentypes.cli import process_config
|
|
5
9
|
from jsonschema_gentypes import configuration
|
|
10
|
+
import referencing
|
|
6
11
|
import tempfile
|
|
7
12
|
import json
|
|
8
13
|
|
|
14
|
+
import referencing.exceptions
|
|
15
|
+
|
|
9
16
|
from polyapi.constants import JSONSCHEMA_TO_PYTHON_TYPE_MAP
|
|
10
17
|
|
|
11
18
|
|
|
@@ -33,8 +40,18 @@ def _temp_store_input_data(input_data: Dict) -> str:
|
|
|
33
40
|
|
|
34
41
|
|
|
35
42
|
def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
|
|
43
|
+
from polyapi.utils import pascalCase
|
|
36
44
|
if not root:
|
|
37
|
-
root = "
|
|
45
|
+
root = "List" if fallback_type == "List" else "Dict"
|
|
46
|
+
if type_spec.get("x-poly-ref") and type_spec["x-poly-ref"].get("path"):
|
|
47
|
+
# x-poly-ref occurs when we have an unresolved reference
|
|
48
|
+
# lets name the root after the reference for some level of visibility
|
|
49
|
+
root += pascalCase(type_spec["x-poly-ref"]["path"].replace(".", " "))
|
|
50
|
+
else:
|
|
51
|
+
# add three random letters for uniqueness
|
|
52
|
+
root += random.choice(string.ascii_letters).upper()
|
|
53
|
+
root += random.choice(string.ascii_letters).upper()
|
|
54
|
+
root += random.choice(string.ascii_letters).upper()
|
|
38
55
|
|
|
39
56
|
root = clean_title(root)
|
|
40
57
|
|
|
@@ -44,8 +61,13 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
|
|
|
44
61
|
# some schemas are so huge, our library cant handle it
|
|
45
62
|
# TODO identify critical recursion penalty and maybe switch underlying logic to iterative?
|
|
46
63
|
return fallback_type, ""
|
|
64
|
+
except referencing.exceptions.CannotDetermineSpecification:
|
|
65
|
+
# just go with fallback_type here
|
|
66
|
+
# we couldn't match the right $ref earlier in resolve_poly_refs
|
|
67
|
+
# {'$ref': '#/definitions/FinanceAccountListModel'}
|
|
68
|
+
return fallback_type, ""
|
|
47
69
|
except:
|
|
48
|
-
logging.
|
|
70
|
+
logging.error(f"Error when generating schema type: {type_spec}\nusing fallback type '{fallback_type}'")
|
|
49
71
|
return fallback_type, ""
|
|
50
72
|
|
|
51
73
|
|
|
@@ -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]
|
|
@@ -11,19 +11,19 @@ from polyapi.schema import wrapped_generate_schema_types, clean_title, map_primi
|
|
|
11
11
|
|
|
12
12
|
# this string should be in every __init__ file.
|
|
13
13
|
# 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"}
|
|
14
|
+
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
15
|
|
|
17
16
|
|
|
18
|
-
def init_the_init(full_path: str) -> None:
|
|
17
|
+
def init_the_init(full_path: str, code_imports="") -> None:
|
|
19
18
|
init_path = os.path.join(full_path, "__init__.py")
|
|
20
19
|
if not os.path.exists(init_path):
|
|
20
|
+
code_imports = code_imports or CODE_IMPORTS
|
|
21
21
|
with open(init_path, "w") as f:
|
|
22
|
-
f.write(
|
|
22
|
+
f.write(code_imports)
|
|
23
23
|
|
|
24
24
|
|
|
25
|
-
def add_import_to_init(full_path: str, next: str) -> None:
|
|
26
|
-
init_the_init(full_path)
|
|
25
|
+
def add_import_to_init(full_path: str, next: str, code_imports="") -> None:
|
|
26
|
+
init_the_init(full_path, code_imports=code_imports)
|
|
27
27
|
|
|
28
28
|
init_path = os.path.join(full_path, "__init__.py")
|
|
29
29
|
with open(init_path, "a+") as f:
|
|
@@ -38,7 +38,7 @@ def get_auth_headers(api_key: str):
|
|
|
38
38
|
return {"Authorization": f"Bearer {api_key}"}
|
|
39
39
|
|
|
40
40
|
|
|
41
|
-
def camelCase(s):
|
|
41
|
+
def camelCase(s: str) -> str:
|
|
42
42
|
s = s.strip()
|
|
43
43
|
if " " in s or "-" in s:
|
|
44
44
|
s = re.sub(r"(_|-)+", " ", s).title().replace(" ", "")
|
|
@@ -48,6 +48,10 @@ def camelCase(s):
|
|
|
48
48
|
return s
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def pascalCase(s) -> str:
|
|
52
|
+
return re.sub(r"(^|_)([a-z])", lambda match: match.group(2).upper(), s)
|
|
53
|
+
|
|
54
|
+
|
|
51
55
|
def print_green(s: str):
|
|
52
56
|
print(Fore.GREEN + s + Style.RESET_ALL)
|
|
53
57
|
|
|
@@ -111,15 +115,20 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
111
115
|
elif type_spec["kind"] == "object":
|
|
112
116
|
if type_spec.get("schema"):
|
|
113
117
|
schema = type_spec["schema"]
|
|
114
|
-
title = schema.get("title", "")
|
|
118
|
+
title = schema.get("title", schema.get("name", ""))
|
|
115
119
|
if title:
|
|
116
120
|
assert isinstance(title, str)
|
|
117
121
|
return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
|
|
118
|
-
|
|
122
|
+
elif schema.get("allOf") and len(schema['allOf']):
|
|
123
|
+
# we are in a case of a single allOf, lets strip off the allOf and move on!
|
|
124
|
+
# our library doesn't handle allOf well yet
|
|
125
|
+
allOf = schema['allOf'][0]
|
|
126
|
+
title = allOf.get("title", allOf.get("name", ""))
|
|
127
|
+
return wrapped_generate_schema_types(allOf, title, "Dict")
|
|
119
128
|
elif schema.get("items"):
|
|
120
129
|
# fallback to schema $ref name if no explicit title
|
|
121
130
|
items = schema.get("items") # type: ignore
|
|
122
|
-
title = items.get("title"
|
|
131
|
+
title = items.get("title") # type: ignore
|
|
123
132
|
if not title:
|
|
124
133
|
# title is actually a reference to another schema
|
|
125
134
|
title = items.get("$ref", "") # type: ignore
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3.dev1
|
|
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
|
|
@@ -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,16 @@ 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
|
+
|
|
185
196
|
## Support
|
|
186
197
|
|
|
187
198
|
If you run into any issues or want help getting started with this project, please contact support@polyapi.io
|
|
@@ -16,6 +16,7 @@ polyapi/execute.py
|
|
|
16
16
|
polyapi/function_cli.py
|
|
17
17
|
polyapi/generate.py
|
|
18
18
|
polyapi/parser.py
|
|
19
|
+
polyapi/poly_schemas.py
|
|
19
20
|
polyapi/prepare.py
|
|
20
21
|
polyapi/py.typed
|
|
21
22
|
polyapi/rendered_spec.py
|
|
@@ -34,6 +35,7 @@ polyapi_python.egg-info/top_level.txt
|
|
|
34
35
|
tests/test_api.py
|
|
35
36
|
tests/test_auth.py
|
|
36
37
|
tests/test_deployables.py
|
|
38
|
+
tests/test_generate.py
|
|
37
39
|
tests/test_parser.py
|
|
38
40
|
tests/test_rendered_spec.py
|
|
39
41
|
tests/test_schema.py
|
|
@@ -3,12 +3,12 @@ requires = ["setuptools>=61.2", "wheel"]
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "polyapi-python"
|
|
6
|
-
version = "0.3.
|
|
6
|
+
version = "0.3.3.dev1"
|
|
7
7
|
description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
|
|
8
8
|
authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
|
|
9
9
|
dependencies = [
|
|
10
|
-
"requests
|
|
11
|
-
"typing_extensions>=4.
|
|
10
|
+
"requests>=2.32.3",
|
|
11
|
+
"typing_extensions>=4.12.2",
|
|
12
12
|
"jsonschema-gentypes==2.6.0",
|
|
13
13
|
"pydantic==2.6.4",
|
|
14
14
|
"stdlib_list==0.10.0",
|
|
@@ -21,7 +21,6 @@ def foobar() -> int:
|
|
|
21
21
|
|
|
22
22
|
EXPECTED_SERVER_FN_DEPLOYMENTS = '''# Poly deployed @ 2024-11-12T14:43:22.631113 - testing.foobar - https://na1.polyapi.io/canopy/polyui/collections/server-functions/jh23h5g3h5b24jh5b2j3h45v2jhg43v52j3h - 086aedd
|
|
23
23
|
# Poly deployed @ 2024-11-11T14:43:22.631113 - testing.foobar - https://dev.polyapi.io/canopy/polyui/collections/server-functions/jh23h5g3h5b24jh5b2j3h45v2jhg43v52j3h - 086aedd
|
|
24
|
-
|
|
25
24
|
from polyapi.typedefs import PolyServerFunction
|
|
26
25
|
|
|
27
26
|
polyConfig: PolyServerFunction = {
|
|
@@ -66,11 +65,11 @@ def foobar(foo: str, bar: Dict[str, str]) -> int:
|
|
|
66
65
|
"""A function that does something really import.
|
|
67
66
|
|
|
68
67
|
Args:
|
|
69
|
-
foo (str):
|
|
70
|
-
bar (Dict[str, str]):
|
|
68
|
+
foo (str):
|
|
69
|
+
bar (Dict[str, str]):
|
|
71
70
|
|
|
72
71
|
Returns:
|
|
73
|
-
int:
|
|
72
|
+
int:
|
|
74
73
|
"""
|
|
75
74
|
print("Okay then!")
|
|
76
75
|
return 7
|
|
@@ -186,7 +186,7 @@ class T(unittest.TestCase):
|
|
|
186
186
|
code = "import requests\n\n\ndef foobar(n: int) -> int:\n return 9\n"
|
|
187
187
|
deployable = parse_function_code(code, "foobar")
|
|
188
188
|
self.assertEqual(deployable["dependencies"], [])
|
|
189
|
-
|
|
189
|
+
|
|
190
190
|
def test_parse_glide_server_function_no_docstring(self):
|
|
191
191
|
code = GLIDE_SIMPLE_SERVER_FN
|
|
192
192
|
deployable = parse_function_code(code, "foobar")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from polyapi.utils import get_type_and_def, rewrite_reserved
|
|
3
|
+
|
|
4
|
+
OPENAPI_FUNCTION = {'kind': 'function', 'spec': {'arguments': [{'name': 'event', 'required': False, 'type': {'kind': 'object', 'schema': {'$schema': 'http://json-schema.org/draft-06/schema#', 'type': 'array', 'items': {'$ref': '#/definitions/WebhookEventTypeElement'}, 'definitions': {'WebhookEventTypeElement': {'type': 'object', 'additionalProperties': False, 'properties': {'title': {'type': 'string'}, 'manufacturerName': {'type': 'string'}, 'carType': {'type': 'string'}, 'id': {'type': 'integer'}}, 'required': ['carType', 'id', 'manufacturerName', 'title'], 'title': 'WebhookEventTypeElement'}}}}}, {'name': 'headers', 'required': False, 'type': {'kind': 'object', 'typeName': 'Record<string, any>'}}, {'name': 'params', 'required': False, 'type': {'kind': 'object', 'typeName': 'Record<string, any>'}}, {'name': 'polyCustom', 'required': False, 'type': {'kind': 'object', 'properties': [{'name': 'responseStatusCode', 'type': {'type': 'number', 'kind': 'primitive'}, 'required': True}, {'name': 'responseContentType', 'type': {'type': 'string', 'kind': 'primitive'}, 'required': True, 'nullable': True}]}}], 'returnType': {'kind': 'void'}, 'synchronous': True}}
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class T(unittest.TestCase):
|
|
8
|
+
def test_get_type_and_def(self):
|
|
9
|
+
arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
|
|
10
|
+
self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
|
|
11
|
+
|
|
12
|
+
def test_rewrite_reserved(self):
|
|
13
|
+
rv = rewrite_reserved("from")
|
|
14
|
+
self.assertEqual(rv, "_from")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{polyapi_python-0.3.2.dev2 → polyapi_python-0.3.3.dev1}/polyapi_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/polyapi_python-0.3.2.dev2/tests/test_utils.py → /polyapi_python-0.3.3.dev1/tests/test_generate.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|