plain 0.16.1__py3-none-any.whl → 0.18.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/csrf/middleware.py CHANGED
@@ -180,6 +180,9 @@ class CsrfViewMiddleware:
180
180
  # saved anyways.
181
181
  request.META["CSRF_COOKIE"] = csrf_secret
182
182
 
183
+ if csrf_response := self._get_csrf_response(request):
184
+ return csrf_response
185
+
183
186
  response = self.get_response(request)
184
187
 
185
188
  if request.META.get("CSRF_COOKIE_NEEDS_UPDATE"):
@@ -395,23 +398,16 @@ class CsrfViewMiddleware:
395
398
  reason = self._bad_token_message("incorrect", token_source)
396
399
  raise RejectRequest(reason)
397
400
 
398
- def process_view(self, request, callback, callback_args, callback_kwargs):
401
+ def _get_csrf_response(self, request):
399
402
  # Wait until request.META["CSRF_COOKIE"] has been manipulated before
400
403
  # bailing out, so that get_token still works
401
- if getattr(callback, "csrf_exempt", False):
404
+ if getattr(request, "csrf_exempt", True):
402
405
  return None
403
406
 
404
407
  # Assume that anything not defined as 'safe' by RFC 9110 needs protection
405
408
  if request.method in ("GET", "HEAD", "OPTIONS", "TRACE"):
406
409
  return None
407
410
 
408
- if getattr(request, "_dont_enforce_csrf_checks", False):
409
- # Mechanism to turn off CSRF checks for test suite. It comes after
410
- # the creation of CSRF cookies, so that everything else continues
411
- # to work exactly the same (e.g. cookies are sent, etc.), but
412
- # before any branches that call the _reject method.
413
- return None
414
-
415
411
  # Reject the request if the Origin header doesn't match an allowed
416
412
  # value.
417
413
  if "HTTP_ORIGIN" in request.META:
plain/csrf/views.py CHANGED
@@ -4,7 +4,28 @@ from plain.views import TemplateView
4
4
  class CsrfFailureView(TemplateView):
5
5
  template_name = "403.html"
6
6
 
7
- def get(self):
8
- response = super().get()
7
+ def get_response(self):
8
+ response = super().get_response()
9
9
  response.status_code = 403
10
10
  return response
11
+
12
+ def post(self):
13
+ return self.get()
14
+
15
+ def put(self):
16
+ return self.get()
17
+
18
+ def patch(self):
19
+ return self.get()
20
+
21
+ def delete(self):
22
+ return self.get()
23
+
24
+ def head(self):
25
+ return self.get()
26
+
27
+ def options(self):
28
+ return self.get()
29
+
30
+ def trace(self):
31
+ return self.get()
plain/http/response.py CHANGED
@@ -187,27 +187,6 @@ class ResponseBase:
187
187
  else ""
188
188
  )
189
189
 
