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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- 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
|