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,687 @@
1
+ """
2
+ Pyjo.Server.Hot - Production web server with hot deployment
3
+ ============================================================
4
+ ::
5
+
6
+ from Pyjo.Server.Hot import Hot
7
+
8
+ hot = Hot()
9
+ hot.run('/home/sri/myapp.py')
10
+
11
+ # Or import the module, as in Google Python Style Guide
12
+
13
+ import Pyjo.Server.Hot
14
+
15
+ hot = Pyjo.Server.Hot.new()
16
+ hot.run('/home/sri/myapp.py')
17
+
18
+ :mod:`Pyjo.Server.Hot` is a full featured, UNIX optimized, pre-forking
19
+ non-blocking I/O HTTP and WebSocket server, built around the very well tested
20
+ and reliable :mod:`Pyjo.Server.Prefork`, with IPv6, TLS, SNI, UNIX domain
21
+ socket, Comet (long polling), keep-alive, multiple event loop and hot
22
+ deployment support that just works. Note that the server uses signals for
23
+ process management, so you should avoid modifying signal handlers in your
24
+ applications.
25
+
26
+ To start applications with it you can use the ``pyjo-hot`` script, or
27
+ ``python -m Pyjo.Server.Hot``, which listens on port ``8080``, automatically
28
+ daemonizes the server process and defaults to ``production`` mode for
29
+ :mod:`Pyjoyment` applications. ::
30
+
31
+ $ pyjo-hot ./myapp.py
32
+
33
+ You can run the same command again for automatic hot deployment. ::
34
+
35
+ $ pyjo-hot ./myapp.py
36
+ Starting hot deployment for Hot server 31841.
37
+
38
+ This second invocation will load the application again, detect the process id
39
+ file with it, and send a :ref:`HUP <hot-hup>` signal to the already running
40
+ server.
41
+
42
+ The server runs the command line of its process again, :data:`sys.orig_argv`,
43
+ for a clean start and for the new manager of a hot deployment, and the
44
+ workers are forks of the manager, made with :func:`os.fork`, so it runs where
45
+ :func:`os.fork` exists, not on Windows. Name resolution in a thread, SOCKS5
46
+ and TLS support can be disabled with the ``PYJO_NO_NNR``, ``PYJO_NO_SOCKS``
47
+ and ``PYJO_NO_TLS`` environment variables.
48
+
49
+ Manager signals
50
+ ---------------
51
+
52
+ The :mod:`Pyjo.Server.Hot` manager process can be controlled at runtime with
53
+ the following signals.
54
+
55
+ INT, TERM
56
+ ~~~~~~~~~
57
+
58
+ Shut down server gracefully. The manager and the workers close the listen
59
+ sockets, so that a new connection is refused, and the workers finish the
60
+ accepted ones; after a hot deployment, the new manager keeps the sockets that
61
+ it has inherited open. A second ``INT``, as of Ctrl-C pressed twice, shuts it
62
+ down immediately, a second ``TERM`` does nothing.
63
+
64
+ QUIT
65
+ ~~~~
66
+
67
+ Shut down server immediately.
68
+
69
+ TTIN
70
+ ~~~~
71
+
72
+ Increase worker pool by one.
73
+
74
+ TTOU
75
+ ~~~~
76
+
77
+ Decrease worker pool by one.
78
+
79
+ .. _hot-hup:
80
+
81
+ HUP
82
+ ~~~
83
+
84
+ Attempt zero downtime software upgrade (hot deployment) without losing any
85
+ incoming connections. ::
86
+
87
+ Manager (old)
88
+ |- Worker [1]
89
+ |- Worker [2]
90
+ |- Worker [3]
91
+ |- Worker [4]
92
+ +- Manager (new)
93
+ |- Worker [1]
94
+ |- Worker [2]
95
+ |- Worker [3]
96
+ +- Worker [4]
97
+
98
+ The new manager will automatically send a ``TERM`` signal to the old manager
99
+ and take over serving requests after starting up successfully.
100
+
101
+ Worker signals
102
+ --------------
103
+
104
+ :mod:`Pyjo.Server.Hot` worker processes can be controlled at runtime with the
105
+ following signals.
106
+
107
+ TERM
108
+ ~~~~
109
+
110
+ Stop worker gracefully, with its copies of the listen sockets closed.
111
+
112
+ QUIT
113
+ ~~~~
114
+
115
+ Stop worker immediately.
116
+
117
+ Settings
118
+ --------
119
+
120
+ :mod:`Pyjo.Server.Hot` can be configured with the following settings, in the
121
+ ``hot`` key of the configuration of the application. ::
122
+
123
+ app.config(hot={'listen': ['http://*:80'], 'workers': 10})
124
+
125
+ accepts
126
+ ~~~~~~~
127
+ ::
128
+
129
+ 'accepts': 100
130
+
131
+ Maximum number of connections a worker is allowed to accept, before stopping
132
+ gracefully and then getting replaced with a newly started worker, defaults to
133
+ the value of :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.accepts`. Setting
134
+ the value to ``0`` will allow workers to accept new connections indefinitely.
135
+ Note that up to half of this value can be subtracted randomly to improve load
136
+ balancing, and to make sure that not all workers restart at the same time.
137
+
138
+ backlog
139
+ ~~~~~~~
140
+ ::
141
+
142
+ 'backlog': 128
143
+
144
+ Listen backlog size, defaults to the value of
145
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.backlog`.
146
+
147
+ clients
148
+ ~~~~~~~
149
+ ::
150
+
151
+ 'clients': 100
152
+
153
+ Maximum number of accepted connections each worker process is allowed to
154
+ handle concurrently, before stopping to accept new incoming connections,
155
+ defaults to the value of :attr:`Pyjo.IOLoop.Pyjo_IOLoop.max_connections`.
156
+ Note that high concurrency works best with applications that perform mostly
157
+ non-blocking operations, to optimize for blocking operations you can decrease
158
+ this value and increase `workers`_ instead for better performance.
159
+
160
+ graceful_timeout
161
+ ~~~~~~~~~~~~~~~~
162
+ ::
163
+
164
+ 'graceful_timeout': 15
165
+
166
+ Maximum amount of time in seconds stopping a worker gracefully may take
167
+ before being forced, defaults to the value of
168
+ :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.graceful_timeout`. Note that
169
+ this value should usually be a little larger than the maximum amount of time
170
+ you expect any one request to take.
171
+
172
+ heartbeat_interval
173
+ ~~~~~~~~~~~~~~~~~~
174
+ ::
175
+
176
+ 'heartbeat_interval': 3
177
+
178
+ Heartbeat interval in seconds, defaults to the value of
179
+ :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.heartbeat_interval`.
180
+
181
+ heartbeat_timeout
182
+ ~~~~~~~~~~~~~~~~~
183
+ ::
184
+
185
+ 'heartbeat_timeout': 2
186
+
187
+ Maximum amount of time in seconds before a worker without a heartbeat will be
188
+ stopped gracefully, defaults to the value of
189
+ :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.heartbeat_timeout`. Note that
190
+ this value should usually be a little larger than the maximum amount of time
191
+ you expect any one operation to block the event loop.
192
+
193
+ inactivity_timeout
194
+ ~~~~~~~~~~~~~~~~~~
195
+ ::
196
+
197
+ 'inactivity_timeout': 10
198
+
199
+ Maximum amount of time in seconds a connection with an active request can be
200
+ inactive before getting closed, defaults to the value of
201
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.inactivity_timeout`. Setting the
202
+ value to ``0`` will allow connections to be inactive indefinitely.
203
+
204
+ keep_alive_timeout
205
+ ~~~~~~~~~~~~~~~~~~
206
+ ::
207
+
208
+ 'keep_alive_timeout': 10
209
+
210
+ Maximum amount of time in seconds a connection without an active request can
211
+ be inactive before getting closed, defaults to the value of
212
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.keep_alive_timeout`. Setting the
213
+ value to ``0`` will allow connections to be inactive indefinitely.
214
+
215
+ listen
216
+ ~~~~~~
217
+ ::
218
+
219
+ 'listen': ['http://*:80']
220
+
221
+ List with one or more locations to listen on, defaults to
222
+ ``http://*:8080``. See also
223
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.listen` for more examples.
224
+
225
+ pid_file
226
+ ~~~~~~~~
227
+ ::
228
+
229
+ 'pid_file': '/var/run/hot.pid'
230
+
231
+ Full path to process id file, defaults to ``hot.pid`` in the same directory
232
+ as the application. Note that this value can only be changed after the server
233
+ has been stopped.
234
+
235
+ proxy
236
+ ~~~~~
237
+ ::
238
+
239
+ 'proxy': True
240
+
241
+ Activate reverse proxy support, which allows for the ``X-Forwarded-For`` and
242
+ ``X-Forwarded-Proto`` headers to be picked up automatically, defaults to the
243
+ value of :attr:`Pyjo.Server.Pyjo_Server.reverse_proxy`.
244
+
245
+ requests
246
+ ~~~~~~~~
247
+ ::
248
+
249
+ 'requests': 50
250
+
251
+ Number of keep-alive requests per connection, defaults to the value of
252
+ :attr:`Pyjo.Server.Daemon.Pyjo_Server_Daemon.max_requests`.
253
+
254
+ spare
255
+ ~~~~~
256
+ ::
257
+
258
+ 'spare': 4
259
+
260
+ Temporarily spawn up to this number of additional workers if there is a need,
261
+ defaults to the value of :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.spare`.
262
+ This allows for new workers to be started while old ones are still shutting
263
+ down gracefully, drastically reducing the performance cost of worker
264
+ restarts.
265
+
266
+ threads
267
+ ~~~~~~~
268
+ ::
269
+
270
+ 'threads': True
271
+
272
+ Run the workers as threads of a :mod:`Pyjo.Server.Prethread` in the place of
273
+ the processes of :mod:`Pyjo.Server.Prefork`, as the option ``--threads`` of
274
+ ``pyjo-hot`` does, which sets the ``PYJO_HOT_THREADS`` environment variable,
275
+ defaults to a false value. The server, its manager and the hot deployment are
276
+ the same.
277
+
278
+ trusted_proxies
279
+ ~~~~~~~~~~~~~~~
280
+ ::
281
+
282
+ 'trusted_proxies': ['10.0.0.0/8', '127.0.0.1', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7']
283
+
284
+ Trusted reverse proxies, addresses or networks in CIDR form.
285
+
286
+ upgrade_timeout
287
+ ~~~~~~~~~~~~~~~
288
+ ::
289
+
290
+ 'upgrade_timeout': 45
291
+
292
+ Maximum amount of time in seconds a zero downtime software upgrade may take
293
+ before getting canceled, defaults to ``180``.
294
+
295
+ workers
296
+ ~~~~~~~
297
+ ::
298
+
299
+ 'workers': 10
300
+
301
+ Number of worker processes, defaults to the value of
302
+ :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.workers`. A good rule of thumb
303
+ is two worker processes per CPU core for applications that perform mostly
304
+ non-blocking operations, blocking operations often require more and benefit
305
+ from decreasing concurrency with `clients`_ (often as low as ``1``). Note
306
+ that during zero downtime software upgrades there will be twice as many
307
+ workers active for a short amount of time.
308
+
309
+ pyjo-hot
310
+ --------
311
+ ::
312
+
313
+ Usage: pyjo-hot [OPTIONS] [APPLICATION]
314
+
315
+ pyjo-hot ./myapp.py
316
+ pyjo-hot -f ./myapp.py
317
+
318
+ Options:
319
+ -f, --foreground Keep manager process in foreground
320
+ -h, --help Show this message
321
+ -s, --stop Stop server gracefully
322
+ -t, --test Test application and exit
323
+ --threads Run the workers as threads
324
+
325
+ Start :mod:`Pyjoyment` applications with the :mod:`Pyjo.Server.Hot` web
326
+ server.
327
+
328
+ See also
329
+ --------
330
+
331
+ :mod:`Pyjoyment`, https://mojolicious.org.
332
+
333
+ Ported from `Mojo::Server::Hypnotoad <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/Hypnotoad.pm>`_
334
+ and `script/hypnotoad <https://github.com/mojolicious/mojo/blob/v9.49/script/hypnotoad>`_ of Mojolicious 9.49.
335
+
336
+ Classes
337
+ -------
338
+ """
339
+
340
+ import Pyjo.Base
341
+ import Pyjo.File
342
+ import Pyjo.GetOpt
343
+ import Pyjo.Server.Prefork
344
+ import Pyjo.Server.Prethread
345
+
346
+ from Pyjo.Util import die, extract_usage, getopt
347
+
348
+ import os
349
+ import signal
350
+ import sys
351
+ import time
352
+ import weakref
353
+
354
+ from collections.abc import Sequence
355
+ from typing import Any, NoReturn, cast
356
+
357
+
358
+ # Renamed from Mojolicious: Mojo::Server::Hypnotoad, as the names of Futurama are not ours
359
+ class Pyjo_Server_Hot(Pyjo.Base.object):
360
+ """
361
+ :mod:`Pyjo.Server.Hot` inherits all methods from :mod:`Pyjo.Base` and
362
+ implements the following new ones.
363
+ """
364
+
365
+ def __init__(self, *, prefork: Pyjo.Server.Prefork.object | None = None, upgrade_timeout: float = 180) -> None:
366
+ """::
367
+
368
+ hot = Pyjo.Server.Hot.new()
369
+ hot = Pyjo.Server.Hot.new(upgrade_timeout=15)
370
+
371
+ Construct a new :mod:`Pyjo.Server.Hot` object.
372
+ """
373
+ super().__init__()
374
+
375
+ self.upgrade_timeout = upgrade_timeout
376
+ """::
377
+
378
+ timeout = hot.upgrade_timeout
379
+ hot.upgrade_timeout = 15
380
+
381
+ Maximum amount of time in seconds a zero downtime software upgrade may
382
+ take before getting canceled, defaults to ``180``.
383
+ """
384
+
385
+ self._config: dict[str, Any] = {}
386
+ self._finished = False
387
+ self._new: int | None = None
388
+ self._prefork = prefork
389
+ self._upgrade: float | None = None
390
+
391
+ @property
392
+ def prefork(self) -> Pyjo.Server.Prefork.object:
393
+ """::
394
+
395
+ prefork = hot.prefork
396
+ hot.prefork = Pyjo.Server.Prefork.new()
397
+
398
+ :mod:`Pyjo.Server.Prefork` object this server manages.
399
+ """
400
+ if self._prefork is None:
401
+ self._prefork = Pyjo.Server.Prefork.new(listen=['http://*:8080'])
402
+ return self._prefork
403
+
404
+ @prefork.setter
405
+ def prefork(self, value: Pyjo.Server.Prefork.object) -> None:
406
+ self._prefork = value
407
+
408
+ def configure(self, name: str) -> None:
409
+ """::
410
+
411
+ hot.configure('hot')
412
+
413
+ Configure server from application settings.
414
+ """
415
+ # Hot settings
416
+ prefork = self.prefork
417
+ c: dict[str, Any] | None = self._settings().get(name)
418
+ if c is None:
419
+ c = {}
420
+ if c.get('upgrade_timeout'):
421
+ self.upgrade_timeout = c['upgrade_timeout']
422
+
423
+ # Not in Mojolicious: the workers are threads of a Pyjo.Server.Prethread, with the application and the listen
424
+ # locations of the server that it replaces
425
+ if (c.get('threads') or os.environ.get('PYJO_HOT_THREADS')) and not isinstance(
426
+ prefork, Pyjo.Server.Prethread.object
427
+ ):
428
+ cleanup, prefork.cleanup = prefork.cleanup, False
429
+ prefork = self.prefork = Pyjo.Server.Prethread.new(app=prefork.app, cleanup=cleanup, listen=prefork.listen)
430
+
431
+ # Pre-fork settings
432
+ if c.get('proxy') is not None:
433
+ prefork.reverse_proxy = c['proxy']
434
+ if c.get('trusted_proxies') is not None:
435
+ prefork.trusted_proxies = c['trusted_proxies']
436
+ if c.get('clients'):
437
+ prefork.max_clients = c['clients']
438
+ if c.get('requests'):
439
+ prefork.max_requests = c['requests']
440
+ for setting in (
441
+ 'accepts',
442
+ 'backlog',
443
+ 'graceful_timeout',
444
+ 'heartbeat_interval',
445
+ 'heartbeat_timeout',
446
+ 'inactivity_timeout',
447
+ 'keep_alive_timeout',
448
+ 'listen',
449
+ 'pid_file',
450
+ 'spare',
451
+ 'workers',
452
+ ):
453
+ if c.get(setting) is not None:
454
+ setattr(prefork, setting, c[setting])
455
+
456
+ def run(self, app: str | os.PathLike[str]) -> None:
457
+ """::
458
+
459
+ hot.run('myapp.py')
460
+
461
+ Run server for application and wait for the manager signals.
462
+ """
463
+ # Unlike Mojolicious: no fork at all, where Mojo has no fork emulation of Perl on Windows
464
+ if not hasattr(os, 'fork'):
465
+ _exit('Pyjo.Server.Hot needs os.fork.')
466
+
467
+ # Remember executable and application for later
468
+ # Left out from Mojolicious: HYPNOTOAD_EXE, as the command line of the process, sys.orig_argv, runs again
469
+ os.environ['PYJO_HOT_APP'] = os.environ.get('PYJO_HOT_APP') or Pyjo.File.new(app).to_abs().to_str()
470
+ app = sys.argv[0] = os.environ['PYJO_HOT_APP']
471
+
472
+ # This is a production server
473
+ os.environ['PYJO_MODE'] = os.environ.get('PYJO_MODE') or 'production'
474
+
475
+ # Clean start (to make sure everything works)
476
+ rev = int(os.environ.get('PYJO_HOT_REV') or 0)
477
+ os.environ['PYJO_HOT_REV'] = str(rev + 1)
478
+ if not rev:
479
+ _exec()
480
+
481
+ # Preload application and configure server
482
+ prefork = self.prefork
483
+ prefork.cleanup = False
484
+ prefork.load_app(app)
485
+ settings = self._settings().setdefault('hot', {})
486
+ if settings.get('pid_file') is None:
487
+ settings['pid_file'] = Pyjo.File.new(os.environ['PYJO_HOT_APP']).sibling('hot.pid').to_str()
488
+ self.configure('hot')
489
+ prefork = self.prefork
490
+ ref = weakref.ref(self)
491
+
492
+ def wait(prefork: Any) -> None:
493
+ if (hot := ref()) is not None:
494
+ hot._manage()
495
+
496
+ def reap(prefork: Any, pid: int) -> None:
497
+ if (hot := ref()) is not None:
498
+ hot._cleanup(pid)
499
+
500
+ def finish(prefork: Any, graceful: bool) -> None:
501
+ if (hot := ref()) is not None:
502
+ hot._finish()
503
+
504
+ # Unlike Mojolicious: HUP, the signal of a reload, attempts a hot deployment, where Mojo has USR2, which is left
505
+ # out, as the signals of the server follow the conventions of containers and not the ones of Mojo
506
+ def reload(prefork: Any) -> None:
507
+ if (hot := ref()) is not None:
508
+ hot._upgrade = hot._upgrade or time.monotonic()
509
+
510
+ prefork.on('wait', wait)
511
+ prefork.on('reap', reap)
512
+ prefork.on('finish', finish)
513
+ prefork.on('reload', reload)
514
+
515
+ # Testing
516
+ if os.environ.get('PYJO_HOT_TEST'):
517
+ _exit('Everything looks good!')
518
+
519
+ # Stop running server
520
+ if os.environ.get('PYJO_HOT_STOP'):
521
+ self._stop()
522
+
523
+ # Initiate hot deployment
524
+ if not os.environ.get('PYJO_HOT_PID'):
525
+ self._hot_deploy()
526
+
527
+ # Daemonize as early as possible (but not for restarts)
528
+ prefork.start()
529
+ if not os.environ.get('PYJO_HOT_FOREGROUND') and int(os.environ['PYJO_HOT_REV']) < 3:
530
+ prefork.daemonize()
531
+
532
+ # Start accepting connections
533
+ prefork.cleanup = True
534
+ prefork.run()
535
+
536
+ def _cleanup(self, pid: int) -> None:
537
+ # Clean up failed upgrade
538
+ if self._new != pid:
539
+ return
540
+ self.prefork.app.log.error('Zero downtime software upgrade failed')
541
+ self._new = self._upgrade = None
542
+
543
+ def _finish(self) -> None:
544
+ # Unlike Mojolicious: no upgrade once the server is finished, where Mojo sets "finish" and checks "finished", so
545
+ # that HUP during a graceful shutdown still starts a new manager
546
+ self._finished = True
547
+ if not (new := self._new):
548
+ return
549
+
550
+ prefork = self.prefork
551
+ prefork.cleanup = False
552
+ Pyjo.File.new(prefork.pid_file).remove()
553
+ prefork.ensure_pid_file(new)
554
+
555
+ def _hot_deploy(self) -> None:
556
+ # Make sure server is running
557
+ if not (pid := self.prefork.check_pid()):
558
+ return
559
+
560
+ # Start hot deployment
561
+ # Unlike Mojolicious: HUP, where Mojo sends USR2
562
+ _kill(signal.SIGHUP, pid)
563
+ _exit(f'Starting hot deployment for Hot server {pid}.')
564
+
565
+ def _manage(self) -> None:
566
+ # Upgraded (wait for all workers to send a heartbeat)
567
+ prefork = self.prefork
568
+ log = prefork.app.log
569
+ current = str(os.getpid())
570
+ if (old := os.environ.get('PYJO_HOT_PID')) and old != current:
571
+ if prefork.healthy != prefork.workers:
572
+ return
573
+ log.info(f'Upgrade successful, stopping {old}')
574
+ # Unlike Mojolicious: TERM stops the old manager gracefully, where Mojo sends QUIT
575
+ _kill(signal.SIGTERM, int(old))
576
+ os.environ['PYJO_HOT_PID'] = current
577
+
578
+ # Upgrade
579
+ if self._upgrade and not self._finished:
580
+ # Fresh start
581
+ ut = self.upgrade_timeout
582
+ if not self._new:
583
+ log.info(f'Starting zero downtime software upgrade ({_number(ut)} seconds)')
584
+ # Not in Mojolicious: the buffers of the standard streams are written before, so that the new manager
585
+ # does not write them again
586
+ _flush()
587
+ try:
588
+ pid = self._new = os.fork()
589
+ except OSError as e:
590
+ raise OSError(e.errno, f"Can't fork: {e.strerror}") from e
591
+ if not pid:
592
+ # Not in Mojolicious: a new manager that cannot be run exits, as the exception would go on with the
593
+ # old manager in its fork
594
+ try:
595
+ _exec()
596
+ except BaseException as e:
597
+ print(e, file=sys.stderr)
598
+ _flush()
599
+ os._exit(255)
600
+
601
+ # Timeout
602
+ if self._upgrade + ut <= time.monotonic() and self._new:
603
+ _kill(signal.SIGKILL, self._new)
604
+
605
+ # Not in Mojolicious: the configuration of an application that has one, as the applications of Pyjoyment have, and
606
+ # one of the server for another one
607
+ def _settings(self) -> dict[str, Any]:
608
+ config = getattr(self.prefork.app, 'config', None)
609
+ return cast(dict[str, Any], config()) if callable(config) else self._config
610
+
611
+ def _stop(self) -> None:
612
+ if not (pid := self.prefork.check_pid()):
613
+ _exit('Hot server not running.')
614
+ # Unlike Mojolicious: TERM stops the server gracefully, where Mojo sends QUIT
615
+ _kill(signal.SIGTERM, pid)
616
+ _exit(f'Stopping Hot server {pid} gracefully.')
617
+
618
+
619
+ # Renamed from Mojolicious: script/hypnotoad, the command line interface, a function of the module, whose usage is the
620
+ # section pyjo-hot of its docstring
621
+ def main(argv: Sequence[str] | None = None) -> None:
622
+ """::
623
+
624
+ main(['-f', 'myapp.py'])
625
+
626
+ The command line interface of ``pyjo-hot``: parse the options and run the
627
+ server for the application.
628
+ """
629
+ args = list(sys.argv[1:] if argv is None else argv)
630
+ opts = getopt(
631
+ args,
632
+ {
633
+ 'f|foreground': Pyjo.GetOpt.env('PYJO_HOT_FOREGROUND'),
634
+ 'h|help': None,
635
+ 's|stop': Pyjo.GetOpt.env('PYJO_HOT_STOP'),
636
+ 't|test': Pyjo.GetOpt.env('PYJO_HOT_TEST'),
637
+ # Not in Mojolicious: the workers are threads, as the setting "threads"
638
+ 'threads': Pyjo.GetOpt.env('PYJO_HOT_THREADS'),
639
+ },
640
+ )
641
+
642
+ if not opts or opts.help or not (app := (args[0] if args else None) or os.environ.get('PYJO_HOT_APP')):
643
+ # The usage without its last newline, as SystemExit writes the message with one
644
+ die(extract_usage(section='pyjo-hot').rstrip('\n'))
645
+ Pyjo_Server_Hot.new().run(app)
646
+
647
+
648
+ # Unlike Mojolicious: the command line of the process, sys.orig_argv, where Mojo runs $0 again and passes the arguments
649
+ # in the environment, as Python runs a module with -m and code with -c too
650
+ def _exec() -> NoReturn:
651
+ _flush()
652
+ try:
653
+ os.execv(sys.executable, [sys.executable, *sys.orig_argv[1:]])
654
+ except OSError as e:
655
+ raise OSError(e.errno, f"Can't exec: {e.strerror}") from e
656
+
657
+
658
+ def _exit(message: str) -> NoReturn:
659
+ print(message)
660
+ sys.exit(0)
661
+
662
+
663
+ def _flush() -> None:
664
+ for stream in (sys.stdout, sys.stderr):
665
+ try:
666
+ stream.flush()
667
+ except Exception:
668
+ pass
669
+
670
+
671
+ # Perl's kill, true if the signal was sent
672
+ def _kill(sig: int, pid: int) -> bool:
673
+ try:
674
+ os.kill(pid, sig)
675
+ return True
676
+ except OSError:
677
+ return False
678
+
679
+
680
+ # A number of seconds as Perl prints it, without ".0"
681
+ def _number(value: float) -> str:
682
+ return str(int(value)) if value == int(value) else str(value)
683
+
684
+
685
+ new = Pyjo_Server_Hot.new
686
+ object = Pyjo_Server_Hot
687
+ Hot = Pyjo_Server_Hot
@@ -0,0 +1,5 @@
1
+ import Pyjo.Server.Hot
2
+
3
+
4
+ # python -m Pyjo.Server.Hot, as pyjo-hot
5
+ Pyjo.Server.Hot.main()