polyapi-python 0.3.15__tar.gz → 0.3.16.dev2__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 (52) hide show
  1. {polyapi_python-0.3.15/polyapi_python.egg-info → polyapi_python-0.3.16.dev2}/PKG-INFO +1 -1
  2. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/generate.py +5 -3
  3. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/http_client.py +16 -6
  4. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/utils.py +11 -5
  5. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2/polyapi_python.egg-info}/PKG-INFO +1 -1
  6. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/pyproject.toml +1 -1
  7. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_api.py +5 -5
  8. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_async_proof.py +86 -0
  9. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_generate.py +46 -2
  10. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_server.py +3 -3
  11. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/LICENSE +0 -0
  12. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/README.md +0 -0
  13. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/__init__.py +0 -0
  14. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/__main__.py +0 -0
  15. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/api.py +0 -0
  16. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/auth.py +0 -0
  17. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/cli.py +0 -0
  18. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/cli_constants.py +0 -0
  19. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/client.py +0 -0
  20. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/config.py +0 -0
  21. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/constants.py +0 -0
  22. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/deployables.py +0 -0
  23. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/error_handler.py +0 -0
  24. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/exceptions.py +0 -0
  25. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/execute.py +0 -0
  26. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/function_cli.py +0 -0
  27. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/parser.py +0 -0
  28. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/poly_schemas.py +0 -0
  29. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/poly_tables.py +0 -0
  30. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/prepare.py +0 -0
  31. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/py.typed +0 -0
  32. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/rendered_spec.py +0 -0
  33. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/schema.py +0 -0
  34. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/server.py +0 -0
  35. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/sync.py +0 -0
  36. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/typedefs.py +0 -0
  37. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/variables.py +0 -0
  38. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi/webhook.py +0 -0
  39. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi_python.egg-info/SOURCES.txt +0 -0
  40. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi_python.egg-info/dependency_links.txt +0 -0
  41. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi_python.egg-info/requires.txt +0 -0
  42. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/polyapi_python.egg-info/top_level.txt +0 -0
  43. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/setup.cfg +0 -0
  44. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_auth.py +0 -0
  45. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_deployables.py +0 -0
  46. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_parser.py +0 -0
  47. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_poly_custom.py +0 -0
  48. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_rendered_spec.py +0 -0
  49. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_schema.py +0 -0
  50. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_tabi.py +0 -0
  51. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/tests/test_utils.py +0 -0
  52. {polyapi_python-0.3.15 → polyapi_python-0.3.16.dev2}/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.15
3
+ Version: 0.3.16.dev2
4
4
  Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
5
5
  Author-email: Dan Fellin <dan@polyapi.io>
6
6
  License: MIT License
@@ -16,7 +16,7 @@ from .webhook import render_webhook_handle
16
16
  from .typedefs import PropertySpecification, SchemaSpecDto, SpecificationDto, VariableSpecDto, TableSpecDto
17
17
  from .api import render_api_function
18
18
  from .server import render_server_function
19
- from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace
19
+ from .utils import add_import_to_init, get_auth_headers, init_the_init, print_green, to_func_namespace, to_type_module_alias
20
20
  from .variables import generate_variables
21
21
  from .poly_tables import generate_tables
22
22
  from . import http_client
