simple-module-settings 0.0.1__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 (56) hide show
  1. simple_module_settings-0.0.1/.gitignore +59 -0
  2. simple_module_settings-0.0.1/LICENSE +21 -0
  3. simple_module_settings-0.0.1/PKG-INFO +69 -0
  4. simple_module_settings-0.0.1/README.md +41 -0
  5. simple_module_settings-0.0.1/package.json +16 -0
  6. simple_module_settings-0.0.1/pyproject.toml +56 -0
  7. simple_module_settings-0.0.1/settings/__init__.py +1 -0
  8. simple_module_settings-0.0.1/settings/_module_settings.py +169 -0
  9. simple_module_settings-0.0.1/settings/cli.py +78 -0
  10. simple_module_settings-0.0.1/settings/constants.py +115 -0
  11. simple_module_settings-0.0.1/settings/contracts/__init__.py +24 -0
  12. simple_module_settings-0.0.1/settings/contracts/accessor.py +236 -0
  13. simple_module_settings-0.0.1/settings/contracts/events.py +20 -0
  14. simple_module_settings-0.0.1/settings/contracts/registry.py +66 -0
  15. simple_module_settings-0.0.1/settings/contracts/schemas.py +154 -0
  16. simple_module_settings-0.0.1/settings/deps.py +56 -0
  17. simple_module_settings-0.0.1/settings/endpoints/__init__.py +0 -0
  18. simple_module_settings-0.0.1/settings/endpoints/api.py +202 -0
  19. simple_module_settings-0.0.1/settings/endpoints/module_api.py +102 -0
  20. simple_module_settings-0.0.1/settings/endpoints/views.py +120 -0
  21. simple_module_settings-0.0.1/settings/env_vars.py +18 -0
  22. simple_module_settings-0.0.1/settings/hydrate.py +63 -0
  23. simple_module_settings-0.0.1/settings/locales/en.json +71 -0
  24. simple_module_settings-0.0.1/settings/models.py +50 -0
  25. simple_module_settings-0.0.1/settings/module.py +76 -0
  26. simple_module_settings-0.0.1/settings/module_registry.py +33 -0
  27. simple_module_settings-0.0.1/settings/pages/Browse.tsx +103 -0
  28. simple_module_settings-0.0.1/settings/pages/Create.tsx +113 -0
  29. simple_module_settings-0.0.1/settings/pages/Edit.tsx +102 -0
  30. simple_module_settings-0.0.1/settings/pages/ModulesEdit.tsx +74 -0
  31. simple_module_settings-0.0.1/settings/pages/components/FieldInput.tsx +96 -0
  32. simple_module_settings-0.0.1/settings/pages/components/ModuleForm.tsx +157 -0
  33. simple_module_settings-0.0.1/settings/pages/components/ValueInput.tsx +92 -0
  34. simple_module_settings-0.0.1/settings/pages/routes.ts +7 -0
  35. simple_module_settings-0.0.1/settings/py.typed +0 -0
  36. simple_module_settings-0.0.1/settings/registration.py +34 -0
  37. simple_module_settings-0.0.1/settings/reload.py +58 -0
  38. simple_module_settings-0.0.1/settings/service.py +162 -0
  39. simple_module_settings-0.0.1/settings/services.py +36 -0
  40. simple_module_settings-0.0.1/settings/settings.py +11 -0
  41. simple_module_settings-0.0.1/settings/store.py +61 -0
  42. simple_module_settings-0.0.1/tests/test_cli_import.py +44 -0
  43. simple_module_settings-0.0.1/tests/test_hydrate.py +65 -0
  44. simple_module_settings-0.0.1/tests/test_module_api.py +56 -0
  45. simple_module_settings-0.0.1/tests/test_module_registry.py +39 -0
  46. simple_module_settings-0.0.1/tests/test_module_settings.py +64 -0
  47. simple_module_settings-0.0.1/tests/test_registration.py +51 -0
  48. simple_module_settings-0.0.1/tests/test_reload.py +98 -0
  49. simple_module_settings-0.0.1/tests/test_settings_accessor.py +179 -0
  50. simple_module_settings-0.0.1/tests/test_settings_api.py +140 -0
  51. simple_module_settings-0.0.1/tests/test_settings_events.py +18 -0
  52. simple_module_settings-0.0.1/tests/test_settings_module.py +48 -0
  53. simple_module_settings-0.0.1/tests/test_settings_schemas.py +107 -0
  54. simple_module_settings-0.0.1/tests/test_settings_service.py +141 -0
  55. simple_module_settings-0.0.1/tests/test_store.py +56 -0
  56. simple_module_settings-0.0.1/tsconfig.json +11 -0
