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/core/server.py
ADDED
|
@@ -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."""
|