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.
- {plain-0.74.0 → plain-0.76.0}/PKG-INFO +2 -2
- {plain-0.74.0 → plain-0.76.0}/plain/CHANGELOG.md +29 -0
- {plain-0.74.0 → plain-0.76.0}/plain/README.md +1 -1
- {plain-0.74.0 → plain-0.76.0}/plain/chores/README.md +1 -1
- {plain-0.74.0 → plain-0.76.0}/plain/cli/core.py +35 -17
- plain-0.76.0/plain/cli/runtime.py +28 -0
- plain-0.76.0/plain/cli/server.py +143 -0
- plain-0.76.0/plain/server/LICENSE +35 -0
- plain-0.76.0/plain/server/README.md +75 -0
- plain-0.76.0/plain/server/__init__.py +9 -0
- plain-0.76.0/plain/server/app.py +52 -0
- plain-0.76.0/plain/server/arbiter.py +555 -0
- plain-0.76.0/plain/server/config.py +118 -0
- plain-0.76.0/plain/server/errors.py +31 -0
- plain-0.76.0/plain/server/glogging.py +292 -0
- plain-0.76.0/plain/server/http/__init__.py +12 -0
- plain-0.76.0/plain/server/http/body.py +283 -0
- plain-0.76.0/plain/server/http/errors.py +150 -0
- plain-0.76.0/plain/server/http/message.py +399 -0
- plain-0.76.0/plain/server/http/parser.py +69 -0
- plain-0.76.0/plain/server/http/unreader.py +88 -0
- plain-0.76.0/plain/server/http/wsgi.py +421 -0
- plain-0.76.0/plain/server/pidfile.py +91 -0
- plain-0.76.0/plain/server/reloader.py +158 -0
- plain-0.76.0/plain/server/sock.py +219 -0
- plain-0.76.0/plain/server/util.py +380 -0
- plain-0.76.0/plain/server/workers/__init__.py +12 -0
- plain-0.76.0/plain/server/workers/base.py +305 -0
- plain-0.76.0/plain/server/workers/gthread.py +393 -0
- plain-0.76.0/plain/server/workers/sync.py +210 -0
- plain-0.76.0/plain/server/workers/workertmp.py +50 -0
- {plain-0.74.0 → plain-0.76.0}/pyproject.toml +1 -1
- {plain-0.74.0 → plain-0.76.0}/.gitignore +0 -0
- {plain-0.74.0 → plain-0.76.0}/LICENSE +0 -0
- {plain-0.74.0 → plain-0.76.0}/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/AGENTS.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/__main__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/compile.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/finders.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/urls.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/assets/views.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/chores/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/chores/registry.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/docs.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/llmdocs.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/md.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/prompt.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/agent/request.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/build.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/changelog.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/chores.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/docs.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/formatting.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/install.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/output.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/preflight.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/print.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/registry.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/scaffold.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/settings.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/shell.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/startup.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/upgrade.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/urls.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/cli/utils.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/csrf/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/csrf/middleware.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/csrf/views.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/debug.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/exceptions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/boundfield.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/exceptions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/fields.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/forms/forms.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/cookie.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/multipartparser.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/request.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/http/response.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/base.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/locks.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/move.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/temp.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/files/utils.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/base.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/exception.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/handlers/wsgi.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/headers.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/hosts.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/json.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/configure.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/debug.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/formatters.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/loggers.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/logs/utils.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/packages/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/packages/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/packages/config.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/packages/registry.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/paginator.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/checks.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/files.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/registry.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/results.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/security.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/preflight/urls.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/runtime/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/runtime/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/runtime/global_settings.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/runtime/utils.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signals/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signals/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/signing.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/AGENTS.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/core.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/test/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/test/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/test/client.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/test/encoding.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/test/exceptions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/converters.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/exceptions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/patterns.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/resolvers.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/routers.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/urls/utils.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/cache.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/crypto.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/datastructures.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/dateparse.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/decorators.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/duration.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/encoding.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/functional.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/hashable.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/html.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/http.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/inspect.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/ipv6.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/itercompat.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/module_loading.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/safestring.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/text.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/timesince.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/timezone.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/utils/tree.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/validators.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/README.md +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/base.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/errors.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/exceptions.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/forms.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/objects.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/redirect.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/views/templates.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/plain/wsgi.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/.gitignore +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/app/.gitignore +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/app/settings.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/app/test/__init__.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/app/urls.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/conftest.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/test_cli.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/test_csrf.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/test_http_hosts.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/test_logs.py +0 -0
- {plain-0.74.0 → plain-0.76.0}/tests/test_runtime.py +0 -0
- {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.
|
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.
|
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.
|
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.
|
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
|
-
|
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
|
-
|
77
|
-
|
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
|
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,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)
|