@@ -0,0 +1,59 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ *.egg
9
+
10
+ # UV
11
+ uv.lock
12
+
13
+ # Node
14
+ node_modules/
15
+
16
+ # IDE
17
+ .idea/
18
+ .vscode/
19
+ *.swp
20
+ *.swo
21
+
22
+ # Environment
23
+ .env
24
+
25
+ # Database
26
+ *.db
27
+ *.sqlite3
28
+
29
+ # Module-managed runtime state (e.g. uploaded dataset files,
30
+ # default storage_dir for SM_DATASETS_STORAGE_DIR).
31
+ var/
32
+
33
+ # file_storage filesystem backend default root (override via SM_FILE_STORAGE_FS_ROOT_PATH).
34
+ uploads/
35
+
36
+ # Vite
37
+ host/static/dist/
38
+
39
+ # Auto-generated frontend module manifest (regenerated by the host at boot
40
+ # or via `make gen-pages`).
41
+ host/client_app/modules.manifest.json
42
+ host/client_app/modules.generated.ts
43
+ host/client_app/modules.generated.css
44
+
45
+ # Worktrees
46
+ .worktrees/
47
+
48
+ # Performance profiles
49
+ .memray/
50
+ .benchmarks/
51
+
52
+ # OS
53
+ .DS_Store
54
+ Thumbs.db
55
+
56
+ .playwright-cli/*
57
+ .playwright-mcp/*
58
+ host/client_app/.playwright-cli/*
59
+ .superpowers/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anto Subash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_module_settings
3
+ Version: 0.0.1
4
+ Summary: Runtime settings UI — modules plug their own settings panels into a shared admin view
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
8
+ Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
9
+ Author-email: Anto Subash <antosubash@live.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: admin,configuration,settings,simple-module
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.12
24
+ Requires-Dist: simple-module-core==0.0.1
25
+ Requires-Dist: simple-module-db==0.0.1
26
+ Requires-Dist: simple-module-hosting==0.0.1
27
+ Description-Content-Type: text/markdown
28
+
29
+ # simple_module_settings
30
+
31
+ Runtime settings UI for [simple_module](https://github.com/antosubash/simple_module_python) apps. Other modules plug their own settings panels into a shared admin view — one page per module tab — without the host having to know about them.
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install simple_module_settings
37
+ ```
38
+
39
+ ## What it provides
40
+
41
+ - `/settings` admin page aggregating every installed module's settings panel.
42
+ - `register_settings_panel()` hook — a module declares `{title, inertia_page, requires_permission}`; `simple_module_settings` renders the tab.
43
+ - DB-backed runtime settings table (separate from env-var-driven `SM_*` settings) for values admins change at runtime.
44
+
45
+ ## Usage
46
+
47
+ Register a panel:
48
+
49
+ ```python
50
+ class OrdersModule(ModuleBase):
51
+ meta = ModuleMeta(name="orders")
52
+
53
+ def register_settings_panel(self):
54
+ return {
55
+ "title": "Orders",
56
+ "inertia_page": "Orders/SettingsPanel",
57
+ "requires_permission": "orders.manage",
58
+ }
59
+ ```
60
+
61
+ That adds an **Orders** tab at `/settings`. The rendered page is a regular Inertia page authored inside the `orders` module.
62
+
63
+ ## Depends on
64
+
65
+ - `simple_module_core`, `simple_module_db`, `simple_module_hosting`
66
+
67
+ ## License
68
+
69
+ MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
@@ -0,0 +1,41 @@
1
+ # simple_module_settings
2
+
3
+ Runtime settings UI for [simple_module](https://github.com/antosubash/simple_module_python) apps. Other modules plug their own settings panels into a shared admin view — one page per module tab — without the host having to know about them.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install simple_module_settings
9
+ ```
10
+
11
+ ## What it provides
12
+
13
+ - `/settings` admin page aggregating every installed module's settings panel.
14
+ - `register_settings_panel()` hook — a module declares `{title, inertia_page, requires_permission}`; `simple_module_settings` renders the tab.
15
+ - DB-backed runtime settings table (separate from env-var-driven `SM_*` settings) for values admins change at runtime.
16
+
17
+ ## Usage
18
+
19
+ Register a panel:
20
+
21
+ ```python
22
+ class OrdersModule(ModuleBase):
23
+ meta = ModuleMeta(name="orders")
24
+
25
+ def register_settings_panel(self):
26
+ return {
27
+ "title": "Orders",
28
+ "inertia_page": "Orders/SettingsPanel",
29
+ "requires_permission": "orders.manage",
30
+ }
31
+ ```
32
+
33
+ That adds an **Orders** tab at `/settings`. The rendered page is a regular Inertia page authored inside the `orders` module.
34
+
35
+ ## Depends on
36
+
37
+ - `simple_module_core`, `simple_module_db`, `simple_module_hosting`
38
+
39
+ ## License
40
+
41
+ MIT — see [LICENSE](https://github.com/antosubash/simple_module_python/blob/main/LICENSE).
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@simple-module-py/settings",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Frontend assets for the Settings module",
6
+ "peerDependencies": {
7
+ "react": "^19.0.0",
8
+ "react-dom": "^19.0.0",
9
+ "@inertiajs/react": "^2.0.0",
10
+ "@simple-module-py/ui": "*"
11
+ },
12
+ "devDependencies": {
13
+ "@simple-module-py/tsconfig": "*"
14
+ },
15
+ "dependencies": {}
16
+ }
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "simple_module_settings"
3
+ version = "0.0.1"
4
+ description = "Runtime settings UI — modules plug their own settings panels into a shared admin view"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ requires-python = ">=3.12"
9
+ authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
10
+ keywords = ["simple-module", "settings", "admin", "configuration"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Framework :: FastAPI",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Topic :: Internet :: WWW/HTTP",
20
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "simple_module_core==0.0.1",
25
+ "simple_module_db==0.0.1",
26
+ "simple_module_hosting==0.0.1",
27
+ ]
28
+
29
+ [project.entry-points.simple_module]
30
+ settings = "settings.module:SettingsModule"
31
+
32
+ [project.scripts]
33
+ sm-settings = "settings.cli:main"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/antosubash/simple_module_python"
37
+ Repository = "https://github.com/antosubash/simple_module_python"
38
+ Issues = "https://github.com/antosubash/simple_module_python/issues"
39
+ Changelog = "https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md"
40
+
41
+ [build-system]
42
+ requires = ["hatchling"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["settings"]
47
+
48
+ # Ship the module-root package.json inside the wheel so the host can
49
+ # discover JS deps via importlib.resources after a pip install.
50
+ [tool.hatch.build.targets.wheel.force-include]
51
+ "package.json" = "settings/package.json"
52
+
53
+ [tool.uv.sources]
54
+ simple_module_core = { workspace = true }
55
+ simple_module_db = { workspace = true }
56
+ simple_module_hosting = { workspace = true }
@@ -0,0 +1 @@
1
+ """Settings module."""
@@ -0,0 +1,169 @@
1
+ """Autodiscover per-module pydantic ``BaseSettings`` attached to ``app.state``.
2
+
3
+ Each module stores a services dataclass on ``app.state.<package>`` whose
4
+ ``.settings`` attribute is a pydantic ``BaseSettings`` subclass.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass
11
+ from typing import Any
12
+
13
+ from fastapi import FastAPI
14
+ from pydantic_settings import BaseSettings
15
+
16
+ from settings.env_vars import env_prefix_for
17
+ from settings.hydrate import value_type_for_field
18
+
19
+ # We intentionally DON'T match the bare word "token" (would mask
20
+ # `verification_token_lifetime_seconds` — just an int) or "key" alone (would
21
+ # mask `s3_bucket_key_prefix`). Only fragments that actually indicate material.
22
+ _SECRET_PATTERNS = re.compile(
23
+ r"(password|secret|api[_-]?key|private[_-]?key|token[_-]?secret)", re.I
24
+ )
25
+ SECRET_MASK = "••••••••"
26
+
27
+
28
+ def is_secret_field(name: str) -> bool:
29
+ """True if a field name suggests it holds credential material."""
30
+ return bool(_SECRET_PATTERNS.search(name))
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class ModuleSettingField:
35
+ name: str
36
+ env_var: str
37
+ value: Any
38
+ default: Any
39
+ description: str
40
+ is_secret: bool
41
+ type: str
42
+ requires_restart: bool
43
+ group: str | None
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class ModuleSettingsView:
48
+ module_name: str
49
+ package: str
50
+ env_prefix: str
51
+ class_name: str
52
+ fields: list[ModuleSettingField]
53
+
54
+
55
+ def _mask(value: Any) -> Any:
56
+ if value in (None, "", [], {}):
57
+ return value
58
+ return SECRET_MASK
59
+
60
+
61
+ def _package_of(mod: Any) -> str:
62
+ return type(mod).__module__.split(".", 1)[0]
63
+
64
+
65
+ def _extract_settings(app: FastAPI, package: str) -> BaseSettings | None:
66
+ """Return the ``BaseSettings`` instance attached to ``app.state.<package>``.
67
+
68
+ The services dataclass exposes it as ``.settings``. We also accept the
69
+ rare case where the module stashes the settings object directly.
70
+ """
71
+ services = getattr(app.state, package, None)
72
+ if services is None:
73
+ return None
74
+ if isinstance(services, BaseSettings):
75
+ return services
76
+ inner = getattr(services, "settings", None)
77
+ return inner if isinstance(inner, BaseSettings) else None
78
+
79
+
80
+ def _field_view(name: str, settings: BaseSettings, prefix: str) -> ModuleSettingField:
81
+ cls = type(settings)
82
+ info = cls.model_fields[name]
83
+ raw_value = getattr(settings, name)
84
+ secret = is_secret_field(name)
85
+ extra = info.json_schema_extra if isinstance(info.json_schema_extra, dict) else {}
86
+ return ModuleSettingField(
87
+ name=name,
88
+ env_var=f"{prefix}{name.upper()}",
89
+ value=_mask(raw_value) if secret else raw_value,
90
+ default=_mask(info.default) if secret else info.default,
91
+ description=info.description or "",
92
+ is_secret=secret,
93
+ type=value_type_for_field(cls, name),
94
+ requires_restart=bool(extra.get("requires_restart", False)),
95
+ group=extra.get("group"),
96
+ )
97
+
98
+
99
+ def collect_module_settings(app: FastAPI) -> list[ModuleSettingsView]:
100
+ """Return a sorted, serializable view of every module's BaseSettings.
101
+
102
+ Folds in both ``app.state.sm.modules`` (plugin modules) and additional
103
+ packages registered via ``app.state.settings.module_registry`` (e.g.
104
+ ``"host"``) that aren't backed by a ``ModuleBase`` instance.
105
+ """
106
+ views: list[ModuleSettingsView] = []
107
+ seen: set[str] = set()
108
+
109
+ for mod in getattr(app.state.sm, "modules", ()):
110
+ package = _package_of(mod)
111
+ settings = _extract_settings(app, package)
112
+ if settings is None:
113
+ continue
114
+ views.append(_build_view(mod.meta.name, package, settings))
115
+ seen.add(package)
116
+
117
+ settings_services = getattr(app.state, "settings", None)
118
+ registry = getattr(settings_services, "module_registry", None)
119
+ if registry is not None:
120
+ for package in registry.all_packages():
121
+ if package in seen:
122
+ continue
123
+ settings = _extract_settings(app, package)
124
+ if settings is None:
125
+ continue
126
+ views.append(_build_view(package.title(), package, settings))
127
+ seen.add(package)
128
+
129
+ views.sort(key=lambda v: v.module_name)
130
+ return views
131
+
132
+
133
+ def _build_view(module_name: str, package: str, settings: BaseSettings) -> ModuleSettingsView:
134
+ prefix = env_prefix_for(package)
135
+ fields = [_field_view(name, settings, prefix) for name in type(settings).model_fields]
136
+ return ModuleSettingsView(
137
+ module_name=module_name,
138
+ package=package,
139
+ env_prefix=prefix,
140
+ class_name=type(settings).__name__,
141
+ fields=fields,
142
+ )
143
+
144
+
145
+ def serialize(views: list[ModuleSettingsView]) -> list[dict[str, Any]]:
146
+ """Convert dataclass views to plain dicts for Inertia props."""
147
+ return [
148
+ {
149
+ "module_name": v.module_name,
150
+ "package": v.package,
151
+ "env_prefix": v.env_prefix,
152
+ "class_name": v.class_name,
153
+ "fields": [
154
+ {
155
+ "name": f.name,
156
+ "env_var": f.env_var,
157
+ "value": f.value,
158
+ "default": f.default,
159
+ "description": f.description,
160
+ "is_secret": f.is_secret,
161
+ "type": f.type,
162
+ "requires_restart": f.requires_restart,
163
+ "group": f.group,
164
+ }
165
+ for f in v.fields
166
+ ],
167
+ }
168
+ for v in views
169
+ ]
@@ -0,0 +1,78 @@
1
+ """``sm-settings`` CLI — currently only ``import-from-env``.
2
+
3
+ One-shot migration: walks every registered module's ``BaseSettings`` and,
4
+ for each field whose legacy ``SM_<PREFIX>_<FIELD>`` env var is set, writes
5
+ a SYSTEM-scoped override into the Settings store.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import os
12
+ import sys
13
+
14
+ from fastapi import FastAPI
15
+
16
+ from settings.constants import MODULE_PACKAGE
17
+ from settings.env_vars import env_prefix_for
18
+ from settings.hydrate import value_type_for_field
19
+ from settings.store import SettingsStore
20
+
21
+
22
+ async def import_from_env_impl(app: FastAPI, store: SettingsStore) -> int:
23
+ """Write a SYSTEM override for every ``SM_<PREFIX>_<FIELD>`` env var set.
24
+
25
+ Returns the count of overrides written. Env vars that don't match a
26
+ registered field are ignored.
27
+ """
28
+ registry = getattr(app.state, MODULE_PACKAGE).module_registry
29
+ count = 0
30
+ for package, cls in registry.items():
31
+ prefix = env_prefix_for(package)
32
+ for field_name in cls.model_fields:
33
+ raw = os.environ.get(f"{prefix}{field_name.upper()}")
34
+ if raw is None:
35
+ continue
36
+ vtype = value_type_for_field(cls, field_name)
37
+ await store.set_override(package, field_name, raw, vtype)
38
+ count += 1
39
+ return count
40
+
41
+
42
+ def main() -> int:
43
+ """Console-script entry point for ``sm-settings``.
44
+
45
+ Supports a single subcommand: ``import-from-env``.
46
+ """
47
+ argv = sys.argv[1:]
48
+ if not argv or argv[0] in ("-h", "--help"):
49
+ print("Usage: sm-settings import-from-env")
50
+ return 0 if argv else 1
51
+ if argv[0] != "import-from-env":
52
+ print(f"Unknown command: {argv[0]}", file=sys.stderr)
53
+ print("Usage: sm-settings import-from-env", file=sys.stderr)
54
+ return 2
55
+
56
+ from simple_module_hosting.app_builder import create_app
57
+ from simple_module_hosting.settings import Settings
58
+
59
+ from settings.service import SettingService
60
+
61
+ app = create_app(Settings())
62
+
63
+ async def run() -> int:
64
+ async with (
65
+ app.router.lifespan_context(app),
66
+ app.state.sm.db.session_factory() as session,
67
+ ):
68
+ store = SettingsStore(SettingService(session))
69
+ n = await import_from_env_impl(app, store)
70
+ await session.commit()
71
+ print(f"Imported {n} override(s) from environment.")
72
+ return 0
73
+
74
+ return asyncio.run(run())
75
+
76
+
77
+ if __name__ == "__main__":
78
+ sys.exit(main())
@@ -0,0 +1,115 @@
1
+ """Centralized constants for the Settings module.
2
+
3
+ Keeps module-level strings (route prefixes, permission ids, table names,
4
+ menu metadata, error messages, field limits, env prefix, i18n namespace,
5
+ scope identifiers) in one place so nothing is duplicated in Python or
6
+ inline-literal'd at call sites.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Final
12
+
13
+ # ── Module identity ──────────────────────────────────────────────────
14
+ MODULE_NAME: Final = "Settings"
15
+ MODULE_PACKAGE: Final = "settings"
16
+ ENV_PREFIX: Final = "SM_SETTINGS_"
17
+ LOCALE_NAMESPACE: Final = MODULE_PACKAGE
18
+
19
+ # ── 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"
24
+ 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
+
45
+ # ── Routing ──────────────────────────────────────────────────────────
46
+ API_PREFIX: Final = "/api/settings"
47
+ VIEW_PREFIX: Final = "/settings"
48
+ VIEW_CREATE_PATH: Final = "/create"
49
+ VIEW_EDIT_PATH: Final = "/{setting_id}/edit"
50
+ VIEW_MODULES_PATH: Final = "/modules"
51
+ API_BY_ID_PATH: Final = "/{setting_id}"
52
+ API_BY_KEY_PATH: Final = "/by-key/{key}"
53
+ API_RESOLVE_PATH: Final = "/resolve/{key}"
54
+ API_SYSTEM_PATH: Final = "/system/{key}"
55
+ API_TENANT_PATH: Final = "/tenant/{scope_id}/{key}"
56
+ API_USER_PATH: Final = "/user/{scope_id}/{key}"
57
+
58
+ # ── Menu ─────────────────────────────────────────────────────────────
59
+ MENU_LABEL: Final = MODULE_NAME
60
+ MENU_URL: Final = VIEW_PREFIX
61
+ MENU_ICON: Final = "settings"
62
+ MENU_ORDER: Final = 30
63
+
64
+ # ── Permissions ──────────────────────────────────────────────────────
65
+ PERM_GROUP: Final = MODULE_NAME
66
+ PERM_VIEW: Final = "settings.view"
67
+ PERM_CREATE: Final = "settings.create"
68
+ PERM_EDIT: Final = "settings.edit"
69
+ PERM_DELETE: Final = "settings.delete"
70
+ ALL_PERMISSIONS: Final = (PERM_VIEW, PERM_CREATE, PERM_EDIT, PERM_DELETE)
71
+
72
+ # ── Database ─────────────────────────────────────────────────────────
73
+ DB_SCHEMA: Final = MODULE_PACKAGE
74
+ TABLE_SETTING: Final = "settings_setting"
75
+ UQ_SCOPE_KEY: Final = "uq_settings_setting_scope_scope_id_key"
76
+
77
+ # ── Field limits ─────────────────────────────────────────────────────
78
+ KEY_MAX_LENGTH: Final = 200
79
+ VALUE_MAX_LENGTH: Final = 4000
80
+ DESCRIPTION_MAX_LENGTH: Final = 2000
81
+ SCOPE_MAX_LENGTH: Final = 10
82
+ SCOPE_ID_MAX_LENGTH: Final = 255
83
+ VALUE_TYPE_MAX_LENGTH: Final = 10
84
+
85
+ # ── Inertia page component names ─────────────────────────────────────
86
+ PAGE_BROWSE: Final = f"{MODULE_NAME}/Browse"
87
+ PAGE_CREATE: Final = f"{MODULE_NAME}/Create"
88
+ PAGE_EDIT: Final = f"{MODULE_NAME}/Edit"
89
+ PAGE_MODULES_EDIT: Final = f"{MODULE_NAME}/ModulesEdit"
90
+
91
+ # ── Inertia prop keys ────────────────────────────────────────────────
92
+ PROP_SETTINGS: Final = "settings"
93
+ PROP_SETTING: Final = "setting"
94
+ PROP_MODULES: Final = "modules"
95
+ PROP_ERROR: Final = "error"
96
+
97
+ # ── User-facing error messages ───────────────────────────────────────
98
+ ERR_SETTING_NOT_FOUND: Final = "Setting not found"
99
+ ERR_KEY_ALREADY_EXISTS: Final = "Setting key already exists"
100
+ ERR_SYSTEM_SCOPE_NO_ID: Final = "system scope must not have a scope_id"
101
+ ERR_SCOPED_REQUIRES_ID: Final = "tenant/user scope requires a scope_id"
102
+ ERR_UNKNOWN_SCOPE: Final = "unknown scope"
103
+ ERR_VALUE_MISMATCH: Final = "value does not parse as declared value_type"
104
+
105
+ # ── HTTP ─────────────────────────────────────────────────────────────
106
+ STATUS_CREATED: Final = 201
107
+ STATUS_NO_CONTENT: Final = 204
108
+ STATUS_NOT_FOUND: Final = 404
109
+ STATUS_CONFLICT: Final = 409
110
+
111
+ # ── Query parameter names ────────────────────────────────────────────
112
+ QP_USER_ID: Final = "user_id"
113
+ QP_TENANT_ID: Final = "tenant_id"
114
+ QP_SCOPE: Final = "scope"
115
+ QP_SCOPE_ID: Final = "scope_id"
@@ -0,0 +1,24 @@
1
+ """Settings contracts — public interface for other modules."""
2
+
3
+ from settings.contracts.accessor import SettingsAccessor
4
+ from settings.contracts.registry import SettingDefinition, SettingsRegistry
5
+ from settings.contracts.schemas import (
6
+ SettingCreate,
7
+ SettingOut,
8
+ SettingScope,
9
+ SettingUpdate,
10
+ SettingUpsert,
11
+ SettingValueType,
12
+ )
13
+
14
+ __all__ = [
15
+ "SettingCreate",
16
+ "SettingDefinition",
17
+ "SettingOut",
18
+ "SettingScope",
19
+ "SettingUpdate",
20
+ "SettingUpsert",
21
+ "SettingValueType",
22
+ "SettingsAccessor",
23
+ "SettingsRegistry",
24
+ ]