plain 0.86.2__tar.gz → 0.97.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of plain might be problematic. Click here for more details.
- {plain-0.86.2 → plain-0.97.0}/.gitignore +2 -0
- {plain-0.86.2 → plain-0.97.0}/PKG-INFO +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/CHANGELOG.md +194 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/views.py +25 -17
- {plain-0.86.2 → plain-0.97.0}/plain/cli/build.py +4 -5
- {plain-0.86.2 → plain-0.97.0}/plain/cli/core.py +8 -6
- plain-0.97.0/plain/cli/docs.py +79 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/formatting.py +4 -2
- plain-0.97.0/plain/cli/install.py +38 -0
- {plain-0.86.2/plain/cli/agent → plain-0.97.0/plain/cli}/llmdocs.py +3 -1
- {plain-0.86.2 → plain-0.97.0}/plain/cli/preflight.py +3 -2
- plain-0.97.0/plain/cli/print.py +9 -0
- {plain-0.86.2/plain/cli/agent → plain-0.97.0/plain/cli}/request.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/cli/runtime.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/cli/server.py +10 -0
- plain-0.97.0/plain/cli/settings.py +64 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/shell.py +1 -1
- plain-0.97.0/plain/cli/skills.py +153 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/startup.py +6 -5
- plain-0.97.0/plain/cli/upgrade.py +81 -0
- {plain-0.86.2 → plain-0.97.0}/plain/csrf/README.md +2 -0
- {plain-0.86.2 → plain-0.97.0}/plain/csrf/middleware.py +2 -3
- {plain-0.86.2 → plain-0.97.0}/plain/exceptions.py +2 -63
- plain-0.97.0/plain/forms/__init__.py +59 -0
- {plain-0.86.2 → plain-0.97.0}/plain/forms/boundfield.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/forms/fields.py +229 -73
- {plain-0.86.2 → plain-0.97.0}/plain/forms/forms.py +21 -9
- plain-0.97.0/plain/http/__init__.py +64 -0
- plain-0.97.0/plain/http/exceptions.py +65 -0
- {plain-0.86.2 → plain-0.97.0}/plain/http/multipartparser.py +35 -50
- {plain-0.86.2 → plain-0.97.0}/plain/http/request.py +213 -160
- {plain-0.86.2 → plain-0.97.0}/plain/http/response.py +12 -46
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/base.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/temp.py +2 -1
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/uploadedfile.py +10 -7
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/uploadhandler.py +23 -38
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/utils.py +12 -5
- {plain-0.86.2 → plain-0.97.0}/plain/internal/handlers/base.py +8 -5
- {plain-0.86.2 → plain-0.97.0}/plain/internal/handlers/exception.py +30 -25
- {plain-0.86.2 → plain-0.97.0}/plain/internal/handlers/wsgi.py +35 -39
- {plain-0.86.2 → plain-0.97.0}/plain/internal/middleware/hosts.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/internal/middleware/https.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/internal/middleware/slash.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/logs/app.py +21 -21
- {plain-0.86.2 → plain-0.97.0}/plain/logs/debug.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/logs/formatters.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/__init__.py +1 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/results.py +1 -1
- plain-0.97.0/plain/preflight/settings.py +54 -0
- {plain-0.86.2 → plain-0.97.0}/plain/runtime/__init__.py +2 -0
- {plain-0.86.2 → plain-0.97.0}/plain/runtime/global_settings.py +22 -12
- plain-0.97.0/plain/runtime/secret.py +20 -0
- {plain-0.86.2 → plain-0.97.0}/plain/runtime/user_settings.py +77 -16
- {plain-0.86.2 → plain-0.97.0}/plain/server/arbiter.py +9 -9
- {plain-0.86.2 → plain-0.97.0}/plain/server/glogging.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/body.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/errors.py +7 -2
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/message.py +3 -2
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/parser.py +3 -2
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/wsgi.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/server/pidfile.py +2 -1
- {plain-0.86.2 → plain-0.97.0}/plain/server/sock.py +22 -1
- {plain-0.86.2 → plain-0.97.0}/plain/server/util.py +5 -1
- {plain-0.86.2 → plain-0.97.0}/plain/server/workers/base.py +13 -11
- {plain-0.86.2 → plain-0.97.0}/plain/server/workers/sync.py +5 -3
- {plain-0.86.2 → plain-0.97.0}/plain/server/workers/thread.py +14 -8
- {plain-0.86.2 → plain-0.97.0}/plain/signing.py +82 -5
- plain-0.97.0/plain/skills/plain-docs/SKILL.md +25 -0
- plain-0.97.0/plain/skills/plain-install/SKILL.md +26 -0
- plain-0.97.0/plain/skills/plain-principles/SKILL.md +27 -0
- plain-0.97.0/plain/skills/plain-request/SKILL.md +32 -0
- plain-0.97.0/plain/skills/plain-shell/SKILL.md +24 -0
- plain-0.97.0/plain/skills/plain-upgrade/SKILL.md +35 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/jinja/extensions.py +6 -5
- {plain-0.86.2 → plain-0.97.0}/plain/test/client.py +14 -12
- {plain-0.86.2 → plain-0.97.0}/plain/test/exceptions.py +3 -6
- plain-0.97.0/plain/urls/exceptions.py +9 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/patterns.py +4 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/resolvers.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/utils/cache.py +5 -5
- {plain-0.86.2 → plain-0.97.0}/plain/utils/datastructures.py +11 -8
- {plain-0.86.2 → plain-0.97.0}/plain/utils/dateparse.py +3 -3
- {plain-0.86.2 → plain-0.97.0}/plain/utils/deconstruct.py +3 -3
- {plain-0.86.2 → plain-0.97.0}/plain/utils/functional.py +4 -2
- {plain-0.86.2 → plain-0.97.0}/plain/utils/html.py +3 -3
- {plain-0.86.2 → plain-0.97.0}/plain/utils/http.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/utils/inspect.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/utils/regex_helper.py +2 -2
- {plain-0.86.2 → plain-0.97.0}/plain/utils/safestring.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/utils/text.py +2 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/timezone.py +5 -5
- {plain-0.86.2 → plain-0.97.0}/plain/utils/tree.py +8 -5
- {plain-0.86.2 → plain-0.97.0}/plain/validators.py +27 -12
- {plain-0.86.2 → plain-0.97.0}/plain/views/__init__.py +0 -1
- {plain-0.86.2 → plain-0.97.0}/plain/views/base.py +12 -12
- {plain-0.86.2 → plain-0.97.0}/plain/views/errors.py +1 -1
- {plain-0.86.2 → plain-0.97.0}/plain/views/forms.py +6 -6
- {plain-0.86.2 → plain-0.97.0}/plain/views/objects.py +36 -43
- {plain-0.86.2 → plain-0.97.0}/plain/views/redirect.py +11 -12
- {plain-0.86.2 → plain-0.97.0}/plain/views/templates.py +4 -2
- {plain-0.86.2 → plain-0.97.0}/pyproject.toml +1 -1
- {plain-0.86.2 → plain-0.97.0}/tests/test_csrf.py +6 -5
- {plain-0.86.2 → plain-0.97.0}/tests/test_logs.py +10 -5
- plain-0.86.2/plain/AGENTS.md +0 -18
- plain-0.86.2/plain/cli/agent/__init__.py +0 -17
- plain-0.86.2/plain/cli/agent/docs.py +0 -81
- plain-0.86.2/plain/cli/agent/md.py +0 -106
- plain-0.86.2/plain/cli/agent/prompt.py +0 -45
- plain-0.86.2/plain/cli/docs.py +0 -39
- plain-0.86.2/plain/cli/install.py +0 -82
- plain-0.86.2/plain/cli/print.py +0 -9
- plain-0.86.2/plain/cli/settings.py +0 -60
- plain-0.86.2/plain/cli/upgrade.py +0 -170
- plain-0.86.2/plain/forms/__init__.py +0 -8
- plain-0.86.2/plain/http/__init__.py +0 -51
- plain-0.86.2/plain/templates/AGENTS.md +0 -3
- plain-0.86.2/plain/urls/exceptions.py +0 -9
- {plain-0.86.2 → plain-0.97.0}/LICENSE +0 -0
- {plain-0.86.2 → plain-0.97.0}/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/__main__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/compile.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/finders.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/assets/urls.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/chores/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/chores/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/chores/core.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/chores/registry.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/changelog.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/chores.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/output.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/registry.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/scaffold.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/urls.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/cli/utils.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/debug.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/forms/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/forms/exceptions.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/http/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/http/cookie.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/http/middleware.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/locks.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/files/move.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/middleware/headers.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/internal/reloader.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/json.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/logs/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/logs/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/logs/configure.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/logs/filters.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/packages/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/packages/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/packages/config.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/packages/registry.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/paginator.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/checks.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/files.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/registry.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/security.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/preflight/urls.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/runtime/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/runtime/utils.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/LICENSE +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/app.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/config.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/errors.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/http/unreader.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/workers/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/server/workers/workertmp.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/signals/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/signals/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/core.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/test/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/test/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/test/encoding.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/converters.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/routers.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/urls/utils.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/crypto.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/decorators.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/duration.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/encoding.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/hashable.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/ipv6.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/itercompat.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/module_loading.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/utils/timesince.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/views/README.md +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/views/exceptions.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/plain/wsgi.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/.gitignore +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/app/.gitignore +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/app/settings.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/app/test/__init__.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/app/urls.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/conftest.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/test_cli.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/test_http_hosts.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/test_runtime.py +0 -0
- {plain-0.86.2 → plain-0.97.0}/tests/test_wsgi.py +0 -0
|
@@ -1,5 +1,199 @@
|
|
|
1
1
|
# plain changelog
|
|
2
2
|
|
|
3
|
+
## [0.97.0](https://github.com/dropseed/plain/releases/plain@0.97.0) (2026-01-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- HTTP exceptions (`NotFoundError404`, `ForbiddenError403`, `BadRequestError400`, and `SuspiciousOperationError400` variants) moved from `plain.exceptions` to `plain.http.exceptions` and are now exported from `plain.http` ([b61f909e29](https://github.com/dropseed/plain/commit/b61f909e29))
|
|
8
|
+
|
|
9
|
+
### Upgrade instructions
|
|
10
|
+
|
|
11
|
+
- Update imports of HTTP exceptions from `plain.exceptions` to `plain.http`:
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
# Before
|
|
15
|
+
from plain.exceptions import NotFoundError404, ForbiddenError403, BadRequestError400
|
|
16
|
+
|
|
17
|
+
# After
|
|
18
|
+
from plain.http import NotFoundError404, ForbiddenError403, BadRequestError400
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## [0.96.0](https://github.com/dropseed/plain/releases/plain@0.96.0) (2026-01-13)
|
|
22
|
+
|
|
23
|
+
### What's changed
|
|
24
|
+
|
|
25
|
+
- Response classes renamed for consistency: `ResponseRedirect` → `RedirectResponse`, `ResponseNotModified` → `NotModifiedResponse`, `ResponseNotAllowed` → `NotAllowedResponse` ([fad5bf28b0](https://github.com/dropseed/plain/commit/fad5bf28b0))
|
|
26
|
+
- Redundant response classes removed: `ResponseNotFound`, `ResponseForbidden`, `ResponseBadRequest`, `ResponseGone`, `ResponseServerError` - use `Response(status_code=X)` instead ([fad5bf28b0](https://github.com/dropseed/plain/commit/fad5bf28b0))
|
|
27
|
+
- HTTP exceptions renamed to include status code suffix: `Http404` → `NotFoundError404`, `PermissionDenied` → `ForbiddenError403`, `BadRequest` → `BadRequestError400`, `SuspiciousOperation` → `SuspiciousOperationError400` ([5a1f020f52](https://github.com/dropseed/plain/commit/5a1f020f52))
|
|
28
|
+
- Added `Secret[T]` type annotation for masking sensitive settings like `SECRET_KEY` in CLI output ([8713dc08b0](https://github.com/dropseed/plain/commit/8713dc08b0))
|
|
29
|
+
- Added `ENV_SETTINGS_PREFIXES` setting to configure which environment variable prefixes are checked for settings (defaults to `["PLAIN_"]`) ([8713dc08b0](https://github.com/dropseed/plain/commit/8713dc08b0))
|
|
30
|
+
- New `plain settings list` and `plain settings get` CLI commands for viewing settings with their sources ([8713dc08b0](https://github.com/dropseed/plain/commit/8713dc08b0))
|
|
31
|
+
- Added preflight check for unused environment variables matching configured prefixes ([8713dc08b0](https://github.com/dropseed/plain/commit/8713dc08b0))
|
|
32
|
+
- Renamed `request.meta` to `request.environ` for clarity ([786b95bef8](https://github.com/dropseed/plain/commit/786b95bef8))
|
|
33
|
+
- Added `request.query_string` and `request.content_length` properties ([786b95bef8](https://github.com/dropseed/plain/commit/786b95bef8), [76dfd477d2](https://github.com/dropseed/plain/commit/76dfd477d2))
|
|
34
|
+
- Renamed X-Forwarded settings: `USE_X_FORWARDED_HOST` → `HTTP_X_FORWARDED_HOST`, `USE_X_FORWARDED_PORT` → `HTTP_X_FORWARDED_PORT`, `USE_X_FORWARDED_FOR` → `HTTP_X_FORWARDED_FOR` ([22f241a55c](https://github.com/dropseed/plain/commit/22f241a55c))
|
|
35
|
+
- Changed `HTTPS_PROXY_HEADER` from a tuple to a string format (e.g., `"X-Forwarded-Proto: https"`) ([7ac2a431b6](https://github.com/dropseed/plain/commit/7ac2a431b6))
|
|
36
|
+
|
|
37
|
+
### Upgrade instructions
|
|
38
|
+
|
|
39
|
+
- Replace Response class imports and usages:
|
|
40
|
+
- `ResponseRedirect` → `RedirectResponse`
|
|
41
|
+
- `ResponseNotModified` → `NotModifiedResponse`
|
|
42
|
+
- `ResponseNotAllowed` → `NotAllowedResponse`
|
|
43
|
+
- `ResponseNotFound` → `Response(status_code=404)`
|
|
44
|
+
- `ResponseForbidden` → `Response(status_code=403)`
|
|
45
|
+
- `ResponseBadRequest` → `Response(status_code=400)`
|
|
46
|
+
- `ResponseGone` → `Response(status_code=410)`
|
|
47
|
+
- `ResponseServerError` → `Response(status_code=500)`
|
|
48
|
+
- Replace exception imports and usages:
|
|
49
|
+
- `Http404` → `NotFoundError404`
|
|
50
|
+
- `PermissionDenied` → `ForbiddenError403`
|
|
51
|
+
- `BadRequest` → `BadRequestError400`
|
|
52
|
+
- `SuspiciousOperation` → `SuspiciousOperationError400`
|
|
53
|
+
- `SuspiciousMultipartForm` → `SuspiciousMultipartFormError400`
|
|
54
|
+
- `SuspiciousFileOperation` → `SuspiciousFileOperationError400`
|
|
55
|
+
- `TooManyFieldsSent` → `TooManyFieldsSentError400`
|
|
56
|
+
- `TooManyFilesSent` → `TooManyFilesSentError400`
|
|
57
|
+
- `RequestDataTooBig` → `RequestDataTooBigError400`
|
|
58
|
+
- Replace `request.meta` with `request.environ`
|
|
59
|
+
- Rename X-Forwarded settings in your configuration:
|
|
60
|
+
- `USE_X_FORWARDED_HOST` → `HTTP_X_FORWARDED_HOST`
|
|
61
|
+
- `USE_X_FORWARDED_PORT` → `HTTP_X_FORWARDED_PORT`
|
|
62
|
+
- `USE_X_FORWARDED_FOR` → `HTTP_X_FORWARDED_FOR`
|
|
63
|
+
- Update `HTTPS_PROXY_HEADER` from tuple format to string format:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# Before
|
|
67
|
+
HTTPS_PROXY_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
|
|
68
|
+
|
|
69
|
+
# After
|
|
70
|
+
HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- Replace `plain setting <name>` command with `plain settings get <name>`
|
|
74
|
+
|
|
75
|
+
## [0.95.0](https://github.com/dropseed/plain/releases/plain@0.95.0) (2025-12-22)
|
|
76
|
+
|
|
77
|
+
### What's changed
|
|
78
|
+
|
|
79
|
+
- Improved thread worker server shutdown behavior with `cancel_futures=True` for faster and cleaner process termination ([72d0620](https://github.com/dropseed/plain/commit/72d0620094))
|
|
80
|
+
|
|
81
|
+
### Upgrade instructions
|
|
82
|
+
|
|
83
|
+
- No changes required
|
|
84
|
+
|
|
85
|
+
## [0.94.0](https://github.com/dropseed/plain/releases/plain@0.94.0) (2025-12-12)
|
|
86
|
+
|
|
87
|
+
### What's changed
|
|
88
|
+
|
|
89
|
+
- `FormFieldMissingError` exceptions are now automatically converted to HTTP 400 Bad Request responses with a warning log instead of causing a 500 error ([b38f6e5](https://github.com/dropseed/plain/commit/b38f6e50db))
|
|
90
|
+
|
|
91
|
+
### Upgrade instructions
|
|
92
|
+
|
|
93
|
+
- No changes required
|
|
94
|
+
|
|
95
|
+
## [0.93.1](https://github.com/dropseed/plain/releases/plain@0.93.1) (2025-12-09)
|
|
96
|
+
|
|
97
|
+
### What's changed
|
|
98
|
+
|
|
99
|
+
- Added type annotation for `request.unique_id` attribute to improve IDE support and type checking ([23af501](https://github.com/dropseed/plain/commit/23af501d09))
|
|
100
|
+
|
|
101
|
+
### Upgrade instructions
|
|
102
|
+
|
|
103
|
+
- No changes required
|
|
104
|
+
|
|
105
|
+
## [0.93.0](https://github.com/dropseed/plain/releases/plain@0.93.0) (2025-12-04)
|
|
106
|
+
|
|
107
|
+
### What's changed
|
|
108
|
+
|
|
109
|
+
- Improved type annotations across forms, HTTP handling, logging, and other core modules for better IDE support and type checking ([ac1eeb0](https://github.com/dropseed/plain/commit/ac1eeb0ea0))
|
|
110
|
+
- Internal refactor of `TimestampSigner` to use composition instead of inheritance from `Signer`, maintaining the same public API ([ac1eeb0](https://github.com/dropseed/plain/commit/ac1eeb0ea0))
|
|
111
|
+
|
|
112
|
+
### Upgrade instructions
|
|
113
|
+
|
|
114
|
+
- No changes required
|
|
115
|
+
|
|
116
|
+
## [0.92.0](https://github.com/dropseed/plain/releases/plain@0.92.0) (2025-12-01)
|
|
117
|
+
|
|
118
|
+
### What's changed
|
|
119
|
+
|
|
120
|
+
- Added `request.client_ip` property to get the client's IP address, with support for `X-Forwarded-For` header when behind a trusted proxy ([cb0bc5d](https://github.com/dropseed/plain/commit/cb0bc5d08f))
|
|
121
|
+
- Added `USE_X_FORWARDED_FOR` setting to enable reading client IP from `X-Forwarded-For` header ([cb0bc5d](https://github.com/dropseed/plain/commit/cb0bc5d08f))
|
|
122
|
+
- Improved `print_event` CLI output styling with dimmed text for less visual noise ([b09edfd](https://github.com/dropseed/plain/commit/b09edfd2a1))
|
|
123
|
+
|
|
124
|
+
### Upgrade instructions
|
|
125
|
+
|
|
126
|
+
- No changes required
|
|
127
|
+
|
|
128
|
+
## [0.91.0](https://github.com/dropseed/plain/releases/plain@0.91.0) (2025-11-24)
|
|
129
|
+
|
|
130
|
+
### What's changed
|
|
131
|
+
|
|
132
|
+
- Request body parsing refactored: the `request.data` attribute has been replaced with `request.json_data` and `request.form_data` for explicit content-type handling ([90332a9](https://github.com/dropseed/plain/commit/90332a9c21))
|
|
133
|
+
- `QueryDict` now has proper type annotations for `get()`, `pop()`, `getlist()`, and `__getitem__()` methods that reflect string return types ([90332a9](https://github.com/dropseed/plain/commit/90332a9c21))
|
|
134
|
+
- Forms now automatically select between `json_data` and `form_data` based on request content-type ([90332a9](https://github.com/dropseed/plain/commit/90332a9c21))
|
|
135
|
+
- View mixins `ObjectTemplateViewMixin` removed in favor of class inheritance for better typing - `UpdateView` and `DeleteView` now inherit from `DetailView` ([569afd6](https://github.com/dropseed/plain/commit/569afd606d))
|
|
136
|
+
- `AppLogger` context logging now uses a `context` dict parameter instead of `**kwargs` for better type checking ([581b406](https://github.com/dropseed/plain/commit/581b4060d3))
|
|
137
|
+
- Removed erroneous `AuthViewMixin` export from `plain.views` ([334bbb6](https://github.com/dropseed/plain/commit/334bbb6e7a))
|
|
138
|
+
|
|
139
|
+
### Upgrade instructions
|
|
140
|
+
|
|
141
|
+
- Replace `request.data` with the appropriate method:
|
|
142
|
+
- For JSON requests: use `request.json_data` (returns a dict, raises `BadRequest` for invalid JSON)
|
|
143
|
+
- For form data: use `request.form_data` (returns a `QueryDict`)
|
|
144
|
+
- Update `app_logger` calls that pass context as kwargs to use the `context` parameter:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# Before
|
|
148
|
+
app_logger.info("Message", user_id=123, action="login")
|
|
149
|
+
|
|
150
|
+
# After
|
|
151
|
+
app_logger.info("Message", context={"user_id": 123, "action": "login"})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## [0.90.0](https://github.com/dropseed/plain/releases/plain@0.90.0) (2025-11-20)
|
|
155
|
+
|
|
156
|
+
### What's changed
|
|
157
|
+
|
|
158
|
+
- Improved type annotations in `timezone.py`: `is_aware()` and `is_naive()` now accept both `datetime` and `time` objects for more flexible type checking ([a43145e](https://github.com/dropseed/plain/commit/a43145e697))
|
|
159
|
+
- Enhanced type annotations in view classes: `convert_value_to_response()` and handler result variables now use more explicit type hints for better IDE support ([dc4454e](https://github.com/dropseed/plain/commit/dc4454e196))
|
|
160
|
+
- Fixed type errors in forms and server workers: URL field now handles bytes properly, and worker wait_fds has explicit type annotation ([fc98d66](https://github.com/dropseed/plain/commit/fc98d666d4))
|
|
161
|
+
|
|
162
|
+
### Upgrade instructions
|
|
163
|
+
|
|
164
|
+
- No changes required
|
|
165
|
+
|
|
166
|
+
## [0.89.0](https://github.com/dropseed/plain/releases/plain@0.89.0) (2025-11-14)
|
|
167
|
+
|
|
168
|
+
### What's changed
|
|
169
|
+
|
|
170
|
+
- Improved type annotations in view classes: `url_args`, `url_kwargs`, and various template/form context dictionaries now have more specific type hints for better IDE support and type checking ([83bcb95](https://github.com/dropseed/plain/commit/83bcb95b09))
|
|
171
|
+
|
|
172
|
+
### Upgrade instructions
|
|
173
|
+
|
|
174
|
+
- No changes required
|
|
175
|
+
|
|
176
|
+
## [0.88.0](https://github.com/dropseed/plain/releases/plain@0.88.0) (2025-11-13)
|
|
177
|
+
|
|
178
|
+
### What's changed
|
|
179
|
+
|
|
180
|
+
- The `plain.forms` module now uses explicit imports instead of wildcard imports, improving IDE autocomplete and type checking support ([eff36f3](https://github.com/dropseed/plain/commit/eff36f31e8e15f84e11164a44c833aeab096ffbd))
|
|
181
|
+
|
|
182
|
+
### Upgrade instructions
|
|
183
|
+
|
|
184
|
+
- No changes required
|
|
185
|
+
|
|
186
|
+
## [0.87.0](https://github.com/dropseed/plain/releases/plain@0.87.0) (2025-11-12)
|
|
187
|
+
|
|
188
|
+
### What's changed
|
|
189
|
+
|
|
190
|
+
- Internal classes now use abstract base classes with `@abstractmethod` decorators instead of raising `NotImplementedError`, improving type checking and IDE support ([91b329a](https://github.com/dropseed/plain/commit/91b329a8adb477031c4358e638b12f35f19bb85d), [81b5f88](https://github.com/dropseed/plain/commit/81b5f88a4bd39785f6b19c3c00c0ed23a36fb72f), [d2e2423](https://github.com/dropseed/plain/commit/d2e24235f497a92f45d5a21fc83d802897c2dec0), [61e7b5a](https://github.com/dropseed/plain/commit/61e7b5a0c8675aaaf65f0a626ff7959a786dca7f))
|
|
191
|
+
- Updated to latest version of `ty` type checker and fixed type errors and warnings throughout the codebase ([f4dbcef](https://github.com/dropseed/plain/commit/f4dbcefa929058be517cb1d4ab35bd73a89f26b8))
|
|
192
|
+
|
|
193
|
+
### Upgrade instructions
|
|
194
|
+
|
|
195
|
+
- No changes required
|
|
196
|
+
|
|
3
197
|
## [0.86.2](https://github.com/dropseed/plain/releases/plain@0.86.2) (2025-11-11)
|
|
4
198
|
|
|
5
199
|
### What's changed
|
|
@@ -8,12 +8,13 @@ from io import BytesIO
|
|
|
8
8
|
|
|
9
9
|
from plain.http import (
|
|
10
10
|
FileResponse,
|
|
11
|
-
|
|
11
|
+
NotFoundError404,
|
|
12
|
+
NotModifiedResponse,
|
|
13
|
+
RedirectResponse,
|
|
12
14
|
Response,
|
|
13
|
-
ResponseNotModified,
|
|
14
|
-
ResponseRedirect,
|
|
15
15
|
StreamingResponse,
|
|
16
16
|
)
|
|
17
|
+
from plain.http.response import ResponseHeaders
|
|
17
18
|
from plain.runtime import settings
|
|
18
19
|
from plain.urls import reverse
|
|
19
20
|
from plain.views import View
|
|
@@ -34,12 +35,15 @@ class AssetView(View):
|
|
|
34
35
|
# Allow a path to be passed in AssetView.as_view(path="...")
|
|
35
36
|
self.asset_path = asset_path
|
|
36
37
|
|
|
37
|
-
def get_url_path(self) -> str:
|
|
38
|
+
def get_url_path(self) -> str | None:
|
|
38
39
|
return self.asset_path or self.url_kwargs["path"]
|
|
39
40
|
|
|
40
|
-
def get(self) -> Response | FileResponse:
|
|
41
|
+
def get(self) -> Response | FileResponse | StreamingResponse:
|
|
41
42
|
url_path = self.get_url_path()
|
|
42
43
|
|
|
44
|
+
if not url_path:
|
|
45
|
+
raise NotFoundError404("Asset path not found")
|
|
46
|
+
|
|
43
47
|
# Make a trailing slash work, but we don't expect it
|
|
44
48
|
url_path = url_path.rstrip("/")
|
|
45
49
|
|
|
@@ -52,7 +56,11 @@ class AssetView(View):
|
|
|
52
56
|
if redirect_response := self.get_redirect_response(url_path):
|
|
53
57
|
return redirect_response
|
|
54
58
|
|
|
59
|
+
# check_asset_path validates and raises if path is invalid
|
|
60
|
+
# After this point, absolute_path is guaranteed to be a valid str
|
|
55
61
|
self.check_asset_path(absolute_path)
|
|
62
|
+
# Type guard: absolute_path is now str (check_asset_path raises if None/invalid)
|
|
63
|
+
assert absolute_path is not None
|
|
56
64
|
|
|
57
65
|
if encoded_path := self.get_encoded_path(absolute_path):
|
|
58
66
|
absolute_path = encoded_path
|
|
@@ -80,7 +88,7 @@ class AssetView(View):
|
|
|
80
88
|
|
|
81
89
|
# Make sure we don't try to escape the compiled assests path
|
|
82
90
|
if not os.path.commonpath([compiled_path, asset_path]) == compiled_path:
|
|
83
|
-
raise
|
|
91
|
+
raise NotFoundError404("Asset not found")
|
|
84
92
|
|
|
85
93
|
return asset_path
|
|
86
94
|
|
|
@@ -93,13 +101,13 @@ class AssetView(View):
|
|
|
93
101
|
|
|
94
102
|
def check_asset_path(self, path: str | None) -> None:
|
|
95
103
|
if not path:
|
|
96
|
-
raise
|
|
104
|
+
raise NotFoundError404("Asset not found")
|
|
97
105
|
|
|
98
106
|
if not os.path.exists(path):
|
|
99
|
-
raise
|
|
107
|
+
raise NotFoundError404("Asset not found")
|
|
100
108
|
|
|
101
109
|
if os.path.isdir(path):
|
|
102
|
-
raise
|
|
110
|
+
raise NotFoundError404("Asset is a directory")
|
|
103
111
|
|
|
104
112
|
@functools.cache
|
|
105
113
|
def get_last_modified(self, path: str) -> str | None:
|
|
@@ -117,7 +125,7 @@ class AssetView(View):
|
|
|
117
125
|
try:
|
|
118
126
|
mtime = os.path.getmtime(path)
|
|
119
127
|
except OSError:
|
|
120
|
-
mtime =
|
|
128
|
+
mtime = 0.0
|
|
121
129
|
|
|
122
130
|
timestamp = int(mtime)
|
|
123
131
|
size = self.get_size(path)
|
|
@@ -127,7 +135,7 @@ class AssetView(View):
|
|
|
127
135
|
def get_size(self, path: str) -> int:
|
|
128
136
|
return os.path.getsize(path)
|
|
129
137
|
|
|
130
|
-
def update_headers(self, headers:
|
|
138
|
+
def update_headers(self, headers: ResponseHeaders, path: str) -> ResponseHeaders:
|
|
131
139
|
headers.setdefault("Access-Control-Allow-Origin", "*")
|
|
132
140
|
|
|
133
141
|
# Always vary on Accept-Encoding
|
|
@@ -206,7 +214,7 @@ class AssetView(View):
|
|
|
206
214
|
return gzip_path
|
|
207
215
|
return None
|
|
208
216
|
|
|
209
|
-
def get_redirect_response(self, path: str) ->
|
|
217
|
+
def get_redirect_response(self, path: str) -> RedirectResponse | None:
|
|
210
218
|
"""If the asset is not found, try to redirect to the fingerprinted path"""
|
|
211
219
|
fingerprinted_url_path = get_fingerprinted_url_path(path)
|
|
212
220
|
|
|
@@ -219,19 +227,19 @@ class AssetView(View):
|
|
|
219
227
|
|
|
220
228
|
namespace = AssetsRouter.namespace
|
|
221
229
|
|
|
222
|
-
return
|
|
230
|
+
return RedirectResponse(
|
|
223
231
|
redirect_to=reverse(f"{namespace}:asset", fingerprinted_url_path),
|
|
224
232
|
headers={
|
|
225
233
|
"Cache-Control": "max-age=60", # Can cache this for a short time, but the fingerprinted path can change
|
|
226
234
|
},
|
|
227
235
|
)
|
|
228
236
|
|
|
229
|
-
def get_conditional_response(self, path: str) ->
|
|
237
|
+
def get_conditional_response(self, path: str) -> NotModifiedResponse | None:
|
|
230
238
|
"""
|
|
231
239
|
Support conditional requests (HTTP 304 response) based on ETag and Last-Modified headers.
|
|
232
240
|
"""
|
|
233
241
|
if self.request.headers.get("If-None-Match") == self.get_etag(path):
|
|
234
|
-
response =
|
|
242
|
+
response = NotModifiedResponse()
|
|
235
243
|
response.headers = self.update_headers(response.headers, path)
|
|
236
244
|
return response
|
|
237
245
|
|
|
@@ -243,7 +251,7 @@ class AssetView(View):
|
|
|
243
251
|
and last_modified
|
|
244
252
|
and if_modified_since >= last_modified
|
|
245
253
|
):
|
|
246
|
-
response =
|
|
254
|
+
response = NotModifiedResponse()
|
|
247
255
|
response.headers = self.update_headers(response.headers, path)
|
|
248
256
|
return response
|
|
249
257
|
return None
|
|
@@ -272,7 +280,7 @@ class AssetView(View):
|
|
|
272
280
|
status_code=416, headers=[("Content-Range", f"bytes */{file_size}")]
|
|
273
281
|
)
|
|
274
282
|
|
|
275
|
-
end = min(end, file_size - 1)
|
|
283
|
+
end = int(min(end, file_size - 1))
|
|
276
284
|
|
|
277
285
|
with open(path, "rb") as f:
|
|
278
286
|
f.seek(start)
|
|
@@ -9,6 +9,7 @@ import click
|
|
|
9
9
|
|
|
10
10
|
import plain.runtime
|
|
11
11
|
from plain.assets.compile import compile_assets, get_compiled_path
|
|
12
|
+
from plain.cli.print import print_event
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@click.command()
|
|
@@ -54,18 +55,16 @@ def build(keep_original: bool, fingerprint: bool, compress: bool) -> None:
|
|
|
54
55
|
.get("run", {})
|
|
55
56
|
.items()
|
|
56
57
|
):
|
|
57
|
-
|
|
58
|
+
print_event(f"{name}...")
|
|
58
59
|
result = subprocess.run(data["cmd"], shell=True)
|
|
59
|
-
print()
|
|
60
60
|
if result.returncode:
|
|
61
61
|
click.secho(f"Error in {name} (exit {result.returncode})", fg="red")
|
|
62
62
|
sys.exit(result.returncode)
|
|
63
63
|
|
|
64
64
|
# Then run installed package build steps (like tailwind, typically should run last...)
|
|
65
65
|
for entry_point in entry_points(group="plain.build"):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
print()
|
|
66
|
+
print_event(f"{entry_point.name}...")
|
|
67
|
+
entry_point.load()()
|
|
69
68
|
|
|
70
69
|
# Compile our assets
|
|
71
70
|
target_dir = get_compiled_path()
|
|
@@ -9,7 +9,6 @@ from click.core import Command, Context
|
|
|
9
9
|
import plain.runtime
|
|
10
10
|
from plain.exceptions import ImproperlyConfigured
|
|
11
11
|
|
|
12
|
-
from .agent import agent
|
|
13
12
|
from .build import build
|
|
14
13
|
from .changelog import changelog
|
|
15
14
|
from .chores import chores
|
|
@@ -18,10 +17,12 @@ from .formatting import PlainContext
|
|
|
18
17
|
from .install import install
|
|
19
18
|
from .preflight import preflight_cli
|
|
20
19
|
from .registry import cli_registry
|
|
20
|
+
from .request import request
|
|
21
21
|
from .scaffold import create
|
|
22
22
|
from .server import server
|
|
23
|
-
from .settings import
|
|
23
|
+
from .settings import settings
|
|
24
24
|
from .shell import run, shell
|
|
25
|
+
from .skills import skills
|
|
25
26
|
from .upgrade import upgrade
|
|
26
27
|
from .urls import urls
|
|
27
28
|
from .utils import utils
|
|
@@ -32,8 +33,9 @@ def plain_cli() -> None:
|
|
|
32
33
|
pass
|
|
33
34
|
|
|
34
35
|
|
|
35
|
-
plain_cli.add_command(agent)
|
|
36
36
|
plain_cli.add_command(docs)
|
|
37
|
+
plain_cli.add_command(request)
|
|
38
|
+
plain_cli.add_command(skills)
|
|
37
39
|
plain_cli.add_command(preflight_cli)
|
|
38
40
|
plain_cli.add_command(create)
|
|
39
41
|
plain_cli.add_command(chores)
|
|
@@ -41,7 +43,7 @@ plain_cli.add_command(build)
|
|
|
41
43
|
plain_cli.add_command(utils)
|
|
42
44
|
plain_cli.add_command(urls)
|
|
43
45
|
plain_cli.add_command(changelog)
|
|
44
|
-
plain_cli.add_command(
|
|
46
|
+
plain_cli.add_command(settings)
|
|
45
47
|
plain_cli.add_command(shell)
|
|
46
48
|
plain_cli.add_command(run)
|
|
47
49
|
plain_cli.add_command(install)
|
|
@@ -61,9 +63,9 @@ class CLIRegistryGroup(click.Group):
|
|
|
61
63
|
def list_commands(self, ctx: Context) -> list[str]:
|
|
62
64
|
return sorted(cli_registry.get_commands().keys())
|
|
63
65
|
|
|
64
|
-
def get_command(self, ctx: Context,
|
|
66
|
+
def get_command(self, ctx: Context, cmd_name: str) -> Command | None:
|
|
65
67
|
commands = cli_registry.get_commands()
|
|
66
|
-
return commands.get(
|
|
68
|
+
return commands.get(cmd_name)
|
|
67
69
|
|
|
68
70
|
|
|
69
71
|
class PlainCommandCollection(click.CommandCollection):
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import pkgutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .llmdocs import LLMDocs
|
|
8
|
+
from .output import iterate_markdown
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@click.command()
|
|
12
|
+
@click.option("--open", is_flag=True, help="Open the README in your default editor")
|
|
13
|
+
@click.option("--source", is_flag=True, help="Include symbolicated source code")
|
|
14
|
+
@click.option("--list", "show_list", is_flag=True, help="List available packages")
|
|
15
|
+
@click.argument("module", default="")
|
|
16
|
+
def docs(module: str, open: bool, source: bool, show_list: bool) -> None:
|
|
17
|
+
"""Show documentation for a package"""
|
|
18
|
+
if show_list:
|
|
19
|
+
# List available packages
|
|
20
|
+
available_packages = []
|
|
21
|
+
try:
|
|
22
|
+
import plain
|
|
23
|
+
|
|
24
|
+
# Check core plain package (namespace package)
|
|
25
|
+
plain_spec = importlib.util.find_spec("plain")
|
|
26
|
+
if plain_spec and plain_spec.submodule_search_locations:
|
|
27
|
+
available_packages.append("plain")
|
|
28
|
+
|
|
29
|
+
# Check other plain.* subpackages
|
|
30
|
+
if hasattr(plain, "__path__"):
|
|
31
|
+
for importer, modname, ispkg in pkgutil.iter_modules(
|
|
32
|
+
plain.__path__, "plain."
|
|
33
|
+
):
|
|
34
|
+
if ispkg:
|
|
35
|
+
available_packages.append(modname)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
if available_packages:
|
|
40
|
+
for pkg in sorted(available_packages):
|
|
41
|
+
click.echo(f"- {pkg}")
|
|
42
|
+
else:
|
|
43
|
+
click.echo("No packages found.")
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if not module:
|
|
47
|
+
raise click.UsageError(
|
|
48
|
+
"You must specify a module. Use --list to see available packages."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Convert hyphens to dots (e.g., plain-models -> plain.models)
|
|
52
|
+
module = module.replace("-", ".")
|
|
53
|
+
|
|
54
|
+
# Automatically prefix if we need to
|
|
55
|
+
if not module.startswith("plain"):
|
|
56
|
+
module = f"plain.{module}"
|
|
57
|
+
|
|
58
|
+
# Get the module path
|
|
59
|
+
spec = importlib.util.find_spec(module)
|
|
60
|
+
if not spec or not spec.origin:
|
|
61
|
+
raise click.UsageError(f"Module {module} not found")
|
|
62
|
+
|
|
63
|
+
module_path = Path(spec.origin).parent
|
|
64
|
+
|
|
65
|
+
if source:
|
|
66
|
+
# Output with symbolicated source
|
|
67
|
+
source_docs = LLMDocs([module_path])
|
|
68
|
+
source_docs.load()
|
|
69
|
+
source_docs.print(relative_to=module_path.parent)
|
|
70
|
+
else:
|
|
71
|
+
# Human-readable README output
|
|
72
|
+
readme_path = module_path / "README.md"
|
|
73
|
+
if not readme_path.exists():
|
|
74
|
+
raise click.UsageError(f"README.md not found for {module}")
|
|
75
|
+
|
|
76
|
+
if open:
|
|
77
|
+
click.launch(str(readme_path))
|
|
78
|
+
else:
|
|
79
|
+
click.echo_via_pager(iterate_markdown(readme_path.read_text()))
|
|
@@ -12,11 +12,13 @@ class PlainHelpFormatter(click.HelpFormatter):
|
|
|
12
12
|
styled_heading = click.style(heading, dim=True)
|
|
13
13
|
self.write(f"{'':>{self.current_indent}}{styled_heading}\n")
|
|
14
14
|
|
|
15
|
-
def write_usage(
|
|
15
|
+
def write_usage( # type: ignore[override]
|
|
16
|
+
self, prog: str, args: str = "", prefix: str = "Usage: "
|
|
17
|
+
) -> None:
|
|
16
18
|
prefix_styled = click.style(prefix, dim=True)
|
|
17
19
|
super().write_usage(prog, args, prefix=prefix_styled)
|
|
18
20
|
|
|
19
|
-
def write_dl(
|
|
21
|
+
def write_dl( # type: ignore[override]
|
|
20
22
|
self,
|
|
21
23
|
rows: list[tuple[str, str]],
|
|
22
24
|
col_max: int = 20,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@click.command()
|
|
8
|
+
@click.argument("packages", nargs=-1, required=True)
|
|
9
|
+
def install(packages: tuple[str, ...]) -> None:
|
|
10
|
+
"""Install Plain packages"""
|
|
11
|
+
# Validate all package names
|
|
12
|
+
invalid_packages = [pkg for pkg in packages if not pkg.startswith("plain")]
|
|
13
|
+
if invalid_packages:
|
|
14
|
+
raise click.UsageError(
|
|
15
|
+
f"The following packages do not start with 'plain': {', '.join(invalid_packages)}\n"
|
|
16
|
+
"This command is only for Plain framework packages."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Install all packages
|
|
20
|
+
if len(packages) == 1:
|
|
21
|
+
click.secho(f"Installing {packages[0]}...", bold=True)
|
|
22
|
+
else:
|
|
23
|
+
click.secho(f"Installing {len(packages)} packages...", bold=True)
|
|
24
|
+
for pkg in packages:
|
|
25
|
+
click.secho(f" - {pkg}")
|
|
26
|
+
click.echo()
|
|
27
|
+
|
|
28
|
+
install_cmd = ["uv", "add"] + list(packages)
|
|
29
|
+
result = subprocess.run(install_cmd, check=False, stderr=sys.stderr)
|
|
30
|
+
|
|
31
|
+
if result.returncode != 0:
|
|
32
|
+
raise click.ClickException("Failed to install packages")
|
|
33
|
+
|
|
34
|
+
click.echo()
|
|
35
|
+
if len(packages) == 1:
|
|
36
|
+
click.secho(f"{packages[0]} installed successfully", fg="green")
|
|
37
|
+
else:
|
|
38
|
+
click.secho(f"{len(packages)} packages installed successfully", fg="green")
|
|
@@ -26,7 +26,7 @@ class LLMDocs:
|
|
|
26
26
|
self.docs.add(path)
|
|
27
27
|
|
|
28
28
|
# Exclude "migrations" code from plain apps, except for plain/models/migrations
|
|
29
|
-
# Also exclude CHANGELOG.md
|
|
29
|
+
# Also exclude CHANGELOG.md, AGENTS.md, and skills directory
|
|
30
30
|
self.docs = {
|
|
31
31
|
doc
|
|
32
32
|
for doc in self.docs
|
|
@@ -35,6 +35,7 @@ class LLMDocs:
|
|
|
35
35
|
and "/plain/models/migrations/" not in str(doc)
|
|
36
36
|
)
|
|
37
37
|
and doc.name not in ("CHANGELOG.md", "AGENTS.md")
|
|
38
|
+
and "/skills/" not in str(doc)
|
|
38
39
|
}
|
|
39
40
|
self.sources = {
|
|
40
41
|
source
|
|
@@ -44,6 +45,7 @@ class LLMDocs:
|
|
|
44
45
|
and "/plain/models/migrations/" not in str(source)
|
|
45
46
|
)
|
|
46
47
|
and source.name != "cli.py"
|
|
48
|
+
and "/skills/" not in str(source)
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
self.docs = sorted(self.docs)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sys
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
|
|
@@ -125,12 +126,12 @@ def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
|
|
|
125
126
|
|
|
126
127
|
if format == "json":
|
|
127
128
|
# Build JSON output
|
|
128
|
-
results = {"passed": not has_errors, "checks": []}
|
|
129
|
+
results: dict[str, Any] = {"passed": not has_errors, "checks": []}
|
|
129
130
|
|
|
130
131
|
for check_class, check_name, issues in check_results:
|
|
131
132
|
visible_issues = [issue for issue in issues if not issue.is_silenced()]
|
|
132
133
|
|
|
133
|
-
check_result = {
|
|
134
|
+
check_result: dict[str, Any] = {
|
|
134
135
|
"name": check_name,
|
|
135
136
|
"passed": len(visible_issues) == 0,
|
|
136
137
|
"issues": [],
|
|
@@ -49,7 +49,7 @@ def request(
|
|
|
49
49
|
content_type: str | None,
|
|
50
50
|
headers: tuple[str, ...],
|
|
51
51
|
) -> None:
|
|
52
|
-
"""Make
|
|
52
|
+
"""Make HTTP requests against the dev database"""
|
|
53
53
|
|
|
54
54
|
try:
|
|
55
55
|
# Only allow in DEBUG mode for security
|
|
@@ -98,7 +98,7 @@ def request(
|
|
|
98
98
|
|
|
99
99
|
# Make the request
|
|
100
100
|
method = method.upper()
|
|
101
|
-
kwargs: dict[str,
|
|
101
|
+
kwargs: dict[str, Any] = {
|
|
102
102
|
"follow": follow,
|
|
103
103
|
}
|
|
104
104
|
if header_dict:
|
|
@@ -24,7 +24,7 @@ def without_runtime_setup(f: F) -> F:
|
|
|
24
24
|
def server(**options):
|
|
25
25
|
...
|
|
26
26
|
"""
|
|
27
|
-
f.without_runtime_setup = True #
|
|
27
|
+
f.without_runtime_setup = True # dynamic attribute for decorator
|
|
28
28
|
return f
|
|
29
29
|
|
|
30
30
|
|
|
@@ -41,5 +41,5 @@ def common_command(f: F) -> F:
|
|
|
41
41
|
def dev(**options):
|
|
42
42
|
...
|
|
43
43
|
"""
|
|
44
|
-
f.is_common_command = True #
|
|
44
|
+
f.is_common_command = True # dynamic attribute for decorator
|
|
45
45
|
return f
|