litestar-vite 0.1.1__py3-none-any.whl → 0.15.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.
- litestar_vite/__init__.py +54 -4
- litestar_vite/__metadata__.py +12 -7
- litestar_vite/cli.py +1048 -10
- litestar_vite/codegen/__init__.py +48 -0
- litestar_vite/codegen/_export.py +229 -0
- litestar_vite/codegen/_inertia.py +619 -0
- litestar_vite/codegen/_openapi.py +280 -0
- litestar_vite/codegen/_routes.py +720 -0
- litestar_vite/codegen/_ts.py +235 -0
- litestar_vite/codegen/_utils.py +141 -0
- litestar_vite/commands.py +73 -0
- litestar_vite/config/__init__.py +997 -0
- litestar_vite/config/_constants.py +97 -0
- litestar_vite/config/_deploy.py +70 -0
- litestar_vite/config/_inertia.py +241 -0
- litestar_vite/config/_paths.py +63 -0
- litestar_vite/config/_runtime.py +235 -0
- litestar_vite/config/_spa.py +93 -0
- litestar_vite/config/_types.py +94 -0
- litestar_vite/deploy.py +366 -0
- litestar_vite/doctor.py +1181 -0
- litestar_vite/exceptions.py +78 -0
- litestar_vite/executor.py +360 -0
- litestar_vite/handler/__init__.py +9 -0
- litestar_vite/handler/_app.py +612 -0
- litestar_vite/handler/_routing.py +130 -0
- litestar_vite/html_transform.py +569 -0
- litestar_vite/inertia/__init__.py +77 -0
- litestar_vite/inertia/_utils.py +119 -0
- litestar_vite/inertia/exception_handler.py +178 -0
- litestar_vite/inertia/helpers.py +1571 -0
- litestar_vite/inertia/middleware.py +54 -0
- litestar_vite/inertia/plugin.py +199 -0
- litestar_vite/inertia/precognition.py +274 -0
- litestar_vite/inertia/request.py +334 -0
- litestar_vite/inertia/response.py +802 -0
- litestar_vite/inertia/types.py +335 -0
- litestar_vite/loader.py +464 -123
- litestar_vite/plugin/__init__.py +687 -0
- litestar_vite/plugin/_process.py +185 -0
- litestar_vite/plugin/_proxy.py +689 -0
- litestar_vite/plugin/_proxy_headers.py +244 -0
- litestar_vite/plugin/_static.py +37 -0
- litestar_vite/plugin/_utils.py +489 -0
- litestar_vite/py.typed +0 -0
- litestar_vite/scaffolding/__init__.py +20 -0
- litestar_vite/scaffolding/generator.py +270 -0
- litestar_vite/scaffolding/templates.py +437 -0
- litestar_vite/templates/__init__.py +0 -0
- litestar_vite/templates/addons/tailwindcss/tailwind.css.j2 +1 -0
- litestar_vite/templates/angular/index.html.j2 +12 -0
- litestar_vite/templates/angular/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular/package.json.j2 +36 -0
- litestar_vite/templates/angular/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular/src/main.ts.j2 +9 -0
- litestar_vite/templates/angular/src/styles.css.j2 +9 -0
- litestar_vite/templates/angular/tsconfig.app.json.j2 +34 -0
- litestar_vite/templates/angular/tsconfig.json.j2 +20 -0
- litestar_vite/templates/angular/vite.config.ts.j2 +21 -0
- litestar_vite/templates/angular-cli/.postcssrc.json.j2 +5 -0
- litestar_vite/templates/angular-cli/angular.json.j2 +36 -0
- litestar_vite/templates/angular-cli/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/angular-cli/package.json.j2 +28 -0
- litestar_vite/templates/angular-cli/proxy.conf.json.j2 +18 -0
- litestar_vite/templates/angular-cli/src/app/app.component.css.j2 +3 -0
- litestar_vite/templates/angular-cli/src/app/app.component.html.j2 +1 -0
- litestar_vite/templates/angular-cli/src/app/app.component.ts.j2 +9 -0
- litestar_vite/templates/angular-cli/src/app/app.config.ts.j2 +5 -0
- litestar_vite/templates/angular-cli/src/index.html.j2 +13 -0
- litestar_vite/templates/angular-cli/src/main.ts.j2 +6 -0
- litestar_vite/templates/angular-cli/src/styles.css.j2 +10 -0
- litestar_vite/templates/angular-cli/tailwind.config.js.j2 +4 -0
- litestar_vite/templates/angular-cli/tsconfig.app.json.j2 +16 -0
- litestar_vite/templates/angular-cli/tsconfig.json.j2 +26 -0
- litestar_vite/templates/angular-cli/tsconfig.spec.json.j2 +9 -0
- litestar_vite/templates/astro/astro.config.mjs.j2 +28 -0
- litestar_vite/templates/astro/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/astro/src/layouts/Layout.astro.j2 +63 -0
- litestar_vite/templates/astro/src/pages/index.astro.j2 +36 -0
- litestar_vite/templates/astro/src/styles/global.css.j2 +1 -0
- litestar_vite/templates/base/.gitignore.j2 +42 -0
- litestar_vite/templates/base/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/base/package.json.j2 +39 -0
- litestar_vite/templates/base/resources/vite-env.d.ts.j2 +1 -0
- litestar_vite/templates/base/tsconfig.json.j2 +37 -0
- litestar_vite/templates/htmx/src/main.js.j2 +8 -0
- litestar_vite/templates/htmx/templates/base.html.j2.j2 +56 -0
- litestar_vite/templates/htmx/templates/index.html.j2.j2 +13 -0
- litestar_vite/templates/htmx/vite.config.ts.j2 +40 -0
- litestar_vite/templates/nuxt/app.vue.j2 +29 -0
- litestar_vite/templates/nuxt/composables/useApi.ts.j2 +33 -0
- litestar_vite/templates/nuxt/nuxt.config.ts.j2 +31 -0
- litestar_vite/templates/nuxt/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/nuxt/pages/index.vue.j2 +54 -0
- litestar_vite/templates/react/index.html.j2 +13 -0
- litestar_vite/templates/react/src/App.css.j2 +56 -0
- litestar_vite/templates/react/src/App.tsx.j2 +19 -0
- litestar_vite/templates/react/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-inertia/index.html.j2 +14 -0
- litestar_vite/templates/react-inertia/package.json.j2 +47 -0
- litestar_vite/templates/react-inertia/resources/App.css.j2 +68 -0
- litestar_vite/templates/react-inertia/resources/main.tsx.j2 +17 -0
- litestar_vite/templates/react-inertia/resources/pages/Home.tsx.j2 +18 -0
- litestar_vite/templates/react-inertia/resources/ssr.tsx.j2 +19 -0
- litestar_vite/templates/react-inertia/vite.config.ts.j2 +59 -0
- litestar_vite/templates/react-router/index.html.j2 +12 -0
- litestar_vite/templates/react-router/src/App.css.j2 +17 -0
- litestar_vite/templates/react-router/src/App.tsx.j2 +7 -0
- litestar_vite/templates/react-router/src/main.tsx.j2 +10 -0
- litestar_vite/templates/react-router/vite.config.ts.j2 +39 -0
- litestar_vite/templates/react-tanstack/index.html.j2 +12 -0
- litestar_vite/templates/react-tanstack/openapi-ts.config.ts.j2 +18 -0
- litestar_vite/templates/react-tanstack/src/App.css.j2 +17 -0
- litestar_vite/templates/react-tanstack/src/main.tsx.j2 +21 -0
- litestar_vite/templates/react-tanstack/src/routeTree.gen.ts.j2 +7 -0
- litestar_vite/templates/react-tanstack/src/routes/__root.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/books.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/src/routes/index.tsx.j2 +9 -0
- litestar_vite/templates/react-tanstack/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte/index.html.j2 +13 -0
- litestar_vite/templates/svelte/src/App.svelte.j2 +30 -0
- litestar_vite/templates/svelte/src/app.css.j2 +45 -0
- litestar_vite/templates/svelte/src/main.ts.j2 +8 -0
- litestar_vite/templates/svelte/src/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte/vite.config.ts.j2 +39 -0
- litestar_vite/templates/svelte-inertia/index.html.j2 +14 -0
- litestar_vite/templates/svelte-inertia/resources/app.css.j2 +21 -0
- litestar_vite/templates/svelte-inertia/resources/main.ts.j2 +11 -0
- litestar_vite/templates/svelte-inertia/resources/pages/Home.svelte.j2 +43 -0
- litestar_vite/templates/svelte-inertia/resources/vite-env.d.ts.j2 +2 -0
- litestar_vite/templates/svelte-inertia/svelte.config.js.j2 +5 -0
- litestar_vite/templates/svelte-inertia/vite.config.ts.j2 +37 -0
- litestar_vite/templates/sveltekit/openapi-ts.config.ts.j2 +15 -0
- litestar_vite/templates/sveltekit/src/app.css.j2 +40 -0
- litestar_vite/templates/sveltekit/src/app.html.j2 +12 -0
- litestar_vite/templates/sveltekit/src/hooks.server.ts.j2 +55 -0
- litestar_vite/templates/sveltekit/src/routes/+layout.svelte.j2 +12 -0
- litestar_vite/templates/sveltekit/src/routes/+page.svelte.j2 +34 -0
- litestar_vite/templates/sveltekit/svelte.config.js.j2 +12 -0
- litestar_vite/templates/sveltekit/tsconfig.json.j2 +14 -0
- litestar_vite/templates/sveltekit/vite.config.ts.j2 +31 -0
- litestar_vite/templates/vue/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue/index.html.j2 +13 -0
- litestar_vite/templates/vue/src/App.vue.j2 +28 -0
- litestar_vite/templates/vue/src/main.ts.j2 +5 -0
- litestar_vite/templates/vue/src/style.css.j2 +45 -0
- litestar_vite/templates/vue/vite.config.ts.j2 +39 -0
- litestar_vite/templates/vue-inertia/env.d.ts.j2 +7 -0
- litestar_vite/templates/vue-inertia/index.html.j2 +14 -0
- litestar_vite/templates/vue-inertia/package.json.j2 +50 -0
- litestar_vite/templates/vue-inertia/resources/main.ts.j2 +18 -0
- litestar_vite/templates/vue-inertia/resources/pages/Home.vue.j2 +22 -0
- litestar_vite/templates/vue-inertia/resources/ssr.ts.j2 +21 -0
- litestar_vite/templates/vue-inertia/resources/style.css.j2 +21 -0
- litestar_vite/templates/vue-inertia/vite.config.ts.j2 +59 -0
- litestar_vite-0.15.0.dist-info/METADATA +230 -0
- litestar_vite-0.15.0.dist-info/RECORD +164 -0
- {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/WHEEL +1 -1
- litestar_vite/config.py +0 -100
- litestar_vite/plugin.py +0 -45
- litestar_vite/template_engine.py +0 -103
- litestar_vite-0.1.1.dist-info/METADATA +0 -68
- litestar_vite-0.1.1.dist-info/RECORD +0 -11
- {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from litestar_vite.config import InertiaConfig
|
|
2
|
+
from litestar_vite.inertia import helpers
|
|
3
|
+
from litestar_vite.inertia.exception_handler import create_inertia_exception_response, exception_to_http_response
|
|
4
|
+
from litestar_vite.inertia.helpers import (
|
|
5
|
+
AlwaysProp,
|
|
6
|
+
OnceProp,
|
|
7
|
+
OptionalProp,
|
|
8
|
+
PropFilter,
|
|
9
|
+
always,
|
|
10
|
+
clear_history,
|
|
11
|
+
defer,
|
|
12
|
+
error,
|
|
13
|
+
except_,
|
|
14
|
+
extract_deferred_props,
|
|
15
|
+
extract_merge_props,
|
|
16
|
+
extract_once_props,
|
|
17
|
+
flash,
|
|
18
|
+
get_shared_props,
|
|
19
|
+
lazy,
|
|
20
|
+
merge,
|
|
21
|
+
once,
|
|
22
|
+
only,
|
|
23
|
+
optional,
|
|
24
|
+
scroll_props,
|
|
25
|
+
share,
|
|
26
|
+
)
|
|
27
|
+
from litestar_vite.inertia.middleware import InertiaMiddleware
|
|
28
|
+
from litestar_vite.inertia.plugin import InertiaPlugin
|
|
29
|
+
from litestar_vite.inertia.precognition import (
|
|
30
|
+
PrecognitionResponse,
|
|
31
|
+
create_precognition_exception_handler,
|
|
32
|
+
normalize_validation_errors,
|
|
33
|
+
precognition,
|
|
34
|
+
)
|
|
35
|
+
from litestar_vite.inertia.request import InertiaDetails, InertiaHeaders, InertiaRequest
|
|
36
|
+
from litestar_vite.inertia.response import InertiaBack, InertiaExternalRedirect, InertiaRedirect, InertiaResponse
|
|
37
|
+
|
|
38
|
+
__all__ = (
|
|
39
|
+
"AlwaysProp",
|
|
40
|
+
"InertiaBack",
|
|
41
|
+
"InertiaConfig",
|
|
42
|
+
"InertiaDetails",
|
|
43
|
+
"InertiaExternalRedirect",
|
|
44
|
+
"InertiaHeaders",
|
|
45
|
+
"InertiaMiddleware",
|
|
46
|
+
"InertiaPlugin",
|
|
47
|
+
"InertiaRedirect",
|
|
48
|
+
"InertiaRequest",
|
|
49
|
+
"InertiaResponse",
|
|
50
|
+
"OnceProp",
|
|
51
|
+
"OptionalProp",
|
|
52
|
+
"PrecognitionResponse",
|
|
53
|
+
"PropFilter",
|
|
54
|
+
"always",
|
|
55
|
+
"clear_history",
|
|
56
|
+
"create_inertia_exception_response",
|
|
57
|
+
"create_precognition_exception_handler",
|
|
58
|
+
"defer",
|
|
59
|
+
"error",
|
|
60
|
+
"except_",
|
|
61
|
+
"exception_to_http_response",
|
|
62
|
+
"extract_deferred_props",
|
|
63
|
+
"extract_merge_props",
|
|
64
|
+
"extract_once_props",
|
|
65
|
+
"flash",
|
|
66
|
+
"get_shared_props",
|
|
67
|
+
"helpers",
|
|
68
|
+
"lazy",
|
|
69
|
+
"merge",
|
|
70
|
+
"normalize_validation_errors",
|
|
71
|
+
"once",
|
|
72
|
+
"only",
|
|
73
|
+
"optional",
|
|
74
|
+
"precognition",
|
|
75
|
+
"scroll_props",
|
|
76
|
+
"share",
|
|
77
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import TYPE_CHECKING, Any
|
|
3
|
+
|
|
4
|
+
if TYPE_CHECKING:
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from litestar_vite.inertia.types import InertiaHeaderType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class InertiaHeaders(str, Enum):
|
|
11
|
+
"""Enum for Inertia Headers.
|
|
12
|
+
|
|
13
|
+
See: https://inertiajs.com/the-protocol
|
|
14
|
+
|
|
15
|
+
This includes both core protocol headers and v2 extensions (partial excludes, reset, error bags,
|
|
16
|
+
and infinite scroll merge intent).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
ENABLED = "X-Inertia"
|
|
20
|
+
VERSION = "X-Inertia-Version"
|
|
21
|
+
LOCATION = "X-Inertia-Location"
|
|
22
|
+
REFERER = "Referer"
|
|
23
|
+
|
|
24
|
+
PARTIAL_DATA = "X-Inertia-Partial-Data"
|
|
25
|
+
PARTIAL_COMPONENT = "X-Inertia-Partial-Component"
|
|
26
|
+
PARTIAL_EXCEPT = "X-Inertia-Partial-Except"
|
|
27
|
+
|
|
28
|
+
RESET = "X-Inertia-Reset"
|
|
29
|
+
ERROR_BAG = "X-Inertia-Error-Bag"
|
|
30
|
+
|
|
31
|
+
INFINITE_SCROLL_MERGE_INTENT = "X-Inertia-Infinite-Scroll-Merge-Intent"
|
|
32
|
+
|
|
33
|
+
# Precognition headers (Laravel Precognition protocol)
|
|
34
|
+
PRECOGNITION = "Precognition"
|
|
35
|
+
PRECOGNITION_SUCCESS = "Precognition-Success"
|
|
36
|
+
PRECOGNITION_VALIDATE_ONLY = "Precognition-Validate-Only"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_enabled_header(enabled: bool = True) -> "dict[str, Any]":
|
|
40
|
+
"""True if inertia is enabled.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
enabled: Whether inertia is enabled.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The headers for inertia.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
return {InertiaHeaders.ENABLED.value: "true" if enabled else "false"}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_version_header(version: str) -> "dict[str, Any]":
|
|
53
|
+
"""Return headers for change swap method response.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
version: The version of the inertia.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The headers for inertia.
|
|
60
|
+
"""
|
|
61
|
+
return {InertiaHeaders.VERSION.value: version}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_partial_data_header(partial: str) -> "dict[str, Any]":
|
|
65
|
+
"""Return headers for a partial data response.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
partial: The partial data.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The headers for inertia.
|
|
72
|
+
"""
|
|
73
|
+
return {InertiaHeaders.PARTIAL_DATA.value: partial}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_partial_component_header(partial: str) -> "dict[str, Any]":
|
|
77
|
+
"""Return headers for a partial data response.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
partial: The partial data.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The headers for inertia.
|
|
84
|
+
"""
|
|
85
|
+
return {InertiaHeaders.PARTIAL_COMPONENT.value: partial}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_headers(inertia_headers: "InertiaHeaderType") -> "dict[str, Any]":
|
|
89
|
+
"""Return headers for Inertia responses.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
inertia_headers: The inertia headers.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the inertia headers are None.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The headers for inertia.
|
|
99
|
+
"""
|
|
100
|
+
if not inertia_headers:
|
|
101
|
+
msg = "Value for inertia_headers cannot be None."
|
|
102
|
+
raise ValueError(msg)
|
|
103
|
+
inertia_headers_dict: "dict[str, Callable[..., dict[str, Any]]]" = {
|
|
104
|
+
"enabled": get_enabled_header,
|
|
105
|
+
"partial_data": get_partial_data_header,
|
|
106
|
+
"partial_component": get_partial_component_header,
|
|
107
|
+
"version": get_version_header,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
header: "dict[str, Any]" = {}
|
|
111
|
+
response: "dict[str, Any]"
|
|
112
|
+
key: "str"
|
|
113
|
+
value: "Any"
|
|
114
|
+
|
|
115
|
+
for key, value in inertia_headers.items():
|
|
116
|
+
if value is not None:
|
|
117
|
+
response = inertia_headers_dict[key](value)
|
|
118
|
+
header.update(response)
|
|
119
|
+
return header
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
3
|
+
from urllib.parse import quote, urlparse, urlunparse
|
|
4
|
+
|
|
5
|
+
from litestar import MediaType
|
|
6
|
+
from litestar.connection import Request
|
|
7
|
+
from litestar.connection.base import AuthT, StateT, UserT
|
|
8
|
+
from litestar.exceptions import (
|
|
9
|
+
HTTPException,
|
|
10
|
+
InternalServerException,
|
|
11
|
+
NotAuthorizedException,
|
|
12
|
+
NotFoundException,
|
|
13
|
+
PermissionDeniedException,
|
|
14
|
+
)
|
|
15
|
+
from litestar.exceptions.responses import (
|
|
16
|
+
create_debug_response, # pyright: ignore[reportUnknownVariableType]
|
|
17
|
+
create_exception_response, # pyright: ignore[reportUnknownVariableType]
|
|
18
|
+
)
|
|
19
|
+
from litestar.repository.exceptions import (
|
|
20
|
+
ConflictError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
21
|
+
NotFoundError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
22
|
+
RepositoryError, # pyright: ignore[reportUnknownVariableType,reportAttributeAccessIssue]
|
|
23
|
+
)
|
|
24
|
+
from litestar.response import Response
|
|
25
|
+
from litestar.status_codes import (
|
|
26
|
+
HTTP_400_BAD_REQUEST,
|
|
27
|
+
HTTP_401_UNAUTHORIZED,
|
|
28
|
+
HTTP_404_NOT_FOUND,
|
|
29
|
+
HTTP_405_METHOD_NOT_ALLOWED,
|
|
30
|
+
HTTP_409_CONFLICT,
|
|
31
|
+
HTTP_422_UNPROCESSABLE_ENTITY,
|
|
32
|
+
HTTP_500_INTERNAL_SERVER_ERROR,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from litestar_vite.inertia.helpers import error, flash
|
|
36
|
+
from litestar_vite.inertia.request import InertiaRequest
|
|
37
|
+
from litestar_vite.inertia.response import InertiaBack, InertiaRedirect, InertiaResponse
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from litestar.connection import Request
|
|
41
|
+
from litestar.connection.base import AuthT, StateT, UserT
|
|
42
|
+
from litestar.response import Response
|
|
43
|
+
|
|
44
|
+
from litestar_vite.inertia.plugin import InertiaPlugin
|
|
45
|
+
|
|
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
|
+
|
|
59
|
+
Inertia detection:
|
|
60
|
+
|
|
61
|
+
- For InertiaRequest instances, uses the request's derived flags (route component + headers).
|
|
62
|
+
- For plain Request instances (e.g., before routing/when middleware didn't run), falls back
|
|
63
|
+
to checking the ``X-Inertia`` header.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
request: The request object.
|
|
67
|
+
exc: The exception to handle.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The response object.
|
|
71
|
+
"""
|
|
72
|
+
is_inertia_header = request.headers.get("x-inertia", "").lower() == "true"
|
|
73
|
+
if isinstance(request, InertiaRequest):
|
|
74
|
+
inertia_enabled = request.inertia_enabled or request.is_inertia or is_inertia_header
|
|
75
|
+
else:
|
|
76
|
+
inertia_enabled = is_inertia_header
|
|
77
|
+
|
|
78
|
+
if not inertia_enabled:
|
|
79
|
+
if isinstance(exc, HTTPException):
|
|
80
|
+
return cast("Response[Any]", create_exception_response(request, exc))
|
|
81
|
+
if isinstance(exc, NotFoundError):
|
|
82
|
+
http_exc = NotFoundException
|
|
83
|
+
elif isinstance(exc, (RepositoryError, ConflictError)):
|
|
84
|
+
http_exc = _HTTPConflictException # type: ignore[assignment]
|
|
85
|
+
else:
|
|
86
|
+
http_exc = InternalServerException # type: ignore[assignment]
|
|
87
|
+
if request.app.debug and http_exc not in {PermissionDeniedException, NotFoundError}:
|
|
88
|
+
return cast("Response[Any]", create_debug_response(request, exc))
|
|
89
|
+
return cast("Response[Any]", create_exception_response(request, http_exc(detail=str(exc.__cause__)))) # pyright: ignore[reportUnknownArgumentType]
|
|
90
|
+
return create_inertia_exception_response(request, exc)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def create_inertia_exception_response(request: "Request[UserT, AuthT, StateT]", exc: "Exception") -> "Response[Any]":
|
|
94
|
+
"""Create the inertia exception response.
|
|
95
|
+
|
|
96
|
+
This function handles exceptions for Inertia-enabled routes, returning appropriate
|
|
97
|
+
responses based on the exception type and status code.
|
|
98
|
+
|
|
99
|
+
Note:
|
|
100
|
+
This function uses defensive programming techniques to handle edge cases:
|
|
101
|
+
- Type-safe handling of exception ``extra`` attribute (may be string, list, dict, or None)
|
|
102
|
+
- Graceful handling when InertiaPlugin is not registered
|
|
103
|
+
- Broad exception handling for flash() calls (non-critical operation)
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
request: The request object.
|
|
107
|
+
exc: The exception to handle.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The response object, either an InertiaResponse, InertiaRedirect, or InertiaBack.
|
|
111
|
+
"""
|
|
112
|
+
is_inertia_header = request.headers.get("x-inertia", "").lower() == "true"
|
|
113
|
+
is_inertia = request.is_inertia if isinstance(request, InertiaRequest) else is_inertia_header
|
|
114
|
+
|
|
115
|
+
status_code = exc.status_code if isinstance(exc, HTTPException) else HTTP_500_INTERNAL_SERVER_ERROR
|
|
116
|
+
preferred_type = MediaType.HTML if not is_inertia else MediaType.JSON
|
|
117
|
+
detail = exc.detail if isinstance(exc, HTTPException) else str(exc)
|
|
118
|
+
extras: Any = None
|
|
119
|
+
if isinstance(exc, HTTPException):
|
|
120
|
+
try:
|
|
121
|
+
extras = exc.extra # pyright: ignore[reportUnknownMemberType]
|
|
122
|
+
except AttributeError:
|
|
123
|
+
extras = None
|
|
124
|
+
content: dict[str, Any] = {"status_code": status_code, "message": detail}
|
|
125
|
+
|
|
126
|
+
inertia_plugin: "InertiaPlugin | None"
|
|
127
|
+
try:
|
|
128
|
+
inertia_plugin = request.app.plugins.get("InertiaPlugin")
|
|
129
|
+
except KeyError:
|
|
130
|
+
inertia_plugin = None
|
|
131
|
+
|
|
132
|
+
if extras:
|
|
133
|
+
content.update({"extra": extras})
|
|
134
|
+
|
|
135
|
+
flash_succeeded = False
|
|
136
|
+
if detail:
|
|
137
|
+
flash_succeeded = flash(request, detail, category="error")
|
|
138
|
+
|
|
139
|
+
if extras and isinstance(extras, (list, tuple)) and len(extras) >= 1: # pyright: ignore[reportUnknownArgumentType]
|
|
140
|
+
first_extra = extras[0] # pyright: ignore[reportUnknownVariableType]
|
|
141
|
+
if isinstance(first_extra, dict):
|
|
142
|
+
message: dict[str, str] = cast("dict[str, str]", first_extra)
|
|
143
|
+
key_value = message.get("key")
|
|
144
|
+
default_field = f"root.{key_value}" if key_value is not None else "root"
|
|
145
|
+
error_detail = str(message.get("message", detail) or detail)
|
|
146
|
+
match = FIELD_ERR_RE.search(error_detail)
|
|
147
|
+
field = match.group(1) if match else default_field
|
|
148
|
+
error(request, field, error_detail or detail)
|
|
149
|
+
|
|
150
|
+
if status_code in {HTTP_422_UNPROCESSABLE_ENTITY, HTTP_400_BAD_REQUEST} or isinstance(
|
|
151
|
+
exc, PermissionDeniedException
|
|
152
|
+
):
|
|
153
|
+
return InertiaBack(request)
|
|
154
|
+
|
|
155
|
+
if inertia_plugin is None:
|
|
156
|
+
return InertiaResponse[Any](media_type=preferred_type, content=content, status_code=status_code)
|
|
157
|
+
|
|
158
|
+
is_unauthorized = status_code == HTTP_401_UNAUTHORIZED or isinstance(exc, NotAuthorizedException)
|
|
159
|
+
redirect_to_login = inertia_plugin.config.redirect_unauthorized_to
|
|
160
|
+
if is_unauthorized and redirect_to_login is not None:
|
|
161
|
+
if request.url.path != redirect_to_login:
|
|
162
|
+
# If flash failed (no session), pass error message via query param
|
|
163
|
+
if not flash_succeeded and detail:
|
|
164
|
+
parsed = urlparse(redirect_to_login)
|
|
165
|
+
error_param = f"error={quote(detail, safe='')}"
|
|
166
|
+
query = f"{parsed.query}&{error_param}" if parsed.query else error_param
|
|
167
|
+
redirect_to_login = urlunparse(parsed._replace(query=query))
|
|
168
|
+
return InertiaRedirect(request, redirect_to=redirect_to_login)
|
|
169
|
+
# Already on login page - redirect back so Inertia processes flash messages
|
|
170
|
+
# (Inertia.js shows 4xx responses in a modal instead of updating page state)
|
|
171
|
+
return InertiaBack(request)
|
|
172
|
+
|
|
173
|
+
if status_code in {HTTP_404_NOT_FOUND, HTTP_405_METHOD_NOT_ALLOWED} and (
|
|
174
|
+
inertia_plugin.config.redirect_404 is not None and request.url.path != inertia_plugin.config.redirect_404
|
|
175
|
+
):
|
|
176
|
+
return InertiaRedirect(request, redirect_to=inertia_plugin.config.redirect_404)
|
|
177
|
+
|
|
178
|
+
return InertiaResponse[Any](media_type=preferred_type, content=content, status_code=status_code)
|