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

Potentially problematic release.


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

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