plain 0.68.0__py3-none-any.whl → 0.103.0__py3-none-any.whl
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/CHANGELOG.md +684 -1
- plain/README.md +1 -1
- plain/agents/.claude/rules/plain.md +88 -0
- plain/agents/.claude/skills/plain-install/SKILL.md +26 -0
- plain/agents/.claude/skills/plain-upgrade/SKILL.md +35 -0
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -36
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +234 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +110 -26
- plain/cli/docs.py +98 -21
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +45 -26
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +27 -75
- plain/cli/print.py +4 -4
- plain/cli/registry.py +96 -10
- plain/cli/{agent/request.py → request.py} +67 -33
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -8
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +27 -16
- plain/paginator.py +31 -21
- plain/preflight/README.md +209 -24
- plain/preflight/__init__.py +1 -0
- plain/preflight/checks.py +3 -1
- plain/preflight/files.py +3 -1
- plain/preflight/registry.py +26 -11
- plain/preflight/results.py +15 -7
- plain/preflight/security.py +15 -13
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +4 -1
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +34 -25
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +13 -5
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +38 -22
- plain/urls/resolvers.py +35 -25
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/METADATA +4 -2
- plain-0.103.0.dist-info/RECORD +198 -0
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/WHEEL +1 -1
- plain-0.103.0.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/templates/AGENTS.md +0 -3
- plain-0.68.0.dist-info/RECORD +0 -169
- plain-0.68.0.dist-info/entry_points.txt +0 -5
- {plain-0.68.0.dist-info → plain-0.103.0.dist-info}/licenses/LICENSE +0 -0
plain/cli/install.py
CHANGED
|
@@ -3,28 +3,11 @@ import sys
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
|
-
from .agent.prompt import prompt_agent
|
|
7
|
-
|
|
8
6
|
|
|
9
7
|
@click.command()
|
|
10
8
|
@click.argument("packages", nargs=-1, required=True)
|
|
11
|
-
|
|
12
|
-
"
|
|
13
|
-
envvar="PLAIN_AGENT_COMMAND",
|
|
14
|
-
help="Run command with generated prompt",
|
|
15
|
-
)
|
|
16
|
-
@click.option(
|
|
17
|
-
"--print",
|
|
18
|
-
"print_only",
|
|
19
|
-
is_flag=True,
|
|
20
|
-
help="Print the prompt without running the agent",
|
|
21
|
-
)
|
|
22
|
-
def install(
|
|
23
|
-
packages: tuple[str, ...],
|
|
24
|
-
agent_command: str | None = None,
|
|
25
|
-
print_only: bool = False,
|
|
26
|
-
) -> None:
|
|
27
|
-
"""Install Plain packages with the help of an agent."""
|
|
9
|
+
def install(packages: tuple[str, ...]) -> None:
|
|
10
|
+
"""Install Plain packages"""
|
|
28
11
|
# Validate all package names
|
|
29
12
|
invalid_packages = [pkg for pkg in packages if not pkg.startswith("plain")]
|
|
30
13
|
if invalid_packages:
|
|
@@ -33,14 +16,14 @@ def install(
|
|
|
33
16
|
"This command is only for Plain framework packages."
|
|
34
17
|
)
|
|
35
18
|
|
|
36
|
-
# Install all packages
|
|
19
|
+
# Install all packages
|
|
37
20
|
if len(packages) == 1:
|
|
38
|
-
click.secho(f"Installing {packages[0]}...", bold=True
|
|
21
|
+
click.secho(f"Installing {packages[0]}...", bold=True)
|
|
39
22
|
else:
|
|
40
|
-
click.secho(f"Installing {len(packages)} packages...", bold=True
|
|
23
|
+
click.secho(f"Installing {len(packages)} packages...", bold=True)
|
|
41
24
|
for pkg in packages:
|
|
42
|
-
click.secho(f" - {pkg}"
|
|
43
|
-
click.echo(
|
|
25
|
+
click.secho(f" - {pkg}")
|
|
26
|
+
click.echo()
|
|
44
27
|
|
|
45
28
|
install_cmd = ["uv", "add"] + list(packages)
|
|
46
29
|
result = subprocess.run(install_cmd, check=False, stderr=sys.stderr)
|
|
@@ -48,35 +31,8 @@ def install(
|
|
|
48
31
|
if result.returncode != 0:
|
|
49
32
|
raise click.ClickException("Failed to install packages")
|
|
50
33
|
|
|
51
|
-
click.echo(
|
|
34
|
+
click.echo()
|
|
52
35
|
if len(packages) == 1:
|
|
53
|
-
click.secho(f"
|
|
36
|
+
click.secho(f"{packages[0]} installed successfully", fg="green")
|
|
54
37
|
else:
|
|
55
|
-
click.secho(
|
|
56
|
-
f"✓ {len(packages)} packages installed successfully", fg="green", err=True
|
|
57
|
-
)
|
|
58
|
-
click.echo(err=True)
|
|
59
|
-
|
|
60
|
-
# Build the prompt for the agent to complete setup
|
|
61
|
-
lines = [
|
|
62
|
-
f"Complete the setup for the following Plain packages that were just installed: {', '.join(packages)}",
|
|
63
|
-
"",
|
|
64
|
-
"## Instructions",
|
|
65
|
-
"",
|
|
66
|
-
"For each package:",
|
|
67
|
-
"1. Run `uv run plain docs <package>` and read the installation instructions",
|
|
68
|
-
"2. If the docs point out that it is a --dev tool, move it to the dev dependencies in pyproject.toml: `uv remove <package> && uv add <package> --dev`",
|
|
69
|
-
"3. Go through the installation instructions and complete any code modifications that are needed",
|
|
70
|
-
"",
|
|
71
|
-
"DO NOT commit any changes",
|
|
72
|
-
"",
|
|
73
|
-
"Report back with:",
|
|
74
|
-
"- Whether the setup completed successfully",
|
|
75
|
-
"- Any manual steps that the user will need to complete",
|
|
76
|
-
"- Any issues or errors encountered",
|
|
77
|
-
]
|
|
78
|
-
|
|
79
|
-
prompt = "\n".join(lines)
|
|
80
|
-
success = prompt_agent(prompt, agent_command, print_only)
|
|
81
|
-
if not success:
|
|
82
|
-
raise click.Abort()
|
|
38
|
+
click.secho(f"{len(packages)} packages installed successfully", fg="green")
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import ast
|
|
2
4
|
from pathlib import Path
|
|
3
5
|
|
|
@@ -7,10 +9,10 @@ import click
|
|
|
7
9
|
class LLMDocs:
|
|
8
10
|
"""Generates LLM-friendly documentation."""
|
|
9
11
|
|
|
10
|
-
def __init__(self, paths):
|
|
12
|
+
def __init__(self, paths: list[Path]):
|
|
11
13
|
self.paths = paths
|
|
12
14
|
|
|
13
|
-
def load(self):
|
|
15
|
+
def load(self) -> None:
|
|
14
16
|
self.docs = set()
|
|
15
17
|
self.sources = set()
|
|
16
18
|
|
|
@@ -24,7 +26,7 @@ class LLMDocs:
|
|
|
24
26
|
self.docs.add(path)
|
|
25
27
|
|
|
26
28
|
# Exclude "migrations" code from plain apps, except for plain/models/migrations
|
|
27
|
-
# Also exclude CHANGELOG.md
|
|
29
|
+
# Also exclude CHANGELOG.md, AGENTS.md, and agents directory
|
|
28
30
|
self.docs = {
|
|
29
31
|
doc
|
|
30
32
|
for doc in self.docs
|
|
@@ -33,6 +35,7 @@ class LLMDocs:
|
|
|
33
35
|
and "/plain/models/migrations/" not in str(doc)
|
|
34
36
|
)
|
|
35
37
|
and doc.name not in ("CHANGELOG.md", "AGENTS.md")
|
|
38
|
+
and "/agents/" not in str(doc)
|
|
36
39
|
}
|
|
37
40
|
self.sources = {
|
|
38
41
|
source
|
|
@@ -42,12 +45,13 @@ class LLMDocs:
|
|
|
42
45
|
and "/plain/models/migrations/" not in str(source)
|
|
43
46
|
)
|
|
44
47
|
and source.name != "cli.py"
|
|
48
|
+
and "/agents/" not in str(source)
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
self.docs = sorted(self.docs)
|
|
48
52
|
self.sources = sorted(self.sources)
|
|
49
53
|
|
|
50
|
-
def display_path(self, path):
|
|
54
|
+
def display_path(self, path: Path) -> Path:
|
|
51
55
|
if "plain" in path.parts:
|
|
52
56
|
root_index = path.parts.index("plain")
|
|
53
57
|
elif "plainx" in path.parts:
|
|
@@ -58,30 +62,37 @@ class LLMDocs:
|
|
|
58
62
|
plain_root = Path(*path.parts[: root_index + 1])
|
|
59
63
|
return path.relative_to(plain_root.parent)
|
|
60
64
|
|
|
61
|
-
def print(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
click.secho(f"</Docs: {display_path}>", fg="yellow")
|
|
70
|
-
click.echo()
|
|
71
|
-
|
|
72
|
-
for source in self.sources:
|
|
73
|
-
if symbolicated := self.symbolicate(source):
|
|
65
|
+
def print(
|
|
66
|
+
self,
|
|
67
|
+
relative_to: Path | None = None,
|
|
68
|
+
include_docs: bool = True,
|
|
69
|
+
include_symbols: bool = True,
|
|
70
|
+
) -> None:
|
|
71
|
+
if include_docs:
|
|
72
|
+
for doc in self.docs:
|
|
74
73
|
if relative_to:
|
|
75
|
-
display_path =
|
|
74
|
+
display_path = doc.relative_to(relative_to)
|
|
76
75
|
else:
|
|
77
|
-
display_path = self.display_path(
|
|
78
|
-
click.secho(f"<
|
|
79
|
-
click.echo(
|
|
80
|
-
click.secho(f"</
|
|
76
|
+
display_path = self.display_path(doc)
|
|
77
|
+
click.secho(f"<Docs: {display_path}>", fg="yellow")
|
|
78
|
+
click.echo(doc.read_text())
|
|
79
|
+
click.secho(f"</Docs: {display_path}>", fg="yellow")
|
|
81
80
|
click.echo()
|
|
82
81
|
|
|
82
|
+
if include_symbols:
|
|
83
|
+
for source in self.sources:
|
|
84
|
+
if symbolicated := self.symbolicate(source):
|
|
85
|
+
if relative_to:
|
|
86
|
+
display_path = source.relative_to(relative_to)
|
|
87
|
+
else:
|
|
88
|
+
display_path = self.display_path(source)
|
|
89
|
+
click.secho(f"<Source: {display_path}>", fg="yellow")
|
|
90
|
+
click.echo(symbolicated)
|
|
91
|
+
click.secho(f"</Source: {display_path}>", fg="yellow")
|
|
92
|
+
click.echo()
|
|
93
|
+
|
|
83
94
|
@staticmethod
|
|
84
|
-
def symbolicate(file_path: Path):
|
|
95
|
+
def symbolicate(file_path: Path) -> str:
|
|
85
96
|
if "internal" in str(file_path).split("/"):
|
|
86
97
|
return ""
|
|
87
98
|
|
|
@@ -89,8 +100,16 @@ class LLMDocs:
|
|
|
89
100
|
|
|
90
101
|
parsed = ast.parse(source)
|
|
91
102
|
|
|
92
|
-
def should_skip(node):
|
|
93
|
-
if isinstance(node, ast.ClassDef
|
|
103
|
+
def should_skip(node: ast.AST) -> bool:
|
|
104
|
+
if isinstance(node, ast.ClassDef):
|
|
105
|
+
if any(
|
|
106
|
+
isinstance(d, ast.Name) and d.id == "internalcode"
|
|
107
|
+
for d in node.decorator_list
|
|
108
|
+
):
|
|
109
|
+
return True
|
|
110
|
+
if node.name.startswith("_"):
|
|
111
|
+
return True
|
|
112
|
+
elif isinstance(node, ast.FunctionDef):
|
|
94
113
|
if any(
|
|
95
114
|
isinstance(d, ast.Name) and d.id == "internalcode"
|
|
96
115
|
for d in node.decorator_list
|
|
@@ -104,7 +123,7 @@ class LLMDocs:
|
|
|
104
123
|
return True
|
|
105
124
|
return False
|
|
106
125
|
|
|
107
|
-
def process_node(node, indent=0):
|
|
126
|
+
def process_node(node: ast.AST, indent: int = 0) -> list[str]:
|
|
108
127
|
lines = []
|
|
109
128
|
prefix = " " * indent
|
|
110
129
|
|
plain/cli/output.py
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
|
|
1
5
|
import click
|
|
2
6
|
|
|
3
7
|
|
|
4
|
-
def style_markdown(content):
|
|
8
|
+
def style_markdown(content: str) -> str:
|
|
5
9
|
return "".join(iterate_markdown(content))
|
|
6
10
|
|
|
7
11
|
|
|
8
|
-
def iterate_markdown(content):
|
|
12
|
+
def iterate_markdown(content: str) -> Iterator[str]:
|
|
9
13
|
"""
|
|
10
14
|
Iterator that does basic markdown for a Click pager.
|
|
11
15
|
|
plain/cli/preflight.py
CHANGED
|
@@ -1,25 +1,20 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import sys
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
|
|
6
7
|
from plain import preflight
|
|
8
|
+
from plain.cli.runtime import common_command
|
|
7
9
|
from plain.packages import packages_registry
|
|
8
|
-
from plain.preflight.registry import checks_registry
|
|
9
|
-
from plain.runtime import settings
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
@
|
|
13
|
-
|
|
14
|
-
"""Run or manage preflight checks."""
|
|
15
|
-
pass
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@preflight_cli.command("check")
|
|
12
|
+
@common_command
|
|
13
|
+
@click.command("preflight")
|
|
19
14
|
@click.option(
|
|
20
15
|
"--deploy",
|
|
21
16
|
is_flag=True,
|
|
22
|
-
help="
|
|
17
|
+
help="Include deployment checks.",
|
|
23
18
|
)
|
|
24
19
|
@click.option(
|
|
25
20
|
"--format",
|
|
@@ -32,16 +27,18 @@ def preflight_cli():
|
|
|
32
27
|
is_flag=True,
|
|
33
28
|
help="Hide progress output and warnings, only show errors.",
|
|
34
29
|
)
|
|
35
|
-
def
|
|
36
|
-
"""
|
|
37
|
-
Use
|
|
38
|
-
|
|
39
|
-
""
|
|
30
|
+
def preflight_cli(deploy: bool, format: str, quiet: bool) -> None:
|
|
31
|
+
"""Validation checks before deployment"""
|
|
32
|
+
# Use stderr for progress messages only in JSON mode (keeps stdout clean for parsing)
|
|
33
|
+
# In text mode, send all output to stdout (so success doesn't appear in error logs)
|
|
34
|
+
use_stderr = format == "json"
|
|
35
|
+
|
|
40
36
|
# Auto-discover and load preflight checks
|
|
41
37
|
packages_registry.autodiscover_modules("preflight", include_app=True)
|
|
42
|
-
|
|
43
38
|
if not quiet:
|
|
44
|
-
click.secho(
|
|
39
|
+
click.secho(
|
|
40
|
+
"Running preflight checks...", dim=True, italic=True, err=use_stderr
|
|
41
|
+
)
|
|
45
42
|
|
|
46
43
|
total_checks = 0
|
|
47
44
|
passed_checks = 0
|
|
@@ -60,23 +57,23 @@ def check_command(deploy, format, quiet):
|
|
|
60
57
|
if format == "text":
|
|
61
58
|
if not quiet:
|
|
62
59
|
# Print check name without newline
|
|
63
|
-
click.echo("Check:", nl=False, err=
|
|
64
|
-
click.secho(f"{check_name} ", bold=True, nl=False, err=
|
|
60
|
+
click.echo("Check:", nl=False, err=use_stderr)
|
|
61
|
+
click.secho(f"{check_name} ", bold=True, nl=False, err=use_stderr)
|
|
65
62
|
|
|
66
63
|
# Determine status icon based on issue severity
|
|
67
64
|
if not visible_issues:
|
|
68
65
|
# No issues - passed
|
|
69
66
|
if not quiet:
|
|
70
|
-
click.secho("✔", fg="green", err=
|
|
67
|
+
click.secho("✔", fg="green", err=use_stderr)
|
|
71
68
|
passed_checks += 1
|
|
72
69
|
else:
|
|
73
70
|
# Has issues - determine icon based on highest severity
|
|
74
71
|
has_errors = any(not issue.warning for issue in visible_issues)
|
|
75
72
|
if not quiet:
|
|
76
73
|
if has_errors:
|
|
77
|
-
click.secho("✗", fg="red", err=
|
|
74
|
+
click.secho("✗", fg="red", err=use_stderr)
|
|
78
75
|
else:
|
|
79
|
-
click.secho("⚠", fg="yellow", err=
|
|
76
|
+
click.secho("⚠", fg="yellow", err=use_stderr)
|
|
80
77
|
|
|
81
78
|
# Print issues with simple indentation
|
|
82
79
|
issues_to_show = (
|
|
@@ -91,26 +88,26 @@ def check_command(deploy, format, quiet):
|
|
|
91
88
|
if quiet:
|
|
92
89
|
# In quiet mode, show check name once, then issues
|
|
93
90
|
if i == 0:
|
|
94
|
-
click.secho(f"{check_name}:", err=
|
|
91
|
+
click.secho(f"{check_name}:", err=use_stderr)
|
|
95
92
|
# Show ID and fix on separate lines with same indentation
|
|
96
93
|
click.secho(
|
|
97
94
|
f" [{issue_type}] {issue.id}:",
|
|
98
95
|
fg=issue_color,
|
|
99
96
|
bold=True,
|
|
100
|
-
err=
|
|
97
|
+
err=use_stderr,
|
|
101
98
|
nl=False,
|
|
102
99
|
)
|
|
103
|
-
click.secho(f" {issue.fix}", err=
|
|
100
|
+
click.secho(f" {issue.fix}", err=use_stderr, dim=True)
|
|
104
101
|
else:
|
|
105
102
|
# Show ID and fix on separate lines with same indentation
|
|
106
103
|
click.secho(
|
|
107
104
|
f" [{issue_type}] {issue.id}: ",
|
|
108
105
|
fg=issue_color,
|
|
109
106
|
bold=True,
|
|
110
|
-
err=
|
|
107
|
+
err=use_stderr,
|
|
111
108
|
nl=False,
|
|
112
109
|
)
|
|
113
|
-
click.secho(f"{issue.fix}", err=
|
|
110
|
+
click.secho(f"{issue.fix}", err=use_stderr, dim=True)
|
|
114
111
|
else:
|
|
115
112
|
# For JSON format, just count passed checks
|
|
116
113
|
if not visible_issues:
|
|
@@ -129,12 +126,12 @@ def check_command(deploy, format, quiet):
|
|
|
129
126
|
|
|
130
127
|
if format == "json":
|
|
131
128
|
# Build JSON output
|
|
132
|
-
results = {"passed": not has_errors, "checks": []}
|
|
129
|
+
results: dict[str, Any] = {"passed": not has_errors, "checks": []}
|
|
133
130
|
|
|
134
131
|
for check_class, check_name, issues in check_results:
|
|
135
132
|
visible_issues = [issue for issue in issues if not issue.is_silenced()]
|
|
136
133
|
|
|
137
|
-
check_result = {
|
|
134
|
+
check_result: dict[str, Any] = {
|
|
138
135
|
"name": check_name,
|
|
139
136
|
"passed": len(visible_issues) == 0,
|
|
140
137
|
"issues": [],
|
|
@@ -195,53 +192,8 @@ def check_command(deploy, format, quiet):
|
|
|
195
192
|
|
|
196
193
|
summary_text = ", ".join(summary_parts) if summary_parts else "no issues"
|
|
197
194
|
|
|
198
|
-
click.secho(f"{icon}{summary_text}", fg=summary_color, err=
|
|
195
|
+
click.secho(f"{icon}{summary_text}", fg=summary_color, err=use_stderr)
|
|
199
196
|
|
|
200
197
|
# Exit with error if there are any errors (not warnings)
|
|
201
198
|
if has_errors:
|
|
202
199
|
sys.exit(1)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@preflight_cli.command("list")
|
|
206
|
-
def list_checks():
|
|
207
|
-
"""List all available preflight checks."""
|
|
208
|
-
packages_registry.autodiscover_modules("preflight", include_app=True)
|
|
209
|
-
|
|
210
|
-
regular = []
|
|
211
|
-
deployment = []
|
|
212
|
-
silenced_checks = settings.PREFLIGHT_SILENCED_CHECKS
|
|
213
|
-
|
|
214
|
-
for name, (check_class, deploy) in sorted(checks_registry.checks.items()):
|
|
215
|
-
# Use class docstring as description
|
|
216
|
-
description = check_class.__doc__ or "No description"
|
|
217
|
-
# Get first line of docstring
|
|
218
|
-
description = description.strip().split("\n")[0]
|
|
219
|
-
|
|
220
|
-
is_silenced = name in silenced_checks
|
|
221
|
-
if deploy:
|
|
222
|
-
deployment.append((name, description, is_silenced))
|
|
223
|
-
else:
|
|
224
|
-
regular.append((name, description, is_silenced))
|
|
225
|
-
|
|
226
|
-
if regular:
|
|
227
|
-
click.echo("Regular checks:")
|
|
228
|
-
for name, description, is_silenced in regular:
|
|
229
|
-
silenced_text = (
|
|
230
|
-
click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
|
|
231
|
-
)
|
|
232
|
-
click.echo(
|
|
233
|
-
f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
if deployment:
|
|
237
|
-
click.echo("\nDeployment checks:")
|
|
238
|
-
for name, description, is_silenced in deployment:
|
|
239
|
-
silenced_text = (
|
|
240
|
-
click.style(" (silenced)", fg="red", dim=True) if is_silenced else ""
|
|
241
|
-
)
|
|
242
|
-
click.echo(
|
|
243
|
-
f" {click.style(name)}: {click.style(description, dim=True)}{silenced_text}"
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
if not regular and not deployment:
|
|
247
|
-
click.echo("No preflight checks found.")
|
plain/cli/print.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import click
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
def print_event(msg, newline=True):
|
|
5
|
-
arrow = click.style("-->", fg=214, bold=True)
|
|
6
|
-
message =
|
|
4
|
+
def print_event(msg: str, newline: bool = True) -> None:
|
|
5
|
+
arrow = click.style("-->", fg=214, bold=True, dim=True)
|
|
6
|
+
message = click.style(msg, dim=True)
|
|
7
7
|
if not newline:
|
|
8
8
|
message += " "
|
|
9
|
-
click.
|
|
9
|
+
click.echo(f"{arrow} {message}", nl=newline)
|
plain/cli/registry.py
CHANGED
|
@@ -1,45 +1,131 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, NamedTuple
|
|
4
|
+
|
|
1
5
|
from plain.packages import packages_registry
|
|
2
6
|
|
|
3
7
|
|
|
8
|
+
class CommandMetadata(NamedTuple):
|
|
9
|
+
"""Metadata about a registered command."""
|
|
10
|
+
|
|
11
|
+
cmd: Any
|
|
12
|
+
shortcut_for: str | None = None
|
|
13
|
+
is_common: bool = False
|
|
14
|
+
|
|
15
|
+
|
|
4
16
|
class CLIRegistry:
|
|
5
|
-
def __init__(self):
|
|
6
|
-
self._commands = {}
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._commands: dict[str, CommandMetadata] = {}
|
|
7
19
|
|
|
8
|
-
def register_command(
|
|
20
|
+
def register_command(
|
|
21
|
+
self,
|
|
22
|
+
cmd: Any,
|
|
23
|
+
name: str,
|
|
24
|
+
shortcut_for: str | None = None,
|
|
25
|
+
is_common: bool = False,
|
|
26
|
+
) -> None:
|
|
9
27
|
"""
|
|
10
28
|
Register a CLI command or group with the specified name.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
cmd: The click command or group to register
|
|
32
|
+
name: The name to register the command under
|
|
33
|
+
shortcut_for: Optional parent command this is a shortcut for (e.g., "models" for migrate)
|
|
34
|
+
is_common: Whether this is a commonly used command to show in the "Common Commands" section
|
|
11
35
|
"""
|
|
12
|
-
self._commands[name] =
|
|
36
|
+
self._commands[name] = CommandMetadata(
|
|
37
|
+
cmd=cmd, shortcut_for=shortcut_for, is_common=is_common
|
|
38
|
+
)
|
|
13
39
|
|
|
14
|
-
def import_modules(self):
|
|
40
|
+
def import_modules(self) -> None:
|
|
15
41
|
"""
|
|
16
42
|
Import modules from installed packages and app to trigger registration.
|
|
17
43
|
"""
|
|
18
44
|
packages_registry.autodiscover_modules("cli", include_app=True)
|
|
19
45
|
|
|
20
|
-
def get_commands(self):
|
|
46
|
+
def get_commands(self) -> dict[str, Any]:
|
|
21
47
|
"""
|
|
22
|
-
Get all registered commands.
|
|
48
|
+
Get all registered commands (just the command objects, not metadata).
|
|
49
|
+
"""
|
|
50
|
+
return {name: metadata.cmd for name, metadata in self._commands.items()}
|
|
51
|
+
|
|
52
|
+
def get_commands_with_metadata(self) -> dict[str, CommandMetadata]:
|
|
53
|
+
"""
|
|
54
|
+
Get all registered commands with their metadata.
|
|
23
55
|
"""
|
|
24
56
|
return self._commands
|
|
25
57
|
|
|
58
|
+
def get_shortcuts(self) -> dict[str, CommandMetadata]:
|
|
59
|
+
"""
|
|
60
|
+
Get only commands that are shortcuts.
|
|
61
|
+
"""
|
|
62
|
+
return {
|
|
63
|
+
name: metadata
|
|
64
|
+
for name, metadata in self._commands.items()
|
|
65
|
+
if metadata.shortcut_for
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
def get_common_commands(self) -> dict[str, CommandMetadata]:
|
|
69
|
+
"""
|
|
70
|
+
Get only commands that are marked as common.
|
|
71
|
+
"""
|
|
72
|
+
return {
|
|
73
|
+
name: metadata
|
|
74
|
+
for name, metadata in self._commands.items()
|
|
75
|
+
if metadata.is_common
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
def get_regular_commands(self) -> dict[str, CommandMetadata]:
|
|
79
|
+
"""
|
|
80
|
+
Get only commands that are not common.
|
|
81
|
+
"""
|
|
82
|
+
return {
|
|
83
|
+
name: metadata
|
|
84
|
+
for name, metadata in self._commands.items()
|
|
85
|
+
if not metadata.is_common
|
|
86
|
+
}
|
|
87
|
+
|
|
26
88
|
|
|
27
89
|
cli_registry = CLIRegistry()
|
|
28
90
|
|
|
29
91
|
|
|
30
|
-
def register_cli(
|
|
92
|
+
def register_cli(
|
|
93
|
+
name: str, shortcut_for: str | None = None, common: bool = False
|
|
94
|
+
) -> Any:
|
|
31
95
|
"""
|
|
32
96
|
Register a CLI command or group with the given name.
|
|
33
97
|
|
|
98
|
+
Args:
|
|
99
|
+
name: The name to register the command under
|
|
100
|
+
shortcut_for: Optional parent command this is a shortcut for.
|
|
101
|
+
For example, @register_cli("migrate", shortcut_for="models")
|
|
102
|
+
indicates that "plain migrate" is a shortcut for "plain models migrate"
|
|
103
|
+
common: Whether this is a commonly used command to show in the "Common Commands" section
|
|
104
|
+
|
|
34
105
|
Usage:
|
|
106
|
+
# Register a regular command group
|
|
35
107
|
@register_cli("users")
|
|
36
108
|
@click.group()
|
|
37
109
|
def users_cli():
|
|
38
110
|
pass
|
|
111
|
+
|
|
112
|
+
# Register a shortcut command
|
|
113
|
+
@register_cli("migrate", shortcut_for="models", common=True)
|
|
114
|
+
@click.command()
|
|
115
|
+
def migrate():
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
# Register a common command
|
|
119
|
+
@register_cli("dev", common=True)
|
|
120
|
+
@click.command()
|
|
121
|
+
def dev():
|
|
122
|
+
pass
|
|
39
123
|
"""
|
|
40
124
|
|
|
41
|
-
def wrapper(cmd):
|
|
42
|
-
cli_registry.register_command(
|
|
125
|
+
def wrapper(cmd: Any) -> Any:
|
|
126
|
+
cli_registry.register_command(
|
|
127
|
+
cmd, name, shortcut_for=shortcut_for, is_common=common
|
|
128
|
+
)
|
|
43
129
|
return cmd
|
|
44
130
|
|
|
45
131
|
return wrapper
|