simple-module-hosting 0.0.25__tar.gz → 0.0.26__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 (63) hide show
  1. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/PKG-INFO +3 -3
  2. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/pyproject.toml +3 -3
  3. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_phase_helpers.py +28 -0
  4. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/app_builder.py +12 -22
  5. simple_module_hosting-0.0.26/tests/test_design_packs_wiring.py +52 -0
  6. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/.gitignore +0 -0
  7. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/LICENSE +0 -0
  8. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/README.md +0 -0
  9. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/__init__.py +0 -0
  10. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/__main__.py +0 -0
  11. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_error_handlers.py +0 -0
  12. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_host_services.py +0 -0
  13. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_hydrate_step.py +0 -0
  14. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_inertia_setup.py +0 -0
  15. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_inertia_shared.py +0 -0
  16. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/_observability.py +0 -0
  17. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/bootstrap_settings.py +0 -0
  18. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/health.py +0 -0
  19. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/host_cli.py +0 -0
  20. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/host_settings.py +0 -0
  21. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/i18n_deps.py +0 -0
  22. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/i18n_manifest.py +0 -0
  23. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/i18n_middleware.py +0 -0
  24. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/inertia_deps.py +0 -0
  25. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/inertia_utils.py +0 -0
  26. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/logging.py +0 -0
  27. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/manifest.py +0 -0
  28. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/middleware.py +0 -0
  29. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/migrations.py +0 -0
  30. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/permissions.py +0 -0
  31. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/py.typed +0 -0
  32. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/redirects.py +0 -0
  33. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/settings.py +0 -0
  34. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/shared_props.py +0 -0
  35. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/simple_module_hosting/static_files.py +0 -0
  36. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_app.py +0 -0
  37. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_branding_head.py +0 -0
  38. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_check_migrations.py +0 -0
  39. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_error_page_shared_props.py +0 -0
  40. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_health.py +0 -0
  41. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_host_cli.py +0 -0
  42. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_hosting_permissions.py +0 -0
  43. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_i18n_manifest.py +0 -0
  44. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_inertia_i18n_shared_props.py +0 -0
  45. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_inertia_manifest.py +0 -0
  46. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_inertia_shared_providers.py +0 -0
  47. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_lifespan_order.py +0 -0
  48. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_locale_middleware.py +0 -0
  49. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_logging.py +0 -0
  50. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_manifest.py +0 -0
  51. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_menu_urls_are_canonical.py +0 -0
  52. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_middleware_order.py +0 -0
  53. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_precompressed_static.py +0 -0
  54. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_proxy_headers.py +0 -0
  55. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_redirects.py +0 -0
  56. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_response_compression.py +0 -0
  57. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_session_cookie_security.py +0 -0
  58. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_settings_i18n.py +0 -0
  59. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_settings_secrets.py +0 -0
  60. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_static_caching.py +0 -0
  61. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_strict_discovery_wiring.py +0 -0
  62. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_tenant_middleware.py +0 -0
  63. {simple_module_hosting-0.0.25 → simple_module_hosting-0.0.26}/tests/test_translator_dep.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simple_module_hosting
3
- Version: 0.0.25
3
+ Version: 0.0.26
4
4
  Summary: FastAPI + Inertia.js host runtime for simple_module — app_builder, middleware stack, CLI (sm / simple-module), scaffolding
5
5
  Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
6
  Project-URL: Repository, https://github.com/antosubash/simple_module_python
@@ -26,8 +26,8 @@ Requires-Dist: fastapi-inertia>=1.0
26
26
  Requires-Dist: fastapi>=0.115
27
27
  Requires-Dist: httpx>=0.27
28
28
  Requires-Dist: jinja2>=3.1
29
- Requires-Dist: simple-module-core==0.0.25
30
- Requires-Dist: simple-module-db==0.0.25
29
+ Requires-Dist: simple-module-core==0.0.26
30
+ Requires-Dist: simple-module-db==0.0.26
31
31
  Requires-Dist: starlette>=0.44
32
32
  Requires-Dist: tomlkit>=0.13
