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.
Files changed (197) hide show
  1. plain/CHANGELOG.md +684 -0
  2. plain/README.md +1 -1
  3. plain/assets/compile.py +25 -12
  4. plain/assets/finders.py +24 -17
  5. plain/assets/fingerprints.py +10 -7
  6. plain/assets/urls.py +1 -1
  7. plain/assets/views.py +47 -33
  8. plain/chores/README.md +25 -23
  9. plain/chores/__init__.py +2 -1
  10. plain/chores/core.py +27 -0
  11. plain/chores/registry.py +23 -53
  12. plain/cli/README.md +185 -16
  13. plain/cli/__init__.py +2 -1
  14. plain/cli/agent.py +236 -0
  15. plain/cli/build.py +7 -8
  16. plain/cli/changelog.py +11 -5
  17. plain/cli/chores.py +32 -34
  18. plain/cli/core.py +112 -28
  19. plain/cli/docs.py +52 -11
  20. plain/cli/formatting.py +40 -17
  21. plain/cli/install.py +10 -54
  22. plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
  23. plain/cli/output.py +6 -2
  24. plain/cli/preflight.py +175 -102
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +95 -26
  27. plain/cli/request.py +206 -0
  28. plain/cli/runtime.py +45 -0
  29. plain/cli/scaffold.py +2 -7
  30. plain/cli/server.py +153 -0
  31. plain/cli/settings.py +53 -49
  32. plain/cli/shell.py +15 -12
  33. plain/cli/startup.py +9 -8
  34. plain/cli/upgrade.py +17 -104
  35. plain/cli/urls.py +12 -7
  36. plain/cli/utils.py +3 -3
  37. plain/csrf/README.md +65 -40
  38. plain/csrf/middleware.py +53 -43
  39. plain/debug.py +5 -2
  40. plain/exceptions.py +22 -114
  41. plain/forms/README.md +453 -24
  42. plain/forms/__init__.py +55 -4
  43. plain/forms/boundfield.py +15 -8
  44. plain/forms/exceptions.py +1 -1
  45. plain/forms/fields.py +346 -143
  46. plain/forms/forms.py +75 -45
  47. plain/http/README.md +356 -9
  48. plain/http/__init__.py +41 -26
  49. plain/http/cookie.py +15 -7
  50. plain/http/exceptions.py +65 -0
  51. plain/http/middleware.py +32 -0
  52. plain/http/multipartparser.py +99 -88
  53. plain/http/request.py +362 -250
  54. plain/http/response.py +99 -197
  55. plain/internal/__init__.py +8 -1
  56. plain/internal/files/base.py +35 -19
  57. plain/internal/files/locks.py +19 -11
  58. plain/internal/files/move.py +8 -3
  59. plain/internal/files/temp.py +25 -6
  60. plain/internal/files/uploadedfile.py +47 -28
  61. plain/internal/files/uploadhandler.py +64 -58
  62. plain/internal/files/utils.py +24 -10
  63. plain/internal/handlers/base.py +34 -23
  64. plain/internal/handlers/exception.py +68 -65
  65. plain/internal/handlers/wsgi.py +65 -54
  66. plain/internal/middleware/headers.py +37 -11
  67. plain/internal/middleware/hosts.py +11 -13
  68. plain/internal/middleware/https.py +17 -7
  69. plain/internal/middleware/slash.py +14 -9
  70. plain/internal/reloader.py +77 -0
  71. plain/json.py +2 -1
  72. plain/logs/README.md +161 -62
  73. plain/logs/__init__.py +1 -1
  74. plain/logs/{loggers.py → app.py} +71 -67
  75. plain/logs/configure.py +63 -14
  76. plain/logs/debug.py +17 -6
  77. plain/logs/filters.py +15 -0
  78. plain/logs/formatters.py +7 -4
  79. plain/packages/README.md +105 -23
  80. plain/packages/config.py +15 -7
  81. plain/packages/registry.py +40 -15
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +208 -23
  84. plain/preflight/__init__.py +5 -24
  85. plain/preflight/checks.py +12 -0
  86. plain/preflight/files.py +19 -13
  87. plain/preflight/registry.py +80 -58
  88. plain/preflight/results.py +37 -0
  89. plain/preflight/security.py +65 -71
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +10 -48
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +43 -33
  95. plain/runtime/secret.py +20 -0
  96. plain/runtime/user_settings.py +110 -38
  97. plain/runtime/utils.py +1 -1
  98. plain/server/LICENSE +35 -0
  99. plain/server/README.md +155 -0
  100. plain/server/__init__.py +9 -0
  101. plain/server/app.py +52 -0
  102. plain/server/arbiter.py +555 -0
  103. plain/server/config.py +118 -0
  104. plain/server/errors.py +31 -0
  105. plain/server/glogging.py +292 -0
  106. plain/server/http/__init__.py +12 -0
  107. plain/server/http/body.py +283 -0
  108. plain/server/http/errors.py +155 -0
  109. plain/server/http/message.py +400 -0
  110. plain/server/http/parser.py +70 -0
  111. plain/server/http/unreader.py +88 -0
  112. plain/server/http/wsgi.py +421 -0
  113. plain/server/pidfile.py +92 -0
  114. plain/server/sock.py +240 -0
  115. plain/server/util.py +317 -0
  116. plain/server/workers/__init__.py +6 -0
  117. plain/server/workers/base.py +304 -0
  118. plain/server/workers/sync.py +212 -0
  119. plain/server/workers/thread.py +399 -0
  120. plain/server/workers/workertmp.py +50 -0
  121. plain/signals/README.md +170 -1
  122. plain/signals/__init__.py +0 -1
  123. plain/signals/dispatch/dispatcher.py +49 -27
  124. plain/signing.py +131 -35
  125. plain/skills/README.md +36 -0
  126. plain/skills/plain-docs/SKILL.md +25 -0
  127. plain/skills/plain-install/SKILL.md +26 -0
  128. plain/skills/plain-request/SKILL.md +39 -0
  129. plain/skills/plain-shell/SKILL.md +24 -0
  130. plain/skills/plain-upgrade/SKILL.md +35 -0
  131. plain/templates/README.md +211 -20
  132. plain/templates/jinja/__init__.py +14 -27
  133. plain/templates/jinja/environments.py +5 -4
  134. plain/templates/jinja/extensions.py +12 -5
  135. plain/templates/jinja/filters.py +7 -2
  136. plain/templates/jinja/globals.py +2 -2
  137. plain/test/README.md +184 -22
  138. plain/test/client.py +340 -222
  139. plain/test/encoding.py +9 -6
  140. plain/test/exceptions.py +7 -2
  141. plain/urls/README.md +157 -73
  142. plain/urls/converters.py +18 -15
  143. plain/urls/exceptions.py +2 -2
  144. plain/urls/patterns.py +56 -40
  145. plain/urls/resolvers.py +38 -28
  146. plain/urls/utils.py +5 -1
  147. plain/utils/README.md +250 -3
  148. plain/utils/cache.py +17 -11
  149. plain/utils/crypto.py +21 -5
  150. plain/utils/datastructures.py +89 -56
  151. plain/utils/dateparse.py +9 -6
  152. plain/utils/deconstruct.py +15 -7
  153. plain/utils/decorators.py +5 -1
  154. plain/utils/dotenv.py +373 -0
  155. plain/utils/duration.py +8 -4
  156. plain/utils/encoding.py +14 -7
  157. plain/utils/functional.py +66 -49
  158. plain/utils/hashable.py +5 -1
  159. plain/utils/html.py +36 -22
  160. plain/utils/http.py +16 -9
  161. plain/utils/inspect.py +14 -6
  162. plain/utils/ipv6.py +7 -3
  163. plain/utils/itercompat.py +6 -1
  164. plain/utils/module_loading.py +7 -3
  165. plain/utils/regex_helper.py +37 -23
  166. plain/utils/safestring.py +14 -6
  167. plain/utils/text.py +41 -23
  168. plain/utils/timezone.py +33 -22
  169. plain/utils/tree.py +35 -19
  170. plain/validators.py +94 -52
  171. plain/views/README.md +156 -79
  172. plain/views/__init__.py +0 -1
  173. plain/views/base.py +25 -18
  174. plain/views/errors.py +13 -5
  175. plain/views/exceptions.py +4 -1
  176. plain/views/forms.py +6 -6
  177. plain/views/objects.py +52 -49
  178. plain/views/redirect.py +18 -15
  179. plain/views/templates.py +5 -3
  180. plain/wsgi.py +3 -1
  181. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
  182. plain-0.101.2.dist-info/RECORD +201 -0
  183. {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
  184. plain-0.101.2.dist-info/entry_points.txt +2 -0
  185. plain/AGENTS.md +0 -18
  186. plain/cli/agent/__init__.py +0 -20
  187. plain/cli/agent/docs.py +0 -80
  188. plain/cli/agent/md.py +0 -87
  189. plain/cli/agent/prompt.py +0 -45
  190. plain/cli/agent/request.py +0 -181
  191. plain/csrf/views.py +0 -31
  192. plain/logs/utils.py +0 -46
  193. plain/preflight/messages.py +0 -81
  194. plain/templates/AGENTS.md +0 -3
  195. plain-0.66.0.dist-info/RECORD +0 -168
  196. plain-0.66.0.dist-info/entry_points.txt +0 -4
  197. {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
- # Preflight
1
+ # plain.preflight
2
2
 
3
- **System checks for Plain applications.**
3
+ **System checks that validate your settings and environment before running your application.**
4
4
 
5
5
  - [Overview](#overview)
6
- - [Development](#development)
7
- - [Deployment](#deployment)
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
- - [Silencing preflight checks](#silencing-preflight-checks)
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 identify issues with your settings or environment before running your application.
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
- ## Development
61
+ ## Running preflight checks
62
+
63
+ ### Development
64
+
65
+ Run preflight checks at any time:
20
66
 
21
- If you use [`plain.dev`](/plain-dev/README.md) for local development, the Plain preflight command is run automatically when you run `plain dev`.
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
- ## Deployment
73
+ ### Deployment
24
74
 
25
- The `plain preflight` command should often be part of your deployment process. Make sure to add the `--deploy` flag to the command to run checks that are only relevant in a production environment.
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
- Use the `@register_check` decorator to add your own preflight check to the system. Just make sure that particular Python module is somehow imported so the check registration runs.
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 register_check, Error
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
- @register_check
40
- def custom_check(package_configs, **kwargs):
41
- return Error("This is a custom error message.", id="custom.C001")
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
- For deployment-specific checks, add the `deploy` argument to the decorator.
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
- @register_check(deploy=True)
48
- def custom_deploy_check(package_configs, **kwargs):
49
- return Error("This is a custom error message for deployment.", id="custom.D001")
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
- ## Silencing preflight checks
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
- The `settings.PREFLIGHT_SILENCED_CHECKS` setting can be used to silence individual checks by their ID (ex. `security.W020`).
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.W020",
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.
@@ -1,36 +1,17 @@
1
- from .messages import (
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
- "CheckMessage",
24
- "Debug",
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 Error, register_check
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
- @register_check
9
- def check_setting_file_upload_temp_dir(package_configs, **kwargs):
10
- setting = getattr(settings, "FILE_UPLOAD_TEMP_DIR", None)
11
- if setting and not Path(setting).is_dir():
12
- return [
13
- Error(
14
- f"The FILE_UPLOAD_TEMP_DIR setting refers to the nonexistent "
15
- f"directory '{setting}'.",
16
- id="files.E001",
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 []
@@ -1,72 +1,94 @@
1
- from plain.utils.inspect import func_accepts_kwargs
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
- class CheckRegistry:
6
- def __init__(self):
7
- self.registered_checks = set()
8
- self.deployment_checks = set()
6
+ from plain.runtime import settings
9
7
 
10
- def register(self, check=None, deploy=False):
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
- def inner(check):
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
- if callable(check):
37
- return inner(check)
38
- else:
39
- return inner
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
- package_configs=None,
44
- include_deployment_checks=False,
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 return list of Errors and Warnings.
32
+ Run all registered checks and yield (check_class, name, results) tuples.
49
33
  """
50
- errors = []
51
- checks = self.get_checks(include_deployment_checks)
52
-
53
- for check in checks:
54
- new_errors = check(package_configs=package_configs, database=database)
55
- if not is_iterable(new_errors):
56
- raise TypeError(
57
- f"The function {check!r} did not return a list. All functions "
58
- "registered with the checks registry must return a list.",
59
- )
60
- errors.extend(new_errors)
61
- return errors
62
-
63
- def get_checks(self, include_deployment_checks=False):
64
- checks = list(self.registered_checks)
65
- if include_deployment_checks:
66
- checks.extend(self.deployment_checks)
67
- return checks
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
- register_check = checks_registry.register
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)