polyapi-python 0.3.20.dev3__tar.gz → 0.3.21.dev0__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 (57) hide show
  1. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/PKG-INFO +8 -1
  2. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/README.md +7 -0
  3. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/ai_function.py +1 -2
  4. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/http_client.py +66 -2
  5. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/server.py +1 -2
  6. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/utils.py +8 -3
  7. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi_python.egg-info/PKG-INFO +8 -1
  8. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/pyproject.toml +1 -1
  9. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_server.py +6 -4
  10. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/LICENSE +0 -0
  11. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/__init__.py +0 -0
  12. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/__main__.py +0 -0
  13. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/api.py +0 -0
  14. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/auth.py +0 -0
  15. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/cli.py +0 -0
  16. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/cli_constants.py +0 -0
  17. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/client.py +0 -0
  18. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/config.py +0 -0
  19. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/constants.py +0 -0
  20. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/deployables.py +0 -0
  21. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/error_handler.py +0 -0
  22. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/exceptions.py +0 -0
  23. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/execute.py +0 -0
  24. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/function_cli.py +0 -0
  25. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/generate.py +0 -0
  26. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/parser.py +0 -0
  27. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/poly_schemas.py +0 -0
  28. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/poly_tables.py +0 -0
  29. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/prepare.py +0 -0
  30. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/py.typed +0 -0
  31. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/rendered_spec.py +0 -0
  32. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/schema.py +0 -0
  33. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/sync.py +0 -0
  34. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/typedefs.py +0 -0
  35. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/variables.py +0 -0
  36. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi/webhook.py +0 -0
  37. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi_python.egg-info/SOURCES.txt +0 -0
  38. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi_python.egg-info/dependency_links.txt +0 -0
  39. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi_python.egg-info/requires.txt +0 -0
  40. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/polyapi_python.egg-info/top_level.txt +0 -0
  41. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/setup.cfg +0 -0
  42. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_api.py +0 -0
  43. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_async_proof.py +0 -0
  44. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_auth.py +0 -0
  45. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_cli.py +0 -0
  46. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_dependency_pins.py +0 -0
  47. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_deployables.py +0 -0
  48. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_generate.py +0 -0
  49. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_generated_poly_boundary.py +0 -0
  50. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_parser.py +0 -0
  51. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_poly_custom.py +0 -0
  52. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_poly_schemas.py +0 -0
  53. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_rendered_spec.py +0 -0
  54. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_schema.py +0 -0
  55. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_tabi.py +0 -0
  56. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/tests/test_utils.py +0 -0
  57. {polyapi_python-0.3.20.dev3 → polyapi_python-0.3.21.dev0}/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.20.dev3
3
+ Version: 0.3.21.dev0
4
4
  Summary: The Python SDK for the integration platform PolyAPI
5
5
  Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
@@ -98,10 +98,17 @@ client per event loop. New clients read these optional environment variables:
98
98
  | `POLY_HTTP_READ_TIMEOUT` | `none` | Read timeout in seconds |
99
99
  | `POLY_HTTP_WRITE_TIMEOUT` | `none` | Write timeout in seconds |
100
100
  | `POLY_HTTP_POOL_TIMEOUT` | `none` | Connection-pool timeout in seconds |
101
+ | `POLY_HTTP_CLOSE_TIMEOUT` | `5` | Deadline in seconds for closing a client's pool |
101
102
 
102
103
  An empty value, `none`, or `null` means unbounded. Limits are per client, so
103
104
  processes with multiple event loops can create one configured pool per loop.
104
105
 
106
+ `POLY_HTTP_CLOSE_TIMEOUT` is read at shutdown rather than at client creation.
107
+ It bounds pool teardown so a single unresponsive socket cannot stall shutdown:
108
+ if the deadline passes, the SDK logs a warning, abandons the remaining
109
+ connections to the operating system, and retires the client so it is never
110
+ reused. Set it to `none` to wait indefinitely.
111
+
105
112
  Call `polyapi.http_client.close()` during synchronous shutdown or await
106
113
  `polyapi.http_client.close_async()` on each owning event loop during
107
114
  asynchronous shutdown. The shutdown functions wait for active SDK requests
@@ -57,10 +57,17 @@ client per event loop. New clients read these optional environment variables:
57
57
  | `POLY_HTTP_READ_TIMEOUT` | `none` | Read timeout in seconds |
58
58
  | `POLY_HTTP_WRITE_TIMEOUT` | `none` | Write timeout in seconds |
59
59
  | `POLY_HTTP_POOL_TIMEOUT` | `none` | Connection-pool timeout in seconds |
