lumen-app 0.4.2__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.
Files changed (56) hide show
  1. lumen_app/__init__.py +7 -0
  2. lumen_app/core/__init__.py +0 -0
  3. lumen_app/core/config.py +661 -0
  4. lumen_app/core/installer.py +274 -0
  5. lumen_app/core/loader.py +45 -0
  6. lumen_app/core/router.py +87 -0
  7. lumen_app/core/server.py +389 -0
  8. lumen_app/core/service.py +49 -0
  9. lumen_app/core/tests/__init__.py +1 -0
  10. lumen_app/core/tests/test_core_integration.py +561 -0
  11. lumen_app/core/tests/test_env_checker.py +487 -0
  12. lumen_app/proto/README.md +12 -0
  13. lumen_app/proto/ml_service.proto +88 -0
  14. lumen_app/proto/ml_service_pb2.py +66 -0
  15. lumen_app/proto/ml_service_pb2.pyi +136 -0
  16. lumen_app/proto/ml_service_pb2_grpc.py +251 -0
  17. lumen_app/server.py +362 -0
  18. lumen_app/utils/env_checker.py +752 -0
  19. lumen_app/utils/installation/__init__.py +25 -0
  20. lumen_app/utils/installation/env_manager.py +152 -0
  21. lumen_app/utils/installation/micromamba_installer.py +459 -0
  22. lumen_app/utils/installation/package_installer.py +149 -0
  23. lumen_app/utils/installation/verifier.py +95 -0
  24. lumen_app/utils/logger.py +181 -0
  25. lumen_app/utils/mamba/cuda.yaml +12 -0
  26. lumen_app/utils/mamba/default.yaml +6 -0
  27. lumen_app/utils/mamba/openvino.yaml +7 -0
  28. lumen_app/utils/mamba/tensorrt.yaml +13 -0
  29. lumen_app/utils/package_resolver.py +309 -0
  30. lumen_app/utils/preset_registry.py +219 -0
  31. lumen_app/web/__init__.py +3 -0
  32. lumen_app/web/api/__init__.py +1 -0
  33. lumen_app/web/api/config.py +229 -0
  34. lumen_app/web/api/hardware.py +201 -0
  35. lumen_app/web/api/install.py +608 -0
  36. lumen_app/web/api/server.py +253 -0
  37. lumen_app/web/core/__init__.py +1 -0
  38. lumen_app/web/core/server_manager.py +348 -0
  39. lumen_app/web/core/state.py +264 -0
  40. lumen_app/web/main.py +145 -0
  41. lumen_app/web/models/__init__.py +28 -0
  42. lumen_app/web/models/config.py +63 -0
  43. lumen_app/web/models/hardware.py +64 -0
  44. lumen_app/web/models/install.py +134 -0
  45. lumen_app/web/models/server.py +95 -0
  46. lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
  47. lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
  48. lumen_app/web/static/index.html +14 -0
  49. lumen_app/web/static/vite.svg +1 -0
  50. lumen_app/web/websockets/__init__.py +1 -0
  51. lumen_app/web/websockets/logs.py +159 -0
  52. lumen_app-0.4.2.dist-info/METADATA +23 -0
  53. lumen_app-0.4.2.dist-info/RECORD +56 -0
  54. lumen_app-0.4.2.dist-info/WHEEL +5 -0
  55. lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
  56. lumen_app-0.4.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,752 @@
