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
lumen_app/server.py ADDED
@@ -0,0 +1,362 @@
1
+ """
2
+ Hub server startup module for Lumen Application.
3
+
4
+ This module provides a unified gRPC server that can host multiple AI services
5
+ (OCR, CLIP, Face, VLM) in a single process. It adapts the proven server architecture
6
+ from individual Lumen service packages to support hub deployment mode.
7
+
8
+ Key features:
9
+ - Load and validate hub configuration
10
+ - Download and verify all required model assets
11
+ - Initialize multiple ML services via AppService
12
+ - Route inference requests via HubRouter
13
+ - Support mDNS service discovery
14
+ - Graceful shutdown with signal handling
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import logging
21
+ import os
22
+ import signal
23
+ import socket
24
+ import sys
25
+ import uuid
26
+ from concurrent import futures
27
+ from typing import cast
28
+
29
+ import colorlog
30
+ import grpc
31
+ from google.protobuf import empty_pb2
32
+ from lumen_resources import Downloader, DownloadResult, load_and_validate_config
33
+ from lumen_resources.lumen_config import LumenConfig, Mdns
34
+ from zeroconf import ServiceInfo, Zeroconf
35
+
36
+ from lumen_app.core.service import AppService
37
+ from lumen_app.proto import ml_service_pb2_grpc
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class ConfigError(Exception):
43
+ """Raised when configuration is invalid."""
44
+
45
+ pass
46
+
47
+
48
+ class _StartupServicerContext:
49
+ """Minimal ServicerContext stub for startup-time capability probing."""
50
+
51
+ def abort(self, code, details):
52
+ raise RuntimeError(f"Capability probe aborted ({code}): {details}")
53
+
54
+ def abort_with_status(self, status):
55
+ raise RuntimeError(f"Capability probe aborted: {status}")
56
+
57
+ def set_code(self, code):
58
+ pass
59
+
60
+ def set_details(self, details):
61
+ pass
62
+
63
+
64
+ def setup_logging(log_level: str = "INFO"):
65
+ """
66
+ Configure the root logger for the application.
67
+
68
+ This function clears any pre-existing handlers, sets the requested log
69
+ level, and attaches a single colorized stream handler for console output.
70
+ """
71
+ root_logger = logging.getLogger()
72
+ root_logger.setLevel(log_level)
73
+
74
+ # Remove any handlers that may have been pre-configured
75
+ for handler in root_logger.handlers[:]:
76
+ root_logger.removeHandler(handler)
77
+
78
+ # Add a colorized console handler
79
+ handler = colorlog.StreamHandler()
80
+ formatter = colorlog.ColoredFormatter(
81
+ # Include the logger name in cyan and keep a fixed-width field to
82
+ # improve alignment of log source labels.
83
+ "%(log_color)s%(levelname)-8s%(cyan)s[%(name)s]%(reset)s %(message)s",
84
+ reset=True,
85
+ log_colors={
86
+ "DEBUG": "cyan",
87
+ "INFO": "green",
88
+ "WARNING": "yellow",
89
+ "ERROR": "red",
90
+ "CRITICAL": "red,bg_white",
91
+ },
92
+ )
93
+ handler.setFormatter(formatter)
94
+ root_logger.addHandler(handler)
95
+
96
+
97
+ def setup_mdns(
98
+ port: int, mdns_config: Mdns | None
99
+ ) -> tuple[Zeroconf | None, ServiceInfo | None]:
100
+ """
101
+ Set up mDNS advertisement.
102
+
103
+ Args:
104
+ port: Service port number
105
+ mdns_config: mDNS configuration dictionary
106
+
107
+ Returns:
108
+ Tuple of (zeroconf, service_info) or (None, None) if setup fails
109
+ """
110
+
111
+ try:
112
+ # Determine advertised IP
113
+ ip = os.getenv("ADVERTISE_IP")
114
+ if not ip:
115
+ try:
116
+ # Best-effort LAN IP detection
117
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
118
+ s.connect(("8.8.8.8", 80))
119
+ ip = s.getsockname()[0]
120
+ s.close()
121
+ except Exception:
122
+ ip = socket.gethostbyname(socket.gethostname())
123
+
124
+ if ip.startswith("127."):
125
+ logger.warning(
126
+ f"mDNS advertising loopback IP {ip}; "
127
+ + "other devices may not reach the service. "
128
+ + "Set ADVERTISE_IP to a LAN IP."
129
+ )
130
+
131
+ # Build TXT record properties
132
+ props = {
133
+ "uuid": os.getenv("SERVICE_UUID", str(uuid.uuid4())),
134
+ "status": os.getenv("SERVICE_STATUS", "ready"),
135
+ "version": os.getenv("SERVICE_VERSION", "1.0.0"),
136
+ }
137
+
138
+ # Get service type and name from config (Mdns may be None or have different attribute names)
139
+ service_type = (
140
+ getattr(mdns_config, "type", None)
141
+ or getattr(mdns_config, "service_type", None)
142
+ or "_lumen._tcp.local."
143
+ )
144
+ instance_name = (
145
+ getattr(mdns_config, "name", None)
146
+ or getattr(mdns_config, "service_name", None)
147
+ or "Lumen-Hub" # Changed from service-specific names
148
+ )
149
+ full_name = f"{instance_name}.{service_type}"
150
+
151
+ # Create service info
152
+ service_info = ServiceInfo(
153
+ type_=service_type,
154
+ name=full_name,
155
+ addresses=[socket.inet_aton(ip)],
156
+ port=port,
157
+ properties=props,
158
+ server=f"{socket.gethostname()}.local.",
159
+ )
160
+
161
+ # Register service
162
+ zeroconf = Zeroconf()
163
+ zeroconf.register_service(service_info)
164
+ logger.info(f"✓ mDNS advertised: {full_name} at {ip}:{port}")
165
+
166
+ return zeroconf, service_info
167
+
168
+ except Exception as e:
169
+ logger.warning(f"mDNS advertisement failed: {e}")
170
+ return None, None
171
+
172
+
173
+ def handle_download_results(results: dict[str, DownloadResult]):
174
+ """
175
+ Processes download results, logs them, and exits if critical failures occurred.
176
+ """
177
+ successful_downloads = []
178
+ failed_downloads = []
179
+
180
+ for _model_type, result in results.items():
181
+ if result.success:
182
+ successful_downloads.append(result)
183
+ else:
184
+ failed_downloads.append(result)
185
+
186
+ # CRITICAL: If any model failed to download, abort the server startup.
187
+ if failed_downloads:
188
+ logger.error(
189
+ "💥 Critical error: Model download failed. Cannot start the server."
190
+ )
191
+ for res in failed_downloads:
192
+ logger.error(f" - Model '{res.model_type}': {res.error}")
193
+ sys.exit(1)
194
+
195
+ # If all downloads were successful, log a summary.
196
+ logger.info("✅ All required models are successfully downloaded and verified.")
197
+ for res in successful_downloads:
198
+ # Also check for non-critical warnings, like missing optional files.
199
+ if res.missing_files:
200
+ logger.warning(
201
+ f" - Model '{res.model_type}' is ready, but has missing optional files: "
202
+ f"{', '.join(res.missing_files)}"
203
+ )
204
+
205
+
206
+ def serve(config_path: str, port_override: int | None = None) -> None:
207
+ """
208
+ Initializes and starts the gRPC hub server based on a validated configuration.
209
+
210
+ This server runner acts as an orchestrator:
211
+ 1. Loads and validates the master lumen_config.yaml.
212
+ 2. Downloads all required model assets.
213
+ 3. Initializes all enabled ML services via AppService.
214
+ 4. Starts gRPC server with HubRouter for request routing.
215
+ 5. Sets up mDNS service discovery (optional).
216
+ 6. Handles graceful shutdown.
217
+ """
218
+ try:
219
+ # Step 1: Load and validate the main configuration file.
220
+ config: LumenConfig = load_and_validate_config(config_path)
221
+
222
+ # Step 1a: Verify and download all required model assets before proceeding.
223
+ logger.info("Verifying and downloading model assets...")
224
+ downloader = Downloader(config, verbose=False)
225
+ download_results = downloader.download_all()
226
+ handle_download_results(download_results)
227
+
228
+ # Ensure we are running in the correct deployment mode.
229
+ if config.deployment.mode != "hub":
230
+ logger.error("This server is designed for 'hub' deployment mode only.")
231
+ sys.exit(1)
232
+
233
+ # Step 2: Initialize all enabled services via AppService
234
+ logger.info("Initializing Lumen Hub services...")
235
+ app_service = AppService.from_app_config(config)
236
+ logger.info(f"Loaded {len(app_service.services)} services")
237
+
238
+ # Step 3: Set up and start the gRPC server.
239
+ server = grpc.server(
240
+ futures.ThreadPoolExecutor(max_workers=10),
241
+ options=[("grpc.so_reuseport", 0)],
242
+ )
243
+
244
+ # Register HubRouter as the gRPC servicer
245
+ ml_service_pb2_grpc.add_InferenceServicer_to_server(app_service.router, server)
246
+
247
+ # Determine port: CLI override > config file > default.
248
+ preferred_port = port_override or config.server.port or 50051
249
+ requested_addr = f"[::]:{preferred_port}"
250
+ try:
251
+ bound_port = server.add_insecure_port(requested_addr)
252
+ except RuntimeError as exc:
253
+ logger.warning(
254
+ f"Port {preferred_port} bind raised {exc}; requesting OS-assigned port."
255
+ )
256
+ bound_port = 0
257
+
258
+ if bound_port == 0:
259
+ try:
260
+ bound_port = server.add_insecure_port("[::]:0")
261
+ except RuntimeError as exc:
262
+ logger.error(f"Unable to bind gRPC server to any port: {exc}")
263
+ sys.exit(1)
264
+
265
+ if bound_port == 0:
266
+ logger.error("Unable to bind gRPC server to any port.")
267
+ sys.exit(1)
268
+
269
+ port = bound_port
270
+ listen_addr = f"[::]:{port}"
271
+ server.start()
272
+ logger.info(f"🚀 Lumen Hub server listening on {listen_addr}")
273
+
274
+ # Log service capabilities now that all services are initialized.
275
+ try:
276
+ startup_context = _StartupServicerContext()
277
+ capabilities = app_service.router.GetCapabilities(
278
+ empty_pb2.Empty(), cast(grpc.ServicerContext, startup_context)
279
+ )
280
+ supported_tasks = [task.name for task in capabilities.tasks]
281
+ logger.info(f"✓ Supported tasks: {', '.join(supported_tasks)}")
282
+ except Exception as e:
283
+ logger.warning(f"Could not retrieve service capabilities: {e}")
284
+
285
+ # Step 4: Set up mDNS and graceful shutdown.
286
+ zeroconf, service_info = None, None
287
+ if config.server.mdns and config.server.mdns.enabled:
288
+ zeroconf, service_info = setup_mdns(port, config.server.mdns)
289
+
290
+ def handle_shutdown(signum, frame):
291
+ logger.info("Shutdown signal received. Stopping server...")
292
+ if zeroconf and service_info:
293
+ logger.info("Unregistering mDNS service...")
294
+ zeroconf.unregister_service(service_info)
295
+ zeroconf.close()
296
+ server.stop(grace=5.0)
297
+
298
+ signal.signal(signal.SIGINT, handle_shutdown)
299
+ signal.signal(signal.SIGTERM, handle_shutdown)
300
+
301
+ logger.info("Server running. Press Ctrl+C to stop.")
302
+ server.wait_for_termination()
303
+ logger.info("Server shutdown complete.")
304
+
305
+ except (ConfigError, FileNotFoundError) as e:
306
+ logger.error(f"Service startup failed: {e}")
307
+ sys.exit(1)
308
+ except Exception as e:
309
+ logger.exception(f"An unexpected error occurred: {e}")
310
+ sys.exit(1)
311
+
312
+
313
+ def main():
314
+ """Main entry point."""
315
+ parser = argparse.ArgumentParser(
316
+ description="Run Lumen Hub gRPC Server",
317
+ formatter_class=argparse.RawDescriptionHelpFormatter,
318
+ epilog="""
319
+ Examples:
320
+ # Run hub server with default config settings
321
+ python -m lumen_app.server --config config/hub.yaml
322
+
323
+ # Run hub server on custom port
324
+ python -m lumen_app.server --config config/hub.yaml --port 50052
325
+
326
+ # Validate config without starting server
327
+ lumen-resources validate config/hub.yaml
328
+ """,
329
+ )
330
+
331
+ parser.add_argument(
332
+ "--config",
333
+ type=str,
334
+ required=True,
335
+ help="Path to YAML configuration file",
336
+ )
337
+
338
+ parser.add_argument(
339
+ "--port",
340
+ type=int,
341
+ help="Port number (overrides config file setting)",
342
+ )
343
+
344
+ parser.add_argument(
345
+ "--log-level",
346
+ type=str,
347
+ default="INFO",
348
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
349
+ help="Logging level",
350
+ )
351
+
352
+ args = parser.parse_args()
353
+
354
+ # Set logging level
355
+ setup_logging(args.log_level)
356
+
357
+ # Start server
358
+ serve(config_path=args.config, port_override=args.port)
359
+
360
+
361
+ if __name__ == "__main__":
362
+ main()