60
+ | `POLY_HTTP_CLOSE_TIMEOUT` | `5` | Deadline in seconds for closing a client's pool |
60
61
 
61
62
  An empty value, `none`, or `null` means unbounded. Limits are per client, so
62
63
  processes with multiple event loops can create one configured pool per loop.
63
64
 
65
+ `POLY_HTTP_CLOSE_TIMEOUT` is read at shutdown rather than at client creation.
66
+ It bounds pool teardown so a single unresponsive socket cannot stall shutdown:
67
+ if the deadline passes, the SDK logs a warning, abandons the remaining
68
+ connections to the operating system, and retires the client so it is never
69
+ reused. Set it to `none` to wait indefinitely.
70
+
64
71
  Call `polyapi.http_client.close()` during synchronous shutdown or await
65
72
  `polyapi.http_client.close_async()` on each owning event loop during
66
73
  asynchronous shutdown. The shutdown functions wait for active SDK requests
@@ -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
 
@@ -45,6 +45,12 @@ def _build_timeout() -> httpx.Timeout:
45
45
  )
46
46
 
47
47
 
48
+ def _close_deadline() -> "float | None":
49
+ # Bound pool teardown so one wedged socket cannot hang invocation exit.
50
+ # None (POLY_HTTP_CLOSE_TIMEOUT=none) restores the old unbounded wait.
51
+ return _env_opt_float("POLY_HTTP_CLOSE_TIMEOUT", 5.0)
52
+
53
+
48
54
  # Import-time snapshot for reference/config surface. Clients re-read env at creation
49
55
  DEFAULT_LIMITS = _build_limits()
50
56
  DEFAULT_TIMEOUT = _build_timeout()
@@ -166,6 +172,40 @@ def _sweep_dead_loops() -> None:
166
172
  _retire_async_client(_async_clients.pop(loop, None), loop)
167
173
 
168
174
 
175
+ def _abandon_close_task(
176
+ close_task: "asyncio.Future[None]",
177
+ client: httpx.AsyncClient,
178
+ loop: asyncio.AbstractEventLoop,
179
+ deadline: "float | None",
180
+ context: str = "",
181
+ ) -> None:
182
+ """Detach a pool close that blew its deadline.
183
+
184
+ The task is cancelled and dropped rather than awaited: waiting for an
185
+ unresponsive pool to unwind is the stall the deadline exists to prevent.
186
+ Cancelling is what keeps loop shutdown bounded too, since asyncio.run()
187
+ tears down by cancelling every task and gathering them; a cancelled close
188
+ completes there instead of blocking. Whatever stays open is released when
189
+ the coroutine is collected, and by the OS at exit.
190
+ """
191
+ def _swallow(finished: "asyncio.Future[None]") -> None:
192
+ # Retrieve the result so a late failure is not reported as an
193
+ # unretrieved task exception.
194
+ if finished.cancelled():
195
+ return
196
+ error = finished.exception()
197
+ if error is not None:
198
+ logger.debug(
199
+ f"Abandoned aclose later failed with {error!r} id={id(client)} "
200
+ f"pid={os.getpid()} loop={id(loop)}")
201
+
202
+ close_task.add_done_callback(_swallow)
203
+ close_task.cancel()
204
+ logger.warning(
205
+ f"aclose exceeded {deadline}s deadline; abandoning pool "
206
+ f"id={id(client)} pid={os.getpid()} loop={id(loop)}{context}")
207
+
208
+
169
209
  def _register_loop_close_hook(loop: asyncio.AbstractEventLoop) -> None:
170
210
  """aclose the loop's async client right before the loop is torn down.
171
211
  """
@@ -177,8 +217,21 @@ def _register_loop_close_hook(loop: asyncio.AbstractEventLoop) -> None:
177
217
  _hooked_loops.discard(loop)
178
218
  cached = _async_clients.pop(loop, None)
179
219
  if cached is not None and not loop.is_closed():
220
+ deadline = _close_deadline()
180
221
  try:
181
- loop.run_until_complete(cached.aclose())
222
+ # asyncio.wait returns at the deadline without cancelling, so
223
+ # loop.close() proceeds even if the pool ignores cancellation.
224
+ close_task = loop.create_task(cached.aclose())
225
+ loop.run_until_complete(
226
+ asyncio.wait({close_task}, timeout=deadline))
227
+ if close_task.done():
228
+ # Surface real aclose failures, including a TimeoutError
229
+ # raised by aclose itself, rather than treating them as
230
+ # our deadline.
231
+ close_task.result()
232
+ else:
233
+ _abandon_close_task(
234
+ close_task, cached, loop, deadline, " on teardown")
182
235
  except Exception:
