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.
@@ -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,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,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