simple-module-hosting 0.0.34__tar.gz → 0.0.35__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_hosting-0.0.34 → simple_module_hosting-0.0.35}/PKG-INFO +4 -4
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/pyproject.toml +5 -4
- simple_module_hosting-0.0.35/simple_module_hosting/_dev_boot.py +83 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_error_handlers.py +9 -13
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_inertia_setup.py +17 -74
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_phase_helpers.py +8 -4
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_registrations.py +2 -0
- simple_module_hosting-0.0.35/simple_module_hosting/_tenant.py +97 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/app_builder.py +16 -25
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/host_settings.py +16 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/inertia_deps.py +1 -1
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/manifest.py +4 -17
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/middleware.py +3 -57
- simple_module_hosting-0.0.35/simple_module_hosting/page_globs.py +52 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_csp_module_sources.py +1 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_diagnostics_state.py +2 -4
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_error_page_shared_props.py +13 -3
- simple_module_hosting-0.0.35/tests/test_invalidation_wiring.py +250 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_manifest.py +48 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_middleware_order.py +10 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_strict_discovery_wiring.py +1 -1
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_tenant_middleware.py +56 -3
- simple_module_hosting-0.0.35/tests/test_tenant_scope_helpers.py +99 -0
- simple_module_hosting-0.0.34/simple_module_hosting/_inertia_json.py +0 -86
- simple_module_hosting-0.0.34/simple_module_hosting/_inertia_url.py +0 -131
- simple_module_hosting-0.0.34/tests/test_inertia_json_encoder.py +0 -83
- simple_module_hosting-0.0.34/tests/test_inertia_manifest.py +0 -71
- simple_module_hosting-0.0.34/tests/test_inertia_relative_url.py +0 -120
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/.gitignore +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/LICENSE +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/README.md +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/__main__.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_db_health.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_favicon.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_host_services.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_hydrate_step.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_lifespan.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_module_routes.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_preapp_config.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/_secret_key.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/assets.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/bootstrap_settings.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/csrf.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/host_cli.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/maintenance.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/permissions.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/session.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/setup_gate.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/shared_props.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/static_files.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_api_error_negotiation.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_app.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_auth_provider_setting.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_boot_drift_check.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_branding_head.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_check_migrations.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_db_health_check.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_design_packs_wiring.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_error_status_coverage.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_favicon_default.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_host_cli.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_host_settings_fields.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_hosting_csrf.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_i18n_types_cover_installed_modules.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_inertia_shared_providers.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_lifespan_order.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_maintenance_mode.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_menu_urls_are_canonical.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_migration_listing.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_migration_multihead.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_preapp_config.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_precompressed_static.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_proxy_headers.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_redirects.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_response_compression.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_secret_key_bootstrap.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_session_cookie_security.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_session_cookie_window.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_setup_gate.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_setup_password_policy.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_setup_public_routes.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_setup_registry.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_setup_routes.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_sqlite_path_anchor.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_static_caching.py +0 -0
- {simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/tests/test_translator_dep.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simple_module_hosting
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.35
|
|
4
4
|
Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
|
|
5
5
|
Project-URL: Homepage, https://github.com/antosubash/simple_module_python
|
|
6
6
|
Project-URL: Repository, https://github.com/antosubash/simple_module_python
|
|
@@ -22,13 +22,13 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
22
22
|
Classifier: Typing :: Typed
|
|
23
23
|
Requires-Python: >=3.12
|
|
24
24
|
Requires-Dist: click>=8.1
|
|
25
|
-
Requires-Dist: fastapi-inertia>=1.0
|
|
26
25
|
Requires-Dist: fastapi>=0.115
|
|
27
26
|
Requires-Dist: httpx>=0.27
|
|
28
27
|
Requires-Dist: itsdangerous>=2.0
|
|
29
28
|
Requires-Dist: jinja2>=3.1
|
|
30
|
-
Requires-Dist: simple-module-core==0.0.
|
|
31
|
-
Requires-Dist: simple-module-db==0.0.
|
|
29
|
+
Requires-Dist: simple-module-core==0.0.35
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.35
|
|
31
|
+
Requires-Dist: simple-module-inertia==0.0.35
|
|
32
32
|
Requires-Dist: starlette>=0.44
|
|
33
33
|
Requires-Dist: tomlkit>=0.13
|
|
34
34
|
Requires-Dist: uvicorn[standard]>=0.34
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_hosting"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.35"
|
|
4
4
|
description = "FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "MIT"
|
|
@@ -23,15 +23,15 @@ classifiers = [
|
|
|
23
23
|
dependencies = [
|
|
24
24
|
"click>=8.1",
|
|
25
25
|
"fastapi>=0.115",
|
|
26
|
-
"fastapi-inertia>=1.0",
|
|
27
26
|
"httpx>=0.27",
|
|
28
27
|
# SessionMiddleware (starlette.middleware.sessions) imports itsdangerous at
|
|
29
28
|
# module import time; starlette treats it as an optional extra, so declare
|
|
30
29
|
# it directly — out-of-tree hosts have nothing else pulling it in.
|
|
31
30
|
"itsdangerous>=2.0",
|
|
32
31
|
"jinja2>=3.1",
|
|
33
|
-
"simple_module_core==0.0.
|
|
34
|
-
"simple_module_db==0.0.
|
|
32
|
+
"simple_module_core==0.0.35",
|
|
33
|
+
"simple_module_db==0.0.35",
|
|
34
|
+
"simple_module_inertia==0.0.35",
|
|
35
35
|
"starlette>=0.44",
|
|
36
36
|
"tomlkit>=0.13",
|
|
37
37
|
"uvicorn[standard]>=0.34",
|
|
@@ -59,3 +59,4 @@ packages = ["simple_module_hosting"]
|
|
|
59
59
|
[tool.uv.sources]
|
|
60
60
|
simple_module_core = { workspace = true }
|
|
61
61
|
simple_module_db = { workspace = true }
|
|
62
|
+
simple_module_inertia = { workspace = true }
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Development-only boot work: the diagnostics run and the frontend artifacts.
|
|
2
|
+
|
|
3
|
+
Extracted from ``app_builder`` so that file reads as the phase sequence and
|
|
4
|
+
nothing else. Everything here is gated on ``settings.is_development`` at the one
|
|
5
|
+
call site, and everything here reads or writes the *source tree* — a deployed
|
|
6
|
+
wheel has no ``host/client_app`` to write a manifest into and no module sources
|
|
7
|
+
for the AST checks to walk, which is why none of it belongs on a production boot
|
|
8
|
+
path.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from simple_module_core.diagnostics import DiagnosticLevel, print_diagnostics, run_diagnostics
|
|
18
|
+
|
|
19
|
+
from simple_module_hosting.i18n_manifest import emit_frontend_types_for_modules
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from simple_module_core import ModuleBase
|
|
23
|
+
from simple_module_core.services import DiagnosticsState
|
|
24
|
+
|
|
25
|
+
from simple_module_hosting.settings import Settings
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
__all__ = ["run_dev_boot"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_dev_boot(
|
|
33
|
+
settings: Settings,
|
|
34
|
+
modules: list[ModuleBase],
|
|
35
|
+
installed_modules: list[ModuleBase],
|
|
36
|
+
*,
|
|
37
|
+
i18n_extra,
|
|
38
|
+
diagnostics_state: DiagnosticsState,
|
|
39
|
+
project_root: Path,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Run the module diagnostics and emit the frontend's generated files.
|
|
42
|
+
|
|
43
|
+
``diagnostics_state.runner`` is left behind on purpose, not just used: the
|
|
44
|
+
in-app Doctor screen re-invokes this exact call through ``rerun()``, and
|
|
45
|
+
re-deriving the arguments from a request would mean re-discovering modules.
|
|
46
|
+
|
|
47
|
+
An ERROR-level diagnostic ends the process. A developer whose module has a
|
|
48
|
+
duplicate name or a framework→plugin import gets one clear message here
|
|
49
|
+
instead of an inexplicable failure several phases later — and the same
|
|
50
|
+
checks run at production boot, where the app would refuse to start anyway.
|
|
51
|
+
"""
|
|
52
|
+
diagnostics_state.runner = lambda: run_diagnostics(
|
|
53
|
+
modules,
|
|
54
|
+
i18n_supported_locales=settings.i18n_supported_locales,
|
|
55
|
+
i18n_default_locale=settings.i18n_default_locale,
|
|
56
|
+
i18n_extra_sources=i18n_extra,
|
|
57
|
+
)
|
|
58
|
+
diagnostics = diagnostics_state.rerun()
|
|
59
|
+
errors = [d for d in diagnostics if d.level == DiagnosticLevel.ERROR]
|
|
60
|
+
if diagnostics:
|
|
61
|
+
print_diagnostics(diagnostics)
|
|
62
|
+
if errors:
|
|
63
|
+
raise SystemExit(f"Module diagnostics: {len(errors)} error(s). Fix before continuing.")
|
|
64
|
+
|
|
65
|
+
_write_pages_manifest(modules, project_root)
|
|
66
|
+
emit_frontend_types_for_modules(settings, installed_modules, project_root)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _write_pages_manifest(modules: list[ModuleBase], project_root: Path) -> None:
|
|
70
|
+
"""Tell Vite where pages shipped inside pip-installed wheels live.
|
|
71
|
+
|
|
72
|
+
Swallows its own failure: a missing or unwritable ``client_app`` must not
|
|
73
|
+
stop the API from booting, and the symptom — pages the frontend cannot find
|
|
74
|
+
— is loud enough on its own. See ``scaffolding.py``.
|
|
75
|
+
"""
|
|
76
|
+
try:
|
|
77
|
+
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
78
|
+
|
|
79
|
+
client_app = project_root / "host" / "client_app"
|
|
80
|
+
if client_app.is_dir():
|
|
81
|
+
write_module_pages_manifest(modules, client_app)
|
|
82
|
+
except Exception:
|
|
83
|
+
logger.exception("Failed to write module pages manifest — frontend may miss pages")
|
|
@@ -9,19 +9,18 @@ from http import HTTPStatus
|
|
|
9
9
|
from fastapi.encoders import jsonable_encoder
|
|
10
10
|
from fastapi.exceptions import RequestValidationError
|
|
11
11
|
from fastapi.responses import JSONResponse
|
|
12
|
-
from
|
|
12
|
+
from simple_module_core.exceptions import NotFoundError
|
|
13
|
+
from simple_module_inertia import (
|
|
13
14
|
Inertia,
|
|
14
15
|
InertiaConfig,
|
|
15
16
|
InertiaVersionConflictException,
|
|
16
17
|
inertia_version_conflict_exception_handler,
|
|
17
18
|
)
|
|
18
|
-
from simple_module_core.exceptions import NotFoundError
|
|
19
19
|
from starlette.exceptions import HTTPException
|
|
20
20
|
from starlette.requests import Request
|
|
21
21
|
from starlette.responses import Response
|
|
22
22
|
|
|
23
23
|
from simple_module_hosting._inertia_shared import _INERTIA_HEADER
|
|
24
|
-
from simple_module_hosting._inertia_url import patch_relative_page_url
|
|
25
24
|
from simple_module_hosting.permissions import PERMISSION_DENIED_PREFIX
|
|
26
25
|
|
|
27
26
|
logger = logging.getLogger(__name__)
|
|
@@ -174,20 +173,17 @@ async def render_error_page(
|
|
|
174
173
|
# that raises while reporting an error leaves the caller with nothing.
|
|
175
174
|
config: InertiaConfig = request.app.state.sm.inertia_config
|
|
176
175
|
# Prefer the app's configured dependency over constructing Inertia
|
|
177
|
-
# directly
|
|
178
|
-
#
|
|
179
|
-
#
|
|
180
|
-
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# through the same url-relativizing patch directly: a fallback that
|
|
184
|
-
# skipped it would reintroduce the exact bug this module exists to
|
|
185
|
-
# fix, just for the one request that hit it before setup finished.
|
|
176
|
+
# directly, so the error page is built the same way as every other
|
|
177
|
+
# page. The raw construction is the fallback for a half-built app —
|
|
178
|
+
# the case this whole handler exists to survive. Both paths emit a
|
|
179
|
+
# root-relative page url: the adapter does that by construction now,
|
|
180
|
+
# so the fallback no longer needs a separate patch to avoid the
|
|
181
|
+
# cross-scheme `pushState` SecurityError behind a TLS proxy.
|
|
186
182
|
inertia_dep = getattr(request.app.state, "inertia_dependency", None)
|
|
187
183
|
if inertia_dep is not None:
|
|
188
184
|
inertia = inertia_dep(request, None)
|
|
189
185
|
else:
|
|
190
|
-
inertia =
|
|
186
|
+
inertia = Inertia(request, config)
|
|
191
187
|
# This does not go through get_inertia, so the share step has to be
|
|
192
188
|
# repeated here. Without it the error page renders raw translation keys
|
|
193
189
|
# (host.error.not_found_title) and loses auth/menus, so a signed-in
|
|
@@ -1,20 +1,15 @@
|
|
|
1
|
-
"""Configure
|
|
1
|
+
"""Configure simple_module_inertia with the Jinja2 template."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
5
|
import logging
|
|
8
|
-
import tempfile
|
|
9
6
|
from pathlib import Path
|
|
10
7
|
|
|
11
8
|
from fastapi import FastAPI
|
|
12
|
-
from
|
|
9
|
+
from simple_module_inertia import InertiaConfig, inertia_dependency_factory
|
|
13
10
|
from starlette.requests import Request
|
|
14
11
|
|
|
15
12
|
from simple_module_hosting._favicon import default_favicon_data_uri
|
|
16
|
-
from simple_module_hosting._inertia_json import json_safe_inertia_dependency
|
|
17
|
-
from simple_module_hosting._inertia_url import relative_page_url_dependency
|
|
18
13
|
from simple_module_hosting.settings import Settings
|
|
19
14
|
|
|
20
15
|
logger = logging.getLogger(__name__)
|
|
@@ -28,6 +23,12 @@ _ROOT_DIRECTORY = "."
|
|
|
28
23
|
# branding's own default so the unbranded title is identical everywhere.
|
|
29
24
|
_DEFAULT_APP_NAME = "SimpleModule"
|
|
30
25
|
|
|
26
|
+
# Built assets are served from the "/static" mount under "dist/", so production
|
|
27
|
+
# asset URLs are prefixed with "static/dist". The manifest is read where Vite
|
|
28
|
+
# writes it; the adapter looks the entry up by Vite's own key.
|
|
29
|
+
_ASSETS_PREFIX = "static/dist"
|
|
30
|
+
_VITE_MANIFEST_RELPATH = Path("static") / "dist" / ".vite" / "manifest.json"
|
|
31
|
+
|
|
31
32
|
|
|
32
33
|
def branding_head(request: Request) -> dict:
|
|
33
34
|
"""Branding metadata for the root template's ``<head>``.
|
|
@@ -61,59 +62,19 @@ def branding_head(request: Request) -> dict:
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
# Built assets are served from the "/static" mount under "dist/", so production
|
|
65
|
-
# asset URLs are prefixed with "static/dist".
|
|
66
|
-
_ASSETS_PREFIX = "static/dist"
|
|
67
|
-
_VITE_MANIFEST_RELPATH = Path("static") / "dist" / ".vite" / "manifest.json"
|
|
68
|
-
|
|
69
|
-
|
|
70
65
|
def _prod_manifest_path(project_root: Path) -> str:
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
fastapi-inertia looks the entry up by ``f"{root_directory}/{entrypoint}"``
|
|
74
|
-
(here ``"./main.tsx"``), but Vite keys its manifest by the entry's path
|
|
75
|
-
relative to the Vite root (``"main.tsx"``) — so the raw Vite manifest would
|
|
76
|
-
``KeyError``. Read it, re-key the ``isEntry`` chunk under the key
|
|
77
|
-
fastapi-inertia expects, and write the normalized copy beside the build
|
|
78
|
-
output (falling back to a temp file if that dir is read-only). Returns ``""``
|
|
79
|
-
when no built manifest exists, leaving production assets unconfigured rather
|
|
80
|
-
than crashing at import time.
|
|
81
|
-
"""
|
|
66
|
+
"""The Vite manifest to read in production, or ``""`` when none is built."""
|
|
82
67
|
candidates = [
|
|
83
68
|
project_root / "host" / _VITE_MANIFEST_RELPATH,
|
|
84
69
|
project_root / _VITE_MANIFEST_RELPATH,
|
|
85
70
|
]
|
|
86
|
-
|
|
87
|
-
if
|
|
71
|
+
found = next((p for p in candidates if p.is_file()), None)
|
|
72
|
+
if found is None:
|
|
88
73
|
logger.warning(
|
|
89
74
|
"Production Vite manifest not found (looked in %s)", [str(c) for c in candidates]
|
|
90
75
|
)
|
|
91
76
|
return ""
|
|
92
|
-
|
|
93
|
-
data = json.loads(vite_manifest.read_text())
|
|
94
|
-
expected_key = f"{_ROOT_DIRECTORY}/{_ENTRYPOINT_FILENAME}"
|
|
95
|
-
if expected_key not in data:
|
|
96
|
-
entry = next((v for v in data.values() if v.get("isEntry")), None)
|
|
97
|
-
if entry is None:
|
|
98
|
-
# No entry to re-key: degrade gracefully (same as no-manifest)
|
|
99
|
-
# rather than returning a path that KeyErrors at render time.
|
|
100
|
-
logger.warning("No isEntry chunk in Vite manifest %s", vite_manifest)
|
|
101
|
-
return ""
|
|
102
|
-
data = {**data, expected_key: entry}
|
|
103
|
-
out = vite_manifest.parent / "inertia-manifest.json"
|
|
104
|
-
try:
|
|
105
|
-
out.write_text(json.dumps(data))
|
|
106
|
-
except OSError:
|
|
107
|
-
# Build dir read-only (e.g. immutable container layer): fall back to
|
|
108
|
-
# a temp file keyed by the source manifest path so multiple apps on
|
|
109
|
-
# one host don't clobber each other's normalized manifests.
|
|
110
|
-
digest = hashlib.sha1(str(vite_manifest).encode()).hexdigest()[:12]
|
|
111
|
-
out = Path(tempfile.gettempdir()) / f"sm-inertia-manifest-{digest}.json"
|
|
112
|
-
out.write_text(json.dumps(data))
|
|
113
|
-
return str(out)
|
|
114
|
-
except Exception:
|
|
115
|
-
logger.exception("Failed to prepare production Inertia manifest from %s", vite_manifest)
|
|
116
|
-
return ""
|
|
77
|
+
return str(found)
|
|
117
78
|
|
|
118
79
|
|
|
119
80
|
def setup_inertia(
|
|
@@ -122,7 +83,7 @@ def setup_inertia(
|
|
|
122
83
|
modules: list,
|
|
123
84
|
project_root: Path,
|
|
124
85
|
) -> InertiaConfig | None:
|
|
125
|
-
"""Configure
|
|
86
|
+
"""Configure the Inertia adapter and attach the dependency factory to app.state.
|
|
126
87
|
|
|
127
88
|
Two host layouts are supported: ``host/templates`` (the framework's
|
|
128
89
|
own host package) and ``templates`` at the project root (what
|
|
@@ -165,14 +126,9 @@ def setup_inertia(
|
|
|
165
126
|
# Expose branding metadata to the root template (pre-hydration head tags).
|
|
166
127
|
templates.env.globals["branding_head"] = branding_head
|
|
167
128
|
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
# as text/html, breaking module loading. Normalize:
|
|
172
|
-
# * `development`/`testing` → keep the dev-server path (Vite serves
|
|
173
|
-
# /main.tsx directly).
|
|
174
|
-
# * Anything else (staging, production, qa, ...) → use the production
|
|
175
|
-
# manifest so built assets are referenced.
|
|
129
|
+
# The adapter only switches to the asset manifest when environment equals
|
|
130
|
+
# the literal "production"; normalise anything non-dev (staging, qa, ...)
|
|
131
|
+
# to that so built assets are referenced rather than a Vite dev url.
|
|
176
132
|
from simple_module_core.environments import NON_PROD_ENVIRONMENTS
|
|
177
133
|
|
|
178
134
|
use_dev_server = settings.environment in NON_PROD_ENVIRONMENTS
|
|
@@ -182,8 +138,6 @@ def setup_inertia(
|
|
|
182
138
|
environment=inertia_environment,
|
|
183
139
|
version=_INERTIA_VERSION,
|
|
184
140
|
dev_url=settings.vite_dev_url if use_dev_server else "",
|
|
185
|
-
# Production reads built assets from the Vite manifest; dev serves them
|
|
186
|
-
# from the Vite dev server, so these only matter when not use_dev_server.
|
|
187
141
|
manifest_json_path="" if use_dev_server else _prod_manifest_path(project_root),
|
|
188
142
|
assets_prefix="" if use_dev_server else _ASSETS_PREFIX,
|
|
189
143
|
templates=templates,
|
|
@@ -192,16 +146,5 @@ def setup_inertia(
|
|
|
192
146
|
root_directory=_ROOT_DIRECTORY,
|
|
193
147
|
use_flash_errors=True,
|
|
194
148
|
)
|
|
195
|
-
|
|
196
|
-
# Two wraps over the stock dependency, each closing a gap in upstream:
|
|
197
|
-
# * JSON branch builds a Starlette JSONResponse directly, so the encoder
|
|
198
|
-
# configured above only ever applies to full page loads — wrap it so a
|
|
199
|
-
# client-side visit encodes the same props the same way.
|
|
200
|
-
# * The page url is absolute, which the browser rejects for pushState
|
|
201
|
-
# behind a TLS-terminating proxy — wrap it back to the root-relative
|
|
202
|
-
# path the Inertia protocol specifies.
|
|
203
|
-
inertia_dep = relative_page_url_dependency(
|
|
204
|
-
json_safe_inertia_dependency(inertia_dependency_factory(inertia_config))
|
|
205
|
-
)
|
|
206
|
-
app.state.inertia_dependency = inertia_dep
|
|
149
|
+
app.state.inertia_dependency = inertia_dependency_factory(inertia_config)
|
|
207
150
|
return inertia_config
|
|
@@ -14,13 +14,13 @@ from typing import TYPE_CHECKING
|
|
|
14
14
|
from fastapi import FastAPI
|
|
15
15
|
from fastapi.exceptions import RequestValidationError
|
|
16
16
|
from fastapi.staticfiles import StaticFiles
|
|
17
|
-
from inertia import (
|
|
18
|
-
InertiaVersionConflictException,
|
|
19
|
-
inertia_version_conflict_exception_handler,
|
|
20
|
-
)
|
|
21
17
|
from simple_module_core.diagnostics import Diagnostic, DiagnosticLevel
|
|
22
18
|
from simple_module_core.exceptions import NotFoundError
|
|
23
19
|
from simple_module_db import CommitBeforeResponseMiddleware
|
|
20
|
+
from simple_module_inertia import (
|
|
21
|
+
InertiaVersionConflictException,
|
|
22
|
+
inertia_version_conflict_exception_handler,
|
|
23
|
+
)
|
|
24
24
|
from starlette.exceptions import HTTPException
|
|
25
25
|
from starlette.middleware.gzip import GZipMiddleware
|
|
26
26
|
from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware
|
|
@@ -145,6 +145,10 @@ def install_middleware(
|
|
|
145
145
|
)
|
|
146
146
|
if settings.multi_tenant:
|
|
147
147
|
app.add_middleware(TenantMiddleware, header=settings.tenant_header or None)
|
|
148
|
+
elif settings.default_tenant:
|
|
149
|
+
# Single-tenant host with tenant-scoped tables: every request is the
|
|
150
|
+
# one tenant (#359). Never used with multi_tenant — no shared fallback.
|
|
151
|
+
app.add_middleware(TenantMiddleware, fixed=settings.default_tenant)
|
|
148
152
|
for mod in modules:
|
|
149
153
|
mod.register_middleware(app)
|
|
150
154
|
app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
|
|
@@ -19,6 +19,7 @@ def run_module_registrations(
|
|
|
19
19
|
*,
|
|
20
20
|
app: FastAPI,
|
|
21
21
|
event_bus,
|
|
22
|
+
invalidation_bus,
|
|
22
23
|
menu_registry,
|
|
23
24
|
perm_registry,
|
|
24
25
|
ff_registry,
|
|
@@ -42,6 +43,7 @@ def run_module_registrations(
|
|
|
42
43
|
mod.register_permissions(perm_registry)
|
|
43
44
|
mod.register_feature_flags(ff_registry)
|
|
44
45
|
dispatch_event_handlers(mod, event_bus, app)
|
|
46
|
+
mod.register_invalidations(invalidation_bus, app)
|
|
45
47
|
health_registry.set_owner(mod.meta.name)
|
|
46
48
|
mod.register_health_checks(health_registry)
|
|
47
49
|
mod.register_public_routes(public_route_registry)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Tenant-context middleware: which tenant a request acts for."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
|
|
7
|
+
from simple_module_db import current_tenant_id, is_valid_tenant_id
|
|
8
|
+
from starlette.datastructures import Headers
|
|
9
|
+
from starlette.requests import Request
|
|
10
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
11
|
+
|
|
12
|
+
_SCOPE_HTTP = "http"
|
|
13
|
+
|
|
14
|
+
TENANT_HEADER = "X-Tenant-ID"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
TenantResolver = Callable[[Request], Awaitable[str | None]]
|
|
18
|
+
"""Module-owned tenant resolution, registered as ``app.state.tenant_resolver``.
|
|
19
|
+
|
|
20
|
+
Returns the tenant the request acts for, or ``None``. It owns *every* source —
|
|
21
|
+
membership, session, subdomain, header — so it is also where each is
|
|
22
|
+
validated. Without one the middleware falls back to the principal's
|
|
23
|
+
``tenant_id`` claim."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TenantMiddleware:
|
|
27
|
+
"""Establish the request's tenant context.
|
|
28
|
+
|
|
29
|
+
Sets the ``current_tenant_id`` context var so that DB queries on
|
|
30
|
+
:class:`~simple_module_db.mixins.MultiTenantMixin` models are
|
|
31
|
+
automatically filtered, and new objects get ``tenant_id`` populated.
|
|
32
|
+
|
|
33
|
+
Also stores the resolved value on ``request.state.tenant_id``.
|
|
34
|
+
|
|
35
|
+
Resolution:
|
|
36
|
+
|
|
37
|
+
1. If a module registered ``app.state.tenant_resolver`` (the ``tenants``
|
|
38
|
+
module does), its answer is final — including ``None``.
|
|
39
|
+
2. Otherwise the authenticated user's ``tenant_id`` attribute (from the
|
|
40
|
+
auth token claims).
|
|
41
|
+
3. Otherwise, for **anonymous** requests only, the configured header —
|
|
42
|
+
useful for API clients and tests. Pass ``header=None`` (the default)
|
|
43
|
+
to disable it. An authenticated user is never allowed to pick a tenant
|
|
44
|
+
by header: a user with no tenant of their own would otherwise be able
|
|
45
|
+
to name anyone's.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self, app: ASGIApp, *, header: str | None = None, fixed: str | None = None
|
|
50
|
+
) -> None:
|
|
51
|
+
self.app = app
|
|
52
|
+
self.header = header
|
|
53
|
+
# Single-tenant hosts: one tenant for every request, nothing resolved.
|
|
54
|
+
self.fixed = fixed
|
|
55
|
+
|
|
56
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
57
|
+
if scope["type"] != _SCOPE_HTTP:
|
|
58
|
+
await self.app(scope, receive, send)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
request = Request(scope)
|
|
62
|
+
tenant_id = await self._resolve(request, scope)
|
|
63
|
+
request.state.tenant_id = tenant_id
|
|
64
|
+
|
|
65
|
+
if tenant_id is not None:
|
|
66
|
+
token = current_tenant_id.set(tenant_id)
|
|
67
|
+
try:
|
|
68
|
+
await self.app(scope, receive, send)
|
|
69
|
+
finally:
|
|
70
|
+
current_tenant_id.reset(token)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
await self.app(scope, receive, send)
|
|
74
|
+
|
|
75
|
+
async def _resolve(self, request: Request, scope: Scope) -> str | None:
|
|
76
|
+
if self.fixed is not None:
|
|
77
|
+
return self.fixed
|
|
78
|
+
app = scope.get("app")
|
|
79
|
+
resolver: TenantResolver | None = getattr(
|
|
80
|
+
getattr(app, "state", None), "tenant_resolver", None
|
|
81
|
+
)
|
|
82
|
+
if resolver is not None:
|
|
83
|
+
return await resolver(request)
|
|
84
|
+
|
|
85
|
+
user = getattr(request.state, "user", None)
|
|
86
|
+
if user is not None:
|
|
87
|
+
return getattr(user, "tenant_id", None)
|
|
88
|
+
|
|
89
|
+
if self.header:
|
|
90
|
+
value = Headers(scope=scope).get(self.header)
|
|
91
|
+
# Unvalidated, an over-long value is a 500 on the first stamped
|
|
92
|
+
# write (VARCHAR(50)) and any junk becomes a tenant name (#366).
|
|
93
|
+
return value if is_valid_tenant_id(value) else None
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["TENANT_HEADER", "TenantMiddleware", "TenantResolver"]
|
{simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/app_builder.py
RENAMED
|
@@ -10,11 +10,12 @@ from fastapi import FastAPI
|
|
|
10
10
|
from simple_module_core import CspSourceRegistry
|
|
11
11
|
from simple_module_core.audit_links import AuditLinkRegistry
|
|
12
12
|
from simple_module_core.design_packs import DesignPackRegistry
|
|
13
|
-
from simple_module_core.diagnostics import
|
|
13
|
+
from simple_module_core.diagnostics import print_diagnostics
|
|
14
14
|
from simple_module_core.discovery import discover_modules, select_auth_provider, topological_sort
|
|
15
15
|
from simple_module_core.events import EventBus
|
|
16
16
|
from simple_module_core.feature_flags import FeatureFlagRegistry
|
|
17
17
|
from simple_module_core.health import HealthRegistry
|
|
18
|
+
from simple_module_core.invalidation import InvalidationBus
|
|
18
19
|
from simple_module_core.menu import MenuRegistry
|
|
19
20
|
from simple_module_core.permissions import PermissionRegistry
|
|
20
21
|
from simple_module_core.public_routes import PublicRouteRegistry
|
|
@@ -24,6 +25,7 @@ from simple_module_db.listeners import register_listeners
|
|
|
24
25
|
from simple_module_db.session import init_db
|
|
25
26
|
|
|
26
27
|
from simple_module_hosting._db_health import register_database_check
|
|
28
|
+
from simple_module_hosting._dev_boot import run_dev_boot
|
|
27
29
|
from simple_module_hosting._inertia_setup import setup_inertia
|
|
28
30
|
from simple_module_hosting._lifespan import build_lifespan
|
|
29
31
|
from simple_module_hosting._phase_helpers import (
|
|
@@ -39,7 +41,7 @@ from simple_module_hosting._preapp_config import merge_host_settings
|
|
|
39
41
|
from simple_module_hosting._registrations import run_module_registrations
|
|
40
42
|
from simple_module_hosting._secret_key import assert_not_placeholder
|
|
41
43
|
from simple_module_hosting.health import router as health_router
|
|
42
|
-
from simple_module_hosting.i18n_manifest import build_i18n_registry
|
|
44
|
+
from simple_module_hosting.i18n_manifest import build_i18n_registry
|
|
43
45
|
from simple_module_hosting.settings import Settings
|
|
44
46
|
from simple_module_hosting.setup_gate import register_migration_step
|
|
45
47
|
from simple_module_hosting.static_files import PrecompressedStaticFiles
|
|
@@ -146,37 +148,21 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
146
148
|
# renders this exact result and its "Re-run checks" calls ``rerun()``.
|
|
147
149
|
diagnostics_state = DiagnosticsState()
|
|
148
150
|
if settings.is_development:
|
|
149
|
-
|
|
151
|
+
run_dev_boot(
|
|
152
|
+
settings,
|
|
150
153
|
modules,
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
+
installed_modules,
|
|
155
|
+
i18n_extra=i18n_extra,
|
|
156
|
+
diagnostics_state=diagnostics_state,
|
|
157
|
+
project_root=_PROJECT_ROOT,
|
|
154
158
|
)
|
|
155
|
-
diagnostics = diagnostics_state.rerun()
|
|
156
|
-
errors = [d for d in diagnostics if d.level == DiagnosticLevel.ERROR]
|
|
157
|
-
if diagnostics:
|
|
158
|
-
print_diagnostics(diagnostics)
|
|
159
|
-
if errors:
|
|
160
|
-
raise SystemExit(f"Module diagnostics: {len(errors)} error(s). Fix before continuing.")
|
|
161
|
-
|
|
162
|
-
# Emit frontend module-pages manifest so Vite can find pages shipped
|
|
163
|
-
# inside pip-installed module wheels. See scaffolding.py.
|
|
164
|
-
try:
|
|
165
|
-
from simple_module_hosting.manifest import write_module_pages_manifest
|
|
166
|
-
|
|
167
|
-
client_app = _PROJECT_ROOT / "host" / "client_app"
|
|
168
|
-
if client_app.is_dir():
|
|
169
|
-
write_module_pages_manifest(modules, client_app)
|
|
170
|
-
except Exception:
|
|
171
|
-
logger.exception("Failed to write module pages manifest — frontend may miss pages")
|
|
172
|
-
|
|
173
|
-
emit_frontend_types_for_modules(settings, installed_modules, _PROJECT_ROOT)
|
|
174
159
|
|
|
175
160
|
# ── Phase 3: Create FastAPI app ────────────────────────
|
|
176
161
|
menu_registry = MenuRegistry()
|
|
177
162
|
perm_registry = PermissionRegistry()
|
|
178
163
|
ff_registry = FeatureFlagRegistry()
|
|
179
164
|
event_bus = EventBus()
|
|
165
|
+
invalidation_bus = InvalidationBus()
|
|
180
166
|
health_registry = HealthRegistry()
|
|
181
167
|
public_route_registry = PublicRouteRegistry()
|
|
182
168
|
setup_registry = SetupRegistry()
|
|
@@ -211,6 +197,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
211
197
|
modules,
|
|
212
198
|
app=app,
|
|
213
199
|
event_bus=event_bus,
|
|
200
|
+
invalidation_bus=invalidation_bus,
|
|
214
201
|
menu_registry=menu_registry,
|
|
215
202
|
perm_registry=perm_registry,
|
|
216
203
|
ff_registry=ff_registry,
|
|
@@ -248,6 +235,9 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
248
235
|
pool_pre_ping=settings.db_pool_pre_ping,
|
|
249
236
|
pool_recycle=settings.db_pool_recycle,
|
|
250
237
|
)
|
|
238
|
+
# Multi-tenant installs fail closed: a tenant-scoped query with no tenant
|
|
239
|
+
# context raises instead of reading every tenant's rows.
|
|
240
|
+
db_state.tenant_strict = settings.multi_tenant
|
|
251
241
|
register_listeners(db_state)
|
|
252
242
|
# The host's own readiness signal, and the only probe-safe check in a
|
|
253
243
|
# default install — module checks reach third parties and are on-demand.
|
|
@@ -292,6 +282,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
292
282
|
i18n_registry=i18n_registry,
|
|
293
283
|
inertia_config=inertia_config,
|
|
294
284
|
modules=tuple(modules),
|
|
285
|
+
invalidation=invalidation_bus,
|
|
295
286
|
diagnostics=diagnostics_state,
|
|
296
287
|
)
|
|
297
288
|
|
{simple_module_hosting-0.0.34 → simple_module_hosting-0.0.35}/simple_module_hosting/host_settings.py
RENAMED
|
@@ -35,6 +35,12 @@ class HostSettings(BaseSettings):
|
|
|
35
35
|
|
|
36
36
|
multi_tenant: bool = False
|
|
37
37
|
tenant_header: str = ""
|
|
38
|
+
default_tenant: str = ""
|
|
39
|
+
"""Single-tenant hosts (``multi_tenant`` off): the tenant every request acts
|
|
40
|
+
as, so a module's ``MultiTenantMixin`` tables work without a tenant
|
|
41
|
+
resolver. Ignored when ``multi_tenant`` is on — a multi-tenant install
|
|
42
|
+
never falls back to a shared tenant. Jobs and CLI commands use
|
|
43
|
+
``tenant_context(settings.default_tenant)``."""
|
|
38
44
|
|
|
39
45
|
maintenance_mode: bool = False
|
|
40
46
|
"""Serve everyone but admins a 503 page.
|
|
@@ -123,6 +129,16 @@ class HostSettings(BaseSettings):
|
|
|
123
129
|
return None
|
|
124
130
|
return value.strip() or None
|
|
125
131
|
|
|
132
|
+
@field_validator("default_tenant", mode="after")
|
|
133
|
+
@classmethod
|
|
134
|
+
def _check_default_tenant(cls, value: str) -> str:
|
|
135
|
+
from simple_module_db import is_valid_tenant_id
|
|
136
|
+
|
|
137
|
+
value = value.strip()
|
|
138
|
+
if value and not is_valid_tenant_id(value):
|
|
139
|
+
raise ValueError(f"default_tenant {value!r} is not a valid tenant id")
|
|
140
|
+
return value
|
|
141
|
+
|
|
126
142
|
@model_validator(mode="after")
|
|
127
143
|
def _check_default_locale_supported(self) -> HostSettings:
|
|
128
144
|
if self.i18n_default_locale not in self.i18n_supported_locales:
|