plain 0.81.0__tar.gz → 0.83.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.
Files changed (210) hide show
  1. {plain-0.81.0 → plain-0.83.0}/PKG-INFO +1 -1
  2. {plain-0.81.0 → plain-0.83.0}/plain/CHANGELOG.md +24 -0
  3. {plain-0.81.0 → plain-0.83.0}/plain/cli/preflight.py +4 -58
  4. plain-0.83.0/plain/http/README.md +89 -0
  5. {plain-0.81.0 → plain-0.83.0}/plain/http/request.py +11 -0
  6. {plain-0.81.0 → plain-0.83.0}/plain/internal/middleware/headers.py +9 -2
  7. plain-0.83.0/plain/preflight/README.md +83 -0
  8. {plain-0.81.0 → plain-0.83.0}/plain/utils/html.py +12 -7
  9. {plain-0.81.0 → plain-0.83.0}/pyproject.toml +1 -1
  10. plain-0.81.0/plain/http/README.md +0 -30
  11. plain-0.81.0/plain/preflight/README.md +0 -61
  12. {plain-0.81.0 → plain-0.83.0}/.gitignore +0 -0
  13. {plain-0.81.0 → plain-0.83.0}/LICENSE +0 -0
  14. {plain-0.81.0 → plain-0.83.0}/README.md +0 -0
  15. {plain-0.81.0 → plain-0.83.0}/plain/AGENTS.md +0 -0
  16. {plain-0.81.0 → plain-0.83.0}/plain/README.md +0 -0
  17. {plain-0.81.0 → plain-0.83.0}/plain/__main__.py +0 -0
  18. {plain-0.81.0 → plain-0.83.0}/plain/assets/README.md +0 -0
  19. {plain-0.81.0 → plain-0.83.0}/plain/assets/__init__.py +0 -0
  20. {plain-0.81.0 → plain-0.83.0}/plain/assets/compile.py +0 -0
  21. {plain-0.81.0 → plain-0.83.0}/plain/assets/finders.py +0 -0
  22. {plain-0.81.0 → plain-0.83.0}/plain/assets/fingerprints.py +0 -0
  23. {plain-0.81.0 → plain-0.83.0}/plain/assets/urls.py +0 -0
  24. {plain-0.81.0 → plain-0.83.0}/plain/assets/views.py +0 -0
  25. {plain-0.81.0 → plain-0.83.0}/plain/chores/README.md +0 -0
  26. {plain-0.81.0 → plain-0.83.0}/plain/chores/__init__.py +0 -0
  27. {plain-0.81.0 → plain-0.83.0}/plain/chores/core.py +0 -0
  28. {plain-0.81.0 → plain-0.83.0}/plain/chores/registry.py +0 -0
  29. {plain-0.81.0 → plain-0.83.0}/plain/cli/README.md +0 -0
  30. {plain-0.81.0 → plain-0.83.0}/plain/cli/__init__.py +0 -0
  31. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/__init__.py +0 -0
  32. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/docs.py +0 -0
  33. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/llmdocs.py +0 -0
  34. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/md.py +0 -0
  35. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/prompt.py +0 -0
  36. {plain-0.81.0 → plain-0.83.0}/plain/cli/agent/request.py +0 -0
  37. {plain-0.81.0 → plain-0.83.0}/plain/cli/build.py +0 -0
  38. {plain-0.81.0 → plain-0.83.0}/plain/cli/changelog.py +0 -0
  39. {plain-0.81.0 → plain-0.83.0}/plain/cli/chores.py +0 -0
  40. {plain-0.81.0 → plain-0.83.0}/plain/cli/core.py +0 -0
  41. {plain-0.81.0 → plain-0.83.0}/plain/cli/docs.py +0 -0
  42. {plain-0.81.0 → plain-0.83.0}/plain/cli/formatting.py +0 -0
  43. {plain-0.81.0 → plain-0.83.0}/plain/cli/install.py +0 -0
  44. {plain-0.81.0 → plain-0.83.0}/plain/cli/output.py +0 -0
  45. {plain-0.81.0 → plain-0.83.0}/plain/cli/print.py +0 -0
  46. {plain-0.81.0 → plain-0.83.0}/plain/cli/registry.py +0 -0
  47. {plain-0.81.0 → plain-0.83.0}/plain/cli/runtime.py +0 -0
  48. {plain-0.81.0 → plain-0.83.0}/plain/cli/scaffold.py +0 -0
  49. {plain-0.81.0 → plain-0.83.0}/plain/cli/server.py +0 -0
  50. {plain-0.81.0 → plain-0.83.0}/plain/cli/settings.py +0 -0
  51. {plain-0.81.0 → plain-0.83.0}/plain/cli/shell.py +0 -0
  52. {plain-0.81.0 → plain-0.83.0}/plain/cli/startup.py +0 -0
  53. {plain-0.81.0 → plain-0.83.0}/plain/cli/upgrade.py +0 -0
  54. {plain-0.81.0 → plain-0.83.0}/plain/cli/urls.py +0 -0
  55. {plain-0.81.0 → plain-0.83.0}/plain/cli/utils.py +0 -0
  56. {plain-0.81.0 → plain-0.83.0}/plain/csrf/README.md +0 -0
  57. {plain-0.81.0 → plain-0.83.0}/plain/csrf/middleware.py +0 -0
  58. {plain-0.81.0 → plain-0.83.0}/plain/debug.py +0 -0
  59. {plain-0.81.0 → plain-0.83.0}/plain/exceptions.py +0 -0
  60. {plain-0.81.0 → plain-0.83.0}/plain/forms/README.md +0 -0
  61. {plain-0.81.0 → plain-0.83.0}/plain/forms/__init__.py +0 -0
  62. {plain-0.81.0 → plain-0.83.0}/plain/forms/boundfield.py +0 -0
  63. {plain-0.81.0 → plain-0.83.0}/plain/forms/exceptions.py +0 -0
  64. {plain-0.81.0 → plain-0.83.0}/plain/forms/fields.py +0 -0
  65. {plain-0.81.0 → plain-0.83.0}/plain/forms/forms.py +0 -0
  66. {plain-0.81.0 → plain-0.83.0}/plain/http/__init__.py +0 -0
  67. {plain-0.81.0 → plain-0.83.0}/plain/http/cookie.py +0 -0
  68. {plain-0.81.0 → plain-0.83.0}/plain/http/middleware.py +0 -0
  69. {plain-0.81.0 → plain-0.83.0}/plain/http/multipartparser.py +0 -0
  70. {plain-0.81.0 → plain-0.83.0}/plain/http/response.py +0 -0
  71. {plain-0.81.0 → plain-0.83.0}/plain/internal/__init__.py +0 -0
  72. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/__init__.py +0 -0
  73. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/base.py +0 -0
  74. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/locks.py +0 -0
  75. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/move.py +0 -0
  76. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/temp.py +0 -0
  77. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/uploadedfile.py +0 -0
  78. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/uploadhandler.py +0 -0
  79. {plain-0.81.0 → plain-0.83.0}/plain/internal/files/utils.py +0 -0
  80. {plain-0.81.0 → plain-0.83.0}/plain/internal/handlers/__init__.py +0 -0
  81. {plain-0.81.0 → plain-0.83.0}/plain/internal/handlers/base.py +0 -0
  82. {plain-0.81.0 → plain-0.83.0}/plain/internal/handlers/exception.py +0 -0
  83. {plain-0.81.0 → plain-0.83.0}/plain/internal/handlers/wsgi.py +0 -0
  84. {plain-0.81.0 → plain-0.83.0}/plain/internal/middleware/__init__.py +0 -0
  85. {plain-0.81.0 → plain-0.83.0}/plain/internal/middleware/hosts.py +0 -0
  86. {plain-0.81.0 → plain-0.83.0}/plain/internal/middleware/https.py +0 -0
  87. {plain-0.81.0 → plain-0.83.0}/plain/internal/middleware/slash.py +0 -0
  88. {plain-0.81.0 → plain-0.83.0}/plain/internal/reloader.py +0 -0
  89. {plain-0.81.0 → plain-0.83.0}/plain/json.py +0 -0
  90. {plain-0.81.0 → plain-0.83.0}/plain/logs/README.md +0 -0
  91. {plain-0.81.0 → plain-0.83.0}/plain/logs/__init__.py +0 -0
  92. {plain-0.81.0 → plain-0.83.0}/plain/logs/configure.py +0 -0
  93. {plain-0.81.0 → plain-0.83.0}/plain/logs/debug.py +0 -0
  94. {plain-0.81.0 → plain-0.83.0}/plain/logs/formatters.py +0 -0
  95. {plain-0.81.0 → plain-0.83.0}/plain/logs/loggers.py +0 -0
  96. {plain-0.81.0 → plain-0.83.0}/plain/packages/README.md +0 -0
  97. {plain-0.81.0 → plain-0.83.0}/plain/packages/__init__.py +0 -0
  98. {plain-0.81.0 → plain-0.83.0}/plain/packages/config.py +0 -0
  99. {plain-0.81.0 → plain-0.83.0}/plain/packages/registry.py +0 -0
  100. {plain-0.81.0 → plain-0.83.0}/plain/paginator.py +0 -0
  101. {plain-0.81.0 → plain-0.83.0}/plain/preflight/__init__.py +0 -0
  102. {plain-0.81.0 → plain-0.83.0}/plain/preflight/checks.py +0 -0
  103. {plain-0.81.0 → plain-0.83.0}/plain/preflight/files.py +0 -0
  104. {plain-0.81.0 → plain-0.83.0}/plain/preflight/registry.py +0 -0
  105. {plain-0.81.0 → plain-0.83.0}/plain/preflight/results.py +0 -0
  106. {plain-0.81.0 → plain-0.83.0}/plain/preflight/security.py +0 -0
  107. {plain-0.81.0 → plain-0.83.0}/plain/preflight/urls.py +0 -0
  108. {plain-0.81.0 → plain-0.83.0}/plain/runtime/README.md +0 -0
  109. {plain-0.81.0 → plain-0.83.0}/plain/runtime/__init__.py +0 -0
  110. {plain-0.81.0 → plain-0.83.0}/plain/runtime/global_settings.py +0 -0
  111. {plain-0.81.0 → plain-0.83.0}/plain/runtime/user_settings.py +0 -0
  112. {plain-0.81.0 → plain-0.83.0}/plain/runtime/utils.py +0 -0
  113. {plain-0.81.0 → plain-0.83.0}/plain/server/LICENSE +0 -0
  114. {plain-0.81.0 → plain-0.83.0}/plain/server/README.md +0 -0
  115. {plain-0.81.0 → plain-0.83.0}/plain/server/__init__.py +0 -0
  116. {plain-0.81.0 → plain-0.83.0}/plain/server/app.py +0 -0
  117. {plain-0.81.0 → plain-0.83.0}/plain/server/arbiter.py +0 -0
  118. {plain-0.81.0 → plain-0.83.0}/plain/server/config.py +0 -0
  119. {plain-0.81.0 → plain-0.83.0}/plain/server/errors.py +0 -0
  120. {plain-0.81.0 → plain-0.83.0}/plain/server/glogging.py +0 -0
  121. {plain-0.81.0 → plain-0.83.0}/plain/server/http/__init__.py +0 -0
  122. {plain-0.81.0 → plain-0.83.0}/plain/server/http/body.py +0 -0
  123. {plain-0.81.0 → plain-0.83.0}/plain/server/http/errors.py +0 -0
  124. {plain-0.81.0 → plain-0.83.0}/plain/server/http/message.py +0 -0
  125. {plain-0.81.0 → plain-0.83.0}/plain/server/http/parser.py +0 -0
  126. {plain-0.81.0 → plain-0.83.0}/plain/server/http/unreader.py +0 -0
  127. {plain-0.81.0 → plain-0.83.0}/plain/server/http/wsgi.py +0 -0
  128. {plain-0.81.0 → plain-0.83.0}/plain/server/pidfile.py +0 -0
  129. {plain-0.81.0 → plain-0.83.0}/plain/server/sock.py +0 -0
  130. {plain-0.81.0 → plain-0.83.0}/plain/server/util.py +0 -0
  131. {plain-0.81.0 → plain-0.83.0}/plain/server/workers/__init__.py +0 -0
  132. {plain-0.81.0 → plain-0.83.0}/plain/server/workers/base.py +0 -0
  133. {plain-0.81.0 → plain-0.83.0}/plain/server/workers/sync.py +0 -0
  134. {plain-0.81.0 → plain-0.83.0}/plain/server/workers/thread.py +0 -0
  135. {plain-0.81.0 → plain-0.83.0}/plain/server/workers/workertmp.py +0 -0
  136. {plain-0.81.0 → plain-0.83.0}/plain/signals/README.md +0 -0
  137. {plain-0.81.0 → plain-0.83.0}/plain/signals/__init__.py +0 -0
  138. {plain-0.81.0 → plain-0.83.0}/plain/signals/dispatch/__init__.py +0 -0
  139. {plain-0.81.0 → plain-0.83.0}/plain/signals/dispatch/dispatcher.py +0 -0
  140. {plain-0.81.0 → plain-0.83.0}/plain/signals/dispatch/license.txt +0 -0
  141. {plain-0.81.0 → plain-0.83.0}/plain/signing.py +0 -0
  142. {plain-0.81.0 → plain-0.83.0}/plain/templates/AGENTS.md +0 -0
  143. {plain-0.81.0 → plain-0.83.0}/plain/templates/README.md +0 -0
  144. {plain-0.81.0 → plain-0.83.0}/plain/templates/__init__.py +0 -0
  145. {plain-0.81.0 → plain-0.83.0}/plain/templates/core.py +0 -0
  146. {plain-0.81.0 → plain-0.83.0}/plain/templates/jinja/__init__.py +0 -0
  147. {plain-0.81.0 → plain-0.83.0}/plain/templates/jinja/environments.py +0 -0
  148. {plain-0.81.0 → plain-0.83.0}/plain/templates/jinja/extensions.py +0 -0
  149. {plain-0.81.0 → plain-0.83.0}/plain/templates/jinja/filters.py +0 -0
  150. {plain-0.81.0 → plain-0.83.0}/plain/templates/jinja/globals.py +0 -0
  151. {plain-0.81.0 → plain-0.83.0}/plain/test/README.md +0 -0
  152. {plain-0.81.0 → plain-0.83.0}/plain/test/__init__.py +0 -0
  153. {plain-0.81.0 → plain-0.83.0}/plain/test/client.py +0 -0
  154. {plain-0.81.0 → plain-0.83.0}/plain/test/encoding.py +0 -0
  155. {plain-0.81.0 → plain-0.83.0}/plain/test/exceptions.py +0 -0
  156. {plain-0.81.0 → plain-0.83.0}/plain/urls/README.md +0 -0
  157. {plain-0.81.0 → plain-0.83.0}/plain/urls/__init__.py +0 -0
  158. {plain-0.81.0 → plain-0.83.0}/plain/urls/converters.py +0 -0
  159. {plain-0.81.0 → plain-0.83.0}/plain/urls/exceptions.py +0 -0
  160. {plain-0.81.0 → plain-0.83.0}/plain/urls/patterns.py +0 -0
  161. {plain-0.81.0 → plain-0.83.0}/plain/urls/resolvers.py +0 -0
  162. {plain-0.81.0 → plain-0.83.0}/plain/urls/routers.py +0 -0
  163. {plain-0.81.0 → plain-0.83.0}/plain/urls/utils.py +0 -0
  164. {plain-0.81.0 → plain-0.83.0}/plain/utils/README.md +0 -0
  165. {plain-0.81.0 → plain-0.83.0}/plain/utils/__init__.py +0 -0
  166. {plain-0.81.0 → plain-0.83.0}/plain/utils/cache.py +0 -0
  167. {plain-0.81.0 → plain-0.83.0}/plain/utils/crypto.py +0 -0
  168. {plain-0.81.0 → plain-0.83.0}/plain/utils/datastructures.py +0 -0
  169. {plain-0.81.0 → plain-0.83.0}/plain/utils/dateparse.py +0 -0
  170. {plain-0.81.0 → plain-0.83.0}/plain/utils/deconstruct.py +0 -0
  171. {plain-0.81.0 → plain-0.83.0}/plain/utils/decorators.py +0 -0
  172. {plain-0.81.0 → plain-0.83.0}/plain/utils/duration.py +0 -0
  173. {plain-0.81.0 → plain-0.83.0}/plain/utils/encoding.py +0 -0
  174. {plain-0.81.0 → plain-0.83.0}/plain/utils/functional.py +0 -0
  175. {plain-0.81.0 → plain-0.83.0}/plain/utils/hashable.py +0 -0
  176. {plain-0.81.0 → plain-0.83.0}/plain/utils/http.py +0 -0
  177. {plain-0.81.0 → plain-0.83.0}/plain/utils/inspect.py +0 -0
  178. {plain-0.81.0 → plain-0.83.0}/plain/utils/ipv6.py +0 -0
  179. {plain-0.81.0 → plain-0.83.0}/plain/utils/itercompat.py +0 -0
  180. {plain-0.81.0 → plain-0.83.0}/plain/utils/module_loading.py +0 -0
  181. {plain-0.81.0 → plain-0.83.0}/plain/utils/regex_helper.py +0 -0
  182. {plain-0.81.0 → plain-0.83.0}/plain/utils/safestring.py +0 -0
  183. {plain-0.81.0 → plain-0.83.0}/plain/utils/text.py +0 -0
  184. {plain-0.81.0 → plain-0.83.0}/plain/utils/timesince.py +0 -0
  185. {plain-0.81.0 → plain-0.83.0}/plain/utils/timezone.py +0 -0
  186. {plain-0.81.0 → plain-0.83.0}/plain/utils/tree.py +0 -0
  187. {plain-0.81.0 → plain-0.83.0}/plain/validators.py +0 -0
  188. {plain-0.81.0 → plain-0.83.0}/plain/views/README.md +0 -0
  189. {plain-0.81.0 → plain-0.83.0}/plain/views/__init__.py +0 -0
  190. {plain-0.81.0 → plain-0.83.0}/plain/views/base.py +0 -0
  191. {plain-0.81.0 → plain-0.83.0}/plain/views/errors.py +0 -0
  192. {plain-0.81.0 → plain-0.83.0}/plain/views/exceptions.py +0 -0
  193. {plain-0.81.0 → plain-0.83.0}/plain/views/forms.py +0 -0
  194. {plain-0.81.0 → plain-0.83.0}/plain/views/objects.py +0 -0
  195. {plain-0.81.0 → plain-0.83.0}/plain/views/redirect.py +0 -0
  196. {plain-0.81.0 → plain-0.83.0}/plain/views/templates.py +0 -0
  197. {plain-0.81.0 → plain-0.83.0}/plain/wsgi.py +0 -0
  198. {plain-0.81.0 → plain-0.83.0}/tests/.gitignore +0 -0
  199. {plain-0.81.0 → plain-0.83.0}/tests/app/.gitignore +0 -0
  200. {plain-0.81.0 → plain-0.83.0}/tests/app/settings.py +0 -0
  201. {plain-0.81.0 → plain-0.83.0}/tests/app/test/__init__.py +0 -0
  202. {plain-0.81.0 → plain-0.83.0}/tests/app/test/default_settings.py +0 -0
  203. {plain-0.81.0 → plain-0.83.0}/tests/app/urls.py +0 -0
  204. {plain-0.81.0 → plain-0.83.0}/tests/conftest.py +0 -0
  205. {plain-0.81.0 → plain-0.83.0}/tests/test_cli.py +0 -0
  206. {plain-0.81.0 → plain-0.83.0}/tests/test_csrf.py +0 -0
  207. {plain-0.81.0 → plain-0.83.0}/tests/test_http_hosts.py +0 -0
  208. {plain-0.81.0 → plain-0.83.0}/tests/test_logs.py +0 -0
  209. {plain-0.81.0 → plain-0.83.0}/tests/test_runtime.py +0 -0
  210. {plain-0.81.0 → plain-0.83.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.81.0
3
+ Version: 0.83.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,29 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
4
+
5
+ ### What's changed
6
+
7
+ - Added comprehensive Content Security Policy (CSP) documentation explaining how to use nonces with inline scripts and styles ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
8
+ - The `json_script` utility function now accepts an optional `nonce` parameter for CSP-compliant inline JSON scripts ([784f3dd972](https://github.com/dropseed/plain/commit/784f3dd972))
9
+
10
+ ### Upgrade instructions
11
+
12
+ - Any `|json_script` usages need to make sure the second argument is a nonce, not a custom encoder (which is now third)
13
+
14
+ ## [0.82.0](https://github.com/dropseed/plain/releases/plain@0.82.0) (2025-10-29)
15
+
16
+ ### What's changed
17
+
18
+ - 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))
19
+ - Added `request.csp_nonce` cached property for generating Content Security Policy nonces ([75071dcc70](https://github.com/dropseed/plain/commit/75071dcc70))
20
+ - Simplified the preflight command by moving `plain preflight check` back to `plain preflight` ([40c2c4560e](https://github.com/dropseed/plain/commit/40c2c4560e))
21
+
22
+ ### Upgrade instructions
23
+
24
+ - If you use `plain preflight check`, update to `plain preflight` (the `check` subcommand has been removed for simplicity)
25
+ - If you use `plain preflight check --deploy`, update to `plain preflight --deploy`
26
+
3
27
  ## [0.81.0](https://github.com/dropseed/plain/releases/plain@0.81.0) (2025-10-22)
4
28
 
5
29
  ### 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.")
@@ -0,0 +1,89 @@
1
+ # HTTP
2
+
3
+ **HTTP request and response handling.**
4
+
5
+ - [Overview](#overview)
6
+ - [Content Security Policy (CSP)](#content-security-policy-csp)
7
+
8
+ ## Overview
9
+
10
+ Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
11
+
12
+ ```python
13
+ from plain.views import View
14
+ from plain.http import Response
15
+
16
+ class ExampleView(View):
17
+ def get(self):
18
+ # Accessing a request header
19
+ print(self.request.headers.get("Example-Header"))
20
+
21
+ # Accessing a query parameter
22
+ print(self.request.query_params.get("example"))
23
+
24
+ # Creating a response
25
+ response = Response("Hello, world!", status_code=200)
26
+
27
+ # Setting a response header
28
+ response.headers["Example-Header"] = "Example Value"
29
+
30
+ return response
31
+ ```
32
+
33
+ ## Content Security Policy (CSP)
34
+
35
+ Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
36
+
37
+ Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
38
+
39
+ ### Configuring CSP Headers
40
+
41
+ Set `DEFAULT_RESPONSE_HEADERS` as a callable function to generate dynamic CSP headers with nonces:
42
+
43
+ ```python
44
+ # app/settings.py
45
+ def DEFAULT_RESPONSE_HEADERS(request):
46
+ """
47
+ Dynamic response headers with CSP nonces.
48
+ """
49
+ nonce = request.csp_nonce
50
+ return {
51
+ "Content-Security-Policy": (
52
+ f"default-src 'self'; "
53
+ f"script-src 'self' 'nonce-{nonce}'; "
54
+ f"style-src 'self' 'nonce-{nonce}'; "
55
+ f"img-src 'self' data:; "
56
+ f"font-src 'self'; "
57
+ f"connect-src 'self'; "
58
+ f"frame-ancestors 'self'; "
59
+ f"base-uri 'self'; "
60
+ f"form-action 'self'"
61
+ ),
62
+ }
63
+ ```
64
+
65
+ Use tools like [Google's CSP Evaluator](https://csp-evaluator.withgoogle.com/) to analyze your CSP policy and identify potential security issues or misconfigurations.
66
+
67
+ ### Using Nonces in Templates
68
+
69
+ Add the nonce attribute to inline scripts and styles in your templates:
70
+
71
+ ```html
72
+ <!-- Inline script with nonce -->
73
+ <script nonce="{{ request.csp_nonce }}">
74
+ console.log("This script is allowed by CSP");
75
+ </script>
76
+
77
+ <!-- Inline style with nonce -->
78
+ <style nonce="{{ request.csp_nonce }}">
79
+ .example { color: red; }
80
+ </style>
81
+ ```
82
+
83
+ External scripts and stylesheets loaded from `'self'` don't need nonces:
84
+
85
+ ```html
86
+ <!-- External scripts/styles work with 'self' directive -->
87
+ <script src="/assets/app.js"></script>
88
+ <link rel="stylesheet" href="/assets/app.css">
89
+ ```
@@ -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
+ ```
@@ -34,25 +34,30 @@ _json_script_escapes = {
34
34
  def json_script(
35
35
  value: Any,
36
36
  element_id: str | None = None,
37
+ nonce: str = "",
37
38
  encoder: type[json.JSONEncoder] | None = None,
38
39
  ) -> SafeString:
39
40
  """
40
41
  Escape all the HTML/XML special characters with their unicode escapes, so
41
42
  value is safe to be output anywhere except for inside a tag attribute. Wrap
42
43
  the escaped JSON in a script tag.
44
+
45
+ Args:
46
+ value: The data to encode as JSON
47
+ element_id: Optional ID attribute for the script tag
48
+ nonce: Optional CSP nonce for inline script tags
49
+ encoder: Optional custom JSON encoder class
43
50
  """
44
51
  from plain.json import PlainJSONEncoder
45
52
 
46
53
  json_str = json.dumps(value, cls=encoder or PlainJSONEncoder).translate(
47
54
  _json_script_escapes
48
55
  )
49
- if element_id:
50
- template = '<script id="{}" type="application/json">{}</script>'
51
- args = (element_id, mark_safe(json_str))
52
- else:
53
- template = '<script type="application/json">{}</script>'
54
- args = (mark_safe(json_str),)
55
- return format_html(template, *args)
56
+ id_attr = f' id="{element_id}"' if element_id else ""
57
+ nonce_attr = f' nonce="{nonce}"' if nonce else ""
58
+ return mark_safe(
59
+ f'<script{id_attr}{nonce_attr} type="application/json">{json_str}</script>'
60
+ )
56
61
 
57
62
 
58
63
  def conditional_escape(text: Any) -> SafeString | str:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.81.0"
3
+ version = "0.83.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,30 +0,0 @@
1
- # HTTP
2
-
3
- **HTTP request and response handling.**
4
-
5
- - [Overview](#overview)
6
-
7
- ## Overview
8
-
9
- Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
10
-
11
- ```python
12
- from plain.views import View
13
- from plain.http import Response
14
-
15
- class ExampleView(View):
16
- def get(self):
17
- # Accessing a request header
18
- print(self.request.headers.get("Example-Header"))
19
-
20
- # Accessing a query parameter
21
- print(self.request.query_params.get("example"))
22
-
23
- # Creating a response
24
- response = Response("Hello, world!", status_code=200)
25
-
26
- # Setting a response header
27
- response.headers["Example-Header"] = "Example Value"
28
-
29
- return response
30
- ```
@@ -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