@@ -506,8 +506,10 @@ def add_function_file(
506
506
  with open(init_path, "r", encoding='utf-8') as f:
507
507
  init_content = f.read()
508
508
 
509
- # Prepare new content to append to __init__.py
510
- new_init_content = init_content + f"\n\nfrom . import {func_namespace}\n\n{func_str}"
509
+ # Import the generated type module under a private alias so PascalCase function
510
+ # names do not shadow their own module symbol in __init__.py.
511
+ type_module_alias = to_type_module_alias(function_name)
512
+ new_init_content = init_content + f"\n\nfrom . import {func_namespace} as {type_module_alias}\n\n{func_str}"
511
513
 
512
514
  # Use temporary files for atomic writes
513
515
  # Write to __init__.py atomically
@@ -3,6 +3,7 @@ import httpx
3
3
 
4
4
  _sync_client: httpx.Client | None = None
5
5
  _async_client: httpx.AsyncClient | None = None
6
+ _async_client_loop: asyncio.AbstractEventLoop | None = None
6
7
 
7
8
 
8
9
  def _get_sync_client() -> httpx.Client:
@@ -13,9 +14,11 @@ def _get_sync_client() -> httpx.Client:
13
14
 
14
15
 
15
16
  def _get_async_client() -> httpx.AsyncClient:
16
- global _async_client
17
- if _async_client is None:
17
+ global _async_client, _async_client_loop
18
+ current_loop = asyncio.get_running_loop()
19
+ if _async_client is None or _async_client_loop is not current_loop:
18
20
  _async_client = httpx.AsyncClient(timeout=None)
21
+ _async_client_loop = current_loop
19
22
  return _async_client
20
23
 
21
24
 
@@ -66,8 +69,15 @@ def close():
66
69
  _sync_client = None
67
70
 
68
71
  async def close_async():
69
- global _sync_client, _async_client
72
+ global _sync_client, _async_client, _async_client_loop
70
73
  close()
71
- if _async_client is not None:
72
- await _async_client.aclose()
73
- _async_client = None
74
+ client = _async_client
75
+ client_loop = _async_client_loop
76
+ _async_client = None
77
+ _async_client_loop = None
78
+ if client is None:
79
+ return
80
+
81
+ current_loop = asyncio.get_running_loop()
82
+ if client_loop is current_loop:
83
+ await client.aclose()
@@ -71,11 +71,17 @@ def print_red(s: str):
71
71
  print(Fore.RED + s + Style.RESET_ALL)
72
72
 
73
73
 
74
+ def to_type_module_alias(function_name: str) -> str:
75
+ """Return the internal alias used for a function's generated type module."""
76
+ return f"_{to_func_namespace(function_name)}_types"
77
+
78
+
74
79
  def add_type_import_path(function_name: str, arg: str) -> str:
75
80
  """if not basic type, coerce to camelCase and add the import path"""
76
81
  # from now, we start qualifying non-basic types :))
77
82
  # e.g. Callable[[EmailAddress, Dict, Dict, Dict], None]
78
83
  # becomes Callable[[Set_profile_email.EmailAddress, Dict, Dict, Dict], None]
84
+ type_module_alias = to_type_module_alias(function_name)
79
85
 
80
86
  if arg.startswith("Callable"):
81
87
  inner = arg[len("Callable["):-1] # strip outer Callable[...]
@@ -84,7 +90,7 @@ def add_type_import_path(function_name: str, arg: str) -> str:
84
90
  for p in parts:
85
91
  clean = p.strip("[] ")
86
92
  if clean and clean not in BASIC_PYTHON_TYPES:
87
- replacement = f"{to_func_namespace(function_name)}.{camelCase(clean)}"
93
+ replacement = f"{type_module_alias}.{camelCase(clean)}"
88
94
  p = p.replace(clean, replacement)
89
95
  qualified.append(p)
90
96
  return "Callable[" + ",".join(qualified) + "]"
@@ -100,11 +106,11 @@ def add_type_import_path(function_name: str, arg: str) -> str:
100
106
  else:
101
107
  if '"' in sub:
102
108
  sub = sub.replace('"', "")
103
- return f'List["{to_func_namespace(function_name)}.{camelCase(sub)}"]'
109
+ return f'List["{type_module_alias}.{camelCase(sub)}"]'
104
110
  else:
105
- return f"List[{to_func_namespace(function_name)}.{camelCase(sub)}]"
111
+ return f"List[{type_module_alias}.{camelCase(sub)}]"
106
112
 
107
- return f"{to_func_namespace(function_name)}.{camelCase(arg)}"
113
+ return f"{type_module_alias}.{camelCase(arg)}"
108
114
 
109
115
 
110
116
  def get_type_and_def(
@@ -352,4 +358,4 @@ def return_type_already_defined_in_args(return_type_name: str, args_def: str) ->
352
358
  basic_match = bool(re.search(basic_pattern, args_def, re.MULTILINE))
353
359
  class_pattern = rf"^class {re.escape(return_type_name)}\(TypedDict"
354
360
  class_match = bool(re.search(class_pattern, args_def, re.MULTILINE))
355
- return basic_match or class_match
361
+ return basic_match or class_match
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.15
3
+ Version: 0.3.16.dev2
4
4
  Summary: The Python Client for PolyAPI, the IPaaS by Developers for Developers
5
5
  Author-email: Dan Fellin <dan@polyapi.io>
6
6
  License: MIT License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "polyapi-python"
7
- version = "0.3.15"
7
+ version = "0.3.16.dev2"
8
8
  description = "The Python Client for PolyAPI, the IPaaS by Developers for Developers"
9
9
  authors = [{ name = "Dan Fellin", email = "dan@polyapi.io" }]
10
10
  dependencies = [
@@ -1,7 +1,7 @@
1
1
  import unittest
2
2
 
3
3
  from polyapi.api import render_api_function
4
- from polyapi.utils import to_func_namespace
4
+ from polyapi.utils import to_type_module_alias
5
5
 
6
6
  ACCUWEATHER = {
7
7
  "id": "f7588018-2364-4586-b60d",
@@ -236,7 +236,7 @@ class T(unittest.TestCase):
236
236
  )
237
237
  self.assertIn(ACCUWEATHER["id"], func_str)
238
238
  self.assertIn("locationId: int,", func_str)
239
- self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str)
239
+ self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str)
240
240
 
241
241
  def test_render_function_zillow(self):
242
242
  name = ZILLOW["name"]
@@ -250,7 +250,7 @@ class T(unittest.TestCase):
250
250
  )
