linglong-web 0.0.1__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.
@@ -0,0 +1,132 @@
1
+ """Linglong Web – 异步 FastAPI 工具集 / Asynchronous FastAPI toolkit."""
2
+ from .__version__ import (
3
+ __author__,
4
+ __description__,
5
+ __license__,
6
+ __version__,
7
+ )
8
+
9
+ from .core.auth import login_required
10
+ from .core.cacher import cacher
11
+ from .core.cluster_lock import cluster_lock
12
+ from .core.config import (
13
+ LinglongConfigBase,
14
+ LinglongConfig,
15
+ init_config,
16
+ )
17
+ from .core.constants import LinglongConst
18
+ from .core.cors import allow_cors_specific
19
+ from .core.db import TableBase
20
+ from .core.ddl_manager import (
21
+ AutoDDLManager,
22
+ DDLManagerConfig,
23
+ )
24
+ from .core.http import (
25
+ HTTPClientConfig,
26
+ AsyncHTTPClient,
27
+ LinglongHTTPError,
28
+ http_client,
29
+ )
30
+ from .core.limiter import LimiterGuard, limiter
31
+ from .core.limiter_local import (
32
+ limiter_local,
33
+ reset_limiter,
34
+ get_limiter_stats,
35
+ )
36
+ from .core.resource import (
37
+ ResourceManager,
38
+ Rmanager,
39
+ DEFAULT_DB_ALIAS,
40
+ init_resources,
41
+ close_resources,
42
+ )
43
+ from .core.response import (
44
+ APIError,
45
+ APIResponse,
46
+ build_api_response,
47
+ build_success_response,
48
+ build_error_response,
49
+ )
50
+ from .core.router import (
51
+ BaseRoute,
52
+ ServerRouter,
53
+ )
54
+ from .core.scheduler import (
55
+ BaseScheduler,
56
+ SchedulerGroup,
57
+ to_group,
58
+ )
59
+ from .core.server import LinglongAppServer
60
+ from .core.server_extensions import BaseServerExtension
61
+ from .core.schemas import (
62
+ PgsqlConfig,
63
+ RedisConfig,
64
+ RabbitMQConfig,
65
+ MongoConfig,
66
+ CeleryConfig,
67
+ ResourceInitConfig,
68
+ )
69
+ from .core.errors import (
70
+ ErrorCode,
71
+ ErrorMsg,
72
+ LinglongHTTPException,
73
+ LoginRequiredError,
74
+ LimiterError,
75
+ ClusterLockError,
76
+ )
77
+
78
+ __all__ = [
79
+ "login_required",
80
+ "cacher",
81
+ "cluster_lock",
82
+ "LinglongConfigBase",
83
+ "LinglongConfig",
84
+ "LinglongConst",
85
+ "init_config",
86
+ "allow_cors_specific",
87
+ "TableBase",
88
+ "AutoDDLManager",
89
+ "DDLManagerConfig",
90
+ "BaseRoute",
91
+ "ServerRouter",
92
+ "APIError",
93
+ "APIResponse",
94
+ "build_api_response",
95
+ "build_success_response",
96
+ "build_error_response",
97
+ "HTTPClientConfig",
98
+ "AsyncHTTPClient",
99
+ "LinglongHTTPError",
100
+ "http_client",
101
+ "LimiterGuard",
102
+ "limiter",
103
+ "limiter_local",
104
+ "reset_limiter",
105
+ "get_limiter_stats",
106
+ "ResourceManager",
107
+ "Rmanager",
108
+ "DEFAULT_DB_ALIAS",
109
+ "init_resources",
110
+ "close_resources",
111
+ "BaseScheduler",
112
+ "SchedulerGroup",
113
+ "to_group",
114
+ "LinglongAppServer",
115
+ "BaseServerExtension",
116
+ "PgsqlConfig",
117
+ "RedisConfig",
118
+ "RabbitMQConfig",
119
+ "MongoConfig",
120
+ "CeleryConfig",
121
+ "ResourceInitConfig",
122
+ "ErrorCode",
123
+ "ErrorMsg",
124
+ "LinglongHTTPException",
125
+ "LoginRequiredError",
126
+ "LimiterError",
127
+ "ClusterLockError",
128
+ "__version__",
129
+ "__author__",
130
+ "__license__",
131
+ "__description__",
132
+ ]
@@ -0,0 +1,9 @@
1
+ """Linglong Web metadata / 版本信息模块."""
2
+
3
+ __version__ = "0.0.1"
4
+ __author__ = "Victor Lai"
5
+ __license__ = "MIT"
6
+ __description__ = (
7
+ "Asynchronous FastAPI toolkit providing service bootstrap, registry hooks, resource orchestration, "
8
+ "and observability helpers"
9
+ )
@@ -0,0 +1,54 @@
1
+ """Linglong Web – 异步 FastAPI 工具集 / Asynchronous FastAPI toolkit."""
2
+ from .auth import login_required
3
+ from .cacher import cacher
4
+ from .cluster_lock import cluster_lock
5
+ from .config import (
6
+ LinglongConfigBase,
7
+ LinglongConfig,
8
+ LinglongConfigProxy,
9
+ init_config,
10
+ )
11
+ from .constants import LinglongConst
12
+ from .cors import allow_cors_specific
13
+ from .db import TableBase
14
+ from .ddl_manager import (
15
+ AutoDDLManager,
16
+ DDLManagerConfig,
17
+ )
18
+ from .http import (
19
+ HTTPClientConfig,
20
+ AsyncHTTPClient,
21
+ LinglongHTTPError,
22
+ http_client,
23
+ )
24
+ from .limiter import (
25
+ LimiterGuard,
26
+ limiter,
27
+ )
28
+ from .limiter_local import (
29
+ limiter_local,
30
+ reset_limiter,
31
+ get_limiter_stats,
32
+ )
33
+ from .resource import (
34
+ ResourceManager,
35
+ Rmanager,
36
+ DEFAULT_DB_ALIAS,
37
+ init_resources,
38
+ close_resources,
39
+ )
40
+ from .response import (
41
+ APIError,
42
+ APIResponse,
43
+ build_api_response,
44
+ build_success_response,
45
+ build_error_response,
46
+ )
47
+ from .router import BaseRoute, ServerRouter
48
+ from .scheduler import (
49
+ BaseScheduler,
50
+ SchedulerGroup,
51
+ to_group,
52
+ )
53
+ from .server import LinglongAppServer
54
+ from .server_extensions import BaseServerExtension
@@ -0,0 +1,31 @@
1
+ """Authentication helpers shared across Linglong services.
2
+
3
+ 提供 Linglong 服务统一的认证装饰器工具。
4
+ """
5
+ from functools import wraps
6
+ from typing import (
7
+ Awaitable,
8
+ Callable,
9
+ )
10
+
11
+ from .errors import LoginRequiredError
12
+ from .types import P, R
13
+ from ..utils.context import get_context_user_id
14
+ from ..utils.log import logger
15
+
16
+
17
+ def login_required(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
18
+ """Ensure the current request has a valid login user id.
19
+
20
+ 确保当前请求已经完成登录校验,如果缺少用户信息则抛出 ``LoginRequiredError``。
21
+ """
22
+
23
+ @wraps(func)
24
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
25
+ user_id = get_context_user_id()
26
+ if user_id is not None and isinstance(user_id, int):
27
+ return await func(*args, **kwargs)
28
+ logger.warning("user is not login")
29
+ raise LoginRequiredError()
30
+
31
+ return wrapper
@@ -0,0 +1,92 @@
1
+ """Redis based caching decorator for Linglong services.
2
+
3
+ 提供基于 Redis 的协程缓存装饰器,支持自定义键后缀与按用户隔离。
4
+ """
5
+ import hashlib
6
+ import inspect
7
+ import os
8
+ from functools import wraps
9
+ from typing import (
10
+ Any,
11
+ Awaitable,
12
+ Callable,
13
+ )
14
+
15
+ import orjson
16
+ from redis import asyncio as aioredis
17
+
18
+ from .resource import Rmanager
19
+ from .types import P, R
20
+ from ..utils.context import get_context_user_id
21
+ from ..utils.log import logger
22
+
23
+
24
+ def _build_cache_key(
25
+ func: Callable[..., Any],
26
+ *,
27
+ add_str: str,
28
+ cache_by_user: bool,
29
+ ) -> str:
30
+ """Generate a stable cache key based on function source metadata.
31
+
32
+ 结合函数文件路径、行号、模块与名称生成稳定的缓存键,可选追加自定义后缀与用户 ID。
33
+ """
34
+
35
+ source_file = inspect.getsourcefile(func)
36
+ filepath = os.path.abspath(source_file) if source_file else "N/A"
37
+ hash_hex = hashlib.md5(filepath.encode("utf-8"), usedforsecurity=False).hexdigest()
38
+
39
+ try:
40
+ _, line_no = inspect.getsourcelines(func)
41
+ except (TypeError, OSError): # pragma: no cover - fallback when inspect fails
42
+ line_no = "N/A"
43
+
44
+ key_parts = [hash_hex, str(line_no), func.__module__, func.__name__]
45
+ if add_str:
46
+ key_parts.append(add_str)
47
+ if cache_by_user:
48
+ user_id = get_context_user_id()
49
+ if user_id:
50
+ key_parts.append(str(user_id))
51
+ return ":".join(key_parts)
52
+
53
+
54
+ def cacher(
55
+ expire_time: int,
56
+ add_str: str = "",
57
+ cache_by_user: bool = False,
58
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
59
+ """Cache coroutine results in Redis for a configurable TTL.
60
+
61
+ Args:
62
+ expire_time: 缓存秒数 / TTL in seconds.
63
+ add_str: 自定义键后缀 / Optional suffix for more granularity.
64
+ cache_by_user: 是否按用户区分缓存 / Split cache entries by user id.
65
+ """
66
+
67
+ def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
68
+ @wraps(func)
69
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
70
+ if Rmanager.RedisPool is None:
71
+ logger.warning("redis cache is not available")
72
+ return await func(*args, **kwargs)
73
+
74
+ redis_client = aioredis.Redis(connection_pool=Rmanager.RedisPool)
75
+ cache_key = _build_cache_key(func, add_str=add_str, cache_by_user=cache_by_user)
76
+
77
+ try:
78
+ cached = await redis_client.get(cache_key)
79
+ if cached:
80
+ logger.debug("redis cache hit: %s", cache_key)
81
+ return orjson.loads(cached)
82
+
83
+ result = await func(*args, **kwargs)
84
+ await redis_client.setex(cache_key, expire_time, orjson.dumps(result))
85
+ return result
86
+ except Exception as exc: # noqa: BLE001
87
+ logger.error("redis cache error: %s", exc, exc_info=True)
88
+ return await func(*args, **kwargs)
89
+
90
+ return wrapper
91
+
92
+ return decorator
@@ -0,0 +1,128 @@
1
+ """Redis backed cluster wide lock helpers.
2
+
3
+ 提供依赖 Redis 的集群锁装饰器,防止分布式并发执行同一段逻辑。
4
+ """
5
+ from functools import wraps
6
+ from typing import (
7
+ Any,
8
+ Awaitable,
9
+ Callable,
10
+ )
11
+
12
+ import redis.asyncio as redis
13
+
14
+ from .errors import ClusterLockError
15
+ from .resource import Rmanager
16
+ from .types import LockKeyBuilder, OnLockFail, P, R
17
+ from ..utils.log import logger
18
+
19
+
20
+ def _get_redis_client() -> redis.Redis | None:
21
+ """Lazily build Redis client from the global pool.
22
+
23
+ 从 Rmanager 注入的连接池创建 Redis 实例,若池未初始化则返回 ``None``。
24
+ """
25
+
26
+ if not Rmanager.RedisPool:
27
+ return None
28
+ return redis.Redis(connection_pool=Rmanager.RedisPool)
29
+
30
+
31
+ def _compose_lock_key(
32
+ lock_prefix: str,
33
+ func: Callable[..., Any],
34
+ include_func_name: bool,
35
+ extra_key: str,
36
+ key_builder: LockKeyBuilder | None,
37
+ args: tuple[Any, ...],
38
+ kwargs: dict[str, Any],
39
+ ) -> str:
40
+ """Build a stable lock key with optional custom suffixes.
41
+
42
+ 将锁前缀、函数名、自定义 key 与用户提供的 ``key_builder`` 结果拼接为最终键。
43
+ """
44
+
45
+ key_parts: list[str] = [lock_prefix]
46
+ if include_func_name:
47
+ key_parts.append(func.__name__)
48
+ if extra_key:
49
+ key_parts.append(extra_key)
50
+ if key_builder:
51
+ custom = key_builder(func, args, kwargs)
52
+ if custom:
53
+ key_parts.append(custom)
54
+ return ":".join(key_parts)
55
+
56
+
57
+ def cluster_lock(
58
+ lock_prefix: str,
59
+ *,
60
+ timeout_seconds: float = 60.0,
61
+ blocking_timeout_seconds: float | None = None,
62
+ extra_key: str = "",
63
+ include_func_name: bool = True,
64
+ key_builder: LockKeyBuilder | None = None,
65
+ error_message: str | None = None,
66
+ on_fail: OnLockFail | None = None,
67
+ ) -> Callable[[Callable[P, Awaitable[R]]], Callable[P, Awaitable[R]]]:
68
+ """Decorate a coroutine to enforce Redis distributed locking.
69
+
70
+ Args:
71
+ lock_prefix: 锁键前缀 / Prefix for Redis lock key.
72
+ timeout_seconds: 锁自动过期时间 / Lock expiration in seconds.
73
+ blocking_timeout_seconds: 获取锁的最大等待时间 / Max wait when acquiring.
74
+ extra_key: 自定义后缀 / Optional static suffix.
75
+ include_func_name: 是否包含函数名 / Append function name or not.
76
+ key_builder: 用户自定义键构造 / Custom builder for dynamic parts.
77
+ error_message: 获取失败提示 / Optional override error text.
78
+ on_fail: 获取失败回调 / Optional coroutine executed on contention.
79
+ """
80
+
81
+ if timeout_seconds <= 0:
82
+ raise ValueError("timeout_seconds must be positive")
83
+
84
+ def decorator(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]:
85
+ @wraps(func)
86
+ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
87
+ redis_client = _get_redis_client()
88
+ if redis_client is None:
89
+ logger.warning("Redis pool is not initialized, skip cluster lock for %s", func.__name__)
90
+ return await func(*args, **kwargs)
91
+
92
+ lock_key = _compose_lock_key(
93
+ lock_prefix=lock_prefix,
94
+ func=func,
95
+ include_func_name=include_func_name,
96
+ extra_key=extra_key,
97
+ key_builder=key_builder,
98
+ args=args,
99
+ kwargs=kwargs,
100
+ )
101
+
102
+ wait_timeout = blocking_timeout_seconds if blocking_timeout_seconds is not None else timeout_seconds
103
+ lock = redis_client.lock(lock_key, timeout=timeout_seconds)
104
+
105
+ try:
106
+ acquired = await lock.acquire(blocking=True, blocking_timeout=wait_timeout)
107
+ except Exception as exc: # noqa: BLE001
108
+ logger.error("Failed to acquire cluster lock %s: %s", lock_key, exc)
109
+ raise
110
+
111
+ if not acquired:
112
+ message = error_message or f"Resource is locked by another worker: {lock_key}"
113
+ if on_fail:
114
+ return await on_fail(message, func, args, kwargs)
115
+ raise ClusterLockError(message)
116
+
117
+ try:
118
+ return await func(*args, **kwargs)
119
+ finally:
120
+ if lock.locked():
121
+ try:
122
+ await lock.release()
123
+ except Exception as exc: # noqa: BLE001
124
+ logger.error("Failed to release cluster lock %s: %s", lock_key, exc)
125
+
126
+ return wrapper
127
+
128
+ return decorator
@@ -0,0 +1,196 @@
1
+ """Linglong Web 配置代理 / Configuration proxy."""
2
+
3
+ import logging
4
+ import os
5
+ from typing import (
6
+ Any,
7
+ Dict,
8
+ )
9
+
10
+ from ..utils.pj_struct import Singleton
11
+ from ..utils.async_read_write_lock import AsyncReadWriteLock
12
+ from ..utils.log import logger
13
+
14
+
15
+ class LinglongConfigBase:
16
+ """框架默认配置 / Default Linglong configuration."""
17
+
18
+ DEBUG = False
19
+ ENV_MODE = "prod"
20
+
21
+ PGSQL_USER = "postgres"
22
+ PGSQL_PASSWORD = "postgres123"
23
+ PGSQL_DB = ""
24
+ PGSQL_HOST = "postgres.internal"
25
+ PGSQL_PORT = "5432"
26
+ PGSQL_POOL_SIZE = 1
27
+ PGSQL_MAX_OVERFLOW = 5
28
+ PGSQL_DATABASES: Dict[str, Dict[str, Any]] | None = None
29
+ DDL_REQUIRED_EXTENSIONS = ("pgcrypto",)
30
+
31
+ MONGODB_URI = ""
32
+ MONGODB_DB = ""
33
+ MONGODB_COLLECTION = ""
34
+
35
+ REDIS_HOST = "redis.internal"
36
+ REDIS_PORT = 6379
37
+ REDIS_PASSWORD = ""
38
+ REDIS_MAXSIZE = None
39
+
40
+ ENABLE_WORKFLOW_CELERY = False
41
+ CELERY_BROKER_URL = None
42
+ CELERY_RESULT_BACKEND = None
43
+ CELERY_RESULT_BACKEND_DB = 0
44
+ CELERY_APP_NAME = "linglong_app"
45
+ CELERY_TASK_SERIALIZER = "json"
46
+ CELERY_ACCEPT_CONTENT = ["json"]
47
+ CELERY_RESULT_SERIALIZER = "json"
48
+ CELERY_TIMEZONE = "UTC"
49
+ CELERY_ENABLE_UTC = True
50
+ CELERY_WORKER_LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
51
+ CELERY_WORKER_TASK_LOG_FORMAT = (
52
+ "%(asctime)s - %(name)s - %(levelname)s - task_id=%(task_id)s task_name=%(task_name)s - %(message)s"
53
+ )
54
+
55
+ LOGGING_LEVEL = logging.DEBUG
56
+ LOGGING_ENABLE_FILE_HANDLER = False
57
+ LOGGING_FILE_ADDR_FORMAT = "/opt/logs/server_app/app-{}.log"
58
+ LOGGING_FILE_MAX_BYTES = 10 * 1024 * 1024
59
+ LOGGING_FILE_BACKUP_COUNT = 10
60
+ LOG_CONFIG = {"exchange_name": "log.topic.exchange"}
61
+
62
+ LOG_PIPELINE_ENABLED = None
63
+ LOG_PIPELINE_IGNORE_LEVELS = []
64
+ LOG_RETENTION_DAYS = 7
65
+ LOG_RETENTION_BATCH_LIMIT = 2000
66
+
67
+ RABBITMQ_HOST = ""
68
+ RABBITMQ_PORT = 5672
69
+ RABBITMQ_VHOST = "/"
70
+ RABBITMQ_USERNAME = ""
71
+ RABBITMQ_PASSWORD = ""
72
+ RABBITMQ_LOG_EXCHANGE = "logs.topic.exchange"
73
+ RABBITMQ_LOG_QUEUE = "logs.business.infra.q"
74
+ RABBITMQ_LOG_BINDING_KEY = "logs.business.#"
75
+ RABBITMQ_LOG_PREFETCH = 200
76
+ RABBITMQ_LOG_ROUTING_TEMPLATE = "logs.business.{service}.{level}"
77
+ RABBITMQ_LOG_CONNECTION_NAME = ""
78
+
79
+ CORS_ALLOWED_ORIGINS = ["http://localhost"]
80
+
81
+ CELERY_WORKER_AUTOSTART = True
82
+ CELERY_WORKER_LOG_LEVEL = "INFO"
83
+ CELERY_WORKER_CONCURRENCY = 4
84
+ CELERY_WORKER_POOL = None
85
+ CELERY_WORKER_ENABLE_BEAT = True
86
+ CELERY_WORKER_HOSTNAME_SUFFIX = ".inline"
87
+ WORKFLOW_INLINE_MAX_CONCURRENCY = 4
88
+ WORKFLOW_INLINE_QUEUE_LIMIT = 1024
89
+
90
+ GRACEFUL_SHUTDOWN_TIMEOUT = 30
91
+
92
+
93
+ class LinglongConfigProxy(Singleton):
94
+ """配置代理,支持线程安全读写 / Config proxy with thread-safe caching."""
95
+
96
+ def __init__(self) -> None:
97
+ self._active_config_class: type[LinglongConfigBase] = LinglongConfigBase
98
+ self._lock = AsyncReadWriteLock()
99
+ self._config_cache: Dict[str, Any] = {}
100
+ self._cache_initialized = False
101
+
102
+ def _ensure_cache_initialized(self) -> None:
103
+ if self._cache_initialized:
104
+ return
105
+ with self._lock.write_locked():
106
+ if self._cache_initialized:
107
+ return
108
+ for key in dir(self._active_config_class):
109
+ if not key.startswith('_') and key.isupper():
110
+ self._config_cache[key] = getattr(self._active_config_class, key)
111
+ self._cache_initialized = True
112
+
113
+ def _set_active_config_class(self, config_class: type[LinglongConfigBase]) -> None:
114
+ with self._lock.write_locked():
115
+ self._active_config_class = config_class
116
+ self._config_cache.clear()
117
+ self._cache_initialized = False
118
+ self._ensure_cache_initialized()
119
+
120
+ def __getattr__(self, name: str) -> Any:
121
+ if name.startswith('_'):
122
+ return object.__getattribute__(self, name)
123
+ self._ensure_cache_initialized()
124
+ with self._lock.read_locked():
125
+ if name in self._config_cache:
126
+ return self._config_cache[name]
127
+ if hasattr(self._active_config_class, name):
128
+ value = getattr(self._active_config_class, name)
129
+ with self._lock.write_locked():
130
+ self._config_cache[name] = value
131
+ return value
132
+ raise AttributeError(f"Config has no attribute '{name}'")
133
+
134
+ def __setattr__(self, name: str, value: Any) -> None:
135
+ if name.startswith('_'):
136
+ object.__setattr__(self, name, value)
137
+ return
138
+ self._ensure_cache_initialized()
139
+ with self._lock.write_locked():
140
+ self._config_cache[name] = value
141
+ setattr(self._active_config_class, name, value)
142
+
143
+ def apply_updates(self, updates: Dict[str, Any]) -> None:
144
+ """批量更新配置值 / Apply a batch of configuration updates."""
145
+
146
+ if not updates:
147
+ return
148
+
149
+ self._ensure_cache_initialized()
150
+ with self._lock.write_locked():
151
+ for key, value in updates.items():
152
+ self._config_cache[key] = value
153
+ setattr(self._active_config_class, key, value)
154
+
155
+ def snapshot(self) -> Dict[str, Any]:
156
+ self._ensure_cache_initialized()
157
+ with self._lock.read_locked():
158
+ return self._config_cache.copy()
159
+
160
+ def reset(self) -> None:
161
+ with self._lock.write_locked():
162
+ self._config_cache.clear()
163
+ self._cache_initialized = False
164
+ self._ensure_cache_initialized()
165
+
166
+ def load_from_dict(self, config_dict: Dict[str, Any]) -> None:
167
+ """从字典批量加载配置 / Load all config values from a dict."""
168
+
169
+ with self._lock.write_locked():
170
+ self._config_cache.clear()
171
+ self._config_cache.update(config_dict)
172
+ for key, value in config_dict.items():
173
+ setattr(self._active_config_class, key, value)
174
+ self._cache_initialized = True
175
+ self._ensure_cache_initialized()
176
+
177
+
178
+ LinglongConfig = LinglongConfigProxy()
179
+
180
+
181
+ def init_config(config_dict: Dict[str, type[LinglongConfigBase]], mode_name: str | None = None) -> LinglongConfigProxy:
182
+ """初始化配置 / Initialize config based on NE_CONFIG or provided mode."""
183
+
184
+ if mode_name is None:
185
+ mode_name = os.getenv('LINGLONG_CONFIG', 'prod')
186
+ logger.info("Linglong config mode not specified, using NE_CONFIG: %s", mode_name)
187
+
188
+ config_cls = config_dict.get(mode_name)
189
+ if config_cls is None:
190
+ raise ValueError(
191
+ f"Config mode '{mode_name}' not found in config_dict. Available: {list(config_dict.keys())}"
192
+ )
193
+
194
+ LinglongConfig._set_active_config_class(config_cls)
195
+ logger.info("Linglong Config initialized with mode: %s, class: %s", mode_name, config_cls.__name__)
196
+ return LinglongConfig
@@ -0,0 +1,38 @@
1
+ """Linglong Web 常量定义 / Linglong Web constants."""
2
+ import http
3
+ from enum import StrEnum
4
+
5
+
6
+ DEFAULT_DB_ALIAS = "default"
7
+
8
+ class HeaderKey(StrEnum):
9
+ """请求头键常量 / HTTP header keys"""
10
+
11
+ REQUEST_ID = "x-linglong-reqid"
12
+
13
+
14
+ class Environment(StrEnum):
15
+ """运行环境常量 / Deployment environment constants"""
16
+
17
+ DEVELOPMENT = "development"
18
+ PRODUCTION = "production"
19
+
20
+
21
+ class LinglongConst:
22
+ """框架级别常量集合 / Framework-wide constants."""
23
+
24
+ OID_HEADER_KEY = HeaderKey.REQUEST_ID
25
+ HTTP_METHODS = tuple(method.value for method in http.HTTPMethod)
26
+ ENVIRONMENT = Environment
27
+
28
+ @classmethod
29
+ def is_http_method_supported(cls, method: str) -> bool:
30
+ """判断 HTTP Method 是否被支持 / Check if HTTP method is allowed."""
31
+
32
+ return method in cls.HTTP_METHODS
33
+
34
+ # 注意:linglong 只维护与 Web 框架基础能力相关的常量。
35
+ # Note: linglong keeps only core web-framework constants.
36
+ #
37
+ # 类似“服务注册 / 健康检查 / 远端配置 / 容器 hostname 判定”等微服务框架能力
38
+ # 应由 cancan 统一内聚管理,禁止通过调用方反向注入到 linglong。