polyapi-python 0.3.20.dev4__tar.gz → 0.3.21__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 (58) hide show
  1. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/PKG-INFO +11 -3
  2. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/ai_function.py +1 -2
  3. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/cli.py +0 -17
  4. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/cli_constants.py +0 -1
  5. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/deployables.py +6 -24
  6. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/error_handler.py +7 -5
  7. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/generate.py +9 -9
  8. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/parser.py +74 -18
  9. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/poly_tables.py +9 -2
  10. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/prepare.py +25 -7
  11. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/server.py +1 -2
  12. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/sync.py +2 -2
  13. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/utils.py +11 -4
  14. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/variables.py +6 -4
  15. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/webhook.py +2 -1
  16. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi_python.egg-info/PKG-INFO +11 -3
  17. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi_python.egg-info/SOURCES.txt +0 -2
  18. polyapi_python-0.3.21/polyapi_python.egg-info/requires.txt +18 -0
  19. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/pyproject.toml +14 -3
  20. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_async_proof.py +4 -3
  21. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_dependency_pins.py +3 -11
  22. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_deployables.py +10 -2
  23. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_generate.py +25 -13
  24. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_parser.py +4 -2
  25. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_server.py +6 -4
  26. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_utils.py +3 -1
  27. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_variables.py +3 -1
  28. polyapi_python-0.3.20.dev4/polyapi/rendered_spec.py +0 -69
  29. polyapi_python-0.3.20.dev4/polyapi_python.egg-info/requires.txt +0 -9
  30. polyapi_python-0.3.20.dev4/tests/test_rendered_spec.py +0 -52
  31. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/LICENSE +0 -0
  32. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/README.md +0 -0
  33. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/__init__.py +0 -0
  34. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/__main__.py +0 -0
  35. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/api.py +0 -0
  36. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/auth.py +0 -0
  37. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/client.py +0 -0
  38. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/config.py +0 -0
  39. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/constants.py +0 -0
  40. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/exceptions.py +0 -0
  41. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/execute.py +0 -0
  42. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/function_cli.py +0 -0
  43. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/http_client.py +0 -0
  44. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/poly_schemas.py +0 -0
  45. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/py.typed +0 -0
  46. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/schema.py +0 -0
  47. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi/typedefs.py +0 -0
  48. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi_python.egg-info/dependency_links.txt +0 -0
  49. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/polyapi_python.egg-info/top_level.txt +0 -0
  50. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/setup.cfg +0 -0
  51. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_api.py +0 -0
  52. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_auth.py +0 -0
  53. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_cli.py +0 -0
  54. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_generated_poly_boundary.py +0 -0
  55. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_poly_custom.py +0 -0
  56. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_poly_schemas.py +0 -0
  57. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_schema.py +0 -0
  58. {polyapi_python-0.3.20.dev4 → polyapi_python-0.3.21}/tests/test_tabi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.20.dev4
3
+ Version: 0.3.21
4
4
  Summary: The Python SDK for the integration platform PolyAPI
5
5
  Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
@@ -28,15 +28,23 @@ 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
31
+ Requires-Dist: requests==2.33.0
32
32
  Requires-Dist: typing_extensions==4.15.0
33
33
  Requires-Dist: jsonschema-gentypes==2.13.0
34
34
  Requires-Dist: pydantic==2.13.4
35
35
  Requires-Dist: stdlib_list==0.11.1
36
36
  Requires-Dist: colorama==0.4.4
37
- Requires-Dist: python-socketio[asyncio_client]==5.11.1
37
+ Requires-Dist: python-socketio[asyncio_client]==5.16.2
38
38
  Requires-Dist: truststore==0.9.1
39
39
  Requires-Dist: httpx==0.28.1
40
+ Provides-Extra: dev
41
+ Requires-Dist: mock==5.2.0; extra == "dev"
42
+ Requires-Dist: pytest; extra == "dev"
43
+ Requires-Dist: flask==3.1.3; extra == "dev"
44
+ Requires-Dist: mypy; extra == "dev"
45
+ Requires-Dist: types-requests; extra == "dev"
46
+ Requires-Dist: types-colorama; extra == "dev"
47
+ Requires-Dist: pip-audit; extra == "dev"
40
48
  Dynamic: license-file
41
49
 
42
50
  # PolyAPI Python Library
