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.
- lumen_app/__init__.py +7 -0
- lumen_app/core/__init__.py +0 -0
- lumen_app/core/config.py +661 -0
- lumen_app/core/installer.py +274 -0
- lumen_app/core/loader.py +45 -0
- lumen_app/core/router.py +87 -0
- lumen_app/core/server.py +389 -0
- lumen_app/core/service.py +49 -0
- lumen_app/core/tests/__init__.py +1 -0
- lumen_app/core/tests/test_core_integration.py +561 -0
- lumen_app/core/tests/test_env_checker.py +487 -0
- lumen_app/proto/README.md +12 -0
- lumen_app/proto/ml_service.proto +88 -0
- lumen_app/proto/ml_service_pb2.py +66 -0
- lumen_app/proto/ml_service_pb2.pyi +136 -0
- lumen_app/proto/ml_service_pb2_grpc.py +251 -0
- lumen_app/server.py +362 -0
- lumen_app/utils/env_checker.py +752 -0
- lumen_app/utils/installation/__init__.py +25 -0
- lumen_app/utils/installation/env_manager.py +152 -0
- lumen_app/utils/installation/micromamba_installer.py +459 -0
- lumen_app/utils/installation/package_installer.py +149 -0
- lumen_app/utils/installation/verifier.py +95 -0
- lumen_app/utils/logger.py +181 -0
- lumen_app/utils/mamba/cuda.yaml +12 -0
- lumen_app/utils/mamba/default.yaml +6 -0
- lumen_app/utils/mamba/openvino.yaml +7 -0
- lumen_app/utils/mamba/tensorrt.yaml +13 -0
- lumen_app/utils/package_resolver.py +309 -0
- lumen_app/utils/preset_registry.py +219 -0
- lumen_app/web/__init__.py +3 -0
- lumen_app/web/api/__init__.py +1 -0
- lumen_app/web/api/config.py +229 -0
- lumen_app/web/api/hardware.py +201 -0
- lumen_app/web/api/install.py +608 -0
- lumen_app/web/api/server.py +253 -0
- lumen_app/web/core/__init__.py +1 -0
- lumen_app/web/core/server_manager.py +348 -0
- lumen_app/web/core/state.py +264 -0
- lumen_app/web/main.py +145 -0
- lumen_app/web/models/__init__.py +28 -0
- lumen_app/web/models/config.py +63 -0
- lumen_app/web/models/hardware.py +64 -0
- lumen_app/web/models/install.py +134 -0
- lumen_app/web/models/server.py +95 -0
- lumen_app/web/static/assets/index-CGuhGHC9.css +1 -0
- lumen_app/web/static/assets/index-DN6HmxWS.js +56 -0
- lumen_app/web/static/index.html +14 -0
- lumen_app/web/static/vite.svg +1 -0
- lumen_app/web/websockets/__init__.py +1 -0
- lumen_app/web/websockets/logs.py +159 -0
- lumen_app-0.4.2.dist-info/METADATA +23 -0
- lumen_app-0.4.2.dist-info/RECORD +56 -0
- lumen_app-0.4.2.dist-info/WHEEL +5 -0
- lumen_app-0.4.2.dist-info/entry_points.txt +3 -0
- 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
|
lumen_app/core/loader.py
ADDED
|
@@ -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
|
lumen_app/core/router.py
ADDED
|
@@ -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()
|