litestar-vite 0.12.0__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

litestar_vite/commands.py CHANGED
@@ -13,11 +13,11 @@ VITE_INIT_TEMPLATES: set[str] = {"package.json.j2", "tsconfig.json.j2", "vite.co
13
13
  DEFAULT_RESOURCES: set[str] = {"styles.css.j2", "main.ts.j2"}
14
14
  DEFAULT_DEV_DEPENDENCIES: dict[str, str] = {
15
15
  "typescript": "^5.7.2",
16
- "vite": "^6.0.3",
17
- "litestar-vite-plugin": "^0.12.0",
18
- "@types/node": "^22.10.1",
16
+ "vite": "^6.0.6",
17
+ "litestar-vite-plugin": "^0.13.0",
18
+ "@types/node": "^22.10.2",
19
19
  }
20
- DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
20
+ DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.9"}
21
21
 
22
22
 
23
23
  def to_json(value: Any) -> str:
@@ -1,17 +1,17 @@
1
- from .config import InertiaConfig
2
- from .exception_handler import create_inertia_exception_response, exception_to_http_response
3
- from .middleware import InertiaMiddleware
4
- from .plugin import InertiaPlugin
5
- from .request import InertiaDetails, InertiaHeaders, InertiaRequest
6
- from .response import (
1
+ from litestar_vite.inertia import helpers
2
+ from litestar_vite.inertia.config import InertiaConfig
3
+ from litestar_vite.inertia.exception_handler import create_inertia_exception_response, exception_to_http_response
4
+ from litestar_vite.inertia.helpers import error, get_shared_props, js_routes_script, lazy, share
5
+ from litestar_vite.inertia.middleware import InertiaMiddleware
6
+ from litestar_vite.inertia.plugin import InertiaPlugin
7
+ from litestar_vite.inertia.request import InertiaDetails, InertiaHeaders, InertiaRequest
8
+ from litestar_vite.inertia.response import (
7
9
  InertiaBack,
8
10
  InertiaExternalRedirect,
9
11
  InertiaRedirect,
10
12
  InertiaResponse,
11
- error,
12
- get_shared_props,
13
- share,
14
13
  )
14
+
15
15
  from .routes import generate_js_routes
16
16
 
17
17
  __all__ = (
@@ -30,5 +30,8 @@ __all__ = (
30
30
  "exception_to_http_response",
31
31
  "generate_js_routes",
32
32
  "get_shared_props",
33
+ "helpers",
34
+ "js_routes_script",
35
+ "lazy",
33
36
  "share",
34
37
  )
@@ -35,7 +35,8 @@ from litestar.status_codes import (
35
35
  HTTP_500_INTERNAL_SERVER_ERROR,
36
36
  )
37
37
 
38
- from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse, error
38
+ from litestar_vite.inertia.helpers import error
39
+ from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse
39
40
 
40
41
  if TYPE_CHECKING:
41
42
  from litestar.connection import Request
@@ -0,0 +1,329 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ from collections import defaultdict
5
+ from collections.abc import Mapping
6
+ from contextlib import contextmanager
7
+ from functools import lru_cache
8
+ from textwrap import dedent
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Callable,
13
+ Coroutine,
14
+ Dict,
15
+ Generator,
16
+ Generic,
17
+ Iterable,
18
+ List,
19
+ TypeVar,
20
+ cast,
21
+ overload,
22
+ )
23
+
24
+ from anyio.from_thread import BlockingPortal, start_blocking_portal
25
+ from litestar.exceptions import ImproperlyConfiguredException
26
+ from litestar.utils.empty import value_or_default
27
+ from litestar.utils.scope.state import ScopeState
28
+ from markupsafe import Markup
29
+ from typing_extensions import ParamSpec, TypeGuard
30
+
31
+ if TYPE_CHECKING:
32
+ from litestar.connection import ASGIConnection
33
+
34
+ from litestar_vite.inertia.plugin import InertiaPlugin
35
+ from litestar_vite.inertia.routes import Routes
36
+
37
+ T = TypeVar("T")
38
+ T_ParamSpec = ParamSpec("T_ParamSpec")
39
+ PropKeyT = TypeVar("PropKeyT", bound=str)
40
+ StaticT = TypeVar("StaticT", bound=object)
41
+
42
+
43
+ @overload
44
+ def lazy(key: str, value_or_callable: None) -> StaticProp[str, None]: ...
45
+
46
+
47
+ @overload
48
+ def lazy(key: str, value_or_callable: T) -> StaticProp[str, T]: ...
49
+
50
+
51
+ @overload
52
+ def lazy(key: str, value_or_callable: Callable[..., None] = ...) -> DeferredProp[str, None]: ...
53
+
54
+
55
+ @overload
56
+ def lazy(key: str, value_or_callable: Callable[..., Coroutine[Any, Any, None]] = ...) -> DeferredProp[str, None]: ...
57
+
58
+
59
+ @overload
60
+ def lazy(
61
+ key: str,
62
+ value_or_callable: Callable[..., T | Coroutine[Any, Any, T]] = ..., # pyright: ignore[reportInvalidTypeVarUse]
63
+ ) -> DeferredProp[str, T]: ...
64
+
65
+
66
+ def lazy(
67
+ key: str,
68
+ value_or_callable: None
69
+ | Callable[T_ParamSpec, None | Coroutine[Any, Any, None]]
70
+ | T
71
+ | Callable[T_ParamSpec, T | Coroutine[Any, Any, T]] = None,
72
+ ) -> StaticProp[str, None] | StaticProp[str, T] | DeferredProp[str, T] | DeferredProp[str, None]:
73
+ """Wrap an async function to return a DeferredProp."""
74
+ if value_or_callable is None:
75
+ return StaticProp[str, None](key=key, value=None)
76
+
77
+ if not callable(value_or_callable):
78
+ return StaticProp[str, T](key=key, value=value_or_callable)
79
+
80
+ return DeferredProp[str, T](key=key, value=cast("Callable[..., T | Coroutine[Any, Any, T]]", value_or_callable))
81
+
82
+
83
+ class StaticProp(Generic[PropKeyT, StaticT]):
84
+ """A wrapper for static property evaluation."""
85
+
86
+ def __init__(self, key: PropKeyT, value: StaticT) -> None:
87
+ self._key = key
88
+ self._result = value
89
+
90
+ @property
91
+ def key(self) -> PropKeyT:
92
+ return self._key
93
+
94
+ def render(self, portal: BlockingPortal | None = None) -> StaticT:
95
+ return self._result
96
+
97
+
98
+ class DeferredProp(Generic[PropKeyT, T]):
99
+ """A wrapper for deferred property evaluation."""
100
+
101
+ def __init__(
102
+ self, key: PropKeyT, value: Callable[..., None | T | Coroutine[Any, Any, T | None]] | None = None
103
+ ) -> None:
104
+ self._key = key
105
+ self._value = value
106
+ self._evaluated = False
107
+ self._result: T | None = None
108
+
109
+ @property
110
+ def key(self) -> PropKeyT:
111
+ return self._key
112
+
113
+ @contextmanager
114
+ def with_portal(self, portal: BlockingPortal | None = None) -> Generator[BlockingPortal, None, None]:
115
+ if portal is None:
116
+ with start_blocking_portal() as p:
117
+ yield p
118
+ else:
119
+ yield portal
120
+
121
+ @staticmethod
122
+ def _is_awaitable(
123
+ v: Callable[..., T | Coroutine[Any, Any, T]],
124
+ ) -> TypeGuard[Coroutine[Any, Any, T]]:
125
+ return inspect.iscoroutinefunction(v)
126
+
127
+ def render(self, portal: BlockingPortal | None = None) -> T | None:
128
+ if self._evaluated:
129
+ return self._result
130
+ if self._value is None or not callable(self._value):
131
+ self._result = self._value
132
+ self._evaluated = True
133
+ return self._result
134
+ if not self._is_awaitable(cast("Callable[..., T]", self._value)):
135
+ self._result = cast("T", self._value())
136
+ self._evaluated = True
137
+ return self._result
138
+ with self.with_portal(portal) as p:
139
+ self._result = p.call(cast("Callable[..., T]", self._value))
140
+ self._evaluated = True
141
+ return self._result
142
+
143
+
144
+ def is_lazy_prop(value: Any) -> TypeGuard[DeferredProp[Any, Any]]:
145
+ """Check if value is a deferred property.
146
+
147
+ Args:
148
+ value: Any value to check
149
+
150
+ Returns:
151
+ bool: True if value is a deferred property
152
+ """
153
+ return isinstance(value, (DeferredProp, StaticProp))
154
+
155
+
156
+ def should_render(value: Any, partial_data: set[str] | None = None) -> bool:
157
+ """Check if value should be rendered.
158
+
159
+ Args:
160
+ value: Any value to check
161
+ partial_data: Optional set of keys for partial rendering
162
+
163
+ Returns:
164
+ bool: True if value should be rendered
165
+ """
166
+ partial_data = partial_data or set()
167
+ if is_lazy_prop(value):
168
+ return value.key in partial_data
169
+ return True
170
+
171
+
172
+ def is_or_contains_lazy_prop(value: Any) -> bool:
173
+ """Check if value is or contains a deferred property.
174
+
175
+ Args:
176
+ value: Any value to check
177
+
178
+ Returns:
179
+ bool: True if value is or contains a deferred property
180
+ """
181
+ if is_lazy_prop(value):
182
+ return True
183
+ if isinstance(value, str):
184
+ return False
185
+ if isinstance(value, Mapping):
186
+ return any(is_or_contains_lazy_prop(v) for v in cast("Mapping[str, Any]", value).values())
187
+ if isinstance(value, Iterable):
188
+ return any(is_or_contains_lazy_prop(v) for v in cast("Iterable[Any]", value))
189
+ return False
190
+
191
+
192
+ def lazy_render(value: T, partial_data: set[str] | None = None, portal: BlockingPortal | None = None) -> T:
193
+ """Filter deferred properties from the value based on partial data.
194
+
195
+ Args:
196
+ value: The value to filter
197
+ partial_data: Keys for partial rendering
198
+ portal: Optional portal to use for async rendering
199
+ Returns:
200
+ The filtered value
201
+ """
202
+ partial_data = partial_data or set()
203
+ if isinstance(value, str):
204
+ return cast("T", value)
205
+ if isinstance(value, Mapping):
206
+ return cast(
207
+ "T",
208
+ {
209
+ k: lazy_render(v, partial_data, portal)
210
+ for k, v in cast("Mapping[str, Any]", value).items()
211
+ if should_render(v, partial_data)
212
+ },
213
+ )
214
+
215
+ if isinstance(value, (list, tuple)):
216
+ filtered = [
217
+ lazy_render(v, partial_data, portal) for v in cast("Iterable[Any]", value) if should_render(v, partial_data)
218
+ ]
219
+ return cast("T", type(value)(filtered)) # pyright: ignore[reportUnknownArgumentType]
220
+
221
+ if is_lazy_prop(value) and should_render(value, partial_data):
222
+ return cast("T", value.render(portal))
223
+
224
+ return cast("T", value)
225
+
226
+
227
+ def get_shared_props(
228
+ request: ASGIConnection[Any, Any, Any, Any],
229
+ partial_data: set[str] | None = None,
230
+ ) -> dict[str, Any]:
231
+ """Return shared session props for a request.
232
+
233
+ Args:
234
+ request: The ASGI connection.
235
+ partial_data: Optional set of keys for partial rendering.
236
+ portal: Optional portal to use for async rendering
237
+ Returns:
238
+ Dict[str, Any]: The shared props.
239
+
240
+ Note:
241
+ Be sure to call this before `self.create_template_context` if you would like to include the `flash` message details.
242
+ """
243
+ props: dict[str, Any] = {}
244
+ flash: dict[str, list[str]] = defaultdict(list)
245
+ errors: dict[str, Any] = {}
246
+ error_bag = request.headers.get("X-Inertia-Error-Bag", None)
247
+
248
+ try:
249
+ errors = request.session.pop("_errors", {})
250
+ shared_props = cast("Dict[str,Any]", request.session.pop("_shared", {}))
251
+ inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
252
+
253
+ # Handle deferred props
254
+ for key, value in shared_props.items():
255
+ if is_lazy_prop(value) and should_render(value, partial_data):
256
+ props[key] = value.render(inertia_plugin.portal)
257
+ continue
258
+ if should_render(value, partial_data):
259
+ props[key] = value
260
+
261
+ for message in cast("List[Dict[str,Any]]", request.session.pop("_messages", [])):
262
+ flash[message["category"]].append(message["message"])
263
+
264
+ props.update(inertia_plugin.config.extra_static_page_props)
265
+ for session_prop in inertia_plugin.config.extra_session_page_props:
266
+ if session_prop not in props and session_prop in request.session:
267
+ props[session_prop] = request.session.get(session_prop)
268
+
269
+ except (AttributeError, ImproperlyConfiguredException):
270
+ msg = "Unable to generate all shared props. A valid session was not found for this request."
271
+ request.logger.warning(msg)
272
+
273
+ props["flash"] = flash
274
+ props["errors"] = {error_bag: errors} if error_bag is not None else errors
275
+ props["csrf_token"] = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
276
+ return props
277
+
278
+
279
+ def share(
280
+ connection: ASGIConnection[Any, Any, Any, Any],
281
+ key: str,
282
+ value: Any,
283
+ ) -> None:
284
+ """Share a value in the session.
285
+
286
+ Args:
287
+ connection: The ASGI connection.
288
+ key: The key to store the value under.
289
+ value: The value to store.
290
+ """
291
+ try:
292
+ connection.session.setdefault("_shared", {}).update({key: value})
293
+ except (AttributeError, ImproperlyConfiguredException):
294
+ msg = "Unable to set `share` session state. A valid session was not found for this request."
295
+ connection.logger.warning(msg)
296
+
297
+
298
+ def error(
299
+ connection: ASGIConnection[Any, Any, Any, Any],
300
+ key: str,
301
+ message: str,
302
+ ) -> None:
303
+ """Set an error message in the session.
304
+
305
+ Args:
306
+ connection: The ASGI connection.
307
+ key: The key to store the error under.
308
+ message: The error message.
309
+ """
310
+ try:
311
+ connection.session.setdefault("_errors", {}).update({key: message})
312
+ except (AttributeError, ImproperlyConfiguredException):
313
+ msg = "Unable to set `error` session state. A valid session was not found for this request."
314
+ connection.logger.warning(msg)
315
+
316
+
317
+ def js_routes_script(js_routes: Routes) -> Markup:
318
+ @lru_cache
319
+ def _markup_safe_json_dumps(js_routes: str) -> Markup:
320
+ js = js_routes.replace("<", "\\u003c").replace(">", "\\u003e").replace("&", "\\u0026").replace("'", "\\u0027")
321
+ return Markup(js)
322
+
323
+ return Markup(
324
+ dedent(f"""
325
+ <script type="module">
326
+ globalThis.routes = JSON.parse('{_markup_safe_json_dumps(js_routes.formatted_routes)}')
327
+ </script>
328
+ """),
329
+ )
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import TYPE_CHECKING
3
+ from contextlib import asynccontextmanager
4
+ from typing import TYPE_CHECKING, AsyncGenerator
4
5
 
5
- from anyio.from_thread import BlockingPortalProvider
6
+ from anyio.from_thread import start_blocking_portal
6
7
  from litestar.plugins import InitPluginProtocol
7
8
 
8
9
  if TYPE_CHECKING:
10
+ from anyio.from_thread import BlockingPortal
9
11
  from litestar import Litestar
10
12
  from litestar.config.app import AppConfig
11
13
 
@@ -23,7 +25,7 @@ def set_js_routes(app: Litestar) -> None:
23
25
  class InertiaPlugin(InitPluginProtocol):
24
26
  """Inertia plugin."""
25
27
 
26
- __slots__ = ("config", "portal")
28
+ __slots__ = ("_portal", "config")
27
29
 
28
30
  def __init__(self, config: InertiaConfig) -> None:
29
31
  """Initialize ``Inertia``.
@@ -33,10 +35,18 @@ class InertiaPlugin(InitPluginProtocol):
33
35
  """
34
36
  self.config = config
35
37
 
36
- self.portal = BlockingPortalProvider()
38
+ @asynccontextmanager
39
+ async def lifespan(self, app: Litestar) -> AsyncGenerator[None, None]:
40
+ """Lifespan to ensure the event loop is available."""
37
41
 
38
- def get_portal(self) -> BlockingPortalProvider:
39
- return self.portal
42
+ with start_blocking_portal() as portal:
43
+ self._portal = portal
44
+ yield
45
+
46
+ @property
47
+ def portal(self) -> BlockingPortal:
48
+ """Get the portal."""
49
+ return self._portal
40
50
 
41
51
  def on_app_init(self, app_config: AppConfig) -> AppConfig:
42
52
  """Configure application for use with Vite.
@@ -52,9 +62,10 @@ class InertiaPlugin(InitPluginProtocol):
52
62
  from litestar.utils.predicates import is_class_and_subclass
53
63
 
54
64
  from litestar_vite.inertia.exception_handler import exception_to_http_response
65
+ from litestar_vite.inertia.helpers import DeferredProp, StaticProp
55
66
  from litestar_vite.inertia.middleware import InertiaMiddleware
56
67
  from litestar_vite.inertia.request import InertiaRequest
57
- from litestar_vite.inertia.response import DeferredProp, InertiaBack, InertiaResponse, StaticProp
68
+ from litestar_vite.inertia.response import InertiaBack, InertiaResponse
58
69
 
59
70
  for mw in app_config.middleware:
60
71
  if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
@@ -81,4 +92,5 @@ class InertiaPlugin(InitPluginProtocol):
81
92
  (lambda x: x is DeferredProp, lambda t, v: t(v)),
82
93
  *(app_config.type_decoders or []),
83
94
  ]
95
+ app_config.lifespan.append(self.lifespan) # pyright: ignore[reportUnknownMemberType]
84
96
  return app_config
@@ -1,31 +1,18 @@
1
1
  from __future__ import annotations
2
2
 
3
- import inspect
4
3
  import itertools
5
- from collections import defaultdict
6
4
  from collections.abc import Mapping
7
- from contextlib import contextmanager
8
- from functools import lru_cache
9
5
  from mimetypes import guess_type
10
6
  from pathlib import PurePath
11
- from textwrap import dedent
12
7
  from typing import (
13
8
  TYPE_CHECKING,
14
9
  Any,
15
- Callable,
16
- Coroutine,
17
- Dict,
18
- Generator,
19
- Generic,
20
10
  Iterable,
21
- List,
22
11
  TypeVar,
23
12
  cast,
24
- overload,
25
13
  )
26
14
  from urllib.parse import quote, urlparse, urlunparse
27
15
 
28
- from anyio.from_thread import BlockingPortal, start_blocking_portal
29
16
  from litestar import Litestar, MediaType, Request, Response
30
17
  from litestar.datastructures.cookie import Cookie
31
18
  from litestar.exceptions import ImproperlyConfiguredException
@@ -37,308 +24,27 @@ from litestar.utils.deprecation import warn_deprecation
37
24
  from litestar.utils.empty import value_or_default
38
25
  from litestar.utils.helpers import get_enum_string_value
39
26
  from litestar.utils.scope.state import ScopeState
40
- from markupsafe import Markup
41
- from typing_extensions import ParamSpec, TypeGuard
42
27
 
43
28
  from litestar_vite.inertia._utils import get_headers
29
+ from litestar_vite.inertia.helpers import (
30
+ get_shared_props,
31
+ is_or_contains_lazy_prop,
32
+ js_routes_script,
33
+ lazy_render,
34
+ should_render,
35
+ )
36
+ from litestar_vite.inertia.plugin import InertiaPlugin
44
37
  from litestar_vite.inertia.types import InertiaHeaderType, PageProps
45
38
  from litestar_vite.plugin import VitePlugin
46
39
 
47
40
  if TYPE_CHECKING:
48
41
  from litestar.app import Litestar
49
42
  from litestar.background_tasks import BackgroundTask, BackgroundTasks
50
- from litestar.connection import ASGIConnection
51
43
  from litestar.connection.base import AuthT, StateT, UserT
52
44
  from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap
53
45
 
54
- from litestar_vite.inertia.plugin import InertiaPlugin
55
- from litestar_vite.inertia.routes import Routes
56
46
 
57
47
  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
48
 
343
49
 
344
50
  class InertiaResponse(Response[T]):
@@ -473,13 +179,21 @@ class InertiaResponse(Response[T]):
473
179
  is_partial_render = cast("bool", getattr(request, "is_partial_render", False))
474
180
  partial_keys = cast("set[str]", getattr(request, "partial_keys", {}))
475
181
  vite_plugin = request.app.plugins.get(VitePlugin)
182
+ inertia_plugin = request.app.plugins.get(InertiaPlugin)
476
183
  template_engine = request.app.template_engine # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
477
184
  headers.update(
478
185
  {"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
479
186
  )
480
- shared_props = get_shared_props(request, partial_data=partial_keys if is_partial_render else None)
187
+ shared_props = get_shared_props(
188
+ request,
189
+ partial_data=partial_keys if is_partial_render else None,
190
+ )
481
191
  if is_or_contains_lazy_prop(self.content):
482
- filtered_content = lazy_render(self.content, partial_keys if is_partial_render else None)
192
+ filtered_content = lazy_render(
193
+ self.content,
194
+ partial_keys if is_partial_render else None,
195
+ inertia_plugin.portal,
196
+ )
483
197
  if filtered_content is not None:
484
198
  shared_props["content"] = filtered_content
485
199
  elif should_render(self.content, partial_keys):
@@ -526,7 +240,6 @@ class InertiaResponse(Response[T]):
526
240
  if self.template_str is not None:
527
241
  body = template_engine.render_string(self.template_str, context).encode(self.encoding) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
528
242
  else:
529
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
530
243
  template_name = self.template_name or inertia_plugin.config.root_template
531
244
  template = template_engine.get_template(template_name) # pyright: ignore[reportUnknownMemberType,reportUnknownVariableType]
532
245
  body = template.render(**context).encode(self.encoding) # pyright: ignore[reportUnknownVariableType,reportUnknownMemberType]
litestar_vite/plugin.py CHANGED
@@ -6,12 +6,12 @@ import signal
6
6
  import subprocess
7
7
  import threading
8
8
  from contextlib import contextmanager
9
+ from dataclasses import dataclass
9
10
  from pathlib import Path
10
- from typing import TYPE_CHECKING, Iterator, cast
11
+ from typing import TYPE_CHECKING, Any, Iterator, Sequence
11
12
 
12
13
  from litestar.cli._utils import console
13
14
  from litestar.contrib.jinja import JinjaTemplateEngine
14
- from litestar.exceptions import ImproperlyConfiguredException
15
15
  from litestar.plugins import CLIPlugin, InitPluginProtocol
16
16
  from litestar.static_files import create_static_files_router # pyright: ignore[reportUnknownVariableType]
17
17
 
@@ -19,6 +19,16 @@ if TYPE_CHECKING:
19
19
  from click import Group
20
20
  from litestar import Litestar
21
21
  from litestar.config.app import AppConfig
22
+ from litestar.datastructures import CacheControlHeader
23
+ from litestar.openapi.spec import SecurityRequirement
24
+ from litestar.types import (
25
+ AfterRequestHookHandler, # pyright: ignore[reportUnknownVariableType]
26
+ AfterResponseHookHandler, # pyright: ignore[reportUnknownVariableType]
27
+ BeforeRequestHookHandler, # pyright: ignore[reportUnknownVariableType]
28
+ ExceptionHandlersMap,
29
+ Guard, # pyright: ignore[reportUnknownVariableType]
30
+ Middleware,
31
+ )
22
32
 
23
33
  from litestar_vite.config import ViteConfig
24
34
  from litestar_vite.loader import ViteAssetLoader
@@ -36,6 +46,20 @@ def set_environment(config: ViteConfig) -> None:
36
46
  os.environ.setdefault("VITE_DEV_MODE", str(config.dev_mode))
37
47
 
38
48
 
49
+ @dataclass
50
+ class StaticFilesConfig:
51
+ after_request: AfterRequestHookHandler | None = None
52
+ after_response: AfterResponseHookHandler | None = None
53
+ before_request: BeforeRequestHookHandler | None = None
54
+ cache_control: CacheControlHeader | None = None
55
+ exception_handlers: ExceptionHandlersMap | None = None
56
+ guards: list[Guard] | None = None
57
+ middleware: Sequence[Middleware] | None = None
58
+ opt: dict[str, Any] | None = None
59
+ security: Sequence[SecurityRequirement] | None = None
60
+ tags: Sequence[str] | None = None
61
+
62
+
39
63
  class ViteProcess:
40
64
  """Manages the Vite process."""
41
65
 
@@ -86,14 +110,20 @@ class ViteProcess:
86
110
  class VitePlugin(InitPluginProtocol, CLIPlugin):
87
111
  """Vite plugin."""
88
112
 
89
- __slots__ = ("_asset_loader", "_config", "_vite_process")
113
+ __slots__ = ("_asset_loader", "_config", "_static_files_config", "_vite_process")
90
114
 
91
- def __init__(self, config: ViteConfig | None = None, asset_loader: ViteAssetLoader | None = None) -> None:
115
+ def __init__(
116
+ self,
117
+ config: ViteConfig | None = None,
118
+ asset_loader: ViteAssetLoader | None = None,
119
+ static_files_config: StaticFilesConfig | None = None,
120
+ ) -> None:
92
121
  """Initialize ``Vite``.
93
122
 
94
123
  Args:
95
124
  config: configuration to use for starting Vite. The default configuration will be used if it is not provided.
96
125
  asset_loader: an initialized asset loader to use for rendering asset tags.
126
+ static_files_config: optional configuration dictionary for the static files router.
97
127
  """
98
128
  from litestar_vite.config import ViteConfig
99
129
 
@@ -102,6 +132,7 @@ class VitePlugin(InitPluginProtocol, CLIPlugin):
102
132
  self._config = config
103
133
  self._asset_loader = asset_loader
104
134
  self._vite_process = ViteProcess()
135
+ self._static_files_config: dict[str, Any] = static_files_config.__dict__ if static_files_config else {}
105
136
 
106
137
  @property
107
138
  def config(self) -> ViteConfig:
@@ -128,37 +159,29 @@ class VitePlugin(InitPluginProtocol, CLIPlugin):
128
159
  """
129
160
  from litestar_vite.loader import render_asset_tag, render_hmr_client
130
161
 
131
- if app_config.template_config is None: # pyright: ignore[reportUnknownMemberType]
132
- msg = "A template configuration is required for Vite."
133
- raise ImproperlyConfiguredException(msg)
134
- if not isinstance(app_config.template_config.engine_instance, JinjaTemplateEngine): # pyright: ignore[reportUnknownMemberType]
135
- msg = "Jinja2 template engine is required for Vite."
136
- raise ImproperlyConfiguredException(msg)
137
- app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
138
- key="vite_hmr",
139
- template_callable=render_hmr_client,
140
- )
141
- app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
142
- key="vite",
143
- template_callable=render_asset_tag,
144
- )
162
+ if app_config.template_config and isinstance(app_config.template_config.engine_instance, JinjaTemplateEngine): # pyright: ignore[reportUnknownMemberType]
163
+ app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
164
+ key="vite_hmr",
165
+ template_callable=render_hmr_client,
166
+ )
167
+ app_config.template_config.engine_instance.register_template_callable( # pyright: ignore[reportUnknownMemberType]
168
+ key="vite",
169
+ template_callable=render_asset_tag,
170
+ )
145
171
  if self._config.set_static_folders:
