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/FastCGI.py ADDED
@@ -0,0 +1,517 @@
1
+ """
2
+ Pyjo.Server.FastCGI - FastCGI connections of the servers
3
+ ========================================================
4
+ ::
5
+
6
+ import Pyjo.Server.Daemon
7
+
8
+ daemon = Pyjo.Server.Daemon.new(listen=['fcgi://127.0.0.1:9000'])
9
+
10
+
11
+ @daemon.unsubscribe('request').on('request')
12
+ def request(daemon, 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
+ daemon.run()
27
+
28
+ :mod:`Pyjo.Server.FastCGI` speaks `FastCGI 1.0
29
+ <https://fastcgi-archives.github.io/FastCGI_Specification.html>`_ on the
30
+ connections of the listen locations ``fcgi://`` and ``fcgi+unix://`` of
31
+ :mod:`Pyjo.Server.Daemon`, and so of the servers built on it,
32
+ :mod:`Pyjo.Server.Prefork`, :mod:`Pyjo.Server.Hot` and
33
+ :mod:`Pyjo.Server.Reload`, with the same applications, timeouts, signals and
34
+ graceful shutdown as HTTP. ::
35
+
36
+ $ ./myapp.py daemon -l fcgi://127.0.0.1:9000
37
+ $ ./myapp.py prefork -l fcgi+unix://%2Ftmp%2Fmyapp.sock
38
+
39
+ A web server in front of the application speaks HTTP to the clients and
40
+ FastCGI to the application, which gets the request as the environment of
41
+ :rfc:`3875`, as :mod:`Pyjo.Server.CGI` does, but in a process that keeps
42
+ running. A connection takes one request at a time, in the role of a
43
+ responder, the only one that it supports, and stays open after it when the
44
+ web server asks for it with ``FCGI_KEEP_CONN``. The content of the response
45
+ can come later, as with :mod:`Pyjo.Server.Daemon`, and goes to the web server
46
+ as soon as it is written. WebSockets are not available, as FastCGI has no
47
+ protocol for them.
48
+
49
+ The application can also be started by the web server, which gives it its
50
+ listen socket as the file descriptor ``0``, as Apache with ``mod_fcgid`` and
51
+ lighttpd do; the command :mod:`Pyjoyment.Command.fastcgi` serves it, and is
52
+ detected by :mod:`Pyjoyment.Commands`, so the script of the application
53
+ needs no command.
54
+
55
+ Environment
56
+ -----------
57
+
58
+ The request is read from the parameters of FastCGI, as the environment of
59
+ CGI that :meth:`Pyjo.Message.Request.Pyjo_Message_Request.parse` takes, with
60
+ a few changes for what the web servers send:
61
+
62
+ * A path in ``SCRIPT_NAME`` without ``PATH_INFO`` is the path of the request,
63
+ as nginx with its ``fastcgi_params`` and Caddy send the whole path in
64
+ ``SCRIPT_NAME``. An application deployed under a path prefix gets it in
65
+ ``SCRIPT_NAME`` and the rest of the path in ``PATH_INFO``.
66
+ * The bytes of the paths beyond ASCII are percent-encoded, as the path is
67
+ escaped, as with :mod:`Pyjo.Server.WSGI`.
68
+ * A request body without ``CONTENT_LENGTH``, of a chunked request whose
69
+ framing the web server removed, is read whole before the request is parsed.
70
+
71
+ Web servers
72
+ -----------
73
+
74
+ * Caddy, with ``transport fastcgi``: it sends the response to the client with
75
+ the ``Status`` header next to its status line. ::
76
+
77
+ example.com {
78
+ reverse_proxy 127.0.0.1:9000 {
79
+ transport fastcgi
80
+ }
81
+ }
82
+
83
+ * Apache with ``mod_proxy_fcgi``: ::
84
+
85
+ ProxyPass / fcgi://127.0.0.1:9000/ enablereuse=on
86
+
87
+ * Apache with ``mod_fcgid``, which starts the application: ::
88
+
89
+ <VirtualHost *:80>
90
+ ServerName localhost
91
+ ScriptAlias / /home/sri/myapp/myapp.py/
92
+ <Directory /home/sri/myapp>
93
+ SetHandler fcgid-script
94
+ Options +ExecCGI
95
+ Require all granted
96
+ </Directory>
97
+ </VirtualHost>
98
+
99
+ * nginx: ::
100
+
101
+ location / {
102
+ include fastcgi_params;
103
+ fastcgi_pass 127.0.0.1:9000;
104
+ fastcgi_keep_conn on;
105
+ }
106
+
107
+ See also
108
+ --------
109
+
110
+ :mod:`Pyjoyment.Command.fastcgi`, :mod:`Pyjo.Server.Daemon`, :mod:`Pyjoyment`,
111
+ https://mojolicious.org.
112
+
113
+ It has no counterpart in Mojolicious; :mod:`Pyjo.Server.CGI` is the
114
+ counterpart of `Mojo::Server::CGI
115
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Server/CGI.pm>`_.
116
+
117
+ Classes
118
+ -------
119
+ """
120
+
121
+ import Pyjo.Base
122
+ import Pyjo.Content.Single
123
+ import Pyjo.Transaction
124
+
125
+ from Pyjo.Util import url_escape
126
+
127
+ import struct
128
+ import weakref
129
+ import wsgiref.util
130
+
131
+ from typing import TYPE_CHECKING, Any
132
+
133
+
134
+ if TYPE_CHECKING:
135
+ import Pyjo.Server.Daemon
136
+
137
+
138
+ # The types of the records
139
+ BEGIN_REQUEST = 1
140
+ ABORT_REQUEST = 2
141
+ END_REQUEST = 3
142
+ PARAMS = 4
143
+ STDIN = 5
144
+ STDOUT = 6
145
+ GET_VALUES = 9
146
+ GET_VALUES_RESULT = 10
147
+ UNKNOWN_TYPE = 11
148
+
149
+ # The role, the flag of BEGIN_REQUEST and the statuses of END_REQUEST
150
+ RESPONDER = 1
151
+ KEEP_CONN = 1
152
+ REQUEST_COMPLETE = 0
153
+ CANT_MPX_CONN = 1
154
+ UNKNOWN_ROLE = 3
155
+
156
+ # The header of a record: version, type, request id, content length, padding length and a reserved byte
157
+ HEADER = struct.Struct('>BBHHBx')
158
+
159
+ # The longest content of a record
160
+ MAX_CONTENT = 65535
161
+
162
+
163
+ class Pyjo_Server_FastCGI(Pyjo.Base.object):
164
+ """
165
+ :mod:`Pyjo.Server.FastCGI` is the FastCGI side of a connection of
166
+ :mod:`Pyjo.Server.Daemon`, which makes it for the connections of its
167
+ listen locations ``fcgi://`` and ``fcgi+unix://``.
168
+ """
169
+
170
+ def __init__(self, daemon: 'Pyjo.Server.Daemon.object', id: str) -> None:
171
+ """::
172
+
173
+ fcgi = Pyjo.Server.FastCGI.new(daemon, id)
174
+
175
+ Construct a new :mod:`Pyjo.Server.FastCGI` object for the connection
176
+ ``id`` of the daemon.
177
+ """
178
+ super().__init__()
179
+
180
+ self.id = id
181
+ """::
182
+
183
+ id = fcgi.id
184
+
185
+ Id of the connection in the event loop of the daemon.
186
+ """
187
+
188
+ self._daemon = weakref.ref(daemon)
189
+ self._buffer = bytearray()
190
+ self._closing = False
191
+ self._request: dict[str, Any] | None = None
192
+ self._writing = False
193
+
194
+ def closed(self) -> None:
195
+ """::
196
+
197
+ fcgi.closed()
198
+
199
+ The connection has been closed, which finishes the transaction of the
200
+ request in progress.
201
+ """
202
+ self._closing = True
203
+ if (req := self._request) is not None:
204
+ self._finish(req)
205
+
206
+ def read(self, chunk: bytes) -> None:
207
+ """::
208
+
209
+ fcgi.read(chunk)
210
+
211
+ Read the records of a chunk of data from the web server.
212
+ """
213
+ buffer = self._buffer
214
+ buffer += chunk
215
+ while len(buffer) >= HEADER.size and not self._closing:
216
+ version, type, id, length, padding = HEADER.unpack_from(buffer)
217
+
218
+ # Not FastCGI
219
+ if version != 1:
220
+ self._trace(f'FastCGI version {version} is not supported')
221
+ return self._close()
222
+
223
+ end = HEADER.size + length + padding
224
+ if len(buffer) < end:
225
+ break
226
+ content = bytes(buffer[HEADER.size : HEADER.size + length])
227
+ del buffer[:end]
228
+ self._record(type, id, content)
229
+
230
+ def _begin(self, id: int, content: bytes) -> None:
231
+ role, flags = struct.unpack_from('>HB', content)
232
+ keep = bool(flags & KEEP_CONN)
233
+
234
+ # One request at a time
235
+ if self._request is not None:
236
+ return self._send(_end(id, CANT_MPX_CONN))
237
+
238
+ # Only responders
239
+ if role != RESPONDER:
240
+ self._send(_end(id, UNKNOWN_ROLE))
241
+ if not keep:
242
+ self._close()
243
+ return
244
+
245
+ self._request = {'id': id, 'keep': keep, 'params': bytearray()}
246
+ if (daemon := self._daemon()) is not None:
247
+ daemon.ioloop.stream(self.id).timeout = daemon.inactivity_timeout
248
+
249
+ def _close(self) -> None:
250
+ self._closing = True
251
+ if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
252
+ stream.close_gracefully()
253
+
254
+ def _env(self, req: dict[str, Any]) -> None:
255
+ if (daemon := self._daemon()) is None:
256
+ return
257
+ env = _environ(_pairs(bytes(req.pop('params'))))
258
+
259
+ tx = req['tx'] = daemon.build_tx()
260
+ tx.connection = self.id
261
+ # Unlike Mojolicious: Pyjoyment names itself
262
+ tx.res.headers.server = 'Pyjoyment (Python)'
263
+
264
+ # A body of unknown length is read whole before the request is parsed
265
+ if env.get('CONTENT_LENGTH'):
266
+ tx.req.parse(env)
267
+ else:
268
+ req['env'], req['body'] = env, bytearray()
269
+
270
+ tx.local_address = env.get('SERVER_ADDR')
271
+ tx.local_port = int(port) if (port := env.get('SERVER_PORT')) else None
272
+ tx.remote_address = env.get('REMOTE_ADDR')
273
+ tx.remote_port = int(port) if (port := env.get('REMOTE_PORT')) else None
274
+
275
+ def _finish(self, req: dict[str, Any]) -> None:
276
+ # Once, as an aborted request may still be written
277
+ if req.get('finished'):
278
+ return
279
+ req['finished'] = True
280
+ if self._request is req:
281
+ self._request = None
282
+ if (tx := req.get('tx')) is not None:
283
+ if (resume := req.get('resume')) is not None:
284
+ tx.unsubscribe('resume', resume)
285
+ tx.closed()
286
+
287
+ if self._closing:
288
+ return
289
+ if not req['keep']:
290
+ return self._close()
291
+ if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
292
+ stream.timeout = daemon.keep_alive_timeout
293
+
294
+ def _get_values(self, content: bytes) -> None:
295
+ daemon = self._daemon()
296
+ max = daemon.ioloop.max_connections if daemon is not None else 1
297
+ values = {'FCGI_MAX_CONNS': str(max), 'FCGI_MAX_REQS': str(max), 'FCGI_MPXS_CONNS': '0'}
298
+ result = {name: values[name] for name in _pairs(content) if name in values}
299
+ self._send(_record(GET_VALUES_RESULT, 0, _encode_pairs(result)))
300
+
301
+ def _record(self, type: int, id: int, content: bytes) -> None:
302
+ # Management records
303
+ if id == 0:
304
+ if type == GET_VALUES:
305
+ return self._get_values(content)
306
+ return self._send(_record(UNKNOWN_TYPE, 0, struct.pack('>B7x', type)))
307
+
308
+ if type == BEGIN_REQUEST:
309
+ return self._begin(id, content)
310
+
311
+ # Records of another request are ignored, as the ones of a request that is not active
312
+ if (req := self._request) is None or req['id'] != id:
313
+ return
314
+
315
+ if type == ABORT_REQUEST:
316
+ self._send(_end(id))
317
+ return self._finish(req)
318
+
319
+ if type == PARAMS and 'params' in req:
320
+ if len(content):
321
+ req['params'] += content
322
+ else:
323
+ self._env(req)
324
+
325
+ elif type == STDIN and req.get('tx') is not None and 'resume' not in req:
326
+ if (body := req.get('body')) is not None:
327
+ body += content
328
+ elif len(content):
329
+ req['tx'].req.parse(content)
330
+ if not len(content):
331
+ self._request_ready(req)
332
+
333
+ def _request_ready(self, req: dict[str, Any]) -> None:
334
+ if (daemon := self._daemon()) is None:
335
+ return
336
+ tx = req['tx']
337
+
338
+ # The request of unknown length with its body
339
+ if (body := req.pop('body', None)) is not None:
340
+ env = req.pop('env')
341
+ if len(body):
342
+ env['CONTENT_LENGTH'] = str(len(body))
343
+ tx.req.parse(env)
344
+ if len(body):
345
+ tx.req.parse(bytes(body))
346
+
347
+ if error := tx.req.error:
348
+ self._trace(error['message'])
349
+
350
+ ref = weakref.ref(self)
351
+
352
+ # The response is written once the transaction is resumed, as the content of a response can come later
353
+ def resume(tx: Pyjo.Transaction.object) -> None:
354
+ req['resumed'] = True
355
+ if (this := ref()) is not None:
356
+ this._write()
357
+
358
+ req['resume'] = resume
359
+ tx.on('resume', resume)
360
+ daemon.emit('request', tx)
361
+ self._write()
362
+
363
+ def _send(self, data: bytes, cb: Any = None) -> None:
364
+ if (daemon := self._daemon()) is not None and (stream := daemon.ioloop.stream(self.id)) is not None:
365
+ stream.write(data, cb)
366
+
367
+ def _trace(self, message: str) -> None:
368
+ if (daemon := self._daemon()) is not None:
369
+ daemon.app.log.trace(message)
370
+
371
+ def _write(self) -> None:
372
+ # Protect from resume event recursion
373
+ if (req := self._request) is None or not req.get('resumed') or req.get('ended') or self._writing:
374
+ return
375
+ self._writing = True
376
+ try:
377
+ tx = req['tx']
378
+ res = tx.res
379
+ finished = False
380
+
381
+ # Response headers, with the status in a header as CGI has it
382
+ if 'offset' not in req:
383
+ res.fix_headers()
384
+ code = res.code or 404
385
+ lines = [f'Status: {code} {res.message or res.default_message()}']
386
+ # Unlike Mojolicious: without the hop-by-hop headers, so chunked content goes without its framing,
387
+ # which the web server adds
388
+ lines += [f'{name}: {value}' for name, value in res.headers.to_list() if not _hop_by_hop(name)]
389
+ data = ('\x0d\x0a'.join(lines) + '\x0d\x0a\x0d\x0a').encode('latin-1')
390
+ req['offset'] = 0
391
+ req['unchunk'] = _Unchunk() if res.content.is_chunked else None
392
+ finished = tx.is_empty
393
+
394
+ # Response body
395
+ else:
396
+ chunk = res.get_body_chunk(req['offset'])
397
+
398
+ # No content yet, the resume event comes when there is some
399
+ if chunk is None:
400
+ return
401
+ if not len(chunk):
402
+ finished = True
403
+ req['offset'] += len(chunk)
404
+ data = req['unchunk'].parse(chunk) if req['unchunk'] is not None else chunk
405
+ finally:
406
+ self._writing = False
407
+
408
+ records = b''.join(
409
+ _record(STDOUT, req['id'], data[i : i + MAX_CONTENT]) for i in range(0, len(data), MAX_CONTENT)
410
+ )
411
+ ref = weakref.ref(self)
412
+
413
+ # Finish the request
414
+ if finished:
415
+ req['ended'] = True
416
+ records += _record(STDOUT, req['id']) + _end(req['id'])
417
+
418
+ def done(stream: Any) -> None:
419
+ if (this := ref()) is not None:
420
+ this._finish(req)
421
+
422
+ return self._send(records, done)
423
+
424
+ # Next chunk
425
+ def written(stream: Any) -> None:
426
+ if (this := ref()) is not None:
427
+ this._write()
428
+
429
+ self._send(records, written)
430
+
431
+
432
+ # Chunked content without its framing, as in Pyjo.Server.WSGI
433
+ class _Unchunk:
434
+ def __init__(self) -> None:
435
+ self._chunks: list[bytes] = []
436
+ self._parser = Pyjo.Content.Single.new()
437
+ self._parser.unsubscribe('read').on('read', self._read)
438
+ self._parser.parse(b'Transfer-Encoding: chunked\x0d\x0a\x0d\x0a')
439
+
440
+ def _read(self, content: Pyjo.Content.Single.object, chunk: bytes) -> None:
441
+ self._chunks.append(chunk)
442
+
443
+ def parse(self, chunk: bytes) -> bytes:
444
+ self._parser.parse(chunk)
445
+ chunks, self._chunks = self._chunks, []
446
+ return b''.join(chunks)
447
+
448
+
449
+ def _encode_length(value: bytes) -> bytes:
450
+ return struct.pack('>B', len(value)) if len(value) < 128 else struct.pack('>I', len(value) | 0x80000000)
451
+
452
+
453
+ def _encode_pairs(pairs: dict[str, str]) -> bytes:
454
+ data = b''
455
+ for name, value in pairs.items():
456
+ name_bytes, value_bytes = name.encode('latin-1'), value.encode('latin-1')
457
+ data += _encode_length(name_bytes) + _encode_length(value_bytes) + name_bytes + value_bytes
458
+ return data
459
+
460
+
461
+ def _end(id: int, status: int = REQUEST_COMPLETE) -> bytes:
462
+ # The application status of a request is 0, as a process of CGI that exits normally
463
+ return _record(END_REQUEST, id, struct.pack('>IB3x', 0, status))
464
+
465
+
466
+ # The environment of CGI that Pyjo.Message.Request parses, from the parameters that the web server sends
467
+ def _environ(params: dict[str, str]) -> dict[str, str]:
468
+ env = dict(params)
469
+
470
+ # The whole path in SCRIPT_NAME, as nginx with fastcgi_params and Caddy send it
471
+ if not env.get('PATH_INFO') and env.get('SCRIPT_NAME'):
472
+ env['PATH_INFO'], env['SCRIPT_NAME'] = env['SCRIPT_NAME'], ''
473
+
474
+ # A path is bytes as the characters 0-255, so the bytes beyond ASCII are percent-encoded, as the path is escaped
475
+ for key in ('PATH_INFO', 'SCRIPT_NAME'):
476
+ if key in env:
477
+ env[key] = url_escape(env[key].encode('latin-1'), rb'\x80-\xff').decode('ascii')
478
+
479
+ # The web server removes the chunked framing of a request
480
+ env.pop('HTTP_TRANSFER_ENCODING', None)
481
+ return env
482
+
483
+
484
+ def _hop_by_hop(name: str) -> bool:
485
+ return wsgiref.util.is_hop_by_hop(name)
486
+
487
+
488
+ # The name-value pairs of FastCGI, whose lengths take one byte, or four with the high bit set, as characters 0-255
489
+ def _pairs(data: bytes) -> dict[str, str]:
490
+ pairs: dict[str, str] = {}
491
+ offset = 0
492
+ while offset < len(data):
493
+ lengths: list[int] = []
494
+ for _ in range(2):
495
+ if offset >= len(data):
496
+ return pairs
497
+ if data[offset] < 128:
498
+ lengths.append(data[offset])
499
+ offset += 1
500
+ else:
501
+ lengths.append(struct.unpack_from('>I', data, offset)[0] & 0x7FFFFFFF)
502
+ offset += 4
503
+ name = data[offset : offset + lengths[0]].decode('latin-1')
504
+ offset += lengths[0]
505
+ pairs[name] = data[offset : offset + lengths[1]].decode('latin-1')
506
+ offset += lengths[1]
507
+ return pairs
508
+
509
+
510
+ def _record(type: int, id: int, content: bytes = b'') -> bytes:
511
+ padding = -len(content) % 8
512
+ return HEADER.pack(1, type, id, len(content), padding) + content + b'\0' * padding
513
+
514
+
515
+ new = Pyjo_Server_FastCGI.new
516
+ object = Pyjo_Server_FastCGI
517
+ FastCGI = Pyjo_Server_FastCGI