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.
Files changed (38) hide show
  1. {polyapi_python-0.2.3.dev8/polyapi_python.egg-info → polyapi_python-0.2.4}/PKG-INFO +2 -2
  2. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/__init__.py +10 -1
  3. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/client.py +19 -0
  4. polyapi_python-0.2.4/polyapi/error_handler.py +85 -0
  5. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/execute.py +3 -1
  6. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/function_cli.py +46 -14
  7. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/generate.py +2 -0
  8. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/server.py +4 -9
  9. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/utils.py +28 -5
  10. polyapi_python-0.2.4/polyapi/webhook.py +137 -0
  11. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4/polyapi_python.egg-info}/PKG-INFO +2 -2
  12. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/pyproject.toml +2 -2
  13. polyapi_python-0.2.3.dev8/polyapi/error_handler.py +0 -52
  14. polyapi_python-0.2.3.dev8/polyapi/webhook.py +0 -92
  15. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/LICENSE +0 -0
  16. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/README.md +0 -0
  17. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/__main__.py +0 -0
  18. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/api.py +0 -0
  19. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/auth.py +0 -0
  20. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/cli.py +0 -0
  21. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/config.py +0 -0
  22. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/constants.py +0 -0
  23. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/exceptions.py +0 -0
  24. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/py.typed +0 -0
  25. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/schema.py +0 -0
  26. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/typedefs.py +0 -0
  27. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi/variables.py +0 -0
  28. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/SOURCES.txt +0 -0
  29. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/dependency_links.txt +0 -0
  30. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/requires.txt +0 -0
  31. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/polyapi_python.egg-info/top_level.txt +0 -0
  32. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/setup.cfg +0 -0
  33. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_api.py +0 -0
  34. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_auth.py +0 -0
  35. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_function_cli.py +0 -0
  36. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_server.py +0 -0
  37. {polyapi_python-0.2.3.dev8 → polyapi_python-0.2.4}/tests/test_utils.py +0 -0
  38. {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.3.dev8
4
- Summary: The PolyAPI Python Client
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
- if resp.status_code != 200 and resp.status_code != 201:
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 = {"polyapi", "requests", "typing_extensions", "jsonschema-gentypes", "pydantic", "cloudevents"}
22
- all_stdlib_symbols = stdlib_list('.'.join([str(v) for v in sys.version_info[0:2]]))
23
- BASE_REQUIREMENTS.update(all_stdlib_symbols) # dont need to pip install stuff in the python standard library
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(n: Optional[str], pip_name_lookup: Mapping[str, List[str]]) -> Optional[str]:
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, description: str, client: bool, server: bool, logs_enabled: bool, subcommands: List
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
- arguments,
196
- return_type,
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(f"Function {args.function_name} not found as top-level function in {args.filename}")
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('\nPlease note that deploying your functions will take a few minutes because it makes use of libraries other than polyapi.')
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
- return {return_action}
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
- return "ResponseType", generate_schema_types(type_spec, root="ResponseType") # type: ignore
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
- return title, generate_schema_types(schema, root=title) # type: ignore
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
- return title, generate_schema_types(schema, root=title)
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.3.dev8
4
- Summary: The PolyAPI Python Client
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.3.dev8"
7
- description = "The PolyAPI Python Client"
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, ""