simple-module-inertia 0.0.35__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.
Files changed (31) hide show
  1. simple_module_inertia-0.0.35/.gitignore +66 -0
  2. simple_module_inertia-0.0.35/LICENSE +21 -0
  3. simple_module_inertia-0.0.35/NOTICE +25 -0
  4. simple_module_inertia-0.0.35/PKG-INFO +102 -0
  5. simple_module_inertia-0.0.35/README.md +75 -0
  6. simple_module_inertia-0.0.35/pyproject.toml +38 -0
  7. simple_module_inertia-0.0.35/simple_module_inertia/__init__.py +47 -0
  8. simple_module_inertia-0.0.35/simple_module_inertia/config.py +41 -0
  9. simple_module_inertia-0.0.35/simple_module_inertia/deps.py +23 -0
  10. simple_module_inertia-0.0.35/simple_module_inertia/errors.py +53 -0
  11. simple_module_inertia-0.0.35/simple_module_inertia/inertia.py +145 -0
  12. simple_module_inertia-0.0.35/simple_module_inertia/manifest.py +70 -0
  13. simple_module_inertia-0.0.35/simple_module_inertia/page.py +105 -0
  14. simple_module_inertia-0.0.35/simple_module_inertia/props.py +121 -0
  15. simple_module_inertia-0.0.35/simple_module_inertia/py.typed +0 -0
  16. simple_module_inertia-0.0.35/simple_module_inertia/request.py +59 -0
  17. simple_module_inertia-0.0.35/simple_module_inertia/resolve.py +99 -0
  18. simple_module_inertia-0.0.35/simple_module_inertia/response.py +46 -0
  19. simple_module_inertia-0.0.35/simple_module_inertia/templating.py +74 -0
  20. simple_module_inertia-0.0.35/tests/test_adapter_config.py +35 -0
  21. simple_module_inertia-0.0.35/tests/test_adapter_errors.py +54 -0
  22. simple_module_inertia-0.0.35/tests/test_adapter_manifest.py +107 -0
  23. simple_module_inertia-0.0.35/tests/test_adapter_package.py +23 -0
  24. simple_module_inertia-0.0.35/tests/test_adapter_page.py +150 -0
  25. simple_module_inertia-0.0.35/tests/test_adapter_props.py +72 -0
  26. simple_module_inertia-0.0.35/tests/test_adapter_render.py +100 -0
  27. simple_module_inertia-0.0.35/tests/test_adapter_request.py +72 -0
  28. simple_module_inertia-0.0.35/tests/test_adapter_resolve_metadata.py +113 -0
  29. simple_module_inertia-0.0.35/tests/test_adapter_resolve_visits.py +116 -0
  30. simple_module_inertia-0.0.35/tests/test_adapter_response.py +50 -0
  31. simple_module_inertia-0.0.35/tests/test_adapter_templating.py +46 -0
