plain 0.60.0__tar.gz → 0.62.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.60.0 → plain-0.62.0}/PKG-INFO +2 -2
- plain-0.62.0/plain/AGENTS.md +18 -0
- {plain-0.60.0 → plain-0.62.0}/plain/CHANGELOG.md +26 -0
- plain-0.62.0/plain/cli/agent/__init__.py +20 -0
- plain-0.62.0/plain/cli/agent/docs.py +80 -0
- plain-0.60.0/plain/cli/docs.py → plain-0.62.0/plain/cli/agent/llmdocs.py +6 -79
- plain-0.62.0/plain/cli/agent/md.py +87 -0
- plain-0.60.0/plain/cli/agent.py → plain-0.62.0/plain/cli/agent/prompt.py +10 -15
- plain-0.62.0/plain/cli/agent/request.py +181 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/core.py +2 -2
- plain-0.62.0/plain/cli/docs.py +38 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/install.py +1 -1
- {plain-0.60.0 → plain-0.62.0}/plain/cli/shell.py +15 -1
- {plain-0.60.0 → plain-0.62.0}/plain/cli/upgrade.py +1 -1
- {plain-0.60.0 → plain-0.62.0}/plain/csrf/middleware.py +1 -1
- {plain-0.60.0 → plain-0.62.0}/plain/internal/handlers/base.py +1 -1
- {plain-0.60.0 → plain-0.62.0}/plain/internal/handlers/exception.py +1 -1
- plain-0.62.0/plain/logs/README.md +139 -0
- plain-0.62.0/plain/logs/__init__.py +3 -0
- plain-0.62.0/plain/logs/configure.py +36 -0
- plain-0.62.0/plain/logs/debug.py +36 -0
- plain-0.62.0/plain/logs/formatters.py +70 -0
- plain-0.62.0/plain/logs/loggers.py +183 -0
- {plain-0.60.0 → plain-0.62.0}/plain/runtime/__init__.py +8 -4
- {plain-0.60.0 → plain-0.62.0}/plain/runtime/global_settings.py +6 -2
- plain-0.62.0/plain/templates/AGENTS.md +3 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/objects.py +4 -3
- {plain-0.60.0 → plain-0.62.0}/pyproject.toml +2 -2
- {plain-0.60.0 → plain-0.62.0}/tests/test_cli.py +0 -8
- plain-0.62.0/tests/test_logs.py +295 -0
- plain-0.60.0/plain/cli/help.py +0 -27
- plain-0.60.0/plain/logs/README.md +0 -61
- plain-0.60.0/plain/logs/__init__.py +0 -5
- plain-0.60.0/plain/logs/configure.py +0 -44
- plain-0.60.0/plain/logs/loggers.py +0 -74
- {plain-0.60.0 → plain-0.62.0}/.gitignore +0 -0
- {plain-0.60.0 → plain-0.62.0}/LICENSE +0 -0
- {plain-0.60.0 → plain-0.62.0}/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/__main__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/compile.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/finders.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/fingerprints.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/urls.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/assets/views.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/chores/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/chores/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/chores/registry.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/build.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/changelog.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/chores.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/formatting.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/output.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/preflight.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/print.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/registry.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/scaffold.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/settings.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/startup.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/urls.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/cli/utils.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/csrf/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/csrf/views.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/debug.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/exceptions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/boundfield.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/exceptions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/fields.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/forms/forms.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/cookie.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/multipartparser.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/request.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/http/response.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/base.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/locks.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/move.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/temp.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/uploadedfile.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/uploadhandler.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/files/utils.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/handlers/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/handlers/wsgi.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/middleware/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/middleware/headers.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/middleware/https.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/internal/middleware/slash.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/json.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/logs/utils.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/packages/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/packages/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/packages/config.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/packages/registry.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/paginator.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/files.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/messages.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/registry.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/security.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/preflight/urls.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/runtime/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/runtime/user_settings.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/runtime/utils.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signals/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signals/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signals/dispatch/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signals/dispatch/dispatcher.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signals/dispatch/license.txt +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/signing.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/core.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/jinja/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/jinja/environments.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/jinja/extensions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/jinja/filters.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/templates/jinja/globals.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/test/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/test/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/test/client.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/test/encoding.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/test/exceptions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/converters.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/exceptions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/patterns.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/resolvers.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/routers.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/urls/utils.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/cache.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/crypto.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/datastructures.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/dateparse.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/deconstruct.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/decorators.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/duration.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/encoding.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/functional.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/hashable.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/html.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/http.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/inspect.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/ipv6.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/itercompat.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/module_loading.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/regex_helper.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/safestring.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/text.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/timesince.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/timezone.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/utils/tree.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/validators.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/README.md +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/base.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/errors.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/exceptions.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/forms.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/redirect.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/views/templates.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/plain/wsgi.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/.gitignore +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/app/.gitignore +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/app/settings.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/app/test/__init__.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/app/test/default_settings.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/app/urls.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/conftest.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/test_csrf.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/test_runtime.py +0 -0
- {plain-0.60.0 → plain-0.62.0}/tests/test_wsgi.py +0 -0
@@ -1,10 +1,10 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: plain
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.62.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
|
7
|
-
Requires-Python: >=3.
|
7
|
+
Requires-Python: >=3.13
|
8
8
|
Requires-Dist: click>=8.0.0
|
9
9
|
Requires-Dist: jinja2>=3.1.2
|
10
10
|
Requires-Dist: opentelemetry-api>=1.34.1
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Plain AGENTS.md
|
2
|
+
|
3
|
+
Plain is a Python web framework that was originally forked from Django. While it still has a lot in common with Django, there are also significant changes -- don't solely rely on knowledge of Django when working with Plain.
|
4
|
+
|
5
|
+
## Commands
|
6
|
+
|
7
|
+
The `plain` CLI is the main entrypoint for the framework. If `plain` is not available by itself, try `uv run plain`.
|
8
|
+
|
9
|
+
- `plain shell -c <command>`: Run a Python command with Plain configured.
|
10
|
+
- `plain run <filename>`: Run a Python script with Plain configured.
|
11
|
+
- `plain agent docs <package>`: Show README.md and symbolicated source files for a specific package.
|
12
|
+
- `plain agent docs --list`: List packages with docs available.
|
13
|
+
- `plain agent request <path> --user <user_id>`: Make an authenticated request to the application and inspect the output.
|
14
|
+
- `plain --help`: List all available commands (including those from installed packages).
|
15
|
+
|
16
|
+
## Code style
|
17
|
+
|
18
|
+
- Imports should be at the top of the file, unless there is a specific reason to import later (e.g. to avoid circular imports).
|
@@ -1,5 +1,31 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.62.0](https://github.com/dropseed/plain/releases/plain@0.62.0) (2025-09-09)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Complete rewrite of logging settings and AppLogger with improved formatters and debug capabilities ([ea7c953](https://github.com/dropseed/plain/commit/ea7c9537e3))
|
8
|
+
- Added `app_logger.debug_mode()` context manager to temporarily change log level ([f535459](https://github.com/dropseed/plain/commit/f53545f9fa))
|
9
|
+
- Minimum Python version updated to 3.13 ([d86e307](https://github.com/dropseed/plain/commit/d86e307efb))
|
10
|
+
|
11
|
+
### Upgrade instructions
|
12
|
+
|
13
|
+
- Make sure you are using Python 3.13 or higher
|
14
|
+
|
15
|
+
## [0.61.0](https://github.com/dropseed/plain/releases/plain@0.61.0) (2025-09-03)
|
16
|
+
|
17
|
+
### What's changed
|
18
|
+
|
19
|
+
- Added new `plain agent` command with subcommands for coding agents including `docs`, `md`, and `request` ([df3edbf](https://github.com/dropseed/plain/commit/df3edbf0bd))
|
20
|
+
- Added `-c` option to `plain shell` to execute commands and exit, similar to `python -c` ([5e67f0b](https://github.com/dropseed/plain/commit/5e67f0bcd8))
|
21
|
+
- The `plain docs --llm` functionality has been moved to `plain agent docs` command ([df3edbf](https://github.com/dropseed/plain/commit/df3edbf0bd))
|
22
|
+
- Removed the `plain help` command in favor of standard `plain --help` ([df3edbf](https://github.com/dropseed/plain/commit/df3edbf0bd))
|
23
|
+
|
24
|
+
### Upgrade instructions
|
25
|
+
|
26
|
+
- Replace `plain docs --llm` usage with `plain agent docs` command
|
27
|
+
- Use `plain --help` instead of `plain help` command
|
28
|
+
|
3
29
|
## [0.60.0](https://github.com/dropseed/plain/releases/plain@0.60.0) (2025-08-27)
|
4
30
|
|
5
31
|
### What's changed
|
@@ -0,0 +1,20 @@
|
|
1
|
+
import click
|
2
|
+
|
3
|
+
from .docs import docs
|
4
|
+
from .md import md
|
5
|
+
from .request import request
|
6
|
+
|
7
|
+
|
8
|
+
@click.group("agent", invoke_without_command=True)
|
9
|
+
@click.pass_context
|
10
|
+
def agent(ctx):
|
11
|
+
"""Tools for coding agents."""
|
12
|
+
if ctx.invoked_subcommand is None:
|
13
|
+
# If no subcommand provided, show all AGENTS.md files
|
14
|
+
ctx.invoke(md, show_all=True, show_list=False, package="")
|
15
|
+
|
16
|
+
|
17
|
+
# Add commands to the group
|
18
|
+
agent.add_command(docs)
|
19
|
+
agent.add_command(md)
|
20
|
+
agent.add_command(request)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
import importlib.util
|
2
|
+
import pkgutil
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from .llmdocs import LLMDocs
|
8
|
+
|
9
|
+
|
10
|
+
@click.command()
|
11
|
+
@click.argument("package", default="", required=False)
|
12
|
+
@click.option(
|
13
|
+
"--list",
|
14
|
+
"show_list",
|
15
|
+
is_flag=True,
|
16
|
+
help="List available packages",
|
17
|
+
)
|
18
|
+
def docs(package, show_list):
|
19
|
+
"""Show LLM-friendly documentation and source for a package."""
|
20
|
+
|
21
|
+
if show_list:
|
22
|
+
# List available packages using same discovery logic as md command
|
23
|
+
try:
|
24
|
+
available_packages = []
|
25
|
+
|
26
|
+
# Check for plain.* subpackages (including core plain)
|
27
|
+
try:
|
28
|
+
import plain
|
29
|
+
|
30
|
+
# Check core plain package (namespace package)
|
31
|
+
plain_spec = importlib.util.find_spec("plain")
|
32
|
+
if plain_spec and plain_spec.submodule_search_locations:
|
33
|
+
available_packages.append("plain")
|
34
|
+
|
35
|
+
# Check other plain.* subpackages
|
36
|
+
for importer, modname, ispkg in pkgutil.iter_modules(
|
37
|
+
plain.__path__, "plain."
|
38
|
+
):
|
39
|
+
if ispkg:
|
40
|
+
available_packages.append(modname)
|
41
|
+
except Exception:
|
42
|
+
pass
|
43
|
+
|
44
|
+
if available_packages:
|
45
|
+
for pkg in sorted(available_packages):
|
46
|
+
click.echo(f"- {pkg}")
|
47
|
+
else:
|
48
|
+
click.echo("No packages found.")
|
49
|
+
except Exception as e:
|
50
|
+
click.echo(f"Error listing packages: {e}")
|
51
|
+
return
|
52
|
+
|
53
|
+
if not package:
|
54
|
+
raise click.UsageError(
|
55
|
+
"Package name required. Usage: plain agent docs [package-name]"
|
56
|
+
)
|
57
|
+
|
58
|
+
# Convert hyphens to dots (e.g., plain-models -> plain.models)
|
59
|
+
package = package.replace("-", ".")
|
60
|
+
|
61
|
+
# Automatically prefix if we need to
|
62
|
+
if not package.startswith("plain"):
|
63
|
+
package = f"plain.{package}"
|
64
|
+
|
65
|
+
try:
|
66
|
+
# Get the path for this specific package
|
67
|
+
spec = importlib.util.find_spec(package)
|
68
|
+
if not spec or not spec.origin:
|
69
|
+
raise click.UsageError(f"Package {package} not found")
|
70
|
+
|
71
|
+
package_path = Path(spec.origin).parent
|
72
|
+
paths = [package_path]
|
73
|
+
|
74
|
+
# Generate docs for this specific package
|
75
|
+
source_docs = LLMDocs(paths)
|
76
|
+
source_docs.load()
|
77
|
+
source_docs.print(relative_to=package_path.parent)
|
78
|
+
|
79
|
+
except Exception as e:
|
80
|
+
raise click.UsageError(f"Error loading documentation for {package}: {e}")
|
@@ -1,75 +1,11 @@
|
|
1
1
|
import ast
|
2
|
-
import importlib.util
|
3
2
|
from pathlib import Path
|
4
3
|
|
5
4
|
import click
|
6
5
|
|
7
|
-
from plain.packages import packages_registry
|
8
|
-
|
9
|
-
from .output import iterate_markdown
|
10
|
-
|
11
|
-
|
12
|
-
@click.command()
|
13
|
-
@click.option("--llm", "llm", is_flag=True)
|
14
|
-
@click.option("--open")
|
15
|
-
@click.argument("module", default="")
|
16
|
-
def docs(module, llm, open):
|
17
|
-
if not module and not llm:
|
18
|
-
raise click.UsageError("You must specify a module or use --llm")
|
19
|
-
|
20
|
-
if llm:
|
21
|
-
paths = [Path(__file__).parent.parent]
|
22
|
-
|
23
|
-
for package_config in packages_registry.get_package_configs():
|
24
|
-
if package_config.name.startswith("app."):
|
25
|
-
# Ignore app packages for now
|
26
|
-
continue
|
27
|
-
|
28
|
-
paths.append(Path(package_config.path))
|
29
|
-
|
30
|
-
source_docs = LLMDocs(paths)
|
31
|
-
source_docs.load()
|
32
|
-
source_docs.print()
|
33
|
-
|
34
|
-
click.secho(
|
35
|
-
"That's everything! Copy this into your AI tool of choice.",
|
36
|
-
err=True,
|
37
|
-
fg="green",
|
38
|
-
)
|
39
|
-
|
40
|
-
return
|
41
|
-
|
42
|
-
if module:
|
43
|
-
# Convert hyphens to dots (e.g., plain-models -> plain.models)
|
44
|
-
module = module.replace("-", ".")
|
45
|
-
|
46
|
-
# Automatically prefix if we need to
|
47
|
-
if not module.startswith("plain"):
|
48
|
-
module = f"plain.{module}"
|
49
|
-
|
50
|
-
# Get the README.md file for the module
|
51
|
-
spec = importlib.util.find_spec(module)
|
52
|
-
if not spec:
|
53
|
-
raise click.UsageError(f"Module {module} not found")
|
54
|
-
|
55
|
-
module_path = Path(spec.origin).parent
|
56
|
-
readme_path = module_path / "README.md"
|
57
|
-
if not readme_path.exists():
|
58
|
-
raise click.UsageError(f"README.md not found for {module}")
|
59
|
-
|
60
|
-
if open:
|
61
|
-
click.launch(str(readme_path))
|
62
|
-
else:
|
63
|
-
click.echo_via_pager(iterate_markdown(readme_path.read_text()))
|
64
|
-
|
65
6
|
|
66
7
|
class LLMDocs:
|
67
|
-
|
68
|
-
"Below is all of the documentation and abbreviated source code for the Plain web framework. "
|
69
|
-
"Your job is to read and understand it, and then act as the Plain Framework Assistant and "
|
70
|
-
"help the developer accomplish whatever they want to do next."
|
71
|
-
"\n\n---\n\n"
|
72
|
-
)
|
8
|
+
"""Generates LLM-friendly documentation."""
|
73
9
|
|
74
10
|
def __init__(self, paths):
|
75
11
|
self.paths = paths
|
@@ -88,6 +24,7 @@ class LLMDocs:
|
|
88
24
|
self.docs.add(path)
|
89
25
|
|
90
26
|
# Exclude "migrations" code from plain apps, except for plain/models/migrations
|
27
|
+
# Also exclude CHANGELOG.md and AGENTS.md
|
91
28
|
self.docs = {
|
92
29
|
doc
|
93
30
|
for doc in self.docs
|
@@ -95,6 +32,7 @@ class LLMDocs:
|
|
95
32
|
"/migrations/" in str(doc)
|
96
33
|
and "/plain/models/migrations/" not in str(doc)
|
97
34
|
)
|
35
|
+
and doc.name not in ("CHANGELOG.md", "AGENTS.md")
|
98
36
|
}
|
99
37
|
self.sources = {
|
100
38
|
source
|
@@ -103,6 +41,7 @@ class LLMDocs:
|
|
103
41
|
"/migrations/" in str(source)
|
104
42
|
and "/plain/models/migrations/" not in str(source)
|
105
43
|
)
|
44
|
+
and source.name != "cli.py"
|
106
45
|
}
|
107
46
|
|
108
47
|
self.docs = sorted(self.docs)
|
@@ -120,8 +59,6 @@ class LLMDocs:
|
|
120
59
|
return path.relative_to(plain_root.parent)
|
121
60
|
|
122
61
|
def print(self, relative_to=None):
|
123
|
-
click.secho(self.preamble, fg="yellow")
|
124
|
-
|
125
62
|
for doc in self.docs:
|
126
63
|
if relative_to:
|
127
64
|
display_path = doc.relative_to(relative_to)
|
@@ -159,14 +96,11 @@ class LLMDocs:
|
|
159
96
|
for d in node.decorator_list
|
160
97
|
):
|
161
98
|
return True
|
162
|
-
if node.name.startswith("_"):
|
99
|
+
if node.name.startswith("_"):
|
163
100
|
return True
|
164
101
|
elif isinstance(node, ast.Assign):
|
165
102
|
for target in node.targets:
|
166
|
-
if (
|
167
|
-
isinstance(target, ast.Name) and target.id.startswith("_")
|
168
|
-
# and not target.id.endswith("__")
|
169
|
-
):
|
103
|
+
if isinstance(target, ast.Name) and target.id.startswith("_"):
|
170
104
|
return True
|
171
105
|
return False
|
172
106
|
|
@@ -186,23 +120,16 @@ class LLMDocs:
|
|
186
120
|
lines.extend(decorators)
|
187
121
|
bases = [ast.unparse(base) for base in node.bases]
|
188
122
|
lines.append(f"{prefix}class {node.name}({', '.join(bases)})")
|
189
|
-
# if ast.get_docstring(node):
|
190
|
-
# lines.append(f'{prefix} """{ast.get_docstring(node)}"""')
|
191
123
|
for child in node.body:
|
192
124
|
child_lines = process_node(child, indent + 1)
|
193
125
|
if child_lines:
|
194
126
|
lines.extend(child_lines)
|
195
|
-
# if not has_body:
|
196
|
-
# lines.append(f"{prefix} pass")
|
197
127
|
|
198
128
|
elif isinstance(node, ast.FunctionDef):
|
199
129
|
decorators = [f"{prefix}@{ast.unparse(d)}" for d in node.decorator_list]
|
200
130
|
lines.extend(decorators)
|
201
131
|
args = ast.unparse(node.args)
|
202
132
|
lines.append(f"{prefix}def {node.name}({args})")
|
203
|
-
# if ast.get_docstring(node):
|
204
|
-
# lines.append(f'{prefix} """{ast.get_docstring(node)}"""')
|
205
|
-
# lines.append(f"{prefix} pass")
|
206
133
|
|
207
134
|
elif isinstance(node, ast.Assign):
|
208
135
|
for target in node.targets:
|
@@ -0,0 +1,87 @@
|
|
1
|
+
import importlib.util
|
2
|
+
import pkgutil
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
import click
|
6
|
+
|
7
|
+
from ..output import iterate_markdown
|
8
|
+
|
9
|
+
|
10
|
+
def _get_packages_with_agents():
|
11
|
+
"""Get dict mapping package names to AGENTS.md paths."""
|
12
|
+
agents_files = {}
|
13
|
+
|
14
|
+
# Check for plain.* subpackages (including core plain)
|
15
|
+
try:
|
16
|
+
import plain
|
17
|
+
|
18
|
+
# Check core plain package (namespace package)
|
19
|
+
plain_spec = importlib.util.find_spec("plain")
|
20
|
+
if plain_spec and plain_spec.submodule_search_locations:
|
21
|
+
# For namespace packages, use the first search location
|
22
|
+
plain_path = Path(plain_spec.submodule_search_locations[0])
|
23
|
+
agents_path = plain_path / "AGENTS.md"
|
24
|
+
if agents_path.exists():
|
25
|
+
agents_files["plain"] = agents_path
|
26
|
+
|
27
|
+
# Check other plain.* subpackages
|
28
|
+
for importer, modname, ispkg in pkgutil.iter_modules(plain.__path__, "plain."):
|
29
|
+
if ispkg:
|
30
|
+
try:
|
31
|
+
spec = importlib.util.find_spec(modname)
|
32
|
+
if spec and spec.origin:
|
33
|
+
package_path = Path(spec.origin).parent
|
34
|
+
# Look for AGENTS.md at package root
|
35
|
+
agents_path = package_path / "AGENTS.md"
|
36
|
+
if agents_path.exists():
|
37
|
+
agents_files[modname] = agents_path
|
38
|
+
except Exception:
|
39
|
+
continue
|
40
|
+
except Exception:
|
41
|
+
pass
|
42
|
+
|
43
|
+
return agents_files
|
44
|
+
|
45
|
+
|
46
|
+
@click.command("md")
|
47
|
+
@click.argument("package", default="", required=False)
|
48
|
+
@click.option(
|
49
|
+
"--all",
|
50
|
+
"show_all",
|
51
|
+
is_flag=True,
|
52
|
+
help="Show AGENTS.md for all packages that have them",
|
53
|
+
)
|
54
|
+
@click.option(
|
55
|
+
"--list",
|
56
|
+
"show_list",
|
57
|
+
is_flag=True,
|
58
|
+
help="List packages with AGENTS.md files",
|
59
|
+
)
|
60
|
+
def md(package, show_all, show_list):
|
61
|
+
"""Show AGENTS.md for a package."""
|
62
|
+
|
63
|
+
agents_files = _get_packages_with_agents()
|
64
|
+
|
65
|
+
if show_list:
|
66
|
+
for pkg in sorted(agents_files.keys()):
|
67
|
+
click.echo(f"- {pkg}")
|
68
|
+
|
69
|
+
return
|
70
|
+
|
71
|
+
if show_all:
|
72
|
+
for pkg in sorted(agents_files.keys()):
|
73
|
+
agents_path = agents_files[pkg]
|
74
|
+
for line in iterate_markdown(agents_path.read_text()):
|
75
|
+
click.echo(line, nl=False)
|
76
|
+
print()
|
77
|
+
|
78
|
+
return
|
79
|
+
|
80
|
+
if not package:
|
81
|
+
raise click.UsageError(
|
82
|
+
"Package name or --all required. Use --list to see available packages."
|
83
|
+
)
|
84
|
+
|
85
|
+
agents_path = agents_files[package]
|
86
|
+
for line in iterate_markdown(agents_path.read_text()):
|
87
|
+
click.echo(line, nl=False)
|
@@ -5,24 +5,19 @@ import subprocess
|
|
5
5
|
import click
|
6
6
|
|
7
7
|
|
8
|
+
def is_agent_environment():
|
9
|
+
"""Check if we're running inside a coding agent."""
|
10
|
+
return bool(
|
11
|
+
os.environ.get("CLAUDECODE")
|
12
|
+
or os.environ.get("CODEX_SANDBOX")
|
13
|
+
or os.environ.get("CURSOR_ENVIRONMENT")
|
14
|
+
)
|
15
|
+
|
16
|
+
|
8
17
|
def prompt_agent(
|
9
18
|
prompt: str, agent_command: str | None = None, print_only: bool = False
|
10
19
|
) -> bool:
|
11
|
-
|
12
|
-
Run an agent command with the given prompt, or display the prompt for manual copying.
|
13
|
-
|
14
|
-
Args:
|
15
|
-
prompt: The prompt to send to the agent
|
16
|
-
agent_command: Optional command to run (e.g., "claude code"). If not provided,
|
17
|
-
will check the PLAIN_AGENT_COMMAND environment variable.
|
18
|
-
print_only: If True, always print the prompt instead of running the agent
|
19
|
-
|
20
|
-
Returns:
|
21
|
-
True if the agent command succeeded (or no agent command was provided),
|
22
|
-
False if the agent command failed.
|
23
|
-
"""
|
24
|
-
# Check if running inside an agent and just print the prompt if so
|
25
|
-
if os.environ.get("CLAUDECODE") or os.environ.get("CODEX_SANDBOX"):
|
20
|
+
if is_agent_environment():
|
26
21
|
click.echo(prompt)
|
27
22
|
return True
|
28
23
|
|
@@ -0,0 +1,181 @@
|
|
1
|
+
import json
|
2
|
+
|
3
|
+
import click
|
4
|
+
|
5
|
+
from plain.runtime import settings
|
6
|
+
from plain.test import Client
|
7
|
+
|
8
|
+
|
9
|
+
@click.command()
|
10
|
+
@click.argument("path")
|
11
|
+
@click.option(
|
12
|
+
"--method",
|
13
|
+
default="GET",
|
14
|
+
help="HTTP method (GET, POST, PUT, PATCH, DELETE, etc.)",
|
15
|
+
)
|
16
|
+
@click.option(
|
17
|
+
"--data",
|
18
|
+
help="Request data (JSON string for POST/PUT/PATCH)",
|
19
|
+
)
|
20
|
+
@click.option(
|
21
|
+
"--user",
|
22
|
+
"user_id",
|
23
|
+
help="User ID to authenticate as (skips normal authentication)",
|
24
|
+
)
|
25
|
+
@click.option(
|
26
|
+
"--follow/--no-follow",
|
27
|
+
default=True,
|
28
|
+
help="Follow redirects (default: True)",
|
29
|
+
)
|
30
|
+
@click.option(
|
31
|
+
"--content-type",
|
32
|
+
help="Content-Type header for request data",
|
33
|
+
)
|
34
|
+
@click.option(
|
35
|
+
"--header",
|
36
|
+
"headers",
|
37
|
+
multiple=True,
|
38
|
+
help="Additional headers (format: 'Name: Value')",
|
39
|
+
)
|
40
|
+
def request(path, method, data, user_id, follow, content_type, headers):
|
41
|
+
"""Make an HTTP request using the test client against the development database."""
|
42
|
+
|
43
|
+
try:
|
44
|
+
# Only allow in DEBUG mode for security
|
45
|
+
if not settings.DEBUG:
|
46
|
+
click.secho("This command only works when DEBUG=True", fg="red", err=True)
|
47
|
+
return
|
48
|
+
|
49
|
+
# Temporarily add testserver to ALLOWED_HOSTS so the test client can make requests
|
50
|
+
original_allowed_hosts = settings.ALLOWED_HOSTS
|
51
|
+
settings.ALLOWED_HOSTS = ["*"]
|
52
|
+
|
53
|
+
try:
|
54
|
+
# Create test client
|
55
|
+
client = Client()
|
56
|
+
|
57
|
+
# If user_id provided, force login
|
58
|
+
if user_id:
|
59
|
+
try:
|
60
|
+
# Get the User model using plain.auth utility
|
61
|
+
from plain.auth import get_user_model
|
62
|
+
|
63
|
+
User = get_user_model()
|
64
|
+
|
65
|
+
# Get the user
|
66
|
+
try:
|
67
|
+
user = User.objects.get(id=user_id)
|
68
|
+
client.force_login(user)
|
69
|
+
click.secho(
|
70
|
+
f"Authenticated as user {user_id}", fg="green", dim=True
|
71
|
+
)
|
72
|
+
except User.DoesNotExist:
|
73
|
+
click.secho(f"User {user_id} not found", fg="red", err=True)
|
74
|
+
return
|
75
|
+
|
76
|
+
except Exception as e:
|
77
|
+
click.secho(f"Authentication error: {e}", fg="red", err=True)
|
78
|
+
return
|
79
|
+
|
80
|
+
# Parse additional headers
|
81
|
+
header_dict = {}
|
82
|
+
for header in headers:
|
83
|
+
if ":" in header:
|
84
|
+
key, value = header.split(":", 1)
|
85
|
+
header_dict[key.strip()] = value.strip()
|
86
|
+
|
87
|
+
# Prepare request data
|
88
|
+
if data and content_type and "json" in content_type.lower():
|
89
|
+
try:
|
90
|
+
# Validate JSON
|
91
|
+
json.loads(data)
|
92
|
+
except json.JSONDecodeError as e:
|
93
|
+
click.secho(f"Invalid JSON data: {e}", fg="red", err=True)
|
94
|
+
return
|
95
|
+
|
96
|
+
# Make the request
|
97
|
+
method = method.upper()
|
98
|
+
kwargs = {
|
99
|
+
"path": path,
|
100
|
+
"follow": follow,
|
101
|
+
"headers": header_dict or None,
|
102
|
+
}
|
103
|
+
|
104
|
+
if method in ("POST", "PUT", "PATCH") and data:
|
105
|
+
kwargs["data"] = data
|
106
|
+
if content_type:
|
107
|
+
kwargs["content_type"] = content_type
|
108
|
+
|
109
|
+
# Call the appropriate client method
|
110
|
+
if method == "GET":
|
111
|
+
response = client.get(**kwargs)
|
112
|
+
elif method == "POST":
|
113
|
+
response = client.post(**kwargs)
|
114
|
+
elif method == "PUT":
|
115
|
+
response = client.put(**kwargs)
|
116
|
+
elif method == "PATCH":
|
117
|
+
response = client.patch(**kwargs)
|
118
|
+
elif method == "DELETE":
|
119
|
+
response = client.delete(**kwargs)
|
120
|
+
elif method == "HEAD":
|
121
|
+
response = client.head(**kwargs)
|
122
|
+
elif method == "OPTIONS":
|
123
|
+
response = client.options(**kwargs)
|
124
|
+
elif method == "TRACE":
|
125
|
+
response = client.trace(**kwargs)
|
126
|
+
else:
|
127
|
+
click.secho(f"Unsupported HTTP method: {method}", fg="red", err=True)
|
128
|
+
return
|
129
|
+
|
130
|
+
# Display response information
|
131
|
+
click.secho(
|
132
|
+
f"HTTP {response.status_code}",
|
133
|
+
fg="green" if response.status_code < 400 else "red",
|
134
|
+
bold=True,
|
135
|
+
)
|
136
|
+
|
137
|
+
# Show additional response info first
|
138
|
+
if hasattr(response, "user"):
|
139
|
+
click.secho(f"Authenticated user: {response.user}", fg="blue", dim=True)
|
140
|
+
|
141
|
+
if hasattr(response, "resolver_match") and response.resolver_match:
|
142
|
+
match = response.resolver_match
|
143
|
+
url_name = match.namespaced_url_name or match.url_name or "unnamed"
|
144
|
+
click.secho(f"URL pattern matched: {url_name}", fg="blue", dim=True)
|
145
|
+
|
146
|
+
# Show headers
|
147
|
+
if response.headers:
|
148
|
+
click.secho("Response Headers:", fg="yellow", bold=True)
|
149
|
+
for key, value in response.headers.items():
|
150
|
+
click.echo(f" {key}: {value}")
|
151
|
+
click.echo()
|
152
|
+
|
153
|
+
# Show response content last
|
154
|
+
if response.content:
|
155
|
+
content_type = response.headers.get("Content-Type", "")
|
156
|
+
|
157
|
+
if "json" in content_type.lower():
|
158
|
+
try:
|
159
|
+
json_data = response.json()
|
160
|
+
click.secho("Response Body (JSON):", fg="yellow", bold=True)
|
161
|
+
click.echo(json.dumps(json_data, indent=2))
|
162
|
+
except Exception:
|
163
|
+
click.secho("Response Body:", fg="yellow", bold=True)
|
164
|
+
click.echo(response.content.decode("utf-8", errors="replace"))
|
165
|
+
elif "html" in content_type.lower():
|
166
|
+
click.secho("Response Body (HTML):", fg="yellow", bold=True)
|
167
|
+
content = response.content.decode("utf-8", errors="replace")
|
168
|
+
click.echo(content)
|
169
|
+
else:
|
170
|
+
click.secho("Response Body:", fg="yellow", bold=True)
|
171
|
+
content = response.content.decode("utf-8", errors="replace")
|
172
|
+
click.echo(content)
|
173
|
+
else:
|
174
|
+
click.secho("(No response body)", fg="yellow", dim=True)
|
175
|
+
|
176
|
+
finally:
|
177
|
+
# Restore original ALLOWED_HOSTS
|
178
|
+
settings.ALLOWED_HOSTS = original_allowed_hosts
|
179
|
+
|
180
|
+
except Exception as e:
|
181
|
+
click.secho(f"Request failed: {e}", fg="red", err=True)
|
@@ -6,12 +6,12 @@ from click.core import Command, Context
|
|
6
6
|
import plain.runtime
|
7
7
|
from plain.exceptions import ImproperlyConfigured
|
8
8
|
|
9
|
+
from .agent import agent
|
9
10
|
from .build import build
|
10
11
|
from .changelog import changelog
|
11
12
|
from .chores import chores
|
12
13
|
from .docs import docs
|
13
14
|
from .formatting import PlainContext
|
14
|
-
from .help import help_cmd
|
15
15
|
from .install import install
|
16
16
|
from .preflight import preflight_checks
|
17
17
|
from .registry import cli_registry
|
@@ -28,6 +28,7 @@ def plain_cli():
|
|
28
28
|
pass
|
29
29
|
|
30
30
|
|
31
|
+
plain_cli.add_command(agent)
|
31
32
|
plain_cli.add_command(docs)
|
32
33
|
plain_cli.add_command(preflight_checks)
|
33
34
|
plain_cli.add_command(create)
|
@@ -41,7 +42,6 @@ plain_cli.add_command(shell)
|
|
41
42
|
plain_cli.add_command(run)
|
42
43
|
plain_cli.add_command(install)
|
43
44
|
plain_cli.add_command(upgrade)
|
44
|
-
plain_cli.add_command(help_cmd)
|
45
45
|
|
46
46
|
|
47
47
|
class CLIRegistryGroup(click.Group):
|
@@ -0,0 +1,38 @@
|
|
1
|
+
import importlib.util
|
2
|
+
from pathlib import Path
|
3
|
+
|
4
|
+
import click
|
5
|
+
|
6
|
+
from .output import iterate_markdown
|
7
|
+
|
8
|
+
|
9
|
+
@click.command()
|
10
|
+
@click.option("--open")
|
11
|
+
@click.argument("module", default="")
|
12
|
+
def docs(module, open):
|
13
|
+
if not module:
|
14
|
+
raise click.UsageError(
|
15
|
+
"You must specify a module. For LLM-friendly docs, use `plain agent docs`."
|
16
|
+
)
|
17
|
+
|
18
|
+
# Convert hyphens to dots (e.g., plain-models -> plain.models)
|
19
|
+
module = module.replace("-", ".")
|
20
|
+
|
21
|
+
# Automatically prefix if we need to
|
22
|
+
if not module.startswith("plain"):
|
23
|
+
module = f"plain.{module}"
|
24
|
+
|
25
|
+
# Get the README.md file for the module
|
26
|
+
spec = importlib.util.find_spec(module)
|
27
|
+
if not spec:
|
28
|
+
raise click.UsageError(f"Module {module} not found")
|
29
|
+
|
30
|
+
module_path = Path(spec.origin).parent
|
31
|
+
readme_path = module_path / "README.md"
|
32
|
+
if not readme_path.exists():
|
33
|
+
raise click.UsageError(f"README.md not found for {module}")
|
34
|
+
|
35
|
+
if open:
|
36
|
+
click.launch(str(readme_path))
|
37
|
+
else:
|
38
|
+
click.echo_via_pager(iterate_markdown(readme_path.read_text()))
|