socket-netty 0.3.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.
Files changed (46) hide show
  1. socket_netty-0.3.0/MANIFEST.in +2 -0
  2. socket_netty-0.3.0/PKG-INFO +402 -0
  3. socket_netty-0.3.0/README.md +382 -0
  4. socket_netty-0.3.0/examples/01_echo_client.py +49 -0
  5. socket_netty-0.3.0/examples/01_echo_server.py +56 -0
  6. socket_netty-0.3.0/examples/02_game_protocol_client.py +96 -0
  7. socket_netty-0.3.0/examples/02_game_protocol_server.py +132 -0
  8. socket_netty-0.3.0/pynetty/__init__.py +137 -0
  9. socket_netty-0.3.0/pynetty/bootstrap/__init__.py +3 -0
  10. socket_netty-0.3.0/pynetty/bootstrap/bootstrap.py +162 -0
  11. socket_netty-0.3.0/pynetty/buffer/__init__.py +18 -0
  12. socket_netty-0.3.0/pynetty/buffer/allocator.py +113 -0
  13. socket_netty-0.3.0/pynetty/buffer/bytebuf.py +256 -0
  14. socket_netty-0.3.0/pynetty/channel/__init__.py +24 -0
  15. socket_netty-0.3.0/pynetty/channel/channel.py +130 -0
  16. socket_netty-0.3.0/pynetty/channel/channel_future.py +143 -0
  17. socket_netty-0.3.0/pynetty/channel/channel_option.py +59 -0
  18. socket_netty-0.3.0/pynetty/channel/channel_pipeline.py +173 -0
  19. socket_netty-0.3.0/pynetty/channel/datagram_channel.py +164 -0
  20. socket_netty-0.3.0/pynetty/channel/event_loop.py +157 -0
  21. socket_netty-0.3.0/pynetty/channel/flow_control.py +99 -0
  22. socket_netty-0.3.0/pynetty/channel/protocol_adapter.py +59 -0
  23. socket_netty-0.3.0/pynetty/exceptions.py +94 -0
  24. socket_netty-0.3.0/pynetty/handler/__init__.py +47 -0
  25. socket_netty-0.3.0/pynetty/handler/channel_handler.py +83 -0
  26. socket_netty-0.3.0/pynetty/handler/channel_handler_context.py +104 -0
  27. socket_netty-0.3.0/pynetty/handler/codec.py +152 -0
  28. socket_netty-0.3.0/pynetty/handler/protolib_codec.py +152 -0
  29. socket_netty-0.3.0/pynetty/handler/ssl_context.py +62 -0
  30. socket_netty-0.3.0/pynetty/handler/timeout.py +159 -0
  31. socket_netty-0.3.0/pyproject.toml +32 -0
  32. socket_netty-0.3.0/setup.cfg +4 -0
  33. socket_netty-0.3.0/socket_netty.egg-info/PKG-INFO +402 -0
  34. socket_netty-0.3.0/socket_netty.egg-info/SOURCES.txt +44 -0
  35. socket_netty-0.3.0/socket_netty.egg-info/dependency_links.txt +1 -0
  36. socket_netty-0.3.0/socket_netty.egg-info/requires.txt +1 -0
  37. socket_netty-0.3.0/socket_netty.egg-info/top_level.txt +1 -0
  38. socket_netty-0.3.0/tests/test_bytebuf.py +75 -0
  39. socket_netty-0.3.0/tests/test_codec.py +281 -0
  40. socket_netty-0.3.0/tests/test_concurrency.py +69 -0
  41. socket_netty-0.3.0/tests/test_core_extras.py +80 -0
  42. socket_netty-0.3.0/tests/test_echo.py +71 -0
  43. socket_netty-0.3.0/tests/test_flow_and_allocator.py +204 -0
  44. socket_netty-0.3.0/tests/test_network_extras.py +138 -0
  45. socket_netty-0.3.0/tests/test_pipeline.py +370 -0
  46. socket_netty-0.3.0/tests/test_tls.py +78 -0
