hubcast 0.1.0__tar.gz
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.
- hubcast-0.1.0/LICENSE +22 -0
- hubcast-0.1.0/PKG-INFO +183 -0
- hubcast-0.1.0/README.md +152 -0
- hubcast-0.1.0/pyproject.toml +62 -0
- hubcast-0.1.0/setup.cfg +4 -0
- hubcast-0.1.0/src/hubcast/__init__.py +27 -0
- hubcast-0.1.0/src/hubcast/_hub.py +321 -0
- hubcast-0.1.0/src/hubcast.egg-info/PKG-INFO +183 -0
- hubcast-0.1.0/src/hubcast.egg-info/SOURCES.txt +12 -0
- hubcast-0.1.0/src/hubcast.egg-info/dependency_links.txt +1 -0
- hubcast-0.1.0/src/hubcast.egg-info/requires.txt +10 -0
- hubcast-0.1.0/src/hubcast.egg-info/top_level.txt +1 -0
- hubcast-0.1.0/tests/test_hub.py +225 -0
- hubcast-0.1.0/tests/test_redis.py +297 -0
hubcast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Parham Roozmand
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
hubcast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hubcast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fan-out to WebSocket subscribers across processes, on one Redis connection.
|
|
5
|
+
Author: Parham Roozmand
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Nappuccino-tlg/hubcast
|
|
8
|
+
Project-URL: Source, https://github.com/Nappuccino-tlg/hubcast
|
|
9
|
+
Project-URL: Issues, https://github.com/Nappuccino-tlg/hubcast/issues
|
|
10
|
+
Keywords: websocket,pubsub,redis,broadcast,fanout,asyncio,realtime
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Framework :: AsyncIO
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: redis
|
|
23
|
+
Requires-Dist: redis>=5.0; extra == "redis"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.3; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.26; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff>=0.8; extra == "dev"
|
|
29
|
+
Requires-Dist: redis>=5.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# hubcast
|
|
33
|
+
|
|
34
|
+
Fan-out to WebSocket subscribers across processes, on one Redis connection.
|
|
35
|
+
|
|
36
|
+
[](https://github.com/Nappuccino-tlg/hubcast/actions/workflows/ci.yml)
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "hubcast[redis]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Two problems, and the second one is the hard one
|
|
45
|
+
|
|
46
|
+
You have a live feed — clicks arriving, prices moving, a build log — and clients watching
|
|
47
|
+
it over WebSockets. Two things go wrong, and only the first is obvious.
|
|
48
|
+
|
|
49
|
+
**Your app runs on more than one process.** The event arrives at instance A. The client
|
|
50
|
+
watching for it is connected to instance B. A knows nothing about B.
|
|
51
|
+
|
|
52
|
+
The usual fix is Redis pub/sub, and the usual implementation gives every connection its
|
|
53
|
+
own subscription. It works, and it stops working at exactly the scale where broadcasting
|
|
54
|
+
was worth doing: ten thousand clients become ten thousand Redis connections, and the
|
|
55
|
+
server runs out of file descriptors long before it runs out of anything interesting.
|
|
56
|
+
|
|
57
|
+
A `Hub` holds **one** Redis connection however many subscribers it has. It subscribes to a
|
|
58
|
+
channel when a topic gains its first local subscriber, drops it when it loses its last, and
|
|
59
|
+
fans out in-process. Redis sees how many *topics* a process cares about, not how many
|
|
60
|
+
clients it is serving.
|
|
61
|
+
|
|
62
|
+
**Someone's phone goes into a tunnel.** Their connection is open, their socket buffer is
|
|
63
|
+
full, and they are not reading. Now what?
|
|
64
|
+
|
|
65
|
+
That is not an edge case, and there is no answer that is right everywhere — which is why
|
|
66
|
+
it is an argument rather than a decision made for you.
|
|
67
|
+
|
|
68
|
+
## Using it
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from redis.asyncio import Redis
|
|
72
|
+
from hubcast import Hub
|
|
73
|
+
|
|
74
|
+
hub = Hub(Redis.from_url("redis://localhost"))
|
|
75
|
+
|
|
76
|
+
# once, at startup
|
|
77
|
+
await hub.start()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Publish from anywhere, on any instance:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
await hub.publish(f"link:{code}", json.dumps({"clicks": total}))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Subscribe for the length of a connection:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
@app.websocket("/live/{code}")
|
|
90
|
+
async def live(websocket: WebSocket, code: str):
|
|
91
|
+
await websocket.accept()
|
|
92
|
+
async with hub.subscribe(f"link:{code}") as messages:
|
|
93
|
+
async for message in messages:
|
|
94
|
+
await websocket.send_text(message)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`subscribe` is a context manager rather than a pair of calls because the failure it
|
|
98
|
+
prevents — a connection dropping and leaving its subscription behind forever — is silent,
|
|
99
|
+
cumulative, and shows up only as memory that never comes back.
|
|
100
|
+
|
|
101
|
+
## When a subscriber falls behind
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
hub = Hub(redis, max_queue=100, overflow=Overflow.DROP_OLDEST)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
| | keeps | right for |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `DROP_OLDEST` *(default)* | the newest | a live feed — the current number is the true one, and a stale one is worse than nothing |
|
|
110
|
+
| `DROP_NEWEST` | the first | an alert stream — the original cause matters more than the hundredth symptom |
|
|
111
|
+
| `CLOSE` | nothing | a consumer that would rather reconnect and resynchronise than carry on with a hole in it |
|
|
112
|
+
|
|
113
|
+
Set per Hub, or per subscription:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
async with hub.subscribe(topic, max_queue=1000, overflow=Overflow.CLOSE) as messages:
|
|
117
|
+
...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`CLOSE` raises `SubscriberTooSlow` **in that subscriber's own iteration** — never in the
|
|
121
|
+
publisher. One slow client is the slow client's problem, and a broadcast must not fail
|
|
122
|
+
because somebody went into a tunnel.
|
|
123
|
+
|
|
124
|
+
Every subscription counts what it discarded:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
messages.dropped # worth putting on a dashboard
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
A number that climbs means a consumer that needs to be faster or a queue that needs to be
|
|
131
|
+
deeper. Not printing it is how that goes unnoticed for a month.
|
|
132
|
+
|
|
133
|
+
## Without a Redis
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
hub = Hub() # in-process, same API
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Useful for a single-process deployment and for tests, and it is the same code path — so
|
|
140
|
+
nothing here is only exercised in production. `redis` is an optional extra, and CI checks
|
|
141
|
+
the package still imports and works with it uninstalled.
|
|
142
|
+
|
|
143
|
+
## What it does not do
|
|
144
|
+
|
|
145
|
+
**Delivery guarantees.** Redis pub/sub is fire-and-forget: a subscriber that is not
|
|
146
|
+
connected when a message is published does not get it later. Nothing here adds replay,
|
|
147
|
+
acknowledgement or ordering across a reconnect. If losing a message is not survivable, you
|
|
148
|
+
want a log — Redis Streams, Kafka — not this.
|
|
149
|
+
|
|
150
|
+
**WebSockets.** A Hub moves strings between processes; it never touches a socket. That
|
|
151
|
+
keeps it usable from anything, and means heartbeats, reconnects and framing stay with the
|
|
152
|
+
framework that already owns them.
|
|
153
|
+
|
|
154
|
+
**Presence.** Knowing *who* is connected across instances is a different problem with its
|
|
155
|
+
own failure mode — an instance that dies leaves its users looking online forever.
|
|
156
|
+
|
|
157
|
+
## One thing worth knowing
|
|
158
|
+
|
|
159
|
+
Every Hub keeps one extra channel open, `<prefix>:__hub__`, and never publishes on it.
|
|
160
|
+
redis-py's `listen()` loops `while self.subscribed`, so a pubsub with no channels ends the
|
|
161
|
+
generator immediately — the listener would exit before the first topic arrived and the Hub
|
|
162
|
+
would be silently deaf. It is visible in `PUBSUB CHANNELS`, so it is named for what it is
|
|
163
|
+
rather than hidden.
|
|
164
|
+
|
|
165
|
+
## Tests
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
169
|
+
pytest
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
The Redis tests use **two Hubs on one Redis**, which is the same arrangement as two
|
|
173
|
+
application processes behind a load balancer — one holds the connection that publishes,
|
|
174
|
+
the other the connection that is subscribed. A single Hub talking to itself proves nothing
|
|
175
|
+
about the part people install this for.
|
|
176
|
+
|
|
177
|
+
## Requirements
|
|
178
|
+
|
|
179
|
+
Python 3.10 or newer. `redis>=5.0` only if you want the cross-process half.
|
|
180
|
+
|
|
181
|
+
## License
|
|
182
|
+
|
|
183
|
+
MIT
|
hubcast-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# hubcast
|
|
2
|
+
|
|
3
|
+
Fan-out to WebSocket subscribers across processes, on one Redis connection.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/Nappuccino-tlg/hubcast/actions/workflows/ci.yml)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install "hubcast[redis]"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Two problems, and the second one is the hard one
|
|
14
|
+
|
|
15
|
+
You have a live feed — clicks arriving, prices moving, a build log — and clients watching
|
|
16
|
+
it over WebSockets. Two things go wrong, and only the first is obvious.
|
|
17
|
+
|
|
18
|
+
**Your app runs on more than one process.** The event arrives at instance A. The client
|
|
19
|
+
watching for it is connected to instance B. A knows nothing about B.
|
|
20
|
+
|
|
21
|
+
The usual fix is Redis pub/sub, and the usual implementation gives every connection its
|
|
22
|
+
own subscription. It works, and it stops working at exactly the scale where broadcasting
|
|
23
|
+
was worth doing: ten thousand clients become ten thousand Redis connections, and the
|
|
24
|
+
server runs out of file descriptors long before it runs out of anything interesting.
|
|
25
|
+
|
|
26
|
+
A `Hub` holds **one** Redis connection however many subscribers it has. It subscribes to a
|
|
27
|
+
channel when a topic gains its first local subscriber, drops it when it loses its last, and
|
|
28
|
+
fans out in-process. Redis sees how many *topics* a process cares about, not how many
|
|
29
|
+
clients it is serving.
|
|
30
|
+
|
|
31
|
+
**Someone's phone goes into a tunnel.** Their connection is open, their socket buffer is
|
|
32
|
+
full, and they are not reading. Now what?
|
|
33
|
+
|
|
34
|
+
That is not an edge case, and there is no answer that is right everywhere — which is why
|
|
35
|
+
it is an argument rather than a decision made for you.
|
|
36
|
+
|
|
37
|
+
## Using it
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from redis.asyncio import Redis
|
|
41
|
+
from hubcast import Hub
|
|
42
|
+
|
|
43
|
+
hub = Hub(Redis.from_url("redis://localhost"))
|
|
44
|
+
|
|
45
|
+
# once, at startup
|
|
46
|
+
await hub.start()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Publish from anywhere, on any instance:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
await hub.publish(f"link:{code}", json.dumps({"clicks": total}))
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Subscribe for the length of a connection:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
@app.websocket("/live/{code}")
|
|
59
|
+
async def live(websocket: WebSocket, code: str):
|
|
60
|
+
await websocket.accept()
|
|
61
|
+
async with hub.subscribe(f"link:{code}") as messages:
|
|
62
|
+
async for message in messages:
|
|
63
|
+
await websocket.send_text(message)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`subscribe` is a context manager rather than a pair of calls because the failure it
|
|
67
|
+
prevents — a connection dropping and leaving its subscription behind forever — is silent,
|
|
68
|
+
cumulative, and shows up only as memory that never comes back.
|
|
69
|
+
|
|
70
|
+
## When a subscriber falls behind
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
hub = Hub(redis, max_queue=100, overflow=Overflow.DROP_OLDEST)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
| | keeps | right for |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `DROP_OLDEST` *(default)* | the newest | a live feed — the current number is the true one, and a stale one is worse than nothing |
|
|
79
|
+
| `DROP_NEWEST` | the first | an alert stream — the original cause matters more than the hundredth symptom |
|
|
80
|
+
| `CLOSE` | nothing | a consumer that would rather reconnect and resynchronise than carry on with a hole in it |
|
|
81
|
+
|
|
82
|
+
Set per Hub, or per subscription:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
async with hub.subscribe(topic, max_queue=1000, overflow=Overflow.CLOSE) as messages:
|
|
86
|
+
...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`CLOSE` raises `SubscriberTooSlow` **in that subscriber's own iteration** — never in the
|
|
90
|
+
publisher. One slow client is the slow client's problem, and a broadcast must not fail
|
|
91
|
+
because somebody went into a tunnel.
|
|
92
|
+
|
|
93
|
+
Every subscription counts what it discarded:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
messages.dropped # worth putting on a dashboard
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
A number that climbs means a consumer that needs to be faster or a queue that needs to be
|
|
100
|
+
deeper. Not printing it is how that goes unnoticed for a month.
|
|
101
|
+
|
|
102
|
+
## Without a Redis
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
hub = Hub() # in-process, same API
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Useful for a single-process deployment and for tests, and it is the same code path — so
|
|
109
|
+
nothing here is only exercised in production. `redis` is an optional extra, and CI checks
|
|
110
|
+
the package still imports and works with it uninstalled.
|
|
111
|
+
|
|
112
|
+
## What it does not do
|
|
113
|
+
|
|
114
|
+
**Delivery guarantees.** Redis pub/sub is fire-and-forget: a subscriber that is not
|
|
115
|
+
connected when a message is published does not get it later. Nothing here adds replay,
|
|
116
|
+
acknowledgement or ordering across a reconnect. If losing a message is not survivable, you
|
|
117
|
+
want a log — Redis Streams, Kafka — not this.
|
|
118
|
+
|
|
119
|
+
**WebSockets.** A Hub moves strings between processes; it never touches a socket. That
|
|
120
|
+
keeps it usable from anything, and means heartbeats, reconnects and framing stay with the
|
|
121
|
+
framework that already owns them.
|
|
122
|
+
|
|
123
|
+
**Presence.** Knowing *who* is connected across instances is a different problem with its
|
|
124
|
+
own failure mode — an instance that dies leaves its users looking online forever.
|
|
125
|
+
|
|
126
|
+
## One thing worth knowing
|
|
127
|
+
|
|
128
|
+
Every Hub keeps one extra channel open, `<prefix>:__hub__`, and never publishes on it.
|
|
129
|
+
redis-py's `listen()` loops `while self.subscribed`, so a pubsub with no channels ends the
|
|
130
|
+
generator immediately — the listener would exit before the first topic arrived and the Hub
|
|
131
|
+
would be silently deaf. It is visible in `PUBSUB CHANNELS`, so it is named for what it is
|
|
132
|
+
rather than hidden.
|
|
133
|
+
|
|
134
|
+
## Tests
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
docker run -d -p 6379:6379 redis:7-alpine
|
|
138
|
+
pytest
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The Redis tests use **two Hubs on one Redis**, which is the same arrangement as two
|
|
142
|
+
application processes behind a load balancer — one holds the connection that publishes,
|
|
143
|
+
the other the connection that is subscribed. A single Hub talking to itself proves nothing
|
|
144
|
+
about the part people install this for.
|
|
145
|
+
|
|
146
|
+
## Requirements
|
|
147
|
+
|
|
148
|
+
Python 3.10 or newer. `redis>=5.0` only if you want the cross-process half.
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hubcast"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Fan-out to WebSocket subscribers across processes, on one Redis connection."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.10"
|
|
8
|
+
authors = [{ name = "Parham Roozmand" }]
|
|
9
|
+
keywords = ["websocket", "pubsub", "redis", "broadcast", "fanout", "asyncio", "realtime"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Framework :: AsyncIO",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Internet :: WWW/HTTP",
|
|
19
|
+
]
|
|
20
|
+
# Redis is optional: without one this is an in-process hub, which is a real use and the
|
|
21
|
+
# one the tests exercise first.
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
redis = ["redis>=5.0"]
|
|
26
|
+
dev = ["pytest>=8.3", "pytest-asyncio>=0.26", "pytest-cov>=6.0", "ruff>=0.8", "redis>=5.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/Nappuccino-tlg/hubcast"
|
|
30
|
+
Source = "https://github.com/Nappuccino-tlg/hubcast"
|
|
31
|
+
Issues = "https://github.com/Nappuccino-tlg/hubcast/issues"
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["setuptools>=68"]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
where = ["src"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
asyncio_mode = "auto"
|
|
42
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
addopts = "-q --cov=hubcast --cov-report=term-missing"
|
|
45
|
+
|
|
46
|
+
[tool.coverage.report]
|
|
47
|
+
exclude_lines = ["pragma: no cover", "if TYPE_CHECKING:"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 100
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
|
|
53
|
+
[tool.ruff.lint]
|
|
54
|
+
select = ["E", "F", "I", "N", "UP", "B", "C4", "SIM", "RUF"]
|
|
55
|
+
# N818 wants SubscriberTooSlowError. The short name is in the public API and
|
|
56
|
+
# `except SubscriberTooSlow:` is the sentence a caller writes.
|
|
57
|
+
ignore = ["N818"]
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint.per-file-ignores]
|
|
60
|
+
# SIM117: a Hub and the subscription taken inside it are two ideas, and nesting them says
|
|
61
|
+
# which one owns the other. Flattening reads as though they were peers.
|
|
62
|
+
"tests/*" = ["SIM117"]
|
hubcast-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Fan-out to WebSocket subscribers across processes, on one Redis connection.
|
|
2
|
+
|
|
3
|
+
The obvious implementation gives every connection its own Redis subscription, and stops
|
|
4
|
+
working at exactly the scale where broadcasting was worth doing. This keeps one connection
|
|
5
|
+
per process however many clients it is serving, and makes the slow-subscriber question --
|
|
6
|
+
which is not an edge case, it is a phone on a train -- something you answer on purpose.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from hubcast._hub import (
|
|
10
|
+
DEFAULT_MAX_QUEUE,
|
|
11
|
+
DEFAULT_PREFIX,
|
|
12
|
+
Hub,
|
|
13
|
+
Overflow,
|
|
14
|
+
SubscriberTooSlow,
|
|
15
|
+
Subscription,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DEFAULT_MAX_QUEUE",
|
|
20
|
+
"DEFAULT_PREFIX",
|
|
21
|
+
"Hub",
|
|
22
|
+
"Overflow",
|
|
23
|
+
"SubscriberTooSlow",
|
|
24
|
+
"Subscription",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|