simple-module-hosting 0.0.14__tar.gz → 0.0.16__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 (51) hide show
  1. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/PKG-INFO +3 -3
  2. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/pyproject.toml +3 -3
  3. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_error_handlers.py +14 -0
  4. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_observability.py +12 -4
  5. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_phase_helpers.py +3 -0
  6. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/logging.py +3 -0
  7. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_logging.py +31 -0
  8. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/.gitignore +0 -0
  9. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/LICENSE +0 -0
  10. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/README.md +0 -0
  11. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/__init__.py +0 -0
  12. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/__main__.py +0 -0
  13. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_host_services.py +0 -0
  14. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_hydrate_step.py +0 -0
  15. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_inertia_setup.py +0 -0
  16. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/_inertia_shared.py +0 -0
  17. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/app_builder.py +0 -0
  18. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/bootstrap_settings.py +0 -0
  19. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/health.py +0 -0
  20. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/host_cli.py +0 -0
  21. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/host_settings.py +0 -0
  22. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/i18n_deps.py +0 -0
  23. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/i18n_manifest.py +0 -0
  24. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/i18n_middleware.py +0 -0
  25. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/inertia_deps.py +0 -0
  26. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/inertia_utils.py +0 -0
  27. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/manifest.py +0 -0
  28. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/middleware.py +0 -0
  29. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/migrations.py +0 -0
  30. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/permissions.py +0 -0
  31. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/py.typed +0 -0
  32. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/redirects.py +0 -0
  33. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/simple_module_hosting/settings.py +0 -0
  34. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_app.py +0 -0
  35. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_check_migrations.py +0 -0
  36. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_health.py +0 -0
  37. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_host_cli.py +0 -0
  38. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_hosting_permissions.py +0 -0
  39. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_i18n_manifest.py +0 -0
  40. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_inertia_i18n_shared_props.py +0 -0
  41. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_lifespan_order.py +0 -0
  42. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_locale_middleware.py +0 -0
  43. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_manifest.py +0 -0
  44. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_middleware_order.py +0 -0
  45. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_redirects.py +0 -0
  46. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_session_cookie_security.py +0 -0
  47. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_settings_i18n.py +0 -0
  48. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_settings_secrets.py +0 -0
  49. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_strict_discovery_wiring.py +0 -0
  50. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/tests/test_tenant_middleware.py +0 -0
  51. {simple_module_hosting-0.0.14 → simple_module_hosting-0.0.16}/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.14
3
+ Version: 0.0.16
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.14
30
- Requires-Dist: simple-module-db==0.0.14
29
+ Requires-Dist: simple-module-core==0.0.16
30
+ Requires-Dist: simple-module-db==0.0.16
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.14"
3
+ version = "0.0.16"
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.14",
30
- "simple_module_db==0.0.14",
29
+ "simple_module_core==0.0.16",
30
+ "simple_module_db==0.0.16",
31
31
  "starlette>=0.44",
32
32
  "tomlkit>=0.13",
33
33
  "uvicorn[standard]>=0.34",
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
 
7
+ from fastapi.encoders import jsonable_encoder
8
+ from fastapi.exceptions import RequestValidationError
7
9
  from fastapi.responses import JSONResponse