@@ -0,0 +1,2 @@
1
+ include README.md
2
+ recursive-include examples *.py
@@ -0,0 +1,402 @@
1
+ Metadata-Version: 2.4
2
+ Name: socket-netty
3
+ Version: 0.3.0
4
+ Summary: Asynchronous networking library for Python, inspired by Netty
5
+ Author: Button
6
+ Keywords: networking,asyncio,netty,tcp,udp,game-server,protocol
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.9
11
+ Classifier: Programming Language :: Python :: 3.10
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: System :: Networking
16
+ Classifier: Framework :: AsyncIO
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: protolib>=0.4.3
20
+
21
+ # pynetty 🐍🛠️
22
+
23
+ Asynchronous networking library for Python, closely inspired by
24
+ [Netty](https://netty.io) (Java). Built on top of `asyncio`, with no
25
+ external dependencies.
26
+
27
+ ## Coverage relative to Netty
28
+
29
+ ```
30
+ Netty Py 🐍
31
+ ├── Bootstrap ✅
32
+ ├── ServerBootstrap ✅
33
+ ├── Channel ✅ (TCP)
34
+ ├── ChannelPipeline ✅
35
+ ├── ChannelHandler ✅ (inbound + outbound)
36
+ ├── ChannelHandlerContext ✅
37
+ ├── EventLoop ✅
38
+ ├── EventLoopGroup ✅
39
+ ├── ChannelFuture ✅ (+ ChannelPromise)
40
+ ├── ByteBuf ✅
41
+ ├── Allocator ✅ (Unpooled + Pooled)
42
+ ├── TCP ✅
43
+ ├── UDP ✅ (DatagramChannel)
44
+ ├── Socket options ✅ (ChannelOption)
45
+ ├── Backpressure ✅ (WriteBufferWaterMark)
46
+ ├── Concurrency ✅ (ChannelExecutor)
47
+ ├── TLS/SSL ✅ (SslContextBuilder)
48
+ ├── Encoders / Decoders ✅ (length-based framing)
49
+ ├── Idle handlers ✅ (IdleStateHandler)
50
+ ├── Timeouts ✅ (Read/WriteTimeoutHandler)
51
+ ├── Exception handling ✅ (exception_caught via the pipeline,
52
+ │ io.netty.*-style exception classes)
53
+ └── Declarative packet codec ✅ (ProtolibCodec, via the protolib
54
+ dependency)
55
+ ```
56
+
57
+ ## Installation
58
+
59
+ ```bash
60
+ pip install socket-netty
61
+ ```
62
+
63
+ (The PyPI distribution is named `socket-netty`, but the importable
64
+ package is still `pynetty` — your code stays `import pynetty`.)
65
+
66
+ Or, for local development from a clone of this repo:
67
+
68
+ ```bash
69
+ pip install -e .
70
+ ```
71
+
72
+ `pynetty` declares [`protolib`](https://pypi.org/project/protolib/) as
73
+ an install dependency, so either install method pulls it in
74
+ automatically — you don't need a separate install step to use
75
+ `ProtolibCodec` below.
76
+
77
+ ## Structure
78
+
79
+ ```
80
+ pynetty/
81
+ exceptions.py -> Netty-style exception hierarchy (DecoderException,
82
+ EncoderException, CorruptedFrameException, etc.)
83
+ buffer/ -> ByteBuf, ByteBufAllocator (Unpooled/Pooled)
84
+ handler/ -> ChannelHandler, codecs, protolib bridge, SSL,
85
+ timeouts/idle
86
+ channel/ -> Channel, DatagramChannel, ChannelPipeline,
87
+ EventLoop/Group, ChannelFuture, ChannelOption,
88
+ flow control (backpressure + concurrency)
89
+ bootstrap/ -> ServerBootstrap (server), Bootstrap (client)
90
+ ```
91
+
92
+ ## Example: TCP echo server
93
+
94
+ ```python
95
+ import asyncio
96
+ from pynetty import ServerBootstrap, ChannelInboundHandler
97
+
98
+ class EchoHandler(ChannelInboundHandler):
99
+ async def channel_active(self, ctx):
100
+ print("Client connected:", ctx.channel.remote_address())
101
+
102
+ async def channel_read(self, ctx, msg):
103
+ await ctx.write(msg) # echo
104
+
105
+ async def exception_caught(self, ctx, exc):
106
+ print("Error:", exc)
107
+ await ctx.close()
108
+
109
+ async def main():
110
+ def init_channel(channel):
111
+ channel.pipeline.add_last("echo", EchoHandler())
112
+
113
+ server = await ServerBootstrap().child_handler(init_channel).bind("0.0.0.0", 9000)
114
+ await server.serve_forever()
115
+
116
+ asyncio.run(main())
117
+ ```
118
+
119
+ ## Exceptions
120
+
121
+ pynetty raises the same exception hierarchy Netty does, under
122
+ `io.netty.*`. Every exception's `str()` is prefixed with its
123
+ fully-qualified Java-style path, so logs and `exception_caught` output
124
+ look exactly like a real Netty stack:
125
+
126
+ ```python
127
+ from pynetty import DecoderException
128
+
129
+ async def exception_caught(self, ctx, exc):
130
+ print(exc)
131
+ # io.netty.handler.codec.DecoderException: Failed to decode packet
132
+ ```
133
+
134
+ Available exceptions (all subclass `NettyException`):
135
+
136
+ | Class | Netty path |
137
+ |---|---|
138
+ | `ChannelException` | `io.netty.channel.ChannelException` |
139
+ | `DuplicateHandlerNameError` | `io.netty.channel.ChannelPipelineException` |
140
+ | `CodecException` | `io.netty.handler.codec.CodecException` |
141
+ | `DecoderException` | `io.netty.handler.codec.DecoderException` |
142
+ | `EncoderException` | `io.netty.handler.codec.EncoderException` |
143
+ | `CorruptedFrameException` | `io.netty.handler.codec.CorruptedFrameException` |
144
+ | `TooLongFrameException` | `io.netty.handler.codec.TooLongFrameException` |
145
+ | `ReadTimeoutError` | `io.netty.handler.timeout.ReadTimeoutException` |
146
+ | `WriteTimeoutError` | `io.netty.handler.timeout.WriteTimeoutException` |
147
+ | `IndexOutOfBoundsError` | `io.netty.buffer.IndexOutOfBoundsException` |
148
+
149
+ `LengthFieldBasedFrameDecoder` and `ByteToMessageCodec` automatically
150
+ wrap any unexpected error raised inside `decode()` into a
151
+ `DecoderException`, and `LengthFieldPrepender` does the same for
152
+ `EncoderException` — matching Netty's own behavior of never letting a
153
+ raw internal error escape a codec unwrapped.
154
+
155
+ ## Socket options
156
+
157
+ ```python
158
+ from pynetty import ServerBootstrap, ChannelOption
159
+
160
+ server = (
161
+ ServerBootstrap()
162
+ .child_handler(init_channel)
163
+ .option(ChannelOption.SO_REUSEADDR, True)
164
+ .option(ChannelOption.SO_BACKLOG, 128)
165
+ .option(ChannelOption.TCP_NODELAY, True) # low latency, typical for games
166
+ )
167
+ await server.bind("0.0.0.0", 9000)
168
+ ```
169
+
170
+ ## TLS/SSL
171
+
172
+ ```python
173
+ from pynetty import ServerBootstrap, Bootstrap, SslContextBuilder
174
+
175
+ # Server
176
+ server_ctx = SslContextBuilder.for_server("cert.pem", "key.pem").build()
177
+ server = ServerBootstrap().child_handler(init_channel).ssl(server_ctx)
178
+ await server.bind("0.0.0.0", 9443)
179
+
180
+ # Client
181
+ client_ctx = SslContextBuilder.for_client().trust_manager("cert.pem").build()
182
+ channel = await Bootstrap().handler(init_client).ssl(client_ctx).connect("myserver.com", 9443)
183
+ ```
184
+
185
+ ## UDP
186
+
187
+ ```python
188
+ from pynetty import DatagramBootstrap, ChannelInboundHandler
189
+
190
+ class UdpHandler(ChannelInboundHandler):
191
+ async def channel_read(self, ctx, msg):
192
+ data, addr = msg # UDP delivers (bytes, (host, port))
193
+ await ctx.write((b"pong", addr))
194
+
195
+ channel = await DatagramBootstrap().handler(
196
+ lambda ch: ch.pipeline.add_last("udp", UdpHandler())
197
+ ).bind("0.0.0.0", 9001)
198
+ ```
199
+
200
+ ## Idle / Timeouts
201
+
202
+ ```python
203
+ from pynetty import IdleStateHandler, ReadTimeoutHandler
204
+
205
+ async def on_idle(ctx, event):
206
+ print("Idle channel:", event.state)
207
+ await ctx.write(b"PING") # e.g. a game protocol heartbeat
208
+
209
+ def init_channel(channel):
210
+ idle = IdleStateHandler(reader_idle_seconds=30)
211
+ idle.on_idle = on_idle
212
+ channel.pipeline.add_last("idle", idle)
213
+ channel.pipeline.add_last("read_timeout", ReadTimeoutHandler(60)) # closes if no data in 60s
214
+ channel.pipeline.add_last("my_handler", MyHandler())
215
+ ```
216
+
217
+ ## EventLoopGroup (real multi-threaded concurrency)
218
+
219
+ ```python
220
+ from pynetty import EventLoopGroup
221
+
222
+ worker_group = EventLoopGroup(num_threads=4)
223
+ worker_group.start()
224
+
225
+ loop = worker_group.next_loop() # round-robin
226
+ future = loop.submit(lambda: my_heavy_coroutine())
227
+ result = future.result(timeout=5) # blocking, from any thread
228
+ ```
229
+
230
+ ## ChannelFuture / ChannelPromise
231
+
232
+ ```python
233
+ from pynetty import ChannelFuture
234
+
235
+ future = channel.new_future()
236
+ future.add_listener(lambda f: print("Finished:", f.is_success()))
237
+
238
+ # Still directly awaitable, Python-style:
239
+ result = await future
240
+ ```
241
+
242
+ ## Backpressure (WriteBufferWaterMark)
243
+
244
+ ```python
245
+ from pynetty import ServerBootstrap, WriteBufferWaterMark
246
+
247
+ server = (
248
+ ServerBootstrap()
249
+ .child_handler(init_channel)
250
+ .water_mark(WriteBufferWaterMark(low=32*1024, high=64*1024))
251
+ )
252
+
253
+ # In the handler:
254
+ if not ctx.channel.is_writable():
255
+ # pause sending more data until it becomes writable again
256
+ ...
257
+ ```
258
+
259
+ ## Concurrency (ChannelExecutor)
260
+
261
+ Guarantees a channel's messages are processed one at a time, in
262
+ order, even across `await` points (avoids race conditions on shared
263
+ handler state):
264
+
265
+ ```python
266
+ from pynetty import ChannelExecutor
267
+
268
+ executor = ChannelExecutor()
269
+ executor.start()
270
+
271
+ async def channel_read(self, ctx, msg):
272
+ await executor.submit(self._process(ctx, msg))
273
+ ```
274
+
275
+ ## Allocator (buffer pool)
276
+
277
+ ```python
278
+ from pynetty import PooledByteBufAllocator
279
+
280
+ allocator = PooledByteBufAllocator()
281
+ buf = allocator.buffer(256)
282
+ buf.write_int(42)
283
+ # ... use the buffer ...
284
+ allocator.release(buf) # goes back to the pool, ready to be reused
285
+ ```
286
+
287
+ ## Protocol framing (length-prefixed packets)
288
+
289
+ Very common in game protocols (Minecraft, Free Fire, etc.):
290
+
291
+ ```python
292
+ from pynetty import LengthFieldBasedFrameDecoder, LengthFieldPrepender
293
+
294
+ def init_channel(channel):
295
+ channel.pipeline.add_last("frame_decoder", LengthFieldBasedFrameDecoder(4))
296
+ channel.pipeline.add_last("frame_prepender", LengthFieldPrepender(4))
297
+ channel.pipeline.add_last("my_handler", MyHandler())
298
+ ```
299
+
300
+ ## Protocol decoding with protolib (declarative packets)
301
+
302
+ `LengthFieldBasedFrameDecoder`/`LengthFieldPrepender` above only solve
303
+ framing (where one packet ends and the next begins) — you still have
304
+ to hand-write the code that turns those raw bytes into a meaningful
305
+ packet. [`protolib`](https://pypi.org/project/protolib/) solves that
306
+ second half: you describe every packet's fields in a `.yml`/`.json`
307
+ file, and `ProtolibCodec` plugs that description directly into the
308
+ pipeline. `channel_read` then hands your handler a ready-made
309
+ `{"name": ..., "params": {...}}` dict instead of raw bytes, and
310
+ `write` accepts the same shape (or a `(name, params)` tuple) going
311
+ out.
312
+
313
+ `ProtolibCodec` has two framing modes, controlled by `framed`:
314
+
315
+ ```python
316
+ from pynetty import ServerBootstrap, ChannelInboundHandler, ProtolibCodec
317
+
318
+ # framed=True (default): the codec frames the stream itself via
319
+ # protolib's own PacketFramer (varint length-prefix, Minecraft-style).
320
+ # Don't also add LengthFieldBasedFrameDecoder in this mode.
321
+ def init_channel(channel):
322
+ channel.pipeline.add_last(
323
+ "protolib", ProtolibCodec("my_protocol.yml", state="play",
324
+ direction_in="toServer", direction_out="toClient"),
325
+ )
326
+ channel.pipeline.add_last("game", GameHandler())
327
+
328
+ class GameHandler(ChannelInboundHandler):
329
+ async def channel_read(self, ctx, msg):
330
+ print(msg["name"], msg["params"]) # ready-made dict
331
+ await ctx.write(("keep_alive", {"id": 1})) # (name, params) out
332
+ ```
333
+
334
+ ```python
335
+ # framed=False: for fixed-size / non-varint protocols (e.g. Minecraft
336
+ # Classic/ClassiCube), keep your own LengthFieldBasedFrameDecoder in
337
+ # front and let ProtolibCodec just parse/serialize the complete frame
338
+ # it's handed.
339
+ def init_channel(channel):
340
+ channel.pipeline.add_last("frame_decoder", LengthFieldBasedFrameDecoder(1))
341
+ channel.pipeline.add_last(
342
+ "protolib", ProtolibCodec("classicube_protocol.yml", framed=False),
343
+ )
344
+ channel.pipeline.add_last("game", GameHandler())
345
+ ```
346
+
347
+ `protocol` accepts a `protolib.Protocol` instance you already built,
348
+ or anything `Protocol(...)` itself accepts (a `.yml`/`.json` path, an
349
+ in-memory string, or a parsed dict) — `ProtolibCodec` builds the
350
+ `Protocol` for you in that case. Any parsing/serialization error
351
+ protolib raises is wrapped into `DecoderException`/`EncoderException`,
352
+ same as the rest of the codecs in this library.
353
+
354
+ ## ByteBuf
355
+
356
+ ```python
357
+ from pynetty import ByteBuf
358
+
359
+ buf = ByteBuf()
360
+ buf.write_varint(1000)
361
+ buf.write_string("hello")
362
+ buf.write_int(-42)
363
+
364
+ data = buf.to_bytes()
365
+
366
+ read_buf = ByteBuf.wrapped(data)
367
+ n = read_buf.read_varint()
368
+ s = read_buf.read_string()
369
+ i = read_buf.read_int()
370
+ ```
371
+
372
+ Supports: `byte`, `unsigned_byte`, `short`, `unsigned_short`, `int`,
373
+ `unsigned_int`, `long`, `float`, `double`, `boolean`, `varint`,
374
+ `varlong` (Minecraft/protobuf style), `string`, and raw bytes.
375
+
376
+ ## Tests
377
+
378
+ ```bash
379
+ python3 tests/test_bytebuf.py
380
+ python3 tests/test_echo.py
381
+ python3 tests/test_core_extras.py # EventLoop, ChannelFuture, Allocator
382
+ python3 tests/test_network_extras.py # UDP, socket options, timeouts, idle
383
+ python3 tests/test_tls.py # end-to-end TLS (generates temporary certs)
384
+ python3 tests/test_concurrency.py # ChannelExecutor
385
+ ```
386
+
387
+ ## Design notes
388
+
389
+ - The entire pipeline is 100% `async`/`await`.
390
+ - `EventLoopGroup` uses real OS threads (each with its own asyncio
391
+ loop), unlike the rest of the library which normally runs on a
392
+ single loop — it's the honest way to replicate Netty's model
393
+ (thread pool) in Python.
394
+ - `ByteBuf` is a simple implementation backed by `bytearray`, with no
395
+ manual refcounting (Python already has GC).
396
+ - `PooledByteBufAllocator` recycles buffers by size "bucket" (powers
397
+ of 2), useful in high-traffic game servers to reduce GC pressure.
398
+ - Exceptions mirror Netty's own `io.netty.*` hierarchy (see
399
+ [Exceptions](#exceptions) above), so error messages and logs read
400
+ the same as a real Netty stack trace.
401
+ - Designed for use both in game projects (custom servers, protocol
402
+ reversing) and as a general-purpose library.