litestar-vite 0.12.0__py3-none-any.whl → 0.12.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.

@@ -1,606 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import inspect
4
- import itertools
5
- from collections import defaultdict
6
- from collections.abc import Mapping
7
- from contextlib import contextmanager
8
- from functools import lru_cache
9
- from mimetypes import guess_type
10
- from pathlib import PurePath
11
- from textwrap import dedent
12
- from typing import (
13
- TYPE_CHECKING,
14
- Any,
15
- Callable,
16
- Coroutine,
17
- Dict,
18
- Generator,
19
- Generic,
20
- Iterable,
21
- List,
22
- TypeVar,
23
- cast,
24
- overload,
25
- )
26
- from urllib.parse import quote, urlparse, urlunparse
27
-
28
- from anyio.from_thread import BlockingPortal, start_blocking_portal
29
- from litestar import Litestar, MediaType, Request, Response
30
- from litestar.datastructures.cookie import Cookie
31
- from litestar.exceptions import ImproperlyConfiguredException
32
- from litestar.response import Redirect
33
- from litestar.response.base import ASGIResponse
34
- from litestar.serialization import get_serializer
35
- from litestar.status_codes import HTTP_200_OK, HTTP_303_SEE_OTHER, HTTP_307_TEMPORARY_REDIRECT, HTTP_409_CONFLICT
36
- from litestar.utils.deprecation import warn_deprecation
37
- from litestar.utils.empty import value_or_default
38
- from litestar.utils.helpers import get_enum_string_value
39
- from litestar.utils.scope.state import ScopeState
40
- from markupsafe import Markup
41
- from typing_extensions import ParamSpec, TypeGuard
42
-
43
- from litestar_vite.inertia._utils import get_headers
44
- from litestar_vite.inertia.types import InertiaHeaderType, PageProps
45
- from litestar_vite.plugin import VitePlugin
46
-
47
- if TYPE_CHECKING:
48
- from litestar.app import Litestar
49
- from litestar.background_tasks import BackgroundTask, BackgroundTasks
50
- from litestar.connection import ASGIConnection
51
- from litestar.connection.base import AuthT, StateT, UserT
52
- from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap
53
-
54
- from litestar_vite.inertia.plugin import InertiaPlugin
55
- from litestar_vite.inertia.routes import Routes
56
-
57
- T = TypeVar("T")
58
- T_ParamSpec = ParamSpec("T_ParamSpec")
59
- PropKeyT = TypeVar("PropKeyT", bound=str)
60
- StaticT = TypeVar("StaticT", bound=object)
61
-
62
-
63
- @overload
64
- def lazy(key: str, value_or_callable: None) -> StaticProp[str, None]: ...
65
-
66
-
67
- @overload
68
- def lazy(key: str, value_or_callable: T) -> StaticProp[str, T]: ...
69
-
70
-
71
- @overload
72
- def lazy(
73
- key: str,
74
- value_or_callable: Callable[..., None] = ...,
75
- ) -> DeferredProp[str, None]: ...
76
-
77
-
78
- @overload
79
- def lazy(
80
- key: str,
81
- value_or_callable: Callable[T_ParamSpec, T | Coroutine[Any, Any, T]] = ..., # pyright: ignore[reportInvalidTypeVarUse]
82
- ) -> DeferredProp[str, T]: ...
83
-
84
-
85
- def lazy( # type: ignore[misc]
86
- key: str,
87
- value_or_callable: T | Callable[T_ParamSpec, T | Coroutine[Any, Any, T]], # pyright: ignore[reportInvalidTypeVarUse]
88
- ) -> StaticProp[str, None] | StaticProp[str, T] | DeferredProp[str, T] | DeferredProp[str, None]:
89
- """Wrap an async function to return a DeferredProp."""
90
- if value_or_callable is None:
91
- return StaticProp[str, None](key=key, value=None)
92
-
93
- if not callable(value_or_callable):
94
- return StaticProp[str, T](key=key, value=value_or_callable)
95
-
96
- return DeferredProp[str, T](key=key, value=value_or_callable) # pyright: ignore[reportArgumentType]
97
-
98
-
99
- class StaticProp(Generic[PropKeyT, StaticT]):
100
- """A wrapper for static property evaluation."""
101
-
102
- def __init__(self, key: PropKeyT, value: StaticT) -> None:
103
- self._key = key
104
- self._result = value
105
-
106
- @property
107
- def key(self) -> PropKeyT:
108
- return self._key
109
-
110
- def render(self, portal: BlockingPortal | None = None) -> StaticT:
111
- return self._result
112
-
113
-
114
- class DeferredProp(Generic[PropKeyT, T]):
115
- """A wrapper for deferred property evaluation."""
116
-
117
- def __init__(self, key: PropKeyT, value: Callable[T_ParamSpec, T | Coroutine[Any, Any, T]] | None = None) -> None:
118
- self._key = key
119
- self._value = value
120
- self._evaluated = False
121
- self._result: T | None = None
122
-
123
- @property
124
- def key(self) -> PropKeyT:
125
- return self._key
126
-
127
- @staticmethod
128
- def _is_awaitable(
129
- v: Callable[T_ParamSpec, T | Coroutine[Any, Any, T]],
130
- ) -> TypeGuard[Coroutine[Any, Any, T]]:
131
- return inspect.iscoroutinefunction(v)
132
-
133
- @staticmethod
134
- @contextmanager
135
- def _with_portal(portal: BlockingPortal | None = None) -> Generator[BlockingPortal, None, None]:
136
- if portal is None:
137
- with start_blocking_portal() as new_portal:
138
- yield new_portal
139
- else:
140
- yield portal
141
-
142
- def render(self, portal: BlockingPortal | None = None) -> T | None:
143
- if self._evaluated:
144
- return self._result
145
- if self._value is None or not callable(self._value):
146
- self._result = self._value
147
- elif not self._is_awaitable(self._value):
148
- self._result = self._value() # type: ignore[call-arg,assignment,unused-ignore]
149
- else:
150
- with self._with_portal(portal) as bp:
151
- self._result = bp.call(self._value) # type: ignore[call-overload]
152
- self._evaluated = True
153
- return self._result # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
154
-
155
-
156
- def is_lazy_prop(value: Any) -> TypeGuard[DeferredProp[Any, Any]]:
157
- """Check if value is a deferred property.
158
-
159
- Args:
160
- value: Any value to check
161
-
162
- Returns:
163
- bool: True if value is a deferred property
164
- """
165
- return isinstance(value, (DeferredProp, StaticProp))
166
-
167
-
168
- def should_render(value: Any, partial_data: set[str] | None = None) -> bool:
169
- """Check if value should be rendered.
170
-
171
- Args:
172
- value: Any value to check
173
- partial_data: Optional set of keys for partial rendering
174
-
175
- Returns:
176
- bool: True if value should be rendered
177
- """
178
- partial_data = partial_data or set()
179
- if is_lazy_prop(value):
180
- return value.key in partial_data
181
- return True
182
-
183
-
184
- def is_or_contains_lazy_prop(value: Any) -> bool:
185
- """Check if value is or contains a deferred property.
186
-
187
- Args:
188
- value: Any value to check
189
-
190
- Returns:
191
- bool: True if value is or contains a deferred property
192
- """
193
- if is_lazy_prop(value):
194
- return True
195
- if isinstance(value, str):
196
- return False
197
- if isinstance(value, Mapping):
198
- return any(is_or_contains_lazy_prop(v) for v in cast("Mapping[str, Any]", value).values())
199
- if isinstance(value, Iterable):
200
- return any(is_or_contains_lazy_prop(v) for v in cast("Iterable[Any]", value))
201
- return False
202
-
203
-
204
- def lazy_render(value: T, partial_data: set[str] | None = None, portal: BlockingPortal | None = None) -> T:
205
- """Filter deferred properties from the value based on partial data.
206
-
207
- Args:
208
- value: The value to filter
209
- partial_data: Keys for partial rendering
210
- portal: Optional portal to use for async rendering
211
- Returns:
212
- The filtered value
213
- """
214
- partial_data = partial_data or set()
215
- if isinstance(value, str):
216
- return cast("T", value)
217
- if isinstance(value, Mapping):
218
- return cast(
219
- "T",
220
- {
221
- k: lazy_render(v, partial_data)
222
- for k, v in cast("Mapping[str, Any]", value).items()
223
- if should_render(v, partial_data)
224
- },
225
- )
226
-
227
- if isinstance(value, (list, tuple)):
228
- filtered = [
229
- lazy_render(v, partial_data) for v in cast("Iterable[Any]", value) if should_render(v, partial_data)
230
- ]
231
- return cast("T", type(value)(filtered)) # pyright: ignore[reportUnknownArgumentType]
232
-
233
- if is_lazy_prop(value) and should_render(value, partial_data):
234
- return cast("T", value.render())
235
-
236
- return cast("T", value)
237
-
238
-
239
- def get_shared_props(
240
- request: ASGIConnection[Any, Any, Any, Any],
241
- partial_data: set[str] | None = None,
242
- ) -> dict[str, Any]:
243
- """Return shared session props for a request.
244
-
245
- Args:
246
- request: The ASGI connection.
247
- partial_data: Optional set of keys for partial rendering.
248
-
249
- Returns:
250
- Dict[str, Any]: The shared props.
251
-
252
- Note:
253
- Be sure to call this before `self.create_template_context` if you would like to include the `flash` message details.
254
- """
255
- props: dict[str, Any] = {}
256
- flash: dict[str, list[str]] = defaultdict(list)
257
- errors: dict[str, Any] = {}
258
- error_bag = request.headers.get("X-Inertia-Error-Bag", None)
259
-
260
- try:
261
- errors = request.session.pop("_errors", {})
262
- shared_props = cast("Dict[str,Any]", request.session.pop("_shared", {}))
263
-
264
- # Handle deferred props
265
- for key, value in shared_props.items():
266
- if is_lazy_prop(value) and should_render(value, partial_data):
267
- props[key] = value.render()
268
- continue
269
- if should_render(value, partial_data):
270
- props[key] = value
271
-
272
- for message in cast("List[Dict[str,Any]]", request.session.pop("_messages", [])):
273
- flash[message["category"]].append(message["message"])
274
-
275
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
276
- props.update(inertia_plugin.config.extra_static_page_props)
277
- for session_prop in inertia_plugin.config.extra_session_page_props:
278
- if session_prop not in props and session_prop in request.session:
279
- props[session_prop] = request.session.get(session_prop)
280
-
281
- except (AttributeError, ImproperlyConfiguredException):
282
- msg = "Unable to generate all shared props. A valid session was not found for this request."
283
- request.logger.warning(msg)
284
-
285
- props["flash"] = flash
286
- props["errors"] = {error_bag: errors} if error_bag is not None else errors
287
- props["csrf_token"] = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
288
- return props
289
-
290
-
291
- def share(
292
- connection: ASGIConnection[Any, Any, Any, Any],
293
- key: str,
294
- value: Any,
295
- ) -> None:
296
- """Share a value in the session.
297
-
298
- Args:
299
- connection: The ASGI connection.
300
- key: The key to store the value under.
301
- value: The value to store.
302
- """
303
- try:
304
- connection.session.setdefault("_shared", {}).update({key: value})
305
- except (AttributeError, ImproperlyConfiguredException):
306
- msg = "Unable to set `share` session state. A valid session was not found for this request."
307
- connection.logger.warning(msg)
308
-
309
-
310
- def error(
311
- connection: ASGIConnection[Any, Any, Any, Any],
312
- key: str,
313
- message: str,
314
- ) -> None:
315
- """Set an error message in the session.
316
-
317
- Args:
318
- connection: The ASGI connection.
319
- key: The key to store the error under.
320
- message: The error message.
321
- """
322
- try:
323
- connection.session.setdefault("_errors", {}).update({key: message})
324
- except (AttributeError, ImproperlyConfiguredException):
325
- msg = "Unable to set `error` session state. A valid session was not found for this request."
326
- connection.logger.warning(msg)
327
-
328
-
329
- def js_routes_script(js_routes: Routes) -> Markup:
330
- @lru_cache
331
- def _markup_safe_json_dumps(js_routes: str) -> Markup:
332
- js = js_routes.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026").replace("'", "\\u0027")
333
- return Markup(js)
334
-
335
- return Markup(
336
- dedent(f"""
337
- <script type="module">
338
- globalThis.routes = JSON.parse('{_markup_safe_json_dumps(js_routes.formatted_routes)}')
339
- </script>
340
- """),
341
- )
342
-
343
-
344
- class InertiaResponse(Response[T]):
345
- """Inertia Response"""
346
-
347
- def __init__(
348
- self,
349
- content: T,
350
- *,
351
- template_name: str | None = None,
352
- template_str: str | None = None,
353
- background: BackgroundTask | BackgroundTasks | None = None,
354
- context: dict[str, Any] | None = None,
355
- cookies: ResponseCookies | None = None,
356
- encoding: str = "utf-8",
357
- headers: ResponseHeaders | None = None,
358
- media_type: MediaType | str | None = None,
359
- status_code: int = HTTP_200_OK,
360
- type_encoders: TypeEncodersMap | None = None,
361
- ) -> None:
362
- """Handle the rendering of a given template into a bytes string.
363
-
364
- Args:
365
- content: A value for the response body that will be rendered into bytes string.
366
- template_name: Path-like name for the template to be rendered, e.g. ``index.html``.
367
- template_str: A string representing the template, e.g. ``tmpl = "Hello <strong>World</strong>"``.
368
- background: A :class:`BackgroundTask <.background_tasks.BackgroundTask>` instance or
369
- :class:`BackgroundTasks <.background_tasks.BackgroundTasks>` to execute after the response is finished.
370
- Defaults to ``None``.
371
- context: A dictionary of key/value pairs to be passed to the temple engine's render method.
372
- cookies: A list of :class:`Cookie <.datastructures.Cookie>` instances to be set under the response
373
- ``Set-Cookie`` header.
374
- encoding: Content encoding
375
- headers: A string keyed dictionary of response headers. Header keys are insensitive.
376
- media_type: A string or member of the :class:`MediaType <.enums.MediaType>` enum. If not set, try to infer
377
- the media type based on the template name. If this fails, fall back to ``text/plain``.
378
- status_code: A value for the response HTTP status code.
379
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
380
- """
381
- if template_name and template_str:
382
- msg = "Either template_name or template_str must be provided, not both."
383
- raise ValueError(msg)
384
- self.content = content
385
- self.background = background
386
- self.cookies: list[Cookie] = (
387
- [Cookie(key=key, value=value) for key, value in cookies.items()]
388
- if isinstance(cookies, Mapping)
389
- else list(cookies or [])
390
- )
391
- self.encoding = encoding
392
- self.headers: dict[str, Any] = (
393
- dict(headers) if isinstance(headers, Mapping) else {h.name: h.value for h in headers or {}}
394
- )
395
- self.media_type = media_type
396
- self.status_code = status_code
397
- self.response_type_encoders = {**(self.type_encoders or {}), **(type_encoders or {})}
398
- self.context = context or {}
399
- self.template_name = template_name
400
- self.template_str = template_str
401
-
402
- def create_template_context(
403
- self,
404
- request: Request[UserT, AuthT, StateT],
405
- page_props: PageProps[T],
406
- type_encoders: TypeEncodersMap | None = None,
407
- ) -> dict[str, Any]:
408
- """Create a context object for the template.
409
-
410
- Args:
411
- request: A :class:`Request <.connection.Request>` instance.
412
- page_props: A formatted object to return the inertia configuration.
413
- type_encoders: A mapping of types to callables that transform them into types supported for serialization.
414
-
415
- Returns:
416
- A dictionary holding the template context
417
- """
418
- csrf_token = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
419
- inertia_props = self.render(page_props, MediaType.JSON, get_serializer(type_encoders)).decode()
420
- return {
421
- **self.context,
422
- "inertia": inertia_props,
423
- "js_routes": js_routes_script(request.app.state.js_routes),
424
- "request": request,
425
- "csrf_input": f'<input type="hidden" name="_csrf_token" value="{csrf_token}" />',
426
- }
427
-
428
- def to_asgi_response( # noqa: C901, PLR0912
429
- self,
430
- app: Litestar | None,
431
- request: Request[UserT, AuthT, StateT],
432
- *,
433
- background: BackgroundTask | BackgroundTasks | None = None,
434
- cookies: Iterable[Cookie] | None = None,
435
- encoded_headers: Iterable[tuple[bytes, bytes]] | None = None,
436
- headers: dict[str, str] | None = None,
437
- is_head_response: bool = False,
438
- media_type: MediaType | str | None = None,
439
- status_code: int | None = None,
440
- type_encoders: TypeEncodersMap | None = None,
441
- ) -> ASGIResponse:
442
- if app is not None:
443
- warn_deprecation(
444
- version="2.1",
445
- deprecated_name="app",
446
- kind="parameter",
447
- removal_in="3.0.0",
448
- alternative="request.app",
449
- )
450
- inertia_enabled = cast(
451
- "bool",
452
- getattr(request, "inertia_enabled", False) or getattr(request, "is_inertia", False),
453
- )
454
- is_inertia = cast("bool", getattr(request, "is_inertia", False))
455
- headers = {**headers, **self.headers} if headers is not None else self.headers
456
- cookies = self.cookies if cookies is None else itertools.chain(self.cookies, cookies)
457
- type_encoders = (
458
- {**type_encoders, **(self.response_type_encoders or {})} if type_encoders else self.response_type_encoders
459
- )
460
- if not inertia_enabled:
461
- media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON)
462
- return ASGIResponse(
463
- background=self.background or background,
464
- body=self.render(self.content, media_type, get_serializer(type_encoders)),
465
- cookies=cookies,
466
- encoded_headers=encoded_headers,
467
- encoding=self.encoding,
468
- headers=headers,
469
- is_head_response=is_head_response,
470
- media_type=media_type,
471
- status_code=self.status_code or status_code,
472
- )
473
- is_partial_render = cast("bool", getattr(request, "is_partial_render", False))
474
- partial_keys = cast("set[str]", getattr(request, "partial_keys", {}))
475
- vite_plugin = request.app.plugins.get(VitePlugin)
476
- template_engine = request.app.template_engine # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
477
- headers.update(
478
- {"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
479
- )
480
- shared_props = get_shared_props(request, partial_data=partial_keys if is_partial_render else None)
481
- if is_or_contains_lazy_prop(self.content):
482
- filtered_content = lazy_render(self.content, partial_keys if is_partial_render else None)
483
- if filtered_content is not None:
484
- shared_props["content"] = filtered_content
485
- elif should_render(self.content, partial_keys):
486
- shared_props["content"] = self.content
487
-
488
- page_props = PageProps[T](
489
- component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
490
- props=shared_props, # pyright: ignore[reportArgumentType]
491
- version=vite_plugin.asset_loader.version_id,
492
- url=request.url.path,
493
- )
494
- if is_inertia:
495
- media_type = get_enum_string_value(self.media_type or media_type or MediaType.JSON)
496
- body = self.render(page_props, media_type, get_serializer(type_encoders))
497
- return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
498
- background=self.background or background,
499
- body=body,
500
- cookies=cookies,
501
- encoded_headers=encoded_headers,
502
- encoding=self.encoding,
503
- headers=headers,
504
- is_head_response=is_head_response,
505
- media_type=media_type,
506
- status_code=self.status_code or status_code,
507
- )
508
-
509
- if not template_engine:
510
- msg = "Template engine is not configured"
511
- raise ImproperlyConfiguredException(msg)
512
- # it should default to HTML at this point unless the user specified something
513
- media_type = media_type or MediaType.HTML
514
- if not media_type:
515
- if self.template_name:
516
- suffixes = PurePath(self.template_name).suffixes
517
- for suffix in suffixes:
518
- if _type := guess_type(f"name{suffix}")[0]:
519
- media_type = _type
520
- break
521
- else:
522
- media_type = MediaType.TEXT
523
- else:
524
- media_type = MediaType.HTML
525
- context = self.create_template_context(request, page_props, type_encoders) # pyright: ignore[reportUnknownMemberType]
526
- if self.template_str is not None:
527
- body = template_engine.render_string(self.template_str, context).encode(self.encoding) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
528
- else:
529
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
530
- template_name = self.template_name or inertia_plugin.config.root_template
531
- template = template_engine.get_template(template_name) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
532
- body = template.render(**context).encode(self.encoding) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
533
-
534
- return ASGIResponse( # pyright: ignore[reportUnknownMemberType]
535
- background=self.background or background,
536
- body=body, # pyright: ignore[reportUnknownArgumentType]
537
- cookies=cookies,
538
- encoded_headers=encoded_headers,
539
- encoding=self.encoding,
540
- headers=headers,
541
- is_head_response=is_head_response,
542
- media_type=media_type,
543
- status_code=self.status_code or status_code,
544
- )
545
-
546
-
547
- class InertiaExternalRedirect(Response[Any]):
548
- """Client side redirect."""
549
-
550
- def __init__(
551
- self,
552
- request: Request[Any, Any, Any],
553
- redirect_to: str,
554
- **kwargs: Any,
555
- ) -> None:
556
- """Initialize external redirect, Set status code to 409 (required by Inertia),
557
- and pass redirect url.
558
- """
559
- super().__init__(
560
- content=b"",
561
- status_code=HTTP_409_CONFLICT,
562
- headers={"X-Inertia-Location": quote(redirect_to, safe="/#%[]=:;$&()+,!?*@'~")},
563
- cookies=request.cookies,
564
- **kwargs,
565
- )
566
-
567
-
568
- class InertiaRedirect(Redirect):
569
- """Client side redirect."""
570
-
571
- def __init__(
572
- self,
573
- request: Request[Any, Any, Any],
574
- redirect_to: str,
575
- **kwargs: Any,
576
- ) -> None:
577
- """Initialize external redirect, Set status code to 409 (required by Inertia),
578
- and pass redirect url.
579
- """
580
- referer = urlparse(request.headers.get("Referer", str(request.base_url)))
581
- redirect_to = urlunparse(urlparse(redirect_to)._replace(scheme=referer.scheme))
582
- super().__init__(
583
- path=redirect_to,
584
- status_code=HTTP_307_TEMPORARY_REDIRECT if request.method == "GET" else HTTP_303_SEE_OTHER,
585
- cookies=request.cookies,
586
- **kwargs,
587
- )
588
-
589
-
590
- class InertiaBack(Redirect):
591
- """Client side redirect."""
592
-
593
- def __init__(
594
- self,
595
- request: Request[Any, Any, Any],
596
- **kwargs: Any,
597
- ) -> None:
598
- """Initialize external redirect, Set status code to 409 (required by Inertia),
599
- and pass redirect url.
600
- """
601
- super().__init__(
602
- path=request.headers.get("Referer", str(request.base_url)),
603
- status_code=HTTP_307_TEMPORARY_REDIRECT if request.method == "GET" else HTTP_303_SEE_OTHER,
604
- cookies=request.cookies,
605
- **kwargs,
606
- )
@@ -1,54 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from functools import cached_property
5
- from typing import TYPE_CHECKING
6
-
7
- from litestar.app import DEFAULT_OPENAPI_CONFIG
8
- from litestar.cli._utils import (
9
- remove_default_schema_routes,
10
- remove_routes_with_patterns,
11
- )
12
- from litestar.routes import ASGIRoute, WebSocketRoute
13
- from litestar.serialization import encode_json
14
-
15
- if TYPE_CHECKING:
16
- from litestar import Litestar
17
-
18
-
19
- @dataclass(frozen=True)
20
- class Routes:
21
- routes: dict[str, str]
22
-
23
- @cached_property
24
- def formatted_routes(self) -> str:
25
- return encode_json(self.routes).decode(encoding="utf-8")
26
-
27
-
28
- EXCLUDED_METHODS = {"HEAD", "OPTIONS", "TRACE"}
29
-
30
-
31
- def generate_js_routes(
32
- app: Litestar,
33
- exclude: tuple[str, ...] | None = None,
34
- schema: bool = False,
35
- ) -> Routes:
36
- sorted_routes = sorted(app.routes, key=lambda r: r.path)
37
- if not schema:
38
- openapi_config = app.openapi_config or DEFAULT_OPENAPI_CONFIG
39
- sorted_routes = remove_default_schema_routes(sorted_routes, openapi_config)
40
- if exclude is not None:
41
- sorted_routes = remove_routes_with_patterns(sorted_routes, exclude)
42
- route_list: dict[str, str] = {}
43
- for route in sorted_routes:
44
- if isinstance(route, (ASGIRoute, WebSocketRoute)):
45
- route_name = route.route_handler.name or route.route_handler.handler_name
46
- if len(route.methods.difference(EXCLUDED_METHODS)) > 0:
47
- route_list[route_name] = route.path
48
- else:
49
- for handler in route.route_handlers:
50
- route_name = handler.name or handler.handler_name
51
- if handler.http_methods.isdisjoint(EXCLUDED_METHODS):
52
- route_list[route_name] = route.path
53
-
54
- return Routes(routes=route_list)
@@ -1,39 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from typing import Any, Generic, TypedDict, TypeVar
5
-
6
- __all__ = (
7
- "InertiaHeaderType",
8
- "PageProps",
9
- )
10
-
11
-
12
- T = TypeVar("T")
13
-
14
-
15
- @dataclass
16
- class PageProps(Generic[T]):
17
- """Inertia Page Props Type."""
18
-
19
- component: str
20
- url: str
21
- version: str
22
- props: dict[str, Any]
23
-
24
-
25
- @dataclass
26
- class InertiaProps(Generic[T]):
27
- """Inertia Props Type."""
28
-
29
- page: PageProps[T]
30
-
31
-
32
- class InertiaHeaderType(TypedDict, total=False):
33
- """Type for inertia_headers parameter in get_headers()."""
34
-
35
- enabled: bool | None
36
- version: str | None
37
- location: str | None
38
- partial_data: str | None
39
- partial_component: str | None