litestar-vite 0.1.1__py3-none-any.whl → 0.15.0__py3-none-any.whl

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 (169) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/cli.py +1048 -10
  4. litestar_vite/codegen/__init__.py +48 -0
  5. litestar_vite/codegen/_export.py +229 -0
  6. litestar_vite/codegen/_inertia.py +619 -0
  7. litestar_vite/codegen/_openapi.py +280 -0
  8. litestar_vite/codegen/_routes.py +720 -0
  9. litestar_vite/codegen/_ts.py +235 -0
  10. litestar_vite/codegen/_utils.py +141 -0
  11. litestar_vite/commands.py +73 -0
  12. litestar_vite/config/__init__.py +997 -0
  13. litestar_vite/config/_constants.py +97 -0
  14. litestar_vite/config/_deploy.py +70 -0
  15. litestar_vite/config/_inertia.py +241 -0
  16. litestar_vite/config/_paths.py +63 -0
  17. litestar_vite/config/_runtime.py +235 -0
  18. litestar_vite/config/_spa.py +93 -0
  19. litestar_vite/config/_types.py +94 -0
  20. litestar_vite/deploy.py +366 -0
  21. litestar_vite/doctor.py +1181 -0
  22. litestar_vite/exceptions.py +78 -0
  23. litestar_vite/executor.py +360 -0
  24. litestar_vite/handler/__init__.py +9 -0
  25. litestar_vite/handler/_app.py +612 -0
  26. litestar_vite/handler/_routing.py +130 -0
  27. litestar_vite/html_transform.py +569 -0
  28. litestar_vite/inertia/__init__.py +77 -0
  29. litestar_vite/inertia/_utils.py +119 -0
  30. litestar_vite/inertia/exception_handler.py +178 -0
  31. litestar_vite/inertia/helpers.py +1571 -0
  32. litestar_vite/inertia/middleware.py +54 -0
  33. litestar_vite/inertia/plugin.py +199 -0
  34. litestar_vite/inertia/precognition.py +274 -0
  35. litestar_vite/inertia/request.py +334 -0
  36. litestar_vite/inertia/response.py +802 -0
  37. litestar_vite/inertia/types.py +335 -0
  38. litestar_vite/loader.py +464 -123
  39. litestar_vite/plugin/__init__.py +687 -0
  40. litestar_vite/plugin/_process.py +185 -0
  41. litestar_vite/plugin/_proxy.py +689 -0
  42. litestar_vite/plugin/_proxy_headers.py +244 -0
  43. litestar_vite/plugin/_static.py +37 -0
  44. litestar_vite/plugin/_utils.py +489 -0
  45. litestar_vite/py.typed +0 -0
  46. litestar_vite/scaffolding/__init__.py +20 -0
  47. litestar_vite/scaffolding/generator.py +270 -0
  48. litestar_vite/scaffolding/templates.py +437 -0
  49. litestar_vite/templates/__init__.py +0 -0
  50. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  51. litestar_vite/templates/angular/index.html.j2 +12 -0
  52. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular/package.json.j2 +36 -0
  54. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  55. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  56. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  57. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  58. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  59. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  60. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  61. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  62. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  63. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  64. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  65. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  66. litestar_vite/templates/angular-cli/package.json.j2 +28 -0
  67. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  68. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  69. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  70. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  71. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  72. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  73. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  74. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  75. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  76. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  77. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  78. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  79. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  80. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  81. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  82. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  83. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  84. litestar_vite/templates/base/.gitignore.j2 +42 -0
  85. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  86. litestar_vite/templates/base/package.json.j2 +39 -0
  87. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  88. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  89. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  90. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  91. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  92. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  93. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  94. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  95. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  96. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  97. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  98. litestar_vite/templates/react/index.html.j2 +13 -0
  99. litestar_vite/templates/react/src/App.css.j2 +56 -0
  100. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  101. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  102. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  103. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  104. litestar_vite/templates/react-inertia/package.json.j2 +47 -0
  105. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  106. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  107. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  108. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  109. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  110. litestar_vite/templates/react-router/index.html.j2 +12 -0
  111. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  112. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  113. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  114. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  115. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  116. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  117. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  118. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  119. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  120. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  121. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  122. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  123. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  124. litestar_vite/templates/svelte/index.html.j2 +13 -0
  125. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  126. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  127. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  128. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  129. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  130. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  131. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  132. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  133. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  134. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  135. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  136. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  137. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  138. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  139. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  140. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  141. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  142. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  143. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  144. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  145. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  146. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  147. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  148. litestar_vite/templates/vue/index.html.j2 +13 -0
  149. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  150. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  151. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  152. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  153. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  154. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  155. litestar_vite/templates/vue-inertia/package.json.j2 +50 -0
  156. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  157. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  158. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  159. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  160. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  161. litestar_vite-0.15.0.dist-info/METADATA +230 -0
  162. litestar_vite-0.15.0.dist-info/RECORD +164 -0
  163. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/WHEEL +1 -1
  164. litestar_vite/config.py +0 -100
  165. litestar_vite/plugin.py +0 -45
  166. litestar_vite/template_engine.py +0 -103
  167. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  168. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  169. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,77 @@
