polyapi-python 0.3.17.dev1__tar.gz → 0.3.18__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 (55) hide show
  1. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/PKG-INFO +8 -8
  2. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/cli.py +14 -2
  3. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/generate.py +30 -5
  4. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/poly_schemas.py +42 -3
  5. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/poly_tables.py +19 -2
  6. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/schema.py +16 -0
  7. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/server.py +17 -3
  8. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/utils.py +58 -4
  9. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi_python.egg-info/PKG-INFO +8 -8
  10. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi_python.egg-info/SOURCES.txt +2 -0
  11. polyapi_python-0.3.18/polyapi_python.egg-info/requires.txt +9 -0
  12. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/pyproject.toml +9 -9
  13. polyapi_python-0.3.18/tests/test_dependency_pins.py +33 -0
  14. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_poly_custom.py +18 -1
  15. polyapi_python-0.3.18/tests/test_poly_schemas.py +124 -0
  16. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_server.py +92 -0
  17. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_utils.py +24 -1
  18. polyapi_python-0.3.17.dev1/polyapi_python.egg-info/requires.txt +0 -9
  19. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/LICENSE +0 -0
  20. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/README.md +0 -0
  21. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/__init__.py +0 -0
  22. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/__main__.py +0 -0
  23. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/api.py +0 -0
  24. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/auth.py +0 -0
  25. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/cli_constants.py +0 -0
  26. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/client.py +0 -0
  27. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/config.py +0 -0
  28. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/constants.py +0 -0
  29. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/deployables.py +0 -0
  30. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/error_handler.py +0 -0
  31. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/exceptions.py +0 -0
  32. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/execute.py +0 -0
  33. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/function_cli.py +0 -0
  34. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/http_client.py +0 -0
  35. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/parser.py +0 -0
  36. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/prepare.py +0 -0
  37. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/py.typed +0 -0
  38. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/rendered_spec.py +0 -0
  39. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/sync.py +0 -0
  40. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/typedefs.py +0 -0
  41. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/variables.py +0 -0
  42. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi/webhook.py +0 -0
  43. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi_python.egg-info/dependency_links.txt +0 -0
  44. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/polyapi_python.egg-info/top_level.txt +0 -0
  45. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/setup.cfg +0 -0
  46. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_api.py +0 -0
  47. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_async_proof.py +0 -0
  48. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_auth.py +0 -0
  49. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_deployables.py +0 -0
  50. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_generate.py +0 -0
  51. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_parser.py +0 -0
  52. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_rendered_spec.py +0 -0
  53. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_schema.py +0 -0
  54. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_tabi.py +0 -0
  55. {polyapi_python-0.3.17.dev1 → polyapi_python-0.3.18}/tests/test_variables.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.17.dev1
3
+ Version: 0.3.18
4
4
  Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
5
- Author-email: Dan Fellin <dan@polyapi.io>
5
+ Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
7
7
 
8
8
  Copyright (c) 2025 PolyAPI Inc.
@@ -28,15 +28,15 @@ Project-URL: Homepage, https://github.com/polyapi/polyapi-python
28
28
  Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
- Requires-Dist: requests>=2.32.3
32
- Requires-Dist: typing_extensions>=4.12.2
31
+ Requires-Dist: requests==2.32.3
32
+ Requires-Dist: typing_extensions==4.12.2
33
33
  Requires-Dist: jsonschema-gentypes==2.10.0
34
- Requires-Dist: pydantic<3.0.0,>=2.8.0
35
- Requires-Dist: stdlib_list<1.0.0,>=0.10.0
34
+ Requires-Dist: pydantic==2.8.0
35
+ Requires-Dist: stdlib_list==0.11.1
36
36
  Requires-Dist: colorama==0.4.4
37
37
  Requires-Dist: python-socketio[asyncio_client]==5.11.1
38
- Requires-Dist: truststore>=0.8.0
39
- Requires-Dist: httpx>=0.28.1
38
+ Requires-Dist: truststore==0.9.1
39
+ Requires-Dist: httpx==0.28.1
40
40
  Dynamic: license-file
41
41
 
42
42
  # PolyAPI Python Library
@@ -1,4 +1,6 @@
1
1
  import os
2
+ import sys
3
+ import tempfile
2
4
  import argparse
3
5
 
4
6
  from polyapi.utils import print_green, print_red
@@ -23,6 +25,16 @@ def _get_version_string():
23
25
 
24
26
 
25
27
  def execute_from_cli():
28
+ # Redirect __pycache__ out of generated dirs to avoid window file-locking
29
+ # failures when rmtree dels on generate.
30
+ if 'PYTHONPYCACHEPREFIX' not in os.environ:
31
+ cache_dir = os.path.join(tempfile.gettempdir(), 'polyapi_pycache')
32
+ os.makedirs(cache_dir, exist_ok=True)
33
+ # here for children just in case
34
+ os.environ['PYTHONPYCACHEPREFIX'] = cache_dir
35
+ # For the startup py_initialize issue
36
+ sys.pycache_prefix = cache_dir
37
+
26
38
  # First we setup all our argument parsing logic