190
- def __setitem__(self, header, value):
191
- self.headers[header] = value
192
-
193
- def __delitem__(self, header):
194
- del self.headers[header]
195
-
196
- def __getitem__(self, header):
197
- return self.headers[header]
198
-
199
- def has_header(self, header):
200
- """Case-insensitive check for a header."""
201
- return header in self.headers
202
-
203
- __contains__ = has_header
204
-
205
- def items(self):
206
- return self.headers.items()
207
-
208
- def get(self, header, alternate=None):
209
- return self.headers.get(header, alternate)
210
-
211
190
  def set_cookie(
212
191
  self,
213
192
  key,
@@ -272,10 +251,6 @@ class ResponseBase:
272
251
  raise ValueError('samesite must be "lax", "none", or "strict".')
273
252
  self.cookies[key]["samesite"] = samesite
274
253
 
275
- def setdefault(self, key, value):
276
- """Set a header unless it has already been set."""
277
- self.headers.setdefault(key, value)
278
-
279
254
  def set_signed_cookie(self, key, value, salt="", **kwargs):
280
255
  value = signing.get_cookie_signer(salt=key + salt).sign(value)
281
256
  return self.set_cookie(key, value, **kwargs)
@@ -587,14 +562,14 @@ class ResponseRedirectBase(Response):
587
562
 
588
563
  def __init__(self, redirect_to, *args, **kwargs):
589
564
  super().__init__(*args, **kwargs)
590
- self["Location"] = iri_to_uri(redirect_to)
565
+ self.headers["Location"] = iri_to_uri(redirect_to)
591
566
  parsed = urlparse(str(redirect_to))
592
567
  if parsed.scheme and parsed.scheme not in self.allowed_schemes:
593
568
  raise DisallowedRedirect(
594
569
  f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
595
570
  )
596
571
 
597
- url = property(lambda self: self["Location"])
572
+ url = property(lambda self: self.headers["Location"])
598
573
 
599
574
  def __repr__(self):
600
575
  return (
@@ -627,7 +602,7 @@ class ResponseNotModified(Response):
627
602
 
628
603
  def __init__(self, *args, **kwargs):
629
604
  super().__init__(*args, **kwargs)
630
- del self["content-type"]
605
+ del self.headers["content-type"]
631
606
 
632
607
  @Response.content.setter
633
608
  def content(self, value):
@@ -663,14 +638,14 @@ class ResponseNotAllowed(Response):
663
638
 
664
639
  def __init__(self, permitted_methods, *args, **kwargs):
665
640
  super().__init__(*args, **kwargs)
666
- self["Allow"] = ", ".join(permitted_methods)
641
+ self.headers["Allow"] = ", ".join(permitted_methods)
667
642
 
668
643
  def __repr__(self):
669
644
  return "<%(cls)s [%(methods)s] status_code=%(status_code)d%(content_type)s>" % { # noqa: UP031
670
645
  "cls": self.__class__.__name__,
671
646
  "status_code": self.status_code,
672
647
  "content_type": self._content_type_for_repr,
673
- "methods": self["Allow"],
648
+ "methods": self.headers["Allow"],
674
649
  }
675
650
 
676
651
 
@@ -23,7 +23,6 @@ BUILTIN_MIDDLEWARE = [
23
23
 
24
24
 
25
25
  class BaseHandler:
26
- _view_middleware = None
27
26
  _middleware_chain = None
28
27
 
29
28
  def load_middleware(self):
@@ -32,8 +31,6 @@ class BaseHandler:
32
31
 
33
32
  Must be called after the environment is fixed (see __call__ in subclasses).
34
33
  """
35
- self._view_middleware = []
36
-
37
34
  get_response = self._get_response
38
35
  handler = convert_exception_to_response(get_response)
39
36
 
@@ -48,12 +45,6 @@ class BaseHandler:
48
45
  f"Middleware factory {middleware_path} returned None."
49
46
  )
50
47
 
51
- if hasattr(mw_instance, "process_view"):
52
- self._view_middleware.insert(
53
- 0,
54
- mw_instance.process_view,
55
- )
56
-
57
48
  handler = convert_exception_to_response(mw_instance)
58
49
 
59
50
  # We only assign to this when initialization is complete as it is used
@@ -82,19 +73,9 @@ class BaseHandler:
82
73
  template_response middleware. This method is everything that happens
83
74
  inside the request/response middleware.
84
75
  """
85
- response = None
86
76
  callback, callback_args, callback_kwargs = self.resolve_request(request)
87
77
 
88
- # Apply view middleware
89
- for middleware_method in self._view_middleware:
90
- response = middleware_method(
91
- request, callback, callback_args, callback_kwargs
92
- )
93
- if response:
94
- break
95
-
96
- if response is None:
97
- response = callback(request, *callback_args, **callback_kwargs)
78
+ response = callback(request, *callback_args, **callback_kwargs)
98
79
 
99
80
  # Complain if the view returned None (a common error).
100
81
  self.check_response(response, callback)
@@ -140,7 +140,7 @@ class WSGIHandler(base.BaseHandler):
140
140
 
141
141
  status = "%d %s" % (response.status_code, response.reason_phrase) # noqa: UP031
142
142
  response_headers = [
143
- *response.items(),
143
+ *response.headers.items(),
144
144
  *(("Set-Cookie", c.output(header="")) for c in response.cookies.values()),
145
145
  ]
146
146
  start_response(status, response_headers)
@@ -19,7 +19,7 @@ class DefaultHeadersMiddleware:
19
19
 
20
20
  # Add the Content-Length header to non-streaming responses if not
21
21
  # already set.
22
- if not response.streaming and not response.has_header("Content-Length"):
22
+ if not response.streaming and "Content-Length" not in response.headers:
23
23
  response.headers["Content-Length"] = str(len(response.content))
24
24
 
25
25
  return response
plain/test/client.py CHANGED
@@ -177,7 +177,7 @@ class ClientHandler(BaseHandler):
177
177
  # CsrfViewMiddleware. This makes life easier, and is probably
178
178
  # required for backwards compatibility with external tests against
179
179
  # admin views.
180
- request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
180
+ request.csrf_exempt = not self.enforce_csrf_checks
181
181
 
182
182
  # Request goes through middleware.
183
183
  response = self.get_response(request)
@@ -615,10 +615,10 @@ class ClientMixin:
615
615
 
616
616
  def _parse_json(self, response, **extra):
617
617
  if not hasattr(response, "_json"):
618
- if not JSON_CONTENT_TYPE_RE.match(response.get("Content-Type")):
618
+ if not JSON_CONTENT_TYPE_RE.match(response.headers.get("Content-Type")):
619
619
  raise ValueError(
620
620
  'Content-Type header is "{}", not "application/json"'.format(
621
- response.get("Content-Type")
621
+ response.headers.get("Content-Type")
622
622
  )
623
623
  )
624
624
  response._json = json.loads(
plain/utils/cache.py CHANGED
@@ -55,7 +55,7 @@ def patch_cache_control(response, **kwargs):
55
55
  return f"{t[0]}={t[1]}"
56
56
 
57
57
  cc = defaultdict(set)
58
- if response.get("Cache-Control"):
58
+ if response.headers.get("Cache-Control"):
59
59
  for field in cc_delim_re.split(response.headers["Cache-Control"]):
60
60
  directive, value = dictitem(field)
61
61
  if directive == "no-cache":
@@ -102,7 +102,7 @@ def get_max_age(response):
102
102
  Return the max-age from the response Cache-Control header as an integer,
103
103
  or None if it wasn't found or wasn't an integer.
104
104
  """
105
- if not response.has_header("Cache-Control"):
105
+ if "Cache-Control" not in response.headers:
106
106
  return
107
107
  cc = dict(
108
108
  _to_tuple(el) for el in cc_delim_re.split(response.headers["Cache-Control"])
@@ -146,7 +146,7 @@ def _not_modified(request, response=None):
146
146
  "Last-Modified",
147
147
  "Vary",
148
148
  ):
149
- if header in response:
149
+ if header in response.headers:
150
150
  new_response.headers[header] = response.headers[header]
151
151
 
152
152
  # Preserve cookies as per the cookie specification: "If a proxy server
@@ -278,7 +278,7 @@ def patch_response_headers(response, cache_timeout=None):
278
278
  cache_timeout = settings.CACHE_MIDDLEWARE_SECONDS
279
279
  if cache_timeout < 0:
280
280
  cache_timeout = 0 # Can't have max-age negative
281
- if not response.has_header("Expires"):
281
+ if "Expires" not in response.headers:
282
282
  response.headers["Expires"] = http_date(time.time() + cache_timeout)
283
283
  patch_cache_control(response, max_age=cache_timeout)
284
284
 
@@ -303,7 +303,7 @@ def patch_vary_headers(response, newheaders):
303
303
  # Note that we need to keep the original order intact, because cache
304
304
  # implementations may rely on the order of the Vary contents in, say,
305
305
  # computing an MD5 hash.
306
- if response.has_header("Vary"):
306
+ if "Vary" in response.headers:
307
307
  vary_headers = cc_delim_re.split(response.headers["Vary"])
308
308
  else:
309
309
  vary_headers = []
plain/utils/decorators.py CHANGED
@@ -1,7 +1,5 @@
1
1
  "Functions that help with dynamically creating decorators for views."
2
2
 
3
- from functools import partial, update_wrapper, wraps
4
-
5
3
 
6
4
  class classonlymethod(classmethod):
7
5
  def __get__(self, instance, cls=None):
@@ -10,81 +8,3 @@ class classonlymethod(classmethod):
10
8
  "This method is available only on the class, not on instances."
11
9
  )
12
10
  return super().__get__(instance, cls)
13
-
14
-
15
- def _update_method_wrapper(_wrapper, decorator):
16
- # _multi_decorate()'s bound_method isn't available in this scope. Cheat by
17
- # using it on a dummy function.
18
- @decorator
19
- def dummy(*args, **kwargs):
20
- pass
21
-
22
- update_wrapper(_wrapper, dummy)
23
-
24
-
25
- def _multi_decorate(decorators, method):
26
- """
27
- Decorate `method` with one or more function decorators. `decorators` can be
28
- a single decorator or an iterable of decorators.
29
- """
30
- if hasattr(decorators, "__iter__"):
31
- # Apply a list/tuple of decorators if 'decorators' is one. Decorator
32
- # functions are applied so that the call order is the same as the
33
- # order in which they appear in the iterable.
34
- decorators = decorators[::-1]
35
- else:
36
- decorators = [decorators]
37
-
38
- def _wrapper(self, *args, **kwargs):
39
- # bound_method has the signature that 'decorator' expects i.e. no
40
- # 'self' argument, but it's a closure over self so it can call
41
- # 'func'. Also, wrap method.__get__() in a function because new
42
- # attributes can't be set on bound method objects, only on functions.
43
- bound_method = wraps(method)(partial(method.__get__(self, type(self))))
44
- for dec in decorators:
45
- bound_method = dec(bound_method)
46
- return bound_method(*args, **kwargs)
47
-
48
- # Copy any attributes that a decorator adds to the function it decorates.
49
- for dec in decorators:
50
- _update_method_wrapper(_wrapper, dec)
51
- # Preserve any existing attributes of 'method', including the name.
52
- update_wrapper(_wrapper, method)
53
- return _wrapper
54
-
55
-
56
- def method_decorator(decorator, name=""):
57
- """
58
- Convert a function decorator into a method decorator
59
- """
60
-
61
- # 'obj' can be a class or a function. If 'obj' is a function at the time it
62
- # is passed to _dec, it will eventually be a method of the class it is
63
- # defined on. If 'obj' is a class, the 'name' is required to be the name
64
- # of the method that will be decorated.
65
- def _dec(obj):
66
- if not isinstance(obj, type):
67
- return _multi_decorate(decorator, obj)
68
- if not (name and hasattr(obj, name)):
69
- raise ValueError(
70
- "The keyword argument `name` must be the name of a method "
71
- f"of the decorated class: {obj}. Got '{name}' instead."
72
- )
73
- method = getattr(obj, name)
74
- if not callable(method):
75
- raise TypeError(
76
- f"Cannot decorate '{name}' as it isn't a callable attribute of "
77
- f"{obj} ({method})."
78
- )
79
- _wrapper = _multi_decorate(decorator, method)
80
- setattr(obj, name, _wrapper)
81
- return obj
82
-
83
- # Don't worry about making _dec look similar to a list/tuple as it's rather
84
- # meaningless.
85
- if not hasattr(decorator, "__iter__"):
86
- update_wrapper(_dec, decorator)
87
- # Change the name to aid debugging.
88
- obj = decorator if hasattr(decorator, "__name__") else decorator.__class__
89
- _dec.__name__ = f"method_decorator({obj.__name__})"
90
- return _dec
plain/views/base.py CHANGED
@@ -19,6 +19,18 @@ class View:
19
19
  url_args: tuple
20
20
  url_kwargs: dict
21
21
 
22
+ # By default, any of these are allowed if a method is defined for it.
23
+ allowed_http_methods = [
24
+ "get",
25
+ "post",
26
+ "put",
27
+ "patch",
28
+ "delete",
29
+ "head",
30
+ "options",
31
+ "trace",
32
+ ]
33
+
22
34
  def __init__(self, *args, **kwargs) -> None:
23
35
  # Views can customize their init, which receives
24
36
  # the args and kwargs from as_view()
@@ -42,9 +54,6 @@ class View:
42
54
  except ResponseException as e:
43
55
  return e.response
44
56
 
45
- # Copy possible attributes set by decorators, e.g. @csrf_exempt, from
46
- # the dispatch method.
47
- view.__dict__.update(cls.get_response.__dict__)
48
57
  view.view_class = cls
49
58
 
50
59
  return view
@@ -57,7 +66,7 @@ class View:
57
66
 
58
67
  handler = getattr(self, self.request.method.lower(), None)
59
68
 
60
- if not handler:
69
+ if not handler or self.request.method.lower() not in self.allowed_http_methods:
61
70
  logger.warning(
62
71
  "Method Not Allowed (%s): %s",
63
72
  self.request.method,
@@ -100,14 +109,4 @@ class View:
100
109
  return response
101
110
 
102
111
  def _allowed_methods(self) -> list[str]:
103
- known_http_method_names = [
104
- "get",
105
- "post",
106
- "put",
107
- "patch",
108
- "delete",
109
- "head",
110
- "options",
111
- "trace",
112
- ]
113
- return [m.upper() for m in known_http_method_names if hasattr(self, m)]
112
+ return [m.upper() for m in self.allowed_http_methods if hasattr(self, m)]
plain/views/csrf.py CHANGED
@@ -1,24 +1,4 @@
1
- from functools import wraps
2
-
3
- from plain.utils.decorators import method_decorator
4
-
5
-
6
- def csrf_exempt(view_func):
7
- """Mark a view function as being exempt from the CSRF view protection."""
8
-
9
- # view_func.csrf_exempt = True would also work, but decorators are nicer
10
- # if they don't have side effects, so return a new function.
11
- @wraps(view_func)
12
- def wrapper_view(*args, **kwargs):
13
- return view_func(*args, **kwargs)
14
-
15
- wrapper_view.csrf_exempt = True
16
- return wrapper_view
17
-
18
-
19
- @method_decorator(csrf_exempt, name="get_response")
20
1
  class CsrfExemptViewMixin:
21
- """CsrfExemptViewMixin needs to come before View in the class definition"""
22
-
23
- def get_response(self):
24
- return super().get_response()
2
+ def setup(self, *args, **kwargs):
3
+ super().setup(*args, **kwargs)
4
+ self.request.csrf_exempt = True
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.16.1
3
+ Version: 0.18.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
@@ -22,8 +22,8 @@ plain/cli/packages.py,sha256=FqRJaizaxpQ8lSS4nYNjqIGpYGDbeTmCXCvkGxusGOM,2160
22
22
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
23
23
  plain/cli/startup.py,sha256=3LIz9JrIZoF52Sa0j0SCypQwEaBDkhvuGaBdtiQLr5Q,680
24
24
  plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
25
- plain/csrf/middleware.py,sha256=pD9un9oLK6YNAEPW0rsVDsHyhyxmAA_IDE5hXs6mHiA,17776
26
- plain/csrf/views.py,sha256=YDgT451X16iUdCxpQ6rcHIy7nD0u7DAvCQl5-Mx5i9Y,219
25
+ plain/csrf/middleware.py,sha256=1szNRF-kKJdjhTw_Jw1Cp2IldtrLtbUqAApJOrVP1qc,17449
26
+ plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
27
27
  plain/forms/README.md,sha256=fglB9MmHiEgfGGdZmcRstNl6eYaFljrElu2mzapK52M,377
28
28
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
29
29
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
@@ -35,7 +35,7 @@ plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
35
35
  plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
36
36
  plain/http/multipartparser.py,sha256=k6BhpilFENQQ1cuGix6aa-jGwbhBVms2A2O01-s3_4c,27304
37
37
  plain/http/request.py,sha256=sqE83ZaB1P2i7iEUQ6d-K_6X4xhpdRdsPiTVraiDoNU,25984
38
- plain/http/response.py,sha256=QtJM4m8_imbYXSkwOAtMu9WCRZktvTsa4ZmVUAvz6bI,24250
38
+ plain/http/response.py,sha256=RR2sUG-ONWKWcZyIbztjWvtFyh0cR-CoxQvnWOyN0io,23619
39
39
  plain/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
40
40
  plain/internal/files/README.md,sha256=kMux-NU5qiH0o1K8IajYQT8VjrYl_jLk9LkGG_kGuSc,45
41
41
  plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
@@ -47,11 +47,11 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
47
47
  plain/internal/files/uploadhandler.py,sha256=eEnd5onstypjHYtg367PnVWwCaF1kAPlLPSV7goIf_E,7198
48
48
  plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
49
49
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
50
- plain/internal/handlers/base.py,sha256=I6549DBZUb-anqox7arzL9wpypH4bUCxZkitO76hljI,4794
50
+ plain/internal/handlers/base.py,sha256=_1aRNrNgqdx8kkUlXYRJNOTnZ_Z236JNYNcTdp7RRjc,4228
51
51
  plain/internal/handlers/exception.py,sha256=DH9gh1FyqgetFpMaB8yLIVE6phBTVPKQLA1GIn9MOeI,4555
52
- plain/internal/handlers/wsgi.py,sha256=EWoH9EeetfgiL0BtoAe2Aof0VWaBrxl74Y9EP4xx0wc,7553
52
+ plain/internal/handlers/wsgi.py,sha256=estA1QKHTk3ZqziWxenHsw5UO2cwPp3Zr0XjkDeM5TY,7561
53
53
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
- plain/internal/middleware/headers.py,sha256=naYsFK0phMGFf1DlRj5fAYz78R5cXToCJfuqm0cR0nI,965
54
+ plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
55
55
  plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
56
56
  plain/internal/middleware/slash.py,sha256=1rZsSSzXAA-vBb-dc2RlZaKW9gTT8YM8fJzwYGL4swA,2575
57
57
  plain/logs/README.md,sha256=H6uVXdInYlasq0Z1WnhWnPmNwYQoZ1MSLPDQ4ZE7u4A,492
@@ -90,7 +90,7 @@ plain/templates/jinja/filters.py,sha256=3KJKKbxcv9dLzUDWPcaa88k3NU2m1GG3iMIgFhzX
90
90
  plain/templates/jinja/globals.py,sha256=qhvQuikkRkOTpHSW5FwdsvoViJNlRgHq3-O7ZyeajsE,669
91
91
  plain/test/README.md,sha256=Zso3Ir7a8vQerzKB6egjROQWkpveLAbscn7VTROPAiU,37
92
92
  plain/test/__init__.py,sha256=rXe88Y602NP8DBnReSyXb7dUzKoWweLuT43j-qwOUl4,138
93
- plain/test/client.py,sha256=MYAvM1HOSfEktNEOSb9662qkhHfvnxYlY00Ckj3dL3I,31349
93
+ plain/test/client.py,sha256=wqypz5iIu28ubaEqMdx4wO7n6IPcBRkgkVXeEdr_qZg,31351
94
94
  plain/urls/README.md,sha256=pWnCvgYkWN7rG7hSyBOtX4ZUP3iO7FhqM6lvwwYll6c,33
95
95
  plain/urls/__init__.py,sha256=3UzwIufXjIks2K_X_Vms2MV19IqvyPLrXUeHU3WP47c,753
96
96
  plain/urls/base.py,sha256=Q1TzI5WvqYkN1U81fDom1q-AID4dXbszEMF6wAeTAI0,3717
@@ -101,7 +101,7 @@ plain/urls/resolvers.py,sha256=Fb2F1cdTxKDn0ZvNX81zHBglZs_aKMgnFuSIJVxNiUk,27508
101
101
  plain/utils/README.md,sha256=Bf5OG-MkOJDz_U8RGVreDfAI4M4nnPaLtk-LdinxHSc,99
102
102
  plain/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
103
103
  plain/utils/_os.py,sha256=oqfiKZRbmHwwWSZP36SIajQnFDbImbB6gcyK0idAGl4,1988
104
- plain/utils/cache.py,sha256=SM0jXxWU3XbaMPkthCsYWDYdZH5eOgGzKUgxV1ISNyE,11476
104
+ plain/utils/cache.py,sha256=3GtF8EU07LmD7CSvWSuF7YTThgeNxVkuU68wBK_35Wk,11489
105
105
  plain/utils/connection.py,sha256=NN7xRhy6qIWuOOhi1x9YdGcFcYhKepTiMUETeEMS0vY,2501
106
106
  plain/utils/crypto.py,sha256=zFDydnaqNMGYFHUc-CAn8f93685a17BhGusAcITH1lI,2662
107
107
  plain/utils/datastructures.py,sha256=g4UYTbxIb_n8F9JWMP4dHPwUz71591fHreGATPO4qEc,10240
@@ -109,7 +109,7 @@ plain/utils/dateformat.py,sha256=nsy71l16QuPN0ozGpVlCU5Et101fhk9L38F-wqT5p5I,102
109
109
  plain/utils/dateparse.py,sha256=u9_tF85YteXSjW9KQzNg_pcCEFDZS3EGorCddcWU0vE,5351
110
110
  plain/utils/dates.py,sha256=hSDKz8eIb3W5QjmGiklFZZELB0inYXsfiRUy49Cx-2Q,1226
111
111
  plain/utils/deconstruct.py,sha256=7NwEFIDCiadAArUBFmiErzDgfIgDWeKqqQFDXwSgQoQ,1830
112
- plain/utils/decorators.py,sha256=6pcFQ5TezYZoO5Ys1KnImOsv0nUMsFCdxal-S6brUOc,3468
112
+ plain/utils/decorators.py,sha256=mLHOo2jLdvYRo2z8lkeVn2vQErlj7xC6XoLwZBYf_z8,358
113
113
  plain/utils/deprecation.py,sha256=qtj33kHEmJU7mGSrNcKidZsMo5W8MN-zrXzUq3aVVy8,131
114
114
  plain/utils/duration.py,sha256=l0Gc41-DeyyAmpdy2XG-YO5UKxMf1NDpWIlQuD5hAn0,1162
115
115
  plain/utils/email.py,sha256=puRTBVuz44YvpnqV3LT4nNIKqdqfY3L8zbDJIkqHk2Y,328
@@ -130,16 +130,16 @@ plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
130
130
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
131
131
  plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
132
132
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
133
- plain/views/base.py,sha256=wMkCAbr3XqXyP8dJr-O9atA1-N6K4-cTFflLhSYGOpY,3227
134
- plain/views/csrf.py,sha256=gO9npd_Ut_LoYF_u7Qb_ZsPRfSeE3aTPG97XlMp4oEo,724
133
+ plain/views/base.py,sha256=HxOOOKZDCCZDmy2cJCEhYswVeIHZ9TukYh1zibeUn6w,3160
134
+ plain/views/csrf.py,sha256=7q6l5xzLWhRnMY64aNj0hR6G-3pxI2yhRwG6k_5j00E,144
135
135
  plain/views/errors.py,sha256=Y4oGX4Z6D2COKcDEfINvXE1acE8Ad15KwNNWPs5BCfc,967
136
136
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
137
137
  plain/views/forms.py,sha256=RhlaUcZCkeqokY_fvv-NOS-kgZAG4XhDLOPbf9K_Zlc,2691
138
138
  plain/views/objects.py,sha256=fRfS6KNehIGqkbPw4nSafj8HStxYExHmbggolBbzcxs,7921
139
139
  plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
140
140
  plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
141
- plain-0.16.1.dist-info/METADATA,sha256=GoCuu4bEkD-XaOiuI-fBcEhUpCYvl0Eb5iChNKN0AqA,2518
142
- plain-0.16.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
- plain-0.16.1.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
144
- plain-0.16.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
- plain-0.16.1.dist-info/RECORD,,
141
+ plain-0.18.0.dist-info/METADATA,sha256=Qik7zQyhLtbB8QcE6qwCar14RSDbiFaalX_6-VejaBc,2518
142
+ plain-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
143
+ plain-0.18.0.dist-info/entry_points.txt,sha256=DHHprvufgd7xypiBiqMANYRnpJ9xPPYhYbnPGwOkWqE,40
144
+ plain-0.18.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
+ plain-0.18.0.dist-info/RECORD,,
File without changes