polyapi-python 0.2.3.dev8__tar.gz → 0.2.4__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.2.3.dev8/polyapi_python.egg-info → polyapi_python-0.2.4}/PKG-INFO +2 -2
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/__init__.py +10 -1
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/client.py +19 -0
- polyapi_python-0.2.4/polyapi/error_handler.py +85 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/execute.py +3 -1
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/function_cli.py +46 -14
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/generate.py +2 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/server.py +4 -9
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/utils.py +28 -5
- polyapi_python-0.2.4/polyapi/webhook.py +137 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4/polyapi_python.egg-info}/PKG-INFO +2 -2
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/pyproject.toml +2 -2
- polyapi_python-0.2.3.dev8/polyapi/error_handler.py +0 -52
- polyapi_python-0.2.3.dev8/polyapi/webhook.py +0 -92
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/LICENSE +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/README.md +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/__main__.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/api.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/auth.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/cli.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/config.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/constants.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/exceptions.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/py.typed +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/schema.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/typedefs.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/variables.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/SOURCES.txt +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/dependency_links.txt +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/requires.txt +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/top_level.txt +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/setup.cfg +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_api.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_auth.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_function_cli.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_server.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_utils.py +0 -0
- {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_variables.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: The PolyAPI
|
|
3
|
+
Version: 0.2.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
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import truststore
|
|
4
|
+
from typing import Dict, Any
|
|
4
5
|
truststore.inject_into_ssl()
|
|
5
6
|
from .cli import CLI_COMMANDS
|
|
6
7
|
|
|
@@ -11,4 +12,12 @@ if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
|
|
|
11
12
|
currdir = os.path.dirname(os.path.abspath(__file__))
|
|
12
13
|
if not os.path.isdir(os.path.join(currdir, "poly")):
|
|
13
14
|
print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
|
|
14
|
-
sys.exit(1)
|
|
15
|
+
sys.exit(1)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
polyCustom: Dict[str, Any] = {
|
|
19
|
+
"executionId": None,
|
|
20
|
+
"executionApiKey": None,
|
|
21
|
+
"responseStatusCode": 200,
|
|
22
|
+
"responseContentType": None,
|
|
23
|
+
}
|
|
@@ -10,6 +10,22 @@ from typing import List, Dict, Any, TypedDict
|
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _wrap_code_in_try_except(code: str) -> str:
|
|
14
|
+
""" this is necessary because client functions with imports will blow up ALL server functions,
|
|
15
|
+
even if they don't use them.
|
|
16
|
+
because the server function will try to load all client functions when loading the library
|
|
17
|
+
"""
|
|
18
|
+
prefix = """logger = logging.getLogger("poly")
|
|
19
|
+
try:
|
|
20
|
+
"""
|
|
21
|
+
suffix = """except ImportError as e:
|
|
22
|
+
logger.debug(e)"""
|
|
23
|
+
|
|
24
|
+
lines = code.split("\n")
|
|
25
|
+
code = "\n ".join(lines)
|
|
26
|
+
return "".join([prefix, code, "\n", suffix])
|
|
27
|
+
|
|
28
|
+
|
|
13
29
|
def render_client_function(
|
|
14
30
|
function_name: str,
|
|
15
31
|
code: str,
|
|
@@ -22,4 +38,7 @@ def render_client_function(
|
|
|
22
38
|
args_def=args_def,
|
|
23
39
|
return_type_def=return_type_def,
|
|
24
40
|
)
|
|
41
|
+
|
|
42
|
+
code = _wrap_code_in_try_except(code)
|
|
43
|
+
|
|
25
44
|
return code + "\n\n", func_type_defs
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import copy
|
|
3
|
+
import socketio # type: ignore
|
|
4
|
+
from socketio.exceptions import ConnectionError # type: ignore
|
|
5
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from polyapi.config import get_api_key_and_url
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# all active webhook handlers, used by unregister_all to cleanup
|
|
11
|
+
active_handlers: List[Dict[str, Any]] = []
|
|
12
|
+
|
|
13
|
+
# global client shared by all error handlers, will be initialized by webhook.start
|
|
14
|
+
client = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def prepare():
|
|
18
|
+
loop = asyncio.get_event_loop()
|
|
19
|
+
loop.run_until_complete(get_client_and_connect())
|
|
20
|
+
print("Client initialized!")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def get_client_and_connect():
|
|
25
|
+
_, base_url = get_api_key_and_url()
|
|
26
|
+
global client
|
|
27
|
+
client = socketio.AsyncClient()
|
|
28
|
+
await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def unregister(data: Dict[str, Any]):
|
|
32
|
+
print(f"Stopping error handler for {data['path']}...")
|
|
33
|
+
assert client
|
|
34
|
+
await client.emit(
|
|
35
|
+
"unregisterErrorHandler",
|
|
36
|
+
data,
|
|
37
|
+
"/events",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def unregister_all():
|
|
42
|
+
_, base_url = get_api_key_and_url()
|
|
43
|
+
# need to reconnect because maybe socketio client disconnected after Ctrl+C?
|
|
44
|
+
try:
|
|
45
|
+
await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
|
|
46
|
+
except ConnectionError:
|
|
47
|
+
pass
|
|
48
|
+
await asyncio.gather(*[unregister(handler) for handler in active_handlers])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
async def on(
|
|
52
|
+
path: str, callback: Callable, options: Optional[Dict[str, Any]] = None
|
|
53
|
+
) -> None:
|
|
54
|
+
print(f"Starting error handler for {path}...")
|
|
55
|
+
|
|
56
|
+
if not client:
|
|
57
|
+
raise Exception("Client not initialized. Abort!")
|
|
58
|
+
|
|
59
|
+
api_key, _ = get_api_key_and_url()
|
|
60
|
+
handler_id = None
|
|
61
|
+
data = copy.deepcopy(options or {})
|
|
62
|
+
data["path"] = path
|
|
63
|
+
data["apiKey"] = api_key
|
|
64
|
+
|
|
65
|
+
def registerCallback(id: int):
|
|
66
|
+
nonlocal handler_id
|
|
67
|
+
handler_id = id
|
|
68
|
+
client.on(f"handleError:{handler_id}", callback, namespace="/events")
|
|
69
|
+
active_handlers.append({"path": path, "id": handler_id, "apiKey": api_key})
|
|
70
|
+
|
|
71
|
+
await client.emit("registerErrorHandler", data, "/events", registerCallback)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def start(*args):
|
|
75
|
+
loop = asyncio.get_event_loop()
|
|
76
|
+
loop.run_until_complete(get_client_and_connect())
|
|
77
|
+
asyncio.gather(*args)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
loop.run_forever()
|
|
81
|
+
except KeyboardInterrupt:
|
|
82
|
+
pass
|
|
83
|
+
finally:
|
|
84
|
+
loop.run_until_complete(unregister_all())
|
|
85
|
+
loop.stop()
|
|
@@ -11,7 +11,9 @@ def execute(function_type, function_id, data) -> Response:
|
|
|
11
11
|
headers = {"Authorization": f"Bearer {api_key}"}
|
|
12
12
|
url = f"{api_url}/functions/{function_type}/{function_id}/execute"
|
|
13
13
|
resp = requests.post(url, json=data, headers=headers)
|
|
14
|
-
|
|
14
|
+
# print(resp.status_code)
|
|
15
|
+
# print(resp.headers["content-type"])
|
|
16
|
+
if resp.status_code < 200 or resp.status_code >= 300:
|
|
15
17
|
error_content = resp.content.decode("utf-8", errors="ignore")
|
|
16
18
|
raise PolyApiException(f"{resp.status_code}: {error_content}")
|
|
17
19
|
return resp
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import types
|
|
5
5
|
import sys
|
|
6
6
|
from typing import Dict, List, Mapping, Optional, Tuple
|
|
7
|
+
from typing import _TypedDictMeta as BaseTypedDict # type: ignore
|
|
7
8
|
from typing_extensions import _TypedDictMeta # type: ignore
|
|
8
9
|
import requests
|
|
9
10
|
from stdlib_list import stdlib_list
|
|
@@ -18,9 +19,18 @@ import importlib
|
|
|
18
19
|
|
|
19
20
|
# these libraries are already installed in the base docker image
|
|
20
21
|
# and shouldnt be included in additional requirements
|
|
21
|
-
BASE_REQUIREMENTS = {
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
BASE_REQUIREMENTS = {
|
|
23
|
+
"polyapi",
|
|
24
|
+
"requests",
|
|
25
|
+
"typing_extensions",
|
|
26
|
+
"jsonschema-gentypes",
|
|
27
|
+
"pydantic",
|
|
28
|
+
"cloudevents",
|
|
29
|
+
}
|
|
30
|
+
all_stdlib_symbols = stdlib_list(".".join([str(v) for v in sys.version_info[0:2]]))
|
|
31
|
+
BASE_REQUIREMENTS.update(
|
|
32
|
+
all_stdlib_symbols
|
|
33
|
+
) # dont need to pip install stuff in the python standard library
|
|
24
34
|
|
|
25
35
|
|
|
26
36
|
def _get_schemas(code: str) -> List[Dict]:
|
|
@@ -28,7 +38,14 @@ def _get_schemas(code: str) -> List[Dict]:
|
|
|
28
38
|
user_code = types.SimpleNamespace()
|
|
29
39
|
exec(code, user_code.__dict__)
|
|
30
40
|
for name, obj in user_code.__dict__.items():
|
|
31
|
-
if (
|
|
41
|
+
if isinstance(obj, BaseTypedDict):
|
|
42
|
+
print_red("ERROR")
|
|
43
|
+
print_red("\nERROR DETAILS: ")
|
|
44
|
+
print(
|
|
45
|
+
"It looks like you have used TypedDict in a custom function. Please use `from typing_extensions import TypedDict` instead. The `typing_extensions` version is more powerful and better allows us to provide rich types for your function."
|
|
46
|
+
)
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
elif (
|
|
32
49
|
isinstance(obj, type)
|
|
33
50
|
and isinstance(obj, _TypedDictMeta)
|
|
34
51
|
and name != "TypedDict"
|
|
@@ -76,6 +93,13 @@ def get_python_type_from_ast(expr: ast.expr) -> str:
|
|
|
76
93
|
if name == "List":
|
|
77
94
|
slice = getattr(expr.slice, "id", "Any")
|
|
78
95
|
return f"List[{slice}]"
|
|
96
|
+
elif name == "Dict":
|
|
97
|
+
if expr.slice and isinstance(expr.slice, ast.Tuple):
|
|
98
|
+
key = get_python_type_from_ast(expr.slice.dims[0])
|
|
99
|
+
value = get_python_type_from_ast(expr.slice.dims[1])
|
|
100
|
+
return f"Dict[{key}, {value}]"
|
|
101
|
+
else:
|
|
102
|
+
return "Dict"
|
|
79
103
|
return "Any"
|
|
80
104
|
else:
|
|
81
105
|
return "Any"
|
|
@@ -104,7 +128,9 @@ def _get_type(expr: ast.expr | None, schemas: List[Dict]) -> Tuple[str, Dict | N
|
|
|
104
128
|
return json_type, _get_type_schema(json_type, python_type, schemas)
|
|
105
129
|
|
|
106
130
|
|
|
107
|
-
def _get_req_name_if_not_in_base(
|
|
131
|
+
def _get_req_name_if_not_in_base(
|
|
132
|
+
n: Optional[str], pip_name_lookup: Mapping[str, List[str]]
|
|
133
|
+
) -> Optional[str]:
|
|
108
134
|
if not n:
|
|
109
135
|
return None
|
|
110
136
|
|
|
@@ -175,7 +201,12 @@ def _func_already_exists(context: str, function_name: str) -> bool:
|
|
|
175
201
|
|
|
176
202
|
|
|
177
203
|
def function_add_or_update(
|
|
178
|
-
context: str,
|
|
204
|
+
context: str,
|
|
205
|
+
description: str,
|
|
206
|
+
client: bool,
|
|
207
|
+
server: bool,
|
|
208
|
+
logs_enabled: bool,
|
|
209
|
+
subcommands: List,
|
|
179
210
|
):
|
|
180
211
|
parser = argparse.ArgumentParser()
|
|
181
212
|
parser.add_argument("subcommand", choices=["add"])
|
|
@@ -191,16 +222,15 @@ def function_add_or_update(
|
|
|
191
222
|
code = f.read()
|
|
192
223
|
|
|
193
224
|
# OK! let's parse the code and generate the arguments
|
|
194
|
-
(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return_type_schema,
|
|
198
|
-
requirements
|
|
199
|
-
) = _parse_code(code, args.function_name)
|
|
225
|
+
(arguments, return_type, return_type_schema, requirements) = _parse_code(
|
|
226
|
+
code, args.function_name
|
|
227
|
+
)
|
|
200
228
|
|
|
201
229
|
if not return_type:
|
|
202
230
|
print_red("ERROR")
|
|
203
|
-
print(
|
|
231
|
+
print(
|
|
232
|
+
f"Function {args.function_name} not found as top-level function in {args.filename}"
|
|
233
|
+
)
|
|
204
234
|
sys.exit(1)
|
|
205
235
|
|
|
206
236
|
data = {
|
|
@@ -216,7 +246,9 @@ def function_add_or_update(
|
|
|
216
246
|
}
|
|
217
247
|
|
|
218
248
|
if server and requirements:
|
|
219
|
-
print_yellow(
|
|
249
|
+
print_yellow(
|
|
250
|
+
"\nPlease note that deploying your functions will take a few minutes because it makes use of libraries other than polyapi."
|
|
251
|
+
)
|
|
220
252
|
data["requirements"] = requirements
|
|
221
253
|
|
|
222
254
|
api_key, api_url = get_api_key_and_url()
|
|
@@ -176,6 +176,7 @@ def render_spec(spec: SpecificationDto):
|
|
|
176
176
|
function_type = spec["type"]
|
|
177
177
|
function_description = spec["description"]
|
|
178
178
|
function_name = spec["name"]
|
|
179
|
+
function_context = spec["context"]
|
|
179
180
|
function_id = spec["id"]
|
|
180
181
|
|
|
181
182
|
arguments: List[PropertySpecification] = []
|
|
@@ -223,6 +224,7 @@ def render_spec(spec: SpecificationDto):
|
|
|
223
224
|
elif function_type == "webhookHandle":
|
|
224
225
|
func_str, func_type_defs = render_webhook_handle(
|
|
225
226
|
function_type,
|
|
227
|
+
function_context,
|
|
226
228
|
function_name,
|
|
227
229
|
function_id,
|
|
228
230
|
function_description,
|
|
@@ -18,7 +18,10 @@ def {function_name}(
|
|
|
18
18
|
Function ID: {function_id}
|
|
19
19
|
\"""
|
|
20
20
|
resp = execute("{function_type}", "{function_id}", {data})
|
|
21
|
-
|
|
21
|
+
try:
|
|
22
|
+
return {return_action}
|
|
23
|
+
except:
|
|
24
|
+
return resp.text
|
|
22
25
|
"""
|
|
23
26
|
|
|
24
27
|
|
|
@@ -54,14 +57,6 @@ def render_server_function(
|
|
|
54
57
|
def _get_server_return_action(return_type_name: str) -> str:
|
|
55
58
|
if return_type_name == "str":
|
|
56
59
|
return_action = "resp.text"
|
|
57
|
-
elif return_type_name == "Any":
|
|
58
|
-
return_action = "resp.text"
|
|
59
|
-
elif return_type_name == "int":
|
|
60
|
-
return_action = "int(resp.text.replace('(int) ', ''))"
|
|
61
|
-
elif return_type_name == "float":
|
|
62
|
-
return_action = "float(resp.text.replace('(float) ', ''))"
|
|
63
|
-
elif return_type_name == "bool":
|
|
64
|
-
return_action = "False if resp.text == 'False' else True"
|
|
65
60
|
else:
|
|
66
61
|
return_action = "resp.json()"
|
|
67
62
|
return return_action
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import os
|
|
3
|
+
import logging
|
|
3
4
|
from typing import Tuple, List
|
|
4
5
|
from colorama import Fore, Style
|
|
5
6
|
from polyapi.constants import BASIC_PYTHON_TYPES
|
|
@@ -9,7 +10,7 @@ from polyapi.schema import generate_schema_types, clean_title, map_primitive_typ
|
|
|
9
10
|
|
|
10
11
|
# this string should be in every __init__ file.
|
|
11
12
|
# it contains all the imports needed for the function or variable code to run
|
|
12
|
-
CODE_IMPORTS = "from typing import List, Dict, Any, TypedDict, Optional\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"
|
|
13
|
+
CODE_IMPORTS = "from typing import List, Dict, Any, TypedDict, Optional\nimport logging\nimport requests\nimport socketio # type: ignore\nfrom polyapi.config import get_api_key_and_url\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update\n\n"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
def init_the_init(full_path: str) -> None:
|
|
@@ -91,7 +92,11 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
91
92
|
if type_spec.get("items"):
|
|
92
93
|
items = type_spec["items"]
|
|
93
94
|
if items.get("$ref"):
|
|
94
|
-
|
|
95
|
+
try:
|
|
96
|
+
return "ResponseType", generate_schema_types(type_spec, root="ResponseType") # type: ignore
|
|
97
|
+
except:
|
|
98
|
+
logging.exception(f"Error when generating schema type: {type_spec}")
|
|
99
|
+
return "Dict", ""
|
|
95
100
|
else:
|
|
96
101
|
item_type, _ = get_type_and_def(items)
|
|
97
102
|
title = f"List[{item_type}]"
|
|
@@ -108,7 +113,12 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
108
113
|
if title:
|
|
109
114
|
assert isinstance(title, str)
|
|
110
115
|
title = clean_title(title)
|
|
111
|
-
|
|
116
|
+
try:
|
|
117
|
+
return title, generate_schema_types(schema, root=title) # type: ignore
|
|
118
|
+
except:
|
|
119
|
+
logging.exception(f"Error when generating schema type: {schema}")
|
|
120
|
+
return "Dict", ""
|
|
121
|
+
|
|
112
122
|
elif schema.get("items"):
|
|
113
123
|
# fallback to schema $ref name if no explicit title
|
|
114
124
|
items = schema.get("items") # type: ignore
|
|
@@ -123,7 +133,11 @@ def get_type_and_def(type_spec: PropertyType) -> Tuple[str, str]:
|
|
|
123
133
|
return "List", ""
|
|
124
134
|
|
|
125
135
|
title = f"List[{title}]"
|
|
126
|
-
|
|
136
|
+
try:
|
|
137
|
+
return title, generate_schema_types(schema, root=title)
|
|
138
|
+
except:
|
|
139
|
+
logging.exception(f"Error when generating schema type: {schema}")
|
|
140
|
+
return "List", ""
|
|
127
141
|
else:
|
|
128
142
|
return "Any", ""
|
|
129
143
|
else:
|
|
@@ -151,4 +165,13 @@ def parse_arguments(function_name: str, arguments: List[PropertySpecification])
|
|
|
151
165
|
arg_string += f", # {description}\n"
|
|
152
166
|
else:
|
|
153
167
|
arg_string += ",\n"
|
|
154
|
-
return arg_string.rstrip("\n"), "\n\n".join(args_def)
|
|
168
|
+
return arg_string.rstrip("\n"), "\n\n".join(args_def)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def poly_full_path(context, name) -> str:
|
|
172
|
+
"""get the functions path as it will be exposed in the poly library"""
|
|
173
|
+
if context:
|
|
174
|
+
path = context + "." + name
|
|
175
|
+
else:
|
|
176
|
+
path = name
|
|
177
|
+
return f"poly.{path}"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import socketio # type: ignore
|
|
3
|
+
from socketio.exceptions import ConnectionError # type: ignore
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Any, Dict, List, Tuple
|
|
6
|
+
|
|
7
|
+
from polyapi.config import get_api_key_and_url
|
|
8
|
+
from polyapi.typedefs import PropertySpecification
|
|
9
|
+
from polyapi.utils import poly_full_path
|
|
10
|
+
|
|
11
|
+
# all active webhook handlers, used by unregister_all to cleanup
|
|
12
|
+
active_handlers: List[Dict[str, Any]] = []
|
|
13
|
+
|
|
14
|
+
# global client shared by all webhooks, will be initialized by webhook.start
|
|
15
|
+
client = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
WEBHOOK_TEMPLATE = """
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def {function_name}(callback, options=None):
|
|
22
|
+
\"""{description}
|
|
23
|
+
|
|
24
|
+
Function ID: {function_id}
|
|
25
|
+
\"""
|
|
26
|
+
from polyapi.webhook import client, active_handlers
|
|
27
|
+
|
|
28
|
+
print("Starting webhook handler for {function_path}...")
|
|
29
|
+
|
|
30
|
+
if not client:
|
|
31
|
+
raise Exception("Client not initialized. Abort!")
|
|
32
|
+
|
|
33
|
+
options = options or {{}}
|
|
34
|
+
eventsClientId = "{client_id}"
|
|
35
|
+
function_id = "{function_id}"
|
|
36
|
+
|
|
37
|
+
api_key, base_url = get_api_key_and_url()
|
|
38
|
+
|
|
39
|
+
def registerCallback(registered: bool):
|
|
40
|
+
if registered:
|
|
41
|
+
client.on('handleWebhookEvent:{function_id}', handleEvent, namespace="/events")
|
|
42
|
+
else:
|
|
43
|
+
print("Could not set register webhook event handler for {function_id}")
|
|
44
|
+
|
|
45
|
+
async def handleEvent(data):
|
|
46
|
+
nonlocal api_key
|
|
47
|
+
nonlocal options
|
|
48
|
+
polyCustom = {{}}
|
|
49
|
+
resp = callback(data.get("body"), data.get("headers"), data.get("params"), polyCustom)
|
|
50
|
+
if options.get("waitForResponse"):
|
|
51
|
+
await client.emit('setWebhookListenerResponse', {{
|
|
52
|
+
"webhookHandleID": function_id,
|
|
53
|
+
"apiKey": api_key,
|
|
54
|
+
"clientID": eventsClientId,
|
|
55
|
+
"executionId": data.get("executionId"),
|
|
56
|
+
"response": {{
|
|
57
|
+
"data": resp,
|
|
58
|
+
"statusCode": polyCustom.get("responseStatusCode", 200),
|
|
59
|
+
"contentType": polyCustom.get("responseContentType", None),
|
|
60
|
+
}},
|
|
61
|
+
}}, namespace="/events")
|
|
62
|
+
|
|
63
|
+
data = {{
|
|
64
|
+
"clientID": eventsClientId,
|
|
65
|
+
"webhookHandleID": function_id,
|
|
66
|
+
"apiKey": api_key,
|
|
67
|
+
"waitForResponse": options.get("waitForResponse"),
|
|
68
|
+
}}
|
|
69
|
+
await client.emit('registerWebhookEventHandler', data, namespace="/events", callback=registerCallback)
|
|
70
|
+
active_handlers.append({{"clientID": eventsClientId, "webhookHandleID": function_id, "apiKey": api_key, "path": "{function_path}"}})
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def get_client_and_connect():
|
|
75
|
+
_, base_url = get_api_key_and_url()
|
|
76
|
+
global client
|
|
77
|
+
client = socketio.AsyncClient()
|
|
78
|
+
await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def unregister(data: Dict[str, Any]):
|
|
82
|
+
print(f"Stopping webhook handler for {data['path']}...")
|
|
83
|
+
assert client
|
|
84
|
+
await client.emit(
|
|
85
|
+
"unregisterWebhookEventHandler",
|
|
86
|
+
{
|
|
87
|
+
"clientID": data["clientID"],
|
|
88
|
+
"webhookHandleID": data["webhookHandleID"],
|
|
89
|
+
"apiKey": data["apiKey"],
|
|
90
|
+
},
|
|
91
|
+
"/events",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def unregister_all():
|
|
96
|
+
_, base_url = get_api_key_and_url()
|
|
97
|
+
# maybe need to reconnect because maybe socketio client disconnected after Ctrl+C?
|
|
98
|
+
# feels like Linux disconnects but Windows stays connected
|
|
99
|
+
try:
|
|
100
|
+
await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
|
|
101
|
+
except ConnectionError:
|
|
102
|
+
pass
|
|
103
|
+
await asyncio.gather(*[unregister(handler) for handler in active_handlers])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_webhook_handle(
|
|
107
|
+
function_type: str,
|
|
108
|
+
function_context: str,
|
|
109
|
+
function_name: str,
|
|
110
|
+
function_id: str,
|
|
111
|
+
function_description: str,
|
|
112
|
+
arguments: List[PropertySpecification],
|
|
113
|
+
return_type: Dict[str, Any],
|
|
114
|
+
) -> Tuple[str, str]:
|
|
115
|
+
func_str = WEBHOOK_TEMPLATE.format(
|
|
116
|
+
description=function_description,
|
|
117
|
+
client_id=uuid.uuid4().hex,
|
|
118
|
+
function_id=function_id,
|
|
119
|
+
function_name=function_name,
|
|
120
|
+
function_path=poly_full_path(function_context, function_name),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return func_str, ""
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def start(*args):
|
|
127
|
+
loop = asyncio.get_event_loop()
|
|
128
|
+
loop.run_until_complete(get_client_and_connect())
|
|
129
|
+
asyncio.gather(*args)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
loop.run_forever()
|
|
133
|
+
except KeyboardInterrupt:
|
|
134
|
+
pass
|
|
135
|
+
finally:
|
|
136
|
+
loop.run_until_complete(unregister_all())
|
|
137
|
+
loop.stop()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: polyapi-python
|
|
3
|
-
Version: 0.2.
|
|
4
|
-
Summary: The PolyAPI
|
|
3
|
+
Version: 0.2.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
|
|
|
@@ -3,8 +3,8 @@ requires = ["setuptools>=61.2", "wheel"]
|
|
|
3
3
|
|
|
4
4
|
[project]
|
|
5
5
|
name = "polyapi-python"
|
|
6
|
-
version = "0.2.
|
|
7
|
-
description = "The PolyAPI
|
|
6
|
+
version = "0.2.4"
|
|
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
10
|
"requests==2.31.0",
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import copy
|
|
3
|
-
import socketio # type: ignore
|
|
4
|
-
from typing import Any, Callable, Dict, Optional
|
|
5
|
-
|
|
6
|
-
from polyapi.config import get_api_key_and_url
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
local_error_handlers: Dict[str, Any] = {}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def on(path: str, callback: Callable, options: Optional[Dict[str, Any]] = None) -> Callable:
|
|
13
|
-
assert not local_error_handlers
|
|
14
|
-
socket = socketio.AsyncClient()
|
|
15
|
-
api_key, base_url = get_api_key_and_url()
|
|
16
|
-
|
|
17
|
-
async def _inner():
|
|
18
|
-
await socket.connect(base_url, transports=["websocket"], namespaces=["/events"])
|
|
19
|
-
|
|
20
|
-
handler_id = None
|
|
21
|
-
data = copy.deepcopy(options or {})
|
|
22
|
-
data["path"] = path
|
|
23
|
-
data["apiKey"] = api_key
|
|
24
|
-
|
|
25
|
-
def registerCallback(id: int):
|
|
26
|
-
nonlocal handler_id, socket
|
|
27
|
-
handler_id = id
|
|
28
|
-
socket.on(f"handleError:{handler_id}", callback, namespace="/events")
|
|
29
|
-
|
|
30
|
-
await socket.emit("registerErrorHandler", data, "/events", registerCallback)
|
|
31
|
-
if local_error_handlers.get(path):
|
|
32
|
-
local_error_handlers[path].append(callback)
|
|
33
|
-
else:
|
|
34
|
-
local_error_handlers[path] = [callback]
|
|
35
|
-
|
|
36
|
-
async def unregister():
|
|
37
|
-
nonlocal handler_id, socket
|
|
38
|
-
if handler_id and socket:
|
|
39
|
-
await socket.emit(
|
|
40
|
-
"unregisterErrorHandler",
|
|
41
|
-
{"id": handler_id, "path": path, "apiKey": api_key},
|
|
42
|
-
namespace="/events",
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
if local_error_handlers.get(path):
|
|
46
|
-
local_error_handlers[path].remove(callback)
|
|
47
|
-
|
|
48
|
-
await socket.wait()
|
|
49
|
-
|
|
50
|
-
return unregister
|
|
51
|
-
|
|
52
|
-
return asyncio.run(_inner())
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import uuid
|
|
2
|
-
from typing import Any, Dict, List, Tuple
|
|
3
|
-
|
|
4
|
-
from polyapi.typedefs import PropertySpecification
|
|
5
|
-
|
|
6
|
-
WEBHOOK_TEMPLATE = """
|
|
7
|
-
import asyncio
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def {function_name}(callback, options=None):
|
|
11
|
-
\"""{description}
|
|
12
|
-
|
|
13
|
-
Function ID: {function_id}
|
|
14
|
-
\"""
|
|
15
|
-
options = options or {{}}
|
|
16
|
-
eventsClientId = "{client_id}"
|
|
17
|
-
function_id = "{function_id}"
|
|
18
|
-
|
|
19
|
-
api_key, base_url = get_api_key_and_url()
|
|
20
|
-
|
|
21
|
-
async def _inner():
|
|
22
|
-
socket = socketio.AsyncClient()
|
|
23
|
-
await socket.connect(base_url, transports=['websocket'], namespaces=['/events'])
|
|
24
|
-
|
|
25
|
-
def registerCallback(registered: bool):
|
|
26
|
-
nonlocal socket
|
|
27
|
-
if registered:
|
|
28
|
-
socket.on('handleWebhookEvent:{function_id}', handleEvent, namespace="/events")
|
|
29
|
-
else:
|
|
30
|
-
print("Could not set register webhook event handler for {function_id}")
|
|
31
|
-
|
|
32
|
-
async def handleEvent(data):
|
|
33
|
-
nonlocal api_key
|
|
34
|
-
nonlocal options
|
|
35
|
-
polyCustom = {{}}
|
|
36
|
-
resp = await callback(data.get("body"), data.get("headers"), data.get("params"), polyCustom)
|
|
37
|
-
if options.get("waitForResponse"):
|
|
38
|
-
await socket.emit('setWebhookListenerResponse', {{
|
|
39
|
-
"webhookHandleID": function_id,
|
|
40
|
-
"apiKey": api_key,
|
|
41
|
-
"clientID": eventsClientId,
|
|
42
|
-
"executionId": data.get("executionId"),
|
|
43
|
-
"response": {{
|
|
44
|
-
"data": resp,
|
|
45
|
-
"statusCode": polyCustom.get("responseStatusCode", 200),
|
|
46
|
-
"contentType": polyCustom.get("responseContentType", None),
|
|
47
|
-
}},
|
|
48
|
-
}}, namespace="/events")
|
|
49
|
-
|
|
50
|
-
data = {{
|
|
51
|
-
"clientID": eventsClientId,
|
|
52
|
-
"webhookHandleID": function_id,
|
|
53
|
-
"apiKey": api_key,
|
|
54
|
-
"waitForResponse": options.get("waitForResponse"),
|
|
55
|
-
}}
|
|
56
|
-
await socket.emit('registerWebhookEventHandler', data, namespace="/events", callback=registerCallback)
|
|
57
|
-
|
|
58
|
-
async def closeEventHandler():
|
|
59
|
-
nonlocal socket
|
|
60
|
-
if not socket:
|
|
61
|
-
return
|
|
62
|
-
|
|
63
|
-
await socket.emit('unregisterWebhookEventHandler', {{
|
|
64
|
-
"clientID": eventsClientId,
|
|
65
|
-
"webhookHandleID": function_id,
|
|
66
|
-
"apiKey": api_key
|
|
67
|
-
}}, namespace="/events")
|
|
68
|
-
|
|
69
|
-
await socket.wait()
|
|
70
|
-
|
|
71
|
-
return closeEventHandler
|
|
72
|
-
|
|
73
|
-
return asyncio.run(_inner())
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def render_webhook_handle(
|
|
78
|
-
function_type: str,
|
|
79
|
-
function_name: str,
|
|
80
|
-
function_id: str,
|
|
81
|
-
function_description: str,
|
|
82
|
-
arguments: List[PropertySpecification],
|
|
83
|
-
return_type: Dict[str, Any],
|
|
84
|
-
) -> Tuple[str, str]:
|
|
85
|
-
func_str = WEBHOOK_TEMPLATE.format(
|
|
86
|
-
description=function_description,
|
|
87
|
-
client_id=uuid.uuid4().hex,
|
|
88
|
-
function_id=function_id,
|
|
89
|
-
function_name=function_name,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
return func_str, ""
|
|
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.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/dependency_links.txt
RENAMED
|
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
|