litestar-vite 0.11.1__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,63 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from enum import Enum
4
- from typing import TYPE_CHECKING, Any, Callable
5
-
6
- if TYPE_CHECKING:
7
- from litestar_vite.inertia.types import InertiaHeaderType
8
-
9
-
10
- class InertiaHeaders(str, Enum):
11
- """Enum for Inertia Headers"""
12
-
13
- ENABLED = "X-Inertia"
14
- VERSION = "X-Inertia-Version"
15
- PARTIAL_DATA = "X-Inertia-Partial-Data"
16
- PARTIAL_COMPONENT = "X-Inertia-Partial-Component"
17
- LOCATION = "X-Inertia-Location"
18
- REFERER = "Referer"
19
-
20
-
21
- def get_enabled_header(enabled: bool = True) -> dict[str, Any]:
22
- """True if inertia is enabled."""
23
-
24
- return {InertiaHeaders.ENABLED.value: "true" if enabled else "false"}
25
-
26
-
27
- def get_version_header(version: str) -> dict[str, Any]:
28
- """Return headers for change swap method response."""
29
- return {InertiaHeaders.VERSION.value: version}
30
-
31
-
32
- def get_partial_data_header(partial: str) -> dict[str, Any]:
33
- """Return headers for a partial data response."""
34
- return {InertiaHeaders.PARTIAL_DATA.value: partial}
35
-
36
-
37
- def get_partial_component_header(partial: str) -> dict[str, Any]:
38
- """Return headers for a partial data response."""
39
- return {InertiaHeaders.PARTIAL_COMPONENT.value: partial}
40
-
41
-
42
- def get_headers(inertia_headers: InertiaHeaderType) -> dict[str, Any]:
43
- """Return headers for Inertia responses."""
44
- if not inertia_headers:
45
- msg = "Value for inertia_headers cannot be None."
46
- raise ValueError(msg)
47
- inertia_headers_dict: dict[str, Callable[..., dict[str, Any]]] = {
48
- "enabled": get_enabled_header,
49
- "partial_data": get_partial_data_header,
50
- "partial_component": get_partial_component_header,
51
- "version": get_version_header,
52
- }
53
-
54
- header: dict[str, Any] = {}
55
- response: dict[str, Any]
56
- key: str
57
- value: Any
58
-
59
- for key, value in inertia_headers.items():
60
- if value is not None:
61
- response = inertia_headers_dict[key](value)
62
- header.update(response)
63
- return header
@@ -1,29 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from typing import Any
5
-
6
- __all__ = ("InertiaConfig",)
7
-
8
-
9
- @dataclass
10
- class InertiaConfig:
11
- """Configuration for InertiaJS support."""
12
-
13
- root_template: str = "index.html"
14
- """Name of the root template to use.
15
-
16
- This must be a path that is found by the Vite Plugin template config
17
- """
18
- component_opt_key: str = "component"
19
- """An identifier to use on routes to get the inertia component to render."""
20
- exclude_from_js_routes_key: str = "exclude_from_routes"
21
- """An identifier to use on routes to exclude a route from the generated routes typescript file."""
22
- redirect_unauthorized_to: str | None = None
23
- """Optionally supply a path where unauthorized requests should redirect."""
24
- redirect_404: str | None = None
25
- """Optionally supply a path where 404 requests should redirect."""
26
- extra_static_page_props: dict[str, Any] = field(default_factory=dict)
27
- """A dictionary of values to automatically add in to page props on every response."""
28
- extra_session_page_props: set[str] = field(default_factory=set)
29
- """A set of session keys for which the value automatically be added (if it exists) to the response."""
@@ -1,116 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import re
4
- from typing import TYPE_CHECKING, Any, cast
5
-
6
- from litestar import MediaType
7
- from litestar.connection import Request
8
- from litestar.connection.base import AuthT, StateT, UserT
9
- from litestar.exceptions import (
10
- HTTPException,
11
- ImproperlyConfiguredException,
12
- InternalServerException,
13
- NotAuthorizedException,
14
- NotFoundException,
15
- PermissionDeniedException,
16
- )
17
- from litestar.exceptions.responses import (
18
- create_debug_response, # pyright: ignore[reportUnknownVariableType]
19
- create_exception_response, # pyright: ignore[reportUnknownVariableType]
20
- )
21
- from litestar.plugins.flash import flash
22
- from litestar.repository.exceptions import (
23
- ConflictError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
24
- NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
25
- RepositoryError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
26
- )
27
- from litestar.response import Response
28
- from litestar.status_codes import (
29
- HTTP_400_BAD_REQUEST,
30
- HTTP_401_UNAUTHORIZED,
31
- HTTP_404_NOT_FOUND,
32
- HTTP_405_METHOD_NOT_ALLOWED,
33
- HTTP_409_CONFLICT,
34
- HTTP_422_UNPROCESSABLE_ENTITY,
35
- HTTP_500_INTERNAL_SERVER_ERROR,
36
- )
37
-
38
- from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse, error
39
-
40
- if TYPE_CHECKING:
41
- from litestar.connection import Request
42
- from litestar.connection.base import AuthT, StateT, UserT
43
- from litestar.response import Response
44
-
45
- from litestar_vite.inertia.plugin import InertiaPlugin
46
-
47
- FIELD_ERR_RE = re.compile(r"field `(.+)`$")
48
-
49
-
50
- class _HTTPConflictException(HTTPException):
51
- """Request conflict with the current state of the target resource."""
52
-
53
- status_code: int = HTTP_409_CONFLICT
54
-
55
-
56
- def exception_to_http_response(request: Request[UserT, AuthT, StateT], exc: Exception) -> Response[Any]:
57
- """Handler for all exceptions subclassed from HTTPException."""
58
- inertia_enabled = getattr(request, "inertia_enabled", False) or getattr(request, "is_inertia", False)
59
-
60
- if not inertia_enabled:
61
- if isinstance(exc, NotFoundError):
62
- http_exc = NotFoundException
63
- elif isinstance(exc, (RepositoryError, ConflictError)):
64
- http_exc = _HTTPConflictException # type: ignore[assignment]
65
- else:
66
- http_exc = InternalServerException # type: ignore[assignment]
67
- if request.app.debug and http_exc not in (PermissionDeniedException, NotFoundError):
68
- return cast("Response[Any]", create_debug_response(request, exc))
69
- return cast("Response[Any]", create_exception_response(request, http_exc(detail=str(exc.__cause__)))) # pyright: ignore[reportUnknownArgumentType]
70
- return create_inertia_exception_response(request, exc)
71
-
72
-
73
- def create_inertia_exception_response(request: Request[UserT, AuthT, StateT], exc: Exception) -> Response[Any]:
74
- """Create the inertia exception response"""
75
- is_inertia = getattr(request, "is_inertia", False)
76
- status_code = getattr(exc, "status_code", HTTP_500_INTERNAL_SERVER_ERROR)
77
- preferred_type = MediaType.HTML if not is_inertia else MediaType.JSON
78
- detail = getattr(exc, "detail", "") # litestar exceptions
79
- extras = getattr(exc, "extra", "") # msgspec exceptions
80
- content: dict[str, Any] = {"status_code": status_code, "message": getattr(exc, "detail", "")}
81
- inertia_plugin = cast("InertiaPlugin", request.app.plugins.get("InertiaPlugin"))
82
- if extras:
83
- content.update({"extra": extras})
84
- try:
85
- flash(request, detail, category="error")
86
- except (AttributeError, ImproperlyConfiguredException):
87
- msg = "Unable to set `flash` session state. A valid session was not found for this request."
88
- request.logger.warning(msg)
89
- if extras and len(extras) >= 1:
90
- message = extras[0]
91
- default_field = f"root.{message.get('key')}" if message.get("key", None) is not None else "root" # type: ignore
92
- error_detail = cast("str", message.get("message", detail)) # type: ignore[union-attr] # pyright: ignore[reportUnknownMemberType]
93
- match = FIELD_ERR_RE.search(error_detail)
94
- field = match.group(1) if match else default_field
95
- if isinstance(message, dict):
96
- error(request, field, error_detail if error_detail else detail)
97
- if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST}:
98
- return InertiaBack(request)
99
- if isinstance(exc, PermissionDeniedException):
100
- return InertiaBack(request)
101
- if (status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException)) and (
102
- inertia_plugin.config.redirect_unauthorized_to is not None
103
- and request.url.path != inertia_plugin.config.redirect_unauthorized_to
104
- ):
105
- return InertiaRedirect(request, redirect_to=inertia_plugin.config.redirect_unauthorized_to)
106
-
107
- if status_code in {HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED} and (
108
- inertia_plugin.config.redirect_404 is not None and request.url.path != inertia_plugin.config.redirect_404
109
- ):
110
- return InertiaRedirect(request, redirect_to=inertia_plugin.config.redirect_404)
111
-
112
- return InertiaResponse[Any](
113
- media_type=preferred_type,
114
- content=content,
115
- status_code=status_code,
116
- )
@@ -1,51 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING, Any
4
-
5
- from litestar import Request
6
- from litestar.middleware import AbstractMiddleware
7
- from litestar.types import Receive, Scope, Send
8
-
9
- from litestar_vite.inertia.response import InertiaRedirect
10
- from litestar_vite.plugin import VitePlugin
11
-
12
- if TYPE_CHECKING:
13
- from litestar.connection.base import (
14
- AuthT,
15
- StateT,
16
- UserT,
17
- )
18
- from litestar.types import ASGIApp, Receive, Scope, Send
19
-
20
-
21
- async def redirect_on_asset_version_mismatch(request: Request[UserT, AuthT, StateT]) -> InertiaRedirect | None:
22
- if getattr(request, "is_inertia", None) is None:
23
- return None
24
- inertia_version = request.headers.get("X-Inertia-Version")
25
- if inertia_version is None:
26
- return None
27
-
28
- vite_plugin = request.app.plugins.get(VitePlugin)
29
- if inertia_version == vite_plugin.asset_loader.version_id:
30
- return None
31
- return InertiaRedirect(request, redirect_to=str(request.url))
32
-
33
-
34
- class InertiaMiddleware(AbstractMiddleware):
35
- def __init__(self, app: ASGIApp) -> None:
36
- super().__init__(app)
37
- self.app = app
38
-
39
- async def __call__(
40
- self,
41
- scope: "Scope",
42
- receive: "Receive",
43
- send: "Send",
44
- ) -> None:
45
- request = Request[Any, Any, Any](scope=scope)
46
- redirect = await redirect_on_asset_version_mismatch(request)
47
- if redirect is not None:
48
- response = redirect.to_asgi_response(app=None, request=request) # pyright: ignore[reportUnknownMemberType]
49
- await response(scope, receive, send)
50
- else:
51
- await self.app(scope, receive, send)
@@ -1,64 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from litestar.exceptions import ImproperlyConfiguredException
6
- from litestar.middleware import DefineMiddleware
7
- from litestar.middleware.session import SessionMiddleware
8
- from litestar.plugins import InitPluginProtocol
9
- from litestar.security.session_auth.middleware import MiddlewareWrapper
10
- from litestar.utils.predicates import is_class_and_subclass
11
-
12
- from litestar_vite.inertia.exception_handler import exception_to_http_response
13
- from litestar_vite.inertia.middleware import InertiaMiddleware
14
- from litestar_vite.inertia.request import InertiaRequest
15
- from litestar_vite.inertia.response import InertiaResponse
16
- from litestar_vite.inertia.routes import generate_js_routes
17
-
18
- if TYPE_CHECKING:
19
- from litestar import Litestar
20
- from litestar.config.app import AppConfig
21
-
22
- from litestar_vite.inertia.config import InertiaConfig
23
-
24
-
25
- def set_js_routes(app: Litestar) -> None:
26
- """Generate the route structure of the application on startup."""
27
- js_routes = generate_js_routes(app)
28
- app.state.js_routes = js_routes
29
-
30
-
31
- class InertiaPlugin(InitPluginProtocol):
32
- """Inertia plugin."""
33
-
34
- __slots__ = ("config",)
35
-
36
- def __init__(self, config: InertiaConfig) -> None:
37
- """Initialize ``Inertia``.
38
-
39
- Args:
40
- config: Inertia configuration.
41
- """
42
- self.config = config
43
-
44
- def on_app_init(self, app_config: AppConfig) -> AppConfig:
45
- """Configure application for use with Vite.
46
-
47
- Args:
48
- app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
49
- """
50
- for mw in app_config.middleware:
51
- if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
52
- mw.middleware,
53
- (MiddlewareWrapper, SessionMiddleware),
54
- ):
55
- break
56
- else:
57
- msg = "The Inertia plugin require a session middleware."
58
- raise ImproperlyConfiguredException(msg)
59
- app_config.exception_handlers.update({Exception: exception_to_http_response}) # pyright: ignore[reportUnknownMemberType]
60
- app_config.request_class = InertiaRequest
61
- app_config.response_class = InertiaResponse
62
- app_config.middleware.append(InertiaMiddleware)
63
- app_config.on_startup.append(set_js_routes)
64
- return app_config
@@ -1,116 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from functools import cached_property
4
- from typing import TYPE_CHECKING
5
- from urllib.parse import unquote
6
-
7
- from litestar import Request
8
- from litestar.connection.base import (
9
- AuthT,
10
- StateT,
11
- UserT,
12
- empty_receive,
13
- empty_send,
14
- )
15
-
16
- from litestar_vite.inertia._utils import InertiaHeaders
17
-
18
- __all__ = ("InertiaDetails", "InertiaRequest")
19
-
20
-
21
- if TYPE_CHECKING:
22
- from litestar.types import Receive, Scope, Send
23
-
24
-
25
- class InertiaDetails:
26
- """InertiaDetails holds all the values sent by Inertia client in headers and provide convenient properties."""
27
-
28
- def __init__(self, request: Request[UserT, AuthT, StateT]) -> None:
29
- """Initialize :class:`InertiaDetails`"""
30
- self.request = request
31
-
32
- def _get_header_value(self, name: InertiaHeaders) -> str | None:
33
- """Parse request header
34
-
35
- Check for uri encoded header and unquotes it in readable format.
36
- """
37
-
38
- if value := self.request.headers.get(name.value.lower()):
39
- is_uri_encoded = self.request.headers.get(f"{name.value.lower()}-uri-autoencoded") == "true"
40
- return unquote(value) if is_uri_encoded else value
41
- return None
42
-
43
- def _get_route_component(self) -> str | None:
44
- """Get the route component.
45
-
46
- Checks for the `component` key within the route configuration.
47
- """
48
- rh = self.request.scope.get("route_handler") # pyright: ignore[reportUnknownMemberType]
49
- if rh:
50
- return rh.opt.get("component")
51
- return None
52
-
53
- def __bool__(self) -> bool:
54
- """Check if request is sent by an Inertia client."""
55
- return self._get_header_value(InertiaHeaders.ENABLED) == "true"
56
-
57
- @cached_property
58
- def route_component(self) -> str | None:
59
- """Partial Data Reload."""
60
- return self._get_route_component()
61
-
62
- @cached_property
63
- def partial_component(self) -> str | None:
64
- """Partial Data Reload."""
65
- return self._get_header_value(InertiaHeaders.PARTIAL_COMPONENT)
66
-
67
- @cached_property
68
- def partial_data(self) -> str | None:
69
- """Partial Data Reload."""
70
- return self._get_header_value(InertiaHeaders.PARTIAL_DATA)
71
-
72
- @cached_property
73
- def referer(self) -> str | None:
74
- """Partial Data Reload."""
75
- return self._get_header_value(InertiaHeaders.REFERER)
76
-
77
- @cached_property
78
- def is_partial_render(self) -> bool:
79
- """Is Partial Data Reload."""
80
- return bool(self.partial_component == self.route_component and self.partial_data)
81
-
82
- @cached_property
83
- def partial_keys(self) -> list[str]:
84
- """Is Partial Data Reload."""
85
- return self.partial_data.split(",") if self.partial_data is not None else []
86
-
87
-
88
- class InertiaRequest(Request[UserT, AuthT, StateT]):
89
- """Inertia Request class to work with Inertia client."""
90
-
91
- __slots__ = ("inertia",)
92
-
93
- def __init__(self, scope: Scope, receive: Receive = empty_receive, send: Send = empty_send) -> None:
94
- """Initialize :class:`InertiaRequest`"""
95
- super().__init__(scope=scope, receive=receive, send=send)
96
- self.inertia = InertiaDetails(self)
97
-
98
- @property
99
- def is_inertia(self) -> bool:
100
- """True if the request contained inertia headers."""
101
- return bool(self.inertia)
102
-
103
- @property
104
- def inertia_enabled(self) -> bool:
105
- """True if the route handler contains an inertia enabled configuration."""
106
- return bool(self.inertia.route_component is not None)
107
-
108
- @property
109
- def is_partial_render(self) -> bool:
110
- """True if the route handler contains an inertia enabled configuration."""
111
- return self.inertia.is_partial_render
112
-
113
- @property
114
- def partial_keys(self) -> set[str]:
115
- """True if the route handler contains an inertia enabled configuration."""
116
- return set(self.inertia.partial_keys)