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,482 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Reload - Auto-restarting development server
|
|
3
|
+
========================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Reload import Reload
|
|
7
|
+
|
|
8
|
+
reload = Reload()
|
|
9
|
+
reload.run('/home/sri/myapp.py')
|
|
10
|
+
|
|
11
|
+
# Or import the module, as in Google Python Style Guide
|
|
12
|
+
|
|
13
|
+
import Pyjo.Server.Reload
|
|
14
|
+
|
|
15
|
+
reload = Pyjo.Server.Reload.new()
|
|
16
|
+
reload.run('/home/sri/myapp.py')
|
|
17
|
+
reload.run('/home/sri/myapp.py', 'prefork', '-w', '1')
|
|
18
|
+
|
|
19
|
+
Pyjo.Server.Reload.new(exec=['npx webpack --watch']).run('/home/sri/myapp.py')
|
|
20
|
+
|
|
21
|
+
:mod:`Pyjo.Server.Reload` is a self-restart capable development server. It
|
|
22
|
+
starts your application with a server command, like
|
|
23
|
+
:mod:`Pyjoyment.Command.daemon` or :mod:`Pyjoyment.Command.prefork`, and
|
|
24
|
+
restarts it whenever a file changes. Listen sockets are only created once and
|
|
25
|
+
then passed on to the server, so they should be configured with
|
|
26
|
+
:mod:`Pyjo.Server.Reload` and not the server command. Note that the server
|
|
27
|
+
uses signals for process management, so you should avoid modifying signal
|
|
28
|
+
handlers in your applications.
|
|
29
|
+
|
|
30
|
+
Additional commands, like asset bundlers in watch mode, can be started
|
|
31
|
+
alongside the server with :attr:`~Pyjo_Server_Reload.exec`. They only get
|
|
32
|
+
started once, are not restarted together with the server, and get stopped when
|
|
33
|
+
:mod:`Pyjo.Server.Reload` shuts down.
|
|
34
|
+
|
|
35
|
+
To start applications with it you can use the ``pyjo-reload`` script, or
|
|
36
|
+
``python -m Pyjo.Server.Reload``. ::
|
|
37
|
+
|
|
38
|
+
$ pyjo-reload ./myapp.py
|
|
39
|
+
Web application available at http://127.0.0.1:3000
|
|
40
|
+
|
|
41
|
+
The server and the commands run in process groups of their own, which the
|
|
42
|
+
signals of POSIX stop, so it does not run on Windows. Name resolution in a
|
|
43
|
+
thread, SOCKS5 and TLS support can be disabled with the ``PYJO_NO_NNR``,
|
|
44
|
+
``PYJO_NO_SOCKS`` and ``PYJO_NO_TLS`` environment variables.
|
|
45
|
+
|
|
46
|
+
Signals
|
|
47
|
+
-------
|
|
48
|
+
|
|
49
|
+
The :mod:`Pyjo.Server.Reload` process can be controlled at runtime with the
|
|
50
|
+
following signals.
|
|
51
|
+
|
|
52
|
+
INT, TERM
|
|
53
|
+
~~~~~~~~~
|
|
54
|
+
|
|
55
|
+
Shut down server immediately. The server, all
|
|
56
|
+
:attr:`~Pyjo_Server_Reload.exec` commands and their subprocesses get a
|
|
57
|
+
``TERM`` signal, and the server also ``QUIT``, and will be killed if they have
|
|
58
|
+
not stopped after 5 seconds, just like for restarts.
|
|
59
|
+
|
|
60
|
+
HUP
|
|
61
|
+
~~~
|
|
62
|
+
|
|
63
|
+
Restart the server, as a change of a file does, while the
|
|
64
|
+
:attr:`~Pyjo_Server_Reload.exec` commands go on.
|
|
65
|
+
|
|
66
|
+
pyjo-reload
|
|
67
|
+
-----------
|
|
68
|
+
::
|
|
69
|
+
|
|
70
|
+
Usage: pyjo-reload [OPTIONS] APPLICATION [COMMAND] [ARGS]
|
|
71
|
+
|
|
72
|
+
pyjo-reload ./myapp.py
|
|
73
|
+
pyjo-reload ./myapp.py prefork -w 1
|
|
74
|
+
pyjo-reload -l http://127.0.0.1:8080 -w assets/css ./myapp.py prefork -w 3
|
|
75
|
+
pyjo-reload -m production -l https://*:443 -l http://[::]:3000 ./myapp.py
|
|
76
|
+
pyjo-reload -l 'https://*:443?cert=./server.crt&key=./server.key' ./myapp.py
|
|
77
|
+
pyjo-reload -w /usr/local/lib -w public -w myapp.conf ./myapp.py
|
|
78
|
+
pyjo-reload -x 'npx webpack --watch' ./myapp.py
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
-b, --backend <name> Reload backend to use for reloading,
|
|
82
|
+
defaults to "Poll"
|
|
83
|
+
-h, --help Show this message
|
|
84
|
+
-l, --listen <location> One or more locations you want to listen on,
|
|
85
|
+
defaults to the value of PYJO_LISTEN, the
|
|
86
|
+
port of PORT or "http://*:3000"
|
|
87
|
+
--log-format <name> Format of log messages, "text" or "json",
|
|
88
|
+
defaults to the value of PYJO_LOG_FORMAT or
|
|
89
|
+
"text"
|
|
90
|
+
-m, --mode <name> Operating mode for your application,
|
|
91
|
+
defaults to the value of PYJO_MODE or
|
|
92
|
+
"development"
|
|
93
|
+
-v, --verbose Print details about what files changed to
|
|
94
|
+
sys.stdout
|
|
95
|
+
-w, --watch <directory/file> One or more directories and files to watch
|
|
96
|
+
for changes, defaults to the application
|
|
97
|
+
script as well as the "lib" and "templates"
|
|
98
|
+
directories in the current working directory
|
|
99
|
+
-x, --exec <command> One or more shell commands to start
|
|
100
|
+
alongside the server, like asset bundlers in
|
|
101
|
+
watch mode
|
|
102
|
+
|
|
103
|
+
Start :mod:`Pyjoyment` applications with the :mod:`Pyjo.Server.Reload`
|
|
104
|
+
restarter. The server command defaults to ``daemon``, and all options of
|
|
105
|
+
``pyjo-reload`` need to come before the application. Shell commands started
|
|
106
|
+
with ``--exec`` run for as long as :mod:`Pyjo.Server.Reload` does, they are
|
|
107
|
+
not restarted together with the server.
|
|
108
|
+
|
|
109
|
+
See also
|
|
110
|
+
--------
|
|
111
|
+
|
|
112
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
113
|
+
|
|
114
|
+
Ported from `Mojo::Server::Morbo <https://github.com/mojolicious/mojo/blob/v9.53/lib/Mojo/Server/Morbo.pm>`_
|
|
115
|
+
and `script/morbo <https://github.com/mojolicious/mojo/blob/v9.53/script/morbo>`_ of Mojolicious 9.53.
|
|
116
|
+
|
|
117
|
+
Classes
|
|
118
|
+
-------
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
import Pyjo.Base
|
|
122
|
+
import Pyjo.Exception
|
|
123
|
+
import Pyjo.GetOpt
|
|
124
|
+
import Pyjo.Server.Daemon
|
|
125
|
+
|
|
126
|
+
from Pyjo.Loader import load_class
|
|
127
|
+
from Pyjo.Util import die, extract_usage, getopt, warn
|
|
128
|
+
|
|
129
|
+
import contextlib
|
|
130
|
+
import os
|
|
131
|
+
import re
|
|
132
|
+
import signal
|
|
133
|
+
import sys
|
|
134
|
+
import time
|
|
135
|
+
|
|
136
|
+
from collections.abc import Sequence
|
|
137
|
+
from pkgutil import extend_path
|
|
138
|
+
from typing import TYPE_CHECKING, Any, NoReturn
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
if TYPE_CHECKING:
|
|
142
|
+
import Pyjo.Server.Reload.Backend
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Not in Mojolicious: a namespace that other distributions can add to, as Pyjo.Server.Reload.Backend is one
|
|
146
|
+
__path__ = extend_path(__path__, __name__)
|
|
147
|
+
|
|
148
|
+
# The signals that stop the manager, and HUP, which restarts the server, blocked across a fork, so that the child does
|
|
149
|
+
# not run the handlers of the manager before its exec
|
|
150
|
+
_SIGNALS = {signal.SIGINT, signal.SIGTERM, signal.SIGHUP} if hasattr(signal, 'SIGHUP') else set[signal.Signals]()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# Renamed from Mojolicious: Mojo::Server::Morbo, as the names of Futurama are not ours
|
|
154
|
+
class Pyjo_Server_Reload(Pyjo.Base.object):
|
|
155
|
+
"""
|
|
156
|
+
:mod:`Pyjo.Server.Reload` inherits all methods from :mod:`Pyjo.Base` and
|
|
157
|
+
implements the following new ones.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
def __init__(
|
|
161
|
+
self,
|
|
162
|
+
*,
|
|
163
|
+
backend: 'Pyjo.Server.Reload.Backend.object | None' = None,
|
|
164
|
+
daemon: Pyjo.Server.Daemon.object | None = None,
|
|
165
|
+
exec: list[str] | None = None,
|
|
166
|
+
silent: bool = True,
|
|
167
|
+
) -> None:
|
|
168
|
+
"""::
|
|
169
|
+
|
|
170
|
+
reload = Pyjo.Server.Reload.new()
|
|
171
|
+
reload = Pyjo.Server.Reload.new(silent=False)
|
|
172
|
+
|
|
173
|
+
Construct a new :mod:`Pyjo.Server.Reload` object.
|
|
174
|
+
"""
|
|
175
|
+
super().__init__()
|
|
176
|
+
|
|
177
|
+
self.exec: list[str] = exec if exec is not None else []
|
|
178
|
+
"""::
|
|
179
|
+
|
|
180
|
+
commands = reload.exec
|
|
181
|
+
reload.exec = ['npx webpack --watch', 'npx tailwindcss -i app.css -o public/assets/app.css --watch']
|
|
182
|
+
|
|
183
|
+
Shell commands to start alongside the server, like asset bundlers in
|
|
184
|
+
watch mode. The ``PYJO_MODE`` environment variable will be set to the
|
|
185
|
+
operating mode of your application, so commands can use it too. The
|
|
186
|
+
standard input of all commands is a pipe that gets closed when
|
|
187
|
+
:mod:`Pyjo.Server.Reload` exits, even if it gets killed unexpectedly,
|
|
188
|
+
which some tools use to shut down. When a command exits on its own, a
|
|
189
|
+
message with its exit status is written to ``sys.stderr``.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
self.silent = silent
|
|
193
|
+
"""::
|
|
194
|
+
|
|
195
|
+
boolean = reload.silent
|
|
196
|
+
reload.silent = False
|
|
197
|
+
|
|
198
|
+
Disable console messages, defaults to a true value.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
self._backend = backend
|
|
202
|
+
self._command: list[str] = []
|
|
203
|
+
self._commands: dict[int, str] = {}
|
|
204
|
+
self._daemon = daemon
|
|
205
|
+
self._finished = False
|
|
206
|
+
self._kill: dict[int, float] = {}
|
|
207
|
+
self._modified = False
|
|
208
|
+
self._stdin: int | None = None
|
|
209
|
+
self._worker: int | None = None
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def backend(self) -> 'Pyjo.Server.Reload.Backend.object':
|
|
213
|
+
"""::
|
|
214
|
+
|
|
215
|
+
backend = reload.backend
|
|
216
|
+
reload.backend = Pyjo.Server.Reload.Backend.Poll.new()
|
|
217
|
+
|
|
218
|
+
Backend, usually a :mod:`Pyjo.Server.Reload.Backend.Poll` object, or
|
|
219
|
+
``Pyjo.Server.Reload.Backend.<Name>`` of the ``PYJO_RELOAD_BACKEND``
|
|
220
|
+
environment variable.
|
|
221
|
+
"""
|
|
222
|
+
if self._backend is None:
|
|
223
|
+
name = f'Pyjo.Server.Reload.Backend.{os.environ.get("PYJO_RELOAD_BACKEND") or "Poll"}'
|
|
224
|
+
if e := load_class(name):
|
|
225
|
+
if isinstance(e, BaseException):
|
|
226
|
+
raise e
|
|
227
|
+
raise Pyjo.Exception.new(f'Can\'t find Reload backend class "{name}" in sys.path. ({sys.path})\n')
|
|
228
|
+
self._backend = sys.modules[name].new()
|
|
229
|
+
return self._backend
|
|
230
|
+
|
|
231
|
+
@backend.setter
|
|
232
|
+
def backend(self, value: 'Pyjo.Server.Reload.Backend.object') -> None:
|
|
233
|
+
self._backend = value
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def daemon(self) -> Pyjo.Server.Daemon.object:
|
|
237
|
+
"""::
|
|
238
|
+
|
|
239
|
+
daemon = reload.daemon
|
|
240
|
+
reload.daemon = Pyjo.Server.Daemon.new()
|
|
241
|
+
|
|
242
|
+
:mod:`Pyjo.Server.Daemon` object used to create listen sockets. Other
|
|
243
|
+
settings will be ignored, since the server runs in a separate process,
|
|
244
|
+
use options of the server command instead.
|
|
245
|
+
"""
|
|
246
|
+
if self._daemon is None:
|
|
247
|
+
self._daemon = Pyjo.Server.Daemon.new(silent=True)
|
|
248
|
+
return self._daemon
|
|
249
|
+
|
|
250
|
+
@daemon.setter
|
|
251
|
+
def daemon(self, value: Pyjo.Server.Daemon.object) -> None:
|
|
252
|
+
self._daemon = value
|
|
253
|
+
|
|
254
|
+
def run(self, app: str, *args: str) -> NoReturn:
|
|
255
|
+
"""::
|
|
256
|
+
|
|
257
|
+
reload.run('myapp.py')
|
|
258
|
+
reload.run('myapp.py', 'prefork', '-w', '1')
|
|
259
|
+
|
|
260
|
+
Run server command for application and wait for the signals. The
|
|
261
|
+
command defaults to ``daemon``.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
# Unlike Mojolicious: no Windows, where Mojo has no fork emulation
|
|
265
|
+
if not hasattr(os, 'fork'):
|
|
266
|
+
die('Pyjo.Server.Reload does not support Windows.\n')
|
|
267
|
+
|
|
268
|
+
# Clean manager environment
|
|
269
|
+
def stop(signum: int, frame: Any) -> None:
|
|
270
|
+
self._finished = True
|
|
271
|
+
for pid in [self._worker, *self._commands]:
|
|
272
|
+
self._stop(pid)
|
|
273
|
+
|
|
274
|
+
# Unlike Mojolicious: HUP restarts the server, as it reloads the other servers of Pyjoyment, where Mojo stops on
|
|
275
|
+
# it
|
|
276
|
+
def restart(signum: int, frame: Any) -> None:
|
|
277
|
+
if not self._finished:
|
|
278
|
+
self._stop(self._worker)
|
|
279
|
+
self._modified = True
|
|
280
|
+
|
|
281
|
+
# Not in Mojolicious: local %SIG, the handlers of the signals are set back at the end
|
|
282
|
+
handlers = {name: signal.signal(name, restart if name == signal.SIGHUP else stop) for name in _SIGNALS}
|
|
283
|
+
try:
|
|
284
|
+
self.backend.watch.insert(0, app)
|
|
285
|
+
sys.argv[0] = app
|
|
286
|
+
self._command = [sys.executable, app, *(args or ['daemon'])]
|
|
287
|
+
self._modified = True
|
|
288
|
+
|
|
289
|
+
# Prepare and cache listen sockets for smooth restarting (inherited by the server)
|
|
290
|
+
self.daemon.start().stop()
|
|
291
|
+
os.environ['PYJO_LISTEN'] = ','.join(str(location) for location in self.daemon.listen)
|
|
292
|
+
# Renamed from Mojolicious: PERL5LIB, the path of the modules of the manager, with its current directory
|
|
293
|
+
os.environ['PYTHONPATH'] = os.pathsep.join(os.path.abspath(path) for path in sys.path)
|
|
294
|
+
|
|
295
|
+
# Commands (like asset bundlers in watch mode) need to know the mode too, and only get started once
|
|
296
|
+
if not os.environ.get('PYJO_MODE'):
|
|
297
|
+
os.environ['PYJO_MODE'] = 'development'
|
|
298
|
+
|
|
299
|
+
# Commands get a pipe as STDIN that stays open until the manager exits, so they can read from it without
|
|
300
|
+
# getting stopped (process group is not in the foreground) and know when to exit if it dies unexpectedly
|
|
301
|
+
# (like esbuild does)
|
|
302
|
+
stdin, self._stdin = os.pipe()
|
|
303
|
+
self._commands = {self._spawn(stdin, *_shell(command)): command for command in self.exec}
|
|
304
|
+
os.close(stdin)
|
|
305
|
+
|
|
306
|
+
while not (self._finished and not self._worker and not self._commands):
|
|
307
|
+
self._manage()
|
|
308
|
+
finally:
|
|
309
|
+
for name, handler in handlers.items():
|
|
310
|
+
signal.signal(name, handler)
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
|
|
313
|
+
def _manage(self) -> None:
|
|
314
|
+
if files := self.backend.modified_files():
|
|
315
|
+
if not self.silent:
|
|
316
|
+
print(
|
|
317
|
+
f'File "{files[0]}" changed, restarting.'
|
|
318
|
+
if len(files) == 1
|
|
319
|
+
else f'{len(files)} files changed, restarting.'
|
|
320
|
+
)
|
|
321
|
+
self._stop(self._worker)
|
|
322
|
+
self._modified = True
|
|
323
|
+
|
|
324
|
+
for pid in [pid for pid in (self._worker, *self._commands) if pid is not None]:
|
|
325
|
+
try:
|
|
326
|
+
done, status = os.waitpid(pid, os.WNOHANG)
|
|
327
|
+
except ChildProcessError:
|
|
328
|
+
done, status = pid, 0
|
|
329
|
+
if done == pid:
|
|
330
|
+
self._kill.pop(pid, None)
|
|
331
|
+
if self._worker == pid:
|
|
332
|
+
self._worker = None
|
|
333
|
+
elif not self._finished:
|
|
334
|
+
command = self._commands.get(pid)
|
|
335
|
+
warn(
|
|
336
|
+
f'Command "{command}" was killed by signal {os.WTERMSIG(status)}.\n'
|
|
337
|
+
if os.WIFSIGNALED(status)
|
|
338
|
+
else f'Command "{command}" exited with status {os.WEXITSTATUS(status)}.\n'
|
|
339
|
+
)
|
|
340
|
+
self._commands.pop(pid, None)
|
|
341
|
+
|
|
342
|
+
# Process did not stop in time
|
|
343
|
+
elif pid in self._kill:
|
|
344
|
+
if self._kill[pid] <= time.monotonic():
|
|
345
|
+
_kill(signal.SIGKILL, -pid)
|
|
346
|
+
|
|
347
|
+
# Process was started after the signal handler ran
|
|
348
|
+
elif self._finished:
|
|
349
|
+
self._stop(pid)
|
|
350
|
+
|
|
351
|
+
if not self._worker and not self._finished and self._modified:
|
|
352
|
+
self._modified = False
|
|
353
|
+
self._worker = self._spawn(None, *self._command)
|
|
354
|
+
|
|
355
|
+
def _spawn(self, stdin: int | None, *command: str) -> int:
|
|
356
|
+
# Not in Mojolicious: the buffers of the standard streams are written before, so that the child does not write
|
|
357
|
+
# them again, and the signals of the manager are blocked, so that the child does not run its handlers
|
|
358
|
+
_flush()
|
|
359
|
+
mask = signal.pthread_sigmask(signal.SIG_BLOCK, _SIGNALS)
|
|
360
|
+
try:
|
|
361
|
+
pid = os.fork()
|
|
362
|
+
except OSError as e:
|
|
363
|
+
signal.pthread_sigmask(signal.SIG_SETMASK, mask)
|
|
364
|
+
raise OSError(e.errno, f"Can't fork: {e.strerror}") from e
|
|
365
|
+
|
|
366
|
+
# Manager (process group gets set in both processes to avoid a race)
|
|
367
|
+
if pid:
|
|
368
|
+
signal.pthread_sigmask(signal.SIG_SETMASK, mask)
|
|
369
|
+
with contextlib.suppress(OSError):
|
|
370
|
+
os.setpgid(pid, pid)
|
|
371
|
+
return pid
|
|
372
|
+
|
|
373
|
+
# Worker (process group includes subprocesses)
|
|
374
|
+
try:
|
|
375
|
+
os.setpgid(0, 0)
|
|
376
|
+
for name in _SIGNALS:
|
|
377
|
+
signal.signal(name, signal.SIG_DFL)
|
|
378
|
+
# Not in Mojolicious: the server ignores QUIT until it has a handler of its own, as QUIT, which stops it
|
|
379
|
+
# immediately, would dump its core before
|
|
380
|
+
if stdin is None:
|
|
381
|
+
signal.signal(signal.SIGQUIT, signal.SIG_IGN)
|
|
382
|
+
signal.pthread_sigmask(signal.SIG_SETMASK, mask)
|
|
383
|
+
if stdin is not None:
|
|
384
|
+
try:
|
|
385
|
+
os.dup2(stdin, 0)
|
|
386
|
+
except OSError as e:
|
|
387
|
+
warn(f"Can't redirect STDIN: {e.strerror}\n")
|
|
388
|
+
os.execvp(command[0], command)
|
|
389
|
+
except OSError as e:
|
|
390
|
+
warn(f'Can\'t exec "{command[0]}": {e.strerror}\n')
|
|
391
|
+
except BaseException:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
# Skip cleanup code inherited from the manager if exec fails
|
|
395
|
+
_flush()
|
|
396
|
+
os._exit(1)
|
|
397
|
+
|
|
398
|
+
def _stop(self, pid: int | None) -> None:
|
|
399
|
+
if not pid:
|
|
400
|
+
return
|
|
401
|
+
# Unlike Mojolicious: the server gets QUIT too, which stops it immediately, as TERM stops the servers of
|
|
402
|
+
# Pyjoyment gracefully
|
|
403
|
+
if pid == self._worker:
|
|
404
|
+
_kill(signal.SIGQUIT, pid)
|
|
405
|
+
_kill(signal.SIGTERM, -pid)
|
|
406
|
+
self._kill.setdefault(pid, time.monotonic() + 5)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Renamed from Mojolicious: script/morbo, the command line interface, a function of the module, whose usage is the
|
|
410
|
+
# section pyjo-reload of its docstring
|
|
411
|
+
def main(argv: Sequence[str] | None = None) -> NoReturn:
|
|
412
|
+
"""::
|
|
413
|
+
|
|
414
|
+
main(['-l', 'http://*:8080', 'myapp.py'])
|
|
415
|
+
main(['-l', 'http://*:8080', 'myapp.py', 'prefork', '-w', '2'])
|
|
416
|
+
|
|
417
|
+
The command line interface of ``pyjo-reload``: parse the options and run
|
|
418
|
+
the server command for the application.
|
|
419
|
+
"""
|
|
420
|
+
args = list(sys.argv[1:] if argv is None else argv)
|
|
421
|
+
exec: list[str] = []
|
|
422
|
+
listen: list[str] = []
|
|
423
|
+
watch: list[str] = []
|
|
424
|
+
opts = getopt(
|
|
425
|
+
args,
|
|
426
|
+
{
|
|
427
|
+
'b|backend=s': Pyjo.GetOpt.env('PYJO_RELOAD_BACKEND'),
|
|
428
|
+
'h|help': None,
|
|
429
|
+
'l|listen=s': listen,
|
|
430
|
+
# Not in Mojolicious: the format of the log messages of Pyjo.Log, as the option of the commands
|
|
431
|
+
'log-format=s': Pyjo.GetOpt.env('PYJO_LOG_FORMAT'),
|
|
432
|
+
'm|mode=s': Pyjo.GetOpt.env('PYJO_MODE'),
|
|
433
|
+
'v|verbose': None,
|
|
434
|
+
'w|watch=s': watch,
|
|
435
|
+
'x|exec=s': exec,
|
|
436
|
+
},
|
|
437
|
+
require_order=True,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
if not opts or opts.help or not args:
|
|
441
|
+
# The usage without its last newline, as SystemExit writes the message with one
|
|
442
|
+
die(extract_usage(section='pyjo-reload').rstrip('\n'))
|
|
443
|
+
app = args.pop(0)
|
|
444
|
+
if args and args[0].startswith('-'):
|
|
445
|
+
die(f'pyjo-reload options need to come before the application, e.g. "pyjo-reload {args[0]} ... {app}"')
|
|
446
|
+
reload = Pyjo_Server_Reload.new(silent=not opts.verbose)
|
|
447
|
+
if listen:
|
|
448
|
+
reload.daemon.listen = listen
|
|
449
|
+
if watch:
|
|
450
|
+
reload.backend.watch = watch
|
|
451
|
+
if exec:
|
|
452
|
+
reload.exec = exec
|
|
453
|
+
reload.run(app, *args)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _flush() -> None:
|
|
457
|
+
for stream in (sys.stdout, sys.stderr):
|
|
458
|
+
try:
|
|
459
|
+
stream.flush()
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# Perl's kill, true if the signal was sent
|
|
465
|
+
def _kill(sig: int, pid: int) -> bool:
|
|
466
|
+
try:
|
|
467
|
+
os.kill(pid, sig)
|
|
468
|
+
return True
|
|
469
|
+
except OSError:
|
|
470
|
+
return False
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
# Perl's exec of one string, through the shell only with its metacharacters, as a command of exec
|
|
474
|
+
def _shell(command: str) -> list[str]:
|
|
475
|
+
if re.search(r"""[$&*(){}\[\]'";\\|?<>~`\n]""", command):
|
|
476
|
+
return ['/bin/sh', '-c', command]
|
|
477
|
+
return command.split()
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
new = Pyjo_Server_Reload.new
|
|
481
|
+
object = Pyjo_Server_Reload
|
|
482
|
+
Reload = Pyjo_Server_Reload
|