plain 0.74.0__tar.gz → 0.76.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 (208) hide show
  1. {plain-0.74.0 → plain-0.76.0}/PKG-INFO +2 -2
  2. {plain-0.74.0 → plain-0.76.0}/plain/CHANGELOG.md +29 -0
  3. {plain-0.74.0 → plain-0.76.0}/plain/README.md +1 -1
  4. {plain-0.74.0 → plain-0.76.0}/plain/chores/README.md +1 -1
  5. {plain-0.74.0 → plain-0.76.0}/plain/cli/core.py +35 -17
  6. plain-0.76.0/plain/cli/runtime.py +28 -0
  7. plain-0.76.0/plain/cli/server.py +143 -0
  8. plain-0.76.0/plain/server/LICENSE +35 -0
  9. plain-0.76.0/plain/server/README.md +75 -0
  10. plain-0.76.0/plain/server/__init__.py +9 -0
  11. plain-0.76.0/plain/server/app.py +52 -0
  12. plain-0.76.0/plain/server/arbiter.py +555 -0
  13. plain-0.76.0/plain/server/config.py +118 -0
  14. plain-0.76.0/plain/server/errors.py +31 -0
  15. plain-0.76.0/plain/server/glogging.py +292 -0
  16. plain-0.76.0/plain/server/http/__init__.py +12 -0
  17. plain-0.76.0/plain/server/http/body.py +283 -0
  18. plain-0.76.0/plain/server/http/errors.py +150 -0
  19. plain-0.76.0/plain/server/http/message.py +399 -0
  20. plain-0.76.0/plain/server/http/parser.py +69 -0
  21. plain-0.76.0/plain/server/http/unreader.py +88 -0
  22. plain-0.76.0/plain/server/http/wsgi.py +421 -0
  23. plain-0.76.0/plain/server/pidfile.py +91 -0
  24. plain-0.76.0/plain/server/reloader.py +158 -0
  25. plain-0.76.0/plain/server/sock.py +219 -0
  26. plain-0.76.0/plain/server/util.py +380 -0
  27. plain-0.76.0/plain/server/workers/__init__.py +12 -0
  28. plain-0.76.0/plain/server/workers/base.py +305 -0
  29. plain-0.76.0/plain/server/workers/gthread.py +393 -0
  30. plain-0.76.0/plain/server/workers/sync.py +210 -0
  31. plain-0.76.0/plain/server/workers/workertmp.py +50 -0
  32. {plain-0.74.0 → plain-0.76.0}/pyproject.toml +1 -1
  33. {plain-0.74.0 → plain-0.76.0}/.gitignore +0 -0
  34. {plain-0.74.0 → plain-0.76.0}/LICENSE +0 -0
  35. {plain-0.74.0 → plain-0.76.0}/README.md +0 -0
  36. {plain-0.74.0 → plain-0.76.0}/plain/AGENTS.md +0 -0
  37. {plain-0.74.0 → plain-0.76.0}/plain/__main__.py +0 -0
  38. {plain-0.74.0 → plain-0.76.0}/plain/assets/README.md +0 -0
  39. {plain-0.74.0 → plain-0.76.0}/plain/assets/__init__.py +0 -0
  40. {plain-0.74.0 → plain-0.76.0}/plain/assets/compile.py +0 -0
  41. {plain-0.74.0 → plain-0.76.0}/plain/assets/finders.py +0 -0
  42. {plain-0.74.0 → plain-0.76.0}/plain/assets/fingerprints.py +0 -0
  43. {plain-0.74.0 → plain-0.76.0}/plain/assets/urls.py +0 -0
  44. {plain-0.74.0 → plain-0.76.0}/plain/assets/views.py +0 -0
  45. {plain-0.74.0 → plain-0.76.0}/plain/chores/__init__.py +0 -0
  46. {plain-0.74.0 → plain-0.76.0}/plain/chores/registry.py +0 -0
  47. {plain-0.74.0 → plain-0.76.0}/plain/cli/README.md +0 -0
  48. {plain-0.74.0 → plain-0.76.0}/plain/cli/__init__.py +0 -0
  49. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/__init__.py +0 -0
  50. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/docs.py +0 -0
  51. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/llmdocs.py +0 -0
  52. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/md.py +0 -0
  53. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/prompt.py +0 -0
  54. {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/request.py +0 -0
  55. {plain-0.74.0 → plain-0.76.0}/plain/cli/build.py +0 -0
  56. {plain-0.74.0 → plain-0.76.0}/plain/cli/changelog.py +0 -0
  57. {plain-0.74.0 → plain-0.76.0}/plain/cli/chores.py +0 -0
  58. {plain-0.74.0 → plain-0.76.0}/plain/cli/docs.py +0 -0
  59. {plain-0.74.0 → plain-0.76.0}/plain/cli/formatting.py +0 -0
  60. {plain-0.74.0 → plain-0.76.0}/plain/cli/install.py +0 -0
  61. {plain-0.74.0 → plain-0.76.0}/plain/cli/output.py +0 -0
  62. {plain-0.74.0 → plain-0.76.0}/plain/cli/preflight.py +0 -0
  63. {plain-0.74.0 → plain-0.76.0}/plain/cli/print.py +0 -0
  64. {plain-0.74.0 → plain-0.76.0}/plain/cli/registry.py +0 -0
  65. {plain-0.74.0 → plain-0.76.0}/plain/cli/scaffold.py +0 -0
  66. {plain-0.74.0 → plain-0.76.0}/plain/cli/settings.py +0 -0
  67. {plain-0.74.0 → plain-0.76.0}/plain/cli/shell.py +0 -0
  68. {plain-0.74.0 → plain-0.76.0}/plain/cli/startup.py +0 -0
  69. {plain-0.74.0 → plain-0.76.0}/plain/cli/upgrade.py +0 -0
  70. {plain-0.74.0 → plain-0.76.0}/plain/cli/urls.py +0 -0
  71. {plain-0.74.0 → plain-0.76.0}/plain/cli/utils.py +0 -0
  72. {plain-0.74.0 → plain-0.76.0}/plain/csrf/README.md +0 -0
  73. {plain-0.74.0 → plain-0.76.0}/plain/csrf/middleware.py +0 -0
  74. {plain-0.74.0 → plain-0.76.0}/plain/csrf/views.py +0 -0
  75. {plain-0.74.0 → plain-0.76.0}/plain/debug.py +0 -0
  76. {plain-0.74.0 → plain-0.76.0}/plain/exceptions.py +0 -0
  77. {plain-0.74.0 → plain-0.76.0}/plain/forms/README.md +0 -0
  78. {plain-0.74.0 → plain-0.76.0}/plain/forms/__init__.py +0 -0
  79. {plain-0.74.0 → plain-0.76.0}/plain/forms/boundfield.py +0 -0
  80. {plain-0.74.0 → plain-0.76.0}/plain/forms/exceptions.py +0 -0
  81. {plain-0.74.0 → plain-0.76.0}/plain/forms/fields.py +0 -0
  82. {plain-0.74.0 → plain-0.76.0}/plain/forms/forms.py +0 -0
  83. {plain-0.74.0 → plain-0.76.0}/plain/http/README.md +0 -0
  84. {plain-0.74.0 → plain-0.76.0}/plain/http/__init__.py +0 -0
  85. {plain-0.74.0 → plain-0.76.0}/plain/http/cookie.py +0 -0
  86. {plain-0.74.0 → plain-0.76.0}/plain/http/multipartparser.py +0 -0
  87. {plain-0.74.0 → plain-0.76.0}/plain/http/request.py +0 -0
  88. {plain-0.74.0 → plain-0.76.0}/plain/http/response.py +0 -0
  89. {plain-0.74.0 → plain-0.76.0}/plain/internal/__init__.py +0 -0
  90. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/__init__.py +0 -0
  91. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/base.py +0 -0
  92. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/locks.py +0 -0
  93. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/move.py +0 -0
  94. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/temp.py +0 -0
  95. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/uploadedfile.py +0 -0
  96. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/uploadhandler.py +0 -0
  97. {plain-0.74.0 → plain-0.76.0}/plain/internal/files/utils.py +0 -0
  98. {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/__init__.py +0 -0
  99. {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/base.py +0 -0
  100. {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/exception.py +0 -0
  101. {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/wsgi.py +0 -0
  102. {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/__init__.py +0 -0
  103. {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/headers.py +0 -0
  104. {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/hosts.py +0 -0
  105. {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/https.py +0 -0
  106. {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/slash.py +0 -0
  107. {plain-0.74.0 → plain-0.76.0}/plain/json.py +0 -0
  108. {plain-0.74.0 → plain-0.76.0}/plain/logs/README.md +0 -0
  109. {plain-0.74.0 → plain-0.76.0}/plain/logs/__init__.py +0 -0
  110. {plain-0.74.0 → plain-0.76.0}/plain/logs/configure.py +0 -0
  111. {plain-0.74.0 → plain-0.76.0}/plain/logs/debug.py +0 -0
  112. {plain-0.74.0 → plain-0.76.0}/plain/logs/formatters.py +0 -0
  113. {plain-0.74.0 → plain-0.76.0}/plain/logs/loggers.py +0 -0
  114. {plain-0.74.0 → plain-0.76.0}/plain/logs/utils.py +0 -0
  115. {plain-0.74.0 → plain-0.76.0}/plain/packages/README.md +0 -0
  116. {plain-0.74.0 → plain-0.76.0}/plain/packages/__init__.py +0 -0
  117. {plain-0.74.0 → plain-0.76.0}/plain/packages/config.py +0 -0
  118. {plain-0.74.0 → plain-0.76.0}/plain/packages/registry.py +0 -0
  119. {plain-0.74.0 → plain-0.76.0}/plain/paginator.py +0 -0
  120. {plain-0.74.0 → plain-0.76.0}/plain/preflight/README.md +0 -0
  121. {plain-0.74.0 → plain-0.76.0}/plain/preflight/__init__.py +0 -0
  122. {plain-0.74.0 → plain-0.76.0}/plain/preflight/checks.py +0 -0
  123. {plain-0.74.0 → plain-0.76.0}/plain/preflight/files.py +0 -0
  124. {plain-0.74.0 → plain-0.76.0}/plain/preflight/registry.py +0 -0
  125. {plain-0.74.0 → plain-0.76.0}/plain/preflight/results.py +0 -0
  126. {plain-0.74.0 → plain-0.76.0}/plain/preflight/security.py +0 -0
  127. {plain-0.74.0 → plain-0.76.0}/plain/preflight/urls.py +0 -0
  128. {plain-0.74.0 → plain-0.76.0}/plain/runtime/README.md +0 -0
  129. {plain-0.74.0 → plain-0.76.0}/plain/runtime/__init__.py +0 -0
  130. {plain-0.74.0 → plain-0.76.0}/plain/runtime/global_settings.py +0 -0
  131. {plain-0.74.0 → plain-0.76.0}/plain/runtime/user_settings.py +0 -0
  132. {plain-0.74.0 → plain-0.76.0}/plain/runtime/utils.py +0 -0
  133. {plain-0.74.0 → plain-0.76.0}/plain/signals/README.md +0 -0
  134. {plain-0.74.0 → plain-0.76.0}/plain/signals/__init__.py +0 -0
  135. {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/__init__.py +0 -0
  136. {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/dispatcher.py +0 -0
  137. {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/license.txt +0 -0
  138. {plain-0.74.0 → plain-0.76.0}/plain/signing.py +0 -0
  139. {plain-0.74.0 → plain-0.76.0}/plain/templates/AGENTS.md +0 -0
  140. {plain-0.74.0 → plain-0.76.0}/plain/templates/README.md +0 -0
  141. {plain-0.74.0 → plain-0.76.0}/plain/templates/__init__.py +0 -0
  142. {plain-0.74.0 → plain-0.76.0}/plain/templates/core.py +0 -0
  143. {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/__init__.py +0 -0
  144. {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/environments.py +0 -0
  145. {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/extensions.py +0 -0
  146. {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/filters.py +0 -0
  147. {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/globals.py +0 -0
  148. {plain-0.74.0 → plain-0.76.0}/plain/test/README.md +0 -0
  149. {plain-0.74.0 → plain-0.76.0}/plain/test/__init__.py +0 -0
  150. {plain-0.74.0 → plain-0.76.0}/plain/test/client.py +0 -0
  151. {plain-0.74.0 → plain-0.76.0}/plain/test/encoding.py +0 -0
  152. {plain-0.74.0 → plain-0.76.0}/plain/test/exceptions.py +0 -0
  153. {plain-0.74.0 → plain-0.76.0}/plain/urls/README.md +0 -0
  154. {plain-0.74.0 → plain-0.76.0}/plain/urls/__init__.py +0 -0
  155. {plain-0.74.0 → plain-0.76.0}/plain/urls/converters.py +0 -0
  156. {plain-0.74.0 → plain-0.76.0}/plain/urls/exceptions.py +0 -0
  157. {plain-0.74.0 → plain-0.76.0}/plain/urls/patterns.py +0 -0
  158. {plain-0.74.0 → plain-0.76.0}/plain/urls/resolvers.py +0 -0
  159. {plain-0.74.0 → plain-0.76.0}/plain/urls/routers.py +0 -0
  160. {plain-0.74.0 → plain-0.76.0}/plain/urls/utils.py +0 -0
  161. {plain-0.74.0 → plain-0.76.0}/plain/utils/README.md +0 -0
  162. {plain-0.74.0 → plain-0.76.0}/plain/utils/__init__.py +0 -0
  163. {plain-0.74.0 → plain-0.76.0}/plain/utils/cache.py +0 -0
  164. {plain-0.74.0 → plain-0.76.0}/plain/utils/crypto.py +0 -0
  165. {plain-0.74.0 → plain-0.76.0}/plain/utils/datastructures.py +0 -0
  166. {plain-0.74.0 → plain-0.76.0}/plain/utils/dateparse.py +0 -0
  167. {plain-0.74.0 → plain-0.76.0}/plain/utils/deconstruct.py +0 -0
  168. {plain-0.74.0 → plain-0.76.0}/plain/utils/decorators.py +0 -0
  169. {plain-0.74.0 → plain-0.76.0}/plain/utils/duration.py +0 -0
  170. {plain-0.74.0 → plain-0.76.0}/plain/utils/encoding.py +0 -0
  171. {plain-0.74.0 → plain-0.76.0}/plain/utils/functional.py +0 -0
  172. {plain-0.74.0 → plain-0.76.0}/plain/utils/hashable.py +0 -0
  173. {plain-0.74.0 → plain-0.76.0}/plain/utils/html.py +0 -0
  174. {plain-0.74.0 → plain-0.76.0}/plain/utils/http.py +0 -0
  175. {plain-0.74.0 → plain-0.76.0}/plain/utils/inspect.py +0 -0
  176. {plain-0.74.0 → plain-0.76.0}/plain/utils/ipv6.py +0 -0
  177. {plain-0.74.0 → plain-0.76.0}/plain/utils/itercompat.py +0 -0
  178. {plain-0.74.0 → plain-0.76.0}/plain/utils/module_loading.py +0 -0
  179. {plain-0.74.0 → plain-0.76.0}/plain/utils/regex_helper.py +0 -0
  180. {plain-0.74.0 → plain-0.76.0}/plain/utils/safestring.py +0 -0
  181. {plain-0.74.0 → plain-0.76.0}/plain/utils/text.py +0 -0
  182. {plain-0.74.0 → plain-0.76.0}/plain/utils/timesince.py +0 -0
  183. {plain-0.74.0 → plain-0.76.0}/plain/utils/timezone.py +0 -0
  184. {plain-0.74.0 → plain-0.76.0}/plain/utils/tree.py +0 -0
  185. {plain-0.74.0 → plain-0.76.0}/plain/validators.py +0 -0
  186. {plain-0.74.0 → plain-0.76.0}/plain/views/README.md +0 -0
  187. {plain-0.74.0 → plain-0.76.0}/plain/views/__init__.py +0 -0
  188. {plain-0.74.0 → plain-0.76.0}/plain/views/base.py +0 -0
  189. {plain-0.74.0 → plain-0.76.0}/plain/views/errors.py +0 -0
  190. {plain-0.74.0 → plain-0.76.0}/plain/views/exceptions.py +0 -0
  191. {plain-0.74.0 → plain-0.76.0}/plain/views/forms.py +0 -0
  192. {plain-0.74.0 → plain-0.76.0}/plain/views/objects.py +0 -0
  193. {plain-0.74.0 → plain-0.76.0}/plain/views/redirect.py +0 -0
  194. {plain-0.74.0 → plain-0.76.0}/plain/views/templates.py +0 -0
  195. {plain-0.74.0 → plain-0.76.0}/plain/wsgi.py +0 -0
  196. {plain-0.74.0 → plain-0.76.0}/tests/.gitignore +0 -0
  197. {plain-0.74.0 → plain-0.76.0}/tests/app/.gitignore +0 -0
  198. {plain-0.74.0 → plain-0.76.0}/tests/app/settings.py +0 -0
  199. {plain-0.74.0 → plain-0.76.0}/tests/app/test/__init__.py +0 -0
  200. {plain-0.74.0 → plain-0.76.0}/tests/app/test/default_settings.py +0 -0
  201. {plain-0.74.0 → plain-0.76.0}/tests/app/urls.py +0 -0
  202. {plain-0.74.0 → plain-0.76.0}/tests/conftest.py +0 -0
  203. {plain-0.74.0 → plain-0.76.0}/tests/test_cli.py +0 -0
  204. {plain-0.74.0 → plain-0.76.0}/tests/test_csrf.py +0 -0
  205. {plain-0.74.0 → plain-0.76.0}/tests/test_http_hosts.py +0 -0
  206. {plain-0.74.0 → plain-0.76.0}/tests/test_logs.py +0 -0
  207. {plain-0.74.0 → plain-0.76.0}/tests/test_runtime.py +0 -0
  208. {plain-0.74.0 → plain-0.76.0}/tests/test_wsgi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain
3
- Version: 0.74.0
3
+ Version: 0.76.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
@@ -44,7 +44,7 @@ The `plain` package includes everything you need to start handling web requests
44
44
  - [plain.cache](/plain-cache/plain/cache/README.md) - A database-driven general purpose cache.
45
45
  - [plain.email](/plain-email/plain/email/README.md) - Send emails with SMTP or custom backends.
46
46
  - [plain.sessions](/plain-sessions/plain/sessions/README.md) - User sessions and cookies.
47
- - [plain.worker](/plain-worker/plain/worker/README.md) - Background jobs stored in the database.
47
+ - [plain.jobs](/plain-jobs/plain/jobs/README.md) - Background jobs stored in the database.
48
48
  - [plain.api](/plain-api/plain/api/README.md) - Build APIs with Plain views.
49
49
 
50
50
  ## Auth Packages
@@ -1,5 +1,34 @@
1
1
  # plain changelog
2
2
 
3
+ ## [0.76.0](https://github.com/dropseed/plain/releases/plain@0.76.0) (2025-10-12)
4
+
5
+ ### What's changed
6
+
7
+ - Added new `plain server` command with built-in WSGI server (vendored gunicorn) ([f9dc2867c7](https://github.com/dropseed/plain/commit/f9dc2867c7))
8
+ - The `plain server` command supports `WEB_CONCURRENCY` environment variable for worker processes ([0c3e8c6f32](https://github.com/dropseed/plain/commit/0c3e8c6f32))
9
+ - Simplified server startup logging to use a single consolidated log line ([b1405b71f0](https://github.com/dropseed/plain/commit/b1405b71f0))
10
+ - Removed `gunicorn` as an external dependency - server functionality is now built into plain core ([cb6c2f484d](https://github.com/dropseed/plain/commit/cb6c2f484d))
11
+ - Internal server environment variables renamed from `GUNICORN_*` to `PLAIN_SERVER_*` ([745c073123](https://github.com/dropseed/plain/commit/745c073123))
12
+ - Removed unused server features including hooks, syslog, proxy protocol, user/group dropping, and config file loading ([be0f82d92b](https://github.com/dropseed/plain/commit/be0f82d92b), [10c206875b](https://github.com/dropseed/plain/commit/10c206875b), [ecf327014c](https://github.com/dropseed/plain/commit/ecf327014c), [fb5a10f50b](https://github.com/dropseed/plain/commit/fb5a10f50b))
13
+
14
+ ### Upgrade instructions
15
+
16
+ - Replace any direct usage of `gunicorn` with the new `plain server` command (ex. `gunicorn plain.wsgi:app --workers 4` becomes `plain server --workers 4`)
17
+ - Update any deployment scripts or Procfiles that use `gunicorn` to use `plain server` instead
18
+ - Remove `gunicorn` from your project dependencies if you added it separately (it's now built into plain)
19
+ - For Heroku deployments, the `$PORT` is not automatically detected - update your Procfile to `web: plain server --bind 0.0.0.0:$PORT`
20
+ - If you were using gunicorn configuration files, migrate the settings to `plain server` command-line options (run `plain server --help` to see available options)
21
+
22
+ ## [0.75.0](https://github.com/dropseed/plain/releases/plain@0.75.0) (2025-10-10)
23
+
24
+ ### What's changed
25
+
26
+ - Documentation references updated from `plain-worker` to `plain-jobs` following the package rename ([24219856e0](https://github.com/dropseed/plain/commit/24219856e0))
27
+
28
+ ### Upgrade instructions
29
+
30
+ - No changes required
31
+
3
32
  ## [0.74.0](https://github.com/dropseed/plain/releases/plain@0.74.0) (2025-10-08)
4
33
 
5
34
  ### What's changed
@@ -31,7 +31,7 @@ The `plain` package includes everything you need to start handling web requests
31
31
  - [plain.cache](/plain-cache/plain/cache/README.md) - A database-driven general purpose cache.
32
32
  - [plain.email](/plain-email/plain/email/README.md) - Send emails with SMTP or custom backends.
33
33
  - [plain.sessions](/plain-sessions/plain/sessions/README.md) - User sessions and cookies.
34
- - [plain.worker](/plain-worker/plain/worker/README.md) - Background jobs stored in the database.
34
+ - [plain.jobs](/plain-jobs/plain/jobs/README.md) - Background jobs stored in the database.
35
35
  - [plain.api](/plain-api/plain/api/README.md) - Build APIs with Plain views.
36
36
 
37
37
  ## Auth Packages
@@ -38,7 +38,7 @@ The `plain chores run` command will execute all registered chores. When and how
38
38
  There are several ways you can run chores depending on your needs:
39
39
 
40
40
  - on deploy
41
- - as a [`plain.worker` scheduled job](/plain-worker/plain/worker/README.md#scheduled-jobs)
41
+ - as a [`plain.jobs` scheduled job](/plain-jobs/plain/jobs/README.md#scheduled-jobs)
42
42
  - as a cron job (using any cron-like system where your app is hosted)
43
43
  - manually as needed
44
44
 
@@ -19,6 +19,7 @@ from .install import install
19
19
  from .preflight import preflight_cli
20
20
  from .registry import cli_registry
21
21
  from .scaffold import create
22
+ from .server import server
22
23
  from .settings import setting
23
24
  from .shell import run, shell
24
25
  from .upgrade import upgrade
@@ -45,6 +46,7 @@ plain_cli.add_command(shell)
45
46
  plain_cli.add_command(run)
46
47
  plain_cli.add_command(install)
47
48
  plain_cli.add_command(upgrade)
49
+ plain_cli.add_command(server)
48
50
 
49
51
 
50
52
  class CLIRegistryGroup(click.Group):
@@ -68,26 +70,33 @@ class PlainCommandCollection(click.CommandCollection):
68
70
  context_class = PlainContext
69
71
 
70
72
  def __init__(self, *args: Any, **kwargs: Any):
71
- sources = []
73
+ # Start with only built-in commands (no setup needed)
74
+ sources = [plain_cli]
75
+
76
+ super().__init__(*args, **kwargs)
77
+ self.sources = sources
78
+ self._registry_group = None
79
+ self._setup_attempted = False
80
+
81
+ def _ensure_registry_loaded(self) -> None:
82
+ """Lazy load the registry group (requires setup)."""
83
+ if self._registry_group is not None or self._setup_attempted:
84
+ return
85
+
86
+ self._setup_attempted = True
72
87
 
73
88
  try:
74
89
  plain.runtime.setup()
75
-
76
- sources = [
77
- CLIRegistryGroup(),
78
- plain_cli,
79
- ]
90
+ self._registry_group = CLIRegistryGroup()
91
+ # Add registry group to sources
92
+ self.sources.insert(0, self._registry_group)
80
93
  except plain.runtime.AppPathNotFound:
81
- # Allow some commands to work regardless of being in a valid app
94
+ # Allow built-in commands to work regardless of being in a valid app
82
95
  click.secho(
83
96
  "Plain `app` directory not found. Some commands may be missing.",
84
97
  fg="yellow",
85
98
  err=True,
86
99
  )
87
-
88
- sources = [
89
- plain_cli,
90
- ]
91
100
  except ImproperlyConfigured as e:
92
101
  # Show what was configured incorrectly and exit
93
102
  click.secho(
@@ -95,7 +104,6 @@ class PlainCommandCollection(click.CommandCollection):
95
104
  fg="red",
96
105
  err=True,
97
106
  )
98
-
99
107
  exit(1)
100
108
  except Exception as e:
101
109
  # Show the exception and exit
@@ -108,19 +116,29 @@ class PlainCommandCollection(click.CommandCollection):
108
116
  fg="red",
109
117
  err=True,
110
118
  )
111
-
112
119
  exit(1)
113
120
 
114
- super().__init__(*args, **kwargs)
115
-
116
- self.sources = sources
117
-
118
121
  def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
122
+ # Try built-in commands first
119
123
  cmd = super().get_command(ctx, cmd_name)
124
+
125
+ if cmd is None:
126
+ # Command not found in built-ins, try registry (requires setup)
127
+ self._ensure_registry_loaded()
128
+ cmd = super().get_command(ctx, cmd_name)
129
+ elif not getattr(cmd, "without_runtime_setup", False):
130
+ # Command found but needs setup - ensure registry is loaded
131
+ self._ensure_registry_loaded()
132
+
120
133
  if cmd:
121
134
  # Pass the formatting down to subcommands automatically
122
135
  cmd.context_class = self.context_class
123
136
  return cmd
124
137
 
138
+ def list_commands(self, ctx: Context) -> list[str]:
139
+ # For help listing, we need to show registry commands too
140
+ self._ensure_registry_loaded()
141
+ return super().list_commands(ctx)
142
+
125
143
 
126
144
  cli = PlainCommandCollection()
@@ -0,0 +1,28 @@
1
+ """
2
+ CLI runtime utilities.
3
+
4
+ This module provides decorators and utilities for CLI commands.
5
+ """
6
+
7
+ from collections.abc import Callable
8
+ from typing import TypeVar
9
+
10
+ F = TypeVar("F", bound=Callable)
11
+
12
+
13
+ def without_runtime_setup(f: F) -> F:
14
+ """
15
+ Decorator to mark commands that don't need plain.runtime.setup().
16
+
17
+ Use this for commands that don't access settings or app code,
18
+ particularly for commands that fork (like server) where setup()
19
+ should happen in the worker process, not the parent.
20
+
21
+ Example:
22
+ @without_runtime_setup
23
+ @click.command()
24
+ def server(**options):
25
+ ...
26
+ """
27
+ f.without_runtime_setup = True # type: ignore[attr-defined] # dynamic attribute for decorator
28
+ return f
@@ -0,0 +1,143 @@
1
+ import click
2
+
3
+ from plain.cli.runtime import without_runtime_setup
4
+
5
+
6
+ @without_runtime_setup
7
+ @click.command()
8
+ @click.option(
9
+ "--bind",
10
+ "-b",
11
+ multiple=True,
12
+ default=["127.0.0.1:8000"],
13
+ help="Address to bind to (HOST:PORT, can be used multiple times)",
14
+ )
15
+ @click.option(
16
+ "--threads",
17
+ type=int,
18
+ default=1,
19
+ help="Number of threads per worker",
20
+ show_default=True,
21
+ )
22
+ @click.option(
23
+ "--workers",
24
+ "-w",
25
+ type=int,
26
+ default=1,
27
+ envvar="WEB_CONCURRENCY",
28
+ help="Number of worker processes",
29
+ show_default=True,
30
+ )
31
+ @click.option(
32
+ "--timeout",
33
+ "-t",
34
+ type=int,
35
+ default=30,
36
+ help="Worker timeout in seconds",
37
+ show_default=True,
38
+ )
39
+ @click.option(
40
+ "--certfile",
41
+ type=click.Path(exists=True),
42
+ help="SSL certificate file",
43
+ )
44
+ @click.option(
45
+ "--keyfile",
46
+ type=click.Path(exists=True),
47
+ help="SSL key file",
48
+ )
49
+ @click.option(
50
+ "--log-level",
51
+ default="info",
52
+ type=click.Choice(["debug", "info", "warning", "error", "critical"]),
53
+ help="Logging level",
54
+ show_default=True,
55
+ )
56
+ @click.option(
57
+ "--reload",
58
+ is_flag=True,
59
+ help="Restart workers when code changes (dev only)",
60
+ )
61
+ @click.option(
62
+ "--reload-extra-file",
63
+ multiple=True,
64
+ type=click.Path(exists=True),
65
+ help="Additional files to watch for reload (can be used multiple times)",
66
+ )
67
+ @click.option(
68
+ "--access-log",
69
+ default="-",
70
+ help="Access log file (use '-' for stdout)",
71
+ show_default=True,
72
+ )
73
+ @click.option(
74
+ "--error-log",
75
+ default="-",
76
+ help="Error log file (use '-' for stderr)",
77
+ show_default=True,
78
+ )
79
+ @click.option(
80
+ "--log-format",
81
+ default="%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
82
+ help="Log format string (applies to both error and access logs)",
83
+ show_default=True,
84
+ )
85
+ @click.option(
86
+ "--access-log-format",
87
+ help="Access log format string (HTTP request details)",
88
+ default='%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"',
89
+ show_default=True,
90
+ )
91
+ @click.option(
92
+ "--max-requests",
93
+ type=int,
94
+ default=0,
95
+ help="Max requests before worker restart (0=disabled)",
96
+ show_default=True,
97
+ )
98
+ @click.option(
99
+ "--pidfile",
100
+ type=click.Path(),
101
+ help="PID file path",
102
+ )
103
+ def server(
104
+ bind: tuple[str, ...],
105
+ threads: int,
106
+ workers: int,
107
+ timeout: int,
108
+ certfile: str | None,
109
+ keyfile: str | None,
110
+ log_level: str,
111
+ reload: bool,
112
+ reload_extra_file: tuple[str, ...],
113
+ access_log: str,
114
+ error_log: str,
115
+ log_format: str,
116
+ access_log_format: str,
117
+ max_requests: int,
118
+ pidfile: str | None,
119
+ ) -> None:
120
+ """
121
+ Run a production-ready WSGI server.
122
+ """
123
+ from plain.server import ServerApplication
124
+ from plain.server.config import Config
125
+
126
+ cfg = Config(
127
+ bind=list(bind),
128
+ threads=threads,
129
+ workers=workers,
130
+ timeout=timeout,
131
+ max_requests=max_requests,
132
+ reload=reload,
133
+ reload_extra_files=list(reload_extra_file) if reload_extra_file else [],
134
+ pidfile=pidfile,
135
+ certfile=certfile,
136
+ keyfile=keyfile,
137
+ loglevel=log_level,
138
+ accesslog=access_log,
139
+ errorlog=error_log,
140
+ log_format=log_format,
141
+ access_log_format=access_log_format,
142
+ )
143
+ ServerApplication(cfg=cfg).run()
@@ -0,0 +1,35 @@
1
+ Plain HTTP Server - License and Attribution
2
+ ============================================
3
+
4
+ This module is based on gunicorn (https://gunicorn.org), integrated from
5
+ commit 1dc4ce9d59c3458305d701c4c6d63aa6b1d1b309 (gunicorn 23.0.0, October 2024).
6
+
7
+ The gunicorn code has been integrated into Plain and modified for Plain's
8
+ specific use case. All files should be considered modified from the original.
9
+
10
+ Original repository: https://github.com/benoitc/gunicorn
11
+
12
+ --------------------------------------------------------------------------------
13
+
14
+ MIT License
15
+
16
+ Copyright (c) 2009-2024 Benoît Chesneau <benoitc@gunicorn.org>
17
+ Copyright (c) 2009-2015 Paul J. Davis <paul.joseph.davis@gmail.com>
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights
22
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
23
+ copies of the Software, and to permit persons to whom the Software is
24
+ furnished to do so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in all
27
+ copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
@@ -0,0 +1,75 @@
1
+ # plain.server
2
+
3
+ **Plain's internal HTTP server based on vendored gunicorn.**
4
+
5
+ ## Overview
6
+
7
+ This module provides a WSGI HTTP server for Plain applications. It is based on [gunicorn](https://gunicorn.org), which has been vendored into Plain's core to provide better integration and control over the HTTP server layer.
8
+
9
+ The server is designed to work seamlessly with Plain's development workflow while still maintaining WSGI compatibility, allowing you to eject to any alternative WSGI server if needed.
10
+
11
+ ## Usage
12
+
13
+ ### Command Line
14
+
15
+ The simplest way to run the server is using the `plain server` command:
16
+
17
+ ```bash
18
+ # Run with defaults (127.0.0.1:8000)
19
+ plain server
20
+
21
+ # Specify host and port
22
+ plain server --bind 0.0.0.0:8080
23
+
24
+ # Run with SSL
25
+ plain server --certfile cert.pem --keyfile key.pem
26
+
27
+ # Enable auto-reload for development
28
+ plain server --reload
29
+
30
+ # Use multiple threads
31
+ plain server --threads 8
32
+ ```
33
+
34
+ ## Configuration Options
35
+
36
+ Common options:
37
+
38
+ - `--bind` / `-b` - Address to bind to (default: `127.0.0.1:8000`)
39
+ - `--workers` / `-w` - Number of worker processes (default: 1, or `$WEB_CONCURRENCY` env var)
40
+ - `--threads` - Number of threads per worker (default: 1)
41
+ - `--timeout` / `-t` - Worker timeout in seconds (default: 30)
42
+ - `--reload` - Enable auto-reload on code changes (default: False)
43
+ - `--reload-extra-file` - Additional files to watch for reloading (can be used multiple times)
44
+ - `--certfile` - Path to SSL certificate file
45
+ - `--keyfile` - Path to SSL key file
46
+ - `--log-level` - Logging level: debug, info, warning, error, critical (default: info)
47
+ - `--access-log` - Access log file path (default: `-` for stdout)
48
+ - `--error-log` - Error log file path (default: `-` for stderr)
49
+ - `--log-format` - Log format string for error logs
50
+ - `--access-log-format` - Access log format string for HTTP request details
51
+ - `--max-requests` - Max requests before worker restart (default: 0, disabled)
52
+ - `--pidfile` - PID file path
53
+
54
+ ### Environment Variables
55
+
56
+ - `WEB_CONCURRENCY` - Sets the number of worker processes (commonly used by Heroku and other PaaS providers)
57
+ - `SENDFILE` - Enable/disable use of sendfile() syscall (set to `1`, `yes`, `true`, or `y` to enable)
58
+ - `FORWARDED_ALLOW_IPS` - Comma-separated list of trusted proxy IPs for secure headers (default: `127.0.0.1,::1`)
59
+
60
+ For a complete list of options, run `plain server --help`.
61
+
62
+ ## WSGI Ejection Point
63
+
64
+ While Plain includes this built-in server, you can still use any WSGI-compatible server you prefer. Plain's `wsgi.py` module provides a standard WSGI application interface:
65
+
66
+ ```bash
67
+ # Using uvicorn
68
+ uvicorn plain.wsgi:app --port 8000
69
+
70
+ # Using waitress
71
+ waitress-serve --port=8000 plain.wsgi:app
72
+
73
+ # Using gunicorn as an alternative
74
+ gunicorn plain.wsgi:app --workers 4
75
+ ```
@@ -0,0 +1,9 @@
1
+ #
2
+ # This file is part of gunicorn released under the MIT license.
3
+ # See the LICENSE for more information.
4
+ #
5
+ # Vendored and modified for Plain.
6
+
7
+ from .app import ServerApplication
8
+
9
+ __all__ = ["ServerApplication"]
@@ -0,0 +1,52 @@
1
+ #
2
+ #
3
+ # This file is part of gunicorn released under the MIT license.
4
+ # See the LICENSE for more information.
5
+ #
6
+ # Vendored and modified for Plain.
7
+
8
+ from __future__ import annotations
9
+
10
+ import sys
11
+ from typing import TYPE_CHECKING, Any
12
+
13
+ from .arbiter import Arbiter
14
+
15
+ if TYPE_CHECKING:
16
+ from .config import Config
17
+
18
+
19
+ class ServerApplication:
20
+ """
21
+ Plain's server application.
22
+
23
+ This class provides the interface for running the WSGI server.
24
+ """
25
+
26
+ def __init__(self, cfg: Config) -> None:
27
+ self.cfg: Config = cfg
28
+ self.callable: Any = None
29
+
30
+ def load(self) -> Any:
31
+ """Load the WSGI application."""
32
+ # Import locally to avoid circular dependencies and allow
33
+ # the WSGI module to handle Plain runtime setup
34
+ from plain.wsgi import app
35
+
36
+ return app
37
+
38
+ def wsgi(self) -> Any:
39
+ """Get the WSGI application."""
40
+ if self.callable is None:
41
+ self.callable = self.load()
42
+ return self.callable
43
+
44
+ def run(self) -> None:
45
+ """Run the server."""
46
+
47
+ try:
48
+ Arbiter(self).run()
49
+ except RuntimeError as e:
50
+ print(f"\nError: {e}\n", file=sys.stderr)
51
+ sys.stderr.flush()
52
+ sys.exit(1)