plain 0.56.1__py3-none-any.whl → 0.57.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plain/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.57.0](https://github.com/dropseed/plain/releases/plain@0.57.0) (2025-08-15)
4
+
5
+ ### What's changed
6
+
7
+ - The `ResponsePermanentRedirect` class has been removed; use `ResponseRedirect` with `status_code=301` instead ([d5735ea](https://github.com/dropseed/plain/commit/d5735ea4f8))
8
+ - The `RedirectView.permanent` attribute has been replaced with `status_code` for more flexible redirect status codes ([12dda16](https://github.com/dropseed/plain/commit/12dda16731))
9
+ - Updated `RedirectView` initialization parameters: `url_name` replaces `pattern_name`, `preserve_query_params` replaces `query_string`, and removed 410 Gone functionality ([3b9ca71](https://github.com/dropseed/plain/commit/3b9ca713bf))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - Replace `ResponsePermanentRedirect` imports with `ResponseRedirect` and pass `status_code=301` to the constructor
14
+ - Update `RedirectView` subclasses to use `status_code=301` instead of `permanent=True`
15
+ - Replace `pattern_name` with `url_name` in RedirectView usage
16
+ - Replace `query_string=True` with `preserve_query_params=True` in RedirectView usage
17
+
3
18
  ## [0.56.1](https://github.com/dropseed/plain/releases/plain@0.56.1) (2025-07-30)
4
19
 
5
20
  ### What's changed
plain/exceptions.py CHANGED
@@ -53,12 +53,6 @@ class DisallowedHost(SuspiciousOperation):
53
53
  pass
54
54
 
55
55
 
56
- class DisallowedRedirect(SuspiciousOperation):
57
- """Redirect to scheme not in allowed list"""
58
-
59
- pass
60
-
61
-
62
56
  class TooManyFieldsSent(SuspiciousOperation):
63
57
  """
64
58
  The number of fields in a GET or POST request exceeded
plain/http/__init__.py CHANGED
@@ -19,7 +19,6 @@ from plain.http.response import (
19
19
  ResponseNotAllowed,
20
20
  ResponseNotFound,
21
21
  ResponseNotModified,
22
- ResponsePermanentRedirect,
23
22
  ResponseRedirect,
24
23
  ResponseServerError,
25
24
  StreamingResponse,
@@ -36,7 +35,6 @@ __all__ = [
36
35
  "ResponseBase",
37
36
  "StreamingResponse",
38
37
  "ResponseRedirect",
39
- "ResponsePermanentRedirect",
40
38
  "ResponseNotModified",
41
39
  "ResponseBadRequest",
42
40
  "ResponseForbidden",
plain/http/response.py CHANGED
@@ -10,10 +10,8 @@ from email.header import Header
10
10
  from functools import cached_property
11
11
  from http.client import responses
12
12
  from http.cookies import SimpleCookie
13
- from urllib.parse import urlparse
14
13
 
15
14
  from plain import signals
16
- from plain.exceptions import DisallowedRedirect
17
15
  from plain.http.cookie import sign_cookie_value
18
16
  from plain.json import PlainJSONEncoder
19
17
  from plain.runtime import settings
@@ -566,19 +564,18 @@ class FileResponse(StreamingResponse):
566
564
  self.headers["Content-Disposition"] = content_disposition
567
565
 
568
566
 
569
- class ResponseRedirectBase(Response):
570
- allowed_schemes = ["http", "https", "ftp"]
567
+ class ResponseRedirect(Response):
568
+ """HTTP redirect response"""
569
+
570
+ status_code = 302
571
571
 
572
572
  def __init__(self, redirect_to, **kwargs):
573
573
  super().__init__(**kwargs)
574
574
  self.headers["Location"] = iri_to_uri(redirect_to)
575
- parsed = urlparse(str(redirect_to))
576
- if parsed.scheme and parsed.scheme not in self.allowed_schemes:
577
- raise DisallowedRedirect(
578
- f"Unsafe redirect to URL with protocol '{parsed.scheme}'"
579
- )
580
575
 
581
- url = property(lambda self: self.headers["Location"])
576
+ @property
577
+ def url(self):
578
+ return self.headers["Location"]
582
579
 
583
580
  def __repr__(self):
584
581
  return (
@@ -592,18 +589,6 @@ class ResponseRedirectBase(Response):
592
589
  )
593
590
 
594
591
 
595
- class ResponseRedirect(ResponseRedirectBase):
596
- """HTTP 302 response"""
597
-
598
- status_code = 302
599
-
600
-
601
- class ResponsePermanentRedirect(ResponseRedirectBase):
602
- """HTTP 301 response"""
603
-
604
- status_code = 301
605
-
606
-
607
592
  class ResponseNotModified(Response):
608
593
  """HTTP 304 response"""
609
594
 
@@ -1,6 +1,6 @@
1
1
  import re
2
2
 
3
- from plain.http import ResponsePermanentRedirect
3
+ from plain.http import ResponseRedirect
4
4
  from plain.runtime import settings
5
5
 
6
6
 
@@ -33,4 +33,6 @@ class HttpsRedirectMiddleware:
33
33
  and not any(pattern.search(path) for pattern in self.https_redirect_exempt)
34
34
  ):
35
35
  host = self.https_redirect_host or request.get_host()
36
- return ResponsePermanentRedirect(f"https://{host}{request.get_full_path()}")
36
+ return ResponseRedirect(
37
+ f"https://{host}{request.get_full_path()}", status_code=301
38
+ )
@@ -1,4 +1,4 @@
1
- from plain.http import ResponsePermanentRedirect
1
+ from plain.http import ResponseRedirect
2
2
  from plain.runtime import settings
3
3
  from plain.urls import Resolver404, get_resolver
4
4
  from plain.utils.http import escape_leading_slashes
@@ -22,7 +22,9 @@ class RedirectSlashMiddleware:
22
22
  # If the given URL is "Not Found", then check if we should redirect to
23
23
  # a path with a slash appended.
24
24
  if response.status_code == 404 and self.should_redirect_with_slash(request):
25
- return ResponsePermanentRedirect(self.get_full_path_with_slash(request))
25
+ return ResponseRedirect(
26
+ self.get_full_path_with_slash(request), status_code=301
27
+ )
26
28
 
27
29
  return response
28
30
 
plain/views/redirect.py CHANGED
@@ -1,29 +1,29 @@
1
- import logging
2
-
3
- from plain.http import (
4
- ResponseGone,
5
- ResponsePermanentRedirect,
6
- ResponseRedirect,
7
- )
1
+ from plain.http import ResponseRedirect
8
2
  from plain.urls import reverse
9
3
 
10
4
  from .base import View
11
5
 
12
- logger = logging.getLogger("plain.request")
13
-
14
6
 
15
7
  class RedirectView(View):
16
8
  """Provide a redirect on any GET request."""
17
9
 
18
- permanent = False
10
+ status_code = 302
19
11
  url: str | None = None
20
- pattern_name: str | None = None
21
- query_string = False
12
+ url_name: str | None = None
13
+ preserve_query_params = False
22
14
 
23
- def __init__(self, url=None, permanent=None):
24
- # Allow url and permanent to be set in RedirectView.as_view(url="...", permanent=True)
15
+ def __init__(
16
+ self, url=None, status_code=None, url_name=None, preserve_query_params=None
17
+ ):
18
+ # Allow attributes to be set in RedirectView.as_view(url="...", status_code=301, etc.)
25
19
  self.url = url or self.url
26
- self.permanent = permanent if permanent is not None else self.permanent
20
+ self.status_code = status_code if status_code is not None else self.status_code
21
+ self.url_name = url_name or self.url_name
22
+ self.preserve_query_params = (
23
+ preserve_query_params
24
+ if preserve_query_params is not None
25
+ else self.preserve_query_params
26
+ )
27
27
 
28
28
  def get_redirect_url(self):
29
29
  """
@@ -33,30 +33,19 @@ class RedirectView(View):
33
33
  """
34
34
  if self.url:
35
35
  url = self.url % self.url_kwargs
36
- elif self.pattern_name:
37
- url = reverse(self.pattern_name, *self.url_args, **self.url_kwargs)
36
+ elif self.url_name:
37
+ url = reverse(self.url_name, *self.url_args, **self.url_kwargs)
38
38
  else:
39
- return None
39
+ raise ValueError("RedirectView requires either url or url_name to be set")
40
40
 
41
41
  args = self.request.meta.get("QUERY_STRING", "")
42
- if args and self.query_string:
42
+ if args and self.preserve_query_params:
43
43
  url = f"{url}?{args}"
44
44
  return url
45
45
 
46
46
  def get(self):
47
47
  url = self.get_redirect_url()
48
- if url:
49
- if self.permanent:
50
- return ResponsePermanentRedirect(url)
51
- else:
52
- return ResponseRedirect(url)
53
- else:
54
- logger.warning(
55
- "Gone: %s",
56
- self.request.path,
57
- extra={"status_code": 410, "request": self.request},
58
- )
59
- return ResponseGone()
48
+ return ResponseRedirect(url, status_code=self.status_code)
60
49
 
61
50
  def head(self):
62
51
  return self.get()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.56.1
3
+ Version: 0.57.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
@@ -1,8 +1,8 @@
1
- plain/CHANGELOG.md,sha256=O3trliUx5vgGnIBDCSGO1laXxkCb7BXBIR61ZuXvP2M,7138
1
+ plain/CHANGELOG.md,sha256=S6LqQuC4QwAYLG_Txkz7OyIT8FgyIFXD_3fX6WP7w3I,8219
2
2
  plain/README.md,sha256=5BJyKhf0TDanWVbOQyZ3zsi5Lov9xk-LlJYCDWofM6Y,4078
3
3
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
4
4
  plain/debug.py,sha256=XdjnXcbPGsi0J2SpHGaLthhYU5AjhBlkHdemaP4sbYY,758
5
- plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
5
+ plain/exceptions.py,sha256=bx2_d1udQrGfw-F_k5GmXatgW1ecWSsWnX6jBoD0CH8,5786
6
6
  plain/json.py,sha256=McJdsbMT1sYwkGRG--f2NSZz0hVXPMix9x3nKaaak2o,1262
7
7
  plain/paginator.py,sha256=iXiOyt2r_YwNrkqCRlaU7V-M_BKaaQ8XZElUBVa6yeU,5844
8
8
  plain/signing.py,sha256=r2KvCOxkrSWCULFxYa9BHYx3L3a2oLq8RDnq_92inTw,8207
@@ -50,11 +50,11 @@ plain/forms/exceptions.py,sha256=NYk1wjYDkk-lA_XMJQDItOebQcL_m_r2eNRc2mkLQkg,315
50
50
  plain/forms/fields.py,sha256=OyL4eZIgJ_XMLPHGar17hLepFmwHV-hSnb_n7s18yUU,34709
51
51
  plain/forms/forms.py,sha256=hF7Dl8rEaiBTZhFQyfbh1Zf54BSEka8RYpBiGqkTa8I,10441
52
52
  plain/http/README.md,sha256=hkjTJJ2_WEGm7vaIxjjNHrzD6EN5VI6pjDJPIR9l1jo,760
53
- plain/http/__init__.py,sha256=DIsDRbBsCGa4qZgq-fUuQS0kkxfbTU_3KpIM9VvH04w,1067
53
+ plain/http/__init__.py,sha256=PfmXBIq7onLO_bbOrVdj5rJeHxqJMYqrIobVKupUzUA,1003
54
54
  plain/http/cookie.py,sha256=THd7nOl-2ugeBPKgOhbD87aM2oxUbNH8HWrarUn0fpM,1955
55
55
  plain/http/multipartparser.py,sha256=Z1dFJNAd8N5RHUuF67jh1jBfZOFepORsre_3ee6CgOQ,27266
56
56
  plain/http/request.py,sha256=93b2gqkfEsBczUyP_9vlueVoxyzzfbnJ423PDAk8aHc,26103
57
- plain/http/response.py,sha256=0xUhkTiT6JwohdwA7ymY2vpdCQVl4hnEExjk01LrJbg,23734
57
+ plain/http/response.py,sha256=FM3otFkKEEkAaV_pVB3JUSMhMk7x_zf5JcDWKhOKviM,23223
58
58
  plain/internal/__init__.py,sha256=fVBaYLCXEQc-7riHMSlw3vMTTuF7-0Bj2I8aGzv0o0w,171
59
59
  plain/internal/files/__init__.py,sha256=VctFgox4Q1AWF3klPaoCC5GIw5KeLafYjY5JmN8mAVw,63
60
60
  plain/internal/files/base.py,sha256=2z19tik2_xgXlI6nfVZ4woSF9WB0RSUzsvOfi1Bz8Wg,4113
@@ -70,8 +70,8 @@ plain/internal/handlers/exception.py,sha256=vfha_6-fz6S6VYCP1PMBfue2Gw-_th6jqaTE
70
70
  plain/internal/handlers/wsgi.py,sha256=dgPT29t_F9llB-c5RYU3SHxGuZNaZ83xRjOfuOmtOl8,8209
71
71
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
72
72
  plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
73
- plain/internal/middleware/https.py,sha256=XpuQK8HicYX1jNanQHqNgyQ9rqe4NLUOZO3ZzKdsP8k,1203
74
- plain/internal/middleware/slash.py,sha256=FMU8b9w0NSx4eJs9Y7Ew6RAoSTbUqe2oOM68kg3wOng,2817
73
+ plain/internal/middleware/https.py,sha256=HcOEWOpoblrFGbrsNOtAj8aEK1sgWbKcPqwEJoDw0no,1232
74
+ plain/internal/middleware/slash.py,sha256=JWcIfGbXEKH00I7STq1AMdHhFGmQHC8lkKENa6280ko,2846
75
75
  plain/logs/README.md,sha256=ON6Zylg_WA_V7QiIbRY7sgsl2nfG7KFzIbxK2x3rPuc,1419
76
76
  plain/logs/__init__.py,sha256=rASvo4qFBDIHfkACmGLNGa6lRGbG9PbNjW6FmBt95ys,168
77
77
  plain/logs/configure.py,sha256=2kDJ-WPv3PV4H46mz5tTfzIa2kvN6cjVlb3t-AEbMyk,1307
@@ -150,10 +150,10 @@ plain/views/errors.py,sha256=jbNCJIzowwCsEvqyJ3opMeZpPDqTyhtrbqb0VnAm2HE,1263
150
150
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
151
151
  plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
152
152
  plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
153
- plain/views/redirect.py,sha256=daq2cQIkdDF78bt43sjuZxRAyJm_t_SKw6tyPmiXPIc,1985
153
+ plain/views/redirect.py,sha256=Xpb3cB7nZYvKgkNqcAxf9Jwm2SWcQ0u2xz4oO5M3vP8,1909
154
154
  plain/views/templates.py,sha256=ivkI7LU7BXDQ0d4Geab96Is4-Cp03KbIntXRT1J8e6I,2139
155
- plain-0.56.1.dist-info/METADATA,sha256=TY8h0a3RgKZAXhZng4sMKuleTRc57MZopCufA_3wJOY,4488
156
- plain-0.56.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
157
- plain-0.56.1.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
158
- plain-0.56.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
159
- plain-0.56.1.dist-info/RECORD,,
155
+ plain-0.57.0.dist-info/METADATA,sha256=9lvnS7TXZ1hqmYFkuMUTtafl0v4nmaTTdI0zh9-4kPU,4488
156
+ plain-0.57.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
157
+ plain-0.57.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
158
+ plain-0.57.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
159
+ plain-0.57.0.dist-info/RECORD,,
File without changes