plain 0.7.0__py3-none-any.whl → 0.8.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
@@ -9,7 +9,7 @@ import string
9
9
  from collections import defaultdict
10
10
  from urllib.parse import urlparse
11
11
 
12
- from plain.exceptions import DisallowedHost, ImproperlyConfigured
12
+ from plain.exceptions import DisallowedHost
13
13
  from plain.http import HttpHeaders, UnreadablePostError
14
14
  from plain.logs import log_response
15
15
  from plain.runtime import settings
@@ -242,44 +242,31 @@ class CsrfViewMiddleware:
242
242
  If the CSRF_USE_SESSIONS setting is false, raises InvalidTokenFormat if
243
243
  the request's secret has invalid characters or an invalid length.
244
244
  """
245
- if settings.CSRF_USE_SESSIONS:
246
- try:
247
- csrf_secret = request.session.get(CSRF_SESSION_KEY)
248
- except AttributeError:
249
- raise ImproperlyConfigured(
250
- "CSRF_USE_SESSIONS is enabled, but request.session is not "
251
- "set. SessionMiddleware must appear before CsrfViewMiddleware "
252
- "in MIDDLEWARE."
253
- )
245
+ try:
246
+ csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
247
+ except KeyError:
248
+ csrf_secret = None
254
249
  else:
255
- try:
256
- csrf_secret = request.COOKIES[settings.CSRF_COOKIE_NAME]
257
- except KeyError:
258
- csrf_secret = None
259
- else:
260
- # This can raise InvalidTokenFormat.
261
- _check_token_format(csrf_secret)
250
+ # This can raise InvalidTokenFormat.
251
+ _check_token_format(csrf_secret)
252
+
262
253
  if csrf_secret is None:
263
254
  return None
264
255
  return csrf_secret
265
256
 
266
257
  def _set_csrf_cookie(self, request, response):
267
- if settings.CSRF_USE_SESSIONS:
268
- if request.session.get(CSRF_SESSION_KEY) != request.META["CSRF_COOKIE"]:
269
- request.session[CSRF_SESSION_KEY] = request.META["CSRF_COOKIE"]
270
- else:
271
- response.set_cookie(
272
- settings.CSRF_COOKIE_NAME,
273
- request.META["CSRF_COOKIE"],
274
- max_age=settings.CSRF_COOKIE_AGE,
275
- domain=settings.CSRF_COOKIE_DOMAIN,
276
- path=settings.CSRF_COOKIE_PATH,
277
- secure=settings.CSRF_COOKIE_SECURE,
278
- httponly=settings.CSRF_COOKIE_HTTPONLY,
279
- samesite=settings.CSRF_COOKIE_SAMESITE,
280
- )
281
- # Set the Vary header since content varies with the CSRF cookie.
282
- patch_vary_headers(response, ("Cookie",))
258
+ response.set_cookie(
259
+ settings.CSRF_COOKIE_NAME,
260
+ request.META["CSRF_COOKIE"],
261
+ max_age=settings.CSRF_COOKIE_AGE,
262
+ domain=settings.CSRF_COOKIE_DOMAIN,
263
+ path=settings.CSRF_COOKIE_PATH,
264
+ secure=settings.CSRF_COOKIE_SECURE,
265
+ httponly=settings.CSRF_COOKIE_HTTPONLY,
266
+ samesite=settings.CSRF_COOKIE_SAMESITE,
267
+ )
268
+ # Set the Vary header since content varies with the CSRF cookie.
269
+ patch_vary_headers(response, ("Cookie",))
283
270
 
284
271
  def _origin_verified(self, request):
285
272
  request_origin = request.META["HTTP_ORIGIN"]
@@ -289,7 +276,7 @@ class CsrfViewMiddleware:
289
276
  pass
290
277
  else:
291
278
  good_origin = "{}://{}".format(
292
- "https" if request.is_secure() else "http",
279
+ "https" if request.is_https() else "http",
293
280
  good_host,
294
281
  )
295
282
  if request_origin == good_origin:
@@ -331,11 +318,7 @@ class CsrfViewMiddleware:
331
318
  ):
332
319
  return
333
320
  # Allow matching the configured cookie domain.
334
- good_referer = (
335
- settings.SESSION_COOKIE_DOMAIN
336
- if settings.CSRF_USE_SESSIONS
337
- else settings.CSRF_COOKIE_DOMAIN
338
- )
321
+ good_referer = settings.CSRF_COOKIE_DOMAIN
339
322
  if good_referer is None:
340
323
  # If no cookie domain is configured, allow matching the current
341
324
  # host:port exactly if it's permitted by ALLOWED_HOSTS.
@@ -435,7 +418,7 @@ class CsrfViewMiddleware:
435
418
  return self._reject(
436
419
  request, REASON_BAD_ORIGIN % request.META["HTTP_ORIGIN"]
437
420
  )
438
- elif request.is_secure():
421
+ elif request.is_https():
439
422
  # If the Origin header wasn't provided, reject HTTPS requests if
440
423
  # the Referer header doesn't match an allowed value.
441
424
  #
plain/http/request.py CHANGED
@@ -140,7 +140,7 @@ class HttpRequest:
140
140
  # Reconstruct the host using the algorithm from PEP 333.
141
141
  host = self.META["SERVER_NAME"]
142
142
  server_port = self.get_port()
143
- if server_port != ("443" if self.is_secure() else "80"):
143
+ if server_port != ("443" if self.is_https() else "80"):
144
144
  host = f"{host}:{server_port}"
145
145
  return host
146
146
 
@@ -281,7 +281,7 @@ class HttpRequest:
281
281
  return "https" if header_value.strip() == secure_value else "http"
282
282
  return self._get_scheme()
283
283
 
284
- def is_secure(self):
284
+ def is_https(self):
285
285
  return self.scheme == "https"
286
286
 
287
287
  @property
@@ -13,6 +13,15 @@ from .exception import convert_exception_to_response
13
13
  logger = logging.getLogger("plain.request")
14
14
 
15
15
 
16
+ # These middleware classes are always used by Plain.
17
+ BUILTIN_MIDDLEWARE = [
18
+ "plain.internal.middleware.headers.DefaultHeadersMiddleware",
19
+ "plain.internal.middleware.https.HttpsRedirectMiddleware",
20
+ "plain.internal.middleware.slash.RedirectSlashMiddleware",
21
+ "plain.csrf.middleware.CsrfViewMiddleware",
22
+ ]
23
+
24
+
16
25
  class BaseHandler:
17
26
  _view_middleware = None
18
27
  _middleware_chain = None
@@ -27,7 +36,10 @@ class BaseHandler:
27
36
 
28
37
  get_response = self._get_response
29
38
  handler = convert_exception_to_response(get_response)
30
- for middleware_path in reversed(settings.MIDDLEWARE):
39
+
40
+ middlewares = reversed(BUILTIN_MIDDLEWARE + settings.MIDDLEWARE)
41
+
42
+ for middleware_path in middlewares:
31
43
  middleware = import_string(middleware_path)
32
44
  mw_instance = middleware(handler)
33
45
 
@@ -0,0 +1,19 @@
1
+ from plain.runtime import settings
2
+
3
+
4
+ class DefaultHeadersMiddleware:
5
+ def __init__(self, get_response):
6
+ self.get_response = get_response
7
+
8
+ def __call__(self, request):
9
+ response = self.get_response(request)
10
+
11
+ for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
12
+ response.headers.setdefault(header, value)
13
+
14
+ # Add the Content-Length header to non-streaming responses if not
15
+ # already set.
16
+ if not response.streaming and not response.has_header("Content-Length"):
17
+ response.headers["Content-Length"] = str(len(response.content))
18
+
19
+ return response
@@ -0,0 +1,36 @@
1
+ import re
2
+
3
+ from plain.http import ResponsePermanentRedirect
4
+ from plain.runtime import settings
5
+
6
+
7
+ class HttpsRedirectMiddleware:
8
+ def __init__(self, get_response):
9
+ self.get_response = get_response
10
+
11
+ # Settings for https (compile regexes once)
12
+ self.https_redirect_enabled = settings.HTTPS_REDIRECT_ENABLED
13
+ self.https_redirect_host = settings.HTTPS_REDIRECT_HOST
14
+ self.https_redirect_exempt = [
15
+ re.compile(r) for r in settings.HTTPS_REDIRECT_EXEMPT
16
+ ]
17
+
18
+ def __call__(self, request):
19
+ """
20
+ Rewrite the URL based on settings.APPEND_SLASH
21
+ """
22
+
23
+ if redirect_response := self.maybe_https_redirect(request):
24
+ return redirect_response
25
+
26
+ return self.get_response(request)
27
+
28
+ def maybe_https_redirect(self, request):
29
+ path = request.path.lstrip("/")
30
+ if (
31
+ self.https_redirect_enabled
32
+ and not request.is_https()
33
+ and not any(pattern.search(path) for pattern in self.https_redirect_exempt)
34
+ ):
35
+ host = self.https_redirect_host or request.get_host()
36
+ return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
@@ -4,25 +4,7 @@ from plain.urls import is_valid_path
4
4
  from plain.utils.http import escape_leading_slashes
5
5
 
6
6
 
7
- class CommonMiddleware:
8
- """
9
- "Common" middleware for taking care of some basic operations:
10
-
11
- - URL rewriting: Based on the APPEND_SLASH setting,
12
- append missing slashes.
13
-
14
- - If APPEND_SLASH is set and the initial URL doesn't end with a
15
- slash, and it is not found in urlpatterns, form a new URL by
16
- appending a slash at the end. If this new URL is found in
17
- urlpatterns, return an HTTP redirect to this new URL; otherwise
18
- process the initial URL as usual.
19
-
20
- This behavior can be customized by subclassing CommonMiddleware and
21
- overriding the response_redirect_class attribute.
22
- """
23
-
24
- response_redirect_class = ResponsePermanentRedirect
25
-
7
+ class RedirectSlashMiddleware:
26
8
  def __init__(self, get_response):
27
9
  self.get_response = get_response
28
10
 
@@ -40,12 +22,7 @@ class CommonMiddleware:
40
22
  # If the given URL is "Not Found", then check if we should redirect to
41
23
  # a path with a slash appended.
42
24
  if response.status_code == 404 and self.should_redirect_with_slash(request):
43
- return self.response_redirect_class(self.get_full_path_with_slash(request))
44
-
45
- # Add the Content-Length header to non-streaming responses if not
46
- # already set.
47
- if not response.streaming and not response.has_header("Content-Length"):
48
- response.headers["Content-Length"] = str(len(response.content))
25
+ return ResponsePermanentRedirect(self.get_full_path_with_slash(request))
49
26
 
50
27
  return response
51
28
 
@@ -16,17 +16,18 @@ SECRET_KEY_WARNING_MSG = (
16
16
  f"vulnerable to attack."
17
17
  )
18
18
 
19
+ # TODO
19
20
  W001 = Warning(
20
- "You do not have 'plain.middleware.security.SecurityMiddleware' "
21
+ "You do not have 'plain.middleware.https.HttpsRedirectMiddleware' "
21
22
  "in your MIDDLEWARE so the SECURE_HSTS_SECONDS, "
22
23
  "SECURE_CONTENT_TYPE_NOSNIFF, SECURE_REFERRER_POLICY, "
23
- "SECURE_CROSS_ORIGIN_OPENER_POLICY, and SECURE_SSL_REDIRECT settings will "
24
+ "SECURE_CROSS_ORIGIN_OPENER_POLICY, and HTTPS_REDIRECT_ENABLED settings will "
24
25
  "have no effect.",
25
26
  id="security.W001",
26
27
  )
27
28
 
28
29
  W008 = Warning(
29
- "Your SECURE_SSL_REDIRECT setting is not set to True. "
30
+ "Your HTTPS_REDIRECT_ENABLED setting is not set to True. "
30
31
  "Unless your site should be available over both SSL and non-SSL "
31
32
  "connections, you may want to either set this setting True "
32
33
  "or configure a load balancer or reverse-proxy server "
@@ -52,22 +53,6 @@ W020 = Warning(
52
53
  W025 = Warning(SECRET_KEY_WARNING_MSG, id="security.W025")
53
54
 
54
55
 
55
- def _security_middleware():
56
- return "plain.middleware.security.SecurityMiddleware" in settings.MIDDLEWARE
57
-
58
-
59
- @register(deploy=True)
60
- def check_security_middleware(package_configs, **kwargs):
61
- passed_check = _security_middleware()
62
- return [] if passed_check else [W001]
63
-
64
-
65
- @register(deploy=True)
66
- def check_ssl_redirect(package_configs, **kwargs):
67
- passed_check = not _security_middleware() or settings.SECURE_SSL_REDIRECT is True
68
- return [] if passed_check else [W008]
69
-
70
-
71
56
  def _check_secret_key(secret_key):
72
57
  return (
73
58
  len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS
@@ -32,9 +32,5 @@ def check_csrf_middleware(package_configs, **kwargs):
32
32
 
33
33
  @register(deploy=True)
34
34
  def check_csrf_cookie_secure(package_configs, **kwargs):
35
- passed_check = (
36
- settings.CSRF_USE_SESSIONS
37
- or not _csrf_middleware()
38
- or settings.CSRF_COOKIE_SECURE is True
39
- )
35
+ passed_check = not _csrf_middleware() or settings.CSRF_COOKIE_SECURE is True
40
36
  return [] if passed_check else [W016]
plain/runtime/README.md CHANGED
@@ -54,10 +54,7 @@ SECRET_KEY = environ["SECRET_KEY"]
54
54
  DEBUG = environ.get("DEBUG", "false").lower() in ("true", "1", "yes")
55
55
 
56
56
  MIDDLEWARE = [
57
- "plain.middleware.security.SecurityMiddleware",
58
57
  "plain.sessions.middleware.SessionMiddleware",
59
- "plain.middleware.common.CommonMiddleware",
60
- "plain.csrf.middleware.CsrfViewMiddleware",
61
58
  "plain.auth.middleware.AuthenticationMiddleware",
62
59
  ]
63
60
 
@@ -29,11 +29,27 @@ TIME_ZONE: str = "UTC"
29
29
  DEFAULT_CHARSET = "utf-8"
30
30
 
31
31
  # List of strings representing installed packages.
32
- INSTALLED_PACKAGES: list = []
32
+ INSTALLED_PACKAGES: list[str] = []
33
33
 
34
34
  # Whether to append trailing slashes to URLs.
35
35
  APPEND_SLASH = True
36
36
 
37
+ # Default headers for all responses.
38
+ DEFAULT_RESPONSE_HEADERS = {
39
+ # "Content-Security-Policy": "default-src 'self'",
40
+ # https://hstspreload.org/
41
+ # "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
42
+ "Cross-Origin-Opener-Policy": "same-origin",
43
+ "Referrer-Policy": "same-origin",
44
+ "X-Content-Type-Options": "nosniff",
45
+ "X-Frame-Options": "DENY",
46
+ }
47
+
48
+ # Whether to redirect all non-HTTPS requests to HTTPS.
49
+ HTTPS_REDIRECT_ENABLED = True
50
+ HTTPS_REDIRECT_EXEMPT = []
51
+ HTTPS_REDIRECT_HOST = None
52
+
37
53
  # A secret key for this particular Plain installation. Used in secret-key
38
54
  # hashing algorithms. Set this in your settings, or Plain will complain
39
55
  # loudly.
@@ -82,7 +98,7 @@ HTTP_ERROR_VIEWS: dict[int] = {}
82
98
  # connections, AND that proxy ensures that user-submitted headers with the
83
99
  # same name are ignored (so that people can't spoof it), set this value to
84
100
  # a tuple of (header_name, header_value). For any requests that come in with
85
- # that header/value, request.is_secure() will return True.
101
+ # that header/value, request.is_https() will return True.
86
102
  # WARNING! Only set this if you fully understand what you're doing. Otherwise,
87
103
  # you may be opening yourself up to a security risk.
88
104
  SECURE_PROXY_SSL_HEADER = None
@@ -94,11 +110,7 @@ SECURE_PROXY_SSL_HEADER = None
94
110
  # List of middleware to use. Order is important; in the request phase, these
95
111
  # middleware will be applied in the order given, and in the response
96
112
  # phase the middleware will be applied in reverse order.
97
- MIDDLEWARE = [
98
- "plain.middleware.security.SecurityMiddleware",
99
- "plain.middleware.common.CommonMiddleware",
100
- "plain.csrf.middleware.CsrfViewMiddleware",
101
- ]
113
+ MIDDLEWARE: list[str] = []
102
114
 
103
115
  ###########
104
116
  # SIGNING #
@@ -120,7 +132,6 @@ CSRF_COOKIE_HTTPONLY = False
120
132
  CSRF_COOKIE_SAMESITE = "Lax"
121
133
  CSRF_HEADER_NAME = "HTTP_X_CSRFTOKEN"
122
134
  CSRF_TRUSTED_ORIGINS: list[str] = []
123
- CSRF_USE_SESSIONS = False
124
135
 
125
136
  ###########
126
137
  # LOGGING #
@@ -150,23 +161,6 @@ ASSETS_BASE_URL: str = ""
150
161
  # message, but Plain will not stop you from e.g. running server.
151
162
  SILENCED_PREFLIGHT_CHECKS = []
152
163
 
153
- #######################
154
- # SECURITY MIDDLEWARE #
155
- #######################
156
- SECURE_REDIRECT_EXEMPT = []
157
- SECURE_SSL_HOST = None
158
- SECURE_SSL_REDIRECT = True
159
-
160
- SECURE_DEFAULT_HEADERS = {
161
- # "Content-Security-Policy": "default-src 'self'",
162
- # https://hstspreload.org/
163
- # "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
164
- "Cross-Origin-Opener-Policy": "same-origin",
165
- "Referrer-Policy": "same-origin",
166
- "X-Content-Type-Options": "nosniff",
167
- "X-Frame-Options": "DENY",
168
- }
169
-
170
164
  #############
171
165
  # Templates #
172
166
  #############
plain/test/client.py CHANGED
@@ -379,7 +379,7 @@ class RequestFactory:
379
379
  # Refs comment in `get_bytes_from_wsgi()`.
380
380
  return path.decode("iso-8859-1")
381
381
 
382
- def get(self, path, data=None, secure=False, *, headers=None, **extra):
382
+ def get(self, path, data=None, secure=True, *, headers=None, **extra):
383
383
  """Construct a GET request."""
384
384
  data = {} if data is None else data
385
385
  return self.generic(
@@ -398,7 +398,7 @@ class RequestFactory:
398
398
  path,
399
399
  data=None,
400
400
  content_type=MULTIPART_CONTENT,
401
- secure=False,
401
+ secure=True,
402
402
  *,
403
403
  headers=None,
404
404
  **extra,
@@ -417,7 +417,7 @@ class RequestFactory:
417
417
  **extra,
418
418
  )
419
419
 
420
- def head(self, path, data=None, secure=False, *, headers=None, **extra):
420
+ def head(self, path, data=None, secure=True, *, headers=None, **extra):
421
421
  """Construct a HEAD request."""
422
422
  data = {} if data is None else data
423
423
  return self.generic(
@@ -431,7 +431,7 @@ class RequestFactory:
431
431
  },
432
432
  )
433
433
 
434
- def trace(self, path, secure=False, *, headers=None, **extra):
434
+ def trace(self, path, secure=True, *, headers=None, **extra):
435
435
  """Construct a TRACE request."""
436
436
  return self.generic("TRACE", path, secure=secure, headers=headers, **extra)
437
437
 
@@ -440,7 +440,7 @@ class RequestFactory:
440
440
  path,
441
441
  data="",
442
442
  content_type="application/octet-stream",
443
- secure=False,
443
+ secure=True,
444
444
  *,
445
445
  headers=None,
446
446
  **extra,
@@ -455,7 +455,7 @@ class RequestFactory:
455
455
  path,
456
456
  data="",
457
457
  content_type="application/octet-stream",
458
- secure=False,
458
+ secure=True,
459
459
  *,
460
460
  headers=None,
461
461
  **extra,
@@ -471,7 +471,7 @@ class RequestFactory:
471
471
  path,
472
472
  data="",
473
473
  content_type="application/octet-stream",
474
- secure=False,
474
+ secure=True,
475
475
  *,
476
476
  headers=None,
477
477
  **extra,
@@ -487,7 +487,7 @@ class RequestFactory:
487
487
  path,
488
488
  data="",
489
489
  content_type="application/octet-stream",
490
- secure=False,
490
+ secure=True,
491
491
  *,
492
492
  headers=None,
493
493
  **extra,
@@ -504,7 +504,7 @@ class RequestFactory:
504
504
  path,
505
505
  data="",
506
506
  content_type="application/octet-stream",
507
- secure=False,
507
+ secure=True,
508
508
  *,
509
509
  headers=None,
510
510
  **extra,
@@ -704,7 +704,7 @@ class Client(ClientMixin, RequestFactory):
704
704
  path,
705
705
  data=None,
706
706
  follow=False,
707
- secure=False,
707
+ secure=True,
708
708
  *,
709
709
  headers=None,
710
710
  **extra,
@@ -725,7 +725,7 @@ class Client(ClientMixin, RequestFactory):
725
725
  data=None,
726
726
  content_type=MULTIPART_CONTENT,
727
727
  follow=False,
728
- secure=False,
728
+ secure=True,
729
729
  *,
730
730
  headers=None,
731
731
  **extra,
@@ -752,7 +752,7 @@ class Client(ClientMixin, RequestFactory):
752
752
  path,
753
753
  data=None,
754
754
  follow=False,
755
- secure=False,
755
+ secure=True,
756
756
  *,
757
757
  headers=None,
758
758
  **extra,
@@ -775,7 +775,7 @@ class Client(ClientMixin, RequestFactory):
775
775
  data="",
776
776
  content_type="application/octet-stream",
777
777
  follow=False,
778
- secure=False,
778
+ secure=True,
779
779
  *,
780
780
  headers=None,
781
781
  **extra,
@@ -803,7 +803,7 @@ class Client(ClientMixin, RequestFactory):
803
803
  data="",
804
804
  content_type="application/octet-stream",
805
805
  follow=False,
806
- secure=False,
806
+ secure=True,
807
807
  *,
808
808
  headers=None,
809
809
  **extra,
@@ -831,7 +831,7 @@ class Client(ClientMixin, RequestFactory):
831
831
  data="",
832
832
  content_type="application/octet-stream",
833
833
  follow=False,
834
- secure=False,
834
+ secure=True,
835
835
  *,
836
836
  headers=None,
837
837
  **extra,
@@ -859,7 +859,7 @@ class Client(ClientMixin, RequestFactory):
859
859
  data="",
860
860
  content_type="application/octet-stream",
861
861
  follow=False,
862
- secure=False,
862
+ secure=True,
863
863
  *,
864
864
  headers=None,
865
865
  **extra,
@@ -886,7 +886,7 @@ class Client(ClientMixin, RequestFactory):
886
886
  path,
887
887
  data="",
888
888
  follow=False,
889
- secure=False,
889
+ secure=True,
890
890
  *,
891
891
  headers=None,
892
892
  **extra,
plain/views/base.py CHANGED
@@ -15,6 +15,10 @@ logger = logging.getLogger("plain.request")
15
15
 
16
16
 
17
17
  class View:
18
+ request: HttpRequest
19
+ url_args: tuple
20
+ url_kwargs: dict
21
+
18
22
  def __init__(self, *args, **kwargs) -> None:
19
23
  # Views can customize their init, which receives
20
24
  # the args and kwargs from as_view()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: plain
3
- Version: 0.7.0
3
+ Version: 0.8.0
4
4
  Summary: A web framework for building products with Python.
5
5
  Author: Dave Gaeddert
6
6
  Author-email: dave.gaeddert@dropseed.dev
@@ -8,6 +8,7 @@ Requires-Python: >=3.11,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
11
12
  Requires-Dist: click (>=8.0.0)
12
13
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
13
14
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
@@ -15,7 +15,7 @@ plain/cli/packages.py,sha256=69VH1bIi1-5N5l2jlBcR5EP0pt-v16sPar9arO3gCSE,2052
15
15
  plain/cli/print.py,sha256=XraUYrgODOJquIiEv78wSCYGRBplHXtXSS9QtFG5hqY,217
16
16
  plain/cli/startup.py,sha256=PJYA-tNWGia-QbTlT0e5HvC8C7yDSq8wkAkIxgfKkvw,680
17
17
  plain/csrf/README.md,sha256=RXMWMtHmzf30gVVNOfj0kD4xlSqFIPgJh-n7dIciaEM,163
18
- plain/csrf/middleware.py,sha256=I7Ev-Y-VaR-Fgs_arVYNePt4qjwF_PozFBCqER2UMIY,18642
18
+ plain/csrf/middleware.py,sha256=MlDQ55B4eRXySbzauFNs8gKhgQy32yWspBfPI0a3PzA,17775
19
19
  plain/csrf/views.py,sha256=YDgT451X16iUdCxpQ6rcHIy7nD0u7DAvCQl5-Mx5i9Y,219
20
20
  plain/debug.py,sha256=fdrWy4RNQOuXo80_jgwthCkMZKjjaF9lDj3Kqln_gJk,604
21
21
  plain/exceptions.py,sha256=tDS6l0epe_L9IlxpEdT2k2hWgEoAu8YBNIumNCtJ-WY,6333
@@ -29,7 +29,7 @@ plain/http/README.md,sha256=00zLFQ-FPjYXu3A8QsLhCCXxaT0ImvI5I-8xd3dp8WA,7
29
29
  plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
30
30
  plain/http/cookie.py,sha256=11FnSG3Plo6T3jZDbPoCw7SKh9ExdBio3pTmIO03URg,597
31
31
  plain/http/multipartparser.py,sha256=Z2PFDuGucj_nFnQagwdxowJcZHqzCfDApkXl5yRlRe4,27325
32
- plain/http/request.py,sha256=sRHjXOrxFMPIXSbwa6-hXJHNhR-EF4E5IprGtHUqeyE,26007
32
+ plain/http/request.py,sha256=CrfXx-Som5AOM5WU62CTuv01VpFTz_qMLQS1Jx9Rwew,26005
33
33
  plain/http/response.py,sha256=h43Gx4PVPGEf63EHHrABYtwYu-8Y9mgAebwiGt8qeLE,24074
34
34
  plain/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  plain/internal/files/README.md,sha256=kMux-NU5qiH0o1K8IajYQT8VjrYl_jLk9LkGG_kGuSc,45
@@ -42,20 +42,19 @@ plain/internal/files/uploadedfile.py,sha256=JRB7T3quQjg-1y3l1ASPxywtSQZhaeMc45uF
42
42
  plain/internal/files/uploadhandler.py,sha256=BZGQDHJMEUeBh9uJtxNVWQkFmHE7jzVTx9CLVt59Jqg,7197
43
43
  plain/internal/files/utils.py,sha256=XrHAs2tMqmywURgz5C6-GSj6sr2R-MCERcWT8yzBp5k,2652
44
44
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
- plain/internal/handlers/base.py,sha256=LcKKRtKlBk17Iy_8MRbx7wskULpnhHzA0fOlunzd9Lo,4506
45
+ plain/internal/handlers/base.py,sha256=tpTrVhC_gZKrIoTJmCWD3bIpucOCGVV1DTkF0W2HZPI,4883
46
46
  plain/internal/handlers/exception.py,sha256=KUZSBzmzE6YSFxAZ336Mye_9vAPVIj9Av-w1SK5R4PA,4579
47
47
  plain/internal/handlers/wsgi.py,sha256=WIZvXlEAOn8lxwDM_HpSP82-ePKVu-Tzgpe65KkXEMk,7538
48
+ plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ plain/internal/middleware/headers.py,sha256=UnJWnlVVLD-10h7PB_QSYeREBzLo-TS3C-_ahmZ6w0I,636
50
+ plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
51
+ plain/internal/middleware/slash.py,sha256=LhQi5aUztE4kJnvRn75u8zaFvAVPPEl_Whu1gYWGs7g,2656
48
52
  plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
49
53
  plain/logs/README.md,sha256=H6uVXdInYlasq0Z1WnhWnPmNwYQoZ1MSLPDQ4ZE7u4A,492
50
54
  plain/logs/__init__.py,sha256=rASvo4qFBDIHfkACmGLNGa6lRGbG9PbNjW6FmBt95ys,168
51
55
  plain/logs/configure.py,sha256=6mV7d1IxkDYT3VBz61qhIj0Esuy5l5QdQfsHaGCfI6w,1063
52
56
  plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
53
57
  plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
54
- plain/middleware/README.md,sha256=MgiLHwAfP8ooBSlDi1JhTwIHMlwphOqAkeWglYRbe8s,52
55
- plain/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
- plain/middleware/common.py,sha256=-YySkYUyaRujYA5Yg7GRD3xFjlQOZpeJP1Stpt6pias,3631
57
- plain/middleware/gzip.py,sha256=2NogLO6hPxVc3otxkhMDl7-r2Zw3vcIkAP29fx4j2eU,2383
58
- plain/middleware/security.py,sha256=WZRn5F9qx33wFTqh4CkBEtHrTuyr7RCt4Gwq4W2mBgE,1043
59
58
  plain/packages/README.md,sha256=Vq1Nw3mmEmZ2IriQavuVi4BjcQC2nb8k7YIbnm8QjIg,799
60
59
  plain/packages/__init__.py,sha256=DnHN1wwHXiXib4Y9BV__x9WrbUaTovoTIxW-tVyScTU,106
61
60
  plain/packages/config.py,sha256=6Vdf1TEQllZkkEvK0WK__zHJYT9nxmS3EyYrbuq0GkM,11201
@@ -67,12 +66,12 @@ plain/preflight/files.py,sha256=wbHCNgps7o1c1zQNBd8FDCaVaqX90UwuvLgEQ_DbUpY,510
67
66
  plain/preflight/messages.py,sha256=u0oc7q7YmBlKYJRcF5SQpzncfOkEzDhZTcpyclQDfHg,2427
68
67
  plain/preflight/registry.py,sha256=ZpxnZPIklXuT8xZVTxCUp_IER3zhd7DdfsmqIpAbLj4,2306
69
68
  plain/preflight/security/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
- plain/preflight/security/base.py,sha256=EY9rgXi8qdzLY1mMq9lMqYJmIV2OhN66Vt96meblxoE,3541
71
- plain/preflight/security/csrf.py,sha256=EZy_DkVqc1kUmBA-UbNmhVsKhRINfmqgWSRlatKy5AA,1237
69
+ plain/preflight/security/base.py,sha256=nsv-g-bFr_188mkOQwC1ZDnyS0rE6eZED8xZT-FEM8M,3074
70
+ plain/preflight/security/csrf.py,sha256=8dKzs5kQwTTKeyfHbkrzdPk3OEoUN8mc-0xhSBo1KmM,1175
72
71
  plain/preflight/urls.py,sha256=O4PQ_v205VA2872fQlhPfxaihDDRCsVp0ZVKQ92aX4k,3019
73
- plain/runtime/README.md,sha256=wR0XrWAb4FGnOON2-O9ySSZCSMZmPkKGx-4DQXd_2h4,2209
72
+ plain/runtime/README.md,sha256=Q8VVO7JRGuYrDxzuYL6ptoilhclbecxKzpRXKgbWGkU,2061
74
73
  plain/runtime/__init__.py,sha256=vIh77lL4e5CoQZz-HxvXeJL5329s8VfPFpNxx9rHgxs,1384
75
- plain/runtime/global_settings.py,sha256=cVwGcdpDFf2OoYeGD3Lfv-mxGF48aKSGzZlLKwp8UKI,5566
74
+ plain/runtime/global_settings.py,sha256=3SLuQjPAHFv5F6QJBiuVy41yxsAoI446Rp4FoK8_A6o,5434
76
75
  plain/runtime/user_settings.py,sha256=JhxmCCOmEMk0QHh82l5iTpEie-UdZh13aXGsLhE5PBw,11255
77
76
  plain/signals/README.md,sha256=cd3tKEgH-xc88CUWyDxl4-qv-HBXx8VT32BXVwA5azA,230
78
77
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
@@ -91,7 +90,7 @@ plain/templates/jinja/filters.py,sha256=3KJKKbxcv9dLzUDWPcaa88k3NU2m1GG3iMIgFhzX
91
90
  plain/templates/jinja/globals.py,sha256=qhvQuikkRkOTpHSW5FwdsvoViJNlRgHq3-O7ZyeajsE,669
92
91
  plain/test/README.md,sha256=Zso3Ir7a8vQerzKB6egjROQWkpveLAbscn7VTROPAiU,37
93
92
  plain/test/__init__.py,sha256=rXe88Y602NP8DBnReSyXb7dUzKoWweLuT43j-qwOUl4,138
94
- plain/test/client.py,sha256=cu43S-NL606VERUYi7NjvzIrVchlNLHqcKro7pnYmS4,31385
93
+ plain/test/client.py,sha256=470yny2wfLEebdVjQckBqC9pqyDkHy8e0EH-rlVjsAQ,31368
95
94
  plain/urls/README.md,sha256=pWnCvgYkWN7rG7hSyBOtX4ZUP3iO7FhqM6lvwwYll6c,33
96
95
  plain/urls/__init__.py,sha256=3UzwIufXjIks2K_X_Vms2MV19IqvyPLrXUeHU3WP47c,753
97
96
  plain/urls/base.py,sha256=ECaOCEXs1ygKn4k1mt5XxSNPNlg5raJvx0aPaj7DFfE,3719
@@ -133,7 +132,7 @@ plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
133
132
  plain/validators.py,sha256=L9v9KtTe4iZhZVramZdKGf33R5Tt95FCdg2AJD2-2n0,19963
134
133
  plain/views/README.md,sha256=qndsXKyNMnipPlLaAvgQeGxqXknNQwlFh31Yxk8rHp8,5994
135
134
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
136
- plain/views/base.py,sha256=WQy5btO4NNkEQAleII57oXUaDdd76a7OnFGdK7uFook,3160
135
+ plain/views/base.py,sha256=wMkCAbr3XqXyP8dJr-O9atA1-N6K4-cTFflLhSYGOpY,3227
137
136
  plain/views/csrf.py,sha256=gO9npd_Ut_LoYF_u7Qb_ZsPRfSeE3aTPG97XlMp4oEo,724
138
137
  plain/views/errors.py,sha256=Y4oGX4Z6D2COKcDEfINvXE1acE8Ad15KwNNWPs5BCfc,967
139
138
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
@@ -142,8 +141,8 @@ plain/views/objects.py,sha256=9QBYyb8PgkRirXCQ8-Pms4_yMzP37dfeL30hWRYmtZg,7909
142
141
  plain/views/redirect.py,sha256=KLnlktzK6ZNMTlaEiZpMKQMEP5zeTgGLJ9BIkIJfwBo,1733
143
142
  plain/views/templates.py,sha256=nF9CcdhhjAyp3LB0RrSYnBaHpHzMfPSw719RCdcXk7o,2007
144
143
  plain/wsgi.py,sha256=R6k5FiAElvGDApEbMPTT0MPqSD7n2e2Az5chQqJZU0I,236
145
- plain-0.7.0.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
146
- plain-0.7.0.dist-info/METADATA,sha256=ILizFFak8iFUNe3VI8xf3UtKZsz2XB8jmg3zWm2QGS0,2716
147
- plain-0.7.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
148
- plain-0.7.0.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
149
- plain-0.7.0.dist-info/RECORD,,
144
+ plain-0.8.0.dist-info/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
145
+ plain-0.8.0.dist-info/METADATA,sha256=asx3_JWRlV7rkPuphIVufUKrMXEhYtQmJ-Dd6yVQdzY,2767
146
+ plain-0.8.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
147
+ plain-0.8.0.dist-info/entry_points.txt,sha256=7O1RZTmMasKYB73bfqQcTwIhsXo7RjEIKv2WbtTtOIM,39
148
+ plain-0.8.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.9.0
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,3 +0,0 @@
1
- # Middleware
2
-
3
- Hook into the request/response cycle.
plain/middleware/gzip.py DELETED
@@ -1,64 +0,0 @@
1
- from plain.utils.cache import patch_vary_headers
2
- from plain.utils.regex_helper import _lazy_re_compile
3
- from plain.utils.text import compress_sequence, compress_string
4
-
5
- re_accepts_gzip = _lazy_re_compile(r"\bgzip\b")
6
-
7
-
8
- class GZipMiddleware:
9
- """
10
- Compress content if the browser allows gzip compression.
11
- Set the Vary header accordingly, so that caches will base their storage
12
- on the Accept-Encoding header.
13
- """
14
-
15
- max_random_bytes = 100
16
-
17
- def __init__(self, get_response):
18
- self.get_response = get_response
19
-
20
- def __call__(self, request):
21
- response = self.get_response(request)
22
-
23
- # It's not worth attempting to compress really short responses.
24
- if not response.streaming and len(response.content) < 200:
25
- return response
26
-
27
- # Avoid gzipping if we've already got a content-encoding.
28
- if response.has_header("Content-Encoding"):
29
- return response
30
-
31
- patch_vary_headers(response, ("Accept-Encoding",))
32
-
33
- ae = request.META.get("HTTP_ACCEPT_ENCODING", "")
34
- if not re_accepts_gzip.search(ae):
35
- return response
36
-
37
- if response.streaming:
38
- response.streaming_content = compress_sequence(
39
- response.streaming_content,
40
- max_random_bytes=self.max_random_bytes,
41
- )
42
- # Delete the `Content-Length` header for streaming content, because
43
- # we won't know the compressed size until we stream it.
44
- del response.headers["Content-Length"]
45
- else:
46
- # Return the compressed content only if it's actually shorter.
47
- compressed_content = compress_string(
48
- response.content,
49
- max_random_bytes=self.max_random_bytes,
50
- )
51
- if len(compressed_content) >= len(response.content):
52
- return response
53
- response.content = compressed_content
54
- response.headers["Content-Length"] = str(len(response.content))
55
-
56
- # If there is a strong ETag, make it weak to fulfill the requirements
57
- # of RFC 9110 Section 8.8.1 while also allowing conditional request
58
- # matches on ETags.
59
- etag = response.get("ETag")
60
- if etag and etag.startswith('"'):
61
- response.headers["ETag"] = "W/" + etag
62
- response.headers["Content-Encoding"] = "gzip"
63
-
64
- return response
@@ -1,31 +0,0 @@
1
- import re
2
-
3
- from plain.http import ResponsePermanentRedirect
4
- from plain.runtime import settings
5
-
6
-
7
- class SecurityMiddleware:
8
- def __init__(self, get_response):
9
- self.get_response = get_response
10
- self.redirect = settings.SECURE_SSL_REDIRECT
11
- self.redirect_host = settings.SECURE_SSL_HOST
12
- self.redirect_exempt = [re.compile(r) for r in settings.SECURE_REDIRECT_EXEMPT]
13
-
14
- self.default_headers = settings.SECURE_DEFAULT_HEADERS
15
-
16
- def __call__(self, request):
17
- path = request.path.lstrip("/")
18
- if (
19
- self.redirect
20
- and not request.is_secure()
21
- and not any(pattern.search(path) for pattern in self.redirect_exempt)
22
- ):
23
- host = self.redirect_host or request.get_host()
24
- return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
25
-
26
- response = self.get_response(request)
27
-
28
- for header, value in self.default_headers.items():
29
- response.headers.setdefault(header, value)
30
-
31
- return response
File without changes
File without changes