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/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