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,395 @@
1
+ """
2
+ Pyjoyment.Validator.Validation - Perform validations
3
+ ====================================================
4
+ ::
5
+
6
+ import Pyjoyment.Validator
7
+
8
+ from Pyjoyment.Validator.Validation import Validation
9
+
10
+ validator = Pyjoyment.Validator.new()
11
+ v = Validation(validator=validator)
12
+ v.input = {'foo': 'bar'}
13
+ v.required('foo').one_of('bar', 'baz')
14
+ print(v.param('foo'))
15
+
16
+ # Or import the module, as in Google Python Style Guide
17
+
18
+ import Pyjoyment.Validator
19
+ import Pyjoyment.Validator.Validation
20
+
21
+ validator = Pyjoyment.Validator.new()
22
+ v = Pyjoyment.Validator.Validation.new(validator=validator)
23
+ v.input = {'foo': 'bar'}
24
+ v.required('foo').one_of('bar', 'baz')
25
+ print(v.param('foo'))
26
+
27
+ :mod:`Pyjoyment.Validator.Validation` performs :mod:`Pyjoyment.Validator`
28
+ validation checks.
29
+
30
+ Checks
31
+ ------
32
+
33
+ In addition to the attributes and methods below, you can also call validation
34
+ checks provided by :attr:`~Pyjoyment_Validator_Validation.validator` on
35
+ :mod:`Pyjoyment.Validator.Validation` objects, similar to
36
+ :meth:`~Pyjoyment_Validator_Validation.check`. ::
37
+
38
+ # Call validation checks
39
+ v.required('foo').size(2, 5).like(re.compile(r'^[A-Z]'))
40
+ v.optional('bar').equal_to('foo')
41
+ v.optional('baz').one_of('test', '123')
42
+
43
+ # Longer version
44
+ v.required('foo').check('size', 2, 5).check('like', re.compile(r'^[A-Z]'))
45
+
46
+ A check with the name of a keyword of Python, as ``in``, is also a method
47
+ with the name followed by an underscore, as ``in_``.
48
+
49
+ See also
50
+ --------
51
+
52
+ :mod:`Pyjoyment`, https://mojolicious.org.
53
+
54
+ Ported from `Mojolicious::Validator::Validation
55
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Validator/Validation.pm>`_ of Mojolicious 9.49.
56
+
57
+ Classes
58
+ -------
59
+ """
60
+
61
+ import Pyjo.Base
62
+
63
+ from Pyjo.Util import secure_compare
64
+
65
+ import builtins
66
+ import functools
67
+ import keyword
68
+ import re
69
+ import sys
70
+
71
+ from typing import TYPE_CHECKING, Any, Self, cast, overload
72
+
73
+
74
+ if TYPE_CHECKING:
75
+ import Pyjoyment.Validator
76
+
77
+
78
+ type Anything = builtins.object
79
+
80
+ # Renamed from Mojolicious: the check "in" is the method one_of, as "in" is a keyword of Python
81
+ _RENAMED = {'one_of': 'in'}
82
+
83
+
84
+ class Pyjoyment_Validator_Validation(Pyjo.Base.object):
85
+ """
86
+ :mod:`Pyjoyment.Validator.Validation` inherits all methods from
87
+ :mod:`Pyjo.Base` and implements the following new ones.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ *,
93
+ csrf_token: str | None = None,
94
+ input: dict[str, Any] | None = None,
95
+ output: dict[str, Any] | None = None,
96
+ topic: str | None = None,
97
+ validator: 'Pyjoyment.Validator.object | None' = None,
98
+ ) -> None:
99
+ """::
100
+
101
+ v = Pyjoyment.Validator.Validation.new(validator=Pyjoyment.Validator.new())
102
+
103
+ Construct a new :mod:`Pyjoyment.Validator.Validation` object.
104
+ """
105
+ self.csrf_token = csrf_token
106
+ """::
107
+
108
+ token = v.csrf_token
109
+ v.csrf_token = 'fa6a08...'
110
+
111
+ CSRF token.
112
+ """
113
+
114
+ self.input: dict[str, Any] = input if input is not None else {}
115
+ """::
116
+
117
+ input = v.input
118
+ v.input = {'foo': 'bar', 'baz': [123, 'yada']}
119
+
120
+ Data to be validated.
121
+ """
122
+
123
+ self.output: dict[str, Any] = output if output is not None else {}
124
+ """::
125
+
126
+ output = v.output
127
+ v.output = {'foo': 'bar', 'baz': [123, 'yada']}
128
+
129
+ Validated data.
130
+ """
131
+
132
+ self.topic = topic
133
+ """::
134
+
135
+ topic = v.topic
136
+ v.topic = 'foo'
137
+
138
+ Name of field currently being validated.
139
+ """
140
+
141
+ self.validator = validator
142
+ """::
143
+
144
+ validator = v.validator
145
+ v.validator = Pyjoyment.Validator.new()
146
+
147
+ :mod:`Pyjoyment.Validator` object this validation belongs to.
148
+ """
149
+
150
+ self._error: dict[str, list[Any]] = {}
151
+
152
+ # Unlike Mojolicious: Mojo registers the checks with Mojo::DynamicMethods, Python looks them up in the checks of
153
+ # the validator when a method is not found
154
+ def __getattr__(self, name: str) -> Any:
155
+ validator: Any = vars(self).get('validator')
156
+ if not name.startswith('_') and validator is not None:
157
+ check = _RENAMED.get(name, name)
158
+ if check.endswith('_') and keyword.iskeyword(check[:-1]):
159
+ check = check[:-1]
160
+ if check in validator.checks:
161
+ return functools.partial(self.check, check)
162
+ package = f'{type(self).__module__}.{type(self).__qualname__}'
163
+
164
+ # The location of the caller, which croak adds to the message
165
+ caller = sys._getframe(1) # pyright: ignore[reportPrivateUsage]
166
+ at = f'at {caller.f_code.co_filename} line {caller.f_lineno}'
167
+ raise AttributeError(
168
+ f'Can\'t locate object method "{name}" via package "{package}" {at}.\n', name=name, obj=self
169
+ )
170
+
171
+ @property
172
+ def failed(self) -> list[str]:
173
+ """::
174
+
175
+ names = v.failed
176
+
177
+ Return a list with all names for values that failed validation. ::
178
+
179
+ # Names of all values that failed
180
+ for name in v.failed:
181
+ print(name)
182
+ """
183
+ return sorted(self._error)
184
+
185
+ @property
186
+ def has_data(self) -> bool:
187
+ """::
188
+
189
+ boolean = v.has_data
190
+
191
+ Check if :attr:`input` is available for validation.
192
+ """
193
+ return bool(self.input)
194
+
195
+ @property
196
+ def passed(self) -> list[str]:
197
+ """::
198
+
199
+ names = v.passed
200
+
201
+ Return a list with all names for values that passed validation. ::
202
+
203
+ # Names of all values that passed
204
+ for name in v.passed:
205
+ print(name)
206
+ """
207
+ return sorted(self.output)
208
+
209
+ def check(self, check: str, /, *args: Any) -> Self:
210
+ """::
211
+
212
+ v = v.check('size', 2, 7)
213
+
214
+ Perform validation check on all values of the current :attr:`topic`,
215
+ no more checks will be performed on them after the first one failed.
216
+ All checks from :mod:`Pyjoyment.Validator` are supported.
217
+ """
218
+ if not self.is_valid():
219
+ return self
220
+
221
+ assert self.validator is not None
222
+ cb = self.validator.checks[check]
223
+ name = self.topic
224
+ assert name is not None
225
+ for value in _list(self.output[name]):
226
+ if not (result := cb(self, name, value, *args)):
227
+ continue
228
+ return self.error(name, [check, result, *args])
229
+
230
+ return self
231
+
232
+ def csrf_protect(self) -> Self:
233
+ """::
234
+
235
+ v = v.csrf_protect()
236
+
237
+ Validate ``csrf_token`` and protect from cross-site request forgery.
238
+ Tokens are masked with a random value to protect against BREACH
239
+ attacks.
240
+ """
241
+ token = self.input.get('csrf_token')
242
+ if not _csrf_valid(token, self.csrf_token):
243
+ self.error('csrf_token', ['csrf_protect'])
244
+ return self
245
+
246
+ @overload
247
+ def error(self, name: str, /) -> list[Any] | None: ...
248
+
249
+ @overload
250
+ def error(self, name: str, value: list[Any], /) -> Self: ...
251
+
252
+ def error(self, name: str, /, *args: list[Any]) -> list[Any] | None | Self:
253
+ """::
254
+
255
+ err = v.error('foo')
256
+ v = v.error('foo', ['custom_check'])
257
+ v = v.error('foo', [check, result, *args])
258
+
259
+ Get or set details for failed validation check, at any given time
260
+ there can only be one per field. ::
261
+
262
+ # Details about failed validation
263
+ check, result, *args = v.error('foo')
264
+
265
+ # Force validation to fail for a field without performing a check
266
+ v.error('foo', ['some_made_up_check_name'])
267
+ """
268
+ if not args:
269
+ return self._error.get(name)
270
+ self._error[name] = args[0]
271
+ self.output.pop(name, None)
272
+ return self
273
+
274
+ def every_param(self, name: str | None = None) -> list[Any]:
275
+ """::
276
+
277
+ values = v.every_param()
278
+ values = v.every_param('foo')
279
+
280
+ Similar to :meth:`param`, but returns all values sharing the same name
281
+ as a list. ::
282
+
283
+ # Get first value
284
+ first = v.every_param('foo')[0]
285
+ """
286
+ if (key := name if name is not None else self.topic) is None or (value := self.output.get(key)) is None:
287
+ return []
288
+ return _list(value)
289
+
290
+ def has_error(self, name: str | None = None) -> bool:
291
+ """::
292
+
293
+ boolean = v.has_error()
294
+ boolean = v.has_error('foo')
295
+
296
+ Check if validation resulted in errors, defaults to checking all
297
+ fields.
298
+ """
299
+ # Unlike Mojolicious: a field named "0" is checked, where Mojo checks all the fields, as "0" is false in Perl
300
+ return name in self._error if name is not None else bool(self._error)
301
+
302
+ def is_valid(self, name: str | None = None) -> bool:
303
+ """::
304
+
305
+ boolean = v.is_valid()
306
+ boolean = v.is_valid('foo')
307
+
308
+ Check if validation was successful and field has a value, defaults to
309
+ checking the current :attr:`topic`.
310
+ """
311
+ return (name if name is not None else self.topic) in self.output
312
+
313
+ def optional(self, name: str, /, *filters: str) -> Self:
314
+ """::
315
+
316
+ v = v.optional('foo')
317
+ v = v.optional('foo', *filters)
318
+
319
+ Change validation :attr:`topic` and apply filters. All filters from
320
+ :mod:`Pyjoyment.Validator` are supported. ::
321
+
322
+ # Trim value and check size
323
+ v.optional('user', 'trim').size(1, 15)
324
+ """
325
+ self.topic = name
326
+ if (input := self.input.get(name)) is None:
327
+ return self
328
+
329
+ values = _list(input)
330
+ assert self.validator is not None
331
+ for cb in [self.validator.filters[f] for f in filters]:
332
+ values = [item for value in values for item in _values(cb(self, name, value))]
333
+ if values and all(value is not None for value in values):
334
+ self.output[name] = values if len(values) > 1 else values[0]
335
+
336
+ return self
337
+
338
+ def param(self, name: str | None = None) -> Any:
339
+ """::
340
+
341
+ value = v.param()
342
+ value = v.param('foo')
343
+
344
+ Access validated values, defaults to the current :attr:`topic`. If
345
+ there are multiple values sharing the same name, and you want to
346
+ access more than just the last one, you can use :meth:`every_param`.
347
+ ::
348
+
349
+ # Get value right away
350
+ user = v.optional('user').size(1, 15).param()
351
+ """
352
+ values = self.every_param(name)
353
+ return values[-1] if values else None
354
+
355
+ def required(self, name: str, /, *filters: str) -> Self:
356
+ """::
357
+
358
+ v = v.required('foo')
359
+ v = v.required('foo', *filters)
360
+
361
+ Change validation :attr:`topic`, apply filters, and make sure a value
362
+ is present. All filters from :mod:`Pyjoyment.Validator` are supported.
363
+ ::
364
+
365
+ # Trim value and check size
366
+ v.required('user', 'trim').size(1, 15)
367
+ """
368
+ if self.optional(name, *filters).is_valid():
369
+ return self
370
+ return self.error(name, ['required'])
371
+
372
+
373
+ def _csrf_valid(token: Anything, csrf: str | None) -> bool:
374
+ if not isinstance(token, str) or not re.search(r'^[0-9a-f]{80}$', token):
375
+ return False
376
+
377
+ # Unmask token to protect against BREACH attacks
378
+ mask = bytes.fromhex(token[:40])
379
+ masked = bytes.fromhex(token[40:])
380
+ return secure_compare(bytes(a ^ b for a, b in zip(mask, masked, strict=True)).hex(), csrf or '')
381
+
382
+
383
+ # The values of a field, which are several for a list
384
+ def _list(value: Any) -> list[Any]:
385
+ return list(cast(list[Any], value)) if isinstance(value, list) else [value]
386
+
387
+
388
+ # The values that a filter returns, which are several for a tuple
389
+ def _values(result: Any) -> tuple[Any, ...]:
390
+ return cast(tuple[Any, ...], result) if isinstance(result, tuple) else (result,)
391
+
392
+
393
+ new = Pyjoyment_Validator_Validation.new
394
+ object = Pyjoyment_Validator_Validation
395
+ Validation = Pyjoyment_Validator_Validation
@@ -0,0 +1,304 @@
1
+ r"""
2
+ Pyjoyment.Validator - Validate values
3
+ =====================================
4
+ ::
5
+
6
+ from Pyjoyment.Validator import Validator
7
+
8
+ validator = Validator()
9
+ v = validator.validation()
10
+ v.input = {'foo': 'bar'}
11
+ v.required('foo').like(re.compile(r'ar$'))
12
+ print(v.param('foo'))
13
+
14
+ # Or import the module, as in Google Python Style Guide
15
+
16
+ import Pyjoyment.Validator
17
+
18
+ validator = Pyjoyment.Validator.new()
19
+ v = validator.validation()
20
+ v.input = {'foo': 'bar'}
21
+ v.required('foo').like(re.compile(r'ar$'))
22
+ print(v.param('foo'))
23
+
24
+ :mod:`Pyjoyment.Validator` validates values for :mod:`Pyjoyment`.
25
+
26
+ Checks
27
+ ------
28
+
29
+ These validation checks are available by default.
30
+
31
+ equal_to ::
32
+
33
+ v = v.equal_to('foo')
34
+
35
+ String value needs to be equal to the value of another field.
36
+
37
+ in ::
38
+
39
+ v = v.one_of('foo', 'bar', 'baz')
40
+
41
+ String value needs to match one of the values in the list. ``in`` is a
42
+ keyword of Python, so the method of the check is ``one_of``, and ``in_`` is
43
+ its alias.
44
+
45
+ like ::
46
+
47
+ v = v.like(re.compile(r'^[A-Z]'))
48
+
49
+ String value needs to match the regular expression.
50
+
51
+ num ::
52
+
53
+ v = v.num()
54
+ v = v.num(2, 5)
55
+ v = v.num(-3, 7)
56
+ v = v.num(2, None)
57
+ v = v.num(None, 5)
58
+
59
+ String value needs to be a non-fractional number (positive or negative) and if
60
+ provided in the given range.
61
+
62
+ size ::
63
+
64
+ v = v.size(2, 5)
65
+ v = v.size(2, None)
66
+ v = v.size(None, 5)
67
+
68
+ String value length or size of :mod:`Pyjo.Upload` object in bytes needs to be
69
+ between these two values.
70
+
71
+ upload ::
72
+
73
+ v = v.upload()
74
+
75
+ Value needs to be a :mod:`Pyjo.Upload` object, representing a file upload.
76
+
77
+ Filters
78
+ -------
79
+
80
+ These filters are available by default.
81
+
82
+ comma_separated ::
83
+
84
+ v = v.optional('foo', 'comma_separated')
85
+
86
+ Split string of comma separated values into separate values.
87
+
88
+ not_empty ::
89
+
90
+ v = v.optional('foo', 'not_empty')
91
+
92
+ Remove empty string values and treat them as if they had not been submitted.
93
+
94
+ trim ::
95
+
96
+ v = v.optional('foo', 'trim')
97
+
98
+ Trim whitespace characters from both ends of string value with
99
+ :func:`Pyjo.Util.trim`.
100
+
101
+ See also
102
+ --------
103
+
104
+ :mod:`Pyjoyment`, https://mojolicious.org.
105
+
106
+ Ported from `Mojolicious::Validator <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Validator.pm>`_
107
+ of Mojolicious 9.49.
108
+
109
+ Classes
110
+ -------
111
+ """
112
+
113
+ import Pyjo.Base
114
+ import Pyjo.String.Unicode
115
+ import Pyjo.Upload
116
+ import Pyjoyment.Validator.Validation
117
+
118
+ from Pyjo.Util import trim
119
+
120
+ import builtins
121
+ import re
122
+
123
+ from collections.abc import Callable
124
+ from typing import Any, Self, overload
125
+
126
+
127
+ type Anything = builtins.object
128
+
129
+ # A check gets the validation, the name and a value of the field, and the arguments of the check, and returns a true
130
+ # value when the value fails
131
+ type Check = Callable[..., Any]
132
+
133
+ # A filter gets the validation, the name and a value of the field, and returns the value, or a tuple of several values
134
+ type Filter = Callable[..., Any]
135
+
136
+
137
+ class Pyjoyment_Validator(Pyjo.Base.object):
138
+ """
139
+ :mod:`Pyjoyment.Validator` inherits all methods from :mod:`Pyjo.Base` and
140
+ implements the following new ones.
141
+ """
142
+
143
+ def __init__(self) -> None:
144
+ """::
145
+
146
+ validator = Pyjoyment.Validator.new()
147
+
148
+ Construct a new :mod:`Pyjoyment.Validator` object.
149
+ """
150
+ self.checks: dict[str, Check] = {}
151
+ """::
152
+
153
+ checks = validator.checks
154
+ validator.checks = {'size': lambda v, name, value, *args: ...}
155
+
156
+ Registered validation checks, by default only ``equal_to``, ``in``,
157
+ ``like``, ``num``, ``size`` and ``upload`` are already defined.
158
+ """
159
+
160
+ self.filters: dict[str, Filter] = {'comma_separated': _comma_separated, 'not_empty': _not_empty, 'trim': _trim}
161
+ """::
162
+
163
+ filters = validator.filters
164
+ validator.filters = {'trim': lambda v, name, value: ...}
165
+
166
+ Registered filters, by default only ``comma_separated``,
167
+ ``not_empty`` and ``trim`` are already defined.
168
+ """
169
+
170
+ self.add_check('equal_to', _equal_to)
171
+ self.add_check('in', _in)
172
+ self.add_check('like', _like)
173
+ self.add_check('num', _num)
174
+ self.add_check('size', _size)
175
+ self.add_check('upload', _upload)
176
+
177
+ @overload
178
+ def add_check[F: Check](self, name: str, cb: None = None) -> Callable[[F], F]: ...
179
+
180
+ @overload
181
+ def add_check(self, name: str, cb: Check) -> Self: ...
182
+
183
+ def add_check(self, name: str, cb: Check | None = None) -> Any:
184
+ """::
185
+
186
+ validator = validator.add_check('size', lambda v, name, value, *args: ...)
187
+
188
+ @validator.add_check('size')
189
+ def size(v, name, value, *args): ...
190
+
191
+ Register a validation check. ::
192
+
193
+ @validator.add_check('foo')
194
+ def foo(v, name, value, *args):
195
+ ...
196
+ return None
197
+ """
198
+ if cb is None:
199
+
200
+ def decorator(cb: Check) -> Check:
201
+ self.add_check(name, cb)
202
+ return cb
203
+
204
+ return decorator
205
+
206
+ # Unlike Mojolicious: Mojo registers the check as a method of Mojolicious::Validator::Validation with
207
+ # Mojo::DynamicMethods, Python looks it up in the checks of the validator when a method is not found
208
+ self.checks[name] = cb
209
+ return self
210
+
211
+ @overload
212
+ def add_filter[F: Filter](self, name: str, cb: None = None) -> Callable[[F], F]: ...
213
+
214
+ @overload
215
+ def add_filter(self, name: str, cb: Filter) -> Self: ...
216
+
217
+ def add_filter(self, name: str, cb: Filter | None = None) -> Any:
218
+ """::
219
+
220
+ validator = validator.add_filter('trim', lambda v, name, value: ...)
221
+
222
+ @validator.add_filter('trim')
223
+ def trim(v, name, value): ...
224
+
225
+ Register a new filter. ::
226
+
227
+ @validator.add_filter('foo')
228
+ def foo(v, name, value):
229
+ ...
230
+ return value
231
+ """
232
+ if cb is None:
233
+
234
+ def decorator(cb: Filter) -> Filter:
235
+ self.add_filter(name, cb)
236
+ return cb
237
+
238
+ return decorator
239
+ self.filters[name] = cb
240
+ return self
241
+
242
+ def validation(self) -> 'Pyjoyment.Validator.Validation.object':
243
+ """::
244
+
245
+ v = validator.validation()
246
+
247
+ Build :mod:`Pyjoyment.Validator.Validation` object to perform
248
+ validations. ::
249
+
250
+ v = validator.validation()
251
+ v.input = {'foo': 'bar'}
252
+ v.required('foo').size(1, 5)
253
+ print(v.param('foo'))
254
+ """
255
+ return Pyjoyment.Validator.Validation.new(validator=self)
256
+
257
+
258
+ def _comma_separated(v: Anything, name: str, value: Anything) -> tuple[str, ...] | None:
259
+ if value is None:
260
+ return None
261
+ return tuple(str(part) for part in Pyjo.String.Unicode.new(str(value)).split(r'\s*,\s*', -1))
262
+
263
+
264
+ def _equal_to(v: 'Pyjoyment.Validator.Validation.object', name: str, value: Anything, to: str) -> bool:
265
+ if (other := v.input.get(to)) is None:
266
+ return True
267
+ return str(value) != str(other)
268
+
269
+
270
+ def _in(v: Anything, name: str, value: Anything, *values: Anything) -> bool:
271
+ return not any(str(value) == str(item) for item in values)
272
+
273
+
274
+ def _like(v: Anything, name: str, value: Anything, regex: str | re.Pattern[str]) -> bool:
275
+ return not re.search(regex, str(value))
276
+
277
+
278
+ def _not_empty(v: Anything, name: str, value: Anything) -> Anything:
279
+ return value if value is not None and len(str(value)) else ()
280
+
281
+
282
+ def _num(v: Anything, name: str, value: Anything, min: int | None = None, max: int | None = None) -> bool:
283
+ if not re.search(r'^-?[0-9]+$', string := str(value)):
284
+ return True
285
+ number = int(string)
286
+ return min is not None and min > number or max is not None and max < number
287
+
288
+
289
+ def _size(v: Anything, name: str, value: Anything, min: int | None = None, max: int | None = None) -> bool:
290
+ length = value.size if isinstance(value, Pyjo.Upload.object) else len(str(value))
291
+ return (min is not None and length < min) or (max is not None and length > max)
292
+
293
+
294
+ def _trim(v: Anything, name: str, value: Anything) -> str | None:
295
+ return trim(str(value)) if value is not None else None
296
+
297
+
298
+ def _upload(v: Anything, name: str, value: Anything) -> bool:
299
+ return not isinstance(value, Pyjo.Upload.object)
300
+
301
+
302
+ new = Pyjoyment_Validator.new
303
+ object = Pyjoyment_Validator
304
+ Validator = Pyjoyment_Validator