litestar-vite 0.11.0__tar.gz → 0.12.0__tar.gz
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-0.11.0 → litestar_vite-0.12.0}/.gitignore +1 -1
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/PKG-INFO +3 -2
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/pyproject.toml +12 -4
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/commands.py +1 -1
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/plugin.py +32 -12
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/response.py +261 -27
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/plugin.py +20 -25
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/conftest.py +11 -4
- litestar_vite-0.11.0/src/py/tests/test_inertia/test_inertia_request.py → litestar_vite-0.12.0/src/py/tests/test_inertia/test_request.py +19 -7
- litestar_vite-0.12.0/src/py/tests/test_inertia/test_response.py +513 -0
- litestar_vite-0.11.0/src/py/tests/test_inertia/test_inertia_response.py +0 -204
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/LICENSE +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/README.md +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/LICENSE +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/Makefile +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/NOTICE +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/README.md +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/src/dev-server-index.html +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/src/index.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/src/inertia-helpers/index.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/tests/__data__/dummy.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/tests/index.test.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/tsconfig.inertia-helpers.json +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/tsconfig.json +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/vitest.config.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/js/vitest.workspace.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/__metadata__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/cli.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/config.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/_utils.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/config.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/exception_handler.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/middleware.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/request.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/routes.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/inertia/types.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/loader.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/py.typed +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/index.html.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/main.ts.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/package.json.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/styles.css.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/tsconfig.json.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/litestar_vite/templates/vite.config.ts.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/templates/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/templates/index.html.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/app.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/public/.gitkeep +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/public/assets/main-l0sNRNKZ.js +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/public/assets/styles-l0sNRNKZ.js +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/public/manifest.json +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/resources/.gitkeep +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/resources/main.ts +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/resources/styles.css +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/templates/.gitkeep +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_app/web/templates/index.html +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_asset_loader.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_cli/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_cli/conftest.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_cli/test_init.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_commands.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_config.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_inertia/__init__.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_inertia/conftest.py +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_inertia/templates/index.html.j2 +0 -0
- {litestar_vite-0.11.0 → litestar_vite-0.12.0}/src/py/tests/test_inertia/test_routes.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: litestar-vite
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -10,6 +10,7 @@ Project-URL: Issue, https://github.com/cofin/litestar-vite/issues/
|
|
|
10
10
|
Project-URL: Source, https://github.com/cofin/litestar-vite
|
|
11
11
|
Author-email: Cody Fincher <cody.fincher@gmail.com>
|
|
12
12
|
License: MIT
|
|
13
|
+
License-File: LICENSE
|
|
13
14
|
Keywords: litestar,vite
|
|
14
15
|
Classifier: Environment :: Web Environment
|
|
15
16
|
Classifier: Intended Audience :: Developers
|
|
@@ -25,7 +25,7 @@ license = { text = "MIT" }
|
|
|
25
25
|
name = "litestar-vite"
|
|
26
26
|
readme = "README.md"
|
|
27
27
|
requires-python = ">=3.8"
|
|
28
|
-
version = "0.
|
|
28
|
+
version = "0.12.0"
|
|
29
29
|
|
|
30
30
|
[project.urls]
|
|
31
31
|
Changelog = "https://cofin.github.io/litestar-vite/latest/changelog"
|
|
@@ -55,7 +55,8 @@ packages = ["src/py/litestar_vite"]
|
|
|
55
55
|
sources = ["src/py"]
|
|
56
56
|
|
|
57
57
|
[dependency-groups]
|
|
58
|
-
dev = [
|
|
58
|
+
dev = [ {include-group = "build"}, { include-group = "linting" }, { include-group = "test" }, { include-group = "docs" } ]
|
|
59
|
+
build = ["bump-my-version"]
|
|
59
60
|
docs = [
|
|
60
61
|
"auto-pytabs[sphinx]>=0.4.0",
|
|
61
62
|
"sphinx-autobuild>=2021.3.14",
|
|
@@ -71,6 +72,7 @@ docs = [
|
|
|
71
72
|
]
|
|
72
73
|
linting = ["pre-commit>=3.4.0", "mypy>=1.5.1", "ruff>=0.0.287", "types-docutils", "slotscheck", "basedpyright", "pyright"]
|
|
73
74
|
test = [
|
|
75
|
+
"litestar[standard]",
|
|
74
76
|
"pytest>=7.4.1",
|
|
75
77
|
"pytest-cov",
|
|
76
78
|
"coverage",
|
|
@@ -85,7 +87,7 @@ test = [
|
|
|
85
87
|
allow_dirty = true
|
|
86
88
|
commit = true
|
|
87
89
|
commit_args = "--no-verify"
|
|
88
|
-
current_version = "0.
|
|
90
|
+
current_version = "0.12.0"
|
|
89
91
|
ignore_missing_files = false
|
|
90
92
|
ignore_missing_version = false
|
|
91
93
|
message = "chore(release): bump to v{new_version}"
|
|
@@ -148,7 +150,7 @@ replace = ' "litestar-vite-plugin": "^{new_version}",'
|
|
|
148
150
|
search = ' "litestar-vite-plugin": "^{current_version}",'
|
|
149
151
|
|
|
150
152
|
[tool.pytest.ini_options]
|
|
151
|
-
addopts = "
|
|
153
|
+
addopts = ["-q", "-ra"]
|
|
152
154
|
filterwarnings = ["ignore::DeprecationWarning:pkg_resources.*"]
|
|
153
155
|
testpaths = ["src/py/tests"]
|
|
154
156
|
|
|
@@ -338,6 +340,12 @@ show_column_numbers = true
|
|
|
338
340
|
warn_no_return = false
|
|
339
341
|
warn_unused_ignores = true
|
|
340
342
|
|
|
343
|
+
[[tool.mypy.overrides]]
|
|
344
|
+
ignore_missing_imports = true
|
|
345
|
+
module = [
|
|
346
|
+
"awaitlet",
|
|
347
|
+
]
|
|
348
|
+
|
|
341
349
|
[[tool.mypy.overrides]]
|
|
342
350
|
disable_error_code = "attr-defined"
|
|
343
351
|
disallow_untyped_decorators = false
|
|
@@ -14,7 +14,7 @@ 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
16
|
"vite": "^6.0.3",
|
|
17
|
-
"litestar-vite-plugin": "^0.
|
|
17
|
+
"litestar-vite-plugin": "^0.12.0",
|
|
18
18
|
"@types/node": "^22.10.1",
|
|
19
19
|
}
|
|
20
20
|
DEFAULT_DEPENDENCIES: dict[str, str] = {"axios": "^1.7.2"}
|
|
@@ -2,18 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
from litestar.middleware import DefineMiddleware
|
|
7
|
-
from litestar.middleware.session import SessionMiddleware
|
|
5
|
+
from anyio.from_thread import BlockingPortalProvider
|
|
8
6
|
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
7
|
|
|
18
8
|
if TYPE_CHECKING:
|
|
19
9
|
from litestar import Litestar
|
|
@@ -24,6 +14,8 @@ if TYPE_CHECKING:
|
|
|
24
14
|
|
|
25
15
|
def set_js_routes(app: Litestar) -> None:
|
|
26
16
|
"""Generate the route structure of the application on startup."""
|
|
17
|
+
from litestar_vite.inertia.routes import generate_js_routes
|
|
18
|
+
|
|
27
19
|
js_routes = generate_js_routes(app)
|
|
28
20
|
app.state.js_routes = js_routes
|
|
29
21
|
|
|
@@ -31,7 +23,7 @@ def set_js_routes(app: Litestar) -> None:
|
|
|
31
23
|
class InertiaPlugin(InitPluginProtocol):
|
|
32
24
|
"""Inertia plugin."""
|
|
33
25
|
|
|
34
|
-
__slots__ = ("config",)
|
|
26
|
+
__slots__ = ("config", "portal")
|
|
35
27
|
|
|
36
28
|
def __init__(self, config: InertiaConfig) -> None:
|
|
37
29
|
"""Initialize ``Inertia``.
|
|
@@ -41,12 +33,29 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
41
33
|
"""
|
|
42
34
|
self.config = config
|
|
43
35
|
|
|
36
|
+
self.portal = BlockingPortalProvider()
|
|
37
|
+
|
|
38
|
+
def get_portal(self) -> BlockingPortalProvider:
|
|
39
|
+
return self.portal
|
|
40
|
+
|
|
44
41
|
def on_app_init(self, app_config: AppConfig) -> AppConfig:
|
|
45
42
|
"""Configure application for use with Vite.
|
|
46
43
|
|
|
47
44
|
Args:
|
|
48
45
|
app_config: The :class:`AppConfig <litestar.config.app.AppConfig>` instance.
|
|
49
46
|
"""
|
|
47
|
+
|
|
48
|
+
from litestar.exceptions import ImproperlyConfiguredException
|
|
49
|
+
from litestar.middleware import DefineMiddleware
|
|
50
|
+
from litestar.middleware.session import SessionMiddleware
|
|
51
|
+
from litestar.security.session_auth.middleware import MiddlewareWrapper
|
|
52
|
+
from litestar.utils.predicates import is_class_and_subclass
|
|
53
|
+
|
|
54
|
+
from litestar_vite.inertia.exception_handler import exception_to_http_response
|
|
55
|
+
from litestar_vite.inertia.middleware import InertiaMiddleware
|
|
56
|
+
from litestar_vite.inertia.request import InertiaRequest
|
|
57
|
+
from litestar_vite.inertia.response import DeferredProp, InertiaBack, InertiaResponse, StaticProp
|
|
58
|
+
|
|
50
59
|
for mw in app_config.middleware:
|
|
51
60
|
if isinstance(mw, DefineMiddleware) and is_class_and_subclass(
|
|
52
61
|
mw.middleware,
|
|
@@ -61,4 +70,15 @@ class InertiaPlugin(InitPluginProtocol):
|
|
|
61
70
|
app_config.response_class = InertiaResponse
|
|
62
71
|
app_config.middleware.append(InertiaMiddleware)
|
|
63
72
|
app_config.on_startup.append(set_js_routes)
|
|
73
|
+
app_config.signature_types.extend([InertiaRequest, InertiaResponse, InertiaBack, StaticProp, DeferredProp])
|
|
74
|
+
app_config.type_encoders = {
|
|
75
|
+
StaticProp: lambda val: val.render(),
|
|
76
|
+
DeferredProp: lambda val: val.render(),
|
|
77
|
+
**(app_config.type_encoders or {}),
|
|
78
|
+
}
|
|
79
|
+
app_config.type_decoders = [
|
|
80
|
+
(lambda x: x is StaticProp, lambda t, v: t(v)),
|
|
81
|
+
(lambda x: x is DeferredProp, lambda t, v: t(v)),
|
|
82
|
+
*(app_config.type_decoders or []),
|
|
83
|
+
]
|
|
64
84
|
return app_config
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import inspect
|
|
3
4
|
import itertools
|
|
4
5
|
from collections import defaultdict
|
|
6
|
+
from collections.abc import Mapping
|
|
7
|
+
from contextlib import contextmanager
|
|
5
8
|
from functools import lru_cache
|
|
6
9
|
from mimetypes import guess_type
|
|
7
10
|
from pathlib import PurePath
|
|
8
11
|
from textwrap import dedent
|
|
9
|
-
from typing import
|
|
12
|
+
from typing import (
|
|
13
|
+
TYPE_CHECKING,
|
|
14
|
+
Any,
|
|
15
|
+
Callable,
|
|
16
|
+
Coroutine,
|
|
17
|
+
Dict,
|
|
18
|
+
Generator,
|
|
19
|
+
Generic,
|
|
20
|
+
Iterable,
|
|
21
|
+
List,
|
|
22
|
+
TypeVar,
|
|
23
|
+
cast,
|
|
24
|
+
overload,
|
|
25
|
+
)
|
|
10
26
|
from urllib.parse import quote, urlparse, urlunparse
|
|
11
27
|
|
|
28
|
+
from anyio.from_thread import BlockingPortal, start_blocking_portal
|
|
12
29
|
from litestar import Litestar, MediaType, Request, Response
|
|
13
30
|
from litestar.datastructures.cookie import Cookie
|
|
14
31
|
from litestar.exceptions import ImproperlyConfiguredException
|
|
@@ -21,6 +38,7 @@ from litestar.utils.empty import value_or_default
|
|
|
21
38
|
from litestar.utils.helpers import get_enum_string_value
|
|
22
39
|
from litestar.utils.scope.state import ScopeState
|
|
23
40
|
from markupsafe import Markup
|
|
41
|
+
from typing_extensions import ParamSpec, TypeGuard
|
|
24
42
|
|
|
25
43
|
from litestar_vite.inertia._utils import get_headers
|
|
26
44
|
from litestar_vite.inertia.types import InertiaHeaderType, PageProps
|
|
@@ -33,53 +51,224 @@ if TYPE_CHECKING:
|
|
|
33
51
|
from litestar.connection.base import AuthT, StateT, UserT
|
|
34
52
|
from litestar.types import ResponseCookies, ResponseHeaders, TypeEncodersMap
|
|
35
53
|
|
|
54
|
+
from litestar_vite.inertia.plugin import InertiaPlugin
|
|
36
55
|
from litestar_vite.inertia.routes import Routes
|
|
37
56
|
|
|
38
|
-
from .plugin import InertiaPlugin
|
|
39
|
-
|
|
40
57
|
T = TypeVar("T")
|
|
58
|
+
T_ParamSpec = ParamSpec("T_ParamSpec")
|
|
59
|
+
PropKeyT = TypeVar("PropKeyT", bound=str)
|
|
60
|
+
StaticT = TypeVar("StaticT", bound=object)
|
|
41
61
|
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
|
|
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(
|
|
45
73
|
key: str,
|
|
46
|
-
|
|
47
|
-
) -> None:
|
|
48
|
-
try:
|
|
49
|
-
connection.session.setdefault("_shared", {}).update({key: value})
|
|
50
|
-
except (AttributeError, ImproperlyConfiguredException):
|
|
51
|
-
msg = "Unable to set `share` session state. A valid session was not found for this request."
|
|
52
|
-
connection.logger.warning(msg)
|
|
74
|
+
value_or_callable: Callable[..., None] = ...,
|
|
75
|
+
) -> DeferredProp[str, None]: ...
|
|
53
76
|
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
@overload
|
|
79
|
+
def lazy(
|
|
57
80
|
key: str,
|
|
58
|
-
|
|
59
|
-
) ->
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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)
|
|
65
237
|
|
|
66
238
|
|
|
67
239
|
def get_shared_props(
|
|
68
240
|
request: ASGIConnection[Any, Any, Any, Any],
|
|
69
241
|
partial_data: set[str] | None = None,
|
|
70
|
-
) ->
|
|
71
|
-
"""Return shared session props for a request
|
|
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.
|
|
72
248
|
|
|
249
|
+
Returns:
|
|
250
|
+
Dict[str, Any]: The shared props.
|
|
73
251
|
|
|
74
|
-
|
|
252
|
+
Note:
|
|
253
|
+
Be sure to call this before `self.create_template_context` if you would like to include the `flash` message details.
|
|
75
254
|
"""
|
|
76
255
|
props: dict[str, Any] = {}
|
|
77
256
|
flash: dict[str, list[str]] = defaultdict(list)
|
|
78
257
|
errors: dict[str, Any] = {}
|
|
79
258
|
error_bag = request.headers.get("X-Inertia-Error-Bag", None)
|
|
259
|
+
|
|
80
260
|
try:
|
|
81
261
|
errors = request.session.pop("_errors", {})
|
|
82
|
-
|
|
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
|
+
|
|
83
272
|
for message in cast("List[Dict[str,Any]]", request.session.pop("_messages", [])):
|
|
84
273
|
flash[message["category"]].append(message["message"])
|
|
85
274
|
|
|
@@ -92,12 +281,51 @@ def get_shared_props(
|
|
|
92
281
|
except (AttributeError, ImproperlyConfiguredException):
|
|
93
282
|
msg = "Unable to generate all shared props. A valid session was not found for this request."
|
|
94
283
|
request.logger.warning(msg)
|
|
284
|
+
|
|
95
285
|
props["flash"] = flash
|
|
96
286
|
props["errors"] = {error_bag: errors} if error_bag is not None else errors
|
|
97
287
|
props["csrf_token"] = value_or_default(ScopeState.from_scope(request.scope).csrf_token, "")
|
|
98
288
|
return props
|
|
99
289
|
|
|
100
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
|
+
|
|
101
329
|
def js_routes_script(js_routes: Routes) -> Markup:
|
|
102
330
|
@lru_cache
|
|
103
331
|
def _markup_safe_json_dumps(js_routes: str) -> Markup:
|
|
@@ -197,7 +425,7 @@ class InertiaResponse(Response[T]):
|
|
|
197
425
|
"csrf_input": f'<input type="hidden" name="_csrf_token" value="{csrf_token}" />',
|
|
198
426
|
}
|
|
199
427
|
|
|
200
|
-
def to_asgi_response(
|
|
428
|
+
def to_asgi_response( # noqa: C901, PLR0912
|
|
201
429
|
self,
|
|
202
430
|
app: Litestar | None,
|
|
203
431
|
request: Request[UserT, AuthT, StateT],
|
|
@@ -250,7 +478,13 @@ class InertiaResponse(Response[T]):
|
|
|
250
478
|
{"Vary": "Accept", **get_headers(InertiaHeaderType(enabled=True))},
|
|
251
479
|
)
|
|
252
480
|
shared_props = get_shared_props(request, partial_data=partial_keys if is_partial_render else None)
|
|
253
|
-
|
|
481
|
+
if is_or_contains_lazy_prop(self.content):
|
|
482
|
+
filtered_content = lazy_render(self.content, partial_keys if is_partial_render else None)
|
|
483
|
+
if filtered_content is not None:
|
|
484
|
+
shared_props["content"] = filtered_content
|
|
485
|
+
elif should_render(self.content, partial_keys):
|
|
486
|
+
shared_props["content"] = self.content
|
|
487
|
+
|
|
254
488
|
page_props = PageProps[T](
|
|
255
489
|
component=request.inertia.route_component, # type: ignore[attr-defined] # pyright: ignore[reportUnknownArgumentType,reportUnknownMemberType,reportAttributeAccessIssue]
|
|
256
490
|
props=shared_props, # pyright: ignore[reportArgumentType]
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import platform
|
|
4
5
|
import signal
|
|
6
|
+
import subprocess
|
|
5
7
|
import threading
|
|
6
8
|
from contextlib import contextmanager
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from typing import TYPE_CHECKING, Iterator, cast
|
|
9
11
|
|
|
12
|
+
from litestar.cli._utils import console
|
|
10
13
|
from litestar.contrib.jinja import JinjaTemplateEngine
|
|
11
14
|
from litestar.exceptions import ImproperlyConfiguredException
|
|
12
15
|
from litestar.plugins import CLIPlugin, InitPluginProtocol
|
|
@@ -37,50 +40,43 @@ class ViteProcess:
|
|
|
37
40
|
"""Manages the Vite process."""
|
|
38
41
|
|
|
39
42
|
def __init__(self) -> None:
|
|
40
|
-
self.process:
|
|
43
|
+
self.process: subprocess.Popen | None = None # pyright: ignore[reportUnknownMemberType,reportMissingTypeArgument]
|
|
41
44
|
self._lock = threading.Lock()
|
|
42
45
|
|
|
43
46
|
def start(self, command: list[str], cwd: Path | str | None) -> None:
|
|
44
47
|
"""Start the Vite process."""
|
|
45
|
-
from litestar.cli._utils import console
|
|
46
|
-
|
|
47
|
-
from litestar_vite.commands import execute_command
|
|
48
48
|
|
|
49
49
|
try:
|
|
50
50
|
with self._lock:
|
|
51
|
-
if self.process and self.process.
|
|
51
|
+
if self.process and self.process.poll() is None: # pyright: ignore[reportUnknownMemberType]
|
|
52
52
|
return
|
|
53
53
|
|
|
54
|
-
self.process = threading.Thread(
|
|
55
|
-
name="vite",
|
|
56
|
-
target=execute_command,
|
|
57
|
-
args=[],
|
|
58
|
-
kwargs={"command_to_run": command, "cwd": cwd},
|
|
59
|
-
daemon=True, # Make thread daemon so it exits when main thread exits
|
|
60
|
-
)
|
|
61
54
|
console.print(f"Starting Vite process with command: {command}")
|
|
62
|
-
self.process.
|
|
55
|
+
self.process = subprocess.Popen(
|
|
56
|
+
command,
|
|
57
|
+
cwd=cwd,
|
|
58
|
+
shell=platform.system() == "Windows",
|
|
59
|
+
)
|
|
63
60
|
except Exception as e:
|
|
64
61
|
console.print(f"[red]Failed to start Vite process: {e!s}[/]")
|
|
65
62
|
raise
|
|
66
63
|
|
|
67
64
|
def stop(self, timeout: float = 5.0) -> None:
|
|
68
65
|
"""Stop the Vite process."""
|
|
69
|
-
from litestar.cli._utils import console
|
|
70
66
|
|
|
71
67
|
try:
|
|
72
68
|
with self._lock:
|
|
73
|
-
if self.process and self.process.
|
|
69
|
+
if self.process and self.process.poll() is None: # pyright: ignore[reportUnknownMemberType]
|
|
74
70
|
# Send SIGTERM to child process
|
|
75
|
-
if hasattr(signal, "SIGTERM")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if hasattr(signal, "SIGKILL")
|
|
82
|
-
|
|
83
|
-
self.process.
|
|
71
|
+
if hasattr(signal, "SIGTERM"):
|
|
72
|
+
self.process.terminate() # pyright: ignore[reportUnknownMemberType]
|
|
73
|
+
try:
|
|
74
|
+
self.process.wait(timeout=timeout) # pyright: ignore[reportUnknownMemberType]
|
|
75
|
+
except subprocess.TimeoutExpired:
|
|
76
|
+
# Force kill if still alive
|
|
77
|
+
if hasattr(signal, "SIGKILL"):
|
|
78
|
+
self.process.kill() # pyright: ignore[reportUnknownMemberType]
|
|
79
|
+
self.process.wait(timeout=1.0) # pyright: ignore[reportUnknownMemberType]
|
|
84
80
|
console.print("Stopping Vite process")
|
|
85
81
|
except Exception as e:
|
|
86
82
|
console.print(f"[red]Failed to stop Vite process: {e!s}[/]")
|
|
@@ -168,7 +164,6 @@ class VitePlugin(InitPluginProtocol, CLIPlugin):
|
|
|
168
164
|
@contextmanager
|
|
169
165
|
def server_lifespan(self, app: Litestar) -> Iterator[None]:
|
|
170
166
|
"""Manage Vite server process lifecycle."""
|
|
171
|
-
from litestar.cli._utils import console
|
|
172
167
|
|
|
173
168
|
if self._config.use_server_lifespan and self._config.dev_mode:
|
|
174
169
|
command_to_run = self._config.run_command if self._config.hot_reload else self._config.build_watch_command
|
|
@@ -18,21 +18,28 @@ def anyio_backend() -> str:
|
|
|
18
18
|
return "asyncio"
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
@pytest.fixture(autouse=True)
|
|
22
|
+
def permissive_tmp_path(tmp_path: Path) -> Generator[Path, None, None]:
|
|
23
|
+
tmp_path.chmod(0o775)
|
|
24
|
+
tmp_path.parent.chmod(0o775)
|
|
25
|
+
yield tmp_path
|
|
26
|
+
|
|
27
|
+
|
|
21
28
|
@pytest.fixture
|
|
22
29
|
def test_app_path() -> Generator[Path, None, None]:
|
|
23
30
|
yield Path(here / "test_app" / "web")
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
@pytest.fixture
|
|
27
|
-
def template_config(test_app_path: Path) -> TemplateConfig[JinjaTemplateEngine]:
|
|
28
|
-
|
|
34
|
+
def template_config(test_app_path: Path) -> Generator[TemplateConfig[JinjaTemplateEngine], None, None]:
|
|
35
|
+
yield TemplateConfig(engine=JinjaTemplateEngine(directory=test_app_path / "templates"))
|
|
29
36
|
|
|
30
37
|
|
|
31
38
|
# Define a fixture for ViteConfig
|
|
32
39
|
@pytest.fixture
|
|
33
|
-
def vite_config(test_app_path: Path) -> ViteConfig:
|
|
40
|
+
def vite_config(test_app_path: Path) -> Generator[ViteConfig, None, None]:
|
|
34
41
|
# Mock the ViteConfig with necessary attributes for testing
|
|
35
|
-
|
|
42
|
+
yield ViteConfig(
|
|
36
43
|
bundle_dir=test_app_path / "public",
|
|
37
44
|
resource_dir=test_app_path / "resources",
|
|
38
45
|
hot_reload=True,
|