jettask 0.2.5__py3-none-any.whl → 0.2.7__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/monitor/run_backlog_collector.py +96 -0
- jettask/monitor/stream_backlog_monitor.py +362 -0
- jettask/pg_consumer/pg_consumer_v2.py +403 -0
- jettask/pg_consumer/sql_utils.py +182 -0
- jettask/scheduler/__init__.py +17 -0
- jettask/scheduler/add_execution_count.sql +11 -0
- jettask/scheduler/add_priority_field.sql +26 -0
- jettask/scheduler/add_scheduler_id.sql +25 -0
- jettask/scheduler/add_scheduler_id_index.sql +10 -0
- jettask/scheduler/loader.py +249 -0
- jettask/scheduler/make_scheduler_id_required.sql +28 -0
- jettask/scheduler/manager.py +696 -0
- jettask/scheduler/migrate_interval_seconds.sql +9 -0
- jettask/scheduler/models.py +200 -0
- jettask/scheduler/multi_namespace_scheduler.py +294 -0
- jettask/scheduler/performance_optimization.sql +45 -0
- jettask/scheduler/run_scheduler.py +186 -0
- jettask/scheduler/scheduler.py +715 -0
- jettask/scheduler/schema.sql +84 -0
- jettask/scheduler/unified_manager.py +450 -0
- jettask/scheduler/unified_scheduler_manager.py +280 -0
- jettask/webui/backend/api/__init__.py +3 -0
- jettask/webui/backend/api/v1/__init__.py +17 -0
- jettask/webui/backend/api/v1/monitoring.py +431 -0
- jettask/webui/backend/api/v1/namespaces.py +504 -0
- jettask/webui/backend/api/v1/queues.py +342 -0
- jettask/webui/backend/api/v1/tasks.py +367 -0
- jettask/webui/backend/core/__init__.py +3 -0
- jettask/webui/backend/core/cache.py +221 -0
- jettask/webui/backend/core/database.py +200 -0
- jettask/webui/backend/core/exceptions.py +102 -0
- jettask/webui/backend/models/__init__.py +3 -0
- jettask/webui/backend/models/requests.py +236 -0
- jettask/webui/backend/models/responses.py +230 -0
- jettask/webui/backend/services/__init__.py +3 -0
- jettask/webui/frontend/index.html +13 -0
- jettask/webui/models/__init__.py +3 -0
- jettask/webui/models/namespace.py +63 -0
- jettask/webui/sql/batch_upsert_functions.sql +178 -0
- jettask/webui/sql/init_database.sql +640 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/METADATA +80 -10
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/RECORD +46 -53
- jettask/webui/frontend/package-lock.json +0 -4833
- jettask/webui/frontend/package.json +0 -30
- jettask/webui/frontend/src/App.css +0 -109
- jettask/webui/frontend/src/App.jsx +0 -66
- jettask/webui/frontend/src/components/NamespaceSelector.jsx +0 -166
- jettask/webui/frontend/src/components/QueueBacklogChart.jsx +0 -298
- jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +0 -638
- jettask/webui/frontend/src/components/QueueDetailsTable.css +0 -65
- jettask/webui/frontend/src/components/QueueDetailsTable.jsx +0 -487
- jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +0 -465
- jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +0 -423
- jettask/webui/frontend/src/components/TaskFilter.jsx +0 -425
- jettask/webui/frontend/src/components/TimeRangeSelector.css +0 -21
- jettask/webui/frontend/src/components/TimeRangeSelector.jsx +0 -160
- jettask/webui/frontend/src/components/charts/QueueChart.jsx +0 -111
- jettask/webui/frontend/src/components/charts/QueueTrendChart.jsx +0 -115
- jettask/webui/frontend/src/components/charts/WorkerChart.jsx +0 -40
- jettask/webui/frontend/src/components/common/StatsCard.jsx +0 -18
- jettask/webui/frontend/src/components/layout/AppLayout.css +0 -95
- jettask/webui/frontend/src/components/layout/AppLayout.jsx +0 -49
- jettask/webui/frontend/src/components/layout/Header.css +0 -106
- jettask/webui/frontend/src/components/layout/Header.jsx +0 -106
- jettask/webui/frontend/src/components/layout/SideMenu.css +0 -137
- jettask/webui/frontend/src/components/layout/SideMenu.jsx +0 -209
- jettask/webui/frontend/src/components/layout/TabsNav.css +0 -244
- jettask/webui/frontend/src/components/layout/TabsNav.jsx +0 -206
- jettask/webui/frontend/src/components/layout/UserInfo.css +0 -197
- jettask/webui/frontend/src/components/layout/UserInfo.jsx +0 -197
- jettask/webui/frontend/src/contexts/LoadingContext.jsx +0 -27
- jettask/webui/frontend/src/contexts/NamespaceContext.jsx +0 -72
- jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +0 -245
- jettask/webui/frontend/src/index.css +0 -114
- jettask/webui/frontend/src/main.jsx +0 -20
- jettask/webui/frontend/src/pages/Alerts.jsx +0 -684
- jettask/webui/frontend/src/pages/Dashboard/index.css +0 -35
- jettask/webui/frontend/src/pages/Dashboard/index.jsx +0 -281
- jettask/webui/frontend/src/pages/Dashboard.jsx +0 -1330
- jettask/webui/frontend/src/pages/QueueDetail.jsx +0 -1117
- jettask/webui/frontend/src/pages/QueueMonitor.jsx +0 -527
- jettask/webui/frontend/src/pages/Queues.jsx +0 -12
- jettask/webui/frontend/src/pages/ScheduledTasks.jsx +0 -809
- jettask/webui/frontend/src/pages/Settings.jsx +0 -800
- jettask/webui/frontend/src/pages/Workers.jsx +0 -12
- jettask/webui/frontend/src/services/api.js +0 -114
- jettask/webui/frontend/src/services/queueTrend.js +0 -152
- jettask/webui/frontend/src/utils/suppressWarnings.js +0 -22
- jettask/webui/frontend/src/utils/userPreferences.js +0 -154
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/WHEEL +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.5.dist-info → jettask-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,200 @@
|
|
1
|
+
"""
|
2
|
+
Database connection management and utilities
|
3
|
+
"""
|
4
|
+
import asyncio
|
5
|
+
from contextlib import asynccontextmanager
|
6
|
+
from typing import Optional, Dict, Any, AsyncGenerator
|
7
|
+
import asyncpg
|
8
|
+
import redis.asyncio as redis
|
9
|
+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
10
|
+
from sqlalchemy.orm import DeclarativeBase
|
11
|
+
import logging
|
12
|
+
|
13
|
+
from .exceptions import DatabaseConnectionError
|
14
|
+
|
15
|
+
logger = logging.getLogger(__name__)
|
16
|
+
|
17
|
+
|
18
|
+
class DatabaseManager:
|
19
|
+
"""数据库连接管理器"""
|
20
|
+
|
21
|
+
def __init__(self):
|
22
|
+
self._pg_pools: Dict[str, asyncpg.Pool] = {}
|
23
|
+
self._redis_pools: Dict[str, redis.ConnectionPool] = {}
|
24
|
+
self._sqlalchemy_engines: Dict[str, Any] = {}
|
25
|
+
self._session_makers: Dict[str, async_sessionmaker] = {}
|
26
|
+
|
27
|
+
async def get_pg_pool(self, connection_string: str, pool_name: str = "default") -> asyncpg.Pool:
|
28
|
+
"""获取PostgreSQL连接池"""
|
29
|
+
if pool_name not in self._pg_pools:
|
30
|
+
try:
|
31
|
+
# 解析连接字符串
|
32
|
+
if connection_string.startswith('postgresql://'):
|
33
|
+
connection_string = connection_string.replace('postgresql://', '')
|
34
|
+
elif connection_string.startswith('postgresql+asyncpg://'):
|
35
|
+
connection_string = connection_string.replace('postgresql+asyncpg://', '')
|
36
|
+
|
37
|
+
# 分离认证和主机信息
|
38
|
+
auth, host_info = connection_string.split('@')
|
39
|
+
user, password = auth.split(':')
|
40
|
+
host_port, database = host_info.split('/')
|
41
|
+
host, port = host_port.split(':')
|
42
|
+
|
43
|
+
pool = await asyncpg.create_pool(
|
44
|
+
host=host,
|
45
|
+
port=int(port),
|
46
|
+
user=user,
|
47
|
+
password=password,
|
48
|
+
database=database,
|
49
|
+
min_size=2,
|
50
|
+
max_size=10,
|
51
|
+
command_timeout=30
|
52
|
+
)
|
53
|
+
|
54
|
+
self._pg_pools[pool_name] = pool
|
55
|
+
logger.info(f"PostgreSQL连接池已创建: {pool_name}")
|
56
|
+
|
57
|
+
except Exception as e:
|
58
|
+
logger.error(f"创建PostgreSQL连接池失败: {e}")
|
59
|
+
raise DatabaseConnectionError(f"PostgreSQL connection failed: {e}")
|
60
|
+
|
61
|
+
return self._pg_pools[pool_name]
|
62
|
+
|
63
|
+
async def get_redis_client(self, connection_string: str, pool_name: str = "default") -> redis.Redis:
|
64
|
+
"""获取Redis客户端"""
|
65
|
+
if pool_name not in self._redis_pools:
|
66
|
+
try:
|
67
|
+
pool = redis.ConnectionPool.from_url(
|
68
|
+
connection_string,
|
69
|
+
max_connections=20,
|
70
|
+
decode_responses=False # 保持原始字节格式
|
71
|
+
)
|
72
|
+
self._redis_pools[pool_name] = pool
|
73
|
+
logger.info(f"Redis连接池已创建: {pool_name}")
|
74
|
+
|
75
|
+
except Exception as e:
|
76
|
+
logger.error(f"创建Redis连接池失败: {e}")
|
77
|
+
raise DatabaseConnectionError(f"Redis connection failed: {e}")
|
78
|
+
|
79
|
+
return redis.Redis(connection_pool=self._redis_pools[pool_name])
|
80
|
+
|
81
|
+
def get_sqlalchemy_session_maker(
|
82
|
+
self,
|
83
|
+
connection_string: str,
|
84
|
+
pool_name: str = "default"
|
85
|
+
) -> async_sessionmaker[AsyncSession]:
|
86
|
+
"""获取SQLAlchemy会话工厂"""
|
87
|
+
if pool_name not in self._session_makers:
|
88
|
+
try:
|
89
|
+
# 确保使用asyncpg驱动
|
90
|
+
if connection_string.startswith('postgresql://'):
|
91
|
+
connection_string = connection_string.replace('postgresql://', 'postgresql+asyncpg://')
|
92
|
+
|
93
|
+
engine = create_async_engine(
|
94
|
+
connection_string,
|
95
|
+
pool_size=5,
|
96
|
+
max_overflow=10,
|
97
|
+
pool_timeout=30,
|
98
|
+
pool_recycle=3600,
|
99
|
+
echo=False # 生产环境关闭SQL日志
|
100
|
+
)
|
101
|
+
|
102
|
+
session_maker = async_sessionmaker(
|
103
|
+
engine,
|
104
|
+
class_=AsyncSession,
|
105
|
+
expire_on_commit=False
|
106
|
+
)
|
107
|
+
|
108
|
+
self._sqlalchemy_engines[pool_name] = engine
|
109
|
+
self._session_makers[pool_name] = session_maker
|
110
|
+
logger.info(f"SQLAlchemy会话工厂已创建: {pool_name}")
|
111
|
+
|
112
|
+
except Exception as e:
|
113
|
+
logger.error(f"创建SQLAlchemy会话工厂失败: {e}")
|
114
|
+
raise DatabaseConnectionError(f"SQLAlchemy connection failed: {e}")
|
115
|
+
|
116
|
+
return self._session_makers[pool_name]
|
117
|
+
|
118
|
+
@asynccontextmanager
|
119
|
+
async def get_pg_connection(self, pool_name: str = "default") -> AsyncGenerator[asyncpg.Connection, None]:
|
120
|
+
"""获取PostgreSQL连接上下文管理器"""
|
121
|
+
if pool_name not in self._pg_pools:
|
122
|
+
raise DatabaseConnectionError(f"PostgreSQL pool '{pool_name}' not found")
|
123
|
+
|
124
|
+
pool = self._pg_pools[pool_name]
|
125
|
+
async with pool.acquire() as conn:
|
126
|
+
yield conn
|
127
|
+
|
128
|
+
@asynccontextmanager
|
129
|
+
async def get_sqlalchemy_session(self, pool_name: str = "default") -> AsyncGenerator[AsyncSession, None]:
|
130
|
+
"""获取SQLAlchemy会话上下文管理器"""
|
131
|
+
if pool_name not in self._session_makers:
|
132
|
+
raise DatabaseConnectionError(f"SQLAlchemy session maker '{pool_name}' not found")
|
133
|
+
|
134
|
+
session_maker = self._session_makers[pool_name]
|
135
|
+
async with session_maker() as session:
|
136
|
+
try:
|
137
|
+
yield session
|
138
|
+
await session.commit()
|
139
|
+
except Exception:
|
140
|
+
await session.rollback()
|
141
|
+
raise
|
142
|
+
finally:
|
143
|
+
await session.close()
|
144
|
+
|
145
|
+
async def close_all(self):
|
146
|
+
"""关闭所有连接"""
|
147
|
+
# 关闭PostgreSQL连接池
|
148
|
+
for name, pool in self._pg_pools.items():
|
149
|
+
try:
|
150
|
+
await pool.close()
|
151
|
+
logger.info(f"PostgreSQL连接池已关闭: {name}")
|
152
|
+
except Exception as e:
|
153
|
+
logger.error(f"关闭PostgreSQL连接池失败 {name}: {e}")
|
154
|
+
|
155
|
+
# 关闭Redis连接池
|
156
|
+
for name, pool in self._redis_pools.items():
|
157
|
+
try:
|
158
|
+
await pool.disconnect()
|
159
|
+
logger.info(f"Redis连接池已关闭: {name}")
|
160
|
+
except Exception as e:
|
161
|
+
logger.error(f"关闭Redis连接池失败 {name}: {e}")
|
162
|
+
|
163
|
+
# 关闭SQLAlchemy引擎
|
164
|
+
for name, engine in self._sqlalchemy_engines.items():
|
165
|
+
try:
|
166
|
+
await engine.dispose()
|
167
|
+
logger.info(f"SQLAlchemy引擎已关闭: {name}")
|
168
|
+
except Exception as e:
|
169
|
+
logger.error(f"关闭SQLAlchemy引擎失败 {name}: {e}")
|
170
|
+
|
171
|
+
# 清空缓存
|
172
|
+
self._pg_pools.clear()
|
173
|
+
self._redis_pools.clear()
|
174
|
+
self._sqlalchemy_engines.clear()
|
175
|
+
self._session_makers.clear()
|
176
|
+
|
177
|
+
|
178
|
+
# 全局数据库管理器实例
|
179
|
+
db_manager = DatabaseManager()
|
180
|
+
|
181
|
+
|
182
|
+
# 数据库模型基类
|
183
|
+
class Base(DeclarativeBase):
|
184
|
+
pass
|
185
|
+
|
186
|
+
|
187
|
+
# 便捷函数
|
188
|
+
async def get_pg_pool(connection_string: str, pool_name: str = "default") -> asyncpg.Pool:
|
189
|
+
"""便捷函数:获取PostgreSQL连接池"""
|
190
|
+
return await db_manager.get_pg_pool(connection_string, pool_name)
|
191
|
+
|
192
|
+
|
193
|
+
async def get_redis_client(connection_string: str, pool_name: str = "default") -> redis.Redis:
|
194
|
+
"""便捷函数:获取Redis客户端"""
|
195
|
+
return await db_manager.get_redis_client(connection_string, pool_name)
|
196
|
+
|
197
|
+
|
198
|
+
def get_session_maker(connection_string: str, pool_name: str = "default") -> async_sessionmaker[AsyncSession]:
|
199
|
+
"""便捷函数:获取SQLAlchemy会话工厂"""
|
200
|
+
return db_manager.get_sqlalchemy_session_maker(connection_string, pool_name)
|
@@ -0,0 +1,102 @@
|
|
1
|
+
"""
|
2
|
+
Custom exceptions for JetTask WebUI Backend
|
3
|
+
"""
|
4
|
+
from typing import Optional, Dict, Any
|
5
|
+
from fastapi import HTTPException
|
6
|
+
|
7
|
+
|
8
|
+
class JetTaskAPIException(HTTPException):
|
9
|
+
"""Base exception for JetTask API"""
|
10
|
+
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
status_code: int,
|
14
|
+
detail: str,
|
15
|
+
error_code: Optional[str] = None,
|
16
|
+
extra_data: Optional[Dict[str, Any]] = None
|
17
|
+
):
|
18
|
+
super().__init__(status_code=status_code, detail=detail)
|
19
|
+
self.error_code = error_code
|
20
|
+
self.extra_data = extra_data or {}
|
21
|
+
|
22
|
+
|
23
|
+
class NamespaceNotFoundError(JetTaskAPIException):
|
24
|
+
"""Namespace not found error"""
|
25
|
+
|
26
|
+
def __init__(self, namespace: str):
|
27
|
+
super().__init__(
|
28
|
+
status_code=404,
|
29
|
+
detail=f"Namespace '{namespace}' not found",
|
30
|
+
error_code="NAMESPACE_NOT_FOUND",
|
31
|
+
extra_data={"namespace": namespace}
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
class QueueNotFoundError(JetTaskAPIException):
|
36
|
+
"""Queue not found error"""
|
37
|
+
|
38
|
+
def __init__(self, queue_name: str, namespace: str = "default"):
|
39
|
+
super().__init__(
|
40
|
+
status_code=404,
|
41
|
+
detail=f"Queue '{queue_name}' not found in namespace '{namespace}'",
|
42
|
+
error_code="QUEUE_NOT_FOUND",
|
43
|
+
extra_data={"queue_name": queue_name, "namespace": namespace}
|
44
|
+
)
|
45
|
+
|
46
|
+
|
47
|
+
class TaskNotFoundError(JetTaskAPIException):
|
48
|
+
"""Task not found error"""
|
49
|
+
|
50
|
+
def __init__(self, task_id: str):
|
51
|
+
super().__init__(
|
52
|
+
status_code=404,
|
53
|
+
detail=f"Task '{task_id}' not found",
|
54
|
+
error_code="TASK_NOT_FOUND",
|
55
|
+
extra_data={"task_id": task_id}
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
class ValidationError(JetTaskAPIException):
|
60
|
+
"""Validation error"""
|
61
|
+
|
62
|
+
def __init__(self, message: str, field: Optional[str] = None):
|
63
|
+
super().__init__(
|
64
|
+
status_code=400,
|
65
|
+
detail=message,
|
66
|
+
error_code="VALIDATION_ERROR",
|
67
|
+
extra_data={"field": field} if field else {}
|
68
|
+
)
|
69
|
+
|
70
|
+
|
71
|
+
class DatabaseConnectionError(JetTaskAPIException):
|
72
|
+
"""Database connection error"""
|
73
|
+
|
74
|
+
def __init__(self, message: str = "Database connection failed"):
|
75
|
+
super().__init__(
|
76
|
+
status_code=503,
|
77
|
+
detail=message,
|
78
|
+
error_code="DATABASE_CONNECTION_ERROR"
|
79
|
+
)
|
80
|
+
|
81
|
+
|
82
|
+
class RateLimitError(JetTaskAPIException):
|
83
|
+
"""Rate limit exceeded error"""
|
84
|
+
|
85
|
+
def __init__(self, limit: int, window: int):
|
86
|
+
super().__init__(
|
87
|
+
status_code=429,
|
88
|
+
detail=f"Rate limit exceeded: {limit} requests per {window} seconds",
|
89
|
+
error_code="RATE_LIMIT_EXCEEDED",
|
90
|
+
extra_data={"limit": limit, "window": window}
|
91
|
+
)
|
92
|
+
|
93
|
+
|
94
|
+
class InternalServerError(JetTaskAPIException):
|
95
|
+
"""Internal server error"""
|
96
|
+
|
97
|
+
def __init__(self, message: str = "Internal server error"):
|
98
|
+
super().__init__(
|
99
|
+
status_code=500,
|
100
|
+
detail=message,
|
101
|
+
error_code="INTERNAL_SERVER_ERROR"
|
102
|
+
)
|
@@ -0,0 +1,236 @@
|
|
1
|
+
"""
|
2
|
+
Request models for JetTask WebUI Backend
|
3
|
+
"""
|
4
|
+
from typing import List, Dict, Any, Optional, Union
|
5
|
+
from datetime import datetime
|
6
|
+
from pydantic import BaseModel, Field, validator
|
7
|
+
from enum import Enum
|
8
|
+
|
9
|
+
|
10
|
+
class TimeRange(str, Enum):
|
11
|
+
"""时间范围枚举"""
|
12
|
+
FIFTEEN_MINUTES = "15m"
|
13
|
+
THIRTY_MINUTES = "30m"
|
14
|
+
ONE_HOUR = "1h"
|
15
|
+
THREE_HOURS = "3h"
|
16
|
+
SIX_HOURS = "6h"
|
17
|
+
TWELVE_HOURS = "12h"
|
18
|
+
ONE_DAY = "24h"
|
19
|
+
THREE_DAYS = "3d"
|
20
|
+
ONE_WEEK = "7d"
|
21
|
+
ONE_MONTH = "30d"
|
22
|
+
|
23
|
+
|
24
|
+
class SortOrder(str, Enum):
|
25
|
+
"""排序方向"""
|
26
|
+
ASC = "asc"
|
27
|
+
DESC = "desc"
|
28
|
+
|
29
|
+
|
30
|
+
class FilterOperator(str, Enum):
|
31
|
+
"""筛选操作符"""
|
32
|
+
EQ = "eq" # 等于
|
33
|
+
NE = "ne" # 不等于
|
34
|
+
GT = "gt" # 大于
|
35
|
+
GTE = "gte" # 大于等于
|
36
|
+
LT = "lt" # 小于
|
37
|
+
LTE = "lte" # 小于等于
|
38
|
+
IN = "in" # 包含
|
39
|
+
NOT_IN = "not_in" # 不包含
|
40
|
+
LIKE = "like" # 模糊匹配
|
41
|
+
REGEX = "regex" # 正则表达式
|
42
|
+
IS_NULL = "is_null" # 为空
|
43
|
+
IS_NOT_NULL = "is_not_null" # 不为空
|
44
|
+
|
45
|
+
|
46
|
+
class FilterCondition(BaseModel):
|
47
|
+
"""筛选条件"""
|
48
|
+
field: str = Field(description="字段名")
|
49
|
+
operator: FilterOperator = Field(description="操作符")
|
50
|
+
value: Optional[Any] = Field(default=None, description="筛选值")
|
51
|
+
|
52
|
+
@validator('value')
|
53
|
+
def validate_value(cls, v, values):
|
54
|
+
operator = values.get('operator')
|
55
|
+
if operator in [FilterOperator.IS_NULL, FilterOperator.IS_NOT_NULL]:
|
56
|
+
return None
|
57
|
+
if operator in [FilterOperator.IN, FilterOperator.NOT_IN] and not isinstance(v, list):
|
58
|
+
raise ValueError(f"Operator {operator} requires a list value")
|
59
|
+
return v
|
60
|
+
|
61
|
+
|
62
|
+
class BaseListRequest(BaseModel):
|
63
|
+
"""基础列表查询请求"""
|
64
|
+
page: int = Field(default=1, ge=1, description="页码")
|
65
|
+
page_size: int = Field(default=20, ge=1, le=100, description="每页大小")
|
66
|
+
sort_field: Optional[str] = Field(default=None, description="排序字段")
|
67
|
+
sort_order: SortOrder = Field(default=SortOrder.DESC, description="排序方向")
|
68
|
+
filters: List[FilterCondition] = Field(default_factory=list, description="筛选条件")
|
69
|
+
search: Optional[str] = Field(default=None, description="搜索关键词")
|
70
|
+
|
71
|
+
|
72
|
+
class TimeRangeRequest(BaseModel):
|
73
|
+
"""时间范围查询请求"""
|
74
|
+
start_time: Optional[datetime] = Field(default=None, description="开始时间")
|
75
|
+
end_time: Optional[datetime] = Field(default=None, description="结束时间")
|
76
|
+
time_range: Optional[TimeRange] = Field(default=None, description="预设时间范围")
|
77
|
+
granularity: Optional[str] = Field(default=None, description="数据粒度")
|
78
|
+
|
79
|
+
@validator('end_time')
|
80
|
+
def validate_time_range(cls, v, values):
|
81
|
+
start_time = values.get('start_time')
|
82
|
+
if start_time and v and start_time >= v:
|
83
|
+
raise ValueError("end_time must be after start_time")
|
84
|
+
return v
|
85
|
+
|
86
|
+
|
87
|
+
# 队列相关请求模型
|
88
|
+
class QueueListRequest(BaseListRequest):
|
89
|
+
"""队列列表查询请求"""
|
90
|
+
namespace: str = Field(default="default", description="命名空间")
|
91
|
+
status: Optional[str] = Field(default=None, description="队列状态筛选")
|
92
|
+
include_stats: bool = Field(default=True, description="是否包含统计信息")
|
93
|
+
|
94
|
+
|
95
|
+
class QueueMetricsRequest(TimeRangeRequest):
|
96
|
+
"""队列指标查询请求"""
|
97
|
+
namespace: str = Field(default="default", description="命名空间")
|
98
|
+
queue_name: str = Field(description="队列名称")
|
99
|
+
metrics: List[str] = Field(default_factory=lambda: ["pending", "processing", "completed"], description="指标类型")
|
100
|
+
include_consumer_groups: bool = Field(default=False, description="是否包含消费者组数据")
|
101
|
+
|
102
|
+
|
103
|
+
class QueueActionRequest(BaseModel):
|
104
|
+
"""队列操作请求"""
|
105
|
+
action: str = Field(description="操作类型")
|
106
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="操作参数")
|
107
|
+
|
108
|
+
|
109
|
+
# 任务相关请求模型
|
110
|
+
class TaskListRequest(BaseListRequest, TimeRangeRequest):
|
111
|
+
"""任务列表查询请求"""
|
112
|
+
namespace: str = Field(default="default", description="命名空间")
|
113
|
+
queue_name: Optional[str] = Field(default=None, description="队列名称")
|
114
|
+
status: Optional[str] = Field(default=None, description="任务状态")
|
115
|
+
consumer_group: Optional[str] = Field(default=None, description="消费者组")
|
116
|
+
worker_id: Optional[str] = Field(default=None, description="工作者ID")
|
117
|
+
task_name: Optional[str] = Field(default=None, description="任务名称")
|
118
|
+
|
119
|
+
|
120
|
+
class TaskActionRequest(BaseModel):
|
121
|
+
"""任务操作请求"""
|
122
|
+
task_ids: List[str] = Field(description="任务ID列表")
|
123
|
+
action: str = Field(description="操作类型")
|
124
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="操作参数")
|
125
|
+
|
126
|
+
|
127
|
+
# 监控相关请求模型
|
128
|
+
class MonitoringRequest(TimeRangeRequest):
|
129
|
+
"""监控数据请求"""
|
130
|
+
namespace: str = Field(default="default", description="命名空间")
|
131
|
+
queues: Optional[List[str]] = Field(default=None, description="队列列表")
|
132
|
+
metrics: List[str] = Field(default_factory=list, description="指标类型")
|
133
|
+
include_groups: bool = Field(default=False, description="是否包含消费者组")
|
134
|
+
|
135
|
+
|
136
|
+
class BacklogTrendRequest(TimeRangeRequest):
|
137
|
+
"""队列积压趋势请求"""
|
138
|
+
namespace: str = Field(default="default", description="命名空间")
|
139
|
+
queues: Optional[List[str]] = Field(default=None, description="队列列表")
|
140
|
+
include_groups: bool = Field(default=False, description="是否包含消费者组级别数据")
|
141
|
+
|
142
|
+
|
143
|
+
# 分析相关请求模型
|
144
|
+
class AnalyticsRequest(TimeRangeRequest):
|
145
|
+
"""分析查询请求"""
|
146
|
+
namespace: str = Field(default="default", description="命名空间")
|
147
|
+
analysis_type: str = Field(description="分析类型")
|
148
|
+
dimensions: List[str] = Field(default_factory=list, description="分析维度")
|
149
|
+
metrics: List[str] = Field(default_factory=list, description="分析指标")
|
150
|
+
filters: List[FilterCondition] = Field(default_factory=list, description="筛选条件")
|
151
|
+
|
152
|
+
|
153
|
+
# 定时任务相关请求模型
|
154
|
+
class ScheduleConfig(BaseModel):
|
155
|
+
"""调度配置"""
|
156
|
+
cron_expression: Optional[str] = Field(default=None, description="Cron表达式")
|
157
|
+
interval_seconds: Optional[int] = Field(default=None, description="间隔秒数")
|
158
|
+
interval_minutes: Optional[int] = Field(default=None, description="间隔分钟数")
|
159
|
+
interval_hours: Optional[int] = Field(default=None, description="间隔小时数")
|
160
|
+
start_date: Optional[datetime] = Field(default=None, description="开始时间")
|
161
|
+
end_date: Optional[datetime] = Field(default=None, description="结束时间")
|
162
|
+
max_runs: Optional[int] = Field(default=None, description="最大运行次数")
|
163
|
+
|
164
|
+
@validator('interval_seconds')
|
165
|
+
def validate_interval_seconds(cls, v):
|
166
|
+
if v is not None and v < 1:
|
167
|
+
raise ValueError("interval_seconds must be at least 1")
|
168
|
+
return v
|
169
|
+
|
170
|
+
|
171
|
+
class ScheduledTaskCreateRequest(BaseModel):
|
172
|
+
"""创建定时任务请求"""
|
173
|
+
name: str = Field(description="任务名称")
|
174
|
+
namespace: str = Field(default="default", description="命名空间")
|
175
|
+
queue_name: str = Field(description="队列名称")
|
176
|
+
task_name: str = Field(description="任务函数名")
|
177
|
+
task_data: Dict[str, Any] = Field(default_factory=dict, description="任务数据")
|
178
|
+
schedule_type: str = Field(description="调度类型")
|
179
|
+
schedule_config: ScheduleConfig = Field(description="调度配置")
|
180
|
+
is_active: bool = Field(default=True, description="是否启用")
|
181
|
+
description: Optional[str] = Field(default=None, description="任务描述")
|
182
|
+
max_retry: int = Field(default=3, ge=0, description="最大重试次数")
|
183
|
+
timeout: Optional[int] = Field(default=None, ge=1, description="超时时间(秒)")
|
184
|
+
|
185
|
+
|
186
|
+
class ScheduledTaskUpdateRequest(ScheduledTaskCreateRequest):
|
187
|
+
"""更新定时任务请求"""
|
188
|
+
pass
|
189
|
+
|
190
|
+
|
191
|
+
class ScheduledTaskListRequest(BaseListRequest):
|
192
|
+
"""定时任务列表查询请求"""
|
193
|
+
namespace: str = Field(default="default", description="命名空间")
|
194
|
+
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
195
|
+
schedule_type: Optional[str] = Field(default=None, description="调度类型")
|
196
|
+
|
197
|
+
|
198
|
+
# 命名空间相关请求模型
|
199
|
+
class NamespaceCreateRequest(BaseModel):
|
200
|
+
"""创建命名空间请求"""
|
201
|
+
name: str = Field(description="命名空间名称", pattern="^[a-zA-Z0-9_-]+$")
|
202
|
+
display_name: str = Field(description="显示名称")
|
203
|
+
description: Optional[str] = Field(default=None, description="描述")
|
204
|
+
redis_url: str = Field(description="Redis连接URL")
|
205
|
+
pg_url: Optional[str] = Field(default=None, description="PostgreSQL连接URL")
|
206
|
+
redis_prefix: str = Field(default="jettask", description="Redis键前缀")
|
207
|
+
|
208
|
+
|
209
|
+
class NamespaceUpdateRequest(BaseModel):
|
210
|
+
"""更新命名空间请求"""
|
211
|
+
display_name: Optional[str] = Field(default=None, description="显示名称")
|
212
|
+
description: Optional[str] = Field(default=None, description="描述")
|
213
|
+
redis_url: Optional[str] = Field(default=None, description="Redis连接URL")
|
214
|
+
pg_url: Optional[str] = Field(default=None, description="PostgreSQL连接URL")
|
215
|
+
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
216
|
+
|
217
|
+
|
218
|
+
class NamespaceListRequest(BaseListRequest):
|
219
|
+
"""命名空间列表查询请求"""
|
220
|
+
is_active: Optional[bool] = Field(default=None, description="是否启用")
|
221
|
+
|
222
|
+
|
223
|
+
# 系统管理相关请求模型
|
224
|
+
class SystemConfigUpdateRequest(BaseModel):
|
225
|
+
"""系统配置更新请求"""
|
226
|
+
config_key: str = Field(description="配置键")
|
227
|
+
config_value: Any = Field(description="配置值")
|
228
|
+
description: Optional[str] = Field(default=None, description="配置描述")
|
229
|
+
|
230
|
+
|
231
|
+
class BatchOperationRequest(BaseModel):
|
232
|
+
"""批量操作请求"""
|
233
|
+
operation: str = Field(description="操作类型")
|
234
|
+
target_type: str = Field(description="目标类型")
|
235
|
+
target_ids: List[str] = Field(description="目标ID列表")
|
236
|
+
parameters: Dict[str, Any] = Field(default_factory=dict, description="操作参数")
|