litestar-vite 0.12.1__py3-none-any.whl → 0.13.1__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.

Potentially problematic release.


This version of litestar-vite might be problematic. Click here for more details.

@@ -0,0 +1,109 @@
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.inertia.config import InertiaConfig
15
+
16
+
17
+ def set_js_routes(app: "Litestar") -> "None":
18
+ """Generate the route structure of the application on startup."""
19
+ from litestar_vite.inertia.routes import generate_js_routes
20
+
21
+ js_routes = generate_js_routes(app)
22
+ app.state.js_routes = js_routes
23
+
24
+
25
+ class InertiaPlugin(InitPluginProtocol):
26
+ """Inertia plugin."""
27
+
28
+ __slots__ = ("_portal", "config")
29
+
30
+ def __init__(self, config: "InertiaConfig") -> "None":
31
+ """Initialize ``Inertia``.
32
+
33
+ Args:
34
+ config: Inertia configuration.
35
+ """
36
+ self.config = config
37
+
38
+ @asynccontextmanager
39
+ async def lifespan(self, app: "Litestar") -> "AsyncGenerator[None, None]":
40
+ """Lifespan to ensure the event loop is available.
41
+
42
+ Args:
43
+ app: The :class:`Litestar <litestar.app.Litestar>` instance.
44
+
45
+ Yields:
46
+ An asynchronous context manager.
47
+ """
48
+
49
+ with start_blocking_portal() as portal:
50
+ self._portal = portal
51
+ yield
52
+
53
+ @property
54
+ def portal(self) -> "BlockingPortal":
55
+ """Get the portal."""
56
+ return self._portal
57
+
58
+ def on_app_init(self, app_config: "AppConfig") -> "AppConfig":
59
+ """Configure application for use with Vite.
60
+
61
+ Args:
62
+ app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
63
+
64
+ Raises:
65
+ ImproperlyConfiguredException: If the Inertia plugin is not properly configured.
66
+
67
+ Returns:
68
+ The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
69
+ """
70
+
71
+ from litestar.exceptions import ImproperlyConfiguredException
72
+ from litestar.middleware import DefineMiddleware
73
+ from litestar.middleware.session import SessionMiddleware
74
+ from litestar.security.session_auth.middleware import MiddlewareWrapper
75
+ from litestar.utils.predicates import is_class_and_subclass
76
+
77
+ from litestar_vite.inertia.exception_handler import exception_to_http_response
78
+ from litestar_vite.inertia.helpers import DeferredProp, StaticProp
79
+ from litestar_vite.inertia.middleware import InertiaMiddleware
80
+ from litestar_vite.inertia.request import InertiaRequest
81
+ from litestar_vite.inertia.response import InertiaBack, InertiaResponse
82
+
83
+ for mw in app_config.middleware:
84
+ if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
85
+ mw.middleware,
86
+ (MiddlewareWrapper, SessionMiddleware),
87
+ ):
88
+ break
89
+ else:
90
+ msg = "The Inertia plugin require a session middleware."
91
+ raise ImproperlyConfiguredException(msg)
92
+ app_config.exception_handlers.update({Exception: exception_to_http_response}) # pyright: ignore[reportUnknownMemberType]
93
+ app_config.request_class = InertiaRequest
94
+ app_config.response_class = InertiaResponse
95
+ app_config.middleware.append(InertiaMiddleware)
96
+ app_config.on_startup.append(set_js_routes)
97
+ app_config.signature_types.extend([InertiaRequest, InertiaResponse, InertiaBack, StaticProp, DeferredProp])
98
+ app_config.type_encoders = {
99
+ StaticProp: lambda val: val.render(),
100
+ DeferredProp: lambda val: val.render(),
101
+ **(app_config.type_encoders or {}),
102
+ }
103
+ app_config.type_decoders = [
104
+ (lambda x: x is StaticProp, lambda t, v: t(v)),
105
+ (lambda x: x is DeferredProp, lambda t, v: t(v)),
106
+ *(app_config.type_decoders or []),
107
+ ]
108
+ app_config.lifespan.append(self.lifespan) # pyright: ignore[reportUnknownMemberType]
109
+ return app_config
@@ -0,0 +1,154 @@
1
+ from functools import cached_property
2
+ from typing import TYPE_CHECKING, Optional
3
+ from urllib.parse import unquote
4
+
5
+ from litestar import Request
6
+ from litestar.connection.base import (
7
+ AuthT,
8
+ StateT,
9
+ UserT,
10
+ empty_receive,
11
+ empty_send,
12
+ )
13
+
14
+ from litestar_vite.inertia._utils import InertiaHeaders
15
+
16
+ __all__ = ("InertiaDetails", "InertiaRequest")
17
+
18
+
19
+ if TYPE_CHECKING:
20
+ from litestar.types import Receive, Scope, Send
21
+
22
+
23
+ class InertiaDetails:
24
+ """InertiaDetails holds all the values sent by Inertia client in headers and provide convenient properties."""
25
+
26
+ def __init__(self, request: "Request[UserT, AuthT, StateT]") -> None:
27
+ """Initialize :class:`InertiaDetails`"""
28
+ self.request = request
29
+
30
+ def _get_header_value(self, name: "InertiaHeaders") -> "Optional[str]":
31
+ """Parse request header
32
+
33
+ Check for uri encoded header and unquotes it in readable format.
34
+
35
+ Args:
36
+ name: The header name.
37
+
38
+ Returns:
39
+ The header value.
40
+ """
41
+
42
+ if value := self.request.headers.get(name.value.lower()):
43
+ is_uri_encoded = self.request.headers.get(f"{name.value.lower()}-uri-autoencoded") == "true"
44
+ return unquote(value) if is_uri_encoded else value
45
+ return None
46
+
47
+ def _get_route_component(self) -> "Optional[str]":
48
+ """Get the route component.
49
+
50
+ Checks for the `component` key within the route handler configuration.
51
+
52
+ Args:
53
+ request: The request object.
54
+
55
+ Returns:
56
+ The route component.
57
+ """
58
+ rh = self.request.scope.get("route_handler") # pyright: ignore[reportUnknownMemberType]
59
+ if rh:
60
+ return rh.opt.get("component")
61
+ return None
62
+
63
+ def __bool__(self) -> bool:
64
+ """Check if request is sent by an Inertia client.
65
+
66
+ Returns:
67
+ True if the request is sent by an Inertia client, False otherwise.
68
+ """
69
+ return self._get_header_value(InertiaHeaders.ENABLED) == "true"
70
+
71
+ @cached_property
72
+ def route_component(self) -> "Optional[str]":
73
+ """Get the route component.
74
+
75
+ Returns:
76
+ The route component.
77
+ """
78
+ return self._get_route_component()
79
+
80
+ @cached_property
81
+ def partial_component(self) -> "Optional[str]":
82
+ """Get the partial component.
83
+
84
+ Returns:
85
+ The partial component.
86
+ """
87
+ return self._get_header_value(InertiaHeaders.PARTIAL_COMPONENT)
88
+
89
+ @cached_property
90
+ def partial_data(self) -> "Optional[str]":
91
+ """Get the partial data.
92
+
93
+ Returns:
94
+ The partial data.
95
+ """
96
+ return self._get_header_value(InertiaHeaders.PARTIAL_DATA)
97
+
98
+ @cached_property
99
+ def referer(self) -> "Optional[str]":
100
+ """Get the referer.
101
+
102
+ Returns:
103
+ The referer.
104
+ """
105
+ return self._get_header_value(InertiaHeaders.REFERER)
106
+
107
+ @cached_property
108
+ def is_partial_render(self) -> bool:
109
+ """Check if the request is a partial render.
110
+
111
+ Returns:
112
+ True if the request is a partial render, False otherwise.
113
+ """
114
+ return bool(self.partial_component == self.route_component and self.partial_data)
115
+
116
+ @cached_property
117
+ def partial_keys(self) -> list[str]:
118
+ """Get the partial keys.
119
+
120
+ Returns:
121
+ The partial keys.
122
+ """
123
+ return self.partial_data.split(",") if self.partial_data is not None else []
124
+
125
+
126
+ class InertiaRequest(Request[UserT, AuthT, StateT]):
127
+ """Inertia Request class to work with Inertia client."""
128
+
129
+ __slots__ = ("inertia",)
130
+
131
+ def __init__(self, scope: "Scope", receive: "Receive" = empty_receive, send: "Send" = empty_send) -> None:
132
+ """Initialize :class:`InertiaRequest`"""
133
+ super().__init__(scope=scope, receive=receive, send=send)
134
+ self.inertia = InertiaDetails(self)
135
+
136
+ @property
137
+ def is_inertia(self) -> bool:
138
+ """True if the request contained inertia headers."""
139
+ return bool(self.inertia)
140
+
141
+ @property
142
+ def inertia_enabled(self) -> bool:
143
+ """True if the route handler contains an inertia enabled configuration."""
144
+ return bool(self.inertia.route_component is not None)
145
+
146
+ @property
147
+ def is_partial_render(self) -> bool:
148
+ """True if the route handler contains an inertia enabled configuration."""
149
+ return self.inertia.is_partial_render
150
+
151
+ @property
152
+ def partial_keys(self) -> "set[str]":
153
+ """True if the route handler contains an inertia enabled configuration."""
154
+ return set(self.inertia.partial_keys)
@@ -0,0 +1,321 @@
1
+ import itertools
2
+ from collections.abc import Iterable, Mapping
3
+ from mimetypes import guess_type
4
+ from pathlib import PurePath
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Optional,
9
+ TypeVar,
10
+ Union,
11
+ cast,
12
+ )
13
+ from urllib.parse import quote, urlparse, urlunparse
14
+
15
+ from litestar import Litestar, MediaType, Request, Response
16
+ from litestar.datastructures.cookie import Cookie
17
+ from litestar.exceptions import ImproperlyConfiguredException
18
+ from litestar.response import Redirect
19
+ from litestar.response.base import ASGIResponse
20
+ from litestar.serialization import get_serializer
21
+ from litestar.status_codes import HTTP_200_OK, HTTP_303_SEE_OTHER, HTTP_307_TEMPORARY_REDIRECT, HTTP_409_CONFLICT
22
+ from litestar.utils.deprecation import warn_deprecation
23
+ from litestar.utils.empty import value_or_default
24
+ from litestar.utils.helpers import get_enum_string_value
25
+ from litestar.utils.scope.state import ScopeState
26
+
27
+ from litestar_vite.inertia._utils import get_headers
28
+ from litestar_vite.inertia.helpers import (
29
+ get_shared_props,
30
+ is_or_contains_lazy_prop,
31
+ js_routes_script,
32
+ lazy_render,
33
+ should_render,
34
+ )
35
+ from litestar_vite.inertia.plugin import InertiaPlugin
36
+ from litestar_vite.inertia.types import InertiaHeaderType, PageProps
37
+ from litestar_vite.plugin import VitePlugin
38
+
39
+ if TYPE_CHECKING:
40
+ from litestar.app import Litestar
41
+ from litestar.background_tasks import BackgroundTask, BackgroundTasks
42
+ from litestar.connection.base import AuthT, StateT, UserT
43
+ from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap
44
+
45
+
46
+ T = TypeVar("T")
47
+
48
+
49
+ class InertiaResponse(Response[T]):
50
+ """Inertia Response"""
51
+
52
+ def __init__(
53
+ self,
54
+ content: T,
55
+ *,
56
+ template_name: "Optional[str]" = None,
57
+ template_str: "Optional[str]" = None,
58
+ background: "Optional[BackgroundTask | BackgroundTasks]" = None,
59
+ context: "Optional[dict[str, Any]]" = None,
60
+ cookies: "Optional[ResponseCookies]" = None,
61
+ encoding: "str" = "utf-8",
62
+ headers: "Optional[ResponseHeaders]" = None,
63
+ media_type: "Optional[Union[MediaType, str]]" = None,
64
+ status_code: "int" = HTTP_200_OK,
65
+ type_encoders: "Optional[TypeEncodersMap]" = None,
66
+ ) -> None:
67
+ """Handle the rendering of a given template into a bytes string.
68
+
69
+ Args:
70
+ content: A value for the response body that will be rendered into bytes string.
71
+ template_name: Path-like name for the template to be rendered, e.g. ``index.html``.
72
+ template_str: A string representing the template, e.g. ``tmpl = "Hello <strong>World</strong>"``.
73
+ background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
74
+ :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
75
+ Defaults to ``None``.
76
+ context: A dictionary of key/value pairs to be passed to the temple engine's render method.
77
+ cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response
78
+ ``Set-Cookie`` header.
79
+ encoding: Content encoding
80
+ headers: A string keyed dictionary of response headers. Header keys are insensitive.
81
+ media_type: A string or member of the :class:`MediaType <.enums.MediaType>` enum. If not set, try to infer
82
+ the media type based on the template name. If this fails, fall back to ``text/plain``.
83
+ status_code: A value for the response HTTP status code.
84
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
85
+
86
+ Raises:
87
+ ValueError: If both template_name and template_str are provided.
88
+ """
89
+ if template_name and template_str:
90
+ msg = "Either template_name or template_str must be provided, not both."
91
+ raise ValueError(msg)
92
+ self.content = content
93
+ self.background = background
94
+ self.cookies: list[Cookie] = (
95
+ [Cookie(key=key, value=value) for key, value in cookies.items()]
96
+ if isinstance(cookies, Mapping)
97
+ else list(cookies or [])
98
+ )
99
+ self.encoding = encoding
100
+ self.headers: dict[str, Any] = (
101
+ dict(headers) if isinstance(headers, Mapping) else {h.name: h.value for h in headers or {}}
102
+ )
103
+ self.media_type = media_type
104
+ self.status_code = status_code
105
+ self.response_type_encoders = {**(self.type_encoders or {}), **(type_encoders or {})}
106
+ self.context = context or {}
107
+ self.template_name = template_name
108
+ self.template_str = template_str
109
+
110
+ def create_template_context(
111
+ self,
112
+ request: "Request[UserT, AuthT, StateT]",
113
+ page_props: "PageProps[T]",
114
+ type_encoders: "Optional[TypeEncodersMap]" = None,
115
+ ) -> "dict[str, Any]":
116
+ """Create a context object for the template.
117
+
118
+ Args:
119
+ request: A :class:`Request <.connection.Request>` instance.
120
+ page_props: A formatted object to return the inertia configuration.
121
+ type_encoders: A mapping of types to callables that transform them into types supported for serialization.
122
+
123
+ Returns:
124
+ A dictionary holding the template context
125
+ """
126
+ csrf_token = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
127
+ inertia_props = self.render(page_props, MediaType.JSON, get_serializer(type_encoders)).decode()
128
+ return {
129
+ **self.context,
130
+ "inertia": inertia_props,
131
+ "js_routes": js_routes_script(request.app.state.js_routes),
132
+ "request": request,
133
+ "csrf_input": f'<input type="hidden" name="_csrf_token" value="{csrf_token}" />',
134
+ }
135
+
136
+ def to_asgi_response( # noqa: C901
137
+ self,
138
+ app: "Optional[Litestar]",
139
+ request: "Request[UserT, AuthT, StateT]",
140
+ *,
141
+ background: "Optional[Union[BackgroundTask, BackgroundTasks]]" = None,
142
+ cookies: "Optional[Iterable[Cookie]]" = None,
143
+ encoded_headers: "Optional[Iterable[tuple[bytes, bytes]]]" = None,
144
+ headers: "Optional[dict[str, str]]" = None,
145
+ is_head_response: "bool" = False,
146
+ media_type: "Optional[Union[MediaType, str]]" = None,
147
+ status_code: "Optional[int]" = None,
148
+ type_encoders: "Optional[TypeEncodersMap]" = None,
149
+ ) -> "ASGIResponse":
150
+ if app is not None:
151
+ warn_deprecation(
152
+ version="2.1",
153
+ deprecated_name="app",
154
+ kind="parameter",
155
+ removal_in="3.0.0",
156
+ alternative="request.app",
157
+ )
158
+ inertia_enabled = cast(
159
+ "bool",
160
+ getattr(request, "inertia_enabled", False) or getattr(request, "is_inertia", False),
161
+ )
162
+ is_inertia = cast("bool", getattr(request, "is_inertia", False))
163
+ headers = {**headers, **self.headers} if headers is not None else self.headers
164
+ cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
165
+ type_encoders = (
166
+ {**type_encoders, **(self.response_type_encoders or {})} if type_encoders else self.response_type_encoders
167
+ )
168
+ if not inertia_enabled:
169
+ media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON)
170
+ return ASGIResponse(
171
+ background=self.background or background,
172
+ body=self.render(self.content, media_type, get_serializer(type_encoders)),
173
+ cookies=cookies,
174
+ encoded_headers=encoded_headers,
175
+ encoding=self.encoding,
176
+ headers=headers,
177
+ is_head_response=is_head_response,
178
+ media_type=media_type,
179
+ status_code=self.status_code or status_code,
180
+ )
181
+ is_partial_render = cast("bool", getattr(request, "is_partial_render", False))
182
+ partial_keys = cast("set[str]", getattr(request, "partial_keys", {}))
183
+ vite_plugin = request.app.plugins.get(VitePlugin)
184
+ inertia_plugin = request.app.plugins.get(InertiaPlugin)
185
+ template_engine = request.app.template_engine # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
186
+ headers.update(
187
+ {"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
188
+ )
189
+ shared_props = get_shared_props(
190
+ request,
191
+ partial_data=partial_keys if is_partial_render else None,
192
+ )
193
+ if is_or_contains_lazy_prop(self.content):
194
+ filtered_content = lazy_render(
195
+ self.content,
196
+ partial_keys if is_partial_render else None,
197
+ inertia_plugin.portal,
198
+ )
199
+ if filtered_content is not None:
200
+ shared_props["content"] = filtered_content
201
+ elif should_render(self.content, partial_keys):
202
+ shared_props["content"] = self.content
203
+
204
+ page_props = PageProps[T](
205
+ component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
206
+ props=shared_props, # pyright: ignore[reportArgumentType]
207
+ version=vite_plugin.asset_loader.version_id,
208
+ url=request.url.path,
209
+ )
210
+ if is_inertia:
211
+ media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON)
212
+ body = self.render(page_props, media_type, get_serializer(type_encoders))
213
+ return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
214
+ background=self.background or background,
215
+ body=body,
216
+ cookies=cookies,
217
+ encoded_headers=encoded_headers,
218
+ encoding=self.encoding,
219
+ headers=headers,
220
+ is_head_response=is_head_response,
221
+ media_type=media_type,
222
+ status_code=self.status_code or status_code,
223
+ )
224
+
225
+ if not template_engine:
226
+ msg = "Template engine is not configured"
227
+ raise ImproperlyConfiguredException(msg)
228
+ # it should default to HTML at this point unless the user specified something
229
+ media_type = media_type or MediaType.HTML
230
+ if not media_type:
231
+ if self.template_name:
232
+ suffixes = PurePath(self.template_name).suffixes
233
+ for suffix in suffixes:
234
+ if _type := guess_type(f"name{suffix}")[0]:
235
+ media_type = _type
236
+ break
237
+ else:
238
+ media_type = MediaType.TEXT
239
+ else:
240
+ media_type = MediaType.HTML
241
+ context = self.create_template_context(request, page_props, type_encoders) # pyright: ignore[reportUnknownMemberType]
242
+ if self.template_str is not None:
243
+ body = template_engine.render_string(self.template_str, context).encode(self.encoding) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
244
+ else:
245
+ template_name = self.template_name or inertia_plugin.config.root_template
246
+ template = template_engine.get_template(template_name) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
247
+ body = template.render(**context).encode(self.encoding) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
248
+
249
+ return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
250
+ background=self.background or background,
251
+ body=body, # pyright: ignore[reportUnknownArgumentType]
252
+ cookies=cookies,
253
+ encoded_headers=encoded_headers,
254
+ encoding=self.encoding,
255
+ headers=headers,
256
+ is_head_response=is_head_response,
257
+ media_type=media_type,
258
+ status_code=self.status_code or status_code,
259
+ )
260
+
261
+
262
+ class InertiaExternalRedirect(Response[Any]):
263
+ """Client side redirect."""
264
+
265
+ def __init__(
266
+ self,
267
+ request: "Request[Any, Any, Any]",
268
+ redirect_to: "str",
269
+ **kwargs: "Any",
270
+ ) -> None:
271
+ """Initialize external redirect, Set status code to 409 (required by Inertia),
272
+ and pass redirect url.
273
+ """
274
+ super().__init__(
275
+ content=b"",
276
+ status_code=HTTP_409_CONFLICT,
277
+ headers={"X-Inertia-Location": quote(redirect_to, safe="/#%[]=:;$&()+,!?*@'~")},
278
+ cookies=request.cookies,
279
+ **kwargs,
280
+ )
281
+
282
+
283
+ class InertiaRedirect(Redirect):
284
+ """Client side redirect."""
285
+
286
+ def __init__(
287
+ self,
288
+ request: "Request[Any, Any, Any]",
289
+ redirect_to: "str",
290
+ **kwargs: "Any",
291
+ ) -> None:
292
+ """Initialize external redirect, Set status code to 409 (required by Inertia),
293
+ and pass redirect url.
294
+ """
295
+ referer = urlparse(request.headers.get("Referer", str(request.base_url)))
296
+ redirect_to = urlunparse(urlparse(redirect_to)._replace(scheme=referer.scheme))
297
+ super().__init__( # pyright: ignore[reportUnknownMemberType]
298
+ path=redirect_to,
299
+ status_code=HTTP_307_TEMPORARY_REDIRECT if request.method == "GET" else HTTP_303_SEE_OTHER,
300
+ cookies=request.cookies,
301
+ **kwargs,
302
+ )
303
+
304
+
305
+ class InertiaBack(Redirect):
306
+ """Client side redirect."""
307
+
308
+ def __init__(
309
+ self,
310
+ request: "Request[Any, Any, Any]",
311
+ **kwargs: "Any",
312
+ ) -> None:
313
+ """Initialize external redirect, Set status code to 409 (required by Inertia),
314
+ and pass redirect url.
315
+ """
316
+ super().__init__( # pyright: ignore[reportUnknownMemberType]
317
+ path=request.headers.get("Referer", str(request.base_url)),
318
+ status_code=HTTP_307_TEMPORARY_REDIRECT if request.method == "GET" else HTTP_303_SEE_OTHER,
319
+ cookies=request.cookies,
320
+ **kwargs,
321
+ )
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass
2
+ from functools import cached_property
3
+ from typing import TYPE_CHECKING, Optional
4
+
5
+ from litestar.app import DEFAULT_OPENAPI_CONFIG
6
+ from litestar.cli._utils import (
7
+ remove_default_schema_routes, # pyright: ignore[reportPrivateImportUsage]
8
+ remove_routes_with_patterns, # pyright: ignore[reportPrivateImportUsage]
9
+ )
10
+ from litestar.routes import ASGIRoute, WebSocketRoute
11
+ from litestar.serialization import encode_json
12
+
13
+ if TYPE_CHECKING:
14
+ from litestar import Litestar
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Routes:
19
+ routes: dict[str, str]
20
+
21
+ @cached_property
22
+ def formatted_routes(self) -> str:
23
+ return encode_json(self.routes).decode(encoding="utf-8")
24
+
25
+
26
+ EXCLUDED_METHODS = {"HEAD", "OPTIONS", "TRACE"}
27
+
28
+
29
+ def generate_js_routes(
30
+ app: "Litestar",
31
+ exclude: "Optional[tuple[str, ...]]" = None,
32
+ schema: "bool" = False,
33
+ ) -> "Routes":
34
+ sorted_routes = sorted(app.routes, key=lambda r: r.path)
35
+ if not schema:
36
+ openapi_config = app.openapi_config or DEFAULT_OPENAPI_CONFIG
37
+ sorted_routes = remove_default_schema_routes(sorted_routes, openapi_config)
38
+ if exclude is not None:
39
+ sorted_routes = remove_routes_with_patterns(sorted_routes, exclude)
40
+ route_list: dict[str, str] = {}
41
+ for route in sorted_routes:
42
+ if isinstance(route, (ASGIRoute, WebSocketRoute)):
43
+ route_name = route.route_handler.name or route.route_handler.handler_name
44
+ if len(route.methods.difference(EXCLUDED_METHODS)) > 0:
45
+ route_list[route_name] = route.path
46
+ else:
47
+ for handler in route.route_handlers:
48
+ route_name = handler.name or handler.handler_name
49
+ if handler.http_methods.isdisjoint(EXCLUDED_METHODS):
50
+ route_list[route_name] = route.path
51
+
52
+ return Routes(routes=route_list)
@@ -0,0 +1,37 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Generic, Optional, TypedDict, TypeVar
3
+
4
+ __all__ = (
5
+ "InertiaHeaderType",
6
+ "PageProps",
7
+ )
8
+
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ @dataclass
14
+ class PageProps(Generic[T]):
15
+ """Inertia Page Props Type."""
16
+
17
+ component: str
18
+ url: str
19
+ version: str
20
+ props: dict[str, Any]
21
+
22
+
23
+ @dataclass
24
+ class InertiaProps(Generic[T]):
25
+ """Inertia Props Type."""
26
+
27
+ page: PageProps[T]
28
+
29
+
30
+ class InertiaHeaderType(TypedDict, total=False):
31
+ """Type for inertia_headers parameter in get_headers()."""
32
+
33
+ enabled: "Optional[bool]"
34
+ version: "Optional[str]"
35
+ location: "Optional[str]"
36
+ partial_data: "Optional[str]"
37
+ partial_component: "Optional[str]"