jettask 0.2.18__py3-none-any.whl → 0.2.20__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.
- jettask/__init__.py +60 -2
- jettask/cli.py +314 -228
- jettask/config/__init__.py +9 -1
- jettask/config/config.py +245 -0
- jettask/config/env_loader.py +381 -0
- jettask/config/lua_scripts.py +158 -0
- jettask/config/nacos_config.py +132 -5
- jettask/core/__init__.py +1 -1
- jettask/core/app.py +1573 -666
- jettask/core/app_importer.py +33 -16
- jettask/core/container.py +532 -0
- jettask/core/task.py +1 -4
- jettask/core/unified_manager_base.py +2 -2
- jettask/executor/__init__.py +38 -0
- jettask/executor/core.py +625 -0
- jettask/executor/executor.py +338 -0
- jettask/executor/orchestrator.py +290 -0
- jettask/executor/process_entry.py +638 -0
- jettask/executor/task_executor.py +317 -0
- jettask/messaging/__init__.py +68 -0
- jettask/messaging/event_pool.py +2188 -0
- jettask/messaging/reader.py +519 -0
- jettask/messaging/registry.py +266 -0
- jettask/messaging/scanner.py +369 -0
- jettask/messaging/sender.py +312 -0
- jettask/persistence/__init__.py +118 -0
- jettask/persistence/backlog_monitor.py +567 -0
- jettask/{backend/data_access.py → persistence/base.py} +58 -57
- jettask/persistence/consumer.py +315 -0
- jettask/{core → persistence}/db_manager.py +23 -22
- jettask/persistence/maintenance.py +81 -0
- jettask/persistence/message_consumer.py +259 -0
- jettask/{backend/namespace_data_access.py → persistence/namespace.py} +66 -98
- jettask/persistence/offline_recovery.py +196 -0
- jettask/persistence/queue_discovery.py +215 -0
- jettask/persistence/task_persistence.py +218 -0
- jettask/persistence/task_updater.py +583 -0
- jettask/scheduler/__init__.py +2 -2
- jettask/scheduler/loader.py +6 -5
- jettask/scheduler/run_scheduler.py +1 -1
- jettask/scheduler/scheduler.py +7 -7
- jettask/scheduler/{unified_scheduler_manager.py → scheduler_coordinator.py} +18 -13
- jettask/task/__init__.py +16 -0
- jettask/{router.py → task/router.py} +26 -8
- jettask/task/task_center/__init__.py +9 -0
- jettask/task/task_executor.py +318 -0
- jettask/task/task_registry.py +291 -0
- jettask/test_connection_monitor.py +73 -0
- jettask/utils/__init__.py +31 -1
- jettask/{monitor/run_backlog_collector.py → utils/backlog_collector.py} +1 -1
- jettask/utils/db_connector.py +1629 -0
- jettask/{db_init.py → utils/db_init.py} +1 -1
- jettask/utils/rate_limit/__init__.py +30 -0
- jettask/utils/rate_limit/concurrency_limiter.py +665 -0
- jettask/utils/rate_limit/config.py +145 -0
- jettask/utils/rate_limit/limiter.py +41 -0
- jettask/utils/rate_limit/manager.py +269 -0
- jettask/utils/rate_limit/qps_limiter.py +154 -0
- jettask/utils/rate_limit/task_limiter.py +384 -0
- jettask/utils/serializer.py +3 -0
- jettask/{monitor/stream_backlog_monitor.py → utils/stream_backlog.py} +14 -6
- jettask/utils/time_sync.py +173 -0
- jettask/webui/__init__.py +27 -0
- jettask/{api/v1 → webui/api}/alerts.py +1 -1
- jettask/{api/v1 → webui/api}/analytics.py +2 -2
- jettask/{api/v1 → webui/api}/namespaces.py +1 -1
- jettask/{api/v1 → webui/api}/overview.py +1 -1
- jettask/{api/v1 → webui/api}/queues.py +3 -3
- jettask/{api/v1 → webui/api}/scheduled.py +1 -1
- jettask/{api/v1 → webui/api}/settings.py +1 -1
- jettask/{api.py → webui/app.py} +253 -145
- jettask/webui/namespace_manager/__init__.py +10 -0
- jettask/{multi_namespace_consumer.py → webui/namespace_manager/multi.py} +69 -22
- jettask/{unified_consumer_manager.py → webui/namespace_manager/unified.py} +1 -1
- jettask/{run.py → webui/run.py} +2 -2
- jettask/{services → webui/services}/__init__.py +1 -3
- jettask/{services → webui/services}/overview_service.py +34 -16
- jettask/{services → webui/services}/queue_service.py +1 -1
- jettask/{backend → webui/services}/queue_stats_v2.py +1 -1
- jettask/{services → webui/services}/settings_service.py +1 -1
- jettask/worker/__init__.py +53 -0
- jettask/worker/lifecycle.py +1507 -0
- jettask/worker/manager.py +583 -0
- jettask/{core/offline_worker_recovery.py → worker/recovery.py} +268 -175
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/METADATA +2 -71
- jettask-0.2.20.dist-info/RECORD +145 -0
- jettask/__main__.py +0 -140
- jettask/api/__init__.py +0 -103
- jettask/backend/__init__.py +0 -1
- jettask/backend/api/__init__.py +0 -3
- jettask/backend/api/v1/__init__.py +0 -17
- jettask/backend/api/v1/monitoring.py +0 -431
- jettask/backend/api/v1/namespaces.py +0 -504
- jettask/backend/api/v1/queues.py +0 -342
- jettask/backend/api/v1/tasks.py +0 -367
- jettask/backend/core/__init__.py +0 -3
- jettask/backend/core/cache.py +0 -221
- jettask/backend/core/database.py +0 -200
- jettask/backend/core/exceptions.py +0 -102
- jettask/backend/dependencies.py +0 -261
- jettask/backend/init_meta_db.py +0 -158
- jettask/backend/main.py +0 -1426
- jettask/backend/main_unified.py +0 -78
- jettask/backend/main_v2.py +0 -394
- jettask/backend/models/__init__.py +0 -3
- jettask/backend/models/requests.py +0 -236
- jettask/backend/models/responses.py +0 -230
- jettask/backend/namespace_api_old.py +0 -267
- jettask/backend/services/__init__.py +0 -3
- jettask/backend/start.py +0 -42
- jettask/backend/unified_api_router.py +0 -1541
- jettask/cleanup_deprecated_tables.sql +0 -16
- jettask/core/consumer_manager.py +0 -1695
- jettask/core/delay_scanner.py +0 -256
- jettask/core/event_pool.py +0 -1700
- jettask/core/heartbeat_process.py +0 -222
- jettask/core/task_batch.py +0 -153
- jettask/core/worker_scanner.py +0 -271
- jettask/executors/__init__.py +0 -5
- jettask/executors/asyncio.py +0 -876
- jettask/executors/base.py +0 -30
- jettask/executors/common.py +0 -148
- jettask/executors/multi_asyncio.py +0 -309
- jettask/gradio_app.py +0 -570
- jettask/integrated_gradio_app.py +0 -1088
- jettask/main.py +0 -0
- jettask/monitoring/__init__.py +0 -3
- jettask/pg_consumer.py +0 -1896
- jettask/run_monitor.py +0 -22
- jettask/run_webui.py +0 -148
- jettask/scheduler/multi_namespace_scheduler.py +0 -294
- jettask/scheduler/unified_manager.py +0 -450
- jettask/task_center_client.py +0 -150
- jettask/utils/serializer_optimized.py +0 -33
- jettask/webui_exceptions.py +0 -67
- jettask-0.2.18.dist-info/RECORD +0 -150
- /jettask/{constants.py → config/constants.py} +0 -0
- /jettask/{backend/config.py → config/task_center.py} +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/pg_consumer_v2.py +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/add_execution_time_field.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_new_tables.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/create_tables_v3.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/migrate_to_new_structure.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql/modify_time_fields.sql +0 -0
- /jettask/{pg_consumer → messaging/pg_consumer}/sql_utils.py +0 -0
- /jettask/{models.py → persistence/models.py} +0 -0
- /jettask/scheduler/{manager.py → task_crud.py} +0 -0
- /jettask/{schema.sql → schemas/schema.sql} +0 -0
- /jettask/{task_center.py → task/task_center/client.py} +0 -0
- /jettask/{monitoring → utils}/file_watcher.py +0 -0
- /jettask/{services/redis_monitor_service.py → utils/redis_monitor.py} +0 -0
- /jettask/{api/v1 → webui/api}/__init__.py +0 -0
- /jettask/{webui_config.py → webui/config.py} +0 -0
- /jettask/{webui_models → webui/models}/__init__.py +0 -0
- /jettask/{webui_models → webui/models}/namespace.py +0 -0
- /jettask/{services → webui/services}/alert_service.py +0 -0
- /jettask/{services → webui/services}/analytics_service.py +0 -0
- /jettask/{services → webui/services}/scheduled_task_service.py +0 -0
- /jettask/{services → webui/services}/task_service.py +0 -0
- /jettask/{webui_sql → webui/sql}/batch_upsert_functions.sql +0 -0
- /jettask/{webui_sql → webui/sql}/verify_database.sql +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.18.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
jettask/config/__init__.py
CHANGED
@@ -3,5 +3,13 @@ Jettask配置模块
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from .performance import PerformanceConfig, perf_config, env_config
|
6
|
+
from .config import JetTaskConfig
|
7
|
+
from . import lua_scripts
|
6
8
|
|
7
|
-
__all__ = [
|
9
|
+
__all__ = [
|
10
|
+
'PerformanceConfig',
|
11
|
+
'perf_config',
|
12
|
+
'env_config',
|
13
|
+
'JetTaskConfig',
|
14
|
+
'lua_scripts',
|
15
|
+
]
|
jettask/config/config.py
ADDED
@@ -0,0 +1,245 @@
|
|
1
|
+
"""
|
2
|
+
配置管理模块 - 统一的配置定义
|
3
|
+
"""
|
4
|
+
|
5
|
+
from dataclasses import dataclass, field
|
6
|
+
from typing import Optional, Dict, Any
|
7
|
+
|
8
|
+
|
9
|
+
@dataclass
|
10
|
+
class RedisConfig:
|
11
|
+
"""Redis 配置"""
|
12
|
+
url: str = "redis://localhost:6379/0"
|
13
|
+
prefix: str = "jettask"
|
14
|
+
|
15
|
+
# 连接池配置
|
16
|
+
max_connections: int = 50
|
17
|
+
decode_responses: bool = True # 文本模式客户端
|
18
|
+
|
19
|
+
def __post_init__(self):
|
20
|
+
"""验证配置"""
|
21
|
+
if not self.url:
|
22
|
+
raise ValueError("Redis URL is required")
|
23
|
+
if not self.prefix:
|
24
|
+
raise ValueError("Redis prefix is required")
|
25
|
+
|
26
|
+
|
27
|
+
@dataclass
|
28
|
+
class ExecutorConfig:
|
29
|
+
"""执行器配置"""
|
30
|
+
type: str = "asyncio" # asyncio, multi_asyncio, process, thread
|
31
|
+
concurrency: int = 10
|
32
|
+
prefetch_multiplier: int = 1 # 预取倍数
|
33
|
+
|
34
|
+
# Worker配置
|
35
|
+
worker_heartbeat_interval: float = 1.0 # 心跳间隔(秒)
|
36
|
+
worker_heartbeat_timeout: float = 3.0 # 心跳超时(秒)
|
37
|
+
|
38
|
+
def __post_init__(self):
|
39
|
+
"""验证配置"""
|
40
|
+
if self.concurrency < 1:
|
41
|
+
raise ValueError("Concurrency must be at least 1")
|
42
|
+
if self.prefetch_multiplier < 1:
|
43
|
+
raise ValueError("Prefetch multiplier must be at least 1")
|
44
|
+
|
45
|
+
|
46
|
+
@dataclass
|
47
|
+
class RateLimitConfig:
|
48
|
+
"""限流配置"""
|
49
|
+
enabled: bool = False
|
50
|
+
strategy: str = "local" # local, ondemand
|
51
|
+
|
52
|
+
# Local sliding window配置
|
53
|
+
qps_limit: Optional[int] = None
|
54
|
+
window_size: float = 1.0
|
55
|
+
sync_interval: float = 5.0 # 配额同步间隔(秒)
|
56
|
+
|
57
|
+
def __post_init__(self):
|
58
|
+
"""验证配置"""
|
59
|
+
if self.enabled and not self.qps_limit:
|
60
|
+
raise ValueError("QPS limit is required when rate limit is enabled")
|
61
|
+
|
62
|
+
|
63
|
+
@dataclass
|
64
|
+
class MessageConfig:
|
65
|
+
"""消息配置"""
|
66
|
+
# 延迟队列扫描
|
67
|
+
delayed_scan_interval: float = 0.05 # 扫描间隔(秒)
|
68
|
+
delayed_batch_size: int = 100 # 批量处理大小
|
69
|
+
|
70
|
+
# 消息读取
|
71
|
+
read_block_time: int = 1000 # 阻塞读取时间(毫秒)
|
72
|
+
read_batch_size: int = 1 # 每次读取消息数
|
73
|
+
|
74
|
+
# 消息重试
|
75
|
+
max_retries: int = 3
|
76
|
+
retry_backoff: float = 1.0 # 重试退避时间(秒)
|
77
|
+
|
78
|
+
|
79
|
+
@dataclass
|
80
|
+
class ConsumerConfig:
|
81
|
+
"""消费者配置"""
|
82
|
+
strategy: str = "heartbeat" # heartbeat, reuse
|
83
|
+
|
84
|
+
# 心跳策略配置
|
85
|
+
heartbeat_interval: float = 1.0
|
86
|
+
heartbeat_timeout: float = 3.0
|
87
|
+
|
88
|
+
# 复用策略配置
|
89
|
+
reuse_timeout: float = 60.0
|
90
|
+
|
91
|
+
def __post_init__(self):
|
92
|
+
"""验证配置"""
|
93
|
+
valid_strategies = ["heartbeat", "reuse"]
|
94
|
+
if self.strategy not in valid_strategies:
|
95
|
+
raise ValueError(f"Consumer strategy must be one of {valid_strategies}")
|
96
|
+
|
97
|
+
|
98
|
+
@dataclass
|
99
|
+
class WorkerConfig:
|
100
|
+
"""Worker 配置"""
|
101
|
+
worker_id: Optional[str] = None # 如果不指定,会自动生成
|
102
|
+
hostname: Optional[str] = None # 如果不指定,会自动获取
|
103
|
+
|
104
|
+
# Worker状态管理
|
105
|
+
state_sync_enabled: bool = True
|
106
|
+
state_pubsub_enabled: bool = True # 是否启用状态变化的Pub/Sub通知
|
107
|
+
|
108
|
+
# Worker扫描配置
|
109
|
+
scanner_enabled: bool = True
|
110
|
+
scanner_interval: float = 1.0 # 扫描间隔(秒)
|
111
|
+
|
112
|
+
|
113
|
+
@dataclass
|
114
|
+
class JetTaskConfig:
|
115
|
+
"""
|
116
|
+
JetTask 统一配置
|
117
|
+
|
118
|
+
示例:
|
119
|
+
config = JetTaskConfig(
|
120
|
+
redis=RedisConfig(
|
121
|
+
url="redis://localhost:6379/0",
|
122
|
+
prefix="myapp"
|
123
|
+
),
|
124
|
+
executor=ExecutorConfig(
|
125
|
+
type="asyncio",
|
126
|
+
concurrency=20
|
127
|
+
),
|
128
|
+
rate_limit=RateLimitConfig(
|
129
|
+
enabled=True,
|
130
|
+
qps_limit=100
|
131
|
+
)
|
132
|
+
)
|
133
|
+
"""
|
134
|
+
redis: RedisConfig = field(default_factory=RedisConfig)
|
135
|
+
executor: ExecutorConfig = field(default_factory=ExecutorConfig)
|
136
|
+
rate_limit: RateLimitConfig = field(default_factory=RateLimitConfig)
|
137
|
+
message: MessageConfig = field(default_factory=MessageConfig)
|
138
|
+
consumer: ConsumerConfig = field(default_factory=ConsumerConfig)
|
139
|
+
worker: WorkerConfig = field(default_factory=WorkerConfig)
|
140
|
+
|
141
|
+
# 其他全局配置
|
142
|
+
debug: bool = False
|
143
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def from_dict(cls, config_dict: Dict[str, Any]) -> 'JetTaskConfig':
|
147
|
+
"""
|
148
|
+
从字典创建配置对象
|
149
|
+
|
150
|
+
Args:
|
151
|
+
config_dict: 配置字典
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
JetTaskConfig实例
|
155
|
+
|
156
|
+
示例:
|
157
|
+
config = JetTaskConfig.from_dict({
|
158
|
+
'redis': {
|
159
|
+
'url': 'redis://localhost:6379/0',
|
160
|
+
'prefix': 'myapp'
|
161
|
+
},
|
162
|
+
'executor': {
|
163
|
+
'concurrency': 20
|
164
|
+
}
|
165
|
+
})
|
166
|
+
"""
|
167
|
+
redis_config = RedisConfig(**config_dict.get('redis', {}))
|
168
|
+
executor_config = ExecutorConfig(**config_dict.get('executor', {}))
|
169
|
+
rate_limit_config = RateLimitConfig(**config_dict.get('rate_limit', {}))
|
170
|
+
message_config = MessageConfig(**config_dict.get('message', {}))
|
171
|
+
consumer_config = ConsumerConfig(**config_dict.get('consumer', {}))
|
172
|
+
worker_config = WorkerConfig(**config_dict.get('worker', {}))
|
173
|
+
|
174
|
+
return cls(
|
175
|
+
redis=redis_config,
|
176
|
+
executor=executor_config,
|
177
|
+
rate_limit=rate_limit_config,
|
178
|
+
message=message_config,
|
179
|
+
consumer=consumer_config,
|
180
|
+
worker=worker_config,
|
181
|
+
debug=config_dict.get('debug', False),
|
182
|
+
extra=config_dict.get('extra', {})
|
183
|
+
)
|
184
|
+
|
185
|
+
def to_dict(self) -> Dict[str, Any]:
|
186
|
+
"""
|
187
|
+
转换为字典
|
188
|
+
|
189
|
+
Returns:
|
190
|
+
配置字典
|
191
|
+
"""
|
192
|
+
from dataclasses import asdict
|
193
|
+
return asdict(self)
|
194
|
+
|
195
|
+
def validate(self):
|
196
|
+
"""验证所有配置"""
|
197
|
+
# 各个子配置在__post_init__中已经验证
|
198
|
+
# 这里可以添加跨配置的验证逻辑
|
199
|
+
pass
|
200
|
+
|
201
|
+
|
202
|
+
# 便捷函数:创建默认配置
|
203
|
+
def create_default_config(redis_url: str = "redis://localhost:6379/0",
|
204
|
+
redis_prefix: str = "jettask") -> JetTaskConfig:
|
205
|
+
"""
|
206
|
+
创建默认配置
|
207
|
+
|
208
|
+
Args:
|
209
|
+
redis_url: Redis连接URL
|
210
|
+
redis_prefix: Redis键前缀
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
默认配置对象
|
214
|
+
"""
|
215
|
+
return JetTaskConfig(
|
216
|
+
redis=RedisConfig(url=redis_url, prefix=redis_prefix)
|
217
|
+
)
|
218
|
+
|
219
|
+
|
220
|
+
# 便捷函数:从环境变量创建配置
|
221
|
+
def create_config_from_env() -> JetTaskConfig:
|
222
|
+
"""
|
223
|
+
从环境变量创建配置
|
224
|
+
|
225
|
+
环境变量:
|
226
|
+
JETTASK_REDIS_URL: Redis连接URL
|
227
|
+
JETTASK_REDIS_PREFIX: Redis键前缀
|
228
|
+
JETTASK_EXECUTOR_CONCURRENCY: 并发数
|
229
|
+
JETTASK_DEBUG: 调试模式
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
配置对象
|
233
|
+
"""
|
234
|
+
import os
|
235
|
+
|
236
|
+
redis_url = os.getenv('JETTASK_REDIS_URL', 'redis://localhost:6379/0')
|
237
|
+
redis_prefix = os.getenv('JETTASK_REDIS_PREFIX', 'jettask')
|
238
|
+
concurrency = int(os.getenv('JETTASK_EXECUTOR_CONCURRENCY', '10'))
|
239
|
+
debug = os.getenv('JETTASK_DEBUG', 'false').lower() == 'true'
|
240
|
+
|
241
|
+
return JetTaskConfig(
|
242
|
+
redis=RedisConfig(url=redis_url, prefix=redis_prefix),
|
243
|
+
executor=ExecutorConfig(concurrency=concurrency),
|
244
|
+
debug=debug
|
245
|
+
)
|
@@ -0,0 +1,381 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
环境变量加载器
|
4
|
+
|
5
|
+
提供统一的环境变量加载和管理功能
|
6
|
+
"""
|
7
|
+
import os
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Optional, Dict, Any
|
10
|
+
from dotenv import load_dotenv
|
11
|
+
import logging
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class EnvLoader:
|
17
|
+
"""环境变量加载器
|
18
|
+
|
19
|
+
功能:
|
20
|
+
1. 从.env文件加载环境变量
|
21
|
+
2. 支持多个.env文件按优先级加载
|
22
|
+
3. 提供类型安全的环境变量获取方法
|
23
|
+
4. 支持默认值
|
24
|
+
|
25
|
+
使用示例:
|
26
|
+
# 基本使用
|
27
|
+
loader = EnvLoader()
|
28
|
+
loader.load_env_file('.env')
|
29
|
+
redis_url = loader.get('JETTASK_REDIS_URL')
|
30
|
+
|
31
|
+
# 加载多个文件(后加载的覆盖先加载的)
|
32
|
+
loader = EnvLoader()
|
33
|
+
loader.load_env_file('.env')
|
34
|
+
loader.load_env_file('.env.local', override=True)
|
35
|
+
|
36
|
+
# 类型转换
|
37
|
+
max_conn = loader.get_int('JETTASK_MAX_CONNECTIONS', default=200)
|
38
|
+
debug = loader.get_bool('DEBUG', default=False)
|
39
|
+
|
40
|
+
# 批量获取配置
|
41
|
+
config = loader.get_config_dict('JETTASK_')
|
42
|
+
"""
|
43
|
+
|
44
|
+
def __init__(self, auto_load: bool = False):
|
45
|
+
"""
|
46
|
+
初始化环境变量加载器
|
47
|
+
|
48
|
+
Args:
|
49
|
+
auto_load: 是否自动加载当前目录的.env文件
|
50
|
+
"""
|
51
|
+
self._loaded_files = []
|
52
|
+
|
53
|
+
if auto_load:
|
54
|
+
self.auto_load()
|
55
|
+
|
56
|
+
def auto_load(self, search_paths: list = None) -> bool:
|
57
|
+
"""
|
58
|
+
自动查找并加载.env文件
|
59
|
+
|
60
|
+
查找顺序:
|
61
|
+
1. .env.local (本地开发配置,优先级最高)
|
62
|
+
2. .env.{ENVIRONMENT} (如 .env.production, .env.development)
|
63
|
+
3. .env (默认配置)
|
64
|
+
|
65
|
+
Args:
|
66
|
+
search_paths: 搜索路径列表,默认为当前目录
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
是否成功加载至少一个文件
|
70
|
+
"""
|
71
|
+
if search_paths is None:
|
72
|
+
search_paths = [Path.cwd()]
|
73
|
+
|
74
|
+
env = os.environ.get('ENVIRONMENT', os.environ.get('ENV', 'development'))
|
75
|
+
|
76
|
+
# 按优先级查找文件
|
77
|
+
env_files = [
|
78
|
+
'.env',
|
79
|
+
f'.env.{env}',
|
80
|
+
'.env.local',
|
81
|
+
]
|
82
|
+
|
83
|
+
loaded_any = False
|
84
|
+
for search_path in search_paths:
|
85
|
+
search_path = Path(search_path)
|
86
|
+
for env_file in env_files:
|
87
|
+
file_path = search_path / env_file
|
88
|
+
if file_path.exists():
|
89
|
+
self.load_env_file(str(file_path), override=True)
|
90
|
+
loaded_any = True
|
91
|
+
|
92
|
+
return loaded_any
|
93
|
+
|
94
|
+
def load_env_file(self, file_path: str, override: bool = True) -> bool:
|
95
|
+
"""
|
96
|
+
从文件加载环境变量
|
97
|
+
|
98
|
+
Args:
|
99
|
+
file_path: .env文件路径
|
100
|
+
override: 是否覆盖已存在的环境变量
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
是否成功加载
|
104
|
+
|
105
|
+
Raises:
|
106
|
+
FileNotFoundError: 文件不存在
|
107
|
+
"""
|
108
|
+
path = Path(file_path).resolve()
|
109
|
+
|
110
|
+
if not path.exists():
|
111
|
+
raise FileNotFoundError(f"Environment file not found: {file_path}")
|
112
|
+
|
113
|
+
if not path.is_file():
|
114
|
+
raise ValueError(f"Not a file: {file_path}")
|
115
|
+
|
116
|
+
logger.info(f"Loading environment variables from: {path}")
|
117
|
+
|
118
|
+
# 加载环境变量
|
119
|
+
success = load_dotenv(str(path), override=override)
|
120
|
+
|
121
|
+
if success:
|
122
|
+
self._loaded_files.append(str(path))
|
123
|
+
logger.info(f"✓ Loaded environment variables from: {path}")
|
124
|
+
else:
|
125
|
+
logger.warning(f"Failed to load environment variables from: {path}")
|
126
|
+
|
127
|
+
return success
|
128
|
+
|
129
|
+
def get(self, key: str, default: Optional[str] = None) -> Optional[str]:
|
130
|
+
"""
|
131
|
+
获取字符串类型的环境变量
|
132
|
+
|
133
|
+
Args:
|
134
|
+
key: 环境变量名
|
135
|
+
default: 默认值
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
环境变量值或默认值
|
139
|
+
"""
|
140
|
+
return os.environ.get(key, default)
|
141
|
+
|
142
|
+
def get_int(self, key: str, default: Optional[int] = None) -> Optional[int]:
|
143
|
+
"""
|
144
|
+
获取整数类型的环境变量
|
145
|
+
|
146
|
+
Args:
|
147
|
+
key: 环境变量名
|
148
|
+
default: 默认值
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
整数值或默认值
|
152
|
+
|
153
|
+
Raises:
|
154
|
+
ValueError: 环境变量值无法转换为整数
|
155
|
+
"""
|
156
|
+
value = os.environ.get(key)
|
157
|
+
if value is None:
|
158
|
+
return default
|
159
|
+
|
160
|
+
try:
|
161
|
+
return int(value)
|
162
|
+
except ValueError:
|
163
|
+
raise ValueError(f"Environment variable {key}='{value}' cannot be converted to int")
|
164
|
+
|
165
|
+
def get_float(self, key: str, default: Optional[float] = None) -> Optional[float]:
|
166
|
+
"""
|
167
|
+
获取浮点数类型的环境变量
|
168
|
+
|
169
|
+
Args:
|
170
|
+
key: 环境变量名
|
171
|
+
default: 默认值
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
浮点数值或默认值
|
175
|
+
|
176
|
+
Raises:
|
177
|
+
ValueError: 环境变量值无法转换为浮点数
|
178
|
+
"""
|
179
|
+
value = os.environ.get(key)
|
180
|
+
if value is None:
|
181
|
+
return default
|
182
|
+
|
183
|
+
try:
|
184
|
+
return float(value)
|
185
|
+
except ValueError:
|
186
|
+
raise ValueError(f"Environment variable {key}='{value}' cannot be converted to float")
|
187
|
+
|
188
|
+
def get_bool(self, key: str, default: Optional[bool] = None) -> Optional[bool]:
|
189
|
+
"""
|
190
|
+
获取布尔类型的环境变量
|
191
|
+
|
192
|
+
支持的true值: true, 1, yes, on, enabled
|
193
|
+
支持的false值: false, 0, no, off, disabled
|
194
|
+
|
195
|
+
Args:
|
196
|
+
key: 环境变量名
|
197
|
+
default: 默认值
|
198
|
+
|
199
|
+
Returns:
|
200
|
+
布尔值或默认值
|
201
|
+
|
202
|
+
Raises:
|
203
|
+
ValueError: 环境变量值无法转换为布尔值
|
204
|
+
"""
|
205
|
+
value = os.environ.get(key)
|
206
|
+
if value is None:
|
207
|
+
return default
|
208
|
+
|
209
|
+
value_lower = value.lower().strip()
|
210
|
+
|
211
|
+
if value_lower in ('true', '1', 'yes', 'on', 'enabled'):
|
212
|
+
return True
|
213
|
+
elif value_lower in ('false', '0', 'no', 'off', 'disabled'):
|
214
|
+
return False
|
215
|
+
else:
|
216
|
+
raise ValueError(
|
217
|
+
f"Environment variable {key}='{value}' cannot be converted to bool. "
|
218
|
+
f"Valid values: true/false, 1/0, yes/no, on/off, enabled/disabled"
|
219
|
+
)
|
220
|
+
|
221
|
+
def get_list(self, key: str, separator: str = ',', default: Optional[list] = None) -> Optional[list]:
|
222
|
+
"""
|
223
|
+
获取列表类型的环境变量(逗号分隔)
|
224
|
+
|
225
|
+
Args:
|
226
|
+
key: 环境变量名
|
227
|
+
separator: 分隔符,默认为逗号
|
228
|
+
default: 默认值
|
229
|
+
|
230
|
+
Returns:
|
231
|
+
列表或默认值
|
232
|
+
"""
|
233
|
+
value = os.environ.get(key)
|
234
|
+
if value is None:
|
235
|
+
return default or []
|
236
|
+
|
237
|
+
# 分割并去除空白
|
238
|
+
return [item.strip() for item in value.split(separator) if item.strip()]
|
239
|
+
|
240
|
+
def get_dict(self, key: str, default: Optional[dict] = None) -> Optional[dict]:
|
241
|
+
"""
|
242
|
+
获取字典类型的环境变量(JSON格式)
|
243
|
+
|
244
|
+
Args:
|
245
|
+
key: 环境变量名
|
246
|
+
default: 默认值
|
247
|
+
|
248
|
+
Returns:
|
249
|
+
字典或默认值
|
250
|
+
|
251
|
+
Raises:
|
252
|
+
ValueError: JSON解析失败
|
253
|
+
"""
|
254
|
+
value = os.environ.get(key)
|
255
|
+
if value is None:
|
256
|
+
return default or {}
|
257
|
+
|
258
|
+
try:
|
259
|
+
import json
|
260
|
+
return json.loads(value)
|
261
|
+
except json.JSONDecodeError as e:
|
262
|
+
raise ValueError(f"Environment variable {key}='{value}' is not valid JSON: {e}")
|
263
|
+
|
264
|
+
def get_config_dict(self, prefix: str = '') -> Dict[str, str]:
|
265
|
+
"""
|
266
|
+
获取所有指定前缀的环境变量(返回字典)
|
267
|
+
|
268
|
+
Args:
|
269
|
+
prefix: 环境变量前缀(如 'JETTASK_')
|
270
|
+
|
271
|
+
Returns:
|
272
|
+
环境变量字典(键不包含前缀)
|
273
|
+
|
274
|
+
示例:
|
275
|
+
# 假设环境变量: JETTASK_REDIS_URL=redis://localhost, JETTASK_PG_URL=postgresql://...
|
276
|
+
config = loader.get_config_dict('JETTASK_')
|
277
|
+
# 返回: {'REDIS_URL': 'redis://localhost', 'PG_URL': 'postgresql://...'}
|
278
|
+
"""
|
279
|
+
config = {}
|
280
|
+
for key, value in os.environ.items():
|
281
|
+
if key.startswith(prefix):
|
282
|
+
# 去除前缀
|
283
|
+
config_key = key[len(prefix):]
|
284
|
+
config[config_key] = value
|
285
|
+
|
286
|
+
return config
|
287
|
+
|
288
|
+
def require(self, key: str) -> str:
|
289
|
+
"""
|
290
|
+
获取必需的环境变量(如果不存在则抛出异常)
|
291
|
+
|
292
|
+
Args:
|
293
|
+
key: 环境变量名
|
294
|
+
|
295
|
+
Returns:
|
296
|
+
环境变量值
|
297
|
+
|
298
|
+
Raises:
|
299
|
+
ValueError: 环境变量未设置
|
300
|
+
"""
|
301
|
+
value = os.environ.get(key)
|
302
|
+
if value is None:
|
303
|
+
raise ValueError(
|
304
|
+
f"Required environment variable '{key}' is not set. "
|
305
|
+
f"Please set it in your environment or .env file."
|
306
|
+
)
|
307
|
+
return value
|
308
|
+
|
309
|
+
def set(self, key: str, value: Any, override: bool = True) -> None:
|
310
|
+
"""
|
311
|
+
设置环境变量
|
312
|
+
|
313
|
+
Args:
|
314
|
+
key: 环境变量名
|
315
|
+
value: 值
|
316
|
+
override: 是否覆盖已存在的值
|
317
|
+
"""
|
318
|
+
if override or key not in os.environ:
|
319
|
+
os.environ[key] = str(value)
|
320
|
+
|
321
|
+
def clear_all(self, prefix: str = '') -> int:
|
322
|
+
"""
|
323
|
+
清除所有指定前缀的环境变量
|
324
|
+
|
325
|
+
Args:
|
326
|
+
prefix: 环境变量前缀(如 'JETTASK_')
|
327
|
+
|
328
|
+
Returns:
|
329
|
+
清除的环境变量数量
|
330
|
+
"""
|
331
|
+
keys_to_remove = [key for key in os.environ.keys() if key.startswith(prefix)]
|
332
|
+
for key in keys_to_remove:
|
333
|
+
del os.environ[key]
|
334
|
+
|
335
|
+
return len(keys_to_remove)
|
336
|
+
|
337
|
+
def get_loaded_files(self) -> list:
|
338
|
+
"""
|
339
|
+
获取已加载的环境变量文件列表
|
340
|
+
|
341
|
+
Returns:
|
342
|
+
文件路径列表
|
343
|
+
"""
|
344
|
+
return self._loaded_files.copy()
|
345
|
+
|
346
|
+
def __repr__(self) -> str:
|
347
|
+
return f"EnvLoader(loaded_files={len(self._loaded_files)})"
|
348
|
+
|
349
|
+
|
350
|
+
# 全局单例实例
|
351
|
+
_global_loader = None
|
352
|
+
|
353
|
+
|
354
|
+
def get_env_loader() -> EnvLoader:
|
355
|
+
"""获取全局环境变量加载器单例"""
|
356
|
+
global _global_loader
|
357
|
+
if _global_loader is None:
|
358
|
+
_global_loader = EnvLoader()
|
359
|
+
return _global_loader
|
360
|
+
|
361
|
+
|
362
|
+
# 便捷函数
|
363
|
+
def load_env(file_path: str = None, override: bool = True) -> EnvLoader:
|
364
|
+
"""
|
365
|
+
加载环境变量(便捷函数)
|
366
|
+
|
367
|
+
Args:
|
368
|
+
file_path: .env文件路径,如果为None则自动查找
|
369
|
+
override: 是否覆盖已存在的环境变量
|
370
|
+
|
371
|
+
Returns:
|
372
|
+
环境变量加载器实例
|
373
|
+
"""
|
374
|
+
loader = get_env_loader()
|
375
|
+
|
376
|
+
if file_path:
|
377
|
+
loader.load_env_file(file_path, override=override)
|
378
|
+
else:
|
379
|
+
loader.auto_load()
|
380
|
+
|
381
|
+
return loader
|