taskwire 0.1.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.
- taskwire/__init__.py +135 -0
- taskwire/ambient.py +161 -0
- taskwire/conformance.py +474 -0
- taskwire/contrib/__init__.py +17 -0
- taskwire/contrib/asgi.py +91 -0
- taskwire/contrib/celery.py +352 -0
- taskwire/contrib/fastapi.py +82 -0
- taskwire/contrib/muxws.py +370 -0
- taskwire/contrib/redis_store.py +575 -0
- taskwire/contrib/schema.py +204 -0
- taskwire/contrib/threads.py +43 -0
- taskwire/contrib/viewsets.py +292 -0
- taskwire/decorators.py +88 -0
- taskwire/delivery.py +123 -0
- taskwire/headers.py +36 -0
- taskwire/models.py +683 -0
- taskwire/py.typed +0 -0
- taskwire/reader.py +120 -0
- taskwire/register.py +182 -0
- taskwire/reporter.py +1162 -0
- taskwire/rest.py +312 -0
- taskwire/settings.py +126 -0
- taskwire/store.py +647 -0
- taskwire/transport.py +95 -0
- taskwire-0.1.0.dist-info/METADATA +138 -0
- taskwire-0.1.0.dist-info/RECORD +28 -0
- taskwire-0.1.0.dist-info/WHEEL +4 -0
- taskwire-0.1.0.dist-info/licenses/LICENSE +21 -0
taskwire/__init__.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""taskwire - progress reporting and awaitable dialogs for long-running operations.
|
|
2
|
+
|
|
3
|
+
The store is the truth; a push is only an accelerator. Every state change is written before anything
|
|
4
|
+
is sent anywhere, so a dropped, coalesced or suppressed push never changes what the next read
|
|
5
|
+
returns - which is what lets the transport be swapped without changing behaviour.
|
|
6
|
+
|
|
7
|
+
The layering is: one **protocol** (the documents of §3 and the state machines of §4, in `models`),
|
|
8
|
+
then three independent **implementations** of it - local (`transport.LocalTransport`), REST
|
|
9
|
+
(`rest`), and WebSocket (`contrib.muxws`) - each with one **adapter** binding it to the world
|
|
10
|
+
outside. Nothing in the core imports a framework, a driver or a UI library (TW-CORE-006).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
from .ambient import ask, current_reporter, progress
|
|
18
|
+
from .conformance import run_conformance
|
|
19
|
+
from .decorators import (
|
|
20
|
+
declared_result_kind,
|
|
21
|
+
declared_result_params,
|
|
22
|
+
dialog_result,
|
|
23
|
+
downloadable_result,
|
|
24
|
+
panel_result,
|
|
25
|
+
)
|
|
26
|
+
from .delivery import Client, OperationRef
|
|
27
|
+
from .headers import CONNECTION_HEADER, TOKEN_HEADER
|
|
28
|
+
from .models import (
|
|
29
|
+
Aggregate,
|
|
30
|
+
Button,
|
|
31
|
+
DialogAnswer,
|
|
32
|
+
DialogReply,
|
|
33
|
+
DialogRequest,
|
|
34
|
+
DialogState,
|
|
35
|
+
Envelope,
|
|
36
|
+
EnvelopeKind,
|
|
37
|
+
Error,
|
|
38
|
+
Input,
|
|
39
|
+
OperationSummary,
|
|
40
|
+
Progress,
|
|
41
|
+
ProgressState,
|
|
42
|
+
Register,
|
|
43
|
+
Result,
|
|
44
|
+
ResultRef,
|
|
45
|
+
Snapshot,
|
|
46
|
+
t,
|
|
47
|
+
Text,
|
|
48
|
+
)
|
|
49
|
+
from .reader import check_taskwire_readers, start_taskwire_reader, stop_taskwire_reader
|
|
50
|
+
from .register import aggregate, build_register
|
|
51
|
+
from .reporter import (
|
|
52
|
+
InertReporter,
|
|
53
|
+
mark_failed,
|
|
54
|
+
mark_queued,
|
|
55
|
+
migrate_namespace,
|
|
56
|
+
new_token,
|
|
57
|
+
operation,
|
|
58
|
+
OperationCancelled,
|
|
59
|
+
Reporter,
|
|
60
|
+
SyncReporter,
|
|
61
|
+
)
|
|
62
|
+
from .settings import configure, configured, reset_configuration, Settings, settings
|
|
63
|
+
from .store import (
|
|
64
|
+
Commit,
|
|
65
|
+
DialogResolution,
|
|
66
|
+
DialogVanished,
|
|
67
|
+
key,
|
|
68
|
+
MemoryStore,
|
|
69
|
+
TaskwireStore,
|
|
70
|
+
)
|
|
71
|
+
from .transport import LocalTransport, NullTransport, TaskwireTransport
|
|
72
|
+
|
|
73
|
+
__all__ = [
|
|
74
|
+
"Aggregate",
|
|
75
|
+
"Button",
|
|
76
|
+
"Client",
|
|
77
|
+
"Commit",
|
|
78
|
+
"DialogAnswer",
|
|
79
|
+
"DialogReply",
|
|
80
|
+
"DialogRequest",
|
|
81
|
+
"DialogResolution",
|
|
82
|
+
"DialogState",
|
|
83
|
+
"DialogVanished",
|
|
84
|
+
"Envelope",
|
|
85
|
+
"EnvelopeKind",
|
|
86
|
+
"Error",
|
|
87
|
+
"InertReporter",
|
|
88
|
+
"Input",
|
|
89
|
+
"LocalTransport",
|
|
90
|
+
"MemoryStore",
|
|
91
|
+
"NullTransport",
|
|
92
|
+
"OperationCancelled",
|
|
93
|
+
"OperationRef",
|
|
94
|
+
"OperationSummary",
|
|
95
|
+
"Progress",
|
|
96
|
+
"ProgressState",
|
|
97
|
+
"Register",
|
|
98
|
+
"Reporter",
|
|
99
|
+
"Result",
|
|
100
|
+
"ResultRef",
|
|
101
|
+
"Settings",
|
|
102
|
+
"Snapshot",
|
|
103
|
+
"SyncReporter",
|
|
104
|
+
"TaskwireStore",
|
|
105
|
+
"TaskwireTransport",
|
|
106
|
+
"Text",
|
|
107
|
+
"__version__",
|
|
108
|
+
"ask",
|
|
109
|
+
"progress",
|
|
110
|
+
"current_reporter",
|
|
111
|
+
"aggregate",
|
|
112
|
+
"build_register",
|
|
113
|
+
"check_taskwire_readers",
|
|
114
|
+
"dialog_result",
|
|
115
|
+
"downloadable_result",
|
|
116
|
+
"panel_result",
|
|
117
|
+
"declared_result_kind",
|
|
118
|
+
"declared_result_params",
|
|
119
|
+
"configure",
|
|
120
|
+
"configured",
|
|
121
|
+
"CONNECTION_HEADER",
|
|
122
|
+
"key",
|
|
123
|
+
"mark_failed",
|
|
124
|
+
"mark_queued",
|
|
125
|
+
"migrate_namespace",
|
|
126
|
+
"new_token",
|
|
127
|
+
"TOKEN_HEADER",
|
|
128
|
+
"operation",
|
|
129
|
+
"reset_configuration",
|
|
130
|
+
"run_conformance",
|
|
131
|
+
"settings",
|
|
132
|
+
"start_taskwire_reader",
|
|
133
|
+
"stop_taskwire_reader",
|
|
134
|
+
"t",
|
|
135
|
+
]
|
taskwire/ambient.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""The ambient reporter: `taskwire.progress` and `taskwire.ask`.
|
|
2
|
+
|
|
3
|
+
Deep in a call stack, threading a `Reporter` through six functions that have no other reason to know
|
|
4
|
+
about progress is the kind of change people decline to make. The ambient proxy is the answer: bind
|
|
5
|
+
once at the top, and `await progress.set(...)` works anywhere underneath.
|
|
6
|
+
|
|
7
|
+
**Python only.** TW-SYM-004 forbids a TypeScript twin: `contextvars` has no browser equivalent, and
|
|
8
|
+
TW-AMB-002 makes an unbound proxy a *silent* no-op - so a proxy that worked under Node and no-opped
|
|
9
|
+
in a browser would fail as a progress bar that never moves, with nothing logged anywhere.
|
|
10
|
+
|
|
11
|
+
## The trap this module exists to avoid
|
|
12
|
+
|
|
13
|
+
Concurrent siblings MUST each receive their own reporter **explicitly** (TW-NEST-014). The ambient
|
|
14
|
+
binding is one variable; a `gather` is many tasks sharing it.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
# WRONG - every branch writes through one binding, and the ranges interleave into nonsense
|
|
18
|
+
async def wrong(reporter):
|
|
19
|
+
with reporter.subtask(0, 50):
|
|
20
|
+
await asyncio.gather(load_a(), load_b(), load_c()) # all three use `progress`
|
|
21
|
+
|
|
22
|
+
# RIGHT - one reporter each, passed in
|
|
23
|
+
async def right(reporter):
|
|
24
|
+
a, b, c = reporter.split(1, 1, 1)
|
|
25
|
+
await asyncio.gather(load_a(a), load_b(b), load_c(c))
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`contextvars` copies its context per task, so the `gather` form is not merely *untidy* - each task
|
|
29
|
+
gets a snapshot of the same binding and their contributions land on one child's range.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import contextvars
|
|
35
|
+
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
_current: contextvars.ContextVar[Any] = contextvars.ContextVar("taskwire_reporter", default=None)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def bind(reporter: Any) -> contextvars.Token:
|
|
42
|
+
"""Bind `reporter` for this context. Returns the `Token` that `unbind` needs."""
|
|
43
|
+
return _current.set(reporter)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def unbind(token: contextvars.Token | None) -> None:
|
|
47
|
+
"""Reset via the `Token` (TW-AMB-005).
|
|
48
|
+
|
|
49
|
+
Resetting rather than overwriting is the whole rule. A raising task that merely wrote `None` back
|
|
50
|
+
would leave the *next* task on the same pooled thread holding a stale binding, and operation A's
|
|
51
|
+
progress would land on operation B's key (TW-INV-011).
|
|
52
|
+
"""
|
|
53
|
+
if token is not None:
|
|
54
|
+
_current.reset(token)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def current_reporter() -> Any:
|
|
58
|
+
"""The bound reporter, or `None`. Applications should use `progress` instead."""
|
|
59
|
+
return _current.get()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class _AmbientProgress:
|
|
63
|
+
"""The module-level singleton exposing the identical `Reporter` surface (TW-AMB-001).
|
|
64
|
+
|
|
65
|
+
Every method is a **silent async no-op when nothing is bound** (TW-AMB-002). Silence is the
|
|
66
|
+
point: library code that reports progress must be callable from a caller that never opened an
|
|
67
|
+
operation, without that caller having to care and without a warning on every line.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
async def set(self, **kwargs: Any) -> None: # noqa: A003 - mirrors Reporter.set
|
|
71
|
+
reporter = _current.get()
|
|
72
|
+
if reporter is None:
|
|
73
|
+
# Still reject `state`, even unbound. Code that would be rejected inside an operation
|
|
74
|
+
# must not pass merely because it ran outside one (TW-PROG-012).
|
|
75
|
+
if "state" in kwargs:
|
|
76
|
+
raise ValueError("taskwire: Reporter.set() has no `state` keyword (TW-PROG-012)")
|
|
77
|
+
return None
|
|
78
|
+
return await reporter.set(**kwargs)
|
|
79
|
+
|
|
80
|
+
async def ask(self, dialog_id: str, **kwargs: Any) -> Any:
|
|
81
|
+
"""TW-AMB-003: `taskwire.ask` is this.
|
|
82
|
+
|
|
83
|
+
Unbound, there is nobody to ask and nothing to wait for, so this returns `None` rather than
|
|
84
|
+
blocking forever - the one place where the no-op is visible in a return value.
|
|
85
|
+
"""
|
|
86
|
+
reporter = _current.get()
|
|
87
|
+
if reporter is None:
|
|
88
|
+
return None
|
|
89
|
+
return await reporter.ask(dialog_id, **kwargs)
|
|
90
|
+
|
|
91
|
+
async def done(self) -> None:
|
|
92
|
+
reporter = _current.get()
|
|
93
|
+
if reporter is None:
|
|
94
|
+
return None
|
|
95
|
+
return await reporter.done()
|
|
96
|
+
|
|
97
|
+
async def fail(self, code: str, message: Any, retryable: bool = False) -> None:
|
|
98
|
+
reporter = _current.get()
|
|
99
|
+
if reporter is None:
|
|
100
|
+
return None
|
|
101
|
+
return await reporter.fail(code, message, retryable)
|
|
102
|
+
|
|
103
|
+
async def flush(self) -> None:
|
|
104
|
+
reporter = _current.get()
|
|
105
|
+
if reporter is None:
|
|
106
|
+
return None
|
|
107
|
+
return await reporter.flush()
|
|
108
|
+
|
|
109
|
+
async def set_result(self, result: Any) -> None:
|
|
110
|
+
reporter = _current.get()
|
|
111
|
+
if reporter is None:
|
|
112
|
+
return None
|
|
113
|
+
return await reporter.set_result(result)
|
|
114
|
+
|
|
115
|
+
async def was_aborted(self) -> bool:
|
|
116
|
+
reporter = _current.get()
|
|
117
|
+
if reporter is None:
|
|
118
|
+
return False
|
|
119
|
+
return await reporter.was_aborted()
|
|
120
|
+
|
|
121
|
+
def subtask(self, start: float, end: float) -> Any:
|
|
122
|
+
"""Unbound, this returns an inert reporter so `with progress.subtask(...)` still works."""
|
|
123
|
+
reporter = _current.get()
|
|
124
|
+
if reporter is None:
|
|
125
|
+
from .reporter import InertReporter
|
|
126
|
+
|
|
127
|
+
return InertReporter()
|
|
128
|
+
return reporter.subtask(start, end)
|
|
129
|
+
|
|
130
|
+
def split(self, *weights: float) -> tuple[Any, ...]:
|
|
131
|
+
reporter = _current.get()
|
|
132
|
+
if reporter is None:
|
|
133
|
+
from .reporter import InertReporter
|
|
134
|
+
|
|
135
|
+
return tuple(InertReporter() for _ in weights)
|
|
136
|
+
return reporter.split(*weights)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def cancelled(self) -> bool:
|
|
140
|
+
reporter = _current.get()
|
|
141
|
+
return bool(reporter is not None and reporter.cancelled)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def token(self) -> str | None:
|
|
145
|
+
reporter = _current.get()
|
|
146
|
+
return reporter.token if reporter is not None else None
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def sync(self) -> Any:
|
|
150
|
+
"""`progress.sync` mirrors `reporter.sync` (TW-API-003)."""
|
|
151
|
+
from .reporter import InertReporter, SyncReporter
|
|
152
|
+
|
|
153
|
+
reporter = _current.get()
|
|
154
|
+
return SyncReporter(reporter if reporter is not None else InertReporter())
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
progress = _AmbientProgress()
|
|
158
|
+
"""The ambient reporter (TW-AMB-001). Bound by `operation()` and by `with reporter.subtask(a, b):`."""
|
|
159
|
+
|
|
160
|
+
ask = progress.ask
|
|
161
|
+
"""TW-AMB-003: `taskwire.ask` is `progress.ask`, and is not a second mechanism."""
|