plain 0.68.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 (195) hide show
  1. plain/CHANGELOG.md +656 -1
  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 -36
  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 +110 -26
  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 +27 -75
  25. plain/cli/print.py +4 -4
  26. plain/cli/registry.py +96 -10
  27. plain/cli/{agent/request.py → request.py} +67 -33
  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 -8
  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 +27 -16
  82. plain/paginator.py +31 -21
  83. plain/preflight/README.md +209 -24
  84. plain/preflight/__init__.py +1 -0
  85. plain/preflight/checks.py +3 -1
  86. plain/preflight/files.py +3 -1
  87. plain/preflight/registry.py +26 -11
  88. plain/preflight/results.py +15 -7
  89. plain/preflight/security.py +15 -13
  90. plain/preflight/settings.py +54 -0
  91. plain/preflight/urls.py +4 -1
  92. plain/runtime/README.md +115 -47
  93. plain/runtime/__init__.py +10 -6
  94. plain/runtime/global_settings.py +34 -25
  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 +13 -5
  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 +38 -22
  145. plain/urls/resolvers.py +35 -25
  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.68.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.68.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/csrf/views.py +0 -31
  191. plain/logs/utils.py +0 -46
  192. plain/templates/AGENTS.md +0 -3
  193. plain-0.68.0.dist-info/RECORD +0 -169
  194. plain-0.68.0.dist-info/entry_points.txt +0 -5
  195. {plain-0.68.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/runtime/README.md CHANGED
@@ -1,20 +1,28 @@
1
1
  # Runtime
2
2
 
3
- **Access app and package settings at runtime.**
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-wide settings](#custom-app-wide-settings)
10
- - [Using Plain in other environments](#using-plain-in-other-environments)
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
- Plain is configured by "settings", which are ultimately just Python variables. Most settings have default values which can be overidden either by your `app/settings.py` file or by environment variables.
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
- While working inside a Plain application or package, you can access settings at runtime via `plain.runtime.settings`.
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.AN_EXAMPLE_SETTING)
55
+ print(settings.TIME_ZONE)
56
+ print(settings.DEBUG)
51
57
  ```
52
58
 
53
- The Plain core settings are defined in [`plain/runtime/global_settings.py`](global_settings.py) and you should look at that for reference. Each installed package can also define its own settings in a `default_settings.py` file.
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
- It's common in both development and production to use environment variables to manage settings. To handle this, any type-annotated setting can be loaded from the env with a `PLAIN_` prefix.
63
+ Type-annotated settings can be loaded from environment variables using a `PLAIN_` prefix.
58
64
 
59
- For example, to set the `SECRET_KEY` setting is defined with a type annotation.
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
- And can be set by an environment variable.
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 more complex types like lists or dictionaries, just use the `list` or `dict` type annotation and JSON-compatible types.
77
+ For lists, dicts, and other complex types, use JSON-encoded values:
72
78
 
73
79
  ```python
74
- LIST_EXAMPLE: list[str]
80
+ ALLOWED_HOSTS: list[str]
75
81
  ```
76
82
 
77
- And set the environment variable with a JSON-encoded string.
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
- PLAIN_LIST_EXAMPLE='["one", "two", "three"]'
90
+ PLAIN_DEBUG=true
81
91
  ```
82
92
 
83
- Custom behavior can always be supported by checking the environment directly.
93
+ ### .env files
84
94
 
85
- ```python
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
- from . import database_url
97
+ ### Custom prefixes
90
98
 
91
- # Make DATABASE a required setting
92
- DATABASE: dict
99
+ You can configure additional environment variable prefixes using `ENV_SETTINGS_PREFIXES`:
93
100
 
94
- # Automatically configure DATABASE if a DATABASE_URL was given in the environment
95
- if "DATABASE_URL" in environ:
96
- DATABASE = database_url.parse_database_url(
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
- ### .env files
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
- An installed package can provide a `default_settings.py` file. It is strongly recommended to prefix any defined settings with the package name to avoid conflicts.
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
- The way you define these settings can impact the runtime behavior. For example, a required setting should be defined with a type annotation but no default value.
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 are only required for settings that don't provide a default value (to enable the environment variable loading). But generally type annotations are recommended as they also provide basic validation at runtime if a setting is defined as a `str` but the user sets it to an `int`, an error will be raised.
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-wide settings
131
+ ## Custom app settings
136
132
 
137
- At times it can be useful to create your own settings that are used across your application. When you define these in `app/settings.py`, you simply prefix them with `APP_` which marks them as a custom setting.
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
- ## Using Plain in other environments
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
- There may be some situations where you want to manually invoke Plain, like in a Python script. To get everything set up, you can call the `plain.runtime.setup()` function.
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.PLAIN_LOG_LEVEL,
67
- app_log_level=settings.APP_LOG_LEVEL,
68
- app_log_format=settings.APP_LOG_FORMAT,
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__",
@@ -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 os import environ
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
- APP_NAME: str = name
16
- APP_VERSION: str = version
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
- DEFAULT_RESPONSE_HEADERS = {
33
- # "Content-Security-Policy": "default-src 'self'",
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 tuple of (header_name, header_value). For any requests that come in with
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 = None
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-Port headers
57
- # when determining the host and port for the request.
58
- USE_X_FORWARDED_HOST = False
59
- USE_X_FORWARDED_PORT = False
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 SuspiciousOperation (RequestDataTooBig) is raised.
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
- # SuspiciousOperation (TooManyFieldsSent) is raised.
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 SuspiciousOperation (TooManyFilesSent) is raised.
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
- PLAIN_LOG_LEVEL: str = environ.get("PLAIN_LOG_LEVEL", "INFO")
141
- APP_LOG_LEVEL: str = environ.get("APP_LOG_LEVEL", "INFO")
142
- APP_LOG_FORMAT: str = environ.get("APP_LOG_FORMAT", "keyvalue")
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
 
@@ -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