socket-netty 0.3.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.
@@ -0,0 +1,24 @@
1
+ from pynetty.channel.channel import Channel
2
+ from pynetty.channel.channel_pipeline import ChannelPipeline, DuplicateHandlerNameError
3
+ from pynetty.channel.channel_future import ChannelFuture, ChannelPromise
4
+ from pynetty.channel.event_loop import EventLoop, EventLoopGroup
5
+ from pynetty.channel.channel_option import ChannelOption, apply_socket_options
6
+ from pynetty.channel.flow_control import WriteBufferWaterMark, ChannelExecutor, FlowControlInboundHandler
7
+ from pynetty.channel.datagram_channel import DatagramChannel, DatagramBootstrap
8
+
9
+ __all__ = [
10
+ "Channel",
11
+ "ChannelPipeline",
12
+ "DuplicateHandlerNameError",
13
+ "ChannelFuture",
14
+ "ChannelPromise",
15
+ "EventLoop",
16
+ "EventLoopGroup",
17
+ "ChannelOption",
18
+ "apply_socket_options",
19
+ "WriteBufferWaterMark",
20
+ "ChannelExecutor",
21
+ "FlowControlInboundHandler",
22
+ "DatagramChannel",
23
+ "DatagramBootstrap",
24
+ ]
@@ -0,0 +1,130 @@
1
+ """
2
+ Channel: represents a network connection (socket) and its associated
3
+ pipeline. Wraps an asyncio.Transport and exposes the Netty-style API
4
+ (write, close, is_active, remote_address, attr).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ from typing import Any, Dict, Optional
11
+
12
+ from pynetty.channel.channel_pipeline import ChannelPipeline
13
+ from pynetty.channel.channel_future import ChannelFuture
14
+ from pynetty.exceptions import ChannelException
15
+
16
+
17
+ class Channel:
18
+ def __init__(self, transport: Optional[asyncio.BaseTransport] = None) -> None:
19
+ self.transport = transport
20
+ self.pipeline = ChannelPipeline(self)
21
+ self._active = False
22
+ self._attrs: Dict[str, Any] = {}
23
+ self._close_event = asyncio.Event()
24
+ self._writable = True
25
+
26
+ # ------------------------------------------------------------------
27
+ # Lifecycle — invoked by asyncio's Protocol
28
+ # ------------------------------------------------------------------
29
+ def _attach_transport(self, transport: asyncio.BaseTransport) -> None:
30
+ self.transport = transport
31
+
32
+ async def _mark_active(self) -> None:
33
+ self._active = True
34
+ await self.pipeline.fire_channel_registered()
35
+ await self.pipeline.fire_channel_active()
36
+
37
+ async def _mark_inactive(self) -> None:
38
+ if not self._active:
39
+ return
40
+ self._active = False
41
+ await self.pipeline.fire_channel_inactive()
42
+ self._close_event.set()
43
+
44
+ async def _fire_read(self, data: Any) -> None:
45
+ await self.pipeline.fire_channel_read(data)
46
+ await self.pipeline.fire_channel_read_complete()
47
+
48
+ async def _fire_exception(self, exc: Exception) -> None:
49
+ await self.pipeline.fire_exception_caught(exc)
50
+
51
+ # ------------------------------------------------------------------
52
+ # Public Netty-style API
53
+ # ------------------------------------------------------------------
54
+ def is_active(self) -> bool:
55
+ return self._active and self.transport is not None and not self.transport.is_closing()
56
+
57
+ def is_writable(self) -> bool:
58
+ """
59
+ Reflects the backpressure state of the underlying transport
60
+ (equivalent to Netty's Channel.isWritable()). Updated via
61
+ asyncio's Protocol pause_writing()/resume_writing().
62
+ """
63
+ return self._writable
64
+
65
+ def _set_writable(self, value: bool) -> None:
66
+ self._writable = value
67
+
68
+ def new_future(self) -> ChannelFuture:
69
+ """Creates a new ChannelFuture associated with this channel (equivalent to a new ChannelPromise)."""
70
+ return ChannelFuture(self)
71
+
72
+ async def write(self, msg: Any) -> None:
73
+ """Writes a message through the pipeline (passes through the outbound handlers)."""
74
+ await self.pipeline.write(msg)
75
+
76
+ async def close(self) -> None:
77
+ await self.pipeline.close()
78
+ # Wait for the transport's connection_lost() to fire
79
+ # _mark_inactive() and finish propagating channel_inactive, so
80
+ # `await channel.close()` is a real guarantee that the
81
+ # channel's lifecycle has fully closed.
82
+ if self.transport is not None:
83
+ try:
84
+ await asyncio.wait_for(self.wait_closed(), timeout=5)
85
+ except asyncio.TimeoutError:
86
+ pass
87
+
88
+ async def wait_closed(self) -> None:
89
+ await self._close_event.wait()
90
+
91
+ def remote_address(self) -> Optional[tuple]:
92
+ if self.transport is None:
93
+ return None
94
+ return self.transport.get_extra_info("peername")
95
+
96
+ def local_address(self) -> Optional[tuple]:
97
+ if self.transport is None:
98
+ return None
99
+ return self.transport.get_extra_info("sockname")
100
+
101
+ def attr(self, key: str, default: Any = None) -> Any:
102
+ return self._attrs.get(key, default)
103
+
104
+ def set_attr(self, key: str, value: Any) -> None:
105
+ self._attrs[key] = value
106
+
107
+ # ------------------------------------------------------------------
108
+ # Actual I/O implementation, invoked only once a message reaches the
109
+ # end of the outbound chain (or the head, in close()'s case)
110
+ # ------------------------------------------------------------------
111
+ async def _do_write(self, msg: Any) -> None:
112
+ if self.transport is None:
113
+ raise ChannelException("Channel has no associated transport; cannot write")
114
+ if isinstance(msg, (bytes, bytearray)):
115
+ self.transport.write(bytes(msg))
116
+ elif hasattr(msg, "to_bytes"):
117
+ # Supports ByteBuf and similar objects
118
+ self.transport.write(msg.to_bytes())
119
+ else:
120
+ raise TypeError(
121
+ f"Cannot write a message of type {type(msg).__name__}; "
122
+ "add an encoder to the pipeline that converts it to bytes"
123
+ )
124
+
125
+ async def _do_close(self) -> None:
126
+ if self.transport is not None and not self.transport.is_closing():
127
+ self.transport.close()
128
+
129
+ def __repr__(self) -> str:
130
+ return f"Channel(remote={self.remote_address()}, active={self.is_active()})"
@@ -0,0 +1,143 @@
1
+ """
2
+ ChannelFuture / ChannelPromise: equivalent to io.netty.channel.ChannelFuture.
3
+
4
+ In Netty, nearly every async operation (connect, write, close, bind)
5
+ returns a ChannelFuture you can add listeners to instead of blocking.
6
+ Here it's modeled as a thin wrapper over asyncio.Future that:
7
+ - Is directly awaitable (`await future`), for Python-style usage.
8
+ - Supports add_listener()/remove_listener(), for Netty-style usage.
9
+ - Exposes is_done(), is_success(), cause(), get_now(), sync(), await_().
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ from typing import Any, Callable, List, Optional, TypeVar
16
+
17
+ T = TypeVar("T")
18
+
19
+ Listener = Callable[["ChannelFuture"], None]
20
+
21
+
22
+ class ChannelFuture:
23
+ def __init__(self, channel: Any, aio_future: Optional[asyncio.Future] = None) -> None:
24
+ self.channel = channel
25
+ self._future: asyncio.Future = aio_future if aio_future is not None else asyncio.get_event_loop().create_future()
26
+ self._listeners: List[Listener] = []
27
+ self._notified = False
28
+ self._future.add_done_callback(self._on_future_done)
29
+
30
+ # ------------------------------------------------------------------
31
+ # Construction / resolution
32
+ # ------------------------------------------------------------------
33
+ @classmethod
34
+ def succeeded(cls, channel: Any, result: Any = None) -> "ChannelFuture":
35
+ fut = cls(channel)
36
+ fut.set_result(result)
37
+ return fut
38
+
39
+ @classmethod
40
+ def failed(cls, channel: Any, exc: Exception) -> "ChannelFuture":
41
+ fut = cls(channel)
42
+ fut.set_exception(exc)
43
+ return fut
44
+
45
+ def set_result(self, result: Any = None) -> "ChannelFuture":
46
+ if not self._future.done():
47
+ self._future.set_result(result)
48
+ self._on_done(self._future)
49
+ return self
50
+
51
+ def set_exception(self, exc: Exception) -> "ChannelFuture":
52
+ if not self._future.done():
53
+ self._future.set_exception(exc)
54
+ self._on_done(self._future)
55
+ return self
56
+
57
+ def _on_future_done(self, _fut: asyncio.Future) -> None:
58
+ # Fallback callback for external futures (passed via
59
+ # aio_future) that complete through another path (e.g. cancel()).
60
+ # If set_result/set_exception already fired _on_done
61
+ # synchronously, this callback arrives later and must not repeat listeners.
62
+ self._on_done(_fut)
63
+
64
+ def _on_done(self, _fut: asyncio.Future) -> None:
65
+ if self._notified:
66
+ return
67
+ self._notified = True
68
+ for listener in self._listeners:
69
+ try:
70
+ listener(self)
71
+ except Exception:
72
+ # A listener that blows up must not take down the rest.
73
+ pass
74
+
75
+ # ------------------------------------------------------------------
76
+ # Netty-style API
77
+ # ------------------------------------------------------------------
78
+ def add_listener(self, listener: Listener) -> "ChannelFuture":
79
+ """
80
+ Registers a callback that receives this ChannelFuture once it
81
+ completes. If it's already complete, it's invoked immediately
82
+ (same behavior as Netty).
83
+ """
84
+ self._listeners.append(listener)
85
+ if self._future.done():
86
+ listener(self)
87
+ return self
88
+
89
+ def remove_listener(self, listener: Listener) -> "ChannelFuture":
90
+ if listener in self._listeners:
91
+ self._listeners.remove(listener)
92
+ return self
93
+
94
+ def is_done(self) -> bool:
95
+ return self._future.done()
96
+
97
+ def is_success(self) -> bool:
98
+ return self._future.done() and self._future.exception() is None
99
+
100
+ def is_cancelled(self) -> bool:
101
+ return self._future.cancelled()
102
+
103
+ def cause(self) -> Optional[BaseException]:
104
+ if not self._future.done() or self._future.cancelled():
105
+ return None
106
+ return self._future.exception()
107
+
108
+ def get_now(self) -> Any:
109
+ """Returns the result if it's already ready, otherwise None (does not block)."""
110
+ if self._future.done() and not self._future.cancelled() and self._future.exception() is None:
111
+ return self._future.result()
112
+ return None
113
+
114
+ async def sync(self) -> "ChannelFuture":
115
+ """Waits for the operation to finish and re-raises the exception if it failed (equivalent to Netty's sync())."""
116
+ await self._future
117
+ return self
118
+
119
+ def __await__(self):
120
+ return self._future.__await__()
121
+
122
+ def __repr__(self) -> str:
123
+ state = "pending" if not self.is_done() else ("success" if self.is_success() else "failed")
124
+ return f"ChannelFuture({state})"
125
+
126
+
127
+ class ChannelPromise(ChannelFuture):
128
+ """
129
+ Equivalent to io.netty.channel.ChannelPromise: a ChannelFuture that
130
+ can also be completed manually from the outside (writer side).
131
+ """
132
+
133
+ def try_success(self, result: Any = None) -> bool:
134
+ if self._future.done():
135
+ return False
136
+ self.set_result(result)
137
+ return True
138
+
139
+ def try_failure(self, exc: Exception) -> bool:
140
+ if self._future.done():
141
+ return False
142
+ self.set_exception(exc)
143
+ return True
@@ -0,0 +1,59 @@
1
+ """
2
+ ChannelOption: equivalent to io.netty.channel.ChannelOption.
3
+
4
+ In Netty you configure socket options via
5
+ `.option(ChannelOption.SO_BACKLOG, 128)` before bind()/connect().
6
+ Here we replicate that declarative API and apply it to the real Python
7
+ socket (via setsockopt) when the Channel is created.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import socket
13
+ from typing import Any, Dict
14
+
15
+
16
+ class ChannelOption:
17
+ """Namespace of option constants, like Netty's ChannelOption class."""
18
+
19
+ SO_BACKLOG = "SO_BACKLOG" # size of the pending-connections queue (server only)
20
+ SO_REUSEADDR = "SO_REUSEADDR" # reuse the address/port immediately after closing
21
+ SO_KEEPALIVE = "SO_KEEPALIVE" # kernel-level TCP keepalive
22
+ SO_RCVBUF = "SO_RCVBUF" # kernel receive-buffer size
23
+ SO_SNDBUF = "SO_SNDBUF" # kernel send-buffer size
24
+ TCP_NODELAY = "TCP_NODELAY" # disables Nagle's algorithm (low latency, useful for games)
25
+ SO_LINGER = "SO_LINGER" # seconds to wait on close with pending data
26
+
27
+
28
+ def apply_socket_options(sock: socket.socket, options: Dict[str, Any]) -> None:
29
+ """Applies a dict of ChannelOption -> value onto a real socket."""
30
+ for name, value in options.items():
31
+ if name == ChannelOption.SO_REUSEADDR:
32
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 if value else 0)
33
+ elif name == ChannelOption.SO_KEEPALIVE:
34
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1 if value else 0)
35
+ elif name == ChannelOption.SO_RCVBUF:
36
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, int(value))
37
+ elif name == ChannelOption.SO_SNDBUF:
38
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, int(value))
39
+ elif name == ChannelOption.TCP_NODELAY:
40
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1 if value else 0)
41
+ elif name == ChannelOption.SO_LINGER:
42
+ # value: seconds (int) or None to disable
43
+ if value is None:
44
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct_linger(0, 0))
45
+ else:
46
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct_linger(1, int(value)))
47
+ elif name == ChannelOption.SO_BACKLOG:
48
+ # SO_BACKLOG is applied when creating the listener
49
+ # (loop.create_server(backlog=...)), not via setsockopt;
50
+ # the bootstrap handles it directly.
51
+ continue
52
+ else:
53
+ raise ValueError(f"Unknown ChannelOption: {name}")
54
+
55
+
56
+ def struct_linger(onoff: int, linger: int) -> bytes:
57
+ import struct
58
+
59
+ return struct.pack("ii", onoff, linger)
@@ -0,0 +1,173 @@
1
+ """
2
+ ChannelPipeline: doubly-linked list of ChannelHandlerContext instances,
3
+ each wrapping a ChannelHandler added to a Channel. It's the central
4
+ entry point for firing inbound events and outbound operations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, TYPE_CHECKING
10
+
11
+ from pynetty.exceptions import DuplicateHandlerNameError
12
+ from pynetty.handler.channel_handler import (
13
+ ChannelHandler,
14
+ ChannelInboundHandler,
15
+ ChannelOutboundHandler,
16
+ )
17
+ from pynetty.handler.channel_handler_context import ChannelHandlerContext
18
+
19
+ if TYPE_CHECKING:
20
+ from pynetty.channel.channel import Channel
21
+
22
+
23
+ class ChannelPipeline:
24
+ def __init__(self, channel: "Channel") -> None:
25
+ self.channel = channel
26
+ # Sentinel head and tail (no real handler), as in Netty
27
+ # (HeadContext / TailContext).
28
+ self._head = ChannelHandlerContext("__head__", _HeadHandler(), self)
29
+ self._tail = ChannelHandlerContext("__tail__", _TailHandler(), self)
30
+ self._head._next = self._tail
31
+ self._tail._prev = self._head
32
+ self._contexts: Dict[str, ChannelHandlerContext] = {}
33
+
34
+ # ------------------------------------------------------------------
35
+ # Handler registration
36
+ # ------------------------------------------------------------------
37
+ def add_last(self, name: str, handler: ChannelHandler) -> "ChannelPipeline":
38
+ if name in self._contexts:
39
+ raise DuplicateHandlerNameError(f"Duplicate handler name: {name!r}")
40
+ ctx = ChannelHandlerContext(name, handler, self)
41
+ prev = self._tail._prev
42
+ prev._next = ctx
43
+ ctx._prev = prev
44
+ ctx._next = self._tail
45
+ self._tail._prev = ctx
46
+ self._contexts[name] = ctx
47
+ return self
48
+
49
+ def add_first(self, name: str, handler: ChannelHandler) -> "ChannelPipeline":
50
+ if name in self._contexts:
51
+ raise DuplicateHandlerNameError(f"Duplicate handler name: {name!r}")
52
+ ctx = ChannelHandlerContext(name, handler, self)
53
+ nxt = self._head._next
54
+ self._head._next = ctx
55
+ ctx._prev = self._head
56
+ ctx._next = nxt
57
+ nxt._prev = ctx
58
+ self._contexts[name] = ctx
59
+ return self
60
+
61
+ def remove(self, name: str) -> "ChannelPipeline":
62
+ ctx = self._contexts.pop(name, None)
63
+ if ctx is None:
64
+ return self
65
+ ctx._prev._next = ctx._next
66
+ ctx._next._prev = ctx._prev
67
+ return self
68
+
69
+ def get(self, name: str) -> "ChannelHandler | None":
70
+ ctx = self._contexts.get(name)
71
+ return ctx.handler if ctx else None
72
+
73
+ def context(self, name: str) -> "ChannelHandlerContext | None":
74
+ return self._contexts.get(name)
75
+
76
+ def names(self) -> list[str]:
77
+ return list(self._contexts.keys())
78
+
79
+ # ------------------------------------------------------------------
80
+ # Firing inbound events (enter through the head)
81
+ # ------------------------------------------------------------------
82
+ async def fire_channel_registered(self) -> None:
83
+ await self._head.handler.channel_registered(self._head)
84
+
85
+ async def fire_channel_active(self) -> None:
86
+ await self._head.handler.channel_active(self._head)
87
+
88
+ async def fire_channel_inactive(self) -> None:
89
+ await self._head.handler.channel_inactive(self._head)
90
+
91
+ async def fire_channel_read(self, msg: Any) -> None:
92
+ await self._head.handler.channel_read(self._head, msg)
93
+
94
+ async def fire_channel_read_complete(self) -> None:
95
+ await self._head.handler.channel_read_complete(self._head)
96
+
97
+ async def fire_exception_caught(self, exc: Exception) -> None:
98
+ await self._head.handler.exception_caught(self._head, exc)
99
+
100
+ # ------------------------------------------------------------------
101
+ # Outbound operations (enter through the tail, travel toward the head)
102
+ # ------------------------------------------------------------------
103
+ async def write(self, msg: Any) -> None:
104
+ await self._tail.handler.write(self._tail, msg)
105
+
106
+ async def close(self) -> None:
107
+ await self._tail.handler.close(self._tail)
108
+
109
+
110
+ class _HeadHandler(ChannelInboundHandler, ChannelOutboundHandler):
111
+ """Sentinel head handler: both inbound AND outbound (marks the start of the chain)."""
112
+
113
+ async def channel_registered(self, ctx: ChannelHandlerContext) -> None:
114
+ await ctx.fire_channel_registered()
115
+
116
+ async def channel_active(self, ctx: ChannelHandlerContext) -> None:
117
+ await ctx.fire_channel_active()
118
+
119
+ async def channel_inactive(self, ctx: ChannelHandlerContext) -> None:
120
+ await ctx.fire_channel_inactive()
121
+
122
+ async def channel_read(self, ctx: ChannelHandlerContext, msg: Any) -> None:
123
+ await ctx.fire_channel_read(msg)
124
+
125
+ async def channel_read_complete(self, ctx: ChannelHandlerContext) -> None:
126
+ await ctx.fire_channel_read_complete()
127
+
128
+ async def write(self, ctx: ChannelHandlerContext, msg: Any) -> None:
129
+ await ctx.channel._do_write(msg)
130
+
131
+ async def close(self, ctx: ChannelHandlerContext) -> None:
132
+ await ctx.channel._do_close()
133
+
134
+ async def exception_caught(self, ctx: ChannelHandlerContext, exc: Exception) -> None:
135
+ # See the note in _TailHandler.exception_caught: with no one
136
+ # else left in the chain, we don't re-raise so we don't kill the
137
+ # task that fired the event.
138
+ pass
139
+
140
+
141
+ class _TailHandler(ChannelInboundHandler, ChannelOutboundHandler):
142
+ """Sentinel tail handler: end of the inbound chain, start of the outbound chain."""
143
+
144
+ async def channel_registered(self, ctx: ChannelHandlerContext) -> None:
145
+ pass
146
+
147
+ async def channel_active(self, ctx: ChannelHandlerContext) -> None:
148
+ pass
149
+
150
+ async def channel_inactive(self, ctx: ChannelHandlerContext) -> None:
151
+ pass
152
+
153
+ async def channel_read(self, ctx: ChannelHandlerContext, msg: Any) -> None:
154
+ # If a message reaches the tail without being consumed, Netty
155
+ # logs it as "unhandled". Here it's simply discarded.
156
+ pass
157
+
158
+ async def channel_read_complete(self, ctx: ChannelHandlerContext) -> None:
159
+ pass
160
+
161
+ async def write(self, ctx: ChannelHandlerContext, msg: Any) -> None:
162
+ await ctx.write(msg)
163
+
164
+ async def close(self, ctx: ChannelHandlerContext) -> None:
165
+ await ctx.close()
166
+
167
+ async def exception_caught(self, ctx: ChannelHandlerContext, exc: Exception) -> None:
168
+ # If the exception reaches the end of the pipeline without any
169
+ # handler having "consumed" it (stopped propagating it), there's
170
+ # no one else left to notify. Netty logs this as a warning; here
171
+ # we don't re-raise so we don't kill the task that fired the
172
+ # event (e.g. ReadTimeoutHandler's watcher).
173
+ pass
@@ -0,0 +1,164 @@
1
+ """
2
+ DatagramChannel: equivalent to io.netty.channel.socket.DatagramChannel.
3
+
4
+ Unlike TCP, UDP has no "connection" or framing — each datagram arrives
5
+ together with the sender's address. The pipeline receives
6
+ (data: bytes, addr: (host, port)) tuples in channel_read, and expects
7
+ the same from write() unless you use connect() to fix a default peer.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ from typing import Any, Optional, Tuple
14
+
15
+ from pynetty.channel.channel_pipeline import ChannelPipeline
16
+ from pynetty.exceptions import ChannelException
17
+
18
+ Address = Tuple[str, int]
19
+
20
+
21
+ class DatagramChannel:
22
+ def __init__(self) -> None:
23
+ self.transport: Optional[asyncio.DatagramTransport] = None
24
+ self.pipeline = ChannelPipeline(self)
25
+ self._active = False
26
+ self._remote_addr: Optional[Address] = None
27
+ self._close_event = asyncio.Event()
28
+
29
+ def _attach_transport(self, transport: asyncio.DatagramTransport) -> None:
30
+ self.transport = transport
31
+
32
+ async def _mark_active(self) -> None:
33
+ self._active = True
34
+ await self.pipeline.fire_channel_registered()
35
+ await self.pipeline.fire_channel_active()
36
+
37
+ async def _mark_inactive(self) -> None:
38
+ if not self._active:
39
+ return
40
+ self._active = False
41
+ await self.pipeline.fire_channel_inactive()
42
+ self._close_event.set()
43
+
44
+ async def _fire_read(self, data: bytes, addr: Address) -> None:
45
+ await self.pipeline.fire_channel_read((data, addr))
46
+ await self.pipeline.fire_channel_read_complete()
47
+
48
+ async def _fire_exception(self, exc: Exception) -> None:
49
+ await self.pipeline.fire_exception_caught(exc)
50
+
51
+ def is_active(self) -> bool:
52
+ return self._active and self.transport is not None and not self.transport.is_closing()
53
+
54
+ def connect(self, addr: Address) -> "DatagramChannel":
55
+ """Fixes a default remote peer (optional; UDP doesn't require a real connection)."""
56
+ self._remote_addr = addr
57
+ return self
58
+
59
+ async def write(self, msg: Any, addr: Optional[Address] = None) -> None:
60
+ """
61
+ Writes through the pipeline. `msg` can be raw bytes or
62
+ (bytes, addr) to specify an explicit destination per send.
63
+ """
64
+ if addr is not None:
65
+ await self.pipeline.write((msg, addr))
66
+ else:
67
+ await self.pipeline.write(msg)
68
+
69
+ async def close(self) -> None:
70
+ await self.pipeline.close()
71
+ if self.transport is not None:
72
+ try:
73
+ await asyncio.wait_for(self.wait_closed(), timeout=5)
74
+ except asyncio.TimeoutError:
75
+ pass
76
+
77
+ async def wait_closed(self) -> None:
78
+ await self._close_event.wait()
79
+
80
+ def local_address(self) -> Optional[Address]:
81
+ if self.transport is None:
82
+ return None
83
+ return self.transport.get_extra_info("sockname")
84
+
85
+ async def _do_write(self, msg: Any) -> None:
86
+ if self.transport is None:
87
+ raise ChannelException("DatagramChannel has no associated transport")
88
+
89
+ if isinstance(msg, tuple) and len(msg) == 2 and isinstance(msg[1], tuple):
90
+ data, addr = msg
91
+ else:
92
+ data = msg
93
+ addr = self._remote_addr
94
+
95
+ if addr is None:
96
+ raise ChannelException(
97
+ "No destination address: use connect(addr) or write(msg, addr=...)"
98
+ )
99
+
100
+ payload = data if isinstance(data, (bytes, bytearray)) else data.to_bytes()
101
+ self.transport.sendto(bytes(payload), addr)
102
+
103
+ async def _do_close(self) -> None:
104
+ if self.transport is not None and not self.transport.is_closing():
105
+ self.transport.close()
106
+
107
+ def __repr__(self) -> str:
108
+ return f"DatagramChannel(local={self.local_address()}, active={self.is_active()})"
109
+
110
+
111
+ class _DatagramProtocolAdapter(asyncio.DatagramProtocol):
112
+ def __init__(self, channel: DatagramChannel) -> None:
113
+ self.channel = channel
114
+ self._loop = asyncio.get_event_loop()
115
+
116
+ def connection_made(self, transport: asyncio.DatagramTransport) -> None:
117
+ self.channel._attach_transport(transport)
118
+ self._loop.create_task(self._safe(self.channel._mark_active()))
119
+
120
+ def datagram_received(self, data: bytes, addr: Address) -> None:
121
+ self._loop.create_task(self._safe(self.channel._fire_read(data, addr)))
122
+
123
+ def error_received(self, exc: Exception) -> None:
124
+ self._loop.create_task(self._safe(self.channel._fire_exception(exc)))
125
+
126
+ def connection_lost(self, exc: Optional[Exception]) -> None:
127
+ if exc is not None:
128
+ self._loop.create_task(self._safe(self.channel._fire_exception(exc)))
129
+ self._loop.create_task(self._safe(self.channel._mark_inactive()))
130
+
131
+ async def _safe(self, coro) -> None:
132
+ try:
133
+ await coro
134
+ except Exception as exc:
135
+ try:
136
+ await self.channel._fire_exception(exc)
137
+ except Exception:
138
+ pass
139
+
140
+
141
+ class DatagramBootstrap:
142
+ """Equivalent to a Bootstrap configured with NioDatagramChannel in Netty."""
143
+
144
+ def __init__(self) -> None:
145
+ self._initializer = None
146
+
147
+ def handler(self, initializer) -> "DatagramBootstrap":
148
+ self._initializer = initializer
149
+ return self
150
+
151
+ async def bind(self, host: str = "0.0.0.0", port: int = 0) -> DatagramChannel:
152
+ if self._initializer is None:
153
+ raise ChannelException("You must call handler(...) before bind()")
154
+
155
+ loop = asyncio.get_event_loop()
156
+ channel = DatagramChannel()
157
+ result = self._initializer(channel)
158
+ if asyncio.iscoroutine(result):
159
+ await result
160
+
161
+ await loop.create_datagram_endpoint(
162
+ lambda: _DatagramProtocolAdapter(channel), local_addr=(host, port)
163
+ )
164
+ return channel