litestar-vite 0.1.1__py3-none-any.whl → 0.15.0rc2__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 (154) hide show
  1. litestar_vite/__init__.py +54 -4
  2. litestar_vite/__metadata__.py +12 -7
  3. litestar_vite/_codegen/__init__.py +26 -0
  4. litestar_vite/_codegen/inertia.py +407 -0
  5. litestar_vite/_codegen/openapi.py +233 -0
  6. litestar_vite/_codegen/routes.py +653 -0
  7. litestar_vite/_codegen/ts.py +235 -0
  8. litestar_vite/_handler/__init__.py +8 -0
  9. litestar_vite/_handler/app.py +524 -0
  10. litestar_vite/_handler/routing.py +130 -0
  11. litestar_vite/cli.py +1147 -10
  12. litestar_vite/codegen.py +39 -0
  13. litestar_vite/commands.py +79 -0
  14. litestar_vite/config.py +1594 -70
  15. litestar_vite/deploy.py +355 -0
  16. litestar_vite/doctor.py +1179 -0
  17. litestar_vite/exceptions.py +78 -0
  18. litestar_vite/executor.py +316 -0
  19. litestar_vite/handler.py +9 -0
  20. litestar_vite/html_transform.py +426 -0
  21. litestar_vite/inertia/__init__.py +53 -0
  22. litestar_vite/inertia/_utils.py +114 -0
  23. litestar_vite/inertia/exception_handler.py +172 -0
  24. litestar_vite/inertia/helpers.py +1043 -0
  25. litestar_vite/inertia/middleware.py +54 -0
  26. litestar_vite/inertia/plugin.py +133 -0
  27. litestar_vite/inertia/request.py +286 -0
  28. litestar_vite/inertia/response.py +706 -0
  29. litestar_vite/inertia/types.py +316 -0
  30. litestar_vite/loader.py +462 -121
  31. litestar_vite/plugin.py +2160 -21
  32. litestar_vite/py.typed +0 -0
  33. litestar_vite/scaffolding/__init__.py +20 -0
  34. litestar_vite/scaffolding/generator.py +270 -0
  35. litestar_vite/scaffolding/templates.py +437 -0
  36. litestar_vite/templates/__init__.py +0 -0
  37. litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
  38. litestar_vite/templates/angular/index.html.j2 +12 -0
  39. litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
  40. litestar_vite/templates/angular/package.json.j2 +35 -0
  41. litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
  42. litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
  43. litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
  44. litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
  45. litestar_vite/templates/angular/src/main.ts.j2 +9 -0
  46. litestar_vite/templates/angular/src/styles.css.j2 +9 -0
  47. litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
  48. litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
  49. litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
  50. litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
  51. litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
  52. litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
  53. litestar_vite/templates/angular-cli/package.json.j2 +27 -0
  54. litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
  55. litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
  56. litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
  57. litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
  58. litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
  59. litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
  60. litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
  61. litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
  62. litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
  63. litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
  64. litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
  65. litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
  66. litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
  67. litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
  68. litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
  69. litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
  70. litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
  71. litestar_vite/templates/base/.gitignore.j2 +42 -0
  72. litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
  73. litestar_vite/templates/base/package.json.j2 +38 -0
  74. litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
  75. litestar_vite/templates/base/tsconfig.json.j2 +37 -0
  76. litestar_vite/templates/htmx/src/main.js.j2 +8 -0
  77. litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
  78. litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
  79. litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
  80. litestar_vite/templates/nuxt/app.vue.j2 +29 -0
  81. litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
  82. litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
  83. litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
  84. litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
  85. litestar_vite/templates/react/index.html.j2 +13 -0
  86. litestar_vite/templates/react/src/App.css.j2 +56 -0
  87. litestar_vite/templates/react/src/App.tsx.j2 +19 -0
  88. litestar_vite/templates/react/src/main.tsx.j2 +10 -0
  89. litestar_vite/templates/react/vite.config.ts.j2 +39 -0
  90. litestar_vite/templates/react-inertia/index.html.j2 +14 -0
  91. litestar_vite/templates/react-inertia/package.json.j2 +46 -0
  92. litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
  93. litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
  94. litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
  95. litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
  96. litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
  97. litestar_vite/templates/react-router/index.html.j2 +12 -0
  98. litestar_vite/templates/react-router/src/App.css.j2 +17 -0
  99. litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
  100. litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
  101. litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
  102. litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
  103. litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
  104. litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
  105. litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
  106. litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
  107. litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
  108. litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
  109. litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
  110. litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
  111. litestar_vite/templates/svelte/index.html.j2 +13 -0
  112. litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
  113. litestar_vite/templates/svelte/src/app.css.j2 +45 -0
  114. litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
  115. litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
  116. litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
  117. litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
  118. litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
  119. litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
  120. litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
  121. litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
  122. litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
  123. litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
  124. litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
  125. litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
  126. litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
  127. litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
  128. litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
  129. litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
  130. litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
  131. litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
  132. litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
  133. litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
  134. litestar_vite/templates/vue/env.d.ts.j2 +7 -0
  135. litestar_vite/templates/vue/index.html.j2 +13 -0
  136. litestar_vite/templates/vue/src/App.vue.j2 +28 -0
  137. litestar_vite/templates/vue/src/main.ts.j2 +5 -0
  138. litestar_vite/templates/vue/src/style.css.j2 +45 -0
  139. litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
  140. litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
  141. litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
  142. litestar_vite/templates/vue-inertia/package.json.j2 +49 -0
  143. litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
  144. litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
  145. litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
  146. litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
  147. litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
  148. litestar_vite-0.15.0rc2.dist-info/METADATA +230 -0
  149. litestar_vite-0.15.0rc2.dist-info/RECORD +151 -0
  150. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +1 -1
  151. litestar_vite/template_engine.py +0 -103
  152. litestar_vite-0.1.1.dist-info/METADATA +0 -68
  153. litestar_vite-0.1.1.dist-info/RECORD +0 -11
  154. {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,54 @@
1
+ from typing import TYPE_CHECKING, Any
2
+
3
+ from litestar.middleware import AbstractMiddleware
4
+ from litestar.types import Receive, Scope, Send
5
+
6
+ from litestar_vite.inertia.request import InertiaRequest
7
+ from litestar_vite.inertia.response import InertiaExternalRedirect
8
+ from litestar_vite.plugin import VitePlugin
9
+
10
+ if TYPE_CHECKING:
11
+ from litestar.types import ASGIApp, Receive, Scope, Send
12
+
13
+
14
+ def redirect_on_asset_version_mismatch(request: "InertiaRequest[Any, Any, Any]") -> "InertiaExternalRedirect | None":
15
+ """Return redirect response when client and server asset versions differ.
16
+
17
+ Returns:
18
+ An InertiaExternalRedirect when versions differ, otherwise None.
19
+ """
20
+ if not request.is_inertia:
21
+ return None
22
+
23
+ inertia_version = request.inertia_version
24
+ if inertia_version is None:
25
+ return None
26
+
27
+ vite_plugin = request.app.plugins.get(VitePlugin)
28
+ if inertia_version == vite_plugin.asset_loader.version_id:
29
+ return None
30
+
31
+ return InertiaExternalRedirect(request, redirect_to=str(request.url))
32
+
33
+
34
+ class InertiaMiddleware(AbstractMiddleware):
35
+ """Middleware for handling Inertia.js protocol requirements.
36
+
37
+ This middleware:
38
+ 1. Detects version mismatches between client and server assets
39
+ 2. Returns 409 Conflict with X-Inertia-Location header when versions differ
40
+ 3. Triggers client-side hard refresh to reload the updated assets
41
+ """
42
+
43
+ def __init__(self, app: "ASGIApp") -> None:
44
+ super().__init__(app)
45
+ self.app = app
46
+
47
+ async def __call__(self, scope: "Scope", receive: "Receive", send: "Send") -> None:
48
+ request: InertiaRequest[Any, Any, Any] = InertiaRequest(scope=scope)
49
+ redirect = redirect_on_asset_version_mismatch(request)
50
+ if redirect is not None:
51
+ response = redirect.to_asgi_response(app=None, request=request) # pyright: ignore[reportUnknownMemberType]
52
+ await response(scope, receive, send)
53
+ else:
54
+ await self.app(scope, receive, send)
@@ -0,0 +1,133 @@
1
+ from contextlib import asynccontextmanager
2
+ from typing import TYPE_CHECKING
3
+
4
+ from anyio.from_thread import start_blocking_portal
5
+ from litestar.plugins import InitPluginProtocol
6
+
7
+ if TYPE_CHECKING:
8
+ from collections.abc import AsyncGenerator
9
+
10
+ from anyio.from_thread import BlockingPortal
11
+ from litestar import Litestar
12
+ from litestar.config.app import AppConfig
13
+
14
+ from litestar_vite.config import InertiaConfig
15
+
16
+
17
+ class InertiaPlugin(InitPluginProtocol):
18
+ """Inertia plugin.
19
+
20
+ This plugin configures Litestar for Inertia.js support, including:
21
+ - Session middleware requirement validation
22
+ - Exception handler for Inertia responses
23
+ - InertiaRequest and InertiaResponse as default classes
24
+ - Type encoders for StaticProp and DeferredProp
25
+
26
+ BlockingPortal Behavior:
27
+ The plugin creates a BlockingPortal during its lifespan for executing
28
+ async DeferredProp callbacks from synchronous type encoders. This is
29
+ necessary because Litestar's JSON serialization happens synchronously,
30
+ but DeferredProp may contain async callables.
31
+
32
+ The portal is shared across all requests during the app's lifetime.
33
+ Type encoders for StaticProp and DeferredProp use ``val.render()``
34
+ which may access this portal for async resolution.
35
+
36
+ If you're using DeferredProp outside of InertiaResponse (e.g., in
37
+ custom serialization), ensure the app lifespan is active and the
38
+ portal is available via ``inertia_plugin.portal``.
39
+
40
+ Example::
41
+
42
+ from litestar_vite.inertia import InertiaPlugin, InertiaConfig
43
+
44
+ app = Litestar(
45
+ plugins=[InertiaPlugin(InertiaConfig())],
46
+ middleware=[ServerSideSessionConfig().middleware],
47
+ )
48
+ """
49
+
50
+ __slots__ = ("_portal", "config")
51
+
52
+ def __init__(self, config: "InertiaConfig") -> "None":
53
+ """Initialize the plugin with Inertia configuration."""
54
+ self.config = config
55
+
56
+ @asynccontextmanager
57
+ async def lifespan(self, app: "Litestar") -> "AsyncGenerator[None, None]":
58
+ """Lifespan to ensure the event loop is available.
59
+
60
+ Args:
61
+ app: The :class:`Litestar <litestar.app.Litestar>` instance.
62
+
63
+ Yields:
64
+ An asynchronous context manager.
65
+ """
66
+
67
+ with start_blocking_portal() as portal:
68
+ self._portal = portal
69
+ yield
70
+
71
+ @property
72
+ def portal(self) -> "BlockingPortal":
73
+ """Return the blocking portal used for deferred prop resolution.
74
+
75
+ Returns:
76
+ The BlockingPortal instance.
77
+ """
78
+ return self._portal
79
+
80
+ def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
81
+ """Configure application for use with Vite.
82
+
83
+ Args:
84
+ app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
85
+
86
+ Raises:
87
+ ImproperlyConfiguredException: If the Inertia plugin is not properly configured.
88
+
89
+ Returns:
90
+ The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
91
+ """
92
+
93
+ from litestar.exceptions import ImproperlyConfiguredException
94
+ from litestar.middleware import DefineMiddleware
95
+ from litestar.middleware.session import SessionMiddleware
96
+ from litestar.security.session_auth.middleware import MiddlewareWrapper
97
+ from litestar.utils.predicates import is_class_and_subclass
98
+
99
+ from litestar_vite.inertia.exception_handler import exception_to_http_response
100
+ from litestar_vite.inertia.helpers import DeferredProp, StaticProp
101
+ from litestar_vite.inertia.middleware import InertiaMiddleware
102
+ from litestar_vite.inertia.request import InertiaRequest
103
+ from litestar_vite.inertia.response import InertiaBack, InertiaResponse
104
+
105
+ for mw in app_config.middleware:
106
+ if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
107
+ mw.middleware, (MiddlewareWrapper, SessionMiddleware)
108
+ ):
109
+ break
110
+ else:
111
+ msg = "The Inertia plugin require a session middleware."
112
+ raise ImproperlyConfiguredException(msg)
113
+ from litestar.exceptions import HTTPException
114
+
115
+ app_config.exception_handlers.update( # pyright: ignore[reportUnknownMemberType]
116
+ {Exception: exception_to_http_response, HTTPException: exception_to_http_response}
117
+ )
118
+ app_config.request_class = InertiaRequest
119
+ app_config.response_class = InertiaResponse
120
+ app_config.middleware.append(InertiaMiddleware)
121
+ app_config.signature_types.extend([InertiaRequest, InertiaResponse, InertiaBack, StaticProp, DeferredProp])
122
+ app_config.type_encoders = {
123
+ StaticProp: lambda val: val.render(),
124
+ DeferredProp: lambda val: val.render(),
125
+ **(app_config.type_encoders or {}),
126
+ }
127
+ app_config.type_decoders = [
128
+ (lambda x: x is StaticProp, lambda t, v: t(v)),
129
+ (lambda x: x is DeferredProp, lambda t, v: t(v)),
130
+ *(app_config.type_decoders or []),
131
+ ]
132
+ app_config.lifespan.append(self.lifespan) # pyright: ignore[reportUnknownMemberType]
133
+ return app_config
@@ -0,0 +1,286 @@
1
+ from functools import cached_property
2
+ from typing import TYPE_CHECKING, cast
3
+ from urllib.parse import unquote
4
+
5
+ from litestar import Request
6
+ from litestar.connection.base import AuthT, StateT, UserT, empty_receive, empty_send
7
+
8
+ from litestar_vite.inertia._utils import InertiaHeaders
9
+
10
+ if TYPE_CHECKING:
11
+ from litestar.types import Receive, Scope, Send
12
+
13
+ from litestar_vite.inertia.plugin import InertiaPlugin
14
+
15
+ __all__ = ("InertiaDetails", "InertiaHeaders", "InertiaRequest")
16
+
17
+ _DEFAULT_COMPONENT_OPT_KEYS: "tuple[str, ...]" = ("component", "page")
18
+
19
+
20
+ class InertiaDetails:
21
+ """InertiaDetails holds all the values sent by Inertia client in headers and provide convenient properties."""
22
+
23
+ def __init__(self, request: "Request[UserT, AuthT, StateT]") -> None:
24
+ """Initialize :class:`InertiaDetails`"""
25
+ self.request = request
26
+
27
+ def _get_header_value(self, name: "InertiaHeaders") -> "str | None":
28
+ """Parse request header
29
+
30
+ Check for uri encoded header and unquotes it in readable format.
31
+
32
+ Args:
33
+ name: The header name.
34
+
35
+ Returns:
36
+ The header value.
37
+ """
38
+
39
+ if value := self.request.headers.get(name.value.lower()):
40
+ is_uri_encoded = self.request.headers.get(f"{name.value.lower()}-uri-autoencoded") == "true"
41
+ return unquote(value) if is_uri_encoded else value
42
+ return None
43
+
44
+ def _get_route_component(self) -> "str | None":
45
+ """Return the route component from handler opts if present.
46
+
47
+ Returns:
48
+ The route component name, or None if not configured on the handler.
49
+ """
50
+ rh = self.request.scope.get("route_handler") # pyright: ignore[reportUnknownMemberType]
51
+ if rh:
52
+ component_opt_keys: "tuple[str, ...]" = _DEFAULT_COMPONENT_OPT_KEYS
53
+ try:
54
+ inertia_plugin: "InertiaPlugin" = self.request.app.plugins.get("InertiaPlugin")
55
+ component_opt_keys = inertia_plugin.config.component_opt_keys
56
+ except KeyError:
57
+ pass
58
+
59
+ for key in component_opt_keys:
60
+ if (value := rh.opt.get(key)) is not None:
61
+ return cast("str", value)
62
+ return None
63
+
64
+ def __bool__(self) -> bool:
65
+ """Return True when the request is sent by an Inertia client.
66
+
67
+ Returns:
68
+ True if the request originated from an Inertia client, otherwise False.
69
+ """
70
+ return self._get_header_value(InertiaHeaders.ENABLED) == "true"
71
+
72
+ @cached_property
73
+ def route_component(self) -> "str | None":
74
+ """Return the route component name.
75
+
76
+ Returns:
77
+ The route component name, or None if not configured.
78
+ """
79
+ return self._get_route_component()
80
+
81
+ @cached_property
82
+ def partial_component(self) -> "str | None":
83
+ """Return the partial component name from headers.
84
+
85
+ Returns:
86
+ The partial component name, or None if not present.
87
+ """
88
+ return self._get_header_value(InertiaHeaders.PARTIAL_COMPONENT)
89
+
90
+ @cached_property
91
+ def partial_data(self) -> "str | None":
92
+ """Return partial-data keys requested by the client.
93
+
94
+ Returns:
95
+ Comma-separated partial-data keys, or None if not present.
96
+ """
97
+ return self._get_header_value(InertiaHeaders.PARTIAL_DATA)
98
+
99
+ @cached_property
100
+ def partial_except(self) -> "str | None":
101
+ """Return partial-except keys requested by the client.
102
+
103
+ Returns:
104
+ Comma-separated partial-except keys, or None if not present.
105
+ """
106
+ return self._get_header_value(InertiaHeaders.PARTIAL_EXCEPT)
107
+
108
+ @cached_property
109
+ def reset_props(self) -> "str | None":
110
+ """Return comma-separated props to reset on navigation.
111
+
112
+ Returns:
113
+ Comma-separated prop keys to reset, or None if not present.
114
+ """
115
+ return self._get_header_value(InertiaHeaders.RESET)
116
+
117
+ @cached_property
118
+ def error_bag(self) -> "str | None":
119
+ """Return the error bag name for scoped validation errors.
120
+
121
+ Returns:
122
+ The error bag name, or None if not present.
123
+ """
124
+ return self._get_header_value(InertiaHeaders.ERROR_BAG)
125
+
126
+ @cached_property
127
+ def merge_intent(self) -> "str | None":
128
+ """Return infinite-scroll merge intent (append/prepend).
129
+
130
+ Returns:
131
+ The merge intent string, or None if not present.
132
+ """
133
+ return self._get_header_value(InertiaHeaders.INFINITE_SCROLL_MERGE_INTENT)
134
+
135
+ @cached_property
136
+ def version(self) -> "str | None":
137
+ """Return the Inertia asset version sent by the client.
138
+
139
+ Returns:
140
+ The version string, or None if not present.
141
+ """
142
+ return self._get_header_value(InertiaHeaders.VERSION)
143
+
144
+ @cached_property
145
+ def referer(self) -> "str | None":
146
+ """Return the referer value if present.
147
+
148
+ Returns:
149
+ The referer value, or None if not present.
150
+ """
151
+ return self._get_header_value(InertiaHeaders.REFERER)
152
+
153
+ @cached_property
154
+ def is_partial_render(self) -> bool:
155
+ """Return True when the request is a partial render.
156
+
157
+ Returns:
158
+ True if the request is a partial render, otherwise False.
159
+ """
160
+ return bool(self.partial_component == self.route_component and (self.partial_data or self.partial_except))
161
+
162
+ @cached_property
163
+ def partial_keys(self) -> list[str]:
164
+ """Return parsed partial-data keys.
165
+
166
+ Returns:
167
+ Parsed partial-data keys.
168
+ """
169
+ return self.partial_data.split(",") if self.partial_data is not None else []
170
+
171
+ @cached_property
172
+ def partial_except_keys(self) -> list[str]:
173
+ """Return parsed partial-except keys (takes precedence over partial_keys).
174
+
175
+ Returns:
176
+ Parsed partial-except keys.
177
+ """
178
+ return self.partial_except.split(",") if self.partial_except is not None else []
179
+
180
+ @cached_property
181
+ def reset_keys(self) -> list[str]:
182
+ """Return parsed reset keys from headers.
183
+
184
+ Returns:
185
+ Parsed reset keys.
186
+ """
187
+ return self.reset_props.split(",") if self.reset_props is not None else []
188
+
189
+
190
+ class InertiaRequest(Request[UserT, AuthT, StateT]):
191
+ """Inertia Request class to work with Inertia client."""
192
+
193
+ __slots__ = ("inertia",)
194
+
195
+ def __init__(self, scope: "Scope", receive: "Receive" = empty_receive, send: "Send" = empty_send) -> None:
196
+ """Initialize :class:`InertiaRequest`"""
197
+ super().__init__(scope=scope, receive=receive, send=send)
198
+ self.inertia = InertiaDetails(self)
199
+
200
+ @property
201
+ def is_inertia(self) -> bool:
202
+ """True if the request contained inertia headers.
203
+
204
+ Returns:
205
+ True if the request contains Inertia headers, otherwise False.
206
+ """
207
+ return bool(self.inertia)
208
+
209
+ @property
210
+ def inertia_enabled(self) -> bool:
211
+ """True if the route handler contains an inertia enabled configuration.
212
+
213
+ Returns:
214
+ True if the route is configured with an Inertia component, otherwise False.
215
+ """
216
+ return bool(self.inertia.route_component is not None)
217
+
218
+ @property
219
+ def is_partial_render(self) -> bool:
220
+ """True if the request is a partial reload.
221
+
222
+ Returns:
223
+ True if the request is a partial reload, otherwise False.
224
+ """
225
+ return self.inertia.is_partial_render
226
+
227
+ @property
228
+ def partial_keys(self) -> "set[str]":
229
+ """Get the props to include in partial render.
230
+
231
+ Returns:
232
+ A set of prop keys to include.
233
+ """
234
+ return set(self.inertia.partial_keys)
235
+
236
+ @property
237
+ def partial_except_keys(self) -> "set[str]":
238
+ """Get the props to exclude from partial render (v2).
239
+
240
+ Takes precedence over partial_keys if both present.
241
+
242
+ Returns:
243
+ A set of prop keys to exclude.
244
+ """
245
+ return set(self.inertia.partial_except_keys)
246
+
247
+ @property
248
+ def reset_keys(self) -> "set[str]":
249
+ """Get the props to reset on navigation (v2).
250
+
251
+ Returns:
252
+ A set of prop keys to reset.
253
+ """
254
+ return set(self.inertia.reset_keys)
255
+
256
+ @property
257
+ def error_bag(self) -> "str | None":
258
+ """Get the error bag name for scoped validation errors (v2).
259
+
260
+ Returns:
261
+ The error bag name, or None if not present.
262
+ """
263
+ return self.inertia.error_bag
264
+
265
+ @property
266
+ def merge_intent(self) -> "str | None":
267
+ """Get the infinite scroll merge intent (v2).
268
+
269
+ Returns 'append' or 'prepend' for infinite scroll merging.
270
+
271
+ Returns:
272
+ The merge intent string, or None if not present.
273
+ """
274
+ return self.inertia.merge_intent
275
+
276
+ @property
277
+ def inertia_version(self) -> "str | None":
278
+ """Get the Inertia asset version sent by the client.
279
+
280
+ The client sends this header so the server can detect version mismatches
281
+ and trigger a hard refresh when assets have changed.
282
+
283
+ Returns:
284
+ The version string sent by the client, or None if not present.
285
+ """
286
+ return self.inertia.version