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
@@ -0,0 +1,725 @@
1
+ r"""
2
+ Pyjo.Message.Request - HTTP request
3
+ ===================================
4
+ ::
5
+
6
+ from Pyjo.Message.Request import Request
7
+
8
+ # Parse
9
+ req = Request()
10
+ req.parse(b'GET /foo HTTP/1.0\x0d\x0a')
11
+ req.parse(b'Content-Length: 12\x0d\x0a')
12
+ req.parse(b'Content-Type: text/plain\x0d\x0a\x0d\x0a')
13
+ req.parse(b'Hello World!')
14
+ print(req.method)
15
+ print(req.headers.content_type)
16
+ print(req.body)
17
+
18
+ # Or import the module, as in Google Python Style Guide
19
+
20
+ import Pyjo.Message.Request
21
+
22
+ # Parse
23
+ req = Pyjo.Message.Request.new()
24
+ req.parse(b'GET /foo HTTP/1.0\x0d\x0a')
25
+ req.parse(b'Content-Length: 12\x0d\x0a')
26
+ req.parse(b'Content-Type: text/plain\x0d\x0a\x0d\x0a')
27
+ req.parse(b'Hello World!')
28
+ print(req.method)
29
+ print(req.headers.content_type)
30
+ print(req.body)
31
+
32
+ # Build
33
+ req = Pyjo.Message.Request.new()
34
+ req.url.parse('http://127.0.0.1/foo/bar')
35
+ req.method = 'GET'
36
+ print(req.to_bytes())
37
+
38
+ :mod:`Pyjo.Message.Request` is a container for HTTP requests, based on
39
+ `RFC 7230 <https://tools.ietf.org/html/rfc7230>`_,
40
+ `RFC 7231 <https://tools.ietf.org/html/rfc7231>`_,
41
+ `RFC 7235 <https://tools.ietf.org/html/rfc7235>`_ and
42
+ `RFC 2817 <https://tools.ietf.org/html/rfc2817>`_.
43
+
44
+ Events
45
+ ------
46
+
47
+ :mod:`Pyjo.Message.Request` inherits all events from :mod:`Pyjo.Message`.
48
+
49
+ See also
50
+ --------
51
+
52
+ :mod:`Pyjoyment`, https://mojolicious.org.
53
+
54
+ Ported from `Mojo::Message::Request <https://github.com/mojolicious/mojo/blob/v9.51/lib/Mojo/Message/Request.pm>`_
55
+ of Mojolicious 9.51.
56
+ """
57
+
58
+ import Pyjo.Cookie.Request
59
+ import Pyjo.Message
60
+ import Pyjo.Parameters
61
+ import Pyjo.URL
62
+
63
+ from Pyjo.Util import b64_decode, b64_encode, decode, url_unescape
64
+
65
+ import base64
66
+ import builtins
67
+ import hashlib
68
+ import os
69
+ import random
70
+ import re
71
+ import time
72
+ import urllib.request
73
+
74
+ from collections.abc import Iterable, Mapping
75
+ from typing import TYPE_CHECKING, Any, Self, cast, overload
76
+
77
+
78
+ if TYPE_CHECKING:
79
+ import http.server
80
+
81
+
82
+ type Anything = builtins.object
83
+
84
+
85
+ class Pyjo_Message_Request(Pyjo.Message.object):
86
+ """
87
+ :mod:`Pyjo.Message.Request` inherits all methods from :mod:`Pyjo.Message`
88
+ and implements the following new ones.
89
+ """
90
+
91
+ def __init__(
92
+ self,
93
+ request: 'urllib.request.Request | http.server.BaseHTTPRequestHandler | None' = None,
94
+ /,
95
+ *,
96
+ env: Mapping[str, Any] | None = None,
97
+ method: str = 'GET',
98
+ proxy: Pyjo.URL.object | None = None,
99
+ request_id: str | None = None,
100
+ reverse_proxy: bool | None = None,
101
+ trusted_proxies: list[str] | None = None,
102
+ url: Pyjo.URL.object | None = None,
103
+ via_proxy: bool = True,
104
+ **kwargs: Any,
105
+ ) -> None:
106
+ """::
107
+
108
+ req = Pyjo.Message.Request.new()
109
+ req = Pyjo.Message.Request.new(method='POST', url=Pyjo.URL.new('http://127.0.0.1/'))
110
+ req = Pyjo.Message.Request.new(urllib.request.Request('http://127.0.0.1/', data=b'Hi!'))
111
+ req = Pyjo.Message.Request.new(handler)
112
+
113
+ Construct a new :mod:`Pyjo.Message.Request` object, from a request of
114
+ the standard library if it is given: the method, URL, headers and data
115
+ of a :class:`urllib.request.Request`, or the request that a
116
+ :class:`http.server.BaseHTTPRequestHandler` has parsed, which is parsed
117
+ again, as :meth:`parse` does, with the body that it reads from the
118
+ ``rfile`` of the handler, of the ``Content-Length`` or chunked, and not
119
+ further. ::
120
+
121
+ # A request in a handler of http.server
122
+ class Handler(http.server.BaseHTTPRequestHandler):
123
+ def do_GET(self):
124
+ req = Pyjo.Message.Request.new(self)
125
+ res = Pyjo.Message.Response.new(code=200).set(body=bytes(req.url.path))
126
+ self.wfile.write(res.to_bytes())
127
+ """
128
+ super().__init__(**kwargs)
129
+
130
+ self.env: Mapping[str, Any] = env if env is not None else {}
131
+ """::
132
+
133
+ env = req.env
134
+ req.env = {'PATH_INFO': '/'}
135
+
136
+ Direct access to the ``CGI`` or ``WSGI`` environment dict if
137
+ available. ::
138
+
139
+ # Check CGI version
140
+ version = req.env['GATEWAY_INTERFACE']
141
+
142
+ # Check WSGI version
143
+ version = req.env['wsgi.version']
144
+ """
145
+
146
+ self.method = method
147
+ """::
148
+
149
+ method = req.method
150
+ req.method = 'POST'
151
+
152
+ HTTP request method, defaults to ``GET``.
153
+ """
154
+
155
+ self.proxy = proxy
156
+ """::
157
+
158
+ url = req.proxy
159
+ req.proxy = Pyjo.URL.new('http://127.0.0.1:3000')
160
+
161
+ Proxy URL for request.
162
+ """
163
+
164
+ self.reverse_proxy = reverse_proxy
165
+ """::
166
+
167
+ boolean = req.reverse_proxy
168
+ req.reverse_proxy = boolean
169
+
170
+ Request has been performed through a reverse proxy.
171
+ """
172
+
173
+ self.trusted_proxies: list[str] = trusted_proxies if trusted_proxies is not None else []
174
+ """::
175
+
176
+ proxies = req.trusted_proxies
177
+ req.trusted_proxies = ['10.0.0.0/8', '127.0.0.1', '172.16.0.0/12', '192.168.0.0/16', 'fc00::/7']
178
+
179
+ Trusted reverse proxies, addresses or networks in CIDR form.
180
+ """
181
+
182
+ self.via_proxy = via_proxy
183
+ """::
184
+
185
+ boolean = req.via_proxy
186
+ req.via_proxy = boolean
187
+
188
+ Request can be performed through a proxy server.
189
+ """
190
+
191
+ self._params: Pyjo.Parameters.object | None = None
192
+ self._request_id = request_id
193
+ self._start_buffer: bytes | None = None
194
+ self._url = url
195
+
196
+ # Not in Mojolicious: a request of urllib or http.server
197
+ if isinstance(request, urllib.request.Request):
198
+ self._from_urllib(request)
199
+ elif request is not None:
200
+ self._from_http_server(request)
201
+
202
+ @property
203
+ def request_id(self) -> str:
204
+ """::
205
+
206
+ request_id = req.request_id
207
+ req.request_id = 'aee7d5d8'
208
+
209
+ Request ID, defaults to a reasonably unique value.
210
+ """
211
+ if self._request_id is None:
212
+ self._request_id = _request_id()
213
+ return self._request_id
214
+
215
+ @request_id.setter
216
+ def request_id(self, value: str) -> None:
217
+ self._request_id = value
218
+
219
+ @property
220
+ def url(self) -> Pyjo.URL.object:
221
+ """::
222
+
223
+ url = req.url
224
+ req.url = Pyjo.URL.new()
225
+
226
+ HTTP request URL, defaults to a :mod:`Pyjo.URL` object. ::
227
+
228
+ # Get request information
229
+ info = req.url.to_abs().userinfo
230
+ host = req.url.to_abs().host
231
+ path = req.url.to_abs().path
232
+ """
233
+ if self._url is None:
234
+ self._url = Pyjo.URL.new()
235
+ return self._url
236
+
237
+ @url.setter
238
+ def url(self, value: Pyjo.URL.object) -> None:
239
+ self._url = value
240
+
241
+ def clone(self) -> Self | None:
242
+ """::
243
+
244
+ clone = req.clone()
245
+
246
+ Return a new :mod:`Pyjo.Message.Request` object cloned from this
247
+ request if possible, otherwise return ``None``.
248
+ """
249
+ # Dynamic requests cannot be cloned
250
+ if (content := self.content.clone()) is None:
251
+ return None
252
+ clone = self.new(content=content, method=self.method, url=self.url.clone(), version=self.version)
253
+ if self.proxy:
254
+ clone.proxy = self.proxy.clone()
255
+
256
+ return clone
257
+
258
+ @overload
259
+ def cookies(self) -> list[Pyjo.Cookie.Request.object]: ...
260
+
261
+ @overload
262
+ def cookies(self, cookie: Pyjo.Cookie.object | Mapping[str, Any], /, *cookies: Anything) -> Self: ...
263
+
264
+ def cookies(self, *cookies: Anything) -> list[Pyjo.Cookie.Request.object] | Self:
265
+ """::
266
+
267
+ cookies = req.cookies()
268
+ req = req.cookies(Pyjo.Cookie.Request.new())
269
+ req = req.cookies({'name': 'foo', 'value': 'bar'})
270
+
271
+ Access request cookies, usually :mod:`Pyjo.Cookie.Request` objects. ::
272
+
273
+ # Names of all cookies
274
+ for cookie in req.cookies():
275
+ print(cookie.name)
276
+ """
277
+ # Parse cookies
278
+ headers = self.headers
279
+ if not cookies:
280
+ return Pyjo.Cookie.Request.parse(headers.cookie)
281
+
282
+ # Add cookies
283
+ values = [headers.cookie] if headers.cookie else []
284
+ for cookie in cookies:
285
+ values.append(
286
+ str(
287
+ Pyjo.Cookie.Request.new(**cast(Mapping[str, Any], cookie))
288
+ if isinstance(cookie, Mapping)
289
+ else cookie
290
+ )
291
+ )
292
+ headers.cookie = '; '.join(values)
293
+
294
+ return self
295
+
296
+ def every_param(self, name: str) -> list[str]:
297
+ """::
298
+
299
+ values = req.every_param('foo')
300
+
301
+ Similar to :meth:`param`, but returns all values sharing the same name
302
+ as a list. ::
303
+
304
+ # Get first value
305
+ print(req.every_param('foo')[0])
306
+ """
307
+ return self.params.every_param(name)
308
+
309
+ def extract_start_line(self, buffer: bytearray) -> bool | None:
310
+ """::
311
+
312
+ boolean = req.extract_start_line(buffer)
313
+
314
+ Extract request-line from a :class:`bytearray`, which loses it.
315
+ """
316
+ # Ignore any leading empty lines
317
+ if not (m := re.match(rb'\s*(.*?)\x0d?\x0a', bytes(buffer))):
318
+ return None
319
+ del buffer[: m.end()]
320
+
321
+ # We have a (hopefully) full request-line
322
+ if not (m := re.fullmatch(rb'(\S+)\s+(\S+)\s+HTTP/(\d\.\d)', m[1])):
323
+ self.error = {'message': 'Bad request start-line'}
324
+ return False
325
+ self.method, self.version = m[1].decode('latin-1'), m[3].decode('latin-1')
326
+ url = self.url
327
+ target = m[2].decode('latin-1')
328
+ if self.method == 'CONNECT':
329
+ url.host_port = target
330
+ elif re.match(r'[^:/?#]+:', target):
331
+ url.parse(target).fragment = None
332
+ else:
333
+ url.path_query = target
334
+ return True
335
+
336
+ def fix_headers(self) -> Self:
337
+ """::
338
+
339
+ req = req.fix_headers()
340
+
341
+ Make sure request has all required headers.
342
+ """
343
+ if self._fix:
344
+ return self
345
+ super().fix_headers()
346
+
347
+ # Empty
348
+ headers = self.headers
349
+ if headers.content_length == '0' and self.method == 'GET':
350
+ headers.remove('Content-Length')
351
+
352
+ # Host
353
+ url = self.url
354
+ if headers.host is None:
355
+ headers.host = url.host_port or ''
356
+
357
+ # Basic authentication
358
+ if (info := url.userinfo) and not headers.authorization:
359
+ headers.authorization = 'Basic ' + _basic_encode(info)
360
+
361
+ # Basic proxy authentication
362
+ if not ((proxy := self.proxy) and self.via_proxy):
363
+ return self
364
+ if not (info := proxy.userinfo):
365
+ return self
366
+ if not headers.proxy_authorization:
367
+ headers.proxy_authorization = 'Basic ' + _basic_encode(info)
368
+ return self
369
+
370
+ def get_start_line_chunk(self, offset: int) -> bytes:
371
+ """::
372
+
373
+ data = req.get_start_line_chunk(offset)
374
+
375
+ Get a chunk of request-line data starting from a specific position.
376
+ Note that this method finalizes the request.
377
+ """
378
+ buffer = self._start_line()
379
+ self.emit('progress', 'start_line', offset)
380
+ return buffer[offset : offset + 131072]
381
+
382
+ @property
383
+ def is_handshake(self) -> bool:
384
+ """::
385
+
386
+ boolean = req.is_handshake
387
+
388
+ Check ``Upgrade`` header for ``websocket`` value.
389
+ """
390
+ return (self.headers.upgrade or '').lower() == 'websocket'
391
+
392
+ @property
393
+ def is_secure(self) -> bool:
394
+ """::
395
+
396
+ boolean = req.is_secure
397
+
398
+ Check if connection is secure.
399
+ """
400
+ url = self.url
401
+ return (url.protocol or url.base.protocol) == 'https'
402
+
403
+ @property
404
+ def is_xhr(self) -> bool:
405
+ """::
406
+
407
+ boolean = req.is_xhr
408
+
409
+ Check ``X-Requested-With`` header for ``XMLHttpRequest`` value.
410
+ """
411
+ return bool(re.search(r'XMLHttpRequest', self.headers.header('X-Requested-With') or '', re.I))
412
+
413
+ @overload
414
+ def param(self, name: str, /) -> str | None: ...
415
+
416
+ @overload
417
+ def param(self, name: str, value: Anything, /, *values: Anything) -> Pyjo.Parameters.object: ...
418
+
419
+ def param(self, name: str, /, *values: Anything) -> str | None | Pyjo.Parameters.object:
420
+ """::
421
+
422
+ value = req.param('foo')
423
+
424
+ Access ``GET`` and ``POST`` parameters extracted from the query string
425
+ and ``application/x-www-form-urlencoded`` or ``multipart/form-data``
426
+ message body. If there are multiple values sharing the same name, and
427
+ you want to access more than just the last one, you can use
428
+ :meth:`every_param`. Note that this method caches all data, so it
429
+ should not be called before the entire request body has been received.
430
+ Parts of the request body need to be loaded into memory to parse
431
+ ``POST`` parameters, so you have to make sure it is not excessively
432
+ large. There's a 16MiB limit for requests by default.
433
+ """
434
+ return self.params.param(name, *values)
435
+
436
+ @property
437
+ def params(self) -> Pyjo.Parameters.object:
438
+ """::
439
+
440
+ params = req.params
441
+
442
+ All ``GET`` and ``POST`` parameters extracted from the query string and
443
+ ``application/x-www-form-urlencoded`` or ``multipart/form-data``
444
+ message body, usually a :mod:`Pyjo.Parameters` object. Note that this
445
+ method caches all data, so it should not be called before the entire
446
+ request body has been received. Parts of the request body need to be
447
+ loaded into memory to parse ``POST`` parameters, so you have to make
448
+ sure it is not excessively large. There's a 16MiB limit for requests by
449
+ default. ::
450
+
451
+ # Get parameter names and values
452
+ params = req.params.to_dict()
453
+ """
454
+ if self._params is None:
455
+ self._params = self.body_params.clone().append(self.query_params)
456
+ return self._params
457
+
458
+ def parse(self, chunk: bytes | Mapping[str, Any]) -> Self:
459
+ """::
460
+
461
+ req = req.parse(b'GET /foo/bar HTTP/1.1')
462
+ req = req.parse({'PATH_INFO': '/'})
463
+
464
+ Parse HTTP request chunks or environment dict.
465
+ """
466
+ env, chunk = (chunk, b'') if isinstance(chunk, Mapping) else (None, chunk)
467
+
468
+ # Parse CGI environment
469
+ if env is not None:
470
+ self.env = env
471
+ self._parse_env(env)
472
+
473
+ # Parse normal message
474
+ if (self._state or '') != 'cgi':
475
+ super().parse(chunk)
476
+
477
+ # Parse CGI content
478
+ else:
479
+ if env is None:
480
+ self._raw_size += len(chunk)
481
+ self.content = self.content.parse_body(chunk)
482
+ super().parse(b'')
483
+
484
+ # Check if we can fix things that require all headers
485
+ if not self.is_finished:
486
+ return self
487
+
488
+ # Base URL
489
+ base = self.url.base
490
+ if not base.scheme:
491
+ base.scheme = 'http'
492
+ headers = self.headers
493
+ if not base.host and (host := headers.host):
494
+ base.host_port = host
495
+
496
+ # Basic authentication
497
+ if basic := _basic(headers.authorization):
498
+ base.userinfo = basic
499
+
500
+ # Basic proxy authentication
501
+ if basic := _basic(headers.proxy_authorization):
502
+ self.proxy = Pyjo.URL.new().set(userinfo=basic)
503
+
504
+ # "X-Forwarded-Proto"
505
+ if self.reverse_proxy and headers.header('X-Forwarded-Proto') == 'https':
506
+ base.scheme = 'https'
507
+
508
+ return self
509
+
510
+ @property
511
+ def query_params(self) -> Pyjo.Parameters.object:
512
+ """::
513
+
514
+ params = req.query_params
515
+
516
+ All ``GET`` parameters, usually a :mod:`Pyjo.Parameters` object. ::
517
+
518
+ # Turn GET parameters to dict and extract value
519
+ print(req.query_params.to_dict()['foo'])
520
+ """
521
+ return self.url.query
522
+
523
+ @property
524
+ def start_line_size(self) -> int:
525
+ """::
526
+
527
+ size = req.start_line_size
528
+
529
+ Size of the request-line in bytes. Note that this method finalizes the
530
+ request.
531
+ """
532
+ return len(self._start_line())
533
+
534
+ # Not in Mojolicious: a request of urllib
535
+ def to_urllib(self) -> urllib.request.Request:
536
+ """::
537
+
538
+ request = req.to_urllib()
539
+
540
+ Turn request into a :class:`urllib.request.Request`, with the absolute
541
+ :attr:`url`, the headers, the body and the :attr:`proxy`. urllib takes
542
+ no userinfo in a URL, so the userinfo of :attr:`url` and :attr:`proxy`
543
+ becomes the ``Authorization`` and ``Proxy-Authorization`` headers, as
544
+ :meth:`fix_headers` does. ::
545
+
546
+ # Send the request with urllib
547
+ with urllib.request.urlopen(req.to_urllib()) as response:
548
+ res = Pyjo.Message.Response.new(response)
549
+ """
550
+ url = self.url.to_abs()
551
+ headers = self.headers
552
+ body = self.build_body() if self.content.is_multipart else self.body
553
+ request = urllib.request.Request(
554
+ str(url),
555
+ data=body or None,
556
+ headers={name: value for name in headers.names() if (value := headers.header(name)) is not None},
557
+ method=self.method,
558
+ )
559
+
560
+ # Basic authentication
561
+ if (info := url.userinfo) and not headers.authorization:
562
+ request.add_header('Authorization', 'Basic ' + _basic_encode(info))
563
+
564
+ # Proxy with basic authentication
565
+ if (proxy := self.proxy) and self.via_proxy:
566
+ request.set_proxy(proxy.host_port or '', proxy.scheme or 'http')
567
+ if (info := proxy.userinfo) and not headers.proxy_authorization:
568
+ request.add_header('Proxy-Authorization', 'Basic ' + _basic_encode(info))
569
+
570
+ return request
571
+
572
+ # Not in Mojolicious: a request of http.server
573
+ def _from_http_server(self, handler: 'http.server.BaseHTTPRequestHandler') -> None:
574
+ start = f'{handler.command} {handler.path} {handler.request_version}\x0d\x0a'
575
+ headers = ''.join(f'{name}: {value}\x0d\x0a' for name, value in handler.headers.items())
576
+ self.parse(f'{start}{headers}\x0d\x0a'.encode('latin-1'))
577
+
578
+ # The body of the Content-Length, or chunked line by line, so that nothing of the next request is read
579
+ rfile = handler.rfile
580
+ if not self.is_finished and (length := self.headers.content_length or '').isdigit():
581
+ self.parse(rfile.read(int(length)))
582
+ while not self.is_finished and (line := rfile.readline(131072)):
583
+ self.parse(line)
584
+
585
+ # Not in Mojolicious: a request of urllib
586
+ def _from_urllib(self, request: urllib.request.Request) -> None:
587
+ self.method = request.get_method()
588
+ self.url = Pyjo.URL.new(request.full_url)
589
+ headers = self.headers
590
+ for name, value in request.header_items():
591
+ headers.add(name, value)
592
+
593
+ # Data as bytes, a file or an iterable of bytes
594
+ data = request.data
595
+ if data is None:
596
+ return
597
+ if isinstance(data, bytes | bytearray):
598
+ self.body = bytes(data)
599
+ elif isinstance(data, memoryview):
600
+ self.body = data.tobytes()
601
+ elif (read := getattr(data, 'read', None)) is not None:
602
+ self.body = bytes(read())
603
+ else:
604
+ self.body = b''.join(bytes(chunk) for chunk in cast(Iterable[bytes], data))
605
+
606
+ def _parse_env(self, env: Mapping[str, Any]) -> None:
607
+ # Bypass normal message parser
608
+ self._state = 'cgi'
609
+
610
+ # Extract headers
611
+ headers = self.headers
612
+ url = self.url
613
+ base = url.base
614
+ for name, value in env.items():
615
+ if not (m := re.match(r'HTTP_(.*)', name, re.I | re.S)):
616
+ continue
617
+ name, value = m[1].replace('_', '-'), str(value)
618
+ headers.header(name, value)
619
+
620
+ # Host/Port
621
+ if name == 'HOST':
622
+ if m := re.search(r':(\d+)$', value):
623
+ base.host, base.port = value[: m.start()], int(m[1])
624
+ else:
625
+ base.host = value
626
+
627
+ # Content-Type is a special case on some servers
628
+ if env.get('CONTENT_TYPE'):
629
+ headers.content_type = str(env['CONTENT_TYPE'])
630
+
631
+ # Content-Length is a special case on some servers
632
+ if env.get('CONTENT_LENGTH'):
633
+ headers.content_length = str(env['CONTENT_LENGTH'])
634
+
635
+ # Query
636
+ if env.get('QUERY_STRING'):
637
+ url.query.parse(str(env['QUERY_STRING']))
638
+
639
+ # Method
640
+ if env.get('REQUEST_METHOD'):
641
+ self.method = str(env['REQUEST_METHOD'])
642
+
643
+ # Scheme/Version
644
+ if m := re.fullmatch(r'([^/]+)/([^/]+)', str(env.get('SERVER_PROTOCOL', ''))):
645
+ base.scheme, self.version = m[1], m[2]
646
+
647
+ # HTTPS
648
+ if str(env.get('HTTPS', '')).upper() == 'ON':
649
+ base.scheme = 'https'
650
+
651
+ # Path
652
+ path = url.path.parse(str(env['PATH_INFO']) if env.get('PATH_INFO') else '')
653
+
654
+ # Base path
655
+ if value := env.get('SCRIPT_NAME'):
656
+ value = str(value)
657
+
658
+ # Make sure there is a trailing slash (important for merging)
659
+ base.path.parse(value if value.endswith('/') else f'{value}/')
660
+
661
+ # Remove SCRIPT_NAME prefix if necessary (unless the server already did it)
662
+ buffer = path.to_str()
663
+ value = re.sub(r'^/|/$', '', value)
664
+ uri = url_unescape(re.sub(r'\?.*$', '', str(env.get('REQUEST_URI') or ''), count=1))
665
+ if not uri.startswith(f'/{value}' + str(env.get('PATH_INFO') or '')):
666
+ buffer = re.sub(rf'^/?{re.escape(value)}(?:/|$)', '', buffer, count=1)
667
+ buffer = re.sub(r'^/', '', buffer, count=1)
668
+ path.parse(buffer)
669
+
670
+ def _start_line(self) -> bytes:
671
+ if self._start_buffer is not None:
672
+ return self._start_buffer
673
+
674
+ # Path
675
+ url = self.url
676
+ path = url.path_query
677
+ if not path.startswith('/'):
678
+ path = f'/{path}'
679
+
680
+ # CONNECT
681
+ method = self.method.upper()
682
+ if method == 'CONNECT':
683
+ port = url.port if url.port is not None else 443 if url.protocol == 'https' else 80
684
+ path = f'{url.ihost}:{port}'
685
+
686
+ # Proxy
687
+ elif self.proxy and self.via_proxy and url.protocol != 'https':
688
+ if not self.is_handshake:
689
+ path = url.clone().set(userinfo=None).to_str()
690
+
691
+ self._start_buffer = f'{method} {path} HTTP/{self.version}\x0d\x0a'.encode('latin-1')
692
+ return self._start_buffer
693
+
694
+
695
+ def _basic(header: str | None) -> str | None:
696
+ if not header or not (m := re.search(r'Basic (.+)$', header)):
697
+ return None
698
+ # Unlike Mojolicious: the userinfo is decoded from UTF-8 where it can be, as the URL decodes it; Mojo keeps the bytes
699
+ info = b64_decode(m[1])
700
+ return decoded if (decoded := decode('UTF-8', info)) is not None else info.decode('latin-1')
701
+
702
+
703
+ def _basic_encode(info: str) -> str:
704
+ # Unlike Mojolicious: the userinfo is encoded as UTF-8, as the URL decodes it from UTF-8; Mojo encodes it as
705
+ # Latin-1 and dies of "Wide character" on any other character
706
+ return b64_encode(info.encode('UTF-8'), '')
707
+
708
+
709
+ # The seed and the counter of request IDs, made on first use, as Perl's state variables
710
+ _seed: str | None = None
711
+ _counter = 0
712
+
713
+
714
+ def _request_id() -> str:
715
+ global _seed, _counter
716
+ if _seed is None:
717
+ _seed, _counter = f'{os.getpid()}{time.time()}{random.random()}', random.randrange(0xFFFFFF)
718
+ _counter = (_counter + 1) % 0xFFFFFF
719
+ b64 = base64.b64encode(hashlib.sha1(f'{_seed}{_counter}'.encode()).digest()).decode()[:12]
720
+ return b64.translate(str.maketrans('+/', '-_'))
721
+
722
+
723
+ new = Pyjo_Message_Request.new
724
+ object = Pyjo_Message_Request
725
+ Request = Pyjo_Message_Request