simple-module-hosting 0.0.36__tar.gz → 0.0.37__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.36 → simple_module_hosting-0.0.37}/.gitignore +1 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/PKG-INFO +4 -4
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/pyproject.toml +4 -4
- simple_module_hosting-0.0.37/simple_module_hosting/_anonymous.py +27 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_error_handlers.py +1 -1
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_hydrate_step.py +1 -1
- simple_module_hosting-0.0.37/simple_module_hosting/_i18n_target.py +40 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_module_routes.py +23 -5
- simple_module_hosting-0.0.37/simple_module_hosting/_openapi.py +98 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_tenant.py +38 -12
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/app_builder.py +25 -5
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/host_cli.py +41 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/host_settings.py +45 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/i18n_manifest.py +26 -18
- simple_module_hosting-0.0.37/simple_module_hosting/i18n_routes.py +52 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/inertia_deps.py +6 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/locales/en.json +3 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/locales/es.json +3 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/maintenance.py +2 -2
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/middleware.py +29 -16
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/permissions.py +90 -8
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/routes.py +2 -2
- simple_module_hosting-0.0.37/simple_module_hosting/token_guard.py +217 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/ui_locales/en.json +30 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/ui_locales/es.json +30 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_app.py +9 -10
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_error_page_shared_props.py +2 -2
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_error_status_coverage.py +10 -6
- simple_module_hosting-0.0.37/tests/test_hosting_openapi.py +158 -0
- simple_module_hosting-0.0.37/tests/test_i18n_manifest_published_target.py +74 -0
- simple_module_hosting-0.0.37/tests/test_i18n_routes.py +113 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_menu_providers_wiring.py +17 -1
- simple_module_hosting-0.0.37/tests/test_token_guard_effective_route.py +150 -0
- simple_module_hosting-0.0.37/tests/test_token_route_guard.py +205 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/LICENSE +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/README.md +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/__main__.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_body_limit.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_boot_checks.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_csp_dynamic.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_db_health.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_dev_boot.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_favicon.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_host_services.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_i18n_build.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_inertia_setup.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_lifespan.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_phase_helpers.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_preapp_config.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_project_root.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_rate_limit.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_registrations.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_request_guard_responses.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_route_guard.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_secret_key.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_settings_registration.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/assets.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/bootstrap_settings.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/csrf.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/manifest.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/page_globs.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/session.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_gate.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/__init__.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/components/ConnectionList.tsx +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/components/StepForm.tsx +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/migrate.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/pages/Wizard.tsx +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/setup_wizard/payloads.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/shared_props.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/static_files.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/tenancy.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_api_error_negotiation.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_auth_provider_setting.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_boot_drift_check.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_branding_head.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_check_migrations.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_check_migrations_expression_note.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_csp_module_sources.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_csp_providers.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_db_health_check.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_design_packs_wiring.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_diagnostics_state.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_favicon_default.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_grant_sources.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_home_url_default.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_host_cli.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_host_settings_fields.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_hosting_csrf.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_hosting_error_json_and_session_cookie.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_i18n_build_overrides.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_i18n_types_cover_installed_modules.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_i18n_ui_namespace.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_inertia_shared_providers.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_invalidation_wiring.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_lifespan_deferred_startup.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_lifespan_order.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_maintenance_mode.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_manifest.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_menu_urls_are_canonical.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_middleware_order.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_migration_listing.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_migration_multihead.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_preapp_config.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_precompressed_static.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_principal_permissions_claim.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_proxy_headers.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_redirects.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_request_guard_auth_parity.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_request_guard_body_limit.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_request_guard_body_limit_middleware_read.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_request_guard_body_limit_started.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_request_guard_rate_limit.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_response_compression.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_route_guard.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_secret_key_bootstrap.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_session_cookie_security.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_session_cookie_window.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_session_vary_dedupe.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_gate.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_gate_single_flight.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_migrate_error_redaction.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_public_routes.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_registry.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_setup_routes.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_sqlite_path_anchor.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_static_caching.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_strict_discovery_wiring.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_tenancy_api.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_tenant_middleware.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_tenant_resolver_diagnostic.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_tenant_scope_helpers.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_tenant_source.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_translator_dep.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tests/test_view_prefixes.py +0 -0
- {simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/tsconfig.json +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.37
|
|
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
|
|
@@ -26,9 +26,9 @@ Requires-Dist: fastapi>=0.115
|
|
|
26
26
|
Requires-Dist: httpx>=0.27
|
|
27
27
|
Requires-Dist: itsdangerous>=2.0
|
|
28
28
|
Requires-Dist: jinja2>=3.1
|
|
29
|
-
Requires-Dist: simple-module-core==0.0.
|
|
30
|
-
Requires-Dist: simple-module-db==0.0.
|
|
31
|
-
Requires-Dist: simple-module-inertia==0.0.
|
|
29
|
+
Requires-Dist: simple-module-core==0.0.37
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.37
|
|
31
|
+
Requires-Dist: simple-module-inertia==0.0.37
|
|
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.37"
|
|
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"
|
|
@@ -29,9 +29,9 @@ dependencies = [
|
|
|
29
29
|
# it directly — out-of-tree hosts have nothing else pulling it in.
|
|
30
30
|
"itsdangerous>=2.0",
|
|
31
31
|
"jinja2>=3.1",
|
|
32
|
-
"simple_module_core==0.0.
|
|
33
|
-
"simple_module_db==0.0.
|
|
34
|
-
"simple_module_inertia==0.0.
|
|
32
|
+
"simple_module_core==0.0.37",
|
|
33
|
+
"simple_module_db==0.0.37",
|
|
34
|
+
"simple_module_inertia==0.0.37",
|
|
35
35
|
"starlette>=0.44",
|
|
36
36
|
"tomlkit>=0.13",
|
|
37
37
|
"uvicorn[standard]>=0.34",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Demote a request's principal to the anonymous visitor, mid-request.
|
|
2
|
+
|
|
3
|
+
Used when a credential authenticated but must not act as its account here: an
|
|
4
|
+
API token on a public route (``token_guard``), or a token whose owner no
|
|
5
|
+
longer holds the permission it requires (``permissions``). "Anonymous" means
|
|
6
|
+
all of it — no user, no permissions, no audit identity, and the tenant an
|
|
7
|
+
anonymous request would have resolved to rather than the one the credential
|
|
8
|
+
pinned.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from simple_module_db.listeners import current_user_id
|
|
14
|
+
from starlette.requests import HTTPConnection
|
|
15
|
+
|
|
16
|
+
from simple_module_hosting._tenant import reresolve_tenant
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def demote_to_anonymous(request: HTTPConnection) -> None:
|
|
20
|
+
"""Drop the principal and re-run tenant resolution as anonymous."""
|
|
21
|
+
request.state.user = None
|
|
22
|
+
request.state.resolved_permissions = set()
|
|
23
|
+
current_user_id.set(None)
|
|
24
|
+
await reresolve_tenant(request)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__all__ = ["demote_to_anonymous"]
|
|
@@ -187,7 +187,7 @@ async def render_error_page(
|
|
|
187
187
|
inertia = Inertia(request, config)
|
|
188
188
|
# This does not go through get_inertia, so the share step has to be
|
|
189
189
|
# repeated here. Without it the error page renders raw translation keys
|
|
190
|
-
# (
|
|
190
|
+
# (ui.error_page.not_found_title) and loses auth/menus, so a signed-in
|
|
191
191
|
# user's 404 has no layout.
|
|
192
192
|
shared = getattr(request.state, "inertia_shared", None)
|
|
193
193
|
if shared:
|
{simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_hydrate_step.py
RENAMED
|
@@ -28,7 +28,7 @@ async def hydrate_all(app: FastAPI, store: Any) -> None:
|
|
|
28
28
|
|
|
29
29
|
for package, cls in settings_services.module_registry.items():
|
|
30
30
|
try:
|
|
31
|
-
hydrated = await hydrate_settings(cls, store, package)
|
|
31
|
+
hydrated = await hydrate_settings(cls, store, package, lenient=True)
|
|
32
32
|
except Exception:
|
|
33
33
|
logger.exception("Hydrating %s failed; falling back to defaults", package)
|
|
34
34
|
continue
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Where generated i18n key files are written: monorepo checkout or published npm package."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
RESOURCE_KEY_RE = re.compile(r"^\s+'([^']+)': '',$", re.MULTILINE)
|
|
9
|
+
|
|
10
|
+
_PUBLISHED_PKG = Path("node_modules") / "@simple-module-py" / "i18n" / "src"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def resolve_i18n_types_dir(project_root: Path) -> tuple[Path | None, bool]:
|
|
14
|
+
"""Where the generated key files go, and whether that is a published package.
|
|
15
|
+
|
|
16
|
+
The monorepo's ``packages/i18n/src`` wins. A scaffolded app has no such
|
|
17
|
+
checkout — ``@simple-module-py/i18n`` is an npm dependency — so walk up from
|
|
18
|
+
``project_root`` to the nearest ``node_modules`` holding it (npm workspaces
|
|
19
|
+
hoist it to the workspace root, above the host; a flat host installs into
|
|
20
|
+
``client_app/``). ``(None, False)`` when
|
|
21
|
+
neither exists, the normal shape of a wheel-installed prod app.
|
|
22
|
+
"""
|
|
23
|
+
local = project_root / "packages" / "i18n" / "src"
|
|
24
|
+
if local.is_dir():
|
|
25
|
+
return local, False
|
|
26
|
+
root = project_root.resolve()
|
|
27
|
+
# A flat host (``smpy new --flat``) keeps its npm install in client_app/.
|
|
28
|
+
for base in (root, root / "client_app", *root.parents):
|
|
29
|
+
published = base / _PUBLISHED_PKG
|
|
30
|
+
if published.is_dir():
|
|
31
|
+
return published, True
|
|
32
|
+
return None, False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def read_resource_keys(resources_path: Path) -> frozenset[str]:
|
|
36
|
+
"""Keys listed in an existing ``generated-resources.ts`` (empty if unreadable)."""
|
|
37
|
+
try:
|
|
38
|
+
return frozenset(RESOURCE_KEY_RE.findall(resources_path.read_text(encoding="utf-8")))
|
|
39
|
+
except OSError:
|
|
40
|
+
return frozenset()
|
|
@@ -6,9 +6,12 @@ responsibility — and to keep both files under the 300-line cap.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from fastapi import APIRouter, FastAPI
|
|
9
|
+
from fastapi import APIRouter, Depends, FastAPI
|
|
10
10
|
from fastapi.routing import APIRoute
|
|
11
11
|
|
|
12
|
+
from simple_module_hosting._openapi import make_unique_id_function
|
|
13
|
+
from simple_module_hosting.token_guard import page_route
|
|
14
|
+
|
|
12
15
|
__all__ = ["wire_module_routes"]
|
|
13
16
|
|
|
14
17
|
|
|
@@ -23,13 +26,26 @@ def wire_module_routes(app: FastAPI, module) -> bool:
|
|
|
23
26
|
in ``simple_module_test`` stay in lockstep if ``ModuleBase`` ever gains
|
|
24
27
|
a new router type.
|
|
25
28
|
"""
|
|
26
|
-
api_router = APIRouter(
|
|
27
|
-
|
|
29
|
+
api_router = APIRouter(
|
|
30
|
+
prefix=module.meta.route_prefix,
|
|
31
|
+
tags=[module.meta.name],
|
|
32
|
+
generate_unique_id_function=make_unique_id_function(),
|
|
33
|
+
)
|
|
34
|
+
# Views are Inertia pages, not part of the published JSON API (#456), and
|
|
35
|
+
# carry the page marker: an API token is refused on every page, gated or
|
|
36
|
+
# not (``token_guard``).
|
|
37
|
+
pages = [Depends(page_route)]
|
|
38
|
+
view_router = APIRouter(
|
|
39
|
+
prefix=module.meta.view_prefix,
|
|
40
|
+
tags=[f"{module.meta.name} Views"],
|
|
41
|
+
include_in_schema=False,
|
|
42
|
+
dependencies=pages,
|
|
43
|
+
)
|
|
28
44
|
module.register_routes(api_router, view_router)
|
|
29
45
|
_clone_bare_prefix_route(view_router, module.meta.view_prefix)
|
|
30
46
|
has_views = bool(view_router.routes)
|
|
31
47
|
app.include_router(api_router)
|
|
32
|
-
app.include_router(view_router)
|
|
48
|
+
app.include_router(view_router, include_in_schema=False)
|
|
33
49
|
|
|
34
50
|
# Second view router for modules that serve both public and admin pages
|
|
35
51
|
# and therefore cannot express both under a single view_prefix — see
|
|
@@ -39,10 +55,12 @@ def wire_module_routes(app: FastAPI, module) -> bool:
|
|
|
39
55
|
admin_router = APIRouter(
|
|
40
56
|
prefix=admin_prefix,
|
|
41
57
|
tags=[f"{module.meta.name} Admin Views"],
|
|
58
|
+
include_in_schema=False,
|
|
59
|
+
dependencies=pages,
|
|
42
60
|
)
|
|
43
61
|
module.register_admin_routes(admin_router)
|
|
44
62
|
_clone_bare_prefix_route(admin_router, admin_prefix)
|
|
45
|
-
app.include_router(admin_router)
|
|
63
|
+
app.include_router(admin_router, include_in_schema=False)
|
|
46
64
|
return has_views
|
|
47
65
|
|
|
48
66
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""OpenAPI export plumbing (#456).
|
|
2
|
+
|
|
3
|
+
Three small pieces, kept apart from ``create_app``:
|
|
4
|
+
|
|
5
|
+
* ``make_unique_id_function`` — stable ``<module>_<function>`` operationIds, so
|
|
6
|
+
generated clients get method names like ``users_list_users`` instead of
|
|
7
|
+
``list_users_api_users__get``.
|
|
8
|
+
* ``install_openapi_filter`` — the published schema is the JSON API only. View
|
|
9
|
+
routes are already ``include_in_schema=False``; this also drops any other
|
|
10
|
+
non-``/api/`` path (host routes, health) so a client generator never sees them.
|
|
11
|
+
* ``API_OPENAPI_URL`` / ``openapi_served`` — where and when it is served.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import re
|
|
18
|
+
import warnings
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
|
|
21
|
+
from fastapi import FastAPI
|
|
22
|
+
from fastapi.routing import APIRoute
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"API_OPENAPI_URL",
|
|
26
|
+
"dedupe_operation_ids",
|
|
27
|
+
"install_openapi_filter",
|
|
28
|
+
"make_unique_id_function",
|
|
29
|
+
"openapi_served",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
API_OPENAPI_URL = "/api/openapi.json"
|
|
35
|
+
_API_PREFIX = "/api/"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def openapi_served(*, is_development: bool, openapi_public: bool) -> bool:
|
|
39
|
+
"""Always in development; elsewhere only when the operator opted in."""
|
|
40
|
+
return is_development or openapi_public
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _snake(name: str) -> str:
|
|
44
|
+
spaced = re.sub(r"(?<=[a-z0-9])(?=[A-Z])", "_", name)
|
|
45
|
+
return re.sub(r"[^0-9a-zA-Z]+", "_", spaced).strip("_").lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_unique_id_function() -> Callable[[APIRoute], str]:
|
|
49
|
+
"""Build an operationId generator: ``<module>_<endpoint function name>``.
|
|
50
|
+
|
|
51
|
+
The module is the route's first tag (``wire_module_routes`` tags each API
|
|
52
|
+
router with the module name). Stateless on purpose: FastAPI calls it again
|
|
53
|
+
when a router is included, so any bookkeeping here would see every route
|
|
54
|
+
twice. Clashes are resolved on the finished schema by ``dedupe_operation_ids``.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def unique_id(route: APIRoute) -> str:
|
|
58
|
+
module = _snake(route.tags[0]) if route.tags and isinstance(route.tags[0], str) else ""
|
|
59
|
+
return f"{module}_{route.name}" if module else route.name
|
|
60
|
+
|
|
61
|
+
return unique_id
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def dedupe_operation_ids(schema: dict) -> None:
|
|
65
|
+
"""Make every ``operationId`` unique, appending method and path on a clash."""
|
|
66
|
+
seen: set[str] = set()
|
|
67
|
+
for path, item in schema.get("paths", {}).items():
|
|
68
|
+
for method, op in item.items():
|
|
69
|
+
if not isinstance(op, dict) or "operationId" not in op:
|
|
70
|
+
continue
|
|
71
|
+
base = op["operationId"]
|
|
72
|
+
if base in seen:
|
|
73
|
+
op["operationId"] = f"{base}_{method}_{_snake(path)}"
|
|
74
|
+
logger.warning(
|
|
75
|
+
"Duplicate OpenAPI operationId %r; using %r", base, op["operationId"]
|
|
76
|
+
)
|
|
77
|
+
seen.add(op["operationId"])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def install_openapi_filter(app: FastAPI) -> None:
|
|
81
|
+
"""Make ``app.openapi()`` list only ``/api/`` paths."""
|
|
82
|
+
original = app.openapi
|
|
83
|
+
|
|
84
|
+
def openapi() -> dict:
|
|
85
|
+
if app.openapi_schema is None:
|
|
86
|
+
# FastAPI warns about duplicate ids as it builds; they are resolved
|
|
87
|
+
# (and logged once) by dedupe_operation_ids below.
|
|
88
|
+
with warnings.catch_warnings():
|
|
89
|
+
warnings.filterwarnings("ignore", message="Duplicate Operation ID")
|
|
90
|
+
schema = original()
|
|
91
|
+
schema["paths"] = {
|
|
92
|
+
p: v for p, v in schema.get("paths", {}).items() if p.startswith(_API_PREFIX)
|
|
93
|
+
}
|
|
94
|
+
dedupe_operation_ids(schema)
|
|
95
|
+
assert app.openapi_schema is not None
|
|
96
|
+
return app.openapi_schema
|
|
97
|
+
|
|
98
|
+
app.openapi = openapi # type: ignore[method-assign]
|
{simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/_tenant.py
RENAMED
|
@@ -17,6 +17,8 @@ TENANT_HEADER = "X-Tenant-ID"
|
|
|
17
17
|
|
|
18
18
|
_CREDENTIAL_VARY = ("Cookie", "Authorization")
|
|
19
19
|
|
|
20
|
+
_STATE_MIDDLEWARE = "_sm_tenant_middleware"
|
|
21
|
+
|
|
20
22
|
|
|
21
23
|
@dataclass(frozen=True, slots=True)
|
|
22
24
|
class TenantResolution:
|
|
@@ -123,37 +125,41 @@ class TenantMiddleware:
|
|
|
123
125
|
return
|
|
124
126
|
|
|
125
127
|
request = Request(scope)
|
|
128
|
+
# Kept so a principal dropped later in the request can be re-resolved
|
|
129
|
+
# (``reresolve_tenant``) with this middleware's own configuration.
|
|
130
|
+
scope.setdefault("state", {})[_STATE_MIDDLEWARE] = self
|
|
126
131
|
resolution = await self._resolve(request, scope)
|
|
127
132
|
tenant_id = resolution.tenant_id
|
|
128
133
|
request.state.tenant_id = tenant_id
|
|
129
134
|
request.state.tenant_source = resolution.source
|
|
130
135
|
request.state.tenant_vary = resolution.vary
|
|
131
136
|
|
|
132
|
-
send = self._vary_sender(send, resolution.vary)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
await self.app(scope, receive, send)
|
|
137
|
+
send = self._vary_sender(scope, send, resolution.vary)
|
|
138
|
+
# Always bound and reset, even for ``None``: a principal demoted later in
|
|
139
|
+
# the request re-resolves the tenant (``reresolve_tenant``) and may set
|
|
140
|
+
# one this request did not start with, which must not outlive it.
|
|
141
|
+
token = current_tenant_id.set(tenant_id)
|
|
142
|
+
try:
|
|
143
|
+
await self.app(scope, receive, send)
|
|
144
|
+
finally:
|
|
145
|
+
current_tenant_id.reset(token)
|
|
142
146
|
|
|
143
147
|
@staticmethod
|
|
144
|
-
def _vary_sender(send: Send, vary: tuple[str, ...]) -> Send:
|
|
148
|
+
def _vary_sender(scope: Scope, send: Send, vary: tuple[str, ...]) -> Send:
|
|
145
149
|
if not vary:
|
|
146
150
|
return send
|
|
147
151
|
|
|
148
152
|
async def wrapped(message: MutableMapping[str, Any]) -> None:
|
|
149
153
|
if message["type"] == "http.response.start":
|
|
154
|
+
# Re-read: a re-resolution (``reresolve_tenant``) may add names.
|
|
155
|
+
names = (scope.get("state") or {}).get("tenant_vary") or vary
|
|
150
156
|
headers = MutableHeaders(scope=message)
|
|
151
157
|
lines = headers.getlist("vary")
|
|
152
158
|
# Every line, not just the first (#423): MutableHeaders.get
|
|
153
159
|
# reads one, and assigning drops the rest — including a
|
|
154
160
|
# ``Vary: *`` on a later line.
|
|
155
161
|
if not any("*" in _tokens(line) for line in lines):
|
|
156
|
-
merged = merge_vary(", ".join(lines),
|
|
162
|
+
merged = merge_vary(", ".join(lines), tuple(names))
|
|
157
163
|
del headers["vary"]
|
|
158
164
|
headers["vary"] = merged
|
|
159
165
|
await send(message)
|
|
@@ -188,10 +194,30 @@ class TenantMiddleware:
|
|
|
188
194
|
return TenantResolution(None)
|
|
189
195
|
|
|
190
196
|
|
|
197
|
+
async def reresolve_tenant(request: Request | Any) -> None:
|
|
198
|
+
"""Resolve the tenant again for the request's *current* principal and rebind it.
|
|
199
|
+
|
|
200
|
+
For a principal dropped after ``TenantMiddleware`` ran: the tenant it
|
|
201
|
+
pinned must not outlive it. No-op without the middleware (single-tenant
|
|
202
|
+
hosts never bound one). The headers both answers depended on stay in
|
|
203
|
+
``Vary``.
|
|
204
|
+
"""
|
|
205
|
+
middleware = (request.scope.get("state") or {}).get(_STATE_MIDDLEWARE)
|
|
206
|
+
if not isinstance(middleware, TenantMiddleware):
|
|
207
|
+
return
|
|
208
|
+
resolution = await middleware._resolve(request, request.scope)
|
|
209
|
+
previous = tuple(getattr(request.state, "tenant_vary", None) or ())
|
|
210
|
+
request.state.tenant_id = resolution.tenant_id
|
|
211
|
+
request.state.tenant_source = resolution.source
|
|
212
|
+
request.state.tenant_vary = (*previous, *resolution.vary)
|
|
213
|
+
current_tenant_id.set(resolution.tenant_id)
|
|
214
|
+
|
|
215
|
+
|
|
191
216
|
__all__ = [
|
|
192
217
|
"TENANT_HEADER",
|
|
193
218
|
"TenantMiddleware",
|
|
194
219
|
"TenantResolution",
|
|
195
220
|
"TenantResolver",
|
|
196
221
|
"merge_vary",
|
|
222
|
+
"reresolve_tenant",
|
|
197
223
|
]
|
{simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/app_builder.py
RENAMED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
|
|
7
|
-
from fastapi import FastAPI
|
|
7
|
+
from fastapi import Depends, FastAPI
|
|
8
8
|
from simple_module_core import BodyLimitRegistry, CspSourceRegistry
|
|
9
9
|
from simple_module_core.audit_links import AuditLinkRegistry
|
|
10
10
|
from simple_module_core.design_packs import DesignPackRegistry
|
|
@@ -27,6 +27,12 @@ from simple_module_hosting._db_health import register_database_check
|
|
|
27
27
|
from simple_module_hosting._dev_boot import run_dev_boot
|
|
28
28
|
from simple_module_hosting._inertia_setup import setup_inertia
|
|
29
29
|
from simple_module_hosting._lifespan import build_lifespan
|
|
30
|
+
from simple_module_hosting._openapi import (
|
|
31
|
+
API_OPENAPI_URL,
|
|
32
|
+
install_openapi_filter,
|
|
33
|
+
make_unique_id_function,
|
|
34
|
+
openapi_served,
|
|
35
|
+
)
|
|
30
36
|
from simple_module_hosting._phase_helpers import (
|
|
31
37
|
attach_public_routes,
|
|
32
38
|
install_middleware,
|
|
@@ -44,16 +50,16 @@ from simple_module_hosting._settings_registration import (
|
|
|
44
50
|
)
|
|
45
51
|
from simple_module_hosting.health import router as health_router
|
|
46
52
|
from simple_module_hosting.i18n_manifest import build_i18n_registry
|
|
53
|
+
from simple_module_hosting.i18n_routes import router as i18n_router
|
|
47
54
|
from simple_module_hosting.settings import Settings
|
|
48
55
|
from simple_module_hosting.setup_gate import register_migration_step
|
|
49
56
|
from simple_module_hosting.setup_wizard import mount_setup_wizard
|
|
50
57
|
from simple_module_hosting.static_files import PrecompressedStaticFiles
|
|
51
58
|
from simple_module_hosting.tenancy import mode_for
|
|
59
|
+
from simple_module_hosting.token_guard import token_route_guard
|
|
52
60
|
|
|
53
61
|
logger = logging.getLogger(__name__)
|
|
54
62
|
|
|
55
|
-
_APP_TITLE = "SimpleModule"
|
|
56
|
-
_APP_VERSION = "0.1.0"
|
|
57
63
|
_DOCS_URL = "/api/docs"
|
|
58
64
|
_REDOC_URL = "/api/redoc"
|
|
59
65
|
_STATIC_MOUNT_PATH = "/static"
|
|
@@ -143,14 +149,27 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
143
149
|
|
|
144
150
|
lifespan = build_lifespan(modules)
|
|
145
151
|
|
|
152
|
+
serve_openapi = openapi_served(
|
|
153
|
+
is_development=settings.is_development, openapi_public=settings.openapi_public
|
|
154
|
+
)
|
|
146
155
|
app = FastAPI(
|
|
147
|
-
title=
|
|
148
|
-
version=
|
|
156
|
+
title=settings.api_title,
|
|
157
|
+
version=settings.api_version,
|
|
158
|
+
description=settings.api_description,
|
|
159
|
+
openapi_url=API_OPENAPI_URL if serve_openapi else None,
|
|
160
|
+
generate_unique_id_function=make_unique_id_function(),
|
|
149
161
|
docs_url=_DOCS_URL if settings.is_development else None,
|
|
150
162
|
redoc_url=_REDOC_URL if settings.is_development else None,
|
|
151
163
|
lifespan=lifespan,
|
|
164
|
+
# API tokens reach only permission-gated, non-page routes (or ones
|
|
165
|
+
# marked ``TokenAllowed``); see ``token_guard``.
|
|
166
|
+
dependencies=[Depends(token_route_guard)],
|
|
152
167
|
)
|
|
153
168
|
|
|
169
|
+
install_openapi_filter(app)
|
|
170
|
+
# Read by AuthMiddleware: the schema is anonymous exactly when it is served.
|
|
171
|
+
app.state.openapi_url = API_OPENAPI_URL if serve_openapi else None
|
|
172
|
+
|
|
154
173
|
# ── Phase 4: Module settings ───────────────────────────
|
|
155
174
|
for mod in modules:
|
|
156
175
|
mod.register_settings(app)
|
|
@@ -240,6 +259,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
|
|
240
259
|
view_prefixes[mod.meta.name] = mod.meta.view_prefix
|
|
241
260
|
|
|
242
261
|
app.include_router(health_router)
|
|
262
|
+
app.include_router(i18n_router) # /i18n/set-locale — LocaleSwitcher posts here
|
|
243
263
|
mount_setup_wizard(app, setup_registry) # /setup — what SetupMiddleware redirects to
|
|
244
264
|
|
|
245
265
|
static_dir = _PROJECT_ROOT / "host" / _STATIC_DIR_NAME
|
{simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/host_cli.py
RENAMED
|
@@ -53,6 +53,47 @@ def gen_pages(
|
|
|
53
53
|
)
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
@app.command("gen-i18n")
|
|
57
|
+
def gen_i18n(
|
|
58
|
+
allow_removals: Annotated[
|
|
59
|
+
bool,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"--allow-removals",
|
|
62
|
+
help="Permit dropping key namespaces of modules not installed here.",
|
|
63
|
+
),
|
|
64
|
+
] = False,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Regenerate the typed i18n key files (keys.generated.ts) without booting the app.
|
|
67
|
+
|
|
68
|
+
Writes into packages/i18n/src in the monorepo, else into the installed
|
|
69
|
+
@simple-module-py/i18n package under node_modules. Fails loudly if neither
|
|
70
|
+
exists.
|
|
71
|
+
"""
|
|
72
|
+
from simple_module_hosting._project_root import resolve_project_root
|
|
73
|
+
from simple_module_hosting.i18n_manifest import (
|
|
74
|
+
NamespaceRemovalError,
|
|
75
|
+
emit_frontend_types_for_modules,
|
|
76
|
+
)
|
|
77
|
+
from simple_module_hosting.settings import Settings
|
|
78
|
+
|
|
79
|
+
logging.basicConfig(level=logging.WARNING, format="%(message)s")
|
|
80
|
+
logging.getLogger("simple_module_hosting.i18n_manifest").setLevel(logging.INFO)
|
|
81
|
+
settings = Settings()
|
|
82
|
+
modules = discover_modules(enabled=settings.modules_enabled, strict=not settings.is_development)
|
|
83
|
+
try:
|
|
84
|
+
emit_frontend_types_for_modules(
|
|
85
|
+
settings,
|
|
86
|
+
modules,
|
|
87
|
+
resolve_project_root(),
|
|
88
|
+
strict=True,
|
|
89
|
+
allow_removals=allow_removals,
|
|
90
|
+
)
|
|
91
|
+
except (NamespaceRemovalError, FileNotFoundError) as exc:
|
|
92
|
+
typer.echo(f"error: {exc}", err=True)
|
|
93
|
+
raise typer.Exit(code=1) from exc
|
|
94
|
+
typer.echo("i18n key files up to date")
|
|
95
|
+
|
|
96
|
+
|
|
56
97
|
@app.command("sync-js-deps")
|
|
57
98
|
def sync_js_deps(
|
|
58
99
|
host_client_app: Annotated[
|
{simple_module_hosting-0.0.36 → simple_module_hosting-0.0.37}/simple_module_hosting/host_settings.py
RENAMED
|
@@ -26,6 +26,42 @@ _RESTART_DB = {"requires_restart": True, "group": "Database"}
|
|
|
26
26
|
|
|
27
27
|
_RESTART_GUARD = {"requires_restart": True, "group": "Request guards"}
|
|
28
28
|
|
|
29
|
+
_RESTART_API = {"requires_restart": True, "group": "API"}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _default_api_version() -> str:
|
|
33
|
+
"""The host project's version, else ``0.0.0``.
|
|
34
|
+
|
|
35
|
+
This repo's host is the installed ``simple-module-host`` distribution. A
|
|
36
|
+
scaffolded host is ``package = false`` (never installed), so there is no
|
|
37
|
+
distribution to ask: read ``[project].version`` from its ``pyproject.toml``
|
|
38
|
+
under ``SM_PROJECT_ROOT`` (or the cwd) instead.
|
|
39
|
+
"""
|
|
40
|
+
import tomllib
|
|
41
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
42
|
+
|
|
43
|
+
from simple_module_core.dotenv import find_env_file
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
return version("simple-module-host")
|
|
47
|
+
except PackageNotFoundError:
|
|
48
|
+
pass
|
|
49
|
+
# Same project-root convention as .env (SM_PROJECT_ROOT, else a bounded walk
|
|
50
|
+
# up from the cwd), so a CLI run from host/ finds the same project as the app.
|
|
51
|
+
root = find_env_file().parent
|
|
52
|
+
for candidate in (root / "host" / "pyproject.toml", root / "pyproject.toml"):
|
|
53
|
+
try:
|
|
54
|
+
project = tomllib.loads(candidate.read_text(encoding="utf-8")).get("project")
|
|
55
|
+
except (OSError, ValueError): # missing/unreadable, bad UTF-8, bad TOML
|
|
56
|
+
continue
|
|
57
|
+
if not isinstance(project, dict):
|
|
58
|
+
continue
|
|
59
|
+
# The first file declaring a [project] is the host's: a dynamic version
|
|
60
|
+
# must not fall through to an unrelated (workspace-root) project's.
|
|
61
|
+
found = project.get("version")
|
|
62
|
+
return found if isinstance(found, str) and found else "0.0.0"
|
|
63
|
+
return "0.0.0"
|
|
64
|
+
|
|
29
65
|
|
|
30
66
|
class HostSettings(BaseSettings):
|
|
31
67
|
"""DB-backed host configuration — defaults live here, overrides in DB."""
|
|
@@ -113,6 +149,15 @@ class HostSettings(BaseSettings):
|
|
|
113
149
|
"""Opt-in per-IP limit for signed-in traffic, same syntax. Blank (default)
|
|
114
150
|
leaves authenticated requests unlimited."""
|
|
115
151
|
|
|
152
|
+
api_title: str = Field(default="SimpleModule", json_schema_extra=_RESTART_API)
|
|
153
|
+
"""``info.title`` of the OpenAPI document (built once at boot)."""
|
|
154
|
+
api_version: str = Field(default_factory=_default_api_version, json_schema_extra=_RESTART_API)
|
|
155
|
+
"""``info.version`` of the OpenAPI document; defaults to the host distribution's version."""
|
|
156
|
+
api_description: str = Field(default="", json_schema_extra=_RESTART_API)
|
|
157
|
+
openapi_public: bool = Field(default=False, json_schema_extra=_RESTART_API)
|
|
158
|
+
"""Serve ``/api/openapi.json`` outside development, anonymously. Off by default:
|
|
159
|
+
the schema lists every endpoint, so exposing it is an operator decision."""
|
|
160
|
+
|
|
116
161
|
db_pool_size: int = Field(default=10, json_schema_extra=_RESTART_DB)
|
|
117
162
|
db_max_overflow: int = Field(default=20, json_schema_extra=_RESTART_DB)
|
|
118
163
|
db_pool_pre_ping: bool = Field(default=True, json_schema_extra=_RESTART_DB)
|