251
251
  self.assertIn(ZILLOW["id"], func_str)
252
252
  self.assertIn("locationId: int,", func_str)
253
- self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str)
253
+ self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str)
254
254
 
255
255
  def test_render_function_twilio_api(self):
256
256
  name = TWILIO["name"]
@@ -265,7 +265,7 @@ class T(unittest.TestCase):
265
265
  self.assertIn(TWILIO["id"], func_str)
266
266
  self.assertIn("conversationSID: str", func_str)
267
267
  self.assertIn("authToken: str", func_str)
268
- self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str)
268
+ self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str)
269
269
 
270
270
  def test_render_function_twilio_get_details(self):
271
271
  # same test but try it as a serverFunction rather than an apiFunction
@@ -279,6 +279,6 @@ class T(unittest.TestCase):
279
279
  TWILIO_GET_DETAILS["function"]["returnType"],
280
280
  )
281
281
  self.assertIn(TWILIO_GET_DETAILS["id"], func_str)
282
- self.assertIn(f"-> {to_func_namespace(name)}.{name}Response", func_str)
282
+ self.assertIn(f"-> {to_type_module_alias(name)}.{name}Response", func_str)
283
283
  self.assertIn("class SubresourceUris", func_type_defs)
284
284
  # self.assertIn('Required["SubresourceUris"]', func_type_defs)
@@ -56,10 +56,12 @@ class TestHttpClientPairing:
56
56
  # Reset singletons so each test starts fresh
57
57
  http_client._sync_client = None
58
58
  http_client._async_client = None
59
+ http_client._async_client_loop = None
59
60
 
60
61
  def teardown_method(self):
61
62
  http_client._sync_client = None
62
63
  http_client._async_client = None
64
+ http_client._async_client_loop = None
63
65
 
64
66
  @patch.object(httpx.Client, "post", return_value=_fake_response())
