plain 0.39.0__py3-none-any.whl → 0.40.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/chores/README.md +64 -0
- plain/csrf/middleware.py +16 -16
- plain/forms/forms.py +5 -22
- plain/http/README.md +1 -1
- plain/http/multipartparser.py +6 -6
- plain/http/request.py +43 -40
- plain/internal/files/uploadhandler.py +4 -4
- plain/internal/handlers/wsgi.py +16 -16
- plain/runtime/global_settings.py +1 -1
- plain/views/redirect.py +1 -1
- plain/views/templates.py +1 -1
- {plain-0.39.0.dist-info → plain-0.40.0.dist-info}/METADATA +1 -1
- {plain-0.39.0.dist-info → plain-0.40.0.dist-info}/RECORD +16 -15
- {plain-0.39.0.dist-info → plain-0.40.0.dist-info}/WHEEL +0 -0
- {plain-0.39.0.dist-info → plain-0.40.0.dist-info}/entry_points.txt +0 -0
- {plain-0.39.0.dist-info → plain-0.40.0.dist-info}/licenses/LICENSE +0 -0
plain/chores/README.md
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Chores
|
2
|
+
|
3
|
+
**Routine maintenance tasks.**
|
4
|
+
|
5
|
+
Chores are registered functions that can be run at any time to keep an app in a desirable state.
|
6
|
+
|
7
|
+

