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
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()
|