plain 0.81.0__tar.gz → 0.82.0__tar.gz
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-0.81.0 → plain-0.82.0}/PKG-INFO +1 -1
- {plain-0.81.0 → plain-0.82.0}/plain/CHANGELOG.md +13 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/preflight.py +4 -58
- {plain-0.81.0 → plain-0.82.0}/plain/http/request.py +11 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/middleware/headers.py +9 -2
- plain-0.82.0/plain/preflight/README.md +83 -0
- {plain-0.81.0 → plain-0.82.0}/pyproject.toml +1 -1
- plain-0.81.0/plain/preflight/README.md +0 -61
- {plain-0.81.0 → plain-0.82.0}/.gitignore +0 -0
- {plain-0.81.0 → plain-0.82.0}/LICENSE +0 -0
- {plain-0.81.0 → plain-0.82.0}/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/AGENTS.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/__main__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/compile.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/finders.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/urls.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/assets/views.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/chores/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/chores/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/chores/core.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/chores/registry.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/docs.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/md.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/agent/request.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/build.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/changelog.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/chores.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/core.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/docs.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/formatting.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/install.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/output.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/print.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/registry.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/runtime.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/scaffold.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/server.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/settings.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/shell.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/startup.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/upgrade.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/urls.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/cli/utils.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/csrf/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/csrf/middleware.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/debug.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/exceptions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/boundfield.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/exceptions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/fields.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/forms/forms.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/cookie.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/middleware.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/multipartparser.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/http/response.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/base.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/locks.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/move.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/temp.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/files/utils.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/handlers/base.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/handlers/exception.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/handlers/wsgi.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/middleware/hosts.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/internal/reloader.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/json.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/configure.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/debug.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/formatters.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/logs/loggers.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/packages/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/packages/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/packages/config.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/packages/registry.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/paginator.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/checks.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/files.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/registry.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/results.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/security.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/preflight/urls.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/runtime/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/runtime/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/runtime/global_settings.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/runtime/utils.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/LICENSE +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/app.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/arbiter.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/config.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/errors.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/glogging.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/body.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/errors.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/message.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/parser.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/unreader.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/http/wsgi.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/pidfile.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/sock.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/util.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/workers/base.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/workers/sync.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/workers/thread.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signals/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signals/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/signing.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/core.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/test/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/test/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/test/client.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/test/encoding.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/test/exceptions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/converters.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/exceptions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/patterns.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/resolvers.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/routers.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/urls/utils.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/cache.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/crypto.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/datastructures.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/dateparse.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/decorators.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/duration.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/encoding.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/functional.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/hashable.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/html.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/http.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/inspect.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/ipv6.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/itercompat.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/module_loading.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/safestring.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/text.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/timesince.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/timezone.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/utils/tree.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/validators.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/README.md +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/base.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/errors.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/exceptions.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/forms.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/objects.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/redirect.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/views/templates.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/plain/wsgi.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/.gitignore +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/app/.gitignore +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/app/settings.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/app/test/__init__.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/app/urls.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/conftest.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_cli.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_csrf.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_http_hosts.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_logs.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_runtime.py +0 -0
- {plain-0.81.0 → plain-0.82.0}/tests/test_wsgi.py +0 -0
|
@@ -1,5 +1,18 @@
|
|
|
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
|
+
|
|
3
16
|
## [0.81.0](https://github.com/dropseed/plain/releases/plain@0.81.0) (2025-10-22)
|
|
4
17
|
|
|
5
18
|
### What's 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.
|
|
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="
|
|
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
|
|
27
|
+
def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
|
|
36
28
|
"""
|
|
37
|
-
|
|
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.")
|
|
@@ -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
|
|
17
|
-
|
|
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]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Preflight
|
|
2
|
+
|
|
3
|
+
**System checks for Plain applications.**
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Development](#development)
|
|
7
|
+
- [Deployment](#deployment)
|
|
8
|
+
- [Custom preflight checks](#custom-preflight-checks)
|
|
9
|
+
- [Silencing preflight checks](#silencing-preflight-checks)
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
Preflight checks help identify issues with your settings or environment before running your application.
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
plain preflight
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
If you use [`plain.dev`](/plain-dev/README.md) for local development, the Plain preflight command is run automatically when you run `plain dev`.
|
|
22
|
+
|
|
23
|
+
## Deployment
|
|
24
|
+
|
|
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
|
+
|
|
27
|
+
```bash
|
|
28
|
+
plain preflight --deploy
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Custom preflight checks
|
|
32
|
+
|
|
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
|
+
|
|
35
|
+
```python
|
|
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 []
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For deployment-specific checks, add `deploy=True` to the decorator.
|
|
56
|
+
|
|
57
|
+
```python
|
|
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 []
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Silencing preflight checks
|
|
75
|
+
|
|
76
|
+
The `settings.PREFLIGHT_SILENCED_CHECKS` setting can be used to silence individual checks by their ID (ex. `security.E020`).
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# app/settings.py
|
|
80
|
+
PREFLIGHT_SILENCED_CHECKS = [
|
|
81
|
+
"security.E020", # Allow empty ALLOWED_HOSTS in deployment
|
|
82
|
+
]
|
|
83
|
+
```
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# Preflight
|
|
2
|
-
|
|
3
|
-
**System checks for Plain applications.**
|
|
4
|
-
|
|
5
|
-
- [Overview](#overview)
|
|
6
|
-
- [Development](#development)
|
|
7
|
-
- [Deployment](#deployment)
|
|
8
|
-
- [Custom preflight checks](#custom-preflight-checks)
|
|
9
|
-
- [Silencing preflight checks](#silencing-preflight-checks)
|
|
10
|
-
|
|
11
|
-
## Overview
|
|
12
|
-
|
|
13
|
-
Preflight checks help identify issues with your settings or environment before running your application.
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
plain preflight check
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
## Development
|
|
20
|
-
|
|
21
|
-
If you use [`plain.dev`](/plain-dev/README.md) for local development, the Plain preflight command is run automatically when you run `plain dev`.
|
|
22
|
-
|
|
23
|
-
## Deployment
|
|
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.
|
|
26
|
-
|
|
27
|
-
```bash
|
|
28
|
-
plain preflight check --deploy
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
## Custom preflight checks
|
|
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.
|
|
34
|
-
|
|
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")
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
For deployment-specific checks, add the `deploy` argument to the decorator.
|
|
45
|
-
|
|
46
|
-
```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")
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## Silencing preflight checks
|
|
53
|
-
|
|
54
|
-
The `settings.PREFLIGHT_SILENCED_CHECKS` setting can be used to silence individual checks by their ID (ex. `security.E020`).
|
|
55
|
-
|
|
56
|
-
```python
|
|
57
|
-
# app/settings.py
|
|
58
|
-
PREFLIGHT_SILENCED_CHECKS = [
|
|
59
|
-
"security.E020", # Allow empty ALLOWED_HOSTS in deployment
|
|
60
|
-
]
|
|
61
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|