plain 0.32.1__tar.gz → 0.33.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 (170) hide show
  1. plain-0.33.0/PKG-INFO +77 -0
  2. plain-0.33.0/plain/README.md +66 -0
  3. {plain-0.32.1 → plain-0.33.0}/plain/assets/README.md +20 -20
  4. {plain-0.32.1 → plain-0.33.0}/plain/assets/compile.py +8 -11
  5. {plain-0.32.1 → plain-0.33.0}/plain/assets/finders.py +9 -0
  6. {plain-0.32.1 → plain-0.33.0}/plain/assets/fingerprints.py +20 -0
  7. {plain-0.32.1 → plain-0.33.0}/plain/assets/urls.py +9 -0
  8. {plain-0.32.1 → plain-0.33.0}/plain/assets/views.py +7 -3
  9. plain-0.33.0/plain/cli/README.md +84 -0
  10. {plain-0.32.1 → plain-0.33.0}/plain/cli/core.py +203 -4
  11. plain-0.33.0/plain/csrf/README.md +18 -0
  12. plain-0.33.0/plain/forms/README.md +72 -0
  13. plain-0.33.0/plain/http/README.md +26 -0
  14. plain-0.33.0/plain/internal/__init__.py +7 -0
  15. plain-0.33.0/plain/logs/README.md +54 -0
  16. plain-0.33.0/plain/packages/README.md +75 -0
  17. plain-0.33.0/plain/preflight/README.md +53 -0
  18. {plain-0.32.1 → plain-0.33.0}/plain/preflight/__init__.py +2 -2
  19. {plain-0.32.1 → plain-0.33.0}/plain/preflight/files.py +2 -2
  20. {plain-0.32.1 → plain-0.33.0}/plain/preflight/messages.py +1 -1
  21. {plain-0.32.1 → plain-0.33.0}/plain/preflight/registry.py +3 -3
  22. {plain-0.32.1 → plain-0.33.0}/plain/preflight/security.py +9 -13
  23. {plain-0.32.1 → plain-0.33.0}/plain/preflight/urls.py +2 -2
  24. plain-0.33.0/plain/runtime/README.md +158 -0
  25. {plain-0.32.1 → plain-0.33.0}/plain/runtime/global_settings.py +2 -2
  26. {plain-0.32.1 → plain-0.33.0}/plain/signals/README.md +1 -1
  27. plain-0.33.0/plain/templates/README.md +80 -0
  28. plain-0.33.0/plain/test/README.md +38 -0
  29. plain-0.33.0/plain/test/__init__.py +6 -0
  30. {plain-0.32.1 → plain-0.33.0}/plain/test/client.py +104 -247
  31. plain-0.33.0/plain/test/encoding.py +98 -0
  32. plain-0.33.0/plain/test/exceptions.py +7 -0
  33. plain-0.33.0/plain/urls/README.md +150 -0
  34. {plain-0.32.1 → plain-0.33.0}/plain/urls/patterns.py +2 -0
  35. plain-0.33.0/plain/utils/README.md +5 -0
  36. {plain-0.32.1 → plain-0.33.0}/plain/views/README.md +2 -2
  37. {plain-0.32.1 → plain-0.33.0}/plain/views/base.py +7 -9
  38. {plain-0.32.1 → plain-0.33.0}/plain/views/forms.py +1 -1
  39. {plain-0.32.1 → plain-0.33.0}/pyproject.toml +1 -1
  40. plain-0.32.1/PKG-INFO +0 -12
  41. plain-0.32.1/plain/README.md +0 -1
  42. plain-0.32.1/plain/cli/README.md +0 -101
  43. plain-0.32.1/plain/csrf/README.md +0 -15
  44. plain-0.32.1/plain/forms/README.md +0 -14
  45. plain-0.32.1/plain/http/README.md +0 -3
  46. plain-0.32.1/plain/internal/files/README.md +0 -3
  47. plain-0.32.1/plain/logs/README.md +0 -24
  48. plain-0.32.1/plain/packages/README.md +0 -41
  49. plain-0.32.1/plain/preflight/README.md +0 -5
  50. plain-0.32.1/plain/runtime/README.md +0 -70
  51. plain-0.32.1/plain/templates/README.md +0 -20
  52. plain-0.32.1/plain/templates/jinja/README.md +0 -213
  53. plain-0.32.1/plain/test/README.md +0 -3
  54. plain-0.32.1/plain/test/__init__.py +0 -8
  55. plain-0.32.1/plain/urls/README.md +0 -3
  56. plain-0.32.1/plain/utils/README.md +0 -3
  57. plain-0.32.1/tests/app/test/__init__.py +0 -0
  58. {plain-0.32.1 → plain-0.33.0}/.gitignore +0 -0
  59. {plain-0.32.1 → plain-0.33.0}/LICENSE +0 -0
  60. {plain-0.32.1 → plain-0.33.0}/README.md +0 -0
  61. {plain-0.32.1 → plain-0.33.0}/plain/__main__.py +0 -0
  62. {plain-0.32.1 → plain-0.33.0}/plain/assets/__init__.py +0 -0
  63. {plain-0.32.1 → plain-0.33.0}/plain/cli/__init__.py +0 -0
  64. {plain-0.32.1 → plain-0.33.0}/plain/cli/formatting.py +0 -0
  65. {plain-0.32.1 → plain-0.33.0}/plain/cli/print.py +0 -0
  66. {plain-0.32.1 → plain-0.33.0}/plain/cli/registry.py +0 -0
  67. {plain-0.32.1 → plain-0.33.0}/plain/cli/startup.py +0 -0
  68. {plain-0.32.1 → plain-0.33.0}/plain/csrf/middleware.py +0 -0
  69. {plain-0.32.1 → plain-0.33.0}/plain/csrf/views.py +0 -0
  70. {plain-0.32.1 → plain-0.33.0}/plain/debug.py +0 -0
  71. {plain-0.32.1 → plain-0.33.0}/plain/exceptions.py +0 -0
  72. {plain-0.32.1 → plain-0.33.0}/plain/forms/__init__.py +0 -0
  73. {plain-0.32.1 → plain-0.33.0}/plain/forms/boundfield.py +0 -0
  74. {plain-0.32.1 → plain-0.33.0}/plain/forms/exceptions.py +0 -0
  75. {plain-0.32.1 → plain-0.33.0}/plain/forms/fields.py +0 -0
  76. {plain-0.32.1 → plain-0.33.0}/plain/forms/forms.py +0 -0
  77. {plain-0.32.1 → plain-0.33.0}/plain/http/__init__.py +0 -0
  78. {plain-0.32.1 → plain-0.33.0}/plain/http/cookie.py +0 -0
  79. {plain-0.32.1 → plain-0.33.0}/plain/http/multipartparser.py +0 -0
  80. {plain-0.32.1 → plain-0.33.0}/plain/http/request.py +0 -0
  81. {plain-0.32.1 → plain-0.33.0}/plain/http/response.py +0 -0
  82. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/__init__.py +0 -0
  83. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/base.py +0 -0
  84. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/locks.py +0 -0
  85. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/move.py +0 -0
  86. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/temp.py +0 -0
  87. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/uploadedfile.py +0 -0
  88. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/uploadhandler.py +0 -0
  89. {plain-0.32.1 → plain-0.33.0}/plain/internal/files/utils.py +0 -0
  90. {plain-0.32.1/plain/internal → plain-0.33.0/plain/internal/handlers}/__init__.py +0 -0
  91. {plain-0.32.1 → plain-0.33.0}/plain/internal/handlers/base.py +0 -0
  92. {plain-0.32.1 → plain-0.33.0}/plain/internal/handlers/exception.py +0 -0
  93. {plain-0.32.1 → plain-0.33.0}/plain/internal/handlers/wsgi.py +0 -0
  94. {plain-0.32.1/plain/internal/handlers → plain-0.33.0/plain/internal/middleware}/__init__.py +0 -0
  95. {plain-0.32.1 → plain-0.33.0}/plain/internal/middleware/headers.py +0 -0
  96. {plain-0.32.1 → plain-0.33.0}/plain/internal/middleware/https.py +0 -0
  97. {plain-0.32.1 → plain-0.33.0}/plain/internal/middleware/slash.py +0 -0
  98. {plain-0.32.1 → plain-0.33.0}/plain/json.py +0 -0
  99. {plain-0.32.1 → plain-0.33.0}/plain/logs/__init__.py +0 -0
  100. {plain-0.32.1 → plain-0.33.0}/plain/logs/configure.py +0 -0
  101. {plain-0.32.1 → plain-0.33.0}/plain/logs/loggers.py +0 -0
  102. {plain-0.32.1 → plain-0.33.0}/plain/logs/utils.py +0 -0
  103. {plain-0.32.1 → plain-0.33.0}/plain/packages/__init__.py +0 -0
  104. {plain-0.32.1 → plain-0.33.0}/plain/packages/config.py +0 -0
  105. {plain-0.32.1 → plain-0.33.0}/plain/packages/registry.py +0 -0
  106. {plain-0.32.1 → plain-0.33.0}/plain/paginator.py +0 -0
  107. {plain-0.32.1 → plain-0.33.0}/plain/runtime/__init__.py +0 -0
  108. {plain-0.32.1 → plain-0.33.0}/plain/runtime/user_settings.py +0 -0
  109. {plain-0.32.1 → plain-0.33.0}/plain/signals/__init__.py +0 -0
  110. {plain-0.32.1 → plain-0.33.0}/plain/signals/dispatch/__init__.py +0 -0
  111. {plain-0.32.1 → plain-0.33.0}/plain/signals/dispatch/dispatcher.py +0 -0
  112. {plain-0.32.1 → plain-0.33.0}/plain/signals/dispatch/license.txt +0 -0
  113. {plain-0.32.1 → plain-0.33.0}/plain/signing.py +0 -0
  114. {plain-0.32.1 → plain-0.33.0}/plain/templates/__init__.py +0 -0
  115. {plain-0.32.1 → plain-0.33.0}/plain/templates/core.py +0 -0
  116. {plain-0.32.1 → plain-0.33.0}/plain/templates/jinja/__init__.py +0 -0
  117. {plain-0.32.1 → plain-0.33.0}/plain/templates/jinja/environments.py +0 -0
  118. {plain-0.32.1 → plain-0.33.0}/plain/templates/jinja/extensions.py +0 -0
  119. {plain-0.32.1 → plain-0.33.0}/plain/templates/jinja/filters.py +0 -0
  120. {plain-0.32.1 → plain-0.33.0}/plain/templates/jinja/globals.py +0 -0
  121. {plain-0.32.1 → plain-0.33.0}/plain/urls/__init__.py +0 -0
  122. {plain-0.32.1 → plain-0.33.0}/plain/urls/converters.py +0 -0
  123. {plain-0.32.1 → plain-0.33.0}/plain/urls/exceptions.py +0 -0
  124. {plain-0.32.1 → plain-0.33.0}/plain/urls/resolvers.py +0 -0
  125. {plain-0.32.1 → plain-0.33.0}/plain/urls/routers.py +0 -0
  126. {plain-0.32.1 → plain-0.33.0}/plain/urls/utils.py +0 -0
  127. {plain-0.32.1/plain/internal/middleware → plain-0.33.0/plain/utils}/__init__.py +0 -0
  128. {plain-0.32.1 → plain-0.33.0}/plain/utils/cache.py +0 -0
  129. {plain-0.32.1 → plain-0.33.0}/plain/utils/connection.py +0 -0
  130. {plain-0.32.1 → plain-0.33.0}/plain/utils/crypto.py +0 -0
  131. {plain-0.32.1 → plain-0.33.0}/plain/utils/datastructures.py +0 -0
  132. {plain-0.32.1 → plain-0.33.0}/plain/utils/dateparse.py +0 -0
  133. {plain-0.32.1 → plain-0.33.0}/plain/utils/deconstruct.py +0 -0
  134. {plain-0.32.1 → plain-0.33.0}/plain/utils/decorators.py +0 -0
  135. {plain-0.32.1 → plain-0.33.0}/plain/utils/duration.py +0 -0
  136. {plain-0.32.1 → plain-0.33.0}/plain/utils/encoding.py +0 -0
  137. {plain-0.32.1 → plain-0.33.0}/plain/utils/functional.py +0 -0
  138. {plain-0.32.1 → plain-0.33.0}/plain/utils/hashable.py +0 -0
  139. {plain-0.32.1 → plain-0.33.0}/plain/utils/html.py +0 -0
  140. {plain-0.32.1 → plain-0.33.0}/plain/utils/http.py +0 -0
  141. {plain-0.32.1 → plain-0.33.0}/plain/utils/inspect.py +0 -0
  142. {plain-0.32.1 → plain-0.33.0}/plain/utils/ipv6.py +0 -0
  143. {plain-0.32.1 → plain-0.33.0}/plain/utils/itercompat.py +0 -0
  144. {plain-0.32.1 → plain-0.33.0}/plain/utils/module_loading.py +0 -0
  145. {plain-0.32.1 → plain-0.33.0}/plain/utils/regex_helper.py +0 -0
  146. {plain-0.32.1 → plain-0.33.0}/plain/utils/safestring.py +0 -0
  147. {plain-0.32.1 → plain-0.33.0}/plain/utils/text.py +0 -0
  148. {plain-0.32.1 → plain-0.33.0}/plain/utils/timesince.py +0 -0
  149. {plain-0.32.1 → plain-0.33.0}/plain/utils/timezone.py +0 -0
  150. {plain-0.32.1 → plain-0.33.0}/plain/utils/tree.py +0 -0
  151. {plain-0.32.1 → plain-0.33.0}/plain/validators.py +0 -0
  152. {plain-0.32.1 → plain-0.33.0}/plain/views/__init__.py +0 -0
  153. {plain-0.32.1 → plain-0.33.0}/plain/views/csrf.py +0 -0
  154. {plain-0.32.1 → plain-0.33.0}/plain/views/errors.py +0 -0
  155. {plain-0.32.1 → plain-0.33.0}/plain/views/exceptions.py +0 -0
  156. {plain-0.32.1 → plain-0.33.0}/plain/views/objects.py +0 -0
  157. {plain-0.32.1 → plain-0.33.0}/plain/views/redirect.py +0 -0
  158. {plain-0.32.1 → plain-0.33.0}/plain/views/templates.py +0 -0
  159. {plain-0.32.1 → plain-0.33.0}/plain/wsgi.py +0 -0
  160. {plain-0.32.1 → plain-0.33.0}/tests/.bolt/assets_collected/assets.json +0 -0
  161. {plain-0.32.1 → plain-0.33.0}/tests/.gitignore +0 -0
  162. {plain-0.32.1 → plain-0.33.0}/tests/app/.gitignore +0 -0
  163. {plain-0.32.1 → plain-0.33.0}/tests/app/settings.py +0 -0
  164. {plain-0.32.1/plain/utils → plain-0.33.0/tests/app/test}/__init__.py +0 -0
  165. {plain-0.32.1 → plain-0.33.0}/tests/app/test/default_settings.py +0 -0
  166. {plain-0.32.1 → plain-0.33.0}/tests/app/urls.py +0 -0
  167. {plain-0.32.1 → plain-0.33.0}/tests/conftest.py +0 -0
  168. {plain-0.32.1 → plain-0.33.0}/tests/test_cli.py +0 -0
  169. {plain-0.32.1 → plain-0.33.0}/tests/test_runtime.py +0 -0
  170. {plain-0.32.1 → plain-0.33.0}/tests/test_wsgi.py +0 -0
