plain 0.33.0__py3-none-any.whl → 0.34.1__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/README.md CHANGED
@@ -29,7 +29,7 @@ The `plain` package includes everything you need to start handling web requests
29
29
 
30
30
  - [plain.models](/plain-models/README.md) - Define and interact with your database models.
31
31
  - [plain.cache](/plain-cache/README.md) - A database-driven general purpose cache.
32
- - [plain.mail](/plain-mail/README.md) - Send emails with SMTP or custom backends.
32
+ - [plain.email](/plain-email/README.md) - Send emails with SMTP or custom backends.
33
33
  - [plain.sessions](/plain-sessions/README.md) - User sessions and cookies.
34
34
  - [plain.worker](/plain-worker/README.md) - Backgrounb jobs stored in the database.
35
35
  - [plain.api](/plain-api/README.md) - Build APIs with Plain views.
@@ -45,10 +45,14 @@ def convert_exception_to_response(get_response):
45
45
 
46
46
  def response_for_exception(request, exc):
47
47
  if isinstance(exc, Http404):
48
- response = get_exception_response(request, 404)
48
+ response = get_exception_response(
49
+ request=request, status_code=404, exception=None
50
+ )
49
51
 
50
52
  elif isinstance(exc, PermissionDenied):
51
- response = get_exception_response(request, 403)
53
+ response = get_exception_response(
54
+ request=request, status_code=403, exception=exc
55
+ )
52
56
  log_response(
53
57
  "Forbidden (Permission denied): %s",
54
58
  request.path,
@@ -58,7 +62,9 @@ def response_for_exception(request, exc):
58
62
  )
59
63
 
60
64
  elif isinstance(exc, MultiPartParserError):
61
- response = get_exception_response(request, 400)
65
+ response = get_exception_response(
66
+ request=request, status_code=400, exception=None
67
+ )
62
68
  log_response(
63
69
  "Bad request (Unable to parse request body): %s",
64
70
  request.path,
@@ -68,7 +74,9 @@ def response_for_exception(request, exc):
68
74
  )
69
75
 
70
76
  elif isinstance(exc, BadRequest):
71
- response = get_exception_response(request, 400)
77
+ response = get_exception_response(
78
+ request=request, status_code=400, exception=exc
79
+ )
72
80
  log_response(
73
81
  "%s: %s",
74
82
  str(exc),
@@ -91,11 +99,15 @@ def response_for_exception(request, exc):
91
99
  exc_info=exc,
92
100
  extra={"status_code": 400, "request": request},
93
101
  )
94
- response = get_exception_response(request, 400)
102
+ response = get_exception_response(
103
+ request=request, status_code=400, exception=None
104
+ )
95
105
 
96
106
  else:
97
107
  signals.got_request_exception.send(sender=None, request=request)
98
- response = get_exception_response(request, 500)
108
+ response = get_exception_response(
109
+ request=request, status_code=500, exception=None
110
+ )
99
111
  log_response(
100
112
  "%s: %s",
101
113
  response.reason_phrase,
@@ -105,34 +117,25 @@ def response_for_exception(request, exc):
105
117
  exception=exc,
106
118
  )
107
119
 
108
- # Force a TemplateResponse to be rendered.
109
- if not getattr(response, "is_rendered", True) and callable(
110
- getattr(response, "render", None)
111
- ):
112
- response = response.render()
113
-
114
120
  return response
115
121
 
116
122
 
117
- def get_exception_response(request, status_code):
123
+ def get_exception_response(*, request, status_code, exception):
118
124
  try:
119
- return get_error_view(status_code)(request)
125
+ view_class = get_error_view(status_code=status_code, exception=exception)
126
+ return view_class(request)
120
127
  except Exception:
121
128
  signals.got_request_exception.send(sender=None, request=request)
122
- return handle_uncaught_exception()
123
129
 
130
+ # In development mode, re-raise the exception to get a full stack trace
131
+ if settings.DEBUG:
132
+ raise
124
133
 
