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,394 @@
1
+ """
2
+ Pyjo.Server.Prethread - Pre-threading non-blocking I/O HTTP and WebSocket server
3
+ ================================================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Prethread import Prethread
7
+
8
+ prethread = Prethread(listen=['http://*:8080'])
9
+
10
+
11
+ @prethread.unsubscribe('request').on('request')
12
+ def request(prethread, tx):
13
+ # Request
14
+ method = tx.req.method
15
+ path = tx.req.url.path
16
+
17
+ # Response
18
+ tx.res.code = 200
19
+ tx.res.headers.content_type = 'text/plain'
20
+ tx.res.body = f'{method} request for {path}!'.encode()
21
+
22
+ # Resume transaction
23
+ tx.resume()
24
+
25
+
26
+ prethread.run()
27
+
28
+ # Or import the module, as in Google Python Style Guide
29
+
30
+ import Pyjo.Server.Prethread
31
+
32
+ prethread = Pyjo.Server.Prethread.new(listen=['http://*:8080'])
33
+
34
+
35
+ @prethread.unsubscribe('request').on('request')
36
+ def request(prethread, tx):
37
+ # Request
38
+ method = tx.req.method
39
+ path = tx.req.url.path
40
+
41
+ # Response
42
+ tx.res.code = 200
43
+ tx.res.headers.content_type = 'text/plain'
44
+ tx.res.body = f'{method} request for {path}!'.encode()
45
+
46
+ # Resume transaction
47
+ tx.resume()
48
+
49
+
50
+ prethread.run()
51
+
52
+ :mod:`Pyjo.Server.Prethread` is a full featured pre-threading non-blocking
53
+ I/O HTTP and WebSocket server, which works as :mod:`Pyjo.Server.Prefork`
54
+ does, with threads of one process for the worker processes. Each worker is a
55
+ copy of the server in a thread of its own, with the :data:`Pyjo.IOLoop.singleton`
56
+ of the thread and the listen sockets of the manager. Note that the server uses
57
+ signals for process management, so you should avoid modifying signal handlers
58
+ in your applications.
59
+
60
+ The workers share the memory of the process and the application that the
61
+ manager has loaded, so the application has to be safe to use in several
62
+ threads at the same time. Python runs one thread at a time where it has the
63
+ global interpreter lock, so that the workers run at the same time only while
64
+ they wait, for the network or a blocking call, and on all processor cores on
65
+ a free-threaded Python. A worker that does not stop when it is told to, as
66
+ one in a blocking call, cannot be killed, as a process can, so the manager
67
+ leaves it, and it ends with the process.
68
+
69
+ The workers need no :func:`os.fork`, so the server runs on Windows too, where
70
+ :mod:`Pyjo.Server.Prefork` does not.
71
+
72
+ Manager signals
73
+ ---------------
74
+
75
+ The :mod:`Pyjo.Server.Prethread` manager process can be controlled at runtime
76
+ with the following signals.
77
+
78
+ INT, TERM
79
+ ~~~~~~~~~
80
+
81
+ Shut down server gracefully. The manager and the workers close the listen
82
+ sockets, so that a new connection is refused, and the workers finish the
83
+ accepted ones, after :attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.shutdown_delay`,
84
+ while the workers serve as before; on Windows, where the workers share the
85
+ sockets of the manager, a new connection waits until the server ends. A
86
+ second ``INT``, as of Ctrl-C pressed twice, shuts it down immediately, also
87
+ during the delay, a second ``TERM`` does nothing.
88
+
89
+ QUIT
90
+ ~~~~
91
+
92
+ Shut down server immediately. On Windows, which has no ``QUIT``, ``BREAK``
93
+ does it, as of Ctrl-Break.
94
+
95
+ HUP
96
+ ~~~
97
+
98
+ Restart the workers gracefully, or emit the event ``reload`` when it has
99
+ subscribers.
100
+
101
+ TTIN
102
+ ~~~~
103
+
104
+ Increase worker pool by one.
105
+
106
+ TTOU
107
+ ~~~~
108
+
109
+ Decrease worker pool by one.
110
+
111
+ The manager does not handle ``CHLD``, so that the application gets the exit
112
+ status of its child processes.
113
+
114
+ Windows has no ``HUP``, ``TTIN`` and ``TTOU``, and a ``TERM`` from another
115
+ process terminates the process there without its handler, so Ctrl-C and
116
+ Ctrl-Break of the console are what shut the server down.
117
+
118
+ Events
119
+ ------
120
+
121
+ :mod:`Pyjo.Server.Prethread` inherits all events from
122
+ :mod:`Pyjo.Server.Prefork`, which pass the id of the thread of a worker, as
123
+ :func:`threading.get_native_id` gives it, where :mod:`Pyjo.Server.Prefork`
124
+ passes its process id. ::
125
+
126
+ prethread.on('spawn', lambda prethread, id: print(f'Worker {id} started'))
127
+
128
+ See also
129
+ --------
130
+
131
+ :mod:`Pyjoyment.Command.prethread`, :mod:`Pyjo.Server.Prefork`,
132
+ :mod:`Pyjoyment`, https://mojolicious.org.
133
+
134
+ It has no counterpart in Mojolicious; it is inspired by
135
+ :mod:`Pyjo.Server.Prefork`, the counterpart of `Mojo::Server::Prefork
136
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Prefork.pm>`_,
137
+ as `Thrall <https://metacpan.org/pod/Thrall>`_ is by Starlight.
138
+
139
+ Classes
140
+ -------
141
+ """
142
+
143
+ import Pyjo.IOLoop
144
+ import Pyjo.Server.Prefork
145
+ import Pyjo.URL
146
+
147
+ import copy
148
+ import os
149
+ import signal
150
+ import socket
151
+ import sys
152
+ import tempfile
153
+ import threading
154
+ import time
155
+ import traceback
156
+ import weakref
157
+
158
+ from typing import Any, NotRequired, TypedDict
159
+
160
+
161
+ class _Thread(TypedDict):
162
+ control: socket.socket
163
+ ended: threading.Event
164
+ killed: NotRequired[float]
165
+ thread: threading.Thread
166
+
167
+
168
+ class Pyjo_Server_Prethread(Pyjo.Server.Prefork.object):
169
+ """
170
+ :mod:`Pyjo.Server.Prethread` inherits all attributes and methods from
171
+ :mod:`Pyjo.Server.Prefork`, with ``prethread.pid`` in a temporary
172
+ directory for the default of :attr:`~Pyjo.Server.Prefork.Pyjo_Server_Prefork.pid_file`.
173
+ """
174
+
175
+ # The signals of the manager, without CHLD, which is the one of the children of the application; Windows has INT,
176
+ # TERM and BREAK of Ctrl-Break, which takes the place of QUIT
177
+ if sys.platform == 'win32':
178
+ _MANAGED = (signal.SIGINT, signal.SIGTERM, signal.SIGBREAK)
179
+ else:
180
+ _MANAGED = (signal.SIGHUP, signal.SIGINT, signal.SIGTERM, signal.SIGQUIT, signal.SIGTTIN, signal.SIGTTOU)
181
+
182
+ # The seconds that a worker has to stop once it is told to stop immediately, before the manager leaves it
183
+ _LEAVE = 1
184
+
185
+ # The workers are threads, which need no fork
186
+ _FORKS = False
187
+
188
+ def __init__(self, *, pid_file: str | os.PathLike[str] | None = None, **kwargs: Any) -> None:
189
+ """::
190
+
191
+ prethread = Pyjo.Server.Prethread.new()
192
+ prethread = Pyjo.Server.Prethread.new(listen=['http://*:8080'], workers=10)
193
+
194
+ Construct a new :mod:`Pyjo.Server.Prethread` object.
195
+ """
196
+ super().__init__(
197
+ pid_file=pid_file if pid_file is not None else os.path.join(tempfile.gettempdir(), 'prethread.pid'),
198
+ **kwargs,
199
+ )
200
+ self._threads: dict[int, _Thread] = {}
201
+ self._pair: tuple[socket.socket, socket.socket] | None = None
202
+
203
+ # The heartbeats go through a socket pair in the place of a pipe, as select of Windows watches sockets only
204
+ def _pipe(self) -> tuple[int, int]:
205
+ reader, writer = self._pair = socket.socketpair()
206
+ return reader.fileno(), writer.fileno()
207
+
208
+ def _read_pipe(self, reader: int) -> bytes:
209
+ assert self._pair is not None
210
+ return self._pair[0].recv(4194304)
211
+
212
+ def _close_pipe(self, reader: int, writer: int) -> None:
213
+ if (pair := self._pair) is not None:
214
+ self._pair = None
215
+ pair[0].close()
216
+ pair[1].close()
217
+
218
+ def _send(self, data: bytes) -> None:
219
+ if (pair := self._pair) is None:
220
+ raise OSError('No pipe')
221
+ pair[1].sendall(data)
222
+
223
+ def _heartbeat(self, finished: bool) -> None:
224
+ try:
225
+ self._send(f'{threading.get_native_id()}:{int(finished)}\n'.encode())
226
+ except Exception:
227
+ Pyjo.IOLoop.singleton.stop()
228
+
229
+ def _kill_worker(self, pid: int, graceful: bool) -> bool:
230
+ if (t := self._threads.get(pid)) is None:
231
+ return False
232
+ if not graceful:
233
+ now = time.monotonic()
234
+ if (killed := t.get('killed')) is not None:
235
+ if killed + self._LEAVE > now:
236
+ return True
237
+ self.app.log.error(f'Worker {pid} does not stop, leaving it')
238
+ self._threads.pop(pid)['control'].close()
239
+ return False
240
+ t['killed'] = now
241
+ try:
242
+ t['control'].send(b'T' if graceful else b'Q')
243
+ except OSError:
244
+ pass
245
+ return True
246
+
247
+ def _delay_worker(self, pid: int) -> None:
248
+ if (t := self._threads.get(pid)) is None:
249
+ return
250
+ try:
251
+ t['control'].send(b'D')
252
+ except OSError:
253
+ pass
254
+
255
+ # The listen sockets are left open on Windows, where the workers share the sockets of the manager, which their
256
+ # reactors watch until they stop, and a new connection waits for the end of the server, as in Mojolicious
257
+ def _close_listen(self) -> None:
258
+ if sys.platform != 'win32':
259
+ super()._close_listen()
260
+
261
+ def _spawn(self) -> None:
262
+ # The listen sockets of the manager, which the worker duplicates, or shares on Windows
263
+ loop = self.ioloop
264
+ listen = [
265
+ (location, loop.acceptor(id).handle) for location, id in zip(self.listen, self.acceptors, strict=True)
266
+ ]
267
+ control, wakee = socket.socketpair()
268
+ ended = threading.Event()
269
+ thread = threading.Thread(
270
+ target=self._serve, args=(listen, wakee, ended), name='Pyjo.Server.Prethread', daemon=True
271
+ )
272
+ thread.start()
273
+ assert (id := thread.native_id) is not None
274
+ self._threads[id] = {'control': control, 'ended': ended, 'thread': thread}
275
+ self.emit('spawn', id)._pool[id] = {'time': time.monotonic()}
276
+
277
+ def _wait(self) -> None:
278
+ super()._wait()
279
+
280
+ # The workers that have stopped, which the last line of a worker tells, with the moment that its thread takes to
281
+ # end after it
282
+ for id, t in list(self._threads.items()):
283
+ if t['ended'].is_set():
284
+ t['thread'].join(1)
285
+ if t['thread'].is_alive():
286
+ continue
287
+ t['control'].close()
288
+ del self._threads[id]
289
+ self.emit('reap', id)._stopped(id)
290
+
291
+ # The worker, a copy of the server in a thread, which runs the event loop of the thread until it stops
292
+ def _serve(
293
+ self, listen: list[tuple[Any, socket.socket | None]], control: socket.socket, ended: threading.Event
294
+ ) -> None:
295
+ worker = copy.copy(self)
296
+ loop = worker.ioloop = Pyjo.IOLoop.singleton
297
+ worker.acceptors, worker._connections, worker._servers = [], {}, {}
298
+ worker.cleanup, worker.silent = False, True
299
+ try:
300
+ # The listen sockets of the manager, duplicated, as the worker closes them. On Windows, the workers share the
301
+ # sockets of the manager and leave them open, as accept of a duplicate, of WSADuplicateSocket or of
302
+ # DuplicateHandle, can block, also on a non-blocking socket, when the duplicate of another worker accepts the
303
+ # connection that select reported to both
304
+ loop.max_accepts = self.accepts
305
+ if max := worker.max_clients:
306
+ loop.max_connections = max
307
+ for location, handle in listen:
308
+ assert handle is not None
309
+ worker._listen(
310
+ _location(location, handle.fileno() if sys.platform == 'win32' else handle.dup().detach())
311
+ )
312
+
313
+ # Heartbeat messages
314
+ finished = False
315
+ ref = weakref.ref(worker)
316
+
317
+ def finish(loop: Any) -> None:
318
+ nonlocal finished
319
+ finished = True
320
+ if (worker := ref()) is not None:
321
+ worker._close_listen()
322
+ worker.max_requests = 1
323
+
324
+ loop.on('finish', finish)
325
+
326
+ def heartbeat(loop: Any) -> None:
327
+ if (worker := ref()) is not None:
328
+ worker._heartbeat(finished)
329
+
330
+ loop.next_tick(heartbeat)
331
+ loop.recurring(self.heartbeat_interval, heartbeat)
332
+
333
+ # The manager stops the worker with a byte, gracefully, after the delay of the graceful shutdown, or
334
+ # immediately, also when it is gone
335
+ immediate = False
336
+
337
+ def command(reactor: Any, writable: bool) -> None:
338
+ nonlocal immediate
339
+ try:
340
+ chunk = control.recv(4096)
341
+ except OSError:
342
+ return
343
+ if not chunk or b'Q' in chunk:
344
+ immediate = True
345
+ loop.stop()
346
+ elif b'T' in chunk:
347
+ loop.stop_gracefully()
348
+ elif (worker := ref()) is not None:
349
+ loop.stop_gracefully(worker.shutdown_delay)
350
+
351
+ loop.reactor.io(control, command).watch(control, True, False)
352
+
353
+ self.app.log.info(f'Worker {threading.get_native_id()} started')
354
+ loop.start()
355
+
356
+ # Not in Mojolicious: the end of the server in the thread of the worker, unless it has stopped immediately
357
+ if not immediate:
358
+ worker._server_stopped()
359
+ except BaseException:
360
+ traceback.print_exc()
361
+ finally:
362
+ loop.reactor.remove(control)
363
+ control.close()
364
+ if sys.platform == 'win32':
365
+ for acceptor in [*(loop.acceptor(id) for id in worker.acceptors), *worker._servers.values()]:
366
+ if acceptor is not None and (handle := acceptor.handle) is not None:
367
+ acceptor.stop()
368
+ handle.detach()
369
+ worker._cleanup()
370
+ loop.reset()
371
+
372
+ # The manager is woken up, to reap the worker at once
373
+ ended.set()
374
+ try:
375
+ self._send(b'\n')
376
+ except OSError:
377
+ pass
378
+
379
+
380
+ # A listen location for the duplicate of a listen socket, or the socket on Windows, without the port and the path that
381
+ # would find the socket of the manager in PYJO_REUSE
382
+ def _location(location: Any, fd: int) -> str:
383
+ url = Pyjo.URL.new(str(location))
384
+ if url.protocol.endswith('+unix'):
385
+ url.host = None
386
+ else:
387
+ url.port = None
388
+ url.query.append(fd=fd)
389
+ return str(url)
390
+
391
+
392
+ new = Pyjo_Server_Prethread.new
393
+ object = Pyjo_Server_Prethread
394
+ Prethread = Pyjo_Server_Prethread
@@ -0,0 +1,114 @@
1
+ """
2
+ Pyjo.Server.Reload.Backend.Poll - Auto-restarting development server backend using stat
3
+ =======================================================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Reload.Backend.Poll import Poll
7
+
8
+ backend = Poll()
9
+ if files := backend.modified_files():
10
+ print('Files have been changed')
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjo.Server.Reload.Backend.Poll
15
+
16
+ backend = Pyjo.Server.Reload.Backend.Poll.new()
17
+ if files := backend.modified_files():
18
+ print('Files have been changed')
19
+
20
+ :mod:`Pyjo.Server.Reload.Backend.Poll` is a backend for
21
+ :mod:`Pyjo.Server.Reload` that checks the size and the modification time
22
+ of the files with :func:`os.stat`, and waits for
23
+ :attr:`~Pyjo.Server.Reload.Backend.Pyjo_Server_Reload_Backend.watch_timeout`
24
+ when none has changed.
25
+
26
+ See also
27
+ --------
28
+
29
+ :mod:`Pyjoyment`, https://mojolicious.org.
30
+
31
+ Ported from `Mojo::Server::Morbo::Backend::Poll <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Morbo/Backend/Poll.pm>`_
32
+ of Mojolicious 9.49.
33
+
34
+ Classes
35
+ -------
36
+ """
37
+
38
+ import Pyjo.File
39
+ import Pyjo.Server.Reload.Backend
40
+
41
+ import os
42
+ import time
43
+
44
+ from typing import Any, cast
45
+
46
+
47
+ # The time the program started, as Perl's $^T
48
+ _START = time.time()
49
+
50
+
51
+ # Renamed from Mojolicious: Mojo::Server::Morbo::Backend::Poll, as the names of Futurama are not ours
52
+ class Pyjo_Server_Reload_Backend_Poll(Pyjo.Server.Reload.Backend.object):
53
+ """
54
+ :mod:`Pyjo.Server.Reload.Backend.Poll` inherits all attributes and methods
55
+ from :mod:`Pyjo.Server.Reload.Backend` and implements the following new
56
+ ones.
57
+ """
58
+
59
+ def __init__(self, **kwargs: Any) -> None:
60
+ """::
61
+
62
+ backend = Pyjo.Server.Reload.Backend.Poll.new()
63
+
64
+ Construct a new :mod:`Pyjo.Server.Reload.Backend.Poll` object.
65
+ """
66
+ super().__init__(**kwargs)
67
+ self._cache: dict[str, list[float]] = {}
68
+
69
+ def modified_files(self) -> list[str]:
70
+ """::
71
+
72
+ files = backend.modified_files()
73
+
74
+ Check file size and mtime to determine which files have changed, this
75
+ is not particularly efficient, but very robust.
76
+ """
77
+ cache = self._cache
78
+ known = set(cache)
79
+ files: list[str] = []
80
+ for file in (f for watch in self.watch for f in _files(os.fspath(cast(str, watch)))):
81
+ known.discard(file)
82
+ try:
83
+ stat = os.stat(file)
84
+ except OSError:
85
+ continue
86
+ stats = cache.setdefault(file, [_START, stat.st_size])
87
+ if stat.st_mtime <= stats[0] and stat.st_size == stats[1]:
88
+ continue
89
+ stats[:] = [stat.st_mtime, stat.st_size]
90
+ files.append(file)
91
+
92
+ # Any leftover file means they were deleted
93
+ for file in sorted(known):
94
+ files.append(file)
95
+ del cache[file]
96
+
97
+ if not len(files):
98
+ time.sleep(self.watch_timeout)
99
+
100
+ return files
101
+
102
+
103
+ # A readable file, or the files of a directory
104
+ def _files(path: str) -> list[str]:
105
+ if os.path.isfile(path) and os.access(path, os.R_OK):
106
+ return [path]
107
+ # Not in Mojolicious: the bytecode that Python writes to __pycache__ when the server imports a module that it watches,
108
+ # which is not a change of a source, and would restart the server once after each start
109
+ return [file.to_str() for file in Pyjo.File.new(path).list_tree() if '__pycache__' not in file.to_list()]
110
+
111
+
112
+ new = Pyjo_Server_Reload_Backend_Poll.new
113
+ object = Pyjo_Server_Reload_Backend_Poll
114
+ Poll = Pyjo_Server_Reload_Backend_Poll
@@ -0,0 +1,126 @@
1
+ """
2
+ Pyjo.Server.Reload.Backend - Auto-restarting development server backend
3
+ =======================================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Reload.Backend import Backend
7
+
8
+
9
+ class Pyjo_Server_Reload_Backend_Inotify(Backend):
10
+ def modified_files(self):
11
+ ...
12
+
13
+
14
+ new = Pyjo_Server_Reload_Backend_Inotify.new
15
+ object = Pyjo_Server_Reload_Backend_Inotify
16
+
17
+ # Or import the module, as in Google Python Style Guide
18
+
19
+ import Pyjo.Server.Reload.Backend
20
+
21
+
22
+ class Pyjo_Server_Reload_Backend_Inotify(Pyjo.Server.Reload.Backend.object):
23
+ def modified_files(self):
24
+ ...
25
+
26
+
27
+ new = Pyjo_Server_Reload_Backend_Inotify.new
28
+ object = Pyjo_Server_Reload_Backend_Inotify
29
+
30
+ :mod:`Pyjo.Server.Reload.Backend` is an abstract base class for
31
+ auto-restarting development server backends used by
32
+ :mod:`Pyjo.Server.Reload`.
33
+
34
+ A backend is a module named ``Pyjo.Server.Reload.Backend.<Name>``, with a
35
+ function ``new`` that constructs it and the class as ``object``, which
36
+ another distribution can add, as :func:`pkgutil.extend_path` keeps the
37
+ namespace open, and the ``PYJO_RELOAD_BACKEND`` environment variable selects
38
+ it.
39
+
40
+ See also
41
+ --------
42
+
43
+ :mod:`Pyjoyment`, https://mojolicious.org.
44
+
45
+ Ported from `Mojo::Server::Morbo::Backend <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Morbo/Backend.pm>`_
46
+ of Mojolicious 9.49.
47
+
48
+ Classes
49
+ -------
50
+ """
51
+
52
+ import Pyjo.Base
53
+
54
+ import os
55
+
56
+ from pkgutil import extend_path
57
+ from typing import Any
58
+
59
+
60
+ # Not in Mojolicious: a namespace that other distributions can add backends to
61
+ __path__ = extend_path(__path__, __name__)
62
+
63
+
64
+ # Renamed from Mojolicious: Mojo::Server::Morbo::Backend, as the names of Futurama are not ours
65
+ class Pyjo_Server_Reload_Backend(Pyjo.Base.object):
66
+ """
67
+ :mod:`Pyjo.Server.Reload.Backend` inherits all methods from
68
+ :mod:`Pyjo.Base` and implements the following new ones.
69
+ """
70
+
71
+ def __init__(self, *, watch: list[Any] | None = None, watch_timeout: float | None = None) -> None:
72
+ """::
73
+
74
+ backend = Pyjo.Server.Reload.Backend.new()
75
+ backend = Pyjo.Server.Reload.Backend.new(watch=['lib'], watch_timeout=2)
76
+
77
+ Construct a new :mod:`Pyjo.Server.Reload.Backend` object.
78
+ """
79
+ super().__init__()
80
+
81
+ self.watch: list[Any] = watch if watch is not None else ['lib', 'templates']
82
+ """::
83
+
84
+ watch = backend.watch
85
+ backend.watch = ['/home/sri/my_app']
86
+
87
+ Files and directories to watch for changes, defaults to the application
88
+ script as well as the ``lib`` and ``templates`` directories in the
89
+ current working directory.
90
+ """
91
+
92
+ self._watch_timeout: float | None = watch_timeout
93
+
94
+ @property
95
+ def watch_timeout(self) -> float:
96
+ """::
97
+
98
+ timeout = backend.watch_timeout
99
+ backend.watch_timeout = 10
100
+
101
+ Maximum amount of time in seconds a backend may block when waiting for
102
+ files to change, defaults to the value of the ``PYJO_RELOAD_TIMEOUT``
103
+ environment variable or ``1``.
104
+ """
105
+ if self._watch_timeout is None:
106
+ self._watch_timeout = float(os.environ.get('PYJO_RELOAD_TIMEOUT') or 1)
107
+ return self._watch_timeout
108
+
109
+ @watch_timeout.setter
110
+ def watch_timeout(self, value: float) -> None:
111
+ self._watch_timeout = value
112
+
113
+ def modified_files(self) -> list[str]:
114
+ """::
115
+
116
+ files = backend.modified_files()
117
+
118
+ Check if files from :attr:`watch` have been modified since the last
119
+ check and return their names. Meant to be overloaded in a subclass.
120
+ """
121
+ raise NotImplementedError('Method "modified_files" not implemented by subclass')
122
+
123
+
124
+ new = Pyjo_Server_Reload_Backend.new
125
+ object = Pyjo_Server_Reload_Backend
126
+ Backend = Pyjo_Server_Reload_Backend