litestar-vite 0.15.0__py3-none-any.whl → 0.15.0rc2__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/_codegen/__init__.py +26 -0
- litestar_vite/_codegen/inertia.py +407 -0
- litestar_vite/{codegen/_openapi.py → _codegen/openapi.py} +11 -58
- litestar_vite/{codegen/_routes.py → _codegen/routes.py} +43 -110
- litestar_vite/{codegen/_ts.py → _codegen/ts.py} +19 -19
- litestar_vite/_handler/__init__.py +8 -0
- litestar_vite/{handler/_app.py → _handler/app.py} +29 -117
- litestar_vite/cli.py +254 -155
- litestar_vite/codegen.py +39 -0
- litestar_vite/commands.py +6 -0
- litestar_vite/{config/__init__.py → config.py} +726 -99
- litestar_vite/deploy.py +3 -14
- litestar_vite/doctor.py +6 -8
- litestar_vite/executor.py +1 -45
- litestar_vite/handler.py +9 -0
- litestar_vite/html_transform.py +5 -148
- litestar_vite/inertia/__init__.py +0 -24
- litestar_vite/inertia/_utils.py +0 -5
- litestar_vite/inertia/exception_handler.py +16 -22
- litestar_vite/inertia/helpers.py +18 -546
- litestar_vite/inertia/plugin.py +11 -77
- litestar_vite/inertia/request.py +0 -48
- litestar_vite/inertia/response.py +17 -113
- litestar_vite/inertia/types.py +0 -19
- litestar_vite/loader.py +7 -7
- litestar_vite/plugin.py +2184 -0
- litestar_vite/templates/angular/package.json.j2 +1 -2
- litestar_vite/templates/angular-cli/package.json.j2 +1 -2
- litestar_vite/templates/base/package.json.j2 +1 -2
- litestar_vite/templates/react-inertia/package.json.j2 +1 -2
- litestar_vite/templates/vue-inertia/package.json.j2 +1 -2
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/METADATA +5 -5
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/RECORD +36 -49
- litestar_vite/codegen/__init__.py +0 -48
- litestar_vite/codegen/_export.py +0 -229
- litestar_vite/codegen/_inertia.py +0 -619
- litestar_vite/codegen/_utils.py +0 -141
- litestar_vite/config/_constants.py +0 -97
- litestar_vite/config/_deploy.py +0 -70
- litestar_vite/config/_inertia.py +0 -241
- litestar_vite/config/_paths.py +0 -63
- litestar_vite/config/_runtime.py +0 -235
- litestar_vite/config/_spa.py +0 -93
- litestar_vite/config/_types.py +0 -94
- litestar_vite/handler/__init__.py +0 -9
- litestar_vite/inertia/precognition.py +0 -274
- litestar_vite/plugin/__init__.py +0 -687
- litestar_vite/plugin/_process.py +0 -185
- litestar_vite/plugin/_proxy.py +0 -689
- litestar_vite/plugin/_proxy_headers.py +0 -244
- litestar_vite/plugin/_static.py +0 -37
- litestar_vite/plugin/_utils.py +0 -489
- /litestar_vite/{handler/_routing.py → _handler/routing.py} +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +0 -0
- {litestar_vite-0.15.0.dist-info → litestar_vite-0.15.0rc2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
"""Precognition support for real-time form validation.
|
|
2
|
-
|
|
3
|
-
Precognition is a Laravel protocol for running server-side validation without
|
|
4
|
-
executing handler side effects. This module provides Litestar integration.
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
1. Enable in config: ``InertiaConfig(precognition=True)``
|
|
8
|
-
2. Add ``@precognition`` decorator to route handlers
|
|
9
|
-
|
|
10
|
-
The plugin automatically handles validation failures (422 responses).
|
|
11
|
-
The decorator prevents handler execution on successful validation (204 responses).
|
|
12
|
-
|
|
13
|
-
See: https://laravel.com/docs/precognition
|
|
14
|
-
|
|
15
|
-
Note on Rate Limiting:
|
|
16
|
-
Real-time validation can result in many requests. Laravel has no official
|
|
17
|
-
rate limiting solution for Precognition. Consider:
|
|
18
|
-
- Throttling Precognition requests separately from normal requests
|
|
19
|
-
- Using debounce on the frontend (laravel-precognition libraries do this)
|
|
20
|
-
- Implementing custom rate limiting that checks for Precognition header
|
|
21
|
-
"""
|
|
22
|
-
|
|
23
|
-
from functools import wraps
|
|
24
|
-
from typing import TYPE_CHECKING, Any
|
|
25
|
-
|
|
26
|
-
from litestar import MediaType, Response
|
|
27
|
-
from litestar.exceptions import ValidationException
|
|
28
|
-
from litestar.status_codes import HTTP_204_NO_CONTENT, HTTP_422_UNPROCESSABLE_ENTITY
|
|
29
|
-
|
|
30
|
-
from litestar_vite.inertia._utils import InertiaHeaders
|
|
31
|
-
|
|
32
|
-
if TYPE_CHECKING:
|
|
33
|
-
from collections.abc import Callable
|
|
34
|
-
|
|
35
|
-
from litestar import Request
|
|
36
|
-
|
|
37
|
-
__all__ = (
|
|
38
|
-
"PrecognitionResponse",
|
|
39
|
-
"create_precognition_exception_handler",
|
|
40
|
-
"normalize_validation_errors",
|
|
41
|
-
"precognition",
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def normalize_validation_errors(exc: ValidationException, validate_only: "set[str] | None" = None) -> "dict[str, Any]":
|
|
46
|
-
"""Normalize Litestar validation errors to Laravel format.
|
|
47
|
-
|
|
48
|
-
Laravel's Precognition protocol expects errors in this format:
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"message": "The given data was invalid.",
|
|
52
|
-
"errors": {
|
|
53
|
-
"email": ["The email field is required."],
|
|
54
|
-
"password": ["The password must be at least 8 characters."]
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Args:
|
|
60
|
-
exc: The ValidationException from Litestar.
|
|
61
|
-
validate_only: If provided, only include errors for these fields.
|
|
62
|
-
Used for partial field validation as the user types.
|
|
63
|
-
|
|
64
|
-
Returns:
|
|
65
|
-
A dict in Laravel's validation error format.
|
|
66
|
-
"""
|
|
67
|
-
errors: "dict[str, list[str]]" = {}
|
|
68
|
-
|
|
69
|
-
# Litestar's ValidationException.detail can be:
|
|
70
|
-
# - A string message
|
|
71
|
-
# - A list of error dicts with 'key', 'message', and 'source'
|
|
72
|
-
# - Other structured data
|
|
73
|
-
detail = exc.detail
|
|
74
|
-
|
|
75
|
-
if isinstance(detail, list):
|
|
76
|
-
for error in detail: # pyright: ignore[reportUnknownVariableType]
|
|
77
|
-
if isinstance(error, dict):
|
|
78
|
-
key: str = str(error.get("key", "")) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
|
79
|
-
message: str = str(error.get("message", str(error))) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
|
80
|
-
source: str = str(error.get("source", "")) # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType]
|
|
81
|
-
|
|
82
|
-
# Build field name from source and key
|
|
83
|
-
if source and key:
|
|
84
|
-
field_name = f"{source}.{key}" if source != "body" else key
|
|
85
|
-
elif key:
|
|
86
|
-
field_name = key
|
|
87
|
-
else:
|
|
88
|
-
field_name = "_root"
|
|
89
|
-
|
|
90
|
-
# Filter by validate_only if specified
|
|
91
|
-
if validate_only and field_name not in validate_only:
|
|
92
|
-
continue
|
|
93
|
-
|
|
94
|
-
if field_name not in errors:
|
|
95
|
-
errors[field_name] = []
|
|
96
|
-
errors[field_name].append(message)
|
|
97
|
-
elif isinstance(detail, str): # pyright: ignore[reportUnnecessaryIsInstance]
|
|
98
|
-
errors["_root"] = [detail]
|
|
99
|
-
|
|
100
|
-
return {"message": "The given data was invalid.", "errors": errors}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class PrecognitionResponse(Response[Any]):
|
|
104
|
-
"""Response for successful Precognition validation.
|
|
105
|
-
|
|
106
|
-
Returns 204 No Content with Precognition-Success header.
|
|
107
|
-
"""
|
|
108
|
-
|
|
109
|
-
def __init__(self) -> None:
|
|
110
|
-
super().__init__(
|
|
111
|
-
content=None, status_code=HTTP_204_NO_CONTENT, headers={InertiaHeaders.PRECOGNITION_SUCCESS.value: "true"}
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def create_precognition_exception_handler(
|
|
116
|
-
*, fallback_handler: "Callable[[Request[Any, Any, Any], ValidationException], Response[Any]] | None" = None
|
|
117
|
-
) -> "Callable[[Request[Any, Any, Any], ValidationException], Response[Any]]":
|
|
118
|
-
"""Create an exception handler for ValidationException that supports Precognition.
|
|
119
|
-
|
|
120
|
-
This handler checks if the request is a Precognition request and returns
|
|
121
|
-
errors in Laravel's format. For non-Precognition requests, it either uses
|
|
122
|
-
the fallback handler or returns Litestar's default format.
|
|
123
|
-
|
|
124
|
-
Args:
|
|
125
|
-
fallback_handler: Optional handler for non-Precognition requests.
|
|
126
|
-
If not provided, returns a standard JSON error response.
|
|
127
|
-
|
|
128
|
-
Returns:
|
|
129
|
-
An exception handler function suitable for Litestar's exception_handlers.
|
|
130
|
-
"""
|
|
131
|
-
|
|
132
|
-
def handler(request: "Request[Any, Any, Any]", exc: ValidationException) -> "Response[Any]":
|
|
133
|
-
# Check if this is a Precognition request
|
|
134
|
-
precognition_header = request.headers.get(InertiaHeaders.PRECOGNITION.value.lower())
|
|
135
|
-
is_precognition = precognition_header == "true"
|
|
136
|
-
|
|
137
|
-
if is_precognition:
|
|
138
|
-
# Get validate_only fields for partial validation
|
|
139
|
-
validate_only_header = request.headers.get(InertiaHeaders.PRECOGNITION_VALIDATE_ONLY.value.lower())
|
|
140
|
-
validate_only = (
|
|
141
|
-
{field.strip() for field in validate_only_header.split(",") if field.strip()}
|
|
142
|
-
if validate_only_header
|
|
143
|
-
else None
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
# Normalize errors to Laravel format
|
|
147
|
-
error_data = normalize_validation_errors(exc, validate_only)
|
|
148
|
-
|
|
149
|
-
# If filtering removed all errors, return success (204)
|
|
150
|
-
if validate_only and not error_data["errors"]:
|
|
151
|
-
return PrecognitionResponse()
|
|
152
|
-
|
|
153
|
-
return Response(
|
|
154
|
-
content=error_data,
|
|
155
|
-
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
|
156
|
-
media_type=MediaType.JSON,
|
|
157
|
-
headers={InertiaHeaders.PRECOGNITION.value: "true"},
|
|
158
|
-
)
|
|
159
|
-
|
|
160
|
-
# Non-Precognition request - use fallback or default
|
|
161
|
-
if fallback_handler is not None:
|
|
162
|
-
return fallback_handler(request, exc)
|
|
163
|
-
|
|
164
|
-
# Default Litestar-style error response
|
|
165
|
-
return Response(
|
|
166
|
-
content={
|
|
167
|
-
"status_code": HTTP_422_UNPROCESSABLE_ENTITY,
|
|
168
|
-
"detail": "Validation failed",
|
|
169
|
-
"extra": exc.detail or [],
|
|
170
|
-
},
|
|
171
|
-
status_code=HTTP_422_UNPROCESSABLE_ENTITY,
|
|
172
|
-
media_type=MediaType.JSON,
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
return handler
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
def precognition(fn: "Callable[..., Any]") -> "Callable[..., Any]":
|
|
179
|
-
"""Decorator to enable Precognition on a route handler.
|
|
180
|
-
|
|
181
|
-
When a Precognition request passes DTO validation, this decorator
|
|
182
|
-
returns a 204 No Content response instead of executing the handler body.
|
|
183
|
-
This prevents side effects (database writes, emails, etc.) during validation.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
fn: The route handler function to wrap.
|
|
187
|
-
|
|
188
|
-
Returns:
|
|
189
|
-
A wrapped handler that short-circuits on valid Precognition requests.
|
|
190
|
-
|
|
191
|
-
Example:
|
|
192
|
-
```python
|
|
193
|
-
from litestar import post
|
|
194
|
-
from litestar_vite.inertia import precognition, InertiaRedirect
|
|
195
|
-
|
|
196
|
-
@post("/users")
|
|
197
|
-
@precognition
|
|
198
|
-
async def create_user(data: UserDTO) -> InertiaRedirect:
|
|
199
|
-
# This only runs for actual form submissions
|
|
200
|
-
# Precognition validation requests return 204 automatically
|
|
201
|
-
user = await User.create(**data.dict())
|
|
202
|
-
return InertiaRedirect(request, f"/users/{user.id}")
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Note:
|
|
206
|
-
- Validation errors are handled by the exception handler (automatic)
|
|
207
|
-
- This decorator handles the success case (prevents handler execution)
|
|
208
|
-
- The decorator checks for Precognition header AFTER DTO validation
|
|
209
|
-
"""
|
|
210
|
-
import asyncio
|
|
211
|
-
|
|
212
|
-
@wraps(fn)
|
|
213
|
-
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
214
|
-
# Find the request object in args or kwargs
|
|
215
|
-
request = _find_request(args, kwargs) # pyright: ignore[reportUnknownVariableType]
|
|
216
|
-
|
|
217
|
-
if request is not None:
|
|
218
|
-
# Check for Precognition header
|
|
219
|
-
precognition_header = request.headers.get(InertiaHeaders.PRECOGNITION.value.lower())
|
|
220
|
-
if precognition_header == "true":
|
|
221
|
-
# Validation passed (we got here), return success
|
|
222
|
-
return PrecognitionResponse()
|
|
223
|
-
|
|
224
|
-
# Not a Precognition request, run handler normally
|
|
225
|
-
return fn(*args, **kwargs)
|
|
226
|
-
|
|
227
|
-
@wraps(fn)
|
|
228
|
-
async def async_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
229
|
-
# Find the request object in args or kwargs
|
|
230
|
-
request = _find_request(args, kwargs) # pyright: ignore[reportUnknownVariableType]
|
|
231
|
-
|
|
232
|
-
if request is not None:
|
|
233
|
-
# Check for Precognition header
|
|
234
|
-
precognition_header = request.headers.get(InertiaHeaders.PRECOGNITION.value.lower())
|
|
235
|
-
if precognition_header == "true":
|
|
236
|
-
# Validation passed (we got here), return success
|
|
237
|
-
return PrecognitionResponse()
|
|
238
|
-
|
|
239
|
-
# Not a Precognition request, run handler normally
|
|
240
|
-
result = fn(*args, **kwargs)
|
|
241
|
-
if asyncio.iscoroutine(result):
|
|
242
|
-
return await result
|
|
243
|
-
return result
|
|
244
|
-
|
|
245
|
-
# Return appropriate wrapper based on function type
|
|
246
|
-
if asyncio.iscoroutinefunction(fn):
|
|
247
|
-
return async_wrapper
|
|
248
|
-
return sync_wrapper
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _find_request(args: tuple[Any, ...], kwargs: "dict[str, Any]") -> "Request[Any, Any, Any] | None": # pyright: ignore[reportUnknownParameterType]
|
|
252
|
-
"""Find Request object in function arguments.
|
|
253
|
-
|
|
254
|
-
Args:
|
|
255
|
-
args: Positional arguments to the handler.
|
|
256
|
-
kwargs: Keyword arguments to the handler.
|
|
257
|
-
|
|
258
|
-
Returns:
|
|
259
|
-
The Request object if found, otherwise None.
|
|
260
|
-
"""
|
|
261
|
-
from litestar import Request
|
|
262
|
-
|
|
263
|
-
# Check kwargs first (named 'request' parameter)
|
|
264
|
-
if "request" in kwargs:
|
|
265
|
-
req = kwargs["request"]
|
|
266
|
-
if isinstance(req, Request): # pyright: ignore[reportUnknownVariableType]
|
|
267
|
-
return req # pyright: ignore[reportUnknownVariableType]
|
|
268
|
-
|
|
269
|
-
# Check positional args
|
|
270
|
-
for arg in args:
|
|
271
|
-
if isinstance(arg, Request): # pyright: ignore[reportUnknownVariableType]
|
|
272
|
-
return arg # pyright: ignore[reportUnknownVariableType]
|
|
273
|
-
|
|
274
|
-
return None
|