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 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
- # Import from installed packages
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
  """
@@ -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
- # Temporarily add testserver to ALLOWED_HOSTS so the test client can make requests
50
- original_allowed_hosts = settings.ALLOWED_HOSTS
51
- settings.ALLOWED_HOSTS = ["*"]
49
+ # Create test client
50
+ client = Client()
52
51
 
53
- try:
54
- # Create test client
55
- client = Client()
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
- # If user_id provided, force login
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
- # Prepare request data
88
- if data and content_type and "json" in content_type.lower():
60
+ # Get the user
89
61
  try:
90
- # Validate JSON
91
- json.loads(data)
92
- except json.JSONDecodeError as e:
93
- click.secho(f"Invalid JSON data: {e}", fg="red", err=True)
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
- # Make the request
97
- method = method.upper()
98
- kwargs = {
99
- "path": path,
100
- "follow": follow,
101
- "headers": header_dict or None,
102
- }
103
-
104
- if method in ("POST", "PUT", "PATCH") and data:
105
- kwargs["data"] = data
106
- if content_type:
107
- kwargs["content_type"] = content_type
108
-
109
- # Call the appropriate client method
110
- if method == "GET":
111
- response = client.get(**kwargs)
112
- elif method == "POST":
113
- response = client.post(**kwargs)
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
- # Display response information
131
- click.secho(
132
- f"HTTP {response.status_code}",
133
- fg="green" if response.status_code < 400 else "red",
134
- bold=True,
135
- )
136
-
137
- # Show additional response info first
138
- if hasattr(response, "user"):
139
- click.secho(f"Authenticated user: {response.user}", fg="blue", dim=True)
140
-
141
- if hasattr(response, "resolver_match") and response.resolver_match:
142
- match = response.resolver_match
143
- url_name = match.namespaced_url_name or match.url_name or "unnamed"
144
- click.secho(f"URL pattern matched: {url_name}", fg="blue", dim=True)
145
-
146
- # Show headers
147
- if response.headers:
148
- click.secho("Response Headers:", fg="yellow", bold=True)
149
- for key, value in response.headers.items():
150
- click.echo(f" {key}: {value}")
151
- click.echo()
152
-
153
- # Show response content last
154
- if response.content:
155
- content_type = response.headers.get("Content-Type", "")
156
-
157
- if "json" in content_type.lower():
158
- try:
159
- json_data = response.json()
160
- click.secho("Response Body (JSON):", fg="yellow", bold=True)
161
- click.echo(json.dumps(json_data, indent=2))
162
- except Exception:
163
- click.secho("Response Body:", fg="yellow", bold=True)
164
- click.echo(response.content.decode("utf-8", errors="replace"))
165
- elif "html" in content_type.lower():
166
- click.secho("Response Body (HTML):", fg="yellow", bold=True)
167
- content = response.content.decode("utf-8", errors="replace")
168
- click.echo(content)
169
- else:
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
- content = response.content.decode("utf-8", errors="replace")
172
- click.echo(content)
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("(No response body)", fg="yellow", dim=True)
175
-
176
- finally:
177
- # Restore original ALLOWED_HOSTS
178
- settings.ALLOWED_HOSTS = original_allowed_hosts
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 preflight_checks
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(preflight_checks)
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)