@@ -0,0 +1,66 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .venv/
8
+ *.egg
9
+
10
+ # UV
11
+ uv.lock
12
+
13
+ # Node
14
+ node_modules/
15
+
16
+ # IDE
17
+ .idea/
18
+ .vscode/
19
+ *.swp
20
+ *.swo
21
+
22
+ # Environment
23
+ .env
24
+
25
+ # Database
26
+ *.db
27
+ *.sqlite3
28
+
29
+ # Module-managed runtime state (e.g. uploaded dataset files,
30
+ # default storage_dir for SM_DATASETS_STORAGE_DIR).
31
+ var/
32
+
33
+ # file_storage filesystem backend default root (override via SM_FILE_STORAGE_FS_ROOT_PATH).
34
+ uploads/
35
+
36
+ # Vite
37
+ host/static/dist/
38
+
39
+ # VitePress (docs)
40
+ docs/.vitepress/cache/
41
+ docs/.vitepress/dist/
42
+
43
+ # Auto-generated frontend module manifest (regenerated by the host at boot
44
+ # or via `make gen-pages`).
45
+ host/client_app/modules.manifest.json
46
+ host/client_app/modules.generated.ts
47
+ host/client_app/modules.generated.css
48
+ host/client_app/modules.assets.json
49
+
50
+ # Worktrees
51
+ .worktrees/
52
+
53
+ # Performance profiles
54
+ .memray/
55
+ .benchmarks/
56
+
57
+ # OS
58
+ .DS_Store
59
+ Thumbs.db
60
+
61
+ .playwright-cli/*
62
+ .playwright-mcp/*
63
+ host/client_app/.playwright-cli/*
64
+ .superpowers/
65
+ .qa/
66
+ .verify/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anto Subash
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,25 @@
1
+ This package is a fork of fastapi-inertia 1.1.0 (https://github.com/hxjo/fastapi-inertia),
2
+ distributed under the MIT License. The original license text follows, as the
3
+ MIT License requires.
4
+
5
+ The MIT License (MIT)
6
+
7
+ Copyright (c) 2022 Bellawatt
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in
17
+ all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
+ THE SOFTWARE.
@@ -0,0 +1,102 @@
1
+ Metadata-Version: 2.5
2
+ Name: simple_module_inertia
3
+ Version: 0.0.35
4
+ Summary: Inertia.js v3 server adapter for FastAPI — the protocol half of simple_module's React frontend
5
+ Project-URL: Homepage, https://github.com/antosubash/simple_module_python
6
+ Project-URL: Repository, https://github.com/antosubash/simple_module_python
7
+ Project-URL: Issues, https://github.com/antosubash/simple_module_python/issues
8
+ Project-URL: Changelog, https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md
9
+ Author-email: Anto Subash <antosubash@live.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ License-File: NOTICE
13
+ Keywords: fastapi,inertia,inertiajs,react,simple-module
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Framework :: FastAPI
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Operating System :: OS Independent
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.12
23
+ Requires-Dist: fastapi>=0.115
24
+ Requires-Dist: jinja2>=3.1
25
+ Requires-Dist: starlette>=0.44
26
+ Description-Content-Type: text/markdown
27
+
28
+ # simple_module_inertia
29
+
30
+ Inertia.js **v3** server adapter for FastAPI. This is the protocol half of a
31
+ simple_module app's React frontend: it turns `inertia.render("Users/Index", props)`
32
+ into either the JSON page object an Inertia visit expects or the HTML document a
33
+ full page load expects, and implements every v3 prop type (`optional`, `always`,
34
+ `defer`, `merge`, `prepend`, `deep_merge`, `once`, `scroll`).
35
+
36
+ Forked from [fastapi-inertia](https://github.com/hxjo/fastapi-inertia) (MIT) —
37
+ see `NOTICE`. Precognition and SSR are not implemented.
38
+
39
+ ## Install
40
+
41
+ `simple_module_hosting` depends on this package, so any app scaffolded with
42
+ `smpy new` already has it. To use the adapter on its own:
43
+
44
+ ```bash
45
+ uv add simple_module_inertia
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ Inside a simple_module app, reach the per-request `Inertia` instance through
51
+ `InertiaDep` and import the response type and prop factories from here:
52
+
53
+ ```python
54
+ from simple_module_hosting.inertia_deps import InertiaDep
55
+ from simple_module_inertia import InertiaResponse, defer, merge, optional
56
+
57
+
58
+ async def index(inertia: InertiaDep) -> InertiaResponse:
59
+ return await inertia.render(
60
+ "Feed/Index",
61
+ {
62
+ "posts": merge(load_posts, match_on="id"), # append on navigation
63
+ "analytics": defer(load_analytics), # loaded after first paint
64
+ "stats": optional(load_stats), # only on partial reloads that ask
65
+ },
66
+ )
67
+ ```
68
+
69
+ Standalone, configure it once and register the two exception handlers:
70
+
71
+ ```python
72
+ from fastapi import Depends, FastAPI
73
+ from fastapi.templating import Jinja2Templates
74
+ from simple_module_inertia import (
75
+ Inertia,
76
+ InertiaConfig,
77
+ InertiaVersionConflictException,
78
+ inertia_dependency_factory,
79
+ inertia_version_conflict_exception_handler,
80
+ )
81
+
82
+ config = InertiaConfig(
83
+ templates=Jinja2Templates(directory="templates"),
84
+ entrypoint_filename="main.tsx",
85
+ root_directory=".",
86
+ dev_url="http://localhost:5173",
87
+ )
88
+ inertia_dep = inertia_dependency_factory(config)
89
+
90
+ app = FastAPI()
91
+ app.add_exception_handler(
92
+ InertiaVersionConflictException, inertia_version_conflict_exception_handler
93
+ )
94
+
95
+
96
+ @app.get("/")
97
+ async def home(inertia: Inertia = Depends(inertia_dep)):
98
+ return await inertia.render("Home", {"greeting": "hello"})
99
+ ```
100
+
101
+ The root template needs `{% inertia_head %}` in `<head>` and `{% inertia_body %}`
102
+ in `<body>`; the page object lands in `<div id="app" data-page="…">`.
@@ -0,0 +1,75 @@
1
+ # simple_module_inertia
2
+
3
+ Inertia.js **v3** server adapter for FastAPI. This is the protocol half of a
4
+ simple_module app's React frontend: it turns `inertia.render("Users/Index", props)`
5
+ into either the JSON page object an Inertia visit expects or the HTML document a
6
+ full page load expects, and implements every v3 prop type (`optional`, `always`,
7
+ `defer`, `merge`, `prepend`, `deep_merge`, `once`, `scroll`).
8
+
9
+ Forked from [fastapi-inertia](https://github.com/hxjo/fastapi-inertia) (MIT) —
10
+ see `NOTICE`. Precognition and SSR are not implemented.
11
+
12
+ ## Install
13
+
14
+ `simple_module_hosting` depends on this package, so any app scaffolded with
15
+ `smpy new` already has it. To use the adapter on its own:
16
+
17
+ ```bash
18
+ uv add simple_module_inertia
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Inside a simple_module app, reach the per-request `Inertia` instance through
24
+ `InertiaDep` and import the response type and prop factories from here:
25
+
26
+ ```python
27
+ from simple_module_hosting.inertia_deps import InertiaDep
28
+ from simple_module_inertia import InertiaResponse, defer, merge, optional
29
+
30
+
31
+ async def index(inertia: InertiaDep) -> InertiaResponse:
32
+ return await inertia.render(
33
+ "Feed/Index",
34
+ {
35
+ "posts": merge(load_posts, match_on="id"), # append on navigation
36
+ "analytics": defer(load_analytics), # loaded after first paint
37
+ "stats": optional(load_stats), # only on partial reloads that ask
38
+ },
39
+ )
40
+ ```
41
+
42
+ Standalone, configure it once and register the two exception handlers:
43
+
44
+ ```python
45
+ from fastapi import Depends, FastAPI
46
+ from fastapi.templating import Jinja2Templates
47
+ from simple_module_inertia import (
48
+ Inertia,
49
+ InertiaConfig,
50
+ InertiaVersionConflictException,
51
+ inertia_dependency_factory,
52
+ inertia_version_conflict_exception_handler,
53
+ )
54
+
55
+ config = InertiaConfig(
56
+ templates=Jinja2Templates(directory="templates"),
57
+ entrypoint_filename="main.tsx",
58
+ root_directory=".",
59
+ dev_url="http://localhost:5173",
60
+ )
61
+ inertia_dep = inertia_dependency_factory(config)
62
+
63
+ app = FastAPI()
64
+ app.add_exception_handler(
65
+ InertiaVersionConflictException, inertia_version_conflict_exception_handler
66
+ )
67
+
68
+
69
+ @app.get("/")
70
+ async def home(inertia: Inertia = Depends(inertia_dep)):
71
+ return await inertia.render("Home", {"greeting": "hello"})
72
+ ```
73
+
74
+ The root template needs `{% inertia_head %}` in `<head>` and `{% inertia_body %}`
75
+ in `<body>`; the page object lands in `<div id="app" data-page="…">`.
@@ -0,0 +1,38 @@
1
+ [project]
2
+ name = "simple_module_inertia"
3
+ version = "0.0.35"
4
+ description = "Inertia.js v3 server adapter for FastAPI — the protocol half of simple_module's React frontend"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE", "NOTICE"]
8
+ requires-python = ">=3.12"
9
+ authors = [{ name = "Anto Subash", email = "antosubash@live.com" }]
10
+ keywords = ["simple-module", "inertia", "inertiajs", "fastapi", "react"]
11
+ classifiers = [
12
+ "Development Status :: 3 - Alpha",
13
+ "Framework :: FastAPI",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "fastapi>=0.115",
23
+ "jinja2>=3.1",
24
+ "starlette>=0.44",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/antosubash/simple_module_python"
29
+ Repository = "https://github.com/antosubash/simple_module_python"
30
+ Issues = "https://github.com/antosubash/simple_module_python/issues"
31
+ Changelog = "https://github.com/antosubash/simple_module_python/blob/main/CHANGELOG.md"
32
+
33
+ [build-system]
34
+ requires = ["hatchling"]
35
+ build-backend = "hatchling.build"
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ packages = ["simple_module_inertia"]
@@ -0,0 +1,47 @@
1
+ """Inertia.js v3 server adapter for FastAPI. See README.md and NOTICE."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from simple_module_inertia.config import InertiaConfig
6
+ from simple_module_inertia.deps import inertia_dependency_factory
7
+ from simple_module_inertia.errors import (
8
+ InertiaVersionConflictException,
9
+ inertia_request_validation_exception_handler,
10
+ inertia_version_conflict_exception_handler,
11
+ )
12
+ from simple_module_inertia.inertia import Inertia
13
+ from simple_module_inertia.page import PropEncodingError
14
+ from simple_module_inertia.props import (
15
+ always,
16
+ deep_merge,
17
+ defer,
18
+ lazy,
19
+ merge,
20
+ once,
21
+ optional,
22
+ prepend,
23
+ scroll,
24
+ )
25
+ from simple_module_inertia.response import InertiaResponse
26
+ from simple_module_inertia.templating import InertiaExtension
27
+
28
+ __all__ = [
29
+ "Inertia",
30
+ "InertiaConfig",
31
+ "InertiaExtension",
32
+ "InertiaResponse",
33
+ "InertiaVersionConflictException",
34
+ "PropEncodingError",
35
+ "always",
36
+ "deep_merge",
37
+ "defer",
38
+ "inertia_dependency_factory",
39
+ "inertia_request_validation_exception_handler",
40
+ "inertia_version_conflict_exception_handler",
41
+ "lazy",
42
+ "merge",
43
+ "once",
44
+ "optional",
45
+ "prepend",
46
+ "scroll",
47
+ ]
@@ -0,0 +1,41 @@
1
+ """Adapter configuration.
2
+
3
+ Field names match upstream ``fastapi-inertia`` so ``simple_module_hosting``'s
4
+ ``setup_inertia`` keeps constructing it the same way. The one addition is that
5
+ ``version`` may be a zero-arg callable, so an app can derive the asset version
6
+ from its Vite manifest hash at boot instead of hard-coding ``"1.0"``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Literal
14
+
15
+ from fastapi.templating import Jinja2Templates
16
+
17
+
18
+ @dataclass
19
+ class InertiaConfig:
20
+ templates: Jinja2Templates
21
+ environment: Literal["development", "production"] = "development"
22
+ version: str | Callable[[], str] = "1.0"
23
+ dev_url: str = "http://localhost:5173"
24
+ ssr_url: str = "http://localhost:13714"
25
+ ssr_enabled: bool = False
26
+ manifest_json_path: str = ""
27
+ root_directory: str = "src"
28
+ root_template_filename: str = "index.html"
29
+ entrypoint_filename: str = "main.js"
30
+ use_flash_messages: bool = False
31
+ use_flash_errors: bool = False
32
+ flash_message_key: str = "messages"
33
+ flash_error_key: str = "errors"
34
+ assets_prefix: str = ""
35
+ extra_template_context: dict[str, Any] = field(default_factory=dict)
36
+
37
+
38
+ def resolved_version(config: InertiaConfig) -> str:
39
+ """The asset version as a string, calling ``version`` if it is a callable."""
40
+ version = config.version
41
+ return str(version()) if callable(version) else str(version)
@@ -0,0 +1,23 @@
1
+ """Build the per-request dependency hosting publishes on ``app.state``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable
6
+ from typing import Any
7
+
8
+ from fastapi import Request
9
+
10
+ from simple_module_inertia.config import InertiaConfig
11
+ from simple_module_inertia.inertia import Inertia
12
+ from simple_module_inertia.templating import InertiaExtension
13
+
14
+
15
+ def inertia_dependency_factory(config: InertiaConfig) -> Callable[..., Inertia]:
16
+ env = config.templates.env
17
+ if InertiaExtension not in env.extensions.values():
18
+ env.add_extension(InertiaExtension)
19
+
20
+ def inertia_dependency(request: Request, client: Any = None) -> Inertia:
21
+ return Inertia(request, config, client)
22
+
23
+ return inertia_dependency
@@ -0,0 +1,53 @@
1
+ """Exception types and handlers. Names match upstream — hosting imports them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from fastapi import Request, Response, status
8
+ from fastapi.exception_handlers import request_validation_exception_handler
9
+ from fastapi.exceptions import RequestValidationError
10
+ from starlette.responses import RedirectResponse
11
+
12
+
13
+ class InertiaVersionConflictException(Exception): # noqa: N818 - upstream name; hosting imports it
14
+ """Raised on a GET whose ``X-Inertia-Version`` no longer matches ours."""
15
+
16
+ def __init__(self, url: str) -> None:
17
+ self.url = url
18
+ super().__init__()
19
+
20
+
21
+ async def inertia_version_conflict_exception_handler(
22
+ _: Request, exc: InertiaVersionConflictException
23
+ ) -> Response:
24
+ return Response(
25
+ status_code=status.HTTP_409_CONFLICT, headers={"X-Inertia-Location": str(exc.url)}
26
+ )
27
+
28
+
29
+ def _field(loc: tuple[Any, ...]) -> Any:
30
+ return loc[1] if len(loc) > 1 else loc[0]
31
+
32
+
33
+ async def inertia_request_validation_exception_handler(
34
+ request: Request, exc: RequestValidationError
35
+ ) -> Response:
36
+ """Flash validation errors and send the browser back, scoped by error bag."""
37
+ if "x-inertia" not in request.headers:
38
+ return await request_validation_exception_handler(request, exc)
39
+
40
+ errors: dict[str, Any] = {}
41
+ bag = request.headers.get("X-Inertia-Error-Bag")
42
+ for error in exc.errors():
43
+ field = _field(tuple(error["loc"]))
44
+ if bag is None:
45
+ errors[field] = error["msg"]
46
+ else:
47
+ errors.setdefault(bag, {})[field] = error["msg"]
48
+ request.session["_errors"] = errors
49
+
50
+ code = (
51
+ status.HTTP_307_TEMPORARY_REDIRECT if request.method == "GET" else status.HTTP_303_SEE_OTHER
52
+ )
53
+ return RedirectResponse(url=request.headers.get("Referer", "/"), status_code=code)
@@ -0,0 +1,145 @@
1
+ """The request-scoped facade. Thin: each stage lives in its own module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, cast
6
+
7
+ from fastapi import Request
8
+ from jinja2.utils import htmlsafe_json_dumps
9
+ from pydantic import BaseModel
10
+ from starlette.responses import RedirectResponse, Response
11
+
12
+ from simple_module_inertia.config import InertiaConfig, resolved_version
13
+ from simple_module_inertia.errors import InertiaVersionConflictException
14
+ from simple_module_inertia.manifest import entry_assets
15
+ from simple_module_inertia.page import build_page, encode_page
16
+ from simple_module_inertia.request import InertiaRequest
17
+ from simple_module_inertia.resolve import resolve_props
18
+ from simple_module_inertia.response import (
19
+ InertiaResponse,
20
+ fragment_redirect,
21
+ json_response,
22
+ location,
23
+ redirect,
24
+ )
25
+ from simple_module_inertia.templating import InertiaContext
26
+
27
+
28
+ class Inertia:
29
+ def __init__(self, request: Request, config: InertiaConfig, client: Any = None) -> None:
30
+ self._request = request
31
+ self._config = config
32
+ self._client = client # kept for signature compatibility; SSR is not implemented
33
+ self._req = InertiaRequest.from_headers(request.headers, request.method)
34
+ self._version = resolved_version(config)
35
+ self._shared: dict[str, Any] = {}
36
+ self._encrypt_history = False
37
+ self._clear_history = False
38
+ self._preserve_fragment = False
39
+ if self._is_stale():
40
+ raise InertiaVersionConflictException(url=str(request.url))
41
+
42
+ # -- request state ----------------------------------------------------
43
+
44
+ def _is_stale(self) -> bool:
45
+ """Only a GET is rejected; the client re-submits mutations itself."""
46
+ return (
47
+ self._req.is_inertia
48
+ and self._req.method == "GET"
49
+ and self._req.version is not None
50
+ and self._req.version != self._version
51
+ )
52
+
53
+ def share(self, **props: Any) -> None:
54
+ self._shared.update(props)
55
+
56
+ def flash(self, message: str, category: str) -> None:
57
+ if not self._config.use_flash_messages:
58
+ raise NotImplementedError("Flash messages are not enabled")
59
+ self._request.session.setdefault("_messages", []).append(
60
+ {"message": message, "category": category}
61
+ )
62
+
63
+ def encrypt_history(self) -> Inertia:
64
+ self._encrypt_history = True
65
+ return self
66
+
67
+ def clear_history(self) -> Inertia:
68
+ self._clear_history = True
69
+ return self
70
+
71
+ def preserve_fragment(self) -> Inertia:
72
+ self._preserve_fragment = True
73
+ return self
74
+
75
+ # -- redirects --------------------------------------------------------
76
+
77
+ def redirect(self, url: str) -> RedirectResponse:
78
+ return redirect(url, method=self._request.method)
79
+
80
+ def back(self) -> RedirectResponse:
81
+ return self.redirect(self._request.headers.get("Referer", "/"))
82
+
83
+ @staticmethod
84
+ def location(url: str) -> Response:
85
+ return location(url)
86
+
87
+ @staticmethod
88
+ def redirect_with_fragment(url: str) -> Response:
89
+ return fragment_redirect(url)
90
+
91
+ # -- render -----------------------------------------------------------
92
+
93
+ def _pop_session(self, key: str, default: Any) -> Any:
94
+ session = self._request.session
95
+ return session.pop(key) if key in session else default
96
+
97
+ async def render(
98
+ self, component: str, props: dict[str, Any] | BaseModel | None = None
99
+ ) -> InertiaResponse:
100
+ page_props: dict[str, Any] = (
101
+ props.model_dump() if isinstance(props, BaseModel) else dict(props or {})
102
+ )
103
+ if self._config.use_flash_messages:
104
+ page_props[self._config.flash_message_key] = self._pop_session("_messages", [])
105
+ errors = self._pop_session("_errors", {}) if self._config.use_flash_errors else {}
106
+ # ``errors`` is the protocol's always-prop; keep the configured key
107
+ # pointing at the same dict so a custom flash_error_key still works.
108
+ if self._config.flash_error_key != "errors":
109
+ page_props[self._config.flash_error_key] = errors
110
+
111
+ resolved = await resolve_props(page_props, self._shared, component, self._req)
112
+ page = build_page(
113
+ component=component,
114
+ resolved=resolved,
115
+ url=str(self._request.url),
116
+ version=self._version,
117
+ errors=errors,
118
+ encrypt_history=self._encrypt_history,
119
+ clear_history=self._clear_history,
120
+ preserve_fragment=self._preserve_fragment,
121
+ )
122
+ if self._req.is_inertia:
123
+ return json_response(page)
124
+ return self._render_html(page)
125
+
126
+ def _render_html(self, page: dict[str, Any]) -> InertiaResponse:
127
+ files = entry_assets(self._config)
128
+ # encode_page yields a JSON-safe dict; htmlsafe_json_dumps serialises it
129
+ # exactly once with HTML-safe escaping. Passing a pre-dumped string here
130
+ # would double-encode the page object into a quoted JSON literal.
131
+ page_json = htmlsafe_json_dumps(encode_page(page))
132
+ context = {
133
+ "inertia": InertiaContext(
134
+ environment=self._config.environment,
135
+ dev_url=self._config.dev_url,
136
+ is_ssr=False,
137
+ data=cast(str, page_json),
138
+ js=files.js,
139
+ css=files.css,
140
+ ),
141
+ **self._config.extra_template_context,
142
+ }
143
+ return self._config.templates.TemplateResponse(
144
+ name=self._config.root_template_filename, request=self._request, context=context
145
+ )