simple-module-settings 0.0.31__tar.gz → 0.0.32__tar.gz
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.
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/PKG-INFO +4 -4
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/pyproject.toml +4 -4
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/_module_settings.py +26 -3
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/module_api.py +10 -2
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/views.py +23 -20
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/ModulesEdit.tsx +3 -1
- simple_module_settings-0.0.32/tests/test_module_settings_render.py +90 -0
- simple_module_settings-0.0.32/tests/test_module_settings_serialize.py +92 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/.gitignore +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/LICENSE +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/README.md +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/package.json +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/__init__.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/cli.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/constants.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/__init__.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/accessor.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/events.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/registry.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/schemas.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/deps.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/__init__.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/api.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/env_vars.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/hydrate.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/locales/en.json +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/models.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/module.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/module_registry.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/Browse.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/Create.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/Edit.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/FieldInput.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/FieldSource.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/KeyField.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/ModuleForm.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/TestConnectionButton.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/components/ValueInput.tsx +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/routes.ts +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/py.typed +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/registration.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/reload.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/service.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/services.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/settings.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/store.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_cli_import.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_concurrent_overrides.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_hydrate.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_module_api.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_module_registry.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_module_settings.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_registration.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_reload.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_accessor.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_api.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_events.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_field_sources.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_module.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_schemas.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_service.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_view_authz.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_store.py +0 -0
- {simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_settings
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.32
|
|
4
4
|
Summary: Runtime settings UI — modules plug their own settings panels into a shared admin view
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -21,9 +21,9 @@ Classifier: Topic :: Internet :: WWW/HTTP
|
|
|
21
21
|
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
|
-
Requires-Dist: simple-module-core==0.0.
|
|
25
|
-
Requires-Dist: simple-module-db==0.0.
|
|
26
|
-
Requires-Dist: simple-module-hosting==0.0.
|
|
24
|
+
Requires-Dist: simple-module-core==0.0.32
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.32
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.32
|
|
27
27
|
Requires-Dist: typer>=0.12
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_settings"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.32"
|
|
4
4
|
description = "Runtime settings UI — modules plug their own settings panels into a shared admin view"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -21,9 +21,9 @@ classifiers = [
|
|
|
21
21
|
"Typing :: Typed",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"simple_module_core==0.0.
|
|
25
|
-
"simple_module_db==0.0.
|
|
26
|
-
"simple_module_hosting==0.0.
|
|
24
|
+
"simple_module_core==0.0.32",
|
|
25
|
+
"simple_module_db==0.0.32",
|
|
26
|
+
"simple_module_hosting==0.0.32",
|
|
27
27
|
"typer>=0.12",
|
|
28
28
|
]
|
|
29
29
|
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/_module_settings.py
RENAMED
|
@@ -12,10 +12,12 @@ from dataclasses import dataclass
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
14
|
from fastapi import FastAPI
|
|
15
|
+
from fastapi.encoders import jsonable_encoder
|
|
15
16
|
from pydantic_settings import BaseSettings
|
|
16
17
|
|
|
17
18
|
from settings.env_vars import env_prefix_for
|
|
18
19
|
from settings.hydrate import value_type_for_field
|
|
20
|
+
from settings.service import SettingService
|
|
19
21
|
|
|
20
22
|
# We intentionally DON'T match the bare word "token" (would mask
|
|
21
23
|
# `verification_token_lifetime_seconds` — just an int) or "key" alone (would
|
|
@@ -236,8 +238,29 @@ def _build_view(
|
|
|
236
238
|
)
|
|
237
239
|
|
|
238
240
|
|
|
241
|
+
async def overrides_by_package(service: SettingService) -> dict[str, frozenset[str]]:
|
|
242
|
+
"""Map package -> field names carrying a stored override.
|
|
243
|
+
|
|
244
|
+
Reads the SYSTEM scope once and buckets by key prefix. Packages with no
|
|
245
|
+
overrides are simply absent, which :func:`collect_module_settings` already
|
|
246
|
+
treats as "nothing overridden". Shared by both the Inertia view and the
|
|
247
|
+
JSON API so the two never drift on what "overridden" means.
|
|
248
|
+
"""
|
|
249
|
+
from settings.store import SettingsStore
|
|
250
|
+
|
|
251
|
+
return await SettingsStore(service).all_override_fields()
|
|
252
|
+
|
|
253
|
+
|
|
239
254
|
def serialize(views: list[ModuleSettingsView]) -> list[dict[str, Any]]:
|
|
240
|
-
"""Convert dataclass views to plain dicts for Inertia props.
|
|
255
|
+
"""Convert dataclass views to plain dicts for Inertia props.
|
|
256
|
+
|
|
257
|
+
Field values arrive as whatever type the module declared — pydantic has
|
|
258
|
+
already coerced ``media_root: Path`` to a ``PosixPath``, ``timeout:
|
|
259
|
+
timedelta`` to a ``timedelta`` — and this screen reflects every installed
|
|
260
|
+
module's settings, so the set of types is open-ended by design. They are
|
|
261
|
+
encoded here rather than handed on as-is: this is the boundary where a
|
|
262
|
+
settings object stops being Python and becomes a prop.
|
|
263
|
+
"""
|
|
241
264
|
return [
|
|
242
265
|
{
|
|
243
266
|
"module_name": v.module_name,
|
|
@@ -248,8 +271,8 @@ def serialize(views: list[ModuleSettingsView]) -> list[dict[str, Any]]:
|
|
|
248
271
|
{
|
|
249
272
|
"name": f.name,
|
|
250
273
|
"env_var": f.env_var,
|
|
251
|
-
"value": f.value,
|
|
252
|
-
"default": f.default,
|
|
274
|
+
"value": jsonable_encoder(f.value),
|
|
275
|
+
"default": jsonable_encoder(f.default),
|
|
253
276
|
"description": f.description,
|
|
254
277
|
"is_secret": f.is_secret,
|
|
255
278
|
"type": f.type,
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/module_api.py
RENAMED
|
@@ -17,6 +17,7 @@ from settings._module_settings import (
|
|
|
17
17
|
SECRET_MASK,
|
|
18
18
|
collect_module_settings,
|
|
19
19
|
is_secret_field,
|
|
20
|
+
overrides_by_package,
|
|
20
21
|
serialize,
|
|
21
22
|
)
|
|
22
23
|
from settings.constants import MODULE_PACKAGE, PERM_DELETE, PERM_EDIT, PERM_VIEW
|
|
@@ -47,8 +48,15 @@ def _strip_mask_sentinels(changes: dict[str, Any]) -> dict[str, Any]:
|
|
|
47
48
|
|
|
48
49
|
|
|
49
50
|
@router.get("", dependencies=_VIEW)
|
|
50
|
-
async def list_modules(
|
|
51
|
-
|
|
51
|
+
async def list_modules(
|
|
52
|
+
request: Request,
|
|
53
|
+
service: SettingService = Depends(get_setting_service),
|
|
54
|
+
) -> dict[str, Any]:
|
|
55
|
+
# Match the Inertia ModulesEdit view: without the overrides map, every
|
|
56
|
+
# field's `source`/`db_override` would report env/default even when a
|
|
57
|
+
# stored override is actually in force.
|
|
58
|
+
overrides = await overrides_by_package(service)
|
|
59
|
+
views = collect_module_settings(request.app, overrides)
|
|
52
60
|
return {"modules": serialize(views)}
|
|
53
61
|
|
|
54
62
|
|
|
@@ -9,6 +9,8 @@ strings below match what ``pages/*.tsx`` declare.
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
+
import asyncio
|
|
13
|
+
|
|
12
14
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
13
15
|
from inertia import InertiaResponse
|
|
14
16
|
from pydantic import ValidationError
|
|
@@ -17,7 +19,12 @@ from simple_module_hosting.inertia_utils import redirect_back_with_errors, valid
|
|
|
17
19
|
from simple_module_hosting.permissions import RequiresPermission
|
|
18
20
|
from starlette.responses import RedirectResponse
|
|
19
21
|
|
|
20
|
-
from settings._module_settings import
|
|
22
|
+
from settings._module_settings import (
|
|
23
|
+
_package_of,
|
|
24
|
+
collect_module_settings,
|
|
25
|
+
overrides_by_package,
|
|
26
|
+
serialize,
|
|
27
|
+
)
|
|
21
28
|
from settings.constants import (
|
|
22
29
|
ERR_SETTING_NOT_FOUND,
|
|
23
30
|
PERM_CREATE,
|
|
@@ -187,7 +194,7 @@ async def modules_view(
|
|
|
187
194
|
an ``SM_*`` env var, or the field default — so a setting that "isn't taking
|
|
188
195
|
effect" explains itself.
|
|
189
196
|
"""
|
|
190
|
-
overrides = await
|
|
197
|
+
overrides = await overrides_by_package(service)
|
|
191
198
|
views = collect_module_settings(request.app, overrides)
|
|
192
199
|
return await inertia.render(
|
|
193
200
|
_PAGE_MODULES_EDIT,
|
|
@@ -200,18 +207,6 @@ async def modules_view(
|
|
|
200
207
|
)
|
|
201
208
|
|
|
202
209
|
|
|
203
|
-
async def _overrides_by_package(service: SettingService) -> dict[str, frozenset[str]]:
|
|
204
|
-
"""Map package -> field names carrying a stored override.
|
|
205
|
-
|
|
206
|
-
Reads the SYSTEM scope once and buckets by key prefix. Packages with no
|
|
207
|
-
overrides are simply absent, which ``collect_module_settings`` already
|
|
208
|
-
treats as "nothing overridden".
|
|
209
|
-
"""
|
|
210
|
-
from settings.store import SettingsStore
|
|
211
|
-
|
|
212
|
-
return await SettingsStore(service).all_override_fields()
|
|
213
|
-
|
|
214
|
-
|
|
215
210
|
def _testable_packages(request: Request) -> list[str]:
|
|
216
211
|
"""Packages whose module registered at least one health check.
|
|
217
212
|
|
|
@@ -257,13 +252,21 @@ async def test_connection(package: str, request: Request) -> dict:
|
|
|
257
252
|
if not checks:
|
|
258
253
|
raise HTTPException(status_code=404, detail=f"{owner.meta.name} has no connection to test")
|
|
259
254
|
|
|
260
|
-
|
|
261
|
-
|
|
255
|
+
# Run independently of one another — a module can register several checks
|
|
256
|
+
# (e.g. SMTP + primary storage + backup storage), and sequentially awaiting
|
|
257
|
+
# each one would block the request for the sum of their latencies instead
|
|
258
|
+
# of the slowest one, the same reasoning dashboard.stats._run_health_checks
|
|
259
|
+
# already applies to probe checks.
|
|
260
|
+
async def _run_one(check) -> dict:
|
|
262
261
|
try:
|
|
263
262
|
outcome = await check.check()
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
263
|
+
return {
|
|
264
|
+
"name": check.name,
|
|
265
|
+
"status": outcome.status.value,
|
|
266
|
+
"detail": outcome.detail or "",
|
|
267
|
+
}
|
|
267
268
|
except Exception as exc:
|
|
268
|
-
|
|
269
|
+
return {"name": check.name, "status": "unhealthy", "detail": str(exc)}
|
|
270
|
+
|
|
271
|
+
results = await asyncio.gather(*[_run_one(c) for c in checks])
|
|
269
272
|
return {"module": owner.meta.name, "checks": results}
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/pages/ModulesEdit.tsx
RENAMED
|
@@ -36,7 +36,9 @@ function ModulesEdit({ modules, testable = [] }: Props) {
|
|
|
36
36
|
return (
|
|
37
37
|
<>
|
|
38
38
|
<Head title="Modules" />
|
|
39
|
-
|
|
39
|
+
{/* Master/detail pane fills the viewport minus the chrome above it,
|
|
40
|
+
whose height the layout publishes as --app-chrome-h. */}
|
|
41
|
+
<div className="flex h-[calc(100vh-var(--app-chrome-h))] bg-background">
|
|
40
42
|
<aside className="w-72 shrink-0 border-r border-border bg-secondary/30 overflow-y-auto">
|
|
41
43
|
<div className="p-3">
|
|
42
44
|
<div className="relative">
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Settings → Modules must render for a module with a rich settings type.
|
|
2
|
+
|
|
3
|
+
This screen reflects *every* installed module's pydantic settings, so the value
|
|
4
|
+
types are open-ended — but none of the modules in this repo declare anything
|
|
5
|
+
the stdlib JSON encoder can't handle, which is why the failure only ever showed
|
|
6
|
+
up downstream. A wheel-installed module with ``media_root: Path`` was enough to
|
|
7
|
+
500 the page, and only on a client-side visit: the HTML render path has its own
|
|
8
|
+
encoder, so reloading the same URL worked and the report read as "sometimes".
|
|
9
|
+
|
|
10
|
+
The demo module below exists to keep a non-JSON-native settings type in the
|
|
11
|
+
suite permanently.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import httpx
|
|
20
|
+
import pytest
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
from pydantic_settings import BaseSettings
|
|
23
|
+
from simple_module_core.module import ModuleMeta
|
|
24
|
+
|
|
25
|
+
_OK = 200
|
|
26
|
+
_SERVER_ERROR = 500
|
|
27
|
+
_INERTIA = {"X-Inertia": "true", "X-Inertia-Version": "1.0"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class _PathCfg(BaseSettings):
|
|
31
|
+
"""Mirrors the shape that broke it: a Path with a relative default."""
|
|
32
|
+
|
|
33
|
+
media_root: Path = Path("var/demo/media")
|
|
34
|
+
workers: int = 2
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class _PathModule:
|
|
38
|
+
meta = ModuleMeta(name="PathDemo")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_PathModule.__module__ = "pathdemo"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class _PathServices:
|
|
46
|
+
settings: _PathCfg
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.fixture
|
|
50
|
+
def app_with_path_setting(app: FastAPI) -> FastAPI:
|
|
51
|
+
"""Register the demo module against the live app the client talks to."""
|
|
52
|
+
app.state.settings.module_registry.register("pathdemo", _PathCfg)
|
|
53
|
+
app.state.pathdemo = _PathServices(settings=_PathCfg())
|
|
54
|
+
return app
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestModulesScreenRenders:
|
|
58
|
+
async def test_client_side_visit_does_not_500(
|
|
59
|
+
self,
|
|
60
|
+
app_with_path_setting: FastAPI,
|
|
61
|
+
authenticated_client: httpx.AsyncClient,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""The reported bug: reaching the page by clicking the sidebar link."""
|
|
64
|
+
resp = await authenticated_client.get("/settings/", headers=_INERTIA)
|
|
65
|
+
|
|
66
|
+
assert resp.status_code != _SERVER_ERROR
|
|
67
|
+
assert resp.status_code == _OK
|
|
68
|
+
|
|
69
|
+
async def test_the_path_survives_as_a_string(
|
|
70
|
+
self,
|
|
71
|
+
app_with_path_setting: FastAPI,
|
|
72
|
+
authenticated_client: httpx.AsyncClient,
|
|
73
|
+
) -> None:
|
|
74
|
+
resp = await authenticated_client.get("/settings/", headers=_INERTIA)
|
|
75
|
+
modules = resp.json()["props"]["modules"]
|
|
76
|
+
|
|
77
|
+
demo = next(m for m in modules if m["package"] == "pathdemo")
|
|
78
|
+
media_root = next(f for f in demo["fields"] if f["name"] == "media_root")
|
|
79
|
+
assert media_root["value"] == "var/demo/media"
|
|
80
|
+
|
|
81
|
+
async def test_full_page_load_still_works(
|
|
82
|
+
self,
|
|
83
|
+
app_with_path_setting: FastAPI,
|
|
84
|
+
authenticated_client: httpx.AsyncClient,
|
|
85
|
+
) -> None:
|
|
86
|
+
"""The path that always worked must keep working."""
|
|
87
|
+
resp = await authenticated_client.get("/settings/")
|
|
88
|
+
|
|
89
|
+
assert resp.status_code == _OK
|
|
90
|
+
assert resp.headers["content-type"].startswith("text/html")
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Serialized module settings must be props, not live Python objects.
|
|
2
|
+
|
|
3
|
+
Settings → Modules reflects every installed module's pydantic settings, so the
|
|
4
|
+
value types are whatever those modules declared — pydantic has already turned
|
|
5
|
+
``media_root: Path`` into a ``PosixPath`` before this screen sees it. Passing
|
|
6
|
+
one on untouched puts a value in the Inertia payload that only the HTML render
|
|
7
|
+
path can encode, so the page renders on a reload and 500s when an admin clicks
|
|
8
|
+
"Settings" in the sidebar.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from datetime import date
|
|
15
|
+
from decimal import Decimal
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from settings._module_settings import (
|
|
21
|
+
ModuleSettingField,
|
|
22
|
+
ModuleSettingsView,
|
|
23
|
+
serialize,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _view_with(value: Any, default: Any = "") -> ModuleSettingsView:
|
|
28
|
+
return ModuleSettingsView(
|
|
29
|
+
module_name="Demo",
|
|
30
|
+
package="demo",
|
|
31
|
+
env_prefix="SM_DEMO_",
|
|
32
|
+
class_name="DemoCfg",
|
|
33
|
+
fields=[
|
|
34
|
+
ModuleSettingField(
|
|
35
|
+
name="media_root",
|
|
36
|
+
env_var="SM_DEMO_MEDIA_ROOT",
|
|
37
|
+
value=value,
|
|
38
|
+
default=default,
|
|
39
|
+
description="",
|
|
40
|
+
is_secret=False,
|
|
41
|
+
type="Path",
|
|
42
|
+
requires_restart=False,
|
|
43
|
+
group=None,
|
|
44
|
+
)
|
|
45
|
+
],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _only_field(views: list[dict]) -> dict:
|
|
50
|
+
return views[0]["fields"][0]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestSerializeProducesJsonSafeProps:
|
|
54
|
+
def test_a_path_value_becomes_a_string(self) -> None:
|
|
55
|
+
result = serialize([_view_with(Path("var/pagebuilder/media"))])
|
|
56
|
+
|
|
57
|
+
assert _only_field(result)["value"] == "var/pagebuilder/media"
|
|
58
|
+
|
|
59
|
+
def test_a_path_default_becomes_a_string(self) -> None:
|
|
60
|
+
"""Defaults reach the payload too — the screen shows both."""
|
|
61
|
+
result = serialize([_view_with("", default=Path("var/media"))])
|
|
62
|
+
|
|
63
|
+
assert _only_field(result)["default"] == "var/media"
|
|
64
|
+
|
|
65
|
+
@pytest.mark.parametrize(
|
|
66
|
+
"value",
|
|
67
|
+
[Path("var/media"), Decimal("1.5"), date(2026, 8, 21)],
|
|
68
|
+
ids=["path", "decimal", "date"],
|
|
69
|
+
)
|
|
70
|
+
def test_the_payload_survives_a_plain_json_dump(self, value: Any) -> None:
|
|
71
|
+
"""The check that matters: Starlette's JSONResponse has no encoder.
|
|
72
|
+
|
|
73
|
+
Asserting on ``json.dumps`` rather than on each converted type keeps
|
|
74
|
+
this honest for value types no one has thought of yet.
|
|
75
|
+
"""
|
|
76
|
+
result = serialize([_view_with(value)])
|
|
77
|
+
|
|
78
|
+
assert json.dumps(result)
|
|
79
|
+
|
|
80
|
+
def test_ordinary_values_are_unchanged(self) -> None:
|
|
81
|
+
result = serialize([_view_with(8000, default=25)])
|
|
82
|
+
field = _only_field(result)
|
|
83
|
+
|
|
84
|
+
assert field["value"] == 8000
|
|
85
|
+
assert field["default"] == 25
|
|
86
|
+
|
|
87
|
+
def test_the_rest_of_the_view_is_intact(self) -> None:
|
|
88
|
+
result = serialize([_view_with(Path("x"))])
|
|
89
|
+
|
|
90
|
+
assert result[0]["package"] == "demo"
|
|
91
|
+
assert _only_field(result)["env_var"] == "SM_DEMO_MEDIA_ROOT"
|
|
92
|
+
assert _only_field(result)["source"] == "default"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/__init__.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/accessor.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/events.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/registry.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/contracts/schemas.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/settings/endpoints/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_concurrent_overrides.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_module_registry.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_module_settings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_accessor.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_events.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_field_sources.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_module.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_schemas.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_service.py
RENAMED
|
File without changes
|
{simple_module_settings-0.0.31 → simple_module_settings-0.0.32}/tests/test_settings_view_authz.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|