146
172
  static_dirs = [Path(self._config.bundle_dir), Path(self._config.resource_dir)]
147
173
  if Path(self._config.public_dir).exists() and self._config.public_dir != self._config.bundle_dir:
148
174
  static_dirs.append(Path(self._config.public_dir))
149
- app_config.route_handlers.append(
150
- create_static_files_router(
151
- directories=cast( # type: ignore[arg-type]
152
- "list[Path]",
153
- static_dirs if self._config.dev_mode else [Path(self._config.bundle_dir)],
154
- ),
155
- path=self._config.asset_url,
156
- name="vite",
157
- html_mode=False,
158
- include_in_schema=False,
159
- opt={"exclude_from_auth": True},
160
- ),
161
- )
175
+ base_config = {
176
+ "directories": static_dirs if self._config.dev_mode else [Path(self._config.bundle_dir)],
177
+ "path": self._config.asset_url,
178
+ "name": "vite",
179
+ "html_mode": False,
180
+ "include_in_schema": False,
181
+ "opt": {"exclude_from_auth": True},
182
+ }
183
+ static_files_config: dict[str, Any] = {**base_config, **self._static_files_config}
184
+ app_config.route_handlers.append(create_static_files_router(**static_files_config))
162
185
  return app_config
163
186
 
164
187
  @contextmanager
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: litestar-vite
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Vite plugin for Litestar
5
5
  Project-URL: Changelog, https://cofin.github.io/litestar-vite/latest/changelog
