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/ASGI.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
r"""
|
|
2
|
+
Pyjo.Server.ASGI - ASGI server
|
|
3
|
+
==============================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Server.ASGI import ASGI
|
|
7
|
+
|
|
8
|
+
asgi = ASGI()
|
|
9
|
+
asgi.unsubscribe('request')
|
|
10
|
+
|
|
11
|
+
@asgi.on('request')
|
|
12
|
+
def request(asgi, 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
|
+
app = asgi.to_asgi_app()
|
|
26
|
+
|
|
27
|
+
# Or import the module, as in Google Python Style Guide
|
|
28
|
+
|
|
29
|
+
import Pyjo.Server.ASGI
|
|
30
|
+
|
|
31
|
+
asgi = Pyjo.Server.ASGI.new()
|
|
32
|
+
asgi.unsubscribe('request')
|
|
33
|
+
|
|
34
|
+
@asgi.on('request')
|
|
35
|
+
def request(asgi, tx):
|
|
36
|
+
# Request
|
|
37
|
+
method = tx.req.method
|
|
38
|
+
path = tx.req.url.path
|
|
39
|
+
|
|
40
|
+
# Response
|
|
41
|
+
tx.res.code = 200
|
|
42
|
+
tx.res.headers.content_type = 'text/plain'
|
|
43
|
+
tx.res.body = f'{method} request for {path}!'.encode()
|
|
44
|
+
|
|
45
|
+
# Resume transaction
|
|
46
|
+
tx.resume()
|
|
47
|
+
|
|
48
|
+
app = asgi.to_asgi_app()
|
|
49
|
+
|
|
50
|
+
# uvicorn myapp:app
|
|
51
|
+
# hypercorn myapp:app
|
|
52
|
+
|
|
53
|
+
:mod:`Pyjo.Server.ASGI` allows :mod:`Pyjoyment` applications to run on all
|
|
54
|
+
`ASGI <https://asgi.readthedocs.io>`_ compatible servers, with HTTP,
|
|
55
|
+
WebSocket and lifespan connections.
|
|
56
|
+
|
|
57
|
+
The application runs in the event loop of the ASGI server: the reactor of
|
|
58
|
+
:mod:`Pyjo.IOLoop` follows the loop that is running, so timers, promises and
|
|
59
|
+
the I/O of :mod:`Pyjoyment` share it, and the response can be given later
|
|
60
|
+
than the ``request`` event, once the transaction is resumed.
|
|
61
|
+
|
|
62
|
+
An HTTP request is read from the connection scope as the ``CGI`` environment
|
|
63
|
+
that :meth:`Pyjo.Message.Request.Pyjo_Message_Request.parse` takes, as
|
|
64
|
+
:mod:`Pyjo.Server.WSGI` does. The ASGI server frames the response itself, so
|
|
65
|
+
chunked content is sent without its framing and without the
|
|
66
|
+
``Transfer-Encoding`` header, and the ``Date`` header is the one of the ASGI
|
|
67
|
+
server, unless the application sets one. A WebSocket handshake is a
|
|
68
|
+
:mod:`Pyjo.Transaction.WebSocket`, as with :mod:`Pyjo.Server.Daemon`; a
|
|
69
|
+
response other than ``101`` rejects it, the ASGI server negotiates
|
|
70
|
+
``permessage-deflate`` on its own, and a ``Ping`` frame, which ASGI cannot
|
|
71
|
+
send, is answered with a ``Pong`` frame, as the ASGI server pings the client
|
|
72
|
+
itself.
|
|
73
|
+
|
|
74
|
+
The lifespan of the ASGI server is the one of the server of the application:
|
|
75
|
+
its startup calls :meth:`Pyjo.App.Pyjo_App.server`, which emits the hook
|
|
76
|
+
``before_server_start`` of :mod:`Pyjoyment`, and its shutdown calls
|
|
77
|
+
:meth:`Pyjo.App.Pyjo_App.server_stopped`, which emits the hook
|
|
78
|
+
``after_server_stop``, and waits for the promise that it returns, as of the
|
|
79
|
+
lifespan of a mounted ASGI application. An exception of either fails the
|
|
80
|
+
lifespan of the ASGI server with its message.
|
|
81
|
+
|
|
82
|
+
This needs a reactor that runs in the event loop of the ASGI server and
|
|
83
|
+
gives awaitables for it. :mod:`Pyjo.Reactor.Asyncio`, the default, does so in
|
|
84
|
+
an ASGI server on :mod:`asyncio`, whose loop may be another implementation of
|
|
85
|
+
the event loop of :mod:`asyncio`, as uvloop is, as the reactor takes the loop
|
|
86
|
+
that the server runs. :mod:`Pyjo.Reactor.Poll` runs a loop of its own, which
|
|
87
|
+
nothing runs in an ASGI server, so a response given later than the
|
|
88
|
+
``request`` event fails, with ``Task got bad yield`` in a server on
|
|
89
|
+
:mod:`asyncio`. A server on another framework, as Hypercorn on Trio, needs a
|
|
90
|
+
reactor for it, which may come from another distribution, as
|
|
91
|
+
:mod:`Pyjo.Reactor` describes.
|
|
92
|
+
|
|
93
|
+
ASGI servers
|
|
94
|
+
------------
|
|
95
|
+
|
|
96
|
+
* uvicorn, on :mod:`asyncio` or uvloop: HTTP, WebSocket and lifespan.
|
|
97
|
+
* Hypercorn, on :mod:`asyncio` or uvloop: HTTP, WebSocket and lifespan; on
|
|
98
|
+
Trio only with a reactor for Trio, which Pyjoyment does not have.
|
|
99
|
+
* Daphne: HTTP and WebSocket. It adds no ``Date`` header, so a response has
|
|
100
|
+
one only when the application sets it, and it sends no reason with a
|
|
101
|
+
WebSocket close code. It has no lifespan, so the start and the end of the
|
|
102
|
+
server are not emitted, and the lifespan of a mounted ASGI application does
|
|
103
|
+
not shut down.
|
|
104
|
+
* Granian: HTTP, WebSocket and lifespan. A ``websocket.close`` that the
|
|
105
|
+
application sends is not delivered while a ``receive`` is waiting, and an
|
|
106
|
+
application of WebSocket always waits for the next message, so a WebSocket
|
|
107
|
+
that the application closes stays open until the client closes it.
|
|
108
|
+
|
|
109
|
+
Events
|
|
110
|
+
------
|
|
111
|
+
|
|
112
|
+
:mod:`Pyjo.Server.ASGI` inherits all events from :mod:`Pyjo.Server`.
|
|
113
|
+
|
|
114
|
+
See also
|
|
115
|
+
--------
|
|
116
|
+
|
|
117
|
+
:mod:`Pyjoyment`, :mod:`Pyjo.Server.WSGI`, https://asgi.readthedocs.io.
|
|
118
|
+
|
|
119
|
+
It has no counterpart in Mojolicious; :mod:`Pyjo.Server.WSGI` is the
|
|
120
|
+
counterpart of
|
|
121
|
+
`Mojo::Server::PSGI <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/PSGI.pm>`_.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
import Pyjo.Content.Single
|
|
125
|
+
import Pyjo.Promise
|
|
126
|
+
import Pyjo.Server
|
|
127
|
+
import Pyjo.Transaction
|
|
128
|
+
import Pyjo.Transaction.HTTP
|
|
129
|
+
import Pyjo.Transaction.WebSocket
|
|
130
|
+
|
|
131
|
+
from Pyjo.Util import url_escape
|
|
132
|
+
from Pyjo.WebSocket import (
|
|
133
|
+
WS_BINARY,
|
|
134
|
+
WS_CLOSE,
|
|
135
|
+
WS_CONTINUATION,
|
|
136
|
+
WS_PING,
|
|
137
|
+
WS_PONG,
|
|
138
|
+
WS_TEXT,
|
|
139
|
+
Frame,
|
|
140
|
+
parse_frame,
|
|
141
|
+
server_handshake,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
import struct
|
|
145
|
+
import sys
|
|
146
|
+
|
|
147
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
148
|
+
from typing import Any
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# The connection scope, a message, and the functions that receive and send messages
|
|
152
|
+
type Scope = Mapping[str, Any]
|
|
153
|
+
type Message = Mapping[str, Any]
|
|
154
|
+
type Receive = Callable[[], Awaitable[Message]]
|
|
155
|
+
type Send = Callable[[Message], Awaitable[None]]
|
|
156
|
+
type Application = Callable[[Scope, Receive, Send], Awaitable[None]]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Pyjo_Server_ASGI(Pyjo.Server.object):
|
|
160
|
+
"""
|
|
161
|
+
:mod:`Pyjo.Server.ASGI` inherits all attributes and methods from
|
|
162
|
+
:mod:`Pyjo.Server` and implements the following new ones.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
# The run of Pyjo.Server takes no arguments, as the one of Mojo::Server; the run of an ASGI server is a coroutine for
|
|
166
|
+
# one connection, as the one of Mojo::Server::PSGI takes the environment
|
|
167
|
+
async def run(self, scope: Scope, receive: Receive, send: Send) -> None: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
168
|
+
"""::
|
|
169
|
+
|
|
170
|
+
await asgi.run(scope, receive, send)
|
|
171
|
+
|
|
172
|
+
Run ``ASGI`` for one connection.
|
|
173
|
+
"""
|
|
174
|
+
type = scope['type']
|
|
175
|
+
if type == 'http':
|
|
176
|
+
await self._http(scope, receive, send)
|
|
177
|
+
elif type == 'websocket':
|
|
178
|
+
await self._websocket(scope, receive, send)
|
|
179
|
+
elif type == 'lifespan':
|
|
180
|
+
await self._lifespan(receive, send)
|
|
181
|
+
else:
|
|
182
|
+
raise ValueError(f'Unsupported ASGI connection type "{type}"')
|
|
183
|
+
|
|
184
|
+
def to_asgi_app(self) -> Application:
|
|
185
|
+
"""::
|
|
186
|
+
|
|
187
|
+
app = asgi.to_asgi_app()
|
|
188
|
+
|
|
189
|
+
Turn the application into an ``ASGI`` application.
|
|
190
|
+
"""
|
|
191
|
+
# Preload application and wrap it
|
|
192
|
+
self.app
|
|
193
|
+
|
|
194
|
+
async def application(scope: Scope, receive: Receive, send: Send) -> None:
|
|
195
|
+
await self.run(scope, receive, send)
|
|
196
|
+
|
|
197
|
+
return application
|
|
198
|
+
|
|
199
|
+
def _build(self, scope: Scope) -> Pyjo.Transaction.HTTP.object:
|
|
200
|
+
tx = self.build_tx()
|
|
201
|
+
if (client := scope.get('client')) is not None:
|
|
202
|
+
tx.remote_address, tx.remote_port = client[0], client[1]
|
|
203
|
+
if (server := scope.get('server')) is not None and server[1] is not None:
|
|
204
|
+
tx.local_address, tx.local_port = server[0], server[1]
|
|
205
|
+
return tx
|
|
206
|
+
|
|
207
|
+
async def _http(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
208
|
+
env = _environ(scope)
|
|
209
|
+
|
|
210
|
+
# Request body of unknown length, as the ASGI server removes the chunked framing
|
|
211
|
+
body: bytes | None = None
|
|
212
|
+
if 'CONTENT_LENGTH' not in env:
|
|
213
|
+
whole = b''
|
|
214
|
+
async for chunk in _body(receive):
|
|
215
|
+
whole += chunk
|
|
216
|
+
if len(whole):
|
|
217
|
+
env['CONTENT_LENGTH'] = str(len(whole))
|
|
218
|
+
body = whole
|
|
219
|
+
|
|
220
|
+
tx = self._build(scope)
|
|
221
|
+
req = tx.req.parse(env)
|
|
222
|
+
if body is not None:
|
|
223
|
+
req.parse(body)
|
|
224
|
+
else:
|
|
225
|
+
async for chunk in _body(receive):
|
|
226
|
+
req.parse(chunk)
|
|
227
|
+
if req.is_finished:
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
resumed = _Resumed(tx)
|
|
231
|
+
self.emit('request', tx)
|
|
232
|
+
await resumed.wait()
|
|
233
|
+
|
|
234
|
+
# Response headers, without the framing of chunked content, and without the Date header that fix_headers adds,
|
|
235
|
+
# as the ASGI server adds its own
|
|
236
|
+
res = tx.res
|
|
237
|
+
dated = res.headers.date is not None
|
|
238
|
+
res.fix_headers()
|
|
239
|
+
if not dated:
|
|
240
|
+
res.headers.remove('Date')
|
|
241
|
+
content = res.content
|
|
242
|
+
chunked = content.is_chunked
|
|
243
|
+
headers = [
|
|
244
|
+
(name.lower().encode('latin-1'), value.encode('latin-1'))
|
|
245
|
+
for name, value in res.headers.to_list()
|
|
246
|
+
if not chunked or name.lower() != 'transfer-encoding'
|
|
247
|
+
]
|
|
248
|
+
await send({'type': 'http.response.start', 'status': res.code or 404, 'headers': headers})
|
|
249
|
+
|
|
250
|
+
# Response body
|
|
251
|
+
if not tx.is_empty:
|
|
252
|
+
unchunk = _Unchunk() if chunked else None
|
|
253
|
+
offset = 0
|
|
254
|
+
while True:
|
|
255
|
+
# No content yet, try again later
|
|
256
|
+
if (chunk := res.get_body_chunk(offset)) is None:
|
|
257
|
+
await resumed.wait()
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
# End of content
|
|
261
|
+
if not len(chunk):
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
offset += len(chunk)
|
|
265
|
+
if unchunk is not None:
|
|
266
|
+
chunk = unchunk.parse(chunk)
|
|
267
|
+
if len(chunk):
|
|
268
|
+
await send({'type': 'http.response.body', 'body': chunk, 'more_body': True})
|
|
269
|
+
|
|
270
|
+
await send({'type': 'http.response.body', 'body': b'', 'more_body': False})
|
|
271
|
+
tx.closed()
|
|
272
|
+
|
|
273
|
+
# The start and the end of the server, which the application learns, and whose cleanup the ASGI server waits for
|
|
274
|
+
async def _lifespan(self, receive: Receive, send: Send) -> None:
|
|
275
|
+
while True:
|
|
276
|
+
message = await receive()
|
|
277
|
+
if message['type'] == 'lifespan.startup':
|
|
278
|
+
try:
|
|
279
|
+
self.app.server(self)
|
|
280
|
+
except Exception as e:
|
|
281
|
+
await send({'type': 'lifespan.startup.failed', 'message': str(e)})
|
|
282
|
+
return
|
|
283
|
+
await send({'type': 'lifespan.startup.complete'})
|
|
284
|
+
elif message['type'] == 'lifespan.shutdown':
|
|
285
|
+
try:
|
|
286
|
+
if isinstance(promise := self.app.server_stopped(self), Pyjo.Promise.object):
|
|
287
|
+
await promise
|
|
288
|
+
except Exception as e:
|
|
289
|
+
await send({'type': 'lifespan.shutdown.failed', 'message': str(e)})
|
|
290
|
+
return
|
|
291
|
+
await send({'type': 'lifespan.shutdown.complete'})
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
async def _websocket(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
295
|
+
message = await receive()
|
|
296
|
+
if message['type'] != 'websocket.connect':
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
# Handshake
|
|
300
|
+
tx = self._build(scope)
|
|
301
|
+
tx.req.parse(_environ(scope))
|
|
302
|
+
if not tx.req.is_handshake:
|
|
303
|
+
tx.req.headers.upgrade = 'websocket'
|
|
304
|
+
ws = Pyjo.Transaction.WebSocket.new(handshake=tx)
|
|
305
|
+
handshake = _Resumed(tx)
|
|
306
|
+
self.emit('request', server_handshake(ws))
|
|
307
|
+
await handshake.wait()
|
|
308
|
+
|
|
309
|
+
# Rejected
|
|
310
|
+
if ws.res.code != 101:
|
|
311
|
+
await send({'type': 'websocket.close', 'code': 1008})
|
|
312
|
+
tx.closed()
|
|
313
|
+
ws.closed()
|
|
314
|
+
return
|
|
315
|
+
|
|
316
|
+
# Established, the ASGI server compresses messages itself
|
|
317
|
+
await send({'type': 'websocket.accept', 'subprotocol': ws.protocol})
|
|
318
|
+
tx.closed()
|
|
319
|
+
ws.set(established=True, compressed=False)
|
|
320
|
+
|
|
321
|
+
# Write messages as they are sent, and read them as they arrive
|
|
322
|
+
writer = _Writer(ws, send)
|
|
323
|
+
while not writer.closed:
|
|
324
|
+
message = await receive()
|
|
325
|
+
if message['type'] == 'websocket.receive':
|
|
326
|
+
if (text := message.get('text')) is not None:
|
|
327
|
+
ws.parse_message((1, 0, 0, 0, WS_TEXT, text.encode()))
|
|
328
|
+
else:
|
|
329
|
+
ws.parse_message((1, 0, 0, 0, WS_BINARY, message.get('bytes') or b''))
|
|
330
|
+
elif message['type'] == 'websocket.disconnect':
|
|
331
|
+
writer.stop()
|
|
332
|
+
code = message.get('code', 1005)
|
|
333
|
+
if code == 1005:
|
|
334
|
+
ws.parse_message((1, 0, 0, 0, WS_CLOSE, b''))
|
|
335
|
+
elif code != 1006:
|
|
336
|
+
reason = (message.get('reason') or '').encode()
|
|
337
|
+
ws.parse_message((1, 0, 0, 0, WS_CLOSE, struct.pack('>H', code) + reason))
|
|
338
|
+
break
|
|
339
|
+
await writer.done
|
|
340
|
+
ws.closed()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# The next resume event of a transaction, which may come before it is waited for
|
|
344
|
+
class _Resumed:
|
|
345
|
+
def __init__(self, tx: Pyjo.Transaction.object) -> None:
|
|
346
|
+
self._resumed = False
|
|
347
|
+
self._promise: Pyjo.Promise.object | None = None
|
|
348
|
+
tx.on('resume', self.resume)
|
|
349
|
+
|
|
350
|
+
def resume(self, *args: Any) -> None:
|
|
351
|
+
self._resumed = True
|
|
352
|
+
if (promise := self._promise) is not None:
|
|
353
|
+
self._promise = None
|
|
354
|
+
promise.resolve()
|
|
355
|
+
|
|
356
|
+
async def wait(self) -> None:
|
|
357
|
+
if not self._resumed:
|
|
358
|
+
self._promise = Pyjo.Promise.new()
|
|
359
|
+
await self._promise
|
|
360
|
+
self._resumed = False
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
# Chunked content without its framing
|
|
364
|
+
class _Unchunk:
|
|
365
|
+
def __init__(self) -> None:
|
|
366
|
+
self._chunks: list[bytes] = []
|
|
367
|
+
self._parser = Pyjo.Content.Single.new()
|
|
368
|
+
self._parser.unsubscribe('read').on('read', self._read)
|
|
369
|
+
self._parser.parse(b'Transfer-Encoding: chunked\x0d\x0a\x0d\x0a')
|
|
370
|
+
|
|
371
|
+
def _read(self, content: Pyjo.Content.Single.object, chunk: bytes) -> None:
|
|
372
|
+
self._chunks.append(chunk)
|
|
373
|
+
|
|
374
|
+
def parse(self, chunk: bytes) -> bytes:
|
|
375
|
+
self._parser.parse(chunk)
|
|
376
|
+
chunks, self._chunks = self._chunks, []
|
|
377
|
+
return b''.join(chunks)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# The frames that a WebSocket transaction writes, as messages of ASGI
|
|
381
|
+
class _Writer:
|
|
382
|
+
def __init__(self, ws: Pyjo.Transaction.WebSocket.object, send: Send) -> None:
|
|
383
|
+
self.closed = False
|
|
384
|
+
self._buffer = bytearray()
|
|
385
|
+
self._message: list[Any] = []
|
|
386
|
+
self._resumed = _Resumed(ws)
|
|
387
|
+
self._send = send
|
|
388
|
+
self._stopped = False
|
|
389
|
+
self._ws = ws
|
|
390
|
+
self.done = Pyjo.Promise.new(self._write())
|
|
391
|
+
|
|
392
|
+
def stop(self) -> None:
|
|
393
|
+
self._stopped = True
|
|
394
|
+
self._resumed.resume()
|
|
395
|
+
|
|
396
|
+
async def _write(self) -> None:
|
|
397
|
+
while not self._stopped:
|
|
398
|
+
chunk = self._ws.server_write()
|
|
399
|
+
self._buffer += chunk
|
|
400
|
+
while not self._stopped and isinstance(frame := parse_frame(self._buffer, sys.maxsize), tuple):
|
|
401
|
+
await self._frame(frame)
|
|
402
|
+
if self.closed or self._ws.is_finished:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
# What has been written, after which the next write, of nothing, emits the event drain, as a server does
|
|
406
|
+
# once the data has been written
|
|
407
|
+
if not len(chunk):
|
|
408
|
+
await self._resumed.wait()
|
|
409
|
+
|
|
410
|
+
async def _frame(self, frame: Frame) -> None:
|
|
411
|
+
fin, _, _, _, op, payload = frame
|
|
412
|
+
|
|
413
|
+
# Close
|
|
414
|
+
if op == WS_CLOSE:
|
|
415
|
+
code = struct.unpack('>H', payload[:2])[0] if len(payload) >= 2 else 1000
|
|
416
|
+
await self._send(
|
|
417
|
+
{'type': 'websocket.close', 'code': code, 'reason': payload[2:].decode('utf-8', 'replace')}
|
|
418
|
+
)
|
|
419
|
+
self.closed = True
|
|
420
|
+
return
|
|
421
|
+
|
|
422
|
+
# A ping, which ASGI cannot send, is answered as by the client, as the ASGI server keeps the connection alive
|
|
423
|
+
if op == WS_PING:
|
|
424
|
+
self._ws.parse_message((1, 0, 0, 0, WS_PONG, payload))
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
# Text or binary message, which may come in fragments
|
|
428
|
+
if op in (WS_TEXT, WS_BINARY):
|
|
429
|
+
self._message = [op, payload]
|
|
430
|
+
elif op == WS_CONTINUATION and self._message:
|
|
431
|
+
self._message[1] += payload
|
|
432
|
+
else:
|
|
433
|
+
return
|
|
434
|
+
if not fin:
|
|
435
|
+
return
|
|
436
|
+
op, payload = self._message
|
|
437
|
+
self._message = []
|
|
438
|
+
if op == WS_TEXT:
|
|
439
|
+
await self._send({'type': 'websocket.send', 'text': payload.decode('utf-8', 'replace')})
|
|
440
|
+
else:
|
|
441
|
+
await self._send({'type': 'websocket.send', 'bytes': payload})
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# The body of a request, from http.request messages
|
|
445
|
+
async def _body(receive: Receive):
|
|
446
|
+
while True:
|
|
447
|
+
message = await receive()
|
|
448
|
+
if message['type'] == 'http.disconnect':
|
|
449
|
+
return
|
|
450
|
+
if len(chunk := message.get('body', b'')):
|
|
451
|
+
yield chunk
|
|
452
|
+
if not message.get('more_body'):
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# The CGI environment of a connection scope
|
|
457
|
+
def _environ(scope: Scope) -> dict[str, str]:
|
|
458
|
+
# The raw path is bytes, which a path of Pyjoyment takes as characters, so the bytes beyond ASCII are percent-encoded,
|
|
459
|
+
# as the path is escaped
|
|
460
|
+
raw = scope.get('raw_path')
|
|
461
|
+
path = url_escape(raw.split(b'?', 1)[0], rb'\x80-\xff').decode('ascii') if raw else scope.get('path', '')
|
|
462
|
+
env = {
|
|
463
|
+
'PATH_INFO': path,
|
|
464
|
+
'QUERY_STRING': scope.get('query_string', b'').decode('latin-1'),
|
|
465
|
+
'REQUEST_METHOD': scope.get('method', 'GET'),
|
|
466
|
+
'SCRIPT_NAME': scope.get('root_path', ''),
|
|
467
|
+
'SERVER_PROTOCOL': f'HTTP/{scope.get("http_version", "1.1")}',
|
|
468
|
+
}
|
|
469
|
+
if scope.get('scheme') in ('https', 'wss'):
|
|
470
|
+
env['HTTPS'] = 'ON'
|
|
471
|
+
|
|
472
|
+
# Headers, joined as CGI has them
|
|
473
|
+
for name, value in scope.get('headers', []):
|
|
474
|
+
key, value = name.decode('latin-1').upper().replace('-', '_'), value.decode('latin-1')
|
|
475
|
+
if key in ('CONTENT_LENGTH', 'CONTENT_TYPE'):
|
|
476
|
+
env[key] = value
|
|
477
|
+
elif key != 'TRANSFER_ENCODING':
|
|
478
|
+
key = f'HTTP_{key}'
|
|
479
|
+
env[key] = f'{env[key]}{"; " if key == "HTTP_COOKIE" else ", "}{value}' if key in env else value
|
|
480
|
+
|
|
481
|
+
return env
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
new = Pyjo_Server_ASGI.new
|
|
485
|
+
object = Pyjo_Server_ASGI
|
|
486
|
+
ASGI = Pyjo_Server_ASGI
|