plain 0.80.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.

Files changed (209) hide show
  1. {plain-0.80.0 → plain-0.82.0}/PKG-INFO +1 -1
  2. {plain-0.80.0 → plain-0.82.0}/plain/CHANGELOG.md +23 -0
  3. {plain-0.80.0 → plain-0.82.0}/plain/cli/preflight.py +4 -58
  4. {plain-0.80.0 → plain-0.82.0}/plain/http/request.py +11 -0
  5. {plain-0.80.0 → plain-0.82.0}/plain/internal/middleware/headers.py +9 -2
  6. plain-0.82.0/plain/preflight/README.md +83 -0
  7. {plain-0.80.0 → plain-0.82.0}/plain/views/README.md +1 -1
  8. {plain-0.80.0 → plain-0.82.0}/plain/views/errors.py +2 -4
  9. {plain-0.80.0 → plain-0.82.0}/pyproject.toml +1 -1
  10. plain-0.80.0/plain/preflight/README.md +0 -61
  11. {plain-0.80.0 → plain-0.82.0}/.gitignore +0 -0
  12. {plain-0.80.0 → plain-0.82.0}/LICENSE +0 -0
  13. {plain-0.80.0 → plain-0.82.0}/README.md +0 -0
  14. {plain-0.80.0 → plain-0.82.0}/plain/AGENTS.md +0 -0
  15. {plain-0.80.0 → plain-0.82.0}/plain/README.md +0 -0
  16. {plain-0.80.0 → plain-0.82.0}/plain/__main__.py +0 -0
  17. {plain-0.80.0 → plain-0.82.0}/plain/assets/README.md +0 -0
  18. {plain-0.80.0 → plain-0.82.0}/plain/assets/__init__.py +0 -0
  19. {plain-0.80.0 → plain-0.82.0}/plain/assets/compile.py +0 -0
  20. {plain-0.80.0 → plain-0.82.0}/plain/assets/finders.py +0 -0
  21. {plain-0.80.0 → plain-0.82.0}/plain/assets/fingerprints.py +0 -0
  22. {plain-0.80.0 → plain-0.82.0}/plain/assets/urls.py +0 -0
  23. {plain-0.80.0 → plain-0.82.0}/plain/assets/views.py +0 -0
  24. {plain-0.80.0 → plain-0.82.0}/plain/chores/README.md +0 -0
  25. {plain-0.80.0 → plain-0.82.0}/plain/chores/__init__.py +0 -0
  26. {plain-0.80.0 → plain-0.82.0}/plain/chores/core.py +0 -0
  27. {plain-0.80.0 → plain-0.82.0}/plain/chores/registry.py +0 -0
  28. {plain-0.80.0 → plain-0.82.0}/plain/cli/README.md +0 -0
  29. {plain-0.80.0 → plain-0.82.0}/plain/cli/__init__.py +0 -0
  30. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/__init__.py +0 -0
  31. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/docs.py +0 -0
  32. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/llmdocs.py +0 -0
  33. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/md.py +0 -0
  34. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/prompt.py +0 -0
  35. {plain-0.80.0 → plain-0.82.0}/plain/cli/agent/request.py +0 -0
  36. {plain-0.80.0 → plain-0.82.0}/plain/cli/build.py +0 -0
  37. {plain-0.80.0 → plain-0.82.0}/plain/cli/changelog.py +0 -0
  38. {plain-0.80.0 → plain-0.82.0}/plain/cli/chores.py +0 -0
  39. {plain-0.80.0 → plain-0.82.0}/plain/cli/core.py +0 -0
  40. {plain-0.80.0 → plain-0.82.0}/plain/cli/docs.py +0 -0
  41. {plain-0.80.0 → plain-0.82.0}/plain/cli/formatting.py +0 -0
  42. {plain-0.80.0 → plain-0.82.0}/plain/cli/install.py +0 -0
  43. {plain-0.80.0 → plain-0.82.0}/plain/cli/output.py +0 -0
  44. {plain-0.80.0 → plain-0.82.0}/plain/cli/print.py +0 -0
  45. {plain-0.80.0 → plain-0.82.0}/plain/cli/registry.py +0 -0
  46. {plain-0.80.0 → plain-0.82.0}/plain/cli/runtime.py +0 -0
  47. {plain-0.80.0 → plain-0.82.0}/plain/cli/scaffold.py +0 -0
  48. {plain-0.80.0 → plain-0.82.0}/plain/cli/server.py +0 -0
  49. {plain-0.80.0 → plain-0.82.0}/plain/cli/settings.py +0 -0
  50. {plain-0.80.0 → plain-0.82.0}/plain/cli/shell.py +0 -0
  51. {plain-0.80.0 → plain-0.82.0}/plain/cli/startup.py +0 -0
  52. {plain-0.80.0 → plain-0.82.0}/plain/cli/upgrade.py +0 -0
  53. {plain-0.80.0 → plain-0.82.0}/plain/cli/urls.py +0 -0
  54. {plain-0.80.0 → plain-0.82.0}/plain/cli/utils.py +0 -0
  55. {plain-0.80.0 → plain-0.82.0}/plain/csrf/README.md +0 -0
  56. {plain-0.80.0 → plain-0.82.0}/plain/csrf/middleware.py +0 -0
  57. {plain-0.80.0 → plain-0.82.0}/plain/debug.py +0 -0
  58. {plain-0.80.0 → plain-0.82.0}/plain/exceptions.py +0 -0
  59. {plain-0.80.0 → plain-0.82.0}/plain/forms/README.md +0 -0
  60. {plain-0.80.0 → plain-0.82.0}/plain/forms/__init__.py +0 -0
  61. {plain-0.80.0 → plain-0.82.0}/plain/forms/boundfield.py +0 -0
  62. {plain-0.80.0 → plain-0.82.0}/plain/forms/exceptions.py +0 -0
  63. {plain-0.80.0 → plain-0.82.0}/plain/forms/fields.py +0 -0
  64. {plain-0.80.0 → plain-0.82.0}/plain/forms/forms.py +0 -0
  65. {plain-0.80.0 → plain-0.82.0}/plain/http/README.md +0 -0
  66. {plain-0.80.0 → plain-0.82.0}/plain/http/__init__.py +0 -0
  67. {plain-0.80.0 → plain-0.82.0}/plain/http/cookie.py +0 -0
  68. {plain-0.80.0 → plain-0.82.0}/plain/http/middleware.py +0 -0
  69. {plain-0.80.0 → plain-0.82.0}/plain/http/multipartparser.py +0 -0
  70. {plain-0.80.0 → plain-0.82.0}/plain/http/response.py +0 -0
  71. {plain-0.80.0 → plain-0.82.0}/plain/internal/__init__.py +0 -0
  72. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/__init__.py +0 -0
  73. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/base.py +0 -0
  74. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/locks.py +0 -0
  75. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/move.py +0 -0
  76. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/temp.py +0 -0
  77. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/uploadedfile.py +0 -0
  78. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/uploadhandler.py +0 -0
  79. {plain-0.80.0 → plain-0.82.0}/plain/internal/files/utils.py +0 -0
  80. {plain-0.80.0 → plain-0.82.0}/plain/internal/handlers/__init__.py +0 -0
  81. {plain-0.80.0 → plain-0.82.0}/plain/internal/handlers/base.py +0 -0
  82. {plain-0.80.0 → plain-0.82.0}/plain/internal/handlers/exception.py +0 -0
  83. {plain-0.80.0 → plain-0.82.0}/plain/internal/handlers/wsgi.py +0 -0
  84. {plain-0.80.0 → plain-0.82.0}/plain/internal/middleware/__init__.py +0 -0
  85. {plain-0.80.0 → plain-0.82.0}/plain/internal/middleware/hosts.py +0 -0
  86. {plain-0.80.0 → plain-0.82.0}/plain/internal/middleware/https.py +0 -0
  87. {plain-0.80.0 → plain-0.82.0}/plain/internal/middleware/slash.py +0 -0
  88. {plain-0.80.0 → plain-0.82.0}/plain/internal/reloader.py +0 -0
  89. {plain-0.80.0 → plain-0.82.0}/plain/json.py +0 -0
  90. {plain-0.80.0 → plain-0.82.0}/plain/logs/README.md +0 -0
  91. {plain-0.80.0 → plain-0.82.0}/plain/logs/__init__.py +0 -0
  92. {plain-0.80.0 → plain-0.82.0}/plain/logs/configure.py +0 -0
  93. {plain-0.80.0 → plain-0.82.0}/plain/logs/debug.py +0 -0
  94. {plain-0.80.0 → plain-0.82.0}/plain/logs/formatters.py +0 -0
  95. {plain-0.80.0 → plain-0.82.0}/plain/logs/loggers.py +0 -0
  96. {plain-0.80.0 → plain-0.82.0}/plain/packages/README.md +0 -0
  97. {plain-0.80.0 → plain-0.82.0}/plain/packages/__init__.py +0 -0
  98. {plain-0.80.0 → plain-0.82.0}/plain/packages/config.py +0 -0
  99. {plain-0.80.0 → plain-0.82.0}/plain/packages/registry.py +0 -0
  100. {plain-0.80.0 → plain-0.82.0}/plain/paginator.py +0 -0
  101. {plain-0.80.0 → plain-0.82.0}/plain/preflight/__init__.py +0 -0
  102. {plain-0.80.0 → plain-0.82.0}/plain/preflight/checks.py +0 -0
  103. {plain-0.80.0 → plain-0.82.0}/plain/preflight/files.py +0 -0
  104. {plain-0.80.0 → plain-0.82.0}/plain/preflight/registry.py +0 -0
  105. {plain-0.80.0 → plain-0.82.0}/plain/preflight/results.py +0 -0
  106. {plain-0.80.0 → plain-0.82.0}/plain/preflight/security.py +0 -0
  107. {plain-0.80.0 → plain-0.82.0}/plain/preflight/urls.py +0 -0
  108. {plain-0.80.0 → plain-0.82.0}/plain/runtime/README.md +0 -0
  109. {plain-0.80.0 → plain-0.82.0}/plain/runtime/__init__.py +0 -0
  110. {plain-0.80.0 → plain-0.82.0}/plain/runtime/global_settings.py +0 -0
  111. {plain-0.80.0 → plain-0.82.0}/plain/runtime/user_settings.py +0 -0
  112. {plain-0.80.0 → plain-0.82.0}/plain/runtime/utils.py +0 -0
  113. {plain-0.80.0 → plain-0.82.0}/plain/server/LICENSE +0 -0
  114. {plain-0.80.0 → plain-0.82.0}/plain/server/README.md +0 -0
  115. {plain-0.80.0 → plain-0.82.0}/plain/server/__init__.py +0 -0
  116. {plain-0.80.0 → plain-0.82.0}/plain/server/app.py +0 -0
  117. {plain-0.80.0 → plain-0.82.0}/plain/server/arbiter.py +0 -0
  118. {plain-0.80.0 → plain-0.82.0}/plain/server/config.py +0 -0
  119. {plain-0.80.0 → plain-0.82.0}/plain/server/errors.py +0 -0
  120. {plain-0.80.0 → plain-0.82.0}/plain/server/glogging.py +0 -0
  121. {plain-0.80.0 → plain-0.82.0}/plain/server/http/__init__.py +0 -0
  122. {plain-0.80.0 → plain-0.82.0}/plain/server/http/body.py +0 -0
  123. {plain-0.80.0 → plain-0.82.0}/plain/server/http/errors.py +0 -0
  124. {plain-0.80.0 → plain-0.82.0}/plain/server/http/message.py +0 -0
  125. {plain-0.80.0 → plain-0.82.0}/plain/server/http/parser.py +0 -0
  126. {plain-0.80.0 → plain-0.82.0}/plain/server/http/unreader.py +0 -0
  127. {plain-0.80.0 → plain-0.82.0}/plain/server/http/wsgi.py +0 -0
  128. {plain-0.80.0 → plain-0.82.0}/plain/server/pidfile.py +0 -0
  129. {plain-0.80.0 → plain-0.82.0}/plain/server/sock.py +0 -0
  130. {plain-0.80.0 → plain-0.82.0}/plain/server/util.py +0 -0
  131. {plain-0.80.0 → plain-0.82.0}/plain/server/workers/__init__.py +0 -0
  132. {plain-0.80.0 → plain-0.82.0}/plain/server/workers/base.py +0 -0
  133. {plain-0.80.0 → plain-0.82.0}/plain/server/workers/sync.py +0 -0
  134. {plain-0.80.0 → plain-0.82.0}/plain/server/workers/thread.py +0 -0
  135. {plain-0.80.0 → plain-0.82.0}/plain/server/workers/workertmp.py +0 -0
  136. {plain-0.80.0 → plain-0.82.0}/plain/signals/README.md +0 -0
  137. {plain-0.80.0 → plain-0.82.0}/plain/signals/__init__.py +0 -0
  138. {plain-0.80.0 → plain-0.82.0}/plain/signals/dispatch/__init__.py +0 -0
  139. {plain-0.80.0 → plain-0.82.0}/plain/signals/dispatch/dispatcher.py +0 -0
  140. {plain-0.80.0 → plain-0.82.0}/plain/signals/dispatch/license.txt +0 -0
  141. {plain-0.80.0 → plain-0.82.0}/plain/signing.py +0 -0
  142. {plain-0.80.0 → plain-0.82.0}/plain/templates/AGENTS.md +0 -0
  143. {plain-0.80.0 → plain-0.82.0}/plain/templates/README.md +0 -0
  144. {plain-0.80.0 → plain-0.82.0}/plain/templates/__init__.py +0 -0
  145. {plain-0.80.0 → plain-0.82.0}/plain/templates/core.py +0 -0
  146. {plain-0.80.0 → plain-0.82.0}/plain/templates/jinja/__init__.py +0 -0
  147. {plain-0.80.0 → plain-0.82.0}/plain/templates/jinja/environments.py +0 -0
  148. {plain-0.80.0 → plain-0.82.0}/plain/templates/jinja/extensions.py +0 -0
  149. {plain-0.80.0 → plain-0.82.0}/plain/templates/jinja/filters.py +0 -0
  150. {plain-0.80.0 → plain-0.82.0}/plain/templates/jinja/globals.py +0 -0
  151. {plain-0.80.0 → plain-0.82.0}/plain/test/README.md +0 -0
  152. {plain-0.80.0 → plain-0.82.0}/plain/test/__init__.py +0 -0
  153. {plain-0.80.0 → plain-0.82.0}/plain/test/client.py +0 -0
  154. {plain-0.80.0 → plain-0.82.0}/plain/test/encoding.py +0 -0
  155. {plain-0.80.0 → plain-0.82.0}/plain/test/exceptions.py +0 -0
  156. {plain-0.80.0 → plain-0.82.0}/plain/urls/README.md +0 -0
  157. {plain-0.80.0 → plain-0.82.0}/plain/urls/__init__.py +0 -0
  158. {plain-0.80.0 → plain-0.82.0}/plain/urls/converters.py +0 -0
  159. {plain-0.80.0 → plain-0.82.0}/plain/urls/exceptions.py +0 -0
  160. {plain-0.80.0 → plain-0.82.0}/plain/urls/patterns.py +0 -0
  161. {plain-0.80.0 → plain-0.82.0}/plain/urls/resolvers.py +0 -0
  162. {plain-0.80.0 → plain-0.82.0}/plain/urls/routers.py +0 -0
  163. {plain-0.80.0 → plain-0.82.0}/plain/urls/utils.py +0 -0
  164. {plain-0.80.0 → plain-0.82.0}/plain/utils/README.md +0 -0
  165. {plain-0.80.0 → plain-0.82.0}/plain/utils/__init__.py +0 -0
  166. {plain-0.80.0 → plain-0.82.0}/plain/utils/cache.py +0 -0
  167. {plain-0.80.0 → plain-0.82.0}/plain/utils/crypto.py +0 -0
  168. {plain-0.80.0 → plain-0.82.0}/plain/utils/datastructures.py +0 -0
  169. {plain-0.80.0 → plain-0.82.0}/plain/utils/dateparse.py +0 -0
  170. {plain-0.80.0 → plain-0.82.0}/plain/utils/deconstruct.py +0 -0
  171. {plain-0.80.0 → plain-0.82.0}/plain/utils/decorators.py +0 -0
  172. {plain-0.80.0 → plain-0.82.0}/plain/utils/duration.py +0 -0
  173. {plain-0.80.0 → plain-0.82.0}/plain/utils/encoding.py +0 -0
  174. {plain-0.80.0 → plain-0.82.0}/plain/utils/functional.py +0 -0
  175. {plain-0.80.0 → plain-0.82.0}/plain/utils/hashable.py +0 -0
  176. {plain-0.80.0 → plain-0.82.0}/plain/utils/html.py +0 -0
  177. {plain-0.80.0 → plain-0.82.0}/plain/utils/http.py +0 -0
  178. {plain-0.80.0 → plain-0.82.0}/plain/utils/inspect.py +0 -0
  179. {plain-0.80.0 → plain-0.82.0}/plain/utils/ipv6.py +0 -0
  180. {plain-0.80.0 → plain-0.82.0}/plain/utils/itercompat.py +0 -0
  181. {plain-0.80.0 → plain-0.82.0}/plain/utils/module_loading.py +0 -0
  182. {plain-0.80.0 → plain-0.82.0}/plain/utils/regex_helper.py +0 -0
  183. {plain-0.80.0 → plain-0.82.0}/plain/utils/safestring.py +0 -0
  184. {plain-0.80.0 → plain-0.82.0}/plain/utils/text.py +0 -0
  185. {plain-0.80.0 → plain-0.82.0}/plain/utils/timesince.py +0 -0
  186. {plain-0.80.0 → plain-0.82.0}/plain/utils/timezone.py +0 -0
  187. {plain-0.80.0 → plain-0.82.0}/plain/utils/tree.py +0 -0
  188. {plain-0.80.0 → plain-0.82.0}/plain/validators.py +0 -0
  189. {plain-0.80.0 → plain-0.82.0}/plain/views/__init__.py +0 -0
  190. {plain-0.80.0 → plain-0.82.0}/plain/views/base.py +0 -0
  191. {plain-0.80.0 → plain-0.82.0}/plain/views/exceptions.py +0 -0
  192. {plain-0.80.0 → plain-0.82.0}/plain/views/forms.py +0 -0
  193. {plain-0.80.0 → plain-0.82.0}/plain/views/objects.py +0 -0
  194. {plain-0.80.0 → plain-0.82.0}/plain/views/redirect.py +0 -0
  195. {plain-0.80.0 → plain-0.82.0}/plain/views/templates.py +0 -0
  196. {plain-0.80.0 → plain-0.82.0}/plain/wsgi.py +0 -0
  197. {plain-0.80.0 → plain-0.82.0}/tests/.gitignore +0 -0
  198. {plain-0.80.0 → plain-0.82.0}/tests/app/.gitignore +0 -0
  199. {plain-0.80.0 → plain-0.82.0}/tests/app/settings.py +0 -0
  200. {plain-0.80.0 → plain-0.82.0}/tests/app/test/__init__.py +0 -0
  201. {plain-0.80.0 → plain-0.82.0}/tests/app/test/default_settings.py +0 -0
  202. {plain-0.80.0 → plain-0.82.0}/tests/app/urls.py +0 -0
  203. {plain-0.80.0 → plain-0.82.0}/tests/conftest.py +0 -0
  204. {plain-0.80.0 → plain-0.82.0}/tests/test_cli.py +0 -0
  205. {plain-0.80.0 → plain-0.82.0}/tests/test_csrf.py +0 -0
  206. {plain-0.80.0 → plain-0.82.0}/tests/test_http_hosts.py +0 -0
  207. {plain-0.80.0 → plain-0.82.0}/tests/test_logs.py +0 -0
  208. {plain-0.80.0 → plain-0.82.0}/tests/test_runtime.py +0 -0
  209. {plain-0.80.0 → plain-0.82.0}/tests/test_wsgi.py +0 -0
@@ -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,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
@@ -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.")
@@ -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]
@@ -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
+ ```
@@ -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
 
@@ -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
  [project]
2
2
  name = "plain"
3
- version = "0.80.0"
3
+ version = "0.82.0"
4
4
  description = "A web framework for building products with Python."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -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