33
33
  Requires-Dist: uvicorn[standard]>=0.34
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "simple_module_hosting"
3
- version = "0.0.25"
3
+ version = "0.0.26"
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"
@@ -26,8 +26,8 @@ dependencies = [
26
26
  "fastapi-inertia>=1.0",
27
27
  "httpx>=0.27",
28
28
  "jinja2>=3.1",
29
- "simple_module_core==0.0.25",
30
- "simple_module_db==0.0.25",
29
+ "simple_module_core==0.0.26",
30
+ "simple_module_db==0.0.26",
31
31
  "starlette>=0.44",
32
32
  "tomlkit>=0.13",
33
33
  "uvicorn[standard]>=0.34",
@@ -32,6 +32,8 @@ from simple_module_hosting._error_handlers import (
32
32
  request_validation_error_handler,
33
33
  unhandled_exception_handler,
34
34
  )
35
+ from simple_module_hosting._host_services import _HostServices
36
+ from simple_module_hosting.host_settings import HostSettings
35
37
  from simple_module_hosting.i18n_middleware import LocaleMiddleware
36
38
  from simple_module_hosting.middleware import (
37
39
  CorrelationIdMiddleware,
@@ -165,6 +167,32 @@ def mount_module_static_dirs(app: FastAPI, modules: list) -> None:
165
167
  )
166
168
 
167
169
 
170
+ def register_host_settings(app: FastAPI) -> None:
171
+ """Register host-level settings under ``package="host"`` (DB-backed).
172
+
173
+ The Settings module must already have run ``register_settings`` — topo
174
+ order puts it early, since its ``meta.depends_on`` is empty. When the
175
+ Settings module isn't enabled there's no registry to register against, so
176
+ this skips quietly.
177
+
178
+ ``settings.registration`` is resolved via importlib rather than a plain
179
+ ``from settings.registration import ...``: the SM009 coupling check is
180
+ AST-based and forbids any static import of a plugin package name from
181
+ within ``framework/*``. Dynamic resolution keeps the framework AST
182
+ plugin-free while still hitting the real helper at runtime.
183
+ """
184
+ if not hasattr(app.state, "settings"):
185
+ return
186
+
187
+ import importlib
188
+
189
+ register_module_settings = importlib.import_module(
190
+ "settings.registration"
191
+ ).register_module_settings
192
+
193
+ register_module_settings(app, "host", HostSettings, lambda s: _HostServices(settings=s))
194
+
195
+
168
196
  def check_settings_registration(app: FastAPI, modules: list) -> list[Diagnostic]:
169
197
  """SM012: warn if a module overrides register_settings but added nothing to app.state.
170
198
 
@@ -10,6 +10,7 @@ from contextlib import asynccontextmanager
10
10
  from pathlib import Path
11
11
 
12
12
  from fastapi import FastAPI
13
+ from simple_module_core.design_packs import DesignPackRegistry
13
14
  from simple_module_core.diagnostics import DiagnosticLevel, print_diagnostics, run_diagnostics
14
15
  from simple_module_core.discovery import discover_modules, topological_sort
15
16
  from simple_module_core.events import EventBus
@@ -22,7 +23,6 @@ from simple_module_core.services import Services
22
23
  from simple_module_db.listeners import register_listeners
23
24
  from simple_module_db.session import init_db
24
25
 
25
- from simple_module_hosting._host_services import _HostServices
26
26
  from simple_module_hosting._inertia_setup import setup_inertia
27
27
  from simple_module_hosting._phase_helpers import (
28
28
  attach_public_routes,
@@ -30,10 +30,10 @@ from simple_module_hosting._phase_helpers import (
30
30
  install_middleware,
31
31
  mount_module_static_dirs,
32
32
  register_exception_handlers,
33
+ register_host_settings,
33
34
  wire_module_routes,
34
35
  )
35
36
  from simple_module_hosting.health import router as health_router
36
- from simple_module_hosting.host_settings import HostSettings
37
37
  from simple_module_hosting.i18n_manifest import build_i18n_registry, emit_frontend_types
38
38
  from simple_module_hosting.migrations import check_migrations
39
39
  from simple_module_hosting.settings import Settings
@@ -165,6 +165,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
165
165
  event_bus = EventBus()
166
166
  health_registry = HealthRegistry()
167
167
  public_route_registry = PublicRouteRegistry()
168
+ design_pack_registry = DesignPackRegistry()
168
169
 
169
170
  @asynccontextmanager
170
171
  async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
@@ -203,25 +204,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
203
204
  for mod in modules:
204
205
  mod.register_settings(app)
205
206
 
206
- # Register host-level settings under package="host" (DB-backed). The
207
- # Settings module must already have run register_settings (topo order
208
- # puts it early; its meta.depends_on = [] so it's among the first).
209
- # When the Settings module isn't enabled, there's no registry to
210
- # register against — skip quietly.
211
- #
212
- # We resolve `settings.registration` via importlib rather than a plain
213
- # `from settings.registration import ...`: the SM009 coupling check is
214
- # AST-based and forbids any static import of a plugin package name
215
- # from within framework/* code. Dynamic resolution keeps the framework
216
- # AST plugin-free while still hitting the real helper at runtime.
217
- if hasattr(app.state, "settings"):
218
- import importlib
219
-
220
- _register_module_settings = importlib.import_module(
221
- "settings.registration"
222
- ).register_module_settings
223
-
224
- _register_module_settings(app, "host", HostSettings, lambda s: _HostServices(settings=s))
207
+ register_host_settings(app)
225
208
 
226
209
  if settings.is_development:
227
210
  settings_diagnostics = check_settings_registration(app, modules)
@@ -236,17 +219,23 @@ def create_app(settings: Settings | None = None) -> FastAPI:
236
219
  _register_event_handlers(mod, event_bus, app)
237
220
  mod.register_health_checks(health_registry)
238
221
  mod.register_public_routes(public_route_registry)
222
+ mod.register_design_packs(design_pack_registry)
239
223
 
240
224
  attach_public_routes(app, settings, public_route_registry)
241
225
 
226
+ # Branding reads the packs off app.state directly: its API validates a
227
+ # submitted slug before persisting it, and its view builds the dropdown.
228
+ app.state.design_packs = design_pack_registry
229
+
242
230
  logger.info(
243
231
  "Registered %d menu items, %d permissions, %d feature flags, "
244
- "%d health checks, %d public routes",
232
+ "%d health checks, %d public routes, %d design packs",
245
233
  len(menu_registry.all_items),
246
234
  len(perm_registry.all_permissions),
247
235
  len(ff_registry.all_flags),
248
236
  len(health_registry.all_checks),
249
237
  len(public_route_registry.routes),
238
+ len(design_pack_registry.all()),
250
239
  )
251
240
 
252
241
  # ── Phase 6: Initialize database ───────────────────────
@@ -291,6 +280,7 @@ def create_app(settings: Settings | None = None) -> FastAPI:
291
280
  feature_flags=ff_registry,
292
281
  health_registry=health_registry,
293
282
  public_routes=public_route_registry,
283
+ design_packs=design_pack_registry,
294
284
  i18n_registry=i18n_registry,
295
285
  inertia_config=inertia_config,
296
286
  modules=tuple(modules),
@@ -0,0 +1,52 @@
1
+ """``create_app`` must collect module design packs onto ``app.state.design_packs``.
2
+
3
+ Branding reads this registry twice — the view builds its dropdown from it, and
4
+ the API validates a submitted slug against it. If the hook were never called,
5
+ or the populated registry never reached ``app.state``, branding would offer an
6
+ empty dropdown and reject every pack, with nothing in the logs to explain why.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import simple_module_hosting.app_builder as app_builder
12
+ from simple_module_core.design_packs import DesignPack, DesignPackRegistry
13
+ from simple_module_core.module import ModuleBase, ModuleMeta
14
+ from simple_module_hosting.app_builder import create_app
15
+
16
+ _PACK = DesignPack(value="pack-fixture", label="Pack Fixture")
17
+
18
+
19
+ class _PackModule(ModuleBase):
20
+ """A module whose only contribution is one design pack."""
21
+
22
+ meta = ModuleMeta(name="PackFixture")
23
+
24
+ def register_design_packs(self, registry: DesignPackRegistry) -> None:
25
+ registry.register(_PACK)
26
+
27
+
28
+ def test_registry_is_published_on_app_state(settings):
29
+ app = create_app(settings)
30
+ assert isinstance(app.state.design_packs, DesignPackRegistry)
31
+
32
+
33
+ def test_module_registered_pack_reaches_app_state(monkeypatch, settings):
34
+ real_discover = app_builder.discover_modules
35
+
36
+ def _with_pack_module(*args, **kwargs):
37
+ return [*real_discover(*args, **kwargs), _PackModule()]
38
+
39
+ monkeypatch.setattr(app_builder, "discover_modules", _with_pack_module)
40
+
41
+ app = create_app(settings)
42
+
43
+ assert app.state.design_packs.has("pack-fixture")
44
+ assert _PACK in app.state.design_packs.all()
45
+
46
+
47
+ def test_registry_is_also_reachable_through_services(settings):
48
+ # ``app.state.sm`` is the aggregate every other registry is published on;
49
+ # design packs should not be the one exception a module author has to
50
+ # remember a different lookup for.
51
+ app = create_app(settings)
52
+ assert app.state.sm.design_packs is app.state.design_packs