plain 0.57.0__py3-none-any.whl → 0.59.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.
@@ -3,31 +3,24 @@ Default Plain settings. Override these with settings in the module pointed to
3
3
  by the PLAIN_SETTINGS_MODULE environment variable.
4
4
  """
5
5
 
6
- ####################
7
- # CORE #
8
- ####################
6
+ from .utils import get_app_name_from_pyproject
7
+
8
+ # MARK: Core Settings
9
9
 
10
10
  DEBUG: bool = False
11
11
 
12
- # Hosts/domain names that are valid for this site.
13
- # "*" matches anything, ".example.com" matches example.com and all subdomains
14
- ALLOWED_HOSTS: list[str] = []
15
-
16
- # Local time zone for this installation. All choices can be found here:
17
- # https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
18
- # systems may support all possibilities). This is interpreted as the default
19
- # user time zone.
20
- TIME_ZONE: str = "UTC"
21
-
22
- # Default charset to use for all Response objects, if a MIME type isn't
23
- # manually specified. It's used to construct the Content-Type header.
24
- DEFAULT_CHARSET = "utf-8"
12
+ APP_NAME: str = get_app_name_from_pyproject()
25
13
 
26
14
  # List of strings representing installed packages.
27
15
  INSTALLED_PACKAGES: list[str] = []
28
16
 
29
- # Whether to append trailing slashes to URLs.
30
- APPEND_SLASH = True
17
+ URLS_ROUTER: str
18
+
19
+ # MARK: HTTP and Security
20
+
21
+ # Hosts/domain names that are valid for this site.
22
+ # "*" matches anything, ".example.com" matches example.com and all subdomains
23
+ ALLOWED_HOSTS: list[str] = []
31
24
 
32
25
  # Default headers for all responses.
33
26
  DEFAULT_RESPONSE_HEADERS = {
@@ -42,7 +35,13 @@ DEFAULT_RESPONSE_HEADERS = {
42
35
 
43
36
  # Whether to redirect all non-HTTPS requests to HTTPS.
44
37
  HTTPS_REDIRECT_ENABLED = True
45
- HTTPS_REDIRECT_EXEMPT = []
38
+
39
+ # Regex patterns for paths that should be exempt from HTTPS redirect
40
+ # Examples: [r"^/health$", r"/api/internal/.*", r"/dev/.*"]
41
+ HTTPS_REDIRECT_EXEMPT_PATHS: list[str] = []
42
+
43
+ # Custom host to redirect to for HTTPS. If None, uses the same host as the request.
44
+ # Useful for redirecting to a different domain (e.g., "secure.example.com")
46
45
  HTTPS_REDIRECT_HOST = None
47
46
 
48
47
  # If your Plain app is behind a proxy that sets a header to specify secure
@@ -68,7 +67,24 @@ SECRET_KEY: str
68
67
  # secret key rotation.
69
68
  SECRET_KEY_FALLBACKS: list[str] = []
70
69
 
71
- URLS_ROUTER: str
70
+ # MARK: Internationalization
71
+
72
+ # Local time zone for this installation. All choices can be found here:
73
+ # https://en.wikipedia.org/wiki/List_of_tz_zones_by_name (although not all
74
+ # systems may support all possibilities). This is interpreted as the default
75
+ # user time zone.
76
+ TIME_ZONE: str = "UTC"
77
+
78
+ # Default charset to use for all Response objects, if a MIME type isn't
79
+ # manually specified. It's used to construct the Content-Type header.
80
+ DEFAULT_CHARSET = "utf-8"
81
+
82
+ # MARK: URL Configuration
83
+
84
+ # Whether to append trailing slashes to URLs.
85
+ APPEND_SLASH = True
86
+
87
+ # MARK: File Uploads
72
88
 
73
89
  # List of upload handler classes to be applied in order.
74
90
  FILE_UPLOAD_HANDLERS = [
@@ -100,41 +116,30 @@ FILE_UPLOAD_TEMP_DIR = None
100
116
  # User-defined overrides for error views by status code
101
117
  HTTP_ERROR_VIEWS: dict[int] = {}
102
118
 
103
- ##############
104
- # MIDDLEWARE #
105
- ##############
119
+ # MARK: Middleware
106
120
 
107
121
  # List of middleware to use. Order is important; in the request phase, these
108
122
  # middleware will be applied in the order given, and in the response
109
123
  # phase the middleware will be applied in reverse order.
110
124
  MIDDLEWARE: list[str] = []
111
125
 
112
- ########
113
- # CSRF #
114
- ########
115
-
116
- # Settings for CSRF cookie.
117
- CSRF_COOKIE_NAME = "csrftoken"
118
- CSRF_COOKIE_AGE = 60 * 60 * 24 * 7 * 52 # 1 year
119
- CSRF_COOKIE_DOMAIN = None
120
- CSRF_COOKIE_PATH = "/"
121
- CSRF_COOKIE_SECURE = True
122
- CSRF_COOKIE_HTTPONLY = False
123
- CSRF_COOKIE_SAMESITE = "Lax"
124
- CSRF_HEADER_NAME = "CSRF-Token"
125
- CSRF_FIELD_NAME = "_csrftoken"
126
+ # MARK: CSRF
127
+
128
+ # A list of trusted origins for unsafe (POST/PUT/DELETE etc.) requests.
129
+ # These origins will be allowed regardless of the normal CSRF checks.
130
+ # Each origin should be a full origin like "https://example.com" or "https://sub.example.com:8080"
126
131
  CSRF_TRUSTED_ORIGINS: list[str] = []
127
132
 
128
- ###########
129
- # LOGGING #
130
- ###########
133
+ # Regex patterns for paths that should be exempt from CSRF protection
134
+ # Examples: [r"^/api/", r"/webhooks/.*", r"/health$"]
135
+ CSRF_EXEMPT_PATHS: list[str] = []
136
+
137
+ # MARK: Logging
131
138
 
132
139
  # Custom logging configuration.
133
140
  LOGGING = {}
134
141
 
135
- ###############
136
- # ASSETS #
137
- ###############
142
+ # MARK: Assets
138
143
 
139
144
  # Whether to redirect the original asset path to the fingerprinted path.
140
145
  ASSETS_REDIRECT_ORIGINAL = True
@@ -143,9 +148,7 @@ ASSETS_REDIRECT_ORIGINAL = True
143
148
  # Ex. "https://cdn.example.com/assets/"
144
149
  ASSETS_BASE_URL: str = ""
145
150
 
146
- ####################
147
- # PREFLIGHT CHECKS #
148
- ####################
151
+ # MARK: Preflight Checks
149
152
 
150
153
  # List of all issues generated by system checks that should be silenced. Light
151
154
  # issues like warnings, infos or debugs will not generate a message. Silencing
@@ -153,14 +156,10 @@ ASSETS_BASE_URL: str = ""
153
156
  # message, but Plain will not stop you from e.g. running server.
154
157
  PREFLIGHT_SILENCED_CHECKS = []
155
158
 
156
- #############
157
- # Templates #
158
- #############
159
+ # MARK: Templates
159
160
 
160
161
  TEMPLATES_JINJA_ENVIRONMENT = "plain.templates.jinja.DefaultEnvironment"
161
162
 
162
- #########
163
- # Shell #
164
- #########
163
+ # MARK: Shell
165
164
 
166
165
  SHELL_IMPORT: str = ""
plain/runtime/utils.py ADDED
@@ -0,0 +1,20 @@
1
+ import tomllib
2
+ from pathlib import Path
3
+
4
+
5
+ def get_app_name_from_pyproject():
6
+ """Get the project name from the nearest pyproject.toml file."""
7
+ current_path = Path.cwd()
8
+
9
+ # Walk up the directory tree looking for pyproject.toml
10
+ for path in [current_path] + list(current_path.parents):
11
+ pyproject_path = path / "pyproject.toml"
12
+ if pyproject_path.exists():
13
+ try:
14
+ with pyproject_path.open("rb") as f:
15
+ pyproject = tomllib.load(f)
16
+ return pyproject.get("project", {}).get("name", "App")
17
+ except (tomllib.TOMLDecodeError, OSError):
18
+ continue
19
+
20
+ return "App"
plain/templates/README.md CHANGED
@@ -70,7 +70,6 @@ class HTMXJSExtension(InclusionTagExtension):
70
70
 
71
71
  def get_context(self, context, *args, **kwargs):
72
72
  return {
73
- "csrf_token": context["csrf_token"],
74
73
  "DEBUG": settings.DEBUG,
75
74
  "extensions": kwargs.get("extensions", []),
76
75
  }
plain/test/client.py CHANGED
@@ -114,8 +114,7 @@ class ClientHandler(BaseHandler):
114
114
  the originating WSGIRequest attached to its ``wsgi_request`` attribute.
115
115
  """