plain-0.33.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: plain
3
+ Version: 0.33.0
4
+ Summary: A web framework for building products with Python.
5
+ Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.11
8
+ Requires-Dist: click>=8.0.0
9
+ Requires-Dist: jinja2>=3.1.2
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Plain
13
+
14
+ **Plain is a web framework for building products with Python.**
15
+
16
+ The core `plain` package provides the backbone of a Python web application (similar to [Flask](https://flask.palletsprojects.com/en/stable/)), while the additional first-party packages can power a more fully-featured database-backed app (similar to [Django](https://www.djangoproject.com/)).
17
+
18
+ All Plain packages are designed to work together and use [PEP 420](https://peps.python.org/pep-0420/) to share the `plain` namespace.
19
+
20
+ To quickly get started with Plain, visit [plainframework.com/start/](https://plainframework.com/start/).
21
+
22
+ ## Core Modules
23
+
24
+ The `plain` package includes everything you need to start handling web requests with Python:
25
+
26
+ - [assets](assets/README.md) - Serve static files and assets.
27
+ - [cli](cli/README.md) - The `plain` CLI, powered by Click.
28
+ - [csrf](csrf/README.md) - Cross-Site Request Forgery protection.
29
+ - [forms](forms/README.md) - HTML forms and form validation.
30
+ - [http](http/README.md) - HTTP request and response handling.
31
+ - [logs](logs/README.md) - Logging configuration and utilities.
32
+ - [preflight](preflight/README.md) - Preflight checks for your app.
33
+ - [runtime](runtime/README.md) - Runtime settings and configuration.
34
+ - [templates](templates/README.md) - Jinja2 templates and rendering.
35
+ - [test](test/README.md) - Test utilities and fixtures.
36
+ - [urls](urls/README.md) - URL routing and request dispatching.
37
+ - [views](views/README.md) - Class-based views and request handlers.
38
+
39
+ ## Foundational Packages
40
+
41
+ - [plain.models](/plain-models/README.md) - Define and interact with your database models.
42
+ - [plain.cache](/plain-cache/README.md) - A database-driven general purpose cache.
43
+ - [plain.mail](/plain-mail/README.md) - Send emails with SMTP or custom backends.
44
+ - [plain.sessions](/plain-sessions/README.md) - User sessions and cookies.
45
+ - [plain.worker](/plain-worker/README.md) - Backgrounb jobs stored in the database.
46
+ - [plain.api](/plain-api/README.md) - Build APIs with Plain views.
47
+
48
+ ## Auth Packages
49
+
50
+ - [plain.auth](/plain-auth/README.md) - User authentication and authorization.
51
+ - [plain.oauth](/plain-oauth/README.md) - OAuth authentication and API access.
52
+ - [plain.passwords](/plain-passwords/README.md) - Password-based login and registration.
53
+ - [plain.loginlink](/plain-loginlink/README.md) - Login links for passwordless authentication.
54
+
55
+ ## Admin Packages
56
+
57
+ - [plain.admin](/plain-admin/README.md) - An admin interface for back-office tasks.
58
+ - [plain.flags](/plain-flags/README.md) - Feature flags.
59
+ - [plain.support](/plain-support/README.md) - Customer support forms.
60
+ - [plain.redirection](/plain-redirection/README.md) - Redirects managed in the database.
61
+ - [plain.pageviews](/plain-pageviews/README.md) - Basic self-hosted page view tracking and reporting.
62
+
63
+ ## Dev Packages
64
+
65
+ - [plain.dev](/plain-dev/README.md) - A single command for local development.
66
+ - [plain.pytest](/plain-pytest/README.md) - Pytest fixtures and helpers.
67
+ - [plain.code](/plain-code/README.md) - Code formatting and linting.
68
+ - [plain.tunnel](/plain-tunnel/README.md) - Expose your local server to the internet.
69
+
70
+ ## Frontend Packages
71
+
72
+ - [plain.tailwind](/plain-tailwind/README.md) - Tailwind CSS integration without Node.js.
73
+ - [plain.htmx](/plain-htmx/README.md) - HTMX integrated into views and templates.
74
+ - [plain.elements](/plain-elements/README.md) - Server-side HTML components.
75
+ - [plain.pages](/plain-pages/README.md) - Static pages with Markdown and Jinja2.
76
+ - [plain.esbuild](/plain-esbuild/README.md) - Simple JavaScript bundling and minification.
77
+ - [plain.vendor](/plain-vendor/README.md) - Vendor JavaScript and CSS libraries.
@@ -0,0 +1,66 @@
1
+ # Plain
2
+
3
+ **Plain is a web framework for building products with Python.**
4
+
5
+ The core `plain` package provides the backbone of a Python web application (similar to [Flask](https://flask.palletsprojects.com/en/stable/)), while the additional first-party packages can power a more fully-featured database-backed app (similar to [Django](https://www.djangoproject.com/)).
6
+
7
+ All Plain packages are designed to work together and use [PEP 420](https://peps.python.org/pep-0420/) to share the `plain` namespace.
8
+
9
+ To quickly get started with Plain, visit [plainframework.com/start/](https://plainframework.com/start/).
10
+
11
+ ## Core Modules
12
+
13
+ The `plain` package includes everything you need to start handling web requests with Python:
14
+
15
+ - [assets](assets/README.md) - Serve static files and assets.
16
+ - [cli](cli/README.md) - The `plain` CLI, powered by Click.
17
+ - [csrf](csrf/README.md) - Cross-Site Request Forgery protection.
18
+ - [forms](forms/README.md) - HTML forms and form validation.
19
+ - [http](http/README.md) - HTTP request and response handling.
20
+ - [logs](logs/README.md) - Logging configuration and utilities.
21
+ - [preflight](preflight/README.md) - Preflight checks for your app.
22
+ - [runtime](runtime/README.md) - Runtime settings and configuration.
23
+ - [templates](templates/README.md) - Jinja2 templates and rendering.
24
+ - [test](test/README.md) - Test utilities and fixtures.
25
+ - [urls](urls/README.md) - URL routing and request dispatching.
26
+ - [views](views/README.md) - Class-based views and request handlers.
27
+
28
+ ## Foundational Packages
29
+
30
+ - [plain.models](/plain-models/README.md) - Define and interact with your database models.
31
+ - [plain.cache](/plain-cache/README.md) - A database-driven general purpose cache.
32
+ - [plain.mail](/plain-mail/README.md) - Send emails with SMTP or custom backends.
33
+ - [plain.sessions](/plain-sessions/README.md) - User sessions and cookies.
34
+ - [plain.worker](/plain-worker/README.md) - Backgrounb jobs stored in the database.
35
+ - [plain.api](/plain-api/README.md) - Build APIs with Plain views.
36
+
37
+ ## Auth Packages
38
+
39
+ - [plain.auth](/plain-auth/README.md) - User authentication and authorization.
40
+ - [plain.oauth](/plain-oauth/README.md) - OAuth authentication and API access.
41
+ - [plain.passwords](/plain-passwords/README.md) - Password-based login and registration.
42
+ - [plain.loginlink](/plain-loginlink/README.md) - Login links for passwordless authentication.
43
+
44
+ ## Admin Packages
45
+
46
+ - [plain.admin](/plain-admin/README.md) - An admin interface for back-office tasks.
47
+ - [plain.flags](/plain-flags/README.md) - Feature flags.
48
+ - [plain.support](/plain-support/README.md) - Customer support forms.
49
+ - [plain.redirection](/plain-redirection/README.md) - Redirects managed in the database.
50
+ - [plain.pageviews](/plain-pageviews/README.md) - Basic self-hosted page view tracking and reporting.
51
+
52
+ ## Dev Packages
53
+
54
+ - [plain.dev](/plain-dev/README.md) - A single command for local development.
55
+ - [plain.pytest](/plain-pytest/README.md) - Pytest fixtures and helpers.
56
+ - [plain.code](/plain-code/README.md) - Code formatting and linting.
57
+ - [plain.tunnel](/plain-tunnel/README.md) - Expose your local server to the internet.
58
+
59
+ ## Frontend Packages
60
+
61
+ - [plain.tailwind](/plain-tailwind/README.md) - Tailwind CSS integration without Node.js.
62
+ - [plain.htmx](/plain-htmx/README.md) - HTMX integrated into views and templates.
63
+ - [plain.elements](/plain-elements/README.md) - Server-side HTML components.
64
+ - [plain.pages](/plain-pages/README.md) - Static pages with Markdown and Jinja2.
65
+ - [plain.esbuild](/plain-esbuild/README.md) - Simple JavaScript bundling and minification.
66
+ - [plain.vendor](/plain-vendor/README.md) - Vendor JavaScript and CSS libraries.
@@ -1,38 +1,37 @@
1
1
  # Assets
2
2
 
3
- Serve static assets (CSS, JS, images, etc.) directly from your app.
4
-
3
+ **Serve static assets (CSS, JS, images, etc.) directly or from a CDN.**
5
4
 
6
5
  ## Usage
7
6
 
8
7
  To serve assets, put them in `app/assets` or `app/{package}/assets`.
9
8
 
10
- Then include the `plain.assets.urls` in your `urls.py`:
9
+ Then include the `AssetsRouter` in your own router, typically under the `assets/` path.
11
10
 
12
11
  ```python
13
12
  # app/urls.py
14
- from plain.urls import include, path
15
- import plain.assets.urls
13
+ from plain.assets.urls import AssetsRouter
14
+ from plain.urls import include, Router
16
15
 
17
16
 
18
- urlpatterns = [
19
- path("assets/", include(plain.assets.urls)),
20
- # ...
21
- ]
17
+ class AppRouter(Router):
18
+ namespace = ""
19
+ urls = [
20
+ include("assets/", AssetsRouter),
21
+ # your other routes here...
22
+ ]
22
23
  ```
23
24
 
24
- Now in your template you can use the `asset()` function to get the URL:
25
+ Now in your template you can use the `asset()` function to get the URL, which will output the fully compiled and fingerprinted URL.
25
26
 
26
27
  ```html
27
28
  <link rel="stylesheet" href="{{ asset('css/style.css') }}">
28
29
  ```
29
30
 
30
-
31
31
  ## Local development
32
32
 
33
33
  When you're working with `settings.DEBUG = True`, the assets will be served directly from their original location. You don't need to run `plain build` or configure anything else.
34
34
 
35
-
36
35
  ## Production deployment
37
36
 
38
37
  In production, one of your deployment steps should be to compile the assets.
@@ -41,11 +40,10 @@ In production, one of your deployment steps should be to compile the assets.
41
40
  plain build
42
41
  ```
43
42
 
44
- By default, this generates "fingerprinted" and compressed versions of the assets, which are then served by your app. This means that a file like `main.css` will result in two new files, like `main.d0db67b.css` and `main.d0db67b.css.gz`.
43
+ By default, this [generates "fingerprinted" and compressed versions of the assets](fingerprints.py#get_file_fingerprint), which are then served by your app. This means that a file like `main.css` will result in two new files, like `main.d0db67b.css` and `main.d0db67b.css.gz`.
45
44
 
46
45
  The purpose of fingerprinting the assets is to allow the browser to cache them indefinitely. When the content of the file changes, the fingerprint will change, and the browser will use the newer file. This cuts down on the number of requests that your app has to handle related to assets.
47
46
 
48
-
49
47
  ## FAQs
50
48
 
51
49
  ### How do you reference assets in Python code?
@@ -67,10 +65,16 @@ mv .plain/assets/compiled /path/to/your/static
67
65
 
68
66
  ### How do I upload the assets to a CDN?
69
67
 
70
- The steps for this will vary, but the general idea is to compile them, and then upload the compiled assets.
68
+ The steps for this will vary, but the general idea is to compile them, and then upload the compiled assets from their [compiled location](compile.py#get_compiled_path).
71
69
 
72
70
  ```bash
71
+ # Compile the assets
73
72
  plain build
73
+
74
+ # List the newly compiled files
75
+ ls .plain/assets/compiled
76
+
77
+ # Upload the files to your CDN
74
78
  ./example-upload-to-cdn-script
75
79
  ```
76
80
 
@@ -81,14 +85,10 @@ Use the `ASSETS_BASE_URL` setting to tell the `{{ asset() }}` template function
81
85
  ASSETS_BASE_URL = "https://cdn.example.com/"
82
86
  ```
83
87
 
84
-
85
88
  ### Why aren't the originals copied to the compiled directory?
86
89
 
87
90
  The default behavior is to fingerprint assets, which is an exact copy of the original file but with a different filename. The originals aren't copied over because you should generally always use this fingerprinted path (that automatically uses longer-lived caching).
88
91
 
89
92
  If you need the originals for any reason, you can use `plain build --keep-original`, though this will typically be combined with `--no-fingerprint` otherwise the fingerprinted files will still get priority in `{{ asset() }}` template calls.
90
93
 
91
-
92
- ### What about source maps or imported css files?
93
-
94
- TODO
94
+ Note that by default, the `ASSETS_REDIRECT_ORIGINAL` setting is `True`, which will redirect requests for the original file to the fingerprinted file.
@@ -1,14 +1,11 @@
1
1
  import gzip
2
- import hashlib
3
2
  import os
4
3
  import shutil
5
4
 
6
5
  from plain.runtime import settings
7
6
 
8
7
  from .finders import iter_assets
9
- from .fingerprints import AssetsFingerprintsManifest
10
-
11
- FINGERPRINT_LENGTH = 7
8
+ from .fingerprints import AssetsFingerprintsManifest, get_file_fingerprint
12
9
 
13
10
  SKIP_COMPRESS_EXTENSIONS = (
14
11
  # Images
@@ -46,12 +43,17 @@ SKIP_COMPRESS_EXTENSIONS = (
46
43
  def get_compiled_path():
47
44
  """
48
45
  Get the path at runtime to the compiled assets directory.
46
+
49
47
  There's no reason currently for this to be a user-facing setting.
50
48
  """
51
49
  return settings.PLAIN_TEMP_PATH / "assets" / "compiled"
52
50
 
53
51
 
54
52
  def compile_assets(*, target_dir, keep_original, fingerprint, compress):
53
+ """
54
+ Compile all assets to the target directory and save a JSON manifest
55
+ mapping the original filenames to the compiled filenames.
56
+ """
55
57
  manifest = AssetsFingerprintsManifest()
56
58
 
57
59
  for asset in iter_assets():
@@ -68,8 +70,7 @@ def compile_assets(*, target_dir, keep_original, fingerprint, compress):
68
70
 
69
71
  yield url_path, resolved_path, compiled_paths
70
72
 
71
- if manifest:
72
- manifest.save()
73
+ manifest.save()
73
74
 
74
75
 
75
76
  def compile_asset(*, asset, target_dir, keep_original, fingerprint, compress):
@@ -98,11 +99,7 @@ def compile_asset(*, asset, target_dir, keep_original, fingerprint, compress):
98
99
  # Fingerprint it with an md5 hash
99
100
  # (maybe need a setting with fnmatch patterns for files to NOT fingerprint?
100
101
  # that would allow pre-fingerprinted files to be used as-is, and keep source maps etc in tact)
101
- with open(asset.absolute_path, "rb") as f:
102
- content = f.read()
103
- fingerprint_hash = hashlib.md5(content, usedforsecurity=False).hexdigest()[
104
- :FINGERPRINT_LENGTH
105
- ]
102
+ fingerprint_hash = get_file_fingerprint(asset.absolute_path)
106
103
 
107
104
  fingerprinted_basename = f"{base}.{fingerprint_hash}{extension}"
108
105
  fingerprinted_path = os.path.join(target_dir, fingerprinted_basename)
@@ -9,6 +9,11 @@ SKIP_ASSETS = (".DS_Store", ".gitignore")
9
9
 
10
10
 
11
11
  def iter_assets():
12
+ """
13
+ Iterate all valid asset files found in the installed
14
+ packages and the app itself.
15
+ """
16
+
12
17
  class Asset:
13
18
  def __init__(self, *, url_path, absolute_path):
14
19
  self.url_path = url_path
@@ -32,6 +37,10 @@ def iter_assets():
32
37
 
33
38
 
34
39
  def iter_asset_dirs():
40
+ """
41
+ Iterate all directories containing assets, from installed
42
+ packages and from app/assets.
43
+ """
35
44
  # Iterate the installed package assets, in order
36
45
  for pkg in packages_registry.get_package_configs():
37
46
  asset_dir = os.path.join(pkg.path, "assets")
@@ -1,10 +1,17 @@
1
+ import hashlib
1
2
  import json
2
3
  from functools import cache
3
4
 
4
5
  from plain.runtime import settings
5
6
 
7
+ FINGERPRINT_LENGTH = 7
8
+
6
9
 
7
10
  class AssetsFingerprintsManifest(dict):
11
+ """
12
+ A manifest of original filenames to fingerprinted filenames.
13
+ """
14
+
8
15
  def __init__(self):
9
16
  self.path = settings.PLAIN_TEMP_PATH / "assets" / "fingerprints.json"
10
17
 
@@ -36,3 +43,16 @@ def get_fingerprinted_url_path(url_path):
36
43
  manifest = _get_manifest()
37
44
  if url_path in manifest:
38
45
  return manifest[url_path]
46
+
47
+
48
+ def get_file_fingerprint(file_path):
49
+ """
50
+ Get the fingerprint hash for a file.
51
+ """
52
+ with open(file_path, "rb") as f:
53
+ content = f.read()
54
+ fingerprint_hash = hashlib.md5(content, usedforsecurity=False).hexdigest()[
55
+ :FINGERPRINT_LENGTH
56
+ ]
57
+
58
+ return fingerprint_hash
@@ -6,6 +6,12 @@ from .views import AssetView
6
6
 
7
7
 
8
8
  class AssetsRouter(Router):
9
+ """
10
+ The router for serving static assets.
11
+
12
+ Include this router in your app router if you are serving assets yourself.
13
+ """
14
+
9
15
  namespace = "assets"
10
16
  urls = [
11
17
  path("<path:path>", AssetView, name="asset"),
@@ -13,6 +19,9 @@ class AssetsRouter(Router):
13
19
 
14
20
 
15
21
  def get_asset_url(url_path):
22
+ """
23
+ Get the full URL to a given asset path.
24
+ """
16
25
  if settings.DEBUG:
17
26
  # In debug, we only ever use the original URL path.
18
27
  resolved_url_path = url_path
@@ -16,9 +16,9 @@ from plain.runtime import settings
16
16
  from plain.urls import reverse
17
17
  from plain.views import View
18
18
 
19
- from .compile import FINGERPRINT_LENGTH, get_compiled_path
19
+ from .compile import get_compiled_path
20
20
  from .finders import iter_assets
21
- from .fingerprints import get_fingerprinted_url_path
21
+ from .fingerprints import FINGERPRINT_LENGTH, get_fingerprinted_url_path
22
22
 
23
23
 
24
24
  class AssetView(View):
@@ -28,8 +28,12 @@ class AssetView(View):
28
28
  This class could be subclassed to further tweak the responses or behavior.
29
29
  """
30
30
 
31
+ def __init__(self, asset_path=None):
32
+ # Allow a path to be passed in AssetView.as_view(path="...")
33
+ self.asset_path = asset_path
34
+
31
35
  def get_url_path(self):
32
- return self.url_kwargs["path"]
36
+ return self.asset_path or self.url_kwargs["path"]
33
37
 
34
38
  def get(self):
35
39
  url_path = self.get_url_path()
@@ -0,0 +1,84 @@
1
+ # CLI
2
+
3
+ **The `plain` CLI and how to add your own commands to it.**
4
+
5
+ Commands are written using [Click](https://click.palletsprojects.com/en/8.1.x/)
6
+ (one of Plain's few dependencies),
7
+ which has been one of those most popular CLI frameworks in Python for a long time.
8
+
9
+ ## Built-in commands
10
+
11
+ ### `plain build`
12
+
13
+ Compile static assets (used in the deploy/production process).
14
+
15
+ Automatically runs `plain tailwind build` if [plain.tailwind](/plain-tailwind/) is installed.
16
+
17
+ ### `plain create`
18
+
19
+ Create a new local package.
20
+
21
+ ### `plain preflight`
22
+
23
+ Run preflight checks to ensure your app is ready to run.
24
+
25
+ ### `plain run`
26
+
27
+ Run a Python script in the context of your app.
28
+
29
+ ### `plain setting`
30
+
31
+ View the runtime value of a named setting.
32
+
33
+ ### `plain shell`
34
+
35
+ Open a Python shell with the Plain loaded.
36
+
37
+ To auto-load models or run other code at shell launch,
38
+ create an `app/shell.py` and it will be imported automatically.
39
+
40
+ ```python
41
+ # app/shell.py
42
+ from app.organizations.models import Organization
43
+
44
+ __all__ = [
45
+ "Organization",
46
+ ]
47
+ ```
48
+
49
+ ### `plain urls`
50
+
51
+ List all the URL patterns in your app.
52
+
53
+ ### `plain utils generate-secret-key`
54
+
55
+ Generate a new secret key for your app, to be used in `settings.SECRET_KEY`.
56
+
57
+ ## Adding commands
58
+
59
+ The `register_cli` decorator can be used to add your own commands to the `plain` CLI.
60
+
61
+ ```python
62
+ import click
63
+ from plain.cli import register_cli
64
+
65
+
66
+ @register_cli("example-subgroup-name")
67
+ @click.group()
68
+ def cli():
69
+ """Custom example commands"""
70
+ pass
71
+
72
+ @cli.command()
73
+ def example_command():
74
+ click.echo("An example command!")
75
+ ```
76
+
77
+ Then you can run the command with `plain`.
78
+
79
+ ```bash
80
+ $ plain example-subgroup-name example-command
81
+ An example command!
82
+ ```
83
+
84
+ Technically you can register a CLI from anywhere, but typically you will do it in either `app/cli.py` or a package's `<pkg>/cli.py`, as those modules will be autoloaded by Plain.