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,309 @@
1
+ """Package resolver for Lumen installations.
2
+
3
+ This module provides utilities for resolving package URLs from GitHub Releases,
4
+ selecting appropriate mirrors based on region, and building pip installation commands.
5
+ """
6
+
7
+ import logging
8
+ import urllib.request
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ from lumen_resources.lumen_config import LumenConfig, Region
13
+
14
+ from ..core.config import DeviceConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MirrorSelector:
20
+ """Selects mirror URLs based on region."""
21
+
22
+ GITHUB_MIRROR_CN = "https://gh-proxy.org/https://github.com"
23
+ PYPI_MIRROR_CN = "https://mirrors.aliyun.com/pypi/simple/"
24
+
25
+ def get_github_urls(self, base_url: str, region: Region) -> list[str]:
26
+ """Get GitHub URLs with mirror fallback.
27
+
28
+ Args:
29
+ base_url: Original GitHub URL (e.g., https://github.com/...)
30
+ region: Region.cn or Region.other
31
+
32
+ Returns:
33
+ List of URLs to try (mirror first if cn, then original)
34
+ """
35
+ urls = []
36
+ if region == Region.cn:
37
+ # Apply ghproxy mirror
38
+ mirror_url = base_url.replace("https://github.com", self.GITHUB_MIRROR_CN)
39
+ urls.append(mirror_url)
40
+ urls.append(base_url)
41
+ return urls
42
+
43
+ def get_pypi_indexes(self, region: Region) -> list[str]:
44
+ """Get PyPI index URLs with mirror fallback.
45
+
46
+ Args:
47
+ region: Region.cn or Region.other
48
+
49
+ Returns:
50
+ List of index URLs (mirror first if cn, then original)
51
+ """
52
+ indexes = []
53
+ if region == Region.cn:
54
+ indexes.append(self.PYPI_MIRROR_CN)
55
+ # Always include PyPI official as fallback
56
+ indexes.append("https://pypi.org/simple/")
57
+ return indexes
58
+
59
+
60
+ class GitHubPackageResolver:
61
+ """Resolves package download URLs from GitHub Releases."""
62
+
63
+ REPO_OWNER = "EdwinZhanCN"
64
+ REPO_NAME = "Lumen"
65
+ API_BASE = "https://api.github.com"
66
+
67
+ def __init__(self, region: Region):
68
+ """Initialize resolver.
69
+
70
+ Args:
71
+ region: Region for mirror selection
72
+ """
73
+ self.region = region
74
+ self.mirror_selector = MirrorSelector()
75
+
76
+ def get_latest_release(self) -> str:
77
+ """Get latest release tag from GitHub API.
78
+
79
+ Returns:
80
+ Release tag (e.g., "v0.1.0")
81
+
82
+ Raises:
83
+ Exception: If API call fails
84
+ """
85
+ url = (
86
+ f"{self.API_BASE}/repos/{self.REPO_OWNER}/{self.REPO_NAME}/releases/latest"
87
+ )
88
+
89
+ logger.debug(f"[GitHubPackageResolver] Fetching latest release from {url}")
90
+
91
+ try:
92
+ with urllib.request.urlopen(url, timeout=30) as response:
93
+ import json
94
+
95
+ data = json.loads(response.read().decode())
96
+ tag = data.get("tag_name")
97
+ if not tag:
98
+ raise Exception("No tag_name found in release")
99
+ logger.info(f"[GitHubPackageResolver] Latest release: {tag}")
100
+ return tag
101
+ except Exception as e:
102
+ logger.error(f"[GitHubPackageResolver] Failed to fetch release: {e}")
103
+ raise Exception(f"Failed to fetch latest release: {e}")
104
+
105
+ def resolve_package_url(self, package_name: str) -> tuple[str, str]:
106
+ """Resolve package wheel download URL.
107
+
108
+ Args:
109
+ package_name: Package name (e.g., "lumen_ocr")
110
+
111
+ Returns:
112
+ Tuple of (download_url, version)
113
+
114
+ Raises:
115
+ Exception: If wheel file not found
116
+ """
117
+ # Get latest release tag
118
+ tag = self.get_latest_release()
119
+
120
+ # We need to get the actual wheel filename from the release assets
121
+ api_url = f"{self.API_BASE}/repos/{self.REPO_OWNER}/{self.REPO_NAME}/releases/tags/{tag}"
122
+
123
+ logger.debug(f"[GitHubPackageResolver] Fetching release assets from {api_url}")
124
+
125
+ try:
126
+ with urllib.request.urlopen(api_url, timeout=30) as response:
127
+ import json
128
+
129
+ data = json.loads(response.read().decode())
130
+ assets = data.get("assets", [])
131
+
132
+ # Find matching wheel
133
+ for asset in assets:
134
+ name = asset.get("name", "")
135
+ if name.startswith(package_name) and name.endswith(
136
+ "-py3-none-any.whl"
137
+ ):
138
+ download_url = asset.get("browser_download_url")
139
+ logger.info(f"[GitHubPackageResolver] Found wheel: {name}")
140
+ return download_url, tag
141
+
142
+ raise Exception(f"Wheel file not found for {package_name}")
143
+
144
+ except Exception as e:
145
+ logger.error(f"[GitHubPackageResolver] Failed to resolve package URL: {e}")
146
+ raise Exception(f"Failed to resolve package URL for {package_name}: {e}")
147
+
148
+ def download_wheel(
149
+ self,
150
+ url: str,
151
+ dest_dir: Path,
152
+ log_callback: Callable[[str], None] | None = None,
153
+ ) -> Path:
154
+ """Download wheel file to destination directory.
155
+
156
+ Args:
157
+ url: Download URL
158
+ dest_dir: Destination directory
159
+ log_callback: Optional callback for log messages
160
+
161
+ Returns:
162
+ Path to downloaded wheel file
163
+
164
+ Raises:
165
+ Exception: If download fails
166
+ """
167
+ dest_dir = Path(dest_dir)
168
+ dest_dir.mkdir(parents=True, exist_ok=True)
169
+
170
+ # Extract filename from URL
171
+ filename = url.split("/")[-1]
172
+ dest_path = dest_dir / filename
173
+
174
+ # Get URLs with mirror fallback
175
+ urls = self.mirror_selector.get_github_urls(url, self.region)
176
+
177
+ # Try each URL
178
+ for download_url in urls:
179
+ try:
180
+ if log_callback:
181
+ log_callback(f"Downloading {filename}...")
182
+
183
+ logger.debug(f"[GitHubPackageResolver] Downloading from {download_url}")
184
+
185
+ def download_progress(block_num, block_size, total_size):
186
+ if log_callback and total_size > 0:
187
+ progress = (block_num * block_size / total_size) * 100
188
+ if progress % 10 < 5: # Log every ~10%
189
+ log_callback(f"Downloading {filename}: {progress:.0f}%")
190
+
191
+ urllib.request.urlretrieve(download_url, dest_path, download_progress)
192
+
193
+ logger.info(f"[GitHubPackageResolver] Downloaded: {dest_path}")
194
+ if log_callback:
195
+ log_callback(f"Downloaded {filename}")
196
+
197
+ return dest_path
198
+
199
+ except Exception as e:
200
+ logger.warning(
201
+ f"[GitHubPackageResolver] Failed to download from {download_url}: {e}"
202
+ )
203
+ if log_callback:
204
+ log_callback(f"Failed: {e}, trying next source...")
205
+ continue
206
+
207
+ raise Exception(f"Failed to download wheel from all sources: {filename}")
208
+
209
+
210
+ class LumenPackageResolver:
211
+ """Resolves Lumen package names and installation commands."""
212
+
213
+ @staticmethod
214
+ def resolve_packages(lumen_config: LumenConfig) -> list[str]:
215
+ """Extract package names from LumenConfig.
216
+
217
+ Args:
218
+ lumen_config: Lumen configuration
219
+
220
+ Returns:
221
+ List of package names (e.g., ["lumen_ocr", "lumen_clip"])
222
+ """
223
+ packages = []
224
+
225
+ # Get deployment services
226
+ deployment = lumen_config.deployment
227
+
228
+ if hasattr(deployment, "services") and deployment.services:
229
+ for service in deployment.services:
230
+ if hasattr(service, "root"):
231
+ root = service.root
232
+ package_name = f"lumen_{root}"
233
+ packages.append(package_name)
234
+
235
+ logger.info(f"[LumenPackageResolver] Resolved packages: {packages}")
236
+ return list(set(packages)) # Remove duplicates
237
+
238
+ @staticmethod
239
+ def build_pip_install_args(
240
+ packages: list[str],
241
+ device_config: DeviceConfig,
242
+ region: Region,
243
+ wheel_paths: dict[str, Path] | None = None,
244
+ ) -> list[str]:
245
+ """Build pip installation command arguments.
246
+
247
+ Args:
248
+ packages: List of package names (e.g., ["lumen_ocr", "lumen_clip"])
249
+ device_config: Device configuration with dependency metadata
250
+ region: Region for mirror selection
251
+ wheel_paths: Dictionary mapping package names to wheel file paths
252
+
253
+ Returns:
254
+ List of pip command arguments
255
+
256
+ Example:
257
+ ["install", "--index-url", "...", "--extra-index-url", "...",
258
+ "/path/to/lumen_ocr-0.4.1-py3-none-any.whl[apple]", "--no-cache-dir"]
259
+ """
260
+ args = ["install"]
261
+
262
+ # Add index URLs
263
+ mirror_selector = MirrorSelector()
264
+ indexes = mirror_selector.get_pypi_indexes(region)
265
+
266
+ # Add --index-url (first mirror)
267
+ if indexes:
268
+ args.extend(["--index-url", indexes[0]])
269
+
270
+ # Add custom extra index URLs (for CUDA, etc.)
271
+ # NOTE: Don't add PyPI as extra-index-url automatically to preserve mirror speed
272
+ if device_config.dependency_metadata:
273
+ meta = device_config.dependency_metadata
274
+
275
+ # Add custom extra index URLs
276
+ if meta.extra_index_url:
277
+ for url in meta.extra_index_url:
278
+ args.extend(["--extra-index-url", url])
279
+
280
+ # Add wheel file paths with extras
281
+ if wheel_paths:
282
+ for pkg in packages:
283
+ if pkg in wheel_paths:
284
+ wheel_path = str(wheel_paths[pkg])
285
+
286
+ # Add extras if specified
287
+ if (
288
+ device_config.dependency_metadata
289
+ and device_config.dependency_metadata.extra_deps
290
+ ):
291
+ extra_deps = ",".join(
292
+ device_config.dependency_metadata.extra_deps
293
+ )
294
+ wheel_path = f"{wheel_path}[{extra_deps}]"
295
+
296
+ args.append(wheel_path)
297
+
298
+ # Add additional install args
299
+ if (
300
+ device_config.dependency_metadata
301
+ and device_config.dependency_metadata.install_args
302
+ ):
303
+ args.extend(device_config.dependency_metadata.install_args)
304
+
305
+ # Add --no-cache-dir to avoid cache issues
306
+ args.append("--no-cache-dir")
307
+
308
+ logger.debug(f"[LumenPackageResolver] Pip args: {args}")
309
+ return args
@@ -0,0 +1,219 @@
1
+ """
2
+ Preset registry for Lumen device configurations.
3
+
4
+ Provides dynamic discovery of available presets from DeviceConfig,
5
+ eliminating hardcoded preset identifiers.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import inspect
11
+ from dataclasses import dataclass
12
+ from typing import Callable
13
+
14
+ from lumen_app.core.config import DeviceConfig
15
+ from lumen_app.utils.logger import get_logger
16
+
17
+ logger = get_logger("lumen.preset_registry")
18
+
19
+
20
+ @dataclass
21
+ class PresetInfo:
22
+ """Information about a device preset."""
23
+
24
+ name: str
25
+ description: str
26
+ factory_method: Callable[[], DeviceConfig]
27
+ requires_drivers: bool = True
28
+ priority: int = 50 # Lower number = higher priority (0-100)
29
+
30
+
31
+ class PresetRegistry:
32
+ """
33
+ Dynamic registry of DeviceConfig presets.
34
+
35
+ Automatically discovers all factory methods (classmethods) from DeviceConfig
36
+ that return DeviceConfig instances, eliminating the need for hardcoded preset names.
37
+ """
38
+
39
+ _presets: dict[str, PresetInfo] | None = None
40
+
41
+ # Preset priority mapping (lower = higher priority)
42
+ _PRESET_PRIORITIES = {
43
+ "nvidia_gpu_high": 5,
44
+ "nvidia_gpu": 10,
45
+ "nvidia_jetson_high": 12,
46
+ "nvidia_jetson": 15,
47
+ "apple_silicon": 20,
48
+ "rockchip": 25,
49
+ "intel_gpu": 30,
50
+ "amd_gpu_win": 35,
51
+ "amd_npu": 40,
52
+ "cpu": 100, # Lowest priority (fallback)
53
+ }
54
+
55
+ @classmethod
56
+ def _discover_presets(cls) -> dict[str, PresetInfo]:
57
+ """Discover all preset factory methods from DeviceConfig."""
58
+ if cls._presets is not None:
59
+ return cls._presets
60
+
61
+ presets = {}
62
+
63
+ # Scan all members of DeviceConfig (including bound methods)
64
+ for name in dir(DeviceConfig):
65
+ # Skip private attributes
66
+ if name.startswith("_"):
67
+ continue
68
+
69
+ # Get the member
70
+ try:
71
+ member = getattr(DeviceConfig, name)
72
+ except AttributeError:
73
+ continue
74
+
75
+ # Skip non-methods and dataclass fields
76
+ if not callable(member):
77
+ continue
78
+
79
+ # Check if it's a classmethod descriptor
80
+ is_classmethod = False
81
+ try:
82
+ raw_attr = inspect.getattr_static(DeviceConfig, name)
83
+ is_classmethod = isinstance(raw_attr, classmethod)
84
+ except AttributeError:
85
+ pass
86
+
87
+ if not is_classmethod:
88
+ continue
89
+
90
+ # Try to call it and see if it returns a DeviceConfig
91
+ try:
92
+ result = member()
93
+ if isinstance(result, DeviceConfig):
94
+ # Get description from DeviceConfig.description or docstring
95
+ if hasattr(result, "description") and result.description:
96
+ description = result.description
97
+ else:
98
+ docstring = inspect.getdoc(member) or ""
99
+ description = docstring.split("\n")[0] if docstring else name
100
+
101
+ # Get priority from mapping, default to 50
102
+ priority = cls._PRESET_PRIORITIES.get(name, 50)
103
+
104
+ presets[name] = PresetInfo(
105
+ name=name,
106
+ description=description,
107
+ factory_method=member,
108
+ requires_drivers=name
109
+ != "cpu", # CPU preset requires no special drivers
110
+ priority=priority,
111
+ )
112
+ logger.debug(f"Discovered preset: {name} (priority: {priority})")
113
+ except Exception as e:
114
+ logger.debug(f"Skipping {name}: {e}")
115
+ continue
116
+
117
+ cls._presets = presets
118
+ logger.info(f"Discovered {len(presets)} device presets")
119
+ return presets
120
+
121
+ @classmethod
122
+ def get_all_presets(cls) -> dict[str, PresetInfo]:
123
+ """Get all available presets."""
124
+ return cls._discover_presets().copy()
125
+
126
+ @classmethod
127
+ def get_preset(cls, name: str) -> PresetInfo | None:
128
+ """Get a specific preset by name."""
129
+ presets = cls._discover_presets()
130
+ return presets.get(name)
131
+
132
+ @classmethod
133
+ def preset_exists(cls, name: str) -> bool:
134
+ """Check if a preset exists."""
135
+ return name in cls._discover_presets()
136
+
137
+ @classmethod
138
+ def get_preset_names(cls) -> list[str]:
139
+ """Get list of all preset names."""
140
+ return list(cls._discover_presets().keys())
141
+
142
+ @classmethod
143
+ def get_detection_order(cls) -> list[str]:
144
+ """
145
+ Get preset names in detection order (highest to lowest priority).
146
+
147
+ Returns presets sorted by priority, with lower numbers checked first.
148
+ This ensures the best available hardware is detected first.
149
+ """
150
+ presets = cls._discover_presets()
151
+ # Sort by priority (lower number = higher priority)
152
+ sorted_presets = sorted(presets.values(), key=lambda p: p.priority)
153
+ return [p.name for p in sorted_presets]
154
+
155
+ @classmethod
156
+ def create_config(cls, preset_name: str) -> DeviceConfig:
157
+ """
158
+ Create a DeviceConfig from preset name.
159
+
160
+ Args:
161
+ preset_name: Name of the preset (e.g., "nvidia_gpu", "apple_silicon")
162
+
163
+ Returns:
164
+ DeviceConfig instance
165
+
166
+ Raises:
167
+ ValueError: If preset doesn't exist
168
+ """
169
+ preset = cls.get_preset(preset_name)
170
+ if preset is None:
171
+ available = ", ".join(cls.get_preset_names())
172
+ raise ValueError(
173
+ f"Unknown preset '{preset_name}'. Available presets: {available}"
174
+ )
175
+
176
+ return preset.factory_method()
177
+
178
+ @classmethod
179
+ def get_driver_requirements(cls, preset_name: str) -> list[str]:
180
+ """
181
+ Get required driver checks for a preset.
182
+
183
+ Args:
184
+ preset_name: Name of the preset
185
+
186
+ Returns:
187
+ List of driver names to check
188
+ """
189
+ preset = cls.get_preset(preset_name)
190
+ if preset is None or not preset.requires_drivers:
191
+ return []
192
+
193
+ config = preset.factory_method()
194
+
195
+ # Map runtime/providers to required drivers
196
+ requirements = []
197
+ providers = config.onnx_providers or []
198
+
199
+ for provider in providers:
200
+ provider_name = (
201
+ provider
202
+ if isinstance(provider, str)
203
+ else (provider[0] if provider else "")
204
+ )
205
+
206
+ if "CUDA" in provider_name or "TensorRT" in provider_name:
207
+ requirements.append("cuda")
208
+ elif "CoreML" in provider_name:
209
+ requirements.append("coreml")
210
+ elif "OpenVINO" in provider_name:
211
+ requirements.append("openvino")
212
+ elif "DML" in provider_name:
213
+ requirements.append("directml")
214
+
215
+ # RKNN runtime
216
+ if config.runtime.value == "rknn":
217
+ requirements.append("rknn")
218
+
219
+ return list(set(requirements)) # Remove duplicates
@@ -0,0 +1,3 @@
1
+ """Lumen Web API - FastAPI-based web interface for Lumen AI services."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """API routers for Lumen Web."""