simple-module-settings 0.0.37__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.
Files changed (125) hide show
  1. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/.gitignore +1 -2
  2. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/PKG-INFO +6 -6
  3. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/README.md +2 -2
  4. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/package.json +1 -1
  5. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/pyproject.toml +4 -4
  6. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_announce.py +2 -1
  7. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_field_meta.py +6 -1
  8. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_listing.py +3 -8
  9. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_module_settings.py +14 -9
  10. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_row_masking.py +5 -4
  11. simple_module_settings-0.0.38/settings/_secrets.py +17 -0
  12. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_unique_write.py +1 -2
  13. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/cli.py +4 -4
  14. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/constants.py +25 -33
  15. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/accessor.py +2 -1
  16. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/registry.py +3 -0
  17. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/schemas.py +5 -7
  18. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/api.py +1 -1
  19. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/module_api.py +5 -4
  20. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/views.py +2 -2
  21. simple_module_settings-0.0.38/settings/hydrate.py +36 -0
  22. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/known_keys.py +1 -1
  23. simple_module_settings-0.0.38/settings/locales/de.json +152 -0
  24. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/locales/en.json +3 -1
  25. simple_module_settings-0.0.38/settings/models.py +12 -0
  26. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/module.py +15 -12
  27. simple_module_settings-0.0.38/settings/module_registry.py +17 -0
  28. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/Browse.tsx +0 -1
  29. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/ModulesEdit.tsx +0 -1
  30. simple_module_settings-0.0.38/settings/pages/api.generated.ts +1043 -0
  31. simple_module_settings-0.0.38/settings/pages/api.ts +9 -0
  32. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/ModuleForm.tsx +17 -10
  33. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/TestConnectionButton.tsx +10 -9
  34. simple_module_settings-0.0.38/settings/registration.py +17 -0
  35. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/reload.py +3 -3
  36. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/service.py +4 -8
  37. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/services.py +6 -4
  38. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/store.py +13 -41
  39. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/tenant_view.py +5 -2
  40. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_clear_route_per_scope.py +1 -1
  41. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_cli_import.py +9 -8
  42. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_duplicate_and_race.py +1 -1
  43. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_env_pinned.py +1 -1
  44. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_hydrate.py +6 -5
  45. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_hydrate_related_fields.py +1 -1
  46. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_known_keys_meta.py +12 -12
  47. simple_module_settings-0.0.38/tests/test_lenient_related_fields_validators.py +110 -0
  48. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_api_json_fields.py +1 -1
  49. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_registry.py +1 -1
  50. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_settings.py +14 -14
  51. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_registration.py +2 -3
  52. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_reload.py +1 -1
  53. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_env_unprefixed.py +1 -1
  54. simple_module_settings-0.0.38/tests/test_settings_framework_reexports.py +48 -0
  55. simple_module_settings-0.0.38/tests/test_settings_group_metadata.py +30 -0
  56. simple_module_settings-0.0.38/tests/test_settings_host_restart_flags.py +22 -0
  57. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_schemas.py +1 -1
  58. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_secret_json_roundtrip.py +2 -2
  59. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_secret_masking.py +2 -1
  60. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_secret_urls.py +4 -4
  61. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_service.py +1 -1
  62. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_typed_secret_masking.py +2 -2
  63. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_store_filters.py +3 -3
  64. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_store_secret_masking.py +3 -3
  65. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_testable_checks.py +5 -5
  66. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_upload_key_delete.py +1 -1
  67. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests-js/JsonFields.test.tsx +4 -4
  68. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests-js/ModuleForm.test.tsx +20 -19
  69. simple_module_settings-0.0.38/tests-js/RestartHint.test.tsx +57 -0
  70. simple_module_settings-0.0.38/tests-js/settings_TestConnectionButton.test.tsx +47 -0
  71. simple_module_settings-0.0.37/settings/_secrets.py +0 -137
  72. simple_module_settings-0.0.37/settings/hydrate.py +0 -223
  73. simple_module_settings-0.0.37/settings/models.py +0 -50
  74. simple_module_settings-0.0.37/settings/module_registry.py +0 -53
  75. simple_module_settings-0.0.37/settings/registration.py +0 -40
  76. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/LICENSE +0 -0
  77. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/__init__.py +0 -0
  78. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_managed_keys.py +0 -0
  79. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/_module_settings_props.py +0 -0
  80. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/browse_query.py +0 -0
  81. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/__init__.py +0 -0
  82. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/events.py +0 -0
  83. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/contracts/invalidation.py +0 -0
  84. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/deps.py +0 -0
  85. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/__init__.py +0 -0
  86. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/_create_form.py +0 -0
  87. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/endpoints/tenant_api.py +0 -0
  88. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/env_vars.py +0 -0
  89. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/errors.py +0 -0
  90. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/Create.tsx +0 -0
  91. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/Edit.tsx +0 -0
  92. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/FieldInput.tsx +0 -0
  93. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/FieldSource.tsx +0 -0
  94. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/JsonInput.tsx +0 -0
  95. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/KeyField.tsx +0 -0
  96. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/ModuleFieldRow.tsx +0 -0
  97. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/ResolvedValue.tsx +0 -0
  98. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/ScopeTabs.tsx +0 -0
  99. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/StoreCards.tsx +0 -0
  100. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/StoreTable.tsx +0 -0
  101. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/ValueInput.tsx +0 -0
  102. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/last-test.ts +0 -0
  103. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/validation-errors.test.ts +0 -0
  104. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/components/validation-errors.ts +0 -0
  105. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/routes.ts +0 -0
  106. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/pages/types.ts +0 -0
  107. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/py.typed +0 -0
  108. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/settings.py +0 -0
  109. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/settings/tenant_scope.py +0 -0
  110. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_concurrent_overrides.py +0 -0
  111. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_api.py +0 -0
  112. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_settings_render.py +0 -0
  113. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_module_settings_serialize.py +0 -0
  114. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_accessor.py +0 -0
  115. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_api.py +0 -0
  116. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_default_clears_override.py +0 -0
  117. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_events.py +0 -0
  118. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_field_sources.py +0 -0
  119. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_module.py +0 -0
  120. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_settings_view_authz.py +0 -0
  121. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_store.py +0 -0
  122. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests/test_tenant_settings.py +0 -0
  123. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests-js/ModulesEdit.test.tsx +0 -0
  124. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tests-js/ScopeTabs.test.tsx +0 -0
  125. {simple_module_settings-0.0.37 → simple_module_settings-0.0.38}/tsconfig.json +0 -0
