lumen-app 0.4.2__py3-none-any.whl → 0.4.3__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.
- lumen_app/utils/installation/micromamba_installer.py +66 -21
- lumen_app/web/api/config.py +85 -24
- lumen_app/web/api/install.py +124 -0
- lumen_app/web/api/tests/__init__.py +1 -0
- lumen_app/web/api/tests/test_install.py +148 -0
- lumen_app/web/core/state.py +12 -0
- lumen_app/web/models/install.py +21 -0
- lumen_app/web/static/assets/index-CMWOChkS.css +1 -0
- lumen_app/web/static/assets/index-D3p85GRV.js +56 -0
- lumen_app/web/static/index.html +2 -2
- lumen_app/web/websockets/logs.py +0 -1
- {lumen_app-0.4.2.dist-info → lumen_app-0.4.3.dist-info}/METADATA +1 -1
- {lumen_app-0.4.2.dist-info → lumen_app-0.4.3.dist-info}/RECORD +16 -14
- lumen_app/web/static/assets/index-CGuhGHC9.css +0 -1
- lumen_app/web/static/assets/index-DN6HmxWS.js +0 -56
- {lumen_app-0.4.2.dist-info → lumen_app-0.4.3.dist-info}/WHEEL +0 -0
- {lumen_app-0.4.2.dist-info → lumen_app-0.4.3.dist-info}/entry_points.txt +0 -0
- {lumen_app-0.4.2.dist-info → lumen_app-0.4.3.dist-info}/top_level.txt +0 -0
|
@@ -276,7 +276,7 @@ class MicromambaInstaller:
|
|
|
276
276
|
return install_dir / "bin" / "micromamba"
|
|
277
277
|
|
|
278
278
|
def _install_windows(self, install_dir: Path, dry_run: bool) -> tuple[bool, str]:
|
|
279
|
-
"""Install micromamba on Windows.
|
|
279
|
+
"""Install micromamba on Windows using direct binary download.
|
|
280
280
|
|
|
281
281
|
Args:
|
|
282
282
|
install_dir: Installation directory
|
|
@@ -287,30 +287,75 @@ class MicromambaInstaller:
|
|
|
287
287
|
"""
|
|
288
288
|
logger.debug("[MicromambaInstaller] Installing on Windows")
|
|
289
289
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
f"Invoke-WebRequest -Uri https://micro.mamba.pm/install.ps1 -OutFile {install_script}; "
|
|
295
|
-
f"& {install_script} -prefix {install_dir} -batch",
|
|
296
|
-
]
|
|
290
|
+
# Get executable path
|
|
291
|
+
exe_path = self._get_executable_path(install_dir)
|
|
292
|
+
bin_dir = exe_path.parent
|
|
293
|
+
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
297
294
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return True, f"Would run: {' '.join(cmd)}"
|
|
295
|
+
# Build download URL (Windows is always win-64)
|
|
296
|
+
base_url = "https://github.com/mamba-org/micromamba-releases/releases/latest/download/micromamba-win-64"
|
|
301
297
|
|
|
302
|
-
|
|
303
|
-
|
|
298
|
+
# Get URLs with mirror fallback
|
|
299
|
+
download_urls = self.mirror_selector.get_micromamba_urls(base_url, self.region)
|
|
304
300
|
|
|
305
|
-
|
|
306
|
-
return True, f"Installed to {self._get_executable_path(install_dir)}"
|
|
307
|
-
else:
|
|
308
|
-
return False, f"Installation failed: {result.stderr}"
|
|
301
|
+
logger.debug(f"[MicromambaInstaller] Platform: win-64, URLs: {download_urls}")
|
|
309
302
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
303
|
+
# Try each URL
|
|
304
|
+
for download_url in download_urls:
|
|
305
|
+
logger.debug(f"[MicromambaInstaller] Trying URL: {download_url}")
|
|
306
|
+
|
|
307
|
+
# Method 1: Try using curl.exe (available on Windows 10+)
|
|
308
|
+
download_cmd = ["curl", "-fsSL", download_url, "-o", str(exe_path)]
|
|
309
|
+
|
|
310
|
+
if dry_run:
|
|
311
|
+
logger.info(
|
|
312
|
+
f"[MicromambaInstaller] Would run: {' '.join(download_cmd)}"
|
|
313
|
+
)
|
|
314
|
+
return True, f"Would download from {download_url}"
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
# Try curl first
|
|
318
|
+
logger.info("[MicromambaInstaller] Downloading micromamba with curl...")
|
|
319
|
+
download_result = subprocess.run(
|
|
320
|
+
download_cmd, capture_output=True, text=True, timeout=120
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
if download_result.returncode != 0:
|
|
324
|
+
# Fallback to PowerShell
|
|
325
|
+
logger.debug(
|
|
326
|
+
"[MicromambaInstaller] curl failed, trying PowerShell..."
|
|
327
|
+
)
|
|
328
|
+
ps_cmd = [
|
|
329
|
+
"powershell",
|
|
330
|
+
"-Command",
|
|
331
|
+
f"Invoke-WebRequest -Uri '{download_url}' -OutFile '{exe_path}' -UseBasicParsing",
|
|
332
|
+
]
|
|
333
|
+
download_result = subprocess.run(
|
|
334
|
+
ps_cmd, capture_output=True, text=True, timeout=120
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if download_result.returncode != 0:
|
|
338
|
+
logger.warning(
|
|
339
|
+
f"[MicromambaInstaller] PowerShell download failed: {download_result.stderr}"
|
|
340
|
+
)
|
|
341
|
+
continue
|
|
342
|
+
|
|
343
|
+
# Configure conda-forge channels
|
|
344
|
+
self._configure_channels(exe_path)
|
|
345
|
+
|
|
346
|
+
logger.info(f"[MicromambaInstaller] Successfully installed: {exe_path}")
|
|
347
|
+
return True, f"Installed to {exe_path}"
|
|
348
|
+
|
|
349
|
+
except subprocess.TimeoutExpired:
|
|
350
|
+
logger.warning("[MicromambaInstaller] Download timed out")
|
|
351
|
+
continue
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.warning(
|
|
354
|
+
f"[MicromambaInstaller] Install error: {type(e).__name__}: {e}"
|
|
355
|
+
)
|
|
356
|
+
continue
|
|
357
|
+
|
|
358
|
+
return False, "Failed to download from all sources"
|
|
314
359
|
|
|
315
360
|
def _install_unix(
|
|
316
361
|
self, install_dir: Path, exe_path: Path, dry_run: bool
|
lumen_app/web/api/config.py
CHANGED
|
@@ -5,8 +5,9 @@ from __future__ import annotations
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Literal
|
|
7
7
|
|
|
8
|
-
import yaml
|
|
9
8
|
from fastapi import APIRouter, HTTPException
|
|
9
|
+
from lumen_resources.exceptions import ConfigError
|
|
10
|
+
from lumen_resources.lumen_config_validator import load_and_validate_config
|
|
10
11
|
|
|
11
12
|
from lumen_app.core.config import Config
|
|
12
13
|
from lumen_app.core.installer import CoreInstaller
|
|
@@ -105,27 +106,40 @@ async def generate_config(request: ConfigRequest):
|
|
|
105
106
|
@router.get("/current")
|
|
106
107
|
async def get_current_config():
|
|
107
108
|
"""Get the currently loaded configuration."""
|
|
108
|
-
|
|
109
|
+
lumen_config = app_state.get_lumen_config()
|
|
109
110
|
|
|
110
|
-
if not
|
|
111
|
+
if not lumen_config:
|
|
111
112
|
return {"loaded": False, "message": "No configuration loaded"}
|
|
112
113
|
|
|
114
|
+
# Extract first enabled service's backend settings for device info
|
|
115
|
+
device_info = None
|
|
116
|
+
for service_config in lumen_config.services.values():
|
|
117
|
+
if service_config.enabled and service_config.backend_settings:
|
|
118
|
+
backend = service_config.backend_settings
|
|
119
|
+
# Get runtime from first model
|
|
120
|
+
runtime = "onnx"
|
|
121
|
+
for model_cfg in service_config.models.values():
|
|
122
|
+
runtime = model_cfg.runtime.value
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
device_info = {
|
|
126
|
+
"runtime": runtime,
|
|
127
|
+
"batch_size": backend.batch_size or 1,
|
|
128
|
+
"precision": "fp32",
|
|
129
|
+
"rknn_device": None,
|
|
130
|
+
"onnx_providers": backend.onnx_providers or [],
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
|
|
113
134
|
return {
|
|
114
135
|
"loaded": True,
|
|
115
|
-
"cache_dir":
|
|
116
|
-
"region":
|
|
117
|
-
"port":
|
|
118
|
-
"service_name":
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
"precision": device_config.precision,
|
|
123
|
-
"rknn_device": device_config.rknn_device,
|
|
124
|
-
"onnx_providers": [
|
|
125
|
-
p if isinstance(p, str) else p[0]
|
|
126
|
-
for p in (device_config.onnx_providers or [])
|
|
127
|
-
],
|
|
128
|
-
},
|
|
136
|
+
"cache_dir": lumen_config.metadata.cache_dir,
|
|
137
|
+
"region": lumen_config.metadata.region.value,
|
|
138
|
+
"port": lumen_config.server.port,
|
|
139
|
+
"service_name": lumen_config.server.mdns.service_name
|
|
140
|
+
if lumen_config.server.mdns
|
|
141
|
+
else "lumen-server",
|
|
142
|
+
"device": device_info,
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
|
|
@@ -207,23 +221,70 @@ async def validate_path(request: dict):
|
|
|
207
221
|
|
|
208
222
|
@router.post("/load")
|
|
209
223
|
async def load_config(config_path: str):
|
|
210
|
-
"""Load a configuration from file."""
|
|
224
|
+
"""Load and validate a configuration from file, then set it in app_state."""
|
|
211
225
|
try:
|
|
212
|
-
path = Path(config_path).expanduser()
|
|
226
|
+
path = Path(config_path).expanduser().resolve()
|
|
227
|
+
|
|
213
228
|
if not path.exists():
|
|
214
229
|
raise HTTPException(
|
|
215
230
|
status_code=404, detail=f"Config file not found: {config_path}"
|
|
216
231
|
)
|
|
217
232
|
|
|
218
|
-
|
|
219
|
-
|
|
233
|
+
# Validate and load using lumen-resources
|
|
234
|
+
try:
|
|
235
|
+
lumen_config = load_and_validate_config(path)
|
|
236
|
+
except ConfigError as e:
|
|
237
|
+
logger.error(f"Configuration validation failed: {e}")
|
|
238
|
+
raise HTTPException(
|
|
239
|
+
status_code=400, detail=f"Invalid configuration: {str(e)}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Store LumenConfig directly in app_state
|
|
243
|
+
app_state.set_lumen_config(lumen_config)
|
|
220
244
|
|
|
221
|
-
|
|
245
|
+
logger.info(f"Configuration loaded successfully from {path}")
|
|
222
246
|
|
|
223
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
"loaded": True,
|
|
249
|
+
"config_path": str(path),
|
|
250
|
+
"cache_dir": lumen_config.metadata.cache_dir,
|
|
251
|
+
"region": lumen_config.metadata.region.value,
|
|
252
|
+
"port": lumen_config.server.port,
|
|
253
|
+
"service_name": lumen_config.server.mdns.service_name
|
|
254
|
+
if lumen_config.server.mdns
|
|
255
|
+
else "lumen-server",
|
|
256
|
+
}
|
|
224
257
|
|
|
225
258
|
except HTTPException:
|
|
226
259
|
raise
|
|
227
260
|
except Exception as e:
|
|
228
|
-
logger.error(f"Failed to load config: {e}")
|
|
261
|
+
logger.error(f"Failed to load config: {e}", exc_info=True)
|
|
229
262
|
raise HTTPException(status_code=500, detail=f"Failed to load config: {str(e)}")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@router.get("/yaml")
|
|
266
|
+
async def get_config_yaml():
|
|
267
|
+
"""Get the current configuration as raw YAML string."""
|
|
268
|
+
lumen_config = app_state.get_lumen_config()
|
|
269
|
+
|
|
270
|
+
if not lumen_config:
|
|
271
|
+
raise HTTPException(status_code=404, detail="No configuration loaded")
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
import yaml
|
|
275
|
+
|
|
276
|
+
# Convert Pydantic model to dict and then to YAML
|
|
277
|
+
config_dict = lumen_config.model_dump(mode="json")
|
|
278
|
+
yaml_str = yaml.dump(config_dict, default_flow_style=False, sort_keys=False)
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"loaded": True,
|
|
282
|
+
"yaml": yaml_str,
|
|
283
|
+
"cache_dir": lumen_config.metadata.cache_dir,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.error(f"Failed to serialize config to YAML: {e}", exc_info=True)
|
|
288
|
+
raise HTTPException(
|
|
289
|
+
status_code=500, detail=f"Failed to serialize config: {str(e)}"
|
|
290
|
+
)
|
lumen_app/web/api/install.py
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import os
|
|
6
7
|
import time
|
|
7
8
|
import uuid
|
|
8
9
|
from pathlib import Path
|
|
@@ -20,18 +21,141 @@ from lumen_app.utils.logger import get_logger
|
|
|
20
21
|
from lumen_app.utils.preset_registry import PresetRegistry
|
|
21
22
|
from lumen_app.web.core.state import app_state
|
|
22
23
|
from lumen_app.web.models.install import (
|
|
24
|
+
CheckInstallationPathResponse,
|
|
23
25
|
InstallLogsResponse,
|
|
24
26
|
InstallSetupRequest,
|
|
25
27
|
InstallStatusResponse,
|
|
26
28
|
InstallStep,
|
|
27
29
|
InstallTaskListResponse,
|
|
28
30
|
InstallTaskResponse,
|
|
31
|
+
ServiceStatus,
|
|
29
32
|
)
|
|
30
33
|
|
|
31
34
|
logger = get_logger("lumen.web.api.install")
|
|
32
35
|
router = APIRouter()
|
|
33
36
|
|
|
34
37
|
|
|
38
|
+
def _check_installation_components(cache_dir: Path) -> dict[str, bool]:
|
|
39
|
+
"""Check which installation components exist at the given path.
|
|
40
|
+
|
|
41
|
+
Returns a dict with keys: micromamba, environment, config, drivers.
|
|
42
|
+
"""
|
|
43
|
+
from lumen_app.utils.installation import MicromambaInstaller, MicromambaStatus
|
|
44
|
+
|
|
45
|
+
components = {
|
|
46
|
+
"micromamba": False,
|
|
47
|
+
"environment": False,
|
|
48
|
+
"config": False,
|
|
49
|
+
"drivers": False,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Check micromamba
|
|
53
|
+
micromamba_installer = MicromambaInstaller(cache_dir)
|
|
54
|
+
micromamba_result = micromamba_installer.check()
|
|
55
|
+
components["micromamba"] = micromamba_result.status == MicromambaStatus.INSTALLED
|
|
56
|
+
|
|
57
|
+
# Check environment exists
|
|
58
|
+
# Micromamba installs to cache_dir/micromamba/, so envs are at cache_dir/micromamba/envs/
|
|
59
|
+
envs_dir = cache_dir / "micromamba" / "envs"
|
|
60
|
+
if envs_dir.exists():
|
|
61
|
+
# Look for any conda environment directories
|
|
62
|
+
components["environment"] = (
|
|
63
|
+
any(
|
|
64
|
+
(envs_dir / d).is_dir()
|
|
65
|
+
for d in os.listdir(envs_dir)
|
|
66
|
+
if (envs_dir / d).is_dir()
|
|
67
|
+
)
|
|
68
|
+
if os.path.exists(envs_dir)
|
|
69
|
+
else False
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Check config file
|
|
73
|
+
config_path = cache_dir / "lumen-config.yaml"
|
|
74
|
+
components["config"] = config_path.exists()
|
|
75
|
+
|
|
76
|
+
# Check drivers (placeholder - in a full implementation, this would check
|
|
77
|
+
# if required drivers are installed for the selected preset)
|
|
78
|
+
# For now, assume drivers are OK if micromamba and environment exist
|
|
79
|
+
components["drivers"] = components["micromamba"] and components["environment"]
|
|
80
|
+
|
|
81
|
+
return components
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@router.get("/check-path", response_model=CheckInstallationPathResponse)
|
|
85
|
+
async def check_installation_path(path: str) -> CheckInstallationPathResponse:
|
|
86
|
+
"""Check if an existing installation exists at the given path.
|
|
87
|
+
|
|
88
|
+
Returns detailed information about what components are installed,
|
|
89
|
+
whether the installation is complete, and the recommended action.
|
|
90
|
+
"""
|
|
91
|
+
import os
|
|
92
|
+
|
|
93
|
+
if not path or not path.strip():
|
|
94
|
+
return CheckInstallationPathResponse(
|
|
95
|
+
has_existing_service=False,
|
|
96
|
+
ready_to_start=False,
|
|
97
|
+
recommended_action="configure_new",
|
|
98
|
+
message="路径为空",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Expand and resolve path
|
|
103
|
+
cache_dir = Path(path).expanduser().resolve()
|
|
104
|
+
|
|
105
|
+
# Check if path is valid
|
|
106
|
+
if not str(cache_dir).startswith(("/", "~")) and os.name != "nt":
|
|
107
|
+
# On non-Windows, paths must be absolute
|
|
108
|
+
if not cache_dir.is_absolute():
|
|
109
|
+
return CheckInstallationPathResponse(
|
|
110
|
+
has_existing_service=False,
|
|
111
|
+
ready_to_start=False,
|
|
112
|
+
recommended_action="configure_new",
|
|
113
|
+
message="请使用绝对路径",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Check components
|
|
117
|
+
components = _check_installation_components(cache_dir)
|
|
118
|
+
|
|
119
|
+
# Determine if there's an existing service
|
|
120
|
+
has_existing_service = components["micromamba"] and components["environment"]
|
|
121
|
+
|
|
122
|
+
# Determine if it's ready to start
|
|
123
|
+
# For MVP: need micromamba + environment + config
|
|
124
|
+
ready_to_start = (
|
|
125
|
+
components["micromamba"]
|
|
126
|
+
and components["environment"]
|
|
127
|
+
and components["config"]
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Determine recommended action
|
|
131
|
+
if ready_to_start:
|
|
132
|
+
recommended_action = "start_existing"
|
|
133
|
+
message = "检测到完整现有安装,可以直接启动服务"
|
|
134
|
+
elif has_existing_service:
|
|
135
|
+
recommended_action = "configure_new"
|
|
136
|
+
message = "检测到部分安装,建议重新配置"
|
|
137
|
+
else:
|
|
138
|
+
recommended_action = "configure_new"
|
|
139
|
+
message = "未检测到现有安装,将进行全新配置"
|
|
140
|
+
|
|
141
|
+
return CheckInstallationPathResponse(
|
|
142
|
+
has_existing_service=has_existing_service,
|
|
143
|
+
service_status=ServiceStatus(**components),
|
|
144
|
+
ready_to_start=ready_to_start,
|
|
145
|
+
recommended_action=recommended_action,
|
|
146
|
+
message=message,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Error checking installation path: {e}", exc_info=True)
|
|
151
|
+
return CheckInstallationPathResponse(
|
|
152
|
+
has_existing_service=False,
|
|
153
|
+
ready_to_start=False,
|
|
154
|
+
recommended_action="configure_new",
|
|
155
|
+
message=f"检查路径时出错: {str(e)}",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
35
159
|
@router.get("/status", response_model=InstallStatusResponse)
|
|
36
160
|
async def get_install_status():
|
|
37
161
|
"""Get current installation status of the system."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Tests for web API endpoints
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Tests for install API endpoints."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from fastapi.testclient import TestClient
|
|
7
|
+
|
|
8
|
+
from lumen_app.web.api.install import _check_installation_components
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestCheckInstallationComponents:
|
|
12
|
+
"""Tests for _check_installation_components function."""
|
|
13
|
+
|
|
14
|
+
def test_empty_directory_returns_no_components(self, tmp_path):
|
|
15
|
+
"""Test that an empty directory returns all components as False."""
|
|
16
|
+
components = _check_installation_components(tmp_path)
|
|
17
|
+
|
|
18
|
+
assert components["micromamba"] is False
|
|
19
|
+
assert components["environment"] is False
|
|
20
|
+
assert components["config"] is False
|
|
21
|
+
assert components["drivers"] is False
|
|
22
|
+
|
|
23
|
+
def test_micromamba_detected(self, tmp_path):
|
|
24
|
+
"""Test that micromamba is detected when present."""
|
|
25
|
+
# Create a mock micromamba executable
|
|
26
|
+
micromamba_dir = tmp_path / "micromamba" / "bin"
|
|
27
|
+
micromamba_dir.mkdir(parents=True)
|
|
28
|
+
micromamba_exe = micromamba_dir / "micromamba"
|
|
29
|
+
micromamba_exe.write_text("#!/bin/sh\necho 'micromamba'")
|
|
30
|
+
micromamba_exe.chmod(0o755)
|
|
31
|
+
|
|
32
|
+
with patch(
|
|
33
|
+
"lumen_app.utils.installation.MicromambaInstaller"
|
|
34
|
+
) as mock_installer_class:
|
|
35
|
+
mock_installer = MagicMock()
|
|
36
|
+
mock_result = MagicMock()
|
|
37
|
+
mock_result.status = "INSTALLED"
|
|
38
|
+
mock_result.executable_path = str(micromamba_exe)
|
|
39
|
+
mock_installer.check.return_value = mock_result
|
|
40
|
+
mock_installer_class.return_value = mock_installer
|
|
41
|
+
|
|
42
|
+
components = _check_installation_components(tmp_path)
|
|
43
|
+
|
|
44
|
+
assert components["micromamba"] is True
|
|
45
|
+
|
|
46
|
+
def test_environment_detected(self, tmp_path):
|
|
47
|
+
"""Test that environment is detected when present."""
|
|
48
|
+
env_dir = tmp_path / "envs" / "lumen_env"
|
|
49
|
+
env_dir.mkdir(parents=True)
|
|
50
|
+
(env_dir / "bin").mkdir()
|
|
51
|
+
|
|
52
|
+
components = _check_installation_components(tmp_path)
|
|
53
|
+
|
|
54
|
+
assert components["environment"] is True
|
|
55
|
+
|
|
56
|
+
def test_config_detected(self, tmp_path):
|
|
57
|
+
"""Test that config is detected when present."""
|
|
58
|
+
config_file = tmp_path / "lumen-config.yaml"
|
|
59
|
+
config_file.write_text("model: test\n")
|
|
60
|
+
|
|
61
|
+
components = _check_installation_components(tmp_path)
|
|
62
|
+
|
|
63
|
+
assert components["config"] is True
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestCheckInstallationPathEndpoint:
|
|
67
|
+
"""Tests for the /check-path endpoint."""
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def client(self):
|
|
71
|
+
"""Create a test client."""
|
|
72
|
+
from lumen_app.web.main import create_app
|
|
73
|
+
|
|
74
|
+
app = create_app()
|
|
75
|
+
return TestClient(app)
|
|
76
|
+
|
|
77
|
+
def test_empty_path_returns_configure_new(self, client):
|
|
78
|
+
"""Test that empty path returns configure_new action."""
|
|
79
|
+
response = client.get("/api/v1/install/check-path?path=")
|
|
80
|
+
assert response.status_code == 200
|
|
81
|
+
|
|
82
|
+
data = response.json()
|
|
83
|
+
assert data["recommended_action"] == "configure_new"
|
|
84
|
+
assert data["has_existing_service"] is False
|
|
85
|
+
assert data["ready_to_start"] is False
|
|
86
|
+
|
|
87
|
+
def test_no_path_returns_configure_new(self, client):
|
|
88
|
+
"""Test that missing path query param returns configure_new."""
|
|
89
|
+
response = client.get("/api/v1/install/check-path")
|
|
90
|
+
assert response.status_code == 200
|
|
91
|
+
|
|
92
|
+
data = response.json()
|
|
93
|
+
assert data["recommended_action"] == "configure_new"
|
|
94
|
+
|
|
95
|
+
def test_complete_installation_detected(self, client, tmp_path):
|
|
96
|
+
"""Test that a complete installation is detected."""
|
|
97
|
+
# Create complete installation
|
|
98
|
+
(tmp_path / "envs" / "lumen_env" / "bin").mkdir(parents=True)
|
|
99
|
+
(tmp_path / "lumen-config.yaml").write_text("model: test\n")
|
|
100
|
+
|
|
101
|
+
# Create mock micromamba
|
|
102
|
+
micromamba_exe = tmp_path / "micromamba" / "bin" / "micromamba"
|
|
103
|
+
micromamba_exe.parent.mkdir(parents=True)
|
|
104
|
+
micromamba_exe.write_text("#!/bin/sh\necho 'micromamba'")
|
|
105
|
+
micromamba_exe.chmod(0o755)
|
|
106
|
+
|
|
107
|
+
with patch("lumen_app.utils.installation.MicromambaInstaller") as mock_class:
|
|
108
|
+
mock_installer = MagicMock()
|
|
109
|
+
mock_result = MagicMock()
|
|
110
|
+
mock_result.status = "INSTALLED"
|
|
111
|
+
mock_result.executable_path = str(micromamba_exe)
|
|
112
|
+
mock_installer.check.return_value = mock_result
|
|
113
|
+
mock_class.return_value = mock_installer
|
|
114
|
+
|
|
115
|
+
response = client.get(f"/api/v1/install/check-path?path={tmp_path}")
|
|
116
|
+
assert response.status_code == 200
|
|
117
|
+
|
|
118
|
+
data = response.json()
|
|
119
|
+
assert data["recommended_action"] == "start_existing"
|
|
120
|
+
assert data["has_existing_service"] is True
|
|
121
|
+
assert data["ready_to_start"] is True
|
|
122
|
+
assert data["service_status"]["micromamba"] is True
|
|
123
|
+
assert data["service_status"]["environment"] is True
|
|
124
|
+
assert data["service_status"]["config"] is True
|
|
125
|
+
|
|
126
|
+
def test_partial_installation_detected(self, client, tmp_path):
|
|
127
|
+
"""Test that a partial installation (only micromamba) is detected."""
|
|
128
|
+
# Create only micromamba executable (no env or config)
|
|
129
|
+
micromamba_exe = tmp_path / "micromamba" / "bin" / "micromamba"
|
|
130
|
+
micromamba_exe.parent.mkdir(parents=True)
|
|
131
|
+
micromamba_exe.write_text("#!/bin/sh\necho 'micromamba'")
|
|
132
|
+
micromamba_exe.chmod(0o755)
|
|
133
|
+
|
|
134
|
+
with patch("lumen_app.utils.installation.MicromambaInstaller") as mock_class:
|
|
135
|
+
mock_installer = MagicMock()
|
|
136
|
+
mock_result = MagicMock()
|
|
137
|
+
mock_result.status = "INSTALLED"
|
|
138
|
+
mock_result.executable_path = str(micromamba_exe)
|
|
139
|
+
mock_installer.check.return_value = mock_result
|
|
140
|
+
mock_class.return_value = mock_installer
|
|
141
|
+
|
|
142
|
+
response = client.get(f"/api/v1/install/check-path?path={tmp_path}")
|
|
143
|
+
assert response.status_code == 200
|
|
144
|
+
|
|
145
|
+
data = response.json()
|
|
146
|
+
assert data["recommended_action"] == "configure_new"
|
|
147
|
+
assert data["has_existing_service"] is True # Has micromamba
|
|
148
|
+
assert data["ready_to_start"] is False # But not complete
|
lumen_app/web/core/state.py
CHANGED
|
@@ -6,6 +6,8 @@ import asyncio
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from lumen_resources.lumen_config import LumenConfig
|
|
10
|
+
|
|
9
11
|
from lumen_app.core.config import Config, DeviceConfig
|
|
10
12
|
from lumen_app.utils.env_checker import EnvironmentReport
|
|
11
13
|
from lumen_app.utils.logger import get_logger
|
|
@@ -50,6 +52,7 @@ class AppState:
|
|
|
50
52
|
self._initialized = False
|
|
51
53
|
self.current_config: Config | None = None
|
|
52
54
|
self.device_config: DeviceConfig | None = None
|
|
55
|
+
self.lumen_config: LumenConfig | None = None
|
|
53
56
|
self.environment_report: EnvironmentReport | None = None
|
|
54
57
|
self.server_status = ServerStatus()
|
|
55
58
|
self.app_service: AppService | None = None
|
|
@@ -120,6 +123,15 @@ class AppState:
|
|
|
120
123
|
"""Get current configuration."""
|
|
121
124
|
return self.current_config, self.device_config
|
|
122
125
|
|
|
126
|
+
def set_lumen_config(self, config: LumenConfig):
|
|
127
|
+
"""Set LumenConfig directly."""
|
|
128
|
+
self.lumen_config = config
|
|
129
|
+
logger.info(f"LumenConfig loaded: {config.metadata.cache_dir}")
|
|
130
|
+
|
|
131
|
+
def get_lumen_config(self) -> LumenConfig | None:
|
|
132
|
+
"""Get current LumenConfig."""
|
|
133
|
+
return self.lumen_config
|
|
134
|
+
|
|
123
135
|
# Task management
|
|
124
136
|
async def create_task(self, task_type: str) -> InstallationTask:
|
|
125
137
|
"""Create a new installation task."""
|
lumen_app/web/models/install.py
CHANGED
|
@@ -7,6 +7,27 @@ from typing import Literal
|
|
|
7
7
|
from pydantic import BaseModel, Field
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class ServiceStatus(BaseModel):
|
|
11
|
+
"""Service component status."""
|
|
12
|
+
|
|
13
|
+
micromamba: bool = False
|
|
14
|
+
environment: bool = False
|
|
15
|
+
config: bool = False
|
|
16
|
+
drivers: bool = False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CheckInstallationPathResponse(BaseModel):
|
|
20
|
+
"""Response for checking installation path."""
|
|
21
|
+
|
|
22
|
+
has_existing_service: bool = False
|
|
23
|
+
service_status: ServiceStatus = Field(default_factory=ServiceStatus)
|
|
24
|
+
ready_to_start: bool = False
|
|
25
|
+
recommended_action: Literal["start_existing", "configure_new", "repair"] = (
|
|
26
|
+
"configure_new"
|
|
27
|
+
)
|
|
28
|
+
message: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
10
31
|
class InstallSetupRequest(BaseModel):
|
|
11
32
|
"""Request to start a complete installation setup.
|
|
12
33
|
|