plain 0.70.0__py3-none-any.whl → 0.71.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 +17 -0
- plain/csrf/middleware.py +5 -5
- plain/forms/forms.py +2 -2
- plain/http/README.md +1 -1
- plain/http/__init__.py +4 -4
- plain/http/request.py +63 -39
- plain/internal/files/uploadhandler.py +4 -4
- plain/internal/handlers/base.py +6 -6
- plain/internal/handlers/exception.py +7 -7
- plain/internal/handlers/wsgi.py +3 -3
- plain/internal/middleware/headers.py +3 -3
- plain/internal/middleware/hosts.py +4 -4
- plain/internal/middleware/https.py +4 -4
- plain/internal/middleware/slash.py +5 -5
- plain/logs/utils.py +2 -2
- plain/runtime/__init__.py +3 -3
- plain/runtime/global_settings.py +5 -8
- plain/test/client.py +3 -3
- plain/views/base.py +5 -7
- {plain-0.70.0.dist-info → plain-0.71.0.dist-info}/METADATA +1 -1
- {plain-0.70.0.dist-info → plain-0.71.0.dist-info}/RECORD +24 -24
- {plain-0.70.0.dist-info → plain-0.71.0.dist-info}/WHEEL +0 -0
- {plain-0.70.0.dist-info → plain-0.71.0.dist-info}/entry_points.txt +0 -0
- {plain-0.70.0.dist-info → plain-0.71.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.71.0](https://github.com/dropseed/plain/releases/plain@0.71.0) (2025-09-30)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Renamed `HttpRequest` to `Request` throughout the codebase for consistency and simplicity ([cd46ff20](https://github.com/dropseed/plain/commit/cd46ff2003))
|
8
|
+
- Renamed `HttpHeaders` to `RequestHeaders` for naming consistency ([cd46ff20](https://github.com/dropseed/plain/commit/cd46ff2003))
|
9
|
+
- Renamed settings: `APP_NAME` → `NAME`, `APP_VERSION` → `VERSION`, `APP_LOG_LEVEL` → `LOG_LEVEL`, `APP_LOG_FORMAT` → `LOG_FORMAT`, `PLAIN_LOG_LEVEL` → `FRAMEWORK_LOG_LEVEL` ([4c5f2166](https://github.com/dropseed/plain/commit/4c5f2166c1))
|
10
|
+
- Added `request.get_preferred_type()` method to select the most preferred media type from Accept header ([b105ba4d](https://github.com/dropseed/plain/commit/b105ba4dd0))
|
11
|
+
- Moved helper functions in `http/request.py` to be static methods of `QueryDict` ([0e1b0133](https://github.com/dropseed/plain/commit/0e1b0133c5))
|
12
|
+
|
13
|
+
### Upgrade instructions
|
14
|
+
|
15
|
+
- Replace all imports and usage of `HttpRequest` with `Request`
|
16
|
+
- Replace all imports and usage of `HttpHeaders` with `RequestHeaders`
|
17
|
+
- Update any custom settings that reference `APP_NAME` to `NAME`, `APP_VERSION` to `VERSION`, `APP_LOG_LEVEL` to `LOG_LEVEL`, `APP_LOG_FORMAT` to `LOG_FORMAT`, and `PLAIN_LOG_LEVEL` to `FRAMEWORK_LOG_LEVEL`
|
18
|
+
- Configuring these settings via the `PLAIN_` prefixed environment variable will need to be updated accordingly
|
19
|
+
|
3
20
|
## [0.70.0](https://github.com/dropseed/plain/releases/plain@0.70.0) (2025-09-30)
|
4
21
|
|
5
22
|
### What's changed
|
plain/csrf/middleware.py
CHANGED
@@ -13,7 +13,7 @@ from .views import CsrfFailureView
|
|
13
13
|
|
14
14
|
if TYPE_CHECKING:
|
15
15
|
from plain.http import Response
|
16
|
-
from plain.http.request import
|
16
|
+
from plain.http.request import Request
|
17
17
|
|
18
18
|
logger = logging.getLogger("plain.security.csrf")
|
19
19
|
|
@@ -27,7 +27,7 @@ class CsrfViewMiddleware:
|
|
27
27
|
like subdomains can have different trust levels and are rejected.
|
28
28
|
"""
|
29
29
|
|
30
|
-
def __init__(self, get_response: Callable[[
|
30
|
+
def __init__(self, get_response: Callable[[Request], Response]):
|
31
31
|
self.get_response = get_response
|
32
32
|
|
33
33
|
# Compile CSRF exempt patterns once for performance
|
@@ -35,7 +35,7 @@ class CsrfViewMiddleware:
|
|
35
35
|
re.compile(r) for r in settings.CSRF_EXEMPT_PATHS
|
36
36
|
]
|
37
37
|
|
38
|
-
def __call__(self, request:
|
38
|
+
def __call__(self, request: Request) -> Response:
|
39
39
|
allowed, reason = self.should_allow_request(request)
|
40
40
|
|
41
41
|
if allowed:
|
@@ -43,7 +43,7 @@ class CsrfViewMiddleware:
|
|
43
43
|
else:
|
44
44
|
return self.reject(request, reason)
|
45
45
|
|
46
|
-
def should_allow_request(self, request:
|
46
|
+
def should_allow_request(self, request: Request) -> tuple[bool, str]:
|
47
47
|
# 1. Allow safe methods (GET, HEAD, OPTIONS)
|
48
48
|
if request.method in ("GET", "HEAD", "OPTIONS"):
|
49
49
|
return True, f"Safe HTTP method: {request.method}"
|
@@ -128,7 +128,7 @@ class CsrfViewMiddleware:
|
|
128
128
|
f"Cross-origin request detected - Origin {origin} does not match Host",
|
129
129
|
)
|
130
130
|
|
131
|
-
def reject(self, request:
|
131
|
+
def reject(self, request: Request, reason: str) -> Response:
|
132
132
|
"""Reject a request with a 403 Forbidden response."""
|
133
133
|
|
134
134
|
response = CsrfFailureView.as_view()(request, reason=reason)
|
plain/forms/forms.py
CHANGED
@@ -14,7 +14,7 @@ from .exceptions import ValidationError
|
|
14
14
|
from .fields import Field, FileField
|
15
15
|
|
16
16
|
if TYPE_CHECKING:
|
17
|
-
from plain.http import
|
17
|
+
from plain.http import Request
|
18
18
|
|
19
19
|
from .boundfield import BoundField
|
20
20
|
|
@@ -70,7 +70,7 @@ class BaseForm:
|
|
70
70
|
def __init__(
|
71
71
|
self,
|
72
72
|
*,
|
73
|
-
request:
|
73
|
+
request: Request,
|
74
74
|
auto_id: str | bool = "id_%s",
|
75
75
|
prefix: str | None = None,
|
76
76
|
initial: dict[str, Any] | None = None,
|
plain/http/README.md
CHANGED
@@ -6,7 +6,7 @@
|
|
6
6
|
|
7
7
|
## Overview
|
8
8
|
|
9
|
-
Typically you will interact with [
|
9
|
+
Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
|
10
10
|
|
11
11
|
```python
|
12
12
|
from plain.views import View
|
plain/http/__init__.py
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
from plain.http.cookie import parse_cookie
|
2
2
|
from plain.http.request import (
|
3
|
-
HttpHeaders,
|
4
|
-
HttpRequest,
|
5
3
|
QueryDict,
|
6
4
|
RawPostDataException,
|
5
|
+
Request,
|
6
|
+
RequestHeaders,
|
7
7
|
UnreadablePostError,
|
8
8
|
)
|
9
9
|
from plain.http.response import (
|
@@ -26,8 +26,8 @@ from plain.http.response import (
|
|
26
26
|
|
27
27
|
__all__ = [
|
28
28
|
"parse_cookie",
|
29
|
-
"
|
30
|
-
"
|
29
|
+
"Request",
|
30
|
+
"RequestHeaders",
|
31
31
|
"QueryDict",
|
32
32
|
"RawPostDataException",
|
33
33
|
"UnreadablePostError",
|
plain/http/request.py
CHANGED
@@ -47,7 +47,7 @@ class RawPostDataException(Exception):
|
|
47
47
|
pass
|
48
48
|
|
49
49
|
|
50
|
-
class
|
50
|
+
class Request:
|
51
51
|
"""A basic HTTP request."""
|
52
52
|
|
53
53
|
# The encoding used in GET/POST dicts. None means use default setting.
|
@@ -89,7 +89,7 @@ class HttpRequest:
|
|
89
89
|
del obj_dict[attr]
|
90
90
|
return obj_dict
|
91
91
|
|
92
|
-
def __deepcopy__(self, memo: dict[int, Any]) ->
|
92
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> Request:
|
93
93
|
obj = copy.copy(self)
|
94
94
|
for attr in self.non_picklable_attrs:
|
95
95
|
if hasattr(self, attr):
|
@@ -98,18 +98,41 @@ class HttpRequest:
|
|
98
98
|
return obj
|
99
99
|
|
100
100
|
@cached_property
|
101
|
-
def headers(self) ->
|
102
|
-
return
|
101
|
+
def headers(self) -> RequestHeaders:
|
102
|
+
return RequestHeaders(self.meta)
|
103
103
|
|
104
104
|
@cached_property
|
105
105
|
def accepted_types(self) -> list[MediaType]:
|
106
|
-
"""Return
|
107
|
-
|
106
|
+
"""Return accepted media types sorted by quality value (highest first).
|
107
|
+
|
108
|
+
When quality values are equal, the original order from the Accept header
|
109
|
+
is preserved (as per HTTP spec).
|
110
|
+
"""
|
111
|
+
header = self.headers.get("Accept", "*/*")
|
112
|
+
types = [MediaType(token) for token in header.split(",") if token.strip()]
|
113
|
+
return sorted(types, key=lambda t: t.quality, reverse=True)
|
114
|
+
|
115
|
+
def get_preferred_type(self, *media_types: str) -> str | None:
|
116
|
+
"""Return the most preferred media type from the given options.
|
117
|
+
|
118
|
+
Checks the Accept header in priority order (by quality value) and returns
|
119
|
+
the first matching media type from the provided options.
|
120
|
+
|
121
|
+
Returns None if none of the options are accepted.
|
122
|
+
|
123
|
+
Example:
|
124
|
+
# Accept: text/html;q=1.0, application/json;q=0.5
|
125
|
+
request.get_preferred_type("application/json", "text/html") # Returns "text/html"
|
126
|
+
"""
|
127
|
+
for accepted in self.accepted_types:
|
128
|
+
for option in media_types:
|
129
|
+
if accepted.match(option):
|
130
|
+
return option
|
131
|
+
return None
|
108
132
|
|
109
133
|
def accepts(self, media_type: str) -> bool:
|
110
|
-
|
111
|
-
|
112
|
-
)
|
134
|
+
"""Check if the given media type is accepted."""
|
135
|
+
return self.get_preferred_type(media_type) is not None
|
113
136
|
|
114
137
|
def _set_content_type_params(self, meta: dict[str, Any]) -> None:
|
115
138
|
"""Set content_type, content_params, and encoding."""
|
@@ -432,7 +455,7 @@ class HttpRequest:
|
|
432
455
|
return unsign_cookie_value(key, cookie_value, salt, max_age, default)
|
433
456
|
|
434
457
|
|
435
|
-
class
|
458
|
+
class RequestHeaders(CaseInsensitiveMapping):
|
436
459
|
HTTP_PREFIX = "HTTP_"
|
437
460
|
# PEP 333 gives two headers which aren't prepended with HTTP_.
|
438
461
|
UNPREFIXED_HEADERS = {"CONTENT_TYPE", "CONTENT_LENGTH"}
|
@@ -563,8 +586,8 @@ class QueryDict(MultiValueDict):
|
|
563
586
|
|
564
587
|
def __setitem__(self, key: str, value: Any) -> None:
|
565
588
|
self._assert_mutable()
|
566
|
-
key = bytes_to_text(key, self.encoding)
|
567
|
-
value = bytes_to_text(value, self.encoding)
|
589
|
+
key = self.bytes_to_text(key, self.encoding)
|
590
|
+
value = self.bytes_to_text(value, self.encoding)
|
568
591
|
super().__setitem__(key, value)
|
569
592
|
|
570
593
|
def __delitem__(self, key: str) -> None:
|
@@ -586,8 +609,8 @@ class QueryDict(MultiValueDict):
|
|
586
609
|
|
587
610
|
def setlist(self, key: str, list_: list[Any]) -> None:
|
588
611
|
self._assert_mutable()
|
589
|
-
key = bytes_to_text(key, self.encoding)
|
590
|
-
list_ = [bytes_to_text(elt, self.encoding) for elt in list_]
|
612
|
+
key = self.bytes_to_text(key, self.encoding)
|
613
|
+
list_ = [self.bytes_to_text(elt, self.encoding) for elt in list_]
|
591
614
|
super().setlist(key, list_)
|
592
615
|
|
593
616
|
def setlistdefault(
|
@@ -598,8 +621,8 @@ class QueryDict(MultiValueDict):
|
|
598
621
|
|
599
622
|
def appendlist(self, key: str, value: Any) -> None:
|
600
623
|
self._assert_mutable()
|
601
|
-
key = bytes_to_text(key, self.encoding)
|
602
|
-
value = bytes_to_text(value, self.encoding)
|
624
|
+
key = self.bytes_to_text(key, self.encoding)
|
625
|
+
value = self.bytes_to_text(value, self.encoding)
|
603
626
|
super().appendlist(key, value)
|
604
627
|
|
605
628
|
def pop(self, key: str, *args: Any) -> Any:
|
@@ -616,8 +639,8 @@ class QueryDict(MultiValueDict):
|
|
616
639
|
|
617
640
|
def setdefault(self, key: str, default: Any = None) -> Any:
|
618
641
|
self._assert_mutable()
|
619
|
-
key = bytes_to_text(key, self.encoding)
|
620
|
-
default = bytes_to_text(default, self.encoding)
|
642
|
+
key = self.bytes_to_text(key, self.encoding)
|
643
|
+
default = self.bytes_to_text(default, self.encoding)
|
621
644
|
return super().setdefault(key, default)
|
622
645
|
|
623
646
|
def copy(self) -> QueryDict:
|
@@ -656,6 +679,23 @@ class QueryDict(MultiValueDict):
|
|
656
679
|
)
|
657
680
|
return "&".join(output)
|
658
681
|
|
682
|
+
# It's neither necessary nor appropriate to use
|
683
|
+
# plain.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
|
684
|
+
# this slightly more restricted function, used by QueryDict.
|
685
|
+
@staticmethod
|
686
|
+
def bytes_to_text(s: Any, encoding: str) -> str:
|
687
|
+
"""
|
688
|
+
Convert bytes objects to strings, using the given encoding. Illegally
|
689
|
+
encoded input characters are replaced with Unicode "unknown" codepoint
|
690
|
+
(\ufffd).
|
691
|
+
|
692
|
+
Return any non-bytes objects without change.
|
693
|
+
"""
|
694
|
+
if isinstance(s, bytes):
|
695
|
+
return str(s, encoding, "replace")
|
696
|
+
else:
|
697
|
+
return s
|
698
|
+
|
659
699
|
|
660
700
|
class MediaType:
|
661
701
|
def __init__(self, media_type_raw_line: str | MediaType):
|
@@ -678,6 +718,11 @@ class MediaType:
|
|
678
718
|
def is_all_types(self) -> bool:
|
679
719
|
return self.main_type == "*" and self.sub_type == "*"
|
680
720
|
|
721
|
+
@property
|
722
|
+
def quality(self) -> float:
|
723
|
+
"""Return the quality value from the Accept header (default 1.0)."""
|
724
|
+
return float(self.params.get("q", 1.0))
|
725
|
+
|
681
726
|
def match(self, other: str | MediaType) -> bool:
|
682
727
|
if self.is_all_types:
|
683
728
|
return True
|
@@ -685,24 +730,3 @@ class MediaType:
|
|
685
730
|
if self.main_type == other.main_type and self.sub_type in {"*", other.sub_type}:
|
686
731
|
return True
|
687
732
|
return False
|
688
|
-
|
689
|
-
|
690
|
-
# It's neither necessary nor appropriate to use
|
691
|
-
# plain.utils.encoding.force_str() for parsing URLs and form inputs. Thus,
|
692
|
-
# this slightly more restricted function, used by QueryDict.
|
693
|
-
def bytes_to_text(s: Any, encoding: str) -> str:
|
694
|
-
"""
|
695
|
-
Convert bytes objects to strings, using the given encoding. Illegally
|
696
|
-
encoded input characters are replaced with Unicode "unknown" codepoint
|
697
|
-
(\ufffd).
|
698
|
-
|
699
|
-
Return any non-bytes objects without change.
|
700
|
-
"""
|
701
|
-
if isinstance(s, bytes):
|
702
|
-
return str(s, encoding, "replace")
|
703
|
-
else:
|
704
|
-
return s
|
705
|
-
|
706
|
-
|
707
|
-
def parse_accept_header(header: str) -> list[MediaType]:
|
708
|
-
return [MediaType(token) for token in header.split(",") if token.strip()]
|
@@ -19,7 +19,7 @@ from plain.utils.module_loading import import_string
|
|
19
19
|
if TYPE_CHECKING:
|
20
20
|
from typing import Any
|
21
21
|
|
22
|
-
from plain.http import
|
22
|
+
from plain.http import Request
|
23
23
|
|
24
24
|
__all__ = [
|
25
25
|
"UploadFileException",
|
@@ -85,7 +85,7 @@ class FileUploadHandler:
|
|
85
85
|
|
86
86
|
chunk_size = 64 * 2**10 # : The default chunk size is 64 KB.
|
87
87
|
|
88
|
-
def __init__(self, request:
|
88
|
+
def __init__(self, request: Request | None = None) -> None:
|
89
89
|
self.file_name = None
|
90
90
|
self.content_type = None
|
91
91
|
self.content_length = None
|
@@ -265,8 +265,8 @@ def load_handler(path: str, *args: Any, **kwargs: Any) -> FileUploadHandler:
|
|
265
265
|
Given a path to a handler, return an instance of that handler.
|
266
266
|
|
267
267
|
E.g.::
|
268
|
-
>>> from plain.http import
|
269
|
-
>>> request =
|
268
|
+
>>> from plain.http import Request
|
269
|
+
>>> request = Request()
|
270
270
|
>>> load_handler(
|
271
271
|
... 'plain.internal.files.uploadhandler.TemporaryFileUploadHandler',
|
272
272
|
... request,
|
plain/internal/handlers/base.py
CHANGED
@@ -18,7 +18,7 @@ from .exception import convert_exception_to_response
|
|
18
18
|
if TYPE_CHECKING:
|
19
19
|
from collections.abc import Callable
|
20
20
|
|
21
|
-
from plain.http import
|
21
|
+
from plain.http import Request, Response
|
22
22
|
from plain.urls import ResolverMatch
|
23
23
|
|
24
24
|
logger = logging.getLogger("plain.request")
|
@@ -43,7 +43,7 @@ tracer = trace.get_tracer("plain")
|
|
43
43
|
|
44
44
|
|
45
45
|
class BaseHandler:
|
46
|
-
_middleware_chain: Callable[[
|
46
|
+
_middleware_chain: Callable[[Request], Response] | None = None
|
47
47
|
|
48
48
|
def load_middleware(self) -> None:
|
49
49
|
"""
|
@@ -72,8 +72,8 @@ class BaseHandler:
|
|
72
72
|
# as a flag for initialization being complete.
|
73
73
|
self._middleware_chain = handler
|
74
74
|
|
75
|
-
def get_response(self, request:
|
76
|
-
"""Return a Response object for the given
|
75
|
+
def get_response(self, request: Request) -> Response:
|
76
|
+
"""Return a Response object for the given Request."""
|
77
77
|
|
78
78
|
span_attributes = {
|
79
79
|
"plain.request.id": request.unique_id,
|
@@ -124,7 +124,7 @@ class BaseHandler:
|
|
124
124
|
)
|
125
125
|
return response
|
126
126
|
|
127
|
-
def _get_response(self, request:
|
127
|
+
def _get_response(self, request: Request) -> Response:
|
128
128
|
"""
|
129
129
|
Resolve and call the view, then apply view, exception, and
|
130
130
|
template_response middleware. This method is everything that happens
|
@@ -141,7 +141,7 @@ class BaseHandler:
|
|
141
141
|
|
142
142
|
return response
|
143
143
|
|
144
|
-
def resolve_request(self, request:
|
144
|
+
def resolve_request(self, request: Request) -> ResolverMatch:
|
145
145
|
"""
|
146
146
|
Retrieve/set the urlrouter for the request. Return the view resolved,
|
147
147
|
with its args and kwargs.
|
@@ -23,12 +23,12 @@ from plain.views.errors import ErrorView
|
|
23
23
|
if TYPE_CHECKING:
|
24
24
|
from collections.abc import Callable
|
25
25
|
|
26
|
-
from plain.http import
|
26
|
+
from plain.http import Request, Response
|
27
27
|
|
28
28
|
|
29
29
|
def convert_exception_to_response(
|
30
|
-
get_response: Callable[[
|
31
|
-
) -> Callable[[
|
30
|
+
get_response: Callable[[Request], Response],
|
31
|
+
) -> Callable[[Request], Response]:
|
32
32
|
"""
|
33
33
|
Wrap the given get_response callable in exception-to-response conversion.
|
34
34
|
|
@@ -43,7 +43,7 @@ def convert_exception_to_response(
|
|
43
43
|
"""
|
44
44
|
|
45
45
|
@wraps(get_response)
|
46
|
-
def inner(request:
|
46
|
+
def inner(request: Request) -> Response:
|
47
47
|
try:
|
48
48
|
response = get_response(request)
|
49
49
|
except Exception as exc:
|
@@ -53,7 +53,7 @@ def convert_exception_to_response(
|
|
53
53
|
return inner
|
54
54
|
|
55
55
|
|
56
|
-
def response_for_exception(request:
|
56
|
+
def response_for_exception(request: Request, exc: Exception) -> Response:
|
57
57
|
if isinstance(exc, Http404):
|
58
58
|
response = get_exception_response(
|
59
59
|
request=request, status_code=404, exception=None
|
@@ -131,7 +131,7 @@ def response_for_exception(request: HttpRequest, exc: Exception) -> Response:
|
|
131
131
|
|
132
132
|
|
133
133
|
def get_exception_response(
|
134
|
-
*, request:
|
134
|
+
*, request: Request, status_code: int, exception: Exception | None
|
135
135
|
) -> Response:
|
136
136
|
try:
|
137
137
|
view_class = get_error_view(status_code=status_code, exception=exception)
|
@@ -149,7 +149,7 @@ def get_exception_response(
|
|
149
149
|
|
150
150
|
def get_error_view(
|
151
151
|
*, status_code: int, exception: Exception | None
|
152
|
-
) -> Callable[[
|
152
|
+
) -> Callable[[Request], Response]:
|
153
153
|
views_by_status = settings.HTTP_ERROR_VIEWS
|
154
154
|
if status_code in views_by_status:
|
155
155
|
view = views_by_status[status_code]
|
plain/internal/handlers/wsgi.py
CHANGED
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|
7
7
|
from urllib.parse import quote
|
8
8
|
|
9
9
|
from plain import signals
|
10
|
-
from plain.http import
|
10
|
+
from plain.http import QueryDict, Request, parse_cookie
|
11
11
|
from plain.internal.handlers import base
|
12
12
|
from plain.utils.datastructures import MultiValueDict
|
13
13
|
from plain.utils.regex_helper import _lazy_re_compile
|
@@ -60,8 +60,8 @@ class LimitedStream(IOBase):
|
|
60
60
|
return line
|
61
61
|
|
62
62
|
|
63
|
-
class WSGIRequest(
|
64
|
-
non_picklable_attrs =
|
63
|
+
class WSGIRequest(Request):
|
64
|
+
non_picklable_attrs = Request.non_picklable_attrs | frozenset(["environ"])
|
65
65
|
meta_non_picklable_attrs = frozenset(["wsgi.errors", "wsgi.input"])
|
66
66
|
|
67
67
|
def __init__(self, environ: dict[str, Any]) -> None:
|
@@ -7,14 +7,14 @@ from plain.runtime import settings
|
|
7
7
|
if TYPE_CHECKING:
|
8
8
|
from collections.abc import Callable
|
9
9
|
|
10
|
-
from plain.http import
|
10
|
+
from plain.http import Request, Response
|
11
11
|
|
12
12
|
|
13
13
|
class DefaultHeadersMiddleware:
|
14
|
-
def __init__(self, get_response: Callable[[
|
14
|
+
def __init__(self, get_response: Callable[[Request], Response]) -> None:
|
15
15
|
self.get_response = get_response
|
16
16
|
|
17
|
-
def __call__(self, request:
|
17
|
+
def __call__(self, request: Request) -> Response:
|
18
18
|
response = self.get_response(request)
|
19
19
|
|
20
20
|
for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
|
@@ -4,7 +4,7 @@ import ipaddress
|
|
4
4
|
import logging
|
5
5
|
from typing import TYPE_CHECKING
|
6
6
|
|
7
|
-
from plain.http import
|
7
|
+
from plain.http import Request, ResponseBadRequest
|
8
8
|
from plain.runtime import settings
|
9
9
|
from plain.utils.regex_helper import _lazy_re_compile
|
10
10
|
|
@@ -29,10 +29,10 @@ class HostValidationMiddleware:
|
|
29
29
|
host is not allowed.
|
30
30
|
"""
|
31
31
|
|
32
|
-
def __init__(self, get_response: Callable[[
|
32
|
+
def __init__(self, get_response: Callable[[Request], Response]) -> None:
|
33
33
|
self.get_response = get_response
|
34
34
|
|
35
|
-
def __call__(self, request:
|
35
|
+
def __call__(self, request: Request) -> Response:
|
36
36
|
if not is_host_valid(request):
|
37
37
|
host = request.host
|
38
38
|
msg = f"Invalid HTTP_HOST header: {host!r}."
|
@@ -55,7 +55,7 @@ class HostValidationMiddleware:
|
|
55
55
|
return self.get_response(request)
|
56
56
|
|
57
57
|
|
58
|
-
def is_host_valid(request:
|
58
|
+
def is_host_valid(request: Request) -> bool:
|
59
59
|
"""
|
60
60
|
Check if the host is valid according to ALLOWED_HOSTS settings.
|
61
61
|
"""
|
@@ -8,17 +8,17 @@ from plain.runtime import settings
|
|
8
8
|
if TYPE_CHECKING:
|
9
9
|
from collections.abc import Callable
|
10
10
|
|
11
|
-
from plain.http import
|
11
|
+
from plain.http import Request, Response
|
12
12
|
|
13
13
|
|
14
14
|
class HttpsRedirectMiddleware:
|
15
|
-
def __init__(self, get_response: Callable[[
|
15
|
+
def __init__(self, get_response: Callable[[Request], Response]) -> None:
|
16
16
|
self.get_response = get_response
|
17
17
|
|
18
18
|
# Settings for HTTPS
|
19
19
|
self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
|
20
20
|
|
21
|
-
def __call__(self, request:
|
21
|
+
def __call__(self, request: Request) -> Response:
|
22
22
|
"""
|
23
23
|
Perform a blanket HTTP→HTTPS redirect when enabled.
|
24
24
|
"""
|
@@ -28,7 +28,7 @@ class HttpsRedirectMiddleware:
|
|
28
28
|
|
29
29
|
return self.get_response(request)
|
30
30
|
|
31
|
-
def maybe_https_redirect(self, request:
|
31
|
+
def maybe_https_redirect(self, request: Request) -> Response | None:
|
32
32
|
if self.https_redirect_enabled and not request.is_https():
|
33
33
|
return ResponseRedirect(
|
34
34
|
f"https://{request.host}{request.get_full_path()}", status_code=301
|
@@ -10,15 +10,15 @@ from plain.utils.http import escape_leading_slashes
|
|
10
10
|
if TYPE_CHECKING:
|
11
11
|
from collections.abc import Callable
|
12
12
|
|
13
|
-
from plain.http import
|
13
|
+
from plain.http import Request, Response
|
14
14
|
from plain.urls import ResolverMatch
|
15
15
|
|
16
16
|
|
17
17
|
class RedirectSlashMiddleware:
|
18
|
-
def __init__(self, get_response: Callable[[
|
18
|
+
def __init__(self, get_response: Callable[[Request], Response]) -> None:
|
19
19
|
self.get_response = get_response
|
20
20
|
|
21
|
-
def __call__(self, request:
|
21
|
+
def __call__(self, request: Request) -> Response:
|
22
22
|
"""
|
23
23
|
Rewrite the URL based on settings.APPEND_SLASH
|
24
24
|
"""
|
@@ -50,7 +50,7 @@ class RedirectSlashMiddleware:
|
|
50
50
|
except Resolver404:
|
51
51
|
return False
|
52
52
|
|
53
|
-
def should_redirect_with_slash(self, request:
|
53
|
+
def should_redirect_with_slash(self, request: Request) -> ResolverMatch | bool:
|
54
54
|
"""
|
55
55
|
Return True if settings.APPEND_SLASH is True and appending a slash to
|
56
56
|
the request path turns an invalid path into a valid one.
|
@@ -60,7 +60,7 @@ class RedirectSlashMiddleware:
|
|
60
60
|
return self._is_valid_path(f"{request.path_info}/")
|
61
61
|
return False
|
62
62
|
|
63
|
-
def get_full_path_with_slash(self, request:
|
63
|
+
def get_full_path_with_slash(self, request: Request) -> str:
|
64
64
|
"""
|
65
65
|
Return the full path of the request with a trailing slash appended.
|
66
66
|
|
plain/logs/utils.py
CHANGED
@@ -4,7 +4,7 @@ import logging
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
5
5
|
|
6
6
|
if TYPE_CHECKING:
|
7
|
-
from plain.http.request import
|
7
|
+
from plain.http.request import Request
|
8
8
|
from plain.http.response import ResponseBase
|
9
9
|
|
10
10
|
request_logger = logging.getLogger("plain.request")
|
@@ -14,7 +14,7 @@ def log_response(
|
|
14
14
|
message: str,
|
15
15
|
*args: Any,
|
16
16
|
response: ResponseBase | None = None,
|
17
|
-
request:
|
17
|
+
request: Request | None = None,
|
18
18
|
logger: logging.Logger = request_logger,
|
19
19
|
level: str | None = None,
|
20
20
|
exception: BaseException | None = None,
|
plain/runtime/__init__.py
CHANGED
@@ -64,9 +64,9 @@ def setup() -> None:
|
|
64
64
|
sys.path.insert(0, APP_PATH.parent.as_posix())
|
65
65
|
|
66
66
|
configure_logging(
|
67
|
-
plain_log_level=settings.
|
68
|
-
app_log_level=settings.
|
69
|
-
app_log_format=settings.
|
67
|
+
plain_log_level=settings.FRAMEWORK_LOG_LEVEL,
|
68
|
+
app_log_level=settings.LOG_LEVEL,
|
69
|
+
app_log_format=settings.LOG_FORMAT,
|
70
70
|
)
|
71
71
|
|
72
72
|
packages_registry.populate(settings.INSTALLED_PACKAGES)
|
plain/runtime/global_settings.py
CHANGED
@@ -3,8 +3,6 @@ Default Plain settings. Override these with settings in the module pointed to
|
|
3
3
|
by the PLAIN_SETTINGS_MODULE environment variable.
|
4
4
|
"""
|
5
5
|
|
6
|
-
from os import environ
|
7
|
-
|
8
6
|
from .utils import get_app_info_from_pyproject
|
9
7
|
|
10
8
|
# MARK: Core Settings
|
@@ -12,8 +10,8 @@ from .utils import get_app_info_from_pyproject
|
|
12
10
|
DEBUG: bool = False
|
13
11
|
|
14
12
|
name, version = get_app_info_from_pyproject()
|
15
|
-
|
16
|
-
|
13
|
+
NAME: str = name
|
14
|
+
VERSION: str = version
|
17
15
|
|
18
16
|
# List of strings representing installed packages.
|
19
17
|
INSTALLED_PACKAGES: list[str] = []
|
@@ -135,11 +133,10 @@ CSRF_TRUSTED_ORIGINS: list[str] = []
|
|
135
133
|
CSRF_EXEMPT_PATHS: list[str] = []
|
136
134
|
|
137
135
|
# MARK: Logging
|
138
|
-
# (Uses some custom env names in addition to PLAIN_ prefixed )
|
139
136
|
|
140
|
-
|
141
|
-
|
142
|
-
|
137
|
+
FRAMEWORK_LOG_LEVEL: str = "INFO"
|
138
|
+
LOG_LEVEL: str = "INFO"
|
139
|
+
LOG_FORMAT: str = "keyvalue"
|
143
140
|
|
144
141
|
# MARK: Assets
|
145
142
|
|
plain/test/client.py
CHANGED
@@ -9,7 +9,7 @@ from io import BytesIO, IOBase
|
|
9
9
|
from typing import TYPE_CHECKING, Any
|
10
10
|
from urllib.parse import unquote_to_bytes, urljoin, urlparse, urlsplit
|
11
11
|
|
12
|
-
from plain.http import
|
12
|
+
from plain.http import QueryDict, RequestHeaders
|
13
13
|
from plain.internal import internalcode
|
14
14
|
from plain.internal.handlers.base import BaseHandler
|
15
15
|
from plain.internal.handlers.wsgi import WSGIRequest
|
@@ -174,7 +174,7 @@ class RequestFactory:
|
|
174
174
|
self.cookies: SimpleCookie[str] = SimpleCookie()
|
175
175
|
self.errors = BytesIO()
|
176
176
|
if headers:
|
177
|
-
self.defaults.update(
|
177
|
+
self.defaults.update(RequestHeaders.to_wsgi_names(headers))
|
178
178
|
|
179
179
|
def _base_environ(self, **request: Any) -> dict[str, Any]:
|
180
180
|
"""
|
@@ -417,7 +417,7 @@ class RequestFactory:
|
|
417
417
|
}
|
418
418
|
)
|
419
419
|
if headers:
|
420
|
-
extra.update(
|
420
|
+
extra.update(RequestHeaders.to_wsgi_names(headers))
|
421
421
|
r.update(extra)
|
422
422
|
# If QUERY_STRING is absent or empty, we want to extract it from the URL.
|
423
423
|
if not r.get("QUERY_STRING"):
|
plain/views/base.py
CHANGED
@@ -12,8 +12,8 @@ from opentelemetry.semconv._incubating.attributes.code_attributes import (
|
|
12
12
|
)
|
13
13
|
|
14
14
|
from plain.http import (
|
15
|
-
HttpRequest,
|
16
15
|
JsonResponse,
|
16
|
+
Request,
|
17
17
|
Response,
|
18
18
|
ResponseBase,
|
19
19
|
ResponseNotAllowed,
|
@@ -30,16 +30,14 @@ tracer = trace.get_tracer("plain")
|
|
30
30
|
|
31
31
|
|
32
32
|
class View:
|
33
|
-
request:
|
33
|
+
request: Request
|
34
34
|
url_args: tuple
|
35
35
|
url_kwargs: dict
|
36
36
|
|
37
37
|
# View.as_view(example="foo") usage can be customized by defining your own __init__ method.
|
38
38
|
# def __init__(self, *args, **kwargs):
|
39
39
|
|
40
|
-
def setup(
|
41
|
-
self, request: HttpRequest, *url_args: object, **url_kwargs: object
|
42
|
-
) -> None:
|
40
|
+
def setup(self, request: Request, *url_args: object, **url_kwargs: object) -> None:
|
43
41
|
if hasattr(self, "get") and not hasattr(self, "head"):
|
44
42
|
self.head = self.get
|
45
43
|
|
@@ -50,9 +48,9 @@ class View:
|
|
50
48
|
@classonlymethod
|
51
49
|
def as_view(
|
52
50
|
cls, *init_args: object, **init_kwargs: object
|
53
|
-
) -> Callable[[
|
51
|
+
) -> Callable[[Request, Any, Any], ResponseBase]:
|
54
52
|
def view(
|
55
|
-
request:
|
53
|
+
request: Request, *url_args: object, **url_kwargs: object
|
56
54
|
) -> ResponseBase:
|
57
55
|
with tracer.start_as_current_span(
|
58
56
|
f"{cls.__name__}",
|
@@ -1,5 +1,5 @@
|
|
1
1
|
plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
|
2
|
-
plain/CHANGELOG.md,sha256=
|
2
|
+
plain/CHANGELOG.md,sha256=JwyNoyCYpzq7iJuIM9DnJVDSAWjdhUKvoHVZFT_tlyQ,21390
|
3
3
|
plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
|
4
4
|
plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
|
5
5
|
plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
|
@@ -46,19 +46,19 @@ plain/cli/agent/md.py,sha256=eRspArtBVQNqxIii6J7sPPZqBjc-qmAF49aLnwZoWAE,2778
|
|
46
46
|
plain/cli/agent/prompt.py,sha256=hMb4RXiMF68xSSHC3gXrCUjv5jZa7flQzL4FZvUMu1I,1261
|
47
47
|
plain/cli/agent/request.py,sha256=Mz_9xehWmkaQKoi2vKwLxIJUHl6n-S2jl4pr6MrvcOg,6589
|
48
48
|
plain/csrf/README.md,sha256=ApWpB-qlEf0LkOKm9Yr-6f_lB9XJEvGFDo_fraw8ghI,2391
|
49
|
-
plain/csrf/middleware.py,sha256=
|
49
|
+
plain/csrf/middleware.py,sha256=p46QBXtJDrcCc5xwgZt7n-SIny4adShaFPTfBVXTNTM,5389
|
50
50
|
plain/csrf/views.py,sha256=ckQp-ZJCdtgFukJLHQDtKARBhng6AWOP-1ijsW5Ddqo,736
|
51
51
|
plain/forms/README.md,sha256=7MJQxNBoKkg0rW16qF6bGpUBxZrMrWjl2DZZk6gjzAU,2258
|
52
52
|
plain/forms/__init__.py,sha256=sK-Y1QC_7iueo1BSbYrGT0uq2LooB0U8_j5VksNyZ4c,236
|
53
53
|
plain/forms/boundfield.py,sha256=PEquPRn1BoVd4ZkIin8tjkBzRDMv-41wO8qHV0S1shg,1967
|
54
54
|
plain/forms/exceptions.py,sha256=MuMUF-33Qsc_HWk5zI3rcWzdvXzQbEOKAZvJKkbrL58,320
|
55
55
|
plain/forms/fields.py,sha256=GQSTI6-eaHivIXj3TQSw2LpyWMbN0NZ-SHjUO_oxqeA,37132
|
56
|
-
plain/forms/forms.py,sha256=
|
57
|
-
plain/http/README.md,sha256=
|
58
|
-
plain/http/__init__.py,sha256=
|
56
|
+
plain/forms/forms.py,sha256=GnJWzjIXOPByfrdiqjjo4xdKdkBw1gBD6Hq4mg73gHQ,11259
|
57
|
+
plain/http/README.md,sha256=32uuWbarAcG_qmP-Fltk-_26HFKfY3dBdlrO2FDb7D0,756
|
58
|
+
plain/http/__init__.py,sha256=jdMvhgalWf8OVkb0EGNLzuo6w4xAU8xDQwRIFvAyJsc,1001
|
59
59
|
plain/http/cookie.py,sha256=x13G3LIr0jxnPK1NQRptmi0DrAq9PsivQnQTm4LKaW0,2191
|
60
60
|
plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs,28350
|
61
|
-
plain/http/request.py,sha256=
|
61
|
+
plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
|
62
62
|
plain/http/response.py,sha256=efAJ2M_uwK8EYMXchOk-b0Jrx3Hukch_rPOW9nG5AV8,24842
|
63
63
|
plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
|
64
64
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
@@ -67,24 +67,24 @@ plain/internal/files/locks.py,sha256=jvLL9kroOo50kUo8dbuajDiFvgSL5NH6x5hudRPPjiQ
|
|
67
67
|
plain/internal/files/move.py,sha256=qE1nAVdJO8PJyxcZyoPORFYNPLf2EEt_dyOyk7cg9HE,3338
|
68
68
|
plain/internal/files/temp.py,sha256=gHpAtgRnTSx2NwHBnt73gfe-Uqmp3nkDBP2BINsRYqs,2909
|
69
69
|
plain/internal/files/uploadedfile.py,sha256=RaMeOMMB5LhH_QTEda9fGcI4kEg5CgCLE3kTgcdIJ18,4869
|
70
|
-
plain/internal/files/uploadhandler.py,sha256=
|
70
|
+
plain/internal/files/uploadhandler.py,sha256=zUEMePuCsoaukRMCy5yBvMHaeOBageUw2sLBHsXpmew,7982
|
71
71
|
plain/internal/files/utils.py,sha256=9aWCkGGGRdZLbI921IOgeUOD9zxx4FgpWCzXvq0lMXU,2871
|
72
72
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
73
|
-
plain/internal/handlers/base.py,sha256=
|
74
|
-
plain/internal/handlers/exception.py,sha256=
|
75
|
-
plain/internal/handlers/wsgi.py,sha256=
|
73
|
+
plain/internal/handlers/base.py,sha256=1xz-9esmx_-2mlUSEjtr55FXxjeExbVVpOdo88_2eAw,6529
|
74
|
+
plain/internal/handlers/exception.py,sha256=9Qf9dfQANuaeNx9-DMFJzg3Y3un61NicxfK7YnK3RTk,5226
|
75
|
+
plain/internal/handlers/wsgi.py,sha256=UnQ1wJSA6zIY8lWVZYlirtRui-VcjxXocbPgJOtG7KQ,8863
|
76
76
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
77
|
-
plain/internal/middleware/headers.py,sha256=
|
78
|
-
plain/internal/middleware/hosts.py,sha256
|
79
|
-
plain/internal/middleware/https.py,sha256=
|
80
|
-
plain/internal/middleware/slash.py,sha256=
|
77
|
+
plain/internal/middleware/headers.py,sha256=WM46oSTlmPsysbstOUoQLV8vsfGxbT9UpjYqEsw0FFQ,1200
|
78
|
+
plain/internal/middleware/hosts.py,sha256=veh42e1JRNwegP4dVx_urQroEC857YAKt3ThDHyr9Rc,6017
|
79
|
+
plain/internal/middleware/https.py,sha256=mB2fxOisij3HFL-N99Pa3gd3fI7ca49L4fxoQ96emEY,1088
|
80
|
+
plain/internal/middleware/slash.py,sha256=VwiMZ__L8gMT0zrpwCAoLEJy2-GhR8Czc6kQnmu8K3w,3195
|
81
81
|
plain/logs/README.md,sha256=rzOHfngjizLgXL21g0svC1Cdya2s_gBA_E-IljtHpy8,4069
|
82
82
|
plain/logs/__init__.py,sha256=gFVMcNn5D6z0JrvUJgGsOeYj1NKNtEXhw0MvPDtkN6w,58
|
83
83
|
plain/logs/configure.py,sha256=JWSTkQ1i1yu4sB-cXzxJwx2Pyax7sUEoOsTN_mj4te0,1363
|
84
84
|
plain/logs/debug.py,sha256=x2M4UcVexnn_5G0WCJd5iX6RFAGqEiRjE81dpMOqBBU,1336
|
85
85
|
plain/logs/formatters.py,sha256=1yNeA7RGa9Ao1TZRu20idQtPOIg43PZogFR-B5n7VnA,2503
|
86
86
|
plain/logs/loggers.py,sha256=FcvfVHbSLarY5IVsEoQcvNxeJkkoBz69a2a39rnyQ-4,5249
|
87
|
-
plain/logs/utils.py,sha256=
|
87
|
+
plain/logs/utils.py,sha256=BuHFynr9Oy8R7LzN3WycBvDY1lNX8tAxJ3TBsnchb0k,1628
|
88
88
|
plain/packages/README.md,sha256=iNqMtwFDVNf2TqKUzLKQW5Y4_GsssmdB4cVerzu27Ro,2674
|
89
89
|
plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
|
90
90
|
plain/packages/config.py,sha256=dxs_i-z6noQF_6j3lq11mhnQ1Bj10u2CXTJY0JgeQgc,3166
|
@@ -98,8 +98,8 @@ plain/preflight/results.py,sha256=OtqHUCdq0hNdwNxSLL4CEocNE4mDo2HqFN-XgxYsjLo,10
|
|
98
98
|
plain/preflight/security.py,sha256=nMbflO2LN49oKAAaa-tYvuweNB0RWv1z3w9niU037n0,3059
|
99
99
|
plain/preflight/urls.py,sha256=Asw_vq-70NRqr15yuBAYL0JCZ04liumORYT3I3KmF_k,437
|
100
100
|
plain/runtime/README.md,sha256=sTqXXJkckwqkk9O06XMMSNRokAYjrZBnB50JD36BsYI,4873
|
101
|
-
plain/runtime/__init__.py,sha256=
|
102
|
-
plain/runtime/global_settings.py,sha256=
|
101
|
+
plain/runtime/__init__.py,sha256=dvF5ipRckVf6LQgY4kdxE_dTlCdncuawQf3o5EqLq9k,2524
|
102
|
+
plain/runtime/global_settings.py,sha256=Q-bQP3tNnnuJZvfevGai639RIF_jhd7Dszt-DzTTz68,5509
|
103
103
|
plain/runtime/user_settings.py,sha256=Tbd0J6bxp18tKmFsQcdlxeFhUQU68PYtsswzQ2IcfNc,11467
|
104
104
|
plain/runtime/utils.py,sha256=sHOv9SWCalBtg32GtZofimM2XaQtf_jAyyf6RQuOlGc,851
|
105
105
|
plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
|
@@ -118,7 +118,7 @@ plain/templates/jinja/filters.py,sha256=g70cw1jzvYco2v-u4SeceOWBX_qxHI5k9AODMn8e
|
|
118
118
|
plain/templates/jinja/globals.py,sha256=TXl6uObqis_KXYP-jL3SvwqhATaoc7_hU8_fwpBMXyk,570
|
119
119
|
plain/test/README.md,sha256=tNzaVjma0sgowIrViJguCgVy8A2d8mUKApZO2RxTYyU,1140
|
120
120
|
plain/test/__init__.py,sha256=MhNHtp7MYBl9kq-pMRGY11kJ6kU1I6vOkjNkit1TYRg,94
|
121
|
-
plain/test/client.py,sha256=
|
121
|
+
plain/test/client.py,sha256=ZZ7sz1DsopkZrdjwL4Uv30bbjjbcOfa59PdH0tHTUkQ,29647
|
122
122
|
plain/test/encoding.py,sha256=txj_FCbC4GxH-JCkopW5LaZz8cGsrKQiculjFkjkzuY,3372
|
123
123
|
plain/test/exceptions.py,sha256=Cn4cauBelCiZPnbIXru-zKePXEQn-dit8M4v74C_dTk,492
|
124
124
|
plain/urls/README.md,sha256=026RkCK6I0GdqK3RE2QBLcCLIsiwtyKxgI2F0KBX95E,3882
|
@@ -155,15 +155,15 @@ plain/utils/timezone.py,sha256=M_I5yvs9NsHbtNBPJgHErvWw9vatzx4M96tRQs5gS3g,6823
|
|
155
155
|
plain/utils/tree.py,sha256=rj_JpZ2kVD3UExWoKnsRdVCoRjvzkuVOONcHzREjSyw,4766
|
156
156
|
plain/views/README.md,sha256=caUSKUhCSs5hdxHC5wIVzKkumPXiuNoOFRnIs3CUHfo,7215
|
157
157
|
plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
|
158
|
-
plain/views/base.py,sha256=
|
158
|
+
plain/views/base.py,sha256=fk9zAY5BMVBeM45dWL7A9BMTdUi6eTFMeVDd5kBVdv8,4478
|
159
159
|
plain/views/errors.py,sha256=tHD7MNnZcMyiQ46RMAnX1Ne3Zbbkr1zAiVfJyaaLtSQ,1447
|
160
160
|
plain/views/exceptions.py,sha256=-YKH1Jd9Zm_yXiz797PVjJB6VWaPCTXClHIUkG2fq78,198
|
161
161
|
plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
162
162
|
plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
|
163
163
|
plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
|
164
164
|
plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
|
165
|
-
plain-0.
|
166
|
-
plain-0.
|
167
|
-
plain-0.
|
168
|
-
plain-0.
|
169
|
-
plain-0.
|
165
|
+
plain-0.71.0.dist-info/METADATA,sha256=3WUR8T6I0LswhEptl-nLFOjLAFD65q0bVZFCxpBEz0Q,4488
|
166
|
+
plain-0.71.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
167
|
+
plain-0.71.0.dist-info/entry_points.txt,sha256=wvMzY-iREvfqRgyLm77htPp4j_8CQslLoZA15_AnNo8,171
|
168
|
+
plain-0.71.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
169
|
+
plain-0.71.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|