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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. 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