plain 0.68.0__py3-none-any.whl → 0.101.2__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.
- plain/CHANGELOG.md +656 -1
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/utils/tree.py
CHANGED
|
@@ -3,7 +3,13 @@ A class for storing a tree graph. Primarily used for filter constructs in the
|
|
|
3
3
|
ORM.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
6
8
|
import copy
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing import Self
|
|
7
13
|
|
|
8
14
|
from plain.utils.hashable import make_hashable
|
|
9
15
|
|
|
@@ -17,16 +23,26 @@ class Node:
|
|
|
17
23
|
|
|
18
24
|
# Standard connector type. Clients usually won't use this at all and
|
|
19
25
|
# subclasses will usually override the value.
|
|
20
|
-
default = "DEFAULT"
|
|
21
|
-
|
|
22
|
-
def __init__(
|
|
26
|
+
default: str = "DEFAULT"
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
children: list[Any] | None = None,
|
|
31
|
+
connector: str | None = None,
|
|
32
|
+
negated: bool = False,
|
|
33
|
+
) -> None:
|
|
23
34
|
"""Construct a new Node. If no connector is given, use the default."""
|
|
24
|
-
self.children = children[:] if children else []
|
|
25
|
-
self.connector = connector or self.default
|
|
26
|
-
self.negated = negated
|
|
35
|
+
self.children: list[Any] = children[:] if children else []
|
|
36
|
+
self.connector: str = connector or self.default
|
|
37
|
+
self.negated: bool = negated
|
|
27
38
|
|
|
28
39
|
@classmethod
|
|
29
|
-
def create(
|
|
40
|
+
def create(
|
|
41
|
+
cls,
|
|
42
|
+
children: list[Any] | None = None,
|
|
43
|
+
connector: str | None = None,
|
|
44
|
+
negated: bool = False,
|
|
45
|
+
) -> Self:
|
|
30
46
|
"""
|
|
31
47
|
Create a new instance using Node() instead of __init__() as some
|
|
32
48
|
subclasses, e.g. plain.models.query_utils.Q, may implement a custom
|
|
@@ -35,40 +51,40 @@ class Node:
|
|
|
35
51
|
"""
|
|
36
52
|
obj = Node(children, connector or cls.default, negated)
|
|
37
53
|
obj.__class__ = cls
|
|
38
|
-
return obj
|
|
54
|
+
return obj # type: ignore[return-value]
|
|
39
55
|
|
|
40
|
-
def __str__(self):
|
|
56
|
+
def __str__(self) -> str:
|
|
41
57
|
template = "(NOT (%s: %s))" if self.negated else "(%s: %s)"
|
|
42
58
|
return template % (self.connector, ", ".join(str(c) for c in self.children))
|
|
43
59
|
|
|
44
|
-
def __repr__(self):
|
|
60
|
+
def __repr__(self) -> str:
|
|
45
61
|
return f"<{self.__class__.__name__}: {self}>"
|
|
46
62
|
|
|
47
|
-
def __copy__(self):
|
|
63
|
+
def __copy__(self) -> Self:
|
|
48
64
|
obj = self.create(connector=self.connector, negated=self.negated)
|
|
49
65
|
obj.children = self.children # Don't [:] as .__init__() via .create() does.
|
|
50
66
|
return obj
|
|
51
67
|
|
|
52
68
|
copy = __copy__
|
|
53
69
|
|
|
54
|
-
def __deepcopy__(self, memodict):
|
|
70
|
+
def __deepcopy__(self, memodict: dict[int, Any]) -> Self:
|
|
55
71
|
obj = self.create(connector=self.connector, negated=self.negated)
|
|
56
72
|
obj.children = copy.deepcopy(self.children, memodict)
|
|
57
73
|
return obj
|
|
58
74
|
|
|
59
|
-
def __len__(self):
|
|
75
|
+
def __len__(self) -> int:
|
|
60
76
|
"""Return the number of children this node has."""
|
|
61
77
|
return len(self.children)
|
|
62
78
|
|
|
63
|
-
def __bool__(self):
|
|
79
|
+
def __bool__(self) -> bool:
|
|
64
80
|
"""Return whether or not this node has children."""
|
|
65
81
|
return bool(self.children)
|
|
66
82
|
|
|
67
|
-
def __contains__(self, other):
|
|
83
|
+
def __contains__(self, other: Any) -> bool:
|
|
68
84
|
"""Return True if 'other' is a direct child of this instance."""
|
|
69
85
|
return other in self.children
|
|
70
86
|
|
|
71
|
-
def __eq__(self, other):
|
|
87
|
+
def __eq__(self, other: Any) -> bool:
|
|
72
88
|
return (
|
|
73
89
|
self.__class__ == other.__class__
|
|
74
90
|
and self.connector == other.connector
|
|
@@ -76,7 +92,7 @@ class Node:
|
|
|
76
92
|
and self.children == other.children
|
|
77
93
|
)
|
|
78
94
|
|
|
79
|
-
def __hash__(self):
|
|
95
|
+
def __hash__(self) -> int:
|
|
80
96
|
return hash(
|
|
81
97
|
(
|
|
82
98
|
self.__class__,
|
|
@@ -86,7 +102,7 @@ class Node:
|
|
|
86
102
|
)
|
|
87
103
|
)
|
|
88
104
|
|
|
89
|
-
def add(self, data, conn_type):
|
|
105
|
+
def add(self, data: Any, conn_type: str) -> Any:
|
|
90
106
|
"""
|
|
91
107
|
Combine this tree and the data represented by data using the
|
|
92
108
|
connector conn_type. The combine is done by squashing the node other
|
|
@@ -121,6 +137,6 @@ class Node:
|
|
|
121
137
|
self.children.append(data)
|
|
122
138
|
return data
|
|
123
139
|
|
|
124
|
-
def negate(self):
|
|
140
|
+
def negate(self) -> None:
|
|
125
141
|
"""Negate the sense of the root connector."""
|
|
126
142
|
self.negated = not self.negated
|
plain/validators.py
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import ipaddress
|
|
2
4
|
import math
|
|
3
5
|
import re
|
|
6
|
+
from collections.abc import Callable
|
|
4
7
|
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
5
9
|
from urllib.parse import urlsplit, urlunsplit
|
|
6
10
|
|
|
7
11
|
from plain.exceptions import ValidationError
|
|
@@ -11,23 +15,40 @@ from plain.utils.ipv6 import is_valid_ipv6_address
|
|
|
11
15
|
from plain.utils.regex_helper import _lazy_re_compile
|
|
12
16
|
from plain.utils.text import pluralize_lazy
|
|
13
17
|
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from plain.utils.functional import SimpleLazyObject
|
|
20
|
+
|
|
14
21
|
# These values, if given to validate(), will trigger the self.required check.
|
|
15
22
|
EMPTY_VALUES = (None, "", [], (), {})
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
@deconstructible
|
|
19
26
|
class RegexValidator:
|
|
20
|
-
regex = ""
|
|
27
|
+
regex: str | re.Pattern[str] | SimpleLazyObject = ""
|
|
21
28
|
message = "Enter a valid value."
|
|
22
29
|
code = "invalid"
|
|
23
30
|
inverse_match = False
|
|
24
31
|
flags = 0
|
|
25
32
|
|
|
26
33
|
def __init__(
|
|
27
|
-
self,
|
|
28
|
-
|
|
34
|
+
self,
|
|
35
|
+
regex: str | re.Pattern[str] | None = None,
|
|
36
|
+
message: str | None = None,
|
|
37
|
+
code: str | None = None,
|
|
38
|
+
inverse_match: bool | None = None,
|
|
39
|
+
flags: int | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
# Only compile regex if explicitly provided or if class default needs compilation
|
|
29
42
|
if regex is not None:
|
|
30
|
-
|
|
43
|
+
regex_to_compile: str | re.Pattern[str] = regex
|
|
44
|
+
elif isinstance(self.regex, str | re.Pattern):
|
|
45
|
+
# Class-level regex is a string or pattern that needs compilation
|
|
46
|
+
regex_to_compile = self.regex
|
|
47
|
+
else:
|
|
48
|
+
# Class-level regex is already compiled (e.g., in URL Validator subclass)
|
|
49
|
+
# Don't recompile it
|
|
50
|
+
regex_to_compile = None
|
|
51
|
+
|
|
31
52
|
if message is not None:
|
|
32
53
|
self.message = message
|
|
33
54
|
if code is not None:
|
|
@@ -36,28 +57,31 @@ class RegexValidator:
|
|
|
36
57
|
self.inverse_match = inverse_match
|
|
37
58
|
if flags is not None:
|
|
38
59
|
self.flags = flags
|
|
39
|
-
if self.flags and not isinstance(self.regex, str):
|
|
40
|
-
raise TypeError(
|
|
41
|
-
"If the flags are set, regex must be a regular expression string."
|
|
42
|
-
)
|
|
43
60
|
|
|
44
|
-
|
|
61
|
+
# Only compile if we have a regex to compile
|
|
62
|
+
if regex_to_compile is not None:
|
|
63
|
+
if self.flags and not isinstance(regex_to_compile, str):
|
|
64
|
+
raise TypeError(
|
|
65
|
+
"If the flags are set, regex must be a regular expression string."
|
|
66
|
+
)
|
|
67
|
+
self.regex = _lazy_re_compile(regex_to_compile, self.flags)
|
|
45
68
|
|
|
46
|
-
def __call__(self, value):
|
|
69
|
+
def __call__(self, value: Any) -> None:
|
|
47
70
|
"""
|
|
48
71
|
Validate that the input contains (or does *not* contain, if
|
|
49
72
|
inverse_match is True) a match for the regular expression.
|
|
50
73
|
"""
|
|
51
|
-
|
|
74
|
+
# self.regex is always a SimpleLazyObject with search() after __init__
|
|
75
|
+
regex_matches = cast(re.Pattern[str], self.regex).search(str(value))
|
|
52
76
|
invalid_input = regex_matches if self.inverse_match else not regex_matches
|
|
53
77
|
if invalid_input:
|
|
54
78
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
|
55
79
|
|
|
56
|
-
def __eq__(self, other):
|
|
80
|
+
def __eq__(self, other: object) -> bool:
|
|
57
81
|
return (
|
|
58
82
|
isinstance(other, RegexValidator)
|
|
59
|
-
and self.regex.pattern == other.regex.pattern
|
|
60
|
-
and self.regex.flags == other.regex.flags
|
|
83
|
+
and self.regex.pattern == other.regex.pattern # type: ignore[union-attr]
|
|
84
|
+
and self.regex.flags == other.regex.flags # type: ignore[union-attr]
|
|
61
85
|
and (self.message == other.message)
|
|
62
86
|
and (self.code == other.code)
|
|
63
87
|
and (self.inverse_match == other.inverse_match)
|
|
@@ -104,12 +128,12 @@ class URLValidator(RegexValidator):
|
|
|
104
128
|
schemes = ["http", "https", "ftp", "ftps"]
|
|
105
129
|
unsafe_chars = frozenset("\t\r\n")
|
|
106
130
|
|
|
107
|
-
def __init__(self, schemes=None, **kwargs):
|
|
131
|
+
def __init__(self, schemes: list[str] | None = None, **kwargs: Any) -> None:
|
|
108
132
|
super().__init__(**kwargs)
|
|
109
133
|
if schemes is not None:
|
|
110
134
|
self.schemes = schemes
|
|
111
135
|
|
|
112
|
-
def __call__(self, value):
|
|
136
|
+
def __call__(self, value: Any) -> None:
|
|
113
137
|
if not isinstance(value, str):
|
|
114
138
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
|
115
139
|
if self.unsafe_chars.intersection(value):
|
|
@@ -182,7 +206,12 @@ class EmailValidator:
|
|
|
182
206
|
)
|
|
183
207
|
domain_allowlist = ["localhost"]
|
|
184
208
|
|
|
185
|
-
def __init__(
|
|
209
|
+
def __init__(
|
|
210
|
+
self,
|
|
211
|
+
message: str | None = None,
|
|
212
|
+
code: str | None = None,
|
|
213
|
+
allowlist: list[str] | None = None,
|
|
214
|
+
) -> None:
|
|
186
215
|
if message is not None:
|
|
187
216
|
self.message = message
|
|
188
217
|
if code is not None:
|
|
@@ -190,7 +219,7 @@ class EmailValidator:
|
|
|
190
219
|
if allowlist is not None:
|
|
191
220
|
self.domain_allowlist = allowlist
|
|
192
221
|
|
|
193
|
-
def __call__(self, value):
|
|
222
|
+
def __call__(self, value: Any) -> None:
|
|
194
223
|
if not value or "@" not in value:
|
|
195
224
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
|
196
225
|
|
|
@@ -209,10 +238,11 @@ class EmailValidator:
|
|
|
209
238
|
pass
|
|
210
239
|
else:
|
|
211
240
|
if self.validate_domain_part(domain_part):
|
|
212
|
-
return
|
|
241
|
+
return None
|
|
213
242
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
|
243
|
+
return None
|
|
214
244
|
|
|
215
|
-
def validate_domain_part(self, domain_part):
|
|
245
|
+
def validate_domain_part(self, domain_part: str) -> bool:
|
|
216
246
|
if self.domain_regex.match(domain_part):
|
|
217
247
|
return True
|
|
218
248
|
|
|
@@ -226,7 +256,7 @@ class EmailValidator:
|
|
|
226
256
|
pass
|
|
227
257
|
return False
|
|
228
258
|
|
|
229
|
-
def __eq__(self, other):
|
|
259
|
+
def __eq__(self, other: object) -> bool:
|
|
230
260
|
return (
|
|
231
261
|
isinstance(other, EmailValidator)
|
|
232
262
|
and (self.domain_allowlist == other.domain_allowlist)
|
|
@@ -238,7 +268,7 @@ class EmailValidator:
|
|
|
238
268
|
validate_email = EmailValidator()
|
|
239
269
|
|
|
240
270
|
|
|
241
|
-
def validate_ipv4_address(value):
|
|
271
|
+
def validate_ipv4_address(value: str) -> None:
|
|
242
272
|
try:
|
|
243
273
|
ipaddress.IPv4Address(value)
|
|
244
274
|
except ValueError:
|
|
@@ -247,14 +277,14 @@ def validate_ipv4_address(value):
|
|
|
247
277
|
)
|
|
248
278
|
|
|
249
279
|
|
|
250
|
-
def validate_ipv6_address(value):
|
|
280
|
+
def validate_ipv6_address(value: str) -> None:
|
|
251
281
|
if not is_valid_ipv6_address(value):
|
|
252
282
|
raise ValidationError(
|
|
253
283
|
"Enter a valid IPv6 address.", code="invalid", params={"value": value}
|
|
254
284
|
)
|
|
255
285
|
|
|
256
286
|
|
|
257
|
-
def validate_ipv46_address(value):
|
|
287
|
+
def validate_ipv46_address(value: str) -> None:
|
|
258
288
|
try:
|
|
259
289
|
validate_ipv4_address(value)
|
|
260
290
|
except ValidationError:
|
|
@@ -275,7 +305,9 @@ ip_address_validator_map = {
|
|
|
275
305
|
}
|
|
276
306
|
|
|
277
307
|
|
|
278
|
-
def ip_address_validators(
|
|
308
|
+
def ip_address_validators(
|
|
309
|
+
protocol: str, unpack_ipv4: bool
|
|
310
|
+
) -> tuple[list[Callable[[str], None]], str]:
|
|
279
311
|
"""
|
|
280
312
|
Depending on the given parameters, return the appropriate validators for
|
|
281
313
|
the GenericIPAddressField.
|
|
@@ -292,14 +324,19 @@ def ip_address_validators(protocol, unpack_ipv4):
|
|
|
292
324
|
)
|
|
293
325
|
|
|
294
326
|
|
|
295
|
-
def int_list_validator(
|
|
327
|
+
def int_list_validator(
|
|
328
|
+
sep: str = ",",
|
|
329
|
+
message: str | None = None,
|
|
330
|
+
code: str = "invalid",
|
|
331
|
+
allow_negative: bool = False,
|
|
332
|
+
) -> RegexValidator:
|
|
296
333
|
regexp = _lazy_re_compile(
|
|
297
334
|
r"^{neg}\d+(?:{sep}{neg}\d+)*\Z".format(
|
|
298
335
|
neg="(-)?" if allow_negative else "",
|
|
299
336
|
sep=re.escape(sep),
|
|
300
337
|
)
|
|
301
338
|
)
|
|
302
|
-
return RegexValidator(regexp, message=message, code=code)
|
|
339
|
+
return RegexValidator(regexp, message=message, code=code) # type: ignore[arg-type]
|
|
303
340
|
|
|
304
341
|
|
|
305
342
|
validate_comma_separated_integer_list = int_list_validator(
|
|
@@ -312,12 +349,12 @@ class BaseValidator:
|
|
|
312
349
|
message = "Ensure this value is %(limit_value)s (it is %(show_value)s)."
|
|
313
350
|
code = "limit_value"
|
|
314
351
|
|
|
315
|
-
def __init__(self, limit_value, message=None):
|
|
352
|
+
def __init__(self, limit_value: Any, message: str | None = None) -> None:
|
|
316
353
|
self.limit_value = limit_value
|
|
317
354
|
if message:
|
|
318
355
|
self.message = message
|
|
319
356
|
|
|
320
|
-
def __call__(self, value):
|
|
357
|
+
def __call__(self, value: Any) -> None:
|
|
321
358
|
cleaned = self.clean(value)
|
|
322
359
|
limit_value = (
|
|
323
360
|
self.limit_value() if callable(self.limit_value) else self.limit_value
|
|
@@ -326,7 +363,7 @@ class BaseValidator:
|
|
|
326
363
|
if self.compare(cleaned, limit_value):
|
|
327
364
|
raise ValidationError(self.message, code=self.code, params=params)
|
|
328
365
|
|
|
329
|
-
def __eq__(self, other):
|
|
366
|
+
def __eq__(self, other: object) -> bool:
|
|
330
367
|
if not isinstance(other, self.__class__):
|
|
331
368
|
return NotImplemented
|
|
332
369
|
return (
|
|
@@ -335,10 +372,10 @@ class BaseValidator:
|
|
|
335
372
|
and self.code == other.code
|
|
336
373
|
)
|
|
337
374
|
|
|
338
|
-
def compare(self, a, b):
|
|
375
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
339
376
|
return a is not b
|
|
340
377
|
|
|
341
|
-
def clean(self, x):
|
|
378
|
+
def clean(self, x: Any) -> Any:
|
|
342
379
|
return x
|
|
343
380
|
|
|
344
381
|
|
|
@@ -347,7 +384,7 @@ class MaxValueValidator(BaseValidator):
|
|
|
347
384
|
message = "Ensure this value is less than or equal to %(limit_value)s."
|
|
348
385
|
code = "max_value"
|
|
349
386
|
|
|
350
|
-
def compare(self, a, b):
|
|
387
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
351
388
|
return a > b
|
|
352
389
|
|
|
353
390
|
|
|
@@ -356,7 +393,7 @@ class MinValueValidator(BaseValidator):
|
|
|
356
393
|
message = "Ensure this value is greater than or equal to %(limit_value)s."
|
|
357
394
|
code = "min_value"
|
|
358
395
|
|
|
359
|
-
def compare(self, a, b):
|
|
396
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
360
397
|
return a < b
|
|
361
398
|
|
|
362
399
|
|
|
@@ -365,7 +402,7 @@ class StepValueValidator(BaseValidator):
|
|
|
365
402
|
message = "Ensure this value is a multiple of step size %(limit_value)s."
|
|
366
403
|
code = "step_size"
|
|
367
404
|
|
|
368
|
-
def compare(self, a, b):
|
|
405
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
369
406
|
return not math.isclose(math.remainder(a, b), 0, abs_tol=1e-9)
|
|
370
407
|
|
|
371
408
|
|
|
@@ -380,10 +417,10 @@ class MinLengthValidator(BaseValidator):
|
|
|
380
417
|
)
|
|
381
418
|
code = "min_length"
|
|
382
419
|
|
|
383
|
-
def compare(self, a, b):
|
|
420
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
384
421
|
return a < b
|
|
385
422
|
|
|
386
|
-
def clean(self, x):
|
|
423
|
+
def clean(self, x: Any) -> int:
|
|
387
424
|
return len(x)
|
|
388
425
|
|
|
389
426
|
|
|
@@ -398,10 +435,10 @@ class MaxLengthValidator(BaseValidator):
|
|
|
398
435
|
)
|
|
399
436
|
code = "max_length"
|
|
400
437
|
|
|
401
|
-
def compare(self, a, b):
|
|
438
|
+
def compare(self, a: Any, b: Any) -> bool:
|
|
402
439
|
return a > b
|
|
403
440
|
|
|
404
|
-
def clean(self, x):
|
|
441
|
+
def clean(self, x: Any) -> int:
|
|
405
442
|
return len(x)
|
|
406
443
|
|
|
407
444
|
|
|
@@ -433,11 +470,11 @@ class DecimalValidator:
|
|
|
433
470
|
),
|
|
434
471
|
}
|
|
435
472
|
|
|
436
|
-
def __init__(self, max_digits, decimal_places):
|
|
473
|
+
def __init__(self, max_digits: int | None, decimal_places: int | None) -> None:
|
|
437
474
|
self.max_digits = max_digits
|
|
438
475
|
self.decimal_places = decimal_places
|
|
439
476
|
|
|
440
|
-
def __call__(self, value):
|
|
477
|
+
def __call__(self, value: Any) -> None:
|
|
441
478
|
digit_tuple, exponent = value.as_tuple()[1:]
|
|
442
479
|
if exponent in {"F", "n", "N"}:
|
|
443
480
|
raise ValidationError(
|
|
@@ -485,7 +522,7 @@ class DecimalValidator:
|
|
|
485
522
|
params={"max": (self.max_digits - self.decimal_places), "value": value},
|
|
486
523
|
)
|
|
487
524
|
|
|
488
|
-
def __eq__(self, other):
|
|
525
|
+
def __eq__(self, other: object) -> bool:
|
|
489
526
|
return (
|
|
490
527
|
isinstance(other, self.__class__)
|
|
491
528
|
and self.max_digits == other.max_digits
|
|
@@ -495,10 +532,15 @@ class DecimalValidator:
|
|
|
495
532
|
|
|
496
533
|
@deconstructible
|
|
497
534
|
class FileExtensionValidator:
|
|
498
|
-
message =
|
|
535
|
+
message = 'File extension "%(extension)s" is not allowed. Allowed extensions are: %(allowed_extensions)s.'
|
|
499
536
|
code = "invalid_extension"
|
|
500
537
|
|
|
501
|
-
def __init__(
|
|
538
|
+
def __init__(
|
|
539
|
+
self,
|
|
540
|
+
allowed_extensions: list[str] | None = None,
|
|
541
|
+
message: str | None = None,
|
|
542
|
+
code: str | None = None,
|
|
543
|
+
) -> None:
|
|
502
544
|
if allowed_extensions is not None:
|
|
503
545
|
allowed_extensions = [
|
|
504
546
|
allowed_extension.lower() for allowed_extension in allowed_extensions
|
|
@@ -509,7 +551,7 @@ class FileExtensionValidator:
|
|
|
509
551
|
if code is not None:
|
|
510
552
|
self.code = code
|
|
511
553
|
|
|
512
|
-
def __call__(self, value):
|
|
554
|
+
def __call__(self, value: Any) -> None:
|
|
513
555
|
extension = Path(value.name).suffix[1:].lower()
|
|
514
556
|
if (
|
|
515
557
|
self.allowed_extensions is not None
|
|
@@ -525,7 +567,7 @@ class FileExtensionValidator:
|
|
|
525
567
|
},
|
|
526
568
|
)
|
|
527
569
|
|
|
528
|
-
def __eq__(self, other):
|
|
570
|
+
def __eq__(self, other: object) -> bool:
|
|
529
571
|
return (
|
|
530
572
|
isinstance(other, self.__class__)
|
|
531
573
|
and self.allowed_extensions == other.allowed_extensions
|
|
@@ -534,9 +576,9 @@ class FileExtensionValidator:
|
|
|
534
576
|
)
|
|
535
577
|
|
|
536
578
|
|
|
537
|
-
def get_available_image_extensions():
|
|
579
|
+
def get_available_image_extensions() -> list[str]:
|
|
538
580
|
try:
|
|
539
|
-
from PIL import Image
|
|
581
|
+
from PIL import Image # type: ignore[import-not-found]
|
|
540
582
|
except ImportError:
|
|
541
583
|
return []
|
|
542
584
|
else:
|
|
@@ -544,7 +586,7 @@ def get_available_image_extensions():
|
|
|
544
586
|
return [ext.lower()[1:] for ext in Image.EXTENSION]
|
|
545
587
|
|
|
546
588
|
|
|
547
|
-
def validate_image_file_extension(value):
|
|
589
|
+
def validate_image_file_extension(value: Any) -> None:
|
|
548
590
|
return FileExtensionValidator(allowed_extensions=get_available_image_extensions())(
|
|
549
591
|
value
|
|
550
592
|
)
|
|
@@ -557,17 +599,17 @@ class ProhibitNullCharactersValidator:
|
|
|
557
599
|
message = "Null characters are not allowed."
|
|
558
600
|
code = "null_characters_not_allowed"
|
|
559
601
|
|
|
560
|
-
def __init__(self, message=None, code=None):
|
|
602
|
+
def __init__(self, message: str | None = None, code: str | None = None) -> None:
|
|
561
603
|
if message is not None:
|
|
562
604
|
self.message = message
|
|
563
605
|
if code is not None:
|
|
564
606
|
self.code = code
|
|
565
607
|
|
|
566
|
-
def __call__(self, value):
|
|
608
|
+
def __call__(self, value: Any) -> None:
|
|
567
609
|
if "\x00" in str(value):
|
|
568
610
|
raise ValidationError(self.message, code=self.code, params={"value": value})
|
|
569
611
|
|
|
570
|
-
def __eq__(self, other):
|
|
612
|
+
def __eq__(self, other: object) -> bool:
|
|
571
613
|
return (
|
|
572
614
|
isinstance(other, self.__class__)
|
|
573
615
|
and self.message == other.message
|