simple-module-hosting 0.0.41__tar.gz → 0.0.42__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/PKG-INFO +5 -5
  2. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/pyproject.toml +5 -5
  3. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/__init__.py +2 -0
  4. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_deferred_startup.py +7 -1
  5. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_lifespan.py +11 -0
  6. simple_module_hosting-0.0.42/simple_module_hosting/data_compatibility.py +37 -0
  7. simple_module_hosting-0.0.42/simple_module_hosting/errors.py +28 -0
  8. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_gate.py +10 -1
  9. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/migrate.py +9 -1
  10. simple_module_hosting-0.0.42/tests/test_hosting_data_compatibility.py +165 -0
  11. simple_module_hosting-0.0.42/tests/test_hosting_public_error_page.py +36 -0
  12. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_lifespan_deferred_startup.py +4 -1
  13. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/.gitignore +0 -0
  14. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/LICENSE +0 -0
  15. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/README.md +0 -0
  16. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/__main__.py +0 -0
  17. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_anonymous.py +0 -0
  18. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_api_version.py +0 -0
  19. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_app_boot.py +0 -0
  20. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_body_limit.py +0 -0
  21. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_boot_checks.py +0 -0
  22. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_cors.py +0 -0
  23. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_csp_dynamic.py +0 -0
  24. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_db_health.py +0 -0
  25. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_dev_boot.py +0 -0
  26. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_error_handlers.py +0 -0
  27. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_error_negotiation.py +0 -0
  28. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_error_page.py +0 -0
  29. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_favicon.py +0 -0
  30. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_forwarded_host.py +0 -0
  31. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_host_services.py +0 -0
  32. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_host_settings_support.py +0 -0
  33. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_hydrate_step.py +0 -0
  34. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_i18n_build.py +0 -0
  35. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_i18n_target.py +0 -0
  36. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_inertia_cache.py +0 -0
  37. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_inertia_setup.py +0 -0
  38. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_inertia_shared.py +0 -0
  39. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_insecure_transport.py +0 -0
  40. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_lease_redis.py +0 -0
  41. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_middleware_pipeline.py +0 -0
  42. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_module_routes.py +0 -0
  43. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_observability.py +0 -0
  44. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_openapi.py +0 -0
  45. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_phase_helpers.py +0 -0
  46. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_preapp_config.py +0 -0
  47. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_process_services.py +0 -0
  48. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_project_root.py +0 -0
  49. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_rate_limit.py +0 -0
  50. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_rate_limit_keys.py +0 -0
  51. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_rate_limit_store.py +0 -0
  52. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_registrations.py +0 -0
  53. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_request_guard_responses.py +0 -0
  54. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_route_access.py +0 -0
  55. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_route_guard.py +0 -0
  56. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_route_paths.py +0 -0
  57. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_secret_key.py +0 -0
  58. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_settings_registration.py +0 -0
  59. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_spa_mount.py +0 -0
  60. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/_tenant.py +0 -0
  61. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/app_builder.py +0 -0
  62. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/assets.py +0 -0
  63. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/bootstrap_settings.py +0 -0
  64. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/csrf.py +0 -0
  65. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/db_options.py +0 -0
  66. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/health.py +0 -0
  67. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/host_cli.py +0 -0
  68. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/host_settings.py +0 -0
  69. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/i18n_deps.py +0 -0
  70. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/i18n_manifest.py +0 -0
  71. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/i18n_middleware.py +0 -0
  72. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/i18n_routes.py +0 -0
  73. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/inertia_deps.py +0 -0
  74. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/inertia_utils.py +0 -0
  75. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/leases.py +0 -0
  76. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/locales/de.json +0 -0
  77. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/locales/en.json +0 -0
  78. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/locales/es.json +0 -0
  79. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/log_sinks.py +0 -0
  80. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/logging.py +0 -0
  81. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/maintenance.py +0 -0
  82. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/manifest.py +0 -0
  83. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/middleware.py +0 -0
  84. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/migrations.py +0 -0
  85. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/page_globs.py +0 -0
  86. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/pagination.py +0 -0
  87. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/permissions.py +0 -0
  88. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/public_access.py +0 -0
  89. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/py.typed +0 -0
  90. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/rate_limit_deps.py +0 -0
  91. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/redirects.py +0 -0
  92. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/resource_deps.py +0 -0
  93. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/sentry.py +0 -0
  94. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/sentry_scrub.py +0 -0
  95. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/sentry_scrub_text.py +0 -0
  96. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/session.py +0 -0
  97. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/settings.py +0 -0
  98. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/__init__.py +0 -0
  99. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/_migration_lock.py +0 -0
  100. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/components/ConnectionList.tsx +0 -0
  101. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/components/StepForm.tsx +0 -0
  102. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/pages/Wizard.tsx +0 -0
  103. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/payloads.py +0 -0
  104. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/setup_wizard/routes.py +0 -0
  105. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/shared_props.py +0 -0
  106. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/static_files.py +0 -0
  107. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/tenancy.py +0 -0
  108. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/token_guard.py +0 -0
  109. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/ui_locales/de.json +0 -0
  110. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/ui_locales/en.json +0 -0
  111. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/simple_module_hosting/ui_locales/es.json +0 -0
  112. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/_hosting_settings_db.py +0 -0
  113. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/_resacc_models.py +0 -0
  114. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/_sentry_transport.py +0 -0
  115. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/conftest.py +0 -0
  116. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_api_error_negotiation.py +0 -0
  117. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_app.py +0 -0
  118. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_auth_provider_setting.py +0 -0
  119. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_boot_config_hardening.py +0 -0
  120. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_boot_drift_check.py +0 -0
  121. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_branding_head.py +0 -0
  122. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_build_time_settings_marked.py +0 -0
  123. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_check_migrations.py +0 -0
  124. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_check_migrations_expression_note.py +0 -0
  125. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_csp_module_sources.py +0 -0
  126. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_csp_providers.py +0 -0
  127. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_db_health_check.py +0 -0
  128. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_design_packs_wiring.py +0 -0
  129. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_diagnostics_state.py +0 -0
  130. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_error_page_shared_props.py +0 -0
  131. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_error_status_coverage.py +0 -0
  132. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_favicon_default.py +0 -0
  133. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_forwarded_host.py +0 -0
  134. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_grant_sources.py +0 -0
  135. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_health.py +0 -0
  136. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_home_url_default.py +0 -0
  137. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_host_cli.py +0 -0
  138. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_host_settings_fields.py +0 -0
  139. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_asset_version.py +0 -0
  140. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_cors_api.py +0 -0
  141. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_cors_unset.py +0 -0
  142. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_csrf.py +0 -0
  143. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_csrf_shared_prop.py +0 -0
  144. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_error_handling_policy.py +0 -0
  145. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_error_json_and_session_cookie.py +0 -0
  146. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_lease_backends.py +0 -0
  147. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_lease_selection.py +0 -0
  148. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_lifespan_cleanup.py +0 -0
  149. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_log_sinks.py +0 -0
  150. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_openapi.py +0 -0
  151. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_openapi_security.py +0 -0
  152. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_pagination_deps.py +0 -0
  153. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_permissions.py +0 -0
  154. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_public_access.py +0 -0
  155. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_sentry.py +0 -0
  156. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_sentry_scrub.py +0 -0
  157. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_hosting_sentry_scrub_gaps.py +0 -0
  158. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_build_overrides.py +0 -0
  159. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_manifest.py +0 -0
  160. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_manifest_published_target.py +0 -0
  161. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_routes.py +0 -0
  162. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_types_cover_installed_modules.py +0 -0
  163. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_i18n_ui_namespace.py +0 -0
  164. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_inertia_cache.py +0 -0
  165. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_inertia_i18n_shared_props.py +0 -0
  166. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_inertia_shared_providers.py +0 -0
  167. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_insecure_transport_warning.py +0 -0
  168. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_invalidation_wiring.py +0 -0
  169. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_lifespan_order.py +0 -0
  170. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_locale_middleware.py +0 -0
  171. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_logging.py +0 -0
  172. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_maintenance_mode.py +0 -0
  173. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_manifest.py +0 -0
  174. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_menu_providers_wiring.py +0 -0
  175. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_menu_urls_are_canonical.py +0 -0
  176. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_merge_host_settings_strict.py +0 -0
  177. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_middleware_order.py +0 -0
  178. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_migration_listing.py +0 -0
  179. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_migration_multihead.py +0 -0
  180. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_native_link_prefixes_strip.py +0 -0
  181. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_preapp_config.py +0 -0
  182. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_preapp_invalid_values.py +0 -0
  183. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_preapp_read_failures.py +0 -0
  184. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_precompressed_static.py +0 -0
  185. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_principal_permissions_claim.py +0 -0
  186. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_proxy_headers.py +0 -0
  187. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_rate_limit_rules_redis.py +0 -0
  188. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_rate_limit_rules_registry.py +0 -0
  189. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_rate_limit_store_shared.py +0 -0
  190. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_redirects.py +0 -0
  191. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_request_guard_auth_parity.py +0 -0
  192. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_request_guard_body_limit.py +0 -0
  193. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_request_guard_body_limit_middleware_read.py +0 -0
  194. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_request_guard_body_limit_started.py +0 -0
  195. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_request_guard_rate_limit.py +0 -0
  196. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_resource_deps.py +0 -0
  197. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_resource_policy_owned.py +0 -0
  198. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_resource_policy_parity.py +0 -0
  199. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_resource_types_wiring.py +0 -0
  200. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_response_compression.py +0 -0
  201. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_route_guard.py +0 -0
  202. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_route_guard_fastapi_contract.py +0 -0
  203. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_route_paths.py +0 -0
  204. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_secret_key_bootstrap.py +0 -0
  205. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_server_rendered_head.py +0 -0
  206. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_session_cookie_secure.py +0 -0
  207. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_session_cookie_security.py +0 -0
  208. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_session_cookie_window.py +0 -0
  209. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_session_vary_dedupe.py +0 -0
  210. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_settings_i18n.py +0 -0
  211. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_settings_secrets.py +0 -0
  212. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_gate.py +0 -0
  213. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_gate_single_flight.py +0 -0
  214. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_migrate_error_redaction.py +0 -0
  215. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_migrate_worker_lock.py +0 -0
  216. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_public_routes.py +0 -0
  217. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_registry.py +0 -0
  218. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_setup_routes.py +0 -0
  219. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_spa_mounts_541.py +0 -0
  220. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_sqlite_path_anchor.py +0 -0
  221. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_static_caching.py +0 -0
  222. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_strict_discovery_wiring.py +0 -0
  223. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenancy_api.py +0 -0
  224. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenant_middleware.py +0 -0
  225. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenant_resolver_diagnostic.py +0 -0
  226. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenant_resolver_types.py +0 -0
  227. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenant_scope_helpers.py +0 -0
  228. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_tenant_source.py +0 -0
  229. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_token_guard_effective_route.py +0 -0
  230. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_token_route_guard.py +0 -0
  231. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_translator_dep.py +0 -0
  232. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests/test_view_prefixes.py +0 -0
  233. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tests-js/hosting_SetupWizard.test.tsx +0 -0
  234. {simple_module_hosting-0.0.41 → simple_module_hosting-0.0.42}/tsconfig.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simple_module_hosting
