plain 0.66.0__py3-none-any.whl → 0.101.2__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 -0
- plain/README.md +1 -1
- 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 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- 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 -13
- 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 +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- 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/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- 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 +56 -40
- plain/urls/resolvers.py +38 -28
- 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.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.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/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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 skills 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 "/skills/" 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 "/skills/" 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,7 +62,7 @@ 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(self, relative_to=None):
|
|
65
|
+
def print(self, relative_to: Path | None = None) -> None:
|
|
62
66
|
for doc in self.docs:
|
|
63
67
|
if relative_to:
|
|
64
68
|
display_path = doc.relative_to(relative_to)
|
|
@@ -81,7 +85,7 @@ class LLMDocs:
|
|
|
81
85
|
click.echo()
|
|
82
86
|
|
|
83
87
|
@staticmethod
|
|
84
|
-
def symbolicate(file_path: Path):
|
|
88
|
+
def symbolicate(file_path: Path) -> str:
|
|
85
89
|
if "internal" in str(file_path).split("/"):
|
|
86
90
|
return ""
|
|
87
91
|
|
|
@@ -89,8 +93,16 @@ class LLMDocs:
|
|
|
89
93
|
|
|
90
94
|
parsed = ast.parse(source)
|
|
91
95
|
|
|
92
|
-
def should_skip(node):
|
|
93
|
-
if isinstance(node, ast.ClassDef
|
|
96
|
+
def should_skip(node: ast.AST) -> bool:
|
|
97
|
+
if isinstance(node, ast.ClassDef):
|
|
98
|
+
if any(
|
|
99
|
+
isinstance(d, ast.Name) and d.id == "internalcode"
|
|
100
|
+
for d in node.decorator_list
|
|
101
|
+
):
|
|
102
|
+
return True
|
|
103
|
+
if node.name.startswith("_"):
|
|
104
|
+
return True
|
|
105
|
+
elif isinstance(node, ast.FunctionDef):
|
|
94
106
|
if any(
|
|
95
107
|
isinstance(d, ast.Name) and d.id == "internalcode"
|
|
96
108
|
for d in node.decorator_list
|
|
@@ -104,7 +116,7 @@ class LLMDocs:
|
|
|
104
116
|
return True
|
|
105
117
|
return False
|
|
106
118
|
|
|
107
|
-
def process_node(node, indent=0):
|
|
119
|
+
def process_node(node: ast.AST, indent: int = 0) -> list[str]:
|
|
108
120
|
lines = []
|
|
109
121
|
prefix = " " * indent
|
|
110
122
|
|
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,126 +1,199 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
1
5
|
import click
|
|
2
6
|
|
|
3
7
|
from plain import preflight
|
|
8
|
+
from plain.cli.runtime import common_command
|
|
4
9
|
from plain.packages import packages_registry
|
|
5
10
|
|
|
6
11
|
|
|
12
|
+
@common_command
|
|
7
13
|
@click.command("preflight")
|
|
8
|
-
@click.argument("package_label", nargs=-1)
|
|
9
14
|
@click.option(
|
|
10
15
|
"--deploy",
|
|
11
16
|
is_flag=True,
|
|
12
|
-
help="
|
|
17
|
+
help="Include deployment checks.",
|
|
13
18
|
)
|
|
14
19
|
@click.option(
|
|
15
|
-
"--
|
|
16
|
-
default="
|
|
17
|
-
type=click.Choice(["
|
|
18
|
-
help="
|
|
20
|
+
"--format",
|
|
21
|
+
default="text",
|
|
22
|
+
type=click.Choice(["text", "json"]),
|
|
23
|
+
help="Output format (default: text)",
|
|
19
24
|
)
|
|
20
25
|
@click.option(
|
|
21
|
-
"--
|
|
26
|
+
"--quiet",
|
|
22
27
|
is_flag=True,
|
|
23
|
-
help="
|
|
28
|
+
help="Hide progress output and warnings, only show errors.",
|
|
24
29
|
)
|
|
25
|
-
def
|
|
26
|
-
"""
|
|
27
|
-
Use
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
and don't raise an exception.
|
|
31
|
-
"""
|
|
32
|
-
include_deployment_checks = deploy
|
|
33
|
-
|
|
34
|
-
if package_label:
|
|
35
|
-
package_configs = [
|
|
36
|
-
packages_registry.get_package_config(label) for label in package_label
|
|
37
|
-
]
|
|
38
|
-
else:
|
|
39
|
-
package_configs = None
|
|
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"
|
|
40
35
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
36
|
+
# Auto-discover and load preflight checks
|
|
37
|
+
packages_registry.autodiscover_modules("preflight", include_app=True)
|
|
38
|
+
if not quiet:
|
|
39
|
+
click.secho(
|
|
40
|
+
"Running preflight checks...", dim=True, italic=True, err=use_stderr
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
total_checks = 0
|
|
44
|
+
passed_checks = 0
|
|
45
|
+
check_results = []
|
|
46
|
+
|
|
47
|
+
# Run checks and collect results
|
|
48
|
+
for check_class, check_name, issues in preflight.run_checks(
|
|
49
|
+
include_deploy_checks=deploy,
|
|
50
|
+
):
|
|
51
|
+
total_checks += 1
|
|
52
|
+
|
|
53
|
+
# Filter out silenced issues
|
|
54
|
+
visible_issues = [issue for issue in issues if not issue.is_silenced()]
|
|
55
|
+
|
|
56
|
+
# For text format, show real-time progress
|
|
57
|
+
if format == "text":
|
|
58
|
+
if not quiet:
|
|
59
|
+
# Print check name without newline
|
|
60
|
+
click.echo("Check:", nl=False, err=use_stderr)
|
|
61
|
+
click.secho(f"{check_name} ", bold=True, nl=False, err=use_stderr)
|
|
46
62
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if preflight.ERROR <= e.level < preflight.CRITICAL and not e.is_silenced()
|
|
68
|
-
]
|
|
69
|
-
criticals = [
|
|
70
|
-
e
|
|
71
|
-
for e in all_issues
|
|
72
|
-
if preflight.CRITICAL <= e.level and not e.is_silenced()
|
|
73
|
-
]
|
|
74
|
-
sorted_issues = [
|
|
75
|
-
(criticals, "CRITICALS"),
|
|
76
|
-
(errors, "ERRORS"),
|
|
77
|
-
(warnings, "WARNINGS"),
|
|
78
|
-
(infos, "INFOS"),
|
|
79
|
-
(debugs, "DEBUGS"),
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
for issues, group_name in sorted_issues:
|
|
83
|
-
if issues:
|
|
84
|
-
visible_issue_count += len(issues)
|
|
85
|
-
formatted = (
|
|
86
|
-
click.style(str(e), fg="red")
|
|
87
|
-
if e.is_serious()
|
|
88
|
-
else click.style(str(e), fg="yellow")
|
|
89
|
-
for e in issues
|
|
63
|
+
# Determine status icon based on issue severity
|
|
64
|
+
if not visible_issues:
|
|
65
|
+
# No issues - passed
|
|
66
|
+
if not quiet:
|
|
67
|
+
click.secho("✔", fg="green", err=use_stderr)
|
|
68
|
+
passed_checks += 1
|
|
69
|
+
else:
|
|
70
|
+
# Has issues - determine icon based on highest severity
|
|
71
|
+
has_errors = any(not issue.warning for issue in visible_issues)
|
|
72
|
+
if not quiet:
|
|
73
|
+
if has_errors:
|
|
74
|
+
click.secho("✗", fg="red", err=use_stderr)
|
|
75
|
+
else:
|
|
76
|
+
click.secho("⚠", fg="yellow", err=use_stderr)
|
|
77
|
+
|
|
78
|
+
# Print issues with simple indentation
|
|
79
|
+
issues_to_show = (
|
|
80
|
+
visible_issues
|
|
81
|
+
if not quiet
|
|
82
|
+
else [issue for issue in visible_issues if not issue.warning]
|
|
90
83
|
)
|
|
91
|
-
|
|
92
|
-
|
|
84
|
+
for i, issue in enumerate(issues_to_show):
|
|
85
|
+
issue_color = "red" if not issue.warning else "yellow"
|
|
86
|
+
issue_type = "ERROR" if not issue.warning else "WARNING"
|
|
93
87
|
|
|
94
|
-
|
|
95
|
-
|
|
88
|
+
if quiet:
|
|
89
|
+
# In quiet mode, show check name once, then issues
|
|
90
|
+
if i == 0:
|
|
91
|
+
click.secho(f"{check_name}:", err=use_stderr)
|
|
92
|
+
# Show ID and fix on separate lines with same indentation
|
|
93
|
+
click.secho(
|
|
94
|
+
f" [{issue_type}] {issue.id}:",
|
|
95
|
+
fg=issue_color,
|
|
96
|
+
bold=True,
|
|
97
|
+
err=use_stderr,
|
|
98
|
+
nl=False,
|
|
99
|
+
)
|
|
100
|
+
click.secho(f" {issue.fix}", err=use_stderr, dim=True)
|
|
101
|
+
else:
|
|
102
|
+
# Show ID and fix on separate lines with same indentation
|
|
103
|
+
click.secho(
|
|
104
|
+
f" [{issue_type}] {issue.id}: ",
|
|
105
|
+
fg=issue_color,
|
|
106
|
+
bold=True,
|
|
107
|
+
err=use_stderr,
|
|
108
|
+
nl=False,
|
|
109
|
+
)
|
|
110
|
+
click.secho(f"{issue.fix}", err=use_stderr, dim=True)
|
|
111
|
+
else:
|
|
112
|
+
# For JSON format, just count passed checks
|
|
113
|
+
if not visible_issues:
|
|
114
|
+
passed_checks += 1
|
|
96
115
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
check_results.append((check_class, check_name, issues))
|
|
117
|
+
|
|
118
|
+
# Output results based on format
|
|
119
|
+
|
|
120
|
+
# Get all issues from check_results instead of maintaining separate list
|
|
121
|
+
all_issues = [issue for _, _, issues in check_results for issue in issues]
|
|
122
|
+
# Errors (non-warnings) cause preflight to fail
|
|
123
|
+
has_errors = any(
|
|
124
|
+
not issue.warning and not issue.is_silenced() for issue in all_issues
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
if format == "json":
|
|
128
|
+
# Build JSON output
|
|
129
|
+
results: dict[str, Any] = {"passed": not has_errors, "checks": []}
|
|
130
|
+
|
|
131
|
+
for check_class, check_name, issues in check_results:
|
|
132
|
+
visible_issues = [issue for issue in issues if not issue.is_silenced()]
|
|
133
|
+
|
|
134
|
+
check_result: dict[str, Any] = {
|
|
135
|
+
"name": check_name,
|
|
136
|
+
"passed": len(visible_issues) == 0,
|
|
137
|
+
"issues": [],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for issue in visible_issues:
|
|
141
|
+
issue_data = {
|
|
142
|
+
"id": issue.id,
|
|
143
|
+
"warning": issue.warning,
|
|
144
|
+
"fix": issue.fix,
|
|
145
|
+
"obj": str(issue.obj) if issue.obj is not None else None,
|
|
146
|
+
}
|
|
147
|
+
check_result["issues"].append(issue_data)
|
|
148
|
+
|
|
149
|
+
results["checks"].append(check_result)
|
|
150
|
+
|
|
151
|
+
click.echo(json.dumps(results, indent=2))
|
|
112
152
|
else:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
153
|
+
# Text format summary
|
|
154
|
+
if not quiet:
|
|
155
|
+
click.echo()
|
|
156
|
+
|
|
157
|
+
# Calculate warning and error counts
|
|
158
|
+
warning_count = sum(
|
|
159
|
+
1
|
|
160
|
+
for _, _, issues in check_results
|
|
161
|
+
if issues
|
|
162
|
+
and not any(
|
|
163
|
+
not issue.warning for issue in issues if not issue.is_silenced()
|
|
122
164
|
)
|
|
123
|
-
|
|
124
|
-
|
|
165
|
+
)
|
|
166
|
+
error_count = sum(
|
|
167
|
+
1
|
|
168
|
+
for _, _, issues in check_results
|
|
169
|
+
if issues
|
|
170
|
+
and any(not issue.warning for issue in issues if not issue.is_silenced())
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Build colored summary parts
|
|
174
|
+
summary_parts = []
|
|
175
|
+
|
|
176
|
+
if passed_checks > 0:
|
|
177
|
+
summary_parts.append(click.style(f"{passed_checks} passed", fg="green"))
|
|
178
|
+
|
|
179
|
+
if warning_count > 0:
|
|
180
|
+
summary_parts.append(click.style(f"{warning_count} warnings", fg="yellow"))
|
|
181
|
+
|
|
182
|
+
if error_count > 0:
|
|
183
|
+
summary_parts.append(click.style(f"{error_count} errors", fg="red"))
|
|
184
|
+
|
|
185
|
+
# Show checkmark if successful (no errors)
|
|
186
|
+
if not has_errors:
|
|
187
|
+
icon = click.style("✔ ", fg="green")
|
|
188
|
+
summary_color = "green"
|
|
125
189
|
else:
|
|
126
|
-
|
|
190
|
+
icon = ""
|
|
191
|
+
summary_color = None
|
|
192
|
+
|
|
193
|
+
summary_text = ", ".join(summary_parts) if summary_parts else "no issues"
|
|
194
|
+
|
|
195
|
+
click.secho(f"{icon}{summary_text}", fg=summary_color, err=use_stderr)
|
|
196
|
+
|
|
197
|
+
# Exit with error if there are any errors (not warnings)
|
|
198
|
+
if has_errors:
|
|
199
|
+
sys.exit(1)
|
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,62 +1,131 @@
|
|
|
1
|
-
from
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, NamedTuple
|
|
3
4
|
|
|
4
5
|
from plain.packages import packages_registry
|
|
5
6
|
|
|
6
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
|
+
|
|
7
16
|
class CLIRegistry:
|
|
8
|
-
def __init__(self):
|
|
9
|
-
self._commands = {}
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._commands: dict[str, CommandMetadata] = {}
|
|
10
19
|
|
|
11
|
-
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:
|
|
12
27
|
"""
|
|
13
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
|
|
14
35
|
"""
|
|
15
|
-
self._commands[name] =
|
|
36
|
+
self._commands[name] = CommandMetadata(
|
|
37
|
+
cmd=cmd, shortcut_for=shortcut_for, is_common=is_common
|
|
38
|
+
)
|
|
16
39
|
|
|
17
|
-
def import_modules(self):
|
|
40
|
+
def import_modules(self) -> None:
|
|
18
41
|
"""
|
|
19
42
|
Import modules from installed packages and app to trigger registration.
|
|
20
43
|
"""
|
|
21
|
-
|
|
22
|
-
for package_config in packages_registry.get_package_configs():
|
|
23
|
-
import_name = f"{package_config.name}.cli"
|
|
24
|
-
try:
|
|
25
|
-
import_module(import_name)
|
|
26
|
-
except ModuleNotFoundError:
|
|
27
|
-
pass
|
|
44
|
+
packages_registry.autodiscover_modules("cli", include_app=True)
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
except ModuleNotFoundError:
|
|
35
|
-
pass
|
|
46
|
+
def get_commands(self) -> dict[str, Any]:
|
|
47
|
+
"""
|
|
48
|
+
Get all registered commands (just the command objects, not metadata).
|
|
49
|
+
"""
|
|
50
|
+
return {name: metadata.cmd for name, metadata in self._commands.items()}
|
|
36
51
|
|
|
37
|
-
def
|
|
52
|
+
def get_commands_with_metadata(self) -> dict[str, CommandMetadata]:
|
|
38
53
|
"""
|
|
39
|
-
Get all registered commands.
|
|
54
|
+
Get all registered commands with their metadata.
|
|
40
55
|
"""
|
|
41
56
|
return self._commands
|
|
42
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
|
+
|
|
43
88
|
|
|
44
89
|
cli_registry = CLIRegistry()
|
|
45
90
|
|
|
46
91
|
|
|
47
|
-
def register_cli(
|
|
92
|
+
def register_cli(
|
|
93
|
+
name: str, shortcut_for: str | None = None, common: bool = False
|
|
94
|
+
) -> Any:
|
|
48
95
|
"""
|
|
49
96
|
Register a CLI command or group with the given name.
|
|
50
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
|
+
|
|
51
105
|
Usage:
|
|
106
|
+
# Register a regular command group
|
|
52
107
|
@register_cli("users")
|
|
53
108
|
@click.group()
|
|
54
109
|
def users_cli():
|
|
55
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
|
|
56
123
|
"""
|
|
57
124
|
|
|
58
|
-
def wrapper(cmd):
|
|
59
|
-
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
|
+
)
|
|
60
129
|
return cmd
|
|
61
130
|
|
|
62
131
|
return wrapper
|