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/Upload.py ADDED
@@ -0,0 +1,139 @@
1
+ """
2
+ Pyjo.Upload - Upload
3
+ ====================
4
+ ::
5
+
6
+ from Pyjo.Upload import Upload
7
+
8
+ upload = Upload()
9
+ print(upload.filename)
10
+ upload.move_to('/home/sri/foo.txt')
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjo.Upload
15
+
16
+ upload = Pyjo.Upload.new()
17
+ print(upload.filename)
18
+ upload.move_to('/home/sri/foo.txt')
19
+
20
+ :mod:`Pyjo.Upload` is a container for uploaded files.
21
+
22
+ See also
23
+ --------
24
+
25
+ :mod:`Pyjoyment`, https://mojolicious.org.
26
+
27
+ Ported from `Mojo::Upload <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/Upload.pm>`_
28
+ of Mojolicious 9.49.
29
+ """
30
+
31
+ import Pyjo.Asset
32
+ import Pyjo.Base
33
+ import Pyjo.Headers
34
+
35
+ import builtins
36
+
37
+ from typing import Self
38
+
39
+
40
+ type Anything = builtins.object
41
+
42
+
43
+ class Pyjo_Upload(Pyjo.Base.object):
44
+ """
45
+ :mod:`Pyjo.Upload` inherits all methods from :mod:`Pyjo.Base` and
46
+ implements the following new ones.
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ *,
52
+ asset: Pyjo.Asset.object | None = None,
53
+ filename: str | None = None,
54
+ headers: Pyjo.Headers.object | None = None,
55
+ name: str | None = None,
56
+ ) -> None:
57
+ """::
58
+
59
+ upload = Pyjo.Upload.new()
60
+ upload = Pyjo.Upload.new(name='foo', filename='foo.txt', asset=Pyjo.Asset.Memory.new())
61
+
62
+ Construct a new :mod:`Pyjo.Upload` object.
63
+ """
64
+ self.asset = asset
65
+ """::
66
+
67
+ asset = upload.asset
68
+ upload.asset = Pyjo.Asset.File.new()
69
+
70
+ Asset containing the uploaded data, usually a :mod:`Pyjo.Asset.File`
71
+ or :mod:`Pyjo.Asset.Memory` object.
72
+ """
73
+
74
+ self.filename = filename
75
+ """::
76
+
77
+ filename = upload.filename
78
+ upload.filename = 'foo.txt'
79
+
80
+ Name of the uploaded file.
81
+ """
82
+
83
+ self.headers = headers
84
+ """::
85
+
86
+ headers = upload.headers
87
+ upload.headers = Pyjo.Headers.new()
88
+
89
+ Headers for upload, usually a :mod:`Pyjo.Headers` object.
90
+ """
91
+
92
+ self.name = name
93
+ """::
94
+
95
+ name = upload.name
96
+ upload.name = 'foo'
97
+
98
+ Name of the upload.
99
+ """
100
+
101
+ def move_to(self, to: Anything) -> Self:
102
+ """::
103
+
104
+ upload = upload.move_to('/home/sri/foo.txt')
105
+
106
+ Move uploaded data into a specific file.
107
+ """
108
+ self._asset().move_to(to)
109
+ return self
110
+
111
+ @property
112
+ def size(self) -> int:
113
+ """::
114
+
115
+ size = upload.size
116
+
117
+ Size of uploaded data in bytes.
118
+ """
119
+ return self._asset().size
120
+
121
+ def slurp(self) -> bytes:
122
+ """::
123
+
124
+ content = upload.slurp()
125
+
126
+ Read all uploaded data at once.
127
+ """
128
+ return self._asset().slurp()
129
+
130
+ # The asset, which an upload without one does not have, as in Mojo
131
+ def _asset(self) -> Pyjo.Asset.object:
132
+ if (asset := self.asset) is None:
133
+ raise AttributeError("'NoneType' object has no attribute 'slurp'")
134
+ return asset
135
+
136
+
137
+ new = Pyjo_Upload.new
138
+ object = Pyjo_Upload
139
+ Upload = Pyjo_Upload
@@ -0,0 +1,391 @@
1
+ """
2
+ Pyjo.UserAgent.CookieJar - Cookie jar for HTTP user agents
3
+ ==========================================================
4
+ ::
5
+
6
+ import Pyjo.Cookie.Response
7
+
8
+ from Pyjo.UserAgent.CookieJar import CookieJar
9
+
10
+ # Add response cookies
11
+ jar = CookieJar()
12
+ jar.add(
13
+ Pyjo.Cookie.Response.new(
14
+ name='foo',
15
+ value='bar',
16
+ domain='localhost',
17
+ path='/test',
18
+ )
19
+ )
20
+
21
+ # Or import the module, as in Google Python Style Guide
22
+
23
+ import Pyjo.Cookie.Response
24
+ import Pyjo.URL
25
+ import Pyjo.UserAgent.CookieJar
26
+
27
+ # Add response cookies
28
+ jar = Pyjo.UserAgent.CookieJar.new()
29
+ jar.add(
30
+ Pyjo.Cookie.Response.new(
31
+ name='foo',
32
+ value='bar',
33
+ domain='localhost',
34
+ path='/test',
35
+ )
36
+ )
37
+
38
+ # Find request cookies
39
+ for cookie in jar.find(Pyjo.URL.new('http://localhost/test')):
40
+ print(cookie.name)
41
+ print(cookie.value)
42
+
43
+ :mod:`Pyjo.UserAgent.CookieJar` is a minimalistic and relaxed cookie jar used
44
+ by :mod:`Pyjo.UserAgent`, based on :rfc:`6265`.
45
+
46
+ See also
47
+ --------
48
+
49
+ :mod:`Pyjoyment`, https://mojolicious.org.
50
+
51
+ Ported from `Mojo::UserAgent::CookieJar <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/UserAgent/CookieJar.pm>`_
52
+ of Mojolicious 9.49.
53
+
54
+ Classes
55
+ -------
56
+ """
57
+
58
+ import Pyjo.Base
59
+ import Pyjo.Cookie.Request
60
+ import Pyjo.Cookie.Response
61
+ import Pyjo.File
62
+ import Pyjo.Path
63
+ import Pyjo.Transaction
64
+ import Pyjo.URL
65
+
66
+ from Pyjo.String.Unicode import split
67
+
68
+ import builtins
69
+ import os
70
+ import re
71
+ import time
72
+
73
+ from collections.abc import Callable
74
+ from typing import Any, Self
75
+
76
+
77
+ type Anything = builtins.object
78
+
79
+
80
+ # Unlike Mojolicious: the file says that Pyjoyment generated it
81
+ COMMENT = '# Netscape HTTP Cookie File\n# This file was generated by Pyjoyment! Edit at your own risk.\n\n'
82
+
83
+
84
+ class Pyjo_UserAgent_CookieJar(Pyjo.Base.object):
85
+ """
86
+ :mod:`Pyjo.UserAgent.CookieJar` inherits all methods from :mod:`Pyjo.Base`
87
+ and implements the following new ones.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ *,
93
+ file: str | os.PathLike[str] | None = None,
94
+ ignore: Callable[[Pyjo.Cookie.Response.object], Any] | None = None,
95
+ max_cookie_size: int = 4096,
96
+ ) -> None:
97
+ """::
98
+
99
+ jar = Pyjo.UserAgent.CookieJar.new()
100
+ jar = Pyjo.UserAgent.CookieJar.new(file='cookies.txt')
101
+
102
+ Construct a new :mod:`Pyjo.UserAgent.CookieJar` object.
103
+ """
104
+ self.file = file
105
+ """::
106
+
107
+ file = jar.file
108
+ jar.file = '/home/sri/cookies.txt'
109
+
110
+ File to :meth:`load` cookies from and :meth:`save` cookies to in
111
+ Netscape format. ::
112
+
113
+ # Save cookies to file
114
+ jar.file = 'cookies.txt'
115
+ jar.save()
116
+
117
+ # Empty cookie jar and load cookies from file
118
+ jar.file = 'cookies.txt'
119
+ jar.empty().load()
120
+ """
121
+
122
+ self.ignore = ignore
123
+ """::
124
+
125
+ ignore = jar.ignore
126
+ jar.ignore = lambda cookie: ...
127
+
128
+ A callback used to decide if a cookie should be ignored by
129
+ :meth:`collect`. ::
130
+
131
+ # Ignore all cookies
132
+ jar.ignore = lambda cookie: True
133
+
134
+ # Ignore cookies for domains "com", "net" and "org"
135
+ jar.ignore = lambda cookie: cookie.domain in ('com', 'net', 'org')
136
+ """
137
+
138
+ self.max_cookie_size = max_cookie_size
139
+ """::
140
+
141
+ size = jar.max_cookie_size
142
+ jar.max_cookie_size = 4096
143
+
144
+ Maximum cookie size in bytes, defaults to ``4096`` (4KiB).
145
+ """
146
+
147
+ self._jar: dict[str, list[Pyjo.Cookie.Response.object]] = {}
148
+
149
+ def add(self, *cookies: Pyjo.Cookie.Response.object) -> Self:
150
+ """::
151
+
152
+ jar = jar.add(*cookies)
153
+
154
+ Add multiple :mod:`Pyjo.Cookie.Response` objects to the jar.
155
+ """
156
+ size = self.max_cookie_size
157
+ for cookie in cookies:
158
+ # Convert max age to expires
159
+ if (age := _number(cookie.max_age)) is not None:
160
+ cookie.expires = 0 if age <= 0 else age + int(time.time())
161
+
162
+ # Check cookie size
163
+ if len(cookie.value or '') > size:
164
+ continue
165
+
166
+ # Replace cookie
167
+ if not (domain := (cookie.domain or '').lower()):
168
+ continue
169
+ if not (path := cookie.path):
170
+ continue
171
+ if not len(name := cookie.name or ''):
172
+ continue
173
+ jar = self._jar.setdefault(domain, [])
174
+ jar[:] = [*(c for c in jar if _compare(c, path, name, domain)), cookie]
175
+
176
+ return self
177
+
178
+ def all(self) -> list[Pyjo.Cookie.Response.object]:
179
+ """::
180
+
181
+ cookies = jar.all()
182
+
183
+ Return all :mod:`Pyjo.Cookie.Response` objects that are currently stored
184
+ in the jar. ::
185
+
186
+ # Names of all cookies
187
+ for cookie in jar.all():
188
+ print(cookie.name)
189
+ """
190
+ jar = self._jar
191
+ return [cookie for domain in sorted(jar) for cookie in jar[domain]]
192
+
193
+ def collect(self, tx: Pyjo.Transaction.object) -> None:
194
+ """::
195
+
196
+ jar.collect(Pyjo.Transaction.HTTP.new())
197
+
198
+ Collect response cookies from transaction.
199
+ """
200
+ url = tx.req.url
201
+ for cookie in tx.res.cookies():
202
+ # Validate domain
203
+ host = (url.ihost or '').lower()
204
+ if not cookie.domain:
205
+ cookie.domain, cookie.host_only = host, True
206
+ domain = cookie.domain.lower()
207
+ if (ignore := self.ignore) and ignore(cookie):
208
+ continue
209
+ if host != domain and (not host.endswith(f'.{domain}') or re.search(r'\.\d+$', host)):
210
+ continue
211
+
212
+ # Validate path
213
+ path = Pyjo.Path.new(cookie.path if cookie.path is not None else url.path.to_dir().to_abs_str())
214
+ path.trailing_slash = False
215
+ if not _path(path := path.to_abs_str(), url.path.to_abs_str()):
216
+ continue
217
+ cookie.path = path
218
+ self.add(cookie)
219
+
220
+ def empty(self) -> Self:
221
+ """::
222
+
223
+ jar = jar.empty()
224
+
225
+ Empty the jar.
226
+ """
227
+ self._jar = {}
228
+ return self
229
+
230
+ def find(self, url: Pyjo.URL.object) -> list[Pyjo.Cookie.Request.object]:
231
+ """::
232
+
233
+ cookies = jar.find(Pyjo.URL.new())
234
+
235
+ Find :mod:`Pyjo.Cookie.Request` objects in the jar for
236
+ :mod:`Pyjo.URL` object. ::
237
+
238
+ # Names of all cookies found
239
+ for cookie in jar.find(Pyjo.URL.new('http://example.com/foo')):
240
+ print(cookie.name)
241
+ """
242
+ found: list[Pyjo.Cookie.Request.object] = []
243
+ domain = host = (url.ihost or '').lower()
244
+ path = url.path.to_abs_str()
245
+ while domain:
246
+ if (old := self._jar.get(domain)) is not None:
247
+ # Grab cookies
248
+ new = self._jar[domain] = []
249
+ for cookie in old:
250
+ if cookie.host_only and host != cookie.domain:
251
+ continue
252
+
253
+ # Check if cookie has expired
254
+ if (expires := cookie.expires) is not None and int(time.time()) > expires:
255
+ continue
256
+ new.append(cookie)
257
+
258
+ # Taste cookie
259
+ if cookie.secure and url.protocol != 'https':
260
+ continue
261
+ if not _path(cookie.path or '', path):
262
+ continue
263
+ found.append(Pyjo.Cookie.Request.new(name=cookie.name, value=cookie.value))
264
+
265
+ # Remove another part
266
+ domain = re.sub(r'^[^.]*\.*', '', domain, count=1)
267
+
268
+ return found
269
+
270
+ def load(self) -> Self:
271
+ """::
272
+
273
+ jar = jar.load()
274
+
275
+ Load cookies from :attr:`file`.
276
+ """
277
+ if not (file := self.file) or not os.access(file, os.R_OK):
278
+ return self
279
+
280
+ # Unlike Mojolicious: the file is UTF-8, where Mojo reads and writes characters as they are, and warns about
281
+ # wide characters
282
+ content = str(Pyjo.File.path(file).slurp('UTF-8'))
283
+
284
+ # Unlike Mojolicious: a line may end with CRLF, as curl and http.cookiejar write the file on Windows, where Mojo
285
+ # keeps the carriage return in the value of the cookie
286
+ for line in split(r'\r?\n', content):
287
+ # Prefix used by curl for HttpOnly cookies
288
+ line = line or ''
289
+ httponly = line.startswith('#HttpOnly_')
290
+ line = line.removeprefix('#HttpOnly_')
291
+ if line.startswith('#'):
292
+ continue
293
+
294
+ # Unlike Mojolicious: the empty value of a cookie at the end of the line is a field, where Mojo's split
295
+ # leaves it out and the cookie with it
296
+ values = [value or '' for value in split('\t', line, -1)]
297
+ if len(values) != 7:
298
+ continue
299
+
300
+ self.add(
301
+ Pyjo.Cookie.Response.new(
302
+ domain=values[0].removeprefix('.'),
303
+ host_only=values[1] == 'FALSE',
304
+ path=values[2],
305
+ secure=values[3] != 'FALSE',
306
+ expires=None if values[4] == '0' else _number(values[4]),
307
+ name=values[5],
308
+ value=values[6],
309
+ httponly=httponly,
310
+ )
311
+ )
312
+
313
+ return self
314
+
315
+ def prepare(self, tx: Pyjo.Transaction.object) -> None:
316
+ """::
317
+
318
+ jar.prepare(Pyjo.Transaction.HTTP.new())
319
+
320
+ Prepare request cookies for transaction.
321
+ """
322
+ if not self._jar:
323
+ return
324
+ req = tx.req
325
+ req.cookies(*self.find(req.url))
326
+
327
+ def save(self) -> Self:
328
+ """::
329
+
330
+ jar = jar.save()
331
+
332
+ Save cookies to :attr:`file`.
333
+ """
334
+ if not (file := self.file):
335
+ return self
336
+
337
+ final = Pyjo.File.path(file)
338
+ tmp = Pyjo.File.path(f'{os.fspath(file)}.{os.getpid()}')
339
+ tmp.spew(COMMENT + self.to_str(), 'UTF-8').move_to(final)
340
+
341
+ return self
342
+
343
+ def to_str(self) -> str:
344
+ """::
345
+
346
+ string = jar.to_str()
347
+
348
+ Stringify cookies in Netscape format.
349
+ """
350
+ lines: list[str] = []
351
+ for cookie in self.all():
352
+ expires = cookie.expires if cookie.expires is not None else 0
353
+ # Unlike Mojolicious: the domain of a cookie for subdomains starts with a dot, as curl and the
354
+ # MozillaCookieJar of http.cookiejar write it and the latter needs it, where Mojo leaves the dot out
355
+ domain = cookie.domain or ''
356
+ line = [
357
+ domain if cookie.host_only else f'.{domain}',
358
+ 'FALSE' if cookie.host_only else 'TRUE',
359
+ cookie.path or '',
360
+ 'TRUE' if cookie.secure else 'FALSE',
361
+ str(int(expires) if float(expires).is_integer() else expires),
362
+ cookie.name or '',
363
+ cookie.value or '',
364
+ ]
365
+ lines.append('\t'.join(line))
366
+
367
+ return '\n'.join([*lines, ''])
368
+
369
+
370
+ def _compare(cookie: Pyjo.Cookie.Response.object, path: str, name: str, domain: str) -> bool:
371
+ return cookie.path != path or cookie.name != name or cookie.domain != domain
372
+
373
+
374
+ # Not in Mojolicious: a number as Perl's looks_like_number takes it, as the value of an integer or a float
375
+ def _number(value: Anything) -> int | float | None:
376
+ if value is None or isinstance(value, bool):
377
+ return None
378
+ try:
379
+ number = float(str(value))
380
+ except ValueError:
381
+ return None
382
+ return int(number) if number.is_integer() else number
383
+
384
+
385
+ def _path(path1: str, path2: str) -> bool:
386
+ return path1 == '/' or path1 == path2 or path2.startswith(f'{path1}/')
387
+
388
+
389
+ new = Pyjo_UserAgent_CookieJar.new
390
+ object = Pyjo_UserAgent_CookieJar
391
+ CookieJar = Pyjo_UserAgent_CookieJar
@@ -0,0 +1,138 @@
1
+ """
2
+ Pyjo.UserAgent.Proxy - User agent proxy manager
3
+ ===============================================
4
+ ::
5
+
6
+ from Pyjo.UserAgent.Proxy import Proxy
7
+
8
+ proxy = Proxy()
9
+ proxy.detect()
10
+ print(proxy.http)
11
+
12
+ # Or import the module, as in Google Python Style Guide
13
+
14
+ import Pyjo.UserAgent.Proxy
15
+
16
+ proxy = Pyjo.UserAgent.Proxy.new()
17
+ proxy.detect()
18
+ print(proxy.http)
19
+
20
+ :mod:`Pyjo.UserAgent.Proxy` manages proxy servers for :mod:`Pyjo.UserAgent`.
21
+
22
+ See also
23
+ --------
24
+
25
+ :mod:`Pyjoyment`, https://mojolicious.org.
26
+
27
+ Ported from `Mojo::UserAgent::Proxy <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojo/UserAgent/Proxy.pm>`_
28
+ of Mojolicious 9.49.
29
+
30
+ Classes
31
+ -------
32
+ """
33
+
34
+ import Pyjo.Base
35
+ import Pyjo.Transaction
36
+ import Pyjo.URL
37
+
38
+ from Pyjo.String.Unicode import split
39
+
40
+ import os
41
+
42
+ from typing import Self
43
+
44
+
45
+ class Pyjo_UserAgent_Proxy(Pyjo.Base.object):
46
+ """
47
+ :mod:`Pyjo.UserAgent.Proxy` inherits all methods from :mod:`Pyjo.Base` and
48
+ implements the following new ones.
49
+ """
50
+
51
+ def __init__(self, *, http: str | None = None, https: str | None = None, no: list[str] | None = None) -> None:
52
+ """::
53
+
54
+ proxy = Pyjo.UserAgent.Proxy.new()
55
+ proxy = Pyjo.UserAgent.Proxy.new(http='http://127.0.0.1:8080')
56
+
57
+ Construct a new :mod:`Pyjo.UserAgent.Proxy` object.
58
+ """
59
+ self.http = http
60
+ """::
61
+
62
+ http = proxy.http
63
+ proxy.http = 'socks://sri:secret@127.0.0.1:8080'
64
+
65
+ Proxy server to use for HTTP and WebSocket requests.
66
+ """
67
+
68
+ self.https = https
69
+ """::
70
+
71
+ https = proxy.https
72
+ proxy.https = 'http://sri:secret@127.0.0.1:8080'
73
+
74
+ Proxy server to use for HTTPS and WebSocket requests.
75
+ """
76
+
77
+ # Renamed from Mojolicious: not, a keyword of Python, as the environment variable NO_PROXY names it
78
+ self.no = no
79
+ """::
80
+
81
+ no = proxy.no
82
+ proxy.no = ['localhost', 'intranet.mojolicious.org']
83
+
84
+ Domains that don't require a proxy server to be used.
85
+ """
86
+
87
+ def detect(self) -> Self:
88
+ """::
89
+
90
+ proxy = proxy.detect()
91
+
92
+ Check environment variables ``HTTP_PROXY``, ``http_proxy``,
93
+ ``HTTPS_PROXY``, ``https_proxy``, ``NO_PROXY`` and ``no_proxy`` for proxy
94
+ information. Automatic proxy detection can be enabled with the
95
+ ``PYJO_PROXY`` environment variable.
96
+ """
97
+ env = os.environ
98
+ self.http = env.get('HTTP_PROXY') or env.get('http_proxy')
99
+ self.https = env.get('HTTPS_PROXY') or env.get('https_proxy')
100
+ self.no = [domain or '' for domain in split(',', env.get('NO_PROXY') or env.get('no_proxy') or '')]
101
+ return self
102
+
103
+ def is_needed(self, domain: str) -> bool:
104
+ """::
105
+
106
+ boolean = proxy.is_needed('intranet.example.com')
107
+
108
+ Check if request for domain would use a proxy server.
109
+ """
110
+ return not any(domain.endswith(no) for no in self.no or [])
111
+
112
+ def prepare(self, tx: Pyjo.Transaction.object) -> None:
113
+ """::
114
+
115
+ proxy.prepare(Pyjo.Transaction.HTTP.new())
116
+
117
+ Prepare proxy server information for transaction.
118
+ """
119
+ if os.environ.get('PYJO_PROXY') not in (None, '', '0'):
120
+ self.detect()
121
+ req = tx.req
122
+ url = req.url
123
+ if not self.is_needed(url.host or ''):
124
+ return
125
+
126
+ # HTTP proxy
127
+ proto = url.protocol
128
+ if (http := self.http) and proto == 'http':
129
+ req.proxy = Pyjo.URL.new(http)
130
+
131
+ # HTTPS proxy
132
+ if (https := self.https) and proto == 'https':
133
+ req.proxy = Pyjo.URL.new(https)
134
+
135
+
136
+ new = Pyjo_UserAgent_Proxy.new
137
+ object = Pyjo_UserAgent_Proxy
138
+ Proxy = Pyjo_UserAgent_Proxy