pi-durable-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_durable/__init__.py +320 -0
- pi_durable/context.py +328 -0
- pi_durable/delta.py +269 -0
- pi_durable/documents.py +366 -0
- pi_durable/entries.py +66 -0
- pi_durable/env/__init__.py +408 -0
- pi_durable/env/decode.py +48 -0
- pi_durable/env/line_scan.py +190 -0
- pi_durable/env/posix.py +1547 -0
- pi_durable/errors.py +89 -0
- pi_durable/harness/__init__.py +1 -0
- pi_durable/harness/agent.py +344 -0
- pi_durable/harness/compaction.py +673 -0
- pi_durable/harness/context.py +515 -0
- pi_durable/harness/define.py +111 -0
- pi_durable/harness/events.py +766 -0
- pi_durable/harness/generation.py +1079 -0
- pi_durable/harness/harness.py +570 -0
- pi_durable/harness/inbox.py +255 -0
- pi_durable/harness/live.py +374 -0
- pi_durable/harness/output.py +467 -0
- pi_durable/harness/prompt.py +288 -0
- pi_durable/harness/provider.py +78 -0
- pi_durable/harness/registry.py +136 -0
- pi_durable/harness/scheduler.py +1977 -0
- pi_durable/harness/submissions.py +357 -0
- pi_durable/harness/task_graph.py +334 -0
- pi_durable/harness/tool.py +1365 -0
- pi_durable/harness/types.py +772 -0
- pi_durable/harness/usage.py +88 -0
- pi_durable/harness/util.py +204 -0
- pi_durable/harness/validation.py +460 -0
- pi_durable/harness/view.py +324 -0
- pi_durable/ids.py +63 -0
- pi_durable/json.py +186 -0
- pi_durable/providers/__init__.py +362 -0
- pi_durable/providers/anthropic.py +603 -0
- pi_durable/providers/base.py +97 -0
- pi_durable/providers/json_parse.py +117 -0
- pi_durable/providers/openai.py +574 -0
- pi_durable/providers/request.py +35 -0
- pi_durable/providers/retry.py +127 -0
- pi_durable/providers/stream.py +183 -0
- pi_durable/providers/transcript.py +224 -0
- pi_durable/providers/usage.py +58 -0
- pi_durable/py.typed +0 -0
- pi_durable/search.py +115 -0
- pi_durable/session/__init__.py +5 -0
- pi_durable/session/drafts.py +762 -0
- pi_durable/session/forks.py +109 -0
- pi_durable/session/observation.py +374 -0
- pi_durable/session/session.py +1193 -0
- pi_durable/session/transaction.py +1088 -0
- pi_durable/storage/__init__.py +5 -0
- pi_durable/storage/jsonl/__init__.py +15 -0
- pi_durable/storage/jsonl/storage.py +958 -0
- pi_durable/storage/memory.py +889 -0
- pi_durable/storage/scan.py +57 -0
- pi_durable/storage/sqlite/__init__.py +26 -0
- pi_durable/storage/sqlite/database.py +62 -0
- pi_durable/storage/sqlite/migrations.py +191 -0
- pi_durable/storage/sqlite/posix.py +314 -0
- pi_durable/storage/sqlite/schema.py +97 -0
- pi_durable/storage/sqlite/storage.py +1130 -0
- pi_durable/tasks.py +89 -0
- pi_durable/testing/__init__.py +41 -0
- pi_durable/testing/assertions.py +44 -0
- pi_durable/testing/env_conformance.py +828 -0
- pi_durable/testing/runner.py +64 -0
- pi_durable/testing/storage_conformance.py +2008 -0
- pi_durable/testing/types.py +99 -0
- pi_durable/tool_search/__init__.py +159 -0
- pi_durable/tools/__init__.py +81 -0
- pi_durable/tools/_values.py +43 -0
- pi_durable/tools/bash.py +250 -0
- pi_durable/tools/edit.py +214 -0
- pi_durable/tools/edit_diff.py +452 -0
- pi_durable/tools/env.py +19 -0
- pi_durable/tools/file_mutation_queue.py +68 -0
- pi_durable/tools/image.py +248 -0
- pi_durable/tools/image_processor.py +77 -0
- pi_durable/tools/path_utils.py +111 -0
- pi_durable/tools/read.py +506 -0
- pi_durable/tools/write.py +71 -0
- pi_durable/truncate.py +185 -0
- pi_durable/types.py +1297 -0
- pi_durable_python-1.3.0.dist-info/METADATA +276 -0
- pi_durable_python-1.3.0.dist-info/RECORD +90 -0
- pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
- pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
pi_durable/__init__.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Python port of ``@earendil-works/pi-durable`` v1.3.0.
|
|
2
|
+
|
|
3
|
+
The public surface is rebuilt against the vendored 1.3.0 specification. This layer provides strict
|
|
4
|
+
JSON copies, branded numeric IDs, public errors, entry tokens, the minimal Context/abort signal,
|
|
5
|
+
the Storage protocol, the Session kernel, document definitions, task definitions, context
|
|
6
|
+
derivation, the in-memory reference backend, and the durable task scheduler with its registry,
|
|
7
|
+
agent resolution, extension hooks, submissions, inbox boundaries, and Harness surface. The
|
|
8
|
+
built-in generation, compaction, and tool tasks, nested tool calls with structured output, provider
|
|
9
|
+
identity, and the usage ledger are included; the `tool_search` subpath adds BM25 deferred-tool
|
|
10
|
+
discovery, the conversation view, agent event stream, and task graph expose committed state, and
|
|
11
|
+
the optional `openai` and `anthropic` extras supply the model adapters generation streams through.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .documents import (
|
|
15
|
+
CommonDocDefinition,
|
|
16
|
+
ConversationDocFamilyToken,
|
|
17
|
+
ConversationDocToken,
|
|
18
|
+
DocDefinition,
|
|
19
|
+
DocFamilyDefinition,
|
|
20
|
+
DocFamilyToken,
|
|
21
|
+
DocToken,
|
|
22
|
+
RewindableConversationDocFamilyToken,
|
|
23
|
+
RewindableConversationDocToken,
|
|
24
|
+
SessionDocFamilyToken,
|
|
25
|
+
SessionDocToken,
|
|
26
|
+
TaskDocFamilyToken,
|
|
27
|
+
TaskDocToken,
|
|
28
|
+
define_doc,
|
|
29
|
+
define_doc_family,
|
|
30
|
+
)
|
|
31
|
+
from .entries import (
|
|
32
|
+
AssistantEntry,
|
|
33
|
+
CompactionEntry,
|
|
34
|
+
Entry,
|
|
35
|
+
ResetEntry,
|
|
36
|
+
SystemEntry,
|
|
37
|
+
ToolResultEntry,
|
|
38
|
+
UserEntry,
|
|
39
|
+
define_entry,
|
|
40
|
+
)
|
|
41
|
+
from .errors import ConversationBusy, ReadAfterWrite, SessionFailed, StorageRequestError
|
|
42
|
+
from .harness.agent import (
|
|
43
|
+
DEFAULT_COMPACTION_POLICY,
|
|
44
|
+
DEFAULT_PROGRESS_POLICY,
|
|
45
|
+
DEFAULT_RETRY_POLICY,
|
|
46
|
+
AgentDoc,
|
|
47
|
+
add_tools,
|
|
48
|
+
configure,
|
|
49
|
+
)
|
|
50
|
+
from .harness.compaction import (
|
|
51
|
+
CompactionCheckpoint,
|
|
52
|
+
CompactionInput,
|
|
53
|
+
CompactionTask,
|
|
54
|
+
SummaryRequest,
|
|
55
|
+
)
|
|
56
|
+
from .harness.define import (
|
|
57
|
+
define_extension,
|
|
58
|
+
define_tool,
|
|
59
|
+
hook,
|
|
60
|
+
section,
|
|
61
|
+
wrap_section,
|
|
62
|
+
wrap_tool,
|
|
63
|
+
)
|
|
64
|
+
from .harness.events import (
|
|
65
|
+
AgentEvent,
|
|
66
|
+
AgentEventStream,
|
|
67
|
+
MessageChange,
|
|
68
|
+
SnapshotEvent,
|
|
69
|
+
ToolEventCall,
|
|
70
|
+
watch_events,
|
|
71
|
+
)
|
|
72
|
+
from .harness.generation import (
|
|
73
|
+
GenerationCheckpoint,
|
|
74
|
+
GenerationInput,
|
|
75
|
+
GenerationResult,
|
|
76
|
+
GenerationTask,
|
|
77
|
+
)
|
|
78
|
+
from .harness.harness import Harness
|
|
79
|
+
from .harness.inbox import InboxDoc, InboxItem, InboxState
|
|
80
|
+
from .harness.live import (
|
|
81
|
+
CompactionFailure,
|
|
82
|
+
CompactionStatus,
|
|
83
|
+
LiveDoc,
|
|
84
|
+
LiveState,
|
|
85
|
+
NestedToolSlot,
|
|
86
|
+
NestedToolSummary,
|
|
87
|
+
ToolSlot,
|
|
88
|
+
)
|
|
89
|
+
from .harness.provider import ProviderDoc, ProviderState
|
|
90
|
+
from .harness.registry import create_registry
|
|
91
|
+
from .harness.task_graph import TaskGraph, TaskGraphNode, TaskGraphState, TaskGraphWatch
|
|
92
|
+
from .harness.tool import (
|
|
93
|
+
NestedCallDoc,
|
|
94
|
+
NestedCallState,
|
|
95
|
+
ToolTask,
|
|
96
|
+
ToolTaskCheckpoint,
|
|
97
|
+
ToolTaskInput,
|
|
98
|
+
ToolTaskResult,
|
|
99
|
+
)
|
|
100
|
+
from .harness.types import (
|
|
101
|
+
Agent,
|
|
102
|
+
AgentChange,
|
|
103
|
+
AgentState,
|
|
104
|
+
AnyTask,
|
|
105
|
+
CompactionHooks,
|
|
106
|
+
CompactionPolicy,
|
|
107
|
+
CompactionReason,
|
|
108
|
+
CompactionResult,
|
|
109
|
+
ContextView,
|
|
110
|
+
Conversation,
|
|
111
|
+
ConversationAbortOptions,
|
|
112
|
+
ConversationCreateOptions,
|
|
113
|
+
ConversationHandle,
|
|
114
|
+
ConversationInit,
|
|
115
|
+
ConversationRetryPolicy,
|
|
116
|
+
ConversationStreamOptions,
|
|
117
|
+
ConversationWatch,
|
|
118
|
+
EnvTarget,
|
|
119
|
+
Extension,
|
|
120
|
+
GenerationHooks,
|
|
121
|
+
HarnessInspection,
|
|
122
|
+
HarnessOptions,
|
|
123
|
+
HookApi,
|
|
124
|
+
HookRegistration,
|
|
125
|
+
HookResult,
|
|
126
|
+
HookRunner,
|
|
127
|
+
HooksOf,
|
|
128
|
+
InputEntry,
|
|
129
|
+
InputSubmissionDraft,
|
|
130
|
+
ModelRef,
|
|
131
|
+
NestedToolExecutionResult,
|
|
132
|
+
ProgressPolicy,
|
|
133
|
+
PromptInput,
|
|
134
|
+
PromptSection,
|
|
135
|
+
QueueMode,
|
|
136
|
+
Registry,
|
|
137
|
+
RegistryReader,
|
|
138
|
+
RegistrySnapshot,
|
|
139
|
+
Settings,
|
|
140
|
+
SettledSubmissionRecord,
|
|
141
|
+
SettledTask,
|
|
142
|
+
Submission,
|
|
143
|
+
SubmissionDraft,
|
|
144
|
+
TaskInspection,
|
|
145
|
+
TaskRuntime,
|
|
146
|
+
ToolControl,
|
|
147
|
+
ToolDiagnostic,
|
|
148
|
+
ToolExecutionApi,
|
|
149
|
+
ToolExecutionMode,
|
|
150
|
+
ToolExecutionResult,
|
|
151
|
+
ToolHookCall,
|
|
152
|
+
ToolHooks,
|
|
153
|
+
ToolRegistration,
|
|
154
|
+
UserInput,
|
|
155
|
+
Wrap,
|
|
156
|
+
)
|
|
157
|
+
from .harness.usage import UsageDoc, UsageState
|
|
158
|
+
from .harness.view import ConversationView
|
|
159
|
+
from .providers import Model, Models
|
|
160
|
+
from .session.session import DocumentState, SessionImpl, create_session
|
|
161
|
+
from .storage.memory import MemoryStorage, PreparedMemoryCommit
|
|
162
|
+
from .tasks import Task, TaskDefinition, define_task
|
|
163
|
+
from .types import DocumentReader, PhaseHandler, Session, SessionEnd, Tx
|
|
164
|
+
|
|
165
|
+
__version__ = "1.3.0"
|
|
166
|
+
|
|
167
|
+
__all__ = [
|
|
168
|
+
"__version__",
|
|
169
|
+
"add_tools",
|
|
170
|
+
"Agent",
|
|
171
|
+
"AgentChange",
|
|
172
|
+
"AgentEvent",
|
|
173
|
+
"AgentEventStream",
|
|
174
|
+
"AgentDoc",
|
|
175
|
+
"AgentState",
|
|
176
|
+
"AnyTask",
|
|
177
|
+
"AssistantEntry",
|
|
178
|
+
"CommonDocDefinition",
|
|
179
|
+
"CompactionCheckpoint",
|
|
180
|
+
"CompactionEntry",
|
|
181
|
+
"CompactionFailure",
|
|
182
|
+
"CompactionHooks",
|
|
183
|
+
"CompactionInput",
|
|
184
|
+
"CompactionPolicy",
|
|
185
|
+
"CompactionReason",
|
|
186
|
+
"CompactionResult",
|
|
187
|
+
"CompactionStatus",
|
|
188
|
+
"CompactionTask",
|
|
189
|
+
"configure",
|
|
190
|
+
"ContextView",
|
|
191
|
+
"Conversation",
|
|
192
|
+
"ConversationAbortOptions",
|
|
193
|
+
"ConversationBusy",
|
|
194
|
+
"ConversationCreateOptions",
|
|
195
|
+
"ConversationDocFamilyToken",
|
|
196
|
+
"ConversationDocToken",
|
|
197
|
+
"ConversationHandle",
|
|
198
|
+
"ConversationInit",
|
|
199
|
+
"ConversationRetryPolicy",
|
|
200
|
+
"ConversationStreamOptions",
|
|
201
|
+
"ConversationView",
|
|
202
|
+
"ConversationWatch",
|
|
203
|
+
"create_registry",
|
|
204
|
+
"create_session",
|
|
205
|
+
"DEFAULT_COMPACTION_POLICY",
|
|
206
|
+
"DEFAULT_PROGRESS_POLICY",
|
|
207
|
+
"DEFAULT_RETRY_POLICY",
|
|
208
|
+
"define_doc",
|
|
209
|
+
"define_doc_family",
|
|
210
|
+
"define_entry",
|
|
211
|
+
"define_extension",
|
|
212
|
+
"define_task",
|
|
213
|
+
"define_tool",
|
|
214
|
+
"DocDefinition",
|
|
215
|
+
"DocFamilyDefinition",
|
|
216
|
+
"DocFamilyToken",
|
|
217
|
+
"DocToken",
|
|
218
|
+
"DocumentReader",
|
|
219
|
+
"DocumentState",
|
|
220
|
+
"Entry",
|
|
221
|
+
"EnvTarget",
|
|
222
|
+
"Extension",
|
|
223
|
+
"GenerationCheckpoint",
|
|
224
|
+
"GenerationHooks",
|
|
225
|
+
"GenerationInput",
|
|
226
|
+
"GenerationResult",
|
|
227
|
+
"GenerationTask",
|
|
228
|
+
"Harness",
|
|
229
|
+
"HarnessInspection",
|
|
230
|
+
"HarnessOptions",
|
|
231
|
+
"HarnessSettings",
|
|
232
|
+
"hook",
|
|
233
|
+
"HookApi",
|
|
234
|
+
"HookRegistration",
|
|
235
|
+
"HookResult",
|
|
236
|
+
"HookRunner",
|
|
237
|
+
"HooksOf",
|
|
238
|
+
"InboxDoc",
|
|
239
|
+
"InboxItem",
|
|
240
|
+
"InboxState",
|
|
241
|
+
"InputEntry",
|
|
242
|
+
"InputSubmissionDraft",
|
|
243
|
+
"LiveDoc",
|
|
244
|
+
"LiveState",
|
|
245
|
+
"MemoryStorage",
|
|
246
|
+
"MessageChange",
|
|
247
|
+
"Model",
|
|
248
|
+
"ModelRef",
|
|
249
|
+
"Models",
|
|
250
|
+
"NestedCallDoc",
|
|
251
|
+
"NestedCallState",
|
|
252
|
+
"NestedToolExecutionResult",
|
|
253
|
+
"NestedToolSlot",
|
|
254
|
+
"NestedToolSummary",
|
|
255
|
+
"PhaseHandler",
|
|
256
|
+
"PreparedMemoryCommit",
|
|
257
|
+
"ProgressPolicy",
|
|
258
|
+
"PromptInput",
|
|
259
|
+
"PromptSection",
|
|
260
|
+
"ProviderDoc",
|
|
261
|
+
"ProviderState",
|
|
262
|
+
"QueueMode",
|
|
263
|
+
"ReadAfterWrite",
|
|
264
|
+
"Registry",
|
|
265
|
+
"RegistryReader",
|
|
266
|
+
"RegistrySnapshot",
|
|
267
|
+
"ResetEntry",
|
|
268
|
+
"RewindableConversationDocFamilyToken",
|
|
269
|
+
"RewindableConversationDocToken",
|
|
270
|
+
"section",
|
|
271
|
+
"Session",
|
|
272
|
+
"SessionDocFamilyToken",
|
|
273
|
+
"SessionDocToken",
|
|
274
|
+
"SessionEnd",
|
|
275
|
+
"SessionFailed",
|
|
276
|
+
"SessionImpl",
|
|
277
|
+
"Settings",
|
|
278
|
+
"SettledSubmissionRecord",
|
|
279
|
+
"SnapshotEvent",
|
|
280
|
+
"SettledTask",
|
|
281
|
+
"StorageRequestError",
|
|
282
|
+
"Submission",
|
|
283
|
+
"SubmissionDraft",
|
|
284
|
+
"SummaryRequest",
|
|
285
|
+
"SystemEntry",
|
|
286
|
+
"Task",
|
|
287
|
+
"TaskDefinition",
|
|
288
|
+
"TaskGraph",
|
|
289
|
+
"TaskGraphNode",
|
|
290
|
+
"TaskGraphState",
|
|
291
|
+
"TaskGraphWatch",
|
|
292
|
+
"TaskDocFamilyToken",
|
|
293
|
+
"TaskDocToken",
|
|
294
|
+
"TaskInspection",
|
|
295
|
+
"TaskRuntime",
|
|
296
|
+
"ToolControl",
|
|
297
|
+
"ToolDiagnostic",
|
|
298
|
+
"ToolEventCall",
|
|
299
|
+
"ToolExecutionApi",
|
|
300
|
+
"ToolExecutionMode",
|
|
301
|
+
"ToolExecutionResult",
|
|
302
|
+
"ToolHookCall",
|
|
303
|
+
"ToolHooks",
|
|
304
|
+
"ToolRegistration",
|
|
305
|
+
"ToolResultEntry",
|
|
306
|
+
"ToolSlot",
|
|
307
|
+
"ToolTask",
|
|
308
|
+
"ToolTaskCheckpoint",
|
|
309
|
+
"ToolTaskInput",
|
|
310
|
+
"ToolTaskResult",
|
|
311
|
+
"Tx",
|
|
312
|
+
"UsageDoc",
|
|
313
|
+
"UsageState",
|
|
314
|
+
"UserEntry",
|
|
315
|
+
"UserInput",
|
|
316
|
+
"Wrap",
|
|
317
|
+
"watch_events",
|
|
318
|
+
"wrap_section",
|
|
319
|
+
"wrap_tool",
|
|
320
|
+
]
|
pi_durable/context.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Invocation-scoped values and cancellation.
|
|
2
|
+
|
|
3
|
+
The v1.3.0 kernel passes a Chord ``Context`` explicitly through every operation. This port keeps
|
|
4
|
+
the explicit parameter but reduces the surface to what durable needs: an optional abort signal
|
|
5
|
+
and an immutable chain of typed values. Aborted work observes cancellation through
|
|
6
|
+
:meth:`AbortSignal.wait`, :func:`await_with_context`, or by checking ``context.abort_signal``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Generic, TypeVar
|
|
15
|
+
|
|
16
|
+
T = TypeVar("T")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AbortError(Exception):
|
|
20
|
+
"""The operation was cancelled through its context.
|
|
21
|
+
|
|
22
|
+
``reason`` is the value passed to :meth:`AbortController.abort`; it is ``None`` when the
|
|
23
|
+
cancellation did not supply one.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, reason: object | None = None) -> None:
|
|
27
|
+
super().__init__("The operation was aborted")
|
|
28
|
+
self.name = "AbortError"
|
|
29
|
+
self.reason = reason
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True, slots=True)
|
|
33
|
+
class ContextKey(Generic[T]):
|
|
34
|
+
"""Typed identity for one value carried by a :class:`Context`."""
|
|
35
|
+
|
|
36
|
+
token: object
|
|
37
|
+
description: str = ""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def create_context_key(description: str) -> ContextKey[T]:
|
|
41
|
+
"""Create a key whose identity, not description, selects a context value."""
|
|
42
|
+
|
|
43
|
+
return ContextKey(token=object(), description=description)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AbortSignal:
|
|
47
|
+
"""An observable cancellation state shared by a context and its derived contexts."""
|
|
48
|
+
|
|
49
|
+
__slots__ = ("_aborted", "_cleanup", "_listeners", "_reason")
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self._aborted = False
|
|
53
|
+
self._reason: object | None = None
|
|
54
|
+
self._listeners: list[Callable[[], None]] = []
|
|
55
|
+
self._cleanup: list[Callable[[], None]] = []
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def aborted(self) -> bool:
|
|
59
|
+
return self._aborted
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def reason(self) -> object | None:
|
|
63
|
+
return self._reason
|
|
64
|
+
|
|
65
|
+
def add_listener(self, listener: Callable[[], None]) -> Callable[[], None]:
|
|
66
|
+
"""Register ``listener`` and return a function that removes it.
|
|
67
|
+
|
|
68
|
+
A listener runs synchronously on the aborting caller. It is not invoked when the signal
|
|
69
|
+
is already aborted; such callers should check :attr:`aborted` or await :meth:`wait`.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
if not self._aborted:
|
|
73
|
+
self._listeners.append(listener)
|
|
74
|
+
return lambda: self._remove(listener)
|
|
75
|
+
|
|
76
|
+
def _remove(self, listener: Callable[[], None]) -> None:
|
|
77
|
+
try:
|
|
78
|
+
self._listeners.remove(listener)
|
|
79
|
+
except ValueError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
def _abort(self, reason: object | None) -> None:
|
|
83
|
+
if self._aborted:
|
|
84
|
+
return
|
|
85
|
+
self._aborted = True
|
|
86
|
+
self._reason = reason
|
|
87
|
+
listeners = self._listeners
|
|
88
|
+
self._listeners = []
|
|
89
|
+
cleanup = self._cleanup
|
|
90
|
+
self._cleanup = []
|
|
91
|
+
for listener in listeners:
|
|
92
|
+
listener()
|
|
93
|
+
for remove in cleanup:
|
|
94
|
+
remove()
|
|
95
|
+
|
|
96
|
+
async def wait(self) -> None:
|
|
97
|
+
"""Resolve when the signal aborts; return immediately when it already has."""
|
|
98
|
+
|
|
99
|
+
if self._aborted:
|
|
100
|
+
return
|
|
101
|
+
loop = asyncio.get_running_loop()
|
|
102
|
+
waiter: asyncio.Future[None] = loop.create_future()
|
|
103
|
+
|
|
104
|
+
def wake() -> None:
|
|
105
|
+
if not waiter.done():
|
|
106
|
+
waiter.set_result(None)
|
|
107
|
+
|
|
108
|
+
remove = self.add_listener(wake)
|
|
109
|
+
if self._aborted:
|
|
110
|
+
wake()
|
|
111
|
+
try:
|
|
112
|
+
await waiter
|
|
113
|
+
finally:
|
|
114
|
+
remove()
|
|
115
|
+
|
|
116
|
+
def raise_if_aborted(self) -> None:
|
|
117
|
+
"""Raise :class:`AbortError` when the signal has aborted."""
|
|
118
|
+
|
|
119
|
+
if self._aborted:
|
|
120
|
+
reason = self._reason
|
|
121
|
+
raise AbortError(reason)
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def any(signals: Sequence[AbortSignal]) -> AbortSignal:
|
|
125
|
+
"""Return a signal aborted when any input signal aborts, with the first reason."""
|
|
126
|
+
|
|
127
|
+
combined = AbortSignal()
|
|
128
|
+
for signal in signals:
|
|
129
|
+
if signal.aborted:
|
|
130
|
+
combined._abort(signal.reason)
|
|
131
|
+
break
|
|
132
|
+
if combined.aborted:
|
|
133
|
+
return combined
|
|
134
|
+
|
|
135
|
+
def abort() -> None:
|
|
136
|
+
for signal in signals:
|
|
137
|
+
if signal.aborted:
|
|
138
|
+
combined._abort(signal.reason)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
combined._cleanup = [signal.add_listener(abort) for signal in signals]
|
|
142
|
+
return combined
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class AbortController:
|
|
146
|
+
"""Owns one independently cancellable :class:`AbortSignal`."""
|
|
147
|
+
|
|
148
|
+
__slots__ = ("signal",)
|
|
149
|
+
|
|
150
|
+
def __init__(self) -> None:
|
|
151
|
+
self.signal = AbortSignal()
|
|
152
|
+
|
|
153
|
+
def abort(self, reason: object | None = None) -> None:
|
|
154
|
+
"""Abort once; later calls are no-ops and keep the first reason."""
|
|
155
|
+
|
|
156
|
+
self.signal._abort(reason)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Context:
|
|
160
|
+
"""Immutable invocation-scoped values and cancellation state.
|
|
161
|
+
|
|
162
|
+
Values are looked up by key identity. The built-in abort signal is itself stored as a context
|
|
163
|
+
value so derived contexts compose without a mutable field.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
__slots__ = ("_name",)
|
|
167
|
+
|
|
168
|
+
_ABORT_SIGNAL_KEY: ContextKey[AbortSignal | None] = create_context_key("[context abort signal]")
|
|
169
|
+
|
|
170
|
+
def __init__(self, name: str = "[Context]") -> None:
|
|
171
|
+
self._name = name
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def abort_signal(self) -> AbortSignal | None:
|
|
175
|
+
return self.value(Context._ABORT_SIGNAL_KEY)
|
|
176
|
+
|
|
177
|
+
def value(self, key: ContextKey[T]) -> T | None:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
def __str__(self) -> str:
|
|
181
|
+
return self._name
|
|
182
|
+
|
|
183
|
+
def __repr__(self) -> str:
|
|
184
|
+
return f"Context({self._name!r})"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class _EmptyContext(Context):
|
|
188
|
+
__slots__ = ()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class _ContextValue(Context):
|
|
192
|
+
__slots__ = ("_parent", "_key", "_value")
|
|
193
|
+
|
|
194
|
+
def __init__(self, parent: Context, key: ContextKey[Any], value: Any) -> None:
|
|
195
|
+
self._parent = parent
|
|
196
|
+
self._key = key
|
|
197
|
+
self._value = value
|
|
198
|
+
self._name = f"{parent}.WithValue({key.description or 'anonymous'})"
|
|
199
|
+
|
|
200
|
+
def value(self, key: ContextKey[T]) -> T | None:
|
|
201
|
+
if key.token is self._key.token:
|
|
202
|
+
return self._value
|
|
203
|
+
return self._parent.value(key)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
BACKGROUND_CONTEXT: Context = _EmptyContext("[Context BACKGROUND_CONTEXT]")
|
|
207
|
+
TODO_CONTEXT: Context = _EmptyContext("[Context TODO_CONTEXT]")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def with_context_value(key: ContextKey[T], value: T, parent: Context) -> Context:
|
|
211
|
+
"""Derive a context containing one additional or replaced value."""
|
|
212
|
+
|
|
213
|
+
return _ContextValue(parent, key, value)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def with_abort_signal(signal: AbortSignal, context: Context) -> Context:
|
|
217
|
+
"""Derive a context cancelled by either the parent signal or ``signal``."""
|
|
218
|
+
|
|
219
|
+
parent = context.abort_signal
|
|
220
|
+
combined = signal if parent is None else AbortSignal.any((parent, signal))
|
|
221
|
+
return with_context_value(Context._ABORT_SIGNAL_KEY, combined, context)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def without_abort_signal(context: Context) -> Context:
|
|
225
|
+
"""Derive a context retaining all values except caller cancellation.
|
|
226
|
+
|
|
227
|
+
Intended for mandatory cleanup only.
|
|
228
|
+
"""
|
|
229
|
+
|
|
230
|
+
return with_context_value(Context._ABORT_SIGNAL_KEY, None, context)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass(frozen=True, slots=True)
|
|
234
|
+
class CancelScope:
|
|
235
|
+
"""An independently cancellable child context and its cancel function."""
|
|
236
|
+
|
|
237
|
+
context: Context
|
|
238
|
+
_controller: AbortController
|
|
239
|
+
|
|
240
|
+
def cancel(self, reason: object | None = None) -> None:
|
|
241
|
+
"""Abort the child context once; later calls keep the first reason."""
|
|
242
|
+
|
|
243
|
+
self._controller.abort(reason)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def with_cancel(context: Context) -> CancelScope:
|
|
247
|
+
"""Derive an independently cancellable child context."""
|
|
248
|
+
|
|
249
|
+
controller = AbortController()
|
|
250
|
+
return CancelScope(context=with_abort_signal(controller.signal, context), _controller=controller)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def await_with_context(awaitable: Awaitable[T], context: Context) -> T:
|
|
254
|
+
"""Await ``awaitable`` until it settles or ``context`` is cancelled.
|
|
255
|
+
|
|
256
|
+
Cancellation rejects only this waiter with :class:`AbortError`; the underlying awaitable is
|
|
257
|
+
left to finish.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
signal = context.abort_signal
|
|
261
|
+
if signal is None:
|
|
262
|
+
return await awaitable
|
|
263
|
+
if signal.aborted:
|
|
264
|
+
_close_coroutine(awaitable)
|
|
265
|
+
raise _cancellation_error(signal)
|
|
266
|
+
|
|
267
|
+
loop = asyncio.get_running_loop()
|
|
268
|
+
waiter: asyncio.Future[None] = loop.create_future()
|
|
269
|
+
|
|
270
|
+
def abort() -> None:
|
|
271
|
+
if not waiter.done():
|
|
272
|
+
waiter.set_exception(_cancellation_error(signal))
|
|
273
|
+
|
|
274
|
+
remove = signal.add_listener(abort)
|
|
275
|
+
if signal.aborted:
|
|
276
|
+
abort()
|
|
277
|
+
task: asyncio.Future[T] = asyncio.ensure_future(awaitable)
|
|
278
|
+
try:
|
|
279
|
+
await asyncio.wait((task, waiter), return_when=asyncio.FIRST_COMPLETED)
|
|
280
|
+
if task.done():
|
|
281
|
+
return task.result()
|
|
282
|
+
task.add_done_callback(_discard_result)
|
|
283
|
+
waiter.result()
|
|
284
|
+
raise AssertionError("unreachable")
|
|
285
|
+
finally:
|
|
286
|
+
remove()
|
|
287
|
+
if not waiter.done():
|
|
288
|
+
waiter.cancel()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _cancellation_error(signal: AbortSignal) -> BaseException:
|
|
292
|
+
"""An aborted context rejects with its reason when it is an error, else ``AbortError``."""
|
|
293
|
+
|
|
294
|
+
reason = signal.reason
|
|
295
|
+
return reason if isinstance(reason, BaseException) else AbortError(reason)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _discard_result(task: asyncio.Future[Any]) -> None:
|
|
299
|
+
if task.cancelled():
|
|
300
|
+
return
|
|
301
|
+
try:
|
|
302
|
+
task.exception()
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _close_coroutine(awaitable: Awaitable[Any]) -> None:
|
|
308
|
+
close = getattr(awaitable, "close", None)
|
|
309
|
+
if close is not None:
|
|
310
|
+
close()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
__all__ = [
|
|
314
|
+
"BACKGROUND_CONTEXT",
|
|
315
|
+
"TODO_CONTEXT",
|
|
316
|
+
"AbortController",
|
|
317
|
+
"AbortError",
|
|
318
|
+
"AbortSignal",
|
|
319
|
+
"CancelScope",
|
|
320
|
+
"Context",
|
|
321
|
+
"ContextKey",
|
|
322
|
+
"await_with_context",
|
|
323
|
+
"create_context_key",
|
|
324
|
+
"with_abort_signal",
|
|
325
|
+
"with_cancel",
|
|
326
|
+
"with_context_value",
|
|
327
|
+
"without_abort_signal",
|
|
328
|
+
]
|