vuer-cli 0.0.3__py3-none-any.whl → 0.0.5__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.
- vuer_cli/add.py +69 -84
- vuer_cli/envs_publish.py +335 -309
- vuer_cli/envs_pull.py +177 -170
- vuer_cli/login.py +459 -0
- vuer_cli/main.py +52 -88
- vuer_cli/mcap_extractor.py +866 -0
- vuer_cli/remove.py +87 -95
- vuer_cli/scripts/demcap.py +171 -0
- vuer_cli/scripts/mcap_playback.py +661 -0
- vuer_cli/scripts/minimap.py +365 -0
- vuer_cli/scripts/ptc_utils.py +434 -0
- vuer_cli/scripts/viz_ptc_cams.py +613 -0
- vuer_cli/scripts/viz_ptc_proxie.py +483 -0
- vuer_cli/sync.py +314 -308
- vuer_cli/upgrade.py +121 -136
- vuer_cli/utils.py +11 -38
- {vuer_cli-0.0.3.dist-info → vuer_cli-0.0.5.dist-info}/METADATA +59 -6
- vuer_cli-0.0.5.dist-info/RECORD +22 -0
- vuer_cli-0.0.3.dist-info/RECORD +0 -14
- {vuer_cli-0.0.3.dist-info → vuer_cli-0.0.5.dist-info}/WHEEL +0 -0
- {vuer_cli-0.0.3.dist-info → vuer_cli-0.0.5.dist-info}/entry_points.txt +0 -0
- {vuer_cli-0.0.3.dist-info → vuer_cli-0.0.5.dist-info}/licenses/LICENSE +0 -0
vuer_cli/sync.py
CHANGED
|
@@ -9,342 +9,348 @@ from typing import Any, List
|
|
|
9
9
|
from .envs_publish import Hub
|
|
10
10
|
from .envs_pull import pull_from_registry
|
|
11
11
|
from .utils import (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
is_dry_run,
|
|
13
|
+
parse_env_spec,
|
|
14
|
+
print_error,
|
|
15
15
|
)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
@dataclass
|
|
19
19
|
class Sync:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Reads environment.json, validates dependencies with backend, and downloads
|
|
23
|
-
all environments and their transitive dependencies to vuer_environments/ directory.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
output: str = "vuer_environments" # Destination directory (default: vuer_environments)
|
|
27
|
-
timeout: int = 3000 # Request timeout in seconds
|
|
28
|
-
concurrent: bool = False # Download concurrently (future enhancement)
|
|
29
|
-
|
|
30
|
-
def __call__(self) -> int:
|
|
31
|
-
"""Execute sync command."""
|
|
32
|
-
try:
|
|
33
|
-
dry_run = is_dry_run()
|
|
34
|
-
|
|
35
|
-
if not dry_run:
|
|
36
|
-
if not Hub.url:
|
|
37
|
-
raise RuntimeError(
|
|
38
|
-
"Missing VUER_HUB_URL. Please set the VUER_HUB_URL environment variable "
|
|
39
|
-
"or pass --hub.url on the command line."
|
|
40
|
-
)
|
|
41
|
-
if not Hub.auth_token:
|
|
42
|
-
raise RuntimeError(
|
|
43
|
-
"Missing VUER_AUTH_TOKEN. Please set the VUER_AUTH_TOKEN environment "
|
|
44
|
-
"variable or pass --hub.auth-token on the command line."
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
# Step 1: Find and parse environment.json
|
|
48
|
-
print(
|
|
49
|
-
"[INFO] Looking for environment.json in current directory...")
|
|
50
|
-
current_dir = Path.cwd()
|
|
51
|
-
env_json_path = current_dir / "environment.json"
|
|
52
|
-
if not env_json_path.exists():
|
|
53
|
-
raise FileNotFoundError(
|
|
54
|
-
f"environment.json not found in {current_dir}. "
|
|
55
|
-
"Please run this command in a directory containing environment.json."
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
# Step 2: Parse dependencies
|
|
59
|
-
print(f"[INFO] Reading environment.json from {current_dir}...")
|
|
60
|
-
dependencies = parse_dependencies(env_json_path)
|
|
61
|
-
|
|
62
|
-
# Step 3: Prepare output directory and lockfile path (environments-lock.yaml is
|
|
63
|
-
# located alongside the vuer_environments directory)
|
|
64
|
-
output_dir = Path(self.output).expanduser().resolve()
|
|
65
|
-
output_dir.mkdir(parents=True, exist_ok=True)
|
|
66
|
-
lock_path = output_dir.parent / "environments-lock.yaml"
|
|
67
|
-
|
|
68
|
-
# Step 4: Handle empty dependencies case - still need to clean up Module/
|
|
69
|
-
if not dependencies:
|
|
70
|
-
print(
|
|
71
|
-
"[INFO] No environments to sync. environment.json has no dependencies.")
|
|
72
|
-
# Still reconcile: remove all environments if lockfile exists
|
|
73
|
-
previous_deps = read_environments_lock(lock_path)
|
|
74
|
-
desired_deps = [] # Empty list since no dependencies
|
|
75
|
-
write_environments_lock(lock_path, desired_deps)
|
|
76
|
-
print(f"[INFO] Wrote environments lock to {lock_path}")
|
|
77
|
-
|
|
78
|
-
# Remove all environments that are no longer needed
|
|
79
|
-
removed = remove_unneeded_env_dirs(output_dir, previous_deps, desired_deps)
|
|
80
|
-
if removed:
|
|
81
|
-
print(f"[INFO] Removed {removed} unused environment directories from {output_dir}")
|
|
82
|
-
else:
|
|
83
|
-
print("[INFO] No environments to remove from vuer_environments/")
|
|
84
|
-
return 0
|
|
85
|
-
|
|
86
|
-
print(f"[INFO] Found {len(dependencies)} dependencies to sync.")
|
|
87
|
-
|
|
88
|
-
# Step 5: Validate dependencies with backend
|
|
89
|
-
print("[INFO] Validating dependencies with backend...")
|
|
90
|
-
resolved_deps = validate_and_get_dependencies(dependencies, dry_run)
|
|
91
|
-
|
|
92
|
-
# Step 6: Collect all environments (direct + transitive)
|
|
93
|
-
all_environments = collect_all_environments(dependencies, resolved_deps)
|
|
94
|
-
print(
|
|
95
|
-
f"[INFO] Total environments to download: {len(all_environments)}")
|
|
96
|
-
|
|
97
|
-
# Step 7: Download all environments to Module/ directory
|
|
98
|
-
# Step 10/11: Reconcile dependencies.toml to match current resolution
|
|
99
|
-
# (If user removes deps from environment.json, we also remove them from Module/.)
|
|
100
|
-
previous_deps = read_environments_lock(lock_path)
|
|
101
|
-
desired_deps = dedupe_keep_order(all_environments)
|
|
102
|
-
write_environments_lock(lock_path, desired_deps)
|
|
103
|
-
print(f"[INFO] Wrote environments lock to {lock_path}")
|
|
104
|
-
|
|
105
|
-
print(f"[INFO] Downloading environments to {output_dir}...")
|
|
106
|
-
# Remove environments that are no longer needed.
|
|
107
|
-
removed = remove_unneeded_env_dirs(output_dir, previous_deps, desired_deps)
|
|
108
|
-
if removed:
|
|
109
|
-
print(f"[INFO] Removed {removed} unused environment directories from {output_dir}")
|
|
110
|
-
|
|
111
|
-
for env_spec in desired_deps:
|
|
112
|
-
# map to nested path: vuer_environments/<name>/<version>
|
|
113
|
-
name, version = parse_env_spec(env_spec)
|
|
114
|
-
env_path = output_dir / name / version
|
|
115
|
-
if env_path.exists():
|
|
116
|
-
print(f"[INFO] Skipping already-synced {env_spec}")
|
|
117
|
-
continue
|
|
118
|
-
print(f"[INFO] Downloading {env_spec}...")
|
|
119
|
-
_env_dir = pull_from_registry(
|
|
120
|
-
env_flag=env_spec,
|
|
121
|
-
output_dir=str(output_dir),
|
|
122
|
-
# Use filesystem-safe filename derived from name-version
|
|
123
|
-
filename=f"{name}-{version}.tgz",
|
|
124
|
-
version=None,
|
|
125
|
-
timeout=self.timeout,
|
|
126
|
-
skip_progress=False,
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
# Keep downloaded environment.json files intact per new policy.
|
|
130
|
-
|
|
131
|
-
print(
|
|
132
|
-
f"[SUCCESS] Synced {len(all_environments)} environments to {output_dir}")
|
|
133
|
-
return 0
|
|
134
|
-
|
|
135
|
-
except FileNotFoundError as e:
|
|
136
|
-
print_error(str(e))
|
|
137
|
-
return 1
|
|
138
|
-
except ValueError as e:
|
|
139
|
-
print_error(str(e))
|
|
140
|
-
return 1
|
|
141
|
-
except RuntimeError as e:
|
|
142
|
-
print_error(str(e))
|
|
143
|
-
return 1
|
|
144
|
-
except Exception as e:
|
|
145
|
-
print_error(f"Unexpected error: {e}")
|
|
146
|
-
return 1
|
|
20
|
+
"""Sync environments listed in environment.json dependencies (like npm install).
|
|
147
21
|
|
|
22
|
+
Reads environment.json, validates dependencies with backend, and downloads
|
|
23
|
+
all environments and their transitive dependencies to vuer_environments/ directory.
|
|
24
|
+
"""
|
|
148
25
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
26
|
+
output: str = (
|
|
27
|
+
"vuer_environments" # Destination directory (default: vuer_environments)
|
|
28
|
+
)
|
|
29
|
+
timeout: int = 3000 # Request timeout in seconds
|
|
30
|
+
concurrent: bool = False # Download concurrently (future enhancement)
|
|
154
31
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
"""
|
|
32
|
+
def __call__(self) -> int:
|
|
33
|
+
"""Execute sync command."""
|
|
158
34
|
try:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
35
|
+
dry_run = is_dry_run()
|
|
36
|
+
|
|
37
|
+
if not dry_run:
|
|
38
|
+
if not Hub.url:
|
|
39
|
+
raise RuntimeError(
|
|
40
|
+
"Missing VUER_HUB_URL. Please set the VUER_HUB_URL environment variable "
|
|
41
|
+
"or pass --hub.url on the command line."
|
|
42
|
+
)
|
|
43
|
+
# Try to get token from credentials file if not in environment
|
|
44
|
+
auth_token = Hub.get_auth_token()
|
|
45
|
+
if not auth_token:
|
|
46
|
+
raise RuntimeError(
|
|
47
|
+
"Missing VUER_AUTH_TOKEN. Please run 'vuer login' to authenticate, "
|
|
48
|
+
"or set the VUER_AUTH_TOKEN environment variable."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Step 1: Find and parse environment.json
|
|
52
|
+
print("[INFO] Looking for environment.json in current directory...")
|
|
53
|
+
current_dir = Path.cwd()
|
|
54
|
+
env_json_path = current_dir / "environment.json"
|
|
55
|
+
if not env_json_path.exists():
|
|
56
|
+
raise FileNotFoundError(
|
|
57
|
+
f"environment.json not found in {current_dir}. "
|
|
58
|
+
"Please run this command in a directory containing environment.json."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Step 2: Parse dependencies
|
|
62
|
+
print(f"[INFO] Reading environment.json from {current_dir}...")
|
|
63
|
+
dependencies = parse_dependencies(env_json_path)
|
|
64
|
+
|
|
65
|
+
# Step 3: Prepare output directory and lockfile path (environments-lock.yaml is
|
|
66
|
+
# located alongside the vuer_environments directory)
|
|
67
|
+
output_dir = Path(self.output).expanduser().resolve()
|
|
68
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
lock_path = output_dir.parent / "environments-lock.yaml"
|
|
70
|
+
|
|
71
|
+
# Step 4: Handle empty dependencies case - still need to clean up Module/
|
|
72
|
+
if not dependencies:
|
|
73
|
+
print("[INFO] No environments to sync. environment.json has no dependencies.")
|
|
74
|
+
# Still reconcile: remove all environments if lockfile exists
|
|
75
|
+
previous_deps = read_environments_lock(lock_path)
|
|
76
|
+
desired_deps = [] # Empty list since no dependencies
|
|
77
|
+
write_environments_lock(lock_path, desired_deps)
|
|
78
|
+
print(f"[INFO] Wrote environments lock to {lock_path}")
|
|
79
|
+
|
|
80
|
+
# Remove all environments that are no longer needed
|
|
81
|
+
removed = remove_unneeded_env_dirs(output_dir, previous_deps, desired_deps)
|
|
82
|
+
if removed:
|
|
83
|
+
print(
|
|
84
|
+
f"[INFO] Removed {removed} unused environment directories from {output_dir}"
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
print("[INFO] No environments to remove from vuer_environments/")
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
print(f"[INFO] Found {len(dependencies)} dependencies to sync.")
|
|
91
|
+
|
|
92
|
+
# Step 5: Validate dependencies with backend
|
|
93
|
+
print("[INFO] Validating dependencies with backend...")
|
|
94
|
+
resolved_deps = validate_and_get_dependencies(dependencies, dry_run)
|
|
95
|
+
|
|
96
|
+
# Step 6: Collect all environments (direct + transitive)
|
|
97
|
+
all_environments = collect_all_environments(dependencies, resolved_deps)
|
|
98
|
+
print(f"[INFO] Total environments to download: {len(all_environments)}")
|
|
99
|
+
|
|
100
|
+
# Step 7: Download all environments to Module/ directory
|
|
101
|
+
# Step 10/11: Reconcile dependencies.toml to match current resolution
|
|
102
|
+
# (If user removes deps from environment.json, we also remove them from Module/.)
|
|
103
|
+
previous_deps = read_environments_lock(lock_path)
|
|
104
|
+
desired_deps = dedupe_keep_order(all_environments)
|
|
105
|
+
write_environments_lock(lock_path, desired_deps)
|
|
106
|
+
print(f"[INFO] Wrote environments lock to {lock_path}")
|
|
107
|
+
|
|
108
|
+
print(f"[INFO] Downloading environments to {output_dir}...")
|
|
109
|
+
# Remove environments that are no longer needed.
|
|
110
|
+
removed = remove_unneeded_env_dirs(output_dir, previous_deps, desired_deps)
|
|
111
|
+
if removed:
|
|
112
|
+
print(
|
|
113
|
+
f"[INFO] Removed {removed} unused environment directories from {output_dir}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
for env_spec in desired_deps:
|
|
117
|
+
# map to nested path: vuer_environments/<name>/<version>
|
|
118
|
+
name, version = parse_env_spec(env_spec)
|
|
119
|
+
env_path = output_dir / name / version
|
|
120
|
+
if env_path.exists():
|
|
121
|
+
print(f"[INFO] Skipping already-synced {env_spec}")
|
|
122
|
+
continue
|
|
123
|
+
print(f"[INFO] Downloading {env_spec}...")
|
|
124
|
+
_env_dir = pull_from_registry(
|
|
125
|
+
env_flag=env_spec,
|
|
126
|
+
output_dir=str(output_dir),
|
|
127
|
+
# Use filesystem-safe filename derived from name-version
|
|
128
|
+
filename=f"{name}-{version}.tgz",
|
|
129
|
+
version=None,
|
|
130
|
+
timeout=self.timeout,
|
|
131
|
+
skip_progress=False,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Keep downloaded environment.json files intact per new policy.
|
|
135
|
+
|
|
136
|
+
print(f"[SUCCESS] Synced {len(all_environments)} environments to {output_dir}")
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
except FileNotFoundError as e:
|
|
140
|
+
print_error(str(e))
|
|
141
|
+
return 1
|
|
142
|
+
except ValueError as e:
|
|
143
|
+
print_error(str(e))
|
|
144
|
+
return 1
|
|
145
|
+
except RuntimeError as e:
|
|
146
|
+
print_error(str(e))
|
|
147
|
+
return 1
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print_error(f"Unexpected error: {e}")
|
|
150
|
+
return 1
|
|
174
151
|
|
|
175
|
-
return dependencies
|
|
176
152
|
|
|
153
|
+
# -- Helper functions --
|
|
177
154
|
|
|
178
|
-
def validate_and_get_dependencies(
|
|
179
|
-
dependencies: List[str], dry_run: bool
|
|
180
|
-
) -> List[str]:
|
|
181
|
-
"""Validate dependencies with backend and get transitive dependencies.
|
|
182
|
-
|
|
183
|
-
Args:
|
|
184
|
-
dependencies: List of dependency specs like ["name@version", ...]
|
|
185
|
-
dry_run: Whether to run in dry-run mode
|
|
186
|
-
|
|
187
|
-
Returns:
|
|
188
|
-
A flat list (not deduplicated) of all resolved environment specs,
|
|
189
|
-
as returned by the backend:
|
|
190
|
-
{"dependencies": ["some-dependency@1.2.5", "numpy@1.24.3", ...]}
|
|
191
|
-
|
|
192
|
-
Raises:
|
|
193
|
-
RuntimeError: If some environments don't exist
|
|
194
|
-
"""
|
|
195
|
-
if dry_run:
|
|
196
|
-
# Dry-run: return mock data
|
|
197
|
-
print("[INFO] (dry-run) Validating dependencies (simulated)...")
|
|
198
|
-
resolved: List[str] = []
|
|
199
|
-
for dep in dependencies:
|
|
200
|
-
# Include the requested deps, plus some mock transitive deps (with possible duplicates)
|
|
201
|
-
resolved.append(dep)
|
|
202
|
-
if "new-dependency" in dep:
|
|
203
|
-
continue
|
|
204
|
-
if "another-dependency" in dep:
|
|
205
|
-
resolved.extend(["react/^18.2.0", "react-dom/^18.2.0", "react/^18.2.0"])
|
|
206
|
-
else:
|
|
207
|
-
resolved.extend(["numpy/^1.24.0", "pandas/~2.0.0", "numpy/^1.24.0"])
|
|
208
|
-
return resolved
|
|
209
|
-
|
|
210
|
-
# Real API call
|
|
211
|
-
import requests
|
|
212
|
-
|
|
213
|
-
url = f"{Hub.url.rstrip('/')}/environments/dependencies"
|
|
214
|
-
headers = {
|
|
215
|
-
"Authorization": f"Bearer {Hub.auth_token}"} if Hub.auth_token else {}
|
|
216
|
-
headers["Content-Type"] = "application/json"
|
|
217
|
-
|
|
218
|
-
payload = {"name_versionId_list": dependencies}
|
|
219
155
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
156
|
+
def parse_dependencies(env_json_path: Path) -> List[str]:
|
|
157
|
+
"""Parse environment.json and extract dependencies list.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of dependency specs like ["some-dependency/^1.2.3", ...]
|
|
161
|
+
"""
|
|
162
|
+
try:
|
|
163
|
+
with env_json_path.open("r", encoding="utf-8") as f:
|
|
164
|
+
data = json.load(f)
|
|
165
|
+
except json.JSONDecodeError as e:
|
|
166
|
+
raise ValueError(f"Invalid environment.json: {e}") from e
|
|
167
|
+
|
|
168
|
+
deps_dict = data.get("dependencies", {})
|
|
169
|
+
if not isinstance(deps_dict, dict):
|
|
170
|
+
raise ValueError("environment.json 'dependencies' field must be an object")
|
|
171
|
+
|
|
172
|
+
dependencies = []
|
|
173
|
+
for name, version_spec in deps_dict.items():
|
|
174
|
+
if not isinstance(version_spec, str):
|
|
175
|
+
version_spec = str(version_spec)
|
|
176
|
+
dependencies.append(f"{name}/{version_spec}")
|
|
177
|
+
|
|
178
|
+
return dependencies
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def validate_and_get_dependencies(dependencies: List[str], dry_run: bool) -> List[str]:
|
|
182
|
+
"""Validate dependencies with backend and get transitive dependencies.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
dependencies: List of dependency specs like ["name@version", ...]
|
|
186
|
+
dry_run: Whether to run in dry-run mode
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
A flat list (not deduplicated) of all resolved environment specs,
|
|
190
|
+
as returned by the backend:
|
|
191
|
+
{"dependencies": ["some-dependency@1.2.5", "numpy@1.24.3", ...]}
|
|
192
|
+
|
|
193
|
+
Raises:
|
|
194
|
+
RuntimeError: If some environments don't exist
|
|
195
|
+
"""
|
|
196
|
+
if dry_run:
|
|
197
|
+
# Dry-run: return mock data
|
|
198
|
+
print("[INFO] (dry-run) Validating dependencies (simulated)...")
|
|
199
|
+
resolved: List[str] = []
|
|
200
|
+
for dep in dependencies:
|
|
201
|
+
# Include the requested deps, plus some mock transitive deps (with possible duplicates)
|
|
202
|
+
resolved.append(dep)
|
|
203
|
+
if "new-dependency" in dep:
|
|
204
|
+
continue
|
|
205
|
+
if "another-dependency" in dep:
|
|
206
|
+
resolved.extend(["react/^18.2.0", "react-dom/^18.2.0", "react/^18.2.0"])
|
|
207
|
+
else:
|
|
208
|
+
resolved.extend(["numpy/^1.24.0", "pandas/~2.0.0", "numpy/^1.24.0"])
|
|
209
|
+
return resolved
|
|
210
|
+
|
|
211
|
+
# Real API call
|
|
212
|
+
import requests
|
|
213
|
+
|
|
214
|
+
auth_token = Hub.get_auth_token()
|
|
215
|
+
url = f"{Hub.url.rstrip('/')}/environments/dependencies"
|
|
216
|
+
headers = {"Authorization": f"Bearer {auth_token}"} if auth_token else {}
|
|
217
|
+
headers["Content-Type"] = "application/json"
|
|
218
|
+
|
|
219
|
+
payload = {"name_versionId_list": dependencies}
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
response = requests.post(url, json=payload, headers=headers, timeout=300)
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
except requests.exceptions.HTTPError as e:
|
|
225
|
+
resp = getattr(e, "response", None)
|
|
226
|
+
status = resp.status_code if resp is not None else "unknown"
|
|
227
|
+
detail = _extract_backend_error(resp)
|
|
228
|
+
raise RuntimeError(f"Failed to validate dependencies ({status}): {detail}") from e
|
|
229
|
+
except requests.exceptions.RequestException as e:
|
|
230
|
+
raise RuntimeError(f"Failed to validate dependencies: {e}") from e
|
|
231
|
+
|
|
232
|
+
data = response.json()
|
|
233
|
+
|
|
234
|
+
# Check for errors (environments not found)
|
|
235
|
+
if "error" in data:
|
|
236
|
+
error_msg = data["error"]
|
|
237
|
+
raise RuntimeError(f"Dependency validation failed: {error_msg}")
|
|
238
|
+
|
|
239
|
+
deps = data.get("dependencies", None)
|
|
240
|
+
if deps is None:
|
|
241
|
+
raise ValueError("Invalid response format: missing 'dependencies' field")
|
|
242
|
+
if not isinstance(deps, list) or not all(isinstance(x, str) for x in deps):
|
|
243
|
+
raise ValueError(
|
|
244
|
+
"Invalid response format: 'dependencies' must be a list of strings"
|
|
245
|
+
)
|
|
246
|
+
return deps
|
|
247
247
|
|
|
248
248
|
|
|
249
249
|
def _extract_backend_error(response: Any) -> str:
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
250
|
+
"""Extract a concise error message from an HTTP response."""
|
|
251
|
+
if response is None:
|
|
252
|
+
return "No response body"
|
|
253
|
+
try:
|
|
254
|
+
payload = response.json()
|
|
255
|
+
if isinstance(payload, dict):
|
|
256
|
+
msg = payload.get("error") or payload.get("message")
|
|
257
|
+
if msg:
|
|
258
|
+
return str(msg)
|
|
259
|
+
return json.dumps(payload, ensure_ascii=False)
|
|
260
|
+
except Exception:
|
|
261
|
+
text = getattr(response, "text", "") or ""
|
|
262
|
+
text = text.strip()
|
|
263
|
+
return text if text else "Empty response body"
|
|
264
264
|
|
|
265
265
|
|
|
266
266
|
def write_environments_lock(path: Path, dependencies: List[str]) -> None:
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
267
|
+
"""Write the environments lock file (YAML) next to `vuer_environments`."""
|
|
268
|
+
deps = dedupe_keep_order(dependencies)
|
|
269
|
+
lines = [
|
|
270
|
+
"# This file is generated by `vuer sync`.",
|
|
271
|
+
"# DO NOT EDIT THIS FILE MANUALLY.",
|
|
272
|
+
"# This file is maintained by the system.",
|
|
273
|
+
"# It lists the resolved (deduplicated) environment dependencies.",
|
|
274
|
+
"",
|
|
275
|
+
"environments:",
|
|
276
|
+
]
|
|
277
|
+
for dep in deps:
|
|
278
|
+
escaped = dep.replace('"', '\\"')
|
|
279
|
+
lines.append(f' - "{escaped}"')
|
|
280
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
281
281
|
|
|
282
282
|
|
|
283
283
|
def dedupe_keep_order(items: List[str]) -> List[str]:
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
"""Deduplicate list while preserving first-seen order."""
|
|
285
|
+
return list(dict.fromkeys(items))
|
|
286
286
|
|
|
287
287
|
|
|
288
288
|
def read_environments_lock(path: Path) -> List[str]:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
289
|
+
"""Read environments from environments-lock.yaml.
|
|
290
|
+
|
|
291
|
+
The file format is expected to be a YAML list under `environments:`. This
|
|
292
|
+
reader implements a minimal parser that extracts entries starting with '-'.
|
|
293
|
+
"""
|
|
294
|
+
if not path.exists():
|
|
295
|
+
return []
|
|
296
|
+
text = path.read_text(encoding="utf-8")
|
|
297
|
+
lines = []
|
|
298
|
+
for raw in text.splitlines():
|
|
299
|
+
s = raw.strip()
|
|
300
|
+
if s.startswith("-"):
|
|
301
|
+
item = s[1:].strip()
|
|
302
|
+
# strip optional quotes
|
|
303
|
+
if (item.startswith('"') and item.endswith('"')) or (
|
|
304
|
+
item.startswith("'") and item.endswith("'")
|
|
305
|
+
):
|
|
306
|
+
item = item[1:-1]
|
|
307
|
+
lines.append(item)
|
|
308
|
+
return lines
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def remove_unneeded_env_dirs(
|
|
312
|
+
output_dir: Path, previous: List[str], desired: List[str]
|
|
313
|
+
) -> int:
|
|
314
|
+
"""Remove env directories that were previously synced but are no longer desired.
|
|
315
|
+
|
|
316
|
+
We only remove directories that correspond to entries in the old dependencies.toml,
|
|
317
|
+
to avoid deleting unrelated user folders under Module/.
|
|
318
|
+
"""
|
|
319
|
+
prev_set = set(previous)
|
|
320
|
+
desired_set = set(desired)
|
|
321
|
+
to_remove = [d for d in previous if d in prev_set and d not in desired_set]
|
|
322
|
+
removed = 0
|
|
323
|
+
for env_spec in to_remove:
|
|
324
|
+
name, version = parse_env_spec(env_spec)
|
|
325
|
+
env_dir = output_dir / name / version
|
|
326
|
+
if env_dir.exists() and env_dir.is_dir():
|
|
327
|
+
shutil.rmtree(env_dir, ignore_errors=True)
|
|
328
|
+
removed += 1
|
|
329
|
+
# If parent directory (name) is now empty, remove it as well.
|
|
330
|
+
parent_dir = output_dir / name
|
|
331
|
+
try:
|
|
332
|
+
if (
|
|
333
|
+
parent_dir.exists() and parent_dir.is_dir() and not any(parent_dir.iterdir())
|
|
334
|
+
):
|
|
335
|
+
shutil.rmtree(parent_dir, ignore_errors=True)
|
|
336
|
+
# Do not increment removed again — count represents removed version dirs.
|
|
337
|
+
except Exception:
|
|
338
|
+
# If listing or removal fails for any reason, ignore to avoid breaking sync.
|
|
339
|
+
pass
|
|
340
|
+
return removed
|
|
335
341
|
|
|
336
342
|
|
|
337
343
|
def collect_all_environments(
|
|
338
|
-
|
|
344
|
+
direct_deps: List[str], dependencies_data: List[str]
|
|
339
345
|
) -> List[str]:
|
|
340
|
-
|
|
346
|
+
"""Collect all environments (direct + transitive) and deduplicate.
|
|
341
347
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
Args:
|
|
349
|
+
direct_deps: Direct dependencies from environment.json
|
|
350
|
+
dependencies_data: Flat list of deps returned by backend (may include duplicates)
|
|
345
351
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
352
|
+
Returns:
|
|
353
|
+
List of unique environment specs (direct deps first)
|
|
354
|
+
"""
|
|
355
|
+
# Ensure direct deps stay first, then append backend deps in their given order.
|
|
356
|
+
return dedupe_keep_order(list(direct_deps) + list(dependencies_data))
|