simple-module-hosting 0.0.39__tar.gz → 0.0.40__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.39 → simple_module_hosting-0.0.40}/PKG-INFO +5 -5
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/pyproject.toml +5 -5
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_inertia_setup.py +9 -2
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_lifespan.py +44 -44
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/csrf.py +15 -2
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/session.py +22 -0
- simple_module_hosting-0.0.40/simple_module_hosting/setup_wizard/_migration_lock.py +53 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/migrate.py +4 -4
- simple_module_hosting-0.0.40/tests/test_hosting_asset_version.py +71 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_csrf.py +29 -0
- simple_module_hosting-0.0.40/tests/test_hosting_lifespan_cleanup.py +53 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_migrate_error_redaction.py +14 -2
- simple_module_hosting-0.0.40/tests/test_setup_migrate_worker_lock.py +74 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/.gitignore +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/LICENSE +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/README.md +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/__init__.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/__main__.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_anonymous.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_api_version.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_app_boot.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_body_limit.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_boot_checks.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_cors.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_csp_dynamic.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_db_health.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_deferred_startup.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_dev_boot.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_error_handlers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_error_negotiation.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_error_page.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_favicon.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_forwarded_host.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_host_services.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_host_settings_support.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_hydrate_step.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_i18n_build.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_i18n_target.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_inertia_shared.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_insecure_transport.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_lease_redis.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_middleware_pipeline.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_module_routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_observability.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_openapi.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_phase_helpers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_preapp_config.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_process_services.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_project_root.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_rate_limit.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_rate_limit_keys.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_rate_limit_store.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_registrations.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_request_guard_responses.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_route_access.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_route_guard.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_route_paths.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_secret_key.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_settings_registration.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_spa_mount.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_tenant.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/app_builder.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/assets.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/bootstrap_settings.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/db_options.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/health.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/host_cli.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/host_settings.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/i18n_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/i18n_middleware.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/i18n_routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/inertia_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/inertia_utils.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/leases.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/locales/de.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/locales/en.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/locales/es.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/log_sinks.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/logging.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/maintenance.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/manifest.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/middleware.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/migrations.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/page_globs.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/pagination.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/permissions.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/public_access.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/py.typed +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/rate_limit_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/redirects.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/resource_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/sentry.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/sentry_scrub.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/sentry_scrub_text.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/settings.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_gate.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/__init__.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/components/ConnectionList.tsx +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/components/StepForm.tsx +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/pages/Wizard.tsx +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/payloads.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/setup_wizard/routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/shared_props.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/static_files.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/tenancy.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/token_guard.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/ui_locales/de.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/ui_locales/en.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/ui_locales/es.json +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/_hosting_settings_db.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/_resacc_models.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/_sentry_transport.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/conftest.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_api_error_negotiation.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_app.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_auth_provider_setting.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_boot_config_hardening.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_boot_drift_check.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_branding_head.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_build_time_settings_marked.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_check_migrations.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_check_migrations_expression_note.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_csp_module_sources.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_csp_providers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_db_health_check.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_design_packs_wiring.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_diagnostics_state.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_error_page_shared_props.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_error_status_coverage.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_favicon_default.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_forwarded_host.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_grant_sources.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_health.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_home_url_default.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_host_cli.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_host_settings_fields.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_cors_api.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_cors_unset.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_csrf_shared_prop.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_error_handling_policy.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_error_json_and_session_cookie.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_lease_backends.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_lease_selection.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_log_sinks.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_openapi.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_openapi_security.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_pagination_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_permissions.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_public_access.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_sentry.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_sentry_scrub.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_hosting_sentry_scrub_gaps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_build_overrides.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_manifest.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_manifest_published_target.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_types_cover_installed_modules.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_i18n_ui_namespace.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_inertia_cache.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_inertia_i18n_shared_props.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_inertia_shared_providers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_insecure_transport_warning.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_invalidation_wiring.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_lifespan_deferred_startup.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_lifespan_order.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_locale_middleware.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_logging.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_maintenance_mode.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_manifest.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_menu_providers_wiring.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_menu_urls_are_canonical.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_merge_host_settings_strict.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_middleware_order.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_migration_listing.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_migration_multihead.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_native_link_prefixes_strip.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_preapp_config.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_preapp_invalid_values.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_preapp_read_failures.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_precompressed_static.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_principal_permissions_claim.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_proxy_headers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_rate_limit_rules_redis.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_rate_limit_rules_registry.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_rate_limit_store_shared.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_redirects.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_request_guard_auth_parity.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_request_guard_body_limit.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_request_guard_body_limit_middleware_read.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_request_guard_body_limit_started.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_request_guard_rate_limit.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_resource_deps.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_resource_policy_owned.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_resource_policy_parity.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_resource_types_wiring.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_response_compression.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_route_guard.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_route_guard_fastapi_contract.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_route_paths.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_secret_key_bootstrap.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_server_rendered_head.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_session_cookie_secure.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_session_cookie_security.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_session_cookie_window.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_session_vary_dedupe.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_settings_i18n.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_settings_secrets.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_gate.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_gate_single_flight.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_public_routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_registry.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_setup_routes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_spa_mounts_541.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_sqlite_path_anchor.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_static_caching.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_strict_discovery_wiring.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenancy_api.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenant_middleware.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenant_resolver_diagnostic.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenant_resolver_types.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenant_scope_helpers.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_tenant_source.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_token_guard_effective_route.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_token_route_guard.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_translator_dep.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests/test_view_prefixes.py +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/tests-js/hosting_SetupWizard.test.tsx +0 -0
- {simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/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.40
|
|
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<9,>=8.1
|
|
25
|
-
Requires-Dist: fastapi<0.
|
|
25
|
+
Requires-Dist: fastapi<0.159,>=0.140
|
|
26
26
|
Requires-Dist: httpx<0.29,>=0.27
|
|
27
27
|
Requires-Dist: itsdangerous<3,>=2.0
|
|
28
28
|
Requires-Dist: jinja2<4,>=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.40
|
|
30
|
+
Requires-Dist: simple-module-db==0.0.40
|
|
31
|
+
Requires-Dist: simple-module-inertia==0.0.40
|
|
32
32
|
Requires-Dist: starlette<2,>=0.44
|
|
33
33
|
Requires-Dist: tomlkit<0.16,>=0.13
|
|
34
34
|
Requires-Dist: uvicorn[standard]<0.55,>=0.34
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "simple_module_hosting"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.40"
|
|
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"
|
|
@@ -22,16 +22,16 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"click>=8.1,<9",
|
|
25
|
-
"fastapi>=0.140,<0.
|
|
25
|
+
"fastapi>=0.140,<0.159",
|
|
26
26
|
"httpx>=0.27,<0.29",
|
|
27
27
|
# SessionMiddleware (starlette.middleware.sessions) imports itsdangerous at
|
|
28
28
|
# module import time; starlette treats it as an optional extra, so declare
|
|
29
29
|
# it directly — out-of-tree hosts have nothing else pulling it in.
|
|
30
30
|
"itsdangerous>=2.0,<3",
|
|
31
31
|
"jinja2>=3.1,<4",
|
|
32
|
-
"simple_module_core==0.0.
|
|
33
|
-
"simple_module_db==0.0.
|
|
34
|
-
"simple_module_inertia==0.0.
|
|
32
|
+
"simple_module_core==0.0.40",
|
|
33
|
+
"simple_module_db==0.0.40",
|
|
34
|
+
"simple_module_inertia==0.0.40",
|
|
35
35
|
"starlette>=0.44,<2",
|
|
36
36
|
"tomlkit>=0.13,<0.16",
|
|
37
37
|
"uvicorn[standard]>=0.34,<0.55",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import logging
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -145,12 +146,18 @@ def setup_inertia(
|
|
|
145
146
|
|
|
146
147
|
use_dev_server = settings.environment in NON_PROD_ENVIRONMENTS
|
|
147
148
|
inertia_environment = "development" if use_dev_server else "production"
|
|
149
|
+
manifest_path = "" if use_dev_server else _prod_manifest_path(project_root)
|
|
150
|
+
version = (
|
|
151
|
+
hashlib.sha256(Path(manifest_path).read_bytes()).hexdigest()
|
|
152
|
+
if manifest_path
|
|
153
|
+
else _INERTIA_VERSION
|
|
154
|
+
)
|
|
148
155
|
|
|
149
156
|
inertia_config = InertiaConfig(
|
|
150
157
|
environment=inertia_environment,
|
|
151
|
-
version=
|
|
158
|
+
version=version,
|
|
152
159
|
dev_url=settings.vite_dev_url if use_dev_server else "",
|
|
153
|
-
manifest_json_path=
|
|
160
|
+
manifest_json_path=manifest_path,
|
|
154
161
|
assets_prefix="" if use_dev_server else _ASSETS_PREFIX,
|
|
155
162
|
templates=templates,
|
|
156
163
|
root_template_filename=_ROOT_TEMPLATE_FILENAME,
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_lifespan.py
RENAMED
|
@@ -15,7 +15,7 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
17
|
from collections.abc import AsyncGenerator, Callable, Sequence
|
|
18
|
-
from contextlib import asynccontextmanager
|
|
18
|
+
from contextlib import AsyncExitStack, asynccontextmanager
|
|
19
19
|
|
|
20
20
|
from fastapi import FastAPI
|
|
21
21
|
from simple_module_core.app_state import get_migration_status
|
|
@@ -97,49 +97,49 @@ def build_lifespan(modules: Sequence) -> Callable:
|
|
|
97
97
|
|
|
98
98
|
@asynccontextmanager
|
|
99
99
|
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await hydrate_settings_from_db(app)
|
|
116
|
-
|
|
117
|
-
# Behind head on a first run the tables a module's on_startup reads do
|
|
118
|
-
# not exist yet. Boot must still reach the wizard, so a hook that fails
|
|
119
|
-
# here is deferred and replayed once the wizard has applied the
|
|
120
|
-
# migrations (run_deferred_startup) rather than aborting the process.
|
|
121
|
-
app.state.deferred_startup = []
|
|
122
|
-
tolerate = not app.state.migration["is_current"]
|
|
123
|
-
for mod in modules:
|
|
124
|
-
try:
|
|
125
|
-
await mod.on_startup(app)
|
|
126
|
-
except Exception:
|
|
127
|
-
if not tolerate:
|
|
128
|
-
raise
|
|
129
|
-
logger.warning(
|
|
130
|
-
"on_startup of %s failed on an unmigrated database; "
|
|
131
|
-
"deferring it until the setup wizard has run the migrations",
|
|
132
|
-
mod.meta.name,
|
|
133
|
-
exc_info=True,
|
|
100
|
+
async with AsyncExitStack() as cleanup:
|
|
101
|
+
cleanup.push_async_callback(app.state.sm.db.engine.dispose)
|
|
102
|
+
# Report first, decide second. On a genuinely fresh install a
|
|
103
|
+
# behind-head database must still boot far enough to serve the wizard —
|
|
104
|
+
# running the migrations is one of the things that wizard does, and
|
|
105
|
+
# check_migrations' RuntimeError would make it unreachable. Anywhere
|
|
106
|
+
# else the original behaviour stands: a schema the code does not match
|
|
107
|
+
# fails the boot. See _is_first_run for why those two cases cannot be
|
|
108
|
+
# told apart by "is setup complete".
|
|
109
|
+
app.state.migration = await migration_status(app.state.sm.db.engine)
|
|
110
|
+
if not app.state.migration["is_current"] and not await _is_first_run(app):
|
|
111
|
+
raise RuntimeError(
|
|
112
|
+
f"Database is {app.state.migration['pending_count']} revision(s) behind "
|
|
113
|
+
f"(at {app.state.migration['current_revision']!r}, head is "
|
|
114
|
+
f"{app.state.migration['head_revision']!r}). Run: make migrate"
|
|
134
115
|
)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
116
|
+
|
|
117
|
+
await hydrate_settings_from_db(app)
|
|
118
|
+
|
|
119
|
+
# Behind head on a first run the tables a module's on_startup reads do
|
|
120
|
+
# not exist yet. Boot must still reach the wizard, so a hook that fails
|
|
121
|
+
# here is deferred and replayed once the wizard has applied the
|
|
122
|
+
# migrations (run_deferred_startup) rather than aborting the process.
|
|
123
|
+
app.state.deferred_startup = []
|
|
124
|
+
tolerate = not app.state.migration["is_current"]
|
|
125
|
+
for mod in modules:
|
|
126
|
+
try:
|
|
127
|
+
await mod.on_startup(app)
|
|
128
|
+
except Exception:
|
|
129
|
+
if not tolerate:
|
|
130
|
+
raise
|
|
131
|
+
logger.warning(
|
|
132
|
+
"on_startup of %s failed on an unmigrated database; "
|
|
133
|
+
"deferring it until the setup wizard has run the migrations",
|
|
134
|
+
mod.meta.name,
|
|
135
|
+
exc_info=True,
|
|
136
|
+
)
|
|
137
|
+
app.state.deferred_startup.append(mod)
|
|
138
|
+
cleanup.push_async_callback(mod.on_shutdown, app)
|
|
139
|
+
# Here rather than in create_app: the host includes its own routers
|
|
140
|
+
# after create_app returns, and those should be guarded too.
|
|
141
|
+
settings = getattr(app.state.sm, "settings", None)
|
|
142
|
+
guard_included_routers(app, warm=getattr(settings, "environment", None) != "testing")
|
|
143
|
+
yield
|
|
144
144
|
|
|
145
145
|
return lifespan
|
|
@@ -19,7 +19,9 @@ bind a token to.
|
|
|
19
19
|
|
|
20
20
|
This lifts the design the pagebuilder module shipped first
|
|
21
21
|
(``pagebuilder/security.py``) into the framework, so every module shares
|
|
22
|
-
one header name and one token-discovery convention.
|
|
22
|
+
one header name and one token-discovery convention. Sign-in clears the users
|
|
23
|
+
session, rotating its token; principal changes that retain the session must
|
|
24
|
+
call ``rotate_csrf_token`` before rendering the next page.
|
|
23
25
|
"""
|
|
24
26
|
|
|
25
27
|
from __future__ import annotations
|
|
@@ -28,7 +30,7 @@ import secrets
|
|
|
28
30
|
|
|
29
31
|
from fastapi import HTTPException, Request
|
|
30
32
|
|
|
31
|
-
__all__ = ["CSRF_HEADER", "RequiresCsrf", "get_csrf_token"]
|
|
33
|
+
__all__ = ["CSRF_HEADER", "RequiresCsrf", "get_csrf_token", "rotate_csrf_token"]
|
|
32
34
|
|
|
33
35
|
CSRF_HEADER = "X-CSRF-Token"
|
|
34
36
|
_SESSION_KEY = "sm_csrf_token"
|
|
@@ -61,6 +63,17 @@ def get_csrf_token(request: Request) -> str:
|
|
|
61
63
|
return token
|
|
62
64
|
|
|
63
65
|
|
|
66
|
+
def rotate_csrf_token(request: Request) -> None:
|
|
67
|
+
"""Discard the old token; the next page render mints a fresh one.
|
|
68
|
+
|
|
69
|
+
Use when changing the principal while deliberately retaining the session.
|
|
70
|
+
Clearing the whole session already discards the token. No session is a no-op.
|
|
71
|
+
"""
|
|
72
|
+
session = request.scope.get("session")
|
|
73
|
+
if session is not None:
|
|
74
|
+
session.pop(_SESSION_KEY, None)
|
|
75
|
+
|
|
76
|
+
|
|
64
77
|
class RequiresCsrf:
|
|
65
78
|
"""FastAPI dependency enforcing the CSRF header on unsafe methods.
|
|
66
79
|
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/session.py
RENAMED
|
@@ -72,6 +72,28 @@ which would sign the entire user base out on deploy — or accepting it forever,
|
|
|
72
72
|
which would leave the hole this key was added to close.
|
|
73
73
|
"""
|
|
74
74
|
|
|
75
|
+
_SIGN_IN_SURVIVORS_ATTR = "sign_in_preserved_session_keys"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def preserve_across_sign_in(app: Any, key: str) -> None:
|
|
79
|
+
"""Keep ``key`` when sign-in replaces the session (it is not user-bound state).
|
|
80
|
+
|
|
81
|
+
For gate state that belongs to the browser rather than the principal, such
|
|
82
|
+
as the site-lock unlock marker: clearing it at sign-in would re-prompt the
|
|
83
|
+
visitor who just passed the gate. Call from ``register_settings``.
|
|
84
|
+
"""
|
|
85
|
+
keys = getattr(app.state, _SIGN_IN_SURVIVORS_ATTR, None)
|
|
86
|
+
if keys is None:
|
|
87
|
+
keys = set()
|
|
88
|
+
setattr(app.state, _SIGN_IN_SURVIVORS_ATTR, keys)
|
|
89
|
+
keys.add(key)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def sign_in_preserved_keys(app: Any) -> frozenset[str]:
|
|
93
|
+
"""Keys this app registered through :func:`preserve_across_sign_in`."""
|
|
94
|
+
return frozenset(getattr(app.state, _SIGN_IN_SURVIVORS_ATTR, ()))
|
|
95
|
+
|
|
96
|
+
|
|
75
97
|
SESSION_COOKIE_NAME = "session"
|
|
76
98
|
"""The session cookie's name (Starlette's default, pinned so the OpenAPI export can cite it)."""
|
|
77
99
|
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Database advisory locks that also work before the first migration (#634)."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Generator
|
|
5
|
+
from contextlib import closing, contextmanager
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import create_engine
|
|
9
|
+
from sqlalchemy.engine import URL
|
|
10
|
+
from sqlalchemy.pool import NullPool
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
_POSTGRES_LOCK = 634539
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@contextmanager
|
|
18
|
+
def migration_lock(url: URL) -> Generator[None]:
|
|
19
|
+
"""Hold until the migration thread exits; no TTL or request cancellation gap."""
|
|
20
|
+
if url.get_backend_name() == "sqlite":
|
|
21
|
+
if not url.database or url.database == ":memory:":
|
|
22
|
+
# An in-memory database cannot be shared by different workers.
|
|
23
|
+
yield
|
|
24
|
+
return
|
|
25
|
+
import sqlite3
|
|
26
|
+
|
|
27
|
+
# A separate SQLite database supplies the advisory lock without locking
|
|
28
|
+
# the application database against Alembic's own connections.
|
|
29
|
+
path = str(Path(url.database).resolve()) + ".migrate-lock.db"
|
|
30
|
+
with closing(sqlite3.connect(path, timeout=300)) as connection:
|
|
31
|
+
try:
|
|
32
|
+
connection.execute("BEGIN EXCLUSIVE")
|
|
33
|
+
yield
|
|
34
|
+
finally:
|
|
35
|
+
connection.rollback()
|
|
36
|
+
return
|
|
37
|
+
if url.get_backend_name() != "postgresql":
|
|
38
|
+
logger.warning(
|
|
39
|
+
"No cross-worker migration lock for %s; relying on the per-process lock",
|
|
40
|
+
url.get_backend_name(),
|
|
41
|
+
)
|
|
42
|
+
yield
|
|
43
|
+
return
|
|
44
|
+
engine = create_engine(url.set(drivername="postgresql+psycopg2"), poolclass=NullPool)
|
|
45
|
+
try:
|
|
46
|
+
with engine.connect() as connection:
|
|
47
|
+
connection.exec_driver_sql("SELECT pg_advisory_lock(%s)", (_POSTGRES_LOCK,))
|
|
48
|
+
try:
|
|
49
|
+
yield
|
|
50
|
+
finally:
|
|
51
|
+
connection.exec_driver_sql("SELECT pg_advisory_unlock(%s)", (_POSTGRES_LOCK,))
|
|
52
|
+
finally:
|
|
53
|
+
engine.dispose()
|
|
@@ -16,12 +16,12 @@ from collections.abc import Generator
|
|
|
16
16
|
from fastapi import HTTPException, Request
|
|
17
17
|
|
|
18
18
|
from simple_module_hosting._deferred_startup import run_deferred_startup
|
|
19
|
+
from simple_module_hosting.setup_wizard._migration_lock import migration_lock
|
|
19
20
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# Serialized here; the second run then finds the schema at head and is a no-op.
|
|
23
|
+
# Alembic and logging have process-global state, even for different databases.
|
|
24
|
+
# Cross-worker serialization is provided by migration_lock inside the thread.
|
|
25
25
|
_MIGRATION_LOCK = asyncio.Lock()
|
|
26
26
|
|
|
27
27
|
|
|
@@ -68,7 +68,7 @@ async def apply_migrations(request: Request, _data: dict) -> dict:
|
|
|
68
68
|
# branch_labels, so the history legitimately has several heads and
|
|
69
69
|
# "head" raises CommandError("Multiple head revisions are present").
|
|
70
70
|
# This is what `make migrate` runs.
|
|
71
|
-
with _preserve_logging():
|
|
71
|
+
with migration_lock(request.app.state.sm.db.engine.url), _preserve_logging():
|
|
72
72
|
command.upgrade(AlembicConfig(ini_path), "heads")
|
|
73
73
|
|
|
74
74
|
try:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Hosting versions built assets by manifest content, so stale visits reload."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from simple_module_hosting._inertia_setup import setup_inertia
|
|
9
|
+
from simple_module_hosting.settings import Settings
|
|
10
|
+
from simple_module_inertia import (
|
|
11
|
+
InertiaVersionConflictException,
|
|
12
|
+
inertia_version_conflict_exception_handler,
|
|
13
|
+
)
|
|
14
|
+
from simple_module_inertia.config import resolved_version
|
|
15
|
+
from starlette.requests import Request
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _setup(root: Path, environment: str):
|
|
19
|
+
(root / "templates").mkdir(exist_ok=True)
|
|
20
|
+
app = FastAPI()
|
|
21
|
+
config = setup_inertia(app, Settings(environment=environment), [], root)
|
|
22
|
+
assert config is not None
|
|
23
|
+
return app, config
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.parametrize("layout", ["host", "."])
|
|
27
|
+
@pytest.mark.parametrize("environment", ["production", "staging"])
|
|
28
|
+
async def test_new_manifest_reloads_visits_from_previous_build(tmp_path, layout, environment):
|
|
29
|
+
manifest = tmp_path / layout / "static" / "dist" / ".vite" / "manifest.json"
|
|
30
|
+
manifest.parent.mkdir(parents=True)
|
|
31
|
+
manifest.write_text(json.dumps({"main.tsx": {"file": "assets/old.js"}}))
|
|
32
|
+
_, old_config = _setup(tmp_path, environment)
|
|
33
|
+
old_version = resolved_version(old_config)
|
|
34
|
+
_, same_config = _setup(tmp_path, environment)
|
|
35
|
+
assert resolved_version(same_config) == old_version
|
|
36
|
+
|
|
37
|
+
manifest.write_text(json.dumps({"main.tsx": {"file": "assets/new.js"}}))
|
|
38
|
+
app, new_config = _setup(tmp_path, environment)
|
|
39
|
+
new_version = resolved_version(new_config)
|
|
40
|
+
assert new_version != old_version
|
|
41
|
+
request = Request(
|
|
42
|
+
{
|
|
43
|
+
"type": "http",
|
|
44
|
+
"method": "GET",
|
|
45
|
+
"scheme": "http",
|
|
46
|
+
"server": ("test", 80),
|
|
47
|
+
"path": "/page",
|
|
48
|
+
"query_string": b"",
|
|
49
|
+
"headers": [(b"x-inertia", b"true"), (b"x-inertia-version", old_version.encode())],
|
|
50
|
+
"session": {},
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
with pytest.raises(InertiaVersionConflictException) as exc:
|
|
54
|
+
app.state.inertia_dependency(request)
|
|
55
|
+
stale = await inertia_version_conflict_exception_handler(request, exc.value)
|
|
56
|
+
assert stale.status_code == 409
|
|
57
|
+
assert stale.headers["X-Inertia-Location"] == "http://test/page"
|
|
58
|
+
request.scope["headers"] = [
|
|
59
|
+
(b"x-inertia", b"true"),
|
|
60
|
+
(b"x-inertia-version", new_version.encode()),
|
|
61
|
+
]
|
|
62
|
+
current = await app.state.inertia_dependency(Request(request.scope)).render("Page")
|
|
63
|
+
assert current.status_code == 200
|
|
64
|
+
assert json.loads(current.body)["version"] == new_version
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.mark.parametrize("environment", ["development", "testing", "production"])
|
|
68
|
+
def test_setup_without_built_assets_preserves_fallback(tmp_path, environment):
|
|
69
|
+
_, config = _setup(tmp_path, environment)
|
|
70
|
+
assert resolved_version(config) == "1.0"
|
|
71
|
+
assert config.manifest_json_path == ""
|
|
@@ -96,3 +96,32 @@ class TestWithoutSessionMiddleware:
|
|
|
96
96
|
async with httpx.AsyncClient(transport=transport, base_url="http://test") as c:
|
|
97
97
|
assert (await c.get("/token")).json()["token"] == ""
|
|
98
98
|
assert (await c.post("/things")).status_code == 200
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@pytest.mark.parametrize("with_session", [False, True])
|
|
102
|
+
def test_rotation_discards_only_csrf_state(with_session):
|
|
103
|
+
from simple_module_hosting.csrf import rotate_csrf_token
|
|
104
|
+
|
|
105
|
+
scope = {"type": "http"}
|
|
106
|
+
if with_session:
|
|
107
|
+
scope["session"] = {"user_id": "keep"}
|
|
108
|
+
request = Request(scope)
|
|
109
|
+
old = get_csrf_token(request)
|
|
110
|
+
rotate_csrf_token(request)
|
|
111
|
+
rotate_csrf_token(request) # safe even before another token has been minted
|
|
112
|
+
if with_session:
|
|
113
|
+
assert request.session == {"user_id": "keep"}
|
|
114
|
+
assert get_csrf_token(request) != old
|
|
115
|
+
else:
|
|
116
|
+
assert get_csrf_token(request) == ""
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_sign_in_survivors_registry():
|
|
120
|
+
from types import SimpleNamespace
|
|
121
|
+
|
|
122
|
+
from simple_module_hosting.session import preserve_across_sign_in, sign_in_preserved_keys
|
|
123
|
+
|
|
124
|
+
app = SimpleNamespace(state=SimpleNamespace())
|
|
125
|
+
assert sign_in_preserved_keys(app) == frozenset()
|
|
126
|
+
preserve_across_sign_in(app, "gate_marker")
|
|
127
|
+
assert sign_in_preserved_keys(app) == {"gate_marker"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Lifecycle failures must still unwind modules and dispose the engine (#634)."""
|
|
2
|
+
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from unittest.mock import AsyncMock
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from fastapi import FastAPI
|
|
8
|
+
from simple_module_hosting import _lifespan
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.parametrize("failure", ["startup", "shutdown", "body", "migration"])
|
|
12
|
+
async def test_lifespan_unwinds_after_failure(monkeypatch, failure):
|
|
13
|
+
calls = []
|
|
14
|
+
app = FastAPI()
|
|
15
|
+
|
|
16
|
+
async def dispose():
|
|
17
|
+
calls.append("dispose")
|
|
18
|
+
|
|
19
|
+
app.state.sm = SimpleNamespace(db=SimpleNamespace(engine=SimpleNamespace(dispose=dispose)))
|
|
20
|
+
status = AsyncMock(return_value={"is_current": True})
|
|
21
|
+
if failure == "migration":
|
|
22
|
+
status.side_effect = RuntimeError("migration")
|
|
23
|
+
monkeypatch.setattr(_lifespan, "migration_status", status)
|
|
24
|
+
monkeypatch.setattr(_lifespan, "hydrate_settings_from_db", AsyncMock())
|
|
25
|
+
modules = []
|
|
26
|
+
for name in ["a", "b", "c"]:
|
|
27
|
+
|
|
28
|
+
async def start(app, name=name):
|
|
29
|
+
calls.append(f"start:{name}")
|
|
30
|
+
if failure == "startup" and name == "b":
|
|
31
|
+
raise RuntimeError("startup")
|
|
32
|
+
|
|
33
|
+
async def stop(app, name=name):
|
|
34
|
+
calls.append(f"stop:{name}")
|
|
35
|
+
if failure in {"startup", "shutdown"} and name == "a":
|
|
36
|
+
raise ValueError("cleanup")
|
|
37
|
+
if failure == "shutdown" and name == "c":
|
|
38
|
+
raise RuntimeError("shutdown")
|
|
39
|
+
|
|
40
|
+
modules.append(SimpleNamespace(on_startup=start, on_shutdown=stop))
|
|
41
|
+
|
|
42
|
+
with pytest.raises((RuntimeError, ValueError)):
|
|
43
|
+
async with _lifespan.build_lifespan(modules)(app):
|
|
44
|
+
if failure == "body":
|
|
45
|
+
raise RuntimeError("body")
|
|
46
|
+
|
|
47
|
+
expected = {
|
|
48
|
+
"startup": ["start:a", "start:b", "stop:a", "dispose"],
|
|
49
|
+
"shutdown": ["start:a", "start:b", "start:c", "stop:c", "stop:b", "stop:a", "dispose"],
|
|
50
|
+
"body": ["start:a", "start:b", "start:c", "stop:c", "stop:b", "stop:a", "dispose"],
|
|
51
|
+
"migration": ["dispose"],
|
|
52
|
+
}
|
|
53
|
+
assert calls == expected[failure]
|
|
@@ -8,6 +8,18 @@ import pytest
|
|
|
8
8
|
from alembic import command
|
|
9
9
|
from fastapi import HTTPException
|
|
10
10
|
from simple_module_hosting.setup_wizard.migrate import apply_migrations
|
|
11
|
+
from sqlalchemy.engine import make_url
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _request(correlation_id):
|
|
15
|
+
engine = SimpleNamespace(url=make_url("sqlite+aiosqlite:///:memory:"))
|
|
16
|
+
return SimpleNamespace(
|
|
17
|
+
state=SimpleNamespace(correlation_id=correlation_id),
|
|
18
|
+
app=SimpleNamespace(
|
|
19
|
+
state=SimpleNamespace(sm=SimpleNamespace(db=SimpleNamespace(engine=engine)))
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
11
23
|
|
|
12
24
|
_SECRET = "postgresql://admin:hunter2@db.internal/prod"
|
|
13
25
|
|
|
@@ -17,7 +29,7 @@ async def test_migration_failure_detail_is_redacted(monkeypatch, caplog) -> None
|
|
|
17
29
|
raise RuntimeError(f"could not connect to {_SECRET}")
|
|
18
30
|
|
|
19
31
|
monkeypatch.setattr(command, "upgrade", _boom)
|
|
20
|
-
request =
|
|
32
|
+
request = _request("cid-123")
|
|
21
33
|
|
|
22
34
|
with pytest.raises(HTTPException) as info:
|
|
23
35
|
await apply_migrations(request, {})
|
|
@@ -45,7 +57,7 @@ async def test_in_process_migration_keeps_app_logging(monkeypatch) -> None:
|
|
|
45
57
|
raise RuntimeError("stop after clobbering")
|
|
46
58
|
|
|
47
59
|
monkeypatch.setattr(command, "upgrade", _clobber)
|
|
48
|
-
request =
|
|
60
|
+
request = _request("")
|
|
49
61
|
try:
|
|
50
62
|
with pytest.raises(HTTPException):
|
|
51
63
|
await apply_migrations(request, {})
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Independent wizard workers must serialize even on a fresh database (#634)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import importlib.util
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from unittest.mock import AsyncMock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
from alembic import command
|
|
13
|
+
from simple_module_hosting.setup_wizard import migrate
|
|
14
|
+
from simple_module_hosting.setup_wizard._migration_lock import migration_lock
|
|
15
|
+
from sqlalchemy.engine import make_url
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_independent_workers_serialize_migrations(monkeypatch, tmp_path):
|
|
19
|
+
# Separate module globals reproduce workers with distinct asyncio locks.
|
|
20
|
+
spec = importlib.util.spec_from_file_location("other_worker", migrate.__file__)
|
|
21
|
+
other = importlib.util.module_from_spec(spec)
|
|
22
|
+
spec.loader.exec_module(other)
|
|
23
|
+
active = maximum = 0
|
|
24
|
+
guard = threading.Lock()
|
|
25
|
+
ready = threading.Barrier(2)
|
|
26
|
+
|
|
27
|
+
def upgrade(config, target):
|
|
28
|
+
nonlocal active, maximum
|
|
29
|
+
assert target == "heads"
|
|
30
|
+
with guard:
|
|
31
|
+
active += 1
|
|
32
|
+
maximum = max(maximum, active)
|
|
33
|
+
time.sleep(0.1) # intentional-sleep: hold the lock so an overlap would show
|
|
34
|
+
with guard:
|
|
35
|
+
active -= 1
|
|
36
|
+
|
|
37
|
+
async def inline_thread(func):
|
|
38
|
+
ready.wait(timeout=5)
|
|
39
|
+
return func()
|
|
40
|
+
|
|
41
|
+
monkeypatch.setattr(asyncio, "to_thread", inline_thread)
|
|
42
|
+
monkeypatch.setattr(command, "upgrade", upgrade)
|
|
43
|
+
monkeypatch.setattr("simple_module_hosting.migrations.migration_status", AsyncMock())
|
|
44
|
+
monkeypatch.setattr(migrate, "run_deferred_startup", AsyncMock())
|
|
45
|
+
monkeypatch.setattr(other, "run_deferred_startup", AsyncMock())
|
|
46
|
+
url = make_url(f"sqlite+aiosqlite:///{tmp_path / 'fresh.db'}")
|
|
47
|
+
|
|
48
|
+
def request():
|
|
49
|
+
return SimpleNamespace(
|
|
50
|
+
app=SimpleNamespace(
|
|
51
|
+
state=SimpleNamespace(
|
|
52
|
+
sm=SimpleNamespace(db=SimpleNamespace(engine=SimpleNamespace(url=url)))
|
|
53
|
+
)
|
|
54
|
+
),
|
|
55
|
+
state=SimpleNamespace(),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
with ThreadPoolExecutor(max_workers=2) as workers:
|
|
59
|
+
futures = [
|
|
60
|
+
workers.submit(asyncio.run, worker.apply_migrations(request(), {}))
|
|
61
|
+
for worker in [migrate, other]
|
|
62
|
+
]
|
|
63
|
+
for future in futures:
|
|
64
|
+
future.result(timeout=5)
|
|
65
|
+
assert maximum == 1
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_sqlite_migration_lock_released_on_failure(tmp_path):
|
|
69
|
+
url = make_url(f"sqlite+aiosqlite:///{tmp_path / 'fresh.db'}")
|
|
70
|
+
with pytest.raises(RuntimeError, match="migration failed"), migration_lock(url):
|
|
71
|
+
raise RuntimeError("migration failed")
|
|
72
|
+
# Reacquiring on another connection must work after an exception.
|
|
73
|
+
with migration_lock(url):
|
|
74
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/__init__.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/__main__.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_anonymous.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_api_version.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_app_boot.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_body_limit.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_boot_checks.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_cors.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_csp_dynamic.py
RENAMED
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_db_health.py
RENAMED
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_dev_boot.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{simple_module_hosting-0.0.39 → simple_module_hosting-0.0.40}/simple_module_hosting/_error_page.py
RENAMED
|
File without changes
|