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/URL/__init__.py ADDED
@@ -0,0 +1,651 @@
1
+ """
2
+ Pyjo.URL - Uniform Resource Locator
3
+ ===================================
4
+ ::
5
+
6
+ from Pyjo.URL import URL
7
+
8
+ # Parse
9
+ url = URL('http://sri:foo@example.com:3000/foo?foo=bar#23')
10
+ print(url.scheme)
11
+ print(url.userinfo)
12
+ print(url.host)
13
+ print(url.port)
14
+ print(url.path)
15
+ print(url.query)
16
+ print(url.fragment)
17
+
18
+ # Or import the module, as in Google Python Style Guide
19
+
20
+ import Pyjo.URL
21
+
22
+ # Parse
23
+ url = Pyjo.URL.new('http://sri:foo@example.com:3000/foo?foo=bar#23')
24
+ print(url.scheme)
25
+ print(url.userinfo)
26
+ print(url.host)
27
+ print(url.port)
28
+ print(url.path)
29
+ print(url.query)
30
+ print(url.fragment)
31
+
32
+ # Build
33
+ url = Pyjo.URL.new()
34
+ url.scheme = 'http'
35
+ url.host = 'example.com'
36
+ url.port = 3000
37
+ url.path = '/foo/bar'
38
+ url.query = ('foo', 'bar')
39
+ url.fragment = '23'
40
+ print(url)
41
+
42
+ :mod:`Pyjo.URL` implements a subset of :rfc:`3986`, :rfc:`3987` and the
43
+ `URL Living Standard <https://url.spec.whatwg.org>`_ for Uniform Resource
44
+ Locators with support for IDNA and IRIs.
45
+
46
+ See also
47
+ --------
48
+
49
+ :mod:`Pyjoyment`, https://mojolicious.org.
50
+
51
+ Ported from `Mojo::URL <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/URL.pm>`_
52
+ of Mojolicious 9.49.
53
+
54
+ Classes
55
+ -------
56
+ """
57
+
58
+ import Pyjo.Base
59
+ import Pyjo.Parameters
60
+ import Pyjo.Path
61
+ import Pyjo.String.Mixin
62
+
63
+ from Pyjo.Util import decode, punycode_decode, punycode_encode, url_escape, url_unescape
64
+
65
+ import builtins
66
+ import re
67
+
68
+ from typing import Self, cast
69
+ from urllib.parse import ParseResult, ParseResultBytes, SplitResult, SplitResultBytes, urlsplit
70
+
71
+
72
+ # The module ends with `object = Pyjo_URL`, which an annotation of `object`
73
+ # would mean, as annotations are evaluated lazily
74
+ type Anything = builtins.object
75
+
76
+ # A result of urllib.parse.urlsplit or urllib.parse.urlparse
77
+ type UrllibResult = SplitResult | ParseResult | SplitResultBytes | ParseResultBytes
78
+
79
+
80
+ class Pyjo_URL(Pyjo.Base.object, Pyjo.String.Mixin.object):
81
+ """
82
+ :mod:`Pyjo.URL` inherits all methods from :mod:`Pyjo.Base` and
83
+ :mod:`Pyjo.String.Mixin` and implements the following new ones.
84
+ """
85
+
86
+ def __init__(self, url: 'str | bytes | UrllibResult | None' = None) -> None:
87
+ """::
88
+
89
+ url = Pyjo.URL.new()
90
+ url = Pyjo.URL.new('http://127.0.0.1:3000/foo?f=b&baz=2#foo')
91
+ url = Pyjo.URL.new(urlsplit('http://127.0.0.1:3000/foo?f=b&baz=2#foo'))
92
+
93
+ Construct a new :mod:`Pyjo.URL` object and :meth:`parse` URL if necessary.
94
+ """
95
+ self._base: Pyjo_URL | None = None
96
+ self._path: Pyjo.Path.object | None = None
97
+ self._query: Pyjo.Parameters.object | None = None
98
+
99
+ self.fragment: str | None = None
100
+ """::
101
+
102
+ fragment = url.fragment
103
+ url.fragment = '♥pyjoyment♥'
104
+
105
+ Fragment part of this URL. ::
106
+
107
+ # "yada"
108
+ Pyjo.URL.new('http://example.com/foo?bar=baz#yada').fragment
109
+ """
110
+
111
+ self.host: str | None = None
112
+ """::
113
+
114
+ host = url.host
115
+ url.host = '127.0.0.1'
116
+
117
+ Host part of this URL. ::
118
+
119
+ # "example.com"
120
+ Pyjo.URL.new('http://sri:t3st@example.com:8080/foo').host
121
+ """
122
+
123
+ self.port: int | None = None
124
+ """::
125
+
126
+ port = url.port
127
+ url.port = 8080
128
+
129
+ Port part of this URL. ::
130
+
131
+ # 8080
132
+ Pyjo.URL.new('http://sri:t3st@example.com:8080/foo').port
133
+ """
134
+
135
+ self.scheme: str | None = None
136
+ """::
137
+
138
+ scheme = url.scheme
139
+ url.scheme = 'http'
140
+
141
+ Scheme part of this URL. ::
142
+
143
+ # "http"
144
+ Pyjo.URL.new('http://example.com/foo').scheme
145
+ """
146
+
147
+ self.userinfo: str | None = None
148
+ """::
149
+
150
+ info = url.userinfo
151
+ url.userinfo = 'root:♥'
152
+
153
+ Userinfo part of this URL. ::
154
+
155
+ # "sri:t3st"
156
+ Pyjo.URL.new('https://sri:t3st@example.com/foo').userinfo
157
+ """
158
+
159
+ if url is not None:
160
+ self.parse(url)
161
+
162
+ def __bool__(self) -> bool:
163
+ """::
164
+
165
+ boolean = bool(url)
166
+
167
+ Always true.
168
+ """
169
+ return True
170
+
171
+ def __str__(self) -> str:
172
+ """::
173
+
174
+ string = str(url)
175
+
176
+ Alias for :meth:`to_str`.
177
+ """
178
+ return self.to_str()
179
+
180
+ @property
181
+ def base(self) -> 'Pyjo_URL':
182
+ """::
183
+
184
+ base = url.base
185
+ url.base = Pyjo.URL.new()
186
+
187
+ Base of this URL, defaults to a :mod:`Pyjo.URL` object. ::
188
+
189
+ # "http://example.com/a/b?c"
190
+ Pyjo.URL.new('/a/b?c').set(base=Pyjo.URL.new('http://example.com')).to_abs()
191
+ """
192
+ if self._base is None:
193
+ self._base = Pyjo_URL()
194
+ return self._base
195
+
196
+ @base.setter
197
+ def base(self, value: 'Pyjo_URL') -> None:
198
+ self._base = value
199
+
200
+ def clone(self) -> Self:
201
+ """::
202
+
203
+ url2 = url.clone()
204
+
205
+ Return a new :mod:`Pyjo.URL` object cloned from this URL.
206
+ """
207
+ clone = self.new()
208
+ clone.fragment, clone.host, clone.port = self.fragment, self.host, self.port
209
+ clone.scheme, clone.userinfo = self.scheme, self.userinfo
210
+ clone._base = self._base.clone() if self._base is not None else None
211
+ clone._path = self._path.clone() if self._path is not None else None
212
+ clone._query = self._query.clone() if self._query is not None else None
213
+ return clone
214
+
215
+ @property
216
+ def host_port(self) -> str | None:
217
+ """::
218
+
219
+ host_port = url.host_port
220
+ url.host_port = 'example.com:8080'
221
+
222
+ Normalized version of :attr:`host` and :attr:`port`. ::
223
+
224
+ # "xn--n3h.net:8080"
225
+ Pyjo.URL.new('http://☃.net:8080/test').host_port
226
+
227
+ # "example.com"
228
+ Pyjo.URL.new('http://example.com/test').host_port
229
+ """
230
+ if (host := self.ihost) is None:
231
+ return None
232
+ if (port := self.port) is None:
233
+ return host
234
+ return f'{host}:{port}'
235
+
236
+ @host_port.setter
237
+ def host_port(self, value: str | None) -> None:
238
+ if value is None:
239
+ return
240
+ if m := re.search(r':(\d+)$', value):
241
+ self.port = int(m[1])
242
+ value = value[: m.start()]
243
+ host = url_unescape(value)
244
+ if re.search(r'[^\x00-\x7f]', host):
245
+ self.ihost = host
246
+ else:
247
+ self.host = host
248
+
249
+ @property
250
+ def ihost(self) -> str | None:
251
+ """::
252
+
253
+ ihost = url.ihost
254
+ url.ihost = 'xn--bcher-kva.ch'
255
+
256
+ Host part of this URL in punycode format. ::
257
+
258
+ # "xn--n3h.net"
259
+ Pyjo.URL.new('http://☃.net').ihost
260
+
261
+ # "example.com"
262
+ Pyjo.URL.new('http://example.com').ihost
263
+ """
264
+ # Check if host needs to be encoded
265
+ if (host := self.host) is None:
266
+ return None
267
+ if not re.search(r'[^\x00-\x7f]', host):
268
+ return host
269
+
270
+ # Encode
271
+ return '.'.join(
272
+ f'xn--{punycode_encode(label)}' if re.search(r'[^\x00-\x7f]', label) else label for label in host.split('.')
273
+ )
274
+
275
+ @ihost.setter
276
+ def ihost(self, value: str) -> None:
277
+ # Decode
278
+ self.host = '.'.join(
279
+ punycode_decode(m[1]) if (m := re.match(r'^xn--(.+)$', label)) else label for label in value.split('.')
280
+ )
281
+
282
+ @property
283
+ def is_abs(self) -> bool:
284
+ """::
285
+
286
+ boolean = url.is_abs
287
+
288
+ Check if URL is absolute. ::
289
+
290
+ # True
291
+ Pyjo.URL.new('http://example.com').is_abs
292
+ Pyjo.URL.new('http://example.com/test/index.html').is_abs
293
+
294
+ # False
295
+ Pyjo.URL.new('test/index.html').is_abs
296
+ Pyjo.URL.new('/test/index.html').is_abs
297
+ Pyjo.URL.new('//example.com/test/index.html').is_abs
298
+ """
299
+ return bool(self.scheme)
300
+
301
+ def parse(self, url: 'str | bytes | UrllibResult') -> Self:
302
+ """::
303
+
304
+ url = url.parse('http://127.0.0.1:3000/foo/bar?fo=o&baz=23#foo')
305
+ url = url.parse(urlsplit('http://127.0.0.1:3000/foo/bar?fo=o&baz=23#foo'))
306
+
307
+ Parse relative or absolute URL. Bytes are characters 0-255, as a Perl
308
+ byte string, and a result of :func:`urllib.parse.urlsplit` or
309
+ :func:`urllib.parse.urlparse` is taken as the URL it holds. ::
310
+
311
+ # "/test/123"
312
+ Pyjo.URL.new().parse('/test/123?foo=bar').path
313
+
314
+ # "example.com"
315
+ Pyjo.URL.new().parse('http://example.com/test/123?foo=bar').host
316
+
317
+ # "sri@example.com"
318
+ Pyjo.URL.new().parse('mailto:sri@example.com').path
319
+ """
320
+ # Not in Mojolicious: urllib of the standard library
321
+ if isinstance(url, (SplitResult, ParseResult, SplitResultBytes, ParseResultBytes)):
322
+ url = url.geturl()
323
+
324
+ # Not in Mojolicious: bytes, which are characters 0-255, as a Perl byte string
325
+ if isinstance(url, bytes):
326
+ url = url.decode('latin-1')
327
+
328
+ # Official regex from RFC 3986
329
+ m = re.match(r'^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?', url)
330
+ assert m is not None
331
+ if m[2] is not None:
332
+ self.scheme = m[2]
333
+ if m[5] is not None:
334
+ self.path = m[5]
335
+ if m[7] is not None:
336
+ self.query = m[7]
337
+ if m[9] is not None:
338
+ self.fragment = _decode(url_unescape(m[9]))
339
+ if (auth := m[4]) is not None:
340
+ if info := re.match(r'^([^@]+)@', auth):
341
+ self.userinfo = _decode(url_unescape(info[1]))
342
+ auth = auth[info.end() :]
343
+ self.host_port = auth
344
+
345
+ return self
346
+
347
+ @property
348
+ def password(self) -> str | None:
349
+ """::
350
+
351
+ password = url.password
352
+
353
+ Password part of :attr:`userinfo`. ::
354
+
355
+ # "s3cret"
356
+ Pyjo.URL.new('http://isabel:s3cret@pyjoyment.org').password
357
+
358
+ # "s:3:c:r:e:t"
359
+ Pyjo.URL.new('http://isabel:s:3:c:r:e:t@pyjoyment.org').password
360
+ """
361
+ m = re.search(r':(.*)$', self.userinfo or '')
362
+ return m[1] if m else None
363
+
364
+ @property
365
+ def path(self) -> Pyjo.Path.object:
366
+ """::
367
+
368
+ path = url.path
369
+ url.path = 'foo/bar'
370
+ url.path = '/foo/bar'
371
+ url.path = Pyjo.Path.new()
372
+
373
+ Path part of this URL, relative paths will be merged with
374
+ :meth:`Pyjo.Path.Pyjo_Path.merge`, defaults to a :mod:`Pyjo.Path` object. ::
375
+
376
+ # "test"
377
+ Pyjo.URL.new('http://example.com/test/Pyjo').path.parts[0]
378
+
379
+ # "/test/DOM/HTML"
380
+ Pyjo.URL.new('http://example.com/test/Pyjo').path.merge('DOM/HTML')
381
+
382
+ # "http://example.com/DOM/HTML"
383
+ Pyjo.URL.new('http://example.com/test/Pyjo').set(path='/DOM/HTML')
384
+
385
+ # "http://example.com/test/DOM/HTML"
386
+ Pyjo.URL.new('http://example.com/test/Pyjo').set(path='DOM/HTML')
387
+
388
+ # "http://example.com/test/Pyjo/DOM/HTML"
389
+ Pyjo.URL.new('http://example.com/test/Pyjo/').set(path='DOM/HTML')
390
+ """
391
+ # Old path
392
+ if self._path is None:
393
+ self._path = Pyjo.Path.new()
394
+ return self._path
395
+
396
+ @path.setter
397
+ def path(self, value: str | bytes | Pyjo.Path.object) -> None:
398
+ # New path
399
+ self._path = value if isinstance(value, Pyjo.Path.object) else self.path.merge(value)
400
+
401
+ @property
402
+ def path_query(self) -> str:
403
+ """::
404
+
405
+ path_query = url.path_query
406
+ url.path_query = '/foo/bar?a=1&b=2'
407
+
408
+ Normalized version of :attr:`path` and :attr:`query`. ::
409
+
410
+ # "/test?a=1&b=2"
411
+ Pyjo.URL.new('http://example.com/test?a=1&b=2').path_query
412
+
413
+ # "/"
414
+ Pyjo.URL.new('http://example.com/').path_query
415
+ """
416
+ query = self.query.to_str()
417
+ return self.path.to_str() + (f'?{query}' if query else '')
418
+
419
+ @path_query.setter
420
+ def path_query(self, value: str | None) -> None:
421
+ if value is None:
422
+ return
423
+ m = re.match(r'^([^?#]*)(?:\?([^#]*))?', value)
424
+ assert m is not None
425
+ self.path = m[1]
426
+ if m[2] is not None:
427
+ self.query = m[2]
428
+
429
+ @property
430
+ def protocol(self) -> str:
431
+ """::
432
+
433
+ proto = url.protocol
434
+
435
+ Normalized version of :attr:`scheme`. ::
436
+
437
+ # "http"
438
+ Pyjo.URL.new('HtTp://example.com').protocol
439
+ """
440
+ return (self.scheme or '').lower()
441
+
442
+ @property
443
+ def query(self) -> Pyjo.Parameters.object:
444
+ """::
445
+
446
+ query = url.query
447
+ url.query = {'merge': 'to'}
448
+ url.query = ['append', 'with']
449
+ url.query = ('replace', 'with')
450
+ url.query = 'a=1&b=2'
451
+ url.query = Pyjo.Parameters.new()
452
+
453
+ Query part of this URL, key/value pairs in a ``list`` will be appended with
454
+ :meth:`Pyjo.Parameters.Pyjo_Parameters.append`, key/value pairs in a
455
+ ``dict`` merged with :meth:`Pyjo.Parameters.Pyjo_Parameters.merge`, and
456
+ key/value pairs in a ``tuple`` replace the parameters, defaults to a
457
+ :mod:`Pyjo.Parameters` object. ::
458
+
459
+ # "2"
460
+ Pyjo.URL.new('http://example.com?a=1&b=2').query.param('b')
461
+
462
+ # "b=2&a=2&c=3"
463
+ Pyjo.URL.new('http://example.com?a=1&b=2').query.merge('a', 2, 'c', 3)
464
+
465
+ # "http://example.com?a=2&c=3"
466
+ Pyjo.URL.new('http://example.com?a=1&b=2').set(query=('a', 2, 'c', 3))
467
+
468
+ # "http://example.com?a=2&a=3"
469
+ Pyjo.URL.new('http://example.com?a=1&b=2').set(query=('a', [2, 3]))
470
+
471
+ # "http://example.com?b=2&a=2&c=3"
472
+ Pyjo.URL.new('http://example.com?a=1&b=2').set(query={'a': 2, 'c': 3})
473
+
474
+ # "http://example.com?b=2"
475
+ Pyjo.URL.new('http://example.com?a=1&b=2').set(query={'a': None})
476
+
477
+ # "http://example.com?a=1&b=2&a=2&c=3"
478
+ Pyjo.URL.new('http://example.com?a=1&b=2').set(query=['a', 2, 'c', 3])
479
+ """
480
+ # Old parameters
481
+ if self._query is None:
482
+ self._query = Pyjo.Parameters.new()
483
+ return self._query
484
+
485
+ @query.setter
486
+ def query(self, value: Anything) -> None:
487
+ q = self.query
488
+
489
+ # Replace with tuple
490
+ if isinstance(value, tuple):
491
+ q.pairs = []
492
+ q.append(*cast(tuple[Anything, ...], value))
493
+
494
+ # Merge with dict
495
+ elif isinstance(value, dict):
496
+ q.merge(*(x for pair in cast(dict[Anything, Anything], value).items() for x in pair))
497
+
498
+ # Append list
499
+ elif isinstance(value, list):
500
+ q.append(*cast(list[Anything], value))
501
+
502
+ # New parameters
503
+ elif isinstance(value, Pyjo.Parameters.object):
504
+ self._query = value
505
+ else:
506
+ q.parse(value)
507
+
508
+ def to_abs(self, base: 'Pyjo_URL | None' = None) -> Self:
509
+ """::
510
+
511
+ abs = url.to_abs()
512
+ abs = url.to_abs(Pyjo.URL.new('http://example.com/foo'))
513
+
514
+ Return a new :mod:`Pyjo.URL` object cloned from this relative URL and turn it
515
+ into an absolute one using :attr:`base` or provided base URL. ::
516
+
517
+ # "http://example.com/foo/baz.xml?test=123"
518
+ Pyjo.URL.new('baz.xml?test=123').to_abs(Pyjo.URL.new('http://example.com/foo/bar.html'))
519
+
520
+ # "http://example.com/baz.xml?test=123"
521
+ Pyjo.URL.new('/baz.xml?test=123').to_abs(Pyjo.URL.new('http://example.com/foo/bar.html'))
522
+
523
+ # "http://example.com/foo/baz.xml?test=123"
524
+ Pyjo.URL.new('//example.com/foo/baz.xml?test=123').to_abs(Pyjo.URL.new('http://example.com/foo/bar.html'))
525
+ """
526
+ abs = self.clone()
527
+ if abs.is_abs:
528
+ return abs
529
+
530
+ # Scheme
531
+ base = base if base is not None else abs.base
532
+ abs.base = base
533
+ abs.scheme = base.scheme
534
+
535
+ # Authority
536
+ if abs.host:
537
+ return abs
538
+ abs.userinfo, abs.host, abs.port = base.userinfo, base.host, base.port
539
+
540
+ # Absolute path
541
+ path = abs.path
542
+ if path.leading_slash:
543
+ return abs
544
+
545
+ # Inherit path
546
+ if not path.parts:
547
+ abs.path = base.path.clone().canonicalize()
548
+
549
+ # Query
550
+ if not abs.query.to_str():
551
+ abs.query = base.query.clone()
552
+
553
+ # Merge paths
554
+ else:
555
+ abs.path = base.path.clone().merge(path).canonicalize()
556
+
557
+ return abs
558
+
559
+ def to_str(self) -> str:
560
+ """::
561
+
562
+ string = url.to_str()
563
+
564
+ Turn URL into a string. Note that :attr:`userinfo` will not be included for
565
+ security reasons. ::
566
+
567
+ # "http://pyjoyment.org"
568
+ Pyjo.URL.new().set(scheme='http', host='pyjoyment.org').to_str()
569
+
570
+ # "http://pyjoyment.org"
571
+ Pyjo.URL.new('http://daniel:s3cret@pyjoyment.org').to_str()
572
+ """
573
+ return self._string(False)
574
+
575
+ def to_unsafe_str(self) -> str:
576
+ """::
577
+
578
+ string = url.to_unsafe_str()
579
+
580
+ Same as :meth:`to_str`, but includes :attr:`userinfo`. ::
581
+
582
+ # "http://daniel:s3cret@pyjoyment.org"
583
+ Pyjo.URL.new('http://daniel:s3cret@pyjoyment.org').to_unsafe_str()
584
+ """
585
+ return self._string(True)
586
+
587
+ # Not in Mojolicious: urllib of the standard library
588
+ def to_urllib(self) -> SplitResult:
589
+ """::
590
+
591
+ split = url.to_urllib()
592
+
593
+ Turn URL into a :class:`urllib.parse.SplitResult`, for the standard library.
594
+ Unlike :meth:`to_str`, it includes :attr:`userinfo`, as the result holds all
595
+ parts of the URL. ::
596
+
597
+ # "isabel"
598
+ Pyjo.URL.new('http://isabel:s3cret@pyjoyment.org/foo').to_urllib().username
599
+ """
600
+ return urlsplit(self.to_unsafe_str())
601
+
602
+ @property
603
+ def username(self) -> str | None:
604
+ """::
605
+
606
+ username = url.username
607
+
608
+ Username part of :attr:`userinfo`. ::
609
+
610
+ # "isabel"
611
+ Pyjo.URL.new('http://isabel:s3cret@pyjoyment.org').username
612
+ """
613
+ m = re.match(r'^([^:]+)', self.userinfo or '')
614
+ return m[1] if m else None
615
+
616
+ def _string(self, unsafe: bool) -> str:
617
+ # Scheme
618
+ url = ''
619
+ if proto := self.protocol:
620
+ url += f'{proto}:'
621
+
622
+ # Authority
623
+ auth = self.host_port
624
+ if auth is not None:
625
+ auth = _encode(auth, rb"^A-Za-z0-9\-._~!$&'()*+,;=:\[\]")
626
+ if unsafe and (info := self.userinfo) is not None:
627
+ auth = _encode(info, rb"^A-Za-z0-9\-._~!$&'()*+,;=:") + '@' + (auth or '')
628
+ if auth is not None:
629
+ url += f'//{auth}'
630
+
631
+ # Path and query
632
+ path = self.path_query
633
+ url += path if not auth or not path or re.match(r'[/?]', path) else f'/{path}'
634
+
635
+ # Fragment
636
+ if (fragment := self.fragment) is None:
637
+ return url
638
+ return url + '#' + _encode(fragment, rb"^A-Za-z0-9\-._~!$&'()*+,;=:@/?")
639
+
640
+
641
+ def _decode(string: str) -> str:
642
+ return decoded if (decoded := decode('UTF-8', string)) is not None else string
643
+
644
+
645
+ def _encode(string: str, pattern: bytes) -> str:
646
+ return url_escape(string.encode('UTF-8'), pattern).decode('ascii')
647
+
648
+
649
+ new = Pyjo_URL.new
650
+ object = Pyjo_URL
651
+ URL = Pyjo_URL