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/transport.py ADDED
@@ -0,0 +1,95 @@
1
+ """The transport port (§2.13) and the two implementations that need no optional dependency.
2
+
3
+ A transport is an **accelerator**, never a source of truth (TW-CORE-002). Everything here may fail,
4
+ drop, coalesce or refuse, and the only consequence is latency: the state was written to the store
5
+ before any of this ran (TW-CORE-001), so a later read returns the same answer either way.
6
+
7
+ That is why `notify()` may never raise (TW-TR-002) and may never block past `push_timeout`
8
+ (TW-TR-003). A degradation in the accelerator must not become a stall in the operation
9
+ (TW-INV-012).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import copy
16
+ import logging
17
+
18
+ from abc import ABC, abstractmethod
19
+ from collections.abc import Awaitable, Callable
20
+
21
+ from .models import Envelope
22
+ from .settings import settings
23
+
24
+ logger = logging.getLogger("taskwire.transport")
25
+
26
+ Subscriber = Callable[[Envelope], Awaitable[None] | None]
27
+
28
+
29
+ class TaskwireTransport(ABC):
30
+ """TW-TR-001: exactly one abstract method, plus an optional hint that is never load-bearing.
31
+
32
+ `notify` takes a namespace and an envelope and nothing else (TW-TR-004). There is deliberately
33
+ no list of interested connections anywhere in this design: the fan-out unit is the namespace,
34
+ which is also the security unit, and a per-token subscription mechanism has a startup race and
35
+ leaks (TW-BP-002).
36
+ """
37
+
38
+ @abstractmethod
39
+ async def notify(self, session: str, envelope: Envelope) -> None:
40
+ """Best-effort delivery. Never raises, never blocks past `settings.push_timeout`."""
41
+
42
+ async def is_online(self, session: str) -> bool: # noqa: ARG002 - specified signature
43
+ """A hint, and never load-bearing. Defaults to True (TW-TR-001)."""
44
+ return True
45
+
46
+
47
+ class NullTransport(TaskwireTransport):
48
+ """The baseline. Polling does all the work and every feature still works (TW-TR-007)."""
49
+
50
+ async def notify(self, _session: str, _envelope: Envelope) -> None:
51
+ return None
52
+
53
+
54
+ class LocalTransport(TaskwireTransport):
55
+ """In-process delivery: no wire, no socket, no serialization step (TW-TR-008).
56
+
57
+ This is a first-class deployment rather than a test double. An application that never leaves its
58
+ own process - a browser-only front end, a single back-end service - uses every feature of
59
+ taskwire through this transport and needs nothing else installed (TW-SYM-001).
60
+ """
61
+
62
+ def __init__(self) -> None:
63
+ self._subscribers: dict[str, list[Subscriber]] = {}
64
+
65
+ def subscribe(self, ns: str, callback: Subscriber) -> Callable[[], None]:
66
+ """Register `callback` for one namespace. Returns the unsubscribe."""
67
+ self._subscribers.setdefault(ns, []).append(callback)
68
+
69
+ def unsubscribe() -> None:
70
+ handlers = self._subscribers.get(ns)
71
+ if handlers and callback in handlers:
72
+ handlers.remove(callback)
73
+
74
+ return unsubscribe
75
+
76
+ async def notify(self, session: str, envelope: Envelope) -> None:
77
+ """Deliver to every subscriber of `session`, in registration order.
78
+
79
+ TW-TR-009: each subscriber receives a document **no other party retains a reference to**.
80
+ In-process delivery is the one path where a live reference could leak where a wire transport
81
+ would have copied, and a subscriber that mutated what it received would otherwise be able to
82
+ alter the store, another subscriber's copy, or what a later read returns. TW-CORE-002 has to
83
+ hold identically on every transport, so the copy is not defensive tidiness - it is the rule.
84
+
85
+ TW-TR-010: a subscriber that raises, or that outlasts `push_timeout`, is a dropped push. It
86
+ does not propagate, it does not reach the reporter, and it does not stop the remaining
87
+ subscribers from being called.
88
+ """
89
+ for callback in list(self._subscribers.get(session, ())):
90
+ try:
91
+ result = callback(copy.deepcopy(envelope))
92
+ if asyncio.iscoroutine(result):
93
+ await asyncio.wait_for(result, timeout=settings.push_timeout)
94
+ except Exception: # noqa: BLE001 - a dropped push is the defined outcome, not an error
95
+ logger.debug("taskwire: local subscriber dropped a push", exc_info=True)
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: taskwire
3
+ Version: 0.1.0
4
+ Summary: Progress reporting and awaitable dialogs for long-running server-side operations.
5
+ Project-URL: Homepage, https://github.com/velis74/taskwire
6
+ Project-URL: Repository, https://github.com/velis74/taskwire
7
+ Project-URL: Issues, https://github.com/velis74/taskwire/issues
8
+ Project-URL: Documentation, https://docs.velis.si/taskwire/
9
+ Author-email: Jure Erznožnik <jure.erznoznik@gmail.com>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: celery,dialogs,dynamicforms,fastapi,progress,websocket
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Internet :: WWW/HTTP
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Provides-Extra: celery
26
+ Requires-Dist: celery>=5.3; extra == 'celery'
27
+ Requires-Dist: nest-asyncio>=1.6; extra == 'celery'
28
+ Provides-Extra: demo
29
+ Requires-Dist: uvicorn>=0.27; extra == 'demo'
30
+ Provides-Extra: dev
31
+ Requires-Dist: coverage; extra == 'dev'
32
+ Requires-Dist: httpx; extra == 'dev'
33
+ Requires-Dist: hypothesis; extra == 'dev'
34
+ Requires-Dist: pytest; extra == 'dev'
35
+ Requires-Dist: pytest-asyncio; extra == 'dev'
36
+ Requires-Dist: pytest-cov; extra == 'dev'
37
+ Requires-Dist: ruff; extra == 'dev'
38
+ Provides-Extra: fastapi
39
+ Requires-Dist: fastapi>=0.100; extra == 'fastapi'
40
+ Provides-Extra: muxws
41
+ Requires-Dist: dynamicforms-fastapi-viewsets[muxws]>=0.5.4; extra == 'muxws'
42
+ Requires-Dist: muxws>=0.3.1; extra == 'muxws'
43
+ Provides-Extra: redis
44
+ Requires-Dist: redis>=5.0; extra == 'redis'
45
+ Provides-Extra: viewsets
46
+ Requires-Dist: dynamicforms-fastapi-viewsets>=0.5.4; extra == 'viewsets'
47
+ Description-Content-Type: text/markdown
48
+
49
+ # <img src="taskwire-icon.svg" alt="" style="width:2.5em; height: 2.5em; vertical-align: middle"> taskwire
50
+
51
+ Progress reporting and awaitable dialogs for long-running operations, in Python and TypeScript.
52
+
53
+ A long-running job — an import, a report, a batch — needs to tell whoever started it what it is
54
+ doing while it is doing it, and sometimes needs to ask them something before it can continue.
55
+ taskwire is that conversation, and the four things it carries:
56
+
57
+ - **progress**, with nested subtasks whose percentages compose correctly rather than overwriting
58
+ each other, and commits coalesced so a tight loop cannot flood anything;
59
+ - **awaitable dialogs** — the worker asks a question mid-job and blocks until the answer arrives,
60
+ with the first answer winning across every tab that is looking;
61
+ - **cancellation**, cooperative and sticky, which raises in the worker at its next progress call;
62
+ - **collectable results** — the job parks a file or a value for later collection and *exits*,
63
+ holding no worker while the record waits.
64
+
65
+ ## The one idea worth knowing
66
+
67
+ **The store is the truth; a push is only an accelerator.** Every state change is written before
68
+ anything is sent anywhere, so a dropped, coalesced or suppressed push never changes what the next
69
+ read returns. There is no event log, no sequence numbers and no replay: a client is entitled to
70
+ current state, and asking for it is always enough.
71
+
72
+ ## One protocol, three implementations
73
+
74
+ The layering is the shape of the source tree:
75
+
76
+ ```
77
+ protocol documents, envelopes, the six kinds, the state
78
+ │ machines, validation. Knows of no transport.
79
+ ┌───────────┼───────────┐
80
+ local REST WS three independent implementations of it
81
+ │ │ │
82
+ (none) fastapi / asgi muxws one adapter each, to the world outside
83
+ ```
84
+
85
+ The **protocol** is specified once and depends on nothing. Each **implementation** carries it over
86
+ one medium and is written against the protocol layer alone, never against another implementation.
87
+ Each **adapter** is the thin piece that binds an implementation to a particular framework, and is
88
+ the only place that framework is imported.
89
+
90
+ | Implementation | Carries the protocol over | Adapter | Needs |
91
+ |---|---|---|---|
92
+ | **local** | the process itself — no wire, no socket, no serialization | none | nothing |
93
+ | **REST** | request/response polling; the baseline everywhere | `contrib.viewsets`, `contrib.fastapi`, `contrib.asgi` | nothing in core |
94
+ | **WS** | one push per envelope down, the six calls up, lowest latency | `contrib.muxws` | [muxws](https://docs.velis.si/muxws/) |
95
+
96
+ Swapping one for another changes latency and nothing else — no feature, no state, no document.
97
+ Both languages ship the protocol and the operating half, so a browser-only application runs
98
+ operations rather than only watching them; the adapters, the Redis store and the Celery entry are
99
+ Python's, and the polling client and the register are TypeScript's.
100
+
101
+ ## Install
102
+
103
+ ```bash
104
+ pip install taskwire # core: no runtime dependencies at all
105
+ pip install "taskwire[redis]" # cross-process store and backplane
106
+ pip install "taskwire[fastapi]" # the REST adapter
107
+ pip install "taskwire[viewsets]" # the REST API as a fastapi-viewsets viewset
108
+ pip install "taskwire[celery]" # the worker entry point
109
+ pip install "taskwire[muxws]" # the WebSocket transport
110
+ ```
111
+
112
+ ```bash
113
+ npm install taskwire
114
+ ```
115
+
116
+ ## The demo
117
+
118
+ ```bash
119
+ pip install -e ".[fastapi,viewsets,demo]" && npm install
120
+ python demo.py # then open http://127.0.0.1:5174
121
+ ```
122
+
123
+ A night batch over one book: four back-office jobs that between them exercise progress, nested
124
+ progress, awaitable dialogs, cancellation and collectable results. See [`demo/README.md`](demo/README.md).
125
+
126
+ ## Status
127
+
128
+ Pre-1.0 and under active construction. The wire format is versioned independently of the package
129
+ (`Envelope.v`), so package semver says nothing about it.
130
+
131
+ ## Links
132
+
133
+ - [Documentation](https://docs.velis.si/taskwire/)
134
+ - [Repository](https://github.com/velis74/taskwire)
135
+
136
+ ## License
137
+
138
+ MIT. Copyright (c) 2026 Jure Erznožnik.
@@ -0,0 +1,28 @@
1
+ taskwire/__init__.py,sha256=MhMXlE841SbzFYoXFAESm4rbZ9mz4bKt8vOK2bafLB8,3253
2
+ taskwire/ambient.py,sha256=kIW-pQhBmEwZNxd-idKLB1WjUGgzv3bdMY2ByBXScwk,6093
3
+ taskwire/conformance.py,sha256=XJHtZYfcPvQUiNS6okZVp9xccAtHm8v7PKbI14UwTR0,19589
4
+ taskwire/decorators.py,sha256=6k2tkdh72TosGstPIDllq5Bo592WH2XyKJ3tvVQwfdg,3382
5
+ taskwire/delivery.py,sha256=6Y49nHkA5dB9wX9_plaxuKd-LkK3C7qD4JqfcqrDKvQ,5068
6
+ taskwire/headers.py,sha256=sAyz5NP9jLJIJqVM0F1MfjEy2Qrr6W_H2eCET24HcKk,1947
7
+ taskwire/models.py,sha256=92ZGordETrudW1qJ1yilX5PI3daTHk59two8WTrtrhQ,24094
8
+ taskwire/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ taskwire/reader.py,sha256=SoRkSD72hkREkD_KADKRVA1FK4bO58KnJNd-rFP0sRk,4996
10
+ taskwire/register.py,sha256=MrPOk1T9xXd0-lLZW26SiMjmiWmrozBREATQJ9oJSj8,7926
11
+ taskwire/reporter.py,sha256=sOaWeVbIz0kUKfOCbpntJwqxul6dmevSlyIrr2XRod4,51073
12
+ taskwire/rest.py,sha256=hz6b473gAVsWVB4O0E81MexhpvRAV_pKxMgpulYbfe0,13568
13
+ taskwire/settings.py,sha256=dbTXTSHW4mjbxdTwONjQ9xfO2wL7Vsep0hGp6heYseA,4957
14
+ taskwire/store.py,sha256=vWGp6GGKeIKNK7SHoYFKs4lJdbDtzlhHsaJ4ZCoLngM,29346
15
+ taskwire/transport.py,sha256=CcgkMblXsktDs92a-1w0Ueqi2y7r5oJePP4IEeYXiOc,4258
16
+ taskwire/contrib/__init__.py,sha256=FWMUvGlCws8DERXwwmHleKVXBwnJEWnhtVKKsVr59bU,694
17
+ taskwire/contrib/asgi.py,sha256=lWtWVAq9w-APL61gvr-BG0uaRQlqu2lV4Re40SJTsk4,3755
18
+ taskwire/contrib/celery.py,sha256=DW0Vik0l02xur6zjMTu8Ai6LwIXqeTi8aFj8Gn9vzM8,17709
19
+ taskwire/contrib/fastapi.py,sha256=I8Mjpy1tTxvlXTJHYh0USaB4eIUHgShegvULHdf7Uzs,3639
20
+ taskwire/contrib/muxws.py,sha256=Uum48s2vWAbtiHfrePy2tnx9O03B7x-5zkmL00VtCeQ,18548
21
+ taskwire/contrib/redis_store.py,sha256=5Bany9xrd5bo489m8timmHvtTT-G0t3k4zXrgNUX2r0,24350
22
+ taskwire/contrib/schema.py,sha256=-I2bd3Us6igAFULml-D-e_fGzbC69xSipFCCOiOcRpQ,7147
23
+ taskwire/contrib/threads.py,sha256=qxypMRqXzsXUzzUpsfTzJLVsn1OPtByJtFhg65mz9CA,1466
24
+ taskwire/contrib/viewsets.py,sha256=G3bZVgRb2Fqf6UHyHclkSTcg2JZkIcFnA2f6iIUi1_A,14824
25
+ taskwire-0.1.0.dist-info/METADATA,sha256=fmButJRdTKmB3nTXrBblUSxgOVELdgdz0e0Wpn8JGcA,6240
26
+ taskwire-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
27
+ taskwire-0.1.0.dist-info/licenses/LICENSE,sha256=g5DUzLsev_7tQp4RFbAQVfqnmRlk9cu_ZE60o6vuPs4,1072
28
+ taskwire-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jure Erznožnik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.