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.
- litestar_vite/__init__.py +54 -4
- litestar_vite/__metadata__.py +12 -7
- litestar_vite/_codegen/__init__.py +26 -0
- litestar_vite/_codegen/inertia.py +407 -0
- litestar_vite/_codegen/openapi.py +233 -0
- litestar_vite/_codegen/routes.py +653 -0
- litestar_vite/_codegen/ts.py +235 -0
- litestar_vite/_handler/__init__.py +8 -0
- litestar_vite/_handler/app.py +524 -0
- litestar_vite/_handler/routing.py +130 -0
- litestar_vite/cli.py +1147 -10
- litestar_vite/codegen.py +39 -0
- litestar_vite/commands.py +79 -0
- litestar_vite/config.py +1594 -70
- litestar_vite/deploy.py +355 -0
- litestar_vite/doctor.py +1179 -0
- litestar_vite/exceptions.py +78 -0
- litestar_vite/executor.py +316 -0
- litestar_vite/handler.py +9 -0
- litestar_vite/html_transform.py +426 -0
- litestar_vite/inertia/__init__.py +53 -0
- litestar_vite/inertia/_utils.py +114 -0
- litestar_vite/inertia/exception_handler.py +172 -0
- litestar_vite/inertia/helpers.py +1043 -0
- litestar_vite/inertia/middleware.py +54 -0
- litestar_vite/inertia/plugin.py +133 -0
- litestar_vite/inertia/request.py +286 -0
- litestar_vite/inertia/response.py +706 -0
- litestar_vite/inertia/types.py +316 -0
- litestar_vite/loader.py +462 -121
- litestar_vite/plugin.py +2160 -21
- litestar_vite/py.typed +0 -0
- litestar_vite/scaffolding/__init__.py +20 -0
- litestar_vite/scaffolding/generator.py +270 -0
- litestar_vite/scaffolding/templates.py +437 -0
- litestar_vite/templates/__init__.py +0 -0
- litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
- litestar_vite/templates/angular/index.html.j2 +12 -0
- litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular/package.json.j2 +35 -0
- litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular/src/main.ts.j2 +9 -0
- litestar_vite/templates/angular/src/styles.css.j2 +9 -0
- litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
- litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
- litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
- litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
- litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
- litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular-cli/package.json.j2 +27 -0
- litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
- litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
- litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
- litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
- litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
- litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
- litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
- litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
- litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
- litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
- litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
- litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
- litestar_vite/templates/base/.gitignore.j2 +42 -0
- litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/base/package.json.j2 +38 -0
- litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
- litestar_vite/templates/base/tsconfig.json.j2 +37 -0
- litestar_vite/templates/htmx/src/main.js.j2 +8 -0
- litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
- litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
- litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
- litestar_vite/templates/nuxt/app.vue.j2 +29 -0
- litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
- litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
- litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
- litestar_vite/templates/react/index.html.j2 +13 -0
- litestar_vite/templates/react/src/App.css.j2 +56 -0
- litestar_vite/templates/react/src/App.tsx.j2 +19 -0
- litestar_vite/templates/react/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-inertia/index.html.j2 +14 -0
- litestar_vite/templates/react-inertia/package.json.j2 +46 -0
- litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
- litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
- litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
- litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
- litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
- litestar_vite/templates/react-router/index.html.j2 +12 -0
- litestar_vite/templates/react-router/src/App.css.j2 +17 -0
- litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
- litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
- litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
- litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
- litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
- litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte/index.html.j2 +13 -0
- litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
- litestar_vite/templates/svelte/src/app.css.j2 +45 -0
- litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
- litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
- litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
- litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
- litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
- litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
- litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
- litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
- litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
- litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
- litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
- litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
- litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
- litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
- litestar_vite/templates/vue/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue/index.html.j2 +13 -0
- litestar_vite/templates/vue/src/App.vue.j2 +28 -0
- litestar_vite/templates/vue/src/main.ts.j2 +5 -0
- litestar_vite/templates/vue/src/style.css.j2 +45 -0
- litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
- litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
- litestar_vite/templates/vue-inertia/package.json.j2 +49 -0
- litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
- litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
- litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
- litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
- litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
- litestar_vite-0.15.0rc2.dist-info/METADATA +230 -0
- litestar_vite-0.15.0rc2.dist-info/RECORD +151 -0
- {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +1 -1
- litestar_vite/template_engine.py +0 -103
- litestar_vite-0.1.1.dist-info/METADATA +0 -68
- litestar_vite-0.1.1.dist-info/RECORD +0 -11
- {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
|