6
6
  Project-URL: Discord, https://discord.gg/X3FJqy8d2j
@@ -1,19 +1,20 @@
1
1
  litestar_vite/__init__.py,sha256=OioNGhH88mdivQlFz9JlbJV8R6wyjSYE3c8C-RIM4Ls,277
2
2
  litestar_vite/__metadata__.py,sha256=_Wo-vNQuj5co9J4FwJAB2rRafbFo8ztTHrXmEPrYrV8,514
3
3
  litestar_vite/cli.py,sha256=CBSRohDLU9cDeKMAfSbFiw1x8OE_b15ZlUaxji9Rdw8,10749
4
- litestar_vite/commands.py,sha256=JTRBvpR_3ddFb2o4-AJSELegw-ZGDsJ_yMaT5FVdp4w,5228
4
+ litestar_vite/commands.py,sha256=NFRTA_VoeFuZVk6bOINJTdP9DGGSAIZRVsM-SlDykNk,5228
5
5
  litestar_vite/config.py,sha256=cZWIwTwNnBYScCty8OxxPaOL8cELx57dm7JQeV8og3Y,4565
6
6
  litestar_vite/loader.py,sha256=nrXL2txXoBZEsdLZnysgBYZSreMXQ7ckLuNcu7MqnSM,10277
