plain 0.66.0__py3-none-any.whl → 0.101.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- plain/CHANGELOG.md +684 -0
- plain/README.md +1 -1
- plain/assets/compile.py +25 -12
- plain/assets/finders.py +24 -17
- plain/assets/fingerprints.py +10 -7
- plain/assets/urls.py +1 -1
- plain/assets/views.py +47 -33
- plain/chores/README.md +25 -23
- plain/chores/__init__.py +2 -1
- plain/chores/core.py +27 -0
- plain/chores/registry.py +23 -53
- plain/cli/README.md +185 -16
- plain/cli/__init__.py +2 -1
- plain/cli/agent.py +236 -0
- plain/cli/build.py +7 -8
- plain/cli/changelog.py +11 -5
- plain/cli/chores.py +32 -34
- plain/cli/core.py +112 -28
- plain/cli/docs.py +52 -11
- plain/cli/formatting.py +40 -17
- plain/cli/install.py +10 -54
- plain/cli/{agent/llmdocs.py → llmdocs.py} +21 -9
- plain/cli/output.py +6 -2
- plain/cli/preflight.py +175 -102
- plain/cli/print.py +4 -4
- plain/cli/registry.py +95 -26
- plain/cli/request.py +206 -0
- plain/cli/runtime.py +45 -0
- plain/cli/scaffold.py +2 -7
- plain/cli/server.py +153 -0
- plain/cli/settings.py +53 -49
- plain/cli/shell.py +15 -12
- plain/cli/startup.py +9 -8
- plain/cli/upgrade.py +17 -104
- plain/cli/urls.py +12 -7
- plain/cli/utils.py +3 -3
- plain/csrf/README.md +65 -40
- plain/csrf/middleware.py +53 -43
- plain/debug.py +5 -2
- plain/exceptions.py +22 -114
- plain/forms/README.md +453 -24
- plain/forms/__init__.py +55 -4
- plain/forms/boundfield.py +15 -8
- plain/forms/exceptions.py +1 -1
- plain/forms/fields.py +346 -143
- plain/forms/forms.py +75 -45
- plain/http/README.md +356 -9
- plain/http/__init__.py +41 -26
- plain/http/cookie.py +15 -7
- plain/http/exceptions.py +65 -0
- plain/http/middleware.py +32 -0
- plain/http/multipartparser.py +99 -88
- plain/http/request.py +362 -250
- plain/http/response.py +99 -197
- plain/internal/__init__.py +8 -1
- plain/internal/files/base.py +35 -19
- plain/internal/files/locks.py +19 -11
- plain/internal/files/move.py +8 -3
- plain/internal/files/temp.py +25 -6
- plain/internal/files/uploadedfile.py +47 -28
- plain/internal/files/uploadhandler.py +64 -58
- plain/internal/files/utils.py +24 -10
- plain/internal/handlers/base.py +34 -23
- plain/internal/handlers/exception.py +68 -65
- plain/internal/handlers/wsgi.py +65 -54
- plain/internal/middleware/headers.py +37 -11
- plain/internal/middleware/hosts.py +11 -13
- plain/internal/middleware/https.py +17 -7
- plain/internal/middleware/slash.py +14 -9
- plain/internal/reloader.py +77 -0
- plain/json.py +2 -1
- plain/logs/README.md +161 -62
- plain/logs/__init__.py +1 -1
- plain/logs/{loggers.py → app.py} +71 -67
- plain/logs/configure.py +63 -14
- plain/logs/debug.py +17 -6
- plain/logs/filters.py +15 -0
- plain/logs/formatters.py +7 -4
- plain/packages/README.md +105 -23
- plain/packages/config.py +15 -7
- plain/packages/registry.py +40 -15
- plain/paginator.py +31 -21
- plain/preflight/README.md +208 -23
- plain/preflight/__init__.py +5 -24
- plain/preflight/checks.py +12 -0
- plain/preflight/files.py +19 -13
- plain/preflight/registry.py +80 -58
- plain/preflight/results.py +37 -0
- plain/preflight/security.py +65 -71
- plain/preflight/settings.py +54 -0
- plain/preflight/urls.py +10 -48
- plain/runtime/README.md +115 -47
- plain/runtime/__init__.py +10 -6
- plain/runtime/global_settings.py +43 -33
- plain/runtime/secret.py +20 -0
- plain/runtime/user_settings.py +110 -38
- plain/runtime/utils.py +1 -1
- plain/server/LICENSE +35 -0
- plain/server/README.md +155 -0
- plain/server/__init__.py +9 -0
- plain/server/app.py +52 -0
- plain/server/arbiter.py +555 -0
- plain/server/config.py +118 -0
- plain/server/errors.py +31 -0
- plain/server/glogging.py +292 -0
- plain/server/http/__init__.py +12 -0
- plain/server/http/body.py +283 -0
- plain/server/http/errors.py +155 -0
- plain/server/http/message.py +400 -0
- plain/server/http/parser.py +70 -0
- plain/server/http/unreader.py +88 -0
- plain/server/http/wsgi.py +421 -0
- plain/server/pidfile.py +92 -0
- plain/server/sock.py +240 -0
- plain/server/util.py +317 -0
- plain/server/workers/__init__.py +6 -0
- plain/server/workers/base.py +304 -0
- plain/server/workers/sync.py +212 -0
- plain/server/workers/thread.py +399 -0
- plain/server/workers/workertmp.py +50 -0
- plain/signals/README.md +170 -1
- plain/signals/__init__.py +0 -1
- plain/signals/dispatch/dispatcher.py +49 -27
- plain/signing.py +131 -35
- plain/skills/README.md +36 -0
- plain/skills/plain-docs/SKILL.md +25 -0
- plain/skills/plain-install/SKILL.md +26 -0
- plain/skills/plain-request/SKILL.md +39 -0
- plain/skills/plain-shell/SKILL.md +24 -0
- plain/skills/plain-upgrade/SKILL.md +35 -0
- plain/templates/README.md +211 -20
- plain/templates/jinja/__init__.py +14 -27
- plain/templates/jinja/environments.py +5 -4
- plain/templates/jinja/extensions.py +12 -5
- plain/templates/jinja/filters.py +7 -2
- plain/templates/jinja/globals.py +2 -2
- plain/test/README.md +184 -22
- plain/test/client.py +340 -222
- plain/test/encoding.py +9 -6
- plain/test/exceptions.py +7 -2
- plain/urls/README.md +157 -73
- plain/urls/converters.py +18 -15
- plain/urls/exceptions.py +2 -2
- plain/urls/patterns.py +56 -40
- plain/urls/resolvers.py +38 -28
- plain/urls/utils.py +5 -1
- plain/utils/README.md +250 -3
- plain/utils/cache.py +17 -11
- plain/utils/crypto.py +21 -5
- plain/utils/datastructures.py +89 -56
- plain/utils/dateparse.py +9 -6
- plain/utils/deconstruct.py +15 -7
- plain/utils/decorators.py +5 -1
- plain/utils/dotenv.py +373 -0
- plain/utils/duration.py +8 -4
- plain/utils/encoding.py +14 -7
- plain/utils/functional.py +66 -49
- plain/utils/hashable.py +5 -1
- plain/utils/html.py +36 -22
- plain/utils/http.py +16 -9
- plain/utils/inspect.py +14 -6
- plain/utils/ipv6.py +7 -3
- plain/utils/itercompat.py +6 -1
- plain/utils/module_loading.py +7 -3
- plain/utils/regex_helper.py +37 -23
- plain/utils/safestring.py +14 -6
- plain/utils/text.py +41 -23
- plain/utils/timezone.py +33 -22
- plain/utils/tree.py +35 -19
- plain/validators.py +94 -52
- plain/views/README.md +156 -79
- plain/views/__init__.py +0 -1
- plain/views/base.py +25 -18
- plain/views/errors.py +13 -5
- plain/views/exceptions.py +4 -1
- plain/views/forms.py +6 -6
- plain/views/objects.py +52 -49
- plain/views/redirect.py +18 -15
- plain/views/templates.py +5 -3
- plain/wsgi.py +3 -1
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/METADATA +4 -2
- plain-0.101.2.dist-info/RECORD +201 -0
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/WHEEL +1 -1
- plain-0.101.2.dist-info/entry_points.txt +2 -0
- plain/AGENTS.md +0 -18
- plain/cli/agent/__init__.py +0 -20
- plain/cli/agent/docs.py +0 -80
- plain/cli/agent/md.py +0 -87
- plain/cli/agent/prompt.py +0 -45
- plain/cli/agent/request.py +0 -181
- plain/csrf/views.py +0 -31
- plain/logs/utils.py +0 -46
- plain/preflight/messages.py +0 -81
- plain/templates/AGENTS.md +0 -3
- plain-0.66.0.dist-info/RECORD +0 -168
- plain-0.66.0.dist-info/entry_points.txt +0 -4
- {plain-0.66.0.dist-info → plain-0.101.2.dist-info}/licenses/LICENSE +0 -0
plain/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,24 +11,35 @@ 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.
|
|
26
|
-
#
|
|
27
|
-
# "
|
|
28
|
-
|
|
31
|
+
# - An empty list [] allows all hosts (useful for development).
|
|
32
|
+
# - ".example.com" matches example.com and all subdomains
|
|
33
|
+
# - "192.168.1.0/24" matches IP addresses in that CIDR range
|
|
34
|
+
ALLOWED_HOSTS: list[str] = []
|
|
29
35
|
|
|
30
36
|
# Default headers for all responses.
|
|
31
|
-
|
|
32
|
-
|
|
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}'",
|
|
33
43
|
# https://hstspreload.org/
|
|
34
44
|
# "Strict-Transport-Security": "max-age=31536000; includeSubDomains; preload",
|
|
35
45
|
"Cross-Origin-Opener-Policy": "same-origin",
|
|
@@ -46,25 +56,28 @@ HTTPS_REDIRECT_ENABLED = True
|
|
|
46
56
|
# If your Plain app is behind a proxy that sets a header to specify secure
|
|
47
57
|
# connections, AND that proxy ensures that user-submitted headers with the
|
|
48
58
|
# same name are ignored (so that people can't spoof it), set this value to
|
|
49
|
-
# a
|
|
50
|
-
# 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.
|
|
51
61
|
# WARNING! Only set this if you fully understand what you're doing. Otherwise,
|
|
52
62
|
# you may be opening yourself up to a security risk.
|
|
53
|
-
HTTPS_PROXY_HEADER =
|
|
63
|
+
# Example: HTTPS_PROXY_HEADER = "X-Forwarded-Proto: https"
|
|
64
|
+
HTTPS_PROXY_HEADER: str = ""
|
|
54
65
|
|
|
55
|
-
# Whether to use the X-Forwarded-Host and X-Forwarded-
|
|
56
|
-
# when determining the host and
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
59
72
|
|
|
60
73
|
# A secret key for this particular Plain installation. Used in secret-key
|
|
61
74
|
# hashing algorithms. Set this in your settings, or Plain will complain
|
|
62
75
|
# loudly.
|
|
63
|
-
SECRET_KEY: str
|
|
76
|
+
SECRET_KEY: Secret[str]
|
|
64
77
|
|
|
65
78
|
# List of secret keys used to verify the validity of signatures. This allows
|
|
66
79
|
# secret key rotation.
|
|
67
|
-
SECRET_KEY_FALLBACKS: list[str] = []
|
|
80
|
+
SECRET_KEY_FALLBACKS: Secret[list[str]] = [] # type: ignore[assignment]
|
|
68
81
|
|
|
69
82
|
# MARK: Internationalization
|
|
70
83
|
|
|
@@ -96,15 +109,15 @@ FILE_UPLOAD_HANDLERS = [
|
|
|
96
109
|
FILE_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
|
|
97
110
|
|
|
98
111
|
# Maximum size in bytes of request data (excluding file uploads) that will be
|
|
99
|
-
# read before a
|
|
112
|
+
# read before a SuspiciousOperationError400 (RequestDataTooBigError400) is raised.
|
|
100
113
|
DATA_UPLOAD_MAX_MEMORY_SIZE = 2621440 # i.e. 2.5 MB
|
|
101
114
|
|
|
102
115
|
# Maximum number of GET/POST parameters that will be read before a
|
|
103
|
-
#
|
|
116
|
+
# SuspiciousOperationError400 (TooManyFieldsSentError400) is raised.
|
|
104
117
|
DATA_UPLOAD_MAX_NUMBER_FIELDS = 1000
|
|
105
118
|
|
|
106
119
|
# Maximum number of files encoded in a multipart upload that will be read
|
|
107
|
-
# before a
|
|
120
|
+
# before a SuspiciousOperationError400 (TooManyFilesSentError400) is raised.
|
|
108
121
|
DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
109
122
|
|
|
110
123
|
# Directory in which upload streamed files will be temporarily saved. A value of
|
|
@@ -112,9 +125,6 @@ DATA_UPLOAD_MAX_NUMBER_FILES = 100
|
|
|
112
125
|
# (i.e. "/tmp" on *nix systems).
|
|
113
126
|
FILE_UPLOAD_TEMP_DIR = None
|
|
114
127
|
|
|
115
|
-
# User-defined overrides for error views by status code
|
|
116
|
-
HTTP_ERROR_VIEWS: dict[int] = {}
|
|
117
|
-
|
|
118
128
|
# MARK: Middleware
|
|
119
129
|
|
|
120
130
|
# List of middleware to use. Order is important; in the request phase, these
|
|
@@ -134,11 +144,11 @@ CSRF_TRUSTED_ORIGINS: list[str] = []
|
|
|
134
144
|
CSRF_EXEMPT_PATHS: list[str] = []
|
|
135
145
|
|
|
136
146
|
# MARK: Logging
|
|
137
|
-
# (Uses some custom env names in addition to PLAIN_ prefixed )
|
|
138
147
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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"
|
|
142
152
|
|
|
143
153
|
# MARK: Assets
|
|
144
154
|
|
|
@@ -151,11 +161,11 @@ ASSETS_BASE_URL: str = ""
|
|
|
151
161
|
|
|
152
162
|
# MARK: Preflight Checks
|
|
153
163
|
|
|
154
|
-
#
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
#
|
|
158
|
-
|
|
164
|
+
# Silence checks by name
|
|
165
|
+
PREFLIGHT_SILENCED_CHECKS: list[str] = []
|
|
166
|
+
|
|
167
|
+
# Silence specific check results by id
|
|
168
|
+
PREFLIGHT_SILENCED_RESULTS: list[str] = []
|
|
159
169
|
|
|
160
170
|
# MARK: Templates
|
|
161
171
|
|
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
|
plain/runtime/user_settings.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import importlib
|
|
2
4
|
import json
|
|
3
5
|
import os
|
|
@@ -9,10 +11,11 @@ from pathlib import Path
|
|
|
9
11
|
|
|
10
12
|
from plain.exceptions import ImproperlyConfigured
|
|
11
13
|
from plain.packages import PackageConfig
|
|
14
|
+
from plain.runtime.secret import Secret
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
_ENVIRONMENT_VARIABLE = "PLAIN_SETTINGS_MODULE"
|
|
17
|
+
_DEFAULT_ENV_SETTINGS_PREFIXES = ["PLAIN_"]
|
|
18
|
+
_CUSTOM_SETTINGS_PREFIX = "APP_"
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
class Settings:
|
|
@@ -26,13 +29,14 @@ class Settings:
|
|
|
26
29
|
Lazy initialization is implemented to defer loading until settings are first accessed.
|
|
27
30
|
"""
|
|
28
31
|
|
|
29
|
-
def __init__(self, settings_module=None):
|
|
32
|
+
def __init__(self, settings_module: str | None = None):
|
|
30
33
|
self._settings_module = settings_module
|
|
31
|
-
self._settings = {}
|
|
32
|
-
self._errors = [] # Collect configuration errors
|
|
34
|
+
self._settings: dict[str, SettingDefinition] = {}
|
|
35
|
+
self._errors: list[str] = [] # Collect configuration errors
|
|
36
|
+
self._env_prefixes: list[str] = [] # Configured env prefixes
|
|
33
37
|
self.configured = False
|
|
34
38
|
|
|
35
|
-
def _setup(self):
|
|
39
|
+
def _setup(self) -> None:
|
|
36
40
|
if self.configured:
|
|
37
41
|
return
|
|
38
42
|
else:
|
|
@@ -42,7 +46,9 @@ class Settings:
|
|
|
42
46
|
|
|
43
47
|
# Determine the settings module
|
|
44
48
|
if self._settings_module is None:
|
|
45
|
-
self._settings_module = os.environ.get(
|
|
49
|
+
self._settings_module = os.environ.get(
|
|
50
|
+
_ENVIRONMENT_VARIABLE, "app.settings"
|
|
51
|
+
)
|
|
46
52
|
|
|
47
53
|
# First load the global settings from plain
|
|
48
54
|
self._load_module_settings(
|
|
@@ -58,8 +64,14 @@ class Settings:
|
|
|
58
64
|
)
|
|
59
65
|
|
|
60
66
|
# Keep a reference to the settings.py module path
|
|
67
|
+
assert mod.__file__ is not None
|
|
61
68
|
self.path = Path(mod.__file__).resolve()
|
|
62
69
|
|
|
70
|
+
# Get env prefixes from settings module (must be configured in settings.py, not env)
|
|
71
|
+
self._env_prefixes = getattr(
|
|
72
|
+
mod, "ENV_SETTINGS_PREFIXES", _DEFAULT_ENV_SETTINGS_PREFIXES
|
|
73
|
+
)
|
|
74
|
+
|
|
63
75
|
# Load default settings from installed packages
|
|
64
76
|
self._load_default_settings(mod)
|
|
65
77
|
# Load environment settings
|
|
@@ -71,7 +83,7 @@ class Settings:
|
|
|
71
83
|
# Check for any collected errors
|
|
72
84
|
self._raise_errors_if_any()
|
|
73
85
|
|
|
74
|
-
def _load_module_settings(self, module):
|
|
86
|
+
def _load_module_settings(self, module: types.ModuleType) -> None:
|
|
75
87
|
annotations = getattr(module, "__annotations__", {})
|
|
76
88
|
settings = dir(module)
|
|
77
89
|
|
|
@@ -100,7 +112,7 @@ class Settings:
|
|
|
100
112
|
required=True,
|
|
101
113
|
)
|
|
102
114
|
|
|
103
|
-
def _load_default_settings(self, settings_module):
|
|
115
|
+
def _load_default_settings(self, settings_module: types.ModuleType) -> None:
|
|
104
116
|
for entry in getattr(settings_module, "INSTALLED_PACKAGES", []):
|
|
105
117
|
if isinstance(entry, PackageConfig):
|
|
106
118
|
app_settings = entry.module.default_settings
|
|
@@ -111,13 +123,20 @@ class Settings:
|
|
|
111
123
|
|
|
112
124
|
self._load_module_settings(app_settings)
|
|
113
125
|
|
|
114
|
-
def _load_env_settings(self):
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
for
|
|
126
|
+
def _load_env_settings(self) -> None:
|
|
127
|
+
# Collect env settings from all configured prefixes
|
|
128
|
+
# First prefix wins if same setting appears with multiple prefixes
|
|
129
|
+
env_settings: dict[
|
|
130
|
+
str, tuple[str, str]
|
|
131
|
+
] = {} # setting_name -> (value, env_var)
|
|
132
|
+
for prefix in self._env_prefixes:
|
|
133
|
+
for key, value in os.environ.items():
|
|
134
|
+
if key.startswith(prefix) and key.isupper():
|
|
135
|
+
setting_name = key[len(prefix) :]
|
|
136
|
+
if setting_name and setting_name not in env_settings:
|
|
137
|
+
env_settings[setting_name] = (value, key)
|
|
138
|
+
|
|
139
|
+
for setting, (value, env_var) in env_settings.items():
|
|
121
140
|
if setting in self._settings:
|
|
122
141
|
setting_def = self._settings[setting]
|
|
123
142
|
try:
|
|
@@ -125,10 +144,11 @@ class Settings:
|
|
|
125
144
|
value, setting_def.annotation, setting
|
|
126
145
|
)
|
|
127
146
|
setting_def.set_value(parsed_value, "env")
|
|
147
|
+
setting_def.env_var_name = env_var
|
|
128
148
|
except ImproperlyConfigured as e:
|
|
129
149
|
self._errors.append(str(e))
|
|
130
150
|
|
|
131
|
-
def _load_explicit_settings(self, settings_module):
|
|
151
|
+
def _load_explicit_settings(self, settings_module: types.ModuleType) -> None:
|
|
132
152
|
for setting in dir(settings_module):
|
|
133
153
|
if setting.isupper():
|
|
134
154
|
setting_value = getattr(settings_module, setting)
|
|
@@ -141,8 +161,8 @@ class Settings:
|
|
|
141
161
|
self._errors.append(str(e))
|
|
142
162
|
continue
|
|
143
163
|
|
|
144
|
-
elif setting.startswith(
|
|
145
|
-
# Accept custom settings prefixed with '{
|
|
164
|
+
elif setting.startswith(_CUSTOM_SETTINGS_PREFIX):
|
|
165
|
+
# Accept custom settings prefixed with '{_CUSTOM_SETTINGS_PREFIX}'
|
|
146
166
|
setting_def = SettingDefinition(
|
|
147
167
|
name=setting,
|
|
148
168
|
default_value=None,
|
|
@@ -158,7 +178,7 @@ class Settings:
|
|
|
158
178
|
else:
|
|
159
179
|
# Collect unrecognized settings individually
|
|
160
180
|
self._errors.append(
|
|
161
|
-
f"Unknown setting '{setting}'. Custom settings must start with '{
|
|
181
|
+
f"Unknown setting '{setting}'. Custom settings must start with '{_CUSTOM_SETTINGS_PREFIX}'."
|
|
162
182
|
)
|
|
163
183
|
|
|
164
184
|
if hasattr(time, "tzset") and self.TIME_ZONE:
|
|
@@ -172,19 +192,19 @@ class Settings:
|
|
|
172
192
|
os.environ["TZ"] = self.TIME_ZONE
|
|
173
193
|
time.tzset()
|
|
174
194
|
|
|
175
|
-
def _check_required_settings(self):
|
|
195
|
+
def _check_required_settings(self) -> None:
|
|
176
196
|
missing = [k for k, v in self._settings.items() if v.required and not v.is_set]
|
|
177
197
|
if missing:
|
|
178
198
|
self._errors.append(f"Missing required setting(s): {', '.join(missing)}.")
|
|
179
199
|
|
|
180
|
-
def _raise_errors_if_any(self):
|
|
200
|
+
def _raise_errors_if_any(self) -> None:
|
|
181
201
|
if self._errors:
|
|
182
202
|
errors = ["- " + e for e in self._errors]
|
|
183
203
|
raise ImproperlyConfigured(
|
|
184
204
|
"Settings configuration errors:\n" + "\n".join(errors)
|
|
185
205
|
)
|
|
186
206
|
|
|
187
|
-
def __getattr__(self, name):
|
|
207
|
+
def __getattr__(self, name: str) -> typing.Any:
|
|
188
208
|
# Avoid recursion by directly returning internal attributes
|
|
189
209
|
if not name.isupper():
|
|
190
210
|
return object.__getattribute__(self, name)
|
|
@@ -196,7 +216,7 @@ class Settings:
|
|
|
196
216
|
else:
|
|
197
217
|
raise AttributeError(f"'Settings' object has no attribute '{name}'")
|
|
198
218
|
|
|
199
|
-
def __setattr__(self, name, value):
|
|
219
|
+
def __setattr__(self, name: str, value: typing.Any) -> None:
|
|
200
220
|
# Handle internal attributes without recursion
|
|
201
221
|
if not name.isupper():
|
|
202
222
|
object.__setattr__(self, name, value)
|
|
@@ -207,18 +227,46 @@ class Settings:
|
|
|
207
227
|
else:
|
|
208
228
|
object.__setattr__(self, name, value)
|
|
209
229
|
|
|
210
|
-
def __repr__(self):
|
|
230
|
+
def __repr__(self) -> str:
|
|
211
231
|
if not self.configured:
|
|
212
232
|
return "<Settings [Unevaluated]>"
|
|
213
233
|
return f'<Settings "{self._settings_module}">'
|
|
214
234
|
|
|
235
|
+
def get_settings(
|
|
236
|
+
self, *, source: str | None = None
|
|
237
|
+
) -> list[tuple[str, SettingDefinition]]:
|
|
238
|
+
"""
|
|
239
|
+
Get settings as a sorted list of (name, definition) tuples.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
source: Filter to settings from a specific source ('default', 'env', 'explicit', 'runtime')
|
|
243
|
+
"""
|
|
244
|
+
self._setup()
|
|
245
|
+
result = []
|
|
246
|
+
for name, defn in sorted(self._settings.items()):
|
|
247
|
+
if source is not None and defn.source != source:
|
|
248
|
+
continue
|
|
249
|
+
result.append((name, defn))
|
|
250
|
+
return result
|
|
251
|
+
|
|
252
|
+
def get_env_settings(self) -> list[tuple[str, SettingDefinition]]:
|
|
253
|
+
"""Get settings that were loaded from environment variables."""
|
|
254
|
+
return self.get_settings(source="env")
|
|
215
255
|
|
|
216
|
-
|
|
256
|
+
|
|
257
|
+
def _parse_env_value(
|
|
258
|
+
value: str, annotation: type | None, setting_name: str
|
|
259
|
+
) -> typing.Any:
|
|
217
260
|
if not annotation:
|
|
218
261
|
raise ImproperlyConfigured(
|
|
219
262
|
f"{setting_name}: Type hint required to set from environment."
|
|
220
263
|
)
|
|
221
264
|
|
|
265
|
+
# Unwrap Secret[T] to get the inner type
|
|
266
|
+
if typing.get_origin(annotation) is Secret:
|
|
267
|
+
if args := typing.get_args(annotation):
|
|
268
|
+
annotation = args[0]
|
|
269
|
+
|
|
222
270
|
if annotation is bool:
|
|
223
271
|
# Special case for bools
|
|
224
272
|
return value.lower() in ("true", "1", "yes")
|
|
@@ -238,7 +286,12 @@ class SettingDefinition:
|
|
|
238
286
|
"""Store detailed information about settings."""
|
|
239
287
|
|
|
240
288
|
def __init__(
|
|
241
|
-
self,
|
|
289
|
+
self,
|
|
290
|
+
name: str,
|
|
291
|
+
default_value: typing.Any = None,
|
|
292
|
+
annotation: type | None = None,
|
|
293
|
+
module: types.ModuleType | None = None,
|
|
294
|
+
required: bool = False,
|
|
242
295
|
):
|
|
243
296
|
self.name = name
|
|
244
297
|
self.default_value = default_value
|
|
@@ -248,14 +301,27 @@ class SettingDefinition:
|
|
|
248
301
|
self.value = default_value
|
|
249
302
|
self.source = "default" # 'default', 'env', 'explicit', or 'runtime'
|
|
250
303
|
self.is_set = False # Indicates if the value was set explicitly
|
|
304
|
+
self.env_var_name: str | None = None # Env var name if loaded from env
|
|
305
|
+
self.is_secret = self._check_if_secret(annotation)
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _check_if_secret(annotation: type | None) -> bool:
|
|
309
|
+
"""Check if annotation is Secret[T]."""
|
|
310
|
+
return annotation is not None and typing.get_origin(annotation) is Secret
|
|
251
311
|
|
|
252
|
-
def
|
|
312
|
+
def display_value(self) -> str:
|
|
313
|
+
"""Return value for display, masked if secret."""
|
|
314
|
+
if self.is_secret:
|
|
315
|
+
return "********"
|
|
316
|
+
return repr(self.value)
|
|
317
|
+
|
|
318
|
+
def set_value(self, value: typing.Any, source: str) -> None:
|
|
253
319
|
self.check_type(value)
|
|
254
320
|
self.value = value
|
|
255
321
|
self.source = source
|
|
256
322
|
self.is_set = True
|
|
257
323
|
|
|
258
|
-
def check_type(self, obj):
|
|
324
|
+
def check_type(self, obj: typing.Any) -> None:
|
|
259
325
|
if not self.annotation:
|
|
260
326
|
return
|
|
261
327
|
|
|
@@ -265,23 +331,29 @@ class SettingDefinition:
|
|
|
265
331
|
)
|
|
266
332
|
|
|
267
333
|
@staticmethod
|
|
268
|
-
def _is_instance_of_type(value, type_hint) -> bool:
|
|
334
|
+
def _is_instance_of_type(value: typing.Any, type_hint: typing.Any) -> bool:
|
|
269
335
|
# Simple types
|
|
270
336
|
if isinstance(type_hint, type):
|
|
271
337
|
return isinstance(value, type_hint)
|
|
272
338
|
|
|
339
|
+
origin = typing.get_origin(type_hint)
|
|
340
|
+
|
|
341
|
+
# Secret[T] - check the inner type (Secret is just a marker)
|
|
342
|
+
if origin is Secret:
|
|
343
|
+
args = typing.get_args(type_hint)
|
|
344
|
+
if args:
|
|
345
|
+
return SettingDefinition._is_instance_of_type(value, args[0])
|
|
346
|
+
return True
|
|
347
|
+
|
|
273
348
|
# Union types
|
|
274
|
-
if
|
|
275
|
-
typing.get_origin(type_hint) is typing.Union
|
|
276
|
-
or typing.get_origin(type_hint) is types.UnionType
|
|
277
|
-
):
|
|
349
|
+
if origin is typing.Union or origin is types.UnionType:
|
|
278
350
|
return any(
|
|
279
351
|
SettingDefinition._is_instance_of_type(value, arg)
|
|
280
352
|
for arg in typing.get_args(type_hint)
|
|
281
353
|
)
|
|
282
354
|
|
|
283
355
|
# List types
|
|
284
|
-
if
|
|
356
|
+
if origin is list:
|
|
285
357
|
return isinstance(value, list) and all(
|
|
286
358
|
SettingDefinition._is_instance_of_type(
|
|
287
359
|
item, typing.get_args(type_hint)[0]
|
|
@@ -290,7 +362,7 @@ class SettingDefinition:
|
|
|
290
362
|
)
|
|
291
363
|
|
|
292
364
|
# Tuple types
|
|
293
|
-
if
|
|
365
|
+
if origin is tuple:
|
|
294
366
|
return isinstance(value, tuple) and all(
|
|
295
367
|
SettingDefinition._is_instance_of_type(
|
|
296
368
|
item, typing.get_args(type_hint)[i]
|
|
@@ -300,5 +372,5 @@ class SettingDefinition:
|
|
|
300
372
|
|
|
301
373
|
raise ValueError(f"Unsupported type hint: {type_hint}")
|
|
302
374
|
|
|
303
|
-
def __str__(self):
|
|
375
|
+
def __str__(self) -> str:
|
|
304
376
|
return f"SettingDefinition(name={self.name}, value={self.value}, source={self.source})"
|
plain/runtime/utils.py
CHANGED
plain/server/LICENSE
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Plain HTTP Server - License and Attribution
|
|
2
|
+
============================================
|
|
3
|
+
|
|
4
|
+
This module is based on gunicorn (https://gunicorn.org), integrated from
|
|
5
|
+
commit 1dc4ce9d59c3458305d701c4c6d63aa6b1d1b309 (gunicorn 23.0.0, October 2024).
|
|
6
|
+
|
|
7
|
+
The gunicorn code has been integrated into Plain and modified for Plain's
|
|
8
|
+
specific use case. All files should be considered modified from the original.
|
|
9
|
+
|
|
10
|
+
Original repository: https://github.com/benoitc/gunicorn
|
|
11
|
+
|
|
12
|
+
--------------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
MIT License
|
|
15
|
+
|
|
16
|
+
Copyright (c) 2009-2024 Benoît Chesneau <benoitc@gunicorn.org>
|
|
17
|
+
Copyright (c) 2009-2015 Paul J. Davis <paul.joseph.davis@gmail.com>
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
in the Software without restriction, including without limitation the rights
|
|
22
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|