116
116
 
117
- def __init__(self, enforce_csrf_checks=True, *args, **kwargs):
118
- self.enforce_csrf_checks = enforce_csrf_checks
117
+ def __init__(self, *args, **kwargs):
119
118
  super().__init__(*args, **kwargs)
120
119
 
121
120
  def __call__(self, environ):
@@ -126,12 +125,6 @@ class ClientHandler(BaseHandler):
126
125
 
127
126
  request_started.send(sender=self.__class__, environ=environ)
128
127
  request = WSGIRequest(environ)
129
- # sneaky little hack so that we can easily get round
130
- # CsrfViewMiddleware. This makes life easier, and is probably
131
- # required for backwards compatibility with external tests against
132
- # admin views.
133
- if not self.enforce_csrf_checks:
134
- request.csrf_exempt = True
135
128
 
136
129
  # Request goes through middleware.
137
130
  response = self.get_response(request)
@@ -420,14 +413,13 @@ class Client(RequestFactory):
420
413
 
421
414
  def __init__(
422
415
  self,
423
- enforce_csrf_checks=False,
424
416
  raise_request_exception=True,
425
417
  *,
426
418
  headers=None,
427
419
  **defaults,
428
420
  ):
429
421
  super().__init__(headers=headers, **defaults)
430
- self.handler = ClientHandler(enforce_csrf_checks)
422
+ self.handler = ClientHandler()
431
423
  self.raise_request_exception = raise_request_exception
