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/delivery.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""`Client`, `OperationRef`, and the delivery decision the last hop applies.
|
|
2
|
+
|
|
3
|
+
`effective_delivery` is `fe_override if present else push_filter(client, operation)` (TW-DEL-001);
|
|
4
|
+
`may_be_suppressed` bounds which envelopes either half may act on (TW-DEL-010).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .models import Progress
|
|
13
|
+
from .settings import configured
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class Client:
|
|
18
|
+
"""Who is asking. Derivable from either a live socket peer **or** an HTTP request (TW-DEL-005).
|
|
19
|
+
|
|
20
|
+
`session` is populated the same way on both paths - by `session_resolver`, the only producer of
|
|
21
|
+
a namespace (TW-AMB-008) - and exactly one of `peer` / `request` is set.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
session: str
|
|
25
|
+
connection: str | None = None
|
|
26
|
+
peer: Any = None
|
|
27
|
+
request: Any = None
|
|
28
|
+
|
|
29
|
+
def __post_init__(self) -> None:
|
|
30
|
+
"""TW-DEL-005: a `Client` comes from a socket or from a request, never from both.
|
|
31
|
+
|
|
32
|
+
Either names one caller; both name none, and `effective_delivery` would then read a socket's
|
|
33
|
+
override map on behalf of an HTTP read. Carrying **neither** is legal and ordinary: a caller
|
|
34
|
+
that reaches `taskwire.rest` directly - a server-rendered page, a script, a test - has a
|
|
35
|
+
namespace and nothing else.
|
|
36
|
+
"""
|
|
37
|
+
if self.peer is not None and self.request is not None:
|
|
38
|
+
raise ValueError("taskwire: a Client comes from a socket or from a request, never both (TW-DEL-005)")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class OperationRef:
|
|
43
|
+
"""What a `push_filter` is shown about the operation a push is carrying (TW-DEL-004)."""
|
|
44
|
+
|
|
45
|
+
token: str
|
|
46
|
+
progress: Progress
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def data(self) -> dict[str, Any]:
|
|
50
|
+
"""Shorthand for the free-form bag, which is what a predicate almost always keys on."""
|
|
51
|
+
return self.progress.data
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def result_kind(self) -> str | None:
|
|
55
|
+
return self.progress.result_kind
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def push_filter_allows(client: Client, operation: OperationRef) -> bool:
|
|
59
|
+
"""Run the configured predicate, swallowing anything it throws.
|
|
60
|
+
|
|
61
|
+
TW-DEL-003: an exception out of `push_filter` is treated as `True`. The predicate is a display
|
|
62
|
+
optimisation; a broken one must degrade to sending more, never to sending less. It is
|
|
63
|
+
emphatically not access control (TW-DEL-021, TW-INV-018) - it runs after the namespace has
|
|
64
|
+
already decided what the caller may read.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
return bool(configured.push_filter(client, operation))
|
|
68
|
+
except Exception: # noqa: BLE001 - deliberately swallowed; see TW-DEL-003
|
|
69
|
+
return True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
OVERRIDES_TAG = "taskwire_overrides"
|
|
73
|
+
"""Where a connection's override map lives (TW-DEL-009).
|
|
74
|
+
|
|
75
|
+
Per-connection memory - `peer.tags` under muxws - and nowhere else. It dies with the socket and is
|
|
76
|
+
persisted on neither side. A map that outlived its socket would be a preference nobody can see and
|
|
77
|
+
nobody can clear, and TW-INV-016 is the client half of the same rule: the client persists nothing.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def overrides_of(peer: Any) -> dict[str, bool]:
|
|
82
|
+
"""The override map this connection last declared. Empty when it has declared none."""
|
|
83
|
+
if peer is None:
|
|
84
|
+
return {}
|
|
85
|
+
tags = getattr(peer, "tags", None)
|
|
86
|
+
if not isinstance(tags, dict):
|
|
87
|
+
return {}
|
|
88
|
+
declared = tags.get(OVERRIDES_TAG)
|
|
89
|
+
return declared if isinstance(declared, dict) else {}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def effective_delivery(client: Client, operation: OperationRef) -> bool:
|
|
93
|
+
"""TW-DEL-001: `fe_override if present else push_filter(client, operation)`.
|
|
94
|
+
|
|
95
|
+
Two halves, and the front end's wins when it has spoken. `false` suppresses, `true` revives
|
|
96
|
+
something the predicate would have suppressed, and *absent* defers - which is why the map is
|
|
97
|
+
consulted by membership rather than by truthiness.
|
|
98
|
+
|
|
99
|
+
**Neither half may raise the ceiling** (TW-DEL-012). An override asserting `true` may only revive
|
|
100
|
+
an envelope on a channel the socket was already entitled to read; it cannot reach into another
|
|
101
|
+
namespace, because the namespace was decided before this function was called. This is not access
|
|
102
|
+
control and must never be pressed into service as any (TW-DEL-021, TW-INV-018).
|
|
103
|
+
"""
|
|
104
|
+
declared = overrides_of(client.peer)
|
|
105
|
+
if operation.token in declared:
|
|
106
|
+
return bool(declared[operation.token])
|
|
107
|
+
return push_filter_allows(client, operation)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def may_be_suppressed(kind: Any, progress: Progress) -> bool:
|
|
111
|
+
"""TW-DEL-010: **only non-terminal `progress` may be suppressed, by either half.**
|
|
112
|
+
|
|
113
|
+
`dialog.open`, `dialog.close`, `cancel` and any `progress` carrying a terminal state go to every
|
|
114
|
+
socket of the namespace whatever the predicate and the overrides say. The reason is not symmetry:
|
|
115
|
+
a question nobody can see is a worker nobody can free, and a terminal state nobody hears about is
|
|
116
|
+
a bar frozen at 97 % (TW-INV-005). A display preference must never be able to wedge a worker
|
|
117
|
+
pool.
|
|
118
|
+
"""
|
|
119
|
+
from .models import EnvelopeKind
|
|
120
|
+
|
|
121
|
+
if EnvelopeKind(kind) is not EnvelopeKind.PROGRESS:
|
|
122
|
+
return False
|
|
123
|
+
return not progress.is_terminal
|
taskwire/headers.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""The two HTTP header names, in one place.
|
|
2
|
+
|
|
3
|
+
Four modules read these names: `contrib.celery`, `contrib.fastapi`, `contrib.asgi` and
|
|
4
|
+
`contrib.viewsets`. A misspelled header is simply an absent header - the operation runs, reports
|
|
5
|
+
nothing to the socket that asked for it, and logs nothing anywhere - so the spelling exists once.
|
|
6
|
+
|
|
7
|
+
Core, not `contrib`, because they are strings and cost no dependency, and because the integration
|
|
8
|
+
layer that wraps every viewset action needs to read them without importing a Celery adapter to do it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
TOKEN_HEADER = "X-Taskwire-Token"
|
|
14
|
+
"""Where the caller's token travels on the request that **starts** an operation.
|
|
15
|
+
|
|
16
|
+
A **header**, never a query parameter (TW-CELERY-011). A query parameter lands in access logs, in
|
|
17
|
+
browser history and in the `Referer` of every subsequent request. A token is an address rather than
|
|
18
|
+
an authorization grant (TW-SEC-003), so that is not a confidentiality disaster - but an address in a
|
|
19
|
+
log is still an address somebody can poll.
|
|
20
|
+
|
|
21
|
+
The caller mints the token and sends it; the server does not issue one. That ordering is the whole
|
|
22
|
+
workflow: a client that had to wait for the server to name the operation could not subscribe until
|
|
23
|
+
the reply came back, and would miss whatever happened in between.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
CONNECTION_HEADER = "X-Taskwire-Connection"
|
|
27
|
+
"""One header serving two purposes, and there must not be a second (TW-REST-002).
|
|
28
|
+
|
|
29
|
+
It names the connection for `progress_delivery` on a read, and it is where `origin_connection` comes
|
|
30
|
+
from on the request that starts an operation (TW-PRIV-001). The tab header that would have been the
|
|
31
|
+
second one is forbidden outright (TW-PROG-007): the server never learns a tab id, because a server
|
|
32
|
+
that did would grow a per-tab index nobody could bound.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
CONNECTION_HEADER_BYTES = CONNECTION_HEADER.lower().encode("ascii")
|
|
36
|
+
"""The same name as a raw ASGI scope key, which is lower-cased bytes rather than a string."""
|