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/App/WSGI.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.App.WSGI - WSGI application
|
|
3
|
+
================================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.App.WSGI import WSGI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def hello(environ, start_response):
|
|
10
|
+
start_response('200 OK', [('Content-Type', 'text/plain')])
|
|
11
|
+
return [b'Hello World!']
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
app = WSGI(app=hello)
|
|
15
|
+
|
|
16
|
+
# Or import the module, as in Google Python Style Guide
|
|
17
|
+
|
|
18
|
+
import Pyjo.App.WSGI
|
|
19
|
+
|
|
20
|
+
app = Pyjo.App.WSGI.new(app=hello)
|
|
21
|
+
|
|
22
|
+
# Mount it in an application of Pyjoyment
|
|
23
|
+
import Pyjoyment
|
|
24
|
+
|
|
25
|
+
pyjoyment = Pyjoyment.new()
|
|
26
|
+
pyjoyment.plugin('Mount', {'/hello': app})
|
|
27
|
+
|
|
28
|
+
:mod:`Pyjo.App.WSGI` is an application of :mod:`Pyjo` that runs a ``WSGI``
|
|
29
|
+
application, as :pep:`3333` describes it, so that a server of :mod:`Pyjo`
|
|
30
|
+
serves it, a route of :mod:`Pyjoyment` embeds it, as
|
|
31
|
+
:mod:`Pyjoyment.Plugin.Mount` does, and :mod:`Pyjo.Test.Pyjo` tests it.
|
|
32
|
+
|
|
33
|
+
The part of the path that a route embeds the application under is
|
|
34
|
+
``SCRIPT_NAME``, after the base path of the request, and the rest of the path is
|
|
35
|
+
``PATH_INFO``. A route that embeds the application at a part of the path that
|
|
36
|
+
does not end with a slash, as ``/php`` of ``/phpx``, which ``SCRIPT_NAME``
|
|
37
|
+
cannot be, does not find it. The body of the request is ``wsgi.input``, of the length of
|
|
38
|
+
``CONTENT_LENGTH``, also when the request was chunked.
|
|
39
|
+
|
|
40
|
+
A ``WSGI`` application blocks, and it runs in the event loop, which waits for
|
|
41
|
+
it, so that the workers of :mod:`Pyjo.Server.Prefork` and
|
|
42
|
+
:mod:`Pyjo.Server.Prethread` serve it at the same time, as for any blocking
|
|
43
|
+
operation, with
|
|
44
|
+
:attr:`~Pyjo.Server.Daemon.Pyjo_Server_Daemon.max_clients` as low as ``1``. A
|
|
45
|
+
response that the application has given whole is written with its
|
|
46
|
+
``Content-Length``. Another one is written as the application gives it, each
|
|
47
|
+
chunk in a tick of the event loop once the one before has been written, and
|
|
48
|
+
after an empty chunk, which the application gives while it has none yet, in the
|
|
49
|
+
next cycle of the event loop, with the chunked transfer encoding when the
|
|
50
|
+
application does not give its ``Content-Length``.
|
|
51
|
+
|
|
52
|
+
An exception of the application before its response starts is a response
|
|
53
|
+
``500``, the exception page of :mod:`Pyjoyment` in a route that embeds it, and
|
|
54
|
+
an exception after that is logged and closes the connection.
|
|
55
|
+
|
|
56
|
+
Attributes
|
|
57
|
+
----------
|
|
58
|
+
|
|
59
|
+
:mod:`Pyjo.App.WSGI` inherits all attributes from :mod:`Pyjo.App` and
|
|
60
|
+
implements the following new ones.
|
|
61
|
+
|
|
62
|
+
See also
|
|
63
|
+
--------
|
|
64
|
+
|
|
65
|
+
:mod:`Pyjoyment`, :mod:`Pyjoyment.Plugin.Mount`, :mod:`Pyjo.Server.WSGI`,
|
|
66
|
+
https://mojolicious.org.
|
|
67
|
+
|
|
68
|
+
It has no counterpart in Mojolicious, it is inspired by
|
|
69
|
+
`Mojolicious::Plugin::MountPSGI <https://metacpan.org/pod/Mojolicious::Plugin::MountPSGI>`_ of CPAN; it is the
|
|
70
|
+
reverse of :mod:`Pyjo.Server.WSGI`.
|
|
71
|
+
|
|
72
|
+
Classes
|
|
73
|
+
-------
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
import Pyjo.App
|
|
77
|
+
import Pyjo.Exception
|
|
78
|
+
import Pyjo.IOLoop
|
|
79
|
+
import Pyjo.Server
|
|
80
|
+
import Pyjo.Server.CGI
|
|
81
|
+
import Pyjo.Server.Prefork
|
|
82
|
+
import Pyjo.Server.Prethread
|
|
83
|
+
import Pyjo.Transaction
|
|
84
|
+
|
|
85
|
+
import io
|
|
86
|
+
import sys
|
|
87
|
+
import traceback
|
|
88
|
+
import weakref
|
|
89
|
+
import wsgiref.util
|
|
90
|
+
|
|
91
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
92
|
+
from typing import Any, cast
|
|
93
|
+
from wsgiref.types import WSGIApplication, WSGIEnvironment
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Pyjo_App_WSGI(Pyjo.App.object):
|
|
97
|
+
"""
|
|
98
|
+
:mod:`Pyjo.App.WSGI` inherits all methods from :mod:`Pyjo.App` and
|
|
99
|
+
implements the following new ones.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
def __init__(self, attrs: dict[str, Any] | None = None, /, **kwargs: Any) -> None:
|
|
103
|
+
"""::
|
|
104
|
+
|
|
105
|
+
app = Pyjo.App.WSGI.new(app=application)
|
|
106
|
+
|
|
107
|
+
Construct a new :mod:`Pyjo.App.WSGI` object.
|
|
108
|
+
"""
|
|
109
|
+
self.app: WSGIApplication | None = None
|
|
110
|
+
"""::
|
|
111
|
+
|
|
112
|
+
application = app.app
|
|
113
|
+
app.app = application
|
|
114
|
+
|
|
115
|
+
The ``WSGI`` application to run.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
self._server: weakref.ref[Pyjo.Server.object] | None = None
|
|
119
|
+
super().__init__(attrs, **kwargs)
|
|
120
|
+
|
|
121
|
+
def handler(self, tx: Any) -> None:
|
|
122
|
+
"""::
|
|
123
|
+
|
|
124
|
+
app.handler(Pyjo.Transaction.HTTP.new())
|
|
125
|
+
app.handler(Pyjoyment.Controller.new())
|
|
126
|
+
|
|
127
|
+
Run the application for the request of the transaction, or of the
|
|
128
|
+
controller of a route that embeds the application, and write its
|
|
129
|
+
response.
|
|
130
|
+
"""
|
|
131
|
+
if (application := self.app) is None:
|
|
132
|
+
raise Pyjo.Exception.new('No WSGI application')
|
|
133
|
+
c = None
|
|
134
|
+
if not isinstance(tx, Pyjo.Transaction.object):
|
|
135
|
+
c = tx
|
|
136
|
+
tx = c.tx
|
|
137
|
+
if not Pyjo.App._within(c): # pyright: ignore[reportPrivateUsage]
|
|
138
|
+
return c.helpers.reply.not_found()
|
|
139
|
+
c.render_later()
|
|
140
|
+
_Call(self, application, tx, c).start()
|
|
141
|
+
|
|
142
|
+
def server(self, server: Pyjo.Server.object) -> None:
|
|
143
|
+
"""::
|
|
144
|
+
|
|
145
|
+
app.server(Pyjo.Server.Prefork.new())
|
|
146
|
+
|
|
147
|
+
Called by the server before it starts, and by
|
|
148
|
+
:mod:`Pyjoyment.Plugin.Mount` for the application that it mounts, so
|
|
149
|
+
that ``wsgi.multithread``, ``wsgi.multiprocess`` and ``wsgi.run_once``
|
|
150
|
+
say how the server runs the application: in the threads of the workers
|
|
151
|
+
of :mod:`Pyjo.Server.Prethread`, in the processes of the workers of
|
|
152
|
+
:mod:`Pyjo.Server.Prefork`, or in a process for each request of
|
|
153
|
+
:mod:`Pyjo.Server.CGI`. Under a ``WSGI`` server, as
|
|
154
|
+
:mod:`Pyjo.Server.WSGI` runs an application of :mod:`Pyjoyment`, they
|
|
155
|
+
are those of the environment of the request, which the ``WSGI``
|
|
156
|
+
server sets.
|
|
157
|
+
"""
|
|
158
|
+
self._server = weakref.ref(server)
|
|
159
|
+
|
|
160
|
+
# wsgi.multithread, wsgi.multiprocess and wsgi.run_once for the server that runs the application, or of the WSGI
|
|
161
|
+
# server, whose environment of the request Pyjo.Server.WSGI keeps
|
|
162
|
+
def _model(self, tx: Any) -> tuple[bool, bool, bool]:
|
|
163
|
+
if 'wsgi.multiprocess' in (env := tx.req.env):
|
|
164
|
+
return bool(env.get('wsgi.multithread')), bool(env['wsgi.multiprocess']), bool(env.get('wsgi.run_once'))
|
|
165
|
+
server = ref() if (ref := self._server) is not None else None
|
|
166
|
+
if isinstance(server, Pyjo.Server.Prethread.object):
|
|
167
|
+
return server.workers > 1, False, False
|
|
168
|
+
if isinstance(server, Pyjo.Server.Prefork.object):
|
|
169
|
+
return False, server.workers > 1, False
|
|
170
|
+
if isinstance(server, Pyjo.Server.CGI.object):
|
|
171
|
+
return False, True, True
|
|
172
|
+
return False, False, False
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# A call of the application, whose response is written as the application gives it
|
|
176
|
+
class _Call:
|
|
177
|
+
def __init__(self, app: Pyjo_App_WSGI, application: WSGIApplication, tx: Any, c: Any) -> None:
|
|
178
|
+
self.app = app
|
|
179
|
+
self.application = application
|
|
180
|
+
self.c = c
|
|
181
|
+
self.tx = tx
|
|
182
|
+
self.response = _Response()
|
|
183
|
+
self.busy = False
|
|
184
|
+
self.chunked = False
|
|
185
|
+
self.closed = False
|
|
186
|
+
self.done = False
|
|
187
|
+
|
|
188
|
+
def start(self) -> None:
|
|
189
|
+
self.tx.on('finish', self._finish)
|
|
190
|
+
try:
|
|
191
|
+
self._step(self.response.start(self.application, _environ(self.tx, self.c, self.app._model(self.tx)))) # pyright: ignore[reportPrivateUsage]
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self._error(e)
|
|
194
|
+
|
|
195
|
+
def _close(self) -> None:
|
|
196
|
+
self.done = True
|
|
197
|
+
try:
|
|
198
|
+
self.response.close()
|
|
199
|
+
except Exception as e:
|
|
200
|
+
self.app.log.error(f'WSGI application failed to close: {_message(e)}')
|
|
201
|
+
|
|
202
|
+
# End of the response
|
|
203
|
+
def _end(self) -> None:
|
|
204
|
+
content = self.tx.res.content
|
|
205
|
+
(content.write_chunk if self.chunked else content.write)(b'')
|
|
206
|
+
self._rendered()
|
|
207
|
+
|
|
208
|
+
def _error(self, e: BaseException) -> None:
|
|
209
|
+
if not self.done:
|
|
210
|
+
self._close()
|
|
211
|
+
message = _message(e)
|
|
212
|
+
|
|
213
|
+
# The response has started, so the connection is closed, as the response cannot change
|
|
214
|
+
if self.response.started:
|
|
215
|
+
self.app.log.error(f'WSGI application failed: {message}')
|
|
216
|
+
if (connection := self.tx.connection) is not None and (
|
|
217
|
+
stream := Pyjo.IOLoop.stream(connection)
|
|
218
|
+
) is not None:
|
|
219
|
+
stream.close()
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
# The exception page of Pyjoyment
|
|
223
|
+
if (c := self.c) is not None:
|
|
224
|
+
c.helpers.reply.exception(e)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
self.app.log.error(f'WSGI application failed: {message}')
|
|
228
|
+
res = self.tx.res
|
|
229
|
+
res.code = 500
|
|
230
|
+
res.headers.content_type = 'text/plain'
|
|
231
|
+
res.body = b'Internal Server Error'
|
|
232
|
+
self.tx.resume()
|
|
233
|
+
|
|
234
|
+
# The end of the transaction, also when the connection is gone before the end of the response
|
|
235
|
+
def _finish(self, *args: Any) -> None:
|
|
236
|
+
self.closed = True
|
|
237
|
+
if not self.busy and not self.done:
|
|
238
|
+
self._close()
|
|
239
|
+
|
|
240
|
+
def _headers(self, chunks: list[bytes] | None) -> None:
|
|
241
|
+
response = self.response
|
|
242
|
+
if response.status is None:
|
|
243
|
+
raise Pyjo.Exception.new('WSGI application did not call start_response')
|
|
244
|
+
res = self.tx.res
|
|
245
|
+
code, _, message = response.status.partition(' ')
|
|
246
|
+
res.code = int(code)
|
|
247
|
+
res.message = message or None
|
|
248
|
+
headers = res.headers
|
|
249
|
+
for name, value in response.headers:
|
|
250
|
+
if not wsgiref.util.is_hop_by_hop(name):
|
|
251
|
+
headers.add(name, value)
|
|
252
|
+
response.started = True
|
|
253
|
+
|
|
254
|
+
# Whole response
|
|
255
|
+
if response.iterator is None:
|
|
256
|
+
self.done = True
|
|
257
|
+
res.body = b''.join(chunks or [])
|
|
258
|
+
return self._rendered()
|
|
259
|
+
|
|
260
|
+
# Response as the application gives it, with the headers only for a HEAD request
|
|
261
|
+
self.chunked = headers.content_length is None and self.tx.req.version == '1.1'
|
|
262
|
+
if self.tx.is_empty:
|
|
263
|
+
self._close()
|
|
264
|
+
return self._end()
|
|
265
|
+
self._step(chunks)
|
|
266
|
+
|
|
267
|
+
# A body without a chunk yet tries again after the events of the next cycle of the event loop
|
|
268
|
+
def _later(self) -> None:
|
|
269
|
+
self.busy = True
|
|
270
|
+
Pyjo.IOLoop.timer(0, self._tick)
|
|
271
|
+
|
|
272
|
+
# The next chunks of the body once the chunk before has been written, in the next tick of the event loop, so that the
|
|
273
|
+
# application is not called in the middle of what the server does
|
|
274
|
+
def _next(self, *args: Any) -> None:
|
|
275
|
+
self.busy = True
|
|
276
|
+
Pyjo.IOLoop.next_tick(self._tick)
|
|
277
|
+
|
|
278
|
+
def _rendered(self) -> None:
|
|
279
|
+
if (c := self.c) is not None:
|
|
280
|
+
c.rendered()
|
|
281
|
+
else:
|
|
282
|
+
self.tx.resume()
|
|
283
|
+
|
|
284
|
+
# What the application has given: the chunks of the body, none yet, or the end of the body
|
|
285
|
+
def _step(self, chunks: list[bytes] | None) -> None:
|
|
286
|
+
response = self.response
|
|
287
|
+
if not response.started:
|
|
288
|
+
if response.status is None and response.iterator is not None and chunks is not None and not len(chunks):
|
|
289
|
+
return self._later()
|
|
290
|
+
return self._headers(chunks)
|
|
291
|
+
|
|
292
|
+
if chunks is None:
|
|
293
|
+
self.done = True
|
|
294
|
+
return self._end()
|
|
295
|
+
if not len(chunks):
|
|
296
|
+
return self._later()
|
|
297
|
+
self._write(chunks)
|
|
298
|
+
|
|
299
|
+
def _tick(self, *args: Any) -> None:
|
|
300
|
+
self.busy = False
|
|
301
|
+
if self.closed:
|
|
302
|
+
if not self.done:
|
|
303
|
+
self._close()
|
|
304
|
+
return
|
|
305
|
+
try:
|
|
306
|
+
self._step(self.response.next())
|
|
307
|
+
except Exception as e:
|
|
308
|
+
self._error(e)
|
|
309
|
+
|
|
310
|
+
def _write(self, chunks: list[bytes]) -> None:
|
|
311
|
+
content = self.tx.res.content
|
|
312
|
+
(content.write_chunk if self.chunked else content.write)(b''.join(chunks), self._next)
|
|
313
|
+
self._rendered()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# What the application gives: the status and the headers of start_response, the chunks of the body, and
|
|
317
|
+
# the iterator of the rest of the body
|
|
318
|
+
class _Response:
|
|
319
|
+
def __init__(self) -> None:
|
|
320
|
+
self.headers: list[tuple[str, str]] = []
|
|
321
|
+
self.iterator: Iterator[bytes] | None = None
|
|
322
|
+
self.result: Iterable[bytes] | None = None
|
|
323
|
+
self.started = False
|
|
324
|
+
self.status: str | None = None
|
|
325
|
+
self.written: list[bytes] = []
|
|
326
|
+
|
|
327
|
+
def close(self) -> None:
|
|
328
|
+
if callable(close := getattr(self.result, 'close', None)):
|
|
329
|
+
close()
|
|
330
|
+
|
|
331
|
+
# The chunks of the next chunk of the body, with what the write of start_response has written, none yet for an empty
|
|
332
|
+
# one, as the application may call start_response when its body is iterated, or none at the end of the body
|
|
333
|
+
def next(self) -> list[bytes] | None:
|
|
334
|
+
if (iterator := self.iterator) is None:
|
|
335
|
+
return None
|
|
336
|
+
try:
|
|
337
|
+
chunk = next(iterator)
|
|
338
|
+
chunks = self.written + ([bytes(chunk)] if len(chunk) else [])
|
|
339
|
+
except StopIteration:
|
|
340
|
+
self.iterator = None
|
|
341
|
+
self.close()
|
|
342
|
+
chunks = self.written
|
|
343
|
+
except BaseException:
|
|
344
|
+
self.close()
|
|
345
|
+
raise
|
|
346
|
+
self.written = []
|
|
347
|
+
return chunks if len(chunks) or self.iterator is not None else None
|
|
348
|
+
|
|
349
|
+
def start(self, application: WSGIApplication, environ: WSGIEnvironment) -> list[bytes] | None:
|
|
350
|
+
result = self.result = application(environ, self.start_response)
|
|
351
|
+
|
|
352
|
+
# A body in a list is whole
|
|
353
|
+
if isinstance(result, list | tuple):
|
|
354
|
+
chunks = [*self.written, *(bytes(chunk) for chunk in cast(list[bytes], result))]
|
|
355
|
+
self.written = []
|
|
356
|
+
self.close()
|
|
357
|
+
return chunks
|
|
358
|
+
|
|
359
|
+
try:
|
|
360
|
+
self.iterator = iter(result)
|
|
361
|
+
except BaseException:
|
|
362
|
+
self.close()
|
|
363
|
+
raise
|
|
364
|
+
return self.next()
|
|
365
|
+
|
|
366
|
+
def start_response(
|
|
367
|
+
self, status: str, headers: list[tuple[str, str]], exc_info: Any = None
|
|
368
|
+
) -> Callable[[bytes], Any]:
|
|
369
|
+
if exc_info is not None:
|
|
370
|
+
try:
|
|
371
|
+
if self.started:
|
|
372
|
+
raise exc_info[1].with_traceback(exc_info[2])
|
|
373
|
+
finally:
|
|
374
|
+
exc_info = None
|
|
375
|
+
elif self.status is not None:
|
|
376
|
+
raise AssertionError('start_response has already been called')
|
|
377
|
+
self.status, self.headers = status, list(headers)
|
|
378
|
+
return self.write
|
|
379
|
+
|
|
380
|
+
def write(self, data: bytes) -> None:
|
|
381
|
+
self.written.append(bytes(data))
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
# The body of the request, which the application reads
|
|
385
|
+
class _Input(io.RawIOBase):
|
|
386
|
+
def __init__(self, content: Any) -> None:
|
|
387
|
+
self._content = content
|
|
388
|
+
self._offset = 0
|
|
389
|
+
|
|
390
|
+
def readable(self) -> bool:
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
def readinto(self, buffer: Any, /) -> int:
|
|
394
|
+
chunk = self._content.get_body_chunk(self._offset)
|
|
395
|
+
if not chunk:
|
|
396
|
+
return 0
|
|
397
|
+
size = min(len(buffer), len(chunk))
|
|
398
|
+
buffer[:size] = chunk[:size]
|
|
399
|
+
self._offset += size
|
|
400
|
+
return size
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
# The environment of PEP 3333 for the request of the transaction: the one of CGI, with the keys of WSGI
|
|
404
|
+
def _environ(tx: Any, c: Any, model: tuple[bool, bool, bool]) -> WSGIEnvironment:
|
|
405
|
+
multithread, multiprocess, run_once = model
|
|
406
|
+
environ: WSGIEnvironment = {
|
|
407
|
+
**Pyjo.App._cgi(tx, c), # pyright: ignore[reportPrivateUsage]
|
|
408
|
+
'wsgi.version': (1, 0),
|
|
409
|
+
'wsgi.url_scheme': 'https' if tx.req.is_secure else 'http',
|
|
410
|
+
'wsgi.input': io.BufferedReader(_Input(tx.req.content)),
|
|
411
|
+
'wsgi.errors': sys.stderr,
|
|
412
|
+
'wsgi.multithread': multithread,
|
|
413
|
+
'wsgi.multiprocess': multiprocess,
|
|
414
|
+
'wsgi.run_once': run_once,
|
|
415
|
+
'wsgi.input_terminated': True,
|
|
416
|
+
}
|
|
417
|
+
return environ
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
# An exception with its traceback, for the log
|
|
421
|
+
def _message(e: BaseException) -> str:
|
|
422
|
+
return ''.join(traceback.format_exception(e)).rstrip()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
new = Pyjo_App_WSGI.new
|
|
426
|
+
object = Pyjo_App_WSGI
|
|
427
|
+
WSGI = Pyjo_App_WSGI
|