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 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
+ ![](https://assets.plainframework.com/docs/plain-chores-run.png)
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.META."""
77
+ """Generate a new random CSRF_COOKIE value, and add it to request.meta."""
78
78
  csrf_secret = _get_new_csrf_string()
79
- request.META.update(
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.META:
99
- csrf_secret = request.META["CSRF_COOKIE"]
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.META["CSRF_COOKIE_NEEDS_UPDATE"] = True
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.META["CSRF_COOKIE"] = csrf_secret
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.META.get("CSRF_COOKIE_NEEDS_UPDATE"):
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.META["CSRF_COOKIE_NEEDS_UPDATE"] = False
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.COOKIES[settings.CSRF_COOKIE_NAME]
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.META["CSRF_COOKIE"],
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.META["HTTP_ORIGIN"]
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.META.get("HTTP_REFERER")
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.POST.get(settings.CSRF_POST_NAME, "")
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.META["CSRF_COOKIE"] has been manipulated before
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.META:
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.META["HTTP_ORIGIN"]
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
- if request.method in ("POST", "PUT", "PATCH"):
68
- if request.headers.get("Content-Type") == "application/json":
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.files = request.FILES
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.GET.get("example"))
17
+ print(self.request.query_params.get("example"))
18
18
 
19
19
  # Creating a response
20
20
  response = Response("Hello, world!", status_code=200)
@@ -54,12 +54,12 @@ class MultiPartParser:
54
54
 
55
55
  boundary_re = _lazy_re_compile(r"[ -~]{0,200}[!-~]")
56
56
 
57
- def __init__(self, META, input_data, upload_handlers, encoding=None):
57
+ def __init__(self, meta, input_data, upload_handlers, encoding=None):
58
58
  """
59
59
  Initialize the MultiPartParser object.
60
60
 
61
- :META:
62
- The standard ``META`` dictionary in Plain request objects.
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 = META.get("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(META.get("CONTENT_LENGTH", 0))
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 = 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.GET = QueryDict(mutable=True)
68
- self.POST = QueryDict(mutable=True)
69
- self.COOKIES = {}
70
- self.META = {}
71
- self.FILES = MultiValueDict()
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.META)
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.META):
134
- host = self.META["HTTP_X_FORWARDED_HOST"]
135
- elif "HTTP_HOST" in self.META:
136
- host = self.META["HTTP_HOST"]
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.META["SERVER_NAME"]
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.META:
170
- port = self.META["HTTP_X_FORWARDED_PORT"]
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.META["SERVER_PORT"]
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.META.get("QUERY_STRING", "")))
202
- if self.META.get("QUERY_STRING", "")
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.META.get(header)
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 GET/POST accesses. If the GET or POST
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, "GET"):
288
- del self.GET
289
- if hasattr(self, "_post"):
290
- del self._post
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, META, post_data):
315
- """Return a tuple of (POST QueryDict, FILES MultiValueDict)."""
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(META, post_data, self.upload_handlers, self.encoding)
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.META.get("CONTENT_LENGTH") or 0)
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._post = QueryDict()
354
+ self._data = QueryDict()
354
355
  self._files = MultiValueDict()
355
356
 
356
- def _load_post_and_files(self):
357
- """Populate self._post and self._files if the content-type is a form type"""
358
- if self.method != "POST":
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 == "multipart/form-data":
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._post, self._files = self.parse_file_upload(self.META, data)
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._post, self._files = (
387
+ self._data, self._files = (
385
388
  QueryDict(self.body, encoding=self._encoding),
386
389
  MultiValueDict(),
387
390
  )
388
391
  else:
389
- self._post, self._files = (
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.POST or
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, META, content_length, boundary, encoding=None
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
- :META:
98
- ``request.META``.
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, META, content_length, boundary, encoding=None
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
@@ -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.META = environ
76
- self.META["PATH_INFO"] = path_info
77
- self.META["SCRIPT_NAME"] = script_name
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["META"]:
93
- del state["META"][attr]
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 GET(self):
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 _get_post(self):
106
- if not hasattr(self, "_post"):
107
- self._load_post_and_files()
108
- return self._post
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 _set_post(self, post):
111
- self._post = post
110
+ def _set_data(self, data):
111
+ self._data = data
112
112
 
113
113
  @cached_property
114
- def COOKIES(self):
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 FILES(self):
119
+ def files(self):
120
120
  if not hasattr(self, "_files"):
121
- self._load_post_and_files()
121
+ self._load_data_and_files()
122
122
  return self._files
123
123
 
124
- POST = property(_get_post, _set_post)
124
+ data = property(_get_data, _set_data)
125
125
 
126
126
 
127
127
  class WSGIHandler(base.BaseHandler):
@@ -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
- CSRF_POST_NAME = "_csrftoken"
137
+ CSRF_FIELD_NAME = "_csrftoken"
138
138
  CSRF_TRUSTED_ORIGINS: list[str] = []
139
139
 
140
140
  ###########
plain/views/redirect.py CHANGED
@@ -38,7 +38,7 @@ class RedirectView(View):
38
38
  else:
39
39
  return None
40
40
 
41
- args = self.request.META.get("QUERY_STRING", "")
41
+ args = self.request.meta.get("QUERY_STRING", "")
42
42
  if args and self.query_string:
43
43
  url = f"{url}?{args}"
44
44
  return url
plain/views/templates.py CHANGED
@@ -12,7 +12,7 @@ from .base import View
12
12
  def csrf_input(request):
13
13
  return format_html(
14
14
  '<input type="hidden" name="{}" value="{}">',
15
- settings.CSRF_POST_NAME,
15
+ settings.CSRF_FIELD_NAME,
16
16
  get_token(request),
17
17
  )
18
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.39.0
3
+ Version: 0.40.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -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=bn8KOE45aucSam0m3F9g8FVfnmjjt-6jxS6iSNwHamU,17413
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=Zcl5euWWxuE-CTcOHsOVYixWESdVk7no2Sz40AxmudY,11204
44
- plain/http/README.md,sha256=G662f56feQWiEHw9tTncZqK8kNB_nbR1HTGeOR1ygNM,713
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=Cyk_UZhxf8JwNza_Yl4_nKCYkmnG7xY9PSVcf9Us57U,27266
48
- plain/http/request.py,sha256=kq3AuM0EWyAD_kqMlorTccm5mzIQ6ZefkCa-jXUntnI,25514
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=eEnd5onstypjHYtg367PnVWwCaF1kAPlLPSV7goIf_E,7198
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=aOGCd9hJEMTVMGfgIDlSFvevd8_XCzZa2dtlR4peqZg,8253
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=gg9XhMJmzbQqfxzst5JaI0hDIzPvTwX5M0AonJk2ISY,5579
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=vMXx8430FtyKcT0V0gyY92SkLtyULBX52KhX4eu4gEA,1985
147
- plain/views/templates.py,sha256=kMcHKkKNvucF91SFGkaq-ugjrCwn4zJBpFV1JkwA544,2027
148
- plain-0.39.0.dist-info/METADATA,sha256=DlIAs31jS4S87R0UhAlZlYdgxZGKOebjo3IcgK5yas4,4297
149
- plain-0.39.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
150
- plain-0.39.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
151
- plain-0.39.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
152
- plain-0.39.0.dist-info/RECORD,,
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