sillo-wire 0.1.0.dev1__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.
- _sillo_wire_bootstrap.py +104 -0
- sillo-stubs/py.typed +1 -0
- sillo-stubs/wire.pyi +48 -0
- sillo_wire/__init__.py +58 -0
- sillo_wire/backlog.py +140 -0
- sillo_wire/consumer.py +177 -0
- sillo_wire/envelope.py +92 -0
- sillo_wire/errors.py +30 -0
- sillo_wire/hub.py +315 -0
- sillo_wire/peer.py +231 -0
- sillo_wire/policy.py +42 -0
- sillo_wire/py.typed +0 -0
- sillo_wire/testing.py +86 -0
- sillo_wire-0.1.0.dev1.dist-info/METADATA +244 -0
- sillo_wire-0.1.0.dev1.dist-info/RECORD +18 -0
- sillo_wire-0.1.0.dev1.dist-info/WHEEL +4 -0
- sillo_wire-0.1.0.dev1.dist-info/licenses/LICENSE +27 -0
- sillo_wire.pth +1 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sillo-wire
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
|
+
Summary: Rooms, presence and fan-out for Sillo WebSockets — bounded queues, replayable backlog, no global state.
|
|
5
|
+
Project-URL: Homepage, https://sillo.build
|
|
6
|
+
Project-URL: Documentation, https://docs.sillo.build/packages/wire/
|
|
7
|
+
Project-URL: Source, https://github.com/sillohq/wire
|
|
8
|
+
Author-email: Chidebele Dunamis <techwithdunamix@gmail.com>
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: asgi,broadcast,presence,pubsub,realtime,sillo,websocket
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: sillo-framework>=0.3
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# sillo-wire
|
|
34
|
+
|
|
35
|
+
Rooms, presence and fan-out for [Sillo](https://sillo.build) WebSockets.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install sillo-wire
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Installs as `sillo-wire`, imports as `sillo.wire`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from sillo import SilloApp
|
|
45
|
+
from sillo.wire import Hub, Peer
|
|
46
|
+
|
|
47
|
+
app = SilloApp()
|
|
48
|
+
hub = Hub()
|
|
49
|
+
|
|
50
|
+
@app.ws_route("/ws/room/{name}")
|
|
51
|
+
async def room(socket, name: str):
|
|
52
|
+
await socket.accept()
|
|
53
|
+
peer = Peer(socket, identity=socket.query_params.get("user"))
|
|
54
|
+
await hub.join(peer, name)
|
|
55
|
+
try:
|
|
56
|
+
async for message in socket.iter_json():
|
|
57
|
+
await hub.broadcast(name, message)
|
|
58
|
+
finally:
|
|
59
|
+
await hub.disconnect(peer)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Why this exists
|
|
63
|
+
|
|
64
|
+
Three things differ from the obvious implementation, and they are the whole
|
|
65
|
+
point of the package.
|
|
66
|
+
|
|
67
|
+
**A broadcast never blocks.** Writing straight to each socket in turn means the
|
|
68
|
+
slowest member of a room sets the pace for everyone else — a client that has
|
|
69
|
+
stopped reading fills its kernel buffer, the write blocks, and the rest of the
|
|
70
|
+
room waits behind it. Here every peer has a bounded queue and a writer task, so
|
|
71
|
+
a broadcast only ever enqueues:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
report = await hub.broadcast("lobby", {"msg": "hello"})
|
|
75
|
+
report.delivered # 41
|
|
76
|
+
report.dropped # 2 queues were full
|
|
77
|
+
report.failed # 1 socket was already gone
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
You get a `DeliveryReport` rather than nothing, because a fan-out you cannot
|
|
81
|
+
measure is a fan-out you cannot operate.
|
|
82
|
+
|
|
83
|
+
**Nothing is global.** A `Hub` is an ordinary object. Two of them are two
|
|
84
|
+
independent worlds, so tests get a fresh one per case instead of remembering to
|
|
85
|
+
flush shared state, and a multi-tenant application keeps traffic apart without
|
|
86
|
+
a naming convention.
|
|
87
|
+
|
|
88
|
+
**History is replayable.** Every envelope carries a monotonic sequence, so a
|
|
89
|
+
client that reconnects asks for what it missed rather than for everything or
|
|
90
|
+
for nothing:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
await hub.replay(peer, "lobby", since=last_seq_the_client_saw)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Slow consumers
|
|
97
|
+
|
|
98
|
+
When a peer's queue fills, what happens is a choice, not a default:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from sillo.wire import Overflow, Peer
|
|
102
|
+
|
|
103
|
+
Peer(socket, overflow=Overflow.DROP_OLDEST) # keep current — prices, cursors
|
|
104
|
+
Peer(socket, overflow=Overflow.DROP_NEWEST) # keep order — reconcile later
|
|
105
|
+
Peer(socket, overflow=Overflow.CLOSE) # disconnect and let it reconnect
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Presence
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
@hub.on_join
|
|
112
|
+
async def joined(room, peer):
|
|
113
|
+
await hub.broadcast(room, {"event": "joined", "who": peer.identity})
|
|
114
|
+
|
|
115
|
+
hub.identities("lobby") # ["ada", "bob"] — people, not sockets
|
|
116
|
+
hub.count("lobby") # 5 — subscriptions
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Two peers can share an identity — the same person with a phone and two tabs —
|
|
120
|
+
and `send_to` reaches all of them:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
await hub.send_to("ada", {"notice": "your export is ready"})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Consumers
|
|
127
|
+
|
|
128
|
+
`RoomConsumer` is the class-based form. It accepts the socket, builds the peer,
|
|
129
|
+
joins the rooms, pumps messages, and guarantees the peer is removed from every
|
|
130
|
+
room when the connection ends — including when a hook raises.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from sillo.wire import Hub, RoomConsumer
|
|
134
|
+
|
|
135
|
+
hub = Hub()
|
|
136
|
+
|
|
137
|
+
class Chat(RoomConsumer):
|
|
138
|
+
hub = hub
|
|
139
|
+
|
|
140
|
+
async def identify(self, ctx):
|
|
141
|
+
return ctx.query_params.get("user")
|
|
142
|
+
|
|
143
|
+
async def rooms(self, ctx):
|
|
144
|
+
return [ctx.path_params["room"]]
|
|
145
|
+
|
|
146
|
+
async def on_message(self, data):
|
|
147
|
+
await self.broadcast({"from": self.peer.identity, "text": data})
|
|
148
|
+
|
|
149
|
+
app.add_ws_route(path="/ws/{room}", handler=Chat.as_handler())
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Backlog
|
|
153
|
+
|
|
154
|
+
Retention is per room and capped by payload bytes, evicting oldest first:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from sillo.wire import Hub, MemoryBacklog, NullBacklog
|
|
158
|
+
|
|
159
|
+
Hub(backlog=MemoryBacklog(capacity_bytes=4 * 1024 * 1024))
|
|
160
|
+
Hub(backlog=NullBacklog()) # keep nothing — typing indicators, telemetry
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`Backlog` is a `Protocol`, so a Redis or Postgres store satisfies it without
|
|
164
|
+
importing anything from here.
|
|
165
|
+
|
|
166
|
+
## Testing
|
|
167
|
+
|
|
168
|
+
`sillo.wire.testing` ships the piece unit tests are missing — a socket:
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from sillo.wire import Hub, Peer
|
|
172
|
+
from sillo_wire.testing import FakeSocket, drain
|
|
173
|
+
|
|
174
|
+
async def test_a_broadcast_reaches_the_room():
|
|
175
|
+
hub, socket = Hub(), FakeSocket()
|
|
176
|
+
peer = Peer(socket)
|
|
177
|
+
await hub.join(peer, "lobby")
|
|
178
|
+
|
|
179
|
+
await hub.broadcast("lobby", {"hello": True})
|
|
180
|
+
await drain(peer) # broadcasts enqueue; this waits for the write
|
|
181
|
+
|
|
182
|
+
assert socket.sent == [{"hello": True}]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`FakeSocket(delay=…)` simulates a client that is slow to read, and
|
|
186
|
+
`FakeSocket(fail=True)` one that has gone away — the two cases that are hardest
|
|
187
|
+
to reproduce against a real server and the two most worth testing.
|
|
188
|
+
|
|
189
|
+
## Reference
|
|
190
|
+
|
|
191
|
+
| | |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `Hub` | `join` `leave` `leave_all` `disconnect` `broadcast` `send_to` `replay` `history` `clear_history` `on_join` `on_leave` `rooms` `members` `identities` `count` `prune` `close` |
|
|
194
|
+
| `Peer` | `offer` `send` `start` `close` `is_idle` `closed` `pending` `identity` |
|
|
195
|
+
| `Envelope` | `payload` `room` `seq` `sent_at` `size()` |
|
|
196
|
+
| `DeliveryReport` | `delivered` `dropped` `failed` `attempted` |
|
|
197
|
+
| `Backlog` | `MemoryBacklog` `NullBacklog`, or your own |
|
|
198
|
+
| `Overflow` | `DROP_OLDEST` `DROP_NEWEST` `CLOSE` |
|
|
199
|
+
|
|
200
|
+
## The two import paths
|
|
201
|
+
|
|
202
|
+
`sillo.wire` and `sillo_wire` name the same objects. The code lives in the
|
|
203
|
+
top-level `sillo_wire` package; `sillo.wire` is an alias, so it reads as part
|
|
204
|
+
of the framework:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from sillo.wire import Hub # both of these
|
|
208
|
+
from sillo_wire import Hub # bind the same class
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
The alias is a meta-path finder registered by a `.pth` at interpreter startup —
|
|
212
|
+
the only hook that runs before an `import sillo.wire` could fail. Type checkers
|
|
213
|
+
never run import hooks, so they are served separately by the partial stubs in
|
|
214
|
+
`sillo-stubs/` (PEP 561), which are additive: mypy resolves `sillo.wire` and
|
|
215
|
+
still uses the framework's own inline types for the rest of `sillo`.
|
|
216
|
+
|
|
217
|
+
Nothing is written into the framework's package directory. Shipping
|
|
218
|
+
`sillo/wire/` in there would be simpler, and it is what this did first — but
|
|
219
|
+
two distributions sharing one directory goes wrong in both directions.
|
|
220
|
+
Installing the framework from a checkout moves where `sillo` resolves and
|
|
221
|
+
orphans the copy in site-packages; removing or replacing the framework leaves
|
|
222
|
+
that directory standing with no `__init__.py`, which is an override rather than
|
|
223
|
+
an addition. Uninstalling either package here leaves the other exactly as it
|
|
224
|
+
was.
|
|
225
|
+
|
|
226
|
+
## Working on it
|
|
227
|
+
|
|
228
|
+
The alias works under an editable install too — the `.pth` is shipped by the
|
|
229
|
+
editable build target as well as the wheel.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
pip install -e ".[dev]"
|
|
233
|
+
pytest --cov # 100% required, bootstrap included
|
|
234
|
+
ruff check sillo_wire tests _sillo_wire_bootstrap.py
|
|
235
|
+
mypy sillo_wire
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Requirements
|
|
239
|
+
|
|
240
|
+
Python 3.10+, `sillo-framework` 0.3 or newer. No other dependencies.
|
|
241
|
+
|
|
242
|
+
## Licence
|
|
243
|
+
|
|
244
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
sillo-stubs/py.typed,sha256=la67KBlbjXN-_-DfGNcdOcjYumVpKG_Tkw-8n5dnGB4,8
|
|
2
|
+
sillo-stubs/wire.pyi,sha256=JPl9KrNkBmSbC0uhwOSHokHhImU-Vy88a7I66IQpJKM,1599
|
|
3
|
+
sillo_wire/__init__.py,sha256=IUQu2nkvHUdZ1aeqTNOXjCRfjsT4fYjw1lx62-2oIPo,1778
|
|
4
|
+
sillo_wire/backlog.py,sha256=xp4eh3lrDugrDy5-kQm1OJi9ksZpL_fMrhI8ittNIXU,4989
|
|
5
|
+
sillo_wire/consumer.py,sha256=z5mfCmXCWDyGjL7Y0loGPSJmZAHNQWYLj1fOqqakd1M,6782
|
|
6
|
+
sillo_wire/envelope.py,sha256=NQJz4Vurmj7pLhXPuGHWNkfPo3rmRr_PrFwgwIRGEwA,3022
|
|
7
|
+
sillo_wire/errors.py,sha256=AzFwuRu4TyE7PYlPZay66NJKPHYwlMu7BW3CCiQN7fA,969
|
|
8
|
+
sillo_wire/hub.py,sha256=71l4-AXNWV7zSH_420YSjrSgkZ57Df1U6Xy8T0Vo5zM,12181
|
|
9
|
+
sillo_wire/peer.py,sha256=i1VYbxer30jb1m0HS9u5XbSnhhJHDCJGqh45wFCxlN8,8594
|
|
10
|
+
sillo_wire/policy.py,sha256=bZWrtA40kwZ-X9ZI69osROZmpFwiGlAlPODsKc7y_8k,1302
|
|
11
|
+
sillo_wire/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
sillo_wire/testing.py,sha256=gLJCviZadqJ1CyLnFukRajvwZNu2DaCQ-XTyX7a5JM4,2849
|
|
13
|
+
_sillo_wire_bootstrap.py,sha256=An8k-vTkg-dTEs4Wm_pzf-Kh1XUBPiRp24mC6IFcczA,3720
|
|
14
|
+
sillo_wire.pth,sha256=t_fzMs4Twwhhm0yv7-lbUsXEWyzBjHh3MOq8RYVDpJQ,29
|
|
15
|
+
sillo_wire-0.1.0.dev1.dist-info/METADATA,sha256=qIVNR1kTDwmESRWwu6w3eThoMSYO-kKTkkoPctVZAXQ,8094
|
|
16
|
+
sillo_wire-0.1.0.dev1.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
17
|
+
sillo_wire-0.1.0.dev1.dist-info/licenses/LICENSE,sha256=SYnJBmVg6ElC6Nx4oK_kH5R5A6wVeKLU0kyf-_UzZs8,1528
|
|
18
|
+
sillo_wire-0.1.0.dev1.dist-info/RECORD,,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present, sillo-Labs OSS.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
+
are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
15
|
+
contributors may be used to endorse or promote products derived from
|
|
16
|
+
this software without specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
sillo_wire.pth
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import _sillo_wire_bootstrap
|