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
plain/runtime/README.md
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
# Runtime
|
|
2
2
|
|
|
3
|
-
**Access
|
|
3
|
+
**Access and configure settings for your Plain application.**
|
|
4
4
|
|
|
5
5
|
- [Overview](#overview)
|
|
6
6
|
- [Environment variables](#environment-variables)
|
|
7
7
|
- [.env files](#env-files)
|
|
8
|
+
- [Custom prefixes](#custom-prefixes)
|
|
8
9
|
- [Package settings](#package-settings)
|
|
9
|
-
- [Custom app
|
|
10
|
-
- [
|
|
10
|
+
- [Custom app settings](#custom-app-settings)
|
|
11
|
+
- [Secret values](#secret-values)
|
|
12
|
+
- [Using Plain outside of an app](#using-plain-outside-of-an-app)
|
|
13
|
+
- [FAQs](#faqs)
|
|
14
|
+
- [Installation](#installation)
|
|
11
15
|
|
|
12
16
|
## Overview
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
You configure Plain through settings, which are Python variables defined in your `app/settings.py` file.
|
|
15
19
|
|
|
16
20
|
```python
|
|
17
21
|
# app/settings.py
|
|
22
|
+
from plain.runtime import Secret
|
|
23
|
+
|
|
24
|
+
SECRET_KEY: Secret[str]
|
|
25
|
+
|
|
18
26
|
URLS_ROUTER = "app.urls.AppRouter"
|
|
19
27
|
|
|
20
28
|
TIME_ZONE = "America/Chicago"
|
|
@@ -23,11 +31,9 @@ INSTALLED_PACKAGES = [
|
|
|
23
31
|
"plain.models",
|
|
24
32
|
"plain.tailwind",
|
|
25
33
|
"plain.auth",
|
|
26
|
-
"plain.passwords",
|
|
27
34
|
"plain.sessions",
|
|
28
35
|
"plain.htmx",
|
|
29
36
|
"plain.admin",
|
|
30
|
-
"plain.elements",
|
|
31
37
|
# Local packages
|
|
32
38
|
"app.users",
|
|
33
39
|
]
|
|
@@ -37,107 +43,99 @@ AUTH_LOGIN_URL = "login"
|
|
|
37
43
|
|
|
38
44
|
MIDDLEWARE = [
|
|
39
45
|
"plain.sessions.middleware.SessionMiddleware",
|
|
40
|
-
"plain.auth.middleware.AuthenticationMiddleware",
|
|
41
46
|
"plain.admin.AdminMiddleware",
|
|
42
47
|
]
|
|
43
48
|
```
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
You can access settings anywhere in your application via `plain.runtime.settings`.
|
|
46
51
|
|
|
47
52
|
```python
|
|
48
53
|
from plain.runtime import settings
|
|
49
54
|
|
|
50
|
-
print(settings.
|
|
55
|
+
print(settings.TIME_ZONE)
|
|
56
|
+
print(settings.DEBUG)
|
|
51
57
|
```
|
|
52
58
|
|
|
53
|
-
|
|
59
|
+
Plain's built-in settings are defined in [`global_settings.py`](./global_settings.py). Each installed package can also define its own settings in a `default_settings.py` file.
|
|
54
60
|
|
|
55
61
|
## Environment variables
|
|
56
62
|
|
|
57
|
-
|
|
63
|
+
Type-annotated settings can be loaded from environment variables using a `PLAIN_` prefix.
|
|
58
64
|
|
|
59
|
-
For example,
|
|
65
|
+
For example, if you define a setting with a type annotation:
|
|
60
66
|
|
|
61
67
|
```python
|
|
62
68
|
SECRET_KEY: str
|
|
63
69
|
```
|
|
64
70
|
|
|
65
|
-
|
|
71
|
+
You can set it via an environment variable:
|
|
66
72
|
|
|
67
73
|
```bash
|
|
68
74
|
PLAIN_SECRET_KEY=supersecret
|
|
69
75
|
```
|
|
70
76
|
|
|
71
|
-
For
|
|
77
|
+
For lists, dicts, and other complex types, use JSON-encoded values:
|
|
72
78
|
|
|
73
79
|
```python
|
|
74
|
-
|
|
80
|
+
ALLOWED_HOSTS: list[str]
|
|
75
81
|
```
|
|
76
82
|
|
|
77
|
-
|
|
83
|
+
```bash
|
|
84
|
+
PLAIN_ALLOWED_HOSTS='["example.com", "www.example.com"]'
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Boolean settings accept `true`, `1`, `yes` (case-insensitive) as truthy values:
|
|
78
88
|
|
|
79
89
|
```bash
|
|
80
|
-
|
|
90
|
+
PLAIN_DEBUG=true
|
|
81
91
|
```
|
|
82
92
|
|
|
83
|
-
|
|
93
|
+
### .env files
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
# plain/models/default_settings.py
|
|
87
|
-
from os import environ
|
|
95
|
+
Plain does not load `.env` files automatically. If you use [`plain.dev`](/plain-dev/README.md), it loads `.env` files for you during development. For production, you need to load them yourself or rely on your deployment platform to inject environment variables.
|
|
88
96
|
|
|
89
|
-
|
|
97
|
+
### Custom prefixes
|
|
90
98
|
|
|
91
|
-
|
|
92
|
-
DATABASE: dict
|
|
99
|
+
You can configure additional environment variable prefixes using `ENV_SETTINGS_PREFIXES`:
|
|
93
100
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
environ["DATABASE_URL"],
|
|
98
|
-
# Enable persistent connections by default
|
|
99
|
-
conn_max_age=int(environ.get("DATABASE_CONN_MAX_AGE", 600)),
|
|
100
|
-
conn_health_checks=environ.get("DATABASE_CONN_HEALTH_CHECKS", "true").lower()
|
|
101
|
-
in [
|
|
102
|
-
"true",
|
|
103
|
-
"1",
|
|
104
|
-
],
|
|
105
|
-
)
|
|
101
|
+
```python
|
|
102
|
+
# app/settings.py
|
|
103
|
+
ENV_SETTINGS_PREFIXES = ["PLAIN_", "MYAPP_"]
|
|
106
104
|
```
|
|
107
105
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Plain itself does not load `.env` files automatically, except in development if you use [`plain.dev`](/plain-dev/README.md). If you use `.env` files in production then you will need to load them yourself.
|
|
106
|
+
Now both `PLAIN_DEBUG=true` and `MYAPP_DEBUG=true` would set the `DEBUG` setting. The first matching prefix wins if the same setting appears with multiple prefixes.
|
|
111
107
|
|
|
112
108
|
## Package settings
|
|
113
109
|
|
|
114
|
-
|
|
110
|
+
Installed packages can provide default settings via a `default_settings.py` file. It's best practice to prefix package settings with the package name to avoid conflicts.
|
|
115
111
|
|
|
116
112
|
```python
|
|
117
113
|
# app/users/default_settings.py
|
|
118
114
|
USERS_DEFAULT_ROLE = "user"
|
|
119
115
|
```
|
|
120
116
|
|
|
121
|
-
|
|
117
|
+
To make a setting required (no default value), define it with only a type annotation:
|
|
122
118
|
|
|
123
119
|
```python
|
|
124
120
|
# app/users/default_settings.py
|
|
125
121
|
USERS_DEFAULT_ROLE: str
|
|
126
122
|
```
|
|
127
123
|
|
|
128
|
-
Type annotations
|
|
124
|
+
Type annotations provide basic runtime validation. If a setting is defined as `str` but someone sets it to an `int`, Plain raises an error.
|
|
129
125
|
|
|
130
126
|
```python
|
|
131
127
|
# app/users/default_settings.py
|
|
132
|
-
USERS_DEFAULT_ROLE: str = "user"
|
|
128
|
+
USERS_DEFAULT_ROLE: str = "user" # Optional with a default
|
|
133
129
|
```
|
|
134
130
|
|
|
135
|
-
## Custom app
|
|
131
|
+
## Custom app settings
|
|
136
132
|
|
|
137
|
-
|
|
133
|
+
You can create your own app-wide settings by prefixing them with `APP_`:
|
|
138
134
|
|
|
139
135
|
```python
|
|
140
136
|
# app/settings.py
|
|
137
|
+
import os
|
|
138
|
+
|
|
141
139
|
# A required env setting
|
|
142
140
|
APP_STRIPE_SECRET_KEY = os.environ["STRIPE_SECRET_KEY"]
|
|
143
141
|
|
|
@@ -149,12 +147,82 @@ with open("app/secret_key.txt") as f:
|
|
|
149
147
|
APP_EXAMPLE_KEY = f.read().strip()
|
|
150
148
|
```
|
|
151
149
|
|
|
152
|
-
|
|
150
|
+
Settings without the `APP_` prefix that aren't recognized by Plain or installed packages will raise an error.
|
|
151
|
+
|
|
152
|
+
## Secret values
|
|
153
|
+
|
|
154
|
+
You can mark sensitive settings using the [`Secret`](./secret.py#Secret) type. Secret values are masked when displayed in logs or debugging output.
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from plain.runtime import Secret
|
|
158
|
+
|
|
159
|
+
SECRET_KEY: Secret[str]
|
|
160
|
+
DATABASE_PASSWORD: Secret[str]
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
At runtime, the value is still a plain string. The `Secret` type is purely a marker that tells Plain to mask the value when displaying settings.
|
|
164
|
+
|
|
165
|
+
## Using Plain outside of an app
|
|
153
166
|
|
|
154
|
-
|
|
167
|
+
If you need to use Plain in a standalone script, call `plain.runtime.setup()` first:
|
|
155
168
|
|
|
156
169
|
```python
|
|
157
170
|
import plain.runtime
|
|
158
171
|
|
|
159
172
|
plain.runtime.setup()
|
|
173
|
+
|
|
174
|
+
# Now you can use Plain normally
|
|
175
|
+
from plain.runtime import settings
|
|
176
|
+
print(settings.DEBUG)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The `setup()` function configures settings, logging, and populates the package registry. You can only call it once.
|
|
180
|
+
|
|
181
|
+
## FAQs
|
|
182
|
+
|
|
183
|
+
#### Where are the default settings defined?
|
|
184
|
+
|
|
185
|
+
Plain's core settings are in [`global_settings.py`](./global_settings.py). Each installed package can also have a `default_settings.py` file with package-specific defaults.
|
|
186
|
+
|
|
187
|
+
#### How do I see what settings are available?
|
|
188
|
+
|
|
189
|
+
Check [`global_settings.py`](./global_settings.py) for core settings. For package-specific settings, look at the `default_settings.py` file in each package.
|
|
190
|
+
|
|
191
|
+
#### What's the difference between required and optional settings?
|
|
192
|
+
|
|
193
|
+
A setting with only a type annotation (no value) is required:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
SECRET_KEY: str # Required - must be set
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
A setting with a value is optional (has a default):
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
DEBUG: bool = False # Optional - defaults to False
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
#### Can I modify settings at runtime?
|
|
206
|
+
|
|
207
|
+
Yes, you can assign new values to settings after setup:
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from plain.runtime import settings
|
|
211
|
+
|
|
212
|
+
settings.DEBUG = True
|
|
160
213
|
```
|
|
214
|
+
|
|
215
|
+
#### What paths are available without setup?
|
|
216
|
+
|
|
217
|
+
`APP_PATH` and `PLAIN_TEMP_PATH` are available immediately without calling `setup()`:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from plain.runtime import APP_PATH, PLAIN_TEMP_PATH
|
|
221
|
+
|
|
222
|
+
print(APP_PATH) # /path/to/project/app
|
|
223
|
+
print(PLAIN_TEMP_PATH) # /path/to/project/.plain
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Installation
|
|
227
|
+
|
|
228
|
+
The runtime module is included with Plain by default. No additional installation is required.
|
plain/runtime/__init__.py
CHANGED
|
@@ -2,10 +2,12 @@ import importlib.metadata
|
|
|
2
2
|
import sys
|
|
3
3
|
from importlib.metadata import entry_points
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import Self
|
|
5
6
|
|
|
6
7
|
from plain.logs.configure import configure_logging
|
|
7
8
|
from plain.packages import packages_registry
|
|
8
9
|
|
|
10
|
+
from .secret import Secret
|
|
9
11
|
from .user_settings import Settings
|
|
10
12
|
|
|
11
13
|
try:
|
|
@@ -32,7 +34,7 @@ class SetupError(RuntimeError):
|
|
|
32
34
|
pass
|
|
33
35
|
|
|
34
36
|
|
|
35
|
-
def setup():
|
|
37
|
+
def setup() -> None:
|
|
36
38
|
"""
|
|
37
39
|
Configure the settings (this happens as a side effect of accessing the
|
|
38
40
|
first setting), configure logging and populate the app registry.
|
|
@@ -63,9 +65,10 @@ def setup():
|
|
|
63
65
|
sys.path.insert(0, APP_PATH.parent.as_posix())
|
|
64
66
|
|
|
65
67
|
configure_logging(
|
|
66
|
-
plain_log_level=settings.
|
|
67
|
-
app_log_level=settings.
|
|
68
|
-
app_log_format=settings.
|
|
68
|
+
plain_log_level=settings.FRAMEWORK_LOG_LEVEL,
|
|
69
|
+
app_log_level=settings.LOG_LEVEL,
|
|
70
|
+
app_log_format=settings.LOG_FORMAT,
|
|
71
|
+
log_stream=settings.LOG_STREAM,
|
|
69
72
|
)
|
|
70
73
|
|
|
71
74
|
packages_registry.populate(settings.INSTALLED_PACKAGES)
|
|
@@ -77,17 +80,18 @@ class SettingsReference(str):
|
|
|
77
80
|
the value in memory but serializes to a settings.NAME attribute reference.
|
|
78
81
|
"""
|
|
79
82
|
|
|
80
|
-
def __new__(self, setting_name):
|
|
83
|
+
def __new__(self, setting_name: str) -> Self:
|
|
81
84
|
value = getattr(settings, setting_name)
|
|
82
85
|
return str.__new__(self, value)
|
|
83
86
|
|
|
84
|
-
def __init__(self, setting_name):
|
|
87
|
+
def __init__(self, setting_name: str):
|
|
85
88
|
self.setting_name = setting_name
|
|
86
89
|
|
|
87
90
|
|
|
88
91
|
__all__ = [
|
|
89
92
|
"setup",
|
|
90
93
|
"settings",
|
|
94
|
+
"Secret",
|
|
91
95
|
"SettingsReference",
|
|
92
96
|
"APP_PATH",
|
|
93
97
|
"__version__",
|
plain/runtime/global_settings.py
CHANGED
|
@@ -3,8 +3,7 @@ Default Plain settings. Override these with settings in the module pointed to
|
|
|
3
3
|
by the PLAIN_SETTINGS_MODULE environment variable.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
6
|
+
from .secret import Secret
|
|
8
7
|
from .utils import get_app_info_from_pyproject
|
|
9
8
|
|
|
10
9
|
# MARK: Core Settings
|
|
@@ -12,14 +11,20 @@ from .utils import get_app_info_from_pyproject
|
|
|
12
11
|
DEBUG: bool = False
|
|
13
12
|
|
|
14
13
|
name, version = get_app_info_from_pyproject()
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
NAME: str = name
|
|
15
|
+
VERSION: str = version
|
|
17
16
|
|
|
18
17
|
# List of strings representing installed packages.
|
|
19
18
|
INSTALLED_PACKAGES: list[str] = []
|
|
20
19
|
|
|
21
20
|
URLS_ROUTER: str
|
|
22
21
|
|
|
22
|
+
# List of environment variable prefixes to check for settings.
|
|
23
|
+
# Settings can be configured via environment variables using these prefixes.
|
|
24
|
+
# Example: ENV_SETTINGS_PREFIXES = ["PLAIN_", "MYAPP_"]
|
|
25
|
+
# Then both PLAIN_DEBUG and MYAPP_DEBUG would set the DEBUG setting.
|
|
26
|
+
ENV_SETTINGS_PREFIXES: list[str] = ["PLAIN_"]
|
|
27
|
+
|
|
23
28
|
# MARK: HTTP and Security
|
|
24
29
|
|
|
25
30
|
# Hosts/domain names that are valid for this site.
|
|
@@ -29,8 +34,12 @@ URLS_ROUTER: str
|
|
|
29
34
|
ALLOWED_HOSTS: list[str] = []
|
|
30
35
|
|
|
31
36
|
# Default headers for all responses.
|
|
32
|
-
|
|
33
|
-
|
|
37
|
+
# Header values can include {request.attribute} placeholders for dynamic content.
|
|
38
|
+
# Example: "script-src 'nonce-{request.csp_nonce}'" will use the request's nonce.
|
|
39
|
+
# Views can override, remove, or extend these headers - see plain/http/README.md
|
|
40
|
+
# for customization patterns.
|
|
41
|
+
DEFAULT_RESPONSE_HEADERS: dict = {
|
|
42
|
+
# "Content-Security-Policy": "default-src 'self'; script-src 'self' 'nonce-{request.csp_nonce}'",
|
|
34
43
|
# https://hstspreload.org/
|
|
35
44
|
# "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
36
45
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -47,25 +56,28 @@ HTTPS_REDIRECT_ENABLED = True
|
|
|
47
56
|
# If your Plain app is behind a proxy that sets a header to specify secure
|
|
48
57
|
# connections, AND that proxy ensures that user-submitted headers with the
|
|
49
58
|
# same name are ignored (so that people can't spoof it), set this value to
|
|
50
|
-
# a
|
|
51
|
-
# that header/value, request.is_https() will return True.
|
|
59
|
+
# a string in the format "Header-Name: value". For any requests that come in
|
|
60
|
+
# with that header/value, request.is_https() will return True.
|
|
52
61
|
# WARNING! Only set this if you fully understand what you're doing. Otherwise,
|
|
53
62
|
# you may be opening yourself up to a security risk.
|
|
54
|
-
HTTPS_PROXY_HEADER =
|
|
63
|
+
# Example: HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"
|
|
64
|
+
HTTPS_PROXY_HEADER: str = ""
|
|
55
65
|
|
|
56
|
-
# Whether to use the X-Forwarded-Host and X-Forwarded-
|
|
57
|
-
# when determining the host and
|
|
58
|
-
|
|
59
|
-
|
|
66
|
+
# Whether to use the X-Forwarded-Host, X-Forwarded-Port, and X-Forwarded-For
|
|
67
|
+
# headers when determining the host, port, and client IP for the request.
|
|
68
|
+
# Only enable these when behind a trusted proxy that overwrites these headers.
|
|
69
|
+
HTTP_X_FORWARDED_HOST: bool = False
|
|
70
|
+
HTTP_X_FORWARDED_PORT: bool = False
|
|
71
|
+
HTTP_X_FORWARDED_FOR: bool = False
|
|
60
72
|
|
|
61
73
|
# A secret key for this particular Plain installation. Used in secret-key
|
|
62
74
|
# hashing algorithms. Set this in your settings, or Plain will complain
|
|
63
75
|
# loudly.
|
|
64
|
-
SECRET_KEY: str
|
|
76
|
+
SECRET_KEY: Secret[str]
|
|
65
77
|
|
|
66
78
|
# List of secret keys used to verify the validity of signatures. This allows
|
|
67
79
|
# secret key rotation.
|
|
68
|
-
SECRET_KEY_FALLBACKS: list[str] = []
|
|
80
|
+
SECRET_KEY_FALLBACKS: Secret[list[str]] = [] # type: ignore[assignment]
|
|
69
81
|
|
|
70
82
|
# MARK: Internationalization
|
|
71
83
|
|
|
@@ -97,15 +109,15 @@ FILE_UPLOAD_HANDLERS = [
|
|
|
97
109
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
|
|
98
110
|
|
|
99
111
|
# Maximum size in bytes of request data (excluding file uploads) that will be
|
|
100
|
-
# read before a
|
|
112
|
+
# read before a SuspiciousOperationError400 (RequestDataTooBigError400) is raised.
|
|
101
113
|
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
|
|
102
114
|
|
|
103
115
|
# Maximum number of GET/POST parameters that will be read before a
|
|
104
|
-
#
|
|
116
|
+
# SuspiciousOperationError400 (TooManyFieldsSentError400) is raised.
|
|
105
117
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
|
|
106
118
|
|
|
107
119
|
# Maximum number of files encoded in a multipart upload that will be read
|
|
108
|
-
# before a
|
|
120
|
+
# before a SuspiciousOperationError400 (TooManyFilesSentError400) is raised.
|
|
109
121
|
DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
110
122
|
|
|
111
123
|
# Directory in which upload streamed files will be temporarily saved. A value of
|
|
@@ -113,9 +125,6 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
|
113
125
|
# (i.e. "/tmp" on *nix systems).
|
|
114
126
|
FILE_UPLOAD_TEMP_DIR = None
|
|
115
127
|
|
|
116
|
-
# User-defined overrides for error views by status code
|
|
117
|
-
HTTP_ERROR_VIEWS: dict[int] = {}
|
|
118
|
-
|
|
119
128
|
# MARK: Middleware
|
|
120
129
|
|
|
121
130
|
# List of middleware to use. Order is important; in the request phase, these
|
|
@@ -135,11 +144,11 @@ CSRF_TRUSTED_ORIGINS: list[str] = []
|
|
|
135
144
|
CSRF_EXEMPT_PATHS: list[str] = []
|
|
136
145
|
|
|
137
146
|
# MARK: Logging
|
|
138
|
-
# (Uses some custom env names in addition to PLAIN_ prefixed )
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
FRAMEWORK_LOG_LEVEL: str = "INFO"
|
|
149
|
+
LOG_LEVEL: str = "INFO"
|
|
150
|
+
LOG_FORMAT: str = "keyvalue"
|
|
151
|
+
LOG_STREAM: str = "split" # "split", "stdout", or "stderr"
|
|
143
152
|
|
|
144
153
|
# MARK: Assets
|
|
145
154
|
|
plain/runtime/secret.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Generic, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Secret(Generic[T]):
|
|
9
|
+
"""
|
|
10
|
+
Marker type for sensitive settings. Values are masked in output/logs.
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
SECRET_KEY: Secret[str]
|
|
14
|
+
DATABASE_PASSWORD: Secret[str]
|
|
15
|
+
|
|
16
|
+
At runtime, the value is still a plain str - this is purely for
|
|
17
|
+
indicating that the setting should be masked when displayed.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
pass
|