1
+ from litestar_vite.config import InertiaConfig
2
+ from litestar_vite.inertia import helpers
3
+ from litestar_vite.inertia.exception_handler import create_inertia_exception_response, exception_to_http_response
4
+ from litestar_vite.inertia.helpers import (
5
+ AlwaysProp,
6
+ OnceProp,
7
+ OptionalProp,
8
+ PropFilter,
9
+ always,
10
+ clear_history,
11
+ defer,
12
+ error,
13
+ except_,
14
+ extract_deferred_props,
15
+ extract_merge_props,
16
+ extract_once_props,
17
+ flash,
18
+ get_shared_props,
19
+ lazy,
20
+ merge,
21
+ once,
22
+ only,
23
+ optional,
24
+ scroll_props,
25
+ share,
26
+ )
27
+ from litestar_vite.inertia.middleware import InertiaMiddleware
28
+ from litestar_vite.inertia.plugin import InertiaPlugin
29
+ from litestar_vite.inertia.precognition import (
30
+ PrecognitionResponse,
31
+ create_precognition_exception_handler,
32
+ normalize_validation_errors,
33
+ precognition,
34
+ )
35
+ from litestar_vite.inertia.request import InertiaDetails, InertiaHeaders, InertiaRequest
36
+ from litestar_vite.inertia.response import InertiaBack, InertiaExternalRedirect, InertiaRedirect, InertiaResponse
37
+
38
+ __all__ = (
39
+ "AlwaysProp",
40
+ "InertiaBack",
41
+ "InertiaConfig",
42
+ "InertiaDetails",
43
+ "InertiaExternalRedirect",
44
+ "InertiaHeaders",
45
+ "InertiaMiddleware",
46
+ "InertiaPlugin",
47
+ "InertiaRedirect",
48
+ "InertiaRequest",
49
+ "InertiaResponse",
50
+ "OnceProp",
51
+ "OptionalProp",
52
+ "PrecognitionResponse",
53
+ "PropFilter",
54
+ "always",
55
+ "clear_history",
56
+ "create_inertia_exception_response",
57
+ "create_precognition_exception_handler",
58
+ "defer",
59
+ "error",
60
+ "except_",
61
+ "exception_to_http_response",
62
+ "extract_deferred_props",
63
+ "extract_merge_props",
64
+ "extract_once_props",
65
+ "flash",
66
+ "get_shared_props",
67
+ "helpers",
68
+ "lazy",
69
+ "merge",
70
+ "normalize_validation_errors",
71
+ "once",
72
+ "only",
73
+ "optional",
74
+ "precognition",
75
+ "scroll_props",
76
+ "share",
77
+ )
@@ -0,0 +1,119 @@
1
+ from enum import Enum
2
+ from typing import TYPE_CHECKING, Any
3
+
4
+ if TYPE_CHECKING:
5
+ from collections.abc import Callable
6
+
7
+ from litestar_vite.inertia.types import InertiaHeaderType
8
+
9
+
10
+ class InertiaHeaders(str, Enum):
11
+ """Enum for Inertia Headers.
12
+
13
+ See: https://inertiajs.com/the-protocol
14
+
15
+ This includes both core protocol headers and v2 extensions (partial excludes, reset, error bags,
16
+ and infinite scroll merge intent).
17
+ """
18
+
19
+ ENABLED = "X-Inertia"
20
+ VERSION = "X-Inertia-Version"
21
+ LOCATION = "X-Inertia-Location"
22
+ REFERER = "Referer"
23
+
24
+ PARTIAL_DATA = "X-Inertia-Partial-Data"
25
+ PARTIAL_COMPONENT = "X-Inertia-Partial-Component"
26
+ PARTIAL_EXCEPT = "X-Inertia-Partial-Except"
27
+
28
+ RESET = "X-Inertia-Reset"
29
+ ERROR_BAG = "X-Inertia-Error-Bag"
30
+
31
+ INFINITE_SCROLL_MERGE_INTENT = "X-Inertia-Infinite-Scroll-Merge-Intent"
32
+
33
+ # Precognition headers (Laravel Precognition protocol)
34
+ PRECOGNITION = "Precognition"
35
+ PRECOGNITION_SUCCESS = "Precognition-Success"
36
+ PRECOGNITION_VALIDATE_ONLY = "Precognition-Validate-Only"
37
+
38
+
39
+ def get_enabled_header(enabled: bool = True) -> "dict[str, Any]":
40
+ """True if inertia is enabled.
41
+
42
+ Args:
43
+ enabled: Whether inertia is enabled.
44
+
45
+ Returns:
46
+ The headers for inertia.
47
+ """
48
+
49
+ return {InertiaHeaders.ENABLED.value: "true" if enabled else "false"}
50
+
51
+
52
+ def get_version_header(version: str) -> "dict[str, Any]":
53
+ """Return headers for change swap method response.
54
+
55
+ Args:
56
+ version: The version of the inertia.
57
+
58
+ Returns:
59
+ The headers for inertia.
60
+ """
61
+ return {InertiaHeaders.VERSION.value: version}
62
+
63
+
64
+ def get_partial_data_header(partial: str) -> "dict[str, Any]":
65
+ """Return headers for a partial data response.
66
+
67
+ Args:
68
+ partial: The partial data.
69
+
70
+ Returns:
71
+ The headers for inertia.
72
+ """
73
+ return {InertiaHeaders.PARTIAL_DATA.value: partial}
74
+
75
+
76
+ def get_partial_component_header(partial: str) -> "dict[str, Any]":
77
+ """Return headers for a partial data response.
78
+
79
+ Args:
80
+ partial: The partial data.
81
+
82
+ Returns:
83
+ The headers for inertia.
84
+ """
85
+ return {InertiaHeaders.PARTIAL_COMPONENT.value: partial}
86
+
87
+
88
+ def get_headers(inertia_headers: "InertiaHeaderType") -> "dict[str, Any]":
89
+ """Return headers for Inertia responses.
90
+
91
+ Args:
92
+ inertia_headers: The inertia headers.
93
+
94
+ Raises:
95
+ ValueError: If the inertia headers are None.
96
+
97
+ Returns:
98
+ The headers for inertia.
99
+ """
100
+ if not inertia_headers:
101
+ msg = "Value for inertia_headers cannot be None."
102
+ raise ValueError(msg)
103
+ inertia_headers_dict: "dict[str, Callable[..., dict[str, Any]]]" = {
104
+ "enabled": get_enabled_header,
105
+ "partial_data": get_partial_data_header,
106
+ "partial_component": get_partial_component_header,
107
+ "version": get_version_header,
108
+ }
109
+
110
+ header: "dict[str, Any]" = {}
111
+ response: "dict[str, Any]"
112
+ key: "str"
113
+ value: "Any"
114
+
115
+ for key, value in inertia_headers.items():
116
+ if value is not None:
117
+ response = inertia_headers_dict[key](value)
118
+ header.update(response)
119
+ return header
@@ -0,0 +1,178 @@
1
+ import re
2
+ from typing import TYPE_CHECKING, Any, cast
3
+ from urllib.parse import quote, urlparse, urlunparse
4
+
5
+ from litestar import MediaType
6
+ from litestar.connection import Request
7
+ from litestar.connection.base import AuthT, StateT, UserT
8
+ from litestar.exceptions import (
9
+ HTTPException,
10
+ InternalServerException,
11
+ NotAuthorizedException,
12
+ NotFoundException,
13
+ PermissionDeniedException,
14
+ )
15
+ from litestar.exceptions.responses import (
16
+ create_debug_response, # pyright: ignore[reportUnknownVariableType]
17
+ create_exception_response, # pyright: ignore[reportUnknownVariableType]
18
+ )
19
+ from litestar.repository.exceptions import (
20
+ ConflictError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
21
+ NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
22
+ RepositoryError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
23
+ )
24
+ from litestar.response import Response
25
+ from litestar.status_codes import (
26
+ HTTP_400_BAD_REQUEST,
27
+ HTTP_401_UNAUTHORIZED,
28
+ HTTP_404_NOT_FOUND,
29
+ HTTP_405_METHOD_NOT_ALLOWED,
30
+ HTTP_409_CONFLICT,
31
+ HTTP_422_UNPROCESSABLE_ENTITY,
32
+ HTTP_500_INTERNAL_SERVER_ERROR,
33
+ )
34
+
35
+ from litestar_vite.inertia.helpers import error, flash
36
+ from litestar_vite.inertia.request import InertiaRequest
37
+ from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse
38
+
39
+ if TYPE_CHECKING:
40
+ from litestar.connection import Request
41
+ from litestar.connection.base import AuthT, StateT, UserT
42
+ from litestar.response import Response
43
+
44
+ from litestar_vite.inertia.plugin import InertiaPlugin
45
+
46
+
47
+ FIELD_ERR_RE = re.compile(r"field `(.+)`$")
48
+
49
+
50
+ class _HTTPConflictException(HTTPException):
51
+ """Request conflict with the current state of the target resource."""
52
+
53
+ status_code: int = HTTP_409_CONFLICT
54
+
55
+
56
+ def exception_to_http_response(request: "Request[UserT, AuthT, StateT]", exc: "Exception") -> "Response[Any]":
57
+ """Handler for all exceptions subclassed from HTTPException.
58
+
59
+ Inertia detection:
60
+
61
+ - For InertiaRequest instances, uses the request's derived flags (route component + headers).
62
+ - For plain Request instances (e.g., before routing/when middleware didn't run), falls back
63
+ to checking the ``X-Inertia`` header.
64
+
65
+ Args:
66
+ request: The request object.
67
+ exc: The exception to handle.
68
+
69
+ Returns:
70
+ The response object.
71
+ """
72
+ is_inertia_header = request.headers.get("x-inertia", "").lower() == "true"
73
+ if isinstance(request, InertiaRequest):
74
+ inertia_enabled = request.inertia_enabled or request.is_inertia or is_inertia_header
75
+ else:
76
+ inertia_enabled = is_inertia_header
77
+
78
+ if not inertia_enabled:
79
+ if isinstance(exc, HTTPException):
80
+ return cast("Response[Any]", create_exception_response(request, exc))
81
+ if isinstance(exc, NotFoundError):
82
+ http_exc = NotFoundException
83
+ elif isinstance(exc, (RepositoryError, ConflictError)):
84
+ http_exc = _HTTPConflictException # type: ignore[assignment]
85
+ else:
86
+ http_exc = InternalServerException # type: ignore[assignment]
87
+ if request.app.debug and http_exc not in {PermissionDeniedException, NotFoundError}:
88
+ return cast("Response[Any]", create_debug_response(request, exc))
89
+ return cast("Response[Any]", create_exception_response(request, http_exc(detail=str(exc.__cause__)))) # pyright: ignore[reportUnknownArgumentType]
90
+ return create_inertia_exception_response(request, exc)
91
+
92
+
93
+ def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]", exc: "Exception") -> "Response[Any]":
94
+ """Create the inertia exception response.
95
+
96
+ This function handles exceptions for Inertia-enabled routes, returning appropriate
97
+ responses based on the exception type and status code.
98
+
99
+ Note:
100
+ This function uses defensive programming techniques to handle edge cases:
101
+ - Type-safe handling of exception ``extra`` attribute (may be string, list, dict, or None)
102
+ - Graceful handling when InertiaPlugin is not registered
103
+ - Broad exception handling for flash() calls (non-critical operation)
104
+
105
+ Args:
106
+ request: The request object.
107
+ exc: The exception to handle.
108
+
109
+ Returns:
110
+ The response object, either an InertiaResponse, InertiaRedirect, or InertiaBack.
111
+ """
112
+ is_inertia_header = request.headers.get("x-inertia", "").lower() == "true"
113
+ is_inertia = request.is_inertia if isinstance(request, InertiaRequest) else is_inertia_header
114
+
115
+ status_code = exc.status_code if isinstance(exc, HTTPException) else HTTP_500_INTERNAL_SERVER_ERROR
116
+ preferred_type = MediaType.HTML if not is_inertia else MediaType.JSON
117
+ detail = exc.detail if isinstance(exc, HTTPException) else str(exc)
118
+ extras: Any = None
119
+ if isinstance(exc, HTTPException):
120
+ try:
121
+ extras = exc.extra # pyright: ignore[reportUnknownMemberType]
122
+ except AttributeError:
123
+ extras = None
124
+ content: dict[str, Any] = {"status_code": status_code, "message": detail}
125
+
126
+ inertia_plugin: "InertiaPlugin | None"
127
+ try:
128
+ inertia_plugin = request.app.plugins.get("InertiaPlugin")
129
+ except KeyError:
130
+ inertia_plugin = None
131
+
132
+ if extras:
133
+ content.update({"extra": extras})
134
+
135
+ flash_succeeded = False
136
+ if detail:
137
+ flash_succeeded = flash(request, detail, category="error")
138
+
139
+ if extras and isinstance(extras, (list, tuple)) and len(extras) >= 1: # pyright: ignore[reportUnknownArgumentType]
140
+ first_extra = extras[0] # pyright: ignore[reportUnknownVariableType]
141
+ if isinstance(first_extra, dict):
142
+ message: dict[str, str] = cast("dict[str, str]", first_extra)
143
+ key_value = message.get("key")
144
+ default_field = f"root.{key_value}" if key_value is not None else "root"
145
+ error_detail = str(message.get("message", detail) or detail)
146
+ match = FIELD_ERR_RE.search(error_detail)
147
+ field = match.group(1) if match else default_field
148
+ error(request, field, error_detail or detail)
149
+
150
+ if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST} or isinstance(
151
+ exc, PermissionDeniedException
152
+ ):
153
+ return InertiaBack(request)
154
+
155
+ if inertia_plugin is None:
156
+ return InertiaResponse[Any](media_type=preferred_type, content=content, status_code=status_code)
157
+
158
+ is_unauthorized = status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException)
159
+ redirect_to_login = inertia_plugin.config.redirect_unauthorized_to
160
+ if is_unauthorized and redirect_to_login is not None:
161
+ if request.url.path != redirect_to_login:
162
+ # If flash failed (no session), pass error message via query param
163
+ if not flash_succeeded and detail:
164
+ parsed = urlparse(redirect_to_login)
165
+ error_param = f"error={quote(detail, safe='')}"
166
+ query = f"{parsed.query}&{error_param}" if parsed.query else error_param
167
+ redirect_to_login = urlunparse(parsed._replace(query=query))
168
+ return InertiaRedirect(request, redirect_to=redirect_to_login)
169
+ # Already on login page - redirect back so Inertia processes flash messages
170
+ # (Inertia.js shows 4xx responses in a modal instead of updating page state)
171
+ return InertiaBack(request)
172
+
173
+ if status_code in {HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED} and (
174
+ inertia_plugin.config.redirect_404 is not None and request.url.path != inertia_plugin.config.redirect_404
175
+ ):
176
+ return InertiaRedirect(request, redirect_to=inertia_plugin.config.redirect_404)
177
+
178
+ return InertiaResponse[Any](media_type=preferred_type, content=content, status_code=status_code)