125
- def handle_uncaught_exception():
126
- """
127
- Processing for any otherwise uncaught exceptions (those that will
128
- generate HTTP 500 responses).
129
- """
130
- if settings.DEBUG:
131
- raise
132
- return ResponseServerError()
134
+ # If we can't load the view, return a 500 response
135
+ return ResponseServerError()
133
136
 
134
137
 
135
- def get_error_view(status_code):
138
+ def get_error_view(*, status_code, exception):
136
139
  views_by_status = settings.HTTP_ERROR_VIEWS
137
140
  if status_code in views_by_status:
138
141
  view = views_by_status[status_code]
@@ -142,4 +145,4 @@ def get_error_view(status_code):
142
145
  return view.as_view()
143
146
 
144
147
  # Create a standard view for any other status code
145
- return ErrorView.as_view(status_code=status_code)
148
+ return ErrorView.as_view(status_code=status_code, exception=exception)
plain/packages/config.py CHANGED
@@ -10,7 +10,9 @@ CONFIG_MODULE_NAME = "config"
10
10
  class PackageConfig:
11
11
  """Class representing a Plain application and its configuration."""
12
12
 
13
- def __init__(self, name, *, label=""):
13
+ package_label: str
14
+
15
+ def __init__(self, name):
14
16
  # Full Python path to the application e.g. 'plain.admin.admin'.
15
17
  self.name = name
16
18
 
@@ -18,28 +20,18 @@ class PackageConfig:
18
20
  # registry when it registers the PackageConfig instance.
19
21
  self.packages_registry = None
20
22
 
21
- # The following attributes could be defined at the class level in a
22
- # subclass, hence the test-and-set pattern.
23
- if label and hasattr(self, "label"):
24
- raise ImproperlyConfigured(
25
- "PackageConfig class should not define a class label attribute and an init label"
26
- )
27
-
28
- if label:
29
- # Set the label explicitly from the init
30
- self.label = label
31
- elif not hasattr(self, "label"):
23
+ if not hasattr(self, "package_label"):
32
24
  # Last component of the Python path to the application e.g. 'admin'.
33
25
  # This value must be unique across a Plain project.
34
- self.label = self.name.rpartition(".")[2]
26
+ self.package_label = self.name.rpartition(".")[2]
35
27
 
36
- if not self.label.isidentifier():
28
+ if not self.package_label.isidentifier():
37
29
  raise ImproperlyConfigured(
38
- f"The app label '{self.label}' is not a valid Python identifier."
30
+ f"The app label '{self.package_label}' is not a valid Python identifier."
39
31
  )
40
32
 
41
33
  def __repr__(self):
42
- return f"<{self.__class__.__name__}: {self.label}>"
34
+ return f"<{self.__class__.__name__}: {self.package_label}>"
43
35
 
44
36
  @cached_property
45
37
  def path(self):
@@ -146,7 +146,7 @@ class PackagesRegistry:
146
146
  message = f"No installed app with label '{package_label}'."
147
147
  for package_config in self.get_package_configs():
148
148
  if package_config.name == package_label:
149
- message += f" Did you mean '{package_config.label}'?"
149
+ message += f" Did you mean '{package_config.package_label}'?"
150
150
  break
151
151
  raise LookupError(message)
152
152
 
@@ -179,11 +179,11 @@ class PackagesRegistry:
179
179
  class Config(PackageConfig):
180
180
  pass
181
181
  """
182
- if package_config.label in self.package_configs:
182
+ if package_config.package_label in self.package_configs:
183
183
  raise ImproperlyConfigured(
184
- f"Package labels aren't unique, duplicates: {package_config.label}"
184
+ f"Package labels aren't unique, duplicates: {package_config.package_label}"
185
185
  )
186
- self.package_configs[package_config.label] = package_config
186
+ self.package_configs[package_config.package_label] = package_config
187
187
  package_config.packages = self
188
188
 
189
189
  return package_config
plain/utils/cache.py CHANGED
@@ -15,13 +15,39 @@ An example: i18n middleware would need to distinguish caches by the
15
15
  "Accept-language" header.
16
16
  """
