polyapi-python 0.3.6.dev0__tar.gz → 0.3.7__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 (48) hide show
  1. {polyapi_python-0.3.6.dev0/polyapi_python.egg-info → polyapi_python-0.3.7}/PKG-INFO +1 -1
  2. polyapi_python-0.3.7/polyapi/__init__.py +101 -0
  3. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/api.py +10 -2
  4. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/config.py +30 -1
  5. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/execute.py +51 -5
  6. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/function_cli.py +2 -3
  7. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/generate.py +110 -29
  8. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/schema.py +5 -1
  9. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/utils.py +66 -15
  10. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/webhook.py +1 -0
  11. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7/polyapi_python.egg-info}/PKG-INFO +1 -1
  12. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/pyproject.toml +1 -1
  13. polyapi_python-0.3.7/tests/test_generate.py +289 -0
  14. polyapi_python-0.3.6.dev0/polyapi/__init__.py +0 -23
  15. polyapi_python-0.3.6.dev0/tests/test_generate.py +0 -83
  16. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/LICENSE +0 -0
  17. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/README.md +0 -0
  18. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/__main__.py +0 -0
  19. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/auth.py +0 -0
  20. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/cli.py +0 -0
  21. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/client.py +0 -0
  22. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/constants.py +0 -0
  23. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/deployables.py +0 -0
  24. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/error_handler.py +0 -0
  25. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/exceptions.py +0 -0
  26. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/parser.py +0 -0
  27. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/poly_schemas.py +0 -0
  28. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/prepare.py +0 -0
  29. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/py.typed +0 -0
  30. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/rendered_spec.py +0 -0
  31. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/server.py +0 -0
  32. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/sync.py +0 -0
  33. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/typedefs.py +0 -0
  34. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi/variables.py +0 -0
  35. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/SOURCES.txt +0 -0
  36. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/dependency_links.txt +0 -0
  37. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/requires.txt +0 -0
  38. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/polyapi_python.egg-info/top_level.txt +0 -0
  39. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/setup.cfg +0 -0
  40. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_api.py +0 -0
  41. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_auth.py +0 -0
  42. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_deployables.py +0 -0
  43. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_parser.py +0 -0
  44. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_rendered_spec.py +0 -0
  45. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_schema.py +0 -0
  46. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_server.py +0 -0
  47. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_utils.py +0 -0
  48. {polyapi_python-0.3.6.dev0 → polyapi_python-0.3.7}/tests/test_variables.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.6.dev0
