plain 0.83.0__tar.gz → 0.84.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 (210) hide show
  1. {plain-0.83.0 → plain-0.84.0}/PKG-INFO +1 -1
  2. {plain-0.83.0 → plain-0.84.0}/plain/CHANGELOG.md +28 -0
  3. plain-0.84.0/plain/http/README.md +142 -0
  4. plain-0.84.0/plain/internal/middleware/headers.py +59 -0
  5. {plain-0.83.0 → plain-0.84.0}/plain/runtime/global_settings.py +6 -2
  6. {plain-0.83.0 → plain-0.84.0}/pyproject.toml +1 -1
  7. plain-0.83.0/plain/http/README.md +0 -89
  8. plain-0.83.0/plain/internal/middleware/headers.py +0 -37
  9. {plain-0.83.0 → plain-0.84.0}/.gitignore +0 -0
  10. {plain-0.83.0 → plain-0.84.0}/LICENSE +0 -0
  11. {plain-0.83.0 → plain-0.84.0}/README.md +0 -0
  12. {plain-0.83.0 → plain-0.84.0}/plain/AGENTS.md +0 -0
  13. {plain-0.83.0 → plain-0.84.0}/plain/README.md +0 -0
  14. {plain-0.83.0 → plain-0.84.0}/plain/__main__.py +0 -0
  15. {plain-0.83.0 → plain-0.84.0}/plain/assets/README.md +0 -0
  16. {plain-0.83.0 → plain-0.84.0}/plain/assets/__init__.py +0 -0
  17. {plain-0.83.0 → plain-0.84.0}/plain/assets/compile.py +0 -0
  18. {plain-0.83.0 → plain-0.84.0}/plain/assets/finders.py +0 -0
  19. {plain-0.83.0 → plain-0.84.0}/plain/assets/fingerprints.py +0 -0
  20. {plain-0.83.0 → plain-0.84.0}/plain/assets/urls.py +0 -0
  21. {plain-0.83.0 → plain-0.84.0}/plain/assets/views.py +0 -0
  22. {plain-0.83.0 → plain-0.84.0}/plain/chores/README.md +0 -0
  23. {plain-0.83.0 → plain-0.84.0}/plain/chores/__init__.py +0 -0
  24. {plain-0.83.0 → plain-0.84.0}/plain/chores/core.py +0 -0
  25. {plain-0.83.0 → plain-0.84.0}/plain/chores/registry.py +0 -0
  26. {plain-0.83.0 → plain-0.84.0}/plain/cli/README.md +0 -0
  27. {plain-0.83.0 → plain-0.84.0}/plain/cli/__init__.py +0 -0
  28. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/__init__.py +0 -0
  29. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/docs.py +0 -0
  30. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/llmdocs.py +0 -0
  31. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/md.py +0 -0
  32. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/prompt.py +0 -0
  33. {plain-0.83.0 → plain-0.84.0}/plain/cli/agent/request.py +0 -0
  34. {plain-0.83.0 → plain-0.84.0}/plain/cli/build.py +0 -0
  35. {plain-0.83.0 → plain-0.84.0}/plain/cli/changelog.py +0 -0
  36. {plain-0.83.0 → plain-0.84.0}/plain/cli/chores.py +0 -0
  37. {plain-0.83.0 → plain-0.84.0}/plain/cli/core.py +0 -0
  38. {plain-0.83.0 → plain-0.84.0}/plain/cli/docs.py +0 -0
  39. {plain-0.83.0 → plain-0.84.0}/plain/cli/formatting.py +0 -0
  40. {plain-0.83.0 → plain-0.84.0}/plain/cli/install.py +0 -0
  41. {plain-0.83.0 → plain-0.84.0}/plain/cli/output.py +0 -0
  42. {plain-0.83.0 → plain-0.84.0}/plain/cli/preflight.py +0 -0
  43. {plain-0.83.0 → plain-0.84.0}/plain/cli/print.py +0 -0
  44. {plain-0.83.0 → plain-0.84.0}/plain/cli/registry.py +0 -0
  45. {plain-0.83.0 → plain-0.84.0}/plain/cli/runtime.py +0 -0
  46. {plain-0.83.0 → plain-0.84.0}/plain/cli/scaffold.py +0 -0
  47. {plain-0.83.0 → plain-0.84.0}/plain/cli/server.py +0 -0
  48. {plain-0.83.0 → plain-0.84.0}/plain/cli/settings.py +0 -0
  49. {plain-0.83.0 → plain-0.84.0}/plain/cli/shell.py +0 -0
  50. {plain-0.83.0 → plain-0.84.0}/plain/cli/startup.py +0 -0
  51. {plain-0.83.0 → plain-0.84.0}/plain/cli/upgrade.py +0 -0
  52. {plain-0.83.0 → plain-0.84.0}/plain/cli/urls.py +0 -0
  53. {plain-0.83.0 → plain-0.84.0}/plain/cli/utils.py +0 -0
  54. {plain-0.83.0 → plain-0.84.0}/plain/csrf/README.md +0 -0
  55. {plain-0.83.0 → plain-0.84.0}/plain/csrf/middleware.py +0 -0
  56. {plain-0.83.0 → plain-0.84.0}/plain/debug.py +0 -0
  57. {plain-0.83.0 → plain-0.84.0}/plain/exceptions.py +0 -0
  58. {plain-0.83.0 → plain-0.84.0}/plain/forms/README.md +0 -0
  59. {plain-0.83.0 → plain-0.84.0}/plain/forms/__init__.py +0 -0
  60. {plain-0.83.0 → plain-0.84.0}/plain/forms/boundfield.py +0 -0
  61. {plain-0.83.0 → plain-0.84.0}/plain/forms/exceptions.py +0 -0
  62. {plain-0.83.0 → plain-0.84.0}/plain/forms/fields.py +0 -0
  63. {plain-0.83.0 → plain-0.84.0}/plain/forms/forms.py +0 -0
  64. {plain-0.83.0 → plain-0.84.0}/plain/http/__init__.py +0 -0
  65. {plain-0.83.0 → plain-0.84.0}/plain/http/cookie.py +0 -0
  66. {plain-0.83.0 → plain-0.84.0}/plain/http/middleware.py +0 -0
  67. {plain-0.83.0 → plain-0.84.0}/plain/http/multipartparser.py +0 -0
  68. {plain-0.83.0 → plain-0.84.0}/plain/http/request.py +0 -0
  69. {plain-0.83.0 → plain-0.84.0}/plain/http/response.py +0 -0
  70. {plain-0.83.0 → plain-0.84.0}/plain/internal/__init__.py +0 -0
  71. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/__init__.py +0 -0
  72. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/base.py +0 -0
  73. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/locks.py +0 -0
  74. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/move.py +0 -0
  75. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/temp.py +0 -0
  76. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/uploadedfile.py +0 -0
  77. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/uploadhandler.py +0 -0
  78. {plain-0.83.0 → plain-0.84.0}/plain/internal/files/utils.py +0 -0
  79. {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/__init__.py +0 -0
  80. {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/base.py +0 -0
  81. {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/exception.py +0 -0
  82. {plain-0.83.0 → plain-0.84.0}/plain/internal/handlers/wsgi.py +0 -0
  83. {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/__init__.py +0 -0
  84. {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/hosts.py +0 -0
  85. {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/https.py +0 -0
  86. {plain-0.83.0 → plain-0.84.0}/plain/internal/middleware/slash.py +0 -0
  87. {plain-0.83.0 → plain-0.84.0}/plain/internal/reloader.py +0 -0
  88. {plain-0.83.0 → plain-0.84.0}/plain/json.py +0 -0
  89. {plain-0.83.0 → plain-0.84.0}/plain/logs/README.md +0 -0
  90. {plain-0.83.0 → plain-0.84.0}/plain/logs/__init__.py +0 -0
  91. {plain-0.83.0 → plain-0.84.0}/plain/logs/configure.py +0 -0
  92. {plain-0.83.0 → plain-0.84.0}/plain/logs/debug.py +0 -0
  93. {plain-0.83.0 → plain-0.84.0}/plain/logs/formatters.py +0 -0
  94. {plain-0.83.0 → plain-0.84.0}/plain/logs/loggers.py +0 -0
  95. {plain-0.83.0 → plain-0.84.0}/plain/packages/README.md +0 -0
  96. {plain-0.83.0 → plain-0.84.0}/plain/packages/__init__.py +0 -0
  97. {plain-0.83.0 → plain-0.84.0}/plain/packages/config.py +0 -0
  98. {plain-0.83.0 → plain-0.84.0}/plain/packages/registry.py +0 -0
  99. {plain-0.83.0 → plain-0.84.0}/plain/paginator.py +0 -0
  100. {plain-0.83.0 → plain-0.84.0}/plain/preflight/README.md +0 -0
  101. {plain-0.83.0 → plain-0.84.0}/plain/preflight/__init__.py +0 -0
  102. {plain-0.83.0 → plain-0.84.0}/plain/preflight/checks.py +0 -0
  103. {plain-0.83.0 → plain-0.84.0}/plain/preflight/files.py +0 -0
  104. {plain-0.83.0 → plain-0.84.0}/plain/preflight/registry.py +0 -0
  105. {plain-0.83.0 → plain-0.84.0}/plain/preflight/results.py +0 -0
  106. {plain-0.83.0 → plain-0.84.0}/plain/preflight/security.py +0 -0
  107. {plain-0.83.0 → plain-0.84.0}/plain/preflight/urls.py +0 -0
  108. {plain-0.83.0 → plain-0.84.0}/plain/runtime/README.md +0 -0
  109. {plain-0.83.0 → plain-0.84.0}/plain/runtime/__init__.py +0 -0
  110. {plain-0.83.0 → plain-0.84.0}/plain/runtime/user_settings.py +0 -0
  111. {plain-0.83.0 → plain-0.84.0}/plain/runtime/utils.py +0 -0
  112. {plain-0.83.0 → plain-0.84.0}/plain/server/LICENSE +0 -0
  113. {plain-0.83.0 → plain-0.84.0}/plain/server/README.md +0 -0
  114. {plain-0.83.0 → plain-0.84.0}/plain/server/__init__.py +0 -0
  115. {plain-0.83.0 → plain-0.84.0}/plain/server/app.py +0 -0
  116. {plain-0.83.0 → plain-0.84.0}/plain/server/arbiter.py +0 -0
  117. {plain-0.83.0 → plain-0.84.0}/plain/server/config.py +0 -0
  118. {plain-0.83.0 → plain-0.84.0}/plain/server/errors.py +0 -0
  119. {plain-0.83.0 → plain-0.84.0}/plain/server/glogging.py +0 -0
  120. {plain-0.83.0 → plain-0.84.0}/plain/server/http/__init__.py +0 -0
  121. {plain-0.83.0 → plain-0.84.0}/plain/server/http/body.py +0 -0
  122. {plain-0.83.0 → plain-0.84.0}/plain/server/http/errors.py +0 -0
  123. {plain-0.83.0 → plain-0.84.0}/plain/server/http/message.py +0 -0
  124. {plain-0.83.0 → plain-0.84.0}/plain/server/http/parser.py +0 -0
  125. {plain-0.83.0 → plain-0.84.0}/plain/server/http/unreader.py +0 -0
  126. {plain-0.83.0 → plain-0.84.0}/plain/server/http/wsgi.py +0 -0
  127. {plain-0.83.0 → plain-0.84.0}/plain/server/pidfile.py +0 -0
  128. {plain-0.83.0 → plain-0.84.0}/plain/server/sock.py +0 -0
  129. {plain-0.83.0 → plain-0.84.0}/plain/server/util.py +0 -0
  130. {plain-0.83.0 → plain-0.84.0}/plain/server/workers/__init__.py +0 -0
  131. {plain-0.83.0 → plain-0.84.0}/plain/server/workers/base.py +0 -0
  132. {plain-0.83.0 → plain-0.84.0}/plain/server/workers/sync.py +0 -0
  133. {plain-0.83.0 → plain-0.84.0}/plain/server/workers/thread.py +0 -0
  134. {plain-0.83.0 → plain-0.84.0}/plain/server/workers/workertmp.py +0 -0
  135. {plain-0.83.0 → plain-0.84.0}/plain/signals/README.md +0 -0
  136. {plain-0.83.0 → plain-0.84.0}/plain/signals/__init__.py +0 -0
  137. {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/__init__.py +0 -0
  138. {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/dispatcher.py +0 -0
  139. {plain-0.83.0 → plain-0.84.0}/plain/signals/dispatch/license.txt +0 -0
  140. {plain-0.83.0 → plain-0.84.0}/plain/signing.py +0 -0
  141. {plain-0.83.0 → plain-0.84.0}/plain/templates/AGENTS.md +0 -0
  142. {plain-0.83.0 → plain-0.84.0}/plain/templates/README.md +0 -0
  143. {plain-0.83.0 → plain-0.84.0}/plain/templates/__init__.py +0 -0
  144. {plain-0.83.0 → plain-0.84.0}/plain/templates/core.py +0 -0
  145. {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/__init__.py +0 -0
  146. {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/environments.py +0 -0
  147. {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/extensions.py +0 -0
  148. {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/filters.py +0 -0
  149. {plain-0.83.0 → plain-0.84.0}/plain/templates/jinja/globals.py +0 -0
  150. {plain-0.83.0 → plain-0.84.0}/plain/test/README.md +0 -0
  151. {plain-0.83.0 → plain-0.84.0}/plain/test/__init__.py +0 -0
  152. {plain-0.83.0 → plain-0.84.0}/plain/test/client.py +0 -0
  153. {plain-0.83.0 → plain-0.84.0}/plain/test/encoding.py +0 -0
  154. {plain-0.83.0 → plain-0.84.0}/plain/test/exceptions.py +0 -0
  155. {plain-0.83.0 → plain-0.84.0}/plain/urls/README.md +0 -0
  156. {plain-0.83.0 → plain-0.84.0}/plain/urls/__init__.py +0 -0
  157. {plain-0.83.0 → plain-0.84.0}/plain/urls/converters.py +0 -0
  158. {plain-0.83.0 → plain-0.84.0}/plain/urls/exceptions.py +0 -0
  159. {plain-0.83.0 → plain-0.84.0}/plain/urls/patterns.py +0 -0
  160. {plain-0.83.0 → plain-0.84.0}/plain/urls/resolvers.py +0 -0
  161. {plain-0.83.0 → plain-0.84.0}/plain/urls/routers.py +0 -0
  162. {plain-0.83.0 → plain-0.84.0}/plain/urls/utils.py +0 -0
  163. {plain-0.83.0 → plain-0.84.0}/plain/utils/README.md +0 -0
  164. {plain-0.83.0 → plain-0.84.0}/plain/utils/__init__.py +0 -0
  165. {plain-0.83.0 → plain-0.84.0}/plain/utils/cache.py +0 -0
  166. {plain-0.83.0 → plain-0.84.0}/plain/utils/crypto.py +0 -0
  167. {plain-0.83.0 → plain-0.84.0}/plain/utils/datastructures.py +0 -0
  168. {plain-0.83.0 → plain-0.84.0}/plain/utils/dateparse.py +0 -0
  169. {plain-0.83.0 → plain-0.84.0}/plain/utils/deconstruct.py +0 -0
  170. {plain-0.83.0 → plain-0.84.0}/plain/utils/decorators.py +0 -0
  171. {plain-0.83.0 → plain-0.84.0}/plain/utils/duration.py +0 -0
  172. {plain-0.83.0 → plain-0.84.0}/plain/utils/encoding.py +0 -0
  173. {plain-0.83.0 → plain-0.84.0}/plain/utils/functional.py +0 -0
  174. {plain-0.83.0 → plain-0.84.0}/plain/utils/hashable.py +0 -0
  175. {plain-0.83.0 → plain-0.84.0}/plain/utils/html.py +0 -0
  176. {plain-0.83.0 → plain-0.84.0}/plain/utils/http.py +0 -0
  177. {plain-0.83.0 → plain-0.84.0}/plain/utils/inspect.py +0 -0
  178. {plain-0.83.0 → plain-0.84.0}/plain/utils/ipv6.py +0 -0
  179. {plain-0.83.0 → plain-0.84.0}/plain/utils/itercompat.py +0 -0
  180. {plain-0.83.0 → plain-0.84.0}/plain/utils/module_loading.py +0 -0
  181. {plain-0.83.0 → plain-0.84.0}/plain/utils/regex_helper.py +0 -0
  182. {plain-0.83.0 → plain-0.84.0}/plain/utils/safestring.py +0 -0
  183. {plain-0.83.0 → plain-0.84.0}/plain/utils/text.py +0 -0
  184. {plain-0.83.0 → plain-0.84.0}/plain/utils/timesince.py +0 -0
  185. {plain-0.83.0 → plain-0.84.0}/plain/utils/timezone.py +0 -0
  186. {plain-0.83.0 → plain-0.84.0}/plain/utils/tree.py +0 -0
  187. {plain-0.83.0 → plain-0.84.0}/plain/validators.py +0 -0
  188. {plain-0.83.0 → plain-0.84.0}/plain/views/README.md +0 -0
  189. {plain-0.83.0 → plain-0.84.0}/plain/views/__init__.py +0 -0
  190. {plain-0.83.0 → plain-0.84.0}/plain/views/base.py +0 -0
  191. {plain-0.83.0 → plain-0.84.0}/plain/views/errors.py +0 -0
  192. {plain-0.83.0 → plain-0.84.0}/plain/views/exceptions.py +0 -0
  193. {plain-0.83.0 → plain-0.84.0}/plain/views/forms.py +0 -0
  194. {plain-0.83.0 → plain-0.84.0}/plain/views/objects.py +0 -0
  195. {plain-0.83.0 → plain-0.84.0}/plain/views/redirect.py +0 -0
  196. {plain-0.83.0 → plain-0.84.0}/plain/views/templates.py +0 -0
  197. {plain-0.83.0 → plain-0.84.0}/plain/wsgi.py +0 -0
  198. {plain-0.83.0 → plain-0.84.0}/tests/.gitignore +0 -0
  199. {plain-0.83.0 → plain-0.84.0}/tests/app/.gitignore +0 -0
  200. {plain-0.83.0 → plain-0.84.0}/tests/app/settings.py +0 -0
  201. {plain-0.83.0 → plain-0.84.0}/tests/app/test/__init__.py +0 -0
  202. {plain-0.83.0 → plain-0.84.0}/tests/app/test/default_settings.py +0 -0
  203. {plain-0.83.0 → plain-0.84.0}/tests/app/urls.py +0 -0
  204. {plain-0.83.0 → plain-0.84.0}/tests/conftest.py +0 -0
  205. {plain-0.83.0 → plain-0.84.0}/tests/test_cli.py +0 -0
  206. {plain-0.83.0 → plain-0.84.0}/tests/test_csrf.py +0 -0
  207. {plain-0.83.0 → plain-0.84.0}/tests/test_http_hosts.py +0 -0
  208. {plain-0.83.0 → plain-0.84.0}/tests/test_logs.py +0 -0
  209. {plain-0.83.0 → plain-0.84.0}/tests/test_runtime.py +0 -0
  210. {plain-0.83.0 → plain-0.84.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.83.0
3
+ Version: 0.84.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,33 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.84.0](https://github.com/dropseed/plain/releases/plain@0.84.0) (2025-10-29)
4
+
5
+ ### What's changed
6
+
7
+ - The `DEFAULT_RESPONSE_HEADERS` setting now supports format string placeholders (e.g., `{request.csp_nonce}`) for dynamic header values instead of requiring a callable function ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
8
+ - Views can now set headers to `None` to explicitly remove default response headers ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
9
+ - Added comprehensive documentation for customizing default response headers including override, remove, and extend patterns ([5199383128](https://github.com/dropseed/plain/commit/5199383128))
10
+
11
+ ### Upgrade instructions
12
+
13
+ - If you have `DEFAULT_RESPONSE_HEADERS` configured as a callable function, convert it to a dictionary with format string placeholders:
14
+
15
+ ```python
16
+ # Before:
17
+ def DEFAULT_RESPONSE_HEADERS(request):
18
+ nonce = request.csp_nonce
19
+ return {
20
+ "Content-Security-Policy": f"script-src 'self' 'nonce-{nonce}'",
21
+ }
22
+
23
+ # After:
24
+ DEFAULT_RESPONSE_HEADERS = {
25
+ "Content-Security-Policy": "script-src 'self' 'nonce-{request.csp_nonce}'",
26
+ }
27
+ ```
28
+
29
+ - If you were overriding default headers to empty strings (`""`) to remove them, change those to `None` instead
30
+
3
31
  ## [0.83.0](https://github.com/dropseed/plain/releases/plain@0.83.0) (2025-10-29)
4
32
 
5
33
  ### What's changed
@@ -0,0 +1,142 @@
1
+ # HTTP
2
+
3
+ **HTTP request and response handling.**
4
+
5
+ - [Overview](#overview)
6
+ - [Content Security Policy (CSP)](#content-security-policy-csp)
7
+ - [Customizing Default Response Headers](#customizing-default-response-headers)
8
+
9
+ ## Overview
10
+
11
+ Typically you will interact with [Request](request.py#Request) and [Response](response.py#ResponseBase) objects in your views and middleware.
12
+
13
+ ```python
14
+ from plain.views import View
15
+ from plain.http import Response
16
+
17
+ class ExampleView(View):
18
+ def get(self):
19
+ # Accessing a request header
20
+ print(self.request.headers.get("Example-Header"))
21
+
22
+ # Accessing a query parameter
23
+ print(self.request.query_params.get("example"))
24
+
25
+ # Creating a response
26
+ response = Response("Hello, world!", status_code=200)
27
+
28
+ # Setting a response header
29
+ response.headers["Example-Header"] = "Example Value"
30
+
31
+ return response
32
+ ```
33
+
34
+ ## Content Security Policy (CSP)
35
+
36
+ Plain includes built-in support for Content Security Policy (CSP) through nonces, allowing you to use strict CSP policies without `'unsafe-inline'`.
37
+
38
+ Each request generates a unique cryptographically secure nonce available via [`request.csp_nonce`](request.py#Request.csp_nonce):
39
+
40
+ ### Configuring CSP Headers
41
+
42
+ Include CSP in `DEFAULT_RESPONSE_HEADERS` using `{request.csp_nonce}` placeholders for dynamic nonces:
43
+
44
+ ```python
45
+ # app/settings.py
46
+ DEFAULT_RESPONSE_HEADERS = {
47
+ "Content-Security-Policy": (
48
+ "default-src 'self'; "
49
+ "script-src 'self' 'nonce-{request.csp_nonce}'; "
50
+ "style-src 'self' 'nonce-{request.csp_nonce}'; "
51
+ "img-src 'self' data:; "
52
+ "font-src 'self'; "
53
+ "connect-src 'self'; "
54
+ "frame-ancestors 'self'; "
55
+ "base-uri 'self'; "
56
+ "form-action 'self'"
57
+ ),
58
+ # Other default headers...
59
+ "X-Frame-Options": "DENY",
60
+ }
61
+ ```
62
+
63
+ The `{request.csp_nonce}` placeholder will be replaced with a unique nonce for each request.
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
+ ```
90
+
91
+ ## Customizing Default Response Headers
92
+
93
+ Plain applies default response headers to all responses via `DEFAULT_RESPONSE_HEADERS` in settings. Views can customize these headers in several ways:
94
+
95
+ ### Override Default Headers
96
+
97
+ Set the header to a different value in your view:
98
+
99
+ ```python
100
+ class MyView(View):
101
+ def get(self):
102
+ response = Response("content")
103
+ # Override the default X-Frame-Options: DENY
104
+ response.headers["X-Frame-Options"] = "SAMEORIGIN"
105
+ return response
106
+ ```
107
+
108
+ ### Remove Default Headers
109
+
110
+ Set the header to `None` to prevent it from being applied:
111
+
112
+ ```python
113
+ class EmbeddableView(View):
114
+ def get(self):
115
+ response = Response("content")
116
+ # Remove X-Frame-Options entirely to allow embedding
117
+ response.headers["X-Frame-Options"] = None
118
+ return response
119
+ ```
120
+
121
+ ### Extend Default Headers
122
+
123
+ Read the default value from settings, modify it, then set it in your view:
124
+
125
+ ```python
126
+ from plain.runtime import settings
127
+
128
+ class MyView(View):
129
+ def get(self):
130
+ response = Response("content")
131
+
132
+ # Get the default CSP policy
133
+ if csp := settings.DEFAULT_RESPONSE_HEADERS.get("Content-Security-Policy"):
134
+ # Format it with the current request to resolve placeholders
135
+ csp = csp.format(request=self.request)
136
+ # Extend with additional sources
137
+ response.headers["Content-Security-Policy"] = (
138
+ f"{csp}; script-src https://analytics.example.com"
139
+ )
140
+
141
+ return response
142
+ ```
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from plain.http import HttpMiddleware
6
+ from plain.runtime import settings
7
+
8
+ if TYPE_CHECKING:
9
+ from plain.http import Request, Response
10
+
11
+
12
+ class DefaultHeadersMiddleware(HttpMiddleware):
13
+ """
14
+ Applies default response headers from settings.DEFAULT_RESPONSE_HEADERS.
15
+
16
+ This middleware runs after the view executes and applies default headers
17
+ to the response using setdefault(), which means:
18
+ - Headers already set by the view won't be overridden
19
+ - Headers not set by the view will use the default value
20
+
21
+ View Customization Patterns:
22
+ - Use default: Don't set the header (middleware applies it)
23
+ - Override: Set the header to a different value
24
+ - Remove: Set the header to None (middleware will delete it)
25
+ - Extend: Read from settings.DEFAULT_RESPONSE_HEADERS, modify, then set
26
+
27
+ Format Strings:
28
+ Header values can include {request.attribute} placeholders for dynamic
29
+ content. Example: 'nonce-{request.csp_nonce}' will be formatted with
30
+ the request's csp_nonce value. Headers without placeholders are used as-is.
31
+
32
+ None Removal:
33
+ Views can set a header to None to opt-out of that default header entirely.
34
+ The middleware will delete any header set to None, preventing the default
35
+ from being applied.
36
+ """
37
+
38
+ def process_request(self, request: Request) -> Response:
39
+ # Get the response from the view (and any inner middleware)
40
+ response = self.get_response(request)
41
+
42
+ # Apply default headers to the response
43
+ for header, value in settings.DEFAULT_RESPONSE_HEADERS.items():
44
+ if header not in response.headers:
45
+ # Header not set - apply default
46
+ if "{" in value:
47
+ response.headers[header] = value.format(request=request)
48
+ else:
49
+ response.headers[header] = value
50
+ elif response.headers[header] is None:
51
+ # Header explicitly set to None by view - remove it
52
+ del response.headers[header]
53
+
54
+ # Add the Content-Length header to non-streaming responses if not
55
+ # already set.
56
+ if not response.streaming and "Content-Length" not in response.headers:
57
+ response.headers["Content-Length"] = str(len(response.content))
58
+
59
+ return response
@@ -27,8 +27,12 @@ URLS_ROUTER: str
27
27
  ALLOWED_HOSTS: list[str] = []
28
28
 
29
29
  # Default headers for all responses.
30
- DEFAULT_RESPONSE_HEADERS = {
31
- # "Content-Security-Policy": "default-src 'self'",
30
+ # Header values can include {request.attribute} placeholders for dynamic content.
31
+ # Example: "script-src 'nonce-{request.csp_nonce}'" will use the request's nonce.
32
+ # Views can override, remove, or extend these headers - see plain/http/README.md
33
+ # for customization patterns.
34
+ DEFAULT_RESPONSE_HEADERS: dict = {
35
+ # "Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-{request.csp_nonce}'",
32
36
  # https://hstspreload.org/
33
37
  # "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
34
38
  "Cross-Origin-Opener-Policy": "same-origin",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain"
3
- version = "0.83.0"
3
+ version = "0.84.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,89 +0,0 @@
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
- ```
@@ -1,37 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from plain.http import HttpMiddleware
6
- from plain.runtime import settings
7
-
8
- if TYPE_CHECKING:
9
- from plain.http import Request, Response
10
-
11
-
12
- class DefaultHeadersMiddleware(HttpMiddleware):
13
- def process_request(self, request: Request) -> Response:
14
- response = self.get_response(request)
15
-
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,
25
- # use allow users to set them to an empty string to indicate they should be removed.
26
- if header in response.headers and response.headers[header] == "":
27
- del response.headers[header]
28
- continue
29
-
30
- response.headers.setdefault(header, value)
31
-
32
- # Add the Content-Length header to non-streaming responses if not
33
- # already set.
34
- if not response.streaming and "Content-Length" not in response.headers:
35
- response.headers["Content-Length"] = str(len(response.content))
36
-
37
- return response
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes