Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|