17
17
 
18
+ import time
18
19
  from collections import defaultdict
19
20
 
20
- from plain.utils.regex_helper import _lazy_re_compile
21
+ from .http import http_date
22
+ from .regex_helper import _lazy_re_compile
21
23
 
22
24
  cc_delim_re = _lazy_re_compile(r"\s*,\s*")
23
25
 
24
26
 
27
+ def patch_response_headers(response, cache_timeout):
28
+ """
29
+ Add HTTP caching headers to the given HttpResponse: Expires and
30
+ Cache-Control.
31
+
32
+ Each header is only added if it isn't already set.
33
+ """
34
+ if cache_timeout < 0:
35
+ cache_timeout = 0 # Can't have max-age negative
36
+ if "Expires" not in response.headers:
37
+ response.headers["Expires"] = http_date(time.time() + cache_timeout)
38
+ patch_cache_control(response, max_age=cache_timeout)
39
+
40
+
41
+ def add_never_cache_headers(response):
42
+ """
43
+ Add headers to a response to indicate that a page should never be cached.
44
+ """
45
+ patch_response_headers(response, cache_timeout=-1)
46
+ patch_cache_control(
47
+ response, no_cache=True, no_store=True, must_revalidate=True, private=True
48
+ )
49
+
50
+
25
51
  def patch_cache_control(response, **kwargs):