432
424
  self.exc_info = None
433
425
  self.extra = None
plain/views/README.md CHANGED
@@ -142,8 +142,6 @@ Rendering forms is done directly in the HTML.
142
142
  {% block content %}
143
143
 
144
144
  <form method="post">
145
- {{ csrf_input }}
146
-
147
145
  <!-- Render general form errors -->
148
146
  {% for error in form.non_field_errors %}
149
147
  <div>{{ error }}</div>
plain/views/objects.py CHANGED
@@ -1,28 +1,55 @@
1
+ from functools import cached_property
2
+
1
3
  from plain.exceptions import ImproperlyConfigured, ObjectDoesNotExist
2
4
  from plain.forms import Form
3
- from plain.http import Http404, Response
5
+ from plain.http import Http404
4
6
 
5
7
  from .forms import FormView
6
8
  from .templates import TemplateView
7
9
 
8
10
 
11
+ class CreateView(FormView):
12
+ """
13
+ View for creating a new object, with a response rendered by a template.
14
+ """
15
+
16
+ # TODO? would rather you have to specify this...
17
+ def get_success_url(self, form):
18
+ """Return the URL to redirect to after processing a valid form."""
19
+ if self.success_url:
20
+ url = self.success_url.format(**self.object.__dict__)
21
+ else:
22
+ try:
23
+ url = self.object.get_absolute_url()
24
+ except AttributeError:
25
+ raise ImproperlyConfigured(
26
+ "No URL to redirect to. Either provide a url or define"
27
+ " a get_absolute_url method on the Model."
28
+ )
29
+ return url
30
+
31
+ def form_valid(self, form):
32
+ """If the form is valid, save the associated model."""
33
+ self.object = form.save()
34
+ return super().form_valid(form)
35
+
36
+
9
37
  class ObjectTemplateViewMixin:
10
38
  context_object_name = ""
11
39
 
12
- def get(self) -> Response:
13
- self.load_object()
14
- return self.render_template()
15
-
16
- def load_object(self) -> None:
40
+ @cached_property
41
+ def object(self):
17
42
  try:
18
- self.object = self.get_object()
43
+ obj = self.get_object()
19
44
  except ObjectDoesNotExist:
20
45
  raise Http404
21
46
 
22
- if not self.object:
47
+ if not obj:
23
48
  # Also raise 404 if the object is None
24
49
  raise Http404
25
50
 
51
+ return obj
52
+
26
53
  def get_object(self): # Intentionally untyped... subclasses must override this.
