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/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."""