simple-module-settings 0.0.36__tar.gz → 0.0.38__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.36 → simple_module_settings-0.0.38}/.gitignore +2 -2
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/PKG-INFO +6 -6
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/README.md +2 -2
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/package.json +1 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/pyproject.toml +4 -4
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_announce.py +2 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_field_meta.py +6 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_listing.py +3 -8
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_module_settings.py +53 -24
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_module_settings_props.py +2 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_row_masking.py +5 -4
- simple_module_settings-0.0.38/settings/_secrets.py +17 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_unique_write.py +1 -2
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/cli.py +4 -4
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/constants.py +25 -33
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/accessor.py +2 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/registry.py +3 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/schemas.py +5 -7
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/api.py +1 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/module_api.py +22 -7
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/views.py +2 -2
- simple_module_settings-0.0.38/settings/hydrate.py +36 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/known_keys.py +1 -1
- simple_module_settings-0.0.38/settings/locales/de.json +152 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/locales/en.json +12 -3
- simple_module_settings-0.0.38/settings/models.py +12 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/module.py +15 -12
- simple_module_settings-0.0.38/settings/module_registry.py +17 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/Browse.tsx +0 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/ModulesEdit.tsx +0 -1
- simple_module_settings-0.0.38/settings/pages/api.generated.ts +1043 -0
- simple_module_settings-0.0.38/settings/pages/api.ts +9 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/FieldInput.tsx +42 -11
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/FieldSource.tsx +5 -5
- simple_module_settings-0.0.38/settings/pages/components/JsonInput.tsx +77 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/ModuleFieldRow.tsx +18 -2
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/ModuleForm.tsx +41 -14
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/TestConnectionButton.tsx +10 -9
- simple_module_settings-0.0.38/settings/pages/components/validation-errors.test.ts +32 -0
- simple_module_settings-0.0.38/settings/pages/components/validation-errors.ts +45 -0
- simple_module_settings-0.0.38/settings/registration.py +17 -0
- simple_module_settings-0.0.38/settings/reload.py +158 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/service.py +4 -8
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/services.py +6 -4
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/store.py +13 -41
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/tenant_view.py +5 -2
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_clear_route_per_scope.py +1 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_cli_import.py +9 -8
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_duplicate_and_race.py +1 -1
- simple_module_settings-0.0.38/tests/test_env_pinned.py +121 -0
- simple_module_settings-0.0.38/tests/test_hydrate.py +230 -0
- simple_module_settings-0.0.38/tests/test_hydrate_related_fields.py +53 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_known_keys_meta.py +12 -12
- simple_module_settings-0.0.38/tests/test_lenient_related_fields_validators.py +110 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_module_api.py +5 -2
- simple_module_settings-0.0.38/tests/test_module_api_json_fields.py +129 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_module_registry.py +1 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_module_settings.py +14 -14
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_registration.py +2 -3
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_reload.py +1 -1
- simple_module_settings-0.0.38/tests/test_settings_default_clears_override.py +80 -0
- simple_module_settings-0.0.38/tests/test_settings_env_unprefixed.py +49 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_field_sources.py +4 -4
- simple_module_settings-0.0.38/tests/test_settings_framework_reexports.py +48 -0
- simple_module_settings-0.0.38/tests/test_settings_group_metadata.py +30 -0
- simple_module_settings-0.0.38/tests/test_settings_host_restart_flags.py +22 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_schemas.py +1 -1
- simple_module_settings-0.0.38/tests/test_settings_secret_json_roundtrip.py +63 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_secret_masking.py +2 -1
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_secret_urls.py +4 -4
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_service.py +1 -1
- simple_module_settings-0.0.38/tests/test_settings_typed_secret_masking.py +97 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_store_filters.py +16 -8
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_store_secret_masking.py +3 -3
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_testable_checks.py +5 -5
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_upload_key_delete.py +1 -1
- simple_module_settings-0.0.38/tests-js/JsonFields.test.tsx +127 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests-js/ModuleForm.test.tsx +64 -10
- simple_module_settings-0.0.38/tests-js/RestartHint.test.tsx +57 -0
- simple_module_settings-0.0.38/tests-js/settings_TestConnectionButton.test.tsx +47 -0
- simple_module_settings-0.0.36/settings/_secrets.py +0 -97
- simple_module_settings-0.0.36/settings/hydrate.py +0 -82
- simple_module_settings-0.0.36/settings/models.py +0 -50
- simple_module_settings-0.0.36/settings/module_registry.py +0 -50
- simple_module_settings-0.0.36/settings/registration.py +0 -40
- simple_module_settings-0.0.36/settings/reload.py +0 -63
- simple_module_settings-0.0.36/tests/test_hydrate.py +0 -91
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/LICENSE +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/__init__.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_managed_keys.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/browse_query.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/__init__.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/events.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/invalidation.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/deps.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/__init__.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/_create_form.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/endpoints/tenant_api.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/env_vars.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/errors.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/Create.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/Edit.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/KeyField.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/ResolvedValue.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/ScopeTabs.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/StoreCards.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/StoreTable.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/ValueInput.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/components/last-test.ts +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/routes.ts +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/pages/types.ts +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/py.typed +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/settings.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/tenant_scope.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_concurrent_overrides.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_module_settings_render.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_module_settings_serialize.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_accessor.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_api.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_events.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_module.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_settings_view_authz.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_store.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests/test_tenant_settings.py +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests-js/ModulesEdit.test.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/tests-js/ScopeTabs.test.tsx +0 -0
- {simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/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.38
|
|
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.38
|
|
25
|
+
Requires-Dist: simple-module-db==0.0.38
|
|
26
|
+
Requires-Dist: simple-module-hosting==0.0.38
|
|
27
27
|
Requires-Dist: typer>=0.12
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -55,7 +55,7 @@ class OrdersModule(ModuleBase):
|
|
|
55
55
|
meta = ModuleMeta(name="orders")
|
|
56
56
|
|
|
57
57
|
def register_settings(self, app: FastAPI) -> None:
|
|
58
|
-
from
|
|
58
|
+
from simple_module_core.module_settings import register_module_settings
|
|
59
59
|
from orders.settings import OrdersSettings
|
|
60
60
|
from orders.services import OrdersServices
|
|
61
61
|
|
|
@@ -64,7 +64,7 @@ class OrdersModule(ModuleBase):
|
|
|
64
64
|
)
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
That surfaces the module's settings fields under `/settings
|
|
67
|
+
That surfaces the module's settings fields under `/admin/settings/`, hydrated from the DB store at startup. The settings class is normally a `simple_module_core.settings_base.DbBackedSettings` (DB is the only source). Registration and hydration live in the framework (#500); this module adds the admin editor, hot reload and the key/value API. `settings.registration`, `settings.module_registry` and `settings.hydrate` are deprecated re-exports.
|
|
68
68
|
|
|
69
69
|
## Depends on
|
|
70
70
|
|
|
@@ -26,7 +26,7 @@ class OrdersModule(ModuleBase):
|
|
|
26
26
|
meta = ModuleMeta(name="orders")
|
|
27
27
|
|
|
28
28
|
def register_settings(self, app: FastAPI) -> None:
|
|
29
|
-
from
|
|
29
|
+
from simple_module_core.module_settings import register_module_settings
|
|
30
30
|
from orders.settings import OrdersSettings
|
|
31
31
|
from orders.services import OrdersServices
|
|
32
32
|
|
|
@@ -35,7 +35,7 @@ class OrdersModule(ModuleBase):
|
|
|
35
35
|
)
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
That surfaces the module's settings fields under `/settings
|
|
38
|
+
That surfaces the module's settings fields under `/admin/settings/`, hydrated from the DB store at startup. The settings class is normally a `simple_module_core.settings_base.DbBackedSettings` (DB is the only source). Registration and hydration live in the framework (#500); this module adds the admin editor, hot reload and the key/value API. `settings.registration`, `settings.module_registry` and `settings.hydrate` are deprecated re-exports.
|
|
39
39
|
|
|
40
40
|
## Depends on
|
|
41
41
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_settings"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.38"
|
|
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.38",
|
|
25
|
+
"simple_module_db==0.0.38",
|
|
26
|
+
"simple_module_hosting==0.0.38",
|
|
27
27
|
"typer>=0.12",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -9,8 +9,9 @@ from __future__ import annotations
|
|
|
9
9
|
from typing import TYPE_CHECKING
|
|
10
10
|
|
|
11
11
|
from simple_module_db.callbacks import register_on_commit
|
|
12
|
+
from simple_module_db.stored_settings import SCOPE_SYSTEM, SCOPE_TENANT
|
|
12
13
|
|
|
13
|
-
from settings.constants import INVALIDATION_CHANNEL
|
|
14
|
+
from settings.constants import INVALIDATION_CHANNEL
|
|
14
15
|
from settings.contracts.invalidation import invalidation_key
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
@@ -8,7 +8,7 @@ this field?" questions live together and are testable without a FastAPI app.
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
10
|
import re
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any, Literal, get_args, get_origin
|
|
12
12
|
|
|
13
13
|
from pydantic_settings import BaseSettings
|
|
14
14
|
|
|
@@ -71,6 +71,11 @@ def choices_for(info) -> list[str] | None:
|
|
|
71
71
|
if isinstance(declared, (list, tuple)) and declared:
|
|
72
72
|
return [str(choice) for choice in declared]
|
|
73
73
|
|
|
74
|
+
if get_origin(info.annotation) is Literal:
|
|
75
|
+
options = get_args(info.annotation)
|
|
76
|
+
if options and all(isinstance(o, str) for o in options):
|
|
77
|
+
return list(options)
|
|
78
|
+
|
|
74
79
|
for constraint in info.metadata:
|
|
75
80
|
pattern = getattr(constraint, "pattern", None)
|
|
76
81
|
if not isinstance(pattern, str):
|
|
@@ -3,18 +3,13 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from simple_module_db import LIKE_ESCAPE_CHAR, like_contains_pattern
|
|
6
|
+
from simple_module_db.stored_settings import SYSTEM_SCOPE_ID, Setting
|
|
6
7
|
from sqlalchemy import func, select
|
|
7
8
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
8
9
|
|
|
9
10
|
from settings._row_masking import out
|
|
10
|
-
from settings.constants import
|
|
11
|
-
ALL_SCOPES,
|
|
12
|
-
DEFAULT_PER_PAGE,
|
|
13
|
-
SCOPE_ALL,
|
|
14
|
-
SYSTEM_SCOPE_ID,
|
|
15
|
-
)
|
|
11
|
+
from settings.constants import ALL_SCOPES, DEFAULT_PER_PAGE, SCOPE_ALL
|
|
16
12
|
from settings.contracts.schemas import SettingOut, SettingScope
|
|
17
|
-
from settings.models import Setting
|
|
18
13
|
|
|
19
14
|
|
|
20
15
|
class SettingListing:
|
|
@@ -116,6 +111,6 @@ class SettingListing:
|
|
|
116
111
|
def _scope_stmt(scope: SettingScope, scope_id: str):
|
|
117
112
|
return (
|
|
118
113
|
select(Setting)
|
|
119
|
-
.where(Setting.scope == scope.value, Setting.scope_id == scope_id)
|
|
114
|
+
.where(Setting.scope == scope.value, Setting.scope_id == scope_id) # ty: ignore[invalid-argument-type]
|
|
120
115
|
.order_by(Setting.key)
|
|
121
116
|
)
|
{simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_module_settings.py
RENAMED
|
@@ -12,21 +12,27 @@ from typing import Any
|
|
|
12
12
|
|
|
13
13
|
from fastapi import FastAPI
|
|
14
14
|
from pydantic_settings import BaseSettings
|
|
15
|
-
|
|
16
|
-
from settings._field_meta import choices_for, env_readable_var, resolve_default
|
|
15
|
+
from simple_module_core.module_settings import get_module_settings_registry
|
|
17
16
|
|
|
18
17
|
# Re-exported: callers reach for ``SECRET_MASK`` / ``is_secret_field`` here.
|
|
19
|
-
from
|
|
18
|
+
from simple_module_core.secret_fields import ( # noqa: F401
|
|
20
19
|
_NEVER_SECRET_TYPES,
|
|
21
20
|
SECRET_MASK,
|
|
21
|
+
annotation_holds_secret,
|
|
22
22
|
conceals_secret,
|
|
23
23
|
embeds_credential,
|
|
24
24
|
is_named_secret,
|
|
25
25
|
is_secret_field,
|
|
26
26
|
mask,
|
|
27
27
|
)
|
|
28
|
+
from simple_module_core.settings_hydrate import (
|
|
29
|
+
env_supplied_fields,
|
|
30
|
+
input_type_for_field,
|
|
31
|
+
value_type_for_field,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from settings._field_meta import choices_for, env_readable_var, resolve_default
|
|
28
35
|
from settings.env_vars import env_prefix_for
|
|
29
|
-
from settings.hydrate import value_type_for_field
|
|
30
36
|
from settings.service import SettingService
|
|
31
37
|
|
|
32
38
|
|
|
@@ -48,7 +54,7 @@ class ModuleSettingField:
|
|
|
48
54
|
The bundled module settings classes declare no ``env_prefix`` — they are
|
|
49
55
|
constructed from pydantic defaults and hydrated from the DB — so their
|
|
50
56
|
``SM_*`` vars are never consulted. Reporting a leftover
|
|
51
|
-
``
|
|
57
|
+
``SM_MAIL_SMTP_HOST`` as the live source would invert the very question
|
|
52
58
|
this screen answers. Classes that *do* declare one (the host's ``Settings``
|
|
53
59
|
with ``SM_``, and anything from the module scaffold) report ``env`` for
|
|
54
60
|
real, because for them pydantic really does read it.
|
|
@@ -71,22 +77,28 @@ class ModuleSettingField:
|
|
|
71
77
|
"""
|
|
72
78
|
choices: list[str] | None = None
|
|
73
79
|
"""Closed set of accepted values, or ``None`` when the field is free text."""
|
|
80
|
+
input_type: str = ""
|
|
81
|
+
"""Editor control: ``type`` with an ``X | None`` unwrapped to ``X``."""
|
|
82
|
+
nullable: bool = False
|
|
83
|
+
"""``None`` is a valid value; the editor sends it for an empty input."""
|
|
74
84
|
|
|
75
85
|
@property
|
|
76
86
|
def source(self) -> str:
|
|
77
87
|
"""Where the live value came from: ``db``, ``env`` or ``default``.
|
|
78
88
|
|
|
79
|
-
Mirrors the precedence in ``hydrate_settings``: DB
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
appears for settings classes that
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
Mirrors the precedence in ``hydrate_settings``: env → DB → default
|
|
90
|
+
(GH #486). A stored override for a field the environment sets is
|
|
91
|
+
ignored, so ``env`` wins even when ``db_override`` is true. ``env`` only
|
|
92
|
+
appears for settings classes that declare an ``env_prefix`` (or name
|
|
93
|
+
the field in ``env_seeded_fields``) — the same rule
|
|
94
|
+
``hydrate.env_supplied_fields`` applies; see :attr:`env_set`. Showing
|
|
95
|
+
this is the difference between "why is this not taking effect" being a
|
|
96
|
+
five-minute question and an afternoon.
|
|
85
97
|
"""
|
|
86
|
-
if self.db_override:
|
|
87
|
-
return "db"
|
|
88
98
|
if self.env_set:
|
|
89
99
|
return "env"
|
|
100
|
+
if self.db_override:
|
|
101
|
+
return "db"
|
|
90
102
|
return "default"
|
|
91
103
|
|
|
92
104
|
|
|
@@ -126,28 +138,42 @@ def _field_view(
|
|
|
126
138
|
settings: BaseSettings,
|
|
127
139
|
prefix: str,
|
|
128
140
|
overridden: frozenset[str] = frozenset(),
|
|
141
|
+
env_supplied: frozenset[str] | None = None,
|
|
129
142
|
) -> ModuleSettingField:
|
|
130
143
|
cls = type(settings)
|
|
144
|
+
if env_supplied is None:
|
|
145
|
+
env_supplied = env_supplied_fields(cls)
|
|
131
146
|
info = cls.model_fields[name]
|
|
132
147
|
raw_value = getattr(settings, name)
|
|
133
148
|
value_type = value_type_for_field(cls, name)
|
|
149
|
+
input_type, nullable = input_type_for_field(cls, name)
|
|
134
150
|
named_secret = is_named_secret(name, value_type)
|
|
135
151
|
extra = info.json_schema_extra if isinstance(info.json_schema_extra, dict) else {}
|
|
136
152
|
default = resolve_default(info)
|
|
137
153
|
env_var = f"{prefix}{name.upper()}"
|
|
138
154
|
live_env_var = env_readable_var(settings, name)
|
|
139
|
-
|
|
140
|
-
#
|
|
141
|
-
#
|
|
142
|
-
|
|
155
|
+
# The same answer ``build_settings`` acts on (GH #486), so a value supplied
|
|
156
|
+
# by the class's ``.env``, a secrets file or an alias is labelled "env"
|
|
157
|
+
# here exactly when hydration lets it beat a stored override.
|
|
158
|
+
env_set = name in env_supplied
|
|
159
|
+
raw_env = os.environ.get(live_env_var) if env_set and live_env_var is not None else None
|
|
160
|
+
|
|
161
|
+
# Declared as ``SecretStr`` (also inside ``X | None`` or a container):
|
|
162
|
+
# masked whatever the field is called, since the store holds it in clear.
|
|
163
|
+
typed_secret = annotation_holds_secret(info.annotation)
|
|
143
164
|
|
|
144
165
|
def hidden(value: Any) -> bool:
|
|
145
|
-
return conceals_secret(name, value, value_type)
|
|
166
|
+
return typed_secret or conceals_secret(name, value, value_type)
|
|
146
167
|
|
|
147
168
|
# ``is_secret`` drives the editor's input type and the "reveal" affordance,
|
|
148
169
|
# so it has to be true when *any* of the three readings is being hidden —
|
|
149
170
|
# otherwise the form offers to edit a value it is showing as dots.
|
|
150
|
-
secret =
|
|
171
|
+
secret = (
|
|
172
|
+
typed_secret
|
|
173
|
+
or named_secret
|
|
174
|
+
or any(embeds_credential(v) for v in (raw_value, default, raw_env))
|
|
175
|
+
)
|
|
176
|
+
group = extra.get("group")
|
|
151
177
|
return ModuleSettingField(
|
|
152
178
|
name=name,
|
|
153
179
|
env_var=env_var,
|
|
@@ -157,7 +183,7 @@ def _field_view(
|
|
|
157
183
|
is_secret=secret,
|
|
158
184
|
type=value_type,
|
|
159
185
|
requires_restart=bool(extra.get("requires_restart", False)),
|
|
160
|
-
group=
|
|
186
|
+
group=group if isinstance(group, str) else None,
|
|
161
187
|
env_set=env_set,
|
|
162
188
|
db_override=name in overridden,
|
|
163
189
|
env_readable=live_env_var is not None,
|
|
@@ -166,6 +192,8 @@ def _field_view(
|
|
|
166
192
|
# Resolved value panel's env row into a row of dots for every key.
|
|
167
193
|
env_value=(mask(raw_env) if hidden(raw_env) else raw_env) if raw_env is not None else None,
|
|
168
194
|
choices=choices_for(info),
|
|
195
|
+
input_type=input_type,
|
|
196
|
+
nullable=nullable,
|
|
169
197
|
)
|
|
170
198
|
|
|
171
199
|
|
|
@@ -176,7 +204,7 @@ def collect_module_settings(
|
|
|
176
204
|
"""Return a sorted, serializable view of every module's BaseSettings.
|
|
177
205
|
|
|
178
206
|
Folds in both ``app.state.sm.modules`` (plugin modules) and additional
|
|
179
|
-
packages registered
|
|
207
|
+
packages registered in the framework's module-settings registry (e.g.
|
|
180
208
|
``"host"``) that aren't backed by a ``ModuleBase`` instance.
|
|
181
209
|
|
|
182
210
|
``overrides`` maps package -> field names carrying a stored override. It
|
|
@@ -187,8 +215,7 @@ def collect_module_settings(
|
|
|
187
215
|
views: list[ModuleSettingsView] = []
|
|
188
216
|
seen: set[str] = set()
|
|
189
217
|
|
|
190
|
-
|
|
191
|
-
registry = getattr(settings_services, "module_registry", None)
|
|
218
|
+
registry = get_module_settings_registry(app)
|
|
192
219
|
|
|
193
220
|
def _manage_url(package: str) -> str | None:
|
|
194
221
|
return registry.manage_url(package) if registry is not None else None
|
|
@@ -228,8 +255,10 @@ def _build_view(
|
|
|
228
255
|
) -> ModuleSettingsView:
|
|
229
256
|
prefix = env_prefix_for(package)
|
|
230
257
|
overridden = (overrides or {}).get(package, frozenset())
|
|
258
|
+
from_env = env_supplied_fields(type(settings))
|
|
231
259
|
fields = [
|
|
232
|
-
_field_view(name, settings, prefix, overridden
|
|
260
|
+
_field_view(name, settings, prefix, overridden, from_env)
|
|
261
|
+
for name in type(settings).model_fields
|
|
233
262
|
]
|
|
234
263
|
return ModuleSettingsView(
|
|
235
264
|
module_name=module_name,
|
{simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/_module_settings_props.py
RENAMED
|
@@ -49,6 +49,8 @@ def serialize(views: list[ModuleSettingsView]) -> list[dict[str, Any]]:
|
|
|
49
49
|
# Turns a pattern-constrained string into a select rather
|
|
50
50
|
# than a text box whose only feedback on a typo is a 422.
|
|
51
51
|
"choices": f.choices,
|
|
52
|
+
"input_type": f.input_type or f.type,
|
|
53
|
+
"nullable": f.nullable,
|
|
52
54
|
}
|
|
53
55
|
for f in v.fields
|
|
54
56
|
],
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Split out of ``service`` so the "is this a secret, and is this write the mask
|
|
4
4
|
being echoed back?" question lives in one file, next to the field-level rules in
|
|
5
|
-
``
|
|
5
|
+
``simple_module_core.secret_fields`` that it delegates to. ``service`` imports these; nothing else
|
|
6
6
|
should need to, because the point of funnelling every read through ``out`` is
|
|
7
7
|
that there is no second way to serialize a row.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from
|
|
12
|
+
from simple_module_core.secret_fields import conceals_secret
|
|
13
|
+
from simple_module_db.stored_settings import Setting
|
|
14
|
+
|
|
13
15
|
from settings.constants import SENSITIVE_KEYS, SENSITIVE_PLACEHOLDER
|
|
14
16
|
from settings.contracts.schemas import SettingOut
|
|
15
|
-
from settings.models import Setting
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
def is_masked(entity: Setting) -> bool:
|
|
@@ -21,7 +22,7 @@ def is_masked(entity: Setting) -> bool:
|
|
|
21
22
|
:data:`SENSITIVE_KEYS` names the one key the hosting layer owns
|
|
22
23
|
(``host.secret_key``). Everything else is judged by the same name/value
|
|
23
24
|
rule the module editor uses, because the store is the *same data* seen
|
|
24
|
-
through a different screen: an override named ``
|
|
25
|
+
through a different screen: an override named ``mail.smtp_password``, or
|
|
25
26
|
any override holding a ``postgresql://user:pw@host/db``, was rendered in
|
|
26
27
|
clear text in the browse table and pre-filled into the edit form purely
|
|
27
28
|
because the allowlist had a single entry in it.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Re-export of :mod:`simple_module_core.secret_fields` (moved there in #500)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from simple_module_core.secret_fields import ( # noqa: F401
|
|
6
|
+
_NEVER_SECRET_TYPES,
|
|
7
|
+
_SECRET_PATTERNS,
|
|
8
|
+
_TYPED_SECRET_KEYS,
|
|
9
|
+
SECRET_MASK,
|
|
10
|
+
annotation_holds_secret,
|
|
11
|
+
conceals_secret,
|
|
12
|
+
embeds_credential,
|
|
13
|
+
is_named_secret,
|
|
14
|
+
is_secret_field,
|
|
15
|
+
mask,
|
|
16
|
+
register_typed_secrets,
|
|
17
|
+
)
|
|
@@ -8,11 +8,10 @@ savepoint, leaving the request's transaction (and its session) usable.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
from simple_module_db.stored_settings import Setting
|
|
11
12
|
from sqlalchemy.exc import IntegrityError
|
|
12
13
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
13
14
|
|
|
14
|
-
from settings.models import Setting
|
|
15
|
-
|
|
16
15
|
|
|
17
16
|
class DuplicateSettingError(Exception):
|
|
18
17
|
"""A row with this (scope, scope_id, key) already exists."""
|
|
@@ -14,10 +14,10 @@ from collections.abc import Mapping
|
|
|
14
14
|
import typer
|
|
15
15
|
from fastapi import FastAPI
|
|
16
16
|
from simple_module_core.dotenv import find_env_file, parse_dotenv
|
|
17
|
+
from simple_module_core.module_settings import module_settings_registry
|
|
18
|
+
from simple_module_core.settings_hydrate import value_type_for_field
|
|
17
19
|
|
|
18
|
-
from settings.constants import MODULE_PACKAGE
|
|
19
20
|
from settings.env_vars import env_prefix_for
|
|
20
|
-
from settings.hydrate import value_type_for_field
|
|
21
21
|
from settings.store import SettingsStore
|
|
22
22
|
|
|
23
23
|
app = typer.Typer(help="Settings module administration.", no_args_is_help=True)
|
|
@@ -35,7 +35,7 @@ async def import_from_env_impl(
|
|
|
35
35
|
"""
|
|
36
36
|
if environ is None:
|
|
37
37
|
environ = {**parse_dotenv(find_env_file()), **os.environ}
|
|
38
|
-
registry =
|
|
38
|
+
registry = module_settings_registry(app_inst)
|
|
39
39
|
count = 0
|
|
40
40
|
for package, cls in registry.items():
|
|
41
41
|
prefix = env_prefix_for(package)
|
|
@@ -71,7 +71,7 @@ def import_from_env() -> None:
|
|
|
71
71
|
if n == 0 and find_env_file().is_file():
|
|
72
72
|
typer.echo(
|
|
73
73
|
"Hint: variables must be named SM_<MODULE>_<FIELD> "
|
|
74
|
-
"(e.g.
|
|
74
|
+
"(e.g. SM_MAIL_SMTP_PORT); none in the environment or .env matched."
|
|
75
75
|
)
|
|
76
76
|
return 0
|
|
77
77
|
|
|
@@ -10,6 +10,28 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
from typing import Final
|
|
12
12
|
|
|
13
|
+
# Re-exported (#500): these now live with the table in the framework.
|
|
14
|
+
from simple_module_db.stored_settings import ( # noqa: F401
|
|
15
|
+
ALL_VALUE_TYPES,
|
|
16
|
+
DESCRIPTION_MAX_LENGTH,
|
|
17
|
+
KEY_MAX_LENGTH,
|
|
18
|
+
SCOPE_ID_MAX_LENGTH,
|
|
19
|
+
SCOPE_MAX_LENGTH,
|
|
20
|
+
SCOPE_SYSTEM,
|
|
21
|
+
SCOPE_TENANT,
|
|
22
|
+
SCOPE_USER,
|
|
23
|
+
SYSTEM_SCOPE_ID,
|
|
24
|
+
TABLE_SETTING,
|
|
25
|
+
UQ_SCOPE_KEY,
|
|
26
|
+
VALUE_MAX_LENGTH,
|
|
27
|
+
VALUE_TYPE_BOOL,
|
|
28
|
+
VALUE_TYPE_FLOAT,
|
|
29
|
+
VALUE_TYPE_INT,
|
|
30
|
+
VALUE_TYPE_JSON,
|
|
31
|
+
VALUE_TYPE_MAX_LENGTH,
|
|
32
|
+
VALUE_TYPE_STRING,
|
|
33
|
+
)
|
|
34
|
+
|
|
13
35
|
# ── Module identity ──────────────────────────────────────────────────
|
|
14
36
|
MODULE_NAME: Final = "Settings"
|
|
15
37
|
MODULE_PACKAGE: Final = "settings"
|
|
@@ -17,30 +39,10 @@ ENV_PREFIX: Final = "SM_SETTINGS_"
|
|
|
17
39
|
LOCALE_NAMESPACE: Final = MODULE_PACKAGE
|
|
18
40
|
|
|
19
41
|
# ── Scopes ───────────────────────────────────────────────────────────
|
|
20
|
-
#
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
SCOPE_USER: Final = "user"
|
|
42
|
+
# Scope, value-type, table and field-limit constants moved to
|
|
43
|
+
# ``simple_module_db.stored_settings`` with the table itself (#500); they are
|
|
44
|
+
# re-exported from the import block at the top of this file.
|
|
24
45
|
ALL_SCOPES: Final = (SCOPE_SYSTEM, SCOPE_TENANT, SCOPE_USER)
|
|
25
|
-
# scope_id is empty for SYSTEM; empty string (not NULL) so composite unique
|
|
26
|
-
# works uniformly on SQLite and PostgreSQL (NULL breaks uniqueness on PG).
|
|
27
|
-
SYSTEM_SCOPE_ID: Final = ""
|
|
28
|
-
|
|
29
|
-
# ── Value types ──────────────────────────────────────────────────────
|
|
30
|
-
# Values are always stored as strings; ``value_type`` tells consumers how
|
|
31
|
-
# to interpret the bytes and lets the UI pick the right input control.
|
|
32
|
-
VALUE_TYPE_STRING: Final = "string"
|
|
33
|
-
VALUE_TYPE_BOOL: Final = "bool"
|
|
34
|
-
VALUE_TYPE_INT: Final = "int"
|
|
35
|
-
VALUE_TYPE_FLOAT: Final = "float"
|
|
36
|
-
VALUE_TYPE_JSON: Final = "json"
|
|
37
|
-
ALL_VALUE_TYPES: Final = (
|
|
38
|
-
VALUE_TYPE_STRING,
|
|
39
|
-
VALUE_TYPE_BOOL,
|
|
40
|
-
VALUE_TYPE_INT,
|
|
41
|
-
VALUE_TYPE_FLOAT,
|
|
42
|
-
VALUE_TYPE_JSON,
|
|
43
|
-
)
|
|
44
46
|
|
|
45
47
|
# ── Secrets ──────────────────────────────────────────────────────────
|
|
46
48
|
# Keys whose stored value must never leave the settings API or the raw
|
|
@@ -104,16 +106,6 @@ INVALIDATION_CHANNEL: Final = "settings.values"
|
|
|
104
106
|
|
|
105
107
|
# ── Database ─────────────────────────────────────────────────────────
|
|
106
108
|
DB_SCHEMA: Final = MODULE_PACKAGE
|
|
107
|
-
TABLE_SETTING: Final = "settings_setting"
|
|
108
|
-
UQ_SCOPE_KEY: Final = "uq_settings_setting_scope_scope_id_key"
|
|
109
|
-
|
|
110
|
-
# ── Field limits ─────────────────────────────────────────────────────
|
|
111
|
-
KEY_MAX_LENGTH: Final = 200
|
|
112
|
-
VALUE_MAX_LENGTH: Final = 4000
|
|
113
|
-
DESCRIPTION_MAX_LENGTH: Final = 2000
|
|
114
|
-
SCOPE_MAX_LENGTH: Final = 10
|
|
115
|
-
SCOPE_ID_MAX_LENGTH: Final = 255
|
|
116
|
-
VALUE_TYPE_MAX_LENGTH: Final = 10
|
|
117
109
|
|
|
118
110
|
# ── Inertia page component names ─────────────────────────────────────
|
|
119
111
|
PAGE_BROWSE: Final = f"{MODULE_NAME}/Browse"
|
{simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/accessor.py
RENAMED
|
@@ -23,7 +23,8 @@ import json
|
|
|
23
23
|
from collections.abc import Callable
|
|
24
24
|
from typing import TYPE_CHECKING, Any, Final
|
|
25
25
|
|
|
26
|
-
from
|
|
26
|
+
from simple_module_db.stored_settings import SYSTEM_SCOPE_ID
|
|
27
|
+
|
|
27
28
|
from settings.contracts.registry import SettingsRegistry
|
|
28
29
|
from settings.contracts.schemas import (
|
|
29
30
|
BOOL_LITERALS_FALSE,
|
{simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/registry.py
RENAMED
|
@@ -58,6 +58,8 @@ class SettingDefinition:
|
|
|
58
58
|
of offering a text box for an opaque id. ``description_key`` is the i18n
|
|
59
59
|
key the tenant settings page translates ``description`` with (the English
|
|
60
60
|
``description`` stays the fallback when the key is missing or unset).
|
|
61
|
+
``label_key`` does the same for the row's title (the raw ``key`` is the
|
|
62
|
+
fallback).
|
|
61
63
|
``clear_via`` names the route that owns clearing the key (e.g. an upload
|
|
62
64
|
route that also reaps the stored file): the generic DELETE routes answer
|
|
63
65
|
422 pointing there while the tenant exists. A platform operator may still
|
|
@@ -76,6 +78,7 @@ class SettingDefinition:
|
|
|
76
78
|
clear_via: str | Mapping[SettingScope, str] = ""
|
|
77
79
|
upload_url: str = ""
|
|
78
80
|
description_key: str = ""
|
|
81
|
+
label_key: str = ""
|
|
79
82
|
|
|
80
83
|
|
|
81
84
|
def clear_route(definition: SettingDefinition, scope: SettingScope | str) -> str:
|
{simple_module_settings-0.0.36 → simple_module_settings-0.0.38}/settings/contracts/schemas.py
RENAMED
|
@@ -7,13 +7,8 @@ from datetime import datetime
|
|
|
7
7
|
from enum import StrEnum
|
|
8
8
|
|
|
9
9
|
from pydantic import ConfigDict, model_validator
|
|
10
|
-
from
|
|
11
|
-
|
|
12
|
-
from settings.constants import (
|
|
10
|
+
from simple_module_db.stored_settings import (
|
|
13
11
|
DESCRIPTION_MAX_LENGTH,
|
|
14
|
-
ERR_SCOPED_REQUIRES_ID,
|
|
15
|
-
ERR_SYSTEM_SCOPE_NO_ID,
|
|
16
|
-
ERR_VALUE_MISMATCH,
|
|
17
12
|
KEY_MAX_LENGTH,
|
|
18
13
|
SCOPE_ID_MAX_LENGTH,
|
|
19
14
|
SCOPE_SYSTEM,
|
|
@@ -27,6 +22,9 @@ from settings.constants import (
|
|
|
27
22
|
VALUE_TYPE_JSON,
|
|
28
23
|
VALUE_TYPE_STRING,
|
|
29
24
|
)
|
|
25
|
+
from sqlmodel import Field, SQLModel
|
|
26
|
+
|
|
27
|
+
from settings.constants import ERR_SCOPED_REQUIRES_ID, ERR_SYSTEM_SCOPE_NO_ID, ERR_VALUE_MISMATCH
|
|
30
28
|
|
|
31
29
|
_BOOL_LITERALS = frozenset(
|
|
32
30
|
{"true", "false", "1", "0", "t", "f", "yes", "no", "y", "n", "on", "off"}
|
|
@@ -92,7 +90,7 @@ def _validate_value_matches_type(value: str, value_type: SettingValueType) -> No
|
|
|
92
90
|
class SettingOut(SQLModel):
|
|
93
91
|
"""A setting returned by the API."""
|
|
94
92
|
|
|
95
|
-
model_config = ConfigDict(from_attributes=True)
|
|
93
|
+
model_config = ConfigDict(from_attributes=True) # ty: ignore[invalid-assignment]
|
|
96
94
|
|
|
97
95
|
id: int
|
|
98
96
|
scope: SettingScope
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
6
|
+
from simple_module_db.stored_settings import SYSTEM_SCOPE_ID
|
|
6
7
|
from simple_module_hosting.permissions import RequiresPermission
|
|
7
8
|
|
|
8
9
|
from settings._unique_write import DuplicateSettingError
|
|
@@ -28,7 +29,6 @@ from settings.constants import (
|
|
|
28
29
|
STATUS_NO_CONTENT,
|
|
29
30
|
STATUS_NOT_FOUND,
|
|
30
31
|
STATUS_UNPROCESSABLE,
|
|
31
|
-
SYSTEM_SCOPE_ID,
|
|
32
32
|
)
|
|
33
33
|
from settings.contracts.schemas import (
|
|
34
34
|
SettingCreate,
|