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.
Files changed (90) hide show
  1. pi_durable/__init__.py +320 -0
  2. pi_durable/context.py +328 -0
  3. pi_durable/delta.py +269 -0
  4. pi_durable/documents.py +366 -0
  5. pi_durable/entries.py +66 -0
  6. pi_durable/env/__init__.py +408 -0
  7. pi_durable/env/decode.py +48 -0
  8. pi_durable/env/line_scan.py +190 -0
  9. pi_durable/env/posix.py +1547 -0
  10. pi_durable/errors.py +89 -0
  11. pi_durable/harness/__init__.py +1 -0
  12. pi_durable/harness/agent.py +344 -0
  13. pi_durable/harness/compaction.py +673 -0
  14. pi_durable/harness/context.py +515 -0
  15. pi_durable/harness/define.py +111 -0
  16. pi_durable/harness/events.py +766 -0
  17. pi_durable/harness/generation.py +1079 -0
  18. pi_durable/harness/harness.py +570 -0
  19. pi_durable/harness/inbox.py +255 -0
  20. pi_durable/harness/live.py +374 -0
  21. pi_durable/harness/output.py +467 -0
  22. pi_durable/harness/prompt.py +288 -0
  23. pi_durable/harness/provider.py +78 -0
  24. pi_durable/harness/registry.py +136 -0
  25. pi_durable/harness/scheduler.py +1977 -0
  26. pi_durable/harness/submissions.py +357 -0
  27. pi_durable/harness/task_graph.py +334 -0
  28. pi_durable/harness/tool.py +1365 -0
  29. pi_durable/harness/types.py +772 -0
  30. pi_durable/harness/usage.py +88 -0
  31. pi_durable/harness/util.py +204 -0
  32. pi_durable/harness/validation.py +460 -0
  33. pi_durable/harness/view.py +324 -0
  34. pi_durable/ids.py +63 -0
  35. pi_durable/json.py +186 -0
  36. pi_durable/providers/__init__.py +362 -0
  37. pi_durable/providers/anthropic.py +603 -0
  38. pi_durable/providers/base.py +97 -0
  39. pi_durable/providers/json_parse.py +117 -0
  40. pi_durable/providers/openai.py +574 -0
  41. pi_durable/providers/request.py +35 -0
  42. pi_durable/providers/retry.py +127 -0
  43. pi_durable/providers/stream.py +183 -0
  44. pi_durable/providers/transcript.py +224 -0
  45. pi_durable/providers/usage.py +58 -0
  46. pi_durable/py.typed +0 -0
  47. pi_durable/search.py +115 -0
  48. pi_durable/session/__init__.py +5 -0
  49. pi_durable/session/drafts.py +762 -0
  50. pi_durable/session/forks.py +109 -0
  51. pi_durable/session/observation.py +374 -0
  52. pi_durable/session/session.py +1193 -0
  53. pi_durable/session/transaction.py +1088 -0
  54. pi_durable/storage/__init__.py +5 -0
  55. pi_durable/storage/jsonl/__init__.py +15 -0
  56. pi_durable/storage/jsonl/storage.py +958 -0
  57. pi_durable/storage/memory.py +889 -0
  58. pi_durable/storage/scan.py +57 -0
  59. pi_durable/storage/sqlite/__init__.py +26 -0
  60. pi_durable/storage/sqlite/database.py +62 -0
  61. pi_durable/storage/sqlite/migrations.py +191 -0
  62. pi_durable/storage/sqlite/posix.py +314 -0
  63. pi_durable/storage/sqlite/schema.py +97 -0
  64. pi_durable/storage/sqlite/storage.py +1130 -0
  65. pi_durable/tasks.py +89 -0
  66. pi_durable/testing/__init__.py +41 -0
  67. pi_durable/testing/assertions.py +44 -0
  68. pi_durable/testing/env_conformance.py +828 -0
  69. pi_durable/testing/runner.py +64 -0
  70. pi_durable/testing/storage_conformance.py +2008 -0
  71. pi_durable/testing/types.py +99 -0
  72. pi_durable/tool_search/__init__.py +159 -0
  73. pi_durable/tools/__init__.py +81 -0
  74. pi_durable/tools/_values.py +43 -0
  75. pi_durable/tools/bash.py +250 -0
  76. pi_durable/tools/edit.py +214 -0
  77. pi_durable/tools/edit_diff.py +452 -0
  78. pi_durable/tools/env.py +19 -0
  79. pi_durable/tools/file_mutation_queue.py +68 -0
  80. pi_durable/tools/image.py +248 -0
  81. pi_durable/tools/image_processor.py +77 -0
  82. pi_durable/tools/path_utils.py +111 -0
  83. pi_durable/tools/read.py +506 -0
  84. pi_durable/tools/write.py +71 -0
  85. pi_durable/truncate.py +185 -0
  86. pi_durable/types.py +1297 -0
  87. pi_durable_python-1.3.0.dist-info/METADATA +276 -0
  88. pi_durable_python-1.3.0.dist-info/RECORD +90 -0
  89. pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
  90. 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
+ ]