7
- litestar_vite/plugin.py,sha256=2ypzvlW5TaOeXTWPa2yHXzkPUf4okRBDl9YHo2qg-cM,8043
7
+ litestar_vite/plugin.py,sha256=nglizc45_CBG1gqZRDxyGo8cc_KZ1yOJfAS0XiSadpg,9119
8
8
  litestar_vite/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- litestar_vite/inertia/__init__.py,sha256=Fab61KXbBnyub2Nx-2AHYv2U6QiYUIrqhEZia_f9xik,863
9
+ litestar_vite/inertia/__init__.py,sha256=KGvxCZhnOw06Pqx5_qjUxj0WWsCR3BR0cVnuNMT7sKQ,1136
10
10
  litestar_vite/inertia/_utils.py,sha256=ijO9Lgka7ZPIAHkby9szbTGoSg0nDShC2bqWT9cDxi0,1956
11
11
  litestar_vite/inertia/config.py,sha256=0Je9SLg0acv0eRvudk3aJLj5k1DjPxULoVOwAfpjnUc,1232
12
- litestar_vite/inertia/exception_handler.py,sha256=0FiW8jVib0xT453BzMPOeJa7bRwnxkOUdJ91kiFHPIo,5308
12
+ litestar_vite/inertia/exception_handler.py,sha256=BU7vOK7C2iW52_J5xGJZMjYX3EqIg1OpAF3PgbaLSZ4,5349
13
+ litestar_vite/inertia/helpers.py,sha256=9XVQUAqmiXOTUGVLgxPWPgftkRCx99jr6LXPiD35YJE,10571
13
14
  litestar_vite/inertia/middleware.py,sha256=23HfQ8D2wNGXUXt_isuGfZ8AFKrr1d_498qGFLynocs,1650