1
+ """
2
+ Environment checker for Lumen AI services.
3
+
4
+ Provides driver validation for different hardware platforms.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import platform
10
+ import subprocess
11
+ from dataclasses import dataclass, field
12
+ from enum import Enum
13
+ from pathlib import Path
14
+
15
+ from lumen_resources.lumen_config import Runtime
16
+
17
+ from lumen_app.core.config import DeviceConfig
18
+ from lumen_app.utils.logger import get_logger
19
+ from lumen_app.utils.preset_registry import PresetRegistry
20
+
21
+ logger = get_logger("lumen.env_checker")
22
+
23
+
24
+ class DriverStatus(Enum):
25
+ """Driver availability status."""
26
+
27
+ AVAILABLE = "available"
28
+ MISSING = "missing"
29
+ INCOMPATIBLE = "incompatible"
30
+
31
+
32
+ @dataclass
33
+ class DriverCheckResult:
34
+ """Result of driver availability check."""
35
+
36
+ name: str
37
+ status: DriverStatus
38
+ details: str = ""
39
+ installable_via_mamba: bool = False
40
+ mamba_config_path: str | None = None
41
+
42
+
43
+ @dataclass
44
+ class EnvironmentReport:
45
+ """Complete environment status report."""
46
+
47
+ preset_name: str
48
+ drivers: list[DriverCheckResult]
49
+ ready: bool
50
+ missing_installable: list[str] = field(default_factory=list)
51
+
52
+
53
+ class DriverChecker:
54
+ """Checks driver availability for different hardware platforms."""
55
+
56
+ @staticmethod
57
+ def check_nvidia_gpu() -> DriverCheckResult:
58
+ """Check NVIDIA GPU/CUDA driver by running nvidia-smi."""
59
+ logger.info("[DriverChecker] Checking NVIDIA GPU/CUDA driver")
60
+
61
+ try:
62
+ logger.debug("[DriverChecker] Executing: nvidia-smi")
63
+ result = subprocess.run(
64
+ ["nvidia-smi"],
65
+ capture_output=True,
66
+ text=True,
67
+ timeout=10,
68
+ )
69
+
70
+ if result.returncode == 0:
71
+ # Parse GPU info from output
72
+ lines = result.stdout.split("\n")
73
+ details = "NVIDIA GPU detected"
74
+
75
+ for line in lines:
76
+ if "CUDA Version" in line:
77
+ details = line.strip()
78
+ break
79
+
80
+ logger.info(f"[DriverChecker] NVIDIA GPU detected: {details}")
81
+ return DriverCheckResult(
82
+ name="CUDA",
83
+ status=DriverStatus.AVAILABLE,
84
+ details=details,
85
+ installable_via_mamba=True,
86
+ mamba_config_path="cuda.yaml",
87
+ )
88
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
89
+ logger.debug(
90
+ f"[DriverChecker] nvidia-smi check failed: {type(e).__name__}: {e}"
91
+ )
92
+
93
+ logger.info("[DriverChecker] NVIDIA GPU not detected")
94
+ return DriverCheckResult(
95
+ name="CUDA",
96
+ status=DriverStatus.MISSING,
97
+ details="nvidia-smi command not found or failed",
98
+ installable_via_mamba=True,
99
+ mamba_config_path="cuda.yaml",
100
+ )
101
+
102
+ @staticmethod
103
+ def check_amd_ryzen_ai_npu() -> DriverCheckResult:
104
+ """Check AMD Ryzen AI NPU driver on Windows."""
105
+ logger.info("[DriverChecker] Checking AMD Ryzen AI NPU")
106
+
107
+ if platform.system() != "Windows":
108
+ logger.debug(
109
+ "[DriverChecker] Not Windows platform, marking as incompatible"
110
+ )
111
+ return DriverCheckResult(
112
+ name="AMD Ryzen AI NPU",
113
+ status=DriverStatus.MISSING,
114
+ details="Only available on Windows",
115
+ installable_via_mamba=False,
116
+ )
117
+
118
+ # Check if amdipu.dll exists
119
+ dll_path = Path(r"C:\Windows\System32\amdipu.dll")
120
+ logger.debug(f"[DriverChecker] Checking for {dll_path}")
121
+
122
+ if not dll_path.exists():
123
+ logger.debug("[DriverChecker] amdipu.dll not found")
124
+ return DriverCheckResult(
125
+ name="AMD Ryzen AI NPU",
126
+ status=DriverStatus.MISSING,
127
+ details="amdipu.dll not found in C:\\Windows\\System32\\",
128
+ installable_via_mamba=False,
129
+ )
130
+
131
+ # Check if amdipu service is running
132
+ try:
133
+ logger.debug("[DriverChecker] Executing: sc query amdipu")
134
+ result = subprocess.run(
135
+ ["sc", "query", "amdipu"],
136
+ capture_output=True,
137
+ text=True,
138
+ timeout=10,
139
+ )
140
+
141
+ if result.returncode == 0 and "RUNNING" in result.stdout:
142
+ logger.info("[DriverChecker] AMD Ryzen AI NPU detected")
143
+ return DriverCheckResult(
144
+ name="AMD Ryzen AI NPU",
145
+ status=DriverStatus.AVAILABLE,
146
+ details="amdipu service is running",
147
+ installable_via_mamba=False,
148
+ )
149
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
150
+ logger.debug(
151
+ f"[DriverChecker] amdipu service check failed: {type(e).__name__}: {e}"
152
+ )
153
+
154
+ logger.info("[DriverChecker] AMD Ryzen AI NPU not available")
155
+ return DriverCheckResult(
156
+ name="AMD Ryzen AI NPU",
157
+ status=DriverStatus.MISSING,
158
+ details="amdipu service is not running",
159
+ installable_via_mamba=False,
160
+ )
161
+
162
+ @staticmethod
163
+ def check_intel_gpu_openvino() -> DriverCheckResult:
164
+ """Check Intel GPU / OpenVINO compatibility."""
165
+ logger.info("[DriverChecker] Checking Intel GPU / OpenVINO")
166
+ is_intel_cpu = False
167
+
168
+ # Check CPU vendor based on platform
169
+ try:
170
+ system = platform.system()
171
+ logger.debug(f"[DriverChecker] Platform: {system}")
172
+
173
+ if system == "Linux":
174
+ logger.debug("[DriverChecker] Reading /proc/cpuinfo")
175
+ with open("/proc/cpuinfo", "r") as f:
176
+ cpuinfo = f.read()
177
+ is_intel_cpu = "GenuineIntel" in cpuinfo
178
+ logger.debug(f"[DriverChecker] Intel CPU detected: {is_intel_cpu}")
179
+
180
+ elif system == "Darwin":
181
+ # macOS - check sysctl
182
+ logger.debug(
183
+ "[DriverChecker] Executing: sysctl -n machdep.cpu.brand_string"
184
+ )
185
+ result = subprocess.run(
186
+ ["sysctl", "-n", "machdep.cpu.brand_string"],
187
+ capture_output=True,
188
+ text=True,
189
+ timeout=5,
190
+ )
191
+ is_intel_cpu = "Intel" in result.stdout
192
+ logger.debug(
193
+ f"[DriverChecker] CPU: {result.stdout.strip()}, Intel: {is_intel_cpu}"
194
+ )
195
+
196
+ elif system == "Windows":
197
+ # Windows - use wmic to check CPU manufacturer
198
+ logger.debug("[DriverChecker] Executing: wmic cpu get manufacturer")
199
+ result = subprocess.run(
200
+ ["wmic", "cpu", "get", "manufacturer"],
201
+ capture_output=True,
202
+ text=True,
203
+ timeout=10,
204
+ )
205
+ if result.returncode == 0:
206
+ # Output format: "Manufacturer\n GenuineIntel \n\n"
207
+ is_intel_cpu = (
208
+ "GenuineIntel" in result.stdout or "Intel" in result.stdout
209
+ )
210
+ logger.debug(f"[DriverChecker] Intel CPU detected: {is_intel_cpu}")
211
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
212
+ logger.debug(
213
+ f"[DriverChecker] CPU vendor check failed: {type(e).__name__}: {e}"
214
+ )
215
+
216
+ # If not Intel CPU, return INCOMPATIBLE
217
+ if not is_intel_cpu:
218
+ logger.info("[DriverChecker] Not Intel CPU - OpenVINO incompatible")
219
+ return DriverCheckResult(
220
+ name="OpenVINO",
221
+ status=DriverStatus.INCOMPATIBLE,
222
+ details="CPU vendor is not Intel",
223
+ installable_via_mamba=True,
224
+ mamba_config_path="openvino.yaml",
225
+ )
226
+
227
+ # Intel CPU detected - OpenVINO can be installed
228
+ # Note: We don't check if openvino package is installed since
229
+ # the runtime environment may not have it yet
230
+ logger.info("[DriverChecker] Intel CPU detected - OpenVINO installable")
231
+ return DriverCheckResult(
232
+ name="OpenVINO",
233
+ status=DriverStatus.MISSING,
234
+ details="Compatible Intel CPU detected, openvino package not installed",
235
+ installable_via_mamba=True,
236
+ mamba_config_path="openvino.yaml",
237
+ )
238
+
239
+ @staticmethod
240
+ def check_apple_silicon() -> DriverCheckResult:
241
+ """Check Apple Silicon CoreML support."""
242
+ logger.info("[DriverChecker] Checking Apple Silicon CoreML")
243
+
244
+ system = platform.system()
245
+ machine = platform.machine()
246
+ logger.debug(f"[DriverChecker] Platform: {system}, Architecture: {machine}")
247
+
248
+ if system != "Darwin":
249
+ logger.debug("[DriverChecker] Not macOS platform")
250
+ return DriverCheckResult(
251
+ name="CoreML",
252
+ status=DriverStatus.INCOMPATIBLE,
253
+ details="Only available on macOS (Darwin)",
254
+ installable_via_mamba=False,
255
+ )
256
+
257
+ if machine != "arm64":
258
+ logger.debug(f"[DriverChecker] Not arm64 architecture: {machine}")
259
+ return DriverCheckResult(
260
+ name="CoreML",
261
+ status=DriverStatus.INCOMPATIBLE,
262
+ details=f"Architecture is {machine}, not arm64",
263
+ installable_via_mamba=False,
264
+ )
265
+
266
+ # Check for Apple chip
267
+ try:
268
+ logger.debug(
269
+ "[DriverChecker] Executing: sysctl -n machdep.cpu.brand_string"
270
+ )
271
+ result = subprocess.run(
272
+ ["sysctl", "-n", "machdep.cpu.brand_string"],
273
+ capture_output=True,
274
+ text=True,
275
+ timeout=5,
276
+ )
277
+
278
+ if result.returncode == 0 and "Apple" in result.stdout:
279
+ logger.info(
280
+ f"[DriverChecker] Apple Silicon detected: {result.stdout.strip()}"
281
+ )
282
+ return DriverCheckResult(
283
+ name="CoreML",
284
+ status=DriverStatus.AVAILABLE,
285
+ details=result.stdout.strip(),
286
+ installable_via_mamba=False,
287
+ )
288
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
289
+ logger.debug(
290
+ f"[DriverChecker] Apple Silicon check failed: {type(e).__name__}: {e}"
291
+ )
292
+
293
+ logger.info("[DriverChecker] Not an Apple Silicon chip")
294
+ return DriverCheckResult(
295
+ name="CoreML",
296
+ status=DriverStatus.MISSING,
297
+ details="Not an Apple Silicon chip",
298
+ installable_via_mamba=False,
299
+ )
300
+
301
+ @staticmethod
302
+ def check_rockchip_rknn() -> DriverCheckResult:
303
+ """Check Rockchip RKNN device node on Linux."""
304
+ logger.info("[DriverChecker] Checking Rockchip RKNN")
305
+
306
+ if platform.system() != "Linux":
307
+ logger.debug("[DriverChecker] Not Linux platform")
308
+ return DriverCheckResult(
309
+ name="RKNN",
310
+ status=DriverStatus.INCOMPATIBLE,
311
+ details="Only available on Linux",
312
+ installable_via_mamba=False,
313
+ )
314
+
315
+ # Check for /dev/rknpu device file
316
+ rknpu_paths = [
317
+ Path("/dev/rknpu"),
318
+ Path("/dev/rknpu0"),
319
+ Path("/dev/rp1"),
320
+ ]
321
+
322
+ logger.debug(
323
+ f"[DriverChecker] Checking for RKNN device nodes: {[str(p) for p in rknpu_paths]}"
324
+ )
325
+ for device_path in rknpu_paths:
326
+ if device_path.exists():
327
+ logger.info(f"[DriverChecker] RKNN device found: {device_path}")
328
+ return DriverCheckResult(
329
+ name="RKNN",
330
+ status=DriverStatus.AVAILABLE,
331
+ details=f"Device found: {device_path}",
332
+ installable_via_mamba=False,
333
+ )
334
+
335
+ # Check for Rockchip in cpuinfo as fallback
336
+ try:
337
+ logger.debug("[DriverChecker] Reading /proc/cpuinfo for Rockchip CPU")
338
+ with open("/proc/cpuinfo", "r") as f:
339
+ cpuinfo = f.read().lower()
340
+ if "rockchip" in cpuinfo or "rk35" in cpuinfo:
341
+ logger.info(
342
+ "[DriverChecker] Rockchip CPU detected but no device node"
343
+ )
344
+ return DriverCheckResult(
345
+ name="RKNN",
346
+ status=DriverStatus.MISSING,
347
+ details="Rockchip CPU detected but device node not found",
348
+ installable_via_mamba=False,
349
+ )
350
+ except OSError as e:
351
+ logger.debug(
352
+ f"[DriverChecker] Failed to read cpuinfo: {type(e).__name__}: {e}"
353
+ )
354
+
355
+ logger.info("[DriverChecker] RKNN not detected")
356
+ return DriverCheckResult(
357
+ name="RKNN",
358
+ status=DriverStatus.MISSING,
359
+ details="RKNN device node not found",
360
+ installable_via_mamba=False,
361
+ )
362
+
363
+ @staticmethod
364
+ def check_amd_gpu_directml() -> DriverCheckResult:
365
+ """Check AMD GPU DirectML support on Windows."""
366
+ logger.info("[DriverChecker] Checking AMD GPU DirectML")
367
+
368
+ if platform.system() != "Windows":
369
+ logger.debug("[DriverChecker] Not Windows platform")
370
+ return DriverCheckResult(
371
+ name="DirectML",
372
+ status=DriverStatus.INCOMPATIBLE,
373
+ details="Only available on Windows",
374
+ installable_via_mamba=False,
375
+ )
376
+
377
+ # Check GPU name using wmic
378
+ try:
379
+ logger.debug(
380
+ "[DriverChecker] Executing: wmic path win32_videocontroller get name"
381
+ )
382
+ result = subprocess.run(
383
+ ["wmic", "path", "win32_videocontroller", "get", "name"],
384
+ capture_output=True,
385
+ text=True,
386
+ timeout=10,
387
+ )
388
+
389
+ if result.returncode == 0:
390
+ output = result.stdout.lower()
391
+ logger.debug(f"[DriverChecker] GPU detection output: {output[:100]}...")
392
+ if "radeon" in output or "amd" in output:
393
+ logger.info("[DriverChecker] AMD Radeon GPU detected")
394
+ return DriverCheckResult(
395
+ name="DirectML",
396
+ status=DriverStatus.AVAILABLE,
397
+ details="AMD Radeon GPU detected",
398
+ installable_via_mamba=False,
399
+ )
400
+ except (subprocess.TimeoutExpired, FileNotFoundError, OSError) as e:
401
+ logger.debug(
402
+ f"[DriverChecker] AMD GPU check failed: {type(e).__name__}: {e}"
403
+ )
404
+
405
+ logger.info("[DriverChecker] No AMD GPU detected")
406
+ return DriverCheckResult(
407
+ name="DirectML",
408
+ status=DriverStatus.MISSING,
409
+ details="No AMD Radeon GPU found",
410
+ installable_via_mamba=False,
411
+ )
412
+
413
+ @staticmethod
414
+ def check_for_preset(preset_name: str) -> list[DriverCheckResult]:
415
+ """
416
+ Check drivers required for a specific DeviceConfig preset.
417
+
418
+ Args:
419
+ preset_name: Name of the preset method (e.g., "nvidia_gpu", "apple_silicon")
420
+
421
+ Returns:
422
+ List of DriverCheckResult for required drivers
423
+ """
424
+ logger.info(f"[DriverChecker] Checking drivers for preset: {preset_name}")
425
+ results = []
426
+
427
+ # Validate preset exists
428
+ if not PresetRegistry.preset_exists(preset_name):
429
+ logger.warning(
430
+ f"[DriverChecker] Unknown preset '{preset_name}', returning empty results"
431
+ )
432
+ return results
433
+
434
+ # Create config from preset to determine required checks
435
+ try:
436
+ logger.debug(f"[DriverChecker] Creating config from preset '{preset_name}'")
437
+ config = PresetRegistry.create_config(preset_name)
438
+ logger.debug(
439
+ f"[DriverChecker] Config created: runtime={config.runtime}, providers={config.onnx_providers}"
440
+ )
441
+ # Delegate to device_config check
442
+ results = DriverChecker.check_for_device_config(config)
443
+ logger.info(
444
+ f"[DriverChecker] Completed preset check, found {len(results)} driver(s)"
445
+ )
446
+ return results
447
+ except Exception as e:
448
+ logger.error(
449
+ f"[DriverChecker] Failed to check preset '{preset_name}': {type(e).__name__}: {e}"
450
+ )
451
+ return results
452
+
453
+ @staticmethod
454
+ def check_for_device_config(device_config: DeviceConfig) -> list[DriverCheckResult]:
455
+ """
456
+ Check drivers required for a DeviceConfig instance.
457
+
458
+ Args:
459
+ device_config: DeviceConfig instance
460
+
461
+ Returns:
462
+ List of DriverCheckResult for required drivers
463
+ """
464
+ logger.info("[DriverChecker] Checking drivers for device config")
465
+ results = []
466
+ providers = device_config.onnx_providers or []
467
+ runtime = device_config.runtime
468
+
469
+ logger.debug(f"[DriverChecker] Config runtime={runtime}, providers={providers}")
470
+
471
+ # Check based on onnx providers
472
+ for provider in providers:
473
+ provider_name = (
474
+ provider
475
+ if isinstance(provider, str)
476
+ else (provider[0] if provider else "")
477
+ )
478
+
479
+ logger.debug(f"[DriverChecker] Checking provider: {provider_name}")
480
+
481
+ if "CUDA" in provider_name or "TensorRT" in provider_name:
482
+ logger.debug("[DriverChecker] Triggering NVIDIA GPU check")
483
+ results.append(DriverChecker.check_nvidia_gpu())
484
+ elif "CoreML" in provider_name:
485
+ logger.debug("[DriverChecker] Triggering Apple Silicon check")
486
+ results.append(DriverChecker.check_apple_silicon())
487
+ elif "OpenVINO" in provider_name:
488
+ logger.debug("[DriverChecker] Triggering Intel GPU/OpenVINO check")
489
+ results.append(DriverChecker.check_intel_gpu_openvino())
490
+ elif "DML" in provider_name:
491
+ logger.debug("[DriverChecker] Triggering AMD GPU DirectML check")
492
+ results.append(DriverChecker.check_amd_gpu_directml())
493
+
494
+ # Check RKNN runtime
495
+ if runtime == Runtime.rknn:
496
+ logger.debug("[DriverChecker] Triggering RKNN check")
497
+ results.append(DriverChecker.check_rockchip_rknn())
498
+
499
+ # Remove duplicates by name
500
+ seen = {}
501
+ for result in results:
502
+ if result.name not in seen:
503
+ seen[result.name] = result
504
+
505
+ unique_results = list(seen.values())
506
+ logger.info(
507
+ f"[DriverChecker] Device config check completed: {len(unique_results)} unique driver(s)"
508
+ )
509
+ return unique_results
510
+
511
+
512
+ class DependencyInstaller:
513
+ """Installs missing dependencies using micromamba."""
514
+
515
+ def __init__(
516
+ self,
517
+ mamba_configs_dir: str | Path | None = None,
518
+ micromamba_path: str | None = None,
519
+ root_prefix: str | Path | None = None,
520
+ ):
521
+ """
522
+ Initialize installer with mamba config directory and micromamba path.
523
+
524
+ Args:
525
+ mamba_configs_dir: Path to directory containing *.yaml files.
526
+ Defaults to lumen_app/utils/mamba
527
+ micromamba_path: Optional path to micromamba executable.
528
+ If None, uses 'micromamba' from PATH
529
+ root_prefix: Optional micromamba root prefix (MAMBA_ROOT_PREFIX).
530
+ If None, micromamba default is used.
531
+ """
532
+ if mamba_configs_dir is None:
533
+ current_file = Path(__file__)
534
+ mamba_configs_dir = current_file.parent / "mamba"
535
+
536
+ self.configs_dir = Path(mamba_configs_dir)
537
+ self.micromamba_path = micromamba_path or "micromamba"
538
+ self.root_prefix = Path(root_prefix).expanduser() if root_prefix else None
539
+
540
+ logger.info(
541
+ f"[DependencyInstaller] Initialized with configs_dir={self.configs_dir}, micromamba_path={self.micromamba_path}, root_prefix={self.root_prefix}"
542
+ )
543
+
544
+ def install_driver(
545
+ self, driver_name: str, env_name: str = "lumen_env", dry_run: bool = False
546
+ ) -> tuple[bool, str]:
547
+ """
548
+ Install driver using micromamba.
549
+
550
+ Args:
551
+ driver_name: Name of driver (e.g., "cuda", "openvino")
552
+ env_name: Name of the conda environment to create/update
553
+ dry_run: If True, only print the command without executing
554
+
555
+ Returns:
556
+ Tuple of (success: bool, message: str)
557
+ """
558
+ logger.info(
559
+ f"[DependencyInstaller] Installing driver '{driver_name}' (dry_run={dry_run})"
560
+ )
561
+
562
+ # Map driver names to config files
563
+ config_map = {
564
+ "cuda": "cuda.yaml",
565
+ "openvino": "openvino.yaml",
566
+ "tensorrt": "tensorrt.yaml",
567
+ }
568
+
569
+ config_filename = config_map.get(driver_name)
570
+ if not config_filename:
571
+ logger.warning(
572
+ f"[DependencyInstaller] No mamba config available for {driver_name}"
573
+ )
574
+ return False, f"No mamba config available for {driver_name}"
575
+
576
+ config_file = self.configs_dir / config_filename
577
+ logger.debug(f"[DependencyInstaller] Config file path: {config_file}")
578
+
579
+ if not config_file.exists():
580
+ logger.error(f"[DependencyInstaller] Config file not found: {config_file}")
581
+ return False, f"Config file not found: {config_file}"
582
+
583
+ # Build micromamba command with specified path
584
+ cmd = [
585
+ self.micromamba_path,
586
+ "install",
587
+ "-y",
588
+ "-n",
589
+ env_name,
590
+ "-f",
591
+ str(config_file),
592
+ ]
593
+ if self.root_prefix:
594
+ cmd.extend(["--root-prefix", str(self.root_prefix)])
595
+
596
+ logger.debug(f"[DependencyInstaller] Installation command: {' '.join(cmd)}")
597
+
598
+ if dry_run:
599
+ logger.info("[DependencyInstaller] Dry run mode - skipping execution")
600
+ return True, f"Would run: {' '.join(cmd)}"
601
+
602
+ try:
603
+ logger.info("[DependencyInstaller] Executing installation (timeout=600s)")
604
+ result = subprocess.run(
605
+ cmd,
606
+ capture_output=True,
607
+ text=True,
608
+ timeout=600, # 10 minutes timeout
609
+ )
610
+
611
+ if result.returncode == 0:
612
+ logger.info(
613
+ f"[DependencyInstaller] Successfully installed {driver_name}"
614
+ )
615
+ return True, f"Successfully installed {driver_name}"
616
+ else:
617
+ logger.error(
618
+ f"[DependencyInstaller] Installation failed: returncode={result.returncode}, stderr={result.stderr}"
619
+ )
620
+ return False, f"Installation failed: {result.stderr}"
621
+
622
+ except subprocess.TimeoutExpired:
623
+ logger.error("[DependencyInstaller] Installation timed out after 600s")
624
+ return False, "Installation timed out"
625
+ except FileNotFoundError:
626
+ logger.error(
627
+ f"[DependencyInstaller] micromamba not found at {self.micromamba_path}"
628
+ )
629
+ return False, f"micromamba not found at {self.micromamba_path}"
630
+ except Exception as e:
631
+ logger.error(
632
+ f"[DependencyInstaller] Installation error: {type(e).__name__}: {e}"
633
+ )
634
+ return False, f"Installation error: {str(e)}"
635
+
636
+ def get_install_command(self, driver_name: str, env_name: str = "lumen_env") -> str:
637
+ """Get the installation command for manual execution."""
638
+ logger.debug(
639
+ f"[DependencyInstaller] Getting install command for '{driver_name}'"
640
+ )
641
+
642
+ config_map = {
643
+ "cuda": "cuda.yaml",
644
+ "openvino": "openvino.yaml",
645
+ "tensorrt": "tensorrt.yaml",
646
+ }
647
+
648
+ config_filename = config_map.get(driver_name)
649
+ if not config_filename:
650
+ logger.warning(
651
+ f"[DependencyInstaller] No mamba config available for {driver_name}"
652
+ )
653
+ return f"# No mamba config available for {driver_name}"
654
+
655
+ config_file = self.configs_dir / config_filename
656
+ if not config_file.exists():
657
+ logger.error(f"[DependencyInstaller] Config file not found: {config_file}")
658
+ return f"# Config file not found: {config_file}"
659
+
660
+ cmd = f"micromamba install -y -n {env_name} -f {config_file}"
661
+ logger.debug(f"[DependencyInstaller] Generated command: {cmd}")
662
+ return cmd
663
+
664
+
665
+ class EnvironmentChecker:
666
+ """Main entry point for environment checking."""
667
+
668
+ @staticmethod
669
+ def check_preset(preset_name: str) -> EnvironmentReport:
670
+ """
671
+ Check environment for a specific preset.
672
+
673
+ Args:
674
+ preset_name: Name of the DeviceConfig preset method
675
+
676
+ Returns:
677
+ EnvironmentReport with full status
678
+ """
679
+ logger.info(
680
+ f"[EnvironmentChecker] Checking environment for preset: {preset_name}"
681
+ )
682
+ drivers = DriverChecker.check_for_preset(preset_name)
683
+
684
+ all_available = all(d.status == DriverStatus.AVAILABLE for d in drivers)
685
+ missing_installable = [
686
+ d.name.lower()
687
+ for d in drivers
688
+ if d.status == DriverStatus.MISSING and d.installable_via_mamba
689
+ ]
690
+
691
+ logger.info(
692
+ f"[EnvironmentChecker] Preset check result: ready={all_available}, drivers={len(drivers)}, missing_installable={len(missing_installable)}"
693
+ )
694
+ for driver in drivers:
695
+ logger.debug(
696
+ f"[EnvironmentChecker] - {driver.name}: {driver.status.value} ({driver.details})"
697
+ )
698
+
699
+ return EnvironmentReport(
700
+ preset_name=preset_name,
701
+ drivers=drivers,
702
+ ready=all_available,
703
+ missing_installable=missing_installable,
704
+ )
705
+
706
+ @staticmethod
707
+ def check_device_config(device_config: DeviceConfig) -> EnvironmentReport:
708
+ """
709
+ Check environment for a DeviceConfig instance.
710
+
711
+ Args:
712
+ device_config: DeviceConfig to check
713
+
714
+ Returns:
715
+ EnvironmentReport with full status
716
+ """
717
+ logger.info("[EnvironmentChecker] Checking environment for device config")
718
+ drivers = DriverChecker.check_for_device_config(device_config)
719
+
720
+ all_available = all(d.status == DriverStatus.AVAILABLE for d in drivers)
721
+ missing_installable = [
722
+ d.name.lower()
723
+ for d in drivers
724
+ if d.status == DriverStatus.MISSING and d.installable_via_mamba
725
+ ]
726
+
727
+ # Determine preset name from config using PresetRegistry
728
+ preset_name = "custom"
729
+ for name in PresetRegistry.get_preset_names():
730
+ preset_config = PresetRegistry.create_config(name)
731
+ if (
732
+ preset_config.runtime == device_config.runtime
733
+ and preset_config.onnx_providers == device_config.onnx_providers
734
+ ):
735
+ preset_name = name
736
+ logger.debug(f"[EnvironmentChecker] Matched preset: {name}")
737
+ break
738
+
739
+ logger.info(
740
+ f"[EnvironmentChecker] Device config check result: preset={preset_name}, ready={all_available}, drivers={len(drivers)}, missing_installable={len(missing_installable)}"
741
+ )
742
+ for driver in drivers:
743
+ logger.debug(
744
+ f"[EnvironmentChecker] - {driver.name}: {driver.status.value} ({driver.details})"
745
+ )
746
+
747
+ return EnvironmentReport(
748
+ preset_name=preset_name,
749
+ drivers=drivers,
750
+ ready=all_available,
751
+ missing_installable=missing_installable,
752
+ )