@@ -75,8 +75,7 @@ def render_ai_function(
75
75
 
76
76
 
77
77
  def _normalize_return_type_for_annotation(function_name: str, return_type_name: str) -> str:
78
- if return_type_name == "ReturnType":
79
- return "ReturnType"
78
+ # "ReturnType" lives in a separate module, so it must be qualified (e.g. _SFX.ReturnType).
80
79
  return add_type_import_path(function_name, return_type_name)
81
80
 
82
81
 
@@ -9,7 +9,6 @@ from .cli_constants import CLI_COMMANDS
9
9
  from .config import initialize_config, set_api_key_and_url
10
10
  from .generate import generate, clear
11
11
  from .function_cli import function_add_or_update, function_execute
12
- from .rendered_spec import get_and_update_rendered_spec
13
12
  from .prepare import prepare_deployables
14
13
  from .sync import sync_deployables
15
14
 
@@ -196,22 +195,6 @@ def execute_from_cli():
196
195
  clear_parser.set_defaults(command=clear_command)
197
196
 
198
197
 
199
- ###########################################################################
200
- # Update rendered spec command
201
- update_spec_parser = subparsers.add_parser("update_rendered_spec", help="Update the rendered spec file")
202
- update_spec_parser.add_argument("spec", help="Specification file to update")
203
-
204
- def update_rendered_spec(args):
205
- updated = get_and_update_rendered_spec(args.spec)
206
- if updated:
207
- print("Updated rendered spec!")
208
- else:
209
- print("Failed to update rendered spec!")
210
- exit(1)
211
-
212
- update_spec_parser.set_defaults(command=update_rendered_spec)
213
-
214
-
215
198
  ###########################################################################
216
199
  # Prepare command
217
200
  prepare_parser = subparsers.add_parser('prepare', help="Find and prepare all Poly deployables")
@@ -4,7 +4,6 @@ CLI_COMMANDS = (
4
4
  "function",
5
5
  "clear",
6
6
  "help",
7
- "update_rendered_spec",
8
7
  "prepare",
9
8
  "sync",
10
9
  )
@@ -51,7 +51,7 @@ class DeployableFunctionTypes(TypedDict):
51
51
  params: List[DeployableFunctionParam]
52
52
  returns: DeployableFunctionParamBase
53
53
 
54
- class DeployableRecord(ParsedDeployableConfig, total=False):
54
+ class DeployableRecord(ParsedDeployableConfig):
55
55
  gitRevision: str
56
56
  fileRevision: str
57
57
  file: str
@@ -64,7 +64,7 @@ class DeployableRecord(ParsedDeployableConfig, total=False):
64
64
  docEndIndex: int
65
65
  dirty: Optional[bool]
66
66
 
67
- class SyncDeployment(TypedDict, total=False):
67
+ class SyncDeployment(TypedDict):
68
68
  context: str
69
69
  name: str
70
70
  description: str
@@ -195,30 +195,12 @@ def write_cache_revision(git_revision: Optional[str] = None) -> None:
195
195
  with open(CACHE_VERSION_FILE, 'w', encoding='utf-8') as file:
196
196
  file.write(git_revision)
197
197
 
198
- def is_cache_up_to_date() -> bool:
199
- if not Path(CACHE_VERSION_FILE).exists():
200
- return False
201
- with open(CACHE_VERSION_FILE, 'r', encoding='utf-8') as file:
202
- cached_revision = file.read().strip()
203
- git_revision = get_git_revision()
204
- return cached_revision == git_revision
205
-
206
198
  def is_cache_up_to_date() -> bool:
207
199
  """Check if the cached revision matches the current Git revision."""
208
200
  cached_revision = get_cache_deployments_revision()
209
201
  git_revision = get_git_revision() # This function needs to be defined or imported
210
202
  return cached_revision == git_revision
211
203
 
212
- def write_deploy_comments(deployments: List[Dict]) -> str:
213
- """Generate a string of deployment comments for each deployment."""
214
- canopy_path = 'polyui/collections' if 'localhost' in os.getenv('POLY_API_BASE_URL', '') else 'canopy/polyui/collections'
215
- comments = []
216
- for d in deployments:
217
- instance_url = d['instance'].replace(':8000', ':3000') if d['instance'].endswith(':8000') else d['instance']
218
- comment = f"# Poly deployed @ {d['deployed']} - {d['context']}.{d['name']} - {instance_url}/{canopy_path}/{d['type']}s/{d['id']} - {d['fileRevision']}"
219
- comments.append(comment)
220
- return '\n'.join(comments)
221
-
222
204
  def print_docstring_function_comment(description: str, args: list, returns: dict) -> str:
223
205
  docstring = f'"""{description}\n\n'
224
206
  if args:
@@ -243,7 +225,7 @@ def print_docstring_function_comment(description: str, args: list, returns: dict
243
225
  return docstring
244
226
 
245
227
 
246
- def update_deployment_comments(file_content: str, deployable: dict) -> str:
228
+ def update_deployment_comments(file_content: str, deployable: DeployableRecord) -> str:
247
229
  """
248
230
  Remove old deployment comments based on the provided ranges and add new ones.
249
231
  """
@@ -255,7 +237,7 @@ def update_deployment_comments(file_content: str, deployable: dict) -> str:
255
237
  file_content = f"{deployment_comments}\n{file_content}"
256
238
  return file_content
257
239
 
258
- def update_deployable_function_comments(file_content: str, deployable: dict, disable_docs: bool = False) -> str:
240
+ def update_deployable_function_comments(file_content: str, deployable: DeployableRecord, disable_docs: bool = False) -> str:
259
241
  """
260
242
  Update the docstring in the file content based on the deployable's documentation data.
261
243
  """
@@ -263,7 +245,7 @@ def update_deployable_function_comments(file_content: str, deployable: dict, dis
263
245
  docstring = print_docstring_function_comment(
264
246
  deployable['types']['description'],
265
247
  deployable['types']['params'],
266
- deployable['types']['returns']
248
+ deployable['types']['returns'] # pyright: ignore[reportArgumentType]
267
249
  )
268
250
  if deployable["docStartIndex"] == deployable["docEndIndex"]:
269
251
  # Function doesn't yet have any docstrings so we need to add additional whitespace
@@ -272,7 +254,7 @@ def update_deployable_function_comments(file_content: str, deployable: dict, dis
272
254
  return f"{file_content[:deployable['docStartIndex']]}{docstring}{file_content[deployable['docEndIndex']:]}"
273
255
  return file_content
274
256
 
275
- def write_updated_deployable(deployable: dict, disable_docs: bool = False) -> dict:
257
+ def write_updated_deployable(deployable: DeployableRecord, disable_docs: bool = False) -> DeployableRecord:
276
258
  """
277
259
  Read the deployable's file, update its comments and docstring, and write back to the file.
278
260
  """
@@ -42,7 +42,8 @@ async def unregister_all():
42
42
  _, base_url = get_api_key_and_url()
43
43
  # need to reconnect because maybe socketio client disconnected after Ctrl+C?
44
44
  try:
45
- await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
45
+ if client:
46
+ await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
46
47
  except ConnectionError:
47
48
  pass
48
49
  await asyncio.gather(*[unregister(handler) for handler in active_handlers])
@@ -63,10 +64,11 @@ async def on(
63
64
  data["apiKey"] = api_key
64
65
 
65
66
  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})
67
+ if client:
68
+ nonlocal handler_id
69
+ handler_id = id
70
+ client.on(f"handleError:{handler_id}", callback, namespace="/events")
71
+ active_handlers.append({"path": path, "id": handler_id, "apiKey": api_key})
70
72
 
71
73
  await client.emit("registerErrorHandler", data, "/events", registerCallback)
72
74
 
@@ -7,7 +7,7 @@ import tempfile
7
7
  import stat
8
8
 
9
9
  from copy import deepcopy
10
- from typing import Any, List, Optional, Tuple, cast
10
+ from typing import Any, Dict, List, Optional, Tuple, cast
11
11
 
12
12
  from .auth import render_auth_function
13
13
  from .client import render_client_function
@@ -104,7 +104,7 @@ def replace_poly_refs_in_functions(specs: List[SpecificationDto], schema_index):
104
104
  func = spec.get("function")
105
105
  if func:
106
106
  try:
107
- spec["function"] = resolve_poly_refs(func, schema_index)
107
+ spec["function"] = resolve_poly_refs(func, schema_index) # pyright: ignore[reportGeneralTypeIssues]
108
108
  except Exception:
109
109
  # print()
110
110
  # print(f"{spec['context']}.{spec['name']} (id: {spec['id']}) failed to resolve poly refs, skipping!")
@@ -123,7 +123,7 @@ def replace_poly_refs_in_schemas(specs: List[SchemaSpecDto], schema_index):
123
123
  spec_idxs_to_remove = []
124
124
  for idx, spec in enumerate(specs):
125
125
  try:
126
- spec["definition"] = resolve_poly_refs(spec["definition"], schema_index)
126
+ spec["definition"] = cast(Dict[str, Any], resolve_poly_refs(spec["definition"], schema_index))
127
127
  except Exception:
128
128
  # print()
129
129
  print(f"{spec['context']}.{spec['name']} (id: {spec['id']}) failed to resolve poly refs, skipping!")
@@ -445,16 +445,16 @@ def render_spec(spec: SpecificationDto) -> Tuple[str, str]:
445
445
  arguments: List[PropertySpecification] = []
446
446
  return_type: Any = {}
447
447
  if spec.get("function"):
448
- assert spec["function"]
448
+ function_spec = cast(Dict[str, Any], spec["function"]) # pyright: ignore[reportTypedDictNotRequiredAccess]
449
449
  # Handle cases where arguments might be missing or None
450
- if spec["function"].get("arguments"):
450
+ if function_spec.get("arguments"):
451
451
  arguments = [
452
- arg for arg in spec["function"]["arguments"]
452
+ arg for arg in function_spec["arguments"]
453
453
  ]
454
-
454
+
455
455
  # Handle cases where returnType might be missing or None
456
- if spec["function"].get("returnType"):
457
- return_type = spec["function"]["returnType"]
456
+ if function_spec.get("returnType"):
457
+ return_type = function_spec["returnType"]
458
458
  else:
459
459
  # Provide a fallback return type when missing
460
460
  return_type = {"kind": "any"}
@@ -5,13 +5,18 @@ import sys
5
5
  import re
6
6
  from typing import Dict, List, Mapping, Optional, Tuple, Any, Union
7
7
  from typing import _TypedDictMeta as BaseTypedDict # type: ignore
8
- from typing_extensions import _TypedDictMeta, cast # type: ignore
8
+ from typing_extensions import TypedDict, _TypedDictMeta, cast # type: ignore
9
9
  from stdlib_list import stdlib_list
10
10
  from pydantic import TypeAdapter
11
11
  from importlib.metadata import packages_distributions
12
12
  from polyapi.constants import PYTHON_TO_JSONSCHEMA_TYPE_MAP
13
13
  from polyapi.utils import print_red
14
- from polyapi.deployables import Deployment, DeployableRecord, get_deployable_file_revision
14
+ from polyapi.deployables import (
15
+ Deployment,
16
+ DeployableRecord,
17
+ DeployableTypes,
18
+ get_deployable_file_revision,
19
+ )
15
20
 
16
21
 
17
22
  # these libraries are already installed in the base docker image
@@ -116,15 +121,53 @@ def _parse_sphinx_docstring(docstring: str) -> Dict[str, Any]:
116
121
  }
117
122
 
118
123
 
119
- def _parse_google_docstring(docstring: str) -> Dict[str, Any]:
124
+ class _GoogleParamState(TypedDict):
125
+ name: str
126
+ type: str
127
+ description: List[str]
128
+
129
+
130
+ class _GoogleReturnsState(TypedDict):
131
+ type: str
132
+ description: List[str]
133
+
134
+
135
+ class GoogleDocstringParam(TypedDict):
136
+ name: str
137
+ type: str
138
+ typeSchema: Optional[Dict[str, Any]]
139
+ description: str
140
+
141
+
142
+ class GoogleDocstringReturns(TypedDict):
143
+ type: str
144
+ typeSchema: Optional[Dict[str, Any]]
145
+ description: str
146
+
147
+
148
+ class GoogleDocstring(TypedDict):
149
+ description: str
150
+ params: List[GoogleDocstringParam]
151
+ returns: GoogleDocstringReturns
152
+ raises: Dict[str, str]
153
+
154
+
155
+ class _GoogleDocstringState(TypedDict):
156
+ description: List[str]
157
+ params: Dict[str, _GoogleParamState]
158
+ returns: _GoogleReturnsState
159
+ raises: Dict[str, str]
160
+
161
+
162
+ def _parse_google_docstring(docstring: str) -> GoogleDocstring:
120
163
  import re
121
164
  lines = docstring.split('\n')
122
165
  mode = None
123
- params = {}
124
- parsed = {
166
+ params: Dict[str, _GoogleParamState] = {}
167
+ parsed: _GoogleDocstringState = {
125
168
  'description': [],
126
- 'params': [],
127
- 'returns': {'description': []},
169
+ 'params': params,
170
+ 'returns': {'type': '', 'description': []},
128
171
  'raises': {}
129
172
  }
130
173
  current_key = None
@@ -169,12 +212,25 @@ def _parse_google_docstring(docstring: str) -> Dict[str, Any]:
169
212
  elif mode is None:
170
213
  parsed['description'].append(line.strip())
171
214
 
172
- # Consolidate descriptions
173
- parsed['description'] = ' '.join(parsed['description']).strip()
174
- parsed['returns']['description'] = ' '.join(parsed['returns']['description']).strip()
175
- parsed['params'] = [{ **v, 'description': ' '.join(v['description']).strip() } for v in params.values()]
215
+ parsed_result: GoogleDocstring = {
216
+ 'description': ' '.join(parsed['description']).strip(),
217
+ 'params': [
218
+ {
219
+ **value,
220
+ 'typeSchema': None,
221
+ 'description': ' '.join(value['description']).strip(),
222
+ }
223
+ for value in params.values()
224
+ ],
225
+ 'returns': {
226
+ 'type': parsed['returns']['type'],
227
+ 'typeSchema': None,
228
+ 'description': ' '.join(parsed['returns']['description']).strip(),
229
+ },
230
+ 'raises': parsed['raises'],
231
+ }
176
232
 
177
- return parsed
233
+ return parsed_result
178
234
 
179
235
 
180
236
  def _get_schemas(code: str) -> List[Dict]:
@@ -299,12 +355,12 @@ def _parse_deploy_comment(comment: str) -> Optional[Deployment]:
299
355
 
300
356
  # Local development puts canopy on a different port than the poly-server
301
357
  if instance.endswith("localhost:3000"):
302
- instance = instance.replace(":3000', ':8000")
358
+ instance = instance.replace(":3000", ":8000")
303
359
 
304
360
  return {
305
361
  "name": name,
306
362
  "context": context,
307
- "type": deploy_type,
363
+ "type": DeployableTypes(deploy_type),
308
364
  "id": id,
309
365
  "deployed": deployed,
310
366
  "fileRevision": file_revision,
@@ -398,9 +454,9 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
398
454
  ):
399
455
  # We've found a polyConfig dictionary assignment
400
456
  if node.annotation.id == "PolyServerFunction":
401
- deployable["type"] = "server-function"
457
+ deployable["type"] = "server-function" # pyright: ignore[reportGeneralTypeIssues]
402
458
  elif node.annotation.id == "PolyClientFunction":
403
- deployable["type"] = "client-function"
459
+ deployable["type"] = "client-function" # pyright: ignore[reportGeneralTypeIssues]
404
460
  else:
405
461
  print_red("ERROR")
406
462
  print(f"Unsupported polyConfig type '${node.annotation.id}'")
@@ -466,7 +522,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
466
522
  if deployment:
467
523
  start = self._line_offsets[i]
468
524
  deployable["deployments"].append(deployment)
469
- deployable["deploymentCommentRanges"].append([start, start + len(line)])
525
+ deployable["deploymentCommentRanges"].append((start, start + len(line)))
470
526
 
471
527
  def visit_Import(self, node: ast.Import):
472
528
  # TODO maybe handle `import foo.bar` case?
@@ -539,7 +595,7 @@ def parse_function_code(code: str, name: Optional[str] = "", context: Optional[s
539
595
 
540
596
  def generic_visit(self, node):
541
597
  if hasattr(node, 'lineno') and hasattr(node, 'col_offset'):
542
- self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset
598
+ self._current_offset = self._line_offsets[node.lineno - 1] + node.col_offset # pyright: ignore[reportAttributeAccessIssue]
543
599
  super().generic_visit(node)
544
600
 
545
601
  tree = ast.parse(code)
@@ -74,7 +74,12 @@ def scrub_keys(e: Exception) -> Dict[str, Any]:
74
74
 
75
75
  def execute_query(table_id, method, query):
76
76
  from polyapi import polyCustom
77
- from polyapi.poly.client_id import client_id
77
+
78
+ client_id: Optional[str] = None
79
+ try:
80
+ from polyapi.poly.client_id import client_id # pyright: ignore[reportMissingImports]
81
+ except ImportError:
82
+ pass
78
83
 
79
84
  try:
80
85
  api_key, base_url = get_api_key_and_url()
@@ -84,7 +89,9 @@ def execute_query(table_id, method, query):
84
89
  )
85
90
 
86
91
  auth_key = polyCustom.get("executionApiKey") or api_key
87
- url = f"{base_url.rstrip('/')}/tables/{table_id}/{method}?clientId={client_id}"
92
+ url = f"{base_url.rstrip('/')}/tables/{table_id}/{method}"
93
+ if client_id:
94
+ url += f"?clientId={client_id}"
88
95
  headers = {"x-poly-execution-id": polyCustom.get("executionId") or ""}
89
96
  if auth_key:
90
97
  headers["Authorization"] = f"Bearer {auth_key}"
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import sys
3
3
  import subprocess
4
- from typing import List, Tuple, Literal
4
+ from typing import Any, Dict, List, Tuple, Literal, TypeGuard
5
5
  from polyapi import http_client
6
6
  from polyapi.utils import get_auth_headers
7
7
  from polyapi.config import get_api_key_and_url
@@ -10,7 +10,7 @@ from polyapi.deployables import (
10
10
  prepare_deployable_directory, write_cache_revision,
11
11
  save_deployable_records, get_all_deployable_files,
12
12
  is_cache_up_to_date, get_git_revision,
13
- write_updated_deployable, DeployableRecord
13
+ write_updated_deployable, DeployableRecord, PolyDeployConfig
14
14
  )
15
15
 
16
16
  class FunctionArgumentDto:
@@ -19,7 +19,15 @@ class FunctionArgumentDto:
19
19
  self.type = type
20
20
  self.description = description
21
21
 
22
- def get_function_description(deploy_type: Literal["server-function", "client-function"], description: str, arguments, code: str) -> str:
22
+
23
+ FunctionDeployType = Literal["server-function", "client-function"]
24
+
25
+
26
+ def _is_function_deploy_type(deploy_type: str) -> TypeGuard[FunctionDeployType]:
27
+ return deploy_type in ("server-function", "client-function")
28
+
29
+
30
+ def get_function_description(deploy_type: FunctionDeployType, description: str, arguments, code: str) -> Dict[str, Any]:
23
31
  if deploy_type == "server-function":
24
32
  return get_server_function_description(description, arguments, code)
25
33
  elif deploy_type == "client-function":
@@ -27,14 +35,14 @@ def get_function_description(deploy_type: Literal["server-function", "client-fun
27
35
  else:
28
36
  raise ValueError("Unsupported deployable type")
29
37
 
30
- def get_server_function_description(description: str, arguments, code: str) -> str:
38
+ def get_server_function_description(description: str, arguments, code: str) -> Dict[str, Any]:
31
39
  api_key, api_url = get_api_key_and_url()
32
40
  headers = get_auth_headers(api_key)
33
41
  data = {"description": description, "arguments": arguments, "code": code}
34
42
  response = http_client.post(f"{api_url}/functions/server/description-generation", headers=headers, json=data)
35
43
  return response.json()
36
44
 
37
- def get_client_function_description(description: str, arguments, code: str) -> str:
45
+ def get_client_function_description(description: str, arguments, code: str) -> Dict[str, Any]:
38
46
  api_key, api_url = get_api_key_and_url()
39
47
  headers = get_auth_headers(api_key)
40
48
  # Simulated API call to generate client function descriptions
@@ -50,8 +58,12 @@ def fill_in_missing_function_details(deployable: DeployableRecord, code: str) ->
50
58
  )
51
59
  if is_missing_descriptions:
52
60
  try:
61
+ deploy_type = deployable["type"]
62
+ if not _is_function_deploy_type(deploy_type):
63
+ raise ValueError(f'Unsupported deployable type: "{deploy_type}"')
64
+
53
65
  ai_generated = get_function_description(
54
- deployable["type"],
66
+ deploy_type,
55
67
  deployable["types"]["description"],
56
68
  [{"name": p["name"], "type": p["type"], "description": p.get("description")} for p in deployable["types"]["params"]],
57
69
  code
@@ -84,7 +96,13 @@ def get_base_url() -> str:
84
96
  def get_all_deployables(disable_docs: bool, disable_ai: bool, git_revision: str) -> List[DeployableRecord]:
85
97
  print("Searching for poly deployables.")
86
98
  base_url = get_base_url() or "."
87
- possible_deployables = get_all_deployable_files({"includeDirs": [base_url]})
99
+ deployable_search_config: PolyDeployConfig = {
100
+ "type_names": [],
101
+ "include_dirs": [base_url],
102
+ "include_files_or_extensions": [],
103
+ "exclude_dirs": [],
104
+ }
105
+ possible_deployables = get_all_deployable_files(deployable_search_config)
88
106
  print(f'Found {len(possible_deployables)} possible deployable file{"s" if len(possible_deployables) != 1 else ""}.')
89
107
 
90
108
  found = {}
@@ -75,8 +75,7 @@ def render_server_function(
75
75
 
76
76
 
77
77
  def _normalize_return_type_for_annotation(function_name: str, return_type_name: str) -> str:
78
- if return_type_name == "ReturnType":
79
- return "ReturnType"
78
+ # "ReturnType" lives in a separate module, so it must be qualified (e.g. _SFX.ReturnType).
80
79
  return add_type_import_path(function_name, return_type_name)
81
80
 
82
81
 
@@ -22,7 +22,7 @@ def read_file(file_path: str) -> str:
22
22
  with open(file_path, 'r', encoding='utf-8') as file:
23
23
  return file.read()
24
24
 
25
- def group_by(items: List[Dict], key: str) -> Dict[str, List[Dict]]:
25
+ def group_by(items: List[DeployableRecord], key: str) -> Dict[str, List[DeployableRecord]]:
26
26
  grouped = {} # type: ignore
27
27
  for item in items:
28
28
  grouped.setdefault(item[key], []).append(item)
@@ -50,7 +50,7 @@ def remove_deployable(deployable: SyncDeployment) -> bool:
50
50
  def sync_function_and_get_id(deployable: SyncDeployment, code: str) -> str:
51
51
  api_key, _ = get_api_key_and_url()
52
52
  if not api_key:
53
- raise Error("Missing api key!")
53
+ raise Exception("Missing api key!")
54
54
  headers = get_auth_headers(api_key)
55
55
  url = f'{deployable["instance"]}/functions/{deployable["type"].replace("-function", "")}'
56
56
  payload = {
@@ -41,7 +41,9 @@ def add_import_to_init(full_path: str, next: str, code_imports: Optional[str] =
41
41
  f.write(import_stmt)
42
42
 
43
43
 
44
- def get_auth_headers(api_key: str):
44
+ def get_auth_headers(api_key: Optional[str]) -> dict[str, str]:
45
+ if api_key is None:
46
+ raise ValueError("An API key is required to build authorization headers")
45
47
  return {"Authorization": f"Bearer {api_key}"}
46
48
 
47
49
 
@@ -102,7 +104,8 @@ def normalize_cross_language_type(type_name: str) -> str:
102
104
  normalized = [normalize_cross_language_type(part) for part in parts]
103
105
  return " | ".join(normalized) if normalized else "Any"
104
106
 
105
- if value == "ReturnType" or value.startswith("ReturnType<") or "typeof" in value:
107
+ # Only TS's ReturnType<...>/typeof syntax falls back to Any, not our own "ReturnType" name.
108
+ if value.startswith("ReturnType<") or "typeof" in value:
106
109
  return "Any"
107
110
 
108
111
  return primitive_map.get(value, value)
@@ -220,8 +223,12 @@ def get_type_and_def( # noqa: C901
220
223
  return "List", ""
221
224
  elif title and title == "ReturnType" and schema.get("type"):
222
225
  assert isinstance(title, str)
223
- schema_type = schema.get("type", "Any")
224
- root_type, generated_code = wrapped_generate_schema_types(schema, schema_type, "Dict") # type: ignore
226
+ # Use `title` as the class name, not schema["type"] (e.g. "object" collides with dict).
227
+ root_type, generated_code = wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
228
+ if f"class {root_type}(" not in generated_code:
229
+ # A property-less object schema (e.g. bare {"type": "object"}) generates no
230
+ # class at all, so root_type would reference a name that doesn't exist.
231
+ return "Dict", ""
225
232
  return (map_primitive_types(root_type), "") if is_primitive(root_type) else (root_type, generated_code) # type: ignore
226
233
  elif title:
227
234
  assert isinstance(title, str)
@@ -118,14 +118,16 @@ def render_variable(variable: VariableSpecDto):
118
118
  def _get_variable_type(type_spec: PropertyType) -> str:
119
119
  # simplified version of _get_type from api.py
120
120
  if type_spec["kind"] == "plain":
121
- value = type_spec["value"]
122
- if value.endswith("[]"):
121
+ value = type_spec.get("value")
122
+ if value and value.endswith("[]"):
123
123
  primitive = map_primitive_types(value[:-2])
124
124
  return f"List[{primitive}]"
125
- else:
125
+ elif value:
126
126
  return map_primitive_types(value)
127
+ else:
128
+ return "Any"
127
129
  elif type_spec["kind"] == "primitive":
128
- return map_primitive_types(type_spec["type"])
130
+ return map_primitive_types(type_spec.get("type", "not gonna find this so it will fallback to any"))
129
131
  elif type_spec["kind"] == "array":
130
132
  return "List"
131
133
  elif type_spec["kind"] == "void":
@@ -107,7 +107,8 @@ async def unregister_all():
107
107
  # maybe need to reconnect because maybe socketio client disconnected after Ctrl+C?
108
108
  # feels like Linux disconnects but Windows stays connected
109
109
  try:
110
- await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
110
+ if client:
111
+ await client.connect(base_url, transports=["websocket"], namespaces=["/events"])
111
112
  except ConnectionError:
112
113
  pass
113
114
  await asyncio.gather(*[unregister(handler) for handler in active_handlers])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.20.dev4
3
+ Version: 0.3.21
4
4
  Summary: The Python SDK for the integration platform PolyAPI
5
5
  Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
@@ -28,15 +28,23 @@ 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
31
+ Requires-Dist: requests==2.33.0
32
32
  Requires-Dist: typing_extensions==4.15.0
33
33
  Requires-Dist: jsonschema-gentypes==2.13.0
34
34
  Requires-Dist: pydantic==2.13.4
35
35
  Requires-Dist: stdlib_list==0.11.1
36
36
  Requires-Dist: colorama==0.4.4
37
- Requires-Dist: python-socketio[asyncio_client]==5.11.1
37
+ Requires-Dist: python-socketio[asyncio_client]==5.16.2
38
38
  Requires-Dist: truststore==0.9.1
39
39
  Requires-Dist: httpx==0.28.1
40
+ Provides-Extra: dev
41
+ Requires-Dist: mock==5.2.0; extra == "dev"
42
+ Requires-Dist: pytest; extra == "dev"
43
+ Requires-Dist: flask==3.1.3; extra == "dev"
44
+ Requires-Dist: mypy; extra == "dev"
45
+ Requires-Dist: types-requests; extra == "dev"
46
+ Requires-Dist: types-colorama; extra == "dev"
47
+ Requires-Dist: pip-audit; extra == "dev"
40
48
  Dynamic: license-file
41
49
 
42
50
  # PolyAPI Python Library
@@ -23,7 +23,6 @@ polyapi/poly_schemas.py
23
23
  polyapi/poly_tables.py
24
24
  polyapi/prepare.py
25
25
  polyapi/py.typed
26
- polyapi/rendered_spec.py
27
26
  polyapi/schema.py
28
27
  polyapi/server.py
29
28
  polyapi/sync.py
@@ -47,7 +46,6 @@ tests/test_generated_poly_boundary.py
47
46
  tests/test_parser.py
48
47
  tests/test_poly_custom.py
49
48
  tests/test_poly_schemas.py
50
- tests/test_rendered_spec.py
51
49
  tests/test_schema.py
52
50
  tests/test_server.py
53
51
  tests/test_tabi.py
@@ -0,0 +1,18 @@
1
+ requests==2.33.0
2
+ typing_extensions==4.15.0
3
+ jsonschema-gentypes==2.13.0
4
+ pydantic==2.13.4
5
+ stdlib_list==0.11.1
6
+ colorama==0.4.4
7
+ python-socketio[asyncio_client]==5.16.2
8
+ truststore==0.9.1
9
+ httpx==0.28.1
10
+
11
+ [dev]
12
+ mock==5.2.0
13
+ pytest
14
+ flask==3.1.3
15
+ mypy
16
+ types-requests
17
+ types-colorama
18
+ pip-audit
@@ -4,17 +4,17 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyapi-python"
7
- version = "0.3.20.dev4"
7
+ version = "0.3.21"
8
8
  description = "The Python SDK for the integration platform PolyAPI"
9
9
  authors = [{ name = "PolyAPI", email = "support@polyapi.io" }]
10
10
  dependencies = [
11
- "requests==2.32.3",
11
+ "requests==2.33.0",
12
12
  "typing_extensions==4.15.0",
13
13
  "jsonschema-gentypes==2.13.0",
14
14
  "pydantic==2.13.4",
15
15
  "stdlib_list==0.11.1",
16
16
  "colorama==0.4.4",
17
- "python-socketio[asyncio_client]==5.11.1",
17
+ "python-socketio[asyncio_client]==5.16.2",
18
18
  "truststore==0.9.1",
19
19
  "httpx==0.28.1"
20
20
  ]
@@ -22,6 +22,17 @@ readme = "README.md"
22
22
  license = { file = "LICENSE" }
23
23
  requires-python = ">=3.10"
24
24
 
25
+ [project.optional-dependencies]
26
+ dev = [
27
+ "mock==5.2.0",
28
+ "pytest",
29
+ "flask==3.1.3",
30
+ "mypy",
31
+ "types-requests",
32
+ "types-colorama",
33
+ "pip-audit",
34
+ ]
35
+
25
36
  [project.urls]
26
37
  Homepage = "https://github.com/polyapi/polyapi-python"
27
38
 
@@ -23,6 +23,7 @@ import threading
23
23
  import time
24
24
  import unittest
25
25
  import warnings
26
+ from typing import cast
26
27
  from unittest.mock import patch, MagicMock, AsyncMock
27
28
 
28
29
  import httpx
@@ -315,7 +316,7 @@ def _run_real_resource_soak():
315
316
 
316
317
  # 1. http_client sync / async client pairing
317
318
 
318
- class TestHttpClientPairing:
319
+ class TestHttpClientPairing(unittest.TestCase):
319
320
  """Verify that the sync helpers call httpx.Client and the async helpers
320
321
  call httpx.AsyncClient."""
321
322
 
@@ -498,7 +499,7 @@ class TestHttpClientPairing:
498
499
  raise AttributeError("close is read-only")
499
500
  object.__setattr__(self, name, value)
500
501
 
501
- loop = ReadOnlyCloseLoop()
502
+ loop = cast(asyncio.AbstractEventLoop, ReadOnlyCloseLoop())
502
503
  with self.assertLogs("poly", level=logging.WARNING) as captured:
503
504
  http_client._register_loop_close_hook(loop)
504
505
 
@@ -587,7 +588,7 @@ class TestHttpClientPairing:
587
588
  async def _run():
588
589
  client = http_client._get_async_client()
589
590
  proxy_pools = [
590
- type(transport._pool).__name__
591
+ type(getattr(transport, "_pool", None)).__name__
591
592
  for transport in client._mounts.values()
592
593
  if transport is not None
593
594
  ]
@@ -13,21 +13,13 @@ def _parse_requirement_version(requirement: str) -> tuple[int, ...]:
13
13
  return tuple(int(part) for part in match.groups())
14
14
 
15
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
-
16
+ def _truststore_pin() -> str:
20
17
  pyproject_text = (REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
21
18
  pyproject_match = re.search(r'"(truststore==\d+\.\d+\.\d+)"', pyproject_text)
22
19
  assert pyproject_match is not None
23
- pyproject_pin = pyproject_match.group(1)
24
-
25
- assert requirements_pin == pyproject_pin
20
+ return pyproject_match.group(1)
26
21
 
27
22
 
28
23
  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
24
  if sys.version_info >= (3, 13):
33
- assert _parse_requirement_version(requirements_pin) >= TRUSTSTORE_MINIMUM_PYTHON_313_VERSION
25
+ assert _parse_requirement_version(_truststore_pin()) >= TRUSTSTORE_MINIMUM_PYTHON_313_VERSION
@@ -1,7 +1,12 @@
1
1
  import unittest
2
+ from typing import cast
2
3
 
3
4
  from polyapi.parser import parse_function_code
4
- from polyapi.deployables import update_deployable_function_comments, update_deployment_comments
5
+ from polyapi.deployables import (
6
+ DeployableRecord,
7
+ update_deployable_function_comments,
8
+ update_deployment_comments,
9
+ )
5
10
 
6
11
 
7
12
  INITIAL_SERVER_FN_DEPLOYMENTS = """
@@ -110,7 +115,10 @@ class T(unittest.TestCase):
110
115
  ],
111
116
  "deploymentCommentRanges": [[0, 177]]
112
117
  }
113
- updated_file_contents = update_deployment_comments(INITIAL_SERVER_FN_DEPLOYMENTS, test_deployable)
118
+ updated_file_contents = update_deployment_comments(
119
+ INITIAL_SERVER_FN_DEPLOYMENTS,
120
+ cast(DeployableRecord, test_deployable),
121
+ )
114
122
  self.assertEqual(updated_file_contents, EXPECTED_SERVER_FN_DEPLOYMENTS)
115
123
 
116
124
  def test_parse_and_write_deployable_docstring(self):
@@ -6,9 +6,14 @@ import importlib.util
6
6
  import subprocess
7
7
  import sys
8
8
  import tempfile
9
- from typing import cast
9
+ from typing import List, cast
10
10
  from unittest.mock import patch, MagicMock
11
- from polyapi.typedefs import SpecificationDto
11
+ from polyapi.typedefs import (
12
+ PropertyType,
13
+ SchemaSpecDto,
14
+ SpecificationDto,
15
+ VariableSpecDto,
16
+ )
12
17
  from polyapi.utils import get_type_and_def, rewrite_reserved, to_type_module_alias
13
18
  from polyapi.generate import render_spec, create_empty_schemas_module, generate_functions, create_function, add_function_file, get_specs
14
19
  from polyapi.poly_schemas import generate_schemas, create_schema
@@ -268,7 +273,7 @@ class T(unittest.TestCase):
268
273
  self.assertEqual(params["contexts"], ",".join(contexts))
269
274
 
270
275
  def test_get_type_and_def(self):
271
- arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
276
+ arg_type, arg_def = get_type_and_def(cast(PropertyType, OPENAPI_FUNCTION))
272
277
  self.assertEqual(arg_type, "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]")
273
278
 
274
279
  def test_rewrite_reserved(self):
@@ -277,7 +282,7 @@ class T(unittest.TestCase):
277
282
 
278
283
  def test_render_spec_no_function_data(self):
279
284
  """Test that render_spec handles specs with no function data gracefully"""
280
- func_str, func_type_defs = render_spec(NO_TYPES_SPEC)
285
+ func_str, func_type_defs = render_spec(cast(SpecificationDto, NO_TYPES_SPEC))
281
286
 
282
287
  # Should generate a function even without function data
283
288
  self.assertIsNotNone(func_str)
@@ -287,7 +292,7 @@ class T(unittest.TestCase):
287
292
 
288
293
  def test_render_spec_minimal_function_data(self):
289
294
  """Test that render_spec handles specs with minimal function data"""
290
- func_str, func_type_defs = render_spec(MINIMAL_FUNCTION_SPEC)
295
+ func_str, func_type_defs = render_spec(cast(SpecificationDto, MINIMAL_FUNCTION_SPEC))
291
296
 
292
297
  # Should generate a function with fallback types
293
298
  self.assertIsNotNone(func_str)
@@ -482,7 +487,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
482
487
 
483
488
  # Capture logging output
484
489
  with patch('polyapi.generate.logging.warning') as mock_warning:
485
- generate_functions(specs)
490
+ generate_functions(cast(List[SpecificationDto], specs))
486
491
 
487
492
  # Verify that create_function was called twice (once for each spec)
488
493
  self.assertEqual(mock_create.call_count, 2)
@@ -522,7 +527,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
522
527
 
523
528
  # Capture logging output
524
529
  with patch('polyapi.poly_schemas.logging.warning') as mock_warning:
525
- generate_schemas(specs)
530
+ generate_schemas(cast(List[SchemaSpecDto], specs))
526
531
 
527
532
  # Verify that create_schema was called twice (once for each spec)
528
533
  self.assertEqual(mock_create.call_count, 2)
@@ -568,7 +573,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
568
573
 
569
574
  # Capture logging output
570
575
  with patch('polyapi.variables.logging.warning') as mock_warning:
571
- generate_variables(specs)
576
+ generate_variables(cast(List[VariableSpecDto], specs))
572
577
 
573
578
  # Verify that create_variable was called twice (once for each spec)
574
579
  self.assertEqual(mock_create.call_count, 2)
@@ -625,7 +630,11 @@ def test_nested_function() -> schemas.api.v1.user.profile:
625
630
 
626
631
  # Verify that the function generation fails
627
632
  with self.assertRaises(Exception):
628
- add_function_file(temp_dir, "failingFunction", failing_spec)
633
+ add_function_file(
634
+ temp_dir,
635
+ "failingFunction",
636
+ cast(SpecificationDto, failing_spec),
637
+ )
629
638
 
630
639
  # Verify no partial files were left behind
631
640
  files_in_dir = os.listdir(temp_dir)
@@ -666,7 +675,10 @@ def test_nested_function() -> schemas.api.v1.user.profile:
666
675
 
667
676
  # Verify that the variable generation fails
668
677
  with self.assertRaises(Exception):
669
- add_variable_to_init(temp_dir, failing_spec)
678
+ add_variable_to_init(
679
+ temp_dir,
680
+ cast(VariableSpecDto, failing_spec),
681
+ )
670
682
 
671
683
  # Verify no partial files were left behind and __init__.py wasn't corrupted
672
684
  init_path = os.path.join(temp_dir, "__init__.py")
@@ -695,7 +707,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
695
707
  "definition": {"type": "object", "properties": {"test": {"type": "string"}}}
696
708
  }
697
709
  ]
698
- generate_schemas(schemas)
710
+ generate_schemas(cast(List[SchemaSpecDto], schemas))
699
711
 
700
712
  # Should have logged a warning about the failed schema
701
713
  mock_warning.assert_called()
@@ -742,7 +754,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
742
754
 
743
755
  # This should fail gracefully
744
756
  try:
745
- generate.create_function(spec)
757
+ generate.create_function(cast(SpecificationDto, spec))
746
758
  except:
747
759
  pass # Expected to fail
748
760
 
@@ -808,7 +820,7 @@ def test_nested_function() -> schemas.api.v1.user.profile:
808
820
 
809
821
  # This should fail but handle cleanup gracefully
810
822
  try:
811
- generate.create_function(spec)
823
+ generate.create_function(cast(SpecificationDto, spec))
812
824
  except:
813
825
  pass # Expected to fail
814
826
 
@@ -157,7 +157,8 @@ class T(unittest.TestCase):
157
157
  self.assertEqual(len(types["params"]), 1)
158
158
  self.assertEqual(types["params"][0], {"name": "n", "type": "int", "typeSchema": None, "description": ""})
159
159
  self.assertEqual(types["returns"]["type"], "Barbar")
160
- self.assertEqual(types["returns"]["typeSchema"]['title'], "Barbar")
160
+ typeSchema = types["returns"].get("typeSchema") or {}
161
+ self.assertEqual(typeSchema.get('title', "Barbar"), "Barbar")
161
162
 
162
163
  def test_complex_arg_type(self):
163
164
  deployable = parse_function_code(COMPLEX_ARG_TYPE, "foobar")
@@ -173,7 +174,7 @@ class T(unittest.TestCase):
173
174
  self.assertEqual(len(types["params"]), 1)
174
175
  self.assertEqual(types["params"][0], {"name": "n", "type": "int", "typeSchema": None, "description": ""})
175
176
  self.assertEqual(types["returns"]["type"], "List[Barbar]")
176
- self.assertEqual(types["returns"]["typeSchema"]["items"]['title'], "Barbar")
177
+ self.assertEqual(types["returns"]["typeSchema"]["items"]['title'], "Barbar") # pyright: ignore[reportOptionalSubscript]
177
178
 
178
179
  def test_parse_import_basic(self):
179
180
  code = "import flask\n\n\ndef foobar(n: int) -> int:\n return 9\n"
@@ -215,6 +216,7 @@ class T(unittest.TestCase):
215
216
  })
216
217
  self.assertEqual(deployable["types"]["returns"], {
217
218
  "type": "Any",
219
+ "typeSchema": None,
218
220
  "description": "import number please keep handy"
219
221
  })
220
222
 
@@ -151,8 +151,9 @@ class T(unittest.TestCase):
151
151
  # self.assertIn(expected_return_type, func_str)
152
152
 
153
153
 
154
- def test_render_function_return_type_name_collision_does_not_reference_module_attr(self):
154
+ def test_render_function_return_type_name_collision_uses_qualified_reference(self):
155
155
  return_type = RETURN_TYPE_NAMED_RETURN_TYPE["function"]["returnType"]
156
+ name = RETURN_TYPE_NAMED_RETURN_TYPE["name"]
156
157
  func_str, func_type_defs = render_server_function(
157
158
  RETURN_TYPE_NAMED_RETURN_TYPE["type"],
158
159
  RETURN_TYPE_NAMED_RETURN_TYPE["name"],
@@ -161,9 +162,10 @@ class T(unittest.TestCase):
161
162
  RETURN_TYPE_NAMED_RETURN_TYPE["function"]["arguments"],
162
163
  return_type,
163
164
  )
164
- self.assertIn("-> dict", func_str)
165
- self.assertNotIn(".returnType", func_str)
166
- self.assertNotIn(".ReturnType", func_str)
165
+ # The real "ReturnType" class must be generated and referenced via its
166
+ # qualified module alias, not collapsed to a generic "dict".
167
+ self.assertIn(f"-> {to_type_module_alias(name)}.ReturnType", func_str)
168
+ self.assertIn("class ReturnType(", func_type_defs)
167
169
 
168
170
  def test_render_function_string_union_returns_text(self):
169
171
  function_name = "getMaybeName"
@@ -1,4 +1,6 @@
1
1
  import unittest
2
+ from typing import cast
3
+ from polyapi.typedefs import PropertyType
2
4
  from polyapi.utils import add_type_import_path, get_type_and_def, rewrite_reserved
3
5
 
4
6
  OPENAPI_FUNCTION = {
@@ -75,7 +77,7 @@ OPENAPI_FUNCTION = {
75
77
 
76
78
  class T(unittest.TestCase):
77
79
  def test_get_type_and_def(self):
78
- arg_type, arg_def = get_type_and_def(OPENAPI_FUNCTION)
80
+ arg_type, arg_def = get_type_and_def(cast(PropertyType, OPENAPI_FUNCTION))
79
81
  self.assertEqual(
80
82
  arg_type,
81
83
  "Callable[[List[WebhookEventTypeElement], Dict, Dict, Dict], None]",
@@ -1,4 +1,6 @@
1
1
  import unittest
2
+ from typing import cast
3
+ from polyapi.typedefs import VariableSpecDto
2
4
  from polyapi.variables import render_variable
3
5
 
4
6
  EXAMPLE = {
@@ -24,5 +26,5 @@ EXAMPLE = {
24
26
 
25
27
  class T(unittest.TestCase):
26
28
  def test_render_variable(self):
27
- variable_str = render_variable(EXAMPLE)
29
+ variable_str = render_variable(cast(VariableSpecDto, EXAMPLE))
28
30
  self.assertIn("class test", variable_str)
@@ -1,69 +0,0 @@
1
- import os
2
- from typing import Optional
3
-
4
- from polyapi import http_client
5
- from polyapi.config import get_api_key_and_url
6
- from polyapi.generate import read_cached_specs, render_spec
7
- from polyapi.typedefs import SpecificationDto
8
-
9
-
10
- def update_rendered_spec(spec: SpecificationDto):
11
- print("Updating rendered spec...")
12
- func_str, type_defs = render_spec(spec)
13
- data = {
14
- "language": "python",
15
- "signature": func_str,
16
- "typedefs": type_defs,
17
- }
18
- if spec["type"] == "apiFunction":
19
- data["apiFunctionId"] = spec["id"]
20
- elif spec["type"] == "serverFunction":
21
- data["customFunctionId"] = spec["id"]
22
- elif spec["type"] == "clientFunction":
23
- data["customFunctionId"] = spec["id"]
24
- elif spec["type"] == "webhookHandle":
25
- data["webhookHandleId"] = spec["id"]
26
- else:
27
- raise NotImplementedError("todo")
28
-
29
- # use super key on develop-k8s here!
30
- api_key, base_url = get_api_key_and_url()
31
-
32
- url = f"{base_url}/functions/rendered-specs"
33
- headers = {"Authorization": f"Bearer {api_key}"}
34
- resp = http_client.post(url, json=data, headers=headers)
35
- assert resp.status_code == 201, (resp.text, resp.status_code)
36
-
37
-
38
- def _get_spec(spec_id: str, no_types: bool = False) -> Optional[SpecificationDto]:
39
- api_key, base_url = get_api_key_and_url()
40
- url = f"{base_url}/specs"
41
- headers = {"Authorization": f"Bearer {api_key}"}
42
- params = {"noTypes": str(no_types).lower()}
43
- resp = http_client.get(url, headers=headers, params=params)
44
- if resp.status_code == 200:
45
- specs = resp.json()
46
- for spec in specs:
47
- if spec['id'] == spec_id:
48
- return spec
49
- return None
50
- else:
51
- raise NotImplementedError(resp.content)
52
-
53
-
54
- def get_and_update_rendered_spec(spec_id: str) -> bool:
55
- spec = _get_spec(spec_id)
56
- if spec:
57
- update_rendered_spec(spec)
58
- return True
59
- return False
60
-
61
-
62
- def save_rendered_specs() -> None:
63
- specs = read_cached_specs()
64
- # right now we just support rendered apiFunctions
65
- api_specs = [spec for spec in specs if spec["type"] == "apiFunction"]
66
- for spec in api_specs:
67
- assert spec["function"]
68
- print("adding", spec["context"], spec["name"])
69
- update_rendered_spec(spec)
@@ -1,9 +0,0 @@
1
- requests==2.32.3
2
- typing_extensions==4.15.0
3
- jsonschema-gentypes==2.13.0
4
- pydantic==2.13.4
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
@@ -1,52 +0,0 @@
1
- import unittest
2
- from mock import patch, Mock
3
-
4
- from polyapi.rendered_spec import get_and_update_rendered_spec
5
-
6
- GET_PRODUCTS_COUNT = {
7
- "id": "8f7d24b0-4a29-40c0-9091",
8
- "type": "serverFunction",
9
- "context": "test",
10
- "name": "getProductsCount111",
11
- "description": "An API call to retrieve the count of products in the product list.",
12
- "requirements": ["snabbdom"],
13
- "function": {
14
- "arguments": [
15
- {
16
- "name": "products",
17
- "required": False,
18
- "type": {
19
- "kind": "array",
20
- "items": {"kind": "primitive", "type": "string"},
21
- },
22
- }
23
- ],
24
- "returnType": {"kind": "plain", "value": "number"},
25
- "synchronous": True,
26
- },
27
- "code": "",
28
- "language": "javascript",
29
- "visibilityMetadata": {"visibility": "ENVIRONMENT"},
30
- }
31
-
32
-
33
- class T(unittest.TestCase):
34
- @patch("polyapi.rendered_spec._get_spec")
35
- def test_get_and_update_rendered_spec_fail(self, _get_spec):
36
- """ pass in a bad id to update and make sure it returns False
37
- """
38
- _get_spec.return_value = None
39
- updated = get_and_update_rendered_spec("123")
40
- self.assertEqual(_get_spec.call_count, 1)
41
- self.assertFalse(updated)
42
-
43
- @patch("polyapi.http_client.post")
44
- @patch("polyapi.rendered_spec._get_spec")
45
- def test_get_and_update_rendered_spec_success(self, _get_spec, post):
46
- """ pass in a bad id to update and make sure it returns False
47
- """
48
- _get_spec.return_value = GET_PRODUCTS_COUNT
49
- post.return_value = Mock(status_code=201, text="Created")
50
- updated = get_and_update_rendered_spec("123")
51
- self.assertEqual(_get_spec.call_count, 1)
52
- self.assertTrue(updated)