plain 0.68.0__py3-none-any.whl → 0.103.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.
- plain/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- 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 +234 -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 +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- 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/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.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.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.103.0.dist-info}/licenses/LICENSE +0 -0
plain/http/__init__.py
CHANGED
|
@@ -1,49 +1,64 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
from .cookie import parse_cookie
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
BadRequestError400,
|
|
4
|
+
ForbiddenError403,
|
|
5
|
+
NotFoundError404,
|
|
6
|
+
RequestDataTooBigError400,
|
|
7
|
+
SuspiciousFileOperationError400,
|
|
8
|
+
SuspiciousMultipartFormError400,
|
|
9
|
+
SuspiciousOperationError400,
|
|
10
|
+
TooManyFieldsSentError400,
|
|
11
|
+
TooManyFilesSentError400,
|
|
12
|
+
)
|
|
13
|
+
from .middleware import HttpMiddleware
|
|
14
|
+
from .request import (
|
|
5
15
|
QueryDict,
|
|
6
16
|
RawPostDataException,
|
|
17
|
+
Request,
|
|
18
|
+
RequestHeaders,
|
|
7
19
|
UnreadablePostError,
|
|
8
20
|
)
|
|
9
|
-
from
|
|
21
|
+
from .response import (
|
|
10
22
|
BadHeaderError,
|
|
11
23
|
FileResponse,
|
|
12
|
-
Http404,
|
|
13
24
|
JsonResponse,
|
|
25
|
+
NotAllowedResponse,
|
|
26
|
+
NotModifiedResponse,
|
|
27
|
+
RedirectResponse,
|
|
14
28
|
Response,
|
|
15
|
-
ResponseBadRequest,
|
|
16
29
|
ResponseBase,
|
|
17
|
-
ResponseForbidden,
|
|
18
|
-
ResponseGone,
|
|
19
|
-
ResponseNotAllowed,
|
|
20
|
-
ResponseNotFound,
|
|
21
|
-
ResponseNotModified,
|
|
22
|
-
ResponseRedirect,
|
|
23
|
-
ResponseServerError,
|
|
24
30
|
StreamingResponse,
|
|
25
31
|
)
|
|
26
32
|
|
|
27
33
|
__all__ = [
|
|
34
|
+
# Middleware
|
|
35
|
+
"HttpMiddleware",
|
|
36
|
+
# Cookies
|
|
28
37
|
"parse_cookie",
|
|
29
|
-
|
|
30
|
-
"
|
|
38
|
+
# Request
|
|
39
|
+
"Request",
|
|
40
|
+
"RequestHeaders",
|
|
31
41
|
"QueryDict",
|
|
32
42
|
"RawPostDataException",
|
|
33
43
|
"UnreadablePostError",
|
|
44
|
+
# Response
|
|
34
45
|
"Response",
|
|
35
46
|
"ResponseBase",
|
|
36
47
|
"StreamingResponse",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"ResponseForbidden",
|
|
41
|
-
"ResponseNotFound",
|
|
42
|
-
"ResponseNotAllowed",
|
|
43
|
-
"ResponseGone",
|
|
44
|
-
"ResponseServerError",
|
|
45
|
-
"Http404",
|
|
46
|
-
"BadHeaderError",
|
|
48
|
+
"RedirectResponse",
|
|
49
|
+
"NotModifiedResponse",
|
|
50
|
+
"NotAllowedResponse",
|
|
47
51
|
"JsonResponse",
|
|
48
52
|
"FileResponse",
|
|
53
|
+
"BadHeaderError",
|
|
54
|
+
# Exceptions
|
|
55
|
+
"NotFoundError404",
|
|
56
|
+
"ForbiddenError403",
|
|
57
|
+
"BadRequestError400",
|
|
58
|
+
"SuspiciousOperationError400",
|
|
59
|
+
"SuspiciousMultipartFormError400",
|
|
60
|
+
"SuspiciousFileOperationError400",
|
|
61
|
+
"TooManyFieldsSentError400",
|
|
62
|
+
"TooManyFilesSentError400",
|
|
63
|
+
"RequestDataTooBigError400",
|
|
49
64
|
]
|
plain/http/cookie.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from http import cookies
|
|
2
4
|
|
|
3
5
|
from plain.runtime import settings
|
|
@@ -5,7 +7,7 @@ from plain.signing import BadSignature, TimestampSigner
|
|
|
5
7
|
from plain.utils.encoding import force_bytes
|
|
6
8
|
|
|
7
9
|
|
|
8
|
-
def parse_cookie(cookie):
|
|
10
|
+
def parse_cookie(cookie: str) -> dict[str, str]:
|
|
9
11
|
"""
|
|
10
12
|
Return a dictionary parsed from a `Cookie:` header string.
|
|
11
13
|
"""
|
|
@@ -24,7 +26,7 @@ def parse_cookie(cookie):
|
|
|
24
26
|
return cookiedict
|
|
25
27
|
|
|
26
28
|
|
|
27
|
-
def _cookie_key(key):
|
|
29
|
+
def _cookie_key(key: str) -> bytes:
|
|
28
30
|
"""
|
|
29
31
|
Generate a key for cookie signing that matches the pattern used by
|
|
30
32
|
set_signed_cookie and get_signed_cookie.
|
|
@@ -32,19 +34,19 @@ def _cookie_key(key):
|
|
|
32
34
|
return b"plain.http.cookies" + force_bytes(key)
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
def get_signed_cookie_signer(key, salt=""):
|
|
37
|
+
def get_signed_cookie_signer(key: str, salt: str = "") -> TimestampSigner:
|
|
36
38
|
"""
|
|
37
39
|
Create a TimestampSigner for signed cookies with the same configuration
|
|
38
40
|
used by both set_signed_cookie and get_signed_cookie.
|
|
39
41
|
"""
|
|
40
42
|
return TimestampSigner(
|
|
41
|
-
key=_cookie_key(settings.SECRET_KEY),
|
|
42
|
-
fallback_keys=
|
|
43
|
+
key=_cookie_key(settings.SECRET_KEY).decode(),
|
|
44
|
+
fallback_keys=[_cookie_key(k).decode() for k in settings.SECRET_KEY_FALLBACKS],
|
|
43
45
|
salt=key + salt,
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def sign_cookie_value(key, value, salt=""):
|
|
49
|
+
def sign_cookie_value(key: str, value: str, salt: str = "") -> str:
|
|
48
50
|
"""
|
|
49
51
|
Sign a cookie value using the standard Plain cookie signing approach.
|
|
50
52
|
"""
|
|
@@ -52,7 +54,13 @@ def sign_cookie_value(key, value, salt=""):
|
|
|
52
54
|
return signer.sign(value)
|
|
53
55
|
|
|
54
56
|
|
|
55
|
-
def unsign_cookie_value(
|
|
57
|
+
def unsign_cookie_value(
|
|
58
|
+
key: str,
|
|
59
|
+
signed_value: str,
|
|
60
|
+
salt: str = "",
|
|
61
|
+
max_age: int | None = None,
|
|
62
|
+
default: str | None = None,
|
|
63
|
+
) -> str | None:
|
|
56
64
|
"""
|
|
57
65
|
Unsign a cookie value using the standard Plain cookie signing approach.
|
|
58
66
|
Returns the default value if the signature is invalid or the cookie has expired.
|
plain/http/exceptions.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HTTP exceptions that are converted to HTTP responses by the exception handler.
|
|
3
|
+
The suffix indicates the HTTP status code that will be returned.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class NotFoundError404(Exception):
|
|
8
|
+
"""The requested resource was not found (HTTP 404)"""
|
|
9
|
+
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ForbiddenError403(Exception):
|
|
14
|
+
"""The user did not have permission to do that (HTTP 403)"""
|
|
15
|
+
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BadRequestError400(Exception):
|
|
20
|
+
"""The request is malformed and cannot be processed (HTTP 400)"""
|
|
21
|
+
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SuspiciousOperationError400(Exception):
|
|
26
|
+
"""The user did something suspicious (HTTP 400)"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SuspiciousMultipartFormError400(SuspiciousOperationError400):
|
|
30
|
+
"""Suspect MIME request in multipart form data"""
|
|
31
|
+
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SuspiciousFileOperationError400(SuspiciousOperationError400):
|
|
36
|
+
"""A Suspicious filesystem operation was attempted"""
|
|
37
|
+
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TooManyFieldsSentError400(SuspiciousOperationError400):
|
|
42
|
+
"""
|
|
43
|
+
The number of fields in a GET or POST request exceeded
|
|
44
|
+
settings.DATA_UPLOAD_MAX_NUMBER_FIELDS.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TooManyFilesSentError400(SuspiciousOperationError400):
|
|
51
|
+
"""
|
|
52
|
+
The number of fields in a GET or POST request exceeded
|
|
53
|
+
settings.DATA_UPLOAD_MAX_NUMBER_FILES.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RequestDataTooBigError400(SuspiciousOperationError400):
|
|
60
|
+
"""
|
|
61
|
+
The size of the request (excluding any file uploads) exceeded
|
|
62
|
+
settings.DATA_UPLOAD_MAX_MEMORY_SIZE.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
pass
|
plain/http/middleware.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from plain.http import Request, Response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpMiddleware(ABC):
|
|
12
|
+
"""
|
|
13
|
+
Abstract base class for HTTP middleware.
|
|
14
|
+
|
|
15
|
+
Subclasses must implement process_request() to handle the request/response cycle.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
class MyMiddleware(HttpMiddleware):
|
|
19
|
+
def process_request(self, request: Request) -> Response:
|
|
20
|
+
# Pre-processing
|
|
21
|
+
response = self.get_response(request)
|
|
22
|
+
# Post-processing
|
|
23
|
+
return response
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, get_response: Callable[[Request], Response]):
|
|
27
|
+
self.get_response = get_response
|
|
28
|
+
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def process_request(self, request: Request) -> Response:
|
|
31
|
+
"""Process the request and return a response. Must be implemented by subclasses."""
|
|
32
|
+
...
|