3
- Version: 0.0.41
3
+ Version: 0.0.42
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.159,>=0.140
25
+ Requires-Dist: fastapi<0.160,>=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.41
30
- Requires-Dist: simple-module-db==0.0.41
31
- Requires-Dist: simple-module-inertia==0.0.41
29
+ Requires-Dist: simple-module-core==0.0.42
30
+ Requires-Dist: simple-module-db==0.0.42
31
+ Requires-Dist: simple-module-inertia==0.0.42
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.41"
3
+ version = "0.0.42"
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.159",
25
+ "fastapi>=0.140,<0.160",
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.41",
33
- "simple_module_db==0.0.41",
34
- "simple_module_inertia==0.0.41",
32
+ "simple_module_core==0.0.42",
33
+ "simple_module_db==0.0.42",
34
+ "simple_module_inertia==0.0.42",
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 simple_module_hosting._preapp_config import merge_host_settings
4
4
  from simple_module_hosting.app_builder import create_app
5
+ from simple_module_hosting.errors import render_error_page
5
6
  from simple_module_hosting.logging import correlation_id, setup_logging
6
7
  from simple_module_hosting.settings import Settings
7
8
  from simple_module_hosting.shared_props import (
@@ -16,5 +17,6 @@ __all__ = [
16
17
  "create_app",
17
18
  "merge_host_settings",
18
19
  "register_inertia_shared_provider",
20
+ "render_error_page",
19
21
  "setup_logging",
20
22
  ]
@@ -11,6 +11,7 @@ from simple_module_core.module_settings import get_module_settings_registry
11
11
  from simple_module_db.stored_settings import SessionOverrideSource, settings_table_exists
12
12
 
13
13
  from simple_module_hosting._hydrate_step import hydrate_all
14
+ from simple_module_hosting.data_compatibility import check_data_compatibility
14
15
 
15
16
  logger = logging.getLogger(__name__)
16
17
 
@@ -45,8 +46,13 @@ async def run_deferred_startup(app: FastAPI) -> None:
45
46
  operator's out-of-band ``make migrate``) still finishes starting its
46
47
  modules. Serialised per app; a hook that fails again is logged and dropped
47
48
  rather than raised, because this runs after the migrations have already
48
- committed and a 500 there would help nobody.
49
+ committed. Compatibility checks run first and still raise: an incompatible
50
+ security state must keep the setup gate closed.
49
51
  """
52
+ pending_checks = getattr(app.state, "deferred_data_compatibility", None)
53
+ if pending_checks:
54
+ await check_data_compatibility(app, pending_checks)
55
+ app.state.deferred_data_compatibility = []
50
56
  deferred = get_deferred_startup(app)
51
57
  if not deferred:
52
58
  return
@@ -22,6 +22,7 @@ from simple_module_core.app_state import get_migration_status
22
22
 
23
23
  from simple_module_hosting._deferred_startup import hydrate_settings_from_db
24
24
  from simple_module_hosting._route_guard import guard_included_routers
25
+ from simple_module_hosting.data_compatibility import check_data_compatibility
25
26
  from simple_module_hosting.migrations import migration_status
26
27
  from simple_module_hosting.setup_gate import STEP_MIGRATIONS
27
28
 
@@ -114,6 +115,16 @@ def build_lifespan(modules: Sequence) -> Callable:
114
115
  f"{app.state.migration['head_revision']!r}). Run: make migrate"
115
116
  )
116
117
 
118
+ # Only a schema at head can be judged: a first run that stopped part-way
119
+ # still has pre-upgrade columns, and refusing it would hide the wizard
120
+ # that finishes the migration. The setup gate runs deferred checks
121
+ # once the schema is current.
122
+ migration = app.state.migration
123
+ if migration["is_current"] and migration.get("current_revision") is not None:
124
+ await check_data_compatibility(app, modules)
125
+ else:
126
+ app.state.deferred_data_compatibility = list(modules)
127
+
117
128
  await hydrate_settings_from_db(app)
118
129
 
119
130
  # Behind head on a first run the tables a module's on_startup reads do
@@ -0,0 +1,37 @@
1
+ """Enforce module data compatibility before hydrated settings and startup."""
2
+
3
+ import logging
4
+ from collections.abc import Sequence
5
+
6
+ from fastapi import FastAPI
7
+ from simple_module_core.data_compatibility import checking_modules, collect_data_compatibility
8
+ from simple_module_core.module import ModuleBase
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class DataCompatibilityError(RuntimeError):
14
+ """An incompatible security state must also keep the setup gate closed."""
15
+
16
+
17
+ async def check_data_compatibility(app: FastAPI, modules: Sequence[ModuleBase]) -> None:
18
+ modules = checking_modules(modules)
19
+ if not modules:
20
+ return
21
+ try:
22
+ async with app.state.sm.db.session_factory() as db:
23
+ issues = await collect_data_compatibility(modules, db)
24
+ except Exception as exc:
25
+ # Log the cause here: callers such as the setup gate only see the verdict.
26
+ logger.exception("Module data compatibility check failed")
27
+ if app.state.sm.settings.environment == "development":
28
+ return
29
+ raise DataCompatibilityError("Could not check module data compatibility") from exc
30
+ errors = []
31
+ for issue in issues:
32
+ message = f"{issue.upgrade_id}: {issue.message} Prepare: {issue.remediation()}"
33
+ logger.warning(message)
34
+ if issue.severity == "error":
35
+ errors.append(message)
36
+ if errors and app.state.sm.settings.environment != "development":
37
+ raise DataCompatibilityError("Data compatibility upgrade required:\n" + "\n".join(errors))
@@ -0,0 +1,28 @@
1
+ """Public error rendering for module exception handlers (#684)."""
2
+
3
+ from collections.abc import Mapping
4
+
5
+ from starlette.requests import Request
6
+ from starlette.responses import JSONResponse, Response
7
+
8
+ from simple_module_hosting._error_negotiation import _wants_json
9
+ from simple_module_hosting._error_page import render_error_page as _render_error_page
10
+
11
+
12
+ async def render_error_page(
13
+ request: Request,
14
+ status_code: int,
15
+ detail: str,
16
+ headers: Mapping[str, str] | None = None,
17
+ *,
18
+ required_permission: str | None = None,
19
+ ) -> Response:
20
+ """Return standard JSON for API clients or the Inertia Error page for browsers.
21
+
22
+ Uses the framework's content negotiation, shared layout props and rendering
23
+ fallback. Status and supplied headers survive either response format.
24
+ ``required_permission`` optionally identifies the permission a 403 lacked.
25
+ """
26
+ if _wants_json(request):
27
+ return JSONResponse(status_code=status_code, content={"detail": detail}, headers=headers)
28
+ return await _render_error_page(request, status_code, detail, headers, required_permission)
@@ -23,6 +23,7 @@ from starlette.types import ASGIApp, Receive, Scope, Send
23
23
 
24
24
  from simple_module_hosting import migrations
25
25
  from simple_module_hosting._deferred_startup import run_deferred_startup
26
+ from simple_module_hosting.data_compatibility import DataCompatibilityError
26
27
 
27
28
  logger = logging.getLogger(__name__)
28
29
  _migration_failure_warned = False
@@ -74,6 +75,11 @@ async def _database_migrated(app) -> bool:
74
75
  if not status:
75
76
  return True
76
77
  if status.get("is_current", True):
78
+ if getattr(app.state, "deferred_data_compatibility", None):
79
+ try:
80
+ await run_deferred_startup(app)
81
+ except DataCompatibilityError:
82
+ return False
77
83
  return True
78
84
 
79
85
  try:
@@ -90,9 +96,12 @@ async def _database_migrated(app) -> bool:
90
96
  if status["is_current"]:
91
97
  # Finish starting the modules whose on_startup needed these tables —
92
98
  # this worker may not be the one that ran the migrations. Never lets
93
- # a failure there change the verdict, which only reports the schema.
99
+ # an ordinary startup-hook failure change the schema verdict. A failed
100
+ # data compatibility check must keep the gate closed.
94
101
  try:
95
102
  await run_deferred_startup(app)
103
+ except DataCompatibilityError:
104
+ return False
96
105
  except Exception:
97
106
  logger.exception("Replaying deferred on_startup hooks failed")
98
107
  return bool(status["is_current"])
@@ -16,6 +16,7 @@ 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.data_compatibility import DataCompatibilityError
19
20
  from simple_module_hosting.setup_wizard._migration_lock import migration_lock
20
21
 
21
22
  logger = logging.getLogger(__name__)
@@ -87,6 +88,13 @@ async def apply_migrations(request: Request, _data: dict) -> dict:
87
88
 
88
89
  request.app.state.migration = await migration_status(request.app.state.sm.db.engine)
89
90
  # Modules whose on_startup could not run before the tables existed.
90
- await run_deferred_startup(request.app)
91
+ try:
92
+ await run_deferred_startup(request.app)
93
+ except DataCompatibilityError as exc:
94
+ # The detail names upgrade ids and the remediation command, never data.
95
+ logger.error("Setup: %s", exc)
96
+ raise HTTPException(
97
+ status_code=409, detail="A module data upgrade is required; see the server log."
98
+ ) from exc
91
99
  logger.info("Setup: migrations applied")
92
100
  return {"migration": request.app.state.migration}
@@ -0,0 +1,165 @@
1
+ """Compatibility errors gate established hosts before settings or startup (#711)."""
2
+
3
+ from types import SimpleNamespace
4
+
5
+ import pytest
6
+ from fastapi import FastAPI
7
+ from simple_module_core.module import ModuleBase, ModuleMeta
8
+ from simple_module_hosting._lifespan import build_lifespan
9
+ from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
10
+
11
+
12
+ @pytest.mark.parametrize(
13
+ ("environment", "severity", "never_migrated", "refuses"),
14
+ [
15
+ ("production", "error", False, True),
16
+ ("testing", "error", False, True),
17
+ ("development", "error", False, False),
18
+ ("production", "warning", False, False),
19
+ ("production", "error", True, False),
20
+ ],
21
+ )
22
+ async def test_hosting_compatibility_before_hydration(
23
+ monkeypatch, environment, severity, never_migrated, refuses
24
+ ):
25
+ from simple_module_core.data_compatibility import DataCompatibilityIssue
26
+
27
+ events = []
28
+
29
+ class Module(ModuleBase):
30
+ meta = ModuleMeta(name="Example")
31
+
32
+ async def check_data_compatibility(self, db):
33
+ events.append("check")
34
+ return [
35
+ DataCompatibilityIssue(
36
+ upgrade_id="example.v1", message="Upgrade", severity=severity
37
+ )
38
+ ]
39
+
40
+ async def on_startup(self, app):
41
+ events.append("startup")
42
+
43
+ async def status(engine):
44
+ return {"is_current": True, "current_revision": None if never_migrated else "local"}
45
+
46
+ async def hydrate(app):
47
+ events.append("hydrate")
48
+
49
+ monkeypatch.setattr("simple_module_hosting._lifespan.migration_status", status)
50
+ monkeypatch.setattr("simple_module_hosting._lifespan.hydrate_settings_from_db", hydrate)
51
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
52
+ app = FastAPI()
53
+ app.state.sm = SimpleNamespace(
54
+ db=SimpleNamespace(engine=engine, session_factory=async_sessionmaker(engine)),
55
+ settings=SimpleNamespace(environment=environment),
56
+ )
57
+ if refuses:
58
+ with pytest.raises(RuntimeError, match=r"example\.v1"):
59
+ async with build_lifespan([Module()])(app):
60
+ pass
61
+ assert events == ["check"]
62
+ else:
63
+ async with build_lifespan([Module()])(app):
64
+ assert events == (
65
+ ["hydrate", "startup"] if never_migrated else ["check", "hydrate", "startup"]
66
+ )
67
+
68
+
69
+ async def test_partly_migrated_first_run_defers_the_check(monkeypatch):
70
+ # A first run that stopped part-way has a revision but pre-upgrade columns:
71
+ # judging it would refuse the boot and hide the wizard that finishes it.
72
+ events = []
73
+
74
+ class Module(ModuleBase):
75
+ meta = ModuleMeta(name="Example")
76
+
77
+ async def check_data_compatibility(self, db):
78
+ events.append("check")
79
+ return []
80
+
81
+ async def status(engine):
82
+ return {"is_current": False, "current_revision": "partial", "pending_count": 3}
83
+
84
+ async def first_run(app):
85
+ return True
86
+
87
+ async def hydrate(app):
88
+ events.append("hydrate")
89
+
90
+ monkeypatch.setattr("simple_module_hosting._lifespan.migration_status", status)
91
+ monkeypatch.setattr("simple_module_hosting._lifespan._is_first_run", first_run)
92
+ monkeypatch.setattr("simple_module_hosting._lifespan.hydrate_settings_from_db", hydrate)
93
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
94
+ app = FastAPI()
95
+ app.state.sm = SimpleNamespace(
96
+ db=SimpleNamespace(engine=engine, session_factory=async_sessionmaker(engine)),
97
+ settings=SimpleNamespace(environment="production"),
98
+ )
99
+ async with build_lifespan([Module()])(app):
100
+ assert "check" not in events
101
+ assert app.state.deferred_data_compatibility
102
+
103
+
104
+ async def test_hosting_deferred_checks_gate_replay_before_hydration(monkeypatch):
105
+ from simple_module_core.data_compatibility import DataCompatibilityIssue
106
+ from simple_module_hosting._deferred_startup import run_deferred_startup
107
+
108
+ class Module(ModuleBase):
109
+ meta = ModuleMeta(name="Example")
110
+
111
+ async def check_data_compatibility(self, db):
112
+ return [
113
+ DataCompatibilityIssue(upgrade_id="example.v1", message="Upgrade", severity="error")
114
+ ]
115
+
116
+ app = FastAPI()
117
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
118
+ app.state.sm = SimpleNamespace(
119
+ db=SimpleNamespace(engine=engine, session_factory=async_sessionmaker(engine)),
120
+ settings=SimpleNamespace(environment="production"),
121
+ )
122
+ app.state.deferred_data_compatibility = [Module()]
123
+ app.state.deferred_startup = []
124
+ try:
125
+ with pytest.raises(RuntimeError, match=r"example\.v1"):
126
+ await run_deferred_startup(app)
127
+ assert app.state.deferred_data_compatibility
128
+ finally:
129
+ await engine.dispose()
130
+
131
+
132
+ @pytest.mark.parametrize("check_raises", [False, True])
133
+ async def test_hosting_setup_gate_keeps_incompatible_replay_closed(monkeypatch, check_raises):
134
+ from simple_module_core.data_compatibility import DataCompatibilityIssue
135
+ from simple_module_hosting.setup_gate import _database_migrated
136
+
137
+ class Module(ModuleBase):
138
+ meta = ModuleMeta(name="Example")
139
+
140
+ async def check_data_compatibility(self, db):
141
+ if check_raises:
142
+ raise RuntimeError("database unavailable")
143
+ return [
144
+ DataCompatibilityIssue(upgrade_id="example.v1", message="Upgrade", severity="error")
145
+ ]
146
+
147
+ app = FastAPI()
148
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:")
149
+ app.state.sm = SimpleNamespace(
150
+ db=SimpleNamespace(engine=engine, session_factory=async_sessionmaker(engine)),
151
+ settings=SimpleNamespace(environment="production"),
152
+ )
153
+ app.state.migration = {"is_current": False, "current_revision": None}
154
+ app.state.deferred_data_compatibility = [Module()]
155
+ app.state.deferred_startup = []
156
+
157
+ async def status(engine):
158
+ return {"is_current": True, "current_revision": "local"}
159
+
160
+ monkeypatch.setattr("simple_module_hosting.setup_gate.migrations.migration_status", status)
161
+ try:
162
+ assert await _database_migrated(app) is False
163
+ assert await _database_migrated(app) is False
164
+ finally:
165
+ await engine.dispose()
@@ -0,0 +1,36 @@
1
+ """Modules can use the public renderer with standard content negotiation (#684)."""
2
+
3
+ import pytest
4
+ from fastapi import Request
5
+
6
+
7
+ @pytest.mark.parametrize(
8
+ "headers, json_response",
9
+ [
10
+ ({"Accept": "application/json"}, True),
11
+ ({"Accept": "text/html"}, False),
12
+ ({"Accept": "application/json", "X-Inertia": "true"}, False),
13
+ ],
14
+ )
15
+ async def test_public_renderer_negotiates_and_preserves_status_and_headers(
16
+ app, authenticated_client, headers, json_response
17
+ ):
18
+ from simple_module_hosting import render_error_page
19
+
20
+ async def custom_error(request: Request):
21
+ return await render_error_page(
22
+ request, 429, detail="Try later", headers={"Retry-After": "30"}
23
+ )
24
+
25
+ app.add_api_route("/custom-error", custom_error)
26
+ response = await authenticated_client.get("/custom-error", headers=headers)
27
+ assert response.status_code == 429
28
+ assert response.headers["Retry-After"] == "30"
29
+ if json_response:
30
+ assert response.json() == {"detail": "Try later"}
31
+ elif "X-Inertia" in headers:
32
+ assert response.json()["component"] == "Error"
33
+ assert response.json()["props"]["message"] == "Try later"
34
+ else:
35
+ assert "data-page" in response.text
36
+ assert "Try later" in response.text
@@ -53,7 +53,10 @@ async def test_failing_hook_is_deferred_then_replayed_on_unmigrated_first_run():
53
53
  async def capture(app_):
54
54
  seen["deferred"] = [m.meta.name for m in app_.state.deferred_startup]
55
55
 
56
- with patch("simple_module_hosting._deferred_startup.hydrate_settings_from_db", AsyncMock()):
56
+ with (
57
+ patch("simple_module_hosting._deferred_startup.hydrate_settings_from_db", AsyncMock()),
58
+ patch("simple_module_hosting._deferred_startup.check_data_compatibility", AsyncMock()),
59
+ ):
57
60
  status = {"is_current": False, "pending_count": 1}
58
61
  with (
59
62
  patch(