27
54
  raise NotImplementedError(
28
55
  f"get_object() is not implemented on {self.__class__.__name__}"
@@ -50,55 +77,9 @@ class DetailView(ObjectTemplateViewMixin, TemplateView):
50
77
  pass
51
78
 
52
79
 
53
- class CreateView(ObjectTemplateViewMixin, FormView):
54
- """
55
- View for creating a new object, with a response rendered by a template.
56
- """
57
-
58
- def post(self) -> Response:
59
- """
60
- Handle POST requests: instantiate a form instance with the passed
61
- POST variables and then check if it's valid.
62
- """
63
- # Context expects self.object to exist
64
- self.load_object()
65
- return super().post()
66
-
67
- def load_object(self) -> None:
68
- self.object = None
69
-
70
- # TODO? would rather you have to specify this...
71
- def get_success_url(self, form):
72
- """Return the URL to redirect to after processing a valid form."""
73
- if self.success_url:
74
- url = self.success_url.format(**self.object.__dict__)
75
- else:
76
- try:
77
- url = self.object.get_absolute_url()
78
- except AttributeError:
79
- raise ImproperlyConfigured(
80
- "No URL to redirect to. Either provide a url or define"
81
- " a get_absolute_url method on the Model."
82
- )
83
- return url
84
-
85
- def form_valid(self, form):
86
- """If the form is valid, save the associated model."""
87
- self.object = form.save()
88
- return super().form_valid(form)
89
-
90
-
91
80
  class UpdateView(ObjectTemplateViewMixin, FormView):
92
81
  """View for updating an object, with a response rendered by a template."""
93
82
 
94
- def post(self) -> Response:
95
- """
96
- Handle POST requests: instantiate a form instance with the passed
97
- POST variables and then check if it's valid.
98
- """
99
- self.load_object()
100
- return super().post()
101
-
102
83
  def get_success_url(self, form):
103
84
  """Return the URL to redirect to after processing a valid form."""
104
85
  if self.success_url:
@@ -115,7 +96,7 @@ class UpdateView(ObjectTemplateViewMixin, FormView):
115
96
 
116
97
  def form_valid(self, form):
117
98
  """If the form is valid, save the associated model."""
118
- self.object = form.save()
99
+ form.save()
119
100
  return super().form_valid(form)
120
101
 
121
102
  def get_form_kwargs(self):
@@ -141,14 +122,6 @@ class DeleteView(ObjectTemplateViewMixin, FormView):
141
122
 
142
123
  form_class = EmptyDeleteForm
143
124
 
144
- def post(self) -> Response:
145
- """
146
- Handle POST requests: instantiate a form instance with the passed
147
- POST variables and then check if it's valid.
148
- """
149
- self.load_object()
150
- return super().post()
151
-
152
125
  def get_form_kwargs(self):
153
126
  """Return the keyword arguments for instantiating the form."""
154
127
  kwargs = super().get_form_kwargs()
@@ -169,9 +142,9 @@ class ListView(TemplateView):
169
142
 
170
143
  context_object_name = ""
171
144
 
172
- def get(self) -> Response:
173
- self.objects = self.get_objects()
174
- return super().get()
145
+ @cached_property
146
+ def objects(self):
147
+ return self.get_objects()
175
148
 
176
149
  def get_objects(self):
177
150
  raise NotImplementedError(
plain/views/templates.py CHANGED
@@ -1,27 +1,11 @@
1
- from plain.csrf.middleware import get_token
2
1
  from plain.exceptions import ImproperlyConfigured
3
2
  from plain.http import Response
4
3
  from plain.runtime import settings
5
4
  from plain.templates import Template, TemplateFileMissing
6
- from plain.utils.functional import lazy
7
- from plain.utils.html import format_html
8
- from plain.utils.safestring import SafeString
9
5
 
10
6
  from .base import View
11
7
 
12
8
 
13
- def csrf_input(request):
14
- return format_html(
15
- '<input type="hidden" name="{}" value="{}">',
16
- settings.CSRF_FIELD_NAME,
17
- get_token(request),
18
- )
19
-
20
-
21
- csrf_input_lazy = lazy(csrf_input, SafeString, str)
22
- csrf_token_lazy = lazy(get_token, str)
23
-
24
-
25
9
  class TemplateView(View):
26
10
  """
27
11
  Render a template.
@@ -37,8 +21,6 @@ class TemplateView(View):
37
21
  return {
38
22
  "request": self.request,
39
23
  "template_names": self.get_template_names(),
40
- "csrf_input": csrf_input_lazy(self.request),
41
- "csrf_token": csrf_token_lazy(self.request),
42
24
  "DEBUG": settings.DEBUG,
43
25
  }
44
26
 
@@ -54,6 +36,12 @@ class TemplateView(View):
54
36
  def get_template(self) -> Template:
55
37
  template_names = self.get_template_names()
56
38
 
39
+ if isinstance(template_names, str):
40
+ raise ImproperlyConfigured(
41
+ f"{self.__class__.__name__}.get_template_names() must return a list of strings, "
42
+ f"not a string. Did you mean to return ['{template_names}']?"
43
+ )
44
+
57
45
  if not template_names:
58
46
  raise ImproperlyConfigured(
59
47
  f"{self.__class__.__name__} requires a template_name or get_template_names()."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.57.0
3
+ Version: 0.59.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=S6LqQuC4QwAYLG_Txkz7OyIT8FgyIFXD_3fX6WP7w3I,8219
1
+ plain/CHANGELOG.md,sha256=AW5V61mourm_d7YB4u2bJ4LABXh1hJ_pZv0Bn46VP8I,10477
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=bx2_d1udQrGfw-F_k5GmXatgW1ecWSsWnX6jBoD0CH8,5786
5
+ plain/exceptions.py,sha256=ljOLqgVwPKlGWqaNmjHcHQf6y053bZ9ogkLFEGcs-Gg,5973
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
@@ -20,7 +20,7 @@ plain/chores/__init__.py,sha256=r9TXtQCH-VbvfnIJ5F8FxgQC35GRWFOfmMZN3q9niLg,67
20
20
  plain/chores/registry.py,sha256=V3WjuekRI22LFvJbqSkUXQtiOtuE2ZK8gKV1TRvxRUI,1866
21
21
  plain/cli/README.md,sha256=5C7vsH0ISxu7q5H6buC25MBOILkI_rzdySitswpQgJw,1032
22
22
  plain/cli/__init__.py,sha256=6w9T7K2WrPwh6DcaMb2oNt_CWU6Bc57nUTO2Bt1p38Y,63
23
- plain/cli/agent.py,sha256=T3jG9hpbndiBsRYf_MhT3GvHrnpLjCh3tgi4pR6Ipmg,1477
23
+ plain/cli/agent.py,sha256=nf-Tuc3abxpyV-nShBn1wq0JWjfgd3zY9lLH6rAZSRs,1678
24
24
  plain/cli/build.py,sha256=Lo6AYghJz0DM9fIVUSiBSOKa5vR0XCOxZWEjza6sc8Q,3172
25
25
  plain/cli/changelog.py,sha256=j-k1yZk9mpm-fLZgeWastiyIisxNSuAJfXTQ2B6WQmk,3457
26
26
  plain/cli/chores.py,sha256=xXSSFvr8T5jWfLWqe6E8YVMw1BkQxyOHHVuY0x9RH0A,2412
@@ -37,13 +37,13 @@ plain/cli/scaffold.py,sha256=mcywA9DzfwoBSqWl5-Zpgcy1mTNUGEgdvoxXUrGcEVk,1351
37
37
  plain/cli/settings.py,sha256=9cx4bue664I2P7kUedlf4YhCPB0tSKSE4Q8mGyzEv2o,1995
38
38
  plain/cli/shell.py,sha256=iIwvlTdTBjLBBUdXMAmIRWSoynszOZI79-mrBg4RegU,1373
39
39
  plain/cli/startup.py,sha256=wLaFuyUb4ewWhtehBCGicrRCXIIGCRbeCT3ce9hUv-A,1022
40
- plain/cli/upgrade.py,sha256=f1rL9M285i4D5UKlEk4ijmAUN3mAMfHCrbDPwBf1ePo,5514
40
+ plain/cli/upgrade.py,sha256=eGVWm0gpn-Pr6uPsfzojRmh_VU5--B0h9dYfQuXSzi8,5625
41
41
  plain/cli/urls.py,sha256=ghCW36aRszxmTo06A50FIvYopb6kQ07QekkDzM6_A1o,3824
42
42
  plain/cli/utils.py,sha256=VwlIh0z7XxzVV8I3qM2kZo07fkJFPoeeVZa1ODG616k,258
43
- plain/csrf/README.md,sha256=cTqQ9oOa3BcMRhMPzqhI2wat1ZOS7aQwJTIFArdMTsk,794
44
- plain/csrf/middleware.py,sha256=U3B9R7ciO_UAf7O3jdNtVu6QZ_3Yrm8isRdnW6bVKX4,17401
43
+ plain/csrf/README.md,sha256=ApWpB-qlEf0LkOKm9Yr-6f_lB9XJEvGFDo_fraw8ghI,2391
44
+ plain/csrf/middleware.py,sha256=d_vb8l0-KxzyqCivVq0jTCsFOm-ljwjmjVuZXKVYR5U,5113
45
45
  plain/csrf/views.py,sha256=HwQqfI6KPelHP9gSXhjfZaTLQic71PKsoZ6DPhr1rKI,572
46
- plain/forms/README.md,sha256=SeqRBAPynu-aNR8FPXesOIjPuUi4X1pT_ZlDK4bD7UA,2280
46
+ plain/forms/README.md,sha256=7MJQxNBoKkg0rW16qF6bGpUBxZrMrWjl2DZZk6gjzAU,2258
47
47
  plain/forms/__init__.py,sha256=UxqPwB8CiYPCQdHmUc59jadqaXqDmXBH8y4bt9vTPms,226
48
48
  plain/forms/boundfield.py,sha256=LhydhCVR0okrli0-QBMjGjAJ8-06gTCXVEaBZhBouQk,1741
49
49
  plain/forms/exceptions.py,sha256=NYk1wjYDkk-lA_XMJQDItOebQcL_m_r2eNRc2mkLQkg,315
@@ -70,7 +70,7 @@ 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=HcOEWOpoblrFGbrsNOtAj8aEK1sgWbKcPqwEJoDw0no,1232
73
+ plain/internal/middleware/https.py,sha256=mS1YejfLB_5qlAoMfanh8Wn2O-RdSpOBdhvw2DRcTHs,1257
74
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
@@ -90,14 +90,15 @@ plain/preflight/security.py,sha256=oxUZBp2M0bpBfUoLYepIxoex2Y90nyjlrL8XU8UTHYY,2
90
90
  plain/preflight/urls.py,sha256=cQ-WnFa_5oztpKdtwhuIGb7pXEml__bHsjs1SWO2YNI,1468
91
91
  plain/runtime/README.md,sha256=sTqXXJkckwqkk9O06XMMSNRokAYjrZBnB50JD36BsYI,4873
92
92
  plain/runtime/__init__.py,sha256=8GtvKROf3HUCtneDYXTbEioPcCtwnV76dP57n2PnjuE,2343
93
- plain/runtime/global_settings.py,sha256=MpJ6vtBMO2EYALaKXrtgcGoxLh9Y549zwwErC5D5K7I,5343
93
+ plain/runtime/global_settings.py,sha256=EddJhfS3LZE6XIKSgib9dzmrw72Z1BbCluO8OgUQU48,5740
94
94
  plain/runtime/user_settings.py,sha256=OzMiEkE6ZQ50nxd1WIqirXPiNuMAQULklYHEzgzLWgA,11027
95
+ plain/runtime/utils.py,sha256=oiWGyC9J1xrypCUkawdB6mGUuKpiK4OL0ztHoHOB08g,674
95
96
  plain/signals/README.md,sha256=XefXqROlDhzw7Z5l_nx6Mhq6n9jjQ-ECGbH0vvhKWYg,272
96
97
  plain/signals/__init__.py,sha256=eAs0kLqptuP6I31dWXeAqRNji3svplpAV4Ez6ktjwXM,131
97
98
  plain/signals/dispatch/__init__.py,sha256=FzEygqV9HsM6gopio7O2Oh_X230nA4d5Q9s0sUjMq0E,292
98
99
  plain/signals/dispatch/dispatcher.py,sha256=VxSlqn9PCOTghPPJLOqZPs6FNQZfV2BJpMfFMSg6Dtc,11531
99
100
  plain/signals/dispatch/license.txt,sha256=o9EhDhsC4Q5HbmD-IfNGVTEkXtNE33r5rIt3lleJ8gc,1727
100
- plain/templates/README.md,sha256=3eCnQvgLNAnbJENxCWevtiwVpop3_sfJMBWUVGQP2qQ,2614
101
+ plain/templates/README.md,sha256=QAQxoygpc0CE13fh4eH4ZILwl2xc-oMdGKtiZLLrNCk,2565
101
102
  plain/templates/__init__.py,sha256=bX76FakE9T7mfK3N0deN85HlwHNQpeigytSC9Z8LcOs,451
102
103
  plain/templates/core.py,sha256=mbcH0yTeFOI3XOg9dYSroXRIcdv9sETEy4HzY-ugwco,1258
103
104
  plain/templates/jinja/__init__.py,sha256=xvYif0feMYR9pWjN0czthq2dk3qI4D5UQjgj9yp4dNA,2776
@@ -107,7 +108,7 @@ plain/templates/jinja/filters.py,sha256=ft5XUr4OLeQayn-MSxrycpFLyyN_yEo7j5WhWMwp
107
108
  plain/templates/jinja/globals.py,sha256=VMpuMZvwWOmb5MbzKK-ox-QEX_WSsXFxq0mm8biJgaU,558
108
109
  plain/test/README.md,sha256=wem0myAd-ij74nNV1MB9g4iH6FZUgii7-_euRq45oHs,1142
109
110
  plain/test/__init__.py,sha256=MhNHtp7MYBl9kq-pMRGY11kJ6kU1I6vOkjNkit1TYRg,94
110
- plain/test/client.py,sha256=XjLjkeDl1rQRaFoeiCvvD5GXfxIxpuic2OnxjoX05tA,25573
111
+ plain/test/client.py,sha256=OcL8wQDOu6PUNYvwcmslT5IGt81ffcPsXn05n-2n9oA,25128
111
112
  plain/test/encoding.py,sha256=YJBOIE-BQRA5yl4zHnQy-9l67mJDTFmfy1DQXK0Wk-Q,3199
112
113
  plain/test/exceptions.py,sha256=b-GHicg87Gh73W3g8QGWuSHi9PrQFVsxgWvEXDLt8gQ,290
113
114
  plain/urls/README.md,sha256=026RkCK6I0GdqK3RE2QBLcCLIsiwtyKxgI2F0KBX95E,3882
@@ -142,18 +143,17 @@ plain/utils/text.py,sha256=42hJv06sadbWfsaAHNhqCQaP1W9qZ69trWDTS-Xva7k,9496
142
143
  plain/utils/timesince.py,sha256=a_-ZoPK_s3Pt998CW4rWp0clZ1XyK2x04hCqak2giII,5928
143
144
  plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
144
145
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
145
- plain/views/README.md,sha256=wKwC6pgxUyIZdCZq5roPpzbGa0Y-tR7mPifr5z659Uk,7245
146
+ plain/views/README.md,sha256=JmRGCQJgSr17uzUfsJfZbix1md3Qj6mmCkzWuoTFurQ,7223
146
147
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
147
148
  plain/views/base.py,sha256=CC9UvMZeAjVvi90vGjoZzsQ0jnhbg3-7qCKQ8-Pb6cg,4184
148
- plain/views/csrf.py,sha256=7q6l5xzLWhRnMY64aNj0hR6G-3pxI2yhRwG6k_5j00E,144
149
149
  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
- plain/views/objects.py,sha256=GGbcfg_9fPZ-PiaBwIHG2e__8GfWDR7JQtQ15wTyiHg,5970
152
+ plain/views/objects.py,sha256=YNb8MO1I99HTmQghC5nFk25TQmaB_s45K5yg5BGt4qY,5018
153
153
  plain/views/redirect.py,sha256=Xpb3cB7nZYvKgkNqcAxf9Jwm2SWcQ0u2xz4oO5M3vP8,1909
154
- plain/views/templates.py,sha256=ivkI7LU7BXDQ0d4Geab96Is4-Cp03KbIntXRT1J8e6I,2139
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,,
154
+ plain/views/templates.py,sha256=oAlebEyfES0rzBhfyEJzFmgLkpkbleA6Eip-8zDp-yk,1863
155
+ plain-0.59.0.dist-info/METADATA,sha256=7JQkFKbt0rGqL6RYe5uQBtXLpk3aPUCDtj0kTtIGcVY,4488
156
+ plain-0.59.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
157
+ plain-0.59.0.dist-info/entry_points.txt,sha256=nn4uKTRRZuEKOJv3810s3jtSMW0Gew7XDYiKIvBRR6M,93
158
+ plain-0.59.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
159
+ plain-0.59.0.dist-info/RECORD,,
plain/views/csrf.py DELETED
@@ -1,4 +0,0 @@
1
- class CsrfExemptViewMixin:
2
- def setup(self, *args, **kwargs):
3
- super().setup(*args, **kwargs)
4
- self.request.csrf_exempt = True
File without changes