lumen-app 0.4.2__py3-none-any.whl → 0.4.4__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/core/server.py CHANGED
@@ -240,7 +240,7 @@ def serve(config_path: str, port_override: int | None = None) -> None:
240
240
 
241
241
  # Determine port: CLI override > config file > default
242
242
  preferred_port = port_override or config.server.port or 50051
243
- requested_addr = f"[::]:{preferred_port}"
243
+ requested_addr = f"0.0.0.0::{preferred_port}"
244
244
  try:
245
245
  bound_port = server.add_insecure_port(requested_addr)
246
246
  except RuntimeError as exc:
@@ -251,7 +251,7 @@ def serve(config_path: str, port_override: int | None = None) -> None:
251
251
 
252
252
  if bound_port == 0:
253
253
  try:
254
- bound_port = server.add_insecure_port("[::]:0")
254
+ bound_port = server.add_insecure_port("0.0.0.0:0")
255
255
  except RuntimeError as exc:
256
256
  logger.error(f"Unable to bind gRPC server to any port: {exc}")
257
257
  sys.exit(1)
@@ -261,7 +261,7 @@ def serve(config_path: str, port_override: int | None = None) -> None:
261
261
  sys.exit(1)
262
262
 
263
263
  port = bound_port
264
- listen_addr = f"[::]:{port}"
264
+ listen_addr = f"0.0.0.0:{port}"
265
265
  server.start()
266
266
 
267
267
  # Log server startup info
@@ -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
- install_script = install_dir / "install.ps1"
291
- cmd = [
292
- "powershell",
293
- "-Command",
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
- if dry_run:
299
- logger.info(f"[MicromambaInstaller] Would run: {' '.join(cmd)}")
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
- try:
303
- result = subprocess.run(cmd, capture_output=True, text=True, timeout=400)
298
+ # Get URLs with mirror fallback
299
+ download_urls = self.mirror_selector.get_micromamba_urls(base_url, self.region)
304
300
 
305
- if result.returncode == 0:
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
- except subprocess.TimeoutExpired:
311
- return False, "Installation timed out"
312
- except Exception as e:
313
- return False, f"Installation error: {str(e)}"
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
@@ -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
- config, device_config = app_state.get_config()
109
+ lumen_config = app_state.get_lumen_config()
109
110
 
110
- if not config or not device_config:
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": config.cache_dir,
116
- "region": config.region,
117
- "port": config.port,
118
- "service_name": config.service_name,
119
- "device": {
120
- "runtime": device_config.runtime.value,
121
- "batch_size": device_config.batch_size,
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
- with open(path) as f:
219
- config_content = yaml.safe_load(f)
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
- # TODO: Parse and validate the config, then set app_state
245
+ logger.info(f"Configuration loaded successfully from {path}")
222
246
 
223
- return {"loaded": True, "config_path": str(path), "config": config_content}
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
+ )
@@ -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
@@ -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."""
@@ -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