taskferry 0.2.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 (48) hide show
  1. taskferry/__init__.py +211 -0
  2. taskferry/aio.py +486 -0
  3. taskferry/backends/__init__.py +38 -0
  4. taskferry/backends/inline.py +235 -0
  5. taskferry/backends/process.py +292 -0
  6. taskferry/backends/subprocess.py +390 -0
  7. taskferry/backends/thread.py +351 -0
  8. taskferry/capabilities.py +90 -0
  9. taskferry/cli.py +445 -0
  10. taskferry/config.py +360 -0
  11. taskferry/contract/__init__.py +56 -0
  12. taskferry/contract/base.py +179 -0
  13. taskferry/contract/inline.py +89 -0
  14. taskferry/contract/job.py +91 -0
  15. taskferry/contract/task.py +91 -0
  16. taskferry/core/__init__.py +130 -0
  17. taskferry/core/capabilities.py +89 -0
  18. taskferry/core/config.py +167 -0
  19. taskferry/core/correlation.py +120 -0
  20. taskferry/core/delivery.py +36 -0
  21. taskferry/core/errors.py +55 -0
  22. taskferry/core/ids.py +37 -0
  23. taskferry/core/observability.py +136 -0
  24. taskferry/core/otel.py +83 -0
  25. taskferry/core/provider.py +50 -0
  26. taskferry/core/py.typed +0 -0
  27. taskferry/core/registry.py +92 -0
  28. taskferry/core/serialization.py +79 -0
  29. taskferry/core/typing.py +16 -0
  30. taskferry/envelope.py +197 -0
  31. taskferry/errors.py +144 -0
  32. taskferry/execution.py +239 -0
  33. taskferry/functions.py +290 -0
  34. taskferry/handle.py +186 -0
  35. taskferry/hooks.py +238 -0
  36. taskferry/plugins.py +183 -0
  37. taskferry/ports.py +356 -0
  38. taskferry/py.typed +0 -0
  39. taskferry/retry.py +205 -0
  40. taskferry/router.py +160 -0
  41. taskferry/runtime.py +609 -0
  42. taskferry/specs.py +353 -0
  43. taskferry/tracking.py +129 -0
  44. taskferry-0.2.0.dist-info/METADATA +109 -0
  45. taskferry-0.2.0.dist-info/RECORD +48 -0
  46. taskferry-0.2.0.dist-info/WHEEL +4 -0
  47. taskferry-0.2.0.dist-info/entry_points.txt +2 -0
  48. taskferry-0.2.0.dist-info/licenses/LICENSE +201 -0