26
52
  """
27
53
  Patch the Cache-Control header by adding all keyword arguments to it.
plain/views/base.py CHANGED
@@ -20,6 +20,7 @@ class View:
20
20
  url_kwargs: dict
21
21
 
22
22
  # By default, any of these are allowed if a method is defined for it.
23
+ # To disallow a defined method, remove it from this list.
23
24
  allowed_http_methods = [
24
25
  "get",
25
26
  "post",
@@ -84,7 +85,7 @@ class View:
84
85
  return result
85
86
 
86
87
  if isinstance(result, str):
87
- return Response(result)
88
+ return Response(result, content_type="text/plain")
88
89
 
89
90
  if isinstance(result, list):
90
91
  return JsonResponse(result, safe=False)
plain/views/errors.py CHANGED
@@ -7,11 +7,19 @@ from .templates import TemplateView
7
7
  class ErrorView(TemplateView):
8
8
  status_code: int
9
9
 
10
- def __init__(self, status_code=None) -> None:
10
+ def __init__(self, *, status_code=None, exception=None) -> None:
11
11
  # Allow creating an ErrorView with a status code
12
12
  # e.g. ErrorView.as_view(status_code=404)
13
- if status_code is not None:
14
- self.status_code = status_code
13
+ self.status_code = status_code or self.status_code
14
+
15
+ # Allow creating an ErrorView with an exception
16
+ self.exception = exception
17
+
18
+ def get_template_context(self):
19
+ context = super().get_template_context()
20
+ context["status_code"] = self.status_code
21
+ context["exception"] = self.exception
22
+ return context
15
23
 
16
24
  def get_template_names(self) -> list[str]:
17
25
  return [f"{self.status_code}.html", "error.html"]
plain/views/templates.py CHANGED
@@ -27,6 +27,10 @@ class TemplateView(View):
27
27
 
28
28
  template_name: str | None = None
29
29
 
30
+ def __init__(self, template_name=None):
31
+ # Allow template_name to be passed in as_view()
32
+ self.template_name = template_name or self.template_name
33
+
30
34
  def get_template_context(self) -> dict:
31
35
  return {
32
36
  "request": self.request,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.33.0
3
+ Version: 0.34.1
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
@@ -40,7 +40,7 @@ The `plain` package includes everything you need to start handling web requests
40
40
 
41
41
  - [plain.models](/plain-models/README.md) - Define and interact with your database models.
42
42
  - [plain.cache](/plain-cache/README.md) - A database-driven general purpose cache.
43
- - [plain.mail](/plain-mail/README.md) - Send emails with SMTP or custom backends.
43
+ - [plain.email](/plain-email/README.md) - Send emails with SMTP or custom backends.
44
44
  - [plain.sessions](/plain-sessions/README.md) - User sessions and cookies.
45
45
  - [plain.worker](/plain-worker/README.md) - Backgrounb jobs stored in the database.
46
46
  - [plain.api](/plain-api/README.md) - Build APIs with Plain views.
@@ -1,4 +1,4 @@
1
- plain/README.md,sha256=rG7ZDDSY8ExsqHvaytw5nC5_7HBwUhKuq_-Z3rdwRIs,3631
1
+ plain/README.md,sha256=toa8J29dPjAU8pSLgvpmXZJ8Oloazs8RE7RNawiNWdk,3633
2
2
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
3
3
  plain/debug.py,sha256=abPkJY4aSbBYGEYSZST_ZY3ohXPGDdz9uWQBYRqfd3M,730
4
4
  plain/exceptions.py,sha256=Z9cbPE5im_Y-bjVq8cqC85gBoqOr80rLFG5wTKixrwE,5894
@@ -47,7 +47,7 @@ plain/internal/files/uploadhandler.py,sha256=eEnd5onstypjHYtg367PnVWwCaF1kAPlLPS
47
47
  plain/internal/files/utils.py,sha256=xN4HTJXDRdcoNyrL1dFd528MBwodRlHZM8DGTD_oBIg,2646
48
48
  plain/internal/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  plain/internal/handlers/base.py,sha256=oRKni79ATI_u7sywGFExrzKvP5dpJTqIp1m521A90Ew,4169
50
- plain/internal/handlers/exception.py,sha256=rv8shMlTJdIhTm99VacILIiu5JRcmtumg8yWuy7GYto,4592
50
+ plain/internal/handlers/exception.py,sha256=vfha_6-fz6S6VYCP1PMBfue2Gw-_th6jqaTE372fGlw,4809
51
51
  plain/internal/handlers/wsgi.py,sha256=aOGCd9hJEMTVMGfgIDlSFvevd8_XCzZa2dtlR4peqZg,8253
52
52
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  plain/internal/middleware/headers.py,sha256=ENIW1Gwat54hv-ejgp2R8QTZm-PlaI7k44WU01YQrNk,964
@@ -60,8 +60,8 @@ plain/logs/loggers.py,sha256=iz9SYcwP9w5QAuwpULl48SFkVyJuuMoQ_fdLgdCHpNg,2121
60
60
  plain/logs/utils.py,sha256=9UzdCCQXJinGDs71Ngw297mlWkhgZStSd67ya4NOW98,1257
61
61
  plain/packages/README.md,sha256=nU2GcoCGmzmVOYRIeF8hp40aza0FF1ckvGqD5jIxocs,2494
62
62
  plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
63
- plain/packages/config.py,sha256=uOO7uE9jajqDhqFBafJQ3ZnfLmQiHikTzOSJ1AlP7ZM,3289
64
- plain/packages/registry.py,sha256=Aklno7y7UrBZlidtUR_YO3B5xqF46UbUtalReNcYHm8,7937
63
+ plain/packages/config.py,sha256=2U7b1cp_kqIuLdSeHGCLrSUV77TdfGRsww3PcXOazaA,2910
64
+ plain/packages/registry.py,sha256=6ogeHZ8t3kBSoLoI7998r0kIbkEPhLGn-7yi-1qVjVo,7969
65
65
  plain/preflight/README.md,sha256=9fE0Ql2JNd3CI420eg1sP_xmaim-6Ejlzi_hfk3tVS0,1511
66
66
  plain/preflight/__init__.py,sha256=j4-yPnrM5hmjumrdkBLOQjFHzRHpA6wCjiFpMNBjIqY,619
67
67
  plain/preflight/files.py,sha256=D_pBSwRXpXy2-3FWywweozuxrhIaR8w5hpPA2d6XMPs,522
@@ -101,7 +101,7 @@ plain/urls/routers.py,sha256=iEsQtTpPNDDVn7r_BQX84FESGSjOeD5qgyO_ep5rzaU,2819
101
101
  plain/urls/utils.py,sha256=WiGq6hHI-5DLFOxCQTAZ2qm0J-UdGosLcjuxlfK6_Tg,2137
102
102
  plain/utils/README.md,sha256=hRRkcg4CxMX-zz8d4Bn6V2uJr_VKgTLurc1jY7QlEx8,198
103
103
  plain/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
104
- plain/utils/cache.py,sha256=0qziMJfzulWnNWlY4MBNfaYHIbKkAXRwy4QZNDr7s3o,4497
104
+ plain/utils/cache.py,sha256=iuvOTIfI1s857iVOAPNLK5lkzlrl0fIiBYaiUXWQu40,5303
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
@@ -126,16 +126,16 @@ plain/utils/timezone.py,sha256=6u0sE-9RVp0_OCe0Y1KiYYQoq5THWLokZFQYY8jf78g,6221
126
126
  plain/utils/tree.py,sha256=wdWzmfsgc26YDF2wxhAY3yVxXTixQYqYDKE9mL3L3ZY,4383
127
127
  plain/views/README.md,sha256=JyqAWMXYdaVnRncmb12yvlBhC629zRtWR7uzkKFwc9Y,5998
128
128
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
129
- plain/views/base.py,sha256=LTnr_JMvzfHPbjp8ynKVq2IwtqBBgZR3Ib2-jBL8K-I,3167
129
+ plain/views/base.py,sha256=6Q67PEioF7b0OHqrYCu56KUCHqJ-Gr4vefMwuAXt38w,3256
130
130
  plain/views/csrf.py,sha256=7q6l5xzLWhRnMY64aNj0hR6G-3pxI2yhRwG6k_5j00E,144
131
- plain/views/errors.py,sha256=Y4oGX4Z6D2COKcDEfINvXE1acE8Ad15KwNNWPs5BCfc,967
131
+ plain/views/errors.py,sha256=jbNCJIzowwCsEvqyJ3opMeZpPDqTyhtrbqb0VnAm2HE,1263
132
132
  plain/views/exceptions.py,sha256=b4euI49ZUKS9O8AGAcFfiDpstzkRAuuj_uYQXzWNHME,138
133
133
  plain/views/forms.py,sha256=5L6dYkwcZFMD3-w_QC2QDElo9hhSPrhVVFq9CB5yL9k,2692
134
134
  plain/views/objects.py,sha256=g5Lzno0Zsv0K449UpcCtxwCoO7WMRAWqKlxxV2V0_qg,8263
135
135
  plain/views/redirect.py,sha256=9zHZgKvtSkdrMX9KmsRM8hJTPmBktxhc4d8OitbuniI,1724
136
- plain/views/templates.py,sha256=cBkFNCSXgVi8cMqQbhsqJ4M_rIQYVl8cUvq9qu4YIes,1951
137
- plain-0.33.0.dist-info/METADATA,sha256=sz1vF6GknoRS8VOTLw_6nmRz-pfn_Ji2itKzURNo2LI,3942
138
- plain-0.33.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
- plain-0.33.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
140
- plain-0.33.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
- plain-0.33.0.dist-info/RECORD,,
136
+ plain/views/templates.py,sha256=gwPqRki3SVcaTmtSCEp1MycUB-1saDYeYzmZZTQLxjE,2117
137
+ plain-0.34.1.dist-info/METADATA,sha256=dLvkActYXqeXXsOPWzMPyteeRejGun_Zg77EY2UQRRI,3944
138
+ plain-0.34.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
139
+ plain-0.34.1.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
140
+ plain-0.34.1.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
141
+ plain-0.34.1.dist-info/RECORD,,
File without changes