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/FastCGI.py ADDED
@@ -0,0 +1,631 @@
1
+ """
2
+ Pyjo.App.FastCGI - FastCGI application
3
+ ======================================
4
+ ::
5
+
6
+ from Pyjo.App.FastCGI import FastCGI
7
+
8
+ app = FastCGI(url='fcgi://127.0.0.1:9000')
9
+
10
+ # Or import the module, as in Google Python Style Guide
11
+
12
+ import Pyjo.App.FastCGI
13
+
14
+ app = Pyjo.App.FastCGI.new(url='fcgi://127.0.0.1:9000')
15
+
16
+ # PHP on php-fpm, on a UNIX domain socket, with its script
17
+ app = Pyjo.App.FastCGI.new(
18
+ url='fcgi+unix://%2Frun%2Fphp%2Ffpm.sock',
19
+ env={'SCRIPT_FILENAME': '/var/www/index.php', 'DOCUMENT_ROOT': '/var/www'},
20
+ )
21
+
22
+ # Mount it in an application of Pyjoyment
23
+ import Pyjoyment
24
+
25
+ pyjoyment = Pyjoyment.new()
26
+ pyjoyment.plugin('Mount', {'/php': app})
27
+ pyjoyment.plugin('Mount', {'/fcgi': 'fcgi://127.0.0.1:9000'})
28
+
29
+ :mod:`Pyjo.App.FastCGI` is an application of :mod:`Pyjo` that runs a
30
+ ``FastCGI`` application, as php-fpm runs PHP, through a connection to it, as
31
+ a client of `FastCGI 1.0
32
+ <https://fastcgi-archives.github.io/FastCGI_Specification.html>`_ in the role
33
+ of a responder, so that a server of :mod:`Pyjo` serves it, a route of
34
+ :mod:`Pyjoyment` embeds it, as :mod:`Pyjoyment.Plugin.Mount` does, and
35
+ :mod:`Pyjo.Test.Pyjo` tests it.
36
+
37
+ The parameters of the request are its environment of ``CGI``: the part of the
38
+ path that a route embeds the application under is ``SCRIPT_NAME``, after the
39
+ base path of the request, which a part of the path that does not end with a
40
+ slash, as ``/php`` of ``/phpx``, cannot be, so that the application is not
41
+ found there, the rest of the path is ``PATH_INFO``, ``/`` for
42
+ the path of the mount itself, as an empty one is the whole path in
43
+ ``SCRIPT_NAME`` for a server of ``FastCGI``, ``REQUEST_URI`` is the path with
44
+ the query, without the header ``Proxy``, whose ``HTTP_PROXY`` an application
45
+ may take for the proxy of its own requests, as with httpoxy, and :attr:`env`
46
+ adds to them, as ``SCRIPT_FILENAME`` for php-fpm, which :attr:`document_root`
47
+ gives for each request otherwise. The body of the request goes in records
48
+ ``STDIN``, each once the one before has been written.
49
+
50
+ The connection is one for each request, through the event loop, which the
51
+ application closes after the response, or with :attr:`max_connections`, which
52
+ it keeps open, kept for the next request of the same event loop. A response of
53
+ ``CGI`` that comes whole, with the end of the request, is written with its
54
+ ``Content-Length``. Another one is written as it comes, each chunk once the
55
+ one before has been written to the client, with the chunked transfer encoding
56
+ when it does not give its ``Content-Length``. What the application writes to
57
+ its standard error is logged.
58
+
59
+ A connection that fails or closes before the response is a response ``502``,
60
+ and one that closes after the start of the response closes the connection of
61
+ the client, as one that is quiet for longer than :attr:`inactivity_timeout`
62
+ does, which, until the response starts, the connection of the client waits
63
+ for without the inactivity timeout of the server.
64
+
65
+ Attributes
66
+ ----------
67
+
68
+ :mod:`Pyjo.App.FastCGI` inherits all attributes from :mod:`Pyjo.App` and
69
+ implements the following new ones.
70
+
71
+ See also
72
+ --------
73
+
74
+ :mod:`Pyjoyment`, :mod:`Pyjoyment.Plugin.Mount`, :mod:`Pyjo.App.WSGI`,
75
+ :mod:`Pyjo.Server.FastCGI`, https://mojolicious.org.
76
+
77
+ It has no counterpart in Mojolicious; it is the reverse of
78
+ :mod:`Pyjo.Server.FastCGI`.
79
+
80
+ Classes
81
+ -------
82
+ """
83
+
84
+ import Pyjo.App
85
+ import Pyjo.App.CGI
86
+ import Pyjo.Exception
87
+ import Pyjo.IOLoop
88
+ import Pyjo.URL
89
+
90
+ from Pyjo.Server.FastCGI import (
91
+ BEGIN_REQUEST,
92
+ END_REQUEST,
93
+ HEADER,
94
+ KEEP_CONN,
95
+ MAX_CONTENT,
96
+ PARAMS,
97
+ RESPONDER,
98
+ STDIN,
99
+ STDOUT,
100
+ _encode_pairs, # pyright: ignore[reportPrivateUsage]
101
+ _record, # pyright: ignore[reportPrivateUsage]
102
+ )
103
+
104
+ import os
105
+ import struct
106
+ import weakref
107
+
108
+ from typing import Any
109
+
110
+
111
+ # The type of a record of the standard error of the application, which a responder may send
112
+ STDERR = 7
113
+
114
+ # The id of the request, the only one of its connection
115
+ ID = 1
116
+
117
+
118
+ class Pyjo_App_FastCGI(Pyjo.App.object):
119
+ """
120
+ :mod:`Pyjo.App.FastCGI` inherits all methods from :mod:`Pyjo.App` and
121
+ implements the following new ones.
122
+ """
123
+
124
+ def __init__(self, attrs: dict[str, Any] | None = None, /, **kwargs: Any) -> None:
125
+ """::
126
+
127
+ app = Pyjo.App.FastCGI.new(url='fcgi://127.0.0.1:9000')
128
+
129
+ Construct a new :mod:`Pyjo.App.FastCGI` object.
130
+ """
131
+ self.connect_timeout: float | None = None
132
+ """::
133
+
134
+ timeout = app.connect_timeout
135
+ app.connect_timeout = 5
136
+
137
+ Maximum amount of time in seconds establishing a connection to the
138
+ application may take before getting canceled, the parameter
139
+ ``connect_timeout`` of the query of :attr:`url` or ``10``.
140
+ """
141
+
142
+ self.document_root: str | os.PathLike[str] | None = None
143
+ """::
144
+
145
+ root = app.document_root
146
+ app.document_root = '/var/www'
147
+
148
+ Directory of the scripts of the application, as of PHP, as the
149
+ application sees it, which takes the script of each request from its
150
+ path, as a web server does: the path after the mount, up to its first
151
+ part that ends with one of :attr:`script_extensions`, names the script,
152
+ whose file in the directory is ``SCRIPT_FILENAME`` and whose path after
153
+ the mount is ``SCRIPT_NAME``, the rest of the path is ``PATH_INFO``, and
154
+ a path that ends with a slash names ``index`` with the first of them in
155
+ it, as ``index.php``. A path with ``..`` is not found. With :attr:`script`, a path that names no script of the
156
+ directory is the one of :attr:`script`, as the one entry of an
157
+ application that takes every path, as with ``try_files`` of nginx,
158
+ which the path decides, not the files, which the application may see
159
+ elsewhere. ``SCRIPT_FILENAME`` of :attr:`env` takes its place.
160
+ """
161
+
162
+ self.env: dict[str, str] = {}
163
+ """::
164
+
165
+ env = app.env
166
+ app.env = {'SCRIPT_FILENAME': '/var/www/index.php'}
167
+
168
+ Parameters that are added to the environment of ``CGI`` of each
169
+ request, and replace the ones of the same name.
170
+ """
171
+
172
+ self.inactivity_timeout: float | None = None
173
+ """::
174
+
175
+ timeout = app.inactivity_timeout
176
+ app.inactivity_timeout = 300
177
+
178
+ Maximum amount of time in seconds a connection to the application can
179
+ be inactive before it gets closed, the parameter
180
+ ``inactivity_timeout`` of the query of :attr:`url` or ``60``. Until the
181
+ response starts, the connection of the client waits for it without
182
+ the inactivity timeout of the server, so that this one gives the
183
+ response ``502`` to an application that takes longer.
184
+ """
185
+
186
+ self.max_connections: int | None = None
187
+ """::
188
+
189
+ max = app.max_connections
190
+ app.max_connections = 4
191
+
192
+ Maximum number of idle connections to the application that are kept
193
+ and used again by the requests of the same event loop, the parameter
194
+ ``max_connections`` of the query of :attr:`url` or ``0``, which closes
195
+ each connection after its request. The requests ask the application
196
+ to keep the connection, as ``fastcgi_keep_conn`` of nginx does, and an
197
+ idle connection that the application closes, or that is inactive for
198
+ longer than :attr:`inactivity_timeout`, is closed and left out. A
199
+ connection that is used again and closes before the response gets the
200
+ request again on a new one.
201
+
202
+ php-fpm binds a worker to a connection for as long as the connection
203
+ is open, also while it is idle, so that each connection that is kept
204
+ takes one of the ``pm.max_children`` of the pool of php-fpm, for each
205
+ event loop, as of each worker of :mod:`Pyjo.Server.Prefork`: too many
206
+ of them leave no worker to the other clients of the pool.
207
+ """
208
+
209
+ self.script: str | None = None
210
+ """::
211
+
212
+ script = app.script
213
+ app.script = '/var/www/index.php'
214
+ app.script = 'index.php'
215
+
216
+ Script of the application for every path, as the one entry of an
217
+ application of PHP that takes every path, with the path after the
218
+ mount as ``PATH_INFO``, or for every path that names no script of
219
+ :attr:`document_root`, in which it is then.
220
+ """
221
+
222
+ self.script_extensions: list[str] | None = None
223
+ """::
224
+
225
+ extensions = app.script_extensions
226
+ app.script_extensions = ['.php', '.phtml']
227
+
228
+ Extensions of the names of the scripts of :attr:`document_root`, the
229
+ parameters ``script_extensions`` of the query of :attr:`url`, or
230
+ ``['.php']``.
231
+ """
232
+
233
+ self.sendfile: Pyjo.App.CGI.Sendfile | None = None
234
+ """::
235
+
236
+ places = app.sendfile
237
+ app.sendfile = '/var/www/private'
238
+ app.sendfile = ['/var/www/private', '/var/www/media']
239
+ app.sendfile = re.compile(r'/var/www/[^/]+/private/.*')
240
+
241
+ Directories of the files that the application may reply with by the
242
+ header ``X-Sendfile``, as Apache and lighttpd take it, with the path of
243
+ the file, absolute or in one of them, or regular expressions of the
244
+ whole path of such a file, once its symbolic links are resolved, as the
245
+ private files that are not in :attr:`document_root`, the parameters
246
+ ``sendfile`` of the query of :attr:`url`, which are directories, or
247
+ none, which passes the header on as it is. A route that embeds the
248
+ application replies then with the file, as the helper ``reply.file`` of
249
+ :mod:`Pyjoyment` does, with the status and the other headers of the
250
+ application, and a file that they do not allow is not found.
251
+ """
252
+
253
+ self.static: bool | None = None
254
+ """::
255
+
256
+ boolean = app.static
257
+ app.static = True
258
+
259
+ Reply with a file of :attr:`document_root` for a path that names it and
260
+ no script, as ``try_files`` of nginx does, as the helper ``reply.file``
261
+ of :mod:`Pyjoyment` does, with ranges and conditional requests, the
262
+ parameter ``static`` of the query of :attr:`url`, false by default.
263
+ The files are those that :mod:`Pyjoyment` sees, as for an application
264
+ on the same file system, and a route that embeds the application
265
+ replies with them, as :mod:`Pyjoyment.Plugin.Mount` makes.
266
+ """
267
+
268
+ self.url: str | Pyjo.URL.object | None = None
269
+ """::
270
+
271
+ url = app.url
272
+ app.url = 'fcgi://127.0.0.1:9000'
273
+ app.url = 'fcgi+unix://%2Frun%2Fphp%2Ffpm.sock'
274
+
275
+ Location of the application, a TCP/IP address and port of ``fcgi://``
276
+ or the path of a UNIX domain socket of ``fcgi+unix://``, escaped as the
277
+ listen locations of :mod:`Pyjo.Server.Daemon` are. Its query can give
278
+ :attr:`connect_timeout`, :attr:`document_root`,
279
+ :attr:`inactivity_timeout`, :attr:`max_connections`, :attr:`script`,
280
+ :attr:`script_extensions`, :attr:`sendfile` and :attr:`static`, as the
281
+ listen locations give their options, which the attributes take the
282
+ place of. ::
283
+
284
+ app.url = 'fcgi://127.0.0.1:9000?document_root=%2Fvar%2Fwww&script=index.php'
285
+ app.url = 'fcgi://127.0.0.1:9000?inactivity_timeout=300&max_connections=4'
286
+ """
287
+
288
+ # The idle connections to the application of each event loop, of the process that made them
289
+ self._pid = os.getpid()
290
+ self._queues: weakref.WeakKeyDictionary[Any, _Queue] = weakref.WeakKeyDictionary()
291
+
292
+ super().__init__(attrs, **kwargs)
293
+
294
+ def close(self) -> None:
295
+ """::
296
+
297
+ app.close()
298
+
299
+ Close the idle connections to the application that are kept, and the
300
+ connections and the servers of :attr:`ua`, as the end of a ``with``
301
+ block does.
302
+ """
303
+ if self._pid == os.getpid():
304
+ for queue in list(self._queues.values()):
305
+ for idle in list(queue):
306
+ idle.stop()
307
+ idle.close()
308
+ queue.clear()
309
+ super().close()
310
+
311
+ def handler(self, tx: Any) -> None:
312
+ """::
313
+
314
+ app.handler(Pyjo.Transaction.HTTP.new())
315
+ app.handler(Pyjoyment.Controller.new())
316
+
317
+ Run the application for the request of the transaction, or of the
318
+ controller of a route that embeds the application, and write its
319
+ response.
320
+ """
321
+ if (url := self.url) is None:
322
+ raise Pyjo.Exception.new('No FastCGI application')
323
+ options, query = _location(url)
324
+ options['timeout'] = _seconds(self.connect_timeout, query.get('connect_timeout'), 10)
325
+ config = {
326
+ 'inactivity': _seconds(self.inactivity_timeout, query.get('inactivity_timeout'), 60),
327
+ 'max': _number(self.max_connections, query.get('max_connections')),
328
+ 'root': os.fspath(self.document_root) if self.document_root is not None else query.get('document_root'),
329
+ 'script': self.script if self.script is not None else query.get('script'),
330
+ 'extensions': _extensions(self.script_extensions, query.get('script_extensions')),
331
+ 'sendfile': Pyjo.App.CGI._places(self.sendfile if self.sendfile is not None else query.get('sendfile')), # pyright: ignore[reportPrivateUsage]
332
+ 'static': self.static if self.static is not None else _flag(query.get('static')),
333
+ }
334
+ _Call.run(self, config, tx, options)
335
+
336
+
337
+ # A request to the application, in records of FastCGI on a connection to it
338
+ class _Call(Pyjo.App.CGI._Response): # pyright: ignore[reportPrivateUsage]
339
+ name = 'FastCGI'
340
+
341
+ # The path of the mount itself, as a server of FastCGI takes an empty PATH_INFO for the whole path in SCRIPT_NAME,
342
+ # as nginx and Caddy send it
343
+ mount_path = '/'
344
+
345
+ def __init__(self, app: Pyjo_App_FastCGI, config: dict[str, Any], tx: Any, c: Any, options: dict[str, Any]) -> None:
346
+ super().__init__(app, config, tx, c)
347
+ self.options = options
348
+ self.max: int = config['max']
349
+ self.buffer = bytearray()
350
+ self.connection: str | None = None
351
+ self.draining = False
352
+ self.ended = False
353
+ self.id: str | None = None
354
+ self.location = ':'.join(str(options[name]) for name in ('path', 'address', 'port') if name in options)
355
+ self.params = b''
356
+ self.received = False
357
+ self.reused = False
358
+
359
+ # The connection to the application has been closed, before the end of the request or after it
360
+ def _closed(self, stream: Any) -> None:
361
+ self.stream = None
362
+ if self.done or self.closed:
363
+ return
364
+
365
+ # A connection that was kept, which the application closed before it got the request, as one of its workers
366
+ # ended, gets the request again on a new one, as the request did nothing yet
367
+ if self.reused and not self.received:
368
+ return self._connect()
369
+ self._failed('FastCGI connection closed before the end of the response')
370
+
371
+ def _connect(self) -> None:
372
+ self.buffer.clear()
373
+ self.offset = 0
374
+ self.reused = False
375
+ self.id = Pyjo.IOLoop.client(self._connected, **self.options)
376
+
377
+ def _connected(self, loop: Any, err: Any, stream: Any) -> None:
378
+ self.connection, self.id = self.id, None
379
+ if self.closed:
380
+ if stream is not None:
381
+ stream.close()
382
+ return
383
+ if err:
384
+ return self._failed(f'FastCGI connection failed: {err}')
385
+
386
+ self.stream = stream
387
+ stream.timeout = self.inactivity
388
+ stream.on('read', self._read)
389
+ stream.on('close', self._closed)
390
+ stream.on('error', self._error)
391
+ stream.on('timeout', self._timeout)
392
+
393
+ # The request, one for the connection, which the application closes after it, unless it is asked to keep it
394
+ begin = _record(BEGIN_REQUEST, ID, struct.pack('>HB5x', RESPONDER, KEEP_CONN if self.max else 0))
395
+ params = b''.join(
396
+ _record(PARAMS, ID, self.params[i : i + MAX_CONTENT]) for i in range(0, len(self.params), MAX_CONTENT)
397
+ )
398
+ stream.write(begin + params + _record(PARAMS, ID))
399
+ self._stdin()
400
+
401
+ def _error(self, stream: Any, err: Any) -> None:
402
+ self._failed(f'FastCGI connection failed: {err}')
403
+
404
+ # The client has the whole response, of its length, before the end of the request, which a connection that is kept
405
+ # waits for
406
+ def _finish(self, *args: Any) -> None:
407
+ length = self.tx.res.headers.content_length
408
+ if (
409
+ self.max
410
+ and self.started
411
+ and not self.done
412
+ and not self.chunked
413
+ and not len(self.pending)
414
+ and self.stream is not None
415
+ and length is not None
416
+ and length.isdigit()
417
+ and self.sent >= int(length)
418
+ ):
419
+ self.closed = self.done = self.draining = True
420
+ return
421
+ super()._finish(*args)
422
+
423
+ def _open(self, params: dict[str, str]) -> None:
424
+ self.params = _encode_pairs(params)
425
+
426
+ # A connection that was kept, or a new one
427
+ if self.max and (idle := _dequeue(self.app, self.location)) is not None:
428
+ self.id, self.reused = idle.id, True
429
+ idle.stream.start()
430
+ return self._connected(None, None, idle.stream)
431
+ self._connect()
432
+
433
+ def _read(self, stream: Any, chunk: bytes) -> None:
434
+ if self.done and not self.draining:
435
+ return
436
+ self.received = True
437
+ buffer = self.buffer
438
+ buffer += chunk
439
+ ended = False
440
+ while len(buffer) >= HEADER.size:
441
+ version, type, id, length, padding = HEADER.unpack_from(buffer)
442
+ if version != 1:
443
+ return self._failed(f'FastCGI version {version} is not supported')
444
+ end = HEADER.size + length + padding
445
+ if len(buffer) < end:
446
+ break
447
+ content = bytes(buffer[HEADER.size : HEADER.size + length])
448
+ del buffer[:end]
449
+ if id != ID:
450
+ continue
451
+ if type == STDOUT:
452
+ if self.draining:
453
+ if len(content):
454
+ return self._drained(False)
455
+ continue
456
+ self._stdout(content)
457
+ elif type == STDERR and len(content):
458
+ self.app.log.warn(f'FastCGI application: {content.decode("utf-8", "replace").rstrip()}')
459
+ elif type == END_REQUEST:
460
+ ended = self.ended = True
461
+ break
462
+ if self.draining:
463
+ if ended:
464
+ self._drained(True)
465
+ return
466
+ self._flush(ended)
467
+
468
+ # The end of the request after the whole response, or more of the response than its length, which closes it
469
+ def _drained(self, ended: bool) -> None:
470
+ self.draining = False
471
+ if not ended:
472
+ self.aborted = True
473
+ self._stop()
474
+
475
+ # The body of the request, a record once the one before has been written, and an empty record at its end
476
+ def _stdin(self, *args: Any) -> None:
477
+ if (stream := self.stream) is None or self.done:
478
+ return
479
+ chunk = self.tx.req.content.get_body_chunk(self.offset) or b''
480
+ if not len(chunk):
481
+ return stream.write(_record(STDIN, ID))
482
+ self.offset += len(chunk)
483
+ records = b''.join(_record(STDIN, ID, chunk[i : i + MAX_CONTENT]) for i in range(0, len(chunk), MAX_CONTENT))
484
+ stream.write(records, self._stdin)
485
+
486
+ def _timeout(self, stream: Any) -> None:
487
+ self._failed('FastCGI connection inactive')
488
+
489
+ # The end of the connection to the application, and of its connecting, or the connection kept for the next request,
490
+ # after a whole response, with nothing after it
491
+ def _stop(self) -> None:
492
+ if (id := self.id) is not None:
493
+ self.id = None
494
+ Pyjo.IOLoop.remove(id)
495
+ if (stream := self.stream) is not None:
496
+ self.stream = None
497
+ stream.unsubscribe('read', self._read).unsubscribe('close', self._closed)
498
+ stream.unsubscribe('error', self._error).unsubscribe('timeout', self._timeout)
499
+ if self.max and self.ended and not self.aborted and not len(self.buffer) and self.connection is not None:
500
+ return _enqueue(self.app, self.max, _Idle(self.location, self.connection, stream))
501
+ stream.close()
502
+
503
+
504
+ # An idle connection to the application, which is closed when the application closes it, its inactivity timeout ends,
505
+ # or the application sends what no request asked for
506
+ class _Idle:
507
+ def __init__(self, location: str, id: str, stream: Any) -> None:
508
+ self.location = location
509
+ self.id = id
510
+ self.queue: weakref.ref[_Queue] | None = None
511
+ self.stream = stream
512
+
513
+ def close(self, *args: Any) -> None:
514
+ self.stream.close()
515
+
516
+ def closed(self, stream: Any) -> None:
517
+ if self.queue is not None and (queue := self.queue()) is not None and self in queue:
518
+ queue.remove(self)
519
+
520
+ def start(self, queue: '_Queue') -> None:
521
+ self.queue = weakref.ref(queue)
522
+ stream = self.stream
523
+ stream.on('close', self.closed)
524
+ stream.on('error', self.close)
525
+ stream.on('read', self.close)
526
+ stream.start()
527
+
528
+ def stop(self) -> None:
529
+ self.queue = None
530
+ self.stream.unsubscribe('close', self.closed).unsubscribe('error', self.close).unsubscribe('read', self.close)
531
+
532
+
533
+ # The idle connections of an event loop, which an idle one refers to weakly, as the stream of the event loop refers to it
534
+ class _Queue(list[_Idle]):
535
+ pass
536
+
537
+
538
+ # An idle connection to the location, which the application has not closed, out of the queue of the event loop
539
+ def _dequeue(app: Pyjo_App_FastCGI, location: str) -> _Idle | None:
540
+ queue = _queue(app)
541
+ for idle in list(queue):
542
+ if idle.location != location:
543
+ continue
544
+ queue.remove(idle)
545
+ idle.stop()
546
+
547
+ # An end of the connection that is there to read
548
+ if idle.stream.is_readable:
549
+ idle.close()
550
+ continue
551
+ return idle
552
+ return None
553
+
554
+
555
+ # An idle connection into the queue of the event loop, after the oldest ones that are too many
556
+ def _enqueue(app: Pyjo_App_FastCGI, max: int, idle: _Idle) -> None:
557
+ queue = _queue(app)
558
+ while len(queue) and len(queue) >= max:
559
+ old = queue.pop(0)
560
+ old.stop()
561
+ old.close()
562
+ queue.append(idle)
563
+ idle.start(queue)
564
+
565
+
566
+ # The extensions of the scripts of the attribute, of the query of the location, or the one of PHP
567
+ def _extensions(value: list[str] | None, query: list[str] | None) -> list[str]:
568
+ if value is not None:
569
+ return value
570
+ return query if query else ['.php']
571
+
572
+
573
+ # The options of the connection to the location of the application, and the parameters of its query
574
+ def _location(url: Any) -> tuple[dict[str, Any], dict[str, Any]]:
575
+ url = Pyjo.URL.new(str(url))
576
+ names = ('connect_timeout', 'document_root', 'inactivity_timeout', 'max_connections', 'script', 'static')
577
+ query: dict[str, Any] = {name: value for name in names if (value := url.query.param(name)) is not None}
578
+ for name in ('script_extensions', 'sendfile'):
579
+ if values := url.query.every_param(name):
580
+ query[name] = values
581
+ if (proto := url.protocol) == 'fcgi+unix':
582
+ return {'path': url.host}, query
583
+ if proto == 'fcgi' and url.host and url.port:
584
+ return {'address': url.host, 'port': url.port}, query
585
+ raise Pyjo.Exception.new(f'Invalid FastCGI location "{url}"')
586
+
587
+
588
+ # A number of the attribute, of the query of the location, or none
589
+ def _number(value: int | None, query: str | None) -> int:
590
+ if value is not None:
591
+ return value
592
+ if query is None:
593
+ return 0
594
+ try:
595
+ return int(query)
596
+ except ValueError:
597
+ raise Pyjo.Exception.new(f'Invalid FastCGI number "{query}"') from None
598
+
599
+
600
+ # The idle connections of the event loop, where the child of a fork closes the ones of its parent, which the parent
601
+ # still uses
602
+ def _queue(app: Pyjo_App_FastCGI) -> _Queue:
603
+ loop = Pyjo.IOLoop.singleton
604
+ if app._pid != (pid := os.getpid()): # pyright: ignore[reportPrivateUsage]
605
+ for idle in app._queues.get(loop, []): # pyright: ignore[reportPrivateUsage]
606
+ idle.stop()
607
+ idle.close()
608
+ app._pid, app._queues = pid, weakref.WeakKeyDictionary() # pyright: ignore[reportPrivateUsage]
609
+ return app._queues.setdefault(loop, _Queue()) # pyright: ignore[reportPrivateUsage]
610
+
611
+
612
+ # A timeout of the attribute, of the query of the location, or its default
613
+ def _seconds(value: float | None, query: str | None, default: float) -> float:
614
+ if value is not None:
615
+ return value
616
+ if query is None:
617
+ return default
618
+ try:
619
+ return float(query)
620
+ except ValueError:
621
+ raise Pyjo.Exception.new(f'Invalid FastCGI timeout "{query}"') from None
622
+
623
+
624
+ # A flag of the query of the location, as the listen locations of Pyjo.Server.Daemon have them
625
+ def _flag(value: str | None) -> bool:
626
+ return value is not None and value not in ('', '0')
627
+
628
+
629
+ new = Pyjo_App_FastCGI.new
630
+ object = Pyjo_App_FastCGI
631
+ FastCGI = Pyjo_App_FastCGI