14
- litestar_vite/inertia/plugin.py,sha256=f2oFwlEDt9WZ8zt-AqiCIRPlkHIyzLtPYYo8WJZX-tI,3186
15
+ litestar_vite/inertia/plugin.py,sha256=iVF1c8E7M6IdZK_S1nZW8ZTi7vfIllMAnIUX0pVdrFQ,3686
15
16
  litestar_vite/inertia/request.py,sha256=Ogt_ikauWrsgKafaip7IL1YhbybwjdBAQ0PQS7cImoQ,3848
16
- litestar_vite/inertia/response.py,sha256=CY-qziEPc9FZ4oXxtgO0Iipg1Hz4x-fiUeLK04Br8kM,23425
17
+ litestar_vite/inertia/response.py,sha256=YjfabkYkGxDwbuo-WLgbUAHcCi0jgZYM_qdnUbI6Pas,13767
17
18
  litestar_vite/inertia/routes.py,sha256=QksJm2RUfL-WbuhOieYnPXXWO5GYnPtmsYEm6Ef8Yeo,1782
18
19
  litestar_vite/inertia/types.py,sha256=tLp0pm1N__hcWC875khf6wH1nuFlKS9-VjDqgsRkXnw,702
19
20
  litestar_vite/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -23,7 +24,7 @@ litestar_vite/templates/package.json.j2,sha256=0JWgdTuaSZ25EmCltF_zbqDdpxfvCLeYu
