polyapi-python 0.3.20.dev1__py3-none-any.whl → 0.3.20.dev3__py3-none-any.whl

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.
polyapi/__init__.py CHANGED
@@ -1,25 +1,36 @@
1
1
  import copy
2
+ import importlib
2
3
  import os
3
- import sys
4
4
  from contextvars import ContextVar, Token
5
5
  from dataclasses import dataclass
6
- from typing import Any, Dict, Literal, Optional, overload
6
+ from types import ModuleType
7
+ from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, overload
7
8
 
8
9
  import truststore
9
10
  from typing_extensions import TypedDict
10
11
 
11
- from .cli_constants import CLI_COMMANDS
12
-
13
12
  truststore.inject_into_ssl()
14
13
 
15
14
  __all__ = ["poly"]
16
15
 
16
+ if TYPE_CHECKING:
17
+ poly: ModuleType
18
+
17
19
 
18
- if len(sys.argv) > 1 and sys.argv[1] not in CLI_COMMANDS:
20
+ def _load_generated_poly() -> ModuleType:
19
21
  currdir = os.path.dirname(os.path.abspath(__file__))
20
22
  if not os.path.isdir(os.path.join(currdir, "poly")):
21
- print("No 'poly' found. Please run 'python3 -m polyapi generate' to generate the 'poly' library for your tenant.")
22
- sys.exit(1)
23
+ raise ModuleNotFoundError(
24
+ "No 'poly' found. Please run 'python3 -m polyapi generate' "
25
+ "to generate the 'poly' library for your tenant."
26
+ )
27
+ return importlib.import_module(".poly", __name__)
28
+
29
+
30
+ def __getattr__(name: str) -> ModuleType:
31
+ if name == "poly":
32
+ return _load_generated_poly()
33
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
23
34
 
24
35
 
25
36
  class PolyCustomDict(TypedDict, total=False):
polyapi/generate.py CHANGED
@@ -53,7 +53,7 @@ def get_specs(contexts: Optional[List[str]] = None, names: Optional[List[str]] =
53
53
  params: Any = {"noTypes": str(no_types).lower()}
54
54
 
55
55
  if contexts:
56
- params["contexts"] = contexts
56
+ params["contexts"] = ",".join(contexts)
57
57
 
58
58
  if names:
59
59
  params["names"] = names
polyapi/http_client.py CHANGED
@@ -1,83 +1,350 @@
1
1
  import asyncio
2
+ import logging
3
+ import os
4
+ import threading
2
5
  import httpx
3
6
 
7
+ logger = logging.getLogger("poly")
8
+
9
+ # Connx pool + timeout defaults for the shared clients.
10
+ # All overridable via POLY_HTTP_* env vars so operators can tune limits or
11
+ # opt into timeouts without editing callers. Empty / "none" parses to None (unbounded).
12
+
13
+
14
+ def _env_opt_int(name: str, default: "int | None") -> "int | None":
15
+ raw = os.environ.get(name)
16
+ if raw is None:
17
+ return default
18
+ raw = raw.strip()
19
+ return None if raw == "" or raw.lower() in ("none", "null") else int(raw)
20
+
21
+
22
+ def _env_opt_float(name: str, default: "float | None") -> "float | None":
23
+ raw = os.environ.get(name)
24
+ if raw is None:
25
+ return default
26
+ raw = raw.strip()
27
+ return None if raw == "" or raw.lower() in ("none", "null") else float(raw)
28
+
29
+
30
+ def _build_limits() -> httpx.Limits:
31
+ return httpx.Limits(
32
+ max_connections=_env_opt_int("POLY_HTTP_MAX_CONNECTIONS", 200),
33
+ max_keepalive_connections=_env_opt_int("POLY_HTTP_MAX_KEEPALIVE_CONNECTIONS", None),
34
+ keepalive_expiry=_env_opt_float("POLY_HTTP_KEEPALIVE_EXPIRY", 30.0),
35
+ )
36
+
37
+
38
+ def _build_timeout() -> httpx.Timeout:
39
+ # Default to the SDK's original unbounded timeout; bound per-phase only via env.
40
+ return httpx.Timeout(
41
+ connect=_env_opt_float("POLY_HTTP_CONNECT_TIMEOUT", None),
42
+ read=_env_opt_float("POLY_HTTP_READ_TIMEOUT", None),
43
+ write=_env_opt_float("POLY_HTTP_WRITE_TIMEOUT", None),
44
+ pool=_env_opt_float("POLY_HTTP_POOL_TIMEOUT", None),
45
+ )
46
+
47
+
48
+ # Import-time snapshot for reference/config surface. Clients re-read env at creation
49
+ DEFAULT_LIMITS = _build_limits()
50
+ DEFAULT_TIMEOUT = _build_timeout()
51
+ # PID that owns the clients below; a fork resets it
52
+ _owner_pid: int = os.getpid()
4
53
  _sync_client: httpx.Client | None = None
5
- _async_client: httpx.AsyncClient | None = None
6
- _async_client_loop: asyncio.AbstractEventLoop | None = None
54
+ # Coordinate lazy creation, active requests, and shutdown of _sync_client.
55
+ _sync_client_lock = threading.Lock()
56
+ _sync_client_condition = threading.Condition(_sync_client_lock)
57
+ _sync_inflight = 0
58
+ _sync_closing = False
59
+ # One async client per event loop.
60
+ _async_clients: "dict[asyncio.AbstractEventLoop, httpx.AsyncClient]" = {}
61
+ # In-flight request count per async client, so close_async can drain before aclose.
62
+ _async_inflight: "dict[httpx.AsyncClient, int]" = {}
63
+ # Event loops whose close() already wrapped
64
+ _hooked_loops: "set[asyncio.AbstractEventLoop]" = set()
65
+ # Shared close operation per event loop. New requests wait for it so they do not
66
+ # race a replacement client into the pool mid-close.
67
+ _closing_loops: "dict[asyncio.AbstractEventLoop, asyncio.Task[None]]" = {}
7
68
 
8
69
 
9
- def _get_sync_client() -> httpx.Client:
70
+ def _after_fork_in_child() -> None:
71
+ """Reset all shared client state in a forked child
72
+ """
73
+ global _owner_pid, _sync_client, _sync_client_lock, _sync_client_condition
74
+ global _sync_inflight, _sync_closing
75
+ _sync_client_lock = threading.Lock()
76
+ _sync_client_condition = threading.Condition(_sync_client_lock)
77
+ _sync_client = None
78
+ _sync_inflight = 0
79
+ _sync_closing = False
80
+ _async_clients.clear()
81
+ _async_inflight.clear()
82
+ _hooked_loops.clear()
83
+ _closing_loops.clear()
84
+ _owner_pid = os.getpid()
85
+
86
+
87
+ if hasattr(os, "register_at_fork"):
88
+ os.register_at_fork(after_in_child=_after_fork_in_child)
89
+
90
+
91
+ def _reset_if_forked() -> None:
92
+ """Drop clients inherited across os.fork() so childisolated
93
+ Clear not close the inherited refs - child makes its own."""
94
+ global _owner_pid, _sync_client, _sync_client_lock, _sync_client_condition
95
+ global _sync_inflight, _sync_closing
96
+ current_pid = os.getpid()
97
+ if current_pid != _owner_pid:
98
+ _sync_client_lock = threading.Lock()
99
+ _sync_client_condition = threading.Condition(_sync_client_lock)
100
+ _sync_client = None
101
+ _sync_inflight = 0
102
+ _sync_closing = False
103
+ _async_clients.clear()
104
+ _async_inflight.clear()
105
+ _hooked_loops.clear()
106
+ _closing_loops.clear()
107
+ _owner_pid = current_pid
108
+
109
+
110
+ def _get_or_create_sync_client() -> httpx.Client:
10
111
  global _sync_client
11
112
  if _sync_client is None:
12
- _sync_client = httpx.Client(timeout=None)
113
+ _sync_client = httpx.Client(limits=_build_limits(), timeout=_build_timeout())
13
114
  return _sync_client
14
115
 
15
116
 
117
+ def _get_sync_client() -> httpx.Client:
118
+ _reset_if_forked()
119
+ with _sync_client_condition:
120
+ while _sync_closing:
121
+ _sync_client_condition.wait()
122
+ return _get_or_create_sync_client()
123
+
124
+
125
+ def _dispatch_sync(method_name: str, *args, **kwargs) -> httpx.Response:
126
+ global _sync_inflight
127
+ _reset_if_forked()
128
+ with _sync_client_condition:
129
+ while _sync_closing:
130
+ _sync_client_condition.wait()
131
+ client = _get_or_create_sync_client()
132
+ _sync_inflight += 1
133
+ try:
134
+ return getattr(client, method_name)(*args, **kwargs)
135
+ finally:
136
+ with _sync_client_condition:
137
+ _sync_inflight -= 1
138
+ if _sync_inflight == 0:
139
+ _sync_client_condition.notify_all()
140
+
141
+
142
+ def _retire_async_client(
143
+ client: httpx.AsyncClient | None,
144
+ loop: asyncio.AbstractEventLoop | None,
145
+ ) -> None:
146
+ """Best-effort close of an async client whose event loop is being dumped.
147
+ """
148
+ if client is None:
149
+ return
150
+ try:
151
+ if loop is not None and loop.is_running():
152
+ loop.call_soon_threadsafe(lambda: loop.create_task(client.aclose()))
153
+ logger.debug(f"Scheduled aclose of async client id={id(client)} pid={os.getpid()} loop={id(loop)}")
154
+ else:
155
+ loop_closed = None if loop is None else loop.is_closed()
156
+ logger.debug(f"Dropping async client id={id(client)} pid={os.getpid()} loop={id(loop)} loop_closed={loop_closed}")
157
+ except Exception:
158
+ logger.warning(f"Failed to retire async client id={id(client)} pid={os.getpid()} loop={id(loop)}", exc_info=True)
159
+
160
+
161
+ def _sweep_dead_loops() -> None:
162
+ """Wipe clients whose event loop has been closed.
163
+ """
164
+ for loop in list(_async_clients):
165
+ if loop.is_closed():
166
+ _retire_async_client(_async_clients.pop(loop, None), loop)
167
+
168
+
169
+ def _register_loop_close_hook(loop: asyncio.AbstractEventLoop) -> None:
170
+ """aclose the loop's async client right before the loop is torn down.
171
+ """
172
+ if loop in _hooked_loops:
173
+ return
174
+ original_close = loop.close
175
+
176
+ def _close_with_cleanup(*args, **kwargs):
177
+ _hooked_loops.discard(loop)
178
+ cached = _async_clients.pop(loop, None)
179
+ if cached is not None and not loop.is_closed():
180
+ try:
181
+ loop.run_until_complete(cached.aclose())
182
+ except Exception:
183
+ logger.warning(
184
+ f"Failed to aclose async client id={id(cached)} pid={os.getpid()} "
185
+ f"loop={id(loop)} on teardown", exc_info=True)
186
+ return original_close(*args, **kwargs)
187
+
188
+ try:
189
+ loop.close = _close_with_cleanup # type: ignore[method-assign]
190
+ _hooked_loops.add(loop)
191
+ except (AttributeError, TypeError):
192
+ logger.warning(
193
+ f"Could not hook loop.close pid={os.getpid()} loop={id(loop)}; "
194
+ "await close_async() before closing this event loop"
195
+ )
196
+
197
+
16
198
  def _get_async_client() -> httpx.AsyncClient:
17
- global _async_client, _async_client_loop
199
+ _reset_if_forked()
18
200
  current_loop = asyncio.get_running_loop()
19
- if _async_client is None or _async_client_loop is not current_loop:
20
- _async_client = httpx.AsyncClient(timeout=None)
21
- _async_client_loop = current_loop
22
- return _async_client
201
+ client = _async_clients.get(current_loop)
202
+ if client is None:
203
+ _sweep_dead_loops()
204
+ limits = _build_limits()
205
+ client = httpx.AsyncClient(limits=limits, timeout=_build_timeout())
206
+ _async_clients[current_loop] = client
207
+ _register_loop_close_hook(current_loop)
208
+ logger.debug(
209
+ f"Created async client id={id(client)} pid={os.getpid()} loop={id(current_loop)} "
210
+ f"max_connections={limits.max_connections} "
211
+ f"max_keepalive={limits.max_keepalive_connections} "
212
+ f"keepalive_expiry={limits.keepalive_expiry}"
213
+ )
214
+ return client
215
+
216
+
217
+ async def _dispatch_async(method_name: str, *args, **kwargs) -> httpx.Response:
218
+ # If a shutdown is closing this loop's client, wait so a replacement is not
219
+ # published mid-close. The shutdown caller owns any close error.
220
+ closing = _closing_loops.get(asyncio.get_running_loop())
221
+ if closing is not None:
222
+ try:
223
+ await asyncio.shield(closing)
224
+ except Exception:
225
+ pass
226
+ # Track in-flight requests per client so close_async can drain before aclose.
227
+ client = _get_async_client()
228
+ _async_inflight[client] = _async_inflight.get(client, 0) + 1
229
+ try:
230
+ return await getattr(client, method_name)(*args, **kwargs)
231
+ finally:
232
+ remaining = _async_inflight.get(client, 1) - 1
233
+ if remaining <= 0:
234
+ _async_inflight.pop(client, None)
235
+ else:
236
+ _async_inflight[client] = remaining
237
+
238
+
239
+ async def _drain_inflight(client: httpx.AsyncClient) -> None:
240
+ # Preserve the SDK's unbounded request behavior during shutdown. The shared
241
+ # close operation retains ownership until every active request finishes.
242
+ while _async_inflight.get(client, 0) > 0:
243
+ await asyncio.sleep(0.05)
244
+
245
+
246
+ async def _wait_for_close_task(close_task: "asyncio.Task[None]") -> None:
247
+ """Wait for shared cleanup before propagating caller cancellation."""
248
+ caller_cancelled = False
249
+ while not close_task.done():
250
+ try:
251
+ await asyncio.shield(close_task)
252
+ except asyncio.CancelledError:
253
+ caller_cancelled = True
254
+ close_task.result()
255
+ if caller_cancelled:
256
+ raise asyncio.CancelledError()
257
+
258
+
259
+ async def _close_clients_for_loop(
260
+ current_loop: asyncio.AbstractEventLoop,
261
+ ) -> None:
262
+ """Close shared clients while retaining ownership through completion."""
263
+ try:
264
+ await asyncio.to_thread(close)
265
+ client = _async_clients.get(current_loop)
266
+ if client is None:
267
+ return
268
+
269
+ await _drain_inflight(client)
270
+ try:
271
+ await client.aclose()
272
+ finally:
273
+ if _async_clients.get(current_loop) is client:
274
+ _async_clients.pop(current_loop, None)
275
+ _async_inflight.pop(client, None)
276
+ finally:
277
+ if _closing_loops.get(current_loop) is asyncio.current_task():
278
+ _closing_loops.pop(current_loop, None)
279
+ _sweep_dead_loops()
23
280
 
24
281
 
25
282
  def post(url, **kwargs) -> httpx.Response:
26
- return _get_sync_client().post(url, **kwargs)
283
+ return _dispatch_sync("post", url, **kwargs)
27
284
 
28
285
 
29
286
  async def async_post(url, **kwargs) -> httpx.Response:
30
- return await _get_async_client().post(url, **kwargs)
287
+ return await _dispatch_async("post", url, **kwargs)
31
288
 
32
289
 
33
290
  def get(url, **kwargs) -> httpx.Response:
34
- return _get_sync_client().get(url, **kwargs)
291
+ return _dispatch_sync("get", url, **kwargs)
35
292
 
36
293
 
37
294
  async def async_get(url, **kwargs) -> httpx.Response:
38
- return await _get_async_client().get(url, **kwargs)
295
+ return await _dispatch_async("get", url, **kwargs)
39
296
 
40
297
 
41
298
  def patch(url, **kwargs) -> httpx.Response:
42
- return _get_sync_client().patch(url, **kwargs)
299
+ return _dispatch_sync("patch", url, **kwargs)
43
300
 
44
301
 
45
302
  async def async_patch(url, **kwargs) -> httpx.Response:
46
- return await _get_async_client().patch(url, **kwargs)
303
+ return await _dispatch_async("patch", url, **kwargs)
47
304
 
48
305
 
49
306
  def delete(url, **kwargs) -> httpx.Response:
50
- return _get_sync_client().delete(url, **kwargs)
307
+ return _dispatch_sync("delete", url, **kwargs)
51
308
 
52
309
 
53
310
  async def async_delete(url, **kwargs) -> httpx.Response:
54
- return await _get_async_client().delete(url, **kwargs)
311
+ return await _dispatch_async("delete", url, **kwargs)
55
312
 
56
313
 
57
314
  def request(method, url, **kwargs) -> httpx.Response:
58
- return _get_sync_client().request(method, url, **kwargs)
315
+ return _dispatch_sync("request", method, url, **kwargs)
59
316
 
60
317
 
61
318
  async def async_request(method, url, **kwargs) -> httpx.Response:
62
- return await _get_async_client().request(method, url, **kwargs)
319
+ return await _dispatch_async("request", method, url, **kwargs)
63
320
 
64
321
 
65
322
  def close():
66
- global _sync_client
67
- if _sync_client is not None:
68
- _sync_client.close()
69
- _sync_client = None
323
+ global _sync_client, _sync_closing
324
+ _reset_if_forked()
325
+ with _sync_client_condition:
326
+ while _sync_closing:
327
+ _sync_client_condition.wait()
328
+ if _sync_client is None:
329
+ return
330
+ _sync_closing = True
331
+ while _sync_inflight > 0:
332
+ _sync_client_condition.wait()
333
+ client = _sync_client
334
+ try:
335
+ client.close()
336
+ finally:
337
+ with _sync_client_condition:
338
+ if _sync_client is client:
339
+ _sync_client = None
340
+ _sync_closing = False
341
+ _sync_client_condition.notify_all()
70
342
 
71
- async def close_async():
72
- global _async_client, _async_client_loop
73
- close()
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
343
 
344
+ async def close_async():
81
345
  current_loop = asyncio.get_running_loop()
82
- if client_loop is current_loop:
83
- await client.aclose()
346
+ close_task = _closing_loops.get(current_loop)
347
+ if close_task is None:
348
+ close_task = asyncio.create_task(_close_clients_for_loop(current_loop))
349
+ _closing_loops[current_loop] = close_task
350
+ await _wait_for_close_task(close_task)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: polyapi-python
3
- Version: 0.3.20.dev1
3
+ Version: 0.3.20.dev3
4
4
  Summary: The Python SDK for the integration platform PolyAPI
5
5
  Author-email: PolyAPI <support@polyapi.io>
6
6
  License: MIT License
@@ -84,6 +84,32 @@ POLY_API_KEY='your_key'
84
84
  POLY_API_BASE_URL='your_server' # e.g. na1.polyapi.io
85
85
  ```
86
86
 
87
+ ### HTTP Client Configuration
88
+
89
+ The SDK reuses one synchronous HTTP client per process and one asynchronous
90
+ client per event loop. New clients read these optional environment variables:
91
+
92
+ | Variable | Default | Description |
93
+ | --- | --- | --- |
94
+ | `POLY_HTTP_MAX_CONNECTIONS` | `200` | Maximum connections per client |
95
+ | `POLY_HTTP_MAX_KEEPALIVE_CONNECTIONS` | `none` | Maximum idle keep-alive connections per client |
96
+ | `POLY_HTTP_KEEPALIVE_EXPIRY` | `30` | Idle keep-alive expiry in seconds |
97
+ | `POLY_HTTP_CONNECT_TIMEOUT` | `none` | Connection timeout in seconds |
98
+ | `POLY_HTTP_READ_TIMEOUT` | `none` | Read timeout in seconds |
99
+ | `POLY_HTTP_WRITE_TIMEOUT` | `none` | Write timeout in seconds |
100
+ | `POLY_HTTP_POOL_TIMEOUT` | `none` | Connection-pool timeout in seconds |
101
+
102
+ An empty value, `none`, or `null` means unbounded. Limits are per client, so
103
+ processes with multiple event loops can create one configured pool per loop.
104
+
105
+ Call `polyapi.http_client.close()` during synchronous shutdown or await
106
+ `polyapi.http_client.close_async()` on each owning event loop during
107
+ asynchronous shutdown. The shutdown functions wait for active SDK requests
108
+ before closing their client. Clients on standard asyncio loops are closed
109
+ automatically when the loop closes. If a custom event-loop implementation
110
+ reports that its close hook could not be installed, explicitly await
111
+ `close_async()` before closing that loop.
112
+
87
113
  ### 3. Test
88
114
 
89
115
  That's it! Now open up a test file and you can run some code like so:
@@ -1,4 +1,4 @@
1
- polyapi/__init__.py,sha256=vV1N9xtIMtpi4o-2jAI3775LkzRbC9JUo87MAnW9zgo,5201
1
+ polyapi/__init__.py,sha256=oEijoOVghgeEbwqtWK_74QU38HBDt4THXHBbsortJ1w,5505
2
2
  polyapi/__main__.py,sha256=V4zhAh_YGxno5f_KSrlkELxcuDh9bR3WSd0n-2r-qQQ,93
3
3
  polyapi/ai_function.py,sha256=GvJKpBZFbI11y9JAKTHXMkztbMHZxNTfL37LLbGKmwI,3017
4
4
  polyapi/api.py,sha256=CzqX99GekhQJVqAAWlL03hUenIi_5iTln9cWgQzz6Ko,2784
@@ -13,8 +13,8 @@ polyapi/error_handler.py,sha256=I_e0iz6VM23FLVQWJljxs2NGcl_OODbi43OcbnqBlp8,2398
13
13
  polyapi/exceptions.py,sha256=Zh7i7eCUhDuXEdUYjatkLFTeZkrx1BJ1P5ePgbJ9eIY,89
14
14
  polyapi/execute.py,sha256=4jLkQely6cM7pNN9pYpGZTbEis-DfwENi5Fl07xSiig,8966
15
15
  polyapi/function_cli.py,sha256=gJE72lRvuWnGl6CG9GpqL78TOnUzxVe4kJMvg0PQixw,4493
16
- polyapi/generate.py,sha256=QLljL5dT7kyPrIPuOxEhqSE2ASm1XzLV7tAGrQOF8eI,22393
17
- polyapi/http_client.py,sha256=TcoFsIvRcNB9wWJT4YePPWFFY6lX90cdqi7C4jKbqqk,2259
16
+ polyapi/generate.py,sha256=U8JQ_XR2gJI_eYNnmvXReILUxapj95QKoBronqMFkyE,22403
17
+ polyapi/http_client.py,sha256=x2pBXjETq1YlDgCoM87PTr6holfTnA_FXpa_br_Yf3k,12194
18
18
  polyapi/parser.py,sha256=t3jJI6g5F7Qg_tirPOzSkQ02VLkSVhnqs9YGapyHx2k,21901
19
19
  polyapi/poly_schemas.py,sha256=JGMQbELPFw4V_0nA0o71WJKFTdxjCGoKqJvIid_Y-a4,10759
20
20
  polyapi/poly_tables.py,sha256=SlP7ZwqS3nIji2uGR2VJQH6IdddZ832rRItcmoYdIBc,20293
@@ -28,8 +28,8 @@ polyapi/typedefs.py,sha256=UeAdyYrgqxrvBRkjisK_xgzQmuTePPvOM74HKf1y0II,5656
28
28
  polyapi/utils.py,sha256=LUqTkbb0b7y3gjsOU-WO1LVYaTMyExTGpYjKY8wVKg8,16300
29
29
  polyapi/variables.py,sha256=hfSDPGQK6YphNCa9MqE0W88WIFfqCQWgpBDWkExMq-A,7582
30
30
  polyapi/webhook.py,sha256=0ceLwHNjNd2Yx_8MX5jOIxiQ5Lwhy2pe81ProAuKon0,5108
31
- polyapi_python-0.3.20.dev1.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
32
- polyapi_python-0.3.20.dev1.dist-info/METADATA,sha256=uQkx5Ut9vT6cP2Gt3rsw3yB1uSWGB9_Q85rel0BlzFg,5898
33
- polyapi_python-0.3.20.dev1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
34
- polyapi_python-0.3.20.dev1.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
35
- polyapi_python-0.3.20.dev1.dist-info/RECORD,,
31
+ polyapi_python-0.3.20.dev3.dist-info/licenses/LICENSE,sha256=6b_I7aPVp8JXhqQwdw7_B84Ca0S4JGjHj0sr_1VOdB4,1068
32
+ polyapi_python-0.3.20.dev3.dist-info/METADATA,sha256=r1r6mN2pdcMXBav1GktbhEUPRGgpe_a1VlFsj91QrhQ,7300
33
+ polyapi_python-0.3.20.dev3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
+ polyapi_python-0.3.20.dev3.dist-info/top_level.txt,sha256=CEFllOnzowci_50RYJac-M54KD2IdAptFsayVVF_f04,8
35
+ polyapi_python-0.3.20.dev3.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (83.0.0)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5