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
Pyjo/Server/Stdio.py
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.Stdio - Standard input and output server
|
|
3
|
+
=====================================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.Stdio import Stdio
|
|
7
|
+
|
|
8
|
+
server = Stdio()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Lines of the standard input in capitals
|
|
12
|
+
@server.on('stream')
|
|
13
|
+
def stream(server, stream):
|
|
14
|
+
stream.on('read', lambda stream, chunk: stream.write(chunk.upper()))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
server.run()
|
|
18
|
+
|
|
19
|
+
# Or import the module, as in Google Python Style Guide
|
|
20
|
+
|
|
21
|
+
import Pyjo.Server.Stdio
|
|
22
|
+
|
|
23
|
+
server = Pyjo.Server.Stdio.new()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# Lines of the standard input in capitals
|
|
27
|
+
@server.on('stream')
|
|
28
|
+
def stream(server, stream):
|
|
29
|
+
stream.on('read', lambda stream, chunk: stream.write(chunk.upper()))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
server.run()
|
|
33
|
+
|
|
34
|
+
:mod:`Pyjo.Server.Stdio` serves a protocol on the standard input and output
|
|
35
|
+
of the process, as the transport stdio of MCP or LSP, or a protocol of lines
|
|
36
|
+
that inetd or systemd serves, with the stream of :mod:`Pyjo.IOLoop.Stdio`,
|
|
37
|
+
while the log goes to the standard error. It serves no HTTP: it builds no
|
|
38
|
+
transaction, and the application gives the protocol with a subscriber of the
|
|
39
|
+
event ``stream``, which a plugin or the application subscribes in the hook
|
|
40
|
+
``before_server_start``, so that the same application serves its routes with
|
|
41
|
+
another server. ::
|
|
42
|
+
|
|
43
|
+
import Pyjo.IOLoop.Messages
|
|
44
|
+
import Pyjo.Server.Stdio
|
|
45
|
+
|
|
46
|
+
# The methods of JSON-RPC, one message on each line
|
|
47
|
+
@app.hook('before_server_start')
|
|
48
|
+
def stdio(server, app):
|
|
49
|
+
if isinstance(server, Pyjo.Server.Stdio.object):
|
|
50
|
+
server.on('stream', lambda server, stream: rpc.serve(Pyjo.IOLoop.Messages.new(stream)))
|
|
51
|
+
|
|
52
|
+
The server takes the standard input and output before it emits the hook,
|
|
53
|
+
so that what the application and its plugins print while they start goes to
|
|
54
|
+
the standard error, but not what a script prints while it is loaded, before
|
|
55
|
+
its command runs.
|
|
56
|
+
|
|
57
|
+
The server runs the event loop until the stream closes, which it does once
|
|
58
|
+
the standard input has ended and the rest is written, or which a subscriber
|
|
59
|
+
of the event ``end`` of the stream does, as
|
|
60
|
+
:meth:`~Pyjo.JSON.RPC.Pyjo_JSON_RPC.serve` does once the requests in
|
|
61
|
+
progress have their responses.
|
|
62
|
+
|
|
63
|
+
The reactor watches pipes on POSIX systems, not on Windows, where the server
|
|
64
|
+
raises an exception, as the stream does.
|
|
65
|
+
|
|
66
|
+
Signals
|
|
67
|
+
-------
|
|
68
|
+
|
|
69
|
+
The :mod:`Pyjo.Server.Stdio` process can be controlled at runtime with the
|
|
70
|
+
following signals.
|
|
71
|
+
|
|
72
|
+
INT, TERM
|
|
73
|
+
~~~~~~~~~
|
|
74
|
+
|
|
75
|
+
Shut down server gracefully: end the standard input, as at its end, and wait
|
|
76
|
+
for the stream to be closed. A second ``INT``, as of Ctrl-C pressed twice,
|
|
77
|
+
shuts it down immediately, a second ``TERM`` does nothing.
|
|
78
|
+
|
|
79
|
+
QUIT
|
|
80
|
+
~~~~
|
|
81
|
+
|
|
82
|
+
Shut down server immediately.
|
|
83
|
+
|
|
84
|
+
HUP
|
|
85
|
+
~~~
|
|
86
|
+
|
|
87
|
+
Ignored, as there is nothing to reload.
|
|
88
|
+
|
|
89
|
+
Events
|
|
90
|
+
------
|
|
91
|
+
|
|
92
|
+
:mod:`Pyjo.Server.Stdio` inherits all events from :mod:`Pyjo.Server` and can
|
|
93
|
+
emit the following new ones.
|
|
94
|
+
|
|
95
|
+
stream
|
|
96
|
+
~~~~~~
|
|
97
|
+
::
|
|
98
|
+
|
|
99
|
+
server.on('stream', lambda server, stream: ...)
|
|
100
|
+
|
|
101
|
+
Emitted when the :mod:`Pyjo.IOLoop.Stdio` stream of the standard input and
|
|
102
|
+
output is ready to be served. ::
|
|
103
|
+
|
|
104
|
+
@server.on('stream')
|
|
105
|
+
def stream(server, stream):
|
|
106
|
+
stream.on('end', lambda stream: stream.close_gracefully())
|
|
107
|
+
|
|
108
|
+
See also
|
|
109
|
+
--------
|
|
110
|
+
|
|
111
|
+
:mod:`Pyjoyment.Command.stdio`, :mod:`Pyjo.IOLoop.Stdio`,
|
|
112
|
+
:mod:`Pyjo.IOLoop.Messages`, :mod:`Pyjo.Server.Daemon`, :mod:`Pyjoyment`,
|
|
113
|
+
https://mojolicious.org.
|
|
114
|
+
|
|
115
|
+
It has no counterpart in Mojolicious.
|
|
116
|
+
|
|
117
|
+
Classes
|
|
118
|
+
-------
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
import Pyjo.Exception
|
|
122
|
+
import Pyjo.IOLoop
|
|
123
|
+
import Pyjo.IOLoop.Stdio
|
|
124
|
+
import Pyjo.Server
|
|
125
|
+
|
|
126
|
+
import signal
|
|
127
|
+
import sys
|
|
128
|
+
import threading
|
|
129
|
+
|
|
130
|
+
from typing import Any
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class Pyjo_Server_Stdio(Pyjo.Server.object):
|
|
134
|
+
"""
|
|
135
|
+
:mod:`Pyjo.Server.Stdio` inherits all methods from :mod:`Pyjo.Server` and
|
|
136
|
+
implements the following new ones.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, *, ioloop: Pyjo.IOLoop.object | None = None, **kwargs: Any) -> None:
|
|
140
|
+
"""::
|
|
141
|
+
|
|
142
|
+
server = Pyjo.Server.Stdio.new()
|
|
143
|
+
|
|
144
|
+
Construct a new :mod:`Pyjo.Server.Stdio` object.
|
|
145
|
+
"""
|
|
146
|
+
super().__init__(**kwargs)
|
|
147
|
+
|
|
148
|
+
self.ioloop: Pyjo.IOLoop.object = ioloop if ioloop is not None else Pyjo.IOLoop.object.singleton()
|
|
149
|
+
"""::
|
|
150
|
+
|
|
151
|
+
loop = server.ioloop
|
|
152
|
+
server.ioloop = Pyjo.IOLoop.new()
|
|
153
|
+
|
|
154
|
+
Event loop object to use for I/O operations, defaults to the global
|
|
155
|
+
:mod:`Pyjo.IOLoop` singleton.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def run(self) -> None:
|
|
159
|
+
"""::
|
|
160
|
+
|
|
161
|
+
server.run()
|
|
162
|
+
|
|
163
|
+
Run server until the stream of the standard input and output closes,
|
|
164
|
+
and wait for the signals ``INT``, ``TERM`` and ``QUIT``.
|
|
165
|
+
"""
|
|
166
|
+
# The standard input and output are taken before the application starts, so that what it prints then goes to
|
|
167
|
+
# the standard error too
|
|
168
|
+
loop = self.ioloop
|
|
169
|
+
stdio = Pyjo.IOLoop.Stdio.new()
|
|
170
|
+
handlers: dict[signal.Signals, Any] = {}
|
|
171
|
+
interval = None
|
|
172
|
+
graceful = immediate = False
|
|
173
|
+
|
|
174
|
+
# INT and TERM end the standard input, as at its end, and QUIT stops the event loop immediately, as a second INT
|
|
175
|
+
# does, as of Ctrl-C pressed twice; the end is left to the next tick, as a handler of Python runs in the middle of
|
|
176
|
+
# what the event loop does
|
|
177
|
+
def stop(signum: int, frame: Any) -> None:
|
|
178
|
+
nonlocal graceful, immediate
|
|
179
|
+
if signum == _QUIT or (signum == signal.SIGINT and graceful):
|
|
180
|
+
immediate = True
|
|
181
|
+
loop.stop()
|
|
182
|
+
elif not graceful:
|
|
183
|
+
graceful = True
|
|
184
|
+
loop.next_tick(lambda loop: stdio.end())
|
|
185
|
+
|
|
186
|
+
def close(stdio: Any) -> None:
|
|
187
|
+
loop.stop()
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
self.app.server(self)
|
|
191
|
+
if not self.has_subscribers('stream'):
|
|
192
|
+
raise Pyjo.Exception.new(
|
|
193
|
+
'Nothing serves the standard input and output, the event "stream" has no subscriber'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Make sure the event loop can be stopped in regular intervals
|
|
197
|
+
interval = loop.recurring(1, lambda loop: None)
|
|
198
|
+
|
|
199
|
+
# A thread other than the main one cannot handle signals
|
|
200
|
+
if threading.current_thread() is threading.main_thread():
|
|
201
|
+
for name in (signal.SIGINT, signal.SIGTERM, _QUIT):
|
|
202
|
+
handlers[name] = signal.signal(name, stop)
|
|
203
|
+
handlers[signal.SIGHUP] = signal.signal(signal.SIGHUP, signal.SIG_IGN)
|
|
204
|
+
|
|
205
|
+
stdio.on('close', close)
|
|
206
|
+
loop.stream(stdio)
|
|
207
|
+
self.emit('stream', stdio)
|
|
208
|
+
if stdio.handle is not None:
|
|
209
|
+
loop.start()
|
|
210
|
+
|
|
211
|
+
# The end of the server, unless it has stopped immediately
|
|
212
|
+
if not immediate:
|
|
213
|
+
self._server_stopped()
|
|
214
|
+
finally:
|
|
215
|
+
stdio.close()
|
|
216
|
+
for name, handler in handlers.items():
|
|
217
|
+
signal.signal(name, handler)
|
|
218
|
+
if interval is not None:
|
|
219
|
+
loop.remove(interval)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Windows has no QUIT, but no stream of the standard input and output either
|
|
223
|
+
_QUIT = signal.SIGBREAK if sys.platform == 'win32' else signal.SIGQUIT
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
new = Pyjo_Server_Stdio.new
|
|
227
|
+
object = Pyjo_Server_Stdio
|
|
228
|
+
Stdio = Pyjo_Server_Stdio
|
Pyjo/Server/WSGI.py
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Server.WSGI - WSGI server
|
|
3
|
+
==============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.WSGI import WSGI
|
|
7
|
+
|
|
8
|
+
wsgi = WSGI()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@wsgi.unsubscribe('request').on('request')
|
|
12
|
+
def request(wsgi, tx):
|
|
13
|
+
# Request
|
|
14
|
+
method = tx.req.method
|
|
15
|
+
path = tx.req.url.path
|
|
16
|
+
|
|
17
|
+
# Response
|
|
18
|
+
tx.res.code = 200
|
|
19
|
+
tx.res.headers.content_type = 'text/plain'
|
|
20
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
21
|
+
|
|
22
|
+
# Resume transaction
|
|
23
|
+
tx.resume()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
app = wsgi.to_wsgi_app()
|
|
27
|
+
|
|
28
|
+
# Or import the module, as in Google Python Style Guide
|
|
29
|
+
|
|
30
|
+
import Pyjo.Server.WSGI
|
|
31
|
+
|
|
32
|
+
wsgi = Pyjo.Server.WSGI.new()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@wsgi.unsubscribe('request').on('request')
|
|
36
|
+
def request(wsgi, tx):
|
|
37
|
+
# Request
|
|
38
|
+
method = tx.req.method
|
|
39
|
+
path = tx.req.url.path
|
|
40
|
+
|
|
41
|
+
# Response
|
|
42
|
+
tx.res.code = 200
|
|
43
|
+
tx.res.headers.content_type = 'text/plain'
|
|
44
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
45
|
+
|
|
46
|
+
# Resume transaction
|
|
47
|
+
tx.resume()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
app = wsgi.to_wsgi_app()
|
|
51
|
+
|
|
52
|
+
:mod:`Pyjo.Server.WSGI` allows :mod:`Pyjoyment` applications to run on all
|
|
53
|
+
``WSGI`` compatible servers, as :pep:`3333` describes them. ::
|
|
54
|
+
|
|
55
|
+
# Serve with the server of wsgiref
|
|
56
|
+
from wsgiref.simple_server import make_server
|
|
57
|
+
|
|
58
|
+
make_server('', 3000, app).serve_forever()
|
|
59
|
+
|
|
60
|
+
The response can be given later than the ``request`` event, and its content
|
|
61
|
+
later than the response, as the event loop of :mod:`Pyjo.IOLoop` runs while
|
|
62
|
+
the server waits for them, since ``WSGI`` is synchronous. In the event loop
|
|
63
|
+
that runs already, as in an application of :mod:`Pyjo` that runs the ``WSGI``
|
|
64
|
+
application with :mod:`Pyjo.App.WSGI`, the chunks of the body are empty until
|
|
65
|
+
the response and its content are there, and ``start_response`` is called
|
|
66
|
+
before the first chunk of the content. The request is read
|
|
67
|
+
from the environment of :pep:`3333` with ``read``, a body of unknown length
|
|
68
|
+
when the server sets ``wsgi.input_terminated``, as for a chunked request. The
|
|
69
|
+
response goes without the hop-by-hop headers that :pep:`3333` forbids, so
|
|
70
|
+
chunked content is sent without its framing, which the server adds. The
|
|
71
|
+
transaction is finished when the server closes the body of the response.
|
|
72
|
+
|
|
73
|
+
WSGI servers
|
|
74
|
+
------------
|
|
75
|
+
|
|
76
|
+
* Gunicorn and Waitress: also chunked requests.
|
|
77
|
+
* uWSGI with ``--http-socket``: a chunked request reaches the application
|
|
78
|
+
without its body.
|
|
79
|
+
* Werkzeug: also chunked requests, with a ``Date`` header of its own next to
|
|
80
|
+
the one of the response.
|
|
81
|
+
|
|
82
|
+
Events
|
|
83
|
+
------
|
|
84
|
+
|
|
85
|
+
:mod:`Pyjo.Server.WSGI` inherits all events from :mod:`Pyjo.Server`.
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
|
|
90
|
+
:mod:`Pyjo.Server.WSGI` inherits all attributes from :mod:`Pyjo.Server`.
|
|
91
|
+
|
|
92
|
+
See also
|
|
93
|
+
--------
|
|
94
|
+
|
|
95
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
96
|
+
|
|
97
|
+
Ported from `Mojo::Server::PSGI <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/PSGI.pm>`_
|
|
98
|
+
of Mojolicious 9.49.
|
|
99
|
+
|
|
100
|
+
Classes
|
|
101
|
+
-------
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
import Pyjo.Content.Single
|
|
105
|
+
import Pyjo.Exception
|
|
106
|
+
import Pyjo.IOLoop
|
|
107
|
+
import Pyjo.Server
|
|
108
|
+
import Pyjo.Transaction
|
|
109
|
+
import Pyjo.Transaction.HTTP
|
|
110
|
+
|
|
111
|
+
from Pyjo.Util import url_escape
|
|
112
|
+
|
|
113
|
+
import wsgiref.util
|
|
114
|
+
|
|
115
|
+
from collections.abc import Iterable, Iterator
|
|
116
|
+
from typing import Any, Self
|
|
117
|
+
from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Pyjo_Server_WSGI(Pyjo.Server.object):
|
|
121
|
+
"""
|
|
122
|
+
:mod:`Pyjo.Server.WSGI` inherits all methods from :mod:`Pyjo.Server` and
|
|
123
|
+
implements the following new ones.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
# reportIncompatibleMethodOverride "Method "run" overrides class "Pyjo_Server" in an incompatible manner": the run of
|
|
127
|
+
# Mojo::Server::PSGI takes the environment, as the subclasses of Mojo::Server take what they need
|
|
128
|
+
def run( # pyright: ignore[reportIncompatibleMethodOverride]
|
|
129
|
+
self, environ: WSGIEnvironment
|
|
130
|
+
) -> tuple[str, list[tuple[str, str]], Iterable[bytes]]:
|
|
131
|
+
"""::
|
|
132
|
+
|
|
133
|
+
status, headers, body = wsgi.run(environ)
|
|
134
|
+
|
|
135
|
+
Run ``WSGI``: the status, the headers and the body of the response,
|
|
136
|
+
whose ``close`` finishes the transaction.
|
|
137
|
+
"""
|
|
138
|
+
tx, resumed = self._request(environ)
|
|
139
|
+
while not resumed:
|
|
140
|
+
_one_tick()
|
|
141
|
+
return self._response(tx)
|
|
142
|
+
|
|
143
|
+
# Renamed from Mojolicious: to_psgi_app
|
|
144
|
+
def to_wsgi_app(self) -> WSGIApplication:
|
|
145
|
+
"""::
|
|
146
|
+
|
|
147
|
+
app = wsgi.to_wsgi_app()
|
|
148
|
+
|
|
149
|
+
Turn :mod:`Pyjoyment` application into ``WSGI`` application.
|
|
150
|
+
"""
|
|
151
|
+
# Preload application and wrap it
|
|
152
|
+
self.app.server(self)
|
|
153
|
+
|
|
154
|
+
def application(environ: WSGIEnvironment, start_response: StartResponse) -> Iterable[bytes]:
|
|
155
|
+
# Not in Mojolicious: in the event loop that runs already, which cannot run for the server, the response is
|
|
156
|
+
# given when it is there, with empty chunks of the body before it
|
|
157
|
+
# TODO: the event loop of an ASGI server, which Pyjo.Server.ASGI runs without the reactor, so that it is not running
|
|
158
|
+
if Pyjo.IOLoop.is_running():
|
|
159
|
+
tx, resumed = self._request(environ)
|
|
160
|
+
return _Later(self, tx, resumed, start_response)
|
|
161
|
+
|
|
162
|
+
status, headers, body = self.run(environ)
|
|
163
|
+
start_response(status, headers)
|
|
164
|
+
return body
|
|
165
|
+
|
|
166
|
+
return application
|
|
167
|
+
|
|
168
|
+
# The transaction of the request, whose response has been given once the list is not empty
|
|
169
|
+
def _request(self, environ: WSGIEnvironment) -> tuple[Pyjo.Transaction.HTTP.object, list[bool]]:
|
|
170
|
+
env = _environ(environ)
|
|
171
|
+
wsgi_input = environ.get('wsgi.input')
|
|
172
|
+
|
|
173
|
+
# Not in Mojolicious: a request body of unknown length, as a server that removes the chunked framing marks it
|
|
174
|
+
body = None
|
|
175
|
+
if not env.get('CONTENT_LENGTH') and wsgi_input is not None and environ.get('wsgi.input_terminated'):
|
|
176
|
+
body = b''
|
|
177
|
+
while chunk := wsgi_input.read(131072):
|
|
178
|
+
body += chunk
|
|
179
|
+
if len(body):
|
|
180
|
+
env['CONTENT_LENGTH'] = str(len(body))
|
|
181
|
+
|
|
182
|
+
tx = self.build_tx()
|
|
183
|
+
req = tx.req.parse(env)
|
|
184
|
+
tx.local_port = int(port) if (port := environ.get('SERVER_PORT')) else None
|
|
185
|
+
tx.remote_address = environ.get('REMOTE_ADDR')
|
|
186
|
+
|
|
187
|
+
# Request body (may block if we try to read too much)
|
|
188
|
+
if body is not None:
|
|
189
|
+
req.parse(body)
|
|
190
|
+
else:
|
|
191
|
+
length = int(env.get('CONTENT_LENGTH') or 0)
|
|
192
|
+
while not req.is_finished and wsgi_input is not None:
|
|
193
|
+
chunk = wsgi_input.read(length if length and length < 131072 else 131072)
|
|
194
|
+
if not chunk:
|
|
195
|
+
break
|
|
196
|
+
req.parse(chunk)
|
|
197
|
+
length -= len(chunk)
|
|
198
|
+
if length <= 0:
|
|
199
|
+
break
|
|
200
|
+
|
|
201
|
+
# Unlike Mojolicious: a response given later than the request event, as the one of an application that a route
|
|
202
|
+
# embeds, is waited for while the event loop runs, where Mojo takes the response at the end of the event
|
|
203
|
+
resumed: list[bool] = []
|
|
204
|
+
|
|
205
|
+
def resume(tx: Pyjo.Transaction.object) -> None:
|
|
206
|
+
resumed.append(True)
|
|
207
|
+
|
|
208
|
+
tx.once('resume', resume)
|
|
209
|
+
self.emit('request', tx)
|
|
210
|
+
return tx, resumed
|
|
211
|
+
|
|
212
|
+
def _response(
|
|
213
|
+
self, tx: Pyjo.Transaction.HTTP.object, wait: bool = True
|
|
214
|
+
) -> tuple[str, list[tuple[str, str]], '_IO']:
|
|
215
|
+
# Response headers
|
|
216
|
+
res = tx.res.fix_headers()
|
|
217
|
+
# Unlike Mojolicious: without the hop-by-hop headers that PEP 3333 forbids, so chunked content goes without its
|
|
218
|
+
# framing, which the server adds
|
|
219
|
+
headers = [(name, value) for name, value in res.headers.to_list() if not wsgiref.util.is_hop_by_hop(name)]
|
|
220
|
+
|
|
221
|
+
# WSGI response
|
|
222
|
+
# Unlike Mojolicious: the status of WSGI has its reason phrase
|
|
223
|
+
status = f'{res.code or 404} {res.message or res.default_message()}'
|
|
224
|
+
return status, headers, _IO(tx, tx.is_empty, res.content.is_chunked, wait)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# The body of the response, which the server iterates and closes
|
|
228
|
+
class _IO(Iterator[bytes]):
|
|
229
|
+
def __init__(self, tx: Pyjo.Transaction.object, empty: bool, chunked: bool, wait: bool = True) -> None:
|
|
230
|
+
self._tx = tx
|
|
231
|
+
self._empty = empty
|
|
232
|
+
self._wait = wait
|
|
233
|
+
self._offset = 0
|
|
234
|
+
# Not in Mojolicious: chunked content without its framing
|
|
235
|
+
self._unchunk = _Unchunk() if chunked else None
|
|
236
|
+
|
|
237
|
+
def __iter__(self) -> Self:
|
|
238
|
+
return self
|
|
239
|
+
|
|
240
|
+
# Renamed from Mojolicious: getline, as the server iterates the body
|
|
241
|
+
def __next__(self) -> bytes:
|
|
242
|
+
# Empty
|
|
243
|
+
if self._empty:
|
|
244
|
+
raise StopIteration
|
|
245
|
+
|
|
246
|
+
while True:
|
|
247
|
+
# Not in Mojolicious: no content yet, so the event loop runs once before it tries again, where Mojo
|
|
248
|
+
# returns an empty chunk for the server to try again, as it does in the event loop that runs already
|
|
249
|
+
chunk = self._tx.res.get_body_chunk(self._offset)
|
|
250
|
+
if chunk is None:
|
|
251
|
+
if not self._wait:
|
|
252
|
+
return b''
|
|
253
|
+
_one_tick()
|
|
254
|
+
continue
|
|
255
|
+
|
|
256
|
+
# End of content
|
|
257
|
+
if not len(chunk):
|
|
258
|
+
raise StopIteration
|
|
259
|
+
|
|
260
|
+
self._offset += len(chunk)
|
|
261
|
+
if self._unchunk is not None:
|
|
262
|
+
chunk = self._unchunk.parse(chunk)
|
|
263
|
+
if len(chunk):
|
|
264
|
+
return bytes(chunk)
|
|
265
|
+
|
|
266
|
+
# Finish transaction
|
|
267
|
+
def close(self) -> None:
|
|
268
|
+
self._tx.closed()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# Not in Mojolicious: the body of a response given in the event loop that runs already, whose chunks are empty until the
|
|
272
|
+
# response is there, which start_response gets before the first chunk of its content
|
|
273
|
+
class _Later(Iterator[bytes]):
|
|
274
|
+
def __init__(
|
|
275
|
+
self,
|
|
276
|
+
server: Pyjo_Server_WSGI,
|
|
277
|
+
tx: Pyjo.Transaction.HTTP.object,
|
|
278
|
+
resumed: list[bool],
|
|
279
|
+
start_response: StartResponse,
|
|
280
|
+
) -> None:
|
|
281
|
+
self._body: _IO | None = None
|
|
282
|
+
self._resumed = resumed
|
|
283
|
+
self._server = server
|
|
284
|
+
self._start_response = start_response
|
|
285
|
+
self._tx = tx
|
|
286
|
+
|
|
287
|
+
def __iter__(self) -> Self:
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def __next__(self) -> bytes:
|
|
291
|
+
if (body := self._body) is None:
|
|
292
|
+
if not self._resumed:
|
|
293
|
+
return b''
|
|
294
|
+
status, headers, body = self._server._response(self._tx, wait=False) # pyright: ignore[reportPrivateUsage]
|
|
295
|
+
self._start_response(status, headers)
|
|
296
|
+
self._body = body
|
|
297
|
+
return next(body)
|
|
298
|
+
|
|
299
|
+
def close(self) -> None:
|
|
300
|
+
if (body := self._body) is not None:
|
|
301
|
+
body.close()
|
|
302
|
+
else:
|
|
303
|
+
self._tx.closed()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# Not in Mojolicious: the event loop runs once while the server waits for the response, but not from inside itself, as
|
|
307
|
+
# in an application of Pyjo that runs the WSGI application, where the asyncio reactor would return at once, and another
|
|
308
|
+
# reactor would run the callbacks of other connections in the middle of the call of the application
|
|
309
|
+
def _one_tick() -> None:
|
|
310
|
+
if Pyjo.IOLoop.is_running():
|
|
311
|
+
raise Pyjo.Exception.new('Response cannot be waited for while the event loop is running')
|
|
312
|
+
Pyjo.IOLoop.one_tick()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# Chunked content without its framing, as in Pyjo.Server.ASGI
|
|
316
|
+
class _Unchunk:
|
|
317
|
+
def __init__(self) -> None:
|
|
318
|
+
self._chunks: list[bytes] = []
|
|
319
|
+
self._parser = Pyjo.Content.Single.new()
|
|
320
|
+
self._parser.unsubscribe('read').on('read', self._read)
|
|
321
|
+
self._parser.parse(b'Transfer-Encoding: chunked\x0d\x0a\x0d\x0a')
|
|
322
|
+
|
|
323
|
+
def _read(self, content: Pyjo.Content.Single.object, chunk: bytes) -> None:
|
|
324
|
+
self._chunks.append(chunk)
|
|
325
|
+
|
|
326
|
+
def parse(self, chunk: bytes) -> bytes:
|
|
327
|
+
self._parser.parse(chunk)
|
|
328
|
+
chunks, self._chunks = self._chunks, []
|
|
329
|
+
return b''.join(chunks)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# Not in Mojolicious: the environment of PEP 3333 as the CGI environment that Pyjo.Message.Request parses
|
|
333
|
+
def _environ(environ: WSGIEnvironment) -> dict[str, Any]:
|
|
334
|
+
env = dict(environ)
|
|
335
|
+
|
|
336
|
+
# A path is bytes as the characters 0-255, which a path of Pyjoyment takes as characters, so the bytes beyond ASCII
|
|
337
|
+
# are percent-encoded, as the path is escaped
|
|
338
|
+
for key in ('PATH_INFO', 'SCRIPT_NAME'):
|
|
339
|
+
if key in env:
|
|
340
|
+
env[key] = url_escape(env[key].encode('latin-1'), rb'\x80-\xff').decode('ascii')
|
|
341
|
+
|
|
342
|
+
# The server removes the chunked framing of a request
|
|
343
|
+
env.pop('HTTP_TRANSFER_ENCODING', None)
|
|
344
|
+
return env
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
new = Pyjo_Server_WSGI.new
|
|
348
|
+
object = Pyjo_Server_WSGI
|
|
349
|
+
WSGI = Pyjo_Server_WSGI
|