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,389 @@
1
+ """
2
+ Server startup module for Lumen App Service (Hub Mode).
3
+
4
+ This module provides the main server initialization and startup logic for running
5
+ multiple Lumen services in hub mode, integrating with lumen-resources for
6
+ configuration and model loading.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import logging
13
+ import os
14
+ import signal
15
+ import socket
16
+ import sys
17
+ import uuid
18
+ from concurrent import futures
19
+ from typing import Any
20
+
21
+ import colorlog
22
+ import grpc
23
+ from lumen_resources import Downloader, DownloadResult, load_and_validate_config
24
+ from lumen_resources.lumen_config import LumenConfig, Mdns
25
+
26
+ from lumen_app.core.service import AppService
27
+ from lumen_app.utils.logger import get_logger
28
+
29
+ try:
30
+ from zeroconf import ServiceInfo, Zeroconf
31
+ except ImportError:
32
+ ServiceInfo = None # type: ignore
33
+ Zeroconf = None # type: ignore
34
+
35
+ logger = get_logger(__name__)
36
+
37
+
38
+ class ConfigError(Exception):
39
+ """Raised when configuration is invalid."""
40
+
41
+ pass
42
+
43
+
44
+ def setup_logging(log_level: str = "INFO"):
45
+ """
46
+ Configure the root logger for the application.
47
+
48
+ This function clears any pre-existing handlers, sets the requested log
49
+ level, and attaches a single colorized stream handler for console output.
50
+ """
51
+ root_logger = logging.getLogger()
52
+ root_logger.setLevel(log_level)
53
+
54
+ # Remove any handlers that may have been pre-configured
55
+ for handler in root_logger.handlers[:]:
56
+ root_logger.removeHandler(handler)
57
+
58
+ # Add a colorized console handler
59
+ handler = colorlog.StreamHandler()
60
+ formatter = colorlog.ColoredFormatter(
61
+ "%(log_color)s%(levelname)-8s%(cyan)s[%(name)s]%(reset)s %(message)s",
62
+ reset=True,
63
+ log_colors={
64
+ "DEBUG": "cyan",
65
+ "INFO": "green",
66
+ "WARNING": "yellow",
67
+ "ERROR": "red",
68
+ "CRITICAL": "red,bg_white",
69
+ },
70
+ )
71
+ handler.setFormatter(formatter)
72
+ root_logger.addHandler(handler)
73
+
74
+
75
+ def setup_mdns(port: int, mdns_config: Mdns | None) -> tuple[Any, Any]:
76
+ """
77
+ Set up mDNS advertisement for hub service.
78
+
79
+ Args:
80
+ port: Service port number
81
+ mdns_config: mDNS configuration
82
+
83
+ Returns:
84
+ Tuple of (zeroconf, service_info) or (None, None) if setup fails
85
+ """
86
+ if Zeroconf is None or ServiceInfo is None:
87
+ logger.warning("zeroconf package not installed, mDNS unavailable")
88
+ return None, None
89
+
90
+ try:
91
+ # Determine advertised IP
92
+ ip = os.getenv("ADVERTISE_IP")
93
+ if not ip:
94
+ try:
95
+ # Best-effort LAN IP detection
96
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
97
+ s.connect(("8.8.8.8", 80))
98
+ ip = s.getsockname()[0]
99
+ s.close()
100
+ except Exception:
101
+ ip = socket.gethostbyname(socket.gethostname())
102
+
103
+ if ip.startswith("127."):
104
+ logger.warning(
105
+ f"mDNS advertising loopback IP {ip}; "
106
+ "other devices may not reach the service. "
107
+ "Set ADVERTISE_IP to a LAN IP."
108
+ )
109
+
110
+ # Build TXT record properties
111
+ props = {
112
+ "uuid": os.getenv("SERVICE_UUID", str(uuid.uuid4())),
113
+ "status": os.getenv("SERVICE_STATUS", "ready"),
114
+ "version": os.getenv("SERVICE_VERSION", "1.0.0"),
115
+ }
116
+
117
+ # Get service type and name from config
118
+ service_type = (
119
+ getattr(mdns_config, "type", None)
120
+ or getattr(mdns_config, "service_type", None)
121
+ or "_lumen._tcp.local."
122
+ )
123
+ instance_name = (
124
+ getattr(mdns_config, "name", None)
125
+ or getattr(mdns_config, "service_name", None)
126
+ or "Lumen-Hub"
127
+ )
128
+ full_name = f"{instance_name}.{service_type}"
129
+
130
+ # Create service info
131
+ service_info = ServiceInfo(
132
+ type_=service_type,
133
+ name=full_name,
134
+ addresses=[socket.inet_aton(ip)],
135
+ port=port,
136
+ properties=props,
137
+ server=f"{socket.gethostname()}.local.",
138
+ )
139
+
140
+ # Register service
141
+ zeroconf = Zeroconf()
142
+ zeroconf.register_service(service_info)
143
+ logger.info(f"✓ mDNS advertised: {full_name} at {ip}:{port}")
144
+
145
+ return zeroconf, service_info
146
+
147
+ except Exception as e:
148
+ logger.warning(f"mDNS advertisement failed: {e}")
149
+ return None, None
150
+
151
+
152
+ def handle_download_results(results: dict[str, DownloadResult]):
153
+ """
154
+ Processes download results, logs them, and exits if critical failures occurred.
155
+
156
+ Args:
157
+ results: Dictionary of model_type to DownloadResult
158
+ """
159
+ successful_downloads = []
160
+ failed_downloads = []
161
+
162
+ for _model_type, result in results.items():
163
+ if result.success:
164
+ successful_downloads.append(result)
165
+ else:
166
+ failed_downloads.append(result)
167
+
168
+ # CRITICAL: If any model failed to download, abort the server startup.
169
+ if failed_downloads:
170
+ logger.error(
171
+ "💥 Critical error: Model download failed. Cannot start the server."
172
+ )
173
+ for res in failed_downloads:
174
+ logger.error(f" - Model '{res.model_type}': {res.error}")
175
+ sys.exit(1)
176
+
177
+ # If all downloads were successful, log a summary.
178
+ logger.info("✅ All required models are successfully downloaded and verified.")
179
+ for res in successful_downloads:
180
+ # Also check for non-critical warnings, like missing optional files.
181
+ if res.missing_files:
182
+ logger.warning(
183
+ f" - Model '{res.model_type}' is ready, but has missing optional files: "
184
+ f"{', '.join(res.missing_files)}"
185
+ )
186
+
187
+
188
+ def serve(config_path: str, port_override: int | None = None) -> None:
189
+ """
190
+ Initializes and starts the gRPC hub server based on a validated configuration.
191
+
192
+ This server runner acts as an orchestrator for multiple services:
193
+ 1. Loads and validates the master lumen_config.yaml.
194
+ 2. Verifies deployment mode is 'hub'.
195
+ 3. Downloads all required model assets.
196
+ 4. Initializes all enabled services via AppService.
197
+ 5. Attaches all services to the gRPC server with routing support.
198
+ 6. Starts listening and handles graceful shutdown.
199
+
200
+ Args:
201
+ config_path: Path to the YAML configuration file
202
+ port_override: Optional port number to override config setting
203
+ """
204
+ try:
205
+ # Step 1: Load and validate the main configuration file
206
+ logger.info(f"Loading configuration from: {config_path}")
207
+ config: LumenConfig = load_and_validate_config(config_path)
208
+
209
+ # Step 2: Ensure we are running in hub mode
210
+ if config.deployment.mode != "hub":
211
+ logger.error(
212
+ f"This server is designed for 'hub' deployment mode only. "
213
+ f"Current mode: {config.deployment.mode}"
214
+ )
215
+ sys.exit(1)
216
+
217
+ # Step 3: Verify and download all required model assets
218
+ logger.info("Verifying and downloading model assets...")
219
+ downloader = Downloader(config, verbose=False)
220
+ download_results = downloader.download_all()
221
+ handle_download_results(download_results)
222
+
223
+ # Step 4: Initialize AppService with all enabled services
224
+ # Note: AppService.from_app_config() already initializes all services
225
+ # via their from_config() factory methods, so no additional initialization needed
226
+ logger.info("Initializing Lumen Hub service...")
227
+ app_service = AppService.from_app_config(config)
228
+ logger.info(f"✓ Loaded {len(app_service.services)} service(s)")
229
+
230
+ # Step 5: Set up and start the gRPC server
231
+ logger.info("Setting up gRPC server...")
232
+ server = grpc.server(
233
+ futures.ThreadPoolExecutor(max_workers=10),
234
+ options=[("grpc.so_reuseport", 0)],
235
+ )
236
+
237
+ # Attach the hub router to the server
238
+ # The router will handle dispatching requests to appropriate services
239
+ app_service.router.attach_to_server(server)
240
+
241
+ # Determine port: CLI override > config file > default
242
+ preferred_port = port_override or config.server.port or 50051
243
+ requested_addr = f"[::]:{preferred_port}"
244
+ try:
245
+ bound_port = server.add_insecure_port(requested_addr)
246
+ except RuntimeError as exc:
247
+ logger.warning(
248
+ f"Port {preferred_port} bind raised {exc}; requesting OS-assigned port."
249
+ )
250
+ bound_port = 0
251
+
252
+ if bound_port == 0:
253
+ try:
254
+ bound_port = server.add_insecure_port("[::]:0")
255
+ except RuntimeError as exc:
256
+ logger.error(f"Unable to bind gRPC server to any port: {exc}")
257
+ sys.exit(1)
258
+
259
+ if bound_port == 0:
260
+ logger.error("Unable to bind gRPC server to any port.")
261
+ sys.exit(1)
262
+
263
+ port = bound_port
264
+ listen_addr = f"[::]:{port}"
265
+ server.start()
266
+
267
+ # Log server startup info
268
+ logger.info(f"🚀 Lumen Hub service listening on {listen_addr}")
269
+ logger.info(f"✓ Running {len(app_service.services)} service(s):")
270
+ for service in app_service.services:
271
+ service_name = service.__class__.__name__
272
+ logger.info(f" - {service_name}")
273
+
274
+ # Log capabilities of each service
275
+ try:
276
+ from google.protobuf import empty_pb2
277
+
278
+ for service in app_service.services:
279
+ try:
280
+ # Create a minimal context for capability probing
281
+ class _StartupServicerContext:
282
+ def abort(self, code, details):
283
+ pass
284
+
285
+ def set_code(self, code):
286
+ pass
287
+
288
+ def set_details(self, details):
289
+ pass
290
+
291
+ startup_context = _StartupServicerContext()
292
+ # Check if service has GetCapabilities method
293
+ if not hasattr(service, "GetCapabilities"):
294
+ continue
295
+ get_capabilities = getattr(service, "GetCapabilities")
296
+ capabilities = get_capabilities(empty_pb2.Empty(), startup_context)
297
+ supported_tasks = [task.name for task in capabilities.tasks]
298
+ service_name = service.__class__.__name__
299
+ logger.info(
300
+ f" ✓ {service_name} tasks: {', '.join(supported_tasks)}"
301
+ )
302
+ except Exception as e:
303
+ logger.warning(
304
+ f"Could not retrieve capabilities for {service.__class__.__name__}: {e}"
305
+ )
306
+ except Exception as e:
307
+ logger.warning(f"Could not retrieve service capabilities: {e}")
308
+
309
+ # Step 6: Set up mDNS and graceful shutdown
310
+ zeroconf, service_info = None, None
311
+ if config.server.mdns and config.server.mdns.enabled:
312
+ zeroconf, service_info = setup_mdns(port, config.server.mdns)
313
+
314
+ def handle_shutdown(signum, frame):
315
+ logger.info("Shutdown signal received. Stopping server...")
316
+ if zeroconf and service_info:
317
+ logger.info("Unregistering mDNS service...")
318
+ zeroconf.unregister_service(service_info)
319
+ zeroconf.close()
320
+ server.stop(grace=5.0)
321
+
322
+ signal.signal(signal.SIGINT, handle_shutdown)
323
+ signal.signal(signal.SIGTERM, handle_shutdown)
324
+
325
+ logger.info("Server running. Press Ctrl+C to stop.")
326
+ server.wait_for_termination()
327
+ logger.info("Server shutdown complete.")
328
+
329
+ except (ConfigError, FileNotFoundError) as e:
330
+ logger.error(f"Service startup failed: {e}")
331
+ sys.exit(1)
332
+ except Exception as e:
333
+ logger.exception(f"An unexpected error occurred: {e}")
334
+ sys.exit(1)
335
+
336
+
337
+ def main():
338
+ """Main entry point for Lumen Hub server."""
339
+ parser = argparse.ArgumentParser(
340
+ description="Run Lumen Hub gRPC Service (multiple services in one process)",
341
+ formatter_class=argparse.RawDescriptionHelpFormatter,
342
+ epilog="""
343
+ Examples:
344
+ # Run hub service with default config settings
345
+ python -m lumen_app.core.server --config config/lumen-hub.yaml
346
+
347
+ # Override port
348
+ python -m lumen_app.core.server --config config/lumen-hub.yaml --port 50052
349
+
350
+ # Enable debug logging
351
+ python -m lumen_app.core.server --config config/lumen-hub.yaml --log-level DEBUG
352
+
353
+ # Validate config without starting server
354
+ lumen-resources validate config/lumen-hub.yaml
355
+ """,
356
+ )
357
+
358
+ parser.add_argument(
359
+ "--config",
360
+ type=str,
361
+ required=True,
362
+ help="Path to YAML configuration file",
363
+ )
364
+
365
+ parser.add_argument(
366
+ "--port",
367
+ type=int,
368
+ help="Port number (overrides config file setting)",
369
+ )
370
+
371
+ parser.add_argument(
372
+ "--log-level",
373
+ type=str,
374
+ default="INFO",
375
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
376
+ help="Logging level",
377
+ )
378
+
379
+ args = parser.parse_args()
380
+
381
+ # Set logging level
382
+ setup_logging(args.log_level)
383
+
384
+ # Start server
385
+ serve(config_path=args.config, port_override=args.port)
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()
@@ -0,0 +1,49 @@
1
+ from pathlib import Path
2
+
3
+ from lumen_resources.lumen_config import LumenConfig, Services
4
+
5
+ from ..utils.logger import get_logger
6
+ from .loader import ServiceLoader # 负责动态 importlib
7
+ from .router import HubRouter
8
+
9
+ logger = get_logger("lumen.service")
10
+
11
+
12
+ class AppService:
13
+ def __init__(self, services: list[Services], config: LumenConfig):
14
+ self.services = services
15
+ self.config = config
16
+ # 将实例映射到路由,支持多对多
17
+ self.router = HubRouter(services)
18
+
19
+ @classmethod
20
+ def from_app_config(cls, config: LumenConfig):
21
+ """
22
+ 核心工厂方法:解析 LumenConfig 并初始化所有子服务
23
+ """
24
+ loader = ServiceLoader()
25
+ instances = []
26
+ cache_dir = Path(config.metadata.cache_dir)
27
+
28
+ # 遍历配置中定义的所有服务
29
+ for name, svc_cfg in config.services.items():
30
+ if not svc_cfg.enabled:
31
+ continue
32
+
33
+ # 1. 动态获取服务类 (例如从 lumen_ocr.registry 获取)
34
+ # svc_cfg.import_info.registry_class 存储了类路径
35
+ if svc_cfg.import_info is not None:
36
+ service_cls = loader.get_class(svc_cfg.import_info.registry_class)
37
+
38
+ # 2. 调用你重构好的 from_config 方法
39
+ instance = service_cls.from_config(
40
+ service_config=svc_cfg, cache_dir=cache_dir
41
+ )
42
+ instances.append(instance)
43
+ logger.info(f"Loaded service: {name} with package {svc_cfg.package}")
44
+ else:
45
+ raise RuntimeError(
46
+ f"Cannot load import_info from configuration for service:{svc_cfg.package}"
47
+ )
48
+
49
+ return cls(services=instances, config=config)
@@ -0,0 +1 @@
1
+ """Tests for lumen_app.core."""