plain 0.66.0__py3-none-any.whl → 0.68.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 +29 -0
- plain/chores/registry.py +1 -18
- plain/cli/agent/request.py +111 -120
- plain/cli/core.py +2 -2
- plain/cli/preflight.py +219 -98
- plain/cli/registry.py +1 -18
- plain/internal/middleware/hosts.py +0 -5
- plain/packages/registry.py +14 -0
- plain/preflight/README.md +5 -5
- plain/preflight/__init__.py +4 -24
- plain/preflight/checks.py +10 -0
- plain/preflight/files.py +17 -13
- plain/preflight/registry.py +64 -57
- plain/preflight/results.py +29 -0
- plain/preflight/security.py +58 -66
- plain/preflight/urls.py +7 -48
- plain/runtime/global_settings.py +9 -8
- plain/templates/jinja/__init__.py +2 -23
- plain/urls/patterns.py +21 -21
- plain/urls/resolvers.py +4 -4
- {plain-0.66.0.dist-info → plain-0.68.0.dist-info}/METADATA +1 -1
- {plain-0.66.0.dist-info → plain-0.68.0.dist-info}/RECORD +25 -24
- {plain-0.66.0.dist-info → plain-0.68.0.dist-info}/entry_points.txt +1 -0
- plain/preflight/messages.py +0 -81
- {plain-0.66.0.dist-info → plain-0.68.0.dist-info}/WHEEL +0 -0
- {plain-0.66.0.dist-info → plain-0.68.0.dist-info}/licenses/LICENSE +0 -0
plain/CHANGELOG.md
CHANGED
@@ -1,5 +1,34 @@
|
|
1
1
|
# plain changelog
|
2
2
|
|
3
|
+
## [0.68.0](https://github.com/dropseed/plain/releases/plain@0.68.0) (2025-09-25)
|
4
|
+
|
5
|
+
### What's changed
|
6
|
+
|
7
|
+
- Major refactor of the preflight check system with new CLI commands and improved output ([b0b610d461](https://github.com/dropseed/plain/commit/b0b610d461))
|
8
|
+
- Preflight checks now use descriptive IDs instead of numeric codes ([cd96c97b25](https://github.com/dropseed/plain/commit/cd96c97b25))
|
9
|
+
- Unified preflight error messages and hints into a single `fix` field ([c7cde12149](https://github.com/dropseed/plain/commit/c7cde12149))
|
10
|
+
- Added `plain-upgrade` as a standalone command for upgrading Plain packages ([42f2eed80c](https://github.com/dropseed/plain/commit/42f2eed80c))
|
11
|
+
|
12
|
+
### Upgrade instructions
|
13
|
+
|
14
|
+
- Use `plain preflight check` instead of `plain preflight` to run all checks
|
15
|
+
- Custom preflight checks should be class based, extending `PreflightCheck` and implementing the `run()` method
|
16
|
+
- Preflight checks need to be registered with a custom name (ex. `@register_check("app.my_custom_check")`) and optionally with `deploy=True` if it should run in only in deploy mode
|
17
|
+
- Preflight results should use `PreflightResult` (optionally with `warning=True`) instead of `preflight.Warning` or `preflight.Error`
|
18
|
+
- Preflight result IDs should be descriptive strings (e.g., `models.lazy_reference_resolution_failed`) instead of numeric codes
|
19
|
+
- `PREFLIGHT_SILENCED_CHECKS` setting has been replaced with `PREFLIGHT_SILENCED_RESULTS` which should contain a list of result IDs to silence. `PREFLIGHT_SILENCED_CHECKS` now silences entire checks by name.
|
20
|
+
|
21
|
+
## [0.67.0](https://github.com/dropseed/plain/releases/plain@0.67.0) (2025-09-22)
|
22
|
+
|
23
|
+
### What's changed
|
24
|
+
|
25
|
+
- `ALLOWED_HOSTS` now defaults to `[]` (empty list) which allows all hosts, making it easier for development setups ([d3cb7712b9](https://github.com/dropseed/plain/commit/d3cb7712b9))
|
26
|
+
- Empty `ALLOWED_HOSTS` in production now triggers a preflight error instead of a warning to ensure proper security configuration ([d3cb7712b9](https://github.com/dropseed/plain/commit/d3cb7712b9))
|
27
|
+
|
28
|
+
### Upgrade instructions
|
29
|
+
|
30
|
+
- No changes required
|
31
|
+
|
3
32
|
## [0.66.0](https://github.com/dropseed/plain/releases/plain@0.66.0) (2025-09-22)
|
4
33
|
|
5
34
|
### What's changed
|
plain/chores/registry.py
CHANGED
@@ -1,6 +1,3 @@
|
|
1
|
-
from importlib import import_module
|
2
|
-
from importlib.util import find_spec
|
3
|
-
|
4
1
|
from plain.packages import packages_registry
|
5
2
|
|
6
3
|
|
@@ -35,21 +32,7 @@ class ChoresRegistry:
|
|
35
32
|
"""
|
36
33
|
Import modules from installed packages and app to trigger registration.
|
37
34
|
"""
|
38
|
-
|
39
|
-
for package_config in packages_registry.get_package_configs():
|
40
|
-
import_name = f"{package_config.name}.chores"
|
41
|
-
try:
|
42
|
-
import_module(import_name)
|
43
|
-
except ModuleNotFoundError:
|
44
|
-
pass
|
45
|
-
|
46
|
-
# Import from app
|
47
|
-
import_name = "app.chores"
|
48
|
-
if find_spec(import_name):
|
49
|
-
try:
|
50
|
-
import_module(import_name)
|
51
|
-
except ModuleNotFoundError:
|
52
|
-
pass
|
35
|
+
packages_registry.autodiscover_modules("chores", include_app=True)
|
53
36
|
|
54
37
|
def get_chores(self):
|
55
38
|
"""
|
plain/cli/agent/request.py
CHANGED
@@ -46,136 +46,127 @@ def request(path, method, data, user_id, follow, content_type, headers):
|
|
46
46
|
click.secho("This command only works when DEBUG=True", fg="red", err=True)
|
47
47
|
return
|
48
48
|
|
49
|
-
#
|
50
|
-
|
51
|
-
settings.ALLOWED_HOSTS = ["*"]
|
49
|
+
# Create test client
|
50
|
+
client = Client()
|
52
51
|
|
53
|
-
|
54
|
-
|
55
|
-
|
52
|
+
# If user_id provided, force login
|
53
|
+
if user_id:
|
54
|
+
try:
|
55
|
+
# Get the User model using plain.auth utility
|
56
|
+
from plain.auth import get_user_model
|
56
57
|
|
57
|
-
|
58
|
-
if user_id:
|
59
|
-
try:
|
60
|
-
# Get the User model using plain.auth utility
|
61
|
-
from plain.auth import get_user_model
|
62
|
-
|
63
|
-
User = get_user_model()
|
64
|
-
|
65
|
-
# Get the user
|
66
|
-
try:
|
67
|
-
user = User.query.get(id=user_id)
|
68
|
-
client.force_login(user)
|
69
|
-
click.secho(
|
70
|
-
f"Authenticated as user {user_id}", fg="green", dim=True
|
71
|
-
)
|
72
|
-
except User.DoesNotExist:
|
73
|
-
click.secho(f"User {user_id} not found", fg="red", err=True)
|
74
|
-
return
|
75
|
-
|
76
|
-
except Exception as e:
|
77
|
-
click.secho(f"Authentication error: {e}", fg="red", err=True)
|
78
|
-
return
|
79
|
-
|
80
|
-
# Parse additional headers
|
81
|
-
header_dict = {}
|
82
|
-
for header in headers:
|
83
|
-
if ":" in header:
|
84
|
-
key, value = header.split(":", 1)
|
85
|
-
header_dict[key.strip()] = value.strip()
|
58
|
+
User = get_user_model()
|
86
59
|
|
87
|
-
|
88
|
-
if data and content_type and "json" in content_type.lower():
|
60
|
+
# Get the user
|
89
61
|
try:
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
62
|
+
user = User.query.get(id=user_id)
|
63
|
+
client.force_login(user)
|
64
|
+
click.secho(
|
65
|
+
f"Authenticated as user {user_id}", fg="green", dim=True
|
66
|
+
)
|
67
|
+
except User.DoesNotExist:
|
68
|
+
click.secho(f"User {user_id} not found", fg="red", err=True)
|
94
69
|
return
|
95
70
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
elif method == "PUT":
|
115
|
-
response = client.put(**kwargs)
|
116
|
-
elif method == "PATCH":
|
117
|
-
response = client.patch(**kwargs)
|
118
|
-
elif method == "DELETE":
|
119
|
-
response = client.delete(**kwargs)
|
120
|
-
elif method == "HEAD":
|
121
|
-
response = client.head(**kwargs)
|
122
|
-
elif method == "OPTIONS":
|
123
|
-
response = client.options(**kwargs)
|
124
|
-
elif method == "TRACE":
|
125
|
-
response = client.trace(**kwargs)
|
126
|
-
else:
|
127
|
-
click.secho(f"Unsupported HTTP method: {method}", fg="red", err=True)
|
71
|
+
except Exception as e:
|
72
|
+
click.secho(f"Authentication error: {e}", fg="red", err=True)
|
73
|
+
return
|
74
|
+
|
75
|
+
# Parse additional headers
|
76
|
+
header_dict = {}
|
77
|
+
for header in headers:
|
78
|
+
if ":" in header:
|
79
|
+
key, value = header.split(":", 1)
|
80
|
+
header_dict[key.strip()] = value.strip()
|
81
|
+
|
82
|
+
# Prepare request data
|
83
|
+
if data and content_type and "json" in content_type.lower():
|
84
|
+
try:
|
85
|
+
# Validate JSON
|
86
|
+
json.loads(data)
|
87
|
+
except json.JSONDecodeError as e:
|
88
|
+
click.secho(f"Invalid JSON data: {e}", fg="red", err=True)
|
128
89
|
return
|
129
90
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
91
|
+
# Make the request
|
92
|
+
method = method.upper()
|
93
|
+
kwargs = {
|
94
|
+
"path": path,
|
95
|
+
"follow": follow,
|
96
|
+
"headers": header_dict or None,
|
97
|
+
}
|
98
|
+
|
99
|
+
if method in ("POST", "PUT", "PATCH") and data:
|
100
|
+
kwargs["data"] = data
|
101
|
+
if content_type:
|
102
|
+
kwargs["content_type"] = content_type
|
103
|
+
|
104
|
+
# Call the appropriate client method
|
105
|
+
if method == "GET":
|
106
|
+
response = client.get(**kwargs)
|
107
|
+
elif method == "POST":
|
108
|
+
response = client.post(**kwargs)
|
109
|
+
elif method == "PUT":
|
110
|
+
response = client.put(**kwargs)
|
111
|
+
elif method == "PATCH":
|
112
|
+
response = client.patch(**kwargs)
|
113
|
+
elif method == "DELETE":
|
114
|
+
response = client.delete(**kwargs)
|
115
|
+
elif method == "HEAD":
|
116
|
+
response = client.head(**kwargs)
|
117
|
+
elif method == "OPTIONS":
|
118
|
+
response = client.options(**kwargs)
|
119
|
+
elif method == "TRACE":
|
120
|
+
response = client.trace(**kwargs)
|
121
|
+
else:
|
122
|
+
click.secho(f"Unsupported HTTP method: {method}", fg="red", err=True)
|
123
|
+
return
|
124
|
+
|
125
|
+
# 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
|
+
)
|
131
|
+
|
132
|
+
# Show additional response info first
|
133
|
+
if hasattr(response, "user"):
|
134
|
+
click.secho(f"Authenticated user: {response.user}", fg="blue", dim=True)
|
135
|
+
|
136
|
+
if hasattr(response, "resolver_match") and response.resolver_match:
|
137
|
+
match = response.resolver_match
|
138
|
+
url_name = match.namespaced_url_name or match.url_name or "unnamed"
|
139
|
+
click.secho(f"URL pattern matched: {url_name}", fg="blue", dim=True)
|
140
|
+
|
141
|
+
# Show headers
|
142
|
+
if response.headers:
|
143
|
+
click.secho("Response Headers:", fg="yellow", bold=True)
|
144
|
+
for key, value in response.headers.items():
|
145
|
+
click.echo(f" {key}: {value}")
|
146
|
+
click.echo()
|
147
|
+
|
148
|
+
# Show response content last
|
149
|
+
if response.content:
|
150
|
+
content_type = response.headers.get("Content-Type", "")
|
151
|
+
|
152
|
+
if "json" in content_type.lower():
|
153
|
+
try:
|
154
|
+
json_data = response.json()
|
155
|
+
click.secho("Response Body (JSON):", fg="yellow", bold=True)
|
156
|
+
click.echo(json.dumps(json_data, indent=2))
|
157
|
+
except Exception:
|
170
158
|
click.secho("Response Body:", fg="yellow", bold=True)
|
171
|
-
|
172
|
-
|
159
|
+
click.echo(response.content.decode("utf-8", errors="replace"))
|
160
|
+
elif "html" in content_type.lower():
|
161
|
+
click.secho("Response Body (HTML):", fg="yellow", bold=True)
|
162
|
+
content = response.content.decode("utf-8", errors="replace")
|
163
|
+
click.echo(content)
|
173
164
|
else:
|
174
|
-
click.secho("
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
165
|
+
click.secho("Response Body:", fg="yellow", bold=True)
|
166
|
+
content = response.content.decode("utf-8", errors="replace")
|
167
|
+
click.echo(content)
|
168
|
+
else:
|
169
|
+
click.secho("(No response body)", fg="yellow", dim=True)
|
179
170
|
|
180
171
|
except Exception as e:
|
181
172
|
click.secho(f"Request failed: {e}", fg="red", err=True)
|
plain/cli/core.py
CHANGED
@@ -13,7 +13,7 @@ from .chores import chores
|
|
13
13
|
from .docs import docs
|
14
14
|
from .formatting import PlainContext
|
15
15
|
from .install import install
|
16
|
-
from .preflight import
|
16
|
+
from .preflight import preflight_cli
|
17
17
|
from .registry import cli_registry
|
18
18
|
from .scaffold import create
|
19
19
|
from .settings import setting
|
@@ -30,7 +30,7 @@ def plain_cli():
|
|
30
30
|
|
31
31
|
plain_cli.add_command(agent)
|
32
32
|
plain_cli.add_command(docs)
|
33
|
-
plain_cli.add_command(
|
33
|
+
plain_cli.add_command(preflight_cli)
|
34
34
|
plain_cli.add_command(create)
|
35
35
|
plain_cli.add_command(chores)
|
36
36
|
plain_cli.add_command(build)
|