183
236
  logger.warning(
184
237
  f"Failed to aclose async client id={id(cached)} pid={os.getpid()} "
@@ -267,8 +320,19 @@ async def _close_clients_for_loop(
267
320
  return
268
321
 
269
322
  await _drain_inflight(client)
323
+ deadline = _close_deadline()
324
+ close_task = asyncio.ensure_future(client.aclose())
270
325
  try:
271
- await client.aclose()
326
+ # asyncio.wait returns at the deadline without cancelling the
327
+ # close, so the bound holds even when the pool never processes
328
+ # cancellation. wait_for would block on that unwind instead.
329
+ await asyncio.wait({close_task}, timeout=deadline)
330
+ if close_task.done():
331
+ # Real aclose errors, including a TimeoutError raised by aclose
332
+ # itself, still propagate to the close_async caller.
333
+ close_task.result()
334
+ else:
335
+ _abandon_close_task(close_task, client, current_loop, deadline)
272
336
  finally:
273
337
  if _async_clients.get(current_loop) is client:
274
338
  _async_clients.pop(current_loop, None)
@@ -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
 
@@ -102,7 +102,8 @@ def normalize_cross_language_type(type_name: str) -> str:
102
102
  normalized = [normalize_cross_language_type(part) for part in parts]
103
103
  return " | ".join(normalized) if normalized else "Any"
104
104
 
105
- if value == "ReturnType" or value.startswith("ReturnType<") or "typeof" in value:
105
+ # Only TS's ReturnType<...>/typeof syntax falls back to Any, not our own "ReturnType" name.
106
+ if value.startswith("ReturnType<") or "typeof" in value:
106
107
  return "Any"
107
108
 
108
109
  return primitive_map.get(value, value)
@@ -220,8 +221,12 @@ def get_type_and_def( # noqa: C901
220
221
  return "List", ""
221
222
  elif title and title == "ReturnType" and schema.get("type"):
222
223
  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
224
+ # Use `title` as the class name, not schema["type"] (e.g. "object" collides with dict).
225
+ root_type, generated_code = wrapped_generate_schema_types(schema, title, "Dict") # type: ignore
226
+ if f"class {root_type}(" not in generated_code:
227
+ # A property-less object schema (e.g. bare {"type": "object"}) generates no
228
+ # class at all, so root_type would reference a name that doesn't exist.
229
+ return "Dict", ""
225
230
  return (map_primitive_types(root_type), "") if is_primitive(root_type) else (root_type, generated_code) # type: ignore
226
231
  elif title:
227
232
  assert isinstance(title, str)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.20.dev3
3
+ Version: 0.3.21.dev0
4
4
  Summary: The Python SDK for the integration platform PolyAPI
5
5
  Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
@@ -98,10 +98,17 @@ client per event loop. New clients read these optional environment variables:
98
98
  | `POLY_HTTP_READ_TIMEOUT` | `none` | Read timeout in seconds |
99
99
  | `POLY_HTTP_WRITE_TIMEOUT` | `none` | Write timeout in seconds |
100
100
  | `POLY_HTTP_POOL_TIMEOUT` | `none` | Connection-pool timeout in seconds |
101
+ | `POLY_HTTP_CLOSE_TIMEOUT` | `5` | Deadline in seconds for closing a client's pool |
101
102
 
102
103
  An empty value, `none`, or `null` means unbounded. Limits are per client, so
103
104
  processes with multiple event loops can create one configured pool per loop.
104
105
 
106
+ `POLY_HTTP_CLOSE_TIMEOUT` is read at shutdown rather than at client creation.
107
+ It bounds pool teardown so a single unresponsive socket cannot stall shutdown:
108
+ if the deadline passes, the SDK logs a warning, abandons the remaining
109
+ connections to the operating system, and retires the client so it is never
110
+ reused. Set it to `none` to wait indefinitely.
111
+
105
112
  Call `polyapi.http_client.close()` during synchronous shutdown or await
106
113
  `polyapi.http_client.close_async()` on each owning event loop during
107
114
  asynchronous shutdown. The shutdown functions wait for active SDK requests
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyapi-python"
7
- version = "0.3.20.dev3"
7
+ version = "0.3.21.dev0"
8
8
  description = "The Python SDK for the integration platform PolyAPI"
9
9
  authors = [{ name = "PolyAPI", email = "support@polyapi.io" }]
10
10
  dependencies = [
@@ -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"