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,274 @@
1
+ """
2
+ Core installer for managing environment and driver installations.
3
+
4
+ This module provides installation functionality including:
5
+ - Micromamba installation
6
+ - Python environment setup via micromamba
7
+ - Driver installation via micromamba
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import Iterable
15
+
16
+ import yaml
17
+ from lumen_resources import LumenConfig
18
+ from lumen_resources.lumen_config import Region
19
+
20
+ from lumen_app.core.config import DeviceConfig
21
+ from lumen_app.utils.env_checker import DependencyInstaller
22
+ from lumen_app.utils.installation import MicromambaInstaller, PythonEnvManager
23
+ from lumen_app.utils.logger import get_logger
24
+ from lumen_app.utils.package_resolver import (
25
+ GitHubPackageResolver,
26
+ LumenPackageResolver,
27
+ )
28
+
29
+ logger = get_logger("lumen.core.installer")
30
+
31
+
32
+ class CoreInstaller:
33
+ """Core installer that manages micromamba and environment setup."""
34
+
35
+ def __init__(
36
+ self,
37
+ cache_dir: str | Path,
38
+ env_name: str = "lumen_env",
39
+ mamba_configs_dir: str | Path | None = None,
40
+ micromamba_target: str = "micromamba",
41
+ region: Region = Region.other,
42
+ ) -> None:
43
+ self.cache_dir = Path(cache_dir).expanduser()
44
+ self.env_name = env_name
45
+ self.mamba_configs_dir = Path(mamba_configs_dir) if mamba_configs_dir else None
46
+ self.micromamba_target = micromamba_target
47
+ self.region = region
48
+
49
+ @property
50
+ def micromamba_exe(self) -> str:
51
+ installer = MicromambaInstaller(self.cache_dir)
52
+ return str(installer.get_executable())
53
+
54
+ @property
55
+ def root_prefix(self) -> str:
56
+ return str(self.cache_dir / self.micromamba_target)
57
+
58
+ def install_micromamba(self, dry_run: bool = False) -> tuple[bool, str]:
59
+ """Install micromamba into cache_dir."""
60
+ logger.info("Installing micromamba...")
61
+ try:
62
+ installer = MicromambaInstaller(self.cache_dir)
63
+ exe_path = installer.install(dry_run=dry_run)
64
+ return True, f"Micromamba installed successfully at {exe_path}"
65
+ except Exception as e:
66
+ logger.error("Failed to install micromamba: %s", e)
67
+ return False, f"Failed to install micromamba: {e}"
68
+
69
+ def create_environment(
70
+ self,
71
+ config_filename: str = "default.yaml",
72
+ dry_run: bool = False,
73
+ ) -> tuple[bool, str]:
74
+ """Create Python environment using micromamba and a config file."""
75
+ logger.info(
76
+ "Creating environment '%s' with config '%s'",
77
+ self.env_name,
78
+ config_filename,
79
+ )
80
+
81
+ if dry_run:
82
+ logger.info("Dry run mode - skipping execution")
83
+ return True, f"Would create environment {self.env_name}"
84
+
85
+ try:
86
+ # Extract yaml_config name from filename (e.g., "default.yaml" -> "default")
87
+ yaml_config = config_filename.replace(".yaml", "")
88
+
89
+ env_manager = PythonEnvManager(
90
+ cache_dir=self.cache_dir,
91
+ micromamba_exe=self.micromamba_exe,
92
+ )
93
+ env_path = env_manager.create_env(yaml_config=yaml_config)
94
+ logger.info(
95
+ "Successfully created environment %s at %s", self.env_name, env_path
96
+ )
97
+ return True, f"Successfully created environment {self.env_name}"
98
+
99
+ except Exception as e:
100
+ logger.error("Environment creation error: %s: %s", type(e).__name__, e)
101
+ return False, f"Environment creation error: {str(e)}"
102
+
103
+ def install_lumen_packages(
104
+ self,
105
+ lumen_config: LumenConfig,
106
+ device_config: DeviceConfig,
107
+ quiet: bool = True,
108
+ ) -> tuple[bool, str]:
109
+ """Install Lumen packages derived from LumenConfig.
110
+
111
+ Downloads wheels from GitHub Releases and installs them with proper
112
+ dependencies based on device configuration.
113
+
114
+ Args:
115
+ lumen_config: Lumen configuration with deployment services
116
+ device_config: Device configuration with dependency metadata
117
+ quiet: Whether to suppress pip output
118
+
119
+ Returns:
120
+ Tuple of (success, message)
121
+ """
122
+ logger.info("Installing Lumen packages from GitHub Releases")
123
+
124
+ # Check environment exists
125
+ env_manager = PythonEnvManager(
126
+ cache_dir=self.cache_dir,
127
+ micromamba_exe=self.micromamba_exe,
128
+ )
129
+
130
+ if not env_manager.env_exists():
131
+ return False, f"Environment not found at {env_manager.get_env_path()}"
132
+
133
+ try:
134
+ # Resolve package names from config
135
+ package_list = LumenPackageResolver.resolve_packages(lumen_config)
136
+
137
+ if not package_list:
138
+ logger.info("No packages to install.")
139
+ return True, "No packages to install"
140
+
141
+ logger.info("Packages to install: %s", ", ".join(package_list))
142
+
143
+ # Create wheel download directory
144
+ wheel_dir = self.cache_dir / "wheels"
145
+ wheel_dir.mkdir(parents=True, exist_ok=True)
146
+
147
+ # Initialize GitHub resolver
148
+ github_resolver = GitHubPackageResolver(region=self.region)
149
+
150
+ # Download all wheels first
151
+ wheel_paths = {}
152
+ for package in package_list:
153
+ logger.info("Downloading wheel for %s...", package)
154
+ try:
155
+ url, version = github_resolver.resolve_package_url(package)
156
+ wheel_path = github_resolver.download_wheel(url, wheel_dir)
157
+ wheel_paths[package] = wheel_path
158
+ logger.info("Downloaded %s version %s", package, version)
159
+ except Exception as e:
160
+ logger.error("Failed to download %s: %s", package, e)
161
+ return False, f"Failed to download {package}: {e}"
162
+
163
+ # Build pip install command with device-specific extras
164
+ pip_args = LumenPackageResolver.build_pip_install_args(
165
+ packages=package_list,
166
+ device_config=device_config,
167
+ region=self.region,
168
+ wheel_paths=wheel_paths,
169
+ )
170
+
171
+ if quiet:
172
+ pip_args.extend(["--quiet", "--no-warn-script-location"])
173
+
174
+ # Run pip install
175
+ logger.info("Running pip install with device-specific extras")
176
+ result = env_manager.run_pip(*pip_args)
177
+
178
+ if result.returncode == 0:
179
+ logger.info("All packages installed successfully")
180
+ return True, "All packages installed successfully"
181
+ else:
182
+ error_msg = result.stderr or result.stdout
183
+ logger.error("Package installation failed: %s", error_msg)
184
+ return False, f"Package installation failed: {error_msg}"
185
+
186
+ except subprocess.TimeoutExpired:
187
+ logger.error("Package installation timed out")
188
+ return False, "Package installation timed out"
189
+ except Exception as e:
190
+ logger.error("Package installation error: %s", e)
191
+ return False, f"Package installation error: {e}"
192
+
193
+ def verify_installation(self) -> tuple[bool, str]:
194
+ """Verify micromamba and environment are installed."""
195
+ try:
196
+ # Check micromamba
197
+ installer = MicromambaInstaller(self.cache_dir)
198
+ check_result = installer.check()
199
+
200
+ if check_result.status.value != "installed":
201
+ return False, "Micromamba not found"
202
+
203
+ # Check environment
204
+ env_manager = PythonEnvManager(
205
+ cache_dir=self.cache_dir,
206
+ micromamba_exe=self.micromamba_exe,
207
+ )
208
+
209
+ if not env_manager.env_exists():
210
+ return False, "Python environment not found"
211
+
212
+ # Optional: check uv availability
213
+ env_path = env_manager.get_env_path()
214
+ cmd = [
215
+ str(self.micromamba_exe),
216
+ "run",
217
+ "-p",
218
+ str(env_path),
219
+ "uv",
220
+ "--version",
221
+ ]
222
+ try:
223
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=10)
224
+ if result.returncode == 0:
225
+ logger.info("uv installed: %s", result.stdout.strip())
226
+ except subprocess.TimeoutExpired:
227
+ logger.warning("uv check timed out")
228
+
229
+ return True, "Installation verified"
230
+ except Exception as e:
231
+ logger.error("Verification error: %s", e)
232
+ return False, f"Verification error: {e}"
233
+
234
+ def save_config(
235
+ self, lumen_config: LumenConfig, config_filename: str = "lumen-config.yaml"
236
+ ) -> tuple[bool, str]:
237
+ """Persist LumenConfig to disk."""
238
+ config_path = self.cache_dir / config_filename
239
+
240
+ try:
241
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
242
+ config_dict = lumen_config.model_dump(mode="json")
243
+ with open(config_path, "w", encoding="utf-8") as f:
244
+ yaml.dump(config_dict, f, default_flow_style=False, allow_unicode=True)
245
+ return True, f"Config saved: {config_path}"
246
+ except Exception as e:
247
+ logger.error("Failed to save config: %s", e)
248
+ return False, f"Failed to save config: {e}"
249
+
250
+ def install_drivers(
251
+ self, driver_names: Iterable[str], dry_run: bool = False
252
+ ) -> list[tuple[str, bool, str]]:
253
+ """Install driver packages using micromamba.
254
+
255
+ Returns:
256
+ List of (driver_name, success, message)
257
+ """
258
+ installer = DependencyInstaller(
259
+ mamba_configs_dir=self.mamba_configs_dir,
260
+ micromamba_path=self.micromamba_exe,
261
+ root_prefix=self.root_prefix,
262
+ )
263
+
264
+ results: list[tuple[str, bool, str]] = []
265
+ for name in driver_names:
266
+ logger.info("Installing driver '%s'...", name)
267
+ success, message = installer.install_driver(
268
+ driver_name=name,
269
+ env_name=self.env_name,
270
+ dry_run=dry_run,
271
+ )
272
+ results.append((name, success, message))
273
+
274
+ return results
@@ -0,0 +1,45 @@
1
+ import importlib
2
+ from typing import Any, Type
3
+
4
+ from lumen_app.utils.logger import get_logger
5
+
6
+ logger = get_logger("lumen.loader")
7
+
8
+
9
+ class ServiceLoader:
10
+ """
11
+ 负责从字符串路径动态加载 Python 类。
12
+ 例如将 "lumen_ocr.registry.GeneralOcrService" 转换为可调用的类对象。
13
+ """
14
+
15
+ @staticmethod
16
+ def get_class(class_path: str) -> Type[Any]:
17
+ """
18
+ 根据类路径字符串获取类对象。
19
+
20
+ Args:
21
+ class_path: 格式为 'package.module.ClassName'
22
+ """
23
+ if not class_path or "." not in class_path:
24
+ raise ValueError(f"Invalid class path: {class_path}")
25
+
26
+ # 1. 拆分路径,例如:'lumen_ocr.registry' 和 'GeneralOcrService'
27
+ module_path, class_name = class_path.rsplit(".", 1)
28
+
29
+ try:
30
+ # 2. 动态导入模块
31
+ # 注意:此时 subprocess 必须运行在已安装该包的 micromamba 环境中
32
+ module = importlib.import_module(module_path)
33
+
34
+ # 3. 从模块中获取类
35
+ cls = getattr(module, class_name)
36
+
37
+ logger.info(f"Successfully imported class: {class_name} from {module_path}")
38
+ return cls
39
+
40
+ except ImportError as e:
41
+ logger.error(f"Failed to import module {module_path}: {e}")
42
+ raise
43
+ except AttributeError as e:
44
+ logger.error(f"Class {class_name} not found in module {module_path}: {e}")
45
+ raise
@@ -0,0 +1,87 @@
1
+ # router.py
2
+ import grpc
3
+
4
+ from lumen_app.proto import ml_service_pb2, ml_service_pb2_grpc
5
+ from lumen_app.utils.logger import get_logger
6
+
7
+ logger = get_logger("lumen.router")
8
+
9
+
10
+ class HubRouter(ml_service_pb2_grpc.InferenceServicer):
11
+ def __init__(self, services: list):
12
+ self.services = services
13
+ # 建立 Task Key -> Service 实例的映射
14
+ self._route_table = {}
15
+ for svc in services:
16
+ for task_key in svc.get_supported_tasks():
17
+ # 如果 key 已存在,这里可以选择附加到列表或简单的覆盖
18
+ # 既然你说交给 SDK 判断,Hub 这里默认选择第一个匹配的服务
19
+ if task_key not in self._route_table:
20
+ self._route_table[task_key] = svc
21
+
22
+ def Infer(self, request_iterator, context):
23
+ """多路复用分发推理请求"""
24
+ # 获取流的第一条消息以识别 Task
25
+ try:
26
+ first_req = next(request_iterator)
27
+ except StopIteration:
28
+ return
29
+
30
+ task_key = first_req.task
31
+
32
+ target_svc = self._route_table.get(task_key)
33
+
34
+ if not target_svc:
35
+ context.abort(grpc.StatusCode.NOT_FOUND, f"Task {task_key} not supported")
36
+
37
+ if target_svc is not None:
38
+ # 构造包装后的迭代器透传给子服务
39
+ def stream_wrapper():
40
+ yield first_req
41
+ for req in request_iterator:
42
+ yield req
43
+
44
+ # 零拷贝转发流式响应
45
+ for resp in target_svc.Infer(stream_wrapper(), context):
46
+ yield resp
47
+
48
+ def GetCapabilities(self, request, context):
49
+ """汇总所有子服务的能力宣告"""
50
+ all_tasks = []
51
+ for svc in self.services:
52
+ caps = svc.GetCapabilities(request, context)
53
+ all_tasks.extend(caps.tasks)
54
+ return ml_service_pb2.Capability(tasks=all_tasks)
55
+
56
+ def attach_to_server(self, server: grpc.Server):
57
+ """
58
+ Attach the hub router to the gRPC server.
59
+
60
+ This registers the router as the single InferenceServicer that handles
61
+ all incoming requests and routes them to appropriate services.
62
+
63
+ Args:
64
+ server: The gRPC server instance to attach to
65
+ """
66
+ ml_service_pb2_grpc.add_InferenceServicer_to_server(self, server)
67
+ logger.info(
68
+ f"HubRouter attached to server with {len(self.services)} service(s)"
69
+ )
70
+ logger.debug(f"Route table: {list(self._route_table.keys())}")
71
+
72
+ def Health(self, request, context):
73
+ """健康检查 - 所有子服务都健康才返回健康"""
74
+ from google.protobuf import empty_pb2
75
+
76
+ for svc in self.services:
77
+ # 调用子服务的 Health 方法
78
+ try:
79
+ svc.Health(empty_pb2.Empty(), context)
80
+ except Exception as e:
81
+ # 如果任何子服务健康检查失败,返回错误
82
+ context.abort(
83
+ grpc.StatusCode.UNAVAILABLE, f"Service unhealthy: {str(e)}"
84
+ )
85
+
86
+ # 所有服务都健康,返回 Empty
87
+ return empty_pb2.Empty()