taskferry/__init__.py ADDED
@@ -0,0 +1,211 @@
1
+ """Taskferry — a portable execution layer for Python.
2
+
3
+ Taskferry models units of work, picks the right kind of execution, and routes them
4
+ to engines that already exist. It is not a task queue, not a worker system, not a
5
+ scheduler and not a workflow engine. Its whole value is that your application
6
+ never has to name one.
7
+
8
+ ```mermaid
9
+ flowchart LR
10
+ APP["Application / library"]
11
+ TP["Taskferry"]
12
+ ENGINE["Execution engine"]
13
+ INFRA["Infrastructure"]
14
+
15
+ APP -->|"what to run"| TP
16
+ TP -->|"how and where"| ENGINE
17
+ ENGINE --> INFRA
18
+ ```
19
+
20
+ Three primitives, deliberately not collapsed into one:
21
+
22
+ ```mermaid
23
+ flowchart TD
24
+ TP["Taskferry"]
25
+
26
+ TP --> INLINE["Inline<br/>run it here, now"]
27
+ TP --> TASK["Task<br/>a named function, on an engine"]
28
+ TP --> JOB["Job<br/>a container/process, to completion"]
29
+
30
+ TASK --> PRO["Procrastinate"]
31
+ TASK --> CT["Cloud Tasks"]
32
+ TASK --> CELERY["Celery · Django Tasks · ..."]
33
+
34
+ JOB --> CR["Cloud Run Jobs"]
35
+ JOB --> K8S["Kubernetes Jobs"]
36
+ JOB --> LOCAL["local process"]
37
+ ```
38
+
39
+ Sixty seconds in
40
+ ----------------
41
+
42
+ from taskferry import Taskferry
43
+
44
+ runtime = Taskferry.local()
45
+
46
+ def add(a: int, b: int) -> int:
47
+ return a + b
48
+
49
+ execution = runtime.inline.submit(add, 20, 22)
50
+ assert execution.result().value == 42
51
+
52
+ That needs no Django, no PostgreSQL, no Redis, no Procrastinate, no cloud account
53
+ and no worker process — and `import taskferry` imports none of them either, ever.
54
+ Adapters are separate distributions (``taskferry-procrastinate``,
55
+ ``taskferry-cloudrun``, ``taskferry-django``, ...) that depend on Taskferry, never
56
+ the other way around.
57
+
58
+ Guarantees, stated plainly
59
+ --------------------------
60
+
61
+ Taskferry promises **at-least-once or at-most-once, depending on the backend**,
62
+ and never exactly-once — no distributed system can honestly offer that. Every
63
+ backend declares its real delivery semantics, and
64
+ :attr:`~taskferry.specs.ExecutionSpec.idempotency_key` is offered as a tool for
65
+ building idempotency, not as a guarantee. See ADR-0010.
66
+
67
+ Async
68
+ -----
69
+
70
+ from taskferry import AsyncTaskferry
71
+
72
+ runtime = AsyncTaskferry.local()
73
+ handle = await runtime.tasks.submit("myapp.tasks:send_email", 42)
74
+ execution = await handle.wait(30)
75
+
76
+ Same method names as the sync runtime — the difference is `await`, not the
77
+ vocabulary. See :mod:`taskferry.aio` and ADR-0015.
78
+
79
+ Everything a normal application needs is importable from here. Deeper modules
80
+ (:mod:`taskferry.ports`, :mod:`taskferry.contract`, :mod:`taskferry.core`) are for
81
+ people writing adapters.
82
+ """
83
+
84
+ from __future__ import annotations
85
+
86
+ from .aio import AsyncExecutionHandle, AsyncTaskferry
87
+ from .capabilities import Capability, CapabilitySet
88
+ from .config import BackendConfig, TaskferryConfig
89
+ from .core.correlation import Correlation, current_correlation, ensure_correlation, use_correlation
90
+ from .core.delivery import DeliveryGuarantee, Ordering
91
+ from .core.provider import ProviderMetadata
92
+ from .core.serialization import JsonSerializer, Serializer
93
+ from .errors import (
94
+ BackendError,
95
+ ConfigurationError,
96
+ ExecutionCancelled,
97
+ ExecutionError,
98
+ ExecutionNotFound,
99
+ FunctionResolutionError,
100
+ RoutingError,
101
+ SerializationError,
102
+ SubmissionError,
103
+ TaskferryError,
104
+ TaskferryTimeoutError,
105
+ UnsupportedCapability,
106
+ )
107
+ from .execution import (
108
+ Execution,
109
+ ExecutionId,
110
+ ExecutionKind,
111
+ ExecutionResult,
112
+ ExecutionState,
113
+ )
114
+ from .functions import FunctionRef, FunctionRegistry
115
+ from .handle import ExecutionHandle
116
+ from .hooks import BaseHook, Hook, HookChain, LoggingHook
117
+ from .plugins import available_backends, register_backend, unregister_backend
118
+ from .ports import BaseBackend, ExecutionBackend, InlineBackend, JobBackend, TaskBackend
119
+ from .retry import Backoff, RetryOwner, RetryPolicy, TimeoutPolicy
120
+ from .router import Route, Router
121
+ from .runtime import Taskferry
122
+ from .specs import (
123
+ AnySpec,
124
+ BackendOptions,
125
+ ExecutionSpec,
126
+ InlineSpec,
127
+ JobSpec,
128
+ Resources,
129
+ TaskSpec,
130
+ )
131
+
132
+ __version__ = "0.2.0"
133
+
134
+ # Grouped by concept rather than alphabetically: this list doubles as the map of
135
+ # the public API, and "everything about executions, together" is far more useful
136
+ # to a reader than strict alphabetical order would be.
137
+ __all__ = [ # noqa: RUF022 - deliberately grouped, see above
138
+ # runtime
139
+ "Taskferry",
140
+ "AsyncTaskferry",
141
+ "TaskferryConfig",
142
+ "BackendConfig",
143
+ "Route",
144
+ "Router",
145
+ # specs
146
+ "AnySpec",
147
+ "BackendOptions",
148
+ "ExecutionSpec",
149
+ "InlineSpec",
150
+ "JobSpec",
151
+ "Resources",
152
+ "TaskSpec",
153
+ # executions
154
+ "Execution",
155
+ "ExecutionHandle",
156
+ "AsyncExecutionHandle",
157
+ "ExecutionId",
158
+ "ExecutionKind",
159
+ "ExecutionResult",
160
+ "ExecutionState",
161
+ # policies
162
+ "Backoff",
163
+ "RetryOwner",
164
+ "RetryPolicy",
165
+ "TimeoutPolicy",
166
+ # capabilities
167
+ "Capability",
168
+ "CapabilitySet",
169
+ # ports (for adapter authors)
170
+ "BaseBackend",
171
+ "ExecutionBackend",
172
+ "InlineBackend",
173
+ "JobBackend",
174
+ "TaskBackend",
175
+ # functions
176
+ "FunctionRef",
177
+ "FunctionRegistry",
178
+ # observability
179
+ "BaseHook",
180
+ "Correlation",
181
+ "Hook",
182
+ "HookChain",
183
+ "LoggingHook",
184
+ "current_correlation",
185
+ "ensure_correlation",
186
+ "use_correlation",
187
+ # semantics + serialization
188
+ "DeliveryGuarantee",
189
+ "JsonSerializer",
190
+ "Ordering",
191
+ "ProviderMetadata",
192
+ "Serializer",
193
+ # plugins
194
+ "available_backends",
195
+ "register_backend",
196
+ "unregister_backend",
197
+ # errors
198
+ "BackendError",
199
+ "ConfigurationError",
200
+ "ExecutionCancelled",
201
+ "ExecutionError",
202
+ "ExecutionNotFound",
203
+ "FunctionResolutionError",
204
+ "RoutingError",
205
+ "SerializationError",
206
+ "SubmissionError",
207
+ "TaskferryError",
208
+ "TaskferryTimeoutError",
209
+ "UnsupportedCapability",
210
+ "__version__",
211
+ ]
taskferry/aio.py ADDED
@@ -0,0 +1,486 @@
1
+ """The async surface: `AsyncTaskferry`, with the same method names.
2
+
3
+ ```python
4
+ from taskferry import AsyncTaskferry
5
+
6
+ runtime = AsyncTaskferry.local()
7
+
8
+ handle = await runtime.tasks.submit("myapp.tasks:send_email", 42)
9
+ execution = await handle.wait(30)
10
+ value = await handle.value()
11
+ ```
12
+
13
+ Two runtimes, one vocabulary
14
+ ----------------------------
15
+
16
+ `AsyncTaskferry` mirrors :class:`~taskferry.runtime.Taskferry` method for method,
17
+ with the *same names*. That is deliberate, and it is why there is a second class
18
+ rather than an `asubmit`/`aget`/`acancel` prefix soup bolted onto the first:
19
+
20
+ * `await` is a keyword, so a prefixed `wait` has nowhere to go — `await_()`,
21
+ `awaited()` and `wait_async()` are all worse than `wait()`;
22
+ * porting a module between the two surfaces becomes adding or removing `await`,
23
+ not rewriting every call site;
24
+ * it is the convention Python has settled on — `httpx.Client` /
25
+ `httpx.AsyncClient`, `redis.Redis` / `redis.asyncio.Redis`.
26
+
27
+ The adapter-facing port keeps the `a`-prefixed names (`asubmit`, `aget`,
28
+ `acancel`, `aresult`, `await_`), because there the two surfaces sit on **one**
29
+ object and have to be told apart. Adapter authors read those; application
30
+ developers do not. See ADR-0015.
31
+
32
+ ```mermaid
33
+ flowchart TD
34
+ APP["async application"]
35
+ ART["AsyncTaskferry"]
36
+ RT["Taskferry<br/>routing · config · backends"]
37
+ BE["backend.asubmit()"]
38
+
39
+ APP -->|"await tasks.submit(...)"| ART
40
+ ART -->|"shares everything"| RT
41
+ ART --> BE
42
+ ```
43
+
44
+ What is shared, and what is not
45
+ -------------------------------
46
+
47
+ An `AsyncTaskferry` **wraps** a sync runtime rather than duplicating it. Routing,
48
+ configuration, the backend cache, the execution index, hooks and the function
49
+ registry are the same objects — so a process can hold one runtime and expose both
50
+ surfaces without two connection pools, two backend instances or two views of
51
+ which execution went where:
52
+
53
+ ```python
54
+ runtime = Taskferry.local()
55
+ aio = AsyncTaskferry(runtime) # same backends, same everything
56
+
57
+ handle = runtime.tasks.submit(fn, 1) # from a management command
58
+ handle = await aio.tasks.submit(fn, 1) # from an async view
59
+ ```
60
+
61
+ Blocking, honestly
62
+ ------------------
63
+
64
+ A backend's async path defaults to `asyncio.to_thread` around its sync path (see
65
+ :class:`~taskferry.ports.BaseBackend`). That is a real async path — the caller's
66
+ event loop keeps running — not an `async def` painted over a blocking call. An
67
+ adapter with a natively async client overrides it and skips the thread; nothing
68
+ here depends on whether it has.
69
+
70
+ The one thing this cannot fix is an `async def` **task function** running on an
71
+ in-process backend: the thread pool runs it with `asyncio.run` on its own loop,
72
+ because that is what a worker in another process would do too.
73
+ """
74
+
75
+ from __future__ import annotations
76
+
77
+ import asyncio
78
+ from collections.abc import Callable, Mapping, Sequence
79
+ from datetime import datetime, timedelta
80
+ from types import TracebackType
81
+ from typing import Any, Self
82
+
83
+ from .capabilities import Capability, CapabilitySet
84
+ from .config import TaskferryConfig
85
+ from .core.correlation import Correlation
86
+ from .core.typing import JSONValue
87
+ from .errors import ExecutionError, UnsupportedCapability
88
+ from .execution import Execution, ExecutionId, ExecutionKind, ExecutionResult, ExecutionState
89
+ from .functions import FunctionRef
90
+ from .ports import ExecutionBackend
91
+ from .retry import NO_RETRY, NO_TIMEOUT, RetryPolicy, TimeoutPolicy
92
+ from .router import Router
93
+ from .runtime import Taskferry, _as_backend_options, _as_timedelta, _as_timeout
94
+ from .specs import AnySpec, BackendOptions, InlineSpec, JobSpec, Resources, TaskSpec
95
+
96
+
97
+ class AsyncExecutionHandle:
98
+ """The async twin of :class:`~taskferry.handle.ExecutionHandle`.
99
+
100
+ Same properties, same method names; the methods that touch the backend are
101
+ coroutines. Cheap, non-blocking reads (:attr:`id`, :attr:`state`,
102
+ :attr:`done`) stay plain attributes — making them coroutines would force an
103
+ `await` on data already in memory.
104
+ """
105
+
106
+ __slots__ = ("_backend", "_execution", "_lock")
107
+
108
+ def __init__(self, execution: Execution, backend: ExecutionBackend) -> None:
109
+ self._execution = execution
110
+ self._backend = backend
111
+ self._lock = asyncio.Lock()
112
+
113
+ # -- identity (no I/O, no await) ------------------------------------------ #
114
+ @property
115
+ def id(self) -> ExecutionId:
116
+ return self._execution.id
117
+
118
+ @property
119
+ def kind(self) -> ExecutionKind:
120
+ return self._execution.kind
121
+
122
+ @property
123
+ def name(self) -> str:
124
+ return self._execution.name
125
+
126
+ @property
127
+ def backend(self) -> str:
128
+ return self._backend.name
129
+
130
+ @property
131
+ def capabilities(self) -> CapabilitySet:
132
+ return self._backend.capabilities
133
+
134
+ @property
135
+ def external_id(self) -> str | None:
136
+ return self._execution.external_id
137
+
138
+ @property
139
+ def execution(self) -> Execution:
140
+ """The most recent snapshot. Does not contact the backend."""
141
+ return self._execution
142
+
143
+ @property
144
+ def state(self) -> ExecutionState:
145
+ return self._execution.state
146
+
147
+ @property
148
+ def done(self) -> bool:
149
+ return self._execution.is_terminal
150
+
151
+ def __repr__(self) -> str:
152
+ return (
153
+ f"<AsyncExecutionHandle {self._execution.kind.value} {self._execution.id} "
154
+ f"backend={self.backend!r} state={self._execution.state.value!r}>"
155
+ )
156
+
157
+ # -- live operations ------------------------------------------------------- #
158
+ async def refresh(self) -> Execution:
159
+ """Re-read from the backend. A terminal snapshot is never re-read."""
160
+ async with self._lock:
161
+ if self._execution.is_terminal:
162
+ return self._execution
163
+ fresh = await self._backend.aget(self.id)
164
+ async with self._lock:
165
+ self._execution = fresh
166
+ return fresh
167
+
168
+ async def status(self) -> ExecutionState:
169
+ return (await self.refresh()).state
170
+
171
+ async def wait(self, timeout: float | None = None) -> Execution:
172
+ """Await terminal state without blocking the event loop."""
173
+ async with self._lock:
174
+ if self._execution.is_terminal:
175
+ return self._execution
176
+ final = await self._backend.await_(self.id, timeout=timeout)
177
+ async with self._lock:
178
+ self._execution = final
179
+ return final
180
+
181
+ async def result(self, timeout: float | None = None) -> ExecutionResult:
182
+ """Return the outcome, raising :class:`~taskferry.errors.ExecutionError` on failure."""
183
+ outcome = await self._backend.aresult(self.id, timeout=timeout)
184
+ if outcome.error is not None:
185
+ raise ExecutionError(
186
+ f"{self.kind.value} {self.id} failed: {outcome.error}",
187
+ backend=self.backend,
188
+ cause_repr=outcome.traceback or outcome.error,
189
+ )
190
+ return outcome
191
+
192
+ async def value(self, timeout: float | None = None) -> Any:
193
+ """The value the execution produced."""
194
+ return (await self.result(timeout)).value
195
+
196
+ async def cancel(self) -> Execution:
197
+ if not self._backend.capabilities.supports(Capability.CANCEL):
198
+ raise UnsupportedCapability(Capability.CANCEL.value, provider=self.backend)
199
+ cancelled = await self._backend.acancel(self.id)
200
+ async with self._lock:
201
+ self._execution = cancelled
202
+ return cancelled
203
+
204
+
205
+ class AsyncTaskferry:
206
+ """The portable execution layer, for async applications.
207
+
208
+ Args:
209
+ runtime: An existing :class:`~taskferry.runtime.Taskferry` to share. When
210
+ omitted, one is built from ``**kwargs`` exactly as the sync
211
+ constructor would.
212
+
213
+ Sharing a runtime is the recommended pattern in a process that has both an
214
+ async web surface and sync management commands — one set of backends, one
215
+ connection pool, one execution index.
216
+ """
217
+
218
+ def __init__(self, runtime: Taskferry | None = None, /, **kwargs: Any) -> None:
219
+ self._runtime = runtime if runtime is not None else Taskferry(**kwargs)
220
+ self.inline = AsyncInlineFacade(self)
221
+ self.tasks = AsyncTaskFacade(self)
222
+ self.jobs = AsyncJobFacade(self)
223
+
224
+ # -- constructors ---------------------------------------------------------- #
225
+ @classmethod
226
+ def local(cls, **kwargs: Any) -> AsyncTaskferry:
227
+ """A runtime that needs no infrastructure. See :meth:`Taskferry.local`."""
228
+ return cls(Taskferry.local(**kwargs))
229
+
230
+ @classmethod
231
+ def from_env(cls, environ: Mapping[str, str] | None = None, **kwargs: Any) -> AsyncTaskferry:
232
+ return cls(Taskferry.from_env(environ, **kwargs))
233
+
234
+ @classmethod
235
+ def from_mapping(cls, data: Mapping[str, Any], **kwargs: Any) -> AsyncTaskferry:
236
+ return cls(Taskferry.from_mapping(data, **kwargs))
237
+
238
+ # -- introspection (shared, synchronous — no I/O) --------------------------- #
239
+ @property
240
+ def sync(self) -> Taskferry:
241
+ """The underlying sync runtime. Use it from sync code in the same process."""
242
+ return self._runtime
243
+
244
+ @property
245
+ def config(self) -> TaskferryConfig:
246
+ return self._runtime.config
247
+
248
+ @property
249
+ def router(self) -> Router:
250
+ return self._runtime.router
251
+
252
+ @property
253
+ def registry(self) -> Any:
254
+ return self._runtime.registry
255
+
256
+ def backend_names(self) -> tuple[str, ...]:
257
+ return self._runtime.backend_names()
258
+
259
+ def backend(self, name: str) -> ExecutionBackend:
260
+ return self._runtime.backend(name)
261
+
262
+ def capabilities(self, name: str) -> CapabilitySet:
263
+ return self._runtime.capabilities(name)
264
+
265
+ def describe(self) -> dict[str, Any]:
266
+ return self._runtime.describe()
267
+
268
+ def __repr__(self) -> str:
269
+ return f"<AsyncTaskferry backends={list(self.backend_names())}>"
270
+
271
+ # -- submission -------------------------------------------------------------- #
272
+ async def submit(
273
+ self,
274
+ spec: AnySpec,
275
+ *,
276
+ backend: str | None = None,
277
+ idempotency_key: str | None = None,
278
+ ) -> AsyncExecutionHandle:
279
+ """Route ``spec`` to a backend and submit it without blocking the loop."""
280
+ target, resolved, prepared = self._runtime._prepare(
281
+ spec, backend=backend, idempotency_key=idempotency_key
282
+ )
283
+ execution = await target.asubmit(prepared)
284
+ self._runtime._track(execution.id, resolved)
285
+ return AsyncExecutionHandle(execution, target)
286
+
287
+ async def get(
288
+ self, execution_id: ExecutionId | str, *, backend: str | None = None
289
+ ) -> AsyncExecutionHandle:
290
+ target = self._runtime._owner_backend(execution_id, backend)
291
+ execution = await target.aget(ExecutionId(str(execution_id)))
292
+ return AsyncExecutionHandle(execution, target)
293
+
294
+ async def cancel(
295
+ self, execution_id: ExecutionId | str, *, backend: str | None = None
296
+ ) -> Execution:
297
+ return await (await self.get(execution_id, backend=backend)).cancel()
298
+
299
+ async def wait(
300
+ self,
301
+ execution_id: ExecutionId | str,
302
+ *,
303
+ timeout: float | None = None,
304
+ backend: str | None = None,
305
+ ) -> Execution:
306
+ return await (await self.get(execution_id, backend=backend)).wait(timeout)
307
+
308
+ async def result(
309
+ self,
310
+ execution_id: ExecutionId | str,
311
+ *,
312
+ timeout: float | None = None,
313
+ backend: str | None = None,
314
+ ) -> ExecutionResult:
315
+ return await (await self.get(execution_id, backend=backend)).result(timeout)
316
+
317
+ # -- lifecycle ---------------------------------------------------------------- #
318
+ async def close(self) -> None:
319
+ """Release every backend's resources, off the event loop."""
320
+ await asyncio.to_thread(self._runtime.close)
321
+
322
+ async def __aenter__(self) -> Self:
323
+ return self
324
+
325
+ async def __aexit__(
326
+ self,
327
+ exc_type: type[BaseException] | None,
328
+ exc: BaseException | None,
329
+ tb: TracebackType | None,
330
+ ) -> None:
331
+ await self.close()
332
+
333
+
334
+ class _AsyncFacade:
335
+ __slots__ = ("_aio",)
336
+
337
+ def __init__(self, aio: AsyncTaskferry) -> None:
338
+ self._aio = aio
339
+
340
+ async def submit_spec(
341
+ self, spec: AnySpec, *, backend: str | None = None
342
+ ) -> AsyncExecutionHandle:
343
+ """Submit an already-built spec."""
344
+ return await self._aio.submit(spec, backend=backend)
345
+
346
+
347
+ class AsyncInlineFacade(_AsyncFacade):
348
+ """``runtime.inline`` — run a callable now, without blocking the loop.
349
+
350
+ "Inline" still means "this process", but on the async surface the work runs
351
+ on a worker thread so the loop keeps serving. An ``async def`` callable is
352
+ awaited on that thread's own loop.
353
+
354
+ If you are already in async code and the work is a coroutine, `await` it
355
+ directly — you do not need Taskferry to run something in the process you are
356
+ already in. Use this when the callable is *sync* and would otherwise block.
357
+ """
358
+
359
+ __slots__ = ()
360
+
361
+ async def submit(
362
+ self,
363
+ func: Callable[..., Any],
364
+ /,
365
+ *args: Any,
366
+ backend: str | None = None,
367
+ name: str = "",
368
+ queue: str = "default",
369
+ retry: RetryPolicy = NO_RETRY,
370
+ labels: Mapping[str, str] | None = None,
371
+ correlation: Correlation | None = None,
372
+ **kwargs: Any,
373
+ ) -> AsyncExecutionHandle:
374
+ spec = InlineSpec(
375
+ func=func,
376
+ args=args,
377
+ kwargs=kwargs,
378
+ name=name,
379
+ queue=queue,
380
+ retry=retry,
381
+ labels=labels or {},
382
+ correlation=correlation,
383
+ )
384
+ return await self._aio.submit(spec, backend=backend)
385
+
386
+ async def run(self, func: Callable[..., Any], /, *args: Any, **kwargs: Any) -> Any:
387
+ """Run ``func`` and return its value, raising on failure."""
388
+ handle = await self.submit(func, *args, **kwargs)
389
+ return await handle.value()
390
+
391
+
392
+ class AsyncTaskFacade(_AsyncFacade):
393
+ """``runtime.tasks`` — hand a named function to a task engine."""
394
+
395
+ __slots__ = ()
396
+
397
+ async def submit(
398
+ self,
399
+ task: str | Callable[..., Any] | FunctionRef,
400
+ /,
401
+ *args: JSONValue,
402
+ backend: str | None = None,
403
+ queue: str = "default",
404
+ priority: int = 0,
405
+ delay: timedelta | float | None = None,
406
+ run_at: datetime | None = None,
407
+ retry: RetryPolicy = NO_RETRY,
408
+ timeout: TimeoutPolicy = NO_TIMEOUT,
409
+ idempotency_key: str | None = None,
410
+ labels: Mapping[str, str] | None = None,
411
+ backend_options: BackendOptions | Mapping[str, Any] | None = None,
412
+ correlation: Correlation | None = None,
413
+ **kwargs: JSONValue,
414
+ ) -> AsyncExecutionHandle:
415
+ ref = self._aio.registry.reference(task)
416
+ spec = TaskSpec(
417
+ task=ref.path,
418
+ args=args,
419
+ kwargs=kwargs,
420
+ queue=queue,
421
+ priority=priority,
422
+ delay=_as_timedelta(delay),
423
+ run_at=run_at,
424
+ retry=retry,
425
+ timeout=timeout,
426
+ idempotency_key=idempotency_key,
427
+ labels=labels or {},
428
+ backend_options=_as_backend_options(backend_options),
429
+ correlation=correlation,
430
+ )
431
+ return await self._aio.submit(spec, backend=backend)
432
+
433
+
434
+ class AsyncJobFacade(_AsyncFacade):
435
+ """``runtime.jobs`` — run an isolated workload to completion."""
436
+
437
+ __slots__ = ()
438
+
439
+ async def submit(
440
+ self,
441
+ job: str,
442
+ /,
443
+ *,
444
+ backend: str | None = None,
445
+ image: str | None = None,
446
+ command: Sequence[str] = (),
447
+ args: Sequence[str] = (),
448
+ env: Mapping[str, str] | None = None,
449
+ resources: Resources | None = None,
450
+ profile: str = "default",
451
+ parallelism: int = 1,
452
+ working_dir: str | None = None,
453
+ retry: RetryPolicy = NO_RETRY,
454
+ timeout: TimeoutPolicy | float | None = None,
455
+ idempotency_key: str | None = None,
456
+ labels: Mapping[str, str] | None = None,
457
+ backend_options: BackendOptions | Mapping[str, Any] | None = None,
458
+ correlation: Correlation | None = None,
459
+ ) -> AsyncExecutionHandle:
460
+ spec = JobSpec(
461
+ job=job,
462
+ image=image,
463
+ command=command,
464
+ args=args,
465
+ env=env or {},
466
+ resources=resources if resources is not None else Resources(),
467
+ profile=profile,
468
+ parallelism=parallelism,
469
+ working_dir=working_dir,
470
+ retry=retry,
471
+ timeout=_as_timeout(timeout),
472
+ idempotency_key=idempotency_key,
473
+ labels=labels or {},
474
+ backend_options=_as_backend_options(backend_options),
475
+ correlation=correlation,
476
+ )
477
+ return await self._aio.submit(spec, backend=backend)
478
+
479
+
480
+ __all__ = [
481
+ "AsyncExecutionHandle",
482
+ "AsyncInlineFacade",
483
+ "AsyncJobFacade",
484
+ "AsyncTaskFacade",
485
+ "AsyncTaskferry",
486
+ ]