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,608 @@
1
+ """Installation API endpoints - simplified one-click setup."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import time
7
+ import uuid
8
+ from pathlib import Path
9
+
10
+ from fastapi import APIRouter, BackgroundTasks, HTTPException
11
+ from lumen_resources import LumenConfig, load_and_validate_config
12
+
13
+ from lumen_app.core.installer import CoreInstaller
14
+ from lumen_app.utils.env_checker import (
15
+ DependencyInstaller,
16
+ EnvironmentChecker,
17
+ )
18
+ from lumen_app.utils.installation import MicromambaInstaller, MicromambaStatus
19
+ from lumen_app.utils.logger import get_logger
20
+ from lumen_app.utils.preset_registry import PresetRegistry
21
+ from lumen_app.web.core.state import app_state
22
+ from lumen_app.web.models.install import (
23
+ InstallLogsResponse,
24
+ InstallSetupRequest,
25
+ InstallStatusResponse,
26
+ InstallStep,
27
+ InstallTaskListResponse,
28
+ InstallTaskResponse,
29
+ )
30
+
31
+ logger = get_logger("lumen.web.api.install")
32
+ router = APIRouter()
33
+
34
+
35
+ @router.get("/status", response_model=InstallStatusResponse)
36
+ async def get_install_status():
37
+ """Get current installation status of the system."""
38
+ logger.info("Checking installation status")
39
+
40
+ # Check micromamba
41
+ cache_dir = Path("~/.lumen").expanduser()
42
+ micromamba_installer = MicromambaInstaller(cache_dir)
43
+ micromamba_result = micromamba_installer.check()
44
+ micromamba_installed = micromamba_result.status == MicromambaStatus.INSTALLED
45
+ micromamba_path = (
46
+ micromamba_result.executable_path if micromamba_installed else None
47
+ )
48
+
49
+ # Check environment (TODO: implement actual check)
50
+ environment_exists = False
51
+ environment_name = None
52
+ environment_path = None
53
+
54
+ # Check drivers (placeholder)
55
+ drivers_checked = False
56
+ drivers = {}
57
+
58
+ missing_components = []
59
+ if not micromamba_installed:
60
+ missing_components.append("micromamba")
61
+ if not environment_exists:
62
+ missing_components.append("environment")
63
+
64
+ return InstallStatusResponse(
65
+ micromamba_installed=micromamba_installed,
66
+ micromamba_path=micromamba_path,
67
+ environment_exists=environment_exists,
68
+ environment_name=environment_name,
69
+ environment_path=environment_path,
70
+ drivers_checked=drivers_checked,
71
+ drivers=drivers,
72
+ ready_for_preset=None if missing_components else "cpu",
73
+ missing_components=missing_components,
74
+ )
75
+
76
+
77
+ @router.post("/setup", response_model=InstallTaskResponse)
78
+ async def start_installation(
79
+ request: InstallSetupRequest, background_tasks: BackgroundTasks
80
+ ):
81
+ """Start a complete installation setup for the selected preset.
82
+
83
+ This will automatically install all required components:
84
+ 1. micromamba (if not present)
85
+ 2. conda environment (if not exists)
86
+ 3. required drivers for the preset
87
+ """
88
+ logger.info(f"Starting installation setup for preset: {request.preset}")
89
+
90
+ # Validate preset
91
+ if not PresetRegistry.preset_exists(request.preset):
92
+ raise HTTPException(status_code=400, detail=f"Unknown preset: {request.preset}")
93
+
94
+ # Create task
95
+ task_id = str(uuid.uuid4())
96
+ current_time = time.time()
97
+
98
+ # Determine installation steps based on current status and preset
99
+ steps = await _plan_installation_steps(request)
100
+
101
+ task = InstallTaskResponse(
102
+ task_id=task_id,
103
+ preset=request.preset,
104
+ status="pending",
105
+ progress=0,
106
+ current_step="Initializing...",
107
+ steps=steps,
108
+ created_at=current_time,
109
+ updated_at=current_time,
110
+ )
111
+
112
+ # Store task in state
113
+ await app_state.store_install_task(task_id, task)
114
+
115
+ # Start background installation
116
+ background_tasks.add_task(_run_installation, task_id, request)
117
+
118
+ return task
119
+
120
+
121
+ @router.get("/tasks", response_model=InstallTaskListResponse)
122
+ async def list_install_tasks():
123
+ """List all installation tasks."""
124
+ tasks = await app_state.get_all_install_tasks()
125
+ return InstallTaskListResponse(tasks=tasks, total=len(tasks))
126
+
127
+
128
+ @router.get("/tasks/{task_id}", response_model=InstallTaskResponse)
129
+ async def get_install_task(task_id: str):
130
+ """Get installation task status and progress."""
131
+ task = await app_state.get_install_task(task_id)
132
+
133
+ if not task:
134
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
135
+
136
+ return task
137
+
138
+
139
+ @router.get("/tasks/{task_id}/logs", response_model=InstallLogsResponse)
140
+ async def get_install_logs(task_id: str, tail: int = 100):
141
+ """Get installation task logs."""
142
+ task = await app_state.get_install_task(task_id)
143
+
144
+ if not task:
145
+ raise HTTPException(status_code=404, detail=f"Task {task_id} not found")
146
+
147
+ logs = await app_state.get_install_task_logs(task_id)
148
+
149
+ # Return last N lines
150
+ logs_to_return = logs[-tail:] if tail > 0 else logs
151
+
152
+ return InstallLogsResponse(
153
+ task_id=task_id, logs=logs_to_return, total_lines=len(logs)
154
+ )
155
+
156
+
157
+ # ===== Internal helper functions =====
158
+
159
+
160
+ async def _plan_installation_steps(request: InstallSetupRequest) -> list[InstallStep]:
161
+ """Plan installation steps based on current state and requirements."""
162
+ steps = []
163
+
164
+ # Step 1: Check/install micromamba
165
+ resolved_cache_dir = Path(request.cache_dir).expanduser()
166
+ micromamba_installer = MicromambaInstaller(resolved_cache_dir)
167
+ micromamba_result = micromamba_installer.check()
168
+ if (
169
+ micromamba_result.status != MicromambaStatus.INSTALLED
170
+ or request.force_reinstall
171
+ ):
172
+ steps.append(
173
+ InstallStep(
174
+ name="Install micromamba",
175
+ status="pending",
176
+ progress=0,
177
+ message="Micromamba package manager will be installed",
178
+ )
179
+ )
180
+ else:
181
+ steps.append(
182
+ InstallStep(
183
+ name="Check micromamba",
184
+ status="pending",
185
+ progress=0,
186
+ message="Verify micromamba installation",
187
+ )
188
+ )
189
+
190
+ # Step 2: Create environment
191
+ steps.append(
192
+ InstallStep(
193
+ name=f"Create environment '{request.environment_name}'",
194
+ status="pending",
195
+ progress=0,
196
+ message="Create conda environment for Lumen",
197
+ )
198
+ )
199
+
200
+ # Step 3: Install drivers (if needed)
201
+ preset_info = PresetRegistry.get_preset(request.preset)
202
+ if preset_info and preset_info.requires_drivers:
203
+ # Check what drivers are needed
204
+ report = EnvironmentChecker.check_preset(request.preset)
205
+ if not report.ready:
206
+ missing_drivers = [
207
+ d.name for d in report.drivers if d.status.value != "available"
208
+ ]
209
+ if missing_drivers:
210
+ steps.append(
211
+ InstallStep(
212
+ name="Install drivers",
213
+ status="pending",
214
+ progress=0,
215
+ message=f"Install required drivers: {', '.join(missing_drivers)}",
216
+ )
217
+ )
218
+
219
+ # Step 4: Install Lumen packages
220
+ steps.append(
221
+ InstallStep(
222
+ name="Install Lumen packages",
223
+ status="pending",
224
+ progress=0,
225
+ message="Install required Lumen packages",
226
+ )
227
+ )
228
+
229
+ # Step 5: Verify installation
230
+ steps.append(
231
+ InstallStep(
232
+ name="Verify installation",
233
+ status="pending",
234
+ progress=0,
235
+ message="Verify all components are installed correctly",
236
+ )
237
+ )
238
+
239
+ return steps
240
+
241
+
242
+ async def _run_installation(task_id: str, request: InstallSetupRequest):
243
+ """Background task to run the installation process."""
244
+ logger.info(f"Running installation task {task_id} for preset {request.preset}")
245
+
246
+ try:
247
+ await _update_task(
248
+ task_id, status="running", current_step="Starting installation..."
249
+ )
250
+
251
+ task = await app_state.get_install_task(task_id)
252
+ if not task:
253
+ logger.error(f"Task {task_id} not found")
254
+ return
255
+
256
+ total_steps = len(task.steps)
257
+ current_step_idx = 0
258
+ resolved_cache_dir = str(Path(request.cache_dir).expanduser())
259
+
260
+ # Step 1: Check/Install micromamba
261
+ if task.steps[current_step_idx].name.startswith("Install micromamba"):
262
+ await _execute_step(task_id, current_step_idx, total_steps)
263
+ success = await _install_micromamba(task_id, resolved_cache_dir)
264
+ if not success:
265
+ await _update_task(
266
+ task_id,
267
+ status="failed",
268
+ error="Failed to install micromamba",
269
+ completed_at=time.time(),
270
+ )
271
+ return
272
+ current_step_idx += 1
273
+ elif task.steps[current_step_idx].name.startswith("Check micromamba"):
274
+ await _execute_step(task_id, current_step_idx, total_steps, quick=True)
275
+ current_step_idx += 1
276
+
277
+ # Step 2: Create environment
278
+ if current_step_idx < total_steps:
279
+ await _execute_step(task_id, current_step_idx, total_steps)
280
+ success = await _create_environment(
281
+ task_id, request.environment_name, resolved_cache_dir
282
+ )
283
+ if not success:
284
+ await _update_task(
285
+ task_id,
286
+ status="failed",
287
+ error="Failed to create environment",
288
+ completed_at=time.time(),
289
+ )
290
+ return
291
+ current_step_idx += 1
292
+
293
+ # Step 3: Install drivers (if needed)
294
+ if (
295
+ current_step_idx < total_steps
296
+ and task.steps[current_step_idx].name == "Install drivers"
297
+ ):
298
+ await _execute_step(task_id, current_step_idx, total_steps)
299
+ success = await _install_drivers(
300
+ task_id, request.preset, request.environment_name, resolved_cache_dir
301
+ )
302
+ if not success:
303
+ await _update_task(
304
+ task_id,
305
+ status="failed",
306
+ error="Failed to install drivers",
307
+ completed_at=time.time(),
308
+ )
309
+ return
310
+ current_step_idx += 1
311
+
312
+ # Step 4: Install Lumen packages
313
+ if (
314
+ current_step_idx < total_steps
315
+ and task.steps[current_step_idx].name == "Install Lumen packages"
316
+ ):
317
+ await _execute_step(task_id, current_step_idx, total_steps)
318
+ success = await _install_lumen_packages(
319
+ task_id, resolved_cache_dir, request.preset
320
+ )
321
+ if not success:
322
+ await _update_task(
323
+ task_id,
324
+ status="failed",
325
+ error="Failed to install Lumen packages",
326
+ completed_at=time.time(),
327
+ )
328
+ return
329
+ current_step_idx += 1
330
+
331
+ # Step 5: Verify installation
332
+ if current_step_idx < total_steps:
333
+ await _execute_step(task_id, current_step_idx, total_steps)
334
+ success = await _verify_installation(
335
+ task_id, resolved_cache_dir, request.environment_name
336
+ )
337
+ if not success:
338
+ await _update_task(
339
+ task_id,
340
+ status="failed",
341
+ error="Failed to verify installation",
342
+ completed_at=time.time(),
343
+ )
344
+ return
345
+ current_step_idx += 1
346
+
347
+ # Complete
348
+ await _update_task(
349
+ task_id,
350
+ status="completed",
351
+ progress=100,
352
+ current_step="Installation completed successfully",
353
+ completed_at=time.time(),
354
+ )
355
+ logger.info(f"Installation task {task_id} completed successfully")
356
+
357
+ except Exception as e:
358
+ logger.error(f"Installation task {task_id} failed: {e}", exc_info=True)
359
+ await _update_task(
360
+ task_id,
361
+ status="failed",
362
+ error=str(e),
363
+ completed_at=time.time(),
364
+ )
365
+
366
+
367
+ async def _execute_step(
368
+ task_id: str, step_idx: int, total_steps: int, quick: bool = False
369
+ ):
370
+ """Mark a step as running and update progress."""
371
+ task = await app_state.get_install_task(task_id)
372
+ if not task:
373
+ return
374
+
375
+ # Update step status
376
+ task.steps[step_idx].status = "running"
377
+ task.steps[step_idx].started_at = time.time()
378
+
379
+ # Calculate overall progress
380
+ base_progress = int((step_idx / total_steps) * 100)
381
+
382
+ await _update_task(
383
+ task_id,
384
+ current_step=task.steps[step_idx].message,
385
+ progress=base_progress,
386
+ )
387
+
388
+ # Simulate quick check
389
+ if quick:
390
+ await asyncio.sleep(0.5)
391
+ await _complete_current_step(task_id)
392
+
393
+
394
+ async def _install_micromamba(task_id: str, cache_dir: str) -> bool:
395
+ """Install micromamba."""
396
+ await _append_log(task_id, "Installing micromamba...")
397
+
398
+ try:
399
+ micromamba_installer = MicromambaInstaller(cache_dir)
400
+ exe_path = micromamba_installer.install(dry_run=False)
401
+
402
+ message = f"Micromamba installed successfully at {exe_path}"
403
+ await _append_log(task_id, message)
404
+ await _complete_current_step(task_id)
405
+ return True
406
+
407
+ except Exception as e:
408
+ error_msg = f"Failed to install micromamba: {e}"
409
+ await _append_log(task_id, error_msg)
410
+ await _fail_current_step(task_id, error_msg)
411
+ return False
412
+
413
+
414
+ async def _create_environment(task_id: str, env_name: str, cache_dir: str) -> bool:
415
+ """Create conda environment."""
416
+ await _append_log(task_id, f"Creating environment: {env_name}")
417
+
418
+ try:
419
+ installer = CoreInstaller(cache_dir=cache_dir, env_name=env_name)
420
+ success, message = installer.create_environment(
421
+ config_filename="default.yaml",
422
+ dry_run=False,
423
+ )
424
+ await _append_log(task_id, message)
425
+
426
+ if success:
427
+ await _complete_current_step(task_id)
428
+ return True
429
+
430
+ await _fail_current_step(task_id, message)
431
+ return False
432
+ except Exception as e:
433
+ error_msg = f"Failed to create environment: {e}"
434
+ await _append_log(task_id, error_msg)
435
+ await _fail_current_step(task_id, error_msg)
436
+ return False
437
+
438
+
439
+ async def _install_drivers(
440
+ task_id: str, preset: str, env_name: str, cache_dir: str
441
+ ) -> bool:
442
+ """Install required drivers for the preset."""
443
+ await _append_log(task_id, f"Installing drivers for preset: {preset}")
444
+
445
+ try:
446
+ # Get required drivers
447
+ report = EnvironmentChecker.check_preset(preset)
448
+ missing_drivers = [
449
+ d
450
+ for d in report.drivers
451
+ if d.status.value != "available" and d.installable_via_mamba
452
+ ]
453
+
454
+ if not missing_drivers:
455
+ await _append_log(task_id, "All drivers already available")
456
+ await _complete_current_step(task_id)
457
+ return True
458
+
459
+ micromamba_installer = MicromambaInstaller(cache_dir)
460
+ micromamba_path = str(micromamba_installer.get_executable())
461
+ root_prefix = str(Path(cache_dir).expanduser() / "micromamba")
462
+ installer = DependencyInstaller(
463
+ micromamba_path=micromamba_path,
464
+ root_prefix=root_prefix,
465
+ )
466
+
467
+ for driver in missing_drivers:
468
+ await _append_log(task_id, f"Installing {driver.name}...")
469
+
470
+ success, message = installer.install_driver(
471
+ driver_name=driver.name,
472
+ env_name=env_name,
473
+ dry_run=False,
474
+ )
475
+
476
+ await _append_log(task_id, message)
477
+
478
+ if not success:
479
+ await _fail_current_step(task_id, f"Failed to install {driver.name}")
480
+ return False
481
+
482
+ await _complete_current_step(task_id)
483
+ return True
484
+
485
+ except Exception as e:
486
+ error_msg = f"Failed to install drivers: {e}"
487
+ await _append_log(task_id, error_msg)
488
+ await _fail_current_step(task_id, error_msg)
489
+ return False
490
+
491
+
492
+ async def _install_lumen_packages(task_id: str, cache_dir: str, preset: str) -> bool:
493
+ """Install Lumen packages derived from lumen-config.yaml.
494
+
495
+ Downloads wheels from GitHub Releases and installs with device-specific extras.
496
+ """
497
+ await _append_log(task_id, "Installing Lumen packages from GitHub Releases...")
498
+
499
+ try:
500
+ config_path = Path(cache_dir).expanduser() / "lumen-config.yaml"
501
+ if not config_path.exists():
502
+ message = f"Config file not found: {config_path}"
503
+ await _append_log(task_id, message)
504
+ await _fail_current_step(task_id, message)
505
+ return False
506
+
507
+ lumen_config: LumenConfig = load_and_validate_config(str(config_path))
508
+
509
+ # Get device config for the preset
510
+ device_config = PresetRegistry.create_config(preset)
511
+ await _append_log(task_id, f"Using preset: {preset}")
512
+
513
+ # Read region from config
514
+ region = lumen_config.metadata.region
515
+ await _append_log(task_id, f"Using region: {region.value}")
516
+
517
+ installer = CoreInstaller(cache_dir=cache_dir, region=region)
518
+
519
+ success, message = installer.install_lumen_packages(lumen_config, device_config)
520
+ await _append_log(task_id, message)
521
+
522
+ if success:
523
+ await _complete_current_step(task_id)
524
+ return True
525
+
526
+ await _fail_current_step(task_id, message)
527
+ return False
528
+ except Exception as e:
529
+ error_msg = f"Failed to install Lumen packages: {e}"
530
+ await _append_log(task_id, error_msg)
531
+ await _fail_current_step(task_id, error_msg)
532
+ return False
533
+
534
+
535
+ async def _verify_installation(task_id: str, cache_dir: str, env_name: str) -> bool:
536
+ """Verify installation status."""
537
+ await _append_log(task_id, "Verifying installation...")
538
+
539
+ try:
540
+ installer = CoreInstaller(cache_dir=cache_dir, env_name=env_name)
541
+ success, message = installer.verify_installation()
542
+ await _append_log(task_id, message)
543
+
544
+ if success:
545
+ await _complete_current_step(task_id)
546
+ return True
547
+
548
+ await _fail_current_step(task_id, message)
549
+ return False
550
+ except Exception as e:
551
+ error_msg = f"Failed to verify installation: {e}"
552
+ await _append_log(task_id, error_msg)
553
+ await _fail_current_step(task_id, error_msg)
554
+ return False
555
+
556
+
557
+ async def _update_task(task_id: str, **updates):
558
+ """Update task fields."""
559
+ task = await app_state.get_install_task(task_id)
560
+ if not task:
561
+ return
562
+
563
+ for key, value in updates.items():
564
+ if hasattr(task, key):
565
+ setattr(task, key, value)
566
+
567
+ task.updated_at = time.time()
568
+ await app_state.store_install_task(task_id, task)
569
+
570
+
571
+ async def _complete_current_step(task_id: str):
572
+ """Mark current running step as completed."""
573
+ task = await app_state.get_install_task(task_id)
574
+ if not task:
575
+ return
576
+
577
+ for step in task.steps:
578
+ if step.status == "running":
579
+ step.status = "completed"
580
+ step.progress = 100
581
+ step.completed_at = time.time()
582
+ break
583
+
584
+ await app_state.store_install_task(task_id, task)
585
+
586
+
587
+ async def _fail_current_step(task_id: str, error_msg: str):
588
+ """Mark current running step as failed."""
589
+ task = await app_state.get_install_task(task_id)
590
+ if not task:
591
+ return
592
+
593
+ for step in task.steps:
594
+ if step.status == "running":
595
+ step.status = "failed"
596
+ step.message = error_msg
597
+ step.completed_at = time.time()
598
+ break
599
+
600
+ await app_state.store_install_task(task_id, task)
601
+
602
+
603
+ async def _append_log(task_id: str, message: str):
604
+ """Append a log message to the task."""
605
+ timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
606
+ log_line = f"[{timestamp}] {message}"
607
+ await app_state.append_install_log(task_id, log_line)
608
+ logger.info(f"Task {task_id}: {message}")