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
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import json
|
|
4
|
+
from typing import Any
|
|
2
5
|
|
|
3
6
|
import click
|
|
4
7
|
|
|
@@ -37,8 +40,28 @@ from plain.test import Client
|
|
|
37
40
|
multiple=True,
|
|
38
41
|
help="Additional headers (format: 'Name: Value')",
|
|
39
42
|
)
|
|
40
|
-
|
|
41
|
-
""
|
|
43
|
+
@click.option(
|
|
44
|
+
"--no-headers",
|
|
45
|
+
is_flag=True,
|
|
46
|
+
help="Hide response headers from output",
|
|
47
|
+
)
|
|
48
|
+
@click.option(
|
|
49
|
+
"--no-body",
|
|
50
|
+
is_flag=True,
|
|
51
|
+
help="Hide response body from output",
|
|
52
|
+
)
|
|
53
|
+
def request(
|
|
54
|
+
path: str,
|
|
55
|
+
method: str,
|
|
56
|
+
data: str | None,
|
|
57
|
+
user_id: str | None,
|
|
58
|
+
follow: bool,
|
|
59
|
+
content_type: str | None,
|
|
60
|
+
headers: tuple[str, ...],
|
|
61
|
+
no_headers: bool,
|
|
62
|
+
no_body: bool,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Make HTTP requests against the dev database"""
|
|
42
65
|
|
|
43
66
|
try:
|
|
44
67
|
# Only allow in DEBUG mode for security
|
|
@@ -61,9 +84,6 @@ def request(path, method, data, user_id, follow, content_type, headers):
|
|
|
61
84
|
try:
|
|
62
85
|
user = User.query.get(id=user_id)
|
|
63
86
|
client.force_login(user)
|
|
64
|
-
click.secho(
|
|
65
|
-
f"Authenticated as user {user_id}", fg="green", dim=True
|
|
66
|
-
)
|
|
67
87
|
except User.DoesNotExist:
|
|
68
88
|
click.secho(f"User {user_id} not found", fg="red", err=True)
|
|
69
89
|
return
|
|
@@ -90,11 +110,11 @@ def request(path, method, data, user_id, follow, content_type, headers):
|
|
|
90
110
|
|
|
91
111
|
# Make the request
|
|
92
112
|
method = method.upper()
|
|
93
|
-
kwargs = {
|
|
94
|
-
"path": path,
|
|
113
|
+
kwargs: dict[str, Any] = {
|
|
95
114
|
"follow": follow,
|
|
96
|
-
"headers": header_dict or None,
|
|
97
115
|
}
|
|
116
|
+
if header_dict:
|
|
117
|
+
kwargs["headers"] = header_dict
|
|
98
118
|
|
|
99
119
|
if method in ("POST", "PUT", "PATCH") and data:
|
|
100
120
|
kwargs["data"] = data
|
|
@@ -103,57 +123,71 @@ def request(path, method, data, user_id, follow, content_type, headers):
|
|
|
103
123
|
|
|
104
124
|
# Call the appropriate client method
|
|
105
125
|
if method == "GET":
|
|
106
|
-
response = client.get(**kwargs)
|
|
126
|
+
response = client.get(path, **kwargs)
|
|
107
127
|
elif method == "POST":
|
|
108
|
-
response = client.post(**kwargs)
|
|
128
|
+
response = client.post(path, **kwargs)
|
|
109
129
|
elif method == "PUT":
|
|
110
|
-
response = client.put(**kwargs)
|
|
130
|
+
response = client.put(path, **kwargs)
|
|
111
131
|
elif method == "PATCH":
|
|
112
|
-
response = client.patch(**kwargs)
|
|
132
|
+
response = client.patch(path, **kwargs)
|
|
113
133
|
elif method == "DELETE":
|
|
114
|
-
response = client.delete(**kwargs)
|
|
134
|
+
response = client.delete(path, **kwargs)
|
|
115
135
|
elif method == "HEAD":
|
|
116
|
-
response = client.head(**kwargs)
|
|
136
|
+
response = client.head(path, **kwargs)
|
|
117
137
|
elif method == "OPTIONS":
|
|
118
|
-
response = client.options(**kwargs)
|
|
138
|
+
response = client.options(path, **kwargs)
|
|
119
139
|
elif method == "TRACE":
|
|
120
|
-
response = client.trace(**kwargs)
|
|
140
|
+
response = client.trace(path, **kwargs)
|
|
121
141
|
else:
|
|
122
142
|
click.secho(f"Unsupported HTTP method: {method}", fg="red", err=True)
|
|
123
143
|
return
|
|
124
144
|
|
|
125
145
|
# Display response information
|
|
126
|
-
click.secho(
|
|
127
|
-
f"HTTP {response.status_code}",
|
|
128
|
-
fg="green" if response.status_code < 400 else "red",
|
|
129
|
-
bold=True,
|
|
130
|
-
)
|
|
146
|
+
click.secho("Response:", fg="yellow", bold=True)
|
|
131
147
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
click.secho(f"Authenticated user: {response.user}", fg="blue", dim=True)
|
|
148
|
+
# Status code
|
|
149
|
+
click.echo(f" Status: {response.status_code}")
|
|
135
150
|
|
|
136
|
-
|
|
151
|
+
# Request ID
|
|
152
|
+
click.echo(f" Request ID: {response.wsgi_request.unique_id}")
|
|
153
|
+
|
|
154
|
+
# User
|
|
155
|
+
if response.user:
|
|
156
|
+
click.echo(f" Authenticated user: {response.user}")
|
|
157
|
+
|
|
158
|
+
# URL pattern
|
|
159
|
+
if response.resolver_match:
|
|
137
160
|
match = response.resolver_match
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
namespaced_url_name = getattr(match, "namespaced_url_name", None)
|
|
162
|
+
url_name_attr = getattr(match, "url_name", None)
|
|
163
|
+
url_name = namespaced_url_name or url_name_attr
|
|
164
|
+
if url_name:
|
|
165
|
+
click.echo(f" URL pattern: {url_name}")
|
|
166
|
+
|
|
167
|
+
click.echo()
|
|
140
168
|
|
|
141
169
|
# Show headers
|
|
142
|
-
if response.headers:
|
|
170
|
+
if response.headers and not no_headers:
|
|
143
171
|
click.secho("Response Headers:", fg="yellow", bold=True)
|
|
144
172
|
for key, value in response.headers.items():
|
|
145
173
|
click.echo(f" {key}: {value}")
|
|
146
174
|
click.echo()
|
|
147
175
|
|
|
148
176
|
# Show response content last
|
|
149
|
-
if response.content:
|
|
177
|
+
if response.content and not no_body:
|
|
150
178
|
content_type = response.headers.get("Content-Type", "")
|
|
151
179
|
|
|
152
180
|
if "json" in content_type.lower():
|
|
153
181
|
try:
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
182
|
+
# The test client adds a json() method to the response
|
|
183
|
+
json_method = getattr(response, "json", None)
|
|
184
|
+
if json_method and callable(json_method):
|
|
185
|
+
json_data: Any = json_method()
|
|
186
|
+
click.secho("Response Body (JSON):", fg="yellow", bold=True)
|
|
187
|
+
click.echo(json.dumps(json_data, indent=2))
|
|
188
|
+
else:
|
|
189
|
+
click.secho("Response Body:", fg="yellow", bold=True)
|
|
190
|
+
click.echo(response.content.decode("utf-8", errors="replace"))
|
|
157
191
|
except Exception:
|
|
158
192
|
click.secho("Response Body:", fg="yellow", bold=True)
|
|
159
193
|
click.echo(response.content.decode("utf-8", errors="replace"))
|
|
@@ -165,7 +199,7 @@ def request(path, method, data, user_id, follow, content_type, headers):
|
|
|
165
199
|
click.secho("Response Body:", fg="yellow", bold=True)
|
|
166
200
|
content = response.content.decode("utf-8", errors="replace")
|
|
167
201
|
click.echo(content)
|
|
168
|
-
|
|
202
|
+
elif not no_body:
|
|
169
203
|
click.secho("(No response body)", fg="yellow", dim=True)
|
|
170
204
|
|
|
171
205
|
except Exception as e:
|
plain/cli/runtime.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CLI runtime utilities.
|
|
3
|
+
|
|
4
|
+
This module provides decorators and utilities for CLI commands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TypeVar
|
|
9
|
+
|
|
10
|
+
F = TypeVar("F", bound=Callable)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def without_runtime_setup(f: F) -> F:
|
|
14
|
+
"""
|
|
15
|
+
Decorator to mark commands that don't need plain.runtime.setup().
|
|
16
|
+
|
|
17
|
+
Use this for commands that don't access settings or app code,
|
|
18
|
+
particularly for commands that fork (like server) where setup()
|
|
19
|
+
should happen in the worker process, not the parent.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
@without_runtime_setup
|
|
23
|
+
@click.command()
|
|
24
|
+
def server(**options):
|
|
25
|
+
...
|
|
26
|
+
"""
|
|
27
|
+
f.without_runtime_setup = True # dynamic attribute for decorator
|
|
28
|
+
return f
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def common_command(f: F) -> F:
|
|
32
|
+
"""
|
|
33
|
+
Decorator to mark commands as commonly used.
|
|
34
|
+
|
|
35
|
+
Common commands are shown in a separate "Common Commands" section
|
|
36
|
+
in the help output, making them easier to discover.
|
|
37
|
+
|
|
38
|
+
Example:
|
|
39
|
+
@common_command
|
|
40
|
+
@click.command()
|
|
41
|
+
def dev(**options):
|
|
42
|
+
...
|
|
43
|
+
"""
|
|
44
|
+
f.is_common_command = True # dynamic attribute for decorator
|
|
45
|
+
return f
|
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(
|