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
plain/preflight/README.md
CHANGED
|
@@ -1,61 +1,246 @@
|
|
|
1
|
-
#
|
|
1
|
+
# plain.preflight
|
|
2
2
|
|
|
3
|
-
**System checks
|
|
3
|
+
**System checks that validate your settings and environment before running your application.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
|
-
- [
|
|
7
|
-
- [
|
|
6
|
+
- [Running preflight checks](#running-preflight-checks)
|
|
7
|
+
- [Development](#development)
|
|
8
|
+
- [Deployment](#deployment)
|
|
9
|
+
- [JSON output](#json-output)
|
|
10
|
+
- [Built-in checks](#built-in-checks)
|
|
8
11
|
- [Custom preflight checks](#custom-preflight-checks)
|
|
9
|
-
- [
|
|
12
|
+
- [Basic checks](#basic-checks)
|
|
13
|
+
- [Deployment-only checks](#deployment-only-checks)
|
|
14
|
+
- [Warnings vs errors](#warnings-vs-errors)
|
|
15
|
+
- [Silencing checks](#silencing-checks)
|
|
16
|
+
- [Silencing entire checks](#silencing-entire-checks)
|
|
17
|
+
- [Silencing specific results](#silencing-specific-results)
|
|
18
|
+
- [FAQs](#faqs)
|
|
19
|
+
- [Installation](#installation)
|
|
10
20
|
|
|
11
21
|
## Overview
|
|
12
22
|
|
|
13
|
-
Preflight checks help
|
|
23
|
+
Preflight checks help you catch configuration problems early. You can run checks to verify that settings are valid, directories exist, URL patterns are correct, and security requirements are met before your application starts.
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
from plain.preflight import PreflightCheck, PreflightResult, register_check
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register_check("custom.database_connection")
|
|
30
|
+
class CheckDatabaseConnection(PreflightCheck):
|
|
31
|
+
"""Verify database is reachable."""
|
|
32
|
+
|
|
33
|
+
def run(self) -> list[PreflightResult]:
|
|
34
|
+
from plain.models import connection
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
connection.ensure_connection()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return [
|
|
40
|
+
PreflightResult(
|
|
41
|
+
fix=f"Database connection failed: {e}. Check your DATABASE_URL.",
|
|
42
|
+
id="custom.database_unreachable",
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
return []
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
When you run `plain preflight`, your check runs alongside the built-in checks:
|
|
14
49
|
|
|
15
50
|
```bash
|
|
16
|
-
plain preflight
|
|
51
|
+
$ plain preflight
|
|
52
|
+
Running preflight checks...
|
|
53
|
+
Check: custom.database_connection ✔
|
|
54
|
+
Check: files.upload_temp_dir ✔
|
|
55
|
+
Check: settings.unused_env_vars ✔
|
|
56
|
+
Check: urls.config ✔
|
|
57
|
+
|
|
58
|
+
4 passed
|
|
17
59
|
```
|
|
18
60
|
|
|
19
|
-
##
|
|
61
|
+
## Running preflight checks
|
|
62
|
+
|
|
63
|
+
### Development
|
|
64
|
+
|
|
65
|
+
Run preflight checks at any time:
|
|
20
66
|
|
|
21
|
-
|
|
67
|
+
```bash
|
|
68
|
+
plain preflight
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
If you use [`plain.dev`](/plain-dev/README.md) for local development, preflight checks run automatically when you start `plain dev`.
|
|
22
72
|
|
|
23
|
-
|
|
73
|
+
### Deployment
|
|
24
74
|
|
|
25
|
-
|
|
75
|
+
Add `--deploy` to include deployment-specific checks like `SECRET_KEY` strength, `DEBUG` mode, and `ALLOWED_HOSTS`:
|
|
26
76
|
|
|
27
77
|
```bash
|
|
28
78
|
plain preflight --deploy
|
|
29
79
|
```
|
|
30
80
|
|
|
81
|
+
This should be part of your deployment process. If any check fails (returns errors, not warnings), the command exits with code 1.
|
|
82
|
+
|
|
83
|
+
### JSON output
|
|
84
|
+
|
|
85
|
+
For CI/CD pipelines or programmatic access, use JSON output:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
plain preflight --format json
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"passed": true,
|
|
94
|
+
"checks": [
|
|
95
|
+
{
|
|
96
|
+
"name": "files.upload_temp_dir",
|
|
97
|
+
"passed": true,
|
|
98
|
+
"issues": []
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use `--quiet` to suppress progress output and only show errors.
|
|
105
|
+
|
|
106
|
+
## Built-in checks
|
|
107
|
+
|
|
108
|
+
Plain includes these checks out of the box:
|
|
109
|
+
|
|
110
|
+
| Check | Description | Deploy only |
|
|
111
|
+
| ------------------------------- | -------------------------------------------------------- | ----------- |
|
|
112
|
+
| `files.upload_temp_dir` | Validates `FILE_UPLOAD_TEMP_DIR` exists | No |
|
|
113
|
+
| `settings.unused_env_vars` | Detects env vars that look like settings but aren't used | No |
|
|
114
|
+
| `urls.config` | Validates URL patterns for common issues | No |
|
|
115
|
+
| `security.secret_key` | Validates `SECRET_KEY` strength | Yes |
|
|
116
|
+
| `security.secret_key_fallbacks` | Validates `SECRET_KEY_FALLBACKS` strength | Yes |
|
|
117
|
+
| `security.debug` | Ensures `DEBUG` is False | Yes |
|
|
118
|
+
| `security.allowed_hosts` | Ensures `ALLOWED_HOSTS` is not empty | Yes |
|
|
119
|
+
|
|
31
120
|
## Custom preflight checks
|
|
32
121
|
|
|
33
|
-
|
|
122
|
+
### Basic checks
|
|
123
|
+
|
|
124
|
+
Create a check by subclassing [`PreflightCheck`](./checks.py#PreflightCheck) and using the [`@register_check`](./registry.py#register_check) decorator:
|
|
34
125
|
|
|
35
126
|
```python
|
|
36
|
-
from plain.preflight import
|
|
127
|
+
from plain.preflight import PreflightCheck, PreflightResult, register_check
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@register_check("custom.redis_connection")
|
|
131
|
+
class CheckRedisConnection(PreflightCheck):
|
|
132
|
+
"""Verify Redis cache is reachable."""
|
|
133
|
+
|
|
134
|
+
def run(self) -> list[PreflightResult]:
|
|
135
|
+
from plain.cache import cache
|
|
136
|
+
|
|
137
|
+
try:
|
|
138
|
+
cache.set("preflight_test", "ok", timeout=1)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return [
|
|
141
|
+
PreflightResult(
|
|
142
|
+
fix=f"Redis connection failed: {e}",
|
|
143
|
+
id="custom.redis_unreachable",
|
|
144
|
+
)
|
|
145
|
+
]
|
|
146
|
+
return []
|
|
147
|
+
```
|
|
37
148
|
|
|
149
|
+
Place this in a `preflight.py` file in your app directory. Plain autodiscovers `preflight.py` modules when running checks.
|
|
38
150
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
151
|
+
### Deployment-only checks
|
|
152
|
+
|
|
153
|
+
For checks that only matter in production, add `deploy=True`:
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
@register_check("custom.ssl_certificate", deploy=True)
|
|
157
|
+
class CheckSSLCertificate(PreflightCheck):
|
|
158
|
+
"""Verify SSL certificate is valid and not expiring soon."""
|
|
159
|
+
|
|
160
|
+
def run(self) -> list[PreflightResult]:
|
|
161
|
+
# Check certificate expiration...
|
|
162
|
+
if days_until_expiry < 30:
|
|
163
|
+
return [
|
|
164
|
+
PreflightResult(
|
|
165
|
+
fix=f"SSL certificate expires in {days_until_expiry} days.",
|
|
166
|
+
id="custom.ssl_expiring_soon",
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
return []
|
|
42
170
|
```
|
|
43
171
|
|
|
44
|
-
|
|
172
|
+
### Warnings vs errors
|
|
173
|
+
|
|
174
|
+
By default, [`PreflightResult`](./results.py#PreflightResult) represents an error that fails the preflight. For non-critical issues, use `warning=True`:
|
|
45
175
|
|
|
46
176
|
```python
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
177
|
+
PreflightResult(
|
|
178
|
+
fix="Consider enabling gzip compression for better performance.",
|
|
179
|
+
id="custom.gzip_disabled",
|
|
180
|
+
warning=True, # Won't cause preflight to fail
|
|
181
|
+
)
|
|
50
182
|
```
|
|
51
183
|
|
|
52
|
-
|
|
184
|
+
Warnings display with a yellow indicator but don't cause the command to exit with an error code.
|
|
185
|
+
|
|
186
|
+
## Silencing checks
|
|
53
187
|
|
|
54
|
-
|
|
188
|
+
### Silencing entire checks
|
|
189
|
+
|
|
190
|
+
To skip a check entirely, add its name to `PREFLIGHT_SILENCED_CHECKS`:
|
|
55
191
|
|
|
56
192
|
```python
|
|
57
193
|
# app/settings.py
|
|
58
194
|
PREFLIGHT_SILENCED_CHECKS = [
|
|
59
|
-
"security.
|
|
195
|
+
"security.debug", # We intentionally run with DEBUG=True in staging
|
|
196
|
+
]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Silencing specific results
|
|
200
|
+
|
|
201
|
+
To silence individual result IDs (not the whole check), use `PREFLIGHT_SILENCED_RESULTS`:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
# app/settings.py
|
|
205
|
+
PREFLIGHT_SILENCED_RESULTS = [
|
|
206
|
+
"security.secret_key_weak", # Using a known weak key in testing
|
|
60
207
|
]
|
|
61
208
|
```
|
|
209
|
+
|
|
210
|
+
## FAQs
|
|
211
|
+
|
|
212
|
+
#### What's the difference between a check name and a result ID?
|
|
213
|
+
|
|
214
|
+
The check name (like `security.secret_key`) identifies the check class. The result ID (like `security.secret_key_weak`) identifies a specific issue that check can report. A single check can return multiple different result IDs.
|
|
215
|
+
|
|
216
|
+
#### Where should I put custom preflight checks?
|
|
217
|
+
|
|
218
|
+
Create a `preflight.py` file in your app directory. Plain autodiscovers these modules when running `plain preflight`.
|
|
219
|
+
|
|
220
|
+
#### How do I run checks programmatically?
|
|
221
|
+
|
|
222
|
+
Use the [`run_checks`](./registry.py#run_checks) function:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from plain.preflight import run_checks
|
|
226
|
+
|
|
227
|
+
for check_class, name, results in run_checks(include_deploy_checks=True):
|
|
228
|
+
for result in results:
|
|
229
|
+
print(f"{name}: {result.fix}")
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Can I attach additional context to a result?
|
|
233
|
+
|
|
234
|
+
Use the `obj` parameter to attach a related object:
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
PreflightResult(
|
|
238
|
+
fix="Invalid URL pattern",
|
|
239
|
+
id="urls.invalid_pattern",
|
|
240
|
+
obj=some_url_pattern, # Will be included in output
|
|
241
|
+
)
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Installation
|
|
245
|
+
|
|
246
|
+
`plain.preflight` is included with the `plain` package. No additional installation is required.
|
plain/preflight/__init__.py
CHANGED
|
@@ -1,36 +1,17 @@
|
|
|
1
|
-
from .
|
|
2
|
-
CRITICAL,
|
|
3
|
-
DEBUG,
|
|
4
|
-
ERROR,
|
|
5
|
-
INFO,
|
|
6
|
-
WARNING,
|
|
7
|
-
CheckMessage,
|
|
8
|
-
Critical,
|
|
9
|
-
Debug,
|
|
10
|
-
Error,
|
|
11
|
-
Info,
|
|
12
|
-
Warning,
|
|
13
|
-
)
|
|
1
|
+
from .checks import PreflightCheck
|
|
14
2
|
from .registry import register_check, run_checks
|
|
3
|
+
from .results import PreflightResult
|
|
15
4
|
|
|
16
5
|
# Import these to force registration of checks
|
|
17
6
|
import plain.preflight.files # NOQA isort:skip
|
|
18
7
|
import plain.preflight.security # NOQA isort:skip
|
|
8
|
+
import plain.preflight.settings # NOQA isort:skip
|
|
19
9
|
import plain.preflight.urls # NOQA isort:skip
|
|
20
10
|
|
|
21
11
|
|
|
22
12
|
__all__ = [
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"Info",
|
|
26
|
-
"Warning",
|
|
27
|
-
"Error",
|
|
28
|
-
"Critical",
|
|
29
|
-
"DEBUG",
|
|
30
|
-
"INFO",
|
|
31
|
-
"WARNING",
|
|
32
|
-
"ERROR",
|
|
33
|
-
"CRITICAL",
|
|
13
|
+
"PreflightCheck",
|
|
14
|
+
"PreflightResult",
|
|
34
15
|
"register_check",
|
|
35
16
|
"run_checks",
|
|
36
17
|
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PreflightCheck(ABC):
|
|
7
|
+
"""Base class for all preflight checks."""
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def run(self) -> list:
|
|
11
|
+
"""Must return a list of Warning/Error results."""
|
|
12
|
+
raise NotImplementedError
|
plain/preflight/files.py
CHANGED
|
@@ -1,19 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
2
4
|
|
|
3
5
|
from plain.runtime import settings
|
|
4
6
|
|
|
5
|
-
from . import
|
|
7
|
+
from .checks import PreflightCheck
|
|
8
|
+
from .registry import register_check
|
|
9
|
+
from .results import PreflightResult
|
|
10
|
+
|
|
6
11
|
|
|
12
|
+
@register_check("files.upload_temp_dir")
|
|
13
|
+
class CheckSettingFileUploadTempDir(PreflightCheck):
|
|
14
|
+
"""Validates that the FILE_UPLOAD_TEMP_DIR setting points to an existing directory."""
|
|
7
15
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
]
|
|
19
|
-
return []
|
|
16
|
+
def run(self) -> list[PreflightResult]:
|
|
17
|
+
setting = settings.FILE_UPLOAD_TEMP_DIR
|
|
18
|
+
if setting and not Path(setting).is_dir():
|
|
19
|
+
return [
|
|
20
|
+
PreflightResult(
|
|
21
|
+
fix=f"FILE_UPLOAD_TEMP_DIR points to nonexistent directory '{setting}'. Create the directory or update the setting.",
|
|
22
|
+
id="files.upload_temp_dir_nonexistent",
|
|
23
|
+
)
|
|
24
|
+
]
|
|
25
|
+
return []
|
plain/preflight/registry.py
CHANGED
|
@@ -1,72 +1,94 @@
|
|
|
1
|
-
from
|
|
2
|
-
from plain.utils.itercompat import is_iterable
|
|
1
|
+
from __future__ import annotations
|
|
3
2
|
|
|
3
|
+
from collections.abc import Callable, Generator
|
|
4
|
+
from typing import Any, TypeVar
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
def __init__(self):
|
|
7
|
-
self.registered_checks = set()
|
|
8
|
-
self.deployment_checks = set()
|
|
6
|
+
from plain.runtime import settings
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
"""
|
|
12
|
-
Can be used as a function or a decorator. Register given function
|
|
13
|
-
`f`. The function should receive **kwargs
|
|
14
|
-
and return list of Errors and Warnings.
|
|
15
|
-
|
|
16
|
-
Example::
|
|
17
|
-
|
|
18
|
-
registry = CheckRegistry()
|
|
19
|
-
@registry.register('mytag', 'anothertag')
|
|
20
|
-
def my_check(package_configs, **kwargs):
|
|
21
|
-
# ... perform checks and collect `errors` ...
|
|
22
|
-
return errors
|
|
23
|
-
# or
|
|
24
|
-
registry.register(my_check, 'mytag', 'anothertag')
|
|
25
|
-
"""
|
|
8
|
+
from .results import PreflightResult
|
|
26
9
|
|
|
27
|
-
|
|
28
|
-
if not func_accepts_kwargs(check):
|
|
29
|
-
raise TypeError(
|
|
30
|
-
"Check functions must accept keyword arguments (**kwargs)."
|
|
31
|
-
)
|
|
32
|
-
checks = self.deployment_checks if deploy else self.registered_checks
|
|
33
|
-
checks.add(check)
|
|
34
|
-
return check
|
|
10
|
+
T = TypeVar("T")
|
|
35
11
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
12
|
+
|
|
13
|
+
class CheckRegistry:
|
|
14
|
+
def __init__(self) -> None:
|
|
15
|
+
self.checks: dict[
|
|
16
|
+
str, tuple[type[Any], bool]
|
|
17
|
+
] = {} # name -> (check_class, deploy)
|
|
18
|
+
|
|
19
|
+
def register_check(
|
|
20
|
+
self, check_class: type[Any], name: str, deploy: bool = False
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Register a check class with a unique name."""
|
|
23
|
+
if name in self.checks:
|
|
24
|
+
raise ValueError(f"Check {name} already registered")
|
|
25
|
+
self.checks[name] = (check_class, deploy)
|
|
40
26
|
|
|
41
27
|
def run_checks(
|
|
42
28
|
self,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
database=False,
|
|
46
|
-
):
|
|
29
|
+
include_deploy_checks: bool = False,
|
|
30
|
+
) -> Generator[tuple[type[Any], str, list[PreflightResult]]]:
|
|
47
31
|
"""
|
|
48
|
-
Run all registered checks and
|
|
32
|
+
Run all registered checks and yield (check_class, name, results) tuples.
|
|
49
33
|
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
34
|
+
# Validate silenced check names
|
|
35
|
+
silenced_checks = settings.PREFLIGHT_SILENCED_CHECKS
|
|
36
|
+
unknown_silenced = set(silenced_checks) - set(self.checks.keys())
|
|
37
|
+
if unknown_silenced:
|
|
38
|
+
unknown_names = ", ".join(sorted(unknown_silenced))
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"Unknown check names in PREFLIGHT_SILENCED_CHECKS: {unknown_names}. "
|
|
41
|
+
"Check for typos or remove outdated check names."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for name, (check_class, deploy) in sorted(self.checks.items()):
|
|
45
|
+
# Skip silenced checks
|
|
46
|
+
if name in silenced_checks:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Skip deployment checks if not requested
|
|
50
|
+
if deploy and not include_deploy_checks:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
# Instantiate and run check
|
|
54
|
+
check = check_class()
|
|
55
|
+
results = check.run()
|
|
56
|
+
yield check_class, name, results
|
|
57
|
+
|
|
58
|
+
def get_checks(
|
|
59
|
+
self, include_deploy_checks: bool = False
|
|
60
|
+
) -> list[tuple[type[Any], str]]:
|
|
61
|
+
"""Get list of (check_class, name) tuples."""
|
|
62
|
+
result: list[tuple[type[Any], str]] = []
|
|
63
|
+
for name, (check_class, deploy) in self.checks.items():
|
|
64
|
+
if deploy and not include_deploy_checks:
|
|
65
|
+
continue
|
|
66
|
+
result.append((check_class, name))
|
|
67
|
+
return result
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
checks_registry = CheckRegistry()
|
|
71
|
-
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def register_check(name: str, *, deploy: bool = False) -> Callable[[type[T]], type[T]]:
|
|
74
|
+
"""
|
|
75
|
+
Decorator to register a check class.
|
|
76
|
+
|
|
77
|
+
Usage:
|
|
78
|
+
@register_check("security.secret_key", deploy=True)
|
|
79
|
+
class CheckSecretKey(PreflightCheck):
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@register_check("files.upload_temp_dir")
|
|
83
|
+
class CheckUploadTempDir(PreflightCheck):
|
|
84
|
+
pass
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def wrapper(cls: type[T]) -> type[T]:
|
|
88
|
+
checks_registry.register_check(cls, name=name, deploy=deploy)
|
|
89
|
+
return cls
|
|
90
|
+
|
|
91
|
+
return wrapper
|
|
92
|
+
|
|
93
|
+
|
|
72
94
|
run_checks = checks_registry.run_checks
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from plain.runtime import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class PreflightResult:
|
|
9
|
+
def __init__(
|
|
10
|
+
self, *, fix: str, id: str, obj: Any = None, warning: bool = False
|
|
11
|
+
) -> None:
|
|
12
|
+
self.fix = fix
|
|
13
|
+
self.obj = obj
|
|
14
|
+
self.id = id
|
|
15
|
+
self.warning = warning
|
|
16
|
+
|
|
17
|
+
def __eq__(self, other: object) -> bool:
|
|
18
|
+
return isinstance(other, self.__class__) and all(
|
|
19
|
+
getattr(self, attr) == getattr(other, attr)
|
|
20
|
+
for attr in ["fix", "obj", "id", "warning"]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def __str__(self) -> str:
|
|
24
|
+
if self.obj is None:
|
|
25
|
+
obj = ""
|
|
26
|
+
elif hasattr(self.obj, "model_options") and hasattr(
|
|
27
|
+
self.obj.model_options, "label"
|
|
28
|
+
):
|
|
29
|
+
# Duck type for model objects - use their meta label
|
|
30
|
+
obj = self.obj.model_options.label
|
|
31
|
+
else:
|
|
32
|
+
obj = str(self.obj)
|
|
33
|
+
id_part = f"({self.id}) " if self.id else ""
|
|
34
|
+
return f"{obj}: {id_part}{self.fix}"
|
|
35
|
+
|
|
36
|
+
def is_silenced(self) -> bool:
|
|
37
|
+
return bool(self.id and self.id in settings.PREFLIGHT_SILENCED_RESULTS)
|