plain 0.80.0__py3-none-any.whl → 0.82.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.

Potentially problematic release.


This version of plain might be problematic. Click here for more details.

plain/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.82.0](https://github.com/dropseed/plain/releases/plain@0.82.0) (2025-10-29)
4
+
5
+ ### What's changed
6
+
7
+ - The `DEFAULT_RESPONSE_HEADERS` setting can now be a callable that accepts a request argument, enabling dynamic header generation per request ([cb92905834](https://github.com/dropseed/plain/commit/cb92905834))
8
+ - Added `request.csp_nonce` cached property for generating Content Security Policy nonces ([75071dcc70](https://github.com/dropseed/plain/commit/75071dcc70))
9
+ - Simplified the preflight command by moving `plain preflight check` back to `plain preflight` ([40c2c4560e](https://github.com/dropseed/plain/commit/40c2c4560e))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - If you use `plain preflight check`, update to `plain preflight` (the `check` subcommand has been removed for simplicity)
14
+ - If you use `plain preflight check --deploy`, update to `plain preflight --deploy`
15
+
16
+ ## [0.81.0](https://github.com/dropseed/plain/releases/plain@0.81.0) (2025-10-22)
17
+
18
+ ### What's changed
19
+
20
+ - Removed support for category-specific error template fallbacks like `4xx.html` and `5xx.html` ([9513f7c4fa](https://github.com/dropseed/plain/commit/9513f7c4fa))
21
+
22
+ ### Upgrade instructions
23
+
24
+ - If you have `4xx.html` or `5xx.html` error templates, rename them to specific status code templates (e.g., `404.html`, `500.html`) or remove them if you prefer the plain HTTP response fallback
25
+
3
26
  ## [0.80.0](https://github.com/dropseed/plain/releases/plain@0.80.0) (2025-10-22)
4
27
 
5
28
  ### What's changed
plain/cli/preflight.py CHANGED
@@ -5,21 +5,13 @@ import click
5
5
 
6
6
  from plain import preflight
7
7
  from plain.packages import packages_registry
8
- from plain.preflight.registry import checks_registry
9
- from plain.runtime import settings
10
8
 
11
9
 
12
- @click.group("preflight")
13
- def preflight_cli() -> None:
14
- """Run or manage preflight checks."""
15
- pass
16
-
17
-
18
- @preflight_cli.command("check")
10
+ @click.command("preflight")
19
11
  @click.option(
20
12
  "--deploy",
21
13
  is_flag=True,
22
- help="Check deployment settings.",
14
+ help="Include deployment checks.",
23
15
  )
24
16
  @click.option(
25
17
  "--format",
@@ -32,14 +24,13 @@ def preflight_cli() -> None:
32
24
  is_flag=True,
33
25
  help="Hide progress output and warnings, only show errors.",
34
26
  )
35
- def check_command(deploy: bool, format: str, quiet: bool) -> None:
27
+ def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
36
28
  """
37
- Use the system check framework to validate entire Plain project.
29
+ Run preflight checks to validate your Plain project.
38
30
  Exit with error code if any errors are found. Warnings do not cause failure.
39
31
  """
40
32
  # Auto-discover and load preflight checks
41
33
  packages_registry.autodiscover_modules("preflight", include_app=True)
42
-
43
34
  if not quiet:
44
35
  click.secho("Running preflight checks...", dim=True, italic=True, err=True)
45
36
 
@@ -200,48 +191,3 @@ def check_command(deploy: bool, format: str, quiet: bool) -> None:
200
191
  # Exit with error if there are any errors (not warnings)
201
192
  if has_errors:
202
193
  sys.exit(1)
203
-
204
-
205
- @preflight_cli.command("list")
206
- def list_checks() -> None:
207
- """List all available preflight checks."""
208
- packages_registry.autodiscover_modules("preflight", include_app=True)
209
-
210
- regular = []
211
- deployment = []
212
- silenced_checks = settings.PREFLIGHT_SILENCED_CHECKS
213
-
214
- for name, (check_class, deploy) in sorted(checks_registry.checks.items()):
215
- # Use class docstring as description
216
- description = check_class.__doc__ or "No description"
217
- # Get first line of docstring
218
- description = description.strip().split("\n")[0]
219
-
220
- is_silenced = name in silenced_checks
221
- if deploy:
222
- deployment.append((name, description, is_silenced))
223
- else:
224
- regular.append((name, description, is_silenced))
225
-
226
- if regular:
227
- click.echo("Regular checks:")
228
- for name, description, is_silenced in regular:
229
- silenced_text = (
230
- click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
231
- )
232
- click.echo(
233
- f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
234
- )
235
-
236
- if deployment:
237
- click.echo("\nDeployment checks:")
238
- for name, description, is_silenced in deployment:
239
- silenced_text = (
240
- click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
241
- )
242
- click.echo(
243
- f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
244
- )
245
-
246
- if not regular and not deployment:
247
- click.echo("No preflight checks found.")
plain/http/request.py CHANGED
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import codecs
4
4
  import copy
5
5
  import json
6
+ import secrets
6
7
  import uuid
7
8
  from collections.abc import Iterator
8
9
  from functools import cached_property
@@ -101,6 +102,16 @@ class Request:
101
102
  def headers(self) -> RequestHeaders:
102
103
  return RequestHeaders(self.meta)
103
104
 
105
+ @cached_property
106
+ def csp_nonce(self) -> str:
107
+ """Generate a cryptographically secure nonce for Content Security Policy.
108
+
109
+ The nonce is generated once per request and cached. It can be used in
110
+ CSP headers and templates to allow specific inline scripts/styles while
111
+ blocking others.
112
+ """
113
+ return secrets.token_urlsafe(16)
114
+
104
115
  @cached_property
105
116
  def accepted_types(self) -> list[MediaType]:
106
117
  """Return accepted media types sorted by quality value (highest first).
@@ -13,8 +13,15 @@ class DefaultHeadersMiddleware(HttpMiddleware):
13
13
  def process_request(self, request: Request) -> Response:
14
14
  response = self.get_response(request)
15
15
 
16
- for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
17
- # Since we don't have a good way to *remote* default response headers,
16
+ # Support callable DEFAULT_RESPONSE_HEADERS for dynamic header generation
17
+ # (e.g., CSP nonces that change per request)
18
+ if callable(settings.DEFAULT_RESPONSE_HEADERS):
19
+ default_headers = settings.DEFAULT_RESPONSE_HEADERS(request)
20
+ else:
21
+ default_headers = settings.DEFAULT_RESPONSE_HEADERS
22
+
23
+ for header, value in default_headers.items():
24
+ # Since we don't have a good way to *remove* default response headers,
18
25
  # use allow users to set them to an empty string to indicate they should be removed.
19
26
  if header in response.headers and response.headers[header] == "":
20
27
  del response.headers[header]
plain/preflight/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  Preflight checks help identify issues with your settings or environment before running your application.
14
14
 
15
15
  ```bash
16
- plain preflight check
16
+ plain preflight
17
17
  ```
18
18
 
19
19
  ## Development
@@ -22,31 +22,53 @@ If you use [`plain.dev`](/plain-dev/README.md) for local development, the Plain
22
22
 
23
23
  ## Deployment
24
24
 
25
- The `plain preflight check` command should often be part of your deployment process. Make sure to add the `--deploy` flag to the command to run checks that are only relevant in a production environment.
25
+ The `plain preflight` command should often be part of your deployment process. Make sure to add the `--deploy` flag to the command to run checks that are only relevant in a production environment.
26
26
 
27
27
  ```bash
28
- plain preflight check --deploy
28
+ plain preflight --deploy
29
29
  ```
30
30
 
31
31
  ## Custom preflight checks
32
32
 
33
- Use the `@register_check` decorator to add your own preflight check to the system. Just make sure that particular Python module is somehow imported so the check registration runs.
33
+ Use the `@register_check` decorator to add your own preflight check to the system. Create a class that inherits from `PreflightCheck` and implements a `run()` method that returns a list of `PreflightResult` objects.
34
34
 
35
35
  ```python
36
- from plain.preflight import register_check, Error
37
-
38
-
39
- @register_check
40
- def custom_check(package_configs, **kwargs):
41
- return Error("This is a custom error message.", id="custom.C001")
36
+ from plain.preflight import PreflightCheck, PreflightResult, register_check
37
+
38
+
39
+ @register_check("custom.example")
40
+ class CustomCheck(PreflightCheck):
41
+ """Description of what this check validates."""
42
+
43
+ def run(self) -> list[PreflightResult]:
44
+ # Your check logic here
45
+ if some_condition:
46
+ return [
47
+ PreflightResult(
48
+ fix="This is a custom error message.",
49
+ id="custom.example_failed",
50
+ )
51
+ ]
52
+ return []
42
53
  ```
43
54
 
44
- For deployment-specific checks, add the `deploy` argument to the decorator.
55
+ For deployment-specific checks, add `deploy=True` to the decorator.
45
56
 
46
57
  ```python
47
- @register_check(deploy=True)
48
- def custom_deploy_check(package_configs, **kwargs):
49
- return Error("This is a custom error message for deployment.", id="custom.D001")
58
+ @register_check("custom.deploy_example", deploy=True)
59
+ class CustomDeployCheck(PreflightCheck):
60
+ """Description of what this deployment check validates."""
61
+
62
+ def run(self) -> list[PreflightResult]:
63
+ # Your deployment check logic here
64
+ if some_deploy_condition:
65
+ return [
66
+ PreflightResult(
67
+ fix="This is a custom error message for deployment.",
68
+ id="custom.deploy_example_failed",
69
+ )
70
+ ]
71
+ return []
50
72
  ```
51
73
 
52
74
  ## Silencing preflight checks
plain/views/README.md CHANGED
@@ -257,7 +257,7 @@ HTTP errors are rendered using templates. Create templates for the errors users
257
257
  - `templates/403.html` - Forbidden
258
258
  - `templates/500.html` - Server error
259
259
 
260
- Plain looks for `{status_code}.html`, then `{category}.html` (e.g., `4xx.html`), then returns a plain HTTP response. Most apps only need the three specific templates above.
260
+ Plain looks for `{status_code}.html` templates, then returns a plain HTTP response if not found. Most apps only need the three specific templates above.
261
261
 
262
262
  Templates receive `status_code` and `exception` in context.
263
263
 
plain/views/errors.py CHANGED
@@ -29,10 +29,8 @@ class ErrorView(TemplateView):
29
29
  return context
30
30
 
31
31
  def get_template_names(self) -> list[str]:
32
- # Try specific status code first (e.g. "404.html")
33
- # Then fall back to category (e.g. "4xx.html" or "5xx.html")
34
- category = f"{str(self.status_code)[0]}xx"
35
- return [f"{self.status_code}.html", f"{category}.html"]
32
+ # Try specific status code template (e.g. "404.html")
33
+ return [f"{self.status_code}.html"]
36
34
 
37
35
  def get_request_handler(self) -> Callable[[], Any]:
38
36
  return self.get # All methods (post, patch, etc.) will use the get()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.80.0
3
+ Version: 0.82.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,5 +1,5 @@
1
1
  plain/AGENTS.md,sha256=As6EFSWWHJ9lYIxb2LMRqNRteH45SRs7a_VFslzF53M,1046
2
- plain/CHANGELOG.md,sha256=CZJXaqSX2G0nv_Bp-_VKzIuVGgDlwtFdD_cEek6ajpg,31725
2
+ plain/CHANGELOG.md,sha256=LafG-4y2ZZGMobjIsHK5Iob3e47rA_umHo__6KUA1rI,33085
3
3
  plain/README.md,sha256=VvzhXNvf4I6ddmjBP9AExxxFXxs7RwyoxdgFm-W5dsg,4072
4
4
  plain/__main__.py,sha256=GK39854Lc_LO_JP8DzY9Y2MIQ4cQEl7SXFJy244-lC8,110
5
5
  plain/debug.py,sha256=C2OnFHtRGMrpCiHSt-P2r58JypgQZ62qzDBpV4mhtFM,855
@@ -30,7 +30,7 @@ plain/cli/docs.py,sha256=PU3v7Z7qgYFG-bClpuDg4JeWwC8uvLYX3ovkQDMseVs,1146
30
30
  plain/cli/formatting.py,sha256=e1doTFalAM11bD_Cvqeu6sTap81JrQcB-4kMjZzAHmY,2737
31
31
  plain/cli/install.py,sha256=mffSYBmSJSj44OPBfu53nBQoyoz4jk69DvppubIB0mU,2791
32
32
  plain/cli/output.py,sha256=uZTHZR-Axeoi2r6fgcDCpDA7iQSRrktBtTf1yBT5djI,1426
33
- plain/cli/preflight.py,sha256=5UXOowjiCMqsGIrpHg60f6Ptjk40rMiDSowN8wy5jSY,8541
33
+ plain/cli/preflight.py,sha256=zpFTkk4yET3mosFBxHY5B-vSlLu1LXvAZ3hrcUZG-oo,6721
34
34
  plain/cli/print.py,sha256=7kv9ddXpwOHRSWp6FFLfX4wbmhV7neoOBlE0VcXWccw,238
35
35
  plain/cli/registry.py,sha256=Z52nVE2bC2h_B_SENnXctn3mx3UWB0qYg969DVP7XX8,1106
36
36
  plain/cli/runtime.py,sha256=YbGYfwkH0VxfuIMbOCwM9wSWiQKusPn_gVeGod8OFaE,743
@@ -61,7 +61,7 @@ plain/http/__init__.py,sha256=gUTIGh-GbSIlh3SP-Db-XAOX4P_j2hh2445w8Cm-zKQ,1032
61
61
  plain/http/cookie.py,sha256=x13G3LIr0jxnPK1NQRptmi0DrAq9PsivQnQTm4LKaW0,2191
62
62
  plain/http/middleware.py,sha256=TPs585IIFjgp-5uUAJtIoigH6uwTS3FJqwFSsQdayd4,960
63
63
  plain/http/multipartparser.py,sha256=3W9osVGV9LshNF3aAUCBp7OBYTgD6hN2jS7T15BIKCs,28350
64
- plain/http/request.py,sha256=ficL1Lh-71tU1SVFKD4beLEJsPk7eesZG0nPPbACMTk,26462
64
+ plain/http/request.py,sha256=JexbxZ7EXbvi-J9kntAdIwR9p3vk2Cekb6UoFJXfK74,26850
65
65
  plain/http/response.py,sha256=9AlV1PfBsimNFW5LV-o9xcRvp1uXEFeXMnq1vAYPYh0,24971
66
66
  plain/internal/__init__.py,sha256=n2AgdfNelt_tp8CS9JDzHMy_aiTUMPGZiFFwKmNz2fg,262
67
67
  plain/internal/reloader.py,sha256=n7B-F-WeUXp37pAnvzKX9tcEbUxHSlYqa4gItyA_zko,2662
@@ -78,7 +78,7 @@ plain/internal/handlers/base.py,sha256=ZlpOYqd45X0wPYlmxHwXR5kyCkWBB6ZJeSGZka5VA
78
78
  plain/internal/handlers/exception.py,sha256=P7oTqVtKoIHBOxCml1BSgNBV_rQWyCHlO5hS87NVjXo,4872
79
79
  plain/internal/handlers/wsgi.py,sha256=d2Hcs4fzTSir6OvtpVromTw0RmmFAqTL4_EaBjoHtNU,8919
80
80
  plain/internal/middleware/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
- plain/internal/middleware/headers.py,sha256=-lgba1Em1FiqFbaeI7bwCkd4yI-UuOnEf3vh1eKK87Q,1100
81
+ plain/internal/middleware/headers.py,sha256=z3rmjIGGYV5JcAfAUm3tcdG34OFoEfYukJrwZsq7JIo,1425
82
82
  plain/internal/middleware/hosts.py,sha256=UXMts7cKA9ocOK-J8DAcZPbKYQtfVyLSLH2QQX3mKjM,5895
83
83
  plain/internal/middleware/https.py,sha256=8n990O0Ej5cRrBlfAnLW2nZ2mx0wn0002C4HuRhVpsM,1117
84
84
  plain/internal/middleware/slash.py,sha256=V_rTb9v8uMUR_N2TLETBwlJFItoL4WD9JnRyFaFs878,3073
@@ -92,7 +92,7 @@ plain/packages/README.md,sha256=iNqMtwFDVNf2TqKUzLKQW5Y4_GsssmdB4cVerzu27Ro,2674
92
92
  plain/packages/__init__.py,sha256=OpQny0xLplPdPpozVUUkrW2gB-IIYyDT1b4zMzOcCC4,160
93
93
  plain/packages/config.py,sha256=dxs_i-z6noQF_6j3lq11mhnQ1Bj10u2CXTJY0JgeQgc,3166
94
94
  plain/packages/registry.py,sha256=bmqY1rQau4MRpbf6DXkUVlqF4XJ9Mz2awSi5E7tCGd4,9032
95
- plain/preflight/README.md,sha256=vR43F_ls81hRSo7J2NNZ4VOMoRaJ1bS5JwA6l4ez36g,1782
95
+ plain/preflight/README.md,sha256=tH3KctMH_Zrz4ohZXlhQAGuZmPw1mMa4jr-iE0SHvC4,2470
96
96
  plain/preflight/__init__.py,sha256=-uBIVLD1DlJUVypQsEcrOtaNAhECbOpKhyoz0c_WMhA,416
97
97
  plain/preflight/checks.py,sha256=kJcr-Hq5bsjKw1BUYR6r6nFg3Ecgrd1DS5SudUr4rSU,289
98
98
  plain/preflight/files.py,sha256=OjD76e-l_cDXJGHMk21LsoPp6V_HxD5v0zyvOKkEDu0,840
@@ -179,17 +179,17 @@ plain/utils/text.py,sha256=teav7elbqEtGnhKG3ajf-V9Hb-Gsg8uqDrogqWizqjI,10094
179
179
  plain/utils/timesince.py,sha256=a_-ZoPK_s3Pt998CW4rWp0clZ1XyK2x04hCqak2giII,5928
180
180
  plain/utils/timezone.py,sha256=M_I5yvs9NsHbtNBPJgHErvWw9vatzx4M96tRQs5gS3g,6823
181
181
  plain/utils/tree.py,sha256=rj_JpZ2kVD3UExWoKnsRdVCoRjvzkuVOONcHzREjSyw,4766
182
- plain/views/README.md,sha256=Kuw6tMfFp0-fu2aDFM0iCKV-FzgZmkPwd78cYKDXaUA,7438
182
+ plain/views/README.md,sha256=F6RuNm2viDM3EGHCuy9rbmQaW-lrCVipdOfTEdWcEqY,7418
183
183
  plain/views/__init__.py,sha256=a-N1nkklVohJTtz0yD1MMaS0g66HviEjsKydNVVjvVc,392
184
184
  plain/views/base.py,sha256=yWh6S68PsYcH1dvRdibQIanBYkjo2iJ8IAbR2PTWQrk,4419
185
- plain/views/errors.py,sha256=og20lx5WGDiQ-thOAYtWNsfjuZiLLVydpk94k810j6A,1632
185
+ plain/views/errors.py,sha256=fV9rGV1RSo10_0P9J-uIFprhrO5lDVlcJKpfyFV22gY,1495
186
186
  plain/views/exceptions.py,sha256=-YKH1Jd9Zm_yXiz797PVjJB6VWaPCTXClHIUkG2fq78,198
187
187
  plain/views/forms.py,sha256=ESZOXuo6IeYixp1RZvPb94KplkowRiwO2eGJCM6zJI0,2400
188
188
  plain/views/objects.py,sha256=5y0PoPPo07dQTTcJ_9kZcx0iI1O7regsooAIK4VqXQ0,5579
189
189
  plain/views/redirect.py,sha256=mIpSAFcaEyeLDyv4Fr6g_ektduG4Wfa6s6L-rkdazmM,2154
190
190
  plain/views/templates.py,sha256=9LgDMCv4C7JzLiyw8jHF-i4350ukwgixC_9y4faEGu0,1885
191
- plain-0.80.0.dist-info/METADATA,sha256=jweqjIZtRoFGSJ0YCjb5dF9bcOAsPZaGbrdTCB4ktY4,4516
192
- plain-0.80.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
- plain-0.80.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
- plain-0.80.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
- plain-0.80.0.dist-info/RECORD,,
191
+ plain-0.82.0.dist-info/METADATA,sha256=Az4gbY6JEIaJb3xC12DeYjEl2fezGU6GXHx64Ku1WLs,4516
192
+ plain-0.82.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
193
+ plain-0.82.0.dist-info/entry_points.txt,sha256=1Ys2lsSeMepD1vz8RSrJopna0RQfUd951vYvCRsvl6A,45
194
+ plain-0.82.0.dist-info/licenses/LICENSE,sha256=m0D5O7QoH9l5Vz_rrX_9r-C8d9UNr_ciK6Qwac7o6yo,3175
195
+ plain-0.82.0.dist-info/RECORD,,
File without changes