@@ -7,8 +7,6 @@ build/
7
7
  .venv/
8
8
  *.egg
9
9
 
10
- # UV
11
- uv.lock
12
10
 
13
11
  # Node
14
12
  node_modules/
@@ -65,3 +63,4 @@ host/client_app/.playwright-cli/*
65
63
  .qa/
66
64
  .verify/
67
65
  /openapi.json
66
+ qa-shots/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_settings
3
- Version: 0.0.37
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.37
25
- Requires-Dist: simple-module-db==0.0.37
26
- Requires-Dist: simple-module-hosting==0.0.37
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 settings.registration import register_module_settings
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/modules`, hydrated from the DB store at startup. The settings class is a `pydantic_settings.BaseSettings` with the module's `SM_<PREFIX>_*` env-var defaults.
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 settings.registration import register_module_settings
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/modules`, hydrated from the DB store at startup. The settings class is a `pydantic_settings.BaseSettings` with the module's `SM_<PREFIX>_*` env-var defaults.
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
  {
2
2
  "name": "@simple-module-py/settings",
3
- "version": "0.1.0",
3
+ "version": "0.0.38",
4
4
  "private": true,
5
5
  "description": "Frontend assets for the Settings module",
6
6
  "peerDependencies": {
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_settings"
3
- version = "0.0.37"
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.37",
25
- "simple_module_db==0.0.37",
26
- "simple_module_hosting==0.0.37",
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, SCOPE_SYSTEM, SCOPE_TENANT
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
  )
@@ -12,11 +12,10 @@ 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 settings._secrets import ( # noqa: F401
18
+ from simple_module_core.secret_fields import ( # noqa: F401
20
19
  _NEVER_SECRET_TYPES,
21
20
  SECRET_MASK,
22
21
  annotation_holds_secret,
@@ -26,8 +25,14 @@ from settings._secrets import ( # noqa: F401
26
25
  is_secret_field,
27
26
  mask,
28
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
29
35
  from settings.env_vars import env_prefix_for
30
- from settings.hydrate import env_supplied_fields, input_type_for_field, value_type_for_field
31
36
  from settings.service import SettingService
32
37
 
33
38
 
@@ -49,7 +54,7 @@ class ModuleSettingField:
49
54
  The bundled module settings classes declare no ``env_prefix`` — they are
50
55
  constructed from pydantic defaults and hydrated from the DB — so their
51
56
  ``SM_*`` vars are never consulted. Reporting a leftover
52
- ``SM_USERS_SMTP_HOST`` as the live source would invert the very question
57
+ ``SM_MAIL_SMTP_HOST`` as the live source would invert the very question
53
58
  this screen answers. Classes that *do* declare one (the host's ``Settings``
54
59
  with ``SM_``, and anything from the module scaffold) report ``env`` for
55
60
  real, because for them pydantic really does read it.
@@ -168,6 +173,7 @@ def _field_view(
168
173
  or named_secret
169
174
  or any(embeds_credential(v) for v in (raw_value, default, raw_env))
170
175
  )
176
+ group = extra.get("group")
171
177
  return ModuleSettingField(
172
178
  name=name,
173
179
  env_var=env_var,
@@ -177,7 +183,7 @@ def _field_view(
177
183
  is_secret=secret,
178
184
  type=value_type,
179
185
  requires_restart=bool(extra.get("requires_restart", False)),
180
- group=extra.get("group"),
186
+ group=group if isinstance(group, str) else None,
181
187
  env_set=env_set,
182
188
  db_override=name in overridden,
183
189
  env_readable=live_env_var is not None,
@@ -198,7 +204,7 @@ def collect_module_settings(
198
204
  """Return a sorted, serializable view of every module's BaseSettings.
199
205
 
200
206
  Folds in both ``app.state.sm.modules`` (plugin modules) and additional
201
- packages registered via ``app.state.settings.module_registry`` (e.g.
207
+ packages registered in the framework's module-settings registry (e.g.
202
208
  ``"host"``) that aren't backed by a ``ModuleBase`` instance.
203
209
 
204
210
  ``overrides`` maps package -> field names carrying a stored override. It
@@ -209,8 +215,7 @@ def collect_module_settings(
209
215
  views: list[ModuleSettingsView] = []
210
216
  seen: set[str] = set()
211
217
 
212
- settings_services = getattr(app.state, "settings", None)
213
- registry = getattr(settings_services, "module_registry", None)
218
+ registry = get_module_settings_registry(app)
214
219
 
215
220
  def _manage_url(package: str) -> str | None:
216
221
  return registry.manage_url(package) if registry is not None else None
@@ -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
- ``_secrets`` that it delegates to. ``service`` imports these; nothing else
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 settings._secrets import conceals_secret
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 ``users.smtp_password``, or
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 = getattr(app_inst.state, MODULE_PACKAGE).module_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. SM_USERS_SMTP_PORT); none in the environment or .env matched."
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
- # Precedence high → low when resolving a key: USER > TENANT > SYSTEM.
21
- SCOPE_SYSTEM: Final = "system"
22
- SCOPE_TENANT: Final = "tenant"
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"
@@ -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 settings.constants import SYSTEM_SCOPE_ID
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,
@@ -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:
@@ -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 sqlmodel import Field, SQLModel
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,
@@ -11,6 +11,8 @@ from typing import Any
11
11
 
12
12
  from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Response, status
13
13
  from pydantic import ValidationError
14
+ from simple_module_core.module_settings import module_settings_registry
15
+ from simple_module_core.settings_hydrate import hydrate_settings
14
16
  from simple_module_hosting.i18n_deps import TranslatorDep
15
17
  from simple_module_hosting.permissions import RequiresPermission
16
18
 
@@ -20,10 +22,9 @@ from settings._module_settings import (
20
22
  overrides_by_package,
21
23
  )
22
24
  from settings._module_settings_props import serialize
23
- from settings.constants import MODULE_PACKAGE, PERM_DELETE, PERM_EDIT, PERM_VIEW
25
+ from settings.constants import PERM_DELETE, PERM_EDIT, PERM_VIEW
24
26
  from settings.contracts.events import SettingsReloaded
25
27
  from settings.deps import get_setting_service
26
- from settings.hydrate import hydrate_settings
27
28
  from settings.reload import EnvPinnedError, apply_changes_and_reload
28
29
  from settings.service import SettingService
29
30
  from settings.store import SettingsStore
@@ -94,7 +95,7 @@ async def update_module(
94
95
  t: TranslatorDep,
95
96
  service: SettingService = Depends(get_setting_service),
96
97
  ) -> dict[str, Any]:
97
- registry = getattr(request.app.state, MODULE_PACKAGE).module_registry
98
+ registry = module_settings_registry(request.app)
98
99
  if registry.get(package) is None:
99
100
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Unknown module package")
100
101
  if registry.manage_url(package) is not None:
@@ -146,7 +147,7 @@ async def clear_module_field(
146
147
  request: Request,
147
148
  service: SettingService = Depends(get_setting_service),
148
149
  ) -> Response:
149
- registry = getattr(request.app.state, MODULE_PACKAGE).module_registry
150
+ registry = module_settings_registry(request.app)
150
151
  cls = registry.get(package)
151
152
  if cls is None:
152
153
  raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Unknown module package")
@@ -10,10 +10,10 @@ strings below match what ``pages/*.tsx`` declare.
10
10
  from __future__ import annotations
11
11
 
12
12
  import asyncio
13
- import math
14
13
 
15
14
  from fastapi import APIRouter, Depends, HTTPException, Request
16
15
  from pydantic import ValidationError
16
+ from simple_module_db.pagination import clamp_page
17
17
  from simple_module_hosting.i18n_deps import TranslatorDep
18
18
  from simple_module_hosting.inertia_deps import InertiaDep
19
19
  from simple_module_hosting.inertia_utils import redirect_back_with_errors, validation_errors_to_dict
@@ -107,7 +107,7 @@ async def browse(
107
107
  # A page past the end (stale ?page= link, or the last row on it was just
108
108
  # deleted) is clamped and re-queried — otherwise the client gets an empty
109
109
  # list with a nonzero total and renders a blank table.
110
- number = min(query.page, max(1, math.ceil(total / query.per_page)))
110
+ number = clamp_page(query.page, total, query.per_page)
111
111
  if number != query.page:
112
112
  items, total = await service.list_filtered(
113
113
  query.scope_filter, query.q, number, query.per_page
@@ -0,0 +1,36 @@
1
+ """Deprecated: hydration moved to the framework (#500).
2
+
3
+ ``simple_module_core.settings_hydrate`` holds the hydrator and field helpers;
4
+ the sync worker twin is ``simple_module_db.stored_settings.hydrate_settings_sync``.
5
+ Kept as a re-export until 0.1.0; ``simple_module_core.deprecation`` enforces the date.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from simple_module_core.deprecation import deprecated_module
11
+ from simple_module_core.settings_hydrate import (
12
+ RELATED_FIELDS_CTX,
13
+ build_settings,
14
+ env_supplied_fields,
15
+ hydrate_settings,
16
+ input_type_for_field,
17
+ value_type_for_field,
18
+ )
19
+ from simple_module_db.stored_settings import hydrate_settings_sync
20
+
21
+ __all__ = [
22
+ "RELATED_FIELDS_CTX",
23
+ "build_settings",
24
+ "env_supplied_fields",
25
+ "hydrate_settings",
26
+ "hydrate_settings_sync",
27
+ "input_type_for_field",
28
+ "value_type_for_field",
29
+ ]
30
+
31
+ deprecated_module(
32
+ "settings.hydrate",
33
+ remove_in="0.1.0",
34
+ use="simple_module_core.settings_hydrate (hydrate_settings_sync: "
35
+ "simple_module_db.stored_settings)",
36
+ )
@@ -17,9 +17,9 @@ from typing import Any
17
17
 
18
18
  from fastapi import FastAPI
19
19
  from fastapi.encoders import jsonable_encoder
20
+ from simple_module_core.secret_fields import conceals_secret, mask
20
21
 
21
22
  from settings._module_settings import collect_module_settings
22
- from settings._secrets import conceals_secret, mask
23
23
 
24
24
 
25
25
  def _from_field(package: str, field) -> dict[str, Any]: