pi-ai-python 1.3.0__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.
- pi_agent/__init__.py +127 -0
- pi_agent/agent.py +617 -0
- pi_agent/agent_loop.py +820 -0
- pi_agent/proxy.py +269 -0
- pi_agent/py.typed +0 -0
- pi_agent/stream_fn.py +27 -0
- pi_agent/tools.py +327 -0
- pi_agent/types.py +394 -0
- pi_ai/__init__.py +263 -0
- pi_ai/abort.py +315 -0
- pi_ai/api/__init__.py +1 -0
- pi_ai/api/anthropic_messages.py +1782 -0
- pi_ai/api/azure_openai_config.py +138 -0
- pi_ai/api/azure_openai_responses.py +245 -0
- pi_ai/api/bedrock_converse_stream.py +1598 -0
- pi_ai/api/classifier_shared.py +211 -0
- pi_ai/api/cloudflare.py +36 -0
- pi_ai/api/cloudflare_workers_ai_system_one.py +80 -0
- pi_ai/api/constrained_sampling.py +279 -0
- pi_ai/api/github_copilot_headers.py +30 -0
- pi_ai/api/google_generative_ai.py +138 -0
- pi_ai/api/google_shared.py +927 -0
- pi_ai/api/google_vertex.py +234 -0
- pi_ai/api/lazy.py +156 -0
- pi_ai/api/llama_cpp_classify.py +449 -0
- pi_ai/api/mistral_conversations.py +756 -0
- pi_ai/api/openai_codex_responses.py +1545 -0
- pi_ai/api/openai_completions.py +1594 -0
- pi_ai/api/openai_decisions.py +210 -0
- pi_ai/api/openai_prompt_cache.py +19 -0
- pi_ai/api/openai_responses.py +418 -0
- pi_ai/api/openai_responses_shared.py +840 -0
- pi_ai/api/openai_sdk.py +204 -0
- pi_ai/api/openrouter_images.py +153 -0
- pi_ai/api/pi_messages.py +449 -0
- pi_ai/api/registry.py +103 -0
- pi_ai/api/simple_options.py +119 -0
- pi_ai/api/system_one_shared.py +148 -0
- pi_ai/api/transform_messages.py +211 -0
- pi_ai/api/typesafe_system_one.py +43 -0
- pi_ai/auth/__init__.py +1 -0
- pi_ai/auth/context.py +47 -0
- pi_ai/auth/credential_store.py +208 -0
- pi_ai/auth/helpers.py +123 -0
- pi_ai/auth/oauth/__init__.py +5 -0
- pi_ai/auth/oauth/_common.py +454 -0
- pi_ai/auth/oauth/anthropic.py +283 -0
- pi_ai/auth/oauth/callback_server.py +339 -0
- pi_ai/auth/oauth/device_code.py +127 -0
- pi_ai/auth/oauth/github_copilot.py +508 -0
- pi_ai/auth/oauth/kimi_coding.py +283 -0
- pi_ai/auth/oauth/load.py +64 -0
- pi_ai/auth/oauth/meta.py +238 -0
- pi_ai/auth/oauth/openai_chatgpt.py +321 -0
- pi_ai/auth/oauth/openai_codex.py +391 -0
- pi_ai/auth/oauth/openrouter.py +173 -0
- pi_ai/auth/oauth/pkce.py +23 -0
- pi_ai/auth/oauth/radius.py +311 -0
- pi_ai/auth/oauth/xai.py +220 -0
- pi_ai/auth/resolve.py +190 -0
- pi_ai/auth/types.py +292 -0
- pi_ai/cli.py +265 -0
- pi_ai/compat.py +651 -0
- pi_ai/env_api_keys.py +137 -0
- pi_ai/event_stream.py +329 -0
- pi_ai/models.py +1191 -0
- pi_ai/models_store.py +121 -0
- pi_ai/oauth.py +90 -0
- pi_ai/providers/__init__.py +1 -0
- pi_ai/providers/all.py +154 -0
- pi_ai/providers/amazon_bedrock.py +116 -0
- pi_ai/providers/ant_ling.py +23 -0
- pi_ai/providers/anthropic.py +105 -0
- pi_ai/providers/azure.py +103 -0
- pi_ai/providers/baseten.py +23 -0
- pi_ai/providers/catalog.py +167 -0
- pi_ai/providers/cerebras.py +23 -0
- pi_ai/providers/cloudflare_ai_gateway.py +30 -0
- pi_ai/providers/cloudflare_auth.py +112 -0
- pi_ai/providers/cloudflare_stream.py +84 -0
- pi_ai/providers/cloudflare_workers_ai.py +26 -0
- pi_ai/providers/data/amazon-bedrock.json +1 -0
- pi_ai/providers/data/ant-ling.json +1 -0
- pi_ai/providers/data/anthropic.json +1 -0
- pi_ai/providers/data/azure.json +1 -0
- pi_ai/providers/data/baseten.json +1 -0
- pi_ai/providers/data/cerebras.json +1 -0
- pi_ai/providers/data/cloudflare-ai-gateway.json +1 -0
- pi_ai/providers/data/cloudflare-workers-ai.json +1 -0
- pi_ai/providers/data/deepseek.json +1 -0
- pi_ai/providers/data/fireworks.json +1 -0
- pi_ai/providers/data/github-copilot.json +1 -0
- pi_ai/providers/data/google-vertex.json +1 -0
- pi_ai/providers/data/google.json +1 -0
- pi_ai/providers/data/groq.json +1 -0
- pi_ai/providers/data/huggingface.json +1 -0
- pi_ai/providers/data/kimi-coding.json +1 -0
- pi_ai/providers/data/manifest.json +1 -0
- pi_ai/providers/data/meta.json +1 -0
- pi_ai/providers/data/minimax-cn.json +1 -0
- pi_ai/providers/data/minimax.json +1 -0
- pi_ai/providers/data/mistral.json +1 -0
- pi_ai/providers/data/moonshotai-cn.json +1 -0
- pi_ai/providers/data/moonshotai.json +1 -0
- pi_ai/providers/data/nvidia.json +1 -0
- pi_ai/providers/data/openai-codex.json +1 -0
- pi_ai/providers/data/openai.json +1 -0
- pi_ai/providers/data/opencode-go.json +1 -0
- pi_ai/providers/data/opencode.json +1 -0
- pi_ai/providers/data/openrouter.json +1 -0
- pi_ai/providers/data/qwen-token-plan-cn.json +1 -0
- pi_ai/providers/data/qwen-token-plan-individual.json +1 -0
- pi_ai/providers/data/qwen-token-plan.json +1 -0
- pi_ai/providers/data/radius.json +1 -0
- pi_ai/providers/data/together.json +1 -0
- pi_ai/providers/data/typesafe.json +1 -0
- pi_ai/providers/data/vercel-ai-gateway.json +1 -0
- pi_ai/providers/data/xai.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-ams.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-cn.json +1 -0
- pi_ai/providers/data/xiaomi-token-plan-sgp.json +1 -0
- pi_ai/providers/data/xiaomi.json +1 -0
- pi_ai/providers/data/zai-coding-cn.json +1 -0
- pi_ai/providers/data/zai.json +1 -0
- pi_ai/providers/deepseek.py +23 -0
- pi_ai/providers/faux.py +680 -0
- pi_ai/providers/fireworks.py +26 -0
- pi_ai/providers/github_copilot.py +46 -0
- pi_ai/providers/google.py +23 -0
- pi_ai/providers/google_vertex.py +131 -0
- pi_ai/providers/groq.py +23 -0
- pi_ai/providers/huggingface.py +23 -0
- pi_ai/providers/kimi_coding.py +32 -0
- pi_ai/providers/meta.py +32 -0
- pi_ai/providers/minimax.py +23 -0
- pi_ai/providers/minimax_cn.py +23 -0
- pi_ai/providers/mistral.py +23 -0
- pi_ai/providers/moonshotai.py +23 -0
- pi_ai/providers/moonshotai_cn.py +23 -0
- pi_ai/providers/nvidia.py +23 -0
- pi_ai/providers/openai.py +44 -0
- pi_ai/providers/openai_codex.py +26 -0
- pi_ai/providers/opencode.py +36 -0
- pi_ai/providers/opencode_go.py +33 -0
- pi_ai/providers/opencode_headers.py +60 -0
- pi_ai/providers/openrouter.py +145 -0
- pi_ai/providers/openrouter_api.py +122 -0
- pi_ai/providers/qwen_token_plan.py +23 -0
- pi_ai/providers/qwen_token_plan_cn.py +23 -0
- pi_ai/providers/qwen_token_plan_individual.py +23 -0
- pi_ai/providers/radius.py +146 -0
- pi_ai/providers/radius_config.py +136 -0
- pi_ai/providers/together.py +23 -0
- pi_ai/providers/typesafe.py +22 -0
- pi_ai/providers/vercel_ai_gateway.py +25 -0
- pi_ai/providers/xai.py +32 -0
- pi_ai/providers/xiaomi.py +23 -0
- pi_ai/providers/xiaomi_token_plan_ams.py +23 -0
- pi_ai/providers/xiaomi_token_plan_cn.py +23 -0
- pi_ai/providers/xiaomi_token_plan_sgp.py +23 -0
- pi_ai/providers/zai.py +23 -0
- pi_ai/providers/zai_coding_cn.py +23 -0
- pi_ai/py.typed +0 -0
- pi_ai/serialization.py +477 -0
- pi_ai/session_resources.py +33 -0
- pi_ai/types.py +1003 -0
- pi_ai/utils/__init__.py +1 -0
- pi_ai/utils/callbacks.py +15 -0
- pi_ai/utils/diagnostics.py +70 -0
- pi_ai/utils/error_body.py +89 -0
- pi_ai/utils/estimate.py +125 -0
- pi_ai/utils/fastjson.py +63 -0
- pi_ai/utils/http.py +409 -0
- pi_ai/utils/json_parse.py +424 -0
- pi_ai/utils/misc.py +193 -0
- pi_ai/utils/model_operations.py +73 -0
- pi_ai/utils/models_error.py +35 -0
- pi_ai/utils/oauth_page.py +121 -0
- pi_ai/utils/overflow.py +87 -0
- pi_ai/utils/retry.py +193 -0
- pi_ai/utils/schema.py +283 -0
- pi_ai/utils/text.py +65 -0
- pi_ai/utils/transcript.py +271 -0
- pi_ai/utils/validation.py +813 -0
- pi_ai_python-1.3.0.dist-info/METADATA +167 -0
- pi_ai_python-1.3.0.dist-info/RECORD +189 -0
- pi_ai_python-1.3.0.dist-info/WHEEL +4 -0
- pi_ai_python-1.3.0.dist-info/entry_points.txt +2 -0
- pi_ai_python-1.3.0.dist-info/licenses/LICENSE +22 -0
pi_ai/abort.py
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Cancellation primitives modeled on the web ``AbortController``/``AbortSignal`` pair.
|
|
2
|
+
|
|
3
|
+
pi passes an ``AbortSignal`` through every layer (``options.signal``). The Python
|
|
4
|
+
port keeps that contract so the same cancellation semantics apply: aborting a
|
|
5
|
+
request ends its stream with ``stop_reason == "aborted"`` instead of raising.
|
|
6
|
+
|
|
7
|
+
Inside async code, :func:`abort_scope` turns an abort into prompt task
|
|
8
|
+
cancellation (any pending ``await`` such as a network read is interrupted) and
|
|
9
|
+
re-raises it as :class:`AbortError`, the same way :func:`asyncio.timeout` turns a
|
|
10
|
+
deadline into :class:`TimeoutError`.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import asyncio
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
from collections.abc import Callable, Iterable
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"AbortError",
|
|
23
|
+
"AbortController",
|
|
24
|
+
"AbortSignal",
|
|
25
|
+
"abort_scope",
|
|
26
|
+
"operation_signal",
|
|
27
|
+
"race_with_abort_signal",
|
|
28
|
+
"sleep",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AbortError(Exception):
|
|
33
|
+
"""Raised when an operation is cancelled through an :class:`AbortSignal`."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, reason: Any = None) -> None:
|
|
36
|
+
if isinstance(reason, BaseException):
|
|
37
|
+
message = str(reason) or "The operation was aborted"
|
|
38
|
+
elif reason is None:
|
|
39
|
+
message = "The operation was aborted"
|
|
40
|
+
else:
|
|
41
|
+
message = str(reason)
|
|
42
|
+
super().__init__(message)
|
|
43
|
+
self.reason = reason
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
_Listener = Callable[[], Any]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AbortSignal:
|
|
50
|
+
"""Read side of an :class:`AbortController`.
|
|
51
|
+
|
|
52
|
+
Listeners run synchronously when the signal aborts. ``abort()`` may be called
|
|
53
|
+
from any thread; listeners that touch an event loop should hand work to it with
|
|
54
|
+
``loop.call_soon_threadsafe`` (the helpers in this module already do).
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
__slots__ = ("__weakref__", "_aborted", "_listeners", "_lock", "_reason")
|
|
58
|
+
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
self._aborted = False
|
|
61
|
+
self._reason: Any = None
|
|
62
|
+
self._listeners: list[_Listener] | None = None
|
|
63
|
+
self._lock = threading.Lock()
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def aborted(self) -> bool:
|
|
67
|
+
return self._aborted
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def reason(self) -> Any:
|
|
71
|
+
return self._reason
|
|
72
|
+
|
|
73
|
+
def throw_if_aborted(self) -> None:
|
|
74
|
+
if self._aborted:
|
|
75
|
+
raise self._reason if isinstance(self._reason, AbortError) else AbortError(self._reason)
|
|
76
|
+
|
|
77
|
+
def add_listener(self, listener: _Listener) -> Callable[[], None]:
|
|
78
|
+
"""Register ``listener`` to run on abort. Returns a function that removes it.
|
|
79
|
+
|
|
80
|
+
If the signal is already aborted the listener runs immediately.
|
|
81
|
+
"""
|
|
82
|
+
with self._lock:
|
|
83
|
+
if not self._aborted:
|
|
84
|
+
if self._listeners is None:
|
|
85
|
+
self._listeners = []
|
|
86
|
+
self._listeners.append(listener)
|
|
87
|
+
return lambda: self._remove(listener)
|
|
88
|
+
listener()
|
|
89
|
+
return _noop
|
|
90
|
+
|
|
91
|
+
def _remove(self, listener: _Listener) -> None:
|
|
92
|
+
with self._lock:
|
|
93
|
+
if self._listeners:
|
|
94
|
+
try:
|
|
95
|
+
self._listeners.remove(listener)
|
|
96
|
+
except ValueError:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def _abort(self, reason: Any) -> None:
|
|
100
|
+
with self._lock:
|
|
101
|
+
if self._aborted:
|
|
102
|
+
return
|
|
103
|
+
self._aborted = True
|
|
104
|
+
self._reason = AbortError(reason) if reason is None or not isinstance(reason, BaseException) else reason
|
|
105
|
+
listeners, self._listeners = self._listeners, None
|
|
106
|
+
for listener in listeners or ():
|
|
107
|
+
try:
|
|
108
|
+
listener()
|
|
109
|
+
except Exception: # listeners must not break abort propagation
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
async def wait(self) -> Any:
|
|
113
|
+
"""Wait until the signal aborts and return its reason."""
|
|
114
|
+
if self._aborted:
|
|
115
|
+
return self._reason
|
|
116
|
+
loop = asyncio.get_running_loop()
|
|
117
|
+
future: asyncio.Future[None] = loop.create_future()
|
|
118
|
+
|
|
119
|
+
remove = self.add_listener(lambda: _resolve_in_loop(loop, future))
|
|
120
|
+
try:
|
|
121
|
+
await future
|
|
122
|
+
finally:
|
|
123
|
+
remove()
|
|
124
|
+
return self._reason
|
|
125
|
+
|
|
126
|
+
@staticmethod
|
|
127
|
+
def abort(reason: Any = None) -> AbortSignal:
|
|
128
|
+
"""An already aborted signal."""
|
|
129
|
+
controller = AbortController()
|
|
130
|
+
controller.abort(reason)
|
|
131
|
+
return controller.signal
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def timeout(seconds: float) -> AbortSignal:
|
|
135
|
+
"""A signal that aborts after ``seconds`` (requires a running event loop)."""
|
|
136
|
+
controller = AbortController()
|
|
137
|
+
loop = asyncio.get_running_loop()
|
|
138
|
+
loop.call_later(seconds, controller.abort, TimeoutError("The operation timed out"))
|
|
139
|
+
return controller.signal
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def any(signals: Iterable[AbortSignal | None]) -> AbortSignal:
|
|
143
|
+
"""A signal that aborts when any of ``signals`` aborts."""
|
|
144
|
+
active = [signal for signal in signals if signal is not None]
|
|
145
|
+
controller = AbortController()
|
|
146
|
+
for signal in active:
|
|
147
|
+
if signal.aborted:
|
|
148
|
+
controller.abort(signal.reason)
|
|
149
|
+
return controller.signal
|
|
150
|
+
for signal in active:
|
|
151
|
+
signal.add_listener(lambda signal=signal: controller.abort(signal.reason))
|
|
152
|
+
return controller.signal
|
|
153
|
+
|
|
154
|
+
def __repr__(self) -> str:
|
|
155
|
+
return f"AbortSignal(aborted={self._aborted})"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _noop() -> None:
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _resolve_future(future: asyncio.Future[Any]) -> None:
|
|
163
|
+
if not future.done():
|
|
164
|
+
future.set_result(None)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class AbortController:
|
|
168
|
+
"""Write side: ``controller.abort()`` aborts ``controller.signal``."""
|
|
169
|
+
|
|
170
|
+
__slots__ = ("signal",)
|
|
171
|
+
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
self.signal = AbortSignal()
|
|
174
|
+
|
|
175
|
+
def abort(self, reason: Any = None) -> None:
|
|
176
|
+
self.signal._abort(reason)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def operation_signal(signal: AbortSignal | None) -> AbortSignal:
|
|
180
|
+
"""Return ``signal`` or a fresh never-aborting signal for APIs whose signal is optional."""
|
|
181
|
+
return signal if signal is not None else AbortSignal()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class abort_scope:
|
|
185
|
+
"""Cancel the current task when ``signal`` aborts, raising :class:`AbortError`.
|
|
186
|
+
|
|
187
|
+
::
|
|
188
|
+
|
|
189
|
+
async with abort_scope(options.get("signal")):
|
|
190
|
+
response = await client.send(request, stream=True)
|
|
191
|
+
async for line in response.aiter_lines():
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
A ``None`` signal makes the scope a no-op. Cancellation that does not come from
|
|
195
|
+
the signal propagates unchanged.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
__slots__ = ("_cancelling", "_exited", "_fired", "_loop", "_remove", "_signal", "_task")
|
|
199
|
+
|
|
200
|
+
def __init__(self, signal: AbortSignal | None) -> None:
|
|
201
|
+
self._signal = signal
|
|
202
|
+
self._task: asyncio.Task[Any] | None = None
|
|
203
|
+
self._loop: asyncio.AbstractEventLoop | None = None
|
|
204
|
+
self._remove: Callable[[], None] | None = None
|
|
205
|
+
self._fired = False
|
|
206
|
+
self._exited = False
|
|
207
|
+
self._cancelling = 0
|
|
208
|
+
|
|
209
|
+
async def __aenter__(self) -> abort_scope:
|
|
210
|
+
signal = self._signal
|
|
211
|
+
if signal is None:
|
|
212
|
+
return self
|
|
213
|
+
signal.throw_if_aborted()
|
|
214
|
+
self._task = asyncio.current_task()
|
|
215
|
+
self._loop = asyncio.get_running_loop()
|
|
216
|
+
if self._task is not None:
|
|
217
|
+
self._cancelling = self._task.cancelling()
|
|
218
|
+
self._remove = signal.add_listener(self._on_abort)
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def _on_abort(self) -> None:
|
|
222
|
+
loop = self._loop
|
|
223
|
+
if loop is None or loop.is_closed():
|
|
224
|
+
return
|
|
225
|
+
try:
|
|
226
|
+
running = asyncio.get_running_loop()
|
|
227
|
+
except RuntimeError:
|
|
228
|
+
running = None
|
|
229
|
+
if running is loop:
|
|
230
|
+
self._cancel()
|
|
231
|
+
else:
|
|
232
|
+
loop.call_soon_threadsafe(self._cancel)
|
|
233
|
+
|
|
234
|
+
def _cancel(self) -> None:
|
|
235
|
+
task = self._task
|
|
236
|
+
if task is not None and not task.done() and not self._fired and not self._exited:
|
|
237
|
+
self._fired = True
|
|
238
|
+
task.cancel()
|
|
239
|
+
|
|
240
|
+
async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> bool | None:
|
|
241
|
+
self._exited = True
|
|
242
|
+
if self._remove is not None:
|
|
243
|
+
self._remove()
|
|
244
|
+
self._remove = None
|
|
245
|
+
if self._fired and self._task is not None:
|
|
246
|
+
if self._task.uncancel() <= self._cancelling:
|
|
247
|
+
if exc_type is asyncio.CancelledError:
|
|
248
|
+
assert self._signal is not None
|
|
249
|
+
raise AbortError(self._signal.reason) from exc
|
|
250
|
+
if sys.version_info < (3, 13):
|
|
251
|
+
# The body finished before the cancellation was delivered. Python 3.13's
|
|
252
|
+
# uncancel() drops that pending cancellation; older versions would raise it
|
|
253
|
+
# at the caller's next await, so take it here.
|
|
254
|
+
try:
|
|
255
|
+
await asyncio.sleep(0)
|
|
256
|
+
except asyncio.CancelledError:
|
|
257
|
+
pass
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
async def race_with_abort_signal(awaitable: Any, signal: AbortSignal | None) -> Any:
|
|
262
|
+
"""Await ``awaitable`` but stop waiting (raising :class:`AbortError`) when ``signal`` aborts.
|
|
263
|
+
|
|
264
|
+
Unlike :class:`abort_scope`, the abandoned operation keeps running in its own task
|
|
265
|
+
and its eventual exception is consumed, matching pi's ``raceWithAbortSignal``.
|
|
266
|
+
"""
|
|
267
|
+
if signal is None:
|
|
268
|
+
return await awaitable
|
|
269
|
+
task = asyncio.ensure_future(awaitable)
|
|
270
|
+
if signal.aborted:
|
|
271
|
+
task.add_done_callback(_consume_task_result)
|
|
272
|
+
signal.throw_if_aborted()
|
|
273
|
+
loop = asyncio.get_running_loop()
|
|
274
|
+
abort_future: asyncio.Future[None] = loop.create_future()
|
|
275
|
+
remove = signal.add_listener(lambda: _resolve_in_loop(loop, abort_future))
|
|
276
|
+
try:
|
|
277
|
+
await asyncio.wait({task, abort_future}, return_when=asyncio.FIRST_COMPLETED)
|
|
278
|
+
except asyncio.CancelledError:
|
|
279
|
+
task.cancel()
|
|
280
|
+
raise
|
|
281
|
+
finally:
|
|
282
|
+
remove()
|
|
283
|
+
if not abort_future.done():
|
|
284
|
+
abort_future.cancel()
|
|
285
|
+
# An abort that fired while the operation ran wins, even if the operation also finished
|
|
286
|
+
# in the same loop iteration (JavaScript abort listeners run synchronously).
|
|
287
|
+
if signal.aborted:
|
|
288
|
+
task.add_done_callback(_consume_task_result)
|
|
289
|
+
signal.throw_if_aborted()
|
|
290
|
+
return task.result()
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _resolve_in_loop(loop: asyncio.AbstractEventLoop, future: asyncio.Future[Any]) -> None:
|
|
294
|
+
try:
|
|
295
|
+
running = asyncio.get_running_loop()
|
|
296
|
+
except RuntimeError:
|
|
297
|
+
running = None
|
|
298
|
+
if running is loop:
|
|
299
|
+
_resolve_future(future)
|
|
300
|
+
elif not loop.is_closed():
|
|
301
|
+
loop.call_soon_threadsafe(_resolve_future, future)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _consume_task_result(task: asyncio.Future[Any]) -> None:
|
|
305
|
+
if not task.cancelled():
|
|
306
|
+
task.exception()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
async def sleep(seconds: float, signal: AbortSignal | None = None) -> None:
|
|
310
|
+
"""``asyncio.sleep`` that raises :class:`AbortError` as soon as ``signal`` aborts."""
|
|
311
|
+
if signal is None:
|
|
312
|
+
await asyncio.sleep(seconds)
|
|
313
|
+
return
|
|
314
|
+
async with abort_scope(signal):
|
|
315
|
+
await asyncio.sleep(max(0.0, seconds))
|
pi_ai/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Provider API (wire protocol) implementations. Each module exposes ``stream`` and ``stream_simple``."""
|