3
+ Version: 0.3.7
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
@@ -0,0 +1,101 @@
1
+ import os
2
+ import sys
3
+ import copy
4
+ import truststore
5
+ from typing import Any, Dict, Optional, overload, Literal
6
+ from typing_extensions import TypedDict
7
+ truststore.inject_into_ssl()
8
+ from .cli import CLI_COMMANDS
9
+
10
+ __all__ = ["poly"]
11
+
12
+
13
+ if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
14
+ currdir = os.path.dirname(os.path.abspath(__file__))
15
+ if not os.path.isdir(os.path.join(currdir, "poly")):
16
+ print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
17
+ sys.exit(1)
18
+
19
+
20
+ class PolyCustomDict(TypedDict, total=False):
21
+ """Type definition for polyCustom dictionary."""
22
+ executionId: Optional[str] # Read-only
23
+ executionApiKey: Optional[str]
24
+ responseStatusCode: int
25
+ responseContentType: Optional[str]
26
+ responseHeaders: Dict[str, str]
27
+
28
+
29
+ class _PolyCustom:
30
+ def __init__(self):
31
+ self._internal_store = {
32
+ "executionId": None,
33
+ "executionApiKey": None,
34
+ "responseStatusCode": 200,
35
+ "responseContentType": None,
36
+ "responseHeaders": {},
37
+ }
38
+ self._execution_id_locked = False
39
+
40
+ def set_once(self, key: str, value: Any) -> None:
41
+ if key == "executionId" and self._execution_id_locked:
42
+ # Silently ignore attempts to overwrite locked executionId
43
+ return
44
+ self._internal_store[key] = value
45
+ if key == "executionId":
46
+ # Lock executionId after setting it
47
+ self.lock_execution_id()
48
+
49
+ def get(self, key: str, default: Any = None) -> Any:
50
+ return self._internal_store.get(key, default)
51
+
52
+ def lock_execution_id(self) -> None:
53
+ self._execution_id_locked = True
54
+
55
+ def unlock_execution_id(self) -> None:
56
+ self._execution_id_locked = False
57
+
58
+ @overload
59
+ def __getitem__(self, key: Literal["executionId"]) -> Optional[str]: ...
60
+
61
+ @overload
62
+ def __getitem__(self, key: Literal["executionApiKey"]) -> Optional[str]: ...
63
+
64
+ @overload
65
+ def __getitem__(self, key: Literal["responseStatusCode"]) -> int: ...
66
+
67
+ @overload
68
+ def __getitem__(self, key: Literal["responseContentType"]) -> Optional[str]: ...
69
+
70
+ @overload
71
+ def __getitem__(self, key: Literal["responseHeaders"]) -> Dict[str, str]: ...
72
+
73
+ def __getitem__(self, key: str) -> Any:
74
+ return self.get(key)
75
+
76
+ @overload
77
+ def __setitem__(self, key: Literal["executionApiKey"], value: Optional[str]) -> None: ...
78
+
79
+ @overload
80
+ def __setitem__(self, key: Literal["responseStatusCode"], value: int) -> None: ...
81
+
82
+ @overload
83
+ def __setitem__(self, key: Literal["responseContentType"], value: Optional[str]) -> None: ...
84
+
85
+ @overload
86
+ def __setitem__(self, key: Literal["responseHeaders"], value: Dict[str, str]) -> None: ...
87
+
88
+ def __setitem__(self, key: str, value: Any) -> None:
89
+ self.set_once(key, value)
90
+
91
+ def __repr__(self) -> str:
92
+ return f"PolyCustom({self._internal_store})"
93
+
94
+ def copy(self) -> '_PolyCustom':
95
+ new = _PolyCustom()
96
+ new._internal_store = copy.deepcopy(self._internal_store)
97
+ new._execution_id_locked = self._execution_id_locked
98
+ return new
99
+
100
+
101
+ polyCustom: PolyCustomDict = _PolyCustom()
@@ -23,8 +23,16 @@ def {function_name}(
23
23
 
24
24
  Function ID: {function_id}
25
25
  \"""
26
- resp = execute("{function_type}", "{function_id}", {data})
27
- return {api_response_type}(resp.json()) # type: ignore
26
+ if get_direct_execute_config():
27
+ resp = direct_execute("{function_type}", "{function_id}", {data})
28
+ return {api_response_type}({{
29
+ "status": resp.status_code,
30
+ "headers": dict(resp.headers),
31
+ "data": resp.json()
32
+ }}) # type: ignore
33
+ else:
34
+ resp = execute("{function_type}", "{function_id}", {data})
35
+ return {api_response_type}(resp.json()) # type: ignore
28
36
 
29
37
 
30
38
  """
@@ -8,6 +8,10 @@ from polyapi.utils import is_valid_polyapi_url, print_green, print_yellow
8
8
  # cached values
9
9
  API_KEY = None
10
10
  API_URL = None
11
+ API_FUNCTION_DIRECT_EXECUTE = None
12
+ MTLS_CERT_PATH = None
13
+ MTLS_KEY_PATH = None
14
+ MTLS_CA_PATH = None
11
15
 
12
16
 
13
17
  def get_config_file_path() -> str:
@@ -45,6 +49,13 @@ def get_api_key_and_url() -> Tuple[str | None, str | None]:
45
49
  API_KEY = key
46
50
  API_URL = url
47
51
 
52
+ # Read and cache MTLS and direct execute settings
53
+ global API_FUNCTION_DIRECT_EXECUTE, MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
54
+ API_FUNCTION_DIRECT_EXECUTE = config.get("polyapi", "api_function_direct_execute", fallback="false").lower() == "true"
55
+ MTLS_CERT_PATH = config.get("polyapi", "mtls_cert_path", fallback=None)
56
+ MTLS_KEY_PATH = config.get("polyapi", "mtls_key_path", fallback=None)
57
+ MTLS_CA_PATH = config.get("polyapi", "mtls_ca_path", fallback=None)
58
+
48
59
  return key, url
49
60
 
50
61
 
@@ -104,4 +115,22 @@ def clear_config():
104
115
 
105
116
  path = get_config_file_path()
106
117
  if os.path.exists(path):
107
- os.remove(path)
118
+ os.remove(path)
119
+
120
+
121
+ def get_mtls_config() -> Tuple[bool, str | None, str | None, str | None]:
122
+ """Return MTLS configuration settings"""
123
+ global MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
124
+ if MTLS_CERT_PATH is None or MTLS_KEY_PATH is None or MTLS_CA_PATH is None:
125
+ # Force a config read if values aren't cached
126
+ get_api_key_and_url()
127
+ return bool(MTLS_CERT_PATH and MTLS_KEY_PATH and MTLS_CA_PATH), MTLS_CERT_PATH, MTLS_KEY_PATH, MTLS_CA_PATH
128
+
129
+
130
+ def get_direct_execute_config() -> bool:
131
+ """Return whether direct execute is enabled"""
132
+ global API_FUNCTION_DIRECT_EXECUTE
133
+ if API_FUNCTION_DIRECT_EXECUTE is None:
134
+ # Force a config read if value isn't cached
135
+ get_api_key_and_url()
136
+ return bool(API_FUNCTION_DIRECT_EXECUTE)
@@ -1,22 +1,68 @@
1
- from typing import Dict
1
+ from typing import Dict, Optional
2
2
  import requests
3
3
  from requests import Response
4
- from polyapi.config import get_api_key_and_url
4
+ from polyapi.config import get_api_key_and_url, get_mtls_config
5
5
  from polyapi.exceptions import PolyApiException
6
6
 
7
+ def direct_execute(function_type, function_id, data) -> Response:
8
+ """ execute a specific function id/type
9
+ """
10
+ api_key, api_url = get_api_key_and_url()
11
+ headers = {"Authorization": f"Bearer {api_key}"}
12
+ url = f"{api_url}/functions/{function_type}/{function_id}/direct-execute"
13
+
14
+ endpoint_info = requests.post(url, json=data, headers=headers)
15
+ if endpoint_info.status_code < 200 or endpoint_info.status_code >= 300:
16
+ raise PolyApiException(f"{endpoint_info.status_code}: {endpoint_info.content.decode('utf-8', errors='ignore')}")
17
+
18
+ endpoint_info_data = endpoint_info.json()
19
+ request_params = endpoint_info_data.copy()
20
+ request_params.pop("url", None)
21
+
22
+ if "maxRedirects" in request_params:
23
+ request_params["allow_redirects"] = request_params.pop("maxRedirects") > 0
24
+
25
+ has_mtls, cert_path, key_path, ca_path = get_mtls_config()
26
+
27
+ if has_mtls:
28
+ resp = requests.request(
29
+ url=endpoint_info_data["url"],
30
+ cert=(cert_path, key_path),
31
+ verify=ca_path,
32
+ **request_params
33
+ )
34
+ else:
35
+ resp = requests.request(
36
+ url=endpoint_info_data["url"],
37
+ verify=False,
38
+ **request_params
39
+ )
40
+
41
+ if resp.status_code < 200 or resp.status_code >= 300:
42
+ error_content = resp.content.decode("utf-8", errors="ignore")
43
+ raise PolyApiException(f"{resp.status_code}: {error_content}")
44
+
45
+ return resp
7
46
 
8
47
  def execute(function_type, function_id, data) -> Response:
9
48
  """ execute a specific function id/type
10
49
  """
11
50
  api_key, api_url = get_api_key_and_url()
12
51
  headers = {"Authorization": f"Bearer {api_key}"}
52
+
13
53
  url = f"{api_url}/functions/{function_type}/{function_id}/execute"
14
- resp = requests.post(url, json=data, headers=headers)
15
- # print(resp.status_code)
16
- # print(resp.headers["content-type"])
54
+
55
+ # Make the request
56
+ resp = requests.post(
57
+ url,
58
+ json=data,
59
+ headers=headers,
60
+ )
61
+
17
62
  if resp.status_code < 200 or resp.status_code >= 300:
18
63
  error_content = resp.content.decode("utf-8", errors="ignore")
19
64
  raise PolyApiException(f"{resp.status_code}: {error_content}")
65
+
20
66
  return resp
21
67
 
22
68
 
@@ -86,13 +86,12 @@ def function_add_or_update(
86
86
 
87
87
  headers = get_auth_headers(api_key)
88
88
  resp = requests.post(url, headers=headers, json=data)
89
- if resp.status_code == 201:
89
+ if resp.status_code in [200, 201]:
90
90
  print_green("DEPLOYED")
91
91
  function_id = resp.json()["id"]
92
92
  print(f"Function ID: {function_id}")
93
93
  if generate:
94
- contexts=generate_contexts.split(",") if generate_contexts else None
95
- generate_library(contexts=contexts)
94
+ generate_library()
96
95
  else:
97
96
  print("Error adding function.")
98
97
  print(resp.status_code)
@@ -14,7 +14,7 @@ from .api import render_api_function
14
14
  from .server import render_server_function
15
15
  from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
16
16
  from .variables import generate_variables
17
- from .config import get_api_key_and_url
17
+ from .config import get_api_key_and_url, get_direct_execute_config
18
18
 
19
19
  SUPPORTED_FUNCTION_TYPES = {
20
20
  "apiFunction",
@@ -46,6 +46,10 @@ def get_specs(contexts=Optional[List[str]], no_types: bool = False) -> List:
46
46
  if contexts:
47
47
  params["contexts"] = contexts
48
48
 
49
+ # Add apiFunctionDirectExecute parameter if direct execute is enabled
50
+ if get_direct_execute_config():
51
+ params["apiFunctionDirectExecute"] = "true"
52
+
49
53
  resp = requests.get(url, headers=headers, params=params)
50
54
  if resp.status_code == 200:
51
55
  return resp.json()
@@ -125,24 +129,26 @@ def parse_function_specs(
125
129
  ) -> List[SpecificationDto]:
126
130
  functions = []
127
131
  for spec in specs:
128
- if not spec or "function" not in spec:
129
- continue
130
-
131
- if not spec["function"]:
132
- continue
133
-
134
- if limit_ids and spec["id"] not in limit_ids:
132
+ if not spec:
135
133
  continue
136
134
 
135
+ # For no_types mode, we might not have function data, but we still want to include the spec
136
+ # if it's a supported function type
137
137
  if spec["type"] not in SUPPORTED_FUNCTION_TYPES:
138
138
  continue
139
139
 
140
- if spec["type"] == "customFunction" and spec["language"] != "python":
141
- # poly libraries only support client functions of same language
140
+ # Skip if we have a limit and this spec is not in it
141
+ if limit_ids and spec.get("id") not in limit_ids:
142
142
  continue
143
143
 
144
+ # For customFunction, check language if we have function data
145
+ if spec["type"] == "customFunction":
146
+ if spec.get("language") and spec["language"] != "python":
147
+ # poly libraries only support client functions of same language
148
+ continue
149
+
144
150
  # Functions with serverSideAsync True will always return a Dict with execution ID
145
- if spec.get('serverSideAsync'):
151
+ if spec.get('serverSideAsync') and spec.get("function"):
146
152
  spec['function']['returnType'] = {'kind': 'plain', 'value': 'object'}
147
153
 
148
154
  functions.append(spec)
@@ -201,6 +207,63 @@ def remove_old_library():
201
207
  shutil.rmtree(path)
202
208
 
203
209
 
210
+ def create_empty_schemas_module():
211
+ """Create an empty schemas module for no-types mode so user code can still import from polyapi.schemas"""
212
+ currdir = os.path.dirname(os.path.abspath(__file__))
213
+ schemas_path = os.path.join(currdir, "schemas")
214
+
215
+ # Create the schemas directory
216
+ if not os.path.exists(schemas_path):
217
+ os.makedirs(schemas_path)
218
+
219
+ # Create an __init__.py file with dynamic schema resolution
220
+ init_path = os.path.join(schemas_path, "__init__.py")
221
+ with open(init_path, "w") as f:
222
+ f.write('''"""Empty schemas module for no-types mode"""
223
+ from typing import Any, Dict
224
+
225
+ class _GenericSchema(Dict[str, Any]):
226
+ """Generic schema type that acts like a Dict for no-types mode"""
227
+ def __init__(self, *args, **kwargs):
228
+ super().__init__(*args, **kwargs)
229
+
230
+ class _SchemaModule:
231
+ """Dynamic module that returns itself for attribute access, allowing infinite nesting"""
232
+
233
+ def __getattr__(self, name: str):
234
+ # For callable access (like schemas.Response()), return the generic schema class
235
+ # For further attribute access (like schemas.random.random2), return self to allow nesting
236
+ return _NestedSchemaAccess()
237
+
238
+ def __call__(self, *args, **kwargs):
239
+ # If someone tries to call the module itself, return a generic schema
240
+ return _GenericSchema(*args, **kwargs)
241
+
242
+ def __dir__(self):
243
+ # Return common schema names for introspection
244
+ return ['Response', 'Request', 'Error', 'Data', 'Result']
245
+
246
+ class _NestedSchemaAccess:
247
+ """Handles nested attribute access and final callable resolution"""
248
+
249
+ def __getattr__(self, name: str):
250
+ # Continue allowing nested access
251
+ return _NestedSchemaAccess()
252
+
253
+ def __call__(self, *args, **kwargs):
254
+ # When finally called, return a generic schema instance
255
+ return _GenericSchema(*args, **kwargs)
256
+
257
+ def __class_getitem__(cls, item):
258
+ # Support type annotations like schemas.Response[str]
259
+ return _GenericSchema
260
+
261
+ # Replace this module with our dynamic module
262
+ import sys
263
+ sys.modules[__name__] = _SchemaModule()
264
+ ''')
265
+
266
+
204
267
  def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> None:
205
268
  generate_msg = f"Generating Poly Python SDK for contexts ${contexts}..." if contexts else "Generating Poly Python SDK..."
206
269
  print(generate_msg, end="", flush=True)
@@ -212,14 +275,23 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
212
275
  limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
213
276
  functions = parse_function_specs(specs, limit_ids=limit_ids)
214
277
 
215
- schemas = get_schemas()
216
- schema_index = build_schema_index(schemas)
217
- if schemas:
218
- schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
219
- schemas = replace_poly_refs_in_schemas(schemas, schema_index)
220
- generate_schemas(schemas, limit_ids=schema_limit_ids)
221
-
222
- functions = replace_poly_refs_in_functions(functions, schema_index)
278
+ # Only process schemas if no_types is False
279
+ if not no_types:
280
+ schemas = get_schemas()
281
+ schema_index = build_schema_index(schemas)
282
+ if schemas:
283
+ schema_limit_ids: List[str] = [] # useful for narrowing down generation to a single function to debug
284
+ schemas = replace_poly_refs_in_schemas(schemas, schema_index)
285
+ generate_schemas(schemas, limit_ids=schema_limit_ids)
286
+
287
+ functions = replace_poly_refs_in_functions(functions, schema_index)
288
+ else:
289
+ # When no_types is True, we still need to process functions but without schema resolution
290
+ # Use an empty schema index to avoid poly-ref resolution
291
+ schema_index = {}
292
+
293
+ # Create an empty schemas module so user code can still import from polyapi.schemas
294
+ create_empty_schemas_module()
223
295
 
224
296
  if functions:
225
297
  generate_functions(functions)
@@ -229,10 +301,11 @@ def generate(contexts: Optional[List[str]] = None, no_types: bool = False) -> No
229
301
  )
230
302
  exit()
231
303
 
232
- variables = get_variables()
233
- if variables:
234
- generate_variables(variables)
235
-
304
+ # Only process variables if no_types is False
305
+ if not no_types:
306
+ variables = get_variables()
307
+ if variables:
308
+ generate_variables(variables)
236
309
 
237
310
  # indicator to vscode extension that this is a polyapi-python project
238
311
  file_path = os.path.join(os.getcwd(), ".polyapi-python")
@@ -262,11 +335,19 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
262
335
 
263
336
  arguments: List[PropertySpecification] = []
264
337
  return_type = {}
265
- if spec["function"]:
266
- arguments = [
267
- arg for arg in spec["function"]["arguments"]
268
- ]
269
- return_type = spec["function"]["returnType"]
338
+ if spec.get("function"):
339
+ # Handle cases where arguments might be missing or None
340
+ if spec["function"].get("arguments"):
341
+ arguments = [
342
+ arg for arg in spec["function"]["arguments"]
343
+ ]
344
+
345
+ # Handle cases where returnType might be missing or None
346
+ if spec["function"].get("returnType"):
347
+ return_type = spec["function"]["returnType"]
348
+ else:
349
+ # Provide a fallback return type when missing
350
+ return_type = {"kind": "any"}
270
351
 
271
352
  if function_type == "apiFunction":
272
353
  func_str, func_type_defs = render_api_function(
@@ -280,7 +361,7 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
280
361
  elif function_type == "customFunction":
281
362
  func_str, func_type_defs = render_client_function(
282
363
  function_name,
283
- spec["code"],
364
+ spec.get("code", ""),
284
365
  arguments,
285
366
  return_type,
286
367
  )
@@ -126,4 +126,8 @@ def clean_title(title: str) -> str:
126
126
 
127
127
  def map_primitive_types(type_: str) -> str:
128
128
  # Define your mapping logic here
129
- return JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(type_, "Any")
129
+ return JSONSCHEMA_TO_PYTHON_TYPE_MAP.get(type_, "Any")
130
+
131
+
132
+ def is_primitive(type_: str) -> bool:
133
+ return type_ in JSONSCHEMA_TO_PYTHON_TYPE_MAP
@@ -11,12 +11,13 @@ from polyapi.schema import (
11
11
  wrapped_generate_schema_types,
12
12
  clean_title,
13
13
  map_primitive_types,
14
+ is_primitive
14
15
  )
15
16
 
16
17
 
17
18
  # this string should be in every __init__ file.
18
19
  # it contains all the imports needed for the function or variable code to run
19
- 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"
20
+ 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, get_direct_execute_config\nfrom polyapi.execute import execute, execute_post, variable_get, variable_update, direct_execute\n\n"
20
21
 
21
22
 
22
23
  def init_the_init(full_path: str, code_imports="") -> None:
@@ -97,20 +98,32 @@ def get_type_and_def(
97
98
  ) -> Tuple[str, str]:
98
99
  """ returns type and type definition for a given PropertyType
99
100
  """
101
+ # Handle cases where type_spec might be None or empty
102
+ if not type_spec:
103
+ return "Any", ""
104
+
105
+ # Handle cases where kind might be missing
106
+ if "kind" not in type_spec:
107
+ return "Any", ""
108
+
100
109
  if type_spec["kind"] == "plain":
101
- value = type_spec["value"]
110
+ value = type_spec.get("value", "")
102
111
  if value.endswith("[]"):
103
112
  primitive = map_primitive_types(value[:-2])
104
113
  return f"List[{primitive}]", ""
105
114
  else:
106
115
  return map_primitive_types(value), ""
107
116
  elif type_spec["kind"] == "primitive":
108
- return map_primitive_types(type_spec["type"]), ""
117
+ return map_primitive_types(type_spec.get("type", "any")), ""
109
118
  elif type_spec["kind"] == "array":
110
119
  if type_spec.get("items"):
111
120
  items = type_spec["items"]
112
121
  if items.get("$ref"):
113
- return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
122
+ # For no-types mode, avoid complex schema generation
123
+ try:
124
+ return wrapped_generate_schema_types(type_spec, "ResponseType", "Dict") # type: ignore
125
+ except:
126
+ return "List[Dict]", ""
114
127
  else:
115
128
  item_type, _ = get_type_and_def(items)
116
129
  title = f"List[{item_type}]"
@@ -128,15 +141,28 @@ def get_type_and_def(
128
141
  # TODO fix me
129
142
  # we don't use ReturnType as name for the list type here, we use _ReturnTypeItem
130
143
  return "List", ""
144
+ elif title and title == "ReturnType" and schema.get("type"):
145
+ assert isinstance(title, str)
146
+ schema_type = schema.get("type", "Any")
147
+ root_type, generated_code = wrapped_generate_schema_types(schema, schema_type, "Dict") # type: ignore
148
+ return (map_primitive_types(root_type), "") if is_primitive(root_type) else (root_type, generated_code) # type: ignore
131
149
  elif title:
132
150
  assert isinstance(title, str)
133
- return wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
151
+ # For no-types mode, avoid complex schema generation
152
+ try:
153
+ root_type, generated_code = wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
154
+ return ("Any", "") if root_type == "ReturnType" else wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
155
+ except:
156
+ return "Dict", ""
134
157
  elif schema.get("allOf") and len(schema["allOf"]):
135
158
  # we are in a case of a single allOf, lets strip off the allOf and move on!
136
159
  # our library doesn't handle allOf well yet
137
160
  allOf = schema["allOf"][0]
138
161
  title = allOf.get("title", allOf.get("name", title_fallback))
139
- return wrapped_generate_schema_types(allOf, title, "Dict")
162
+ try:
163
+ return wrapped_generate_schema_types(allOf, title, "Dict")
164
+ except:
165
+ return "Dict", ""
140
166
  elif schema.get("items"):
141
167
  # fallback to schema $ref name if no explicit title
142
168
  items = schema.get("items") # type: ignore
@@ -150,9 +176,15 @@ def get_type_and_def(
150
176
  return "List", ""
151
177
 
152
178
  title = f"List[{title}]"
153
- return wrapped_generate_schema_types(schema, title, "List")
179
+ try:
180
+ return wrapped_generate_schema_types(schema, title, "List")
181
+ except:
182
+ return "List[Dict]", ""
183
+ elif schema.get("properties"):
184
+ result = wrapped_generate_schema_types(schema, "ResponseType", "Dict") # type: ignore
185
+ return result
154
186
  else:
155
- return "Any", ""
187
+ return "Dict", ""
156
188
  else:
157
189
  return "Dict", ""
158
190
  elif type_spec["kind"] == "function":
@@ -187,12 +219,22 @@ def get_type_and_def(
187
219
 
188
220
 
189
221
  def _maybe_add_fallback_schema_name(a: PropertySpecification):
190
- if a["type"]["kind"] == "object" and a["type"].get("schema"):
222
+ # Handle cases where type might be missing
223
+ if not a.get("type"):
224
+ return
225
+
226
+ if a["type"].get("kind") == "object" and a["type"].get("schema"):
191
227
  schema = a["type"].get("schema", {})
192
- if not schema.get("title") and not schema.get("name") and a["name"]:
228
+ if not schema.get("title") and not schema.get("name") and a.get("name"):
193
229
  schema["title"] = a["name"].title()
194
230
 
195
231
 
232
+ def _clean_description(text: str) -> str:
233
+ """Flatten new-lines and collapse excess whitespace."""
234
+ text = text.replace("\\n", " ").replace("\n", " ")
235
+ return re.sub(r"\s+", " ", text).strip()
236
+
237
+
196
238
  def parse_arguments(
197
239
  function_name: str, arguments: List[PropertySpecification]
198
240
  ) -> Tuple[str, str]:
@@ -200,18 +242,27 @@ def parse_arguments(
200
242
  arg_string = ""
201
243
  for idx, a in enumerate(arguments):
202
244
  _maybe_add_fallback_schema_name(a)
203
- arg_type, arg_def = get_type_and_def(a["type"])
245
+
246
+ # Handle cases where type might be missing
247
+ arg_type_spec = a.get("type", {"kind": "any"})
248
+ arg_type, arg_def = get_type_and_def(arg_type_spec)
204
249
  if arg_def:
205
250
  args_def.append(arg_def)
206
- a["name"] = rewrite_arg_name(a["name"])
251
+
252
+ # Handle cases where name might be missing
253
+ arg_name = a.get("name", f"arg{idx}")
254
+ a["name"] = rewrite_arg_name(arg_name)
255
+
207
256
  arg_string += (
208
257
  f" {a['name']}: {add_type_import_path(function_name, arg_type)}"
209
258
  )
210
- if not a["required"]:
259
+
260
+ # Handle cases where required might be missing
261
+ if not a.get("required", True):
211
262
  arg_string += " = None"
212
263
 
213
- description = a.get("description", "")
214
- description = description.replace("\n", " ")
264
+ description = _clean_description(a.get("description", ""))
265
+
215
266
  if description:
216
267
  if idx == len(arguments) - 1:
217
268
  arg_string += f" # {description}\n"
@@ -65,6 +65,7 @@ async def {function_name}(
65
65
  "data": resp,
66
66
  "statusCode": polyCustom.get("responseStatusCode", 200),
67
67
  "contentType": polyCustom.get("responseContentType", None),
68
+ "headers": polyCustom.get("responseHeaders", {{}}),
68
69
  }},
69
70
  }}, namespace="/events")
70
71
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.6.dev0
3
+ Version: 0.3.7
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
@@ -3,7 +3,7 @@ requires = ["setuptools>=61.2", "wheel"]
3
3
 
4
4
  [project]
5
5
  name = "polyapi-python"
6
- version = "0.3.6.dev0"
6
+ version = "0.3.7"
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 = [
@@ -0,0 +1,289 @@
1
+ import unittest
2
+ import os
3
+ import shutil
4
+ import importlib.util
5
+ from polyapi.utils import get_type_and_def, rewrite_reserved
6
+ from polyapi.generate import render_spec, create_empty_schemas_module
7
+
8
+ OPENAPI_FUNCTION = {
9
+ "kind": "function",
10
+ "spec": {
11
+ "arguments": [
12
+ {
13
+ "name": "event",
14
+ "required": False,
15
+ "type": {
16
+ "kind": "object",
17
+ "schema": {
18
+ "$schema": "http://json-schema.org/draft-06/schema#",
19
+ "type": "array",
20
+ "items": {"$ref": "#/definitions/WebhookEventTypeElement"},
21
+ "definitions": {
22
+ "WebhookEventTypeElement": {
23
+ "type": "object",
24
+ "additionalProperties": False,
25
+ "properties": {
26
+ "title": {"type": "string"},
27
+ "manufacturerName": {"type": "string"},
28
+ "carType": {"type": "string"},
29
+ "id": {"type": "integer"},
30
+ },
31
+ "required": [
32
+ "carType",
33
+ "id",
34
+ "manufacturerName",
35
+ "title",
36
+ ],
37
+ "title": "WebhookEventTypeElement",
38
+ }
39
+ },
40
+ },
41
+ },
42
+ },
43
+ {
44
+ "name": "headers",
45
+ "required": False,
46
+ "type": {"kind": "object", "typeName": "Record<string, any>"},
47
+ },
48
+ {
49
+ "name": "params",
50
+ "required": False,
51
+ "type": {"kind": "object", "typeName": "Record<string, any>"},
52
+ },
53
+ {
54
+ "name": "polyCustom",
55
+ "required": False,
56
+ "type": {
57
+ "kind": "object",
58
+ "properties": [
59
+ {
60
+ "name": "responseStatusCode",
61
+ "type": {"type": "number", "kind": "primitive"},
62
+ "required": True,
63
+ },
64
+ {
65
+ "name": "responseContentType",
66
+ "type": {"type": "string", "kind": "primitive"},
67
+ "required": True,
68
+ "nullable": True,
69
+ },
70
+ ],
71
+ },
72
+ },
73
+ ],
74
+ "returnType": {"kind": "void"},
75
+ "synchronous": True,
76
+ },
77
+ }
78
+
79
+ # Test spec with missing function data (simulating no_types=true)
80
+ NO_TYPES_SPEC = {
81
+ "id": "test-id-123",
82
+ "type": "serverFunction",
83
+ "context": "test",
84
+ "name": "testFunction",
85
+ "description": "A test function for no-types mode",
86
+ # Note: no "function" field, simulating no_types=true response
87
+ }
88
+
89
+ # Test spec with minimal function data
90
+ MINIMAL_FUNCTION_SPEC = {
91
+ "id": "test-id-456",
92
+ "type": "apiFunction",
93
+ "context": "test",
94
+ "name": "minimalFunction",
95
+ "description": "A minimal function spec",
96
+ "function": {
97
+ # Note: no "arguments" or "returnType" fields
98
+ }
99
+ }
100
+
101
+
102
+ class T(unittest.TestCase):
103
+ def test_get_type_and_def(self):
104
+ arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
105
+ self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
106
+
107
+ def test_rewrite_reserved(self):
108
+ rv = rewrite_reserved("from")
109
+ self.assertEqual(rv, "_from")
110
+
111
+ def test_render_spec_no_function_data(self):
112
+ """Test that render_spec handles specs with no function data gracefully"""
113
+ func_str, func_type_defs = render_spec(NO_TYPES_SPEC)
114
+
115
+ # Should generate a function even without function data
116
+ self.assertIsNotNone(func_str)
117
+ self.assertIsNotNone(func_type_defs)
118
+ self.assertIn("testFunction", func_str)
119
+ self.assertIn("test-id-123", func_str)
120
+
121
+ def test_render_spec_minimal_function_data(self):
122
+ """Test that render_spec handles specs with minimal function data"""
123
+ func_str, func_type_defs = render_spec(MINIMAL_FUNCTION_SPEC)
124
+
125
+ # Should generate a function with fallback types
126
+ self.assertIsNotNone(func_str)
127
+ self.assertIsNotNone(func_type_defs)
128
+ self.assertIn("minimalFunction", func_str)
129
+ self.assertIn("test-id-456", func_str)
130
+ # Should use Any as fallback return type in the type definitions
131
+ self.assertIn("Any", func_type_defs)
132
+
133
+ def test_create_empty_schemas_module(self):
134
+ """Test that create_empty_schemas_module creates the necessary files"""
135
+ # Clean up any existing schemas directory
136
+ schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
137
+ if os.path.exists(schemas_path):
138
+ shutil.rmtree(schemas_path)
139
+
140
+ # Create empty schemas module
141
+ create_empty_schemas_module()
142
+
143
+ # Verify the directory and __init__.py file were created
144
+ self.assertTrue(os.path.exists(schemas_path))
145
+ init_path = os.path.join(schemas_path, "__init__.py")
146
+ self.assertTrue(os.path.exists(init_path))
147
+
148
+ # Verify the content of __init__.py includes dynamic schema handling
149
+ with open(init_path, "r") as f:
150
+ content = f.read()
151
+ self.assertIn("Empty schemas module for no-types mode", content)
152
+ self.assertIn("_GenericSchema", content)
153
+ self.assertIn("_SchemaModule", content)
154
+ self.assertIn("__getattr__", content)
155
+
156
+ # Clean up
157
+ shutil.rmtree(schemas_path)
158
+
159
+ def test_no_types_workflow(self):
160
+ """Test the complete no-types workflow including schema imports and function parsing"""
161
+ import tempfile
162
+ import sys
163
+ from unittest.mock import patch
164
+
165
+ # Clean up any existing schemas directory
166
+ schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
167
+ if os.path.exists(schemas_path):
168
+ shutil.rmtree(schemas_path)
169
+
170
+ # Mock get_specs to return empty list (simulating no functions)
171
+ with patch('polyapi.generate.get_specs', return_value=[]):
172
+ try:
173
+ # This should exit with SystemExit due to no functions
174
+ from polyapi.generate import generate
175
+ generate(no_types=True)
176
+ except SystemExit:
177
+ pass # Expected when no functions exist
178
+
179
+ # Verify schemas module was created
180
+ self.assertTrue(os.path.exists(schemas_path))
181
+ init_path = os.path.join(schemas_path, "__init__.py")
182
+ self.assertTrue(os.path.exists(init_path))
183
+
184
+ # Test that we can import schemas and use arbitrary schema names
185
+ from polyapi import schemas
186
+
187
+ # Test various schema access
188
+ Response = schemas.Response
189
+ CustomType = schemas.CustomType
190
+ AnyName = schemas.SomeArbitrarySchemaName
191
+
192
+ # All should return the same generic schema class type
193
+ self.assertEqual(type(Response).__name__, '_NestedSchemaAccess')
194
+ self.assertEqual(type(CustomType).__name__, '_NestedSchemaAccess')
195
+ self.assertEqual(type(AnyName).__name__, '_NestedSchemaAccess')
196
+
197
+ # Test creating instances
198
+ response_instance = Response()
199
+ custom_instance = CustomType()
200
+
201
+ self.assertIsInstance(response_instance, dict)
202
+ self.assertIsInstance(custom_instance, dict)
203
+
204
+ # Test that function code with schema references can be parsed
205
+ test_code = '''
206
+ from polyapi import polyCustom, schemas
207
+
208
+ def test_function() -> schemas.Response:
209
+ polyCustom["executionId"] = "123"
210
+ return polyCustom
211
+ '''
212
+
213
+ # Create a temporary file with the test code
214
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
215
+ f.write(test_code)
216
+ temp_file = f.name
217
+
218
+ try:
219
+ # Test that the parser can handle this code
220
+ from polyapi.parser import parse_function_code
221
+ result = parse_function_code(test_code, 'test_function', 'test_context')
222
+
223
+ self.assertEqual(result['name'], 'test_function')
224
+ self.assertEqual(result['context'], 'test_context')
225
+ # Return type should be Any since we're in no-types mode
226
+ self.assertEqual(result['types']['returns']['type'], 'Any')
227
+
228
+ finally:
229
+ # Clean up temp file
230
+ os.unlink(temp_file)
231
+
232
+ # Clean up schemas directory
233
+ shutil.rmtree(schemas_path)
234
+
235
+ def test_nested_schema_access(self):
236
+ """Test that nested schema access like schemas.random.random2.random3 works"""
237
+ # Clean up any existing schemas directory
238
+ schemas_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "polyapi", "schemas")
239
+ if os.path.exists(schemas_path):
240
+ shutil.rmtree(schemas_path)
241
+
242
+ # Create empty schemas module
243
+ create_empty_schemas_module()
244
+
245
+ # Test that we can import and use nested schemas
246
+ from polyapi import schemas
247
+
248
+ # Test various levels of nesting
249
+ simple = schemas.Response
250
+ nested = schemas.random.random2
251
+ deep_nested = schemas.api.v1.user.profile.data
252
+ very_deep = schemas.some.very.deep.nested.schema.access
253
+
254
+ # All should be _NestedSchemaAccess instances
255
+ self.assertEqual(type(simple).__name__, '_NestedSchemaAccess')
256
+ self.assertEqual(type(nested).__name__, '_NestedSchemaAccess')
257
+ self.assertEqual(type(deep_nested).__name__, '_NestedSchemaAccess')
258
+ self.assertEqual(type(very_deep).__name__, '_NestedSchemaAccess')
259
+
260
+ # Test that they can be called and return generic schemas
261
+ simple_instance = simple()
262
+ nested_instance = nested()
263
+ deep_instance = deep_nested()
264
+ very_deep_instance = very_deep()
265
+
266
+ # All should be dictionaries
267
+ self.assertIsInstance(simple_instance, dict)
268
+ self.assertIsInstance(nested_instance, dict)
269
+ self.assertIsInstance(deep_instance, dict)
270
+ self.assertIsInstance(very_deep_instance, dict)
271
+
272
+ # Test that function code with nested schemas can be parsed
273
+ test_code = '''
274
+ from polyapi import polyCustom, schemas
275
+
276
+ def test_nested_function() -> schemas.api.v1.user.profile:
277
+ return schemas.api.v1.user.profile()
278
+ '''
279
+
280
+ from polyapi.parser import parse_function_code
281
+ result = parse_function_code(test_code, 'test_nested_function', 'test_context')
282
+
283
+ self.assertEqual(result['name'], 'test_nested_function')
284
+ self.assertEqual(result['context'], 'test_context')
285
+ # Return type should be Any since we're in no-types mode
286
+ self.assertEqual(result['types']['returns']['type'], 'Any')
287
+
288
+ # Clean up schemas directory
289
+ shutil.rmtree(schemas_path)
@@ -1,23 +0,0 @@
1
- import os
2
- import sys
3
- import truststore
4
- from typing import Dict, Any
5
- truststore.inject_into_ssl()
6
- from .cli import CLI_COMMANDS
7
-
8
- __all__ = ["poly"]
9
-
10
-
11
- if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
12
- currdir = os.path.dirname(os.path.abspath(__file__))
13
- if not os.path.isdir(os.path.join(currdir, "poly")):
14
- print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
15
- sys.exit(1)
16
-
17
-
18
- polyCustom: Dict[str, Any] = {
19
- "executionId": None,
20
- "executionApiKey": None,
21
- "responseStatusCode": 200,
22
- "responseContentType": None,
23
- }
@@ -1,83 +0,0 @@
1
- import unittest
2
- from polyapi.utils import get_type_and_def, rewrite_reserved
3
-
4
- OPENAPI_FUNCTION = {
5
- "kind": "function",
6
- "spec": {
7
- "arguments": [
8
- {
9
- "name": "event",
10
- "required": False,
11
- "type": {
12
- "kind": "object",
13
- "schema": {
14
- "$schema": "http://json-schema.org/draft-06/schema#",
15
- "type": "array",
16
- "items": {"$ref": "#/definitions/WebhookEventTypeElement"},
17
- "definitions": {
18
- "WebhookEventTypeElement": {
19
- "type": "object",
20
- "additionalProperties": False,
21
- "properties": {
22
- "title": {"type": "string"},
23
- "manufacturerName": {"type": "string"},
24
- "carType": {"type": "string"},
25
- "id": {"type": "integer"},
26
- },
27
- "required": [
28
- "carType",
29
- "id",
30
- "manufacturerName",
31
- "title",
32
- ],
33
- "title": "WebhookEventTypeElement",
34
- }
35
- },
36
- },
37
- },
38
- },
39
- {
40
- "name": "headers",
41
- "required": False,
42
- "type": {"kind": "object", "typeName": "Record<string, any>"},
43
- },
44
- {
45
- "name": "params",
46
- "required": False,
47
- "type": {"kind": "object", "typeName": "Record<string, any>"},
48
- },
49
- {
50
- "name": "polyCustom",
51
- "required": False,
52
- "type": {
53
- "kind": "object",
54
- "properties": [
55
- {
56
- "name": "responseStatusCode",
57
- "type": {"type": "number", "kind": "primitive"},
58
- "required": True,
59
- },
60
- {
61
- "name": "responseContentType",
62
- "type": {"type": "string", "kind": "primitive"},
63
- "required": True,
64
- "nullable": True,
65
- },
66
- ],
67
- },
68
- },
69
- ],
70
- "returnType": {"kind": "void"},
71
- "synchronous": True,
72
- },
73
- }
74
-
75
-
76
- class T(unittest.TestCase):
77
- def test_get_type_and_def(self):
78
- arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
79
- self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
80
-
81
- def test_rewrite_reserved(self):
82
- rv = rewrite_reserved("from")
83
- self.assertEqual(rv, "_from")