Pyjoyment 0.1.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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/IOLoop/Datagram.py
ADDED
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.IOLoop.Datagram - Non-blocking datagram socket
|
|
3
|
+
===================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.IOLoop.Datagram import Datagram
|
|
7
|
+
|
|
8
|
+
# Echo server
|
|
9
|
+
server = Datagram()
|
|
10
|
+
server.on('message', lambda server, data, peer: server.send(b'Echo: ' + data, peer))
|
|
11
|
+
server.bind(address='127.0.0.1', port=9999)
|
|
12
|
+
|
|
13
|
+
# Or import the module, as in Google Python Style Guide
|
|
14
|
+
|
|
15
|
+
import Pyjo.IOLoop.Datagram
|
|
16
|
+
|
|
17
|
+
# Echo server
|
|
18
|
+
server = Pyjo.IOLoop.Datagram.new()
|
|
19
|
+
server.on('message', lambda server, data, peer: server.send(b'Echo: ' + data, peer))
|
|
20
|
+
server.bind(address='127.0.0.1', port=9999)
|
|
21
|
+
|
|
22
|
+
# Client
|
|
23
|
+
client = Pyjo.IOLoop.Datagram.new()
|
|
24
|
+
client.on('message', lambda client, data, peer: print(data))
|
|
25
|
+
client.on('error', lambda client, err: print(err))
|
|
26
|
+
client.send(b'Hello!', ('127.0.0.1', 9999))
|
|
27
|
+
|
|
28
|
+
# Start reactor if necessary
|
|
29
|
+
if not client.reactor.is_running:
|
|
30
|
+
client.reactor.start()
|
|
31
|
+
|
|
32
|
+
:mod:`Pyjo.IOLoop.Datagram` is a container for datagram sockets used by
|
|
33
|
+
:mod:`Pyjo.IOLoop`: UDP over IPv4 and IPv6, and UNIX domain datagram sockets.
|
|
34
|
+
Every :meth:`send` is one datagram, and every ``message`` event is one
|
|
35
|
+
datagram that has arrived.
|
|
36
|
+
|
|
37
|
+
A socket that is neither bound with :meth:`bind` nor connected with
|
|
38
|
+
:meth:`connect` is bound to a random port by the first :meth:`send`, as the
|
|
39
|
+
family of the address of the peer needs it.
|
|
40
|
+
|
|
41
|
+
The end of a ``with`` block closes the socket.
|
|
42
|
+
|
|
43
|
+
Events
|
|
44
|
+
------
|
|
45
|
+
|
|
46
|
+
:mod:`Pyjo.IOLoop.Datagram` inherits all events from :mod:`Pyjo.EventEmitter`
|
|
47
|
+
and can emit the following new ones.
|
|
48
|
+
|
|
49
|
+
close
|
|
50
|
+
~~~~~
|
|
51
|
+
::
|
|
52
|
+
|
|
53
|
+
datagram.on('close', lambda datagram: ...)
|
|
54
|
+
|
|
55
|
+
Emitted if the socket gets closed.
|
|
56
|
+
|
|
57
|
+
connect
|
|
58
|
+
~~~~~~~
|
|
59
|
+
::
|
|
60
|
+
|
|
61
|
+
datagram.on('connect', lambda datagram: ...)
|
|
62
|
+
|
|
63
|
+
Emitted once the socket has been connected with :meth:`connect`.
|
|
64
|
+
|
|
65
|
+
error
|
|
66
|
+
~~~~~
|
|
67
|
+
::
|
|
68
|
+
|
|
69
|
+
datagram.on('error', lambda datagram, err: ...)
|
|
70
|
+
|
|
71
|
+
Emitted if an error occurs on the socket, fatal if unhandled. The error is the
|
|
72
|
+
:class:`OSError` of the handle, as a datagram that cannot be sent or the
|
|
73
|
+
refusal of a peer that a connected socket gets, or a message for a name that
|
|
74
|
+
cannot be resolved. The socket stays open.
|
|
75
|
+
|
|
76
|
+
message
|
|
77
|
+
~~~~~~~
|
|
78
|
+
::
|
|
79
|
+
|
|
80
|
+
datagram.on('message', lambda datagram, data, peer: ...)
|
|
81
|
+
|
|
82
|
+
Emitted for every datagram that arrives, with the address of its sender, as
|
|
83
|
+
:meth:`socket.socket.recvfrom` gives it, which :meth:`send` takes as the
|
|
84
|
+
address of a reply.
|
|
85
|
+
|
|
86
|
+
See also
|
|
87
|
+
--------
|
|
88
|
+
|
|
89
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
90
|
+
|
|
91
|
+
Inspired by `dgram <https://nodejs.org/api/dgram.html>`_ of Node.js, as
|
|
92
|
+
Mojolicious has no datagram sockets.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
import Pyjo.EventEmitter
|
|
96
|
+
import Pyjo.IOLoop
|
|
97
|
+
import Pyjo.IOLoop.Client
|
|
98
|
+
import Pyjo.Reactor
|
|
99
|
+
|
|
100
|
+
import atexit
|
|
101
|
+
import builtins
|
|
102
|
+
import collections
|
|
103
|
+
import errno
|
|
104
|
+
import os
|
|
105
|
+
import socket
|
|
106
|
+
import stat
|
|
107
|
+
import struct
|
|
108
|
+
import sys
|
|
109
|
+
import weakref
|
|
110
|
+
|
|
111
|
+
from collections.abc import AsyncIterator, Callable
|
|
112
|
+
from dataclasses import dataclass
|
|
113
|
+
from types import TracebackType
|
|
114
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if TYPE_CHECKING:
|
|
118
|
+
import Pyjo.Promise
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# The address of a peer: a tuple as recvfrom gives it, or the path of a UNIX domain socket
|
|
122
|
+
type Peer = tuple[Any, ...] | str
|
|
123
|
+
|
|
124
|
+
type Anything = builtins.object
|
|
125
|
+
|
|
126
|
+
# A callback of a send gets the datagram socket once the datagram has been sent
|
|
127
|
+
type SendCallback = Callable[[Any], Anything]
|
|
128
|
+
|
|
129
|
+
# The largest datagram that UDP carries
|
|
130
|
+
_SIZE = 65535
|
|
131
|
+
|
|
132
|
+
# Python has no UNIX domain sockets on Windows
|
|
133
|
+
_NO_UNIX = 'UNIX domain sockets are not supported'
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class _Datagram:
|
|
138
|
+
"""A datagram waiting to be sent, and the name of its peer while it is being resolved."""
|
|
139
|
+
|
|
140
|
+
data: bytes
|
|
141
|
+
cb: SendCallback | None
|
|
142
|
+
failed: Callable[[Any], Anything] | None = None
|
|
143
|
+
family: int = 0
|
|
144
|
+
sockaddr: Any = None
|
|
145
|
+
resolver: Any = None
|
|
146
|
+
error: str | None = None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Pyjo_IOLoop_Datagram(Pyjo.EventEmitter.object):
|
|
150
|
+
"""
|
|
151
|
+
:mod:`Pyjo.IOLoop.Datagram` inherits all methods from
|
|
152
|
+
:mod:`Pyjo.EventEmitter` and implements the following new ones.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, handle: socket.socket | None = None) -> None:
|
|
156
|
+
"""::
|
|
157
|
+
|
|
158
|
+
datagram = Pyjo.IOLoop.Datagram.new()
|
|
159
|
+
datagram = Pyjo.IOLoop.Datagram.new(handle)
|
|
160
|
+
|
|
161
|
+
Construct a new :mod:`Pyjo.IOLoop.Datagram` object, with a datagram
|
|
162
|
+
socket that is already bound or connected, if given.
|
|
163
|
+
"""
|
|
164
|
+
super().__init__()
|
|
165
|
+
|
|
166
|
+
self._connecting: Any = None
|
|
167
|
+
self._graceful = False
|
|
168
|
+
self._handle = handle
|
|
169
|
+
self._ended = False
|
|
170
|
+
self._paused = False
|
|
171
|
+
self._queue: collections.deque[_Datagram] = collections.deque()
|
|
172
|
+
self._reactor: weakref.ref[Pyjo.Reactor.object] | None = None
|
|
173
|
+
|
|
174
|
+
if handle is not None:
|
|
175
|
+
handle.setblocking(False)
|
|
176
|
+
_DATAGRAMS.add(self)
|
|
177
|
+
|
|
178
|
+
def __del__(self) -> None:
|
|
179
|
+
# Module globals may be gone at the end of the interpreter
|
|
180
|
+
try:
|
|
181
|
+
if not sys.is_finalizing() and getattr(self, '_handle', None) is not None:
|
|
182
|
+
reactor = self._reactor() if self._reactor is not None else None
|
|
183
|
+
Pyjo.Reactor._finalize(self.close, reactor) # pyright: ignore[reportPrivateUsage]
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
# Not in Mojolicious: a context manager, which closes the socket where the block ends, as PyPy frees the object only
|
|
188
|
+
# when its garbage collector runs
|
|
189
|
+
def __enter__(self) -> Self:
|
|
190
|
+
return self
|
|
191
|
+
|
|
192
|
+
def __exit__(
|
|
193
|
+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
|
194
|
+
) -> None:
|
|
195
|
+
self.close()
|
|
196
|
+
|
|
197
|
+
def __aiter__(self) -> AsyncIterator[tuple[bytes, Any]]:
|
|
198
|
+
"""::
|
|
199
|
+
|
|
200
|
+
async for data, peer in datagram:
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
Same as :meth:`iterate`.
|
|
204
|
+
"""
|
|
205
|
+
return self.iterate()
|
|
206
|
+
|
|
207
|
+
@property
|
|
208
|
+
def reactor(self) -> Pyjo.Reactor.object:
|
|
209
|
+
"""::
|
|
210
|
+
|
|
211
|
+
reactor = datagram.reactor
|
|
212
|
+
datagram.reactor = Pyjo.Reactor.Poll.new()
|
|
213
|
+
|
|
214
|
+
Low-level event reactor, defaults to the ``reactor`` attribute value of
|
|
215
|
+
the global :mod:`Pyjo.IOLoop` singleton. Note that this attribute is
|
|
216
|
+
weakened.
|
|
217
|
+
"""
|
|
218
|
+
if self._reactor is None:
|
|
219
|
+
self._reactor = weakref.ref(Pyjo.IOLoop.singleton.reactor)
|
|
220
|
+
if (reactor := self._reactor()) is None:
|
|
221
|
+
raise ReferenceError('weakly-referenced object no longer exists')
|
|
222
|
+
return reactor
|
|
223
|
+
|
|
224
|
+
@reactor.setter
|
|
225
|
+
def reactor(self, value: Pyjo.Reactor.object) -> None:
|
|
226
|
+
self._reactor = weakref.ref(value)
|
|
227
|
+
|
|
228
|
+
def add_membership(self, group: str, interface: str | int | None = None) -> Self:
|
|
229
|
+
"""::
|
|
230
|
+
|
|
231
|
+
datagram = datagram.add_membership('224.0.0.114')
|
|
232
|
+
datagram = datagram.add_membership('224.0.0.114', '192.168.1.1')
|
|
233
|
+
datagram = datagram.add_membership('ff02::114', 'eth0')
|
|
234
|
+
|
|
235
|
+
Join a multicast group, on the interface with the address for IPv4,
|
|
236
|
+
or the name or the index for IPv6, or on the one that the system
|
|
237
|
+
chooses.
|
|
238
|
+
"""
|
|
239
|
+
self._membership(group, interface, True)
|
|
240
|
+
return self
|
|
241
|
+
|
|
242
|
+
def bind(self, **options: Any) -> Self:
|
|
243
|
+
"""::
|
|
244
|
+
|
|
245
|
+
datagram = datagram.bind(port=9999)
|
|
246
|
+
|
|
247
|
+
Bind the socket to a local address, and start receiving datagrams.
|
|
248
|
+
|
|
249
|
+
These options are currently available:
|
|
250
|
+
|
|
251
|
+
address
|
|
252
|
+
::
|
|
253
|
+
|
|
254
|
+
address='127.0.0.1'
|
|
255
|
+
|
|
256
|
+
Local address to bind to, defaults to ``0.0.0.0``.
|
|
257
|
+
|
|
258
|
+
ipv6_only
|
|
259
|
+
::
|
|
260
|
+
|
|
261
|
+
ipv6_only=True
|
|
262
|
+
|
|
263
|
+
Receive only IPv6 datagrams on an IPv6 address, with the
|
|
264
|
+
``IPV6_V6ONLY`` socket option.
|
|
265
|
+
|
|
266
|
+
path
|
|
267
|
+
::
|
|
268
|
+
|
|
269
|
+
path='/tmp/myapp.sock'
|
|
270
|
+
|
|
271
|
+
Path for UNIX domain datagram socket to bind to.
|
|
272
|
+
|
|
273
|
+
port
|
|
274
|
+
::
|
|
275
|
+
|
|
276
|
+
port=9999
|
|
277
|
+
|
|
278
|
+
Port to bind to, defaults to a random port.
|
|
279
|
+
|
|
280
|
+
reuse
|
|
281
|
+
::
|
|
282
|
+
|
|
283
|
+
reuse=True
|
|
284
|
+
|
|
285
|
+
Allow multiple sockets to use the same port with the
|
|
286
|
+
``SO_REUSEPORT`` socket option, or ``SO_REUSEADDR`` on Windows,
|
|
287
|
+
which has no ``SO_REUSEPORT``.
|
|
288
|
+
"""
|
|
289
|
+
path = options.get('path')
|
|
290
|
+
created: socket.socket | None = None
|
|
291
|
+
try:
|
|
292
|
+
# UNIX domain socket
|
|
293
|
+
if path:
|
|
294
|
+
if os.path.exists(path) and stat.S_ISSOCK(os.stat(path).st_mode):
|
|
295
|
+
os.remove(path)
|
|
296
|
+
if sys.platform == 'win32':
|
|
297
|
+
raise OSError(errno.EAFNOSUPPORT, _NO_UNIX)
|
|
298
|
+
handle = created = self._handle or socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
|
299
|
+
handle.bind(path)
|
|
300
|
+
|
|
301
|
+
# IP socket
|
|
302
|
+
else:
|
|
303
|
+
host = str(options.get('address') or '0.0.0.0').replace('[', '').replace(']', '')
|
|
304
|
+
family, _, _, _, sockaddr = socket.getaddrinfo(
|
|
305
|
+
host, options.get('port') or 0, type=socket.SOCK_DGRAM, flags=socket.AI_PASSIVE
|
|
306
|
+
)[0]
|
|
307
|
+
handle = created = self._handle or socket.socket(family, socket.SOCK_DGRAM)
|
|
308
|
+
# SO_REUSEADDR on Windows, which has no SO_REUSEPORT, and lets several sockets of datagrams have the
|
|
309
|
+
# same port there, as reuseAddr of dgram of Node.js does
|
|
310
|
+
if options.get('reuse'):
|
|
311
|
+
if sys.platform == 'win32':
|
|
312
|
+
handle.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
313
|
+
else:
|
|
314
|
+
handle.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
315
|
+
if options.get('ipv6_only') and family == socket.AF_INET6:
|
|
316
|
+
handle.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1)
|
|
317
|
+
handle.bind(sockaddr)
|
|
318
|
+
except OSError as e:
|
|
319
|
+
# The socket that cannot be bound is closed, as Pyjo.IOLoop.Server does with one that cannot listen
|
|
320
|
+
if created is not None and created is not self._handle:
|
|
321
|
+
created.close()
|
|
322
|
+
raise _error(e, "Can't bind datagram socket") from e
|
|
323
|
+
|
|
324
|
+
handle.setblocking(False)
|
|
325
|
+
self._handle = handle
|
|
326
|
+
self.start()
|
|
327
|
+
return self
|
|
328
|
+
|
|
329
|
+
@property
|
|
330
|
+
def broadcast(self) -> bool:
|
|
331
|
+
"""::
|
|
332
|
+
|
|
333
|
+
boolean = datagram.broadcast
|
|
334
|
+
datagram.broadcast = True
|
|
335
|
+
|
|
336
|
+
Allow datagrams to a broadcast address, with the ``SO_BROADCAST``
|
|
337
|
+
socket option.
|
|
338
|
+
"""
|
|
339
|
+
return bool(self._socket().getsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST))
|
|
340
|
+
|
|
341
|
+
@broadcast.setter
|
|
342
|
+
def broadcast(self, value: bool) -> None:
|
|
343
|
+
self._socket().setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, int(value))
|
|
344
|
+
|
|
345
|
+
@property
|
|
346
|
+
def bytes_waiting(self) -> int:
|
|
347
|
+
"""::
|
|
348
|
+
|
|
349
|
+
num = datagram.bytes_waiting
|
|
350
|
+
|
|
351
|
+
Number of bytes of the datagrams that have been enqueued with
|
|
352
|
+
:meth:`send` and are waiting to be sent.
|
|
353
|
+
"""
|
|
354
|
+
return sum(len(d.data) for d in self._queue)
|
|
355
|
+
|
|
356
|
+
def close(self) -> None:
|
|
357
|
+
"""::
|
|
358
|
+
|
|
359
|
+
datagram.close()
|
|
360
|
+
|
|
361
|
+
Close socket immediately, the datagrams that wait to be sent are
|
|
362
|
+
dropped.
|
|
363
|
+
"""
|
|
364
|
+
reactor = self._reactor() if self._reactor is not None else Pyjo.IOLoop.singleton.reactor
|
|
365
|
+
for d in self._queue:
|
|
366
|
+
self._cancel(d.resolver)
|
|
367
|
+
self._queue.clear()
|
|
368
|
+
if self._connecting is not None and not isinstance(self._connecting, bool):
|
|
369
|
+
self._cancel(self._connecting)
|
|
370
|
+
self._connecting = None
|
|
371
|
+
if (handle := self._handle) is None:
|
|
372
|
+
return
|
|
373
|
+
self._handle = None
|
|
374
|
+
# Not in Mojolicious: the handle of a socket whose reactor is gone is closed, as Perl frees it with the socket
|
|
375
|
+
if reactor is None:
|
|
376
|
+
handle.close()
|
|
377
|
+
return
|
|
378
|
+
reactor.remove(handle)
|
|
379
|
+
handle.close()
|
|
380
|
+
self._ended = True
|
|
381
|
+
self.emit('close')
|
|
382
|
+
|
|
383
|
+
def close_gracefully(self) -> None:
|
|
384
|
+
"""::
|
|
385
|
+
|
|
386
|
+
datagram.close_gracefully()
|
|
387
|
+
|
|
388
|
+
Close socket once the datagrams that wait have been sent.
|
|
389
|
+
"""
|
|
390
|
+
if self._queue:
|
|
391
|
+
self._graceful = True
|
|
392
|
+
else:
|
|
393
|
+
self.close()
|
|
394
|
+
|
|
395
|
+
def close_gracefully_p(self) -> 'Pyjo.Promise.object':
|
|
396
|
+
"""::
|
|
397
|
+
|
|
398
|
+
promise = datagram.close_gracefully_p()
|
|
399
|
+
|
|
400
|
+
Same as :meth:`close_gracefully`, but returns a :mod:`Pyjo.Promise`
|
|
401
|
+
object, which is fulfilled once the socket has been closed, also when it
|
|
402
|
+
is closed already, or has never been opened. ::
|
|
403
|
+
|
|
404
|
+
await datagram.send_p(b'Bye!')
|
|
405
|
+
await datagram.close_gracefully_p()
|
|
406
|
+
"""
|
|
407
|
+
import Pyjo.Promise
|
|
408
|
+
|
|
409
|
+
if self._handle is None:
|
|
410
|
+
return Pyjo.Promise.resolve()
|
|
411
|
+
promise = self.once_p('close')
|
|
412
|
+
self.close_gracefully()
|
|
413
|
+
return promise
|
|
414
|
+
|
|
415
|
+
def connect(self, **options: Any) -> Self:
|
|
416
|
+
"""::
|
|
417
|
+
|
|
418
|
+
datagram = datagram.connect(address='127.0.0.1', port=9999)
|
|
419
|
+
|
|
420
|
+
Connect the socket to a peer, so that :meth:`send` needs no address and
|
|
421
|
+
only the datagrams of the peer arrive, emitting ``connect`` once it is
|
|
422
|
+
connected, or ``error`` if it cannot be. A name is resolved without
|
|
423
|
+
blocking.
|
|
424
|
+
|
|
425
|
+
These options are currently available:
|
|
426
|
+
|
|
427
|
+
address
|
|
428
|
+
::
|
|
429
|
+
|
|
430
|
+
address='mojolicious.org'
|
|
431
|
+
|
|
432
|
+
Address or host name of the peer to connect to, defaults to
|
|
433
|
+
``127.0.0.1``.
|
|
434
|
+
|
|
435
|
+
path
|
|
436
|
+
::
|
|
437
|
+
|
|
438
|
+
path='/tmp/myapp.sock'
|
|
439
|
+
|
|
440
|
+
Path of UNIX domain datagram socket to connect to.
|
|
441
|
+
|
|
442
|
+
port
|
|
443
|
+
::
|
|
444
|
+
|
|
445
|
+
port=9999
|
|
446
|
+
|
|
447
|
+
Port to connect to.
|
|
448
|
+
"""
|
|
449
|
+
ref = weakref.ref(self)
|
|
450
|
+
|
|
451
|
+
def connected(family: int, sockaddr: Any, err: str | None) -> None:
|
|
452
|
+
if (datagram := ref()) is not None:
|
|
453
|
+
datagram._connected(family, sockaddr, err)
|
|
454
|
+
|
|
455
|
+
self._connecting = True
|
|
456
|
+
if path := options.get('path'):
|
|
457
|
+
self._connecting = self._resolve(path, 0, connected, defer=True, unix=True)
|
|
458
|
+
else:
|
|
459
|
+
host = str(options.get('address') or '127.0.0.1').replace('[', '').replace(']', '')
|
|
460
|
+
self._connecting = self._resolve(host, int(options.get('port') or 0), connected, defer=True)
|
|
461
|
+
return self
|
|
462
|
+
|
|
463
|
+
def drop_membership(self, group: str, interface: str | int | None = None) -> Self:
|
|
464
|
+
"""::
|
|
465
|
+
|
|
466
|
+
datagram = datagram.drop_membership('224.0.0.114')
|
|
467
|
+
|
|
468
|
+
Leave a multicast group, with the same arguments as
|
|
469
|
+
:meth:`add_membership`.
|
|
470
|
+
"""
|
|
471
|
+
self._membership(group, interface, False)
|
|
472
|
+
return self
|
|
473
|
+
|
|
474
|
+
@property
|
|
475
|
+
def handle(self) -> socket.socket | None:
|
|
476
|
+
"""::
|
|
477
|
+
|
|
478
|
+
handle = datagram.handle
|
|
479
|
+
|
|
480
|
+
Get handle for socket, a :class:`socket.socket` object, or ``None``
|
|
481
|
+
before it is bound.
|
|
482
|
+
"""
|
|
483
|
+
return self._handle
|
|
484
|
+
|
|
485
|
+
@property
|
|
486
|
+
def multicast_loopback(self) -> bool:
|
|
487
|
+
"""::
|
|
488
|
+
|
|
489
|
+
boolean = datagram.multicast_loopback
|
|
490
|
+
datagram.multicast_loopback = False
|
|
491
|
+
|
|
492
|
+
Receive the multicast datagrams that the socket sends, with the
|
|
493
|
+
``IP_MULTICAST_LOOP`` or ``IPV6_MULTICAST_LOOP`` socket option.
|
|
494
|
+
"""
|
|
495
|
+
level, name = self._option(socket.IP_MULTICAST_LOOP, socket.IPV6_MULTICAST_LOOP)
|
|
496
|
+
return bool(self._socket().getsockopt(level, name))
|
|
497
|
+
|
|
498
|
+
@multicast_loopback.setter
|
|
499
|
+
def multicast_loopback(self, value: bool) -> None:
|
|
500
|
+
level, name = self._option(socket.IP_MULTICAST_LOOP, socket.IPV6_MULTICAST_LOOP)
|
|
501
|
+
self._socket().setsockopt(level, name, int(value))
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def multicast_ttl(self) -> int:
|
|
505
|
+
"""::
|
|
506
|
+
|
|
507
|
+
ttl = datagram.multicast_ttl
|
|
508
|
+
datagram.multicast_ttl = 64
|
|
509
|
+
|
|
510
|
+
Number of hops of multicast datagrams, with the ``IP_MULTICAST_TTL`` or
|
|
511
|
+
``IPV6_MULTICAST_HOPS`` socket option.
|
|
512
|
+
"""
|
|
513
|
+
level, name = self._option(socket.IP_MULTICAST_TTL, socket.IPV6_MULTICAST_HOPS)
|
|
514
|
+
return self._socket().getsockopt(level, name)
|
|
515
|
+
|
|
516
|
+
@multicast_ttl.setter
|
|
517
|
+
def multicast_ttl(self, value: int) -> None:
|
|
518
|
+
level, name = self._option(socket.IP_MULTICAST_TTL, socket.IPV6_MULTICAST_HOPS)
|
|
519
|
+
self._socket().setsockopt(level, name, value)
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def port(self) -> int | None:
|
|
523
|
+
"""::
|
|
524
|
+
|
|
525
|
+
port = datagram.port
|
|
526
|
+
|
|
527
|
+
Get port the socket is bound to, ``None`` for a UNIX domain socket or
|
|
528
|
+
before it is bound.
|
|
529
|
+
"""
|
|
530
|
+
if (handle := self._handle) is None or handle.family not in (socket.AF_INET, socket.AF_INET6):
|
|
531
|
+
return None
|
|
532
|
+
return handle.getsockname()[1]
|
|
533
|
+
|
|
534
|
+
def connect_p(self, **options: Any) -> 'Pyjo.Promise.object':
|
|
535
|
+
"""::
|
|
536
|
+
|
|
537
|
+
promise = datagram.connect_p(address='127.0.0.1', port=9999)
|
|
538
|
+
|
|
539
|
+
Same as :meth:`connect`, but returns a :mod:`Pyjo.Promise` object,
|
|
540
|
+
which is fulfilled with the socket once it is connected, and rejected
|
|
541
|
+
with the error of the event ``error`` if it cannot be. The socket keeps
|
|
542
|
+
what arrives for :meth:`recv_p` from then on, also the refusal of the
|
|
543
|
+
peer, as a coroutine that awaits the promise goes on later. ::
|
|
544
|
+
|
|
545
|
+
datagram = await Pyjo.IOLoop.Datagram.new().connect_p(address='127.0.0.1', port=9999)
|
|
546
|
+
await datagram.send_p(b'Hello!')
|
|
547
|
+
"""
|
|
548
|
+
|
|
549
|
+
def connected(*args: Any) -> Self:
|
|
550
|
+
self._messages()
|
|
551
|
+
return self
|
|
552
|
+
|
|
553
|
+
promise = self.once_p('connect').then(connected)
|
|
554
|
+
self.connect(**options)
|
|
555
|
+
return promise
|
|
556
|
+
|
|
557
|
+
def iterate(self) -> AsyncIterator[tuple[bytes, Any]]:
|
|
558
|
+
"""::
|
|
559
|
+
|
|
560
|
+
async for data, peer in datagram.iterate():
|
|
561
|
+
...
|
|
562
|
+
|
|
563
|
+
The datagrams that arrive as an asynchronous iterator, with the address
|
|
564
|
+
of their sender, until the socket is closed, which :meth:`recv_p`
|
|
565
|
+
takes. ::
|
|
566
|
+
|
|
567
|
+
# Echo server, one datagram after another
|
|
568
|
+
async for data, peer in datagram:
|
|
569
|
+
await datagram.send_p(b'Echo: ' + data, peer)
|
|
570
|
+
"""
|
|
571
|
+
return self._messages().iterate()
|
|
572
|
+
|
|
573
|
+
def once_p(self, name: str) -> 'Pyjo.Promise.object':
|
|
574
|
+
"""::
|
|
575
|
+
|
|
576
|
+
promise = datagram.once_p('close')
|
|
577
|
+
|
|
578
|
+
Same as :meth:`~Pyjo.EventEmitter.Pyjo_EventEmitter.once_p` of
|
|
579
|
+
:mod:`Pyjo.EventEmitter`, but fulfilled at once for the event
|
|
580
|
+
``close`` of a socket that is closed already, as it comes no more.
|
|
581
|
+
"""
|
|
582
|
+
import Pyjo.Promise
|
|
583
|
+
|
|
584
|
+
if name == 'close' and self._ended:
|
|
585
|
+
return Pyjo.Promise.resolve()
|
|
586
|
+
return super().once_p(name)
|
|
587
|
+
|
|
588
|
+
def recv_p(self, timeout: float | None = None) -> 'Pyjo.Promise.object':
|
|
589
|
+
"""::
|
|
590
|
+
|
|
591
|
+
promise = datagram.recv_p()
|
|
592
|
+
promise = datagram.recv_p(timeout=5)
|
|
593
|
+
|
|
594
|
+
Return a :mod:`Pyjo.Promise` object, which is fulfilled with the next
|
|
595
|
+
datagram and the address of its sender, as the event ``message`` gives
|
|
596
|
+
them, or ``None`` once the socket is closed, also when it is closed
|
|
597
|
+
already, and rejected with the error of the event ``error``, after
|
|
598
|
+
which the socket stays open and the next promise waits again, or a
|
|
599
|
+
:class:`TimeoutError` when no datagram has arrived for ``timeout``
|
|
600
|
+
seconds. The socket reads only while a promise waits, and the datagrams
|
|
601
|
+
that arrive before wait in the socket, as the streams do. ::
|
|
602
|
+
|
|
603
|
+
if (received := await datagram.recv_p()) is not None:
|
|
604
|
+
data, peer = received
|
|
605
|
+
print(f'{data!r} from {peer}')
|
|
606
|
+
"""
|
|
607
|
+
pull = self._messages()
|
|
608
|
+
promise = pull.next_p()
|
|
609
|
+
if timeout is not None and promise in pull.waiting:
|
|
610
|
+
reactor = self.reactor
|
|
611
|
+
|
|
612
|
+
def expired(reactor: Any) -> None:
|
|
613
|
+
pull.cancel(promise, TimeoutError('Receive timeout'))
|
|
614
|
+
|
|
615
|
+
timer = reactor.timer(timeout, expired)
|
|
616
|
+
|
|
617
|
+
def settled(*args: Any) -> None:
|
|
618
|
+
reactor.remove(timer)
|
|
619
|
+
|
|
620
|
+
promise.then(settled, settled)
|
|
621
|
+
return promise
|
|
622
|
+
|
|
623
|
+
def send(self, data: bytes, peer: Peer | None = None, cb: SendCallback | None = None) -> Self:
|
|
624
|
+
"""::
|
|
625
|
+
|
|
626
|
+
datagram = datagram.send(b'Hello!')
|
|
627
|
+
datagram = datagram.send(b'Hello!', ('127.0.0.1', 9999))
|
|
628
|
+
datagram = datagram.send(b'Hello!', ('localhost', 9999), lambda datagram: ...)
|
|
629
|
+
datagram = datagram.send(b'Hello!', '/tmp/myapp.sock')
|
|
630
|
+
|
|
631
|
+
Enqueue a datagram to be sent as soon as possible, to the peer of
|
|
632
|
+
:meth:`connect`, or to the address given to a socket that is not
|
|
633
|
+
connected, whose name is resolved without blocking. The optional callback will be executed once the
|
|
634
|
+
datagram has been sent.
|
|
635
|
+
"""
|
|
636
|
+
return self._send(data, peer, cb)
|
|
637
|
+
|
|
638
|
+
def send_p(self, data: bytes, peer: Peer | None = None) -> 'Pyjo.Promise.object':
|
|
639
|
+
"""::
|
|
640
|
+
|
|
641
|
+
promise = datagram.send_p(b'Hello!')
|
|
642
|
+
promise = datagram.send_p(b'Hello!', ('127.0.0.1', 9999))
|
|
643
|
+
|
|
644
|
+
Same as :meth:`send`, but returns a :mod:`Pyjo.Promise` object, which
|
|
645
|
+
is fulfilled once the datagram has been sent, and rejected with the
|
|
646
|
+
error of the datagram, in the place of the event ``error``, or if the
|
|
647
|
+
socket is closed before, or is closed already. ::
|
|
648
|
+
|
|
649
|
+
await datagram.send_p(b'Hello!', ('127.0.0.1', 9999))
|
|
650
|
+
"""
|
|
651
|
+
import Pyjo.Exception
|
|
652
|
+
import Pyjo.Promise
|
|
653
|
+
|
|
654
|
+
if self._ended:
|
|
655
|
+
return Pyjo.Promise.reject(Pyjo.Exception.new('Socket closed before the datagram has been sent'))
|
|
656
|
+
promise = Pyjo.Promise.new()
|
|
657
|
+
|
|
658
|
+
def closed(datagram: Any) -> None:
|
|
659
|
+
promise.reject(Pyjo.Exception.new('Socket closed before the datagram has been sent'))
|
|
660
|
+
|
|
661
|
+
wrapper = self.once('close', closed)
|
|
662
|
+
|
|
663
|
+
def sent(datagram: Any) -> None:
|
|
664
|
+
self.unsubscribe('close', wrapper)
|
|
665
|
+
promise.resolve()
|
|
666
|
+
|
|
667
|
+
def failed(err: Any) -> None:
|
|
668
|
+
self.unsubscribe('close', wrapper)
|
|
669
|
+
promise.reject(err)
|
|
670
|
+
|
|
671
|
+
self._send(data, peer, sent, failed)
|
|
672
|
+
return self._hold(promise)
|
|
673
|
+
|
|
674
|
+
def start(self) -> None:
|
|
675
|
+
"""::
|
|
676
|
+
|
|
677
|
+
datagram.start()
|
|
678
|
+
|
|
679
|
+
Start or resume watching for new datagrams on the socket.
|
|
680
|
+
"""
|
|
681
|
+
if (handle := self._handle) is None:
|
|
682
|
+
return
|
|
683
|
+
self._paused = False
|
|
684
|
+
ref = weakref.ref(self)
|
|
685
|
+
|
|
686
|
+
def cb(reactor: Any, writable: bool) -> None:
|
|
687
|
+
if (datagram := ref()) is not None:
|
|
688
|
+
datagram._flush() if writable else datagram._read_handle()
|
|
689
|
+
|
|
690
|
+
reactor = self.reactor
|
|
691
|
+
reactor.io(handle, cb)
|
|
692
|
+
reactor.watch(handle, True, bool(self._queue))
|
|
693
|
+
|
|
694
|
+
def stop(self) -> None:
|
|
695
|
+
"""::
|
|
696
|
+
|
|
697
|
+
datagram.stop()
|
|
698
|
+
|
|
699
|
+
Stop watching for new datagrams on the socket.
|
|
700
|
+
"""
|
|
701
|
+
if self._handle is not None and not self._paused:
|
|
702
|
+
self._paused = True
|
|
703
|
+
self.reactor.watch(self._handle, False, bool(self._queue))
|
|
704
|
+
|
|
705
|
+
@property
|
|
706
|
+
def ttl(self) -> int:
|
|
707
|
+
"""::
|
|
708
|
+
|
|
709
|
+
ttl = datagram.ttl
|
|
710
|
+
datagram.ttl = 64
|
|
711
|
+
|
|
712
|
+
Number of hops of unicast datagrams, with the ``IP_TTL`` or
|
|
713
|
+
``IPV6_UNICAST_HOPS`` socket option.
|
|
714
|
+
"""
|
|
715
|
+
level, name = self._option(socket.IP_TTL, socket.IPV6_UNICAST_HOPS)
|
|
716
|
+
return self._socket().getsockopt(level, name)
|
|
717
|
+
|
|
718
|
+
@ttl.setter
|
|
719
|
+
def ttl(self, value: int) -> None:
|
|
720
|
+
level, name = self._option(socket.IP_TTL, socket.IPV6_UNICAST_HOPS)
|
|
721
|
+
self._socket().setsockopt(level, name, value)
|
|
722
|
+
|
|
723
|
+
def _cancel(self, resolver: Any) -> None:
|
|
724
|
+
if resolver is None or isinstance(resolver, bool):
|
|
725
|
+
return
|
|
726
|
+
reactor = self._reactor() if self._reactor is not None else None
|
|
727
|
+
if reactor is not None:
|
|
728
|
+
reactor.remove(resolver.handle)
|
|
729
|
+
resolver.close()
|
|
730
|
+
|
|
731
|
+
# Closed by a callback or a subscriber, which the type checker does not see
|
|
732
|
+
def _closed(self) -> bool:
|
|
733
|
+
return self._handle is None
|
|
734
|
+
|
|
735
|
+
def _connected(self, family: int, sockaddr: Any, err: str | None) -> None:
|
|
736
|
+
# Closed in the meantime
|
|
737
|
+
if self._connecting is None:
|
|
738
|
+
return
|
|
739
|
+
self._connecting = None
|
|
740
|
+
if err is not None:
|
|
741
|
+
self.emit('error', err)
|
|
742
|
+
return
|
|
743
|
+
try:
|
|
744
|
+
if self._handle is None:
|
|
745
|
+
self._handle = socket.socket(family, socket.SOCK_DGRAM)
|
|
746
|
+
self._handle.setblocking(False)
|
|
747
|
+
self._handle.connect(sockaddr)
|
|
748
|
+
except OSError as e:
|
|
749
|
+
self.emit('error', e)
|
|
750
|
+
return
|
|
751
|
+
self.start()
|
|
752
|
+
self.emit('connect')._flush()
|
|
753
|
+
|
|
754
|
+
# The error of a datagram, which its promise gets, or the event error
|
|
755
|
+
def _failed(self, d: '_Datagram', err: Any) -> None:
|
|
756
|
+
if d.failed is not None:
|
|
757
|
+
d.failed(err)
|
|
758
|
+
else:
|
|
759
|
+
self.emit('error', err)
|
|
760
|
+
|
|
761
|
+
def _flush(self) -> None:
|
|
762
|
+
queue = self._queue
|
|
763
|
+
while queue:
|
|
764
|
+
d = queue[0]
|
|
765
|
+
|
|
766
|
+
# The name of the peer is being resolved, or the socket is being connected
|
|
767
|
+
if d.resolver is not None or self._connecting is not None:
|
|
768
|
+
return
|
|
769
|
+
|
|
770
|
+
if d.error is not None:
|
|
771
|
+
queue.popleft()
|
|
772
|
+
self._failed(d, d.error)
|
|
773
|
+
continue
|
|
774
|
+
|
|
775
|
+
# A socket that is not bound yet is bound to a random port
|
|
776
|
+
if (handle := self._handle) is None:
|
|
777
|
+
handle = self._handle = socket.socket(d.family or socket.AF_INET, socket.SOCK_DGRAM)
|
|
778
|
+
handle.setblocking(False)
|
|
779
|
+
self.start()
|
|
780
|
+
|
|
781
|
+
# An address of another family than the socket, which the socket cannot send to
|
|
782
|
+
if d.family and d.family != handle.family:
|
|
783
|
+
queue.popleft()
|
|
784
|
+
self._failed(d, OSError(errno.EAFNOSUPPORT, f'Address {d.sockaddr} is not of the family of the socket'))
|
|
785
|
+
continue
|
|
786
|
+
|
|
787
|
+
try:
|
|
788
|
+
if d.sockaddr is None:
|
|
789
|
+
handle.send(d.data)
|
|
790
|
+
else:
|
|
791
|
+
handle.sendto(d.data, d.sockaddr)
|
|
792
|
+
except (BlockingIOError, InterruptedError):
|
|
793
|
+
self._watch()
|
|
794
|
+
return
|
|
795
|
+
except OSError as e:
|
|
796
|
+
queue.popleft()
|
|
797
|
+
self._failed(d, _refused(e))
|
|
798
|
+
if self._closed():
|
|
799
|
+
return
|
|
800
|
+
continue
|
|
801
|
+
|
|
802
|
+
queue.popleft()
|
|
803
|
+
if d.cb is not None:
|
|
804
|
+
d.cb(self)
|
|
805
|
+
if self._closed():
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
self._watch()
|
|
809
|
+
if self._graceful:
|
|
810
|
+
self.close()
|
|
811
|
+
|
|
812
|
+
# The pull of the datagrams, whose errors leave the socket open, and which a socket that is closed already has ended
|
|
813
|
+
def _messages(self) -> Any:
|
|
814
|
+
pull = self._pull('message', True, value=False, fatal=False)
|
|
815
|
+
if self._ended:
|
|
816
|
+
pull.end()
|
|
817
|
+
return pull
|
|
818
|
+
|
|
819
|
+
def _membership(self, group: str, interface: str | int | None, add: bool) -> None:
|
|
820
|
+
handle = self._socket()
|
|
821
|
+
if ':' in group:
|
|
822
|
+
index = socket.if_nametoindex(interface) if isinstance(interface, str) else interface or 0
|
|
823
|
+
mreq = socket.inet_pton(socket.AF_INET6, group) + struct.pack('@I', index)
|
|
824
|
+
name = socket.IPV6_JOIN_GROUP if add else socket.IPV6_LEAVE_GROUP
|
|
825
|
+
handle.setsockopt(socket.IPPROTO_IPV6, name, mreq)
|
|
826
|
+
else:
|
|
827
|
+
mreq = socket.inet_aton(group) + socket.inet_aton(str(interface or '0.0.0.0'))
|
|
828
|
+
name = socket.IP_ADD_MEMBERSHIP if add else socket.IP_DROP_MEMBERSHIP
|
|
829
|
+
handle.setsockopt(socket.IPPROTO_IP, name, mreq)
|
|
830
|
+
|
|
831
|
+
def _option(self, ipv4: int, ipv6: int) -> tuple[int, int]:
|
|
832
|
+
if self._socket().family == socket.AF_INET6:
|
|
833
|
+
return socket.IPPROTO_IPV6, ipv6
|
|
834
|
+
return socket.IPPROTO_IP, ipv4
|
|
835
|
+
|
|
836
|
+
def _read_handle(self) -> None:
|
|
837
|
+
if (handle := self._handle) is None:
|
|
838
|
+
return
|
|
839
|
+
try:
|
|
840
|
+
data, peer = handle.recvfrom(_SIZE)
|
|
841
|
+
except (BlockingIOError, InterruptedError):
|
|
842
|
+
return
|
|
843
|
+
except OSError as e:
|
|
844
|
+
self.emit('error', _refused(e))
|
|
845
|
+
return
|
|
846
|
+
self.emit('message', data, peer)
|
|
847
|
+
|
|
848
|
+
# The address of a peer, at once for a path or a numeric address, else resolved without blocking unless that is
|
|
849
|
+
# turned off; done gets the family, the address and an error, and the result is what is still being resolved
|
|
850
|
+
def _resolve(
|
|
851
|
+
self,
|
|
852
|
+
host: str,
|
|
853
|
+
port: int,
|
|
854
|
+
done: Callable[[int, Any, str | None], None],
|
|
855
|
+
*,
|
|
856
|
+
defer: bool = False,
|
|
857
|
+
sockaddr: Any = None,
|
|
858
|
+
unix: bool = False,
|
|
859
|
+
) -> Any:
|
|
860
|
+
reactor = self.reactor
|
|
861
|
+
family = self._handle.family if self._handle is not None else 0
|
|
862
|
+
|
|
863
|
+
def finish(family: int, address: Any, err: str | None) -> Any:
|
|
864
|
+
if not defer:
|
|
865
|
+
done(family, address, err)
|
|
866
|
+
return None
|
|
867
|
+
reactor.next_tick(lambda reactor: done(family, address, err))
|
|
868
|
+
return True
|
|
869
|
+
|
|
870
|
+
# UNIX domain socket, which Python has not on Windows
|
|
871
|
+
if unix:
|
|
872
|
+
if sys.platform == 'win32':
|
|
873
|
+
return finish(family, host, _NO_UNIX)
|
|
874
|
+
return finish(socket.AF_UNIX, host, None)
|
|
875
|
+
|
|
876
|
+
# Numeric address, kept as it is with the flow information and the scope of IPv6
|
|
877
|
+
try:
|
|
878
|
+
info = socket.getaddrinfo(host, port, family, socket.SOCK_DGRAM, 0, socket.AI_NUMERICHOST)[0]
|
|
879
|
+
address = tuple(sockaddr) if sockaddr is not None and len(sockaddr) == 4 else info[4]
|
|
880
|
+
return finish(info[0], address, None)
|
|
881
|
+
except OSError:
|
|
882
|
+
pass
|
|
883
|
+
|
|
884
|
+
# Blocking name resolution
|
|
885
|
+
if not Pyjo.IOLoop.Client.NNR:
|
|
886
|
+
try:
|
|
887
|
+
infos = socket.getaddrinfo(host, port, family, socket.SOCK_DGRAM)
|
|
888
|
+
except OSError as e:
|
|
889
|
+
return finish(0, None, f"Can't resolve: {e.strerror or e}")
|
|
890
|
+
return finish(*self._pick(infos))
|
|
891
|
+
|
|
892
|
+
# Non-blocking name resolution
|
|
893
|
+
dns = Pyjo.IOLoop.Client._Resolver(host, port, socket.SOCK_DGRAM, family) # pyright: ignore[reportPrivateUsage]
|
|
894
|
+
|
|
895
|
+
ref = weakref.ref(self)
|
|
896
|
+
|
|
897
|
+
def resolved(reactor: Any, writable: bool) -> None:
|
|
898
|
+
reactor.remove(dns.handle)
|
|
899
|
+
try:
|
|
900
|
+
infos = dns.future.result()
|
|
901
|
+
except OSError as e:
|
|
902
|
+
return done(0, None, f"Can't resolve: {e.strerror or e}")
|
|
903
|
+
finally:
|
|
904
|
+
dns.close()
|
|
905
|
+
if (datagram := ref()) is not None:
|
|
906
|
+
done(*datagram._pick(infos))
|
|
907
|
+
|
|
908
|
+
reactor.io(dns.handle, resolved).watch(dns.handle, True, False)
|
|
909
|
+
return dns
|
|
910
|
+
|
|
911
|
+
# The first address of a name, or the first of the family of the socket, which may have been made in the meantime
|
|
912
|
+
def _pick(self, infos: list[Any]) -> tuple[int, Any, str | None]:
|
|
913
|
+
if self._handle is None:
|
|
914
|
+
return infos[0][0], infos[0][4], None
|
|
915
|
+
for info in infos:
|
|
916
|
+
if info[0] == self._handle.family:
|
|
917
|
+
return info[0], info[4], None
|
|
918
|
+
return 0, None, "Can't resolve: no address of the family of the socket"
|
|
919
|
+
|
|
920
|
+
def _send(
|
|
921
|
+
self, data: bytes, peer: Peer | None, cb: SendCallback | None, failed: Callable[[Any], Anything] | None = None
|
|
922
|
+
) -> Self:
|
|
923
|
+
d = _Datagram(data, cb, failed)
|
|
924
|
+
self._queue.append(d)
|
|
925
|
+
|
|
926
|
+
if peer is not None:
|
|
927
|
+
ref = weakref.ref(self)
|
|
928
|
+
|
|
929
|
+
def resolved(family: int, sockaddr: Any, err: str | None) -> None:
|
|
930
|
+
d.resolver, d.family, d.sockaddr, d.error = None, family, sockaddr, err
|
|
931
|
+
if (datagram := ref()) is not None:
|
|
932
|
+
datagram._flush()
|
|
933
|
+
|
|
934
|
+
if isinstance(peer, str):
|
|
935
|
+
d.resolver = self._resolve(peer, 0, resolved, unix=True)
|
|
936
|
+
else:
|
|
937
|
+
d.resolver = self._resolve(str(peer[0]), int(peer[1]), resolved, sockaddr=peer)
|
|
938
|
+
|
|
939
|
+
self._flush()
|
|
940
|
+
return self
|
|
941
|
+
|
|
942
|
+
def _socket(self) -> socket.socket:
|
|
943
|
+
if (handle := self._handle) is None:
|
|
944
|
+
raise OSError(errno.EBADF, 'Datagram socket is not bound')
|
|
945
|
+
return handle
|
|
946
|
+
|
|
947
|
+
def _watch(self) -> None:
|
|
948
|
+
if self._handle is not None:
|
|
949
|
+
self.reactor.watch(self._handle, not self._paused, bool(self._queue))
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
# The datagram sockets that exist, closed at the end of the interpreter, as Perl closes the handles that are left in its
|
|
953
|
+
# global destruction and Python warns that they were not closed
|
|
954
|
+
_DATAGRAMS: 'weakref.WeakSet[Pyjo_IOLoop_Datagram]' = weakref.WeakSet()
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@atexit.register
|
|
958
|
+
def _cleanup() -> None:
|
|
959
|
+
for datagram in list(_DATAGRAMS):
|
|
960
|
+
try:
|
|
961
|
+
datagram.close()
|
|
962
|
+
except Exception:
|
|
963
|
+
pass
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def _error(e: OSError, message: str) -> OSError:
|
|
967
|
+
error = type(e)(f'{message}: {e.strerror}' if e.strerror else message)
|
|
968
|
+
error.errno = e.errno
|
|
969
|
+
return error
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
# Windows reports a datagram that a port refused as a reset of the connection (WSAECONNRESET), where other systems
|
|
973
|
+
# report it as refused
|
|
974
|
+
def _refused(e: OSError) -> OSError:
|
|
975
|
+
if sys.platform == 'win32' and e.winerror == 10054:
|
|
976
|
+
return ConnectionRefusedError(errno.ECONNREFUSED, 'Connection refused')
|
|
977
|
+
return e
|
|
978
|
+
|
|
979
|
+
|
|
980
|
+
new = Pyjo_IOLoop_Datagram.new
|
|
981
|
+
object = Pyjo_IOLoop_Datagram
|
|
982
|
+
Datagram = Pyjo_IOLoop_Datagram
|