23
24
  litestar_vite/templates/styles.css.j2,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
25
  litestar_vite/templates/tsconfig.json.j2,sha256=q1REIuVyXUHCy4Zi2kgTkmrhdT98vyY89k-WTrImOj8,843
25
26
  litestar_vite/templates/vite.config.ts.j2,sha256=bF5kOPFafYMkhhV0VkIwetN-_zoVMGVM1jEMX_wKoNc,1037
26
- litestar_vite-0.12.0.dist-info/METADATA,sha256=0Nv7ZBioTYS6JTE0KzPht82kwBhcBGH7W1cBRB8vgCM,6244
27
- litestar_vite-0.12.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
28
- litestar_vite-0.12.0.dist-info/licenses/LICENSE,sha256=HeTiEfEgvroUXZe_xAmYHxtTBgw--mbXyZLsWDYabHc,1069
29
- litestar_vite-0.12.0.dist-info/RECORD,,
27
+ litestar_vite-0.13.0.dist-info/METADATA,sha256=qxVk_WT5C6YZaPsDHfPaoauTTXJQh9dL_6BUCVrUeCY,6244
28
+ litestar_vite-0.13.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ litestar_vite-0.13.0.dist-info/licenses/LICENSE,sha256=HeTiEfEgvroUXZe_xAmYHxtTBgw--mbXyZLsWDYabHc,1069
30
+ litestar_vite-0.13.0.dist-info/RECORD,,