litestar-vite 0.1.1__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/__init__.py +54 -4
- litestar_vite/__metadata__.py +12 -7
- litestar_vite/_codegen/__init__.py +26 -0
- litestar_vite/_codegen/inertia.py +407 -0
- litestar_vite/_codegen/openapi.py +233 -0
- litestar_vite/_codegen/routes.py +653 -0
- litestar_vite/_codegen/ts.py +235 -0
- litestar_vite/_handler/__init__.py +8 -0
- litestar_vite/_handler/app.py +524 -0
- litestar_vite/_handler/routing.py +130 -0
- litestar_vite/cli.py +1147 -10
- litestar_vite/codegen.py +39 -0
- litestar_vite/commands.py +79 -0
- litestar_vite/config.py +1594 -70
- litestar_vite/deploy.py +355 -0
- litestar_vite/doctor.py +1179 -0
- litestar_vite/exceptions.py +78 -0
- litestar_vite/executor.py +316 -0
- litestar_vite/handler.py +9 -0
- litestar_vite/html_transform.py +426 -0
- litestar_vite/inertia/__init__.py +53 -0
- litestar_vite/inertia/_utils.py +114 -0
- litestar_vite/inertia/exception_handler.py +172 -0
- litestar_vite/inertia/helpers.py +1043 -0
- litestar_vite/inertia/middleware.py +54 -0
- litestar_vite/inertia/plugin.py +133 -0
- litestar_vite/inertia/request.py +286 -0
- litestar_vite/inertia/response.py +706 -0
- litestar_vite/inertia/types.py +316 -0
- litestar_vite/loader.py +462 -121
- litestar_vite/plugin.py +2160 -21
- 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 +35 -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 +27 -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 +38 -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 +46 -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 +49 -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.0rc2.dist-info/METADATA +230 -0
- litestar_vite-0.15.0rc2.dist-info/RECORD +151 -0
- {litestar_vite-0.1.1.dist-info → litestar_vite-0.15.0rc2.dist-info}/WHEEL +1 -1
- 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.0rc2.dist-info}/licenses/LICENSE +0 -0
litestar_vite/deploy.py
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Vite CDN deployment utilities.
|
|
2
|
+
|
|
3
|
+
Provides a deployer for publishing built Vite assets to any fsspec backend.
|
|
4
|
+
DeployConfig is defined in litestar_vite.config and passed into ViteDeployer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# pyright: reportUnknownVariableType=false, reportUnknownMemberType=false, reportMissingTypeStubs=false
|
|
8
|
+
|
|
9
|
+
from collections.abc import Callable, Iterable
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from importlib.util import find_spec
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, cast
|
|
14
|
+
|
|
15
|
+
from litestar.exceptions import SerializationException
|
|
16
|
+
from litestar.serialization import decode_json
|
|
17
|
+
|
|
18
|
+
from litestar_vite.config import DeployConfig as _DeployConfig
|
|
19
|
+
from litestar_vite.exceptions import MissingDependencyError
|
|
20
|
+
|
|
21
|
+
__all__ = ("FileInfo", "SyncPlan", "SyncResult", "ViteDeployer", "format_bytes")
|
|
22
|
+
|
|
23
|
+
AbstractFileSystem = Any
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _suggest_install_extra(storage_backend: "str | None") -> str:
|
|
27
|
+
"""Suggest an install target based on backend scheme.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
storage_backend: The storage backend URL.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Suggested package to install.
|
|
34
|
+
"""
|
|
35
|
+
if not storage_backend:
|
|
36
|
+
return "fsspec"
|
|
37
|
+
scheme = storage_backend.split("://", 1)[0]
|
|
38
|
+
mapping = {"gcs": "gcsfs", "s3": "s3fs", "abfs": "adlfs", "az": "adlfs", "sftp": "fsspec", "ftp": "fsspec"}
|
|
39
|
+
return mapping.get(scheme, "fsspec")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _import_fsspec(storage_backend: "str | None") -> tuple[Any, Callable[..., tuple[Any, Any]]]:
|
|
43
|
+
"""Import fsspec lazily with a helpful error when missing.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
storage_backend: The storage backend URL for error messaging.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of fsspec module and url_to_fs function.
|
|
50
|
+
|
|
51
|
+
Raises:
|
|
52
|
+
MissingDependencyError: If fsspec is not installed.
|
|
53
|
+
"""
|
|
54
|
+
if find_spec("fsspec") is None:
|
|
55
|
+
msg = "fsspec"
|
|
56
|
+
raise MissingDependencyError(msg, install_package=_suggest_install_extra(storage_backend))
|
|
57
|
+
|
|
58
|
+
import fsspec # pyright: ignore
|
|
59
|
+
from fsspec.core import url_to_fs # pyright: ignore
|
|
60
|
+
|
|
61
|
+
return fsspec, url_to_fs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class FileInfo:
|
|
66
|
+
"""Lightweight file metadata used for sync planning."""
|
|
67
|
+
|
|
68
|
+
path: str
|
|
69
|
+
size: int
|
|
70
|
+
mtime: float
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class SyncPlan:
|
|
75
|
+
"""Diff plan for deployment."""
|
|
76
|
+
|
|
77
|
+
to_upload: list[str]
|
|
78
|
+
to_delete: list[str]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class SyncResult:
|
|
83
|
+
"""Deployment result summary."""
|
|
84
|
+
|
|
85
|
+
uploaded: list[str]
|
|
86
|
+
deleted: list[str]
|
|
87
|
+
uploaded_bytes: int
|
|
88
|
+
deleted_bytes: int
|
|
89
|
+
dry_run: bool
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ViteDeployer:
|
|
93
|
+
"""Deploy built Vite assets to a remote fsspec backend."""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
*,
|
|
98
|
+
bundle_dir: Path,
|
|
99
|
+
manifest_name: str,
|
|
100
|
+
deploy_config: _DeployConfig,
|
|
101
|
+
fs: "AbstractFileSystem | None" = None,
|
|
102
|
+
remote_path: str | None = None,
|
|
103
|
+
) -> None:
|
|
104
|
+
self._fsspec, self._url_to_fs = _import_fsspec(deploy_config.storage_backend)
|
|
105
|
+
if not deploy_config.enabled:
|
|
106
|
+
msg = "Deployment is disabled. Enable DeployConfig.enabled to proceed."
|
|
107
|
+
raise ValueError(msg)
|
|
108
|
+
if not deploy_config.storage_backend:
|
|
109
|
+
msg = "DeployConfig.storage_backend is required (e.g. gcs://bucket/assets)."
|
|
110
|
+
raise ValueError(msg)
|
|
111
|
+
|
|
112
|
+
self.bundle_dir = bundle_dir
|
|
113
|
+
self.manifest_path = bundle_dir / manifest_name
|
|
114
|
+
self.config = deploy_config
|
|
115
|
+
self._fs, self.remote_path = self._init_filesystem(fs, remote_path)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def fs(self) -> "AbstractFileSystem":
|
|
119
|
+
"""Filesystem for deployment operations.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
The filesystem used for deployment operations.
|
|
123
|
+
"""
|
|
124
|
+
return self._fs
|
|
125
|
+
|
|
126
|
+
def collect_local_files(self) -> dict[str, FileInfo]:
|
|
127
|
+
"""Collect local files to publish.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Mapping of relative paths to file metadata.
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
manifest_paths: set[str] = (
|
|
134
|
+
self._paths_from_manifest(self.manifest_path) if self.manifest_path.exists() else set[str]()
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
include_manifest = self.config.include_manifest and self.manifest_path.exists()
|
|
138
|
+
files: dict[str, FileInfo] = {}
|
|
139
|
+
|
|
140
|
+
if manifest_paths:
|
|
141
|
+
candidate_paths: list[Path] = [self.bundle_dir / p for p in manifest_paths]
|
|
142
|
+
if include_manifest:
|
|
143
|
+
candidate_paths.append(self.manifest_path)
|
|
144
|
+
candidates: Iterable[Path] = candidate_paths
|
|
145
|
+
else:
|
|
146
|
+
candidates = self.bundle_dir.rglob("*")
|
|
147
|
+
|
|
148
|
+
for path in candidates:
|
|
149
|
+
if path.is_dir():
|
|
150
|
+
continue
|
|
151
|
+
if not path.exists():
|
|
152
|
+
continue
|
|
153
|
+
rel_path = path.relative_to(self.bundle_dir).as_posix()
|
|
154
|
+
stat = path.stat()
|
|
155
|
+
files[rel_path] = FileInfo(path=rel_path, size=stat.st_size, mtime=stat.st_mtime)
|
|
156
|
+
|
|
157
|
+
index_html = self.bundle_dir / "index.html"
|
|
158
|
+
if index_html.exists():
|
|
159
|
+
stat = index_html.stat()
|
|
160
|
+
files.setdefault("index.html", FileInfo(path="index.html", size=stat.st_size, mtime=stat.st_mtime))
|
|
161
|
+
|
|
162
|
+
return files
|
|
163
|
+
|
|
164
|
+
def collect_remote_files(self) -> dict[str, FileInfo]:
|
|
165
|
+
"""Collect remote files from the target storage.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Mapping of relative remote paths to file metadata.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
entries = cast("list[dict[str, Any]]", self.fs.ls(self.remote_path, detail=True))
|
|
173
|
+
except (FileNotFoundError, OSError):
|
|
174
|
+
return {}
|
|
175
|
+
|
|
176
|
+
remote_files: dict[str, FileInfo] = {}
|
|
177
|
+
base = self.remote_path.rstrip("/")
|
|
178
|
+
for entry in entries:
|
|
179
|
+
name = entry.get("name")
|
|
180
|
+
if name is None:
|
|
181
|
+
continue
|
|
182
|
+
if entry.get("type") == "directory":
|
|
183
|
+
continue
|
|
184
|
+
rel_path = self._relative_remote_path(name, base)
|
|
185
|
+
remote_files[rel_path] = FileInfo(
|
|
186
|
+
path=rel_path, size=int(entry.get("size", 0)), mtime=float(entry.get("mtime", 0.0))
|
|
187
|
+
)
|
|
188
|
+
return remote_files
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def compute_diff(local: dict[str, FileInfo], remote: dict[str, FileInfo], delete_orphaned: bool) -> SyncPlan:
|
|
192
|
+
"""Compute which files to upload or delete.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
local: Local files keyed by relative path.
|
|
196
|
+
remote: Remote files keyed by relative path.
|
|
197
|
+
delete_orphaned: Whether to remove remote-only files.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
SyncPlan listing upload and delete actions.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
to_upload: list[str] = []
|
|
204
|
+
for path, info in local.items():
|
|
205
|
+
remote_info = remote.get(path)
|
|
206
|
+
if remote_info is None or remote_info.size != info.size:
|
|
207
|
+
to_upload.append(path)
|
|
208
|
+
|
|
209
|
+
to_delete: list[str] = [path for path in remote if path not in local] if delete_orphaned else []
|
|
210
|
+
|
|
211
|
+
return SyncPlan(to_upload=to_upload, to_delete=to_delete)
|
|
212
|
+
|
|
213
|
+
def sync(self, *, dry_run: bool = False, on_progress: Callable[[str, str], None] | None = None) -> SyncResult:
|
|
214
|
+
"""Sync local bundle to remote storage.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
dry_run: When True, compute the plan without uploading or deleting.
|
|
218
|
+
on_progress: Optional callback receiving an action and path for each step.
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
SyncResult summarizing the deployment.
|
|
222
|
+
"""
|
|
223
|
+
|
|
224
|
+
local_files = self.collect_local_files()
|
|
225
|
+
remote_files = self.collect_remote_files()
|
|
226
|
+
plan = self.compute_diff(local_files, remote_files, delete_orphaned=self.config.delete_orphaned)
|
|
227
|
+
|
|
228
|
+
uploaded: list[str] = []
|
|
229
|
+
deleted: list[str] = []
|
|
230
|
+
uploaded_bytes = 0
|
|
231
|
+
deleted_bytes = 0
|
|
232
|
+
|
|
233
|
+
if dry_run:
|
|
234
|
+
return SyncResult(
|
|
235
|
+
uploaded=plan.to_upload,
|
|
236
|
+
deleted=plan.to_delete,
|
|
237
|
+
uploaded_bytes=sum(local_files[p].size for p in plan.to_upload),
|
|
238
|
+
deleted_bytes=sum(remote_files[p].size for p in plan.to_delete),
|
|
239
|
+
dry_run=True,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
for path in plan.to_upload:
|
|
243
|
+
local_path = self.bundle_dir / path
|
|
244
|
+
remote_path = self._join_remote(path)
|
|
245
|
+
content_type: str | None = self.config.content_types.get(Path(path).suffix)
|
|
246
|
+
if content_type:
|
|
247
|
+
self.fs.put(local_path.as_posix(), remote_path, content_type=content_type)
|
|
248
|
+
else:
|
|
249
|
+
self.fs.put(local_path.as_posix(), remote_path)
|
|
250
|
+
uploaded.append(path)
|
|
251
|
+
uploaded_bytes += local_files[path].size
|
|
252
|
+
if on_progress:
|
|
253
|
+
on_progress("upload", path)
|
|
254
|
+
|
|
255
|
+
for path in plan.to_delete:
|
|
256
|
+
remote_path = self._join_remote(path)
|
|
257
|
+
self.fs.rm(remote_path)
|
|
258
|
+
deleted.append(path)
|
|
259
|
+
deleted_bytes += remote_files[path].size
|
|
260
|
+
if on_progress:
|
|
261
|
+
on_progress("delete", path)
|
|
262
|
+
|
|
263
|
+
return SyncResult(
|
|
264
|
+
uploaded=uploaded,
|
|
265
|
+
deleted=deleted,
|
|
266
|
+
uploaded_bytes=uploaded_bytes,
|
|
267
|
+
deleted_bytes=deleted_bytes,
|
|
268
|
+
dry_run=False,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
def _init_filesystem(
|
|
272
|
+
self, fs: "AbstractFileSystem | None", remote_path: str | None
|
|
273
|
+
) -> "tuple[AbstractFileSystem, str]":
|
|
274
|
+
if fs is not None and remote_path is not None:
|
|
275
|
+
return fs, remote_path
|
|
276
|
+
|
|
277
|
+
if fs is not None:
|
|
278
|
+
_, resolved_path = self._url_to_fs(self.config.storage_backend or "", **self.config.storage_options)
|
|
279
|
+
resolved_str = str(resolved_path)
|
|
280
|
+
return fs, remote_path or resolved_str
|
|
281
|
+
|
|
282
|
+
filesystem, resolved_path = self._url_to_fs(self.config.storage_backend or "", **self.config.storage_options)
|
|
283
|
+
resolved_str = str(resolved_path)
|
|
284
|
+
return filesystem, remote_path or resolved_str
|
|
285
|
+
|
|
286
|
+
def _paths_from_manifest(self, manifest_path: Path) -> set[str]:
|
|
287
|
+
"""Extract file paths referenced by manifest.json.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Set of file paths.
|
|
291
|
+
"""
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
manifest_data: Any = decode_json(manifest_path.read_text(encoding="utf-8"))
|
|
295
|
+
except (OSError, UnicodeDecodeError, SerializationException):
|
|
296
|
+
return set[str]()
|
|
297
|
+
|
|
298
|
+
paths: set[str] = set()
|
|
299
|
+
if isinstance(manifest_data, dict):
|
|
300
|
+
for value in manifest_data.values():
|
|
301
|
+
if not isinstance(value, dict):
|
|
302
|
+
continue
|
|
303
|
+
file_path = value.get("file")
|
|
304
|
+
if isinstance(file_path, str):
|
|
305
|
+
paths.add(file_path)
|
|
306
|
+
for field in ("css", "assets"):
|
|
307
|
+
for item in value.get(field, []) or []:
|
|
308
|
+
if isinstance(item, str):
|
|
309
|
+
paths.add(item)
|
|
310
|
+
return paths
|
|
311
|
+
|
|
312
|
+
def _relative_remote_path(self, full_path: str, base: str) -> str:
|
|
313
|
+
"""Compute remote path relative to deployment root.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
The remote path relative to the deployment root.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
if "://" in full_path:
|
|
320
|
+
full_path = full_path.split("://", 1)[1]
|
|
321
|
+
if "://" in base:
|
|
322
|
+
base = base.split("://", 1)[1]
|
|
323
|
+
full_path = full_path.lstrip("/")
|
|
324
|
+
base = base.lstrip("/")
|
|
325
|
+
if not base:
|
|
326
|
+
return full_path.lstrip("/")
|
|
327
|
+
cleaned = full_path.removeprefix(base)
|
|
328
|
+
return cleaned.lstrip("/")
|
|
329
|
+
|
|
330
|
+
def _join_remote(self, relative_path: str) -> str:
|
|
331
|
+
"""Join remote base and relative path.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The full remote path.
|
|
335
|
+
"""
|
|
336
|
+
|
|
337
|
+
if not self.remote_path:
|
|
338
|
+
return relative_path
|
|
339
|
+
return f"{self.remote_path.rstrip('/')}/{relative_path.lstrip('/')}"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def format_bytes(size: int) -> str:
|
|
343
|
+
"""Human friendly byte formatting.
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
The formatted byte size string.
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
units = ["B", "KB", "MB", "GB", "TB"]
|
|
350
|
+
value = float(size)
|
|
351
|
+
for unit in units:
|
|
352
|
+
if value < 1024 or unit == "TB":
|
|
353
|
+
return f"{value:.1f} {unit}"
|
|
354
|
+
value /= 1024
|
|
355
|
+
return f"{value:.1f} TB"
|