65
67
  def test_sync_post_uses_sync_client(self, mock_post):
@@ -79,6 +81,90 @@ class TestHttpClientPairing:
79
81
  mock_post.assert_called_once()
80
82
  assert resp.status_code == 200
81
83
  assert http_client._async_client is not None
84
+ assert http_client._async_client_loop is not None
85
+
86
+ def test_async_post_reuses_client_within_same_loop(self):
87
+ first_client = MagicMock()
88
+ first_client.post = AsyncMock(return_value=_fake_response())
89
+
90
+ with patch("polyapi.http_client.httpx.AsyncClient", return_value=first_client) as mock_async_client:
91
+ async def _run():
92
+ first_response = await http_client.async_post("https://example.com/first", json={})
93
+ second_response = await http_client.async_post("https://example.com/second", json={})
94
+ return first_response, second_response, asyncio.get_running_loop()
95
+
96
+ first_response, second_response, current_loop = asyncio.run(_run())
97
+
98
+ assert first_response.status_code == 200
99
+ assert second_response.status_code == 200
100
+ assert mock_async_client.call_count == 1
101
+ assert first_client.post.await_count == 2
102
+ assert http_client._async_client is first_client
103
+ assert http_client._async_client_loop is current_loop
104
+
105
+ def test_async_post_recreates_client_after_loop_change(self):
106
+ first_client = MagicMock()
107
+ first_client.post = AsyncMock(return_value=_fake_response())
108
+ second_client = MagicMock()
109
+ second_client.post = AsyncMock(return_value=_fake_response())
110
+
111
+ with patch(
112
+ "polyapi.http_client.httpx.AsyncClient",
113
+ side_effect=[first_client, second_client],
114
+ ) as mock_async_client:
115
+ async def _run_once(url: str):
116
+ response = await http_client.async_post(url, json={})
117
+ return response, http_client._async_client, asyncio.get_running_loop()
118
+
119
+ first_response, first_cached_client, first_loop = asyncio.run(_run_once("https://example.com/first"))
120
+ second_response, second_cached_client, second_loop = asyncio.run(_run_once("https://example.com/second"))
121
+
122
+ assert first_response.status_code == 200
123
+ assert second_response.status_code == 200
124
+ assert mock_async_client.call_count == 2
125
+ assert first_client.post.await_count == 1
126
+ assert second_client.post.await_count == 1
127
+ assert first_cached_client is first_client
128
+ assert second_cached_client is second_client
129
+ assert first_loop is not second_loop
130
+ assert http_client._async_client is second_client
131
+ assert http_client._async_client_loop is second_loop
132
+
133
+ def test_close_async_clears_cached_client_for_current_loop(self):
134
+ async def _run():
135
+ cached_client = MagicMock()
136
+ cached_client.aclose = AsyncMock()
137
+ http_client._async_client = cached_client
138
+ http_client._async_client_loop = asyncio.get_running_loop()
139
+
140
+ await http_client.close_async()
141
+
142
+ return cached_client
143
+
144
+ cached_client = asyncio.run(_run())
145
+
146
+ cached_client.aclose.assert_awaited_once()
147
+ assert http_client._async_client is None
148
+ assert http_client._async_client_loop is None
149
+
150
+ def test_close_async_drops_stale_client_without_cross_loop_close(self):
151
+ stale_client = MagicMock()
152
+ stale_client.aclose = AsyncMock()
153
+
154
+ async def _seed_stale_client():
155
+ http_client._async_client = stale_client
156
+ http_client._async_client_loop = asyncio.get_running_loop()
157
+
158
+ asyncio.run(_seed_stale_client())
159
+
160
+ async def _close_on_new_loop():
161
+ await http_client.close_async()
162
+
163
+ asyncio.run(_close_on_new_loop())
164
+
165
+ stale_client.aclose.assert_not_awaited()
166
+ assert http_client._async_client is None
167
+ assert http_client._async_client_loop is None
82
168
 