8
10
  from inertia import (
9
11
  Inertia,
@@ -49,6 +51,18 @@ async def not_found_error_handler(request: Request, exc: NotFoundError) -> Respo
49
51
  return await render_error_page(request, 404, str(exc))
50
52
 
51
53
 
54
+ async def request_validation_error_handler(
55
+ request: Request, exc: RequestValidationError
56
+ ) -> Response:
57
+ """Return an Inertia error page for browser requests with invalid params."""
58
+ accept = request.headers.get("accept", "")
59
+ if "text/html" in accept:
60
+ return await render_error_page(
61
+ request, 422, "The requested URL contains invalid parameters."
62
+ )
63
+ return JSONResponse(status_code=422, content={"detail": jsonable_encoder(exc.errors())})
64
+
65
+
52
66
  async def unhandled_exception_handler(request: Request, exc: Exception) -> Response:
53
67
  logger.exception("Unhandled exception: %s", exc)
54
68
  return await render_error_page(request, 500, "")
@@ -26,10 +26,16 @@ _QUIET_PREFIXES = ("/health", "/static/")
26
26
  class CorrelationIdMiddleware:
27
27
  """Generate or propagate a correlation ID for every request.
28
28
 
29
- Reads the incoming ``X-Correlation-ID`` header (or generates a UUID4) and
30
- stores it in a :class:`~contextvars.ContextVar` so that every log record
31
- emitted during the request automatically includes the ID. The same value
32
- is echoed back in the response header.
29
+ Reads the incoming ``X-Correlation-ID`` header (or generates a UUID4) and:
30
+
31
+ * stores it in the ``simple_module_hosting.logging.correlation_id``
32
+ ContextVar so the stdlib logging filter (or a user-supplied structlog
33
+ processor — see ``docs/framework/middleware.md``) picks it up with no
34
+ per-handler plumbing;
35
+ * exposes it on ``request.state.correlation_id`` for handlers that
36
+ prefer the request object over the contextvar;
37
+ * echoes the value back as the ``X-Correlation-ID`` response header so
38
+ clients can cross-reference their request with server-side logs.
33
39
  """
34
40
 
35
41
  HEADER = "X-Correlation-ID"
@@ -43,6 +49,8 @@ class CorrelationIdMiddleware:
43
49
  return
44
50
 
45
51
  cid = Headers(scope=scope).get(self.HEADER) or uuid.uuid4().hex
52
+ # Skip allocating a Request wrapper — downstream Request(scope).state reads this same dict.
53
+ scope.setdefault("state", {})["correlation_id"] = cid
46
54
 
47
55
  async def send_with_header(message: Message) -> None:
48
56
  if message["type"] == _MSG_RESPONSE_START:
@@ -12,6 +12,7 @@ from pathlib import Path
12
12
  from typing import TYPE_CHECKING
13
13
 
14
14
  from fastapi import APIRouter, FastAPI
15
+ from fastapi.exceptions import RequestValidationError
15
16
  from fastapi.routing import APIRoute
16
17
  from fastapi.staticfiles import StaticFiles
17
18
  from inertia import (
@@ -26,6 +27,7 @@ from starlette.middleware.sessions import SessionMiddleware
26
27
  from simple_module_hosting._error_handlers import (
27
28
  http_exception_handler,
28
29
  not_found_error_handler,
30
+ request_validation_error_handler,
29
31
  unhandled_exception_handler,
30
32
  )
31
33
  from simple_module_hosting.i18n_middleware import LocaleMiddleware
@@ -53,6 +55,7 @@ def register_exception_handlers(app: FastAPI, modules: list) -> None:
53
55
  )
54
56
  app.add_exception_handler(HTTPException, http_exception_handler)
55
57
  app.add_exception_handler(NotFoundError, not_found_error_handler)
58
+ app.add_exception_handler(RequestValidationError, request_validation_error_handler)
56
59
  app.add_exception_handler(Exception, unhandled_exception_handler)
57
60
  for mod in modules:
58
61
  mod.register_exception_handlers(app)
@@ -38,6 +38,9 @@ class JsonFormatter(logging.Formatter):
38
38
  "entity",
39
39
  "entity_id",
40
40
  "db_duration_ms",
41
+ # Bound by log filters on Celery / job-runner workers.
42
+ "task_id",
43
+ "task_name",
41
44
  )
42
45
 
43
46
  def format(self, record: logging.LogRecord) -> str:
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import logging
7
+ from typing import TYPE_CHECKING
7
8
 
8
9
  import httpx
9
10
  from simple_module_hosting.logging import (
@@ -12,6 +13,13 @@ from simple_module_hosting.logging import (
12
13
  correlation_id,
13
14
  setup_logging,
14
15
  )
16
+ from simple_module_hosting.middleware import CorrelationIdMiddleware
17
+ from starlette.applications import Starlette
18
+ from starlette.responses import JSONResponse
19
+ from starlette.routing import Route
20
+
21
+ if TYPE_CHECKING:
22
+ from starlette.requests import Request
15
23
 
16
24
  # ── JsonFormatter ──────────────────────────────────────────────────────
17
25
 
@@ -174,6 +182,29 @@ class TestCorrelationIdMiddleware:
174
182
  r2 = await client.get("/health")
175
183
  assert r1.headers["x-correlation-id"] != r2.headers["x-correlation-id"]
176
184
 
185
+ async def test_state_contextvar_and_header_agree(self):
186
+ # Background tasks read the ContextVar; handlers read request.state;
187
+ # clients read the response header — all three must agree per request.
188
+ async def echo(request: Request) -> JSONResponse:
189
+ return JSONResponse(
190
+ {
191
+ "state": request.state.correlation_id,
192
+ "contextvar": correlation_id.get(""),
193
+ }
194
+ )
195
+
196
+ app = Starlette(routes=[Route("/echo", echo)])
197
+ app.add_middleware(CorrelationIdMiddleware)
198
+
199
+ transport = httpx.ASGITransport(app=app)
200
+ async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as c:
201
+ resp = await c.get("/echo", headers={CorrelationIdMiddleware.HEADER: "trace-xyz"})
202
+
203
+ assert resp.headers[CorrelationIdMiddleware.HEADER] == "trace-xyz"
204
+ body = resp.json()
205
+ assert body["state"] == "trace-xyz"
206
+ assert body["contextvar"] == "trace-xyz"
207
+
177
208
 
178
209
  # ── Request logging middleware (integration) ────────────────────────────
179
210