27
39
  # Then we parse the arguments (waaay at the bottom)
28
40
  parser = argparse.ArgumentParser(
@@ -114,7 +126,7 @@ def execute_from_cli():
114
126
  fn_add_parser.add_argument("--client", action="store_true", help="Marks the function as a client function")
115
127
  fn_add_parser.add_argument("--logs", choices=["enabled", "disabled"], default=None, help="Enable or disable logs for the function.")
116
128
  fn_add_parser.add_argument("--execution-api-key", required=False, default="", help="API key for execution (for server functions only).")
117
- fn_add_parser.add_argument("--disable-ai", "--skip-generate", action="store_true", help="Pass --disable-ai skip AI generation of missing descriptions")
129
+ fn_add_parser.add_argument("--skip-generate", action="store_true", help="Skip running generate after function add command, especially useful if you are deploying a bunch of functions at once. Run generate manually at the end!")
118
130
  fn_add_parser.add_argument("--generate-contexts", type=str, help="Server function only – only include certain contexts to speed up function execution")
119
131
  fn_add_parser.add_argument("--visibility", type=str, default="environment", help="Specifies the visibility of a function. Options: PUBLIC, TENANT, ENVIRONMENT. Case insensitive")
120
132
 
@@ -147,7 +159,7 @@ def execute_from_cli():
147
159
  client=args.client,
148
160
  server=args.server,
149
161
  logs_enabled=logs_enabled,
150
- generate=not args.disable_ai,
162
+ generate=not args.skip_generate,
151
163
  execution_api_key=args.execution_api_key,
152
164
  generate_contexts=args.generate_contexts,
153
165
  visibility=visibility
@@ -4,6 +4,7 @@ import uuid
4
4
  import shutil
5
5
  import logging
6
6
  import tempfile
7
+ import stat
7
8
 
8
9
  from copy import deepcopy
9
10
  from typing import Any, List, Optional, Tuple, cast
@@ -209,23 +210,33 @@ def get_tables(specs: List[SpecificationDto]) -> List[TableSpecDto]:
209
210
  return [cast(TableSpecDto, spec) for spec in specs if spec["type"] == "table"]
210
211
 
211
212
 
213
+ def _rmtree_readonly_handler(func, path, exc):
214
+ # Windows marks __pycache__ .pyc files read-only; clear the bit and retry.
215
+ os.chmod(path, stat.S_IWRITE)
216
+ func(path)
217
+
218
+
219
+ def _rmtree(path):
220
+ shutil.rmtree(path, onerror=_rmtree_readonly_handler)
221
+
222
+
212
223
  def remove_old_library():
213
224
  currdir = os.path.dirname(os.path.abspath(__file__))
214
225
  path = os.path.join(currdir, "poly")
215
226
  if os.path.exists(path):
216
- shutil.rmtree(path)
227
+ _rmtree(path)
217
228
 
218
229
  path = os.path.join(currdir, "vari")
219
230
  if os.path.exists(path):
220
- shutil.rmtree(path)
231
+ _rmtree(path)
221
232
 
222
233
  path = os.path.join(currdir, "schemas")
223
234
  if os.path.exists(path):
224
- shutil.rmtree(path)
235
+ _rmtree(path)
225
236
 
226
237
  path = os.path.join(currdir, "tabi")
227
238
  if os.path.exists(path):
228
- shutil.rmtree(path)
239
+ _rmtree(path)
229
240
 
230
241
 
231
242
  def create_empty_schemas_module():
@@ -399,7 +410,21 @@ def clear() -> None:
399
410
 
400
411
  def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
401
412
  function_type = spec["type"]
402
- function_description = spec["description"]
413
+ raw_description = spec.get("description", "")
414
+ def _flatten_description(value: Any) -> List[str]:
415
+ if value is None:
416
+ return []
417
+ if isinstance(value, list):
418
+ flat: List[str] = []
419
+ for item in value:
420
+ flat.extend(_flatten_description(item))
421
+ return flat
422
+ return [str(value)]
423
+
424
+ if isinstance(raw_description, str):
425
+ function_description = raw_description
426
+ else:
427
+ function_description = "\n".join(_flatten_description(raw_description))
403
428
  function_name = spec["name"]
404
429
  function_context = spec["context"]
405
430
  function_id = spec["id"]
@@ -1,8 +1,9 @@
1
1
  import os
2
+ import re
2
3
  import logging
3
4
  import tempfile
4
5
  import shutil
5
- from typing import Any, Dict, List, Tuple
6
+ from typing import Any, Dict, List, Optional, Tuple
6
7
 
7
8
  from polyapi.schema import wrapped_generate_schema_types
8
9
  from polyapi.utils import add_import_to_init, init_the_init, to_func_namespace
@@ -23,7 +24,7 @@ FALLBACK_SPEC_TEMPLATE = """class {name}(TypedDict, total=False):
23
24
  """
24
25
 
25
26
 
26
- def generate_schemas(specs: List[SchemaSpecDto], limit_ids: List[str] = None):
27
+ def generate_schemas(specs: List[SchemaSpecDto], limit_ids: Optional[List[str]] = None):
27
28
  failed_schemas = []
28
29
  successful_schemas = []
29
30
  if limit_ids:
@@ -209,6 +210,44 @@ def add_schema_to_init(full_path: str, spec: SchemaSpecDto):
209
210
  f.write(render_poly_schema(spec) + "\n\n")
210
211
 
211
212
 
213
+ def _fix_typed_dict_imports(code: str) -> str:
214
+ """Move TypedDict/NotRequired from `typing` to `typing_extensions` in generated code.
215
+
216
+ jsonschema_gentypes spits out `from typing import ..., TypedDict, ...` which makes
217
+ typing._TypedDictMeta instances. The deploy validator wants typing_extensions.TypedDict,
218
+ so let's rewrite the imports here before writing schema files to disk.
219
+ """
220
+ lines = code.split('\n')
221
+ new_lines = []
222
+ has_te_import = False
223
+
224
+ for line in lines:
225
+ if re.match(r'from\s+typing_extensions\s+import', line):
226
+ has_te_import = True
227
+ # Ensure TypedDict and NotRequired are in the existing typing_extensions line
228
+ name_set = {n.strip() for n in line.split('import', 1)[1].split(',')}
229
+ name_set |= {'TypedDict', 'NotRequired'}
230
+ new_lines.append(f"from typing_extensions import {', '.join(sorted(name_set))}")
231
+ continue
232
+
233
+ if re.match(r'from\s+typing\s+import', line):
234
+ # Strip TypedDict and NotRequired from the typing import
235
+ names_str = line.split('import', 1)[1]
236
+ names: list[str] = [n.strip() for n in names_str.split(',')]
237
+ names = [n for n in names if n not in ('TypedDict', 'NotRequired', '')]
238
+ if names:
239
+ new_lines.append(f"from typing import {', '.join(names)}")
240
+ # drop the line entirely if nothing is left
241
+ continue
242
+
243
+ new_lines.append(line)
244
+
245
+ result = '\n'.join(new_lines)
246
+ if not has_te_import:
247
+ result = 'from typing_extensions import NotRequired, TypedDict\n' + result
248
+ return result
249
+
250
+
212
251
  def render_poly_schema(spec: SchemaSpecDto) -> str:
213
252
  definition = spec["definition"]
214
253
  if not definition.get("type"):
@@ -216,5 +255,5 @@ def render_poly_schema(spec: SchemaSpecDto) -> str:
216
255
  root, schema_types = wrapped_generate_schema_types(
217
256
  definition, root=spec["name"], fallback_type=Dict
218
257
  )
219
- return schema_types
258
+ return _fix_typed_dict_imports(schema_types)
220
259
  # return FALLBACK_SPEC_TEMPLATE.format(name=spec["name"])
@@ -531,10 +531,27 @@ def _render_table(table: TableSpecDto) -> str:
531
531
  table_where_class = _render_table_where_class(
532
532
  table["name"], columns, required_columns
533
533
  )
534
- if table.get("description", ""):
534
+ raw_description = table.get("description", "")
535
+
536
+ def _flatten_description(value: Any) -> List[str]:
537
+ if value is None:
538
+ return []
539
+ if isinstance(value, list):
540
+ flat: List[str] = []
541
+ for item in value:
542
+ flat.extend(_flatten_description(item))
543
+ return flat
544
+ return [str(value)]
545
+
546
+ if isinstance(raw_description, str):
547
+ normalized_description = raw_description
548
+ else:
549
+ normalized_description = "\n".join(_flatten_description(raw_description))
550
+
551
+ if normalized_description:
535
552
  table_description = '\n """'
536
553
  table_description += "\n ".join(
537
- table["description"].replace('"', "'").split("\n")
554
+ normalized_description.replace('"', "'").split("\n")
538
555
  )
539
556
  table_description += '\n """'
540
557
  else:
@@ -50,6 +50,13 @@ def wrapped_generate_schema_types(type_spec: dict, root, fallback_type):
50
50
  # if we have no root, just add "My"
51
51
  root = "My" + root
52
52
 
53
+ if isinstance(root, list):
54
+ root = "_".join([str(x) for x in root if x is not None]) or fallback_type
55
+ elif root is None:
56
+ root = fallback_type
57
+ elif not isinstance(root, str):
58
+ root = str(root)
59
+
53
60
  root = clean_title(root)
54
61
 
55
62
  try:
@@ -150,10 +157,19 @@ def clean_title(title: str) -> str:
150
157
  """ used by library generation, sometimes functions can be added with spaces in the title
151
158
  or other nonsense. fix them!
152
159
  """
160
+ if isinstance(title, list):
161
+ title = "_".join([str(x) for x in title if x is not None])
162
+ elif title is None:
163
+ title = ""
164
+ elif not isinstance(title, str):
165
+ title = str(title)
166
+
153
167
  title = title.replace(" ", "")
154
168
  # certain reserved words cant be titles, let's replace them
155
169
  if title == "List":
156
170
  title = "List_"
171
+ if not title:
172
+ title = "Dict"
157
173
  return title
158
174
 
159
175
 
@@ -62,7 +62,7 @@ def render_server_function(
62
62
  return_type_def=return_type_def,
63
63
  )
64
64
  func_str = SERVER_FUNCTION_TEMPLATE.format(
65
- return_type_name=add_type_import_path(function_name, return_type_name),
65
+ return_type_name=_normalize_return_type_for_annotation(function_name, return_type_name),
66
66
  function_type="server",
67
67
  function_name=function_name,
68
68
  function_id=function_id,
@@ -74,9 +74,23 @@ def render_server_function(
74
74
  return func_str, func_type_defs
75
75
 
76
76
 
77
+ def _normalize_return_type_for_annotation(function_name: str, return_type_name: str) -> str:
78
+ if return_type_name == "ReturnType":
79
+ return "ReturnType"
80
+ return add_type_import_path(function_name, return_type_name)
81
+
82
+
77
83
  def _get_server_return_action(return_type_name: str) -> str:
78
- if return_type_name == "str":
84
+ normalized_type = return_type_name.replace(" ", "")
85
+
86
+ if normalized_type in {"str", "Optional[str]"}:
79
87
  return_action = "resp.text"
88
+ elif "|" in normalized_type:
89
+ union_parts = {part for part in normalized_type.split("|") if part}
90
+ if union_parts == {"str", "None"}:
91
+ return_action = "resp.text"
92
+ else:
93
+ return_action = "resp.json()"
80
94
  else:
81
95
  return_action = "resp.json()"
82
- return return_action
96
+ return return_action
@@ -71,6 +71,43 @@ def print_red(s: str):
71
71
  print(Fore.RED + s + Style.RESET_ALL)
72
72
 
73
73
 
74
+ def normalize_cross_language_type(type_name: str) -> str:
75
+ value = (type_name or "").strip()
76
+ if not value:
77
+ return "Any"
78
+
79
+ primitive_map = {
80
+ "string": "str",
81
+ "number": "float",
82
+ "integer": "int",
83
+ "boolean": "bool",
84
+ "null": "None",
85
+ "void": "None",
86
+ "any": "Any",
87
+ "object": "Dict",
88
+ }
89
+
90
+ if value.startswith("Promise<") and value.endswith(">"):
91
+ return normalize_cross_language_type(value[len("Promise<"):-1])
92
+
93
+ if value.startswith("Awaited<") and value.endswith(">"):
94
+ return normalize_cross_language_type(value[len("Awaited<"):-1])
95
+
96
+ if value.endswith("[]"):
97
+ item_type = normalize_cross_language_type(value[:-2])
98
+ return f"List[{item_type}]"
99
+
100
+ if "|" in value:
101
+ parts = [p.strip() for p in value.split("|") if p.strip()]
102
+ normalized = [normalize_cross_language_type(part) for part in parts]
103
+ return " | ".join(normalized) if normalized else "Any"
104
+
105
+ if value == "ReturnType" or value.startswith("ReturnType<") or "typeof" in value:
106
+ return "Any"
107
+
108
+ return primitive_map.get(value, value)
109
+
110
+
74
111
  def to_type_module_alias(function_name: str) -> str:
75
112
  """Return the internal alias used for a function's generated type module."""
76
113
  return f"_{to_func_namespace(function_name)}_types"
@@ -81,6 +118,14 @@ def add_type_import_path(function_name: str, arg: str) -> str:
81
118
  # from now, we start qualifying non-basic types :))
82
119
  # e.g. Callable[[EmailAddress, Dict, Dict, Dict], None]
83
120
  # becomes Callable[[Set_profile_email.EmailAddress, Dict, Dict, Dict], None]
121
+ arg = normalize_cross_language_type(arg)
122
+
123
+ if "|" in arg:
124
+ return " | ".join(
125
+ add_type_import_path(function_name, token.strip())
126
+ for token in arg.split("|")
127
+ if token.strip()
128
+ )
84
129
  type_module_alias = to_type_module_alias(function_name)
85
130
 
86
131
  if arg.startswith("Callable"):
@@ -96,7 +141,7 @@ def add_type_import_path(function_name: str, arg: str) -> str:
96
141
  return "Callable[" + ",".join(qualified) + "]"
97
142
  # return arg
98
143
 
99
- if arg in BASIC_PYTHON_TYPES:
144
+ if arg == "Any" or arg in BASIC_PYTHON_TYPES:
100
145
  return arg
101
146
 
102
147
  if arg.startswith("List["):
@@ -127,14 +172,23 @@ def get_type_and_def(
127
172
  return "Any", ""
128
173
 
129
174
  if type_spec["kind"] == "plain":
130
- value = type_spec.get("value", "")
175
+ value = normalize_cross_language_type(type_spec.get("value", ""))
176
+
177
+ if "|" in value or value in BASIC_PYTHON_TYPES:
178
+ return value, ""
179
+
131
180
  if value.endswith("[]"):
132
- primitive = map_primitive_types(value[:-2])
181
+ primitive = normalize_cross_language_type(value[:-2])
182
+ if primitive not in BASIC_PYTHON_TYPES:
183
+ primitive = map_primitive_types(primitive)
133
184
  return f"List[{primitive}]", ""
134
185
  else:
135
186
  return map_primitive_types(value), ""
136
187
  elif type_spec["kind"] == "primitive":
137
- return map_primitive_types(type_spec.get("type", "any")), ""
188
+ primitive = normalize_cross_language_type(type_spec.get("type", "any"))
189
+ if primitive in BASIC_PYTHON_TYPES:
190
+ return primitive, ""
191
+ return map_primitive_types(primitive), ""
138
192
  elif type_spec["kind"] == "array":
139
193
  if type_spec.get("items"):
140
194
  items = type_spec["items"]
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.17.dev1
3
+ Version: 0.3.18
4
4
  Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
5
- Author-email: Dan Fellin <dan@polyapi.io>
5
+ Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
7
7
 
8
8
  Copyright (c) 2025 PolyAPI Inc.
@@ -28,15 +28,15 @@ Project-URL: Homepage, https://github.com/polyapi/polyapi-python
28
28
  Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
- Requires-Dist: requests>=2.32.3
32
- Requires-Dist: typing_extensions>=4.12.2
31
+ Requires-Dist: requests==2.32.3
32
+ Requires-Dist: typing_extensions==4.12.2
33
33
  Requires-Dist: jsonschema-gentypes==2.10.0
34
- Requires-Dist: pydantic<3.0.0,>=2.8.0
35
- Requires-Dist: stdlib_list<1.0.0,>=0.10.0
34
+ Requires-Dist: pydantic==2.8.0
35
+ Requires-Dist: stdlib_list==0.11.1
36
36
  Requires-Dist: colorama==0.4.4
37
37
  Requires-Dist: python-socketio[asyncio_client]==5.11.1
38
- Requires-Dist: truststore>=0.8.0
39
- Requires-Dist: httpx>=0.28.1
38
+ Requires-Dist: truststore==0.9.1
39
+ Requires-Dist: httpx==0.28.1
40
40
  Dynamic: license-file
41
41
 
42
42
  # PolyAPI Python Library
@@ -38,10 +38,12 @@ polyapi_python.egg-info/top_level.txt
38
38
  tests/test_api.py
39
39
  tests/test_async_proof.py
40
40
  tests/test_auth.py
41
+ tests/test_dependency_pins.py
41
42
  tests/test_deployables.py
42
43
  tests/test_generate.py
43
44
  tests/test_parser.py
44
45
  tests/test_poly_custom.py
46
+ tests/test_poly_schemas.py
45
47
  tests/test_rendered_spec.py
46
48
  tests/test_schema.py
47
49
  tests/test_server.py
@@ -0,0 +1,9 @@
1
+ requests==2.32.3
2
+ typing_extensions==4.12.2
3
+ jsonschema-gentypes==2.10.0
4
+ pydantic==2.8.0
5
+ stdlib_list==0.11.1
6
+ colorama==0.4.4
7
+ python-socketio[asyncio_client]==5.11.1
8
+ truststore==0.9.1
9
+ httpx==0.28.1
@@ -4,19 +4,19 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyapi-python"
7
- version = "0.3.17.dev1"
7
+ version = "0.3.18"
8
8
  description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
9
- authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
9
+ authors = [{ name = "PolyAPI", email = "support@polyapi.io" }]
10
10
  dependencies = [
11
- "requests>=2.32.3",
12
- "typing_extensions>=4.12.2",
11
+ "requests==2.32.3",
12
+ "typing_extensions==4.12.2",
13
13
  "jsonschema-gentypes==2.10.0",
14
- "pydantic>=2.8.0,<3.0.0",
15
- "stdlib_list>=0.10.0,<1.0.0",
14
+ "pydantic==2.8.0",
15
+ "stdlib_list==0.11.1",
16
16
  "colorama==0.4.4",
17
17
  "python-socketio[asyncio_client]==5.11.1",
18
- "truststore>=0.8.0",
19
- "httpx>=0.28.1"
18
+ "truststore==0.9.1",
19
+ "httpx==0.28.1"
20
20
  ]
21
21
  readme = "README.md"
22
22
  license = { file = "LICENSE" }
@@ -32,4 +32,4 @@ exclude = ["polyapi/poly*", "polyapi/vari*", "polyapi/.config.env", "polyapi/cac
32
32
  [tool.mypy]
33
33
  # for now redef errors happen sometimes, we will clean this up in the future!
34
34
  disable_error_code = "no-redef,name-defined"
35
- implicit_optional = true
35
+ implicit_optional = true
@@ -0,0 +1,33 @@
1
+ import re
2
+ import sys
3
+ from pathlib import Path
4
+
5
+
6
+ REPO_ROOT = Path(__file__).resolve().parents[1]
7
+ TRUSTSTORE_MINIMUM_PYTHON_313_VERSION = (0, 9, 1)
8
+
9
+
10
+ def _parse_requirement_version(requirement: str) -> tuple[int, ...]:
11
+ match = re.fullmatch(r"truststore==(\d+)\.(\d+)\.(\d+)", requirement)
12
+ assert match is not None
13
+ return tuple(int(part) for part in match.groups())
14
+
15
+
16
+ def test_truststore_pin_matches_between_dependency_surfaces():
17
+ requirements_lines = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines()
18
+ requirements_pin = next(line for line in requirements_lines if line.startswith("truststore=="))
19
+
20
+ pyproject_text = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
21
+ pyproject_match = re.search(r'"(truststore==\d+\.\d+\.\d+)"', pyproject_text)
22
+ assert pyproject_match is not None
23
+ pyproject_pin = pyproject_match.group(1)
24
+
25
+ assert requirements_pin == pyproject_pin
26
+
27
+
28
+ def test_truststore_pin_supports_python_313_ssl_chain_api():
29
+ requirements_lines = (REPO_ROOT / "requirements.txt").read_text(encoding="utf-8").splitlines()
30
+ requirements_pin = next(line for line in requirements_lines if line.startswith("truststore=="))
31
+
32
+ if sys.version_info >= (3, 13):
33
+ assert _parse_requirement_version(requirements_pin) >= TRUSTSTORE_MINIMUM_PYTHON_313_VERSION
@@ -7,7 +7,16 @@ from concurrent.futures import ThreadPoolExecutor
7
7
  def _reload_polyapi():
8
8
  sys.modules.pop("polyapi", None)
9
9
  sys.modules.pop("polyapi.cli", None)
10
- return importlib.import_module("polyapi")
10
+ polyapi = importlib.import_module("polyapi")
11
+
12
+ for module_name, module in list(sys.modules.items()):
13
+ if not module_name.startswith("polyapi.") or module_name == "polyapi.cli":
14
+ continue
15
+ submodule_name = module_name.removeprefix("polyapi.")
16
+ if "." not in submodule_name:
17
+ setattr(polyapi, submodule_name, module)
18
+
19
+ return polyapi
11
20
 
12
21
 
13
22
  def test_import_polyapi_does_not_import_cli():
@@ -24,6 +33,14 @@ def test_cli_constants_shared_between_runtime_and_cli():
24
33
  assert tuple(cli_module.CLI_COMMANDS) == cli_constants.CLI_COMMANDS
25
34
 
26
35
 
36
+ def test_reload_preserves_existing_submodule_bindings():
37
+ rendered_spec = importlib.import_module("polyapi.rendered_spec")
38
+
39
+ polyapi = _reload_polyapi()
40
+
41
+ assert polyapi.rendered_spec is rendered_spec
42
+
43
+
27
44
  def test_poly_custom_nested_scopes_restore_previous_state():
28
45
  polyapi = _reload_polyapi()
29
46
  poly_custom = polyapi.polyCustom
@@ -0,0 +1,124 @@
1
+ import unittest
2
+
3
+ from polyapi.poly_schemas import _fix_typed_dict_imports
4
+
5
+
6
+ class TestFixTypedDictImports(unittest.TestCase):
7
+ """Tests for _fix_typed_dict_imports, which rewrites TypedDict/NotRequired
8
+ from `typing` to `typing_extensions` in jsonschema_gentypes output."""
9
+
10
+ # TypedDict moved out of `typing`
11
+
12
+ def test_typeddict_removed_from_typing_import(self):
13
+ code = "from typing import Union, List, TypedDict\n\nclass Foo(TypedDict):\n x: int"
14
+ result = _fix_typed_dict_imports(code)
15
+ self.assertNotIn("from typing import Union, List, TypedDict", result)
16
+ self.assertNotIn("TypedDict", result.split("from typing_extensions")[0].split("from typing")[-1] if "from typing import" in result else "")
17
+
18
+ def test_typeddict_present_in_typing_extensions(self):
19
+ code = "from typing import Union, List, TypedDict\n\nclass Foo(TypedDict):\n x: int"
20
+ result = _fix_typed_dict_imports(code)
21
+ self.assertIn("from typing_extensions import", result)
22
+ self.assertIn("TypedDict", result)
23
+
24
+ def test_notrequired_removed_from_typing_import(self):
25
+ code = "from typing import Union, List, TypedDict, NotRequired\n\nclass Foo(TypedDict):\n x: int"
26
+ result = _fix_typed_dict_imports(code)
27
+ # NotRequired must NOT appear in a `from typing import` line
28
+ for line in result.splitlines():
29
+ if line.startswith("from typing import"):
30
+ self.assertNotIn("NotRequired", line)
31
+
32
+ def test_notrequired_present_in_typing_extensions(self):
33
+ code = "from typing import Union, TypedDict, NotRequired\n\nclass Foo(TypedDict):\n x: NotRequired[int]"
34
+ result = _fix_typed_dict_imports(code)
35
+ self.assertIn("NotRequired", result)
36
+ te_line = next(l for l in result.splitlines() if "from typing_extensions import" in l)
37
+ self.assertIn("NotRequired", te_line)
38
+
39
+ # Remaining typing imports preserved
40
+
41
+ def test_other_typing_names_kept(self):
42
+ code = "from typing import Union, List, TypedDict, Optional"
43
+ result = _fix_typed_dict_imports(code)
44
+ typing_line = next((l for l in result.splitlines() if l.startswith("from typing import")), "")
45
+ self.assertIn("Union", typing_line)
46
+ self.assertIn("List", typing_line)
47
+ self.assertIn("Optional", typing_line)
48
+
49
+ def test_typing_line_dropped_when_only_typeddict(self):
50
+ # If TypedDict was the only import, the `from typing import` line should be gone
51
+ code = "from typing import TypedDict\n\nclass Foo(TypedDict):\n pass"
52
+ result = _fix_typed_dict_imports(code)
53
+ self.assertNotIn("from typing import", result)
54
+
55
+ # Existing typing_extensions import is merged, not duplicated
56
+
57
+ def test_existing_te_import_gets_typeddict_merged(self):
58
+ code = (
59
+ "from typing import Union, TypedDict\n"
60
+ "from typing_extensions import NotRequired\n"
61
+ "\nclass Foo(TypedDict):\n x: NotRequired[int]"
62
+ )
63
+ result = _fix_typed_dict_imports(code)
64
+ te_lines = [l for l in result.splitlines() if "from typing_extensions import" in l]
65
+ self.assertEqual(len(te_lines), 1, "Should have exactly one typing_extensions import line")
66
+ self.assertIn("TypedDict", te_lines[0])
67
+ self.assertIn("NotRequired", te_lines[0])
68
+
69
+ def test_no_duplicate_typing_extensions_line(self):
70
+ code = (
71
+ "from typing_extensions import NotRequired\n"
72
+ "from typing import List, TypedDict\n"
73
+ )
74
+ result = _fix_typed_dict_imports(code)
75
+ te_lines = [l for l in result.splitlines() if "from typing_extensions import" in l]
76
+ self.assertEqual(len(te_lines), 1)
77
+
78
+ # No existing typing_extensions import — one is prepended
79
+
80
+ def test_te_import_prepended_when_absent(self):
81
+ code = "from typing import Union, TypedDict\n\nclass Foo(TypedDict):\n pass"
82
+ result = _fix_typed_dict_imports(code)
83
+ first_meaningful = next(l for l in result.splitlines() if l.strip())
84
+ self.assertIn("from typing_extensions import", first_meaningful)
85
+
86
+ # Code with no TypedDict at all is left structurally intact
87
+
88
+ def test_code_without_typeddict_unchanged_structure(self):
89
+ code = "from typing import Union, List\n\nx: List[int] = []"
90
+ result = _fix_typed_dict_imports(code)
91
+ # typing_extensions is prepended (no existing te import)
92
+ self.assertIn("from typing_extensions import", result)
93
+ # original typing import preserved
94
+ self.assertIn("from typing import Union, List", result)
95
+ # original assignment preserved
96
+ self.assertIn("x: List[int] = []", result)
97
+
98
+ def test_code_with_no_imports_gets_te_prepended(self):
99
+ code = "x = 1\ny = 2"
100
+ result = _fix_typed_dict_imports(code)
101
+ self.assertTrue(result.startswith("from typing_extensions import"))
102
+
103
+ # The rewrite doesn't break class bodies that use TypedDict
104
+
105
+ def test_typeddict_class_body_preserved(self):
106
+ code = (
107
+ "from typing import Union, List, TypedDict\n"
108
+ "\nclass Item(TypedDict):\n"
109
+ " name: str\n"
110
+ " value: Union[int, None]\n"
111
+ )
112
+ result = _fix_typed_dict_imports(code)
113
+ self.assertIn("class Item(TypedDict):", result)
114
+ self.assertIn("name: str", result)
115
+ self.assertIn("value: Union[int, None]", result)
116
+
117
+ def test_empty_string_input(self):
118
+ result = _fix_typed_dict_imports("")
119
+ # Should at minimum prepend the typing_extensions import
120
+ self.assertIn("from typing_extensions import", result)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ unittest.main()
@@ -66,6 +66,34 @@ LIST_RECOMMENDATIONS = {
66
66
  }
67
67
 
68
68
 
69
+ RETURN_TYPE_NAMED_RETURN_TYPE = {
70
+ "id": "ret-1234",
71
+ "type": "serverFunction",
72
+ "context": "mixed",
73
+ "name": "fooFunc",
74
+ "description": "Return type name collision regression test.",
75
+ "requirements": [],
76
+ "function": {
77
+ "arguments": [],
78
+ "returnType": {
79
+ "kind": "object",
80
+ "schema": {
81
+ "title": "ReturnType",
82
+ "type": "object",
83
+ "properties": {
84
+ "value": {"type": "string"},
85
+ },
86
+ "required": ["value"],
87
+ },
88
+ },
89
+ "synchronous": True,
90
+ },
91
+ "code": "",
92
+ "language": "javascript",
93
+ "visibilityMetadata": {"visibility": "ENVIRONMENT"},
94
+ }
95
+
96
+
69
97
  class T(unittest.TestCase):
70
98
  def test_render_function_twilio_server(self):
71
99
  # same test but try it as a serverFunction rather than an apiFunction
@@ -117,3 +145,67 @@ class T(unittest.TestCase):
117
145
  # stay_date: Required[str]
118
146
  # """ Required property """'''
119
147
  # self.assertIn(expected_return_type, func_str)
148
+
149
+
150
+ def test_render_function_return_type_name_collision_does_not_reference_module_attr(self):
151
+ return_type = RETURN_TYPE_NAMED_RETURN_TYPE["function"]["returnType"]
152
+ func_str, func_type_defs = render_server_function(
153
+ RETURN_TYPE_NAMED_RETURN_TYPE["type"],
154
+ RETURN_TYPE_NAMED_RETURN_TYPE["name"],
155
+ RETURN_TYPE_NAMED_RETURN_TYPE["id"],
156
+ RETURN_TYPE_NAMED_RETURN_TYPE["description"],
157
+ RETURN_TYPE_NAMED_RETURN_TYPE["function"]["arguments"],
158
+ return_type,
159
+ )
160
+ self.assertIn("-> dict", func_str)
161
+ self.assertNotIn(".returnType", func_str)
162
+ self.assertNotIn(".ReturnType", func_str)
163
+
164
+ def test_render_function_string_union_returns_text(self):
165
+ function_name = "getMaybeName"
166
+ return_type = {"kind": "plain", "value": "Promise<string | null>"}
167
+
168
+ func_str, _ = render_server_function(
169
+ "serverFunction",
170
+ function_name,
171
+ "ret-str-null-1",
172
+ "",
173
+ [],
174
+ return_type,
175
+ )
176
+
177
+ self.assertIn("-> str | None", func_str)
178
+ self.assertIn("try:\n return resp.text", func_str)
179
+ self.assertNotIn("return resp.json()", func_str)
180
+
181
+ def test_render_function_mixed_string_union_returns_json(self):
182
+ function_name = "getMaybePayload"
183
+ return_type = {"kind": "plain", "value": "string | object"}
184
+
185
+ func_str, _ = render_server_function(
186
+ "serverFunction",
187
+ function_name,
188
+ "ret-str-obj-1",
189
+ "",
190
+ [],
191
+ return_type,
192
+ )
193
+
194
+ self.assertIn("-> str | Dict", func_str)
195
+ self.assertIn("try:\n return resp.json()", func_str)
196
+
197
+ def test_render_function_number_nullable_returns_json(self):
198
+ function_name = "getMaybeCount"
199
+ return_type = {"kind": "plain", "value": "number | null"}
200
+
201
+ func_str, _ = render_server_function(
202
+ "serverFunction",
203
+ function_name,
204
+ "ret-num-null-1",
205
+ "",
206
+ [],
207
+ return_type,
208
+ )
209
+
210
+ self.assertIn("-> float | None", func_str)
211
+ self.assertIn("try:\n return resp.json()", func_str)
@@ -1,5 +1,5 @@
1
1
  import unittest
2
- from polyapi.utils import get_type_and_def, rewrite_reserved
2
+ from polyapi.utils import add_type_import_path, get_type_and_def, rewrite_reserved
3
3
 
4
4
  OPENAPI_FUNCTION = {
5
5
  "kind": "function",
@@ -84,3 +84,26 @@ class T(unittest.TestCase):
84
84
  def test_rewrite_reserved(self):
85
85
  rv = rewrite_reserved("from")
86
86
  self.assertEqual(rv, "_from")
87
+
88
+ def test_plain_return_type_utility_normalizes_to_any(self):
89
+ arg_type, arg_def = get_type_and_def({"kind": "plain", "value": "ReturnType<typeof fooFunc>"})
90
+ self.assertEqual(arg_type, "Any")
91
+ self.assertEqual(arg_def, "")
92
+
93
+ def test_plain_promise_union_normalizes_to_python_union(self):
94
+ arg_type, arg_def = get_type_and_def({"kind": "plain", "value": "Promise<string | null>"})
95
+ self.assertEqual(arg_type, "str | None")
96
+ self.assertEqual(arg_def, "")
97
+
98
+ def test_add_type_import_path_never_qualifies_return_type_utility(self):
99
+ arg_type = add_type_import_path("fooFunc", "ReturnType<typeof fooFunc>")
100
+ self.assertEqual(arg_type, "Any")
101
+
102
+ def test_plain_promise_array_union_normalizes_to_python_union(self):
103
+ arg_type, arg_def = get_type_and_def({"kind": "plain", "value": "Promise<string[] | null>"})
104
+ self.assertEqual(arg_type, "List[str] | None")
105
+ self.assertEqual(arg_def, "")
106
+
107
+ def test_add_type_import_path_keeps_array_union_primitives_valid(self):
108
+ arg_type = add_type_import_path("fooFunc", "Promise<string[] | null>")
109
+ self.assertEqual(arg_type, "List[str] | None")
@@ -1,9 +0,0 @@
1
- requests>=2.32.3
2
- typing_extensions>=4.12.2
3
- jsonschema-gentypes==2.10.0
4
- pydantic<3.0.0,>=2.8.0
5
- stdlib_list<1.0.0,>=0.10.0
6
- colorama==0.4.4
7
- python-socketio[asyncio_client]==5.11.1
8
- truststore>=0.8.0
9
- httpx>=0.28.1