plain 0.68.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 +656 -1
- 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 -36
- 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 +110 -26
- 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 +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/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 +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.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.68.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/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.101.2.dist-info}/licenses/LICENSE +0 -0
plain/cli/scaffold.py
CHANGED
|
@@ -7,13 +7,8 @@ import plain.runtime
|
|
|
7
7
|
|
|
8
8
|
@click.command()
|
|
9
9
|
@click.argument("package_name")
|
|
10
|
-
def create(package_name):
|
|
11
|
-
"""
|
|
12
|
-
Create a new local package.
|
|
13
|
-
|
|
14
|
-
The PACKAGE_NAME is typically a plural noun, like "users" or "posts",
|
|
15
|
-
where you might create a "User" or "Post" model inside of the package.
|
|
16
|
-
"""
|
|
10
|
+
def create(package_name: str) -> None:
|
|
11
|
+
"""Create a new local package"""
|
|
17
12
|
package_dir = plain.runtime.APP_PATH / package_name
|
|
18
13
|
package_dir.mkdir(exist_ok=True)
|
|
19
14
|
|
plain/cli/server.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from plain.cli.runtime import without_runtime_setup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_workers(ctx: click.Context, param: click.Parameter, value: str) -> int:
|
|
9
|
+
"""Parse workers value - accepts int or 'auto' for CPU count."""
|
|
10
|
+
if value == "auto":
|
|
11
|
+
return os.cpu_count() or 1
|
|
12
|
+
return int(value)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@without_runtime_setup
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.option(
|
|
18
|
+
"--bind",
|
|
19
|
+
"-b",
|
|
20
|
+
multiple=True,
|
|
21
|
+
default=["127.0.0.1:8000"],
|
|
22
|
+
help="Address to bind to (HOST:PORT, can be used multiple times)",
|
|
23
|
+
)
|
|
24
|
+
@click.option(
|
|
25
|
+
"--threads",
|
|
26
|
+
type=int,
|
|
27
|
+
default=1,
|
|
28
|
+
help="Number of threads per worker",
|
|
29
|
+
show_default=True,
|
|
30
|
+
)
|
|
31
|
+
@click.option(
|
|
32
|
+
"--workers",
|
|
33
|
+
"-w",
|
|
34
|
+
type=str,
|
|
35
|
+
default="1",
|
|
36
|
+
envvar="WEB_CONCURRENCY",
|
|
37
|
+
callback=parse_workers,
|
|
38
|
+
help="Number of worker processes (or 'auto' for CPU count)",
|
|
39
|
+
show_default=True,
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--timeout",
|
|
43
|
+
"-t",
|
|
44
|
+
type=int,
|
|
45
|
+
default=30,
|
|
46
|
+
help="Worker timeout in seconds",
|
|
47
|
+
show_default=True,
|
|
48
|
+
)
|
|
49
|
+
@click.option(
|
|
50
|
+
"--certfile",
|
|
51
|
+
type=click.Path(exists=True),
|
|
52
|
+
help="SSL certificate file",
|
|
53
|
+
)
|
|
54
|
+
@click.option(
|
|
55
|
+
"--keyfile",
|
|
56
|
+
type=click.Path(exists=True),
|
|
57
|
+
help="SSL key file",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--log-level",
|
|
61
|
+
default="info",
|
|
62
|
+
type=click.Choice(["debug", "info", "warning", "error", "critical"]),
|
|
63
|
+
help="Logging level",
|
|
64
|
+
show_default=True,
|
|
65
|
+
)
|
|
66
|
+
@click.option(
|
|
67
|
+
"--reload",
|
|
68
|
+
is_flag=True,
|
|
69
|
+
help="Restart workers when code changes (dev only)",
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--access-log",
|
|
73
|
+
default="-",
|
|
74
|
+
help="Access log file (use '-' for stdout)",
|
|
75
|
+
show_default=True,
|
|
76
|
+
)
|
|
77
|
+
@click.option(
|
|
78
|
+
"--error-log",
|
|
79
|
+
default="-",
|
|
80
|
+
help="Error log file (use '-' for stderr)",
|
|
81
|
+
show_default=True,
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--log-format",
|
|
85
|
+
default="%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
|
86
|
+
help="Log format string (applies to both error and access logs)",
|
|
87
|
+
show_default=True,
|
|
88
|
+
)
|
|
89
|
+
@click.option(
|
|
90
|
+
"--access-log-format",
|
|
91
|
+
help="Access log format string (HTTP request details)",
|
|
92
|
+
default='%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"',
|
|
93
|
+
show_default=True,
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--max-requests",
|
|
97
|
+
type=int,
|
|
98
|
+
default=0,
|
|
99
|
+
help="Max requests before worker restart (0=disabled)",
|
|
100
|
+
show_default=True,
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--pidfile",
|
|
104
|
+
type=click.Path(),
|
|
105
|
+
help="PID file path",
|
|
106
|
+
)
|
|
107
|
+
def server(
|
|
108
|
+
bind: tuple[str, ...],
|
|
109
|
+
threads: int,
|
|
110
|
+
workers: int,
|
|
111
|
+
timeout: int,
|
|
112
|
+
certfile: str | None,
|
|
113
|
+
keyfile: str | None,
|
|
114
|
+
log_level: str,
|
|
115
|
+
reload: bool,
|
|
116
|
+
access_log: str,
|
|
117
|
+
error_log: str,
|
|
118
|
+
log_format: str,
|
|
119
|
+
access_log_format: str,
|
|
120
|
+
max_requests: int,
|
|
121
|
+
pidfile: str | None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Production-ready WSGI server"""
|
|
124
|
+
from plain.runtime import settings
|
|
125
|
+
|
|
126
|
+
# Show settings loaded from environment
|
|
127
|
+
if env_settings := settings.get_env_settings():
|
|
128
|
+
click.secho("Settings from env:", dim=True)
|
|
129
|
+
for name, defn in env_settings:
|
|
130
|
+
click.secho(
|
|
131
|
+
f" {defn.env_var_name} -> {name}={defn.display_value()}", dim=True
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
from plain.server import ServerApplication
|
|
135
|
+
from plain.server.config import Config
|
|
136
|
+
|
|
137
|
+
cfg = Config(
|
|
138
|
+
bind=list(bind),
|
|
139
|
+
threads=threads,
|
|
140
|
+
workers=workers,
|
|
141
|
+
timeout=timeout,
|
|
142
|
+
max_requests=max_requests,
|
|
143
|
+
reload=reload,
|
|
144
|
+
pidfile=pidfile,
|
|
145
|
+
certfile=certfile,
|
|
146
|
+
keyfile=keyfile,
|
|
147
|
+
loglevel=log_level,
|
|
148
|
+
accesslog=access_log,
|
|
149
|
+
errorlog=error_log,
|
|
150
|
+
log_format=log_format,
|
|
151
|
+
access_log_format=access_log_format,
|
|
152
|
+
)
|
|
153
|
+
ServerApplication(cfg=cfg).run()
|
plain/cli/settings.py
CHANGED
|
@@ -3,58 +3,62 @@ import click
|
|
|
3
3
|
import plain.runtime
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
@click.
|
|
6
|
+
@click.group()
|
|
7
|
+
def settings() -> None:
|
|
8
|
+
"""View and inspect settings"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@settings.command()
|
|
7
13
|
@click.argument("setting_name")
|
|
8
|
-
def
|
|
9
|
-
"""
|
|
14
|
+
def get(setting_name: str) -> None:
|
|
15
|
+
"""Get the value of a specific setting"""
|
|
10
16
|
try:
|
|
11
|
-
|
|
12
|
-
click.echo(
|
|
17
|
+
value = getattr(plain.runtime.settings, setting_name)
|
|
18
|
+
click.echo(value)
|
|
13
19
|
except AttributeError:
|
|
14
20
|
click.secho(f'Setting "{setting_name}" not found', fg="red")
|
|
15
21
|
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
#
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# console = Console()
|
|
60
|
-
# console.print(table)
|
|
23
|
+
@settings.command(name="list")
|
|
24
|
+
def list_settings() -> None:
|
|
25
|
+
"""List all settings with their sources"""
|
|
26
|
+
if not (items := plain.runtime.settings.get_settings()):
|
|
27
|
+
click.echo("No settings configured.")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# Calculate column widths
|
|
31
|
+
max_name = max(len(name) for name, _ in items)
|
|
32
|
+
max_source = max(len(defn.env_var_name or defn.source) for _, defn in items)
|
|
33
|
+
|
|
34
|
+
# Print header
|
|
35
|
+
header = (
|
|
36
|
+
click.style(f"{'Setting':<{max_name}}", bold=True)
|
|
37
|
+
+ " "
|
|
38
|
+
+ click.style(f"{'Source':<{max_source}}", bold=True)
|
|
39
|
+
+ " "
|
|
40
|
+
+ click.style("Value", bold=True)
|
|
41
|
+
)
|
|
42
|
+
click.echo(header)
|
|
43
|
+
click.secho("-" * (max_name + max_source + 10), dim=True)
|
|
44
|
+
|
|
45
|
+
# Print each setting
|
|
46
|
+
for name, defn in items:
|
|
47
|
+
source_info = defn.env_var_name or defn.source
|
|
48
|
+
value = defn.display_value()
|
|
49
|
+
|
|
50
|
+
# Style based on source
|
|
51
|
+
if defn.source == "env":
|
|
52
|
+
source_styled = click.style(f"{source_info:<{max_source}}", fg="green")
|
|
53
|
+
elif defn.source == "explicit":
|
|
54
|
+
source_styled = click.style(f"{source_info:<{max_source}}", fg="cyan")
|
|
55
|
+
else:
|
|
56
|
+
source_styled = click.style(f"{source_info:<{max_source}}", dim=True)
|
|
57
|
+
|
|
58
|
+
# Style secret values
|
|
59
|
+
if defn.is_secret:
|
|
60
|
+
value_styled = click.style(value, dim=True)
|
|
61
|
+
else:
|
|
62
|
+
value_styled = value
|
|
63
|
+
|
|
64
|
+
click.echo(f"{name:<{max_name}} {source_styled} {value_styled}")
|
plain/cli/shell.py
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import os
|
|
2
4
|
import subprocess
|
|
3
5
|
import sys
|
|
4
6
|
|
|
5
7
|
import click
|
|
6
8
|
|
|
9
|
+
from plain.cli.runtime import common_command
|
|
10
|
+
|
|
7
11
|
|
|
12
|
+
@common_command
|
|
8
13
|
@click.command()
|
|
9
14
|
@click.option(
|
|
10
15
|
"-i",
|
|
@@ -17,11 +22,8 @@ import click
|
|
|
17
22
|
"--command",
|
|
18
23
|
help="Execute the given command and exit.",
|
|
19
24
|
)
|
|
20
|
-
def shell(interface, command):
|
|
21
|
-
"""
|
|
22
|
-
Runs a Python interactive interpreter. Tries to use IPython or
|
|
23
|
-
bpython, if one of them is available.
|
|
24
|
-
"""
|
|
25
|
+
def shell(interface: str | None, command: str | None) -> None:
|
|
26
|
+
"""Interactive Python shell"""
|
|
25
27
|
|
|
26
28
|
if command:
|
|
27
29
|
# Execute the command and exit
|
|
@@ -32,13 +34,14 @@ def shell(interface, command):
|
|
|
32
34
|
sys.exit(result.returncode)
|
|
33
35
|
return
|
|
34
36
|
|
|
37
|
+
interface_list: list[str]
|
|
35
38
|
if interface:
|
|
36
|
-
|
|
39
|
+
interface_list = [interface]
|
|
37
40
|
else:
|
|
38
41
|
|
|
39
|
-
def get_default_interface():
|
|
42
|
+
def get_default_interface() -> list[str]:
|
|
40
43
|
try:
|
|
41
|
-
import IPython # noqa
|
|
44
|
+
import IPython # noqa: F401 # type: ignore[import-not-found]
|
|
42
45
|
|
|
43
46
|
return ["python", "-m", "IPython"]
|
|
44
47
|
except ImportError:
|
|
@@ -46,10 +49,10 @@ def shell(interface, command):
|
|
|
46
49
|
|
|
47
50
|
return ["python"]
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
interface_list = get_default_interface()
|
|
50
53
|
|
|
51
54
|
result = subprocess.run(
|
|
52
|
-
|
|
55
|
+
interface_list,
|
|
53
56
|
env={
|
|
54
57
|
"PYTHONSTARTUP": os.path.join(os.path.dirname(__file__), "startup.py"),
|
|
55
58
|
**os.environ,
|
|
@@ -61,8 +64,8 @@ def shell(interface, command):
|
|
|
61
64
|
|
|
62
65
|
@click.command()
|
|
63
66
|
@click.argument("script", nargs=1, type=click.Path(exists=True))
|
|
64
|
-
def run(script):
|
|
65
|
-
"""
|
|
67
|
+
def run(script: str) -> None:
|
|
68
|
+
"""Execute Python scripts with app context"""
|
|
66
69
|
before_script = "import plain.runtime; plain.runtime.setup()"
|
|
67
70
|
command = f"{before_script}; exec(open('{script}').read())"
|
|
68
71
|
result = subprocess.run(["python", "-c", command])
|
plain/cli/startup.py
CHANGED
|
@@ -3,19 +3,19 @@ import plain.runtime
|
|
|
3
3
|
plain.runtime.setup()
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def print_bold(s):
|
|
6
|
+
def print_bold(s: str) -> None:
|
|
7
7
|
print("\033[1m", end="")
|
|
8
8
|
print(s)
|
|
9
9
|
print("\033[0m", end="")
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def print_italic(s):
|
|
12
|
+
def print_italic(s: str) -> None:
|
|
13
13
|
print("\x1b[3m", end="")
|
|
14
14
|
print(s)
|
|
15
15
|
print("\x1b[0m", end="")
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
def print_dim(s):
|
|
18
|
+
def print_dim(s: str) -> None:
|
|
19
19
|
print("\x1b[2m", end="")
|
|
20
20
|
print(s)
|
|
21
21
|
print("\x1b[0m", end="")
|
|
@@ -29,12 +29,13 @@ if shell_import := plain.runtime.settings.SHELL_IMPORT:
|
|
|
29
29
|
print_bold(f"Importing {shell_import}")
|
|
30
30
|
module = import_module(shell_import)
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
if module.__file__:
|
|
33
|
+
with open(module.__file__) as f:
|
|
34
|
+
contents = f.read()
|
|
35
|
+
for line in contents.splitlines():
|
|
36
|
+
print_dim(f"{line}")
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
print()
|
|
38
39
|
|
|
39
40
|
# Emulate `from module import *`
|
|
40
41
|
names = getattr(
|
plain/cli/upgrade.py
CHANGED
|
@@ -5,68 +5,42 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import click
|
|
7
7
|
|
|
8
|
-
from .
|
|
8
|
+
from .runtime import without_runtime_setup
|
|
9
9
|
|
|
10
10
|
LOCK_FILE = Path("uv.lock")
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
@without_runtime_setup
|
|
13
14
|
@click.command()
|
|
14
15
|
@click.argument("packages", nargs=-1)
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
)
|
|
18
|
-
@click.option(
|
|
19
|
-
"--agent-command",
|
|
20
|
-
envvar="PLAIN_AGENT_COMMAND",
|
|
21
|
-
help="Run command with generated prompt",
|
|
22
|
-
)
|
|
23
|
-
@click.option(
|
|
24
|
-
"--print",
|
|
25
|
-
"print_only",
|
|
26
|
-
is_flag=True,
|
|
27
|
-
help="Print the prompt without running the agent",
|
|
28
|
-
)
|
|
29
|
-
def upgrade(
|
|
30
|
-
packages: tuple[str, ...],
|
|
31
|
-
diff: bool,
|
|
32
|
-
agent_command: str | None = None,
|
|
33
|
-
print_only: bool = False,
|
|
34
|
-
) -> None:
|
|
35
|
-
"""Upgrade Plain packages with the help of an agent."""
|
|
16
|
+
def upgrade(packages: tuple[str, ...]) -> None:
|
|
17
|
+
"""Upgrade Plain packages"""
|
|
36
18
|
if not packages:
|
|
37
|
-
click.secho("Getting installed packages...", bold=True
|
|
19
|
+
click.secho("Getting installed packages...", bold=True)
|
|
38
20
|
packages = tuple(sorted(get_installed_plain_packages()))
|
|
39
21
|
for pkg in packages:
|
|
40
|
-
click.secho(f"- {click.style(pkg, fg='yellow')}"
|
|
41
|
-
click.echo(
|
|
22
|
+
click.secho(f"- {click.style(pkg, fg='yellow')}")
|
|
23
|
+
click.echo()
|
|
42
24
|
|
|
43
25
|
if not packages:
|
|
44
26
|
raise click.UsageError("No plain packages found or specified.")
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
before_after = versions_from_diff(packages)
|
|
48
|
-
else:
|
|
49
|
-
before_after = upgrade_packages(packages)
|
|
28
|
+
before_after = upgrade_packages(packages)
|
|
50
29
|
|
|
51
|
-
#
|
|
52
|
-
|
|
30
|
+
# Show what was upgraded
|
|
31
|
+
upgraded = {
|
|
53
32
|
pkg: versions
|
|
54
33
|
for pkg, versions in before_after.items()
|
|
55
34
|
if versions[0] != versions[1]
|
|
56
35
|
}
|
|
57
36
|
|
|
58
|
-
if not
|
|
59
|
-
click.secho(
|
|
60
|
-
"No packages were upgraded. If uv.lock has already been updated, use --diff instead.",
|
|
61
|
-
fg="green",
|
|
62
|
-
err=True,
|
|
63
|
-
)
|
|
37
|
+
if not upgraded:
|
|
38
|
+
click.secho("All packages already at latest version.", fg="green")
|
|
64
39
|
return
|
|
65
40
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
raise click.Abort()
|
|
41
|
+
click.secho("Upgraded packages:", bold=True)
|
|
42
|
+
for pkg, (before, after) in upgraded.items():
|
|
43
|
+
click.echo(f" {pkg}: {before} -> {after}")
|
|
70
44
|
|
|
71
45
|
|
|
72
46
|
def get_installed_plain_packages() -> list[str]:
|
|
@@ -90,29 +64,6 @@ def parse_lock_versions(lock_text: str, packages: set[str]) -> dict[str, str]:
|
|
|
90
64
|
return versions
|
|
91
65
|
|
|
92
66
|
|
|
93
|
-
def versions_from_diff(
|
|
94
|
-
packages: tuple[str, ...],
|
|
95
|
-
) -> dict[str, tuple[str | None, str | None]]:
|
|
96
|
-
result = subprocess.run(
|
|
97
|
-
["git", "status", "--porcelain", str(LOCK_FILE)], capture_output=True, text=True
|
|
98
|
-
)
|
|
99
|
-
if not result.stdout.strip():
|
|
100
|
-
raise click.UsageError(
|
|
101
|
-
"--diff specified but uv.lock has no uncommitted changes"
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
prev_text = subprocess.run(
|
|
105
|
-
["git", "show", f"HEAD:{LOCK_FILE}"], capture_output=True, text=True, check=True
|
|
106
|
-
).stdout
|
|
107
|
-
current_text = LOCK_FILE.read_text()
|
|
108
|
-
|
|
109
|
-
packages_set = set(packages)
|
|
110
|
-
before = parse_lock_versions(prev_text, packages_set)
|
|
111
|
-
after = parse_lock_versions(current_text, packages_set)
|
|
112
|
-
|
|
113
|
-
return {pkg: (before.get(pkg), after.get(pkg)) for pkg in packages}
|
|
114
|
-
|
|
115
|
-
|
|
116
67
|
def upgrade_packages(
|
|
117
68
|
packages: tuple[str, ...],
|
|
118
69
|
) -> dict[str, tuple[str | None, str | None]]:
|
|
@@ -122,47 +73,9 @@ def upgrade_packages(
|
|
|
122
73
|
for pkg in packages:
|
|
123
74
|
upgrade_args.extend(["--upgrade-package", pkg])
|
|
124
75
|
|
|
125
|
-
click.secho("Upgrading with uv sync...", bold=True
|
|
76
|
+
click.secho("Upgrading with uv sync...", bold=True)
|
|
126
77
|
subprocess.run(upgrade_args, check=True, stdout=sys.stderr)
|
|
127
|
-
click.echo(
|
|
78
|
+
click.echo()
|
|
128
79
|
|
|
129
80
|
after = parse_lock_versions(LOCK_FILE.read_text(), set(packages))
|
|
130
81
|
return {pkg: (before.get(pkg), after.get(pkg)) for pkg in packages}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def build_prompt(before_after: dict[str, tuple[str | None, str | None]]) -> str:
|
|
134
|
-
lines = [
|
|
135
|
-
"These packages have been updated and may require additional changes to the code:",
|
|
136
|
-
"",
|
|
137
|
-
]
|
|
138
|
-
for pkg, (before, after) in before_after.items():
|
|
139
|
-
lines.append(f"- {pkg}: {before} -> {after}")
|
|
140
|
-
|
|
141
|
-
lines.extend(
|
|
142
|
-
[
|
|
143
|
-
"",
|
|
144
|
-
"## Instructions",
|
|
145
|
-
"",
|
|
146
|
-
"1. **Process each package systematically:**",
|
|
147
|
-
" - For each package, run: `uv run plain-changelog {package} --from {before} --to {after}`",
|
|
148
|
-
" - Read the 'Upgrade instructions' section carefully",
|
|
149
|
-
" - If it says 'No changes required', skip to the next package",
|
|
150
|
-
" - Apply any required code changes as specified",
|
|
151
|
-
"",
|
|
152
|
-
"2. **Important guidelines:**",
|
|
153
|
-
" - Process ALL packages before testing or validation",
|
|
154
|
-
" - After all packages are updated, run `uv run plain fix --unsafe-fixes` and `uv run plain pre-commit` to check results",
|
|
155
|
-
" - DO NOT commit any changes",
|
|
156
|
-
" - Keep code changes minimal and focused - avoid unnecessary comments",
|
|
157
|
-
"",
|
|
158
|
-
"3. **Available tools:**",
|
|
159
|
-
" - Python shell: `uv run python`",
|
|
160
|
-
" - If you have a subagents feature and there are more than three packages here, use subagents",
|
|
161
|
-
"",
|
|
162
|
-
"4. **Workflow:**",
|
|
163
|
-
" - Review changelog for each package → Apply changes → Move to next package",
|
|
164
|
-
" - Only after all packages: run pre-commit checks",
|
|
165
|
-
" - Report any issues or conflicts encountered",
|
|
166
|
-
]
|
|
167
|
-
)
|
|
168
|
-
return "\n".join(lines)
|
plain/cli/urls.py
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
|
|
1
5
|
import click
|
|
2
6
|
|
|
3
7
|
|
|
4
8
|
@click.group()
|
|
5
|
-
def urls():
|
|
6
|
-
"""URL
|
|
7
|
-
pass
|
|
9
|
+
def urls() -> None:
|
|
10
|
+
"""URL configuration commands"""
|
|
8
11
|
|
|
9
12
|
|
|
10
13
|
@urls.command("list")
|
|
11
14
|
@click.option("--flat", is_flag=True, help="List all URLs in a flat list")
|
|
12
|
-
def list_urls(flat):
|
|
13
|
-
"""
|
|
15
|
+
def list_urls(flat: bool) -> None:
|
|
16
|
+
"""List all URL patterns"""
|
|
14
17
|
from plain.runtime import settings
|
|
15
18
|
from plain.urls import URLResolver, get_resolver
|
|
16
19
|
|
|
@@ -20,7 +23,9 @@ def list_urls(flat):
|
|
|
20
23
|
resolver = get_resolver(settings.URLS_ROUTER)
|
|
21
24
|
if flat:
|
|
22
25
|
|
|
23
|
-
def flat_list(
|
|
26
|
+
def flat_list(
|
|
27
|
+
patterns: list, prefix: str = "", curr_ns: str = ""
|
|
28
|
+
) -> Iterator[str]:
|
|
24
29
|
for pattern in patterns:
|
|
25
30
|
full_pattern = f"{prefix}{pattern.pattern}"
|
|
26
31
|
if isinstance(pattern, URLResolver):
|
|
@@ -50,7 +55,7 @@ def list_urls(flat):
|
|
|
50
55
|
click.echo(p)
|
|
51
56
|
else:
|
|
52
57
|
|
|
53
|
-
def print_tree(patterns, prefix="", curr_ns=""):
|
|
58
|
+
def print_tree(patterns: list, prefix: str = "", curr_ns: str = "") -> None:
|
|
54
59
|
count = len(patterns)
|
|
55
60
|
for idx, pattern in enumerate(patterns):
|
|
56
61
|
is_last = idx == (count - 1)
|
plain/cli/utils.py
CHANGED
|
@@ -4,12 +4,12 @@ from plain.utils.crypto import get_random_string
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
@click.group()
|
|
7
|
-
def utils():
|
|
8
|
-
|
|
7
|
+
def utils() -> None:
|
|
8
|
+
"""Utility commands"""
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@utils.command()
|
|
12
|
-
def generate_secret_key():
|
|
12
|
+
def generate_secret_key() -> None:
|
|
13
13
|
"""Generate a new secret key"""
|
|
14
14
|
new_secret_key = get_random_string(50)
|
|
15
15
|
click.echo(new_secret_key)
|