83
169
  @patch.object(httpx.Client, "get", return_value=_fake_response())
84
170
  def test_sync_get(self, mock_get):
@@ -2,9 +2,11 @@ import unittest
2
2
  import os
3
3
  import shutil
4
4
  import importlib.util
5
+ import tempfile
5
6
  from unittest.mock import patch, MagicMock
6
- from polyapi.utils import get_type_and_def, rewrite_reserved
7
- from polyapi.generate import render_spec, create_empty_schemas_module, generate_functions, create_function
7
+ from polyapi.typedefs import SpecificationDto
8
+ from polyapi.utils import get_type_and_def, rewrite_reserved, to_type_module_alias
9
+ from polyapi.generate import render_spec, create_empty_schemas_module, generate_functions, create_function, add_function_file
8
10
  from polyapi.poly_schemas import generate_schemas, create_schema
9
11
  from polyapi.variables import generate_variables, create_variable
10
12
 
@@ -660,3 +662,45 @@ def test_nested_function() -> schemas.api.v1.user.profile:
660
662
  # The failed function directory should not exist
661
663
  func_dir = os.path.join(context_dir, "failing_function")
662
664
  self.assertFalse(os.path.exists(func_dir))
665
+
666
+ def test_add_function_file_aliases_pascal_case_type_module(self):
667
+ spec: SpecificationDto = {
668
+ "id": "test-func-id",
669
+ "name": "CreateAJsonPost",
670
+ "context": "test_context",
671
+ "type": "apiFunction",
672
+ "description": "Test function",
673
+ "language": "python",
674
+ "function": {
675
+ "arguments": [],
676
+ "returnType": {
677
+ "kind": "object",
678
+ "schema": {
679
+ "type": "object",
680
+ "properties": {
681
+ "id": {"type": "integer"},
682
+ },
683
+ "required": ["id"],
684
+ "title": "ReturnType",
685
+ },
686
+ },
687
+ },
688
+ }
689
+
690
+ with tempfile.TemporaryDirectory() as temp_dir:
691
+ add_function_file(temp_dir, spec["name"], spec)
692
+
693
+ init_path = os.path.join(temp_dir, "__init__.py")
694
+ with open(init_path, "r", encoding="utf-8") as file:
695
+ init_content = file.read()
696
+
697
+ type_module_alias = to_type_module_alias(spec["name"])
698
+ self.assertIn(
699
+ f"from . import {spec['name']} as {type_module_alias}",
700
+ init_content,
701
+ )
702
+ self.assertIn(f"def {spec['name']}(", init_content)
703
+ self.assertIn(
704
+ f"-> {type_module_alias}.{spec['name']}Response",
705
+ init_content,
706
+ )
@@ -1,6 +1,6 @@
1
1
  import unittest
2
2
 
3
- from polyapi.utils import to_func_namespace
3
+ from polyapi.utils import to_type_module_alias
4
4
 
5
5
  from .test_api import TWILIO
6
6
  from polyapi.server import render_server_function
@@ -81,7 +81,7 @@ class T(unittest.TestCase):
81
81
  self.assertIn(TWILIO["id"], func_str)
82
82
  self.assertIn("conversationSID: str", func_str)
83
83
  self.assertIn("authToken: str", func_str)
84
- self.assertIn(f"-> {to_func_namespace(name)}.ResponseType", func_str)
84
+ self.assertIn(f"-> {to_type_module_alias(name)}.ResponseType", func_str)
85
85
 
86
86
  def test_render_function_get_products_count(self):
87
87
  return_type = GET_PRODUCTS_COUNT["function"]["returnType"]
@@ -116,4 +116,4 @@ class T(unittest.TestCase):
116
116
 
117
117
  # stay_date: Required[str]
118
118
  # """ Required property """'''
119
- # self.assertIn(expected_return_type, func_str)
119
+ # self.assertIn(expected_return_type, func_str)