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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -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