|
8
|
+
|
9
|
+
A good example is the clearing of expired sessions in [`plain.sessions`](/plain-sessions/plain/sessions/chores.py) — since the sessions are stored in the database, occasionally you will want to delete any sessions that are expired and no longer in use.
|
10
|
+
|
11
|
+
```python
|
12
|
+
# plain/sessions/chores.py
|
13
|
+
from plain.chores import register_chore
|
14
|
+
from plain.utils import timezone
|
15
|
+
|
16
|
+
from .models import Session
|
17
|
+
|
18
|
+
|
19
|
+
@register_chore("sessions")
|
20
|
+
def clear_expired():
|
21
|
+
"""
|
22
|
+
Delete sessions that have expired.
|
23
|
+
"""
|
24
|
+
result = Session.objects.filter(expires_at__lt=timezone.now()).delete()
|
25
|
+
return f"{result[0]} expired sessions deleted"
|
26
|
+
```
|
27
|
+
|
28
|
+
## Running chores
|
29
|
+
|
30
|
+
The `plain chores run` command will execute all registered chores. When and how to run this is up to the user, but running them hourly is a safe assumption in most cases (assuming you have any chores — `plain chores list`).
|
31
|
+
|
32
|
+
There are several ways you can run chores depending on your needs:
|
33
|
+
|
34
|
+
- on deploy
|
35
|
+
- as a [`plain.worker` scheduled job](/plain-worker/plain/worker/README.md#scheduled-jobs)
|
36
|
+
- as a cron job (using any cron-like system where your app is hosted)
|
37
|
+
- manually as needed
|
38
|
+
|
39
|
+
## Writing chores
|
40
|
+
|
41
|
+
A chore is a function decorated with `@register_chore(chore_group_name)`. It can write a description as a docstring, and it can return a value that will be printed when the chore is run.
|
42
|
+
|
43
|
+
```python
|
44
|
+
# app/chores.py
|
45
|
+
from plain.chores import register_chore
|
46
|
+
|
47
|
+
|
48
|
+
@register_chore("app")
|
49
|
+
def chore_name():
|
50
|
+
"""
|
51
|
+
A chore description can go here
|
52
|
+
"""
|
53
|
+
# Do a thing!
|
54
|
+
return "We did it!"
|
55
|
+
```
|
56
|
+
|
57
|
+
A good chore is:
|
58
|
+
|
59
|
+
- Fast
|
60
|
+
- Idempotent
|
61
|
+
- Recurring
|
62
|
+
- Stateless
|
63
|
+
|
64
|
+
If chores are written in `app/chores.py` or `{pkg}/chores.py`, then they will be imported automatically and registered.
|
plain/csrf/middleware.py
CHANGED
@@ -74,9 +74,9 @@ def _unmask_cipher_token(token):
|
|
74
74
|
|
75
75
|
|
76
76
|
def _add_new_csrf_cookie(request):
|
77
|
-
"""Generate a new random CSRF_COOKIE value, and add it to request.
|
77
|
+
"""Generate a new random CSRF_COOKIE value, and add it to request.meta."""
|
78
78
|
csrf_secret = _get_new_csrf_string()
|
79
|
-
request.
|
79
|
+
request.meta.update(
|
80
80
|
{
|
81
81
|
"CSRF_COOKIE": csrf_secret,
|
82
82
|
"CSRF_COOKIE_NEEDS_UPDATE": True,
|
@@ -95,12 +95,12 @@ def get_token(request):
|
|
95
95
|
header to the outgoing response. For this reason, you may need to use this
|
96
96
|
function lazily, as is done by the csrf context processor.
|
97
97
|
"""
|
98
|
-
if "CSRF_COOKIE" in request.
|
99
|
-
csrf_secret = request.
|
98
|
+
if "CSRF_COOKIE" in request.meta:
|
99
|
+
csrf_secret = request.meta["CSRF_COOKIE"]
|
100
100
|
# Since the cookie is being used, flag to send the cookie in
|
101
101
|
# process_response() (even if the client already has it) in order to
|
102
102
|
# renew the expiry timer.
|
103
|
-
request.
|
103
|
+
request.meta["CSRF_COOKIE_NEEDS_UPDATE"] = True
|
104
104
|
else:
|
105
105
|
csrf_secret = _add_new_csrf_cookie(request)
|
106
106
|
return _mask_cipher_secret(csrf_secret)
|
@@ -177,14 +177,14 @@ class CsrfViewMiddleware:
|
|
177
177
|
# masked, this also causes it to be replaced with the unmasked
|
178
178
|
# form, but only in cases where the secret is already getting
|
179
179
|
# saved anyways.
|
180
|
-
request.
|
180
|
+
request.meta["CSRF_COOKIE"] = csrf_secret
|
181
181
|
|
182
182
|
if csrf_response := self._get_csrf_response(request):
|
183
183
|
return csrf_response
|
184
184
|
|
185
185
|
response = self.get_response(request)
|
186
186
|
|
187
|
-
if request.
|
187
|
+
if request.meta.get("CSRF_COOKIE_NEEDS_UPDATE"):
|
188
188
|
self._set_csrf_cookie(request, response)
|
189
189
|
# Unset the flag to prevent _set_csrf_cookie() from being
|
190
190
|
# unnecessarily called again in process_response() by other
|
@@ -193,7 +193,7 @@ class CsrfViewMiddleware:
|
|
193
193
|
# CSRF_COOKIE_NEEDS_UPDATE is still respected in subsequent calls
|
194
194
|
# e.g. in case rotate_token() is called in process_response() later
|
195
195
|
# by custom middleware but before those subsequent calls.
|
196
|
-
request.
|
196
|
+
request.meta["CSRF_COOKIE_NEEDS_UPDATE"] = False
|
197
197
|
|
198
198
|
return response
|
199
199
|
|
@@ -246,7 +246,7 @@ class CsrfViewMiddleware:
|
|
246
246
|
the request's secret has invalid characters or an invalid length.
|
247
247
|
"""
|
248
248
|
try:
|
249
|
-
csrf_secret = request.
|
249
|
+
csrf_secret = request.cookies[settings.CSRF_COOKIE_NAME]
|
250
250
|
except KeyError:
|
251
251
|
csrf_secret = None
|
252
252
|
else:
|
@@ -260,7 +260,7 @@ class CsrfViewMiddleware:
|
|
260
260
|
def _set_csrf_cookie(self, request, response):
|
261
261
|
response.set_cookie(
|
262
262
|
settings.CSRF_COOKIE_NAME,
|
263
|
-
request.
|
263
|
+
request.meta["CSRF_COOKIE"],
|
264
264
|
max_age=settings.CSRF_COOKIE_AGE,
|
265
265
|
domain=settings.CSRF_COOKIE_DOMAIN,
|
266
266
|
path=settings.CSRF_COOKIE_PATH,
|
@@ -272,7 +272,7 @@ class CsrfViewMiddleware:
|
|
272
272
|
patch_vary_headers(response, ("Cookie",))
|
273
273
|
|
274
274
|
def _origin_verified(self, request):
|
275
|
-
request_origin = request.
|
275
|
+
request_origin = request.meta["HTTP_ORIGIN"]
|
276
276
|
try:
|
277
277
|
good_host = request.get_host()
|
278
278
|
except DisallowedHost:
|
@@ -298,7 +298,7 @@ class CsrfViewMiddleware:
|
|
298
298
|
)
|
299
299
|
|
300
300
|
def _check_referer(self, request):
|
301
|
-
referer = request.
|
301
|
+
referer = request.meta.get("HTTP_REFERER")
|
302
302
|
if referer is None:
|
303
303
|
raise RejectRequest(REASON_NO_REFERER)
|
304
304
|
|
@@ -364,7 +364,7 @@ class CsrfViewMiddleware:
|
|
364
364
|
request_csrf_token = ""
|
365
365
|
if request.method == "POST":
|
366
366
|
try:
|
367
|
-
request_csrf_token = request.
|
367
|
+
request_csrf_token = request.data.get(settings.CSRF_FIELD_NAME, "")
|
368
368
|
except UnreadablePostError:
|
369
369
|
# Handle a broken connection before we've completed reading the
|
370
370
|
# POST data. process_view shouldn't raise any exceptions, so
|
@@ -398,7 +398,7 @@ class CsrfViewMiddleware:
|
|
398
398
|
raise RejectRequest(reason)
|
399
399
|
|
400
400
|
def _get_csrf_response(self, request):
|
401
|
-
# Wait until request.
|
401
|
+
# Wait until request.meta["CSRF_COOKIE"] has been manipulated before
|
402
402
|
# bailing out, so that get_token still works
|
403
403
|
if getattr(request, "csrf_exempt", True):
|
404
404
|
return None
|
@@ -409,10 +409,10 @@ class CsrfViewMiddleware:
|
|
409
409
|
|
410
410
|
# Reject the request if the Origin header doesn't match an allowed
|
411
411
|
# value.
|
412
|
-
if "HTTP_ORIGIN" in request.
|
412
|
+
if "HTTP_ORIGIN" in request.meta:
|
413
413
|
if not self._origin_verified(request):
|
414
414
|
return self._reject(
|
415
|
-
request, REASON_BAD_ORIGIN % request.
|
415
|
+
request, REASON_BAD_ORIGIN % request.meta["HTTP_ORIGIN"]
|
416
416
|
)
|
417
417
|
elif request.is_https():
|
418
418
|
# If the Origin header wasn't provided, reject HTTPS requests if
|
plain/forms/forms.py
CHANGED
@@ -3,10 +3,8 @@ Form classes
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
import copy
|
6
|
-
import json
|
7
6
|
|
8
7
|
from plain.exceptions import NON_FIELD_ERRORS
|
9
|
-
from plain.utils.datastructures import MultiValueDict
|
10
8
|
from plain.utils.functional import cached_property
|
11
9
|
|
12
10
|
from .exceptions import ValidationError
|
@@ -64,27 +62,12 @@ class BaseForm:
|
|
64
62
|
prefix=None,
|
65
63
|
initial=None,
|
66
64
|
):
|
67
|
-
|
68
|
-
|
69
|
-
self.data = json.loads(request.body)
|
70
|
-
self.is_json_request = True
|
71
|
-
elif request.headers.get("Content-Type") in [
|
72
|
-
"application/x-www-form-urlencoded",
|
73
|
-
"multipart/form-data",
|
74
|
-
]:
|
75
|
-
self.data = request.POST
|
76
|
-
self.is_json_request = False
|
77
|
-
else:
|
78
|
-
raise ValueError(
|
79
|
-
"Unsupported Content-Type. Supported types are "
|
80
|
-
"'application/json', 'application/x-www-form-urlencoded', "
|
81
|
-
"and 'multipart/form-data'."
|
82
|
-
)
|
83
|
-
else:
|
84
|
-
self.data = MultiValueDict()
|
85
|
-
self.is_json_request = False
|
65
|
+
self.data = request.data
|
66
|
+
self.files = request.files
|
86
67
|
|
87
|
-
self.
|
68
|
+
self.is_json_request = request.headers.get("Content-Type", "").startswith(
|
69
|
+
"application/json"
|
70
|
+
)
|
88
71
|
|
89
72
|
self.is_bound = bool(self.data or self.files)
|
90
73
|
|
plain/http/README.md
CHANGED
@@ -14,7 +14,7 @@ class ExampleView(View):
|
|
14
14
|
print(self.request.headers.get("Example-Header"))
|
15
15
|
|
16
16
|
# Accessing a query parameter
|
17
|
-
print(self.request.
|
17
|
+
print(self.request.query_params.get("example"))
|
18
18
|
|
19
19
|
# Creating a response
|
20
20
|
response = Response("Hello, world!", status_code=200)
|
plain/http/multipartparser.py
CHANGED
@@ -54,12 +54,12 @@ class MultiPartParser:
|
|
54
54
|
|
55
55
|
boundary_re = _lazy_re_compile(r"[ -~]{0,200}[!-~]")
|
56
56
|
|
57
|
-
def __init__(self,
|
57
|
+
def __init__(self, meta, input_data, upload_handlers, encoding=None):
|
58
58
|
"""
|
59
59
|
Initialize the MultiPartParser object.
|
60
60
|
|
61
|
-
:
|
62
|
-
The standard ``
|
61
|
+
:meta:
|
62
|
+
The standard ``meta`` dictionary in Plain request objects.
|
63
63
|
:input_data:
|
64
64
|
The raw post data, as a file-like object.
|
65
65
|
:upload_handlers:
|
@@ -69,7 +69,7 @@ class MultiPartParser:
|
|
69
69
|
The encoding with which to treat the incoming data.
|
70
70
|
"""
|
71
71
|
# Content-Type should contain multipart and the boundary information.
|
72
|
-
content_type =
|
72
|
+
content_type = meta.get("CONTENT_TYPE", "")
|
73
73
|
if not content_type.startswith("multipart/"):
|
74
74
|
raise MultiPartParserError(f"Invalid Content-Type: {content_type}")
|
75
75
|
|
@@ -91,7 +91,7 @@ class MultiPartParser:
|
|
91
91
|
# Content-Length should contain the length of the body we are about
|
92
92
|
# to receive.
|
93
93
|
try:
|
94
|
-
content_length = int(
|
94
|
+
content_length = int(meta.get("CONTENT_LENGTH", 0))
|
95
95
|
except (ValueError, TypeError):
|
96
96
|
content_length = 0
|
97
97
|
|
@@ -107,7 +107,7 @@ class MultiPartParser:
|
|
107
107
|
possible_sizes = [x.chunk_size for x in upload_handlers if x.chunk_size]
|
108
108
|
self._chunk_size = min([2**31 - 4] + possible_sizes)
|
109
109
|
|
110
|
-
self._meta =
|
110
|
+
self._meta = meta
|
111
111
|
self._encoding = encoding or settings.DEFAULT_CHARSET
|
112
112
|
self._content_length = content_length
|
113
113
|
self._upload_handlers = upload_handlers
|
plain/http/request.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
import codecs
|
2
2
|
import copy
|
3
|
+
import json
|
3
4
|
import uuid
|
4
5
|
from io import BytesIO
|
5
6
|
from itertools import chain
|
@@ -64,11 +65,11 @@ class HttpRequest:
|
|
64
65
|
# A unique ID we can use to trace this request
|
65
66
|
self.unique_id = str(uuid.uuid4())
|
66
67
|
|
67
|
-
self.
|
68
|
-
self.
|
69
|
-
self.
|
70
|
-
self.
|
71
|
-
self.
|
68
|
+
self.query_params = QueryDict(mutable=True)
|
69
|
+
self.data = QueryDict(mutable=True)
|
70
|
+
self.cookies = {}
|
71
|
+
self.meta = {}
|
72
|
+
self.files = MultiValueDict()
|
72
73
|
|
73
74
|
self.path = ""
|
74
75
|
self.path_info = ""
|
@@ -99,7 +100,7 @@ class HttpRequest:
|
|
99
100
|
|
100
101
|
@cached_property
|
101
102
|
def headers(self):
|
102
|
-
return HttpHeaders(self.
|
103
|
+
return HttpHeaders(self.meta)
|
103
104
|
|
104
105
|
@cached_property
|
105
106
|
def accepted_types(self):
|
@@ -130,13 +131,13 @@ class HttpRequest:
|
|
130
131
|
allowed hosts protection, so may return an insecure host.
|
131
132
|
"""
|
132
133
|
# We try three options, in order of decreasing preference.
|
133
|
-
if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.
|
134
|
-
host = self.
|
135
|
-
elif "HTTP_HOST" in self.
|
136
|
-
host = self.
|
134
|
+
if settings.USE_X_FORWARDED_HOST and ("HTTP_X_FORWARDED_HOST" in self.meta):
|
135
|
+
host = self.meta["HTTP_X_FORWARDED_HOST"]
|
136
|
+
elif "HTTP_HOST" in self.meta:
|
137
|
+
host = self.meta["HTTP_HOST"]
|
137
138
|
else:
|
138
139
|
# Reconstruct the host using the algorithm from PEP 333.
|
139
|
-
host = self.
|
140
|
+
host = self.meta["SERVER_NAME"]
|
140
141
|
server_port = self.get_port()
|
141
142
|
if server_port != ("443" if self.is_https() else "80"):
|
142
143
|
host = f"{host}:{server_port}"
|
@@ -166,10 +167,10 @@ class HttpRequest:
|
|
166
167
|
|
167
168
|
def get_port(self):
|
168
169
|
"""Return the port number for the request as a string."""
|
169
|
-
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.
|
170
|
-
port = self.
|
170
|
+
if settings.USE_X_FORWARDED_PORT and "HTTP_X_FORWARDED_PORT" in self.meta:
|
171
|
+
port = self.meta["HTTP_X_FORWARDED_PORT"]
|
171
172
|
else:
|
172
|
-
port = self.
|
173
|
+
port = self.meta["SERVER_PORT"]
|
173
174
|
return str(port)
|
174
175
|
|
175
176
|
def get_full_path(self, force_append_slash=False):
|
@@ -198,8 +199,8 @@ class HttpRequest:
|
|
198
199
|
return "{}{}{}".format(
|
199
200
|
escape_uri_path(path),
|
200
201
|
"/" if force_append_slash and not path.endswith("/") else "",
|
201
|
-
("?" + iri_to_uri(self.
|
202
|
-
if self.
|
202
|
+
("?" + iri_to_uri(self.meta.get("QUERY_STRING", "")))
|
203
|
+
if self.meta.get("QUERY_STRING", "")
|
203
204
|
else "",
|
204
205
|
)
|
205
206
|
|
@@ -263,7 +264,7 @@ class HttpRequest:
|
|
263
264
|
"The HTTPS_PROXY_HEADER setting must be a tuple containing "
|
264
265
|
"two values."
|
265
266
|
)
|
266
|
-
header_value = self.
|
267
|
+
header_value = self.meta.get(header)
|
267
268
|
if header_value is not None:
|
268
269
|
header_value, *_ = header_value.split(",", 1)
|
269
270
|
return "https" if header_value.strip() == secure_value else "http"
|
@@ -279,15 +280,15 @@ class HttpRequest:
|
|
279
280
|
@encoding.setter
|
280
281
|
def encoding(self, val):
|
281
282
|
"""
|
282
|
-
Set the encoding used for
|
283
|
+
Set the encoding used for query_params/data accesses. If the query_params or data
|
283
284
|
dictionary has already been created, remove and recreate it on the
|
284
285
|
next access (so that it is decoded correctly).
|
285
286
|
"""
|
286
287
|
self._encoding = val
|
287
|
-
if hasattr(self, "
|
288
|
-
del self.
|
289
|
-
if hasattr(self, "
|
290
|
-
del self.
|
288
|
+
if hasattr(self, "query_params"):
|
289
|
+
del self.query_params
|
290
|
+
if hasattr(self, "_data"):
|
291
|
+
del self._data
|
291
292
|
|
292
293
|
def _initialize_handlers(self):
|
293
294
|
self._upload_handlers = [
|
@@ -311,15 +312,15 @@ class HttpRequest:
|
|
311
312
|
)
|
312
313
|
self._upload_handlers = upload_handlers
|
313
314
|
|
314
|
-
def parse_file_upload(self,
|
315
|
-
"""Return a tuple of (
|
315
|
+
def parse_file_upload(self, meta, post_data):
|
316
|
+
"""Return a tuple of (data QueryDict, files MultiValueDict)."""
|
316
317
|
self.upload_handlers = ImmutableList(
|
317
318
|
self.upload_handlers,
|
318
319
|
warning=(
|
319
320
|
"You cannot alter upload handlers after the upload has been processed."
|
320
321
|
),
|
321
322
|
)
|
322
|
-
parser = MultiPartParser(
|
323
|
+
parser = MultiPartParser(meta, post_data, self.upload_handlers, self.encoding)
|
323
324
|
return parser.parse()
|
324
325
|
|
325
326
|
@property
|
@@ -333,7 +334,7 @@ class HttpRequest:
|
|
333
334
|
# Limit the maximum request data size that will be handled in-memory.
|
334
335
|
if (
|
335
336
|
settings.DATA_UPLOAD_MAX_MEMORY_SIZE is not None
|
336
|
-
and int(self.
|
337
|
+
and int(self.meta.get("CONTENT_LENGTH") or 0)
|
337
338
|
> settings.DATA_UPLOAD_MAX_MEMORY_SIZE
|
338
339
|
):
|
339
340
|
raise RequestDataTooBig(
|
@@ -350,29 +351,31 @@ class HttpRequest:
|
|
350
351
|
return self._body
|
351
352
|
|
352
353
|
def _mark_post_parse_error(self):
|
353
|
-
self.
|
354
|
+
self._data = QueryDict()
|
354
355
|
self._files = MultiValueDict()
|
355
356
|
|
356
|
-
def
|
357
|
-
"""Populate self.
|
358
|
-
|
359
|
-
self._post, self._files = (
|
360
|
-
QueryDict(encoding=self._encoding),
|
361
|
-
MultiValueDict(),
|
362
|
-
)
|
363
|
-
return
|
357
|
+
def _load_data_and_files(self):
|
358
|
+
"""Populate self._data and self._files"""
|
359
|
+
|
364
360
|
if self._read_started and not hasattr(self, "_body"):
|
365
361
|
self._mark_post_parse_error()
|
366
362
|
return
|
367
363
|
|
368
|
-
if self.content_type
|
364
|
+
if self.content_type.startswith("application/json"):
|
365
|
+
try:
|
366
|
+
self._data = json.loads(self.body)
|
367
|
+
self._files = MultiValueDict()
|
368
|
+
except json.JSONDecodeError:
|
369
|
+
self._mark_post_parse_error()
|
370
|
+
raise
|
371
|
+
elif self.content_type == "multipart/form-data":
|
369
372
|
if hasattr(self, "_body"):
|
370
373
|
# Use already read data
|
371
374
|
data = BytesIO(self._body)
|
372
375
|
else:
|
373
376
|
data = self
|
374
377
|
try:
|
375
|
-
self.
|
378
|
+
self._data, self._files = self.parse_file_upload(self.meta, data)
|
376
379
|
except (MultiPartParserError, TooManyFilesSent):
|
377
380
|
# An error occurred while parsing POST data. Since when
|
378
381
|
# formatting the error the request handler might access
|
@@ -381,12 +384,12 @@ class HttpRequest:
|
|
381
384
|
self._mark_post_parse_error()
|
382
385
|
raise
|
383
386
|
elif self.content_type == "application/x-www-form-urlencoded":
|
384
|
-
self.
|
387
|
+
self._data, self._files = (
|
385
388
|
QueryDict(self.body, encoding=self._encoding),
|
386
389
|
MultiValueDict(),
|
387
390
|
)
|
388
391
|
else:
|
389
|
-
self.
|
392
|
+
self._data, self._files = (
|
390
393
|
QueryDict(encoding=self._encoding),
|
391
394
|
MultiValueDict(),
|
392
395
|
)
|
@@ -400,7 +403,7 @@ class HttpRequest:
|
|
400
403
|
#
|
401
404
|
# Expects self._stream to be set to an appropriate source of bytes by
|
402
405
|
# a corresponding request subclass (e.g. WSGIRequest).
|
403
|
-
# Also when request data has already been read by request.
|
406
|
+
# Also when request data has already been read by request.data or
|
404
407
|
# request.body, self._stream points to a BytesIO instance
|
405
408
|
# containing that data.
|
406
409
|
|
@@ -85,7 +85,7 @@ class FileUploadHandler:
|
|
85
85
|
self.request = request
|
86
86
|
|
87
87
|
def handle_raw_input(
|
88
|
-
self, input_data,
|
88
|
+
self, input_data, meta, content_length, boundary, encoding=None
|
89
89
|
):
|
90
90
|
"""
|
91
91
|
Handle the raw input from the client.
|
@@ -94,8 +94,8 @@ class FileUploadHandler:
|
|
94
94
|
|
95
95
|
:input_data:
|
96
96
|
An object that supports reading via .read().
|
97
|
-
:
|
98
|
-
``request.
|
97
|
+
:meta:
|
98
|
+
``request.meta``.
|
99
99
|
:content_length:
|
100
100
|
The (integer) value of the Content-Length header from the
|
101
101
|
client.
|
@@ -199,7 +199,7 @@ class MemoryFileUploadHandler(FileUploadHandler):
|
|
199
199
|
"""
|
200
200
|
|
201
201
|
def handle_raw_input(
|
202
|
-
self, input_data,
|
202
|
+
self, input_data, meta, content_length, boundary, encoding=None
|
203
203
|
):
|
204
204
|
"""
|
205
205
|
Use the content_length to signal whether or not this handler should be
|
plain/internal/handlers/wsgi.py
CHANGED
@@ -72,9 +72,9 @@ class WSGIRequest(HttpRequest):
|
|
72
72
|
self.path = "{}/{}".format(
|
73
73
|
script_name.rstrip("/"), path_info.replace("/", "", 1)
|
74
74
|
)
|
75
|
-
self.
|
76
|
-
self.
|
77
|
-
self.
|
75
|
+
self.meta = environ
|
76
|
+
self.meta["PATH_INFO"] = path_info
|
77
|
+
self.meta["SCRIPT_NAME"] = script_name
|
78
78
|
self.method = environ["REQUEST_METHOD"].upper()
|
79
79
|
# Set content_type, content_params, and encoding.
|
80
80
|
self._set_content_type_params(environ)
|
@@ -89,39 +89,39 @@ class WSGIRequest(HttpRequest):
|
|
89
89
|
def __getstate__(self):
|
90
90
|
state = super().__getstate__()
|
91
91
|
for attr in self.meta_non_picklable_attrs:
|
92
|
-
if attr in state["
|
93
|
-
del state["
|
92
|
+
if attr in state["meta"]:
|
93
|
+
del state["meta"][attr]
|
94
94
|
return state
|
95
95
|
|
96
96
|
def _get_scheme(self):
|
97
97
|
return self.environ.get("wsgi.url_scheme")
|
98
98
|
|
99
99
|
@cached_property
|
100
|
-
def
|
100
|
+
def query_params(self):
|
101
101
|
# The WSGI spec says 'QUERY_STRING' may be absent.
|
102
102
|
raw_query_string = get_bytes_from_wsgi(self.environ, "QUERY_STRING", "")
|
103
103
|
return QueryDict(raw_query_string, encoding=self._encoding)
|
104
104
|
|
105
|
-
def
|
106
|
-
if not hasattr(self, "
|
107
|
-
self.
|
108
|
-
return self.
|
105
|
+
def _get_data(self):
|
106
|
+
if not hasattr(self, "_data"):
|
107
|
+
self._load_data_and_files()
|
108
|
+
return self._data
|
109
109
|
|
110
|
-
def
|
111
|
-
self.
|
110
|
+
def _set_data(self, data):
|
111
|
+
self._data = data
|
112
112
|
|
113
113
|
@cached_property
|
114
|
-
def
|
114
|
+
def cookies(self):
|
115
115
|
raw_cookie = get_str_from_wsgi(self.environ, "HTTP_COOKIE", "")
|
116
116
|
return parse_cookie(raw_cookie)
|
117
117
|
|
118
118
|
@property
|
119
|
-
def
|
119
|
+
def files(self):
|
120
120
|
if not hasattr(self, "_files"):
|
121
|
-
self.
|
121
|
+
self._load_data_and_files()
|
122
122
|
return self._files
|
123
123
|
|
124
|
-
|
124
|
+
data = property(_get_data, _set_data)
|
125
125
|
|
126
126
|
|
127
127
|
class WSGIHandler(base.BaseHandler):
|
plain/runtime/global_settings.py
CHANGED
@@ -134,7 +134,7 @@ CSRF_COOKIE_SECURE = True
|
|
134
134
|
CSRF_COOKIE_HTTPONLY = False
|
135
135
|
CSRF_COOKIE_SAMESITE = "Lax"
|
136
136
|
CSRF_HEADER_NAME = "CSRF-Token"
|
137
|
-
|
137
|
+
CSRF_FIELD_NAME = "_csrftoken"
|
138
138
|
CSRF_TRUSTED_ORIGINS: list[str] = []
|
139
139
|
|
140
140
|
###########
|
plain/views/redirect.py
CHANGED
plain/views/templates.py
CHANGED
@@ -14,6 +14,7 @@ plain/assets/finders.py,sha256=2k8QZAbfUbc1LykxbzdazTSB6xNxJZnsZaGhWbSFZZs,1452
|
|
14
14
|
plain/assets/fingerprints.py,sha256=2LPHLUkoITMseLDmemTpBtMRDWCR2H5GAHjC6AN4gz0,1367
|
15
15
|
plain/assets/urls.py,sha256=zQUA8bAlh9qVqskPJJrqWd9DjvetOi5jPSqy4vUX0J4,1161
|
16
16
|
plain/assets/views.py,sha256=T_0Qh6v9qBerEBYbhToigwOzsij-x1z_R-1zETQcIh0,9447
|
17
|
+
plain/chores/README.md,sha256=Da8Nw8ZF7PlAE_iVb0AqJGbLOu0F6HC0cj1K451KBak,1946
|
17
18
|
plain/chores/__init__.py,sha256=r9TXtQCH-VbvfnIJ5F8FxgQC35GRWFOfmMZN3q9niLg,67
|
18
19
|
plain/chores/registry.py,sha256=V3WjuekRI22LFvJbqSkUXQtiOtuE2ZK8gKV1TRvxRUI,1866
|
19
20
|
plain/cli/README.md,sha256=ompPcgzY2Fdpm579ITmCpFIaZIsiXYbfD61mqkq312M,1860
|
@@ -33,19 +34,19 @@ plain/cli/startup.py,sha256=wLaFuyUb4ewWhtehBCGicrRCXIIGCRbeCT3ce9hUv-A,1022
|
|
33
34
|
plain/cli/urls.py,sha256=CS9NFpwZBWveAR8F3YsoUNySDEK_PwF73oSgLDfkOdI,3776
|
34
35
|
plain/cli/utils.py,sha256=VwlIh0z7XxzVV8I3qM2kZo07fkJFPoeeVZa1ODG616k,258
|
35
36
|
plain/csrf/README.md,sha256=nxCpPk1HF5eAM-7paxg9D-9RVCU9jXsSPAVHkJvA_DU,717
|
36
|
-
plain/csrf/middleware.py,sha256=
|
37
|
+
plain/csrf/middleware.py,sha256=MJusxqfa_SVpzHchfkTOCzccCl8twT8Cr_ymxLb5EvY,17414
|
37
38
|
plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
|
38
39
|
plain/forms/README.md,sha256=TawBdy1jP0-8HUsfUzd7vvgkwl3EJGejDxFhWR8F-80,2242
|
39
40
|
plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
|
40
41
|
plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
|
41
42
|
plain/forms/exceptions.py,sha256=NYk1wjYDkk-lA_XMJQDItOebQcL_m_r2eNRc2mkLQkg,315
|
42
43
|
plain/forms/fields.py,sha256=C71ed7m8VBnsgS-eIC6Xsmz7ruNSkdGa1BuSo7V3QKc,34574
|
43
|
-
plain/forms/forms.py,sha256=
|
44
|
-
plain/http/README.md,sha256=
|
44
|
+
plain/forms/forms.py,sha256=gyDUrl7wh7n3fqKMB6eH0-AvCmWXST3oc-jBWUf9rmU,10454
|
45
|
+
plain/http/README.md,sha256=F9wbahgSU3jIDEG14gJjdPJRem4weUNvwnwhb7o3cu0,722
|
45
46
|
plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
|
46
47
|
plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
|
47
|
-
plain/http/multipartparser.py,sha256=
|
48
|
-
plain/http/request.py,sha256=
|
48
|
+
plain/http/multipartparser.py,sha256=Z1dFJNAd8N5RHUuF67jh1jBfZOFepORsre_3ee6CgOQ,27266
|
49
|
+
plain/http/request.py,sha256=Xlolhpp2ZfZIi0TOz960U16CdqGi8jMhmHsBtxpkR_4,25631
|
49
50
|
plain/http/response.py,sha256=IGSg22ioob6kSRkKGQsQF_exb2Lch8RKiZr_Xoi1_xA,23644
|
50
51
|
plain/internal/__init__.py,sha256=fVBaYLCXEQc-7riHMSlw3vMTTuF7-0Bj2I8aGzv0o0w,171
|
51
52
|
plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
|
@@ -54,12 +55,12 @@ plain/internal/files/locks.py,sha256=z03q7IZD4tPMK3s1HKF3w_uetkFj6w6FTheLUxZsfB0
|
|
54
55
|
plain/internal/files/move.py,sha256=jfdD29QhamxZjXRgqmZS4dJoJ4sK6M7QK1Km-69jWeo,3238
|
55
56
|
plain/internal/files/temp.py,sha256=UJJnCI8dqPIC8XXHU3-jG2-0svbkrgGlBs4yhciLm4c,2506
|
56
57
|
plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uFPIxvl7c,4192
|
57
|
-
plain/internal/files/uploadhandler.py,sha256=
|
58
|
+
plain/internal/files/uploadhandler.py,sha256=63_QUwAwfq3bevw79i0S7zt2EB2UBoO7MaauvezaVMY,7198
|
58
59
|
plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
|
59
60
|
plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
60
61
|
plain/internal/handlers/base.py,sha256=oRKni79ATI_u7sywGFExrzKvP5dpJTqIp1m521A90Ew,4169
|
61
62
|
plain/internal/handlers/exception.py,sha256=vfha_6-fz6S6VYCP1PMBfue2Gw-_th6jqaTE372fGlw,4809
|
62
|
-
plain/internal/handlers/wsgi.py,sha256=
|
63
|
+
plain/internal/handlers/wsgi.py,sha256=sOgqnE4fNzKD2a5EMubiEpHEK9Mi69FWx3BVWxGgMw0,8262
|
63
64
|
plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
64
65
|
plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
|
65
66
|
plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
|
@@ -82,7 +83,7 @@ plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2
|
|
82
83
|
plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
|
83
84
|
plain/runtime/README.md,sha256=S_FIOmSq8LkVQHh9Xm6s3EJWKTVdlSr5A_bNXgh02X8,4740
|
84
85
|
plain/runtime/__init__.py,sha256=o2RVETiL8U0lMFBpbtfnxflhw_4MFllMV6CEpX3RqZs,1965
|
85
|
-
plain/runtime/global_settings.py,sha256=
|
86
|
+
plain/runtime/global_settings.py,sha256=Vi7jabMF-SBolkP5qx1nLnGoDt_kD1KT_wgtbjNlPHw,5580
|
86
87
|
plain/runtime/user_settings.py,sha256=uRHHVfzUvHon91_fOKj7K2WaBYwJ1gCPLfeXqKj5CTs,10902
|
87
88
|
plain/signals/README.md,sha256=FInfJXdVQkb7u93PvD8XgPbz_f6m0l2xIu_4PyttV1E,234
|
88
89
|
plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
|
@@ -143,10 +144,10 @@ plain/views/errors.py,sha256=jbNCJIzowwCsEvqyJ3opMeZpPDqTyhtrbqb0VnAm2HE,1263
|
|
143
144
|
plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
|
144
145
|
plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
|
145
146
|
plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
|
146
|
-
plain/views/redirect.py,sha256=
|
147
|
-
plain/views/templates.py,sha256=
|
148
|
-
plain-0.
|
149
|
-
plain-0.
|
150
|
-
plain-0.
|
151
|
-
plain-0.
|
152
|
-
plain-0.
|
147
|
+
plain/views/redirect.py,sha256=daq2cQIkdDF78bt43sjuZxRAyJm_t_SKw6tyPmiXPIc,1985
|
148
|
+
plain/views/templates.py,sha256=ORBabNc9p2QbW_rTfq2650yJtzUuUJoPXoHogrhYLCo,2028
|
149
|
+
plain-0.40.0.dist-info/METADATA,sha256=WccAr8tpVrNh7gYPQXG7gCS3dUtm1zv9Nn8IkHTQpFI,4297
|
150
|
+
plain-0.40.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
151
|
+
plain-0.40.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
|
152
|
+
plain-0.40.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
|
153
|
+
plain-0.40.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|