jettask 0.2.1__py3-none-any.whl → 0.2.4__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.
Files changed (89) hide show
  1. jettask/constants.py +213 -0
  2. jettask/core/app.py +525 -205
  3. jettask/core/cli.py +193 -185
  4. jettask/core/consumer_manager.py +126 -34
  5. jettask/core/context.py +3 -0
  6. jettask/core/enums.py +137 -0
  7. jettask/core/event_pool.py +501 -168
  8. jettask/core/message.py +147 -0
  9. jettask/core/offline_worker_recovery.py +181 -114
  10. jettask/core/task.py +10 -174
  11. jettask/core/task_batch.py +153 -0
  12. jettask/core/unified_manager_base.py +243 -0
  13. jettask/core/worker_scanner.py +54 -54
  14. jettask/executors/asyncio.py +184 -64
  15. jettask/webui/backend/config.py +51 -0
  16. jettask/webui/backend/data_access.py +2083 -92
  17. jettask/webui/backend/data_api.py +3294 -0
  18. jettask/webui/backend/dependencies.py +261 -0
  19. jettask/webui/backend/init_meta_db.py +158 -0
  20. jettask/webui/backend/main.py +1358 -69
  21. jettask/webui/backend/main_unified.py +78 -0
  22. jettask/webui/backend/main_v2.py +394 -0
  23. jettask/webui/backend/namespace_api.py +295 -0
  24. jettask/webui/backend/namespace_api_old.py +294 -0
  25. jettask/webui/backend/namespace_data_access.py +611 -0
  26. jettask/webui/backend/queue_backlog_api.py +727 -0
  27. jettask/webui/backend/queue_stats_v2.py +521 -0
  28. jettask/webui/backend/redis_monitor_api.py +476 -0
  29. jettask/webui/backend/unified_api_router.py +1601 -0
  30. jettask/webui/db_init.py +204 -32
  31. jettask/webui/frontend/package-lock.json +492 -1
  32. jettask/webui/frontend/package.json +4 -1
  33. jettask/webui/frontend/src/App.css +105 -7
  34. jettask/webui/frontend/src/App.jsx +49 -20
  35. jettask/webui/frontend/src/components/NamespaceSelector.jsx +166 -0
  36. jettask/webui/frontend/src/components/QueueBacklogChart.jsx +298 -0
  37. jettask/webui/frontend/src/components/QueueBacklogTrend.jsx +638 -0
  38. jettask/webui/frontend/src/components/QueueDetailsTable.css +65 -0
  39. jettask/webui/frontend/src/components/QueueDetailsTable.jsx +487 -0
  40. jettask/webui/frontend/src/components/QueueDetailsTableV2.jsx +465 -0
  41. jettask/webui/frontend/src/components/ScheduledTaskFilter.jsx +423 -0
  42. jettask/webui/frontend/src/components/TaskFilter.jsx +425 -0
  43. jettask/webui/frontend/src/components/TimeRangeSelector.css +21 -0
  44. jettask/webui/frontend/src/components/TimeRangeSelector.jsx +160 -0
  45. jettask/webui/frontend/src/components/layout/AppLayout.css +95 -0
  46. jettask/webui/frontend/src/components/layout/AppLayout.jsx +49 -0
  47. jettask/webui/frontend/src/components/layout/Header.css +34 -10
  48. jettask/webui/frontend/src/components/layout/Header.jsx +31 -23
  49. jettask/webui/frontend/src/components/layout/SideMenu.css +137 -0
  50. jettask/webui/frontend/src/components/layout/SideMenu.jsx +209 -0
  51. jettask/webui/frontend/src/components/layout/TabsNav.css +244 -0
  52. jettask/webui/frontend/src/components/layout/TabsNav.jsx +206 -0
  53. jettask/webui/frontend/src/components/layout/UserInfo.css +197 -0
  54. jettask/webui/frontend/src/components/layout/UserInfo.jsx +197 -0
  55. jettask/webui/frontend/src/contexts/NamespaceContext.jsx +72 -0
  56. jettask/webui/frontend/src/contexts/TabsContext.backup.jsx +245 -0
  57. jettask/webui/frontend/src/main.jsx +1 -0
  58. jettask/webui/frontend/src/pages/Alerts.jsx +684 -0
  59. jettask/webui/frontend/src/pages/Dashboard.jsx +1330 -0
  60. jettask/webui/frontend/src/pages/QueueDetail.jsx +1109 -10
  61. jettask/webui/frontend/src/pages/QueueMonitor.jsx +236 -115
  62. jettask/webui/frontend/src/pages/Queues.jsx +5 -1
  63. jettask/webui/frontend/src/pages/ScheduledTasks.jsx +809 -0
  64. jettask/webui/frontend/src/pages/Settings.jsx +800 -0
  65. jettask/webui/frontend/src/services/api.js +7 -5
  66. jettask/webui/frontend/src/utils/suppressWarnings.js +22 -0
  67. jettask/webui/frontend/src/utils/userPreferences.js +154 -0
  68. jettask/webui/multi_namespace_consumer.py +543 -0
  69. jettask/webui/pg_consumer.py +983 -246
  70. jettask/webui/static/dist/assets/index-7129cfe1.css +1 -0
  71. jettask/webui/static/dist/assets/index-8d1935cc.js +774 -0
  72. jettask/webui/static/dist/index.html +2 -2
  73. jettask/webui/task_center.py +216 -0
  74. jettask/webui/task_center_client.py +150 -0
  75. jettask/webui/unified_consumer_manager.py +193 -0
  76. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/METADATA +1 -1
  77. jettask-0.2.4.dist-info/RECORD +134 -0
  78. jettask/webui/pg_consumer_slow.py +0 -1099
  79. jettask/webui/pg_consumer_test.py +0 -678
  80. jettask/webui/static/dist/assets/index-823408e8.css +0 -1
  81. jettask/webui/static/dist/assets/index-9968b0b8.js +0 -543
  82. jettask/webui/test_pg_consumer_recovery.py +0 -547
  83. jettask/webui/test_recovery_simple.py +0 -492
  84. jettask/webui/test_self_recovery.py +0 -467
  85. jettask-0.2.1.dist-info/RECORD +0 -91
  86. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/WHEEL +0 -0
  87. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/entry_points.txt +0 -0
  88. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/licenses/LICENSE +0 -0
  89. {jettask-0.2.1.dist-info → jettask-0.2.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,295 @@
1
+ """
2
+ 命名空间管理API - 重构版本
3
+ 使用数据库持久化命名空间数据,使用名称作为路径参数
4
+ """
5
+ from fastapi import APIRouter, HTTPException, Query
6
+ from pydantic import BaseModel
7
+ from typing import Dict, Any, Optional, List
8
+ from datetime import datetime
9
+ import json
10
+ from sqlalchemy import text
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ router = APIRouter(prefix="/api/namespaces", tags=["namespaces"])
16
+
17
+ # 使用任务中心专用的元数据库连接
18
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
19
+ from sqlalchemy.orm import sessionmaker
20
+ from jettask.webui.backend.config import task_center_config
21
+ import traceback
22
+
23
+ # 创建异步引擎 - 连接到任务中心元数据库
24
+ # 注意:这是任务中心自己的数据库,不是JetTask应用的数据库
25
+ engine = create_async_engine(task_center_config.meta_database_url, echo=False)
26
+ AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
27
+
28
+
29
+ class NamespaceCreate(BaseModel):
30
+ """创建命名空间请求"""
31
+ name: str
32
+ description: Optional[str] = None
33
+ redis_config: Dict[str, Any]
34
+ pg_config: Dict[str, Any]
35
+
36
+
37
+ class NamespaceUpdate(BaseModel):
38
+ """更新命名空间请求"""
39
+ description: Optional[str] = None
40
+ redis_config: Optional[Dict[str, Any]] = None
41
+ pg_config: Optional[Dict[str, Any]] = None
42
+ is_active: Optional[bool] = None
43
+
44
+
45
+ class NamespaceResponse(BaseModel):
46
+ """命名空间响应"""
47
+ id: int # 自增整数ID
48
+ name: str
49
+ description: Optional[str]
50
+ redis_config: Dict[str, Any]
51
+ pg_config: Dict[str, Any]
52
+ is_active: bool
53
+ version: int # 版本号
54
+ created_at: datetime
55
+ updated_at: datetime
56
+ connection_url: str
57
+
58
+
59
+ @router.get("", response_model=List[NamespaceResponse])
60
+ async def list_namespaces(
61
+ page: int = Query(1, ge=1),
62
+ page_size: int = Query(20, ge=1, le=100),
63
+ is_active: Optional[bool] = None
64
+ ):
65
+ """列出所有命名空间"""
66
+ try:
67
+ async with AsyncSessionLocal() as session:
68
+ query = """
69
+ SELECT id, name, description, redis_config, pg_config,
70
+ is_active, version, created_at, updated_at
71
+ FROM namespaces
72
+ """
73
+ params = {}
74
+
75
+ if is_active is not None:
76
+ query += " WHERE is_active = :is_active"
77
+ params['is_active'] = is_active
78
+
79
+ query += " ORDER BY created_at DESC"
80
+ query += " LIMIT :limit OFFSET :offset"
81
+ params['limit'] = page_size
82
+ params['offset'] = (page - 1) * page_size
83
+
84
+ result = await session.execute(text(query), params)
85
+ rows = result.fetchall()
86
+
87
+ namespaces = []
88
+ for row in rows:
89
+ namespaces.append(NamespaceResponse(
90
+ id=row.id,
91
+ name=row.name,
92
+ description=row.description,
93
+ redis_config=row.redis_config,
94
+ pg_config=row.pg_config,
95
+ is_active=row.is_active,
96
+ version=row.version,
97
+ created_at=row.created_at,
98
+ updated_at=row.updated_at,
99
+ connection_url=f"/api/namespaces/{row.name}" # 使用名称
100
+ ))
101
+
102
+ return namespaces
103
+ except Exception as e:
104
+ logger.error(f"Failed to list namespaces: {e}")
105
+ traceback.print_exc()
106
+ raise HTTPException(status_code=500, detail=str(e))
107
+
108
+
109
+ @router.post("", response_model=NamespaceResponse, status_code=201)
110
+ async def create_namespace(namespace: NamespaceCreate):
111
+ """创建新的命名空间"""
112
+ try:
113
+ async with AsyncSessionLocal() as session:
114
+ # 检查名称是否已存在
115
+ check_query = text("SELECT COUNT(*) FROM namespaces WHERE name = :name")
116
+ result = await session.execute(check_query, {'name': namespace.name})
117
+ if result.scalar() > 0:
118
+ raise HTTPException(status_code=400, detail=f"命名空间 '{namespace.name}' 已存在")
119
+
120
+ # 创建命名空间(使用自增ID)
121
+ insert_query = text("""
122
+ INSERT INTO namespaces (name, description, redis_config, pg_config, version)
123
+ VALUES (:name, :description, :redis_config, :pg_config, 1)
124
+ RETURNING id, name, description, redis_config, pg_config,
125
+ is_active, version, created_at, updated_at
126
+ """)
127
+
128
+ result = await session.execute(insert_query, {
129
+ 'name': namespace.name,
130
+ 'description': namespace.description,
131
+ 'redis_config': json.dumps(namespace.redis_config),
132
+ 'pg_config': json.dumps(namespace.pg_config)
133
+ })
134
+
135
+ row = result.fetchone()
136
+ await session.commit()
137
+
138
+ return NamespaceResponse(
139
+ id=row.id,
140
+ name=row.name,
141
+ description=row.description,
142
+ redis_config=row.redis_config,
143
+ pg_config=row.pg_config,
144
+ is_active=row.is_active,
145
+ version=row.version,
146
+ created_at=row.created_at,
147
+ updated_at=row.updated_at,
148
+ connection_url=f"/api/namespaces/{row.name}" # 使用名称
149
+ )
150
+ except HTTPException:
151
+ raise
152
+ except Exception as e:
153
+ logger.error(f"Failed to create namespace: {e}")
154
+ traceback.print_exc()
155
+ raise HTTPException(status_code=500, detail=str(e))
156
+
157
+
158
+ @router.get("/{namespace_name}", response_model=NamespaceResponse)
159
+ async def get_namespace(namespace_name: str):
160
+ """获取指定命名空间的详细信息"""
161
+ try:
162
+ async with AsyncSessionLocal() as session:
163
+ query = text("""
164
+ SELECT id, name, description, redis_config, pg_config,
165
+ is_active, version, created_at, updated_at
166
+ FROM namespaces
167
+ WHERE name = :name
168
+ """)
169
+
170
+ result = await session.execute(query, {'name': namespace_name})
171
+ row = result.fetchone()
172
+
173
+ if not row:
174
+ raise HTTPException(status_code=404, detail="命名空间不存在")
175
+
176
+ return NamespaceResponse(
177
+ id=row.id,
178
+ name=row.name,
179
+ description=row.description,
180
+ redis_config=row.redis_config,
181
+ pg_config=row.pg_config,
182
+ is_active=row.is_active,
183
+ version=row.version,
184
+ created_at=row.created_at,
185
+ updated_at=row.updated_at,
186
+ connection_url=f"/api/namespaces/{row.name}" # 使用名称
187
+ )
188
+ except HTTPException:
189
+ raise
190
+ except Exception as e:
191
+ logger.error(f"Failed to get namespace: {e}")
192
+ traceback.print_exc()
193
+ raise HTTPException(status_code=500, detail=str(e))
194
+
195
+
196
+ @router.put("/{namespace_name}", response_model=NamespaceResponse)
197
+ async def update_namespace(namespace_name: str, namespace: NamespaceUpdate):
198
+ """更新命名空间"""
199
+ try:
200
+ async with AsyncSessionLocal() as session:
201
+ # 检查是否存在
202
+ check_query = text("SELECT id, name FROM namespaces WHERE name = :name")
203
+ result = await session.execute(check_query, {'name': namespace_name})
204
+ row = result.fetchone()
205
+
206
+ if not row:
207
+ raise HTTPException(status_code=404, detail="命名空间不存在")
208
+
209
+ # 构建更新语句
210
+ updates = []
211
+ params = {'name': namespace_name}
212
+
213
+ if namespace.description is not None:
214
+ updates.append("description = :description")
215
+ params['description'] = namespace.description
216
+
217
+ if namespace.redis_config is not None:
218
+ updates.append("redis_config = :redis_config")
219
+ params['redis_config'] = json.dumps(namespace.redis_config)
220
+
221
+ if namespace.pg_config is not None:
222
+ updates.append("pg_config = :pg_config")
223
+ params['pg_config'] = json.dumps(namespace.pg_config)
224
+
225
+ if namespace.is_active is not None:
226
+ updates.append("is_active = :is_active")
227
+ params['is_active'] = namespace.is_active
228
+
229
+ if not updates:
230
+ raise HTTPException(status_code=400, detail="没有要更新的字段")
231
+
232
+ # 如果更新了redis_config或pg_config,递增版本号
233
+ if 'redis_config' in params or 'pg_config' in params:
234
+ updates.append("version = version + 1")
235
+
236
+ update_query = text(f"""
237
+ UPDATE namespaces
238
+ SET {', '.join(updates)}
239
+ WHERE name = :name
240
+ RETURNING id, name, description, redis_config, pg_config,
241
+ is_active, version, created_at, updated_at
242
+ """)
243
+
244
+ result = await session.execute(update_query, params)
245
+ row = result.fetchone()
246
+ await session.commit()
247
+
248
+ return NamespaceResponse(
249
+ id=row.id,
250
+ name=row.name,
251
+ description=row.description,
252
+ redis_config=row.redis_config,
253
+ pg_config=row.pg_config,
254
+ is_active=row.is_active,
255
+ version=row.version,
256
+ created_at=row.created_at,
257
+ updated_at=row.updated_at,
258
+ connection_url=f"/api/namespaces/{row.name}" # 使用名称
259
+ )
260
+ except HTTPException:
261
+ raise
262
+ except Exception as e:
263
+ logger.error(f"Failed to update namespace: {e}")
264
+ traceback.print_exc()
265
+ raise HTTPException(status_code=500, detail=str(e))
266
+
267
+
268
+ @router.delete("/{namespace_name}")
269
+ async def delete_namespace(namespace_name: str):
270
+ """删除命名空间"""
271
+ try:
272
+ async with AsyncSessionLocal() as session:
273
+ # 检查是否为默认命名空间
274
+ if namespace_name == 'default':
275
+ raise HTTPException(status_code=400, detail="不能删除默认命名空间")
276
+
277
+ check_query = text("SELECT name FROM namespaces WHERE name = :name")
278
+ result = await session.execute(check_query, {'name': namespace_name})
279
+ row = result.fetchone()
280
+
281
+ if not row:
282
+ raise HTTPException(status_code=404, detail="命名空间不存在")
283
+
284
+ # 删除命名空间
285
+ delete_query = text("DELETE FROM namespaces WHERE name = :name")
286
+ await session.execute(delete_query, {'name': namespace_name})
287
+ await session.commit()
288
+
289
+ return {"message": "命名空间已删除"}
290
+ except HTTPException:
291
+ raise
292
+ except Exception as e:
293
+ logger.error(f"Failed to delete namespace: {e}")
294
+ traceback.print_exc()
295
+ raise HTTPException(status_code=500, detail=str(e))
@@ -0,0 +1,294 @@
1
+ """
2
+ 命名空间管理API - 重构版本
3
+ 使用数据库持久化命名空间数据
4
+ """
5
+ from fastapi import APIRouter, HTTPException, Query
6
+ from pydantic import BaseModel
7
+ from typing import Dict, Any, Optional, List
8
+ from datetime import datetime
9
+ import json
10
+ from sqlalchemy import text
11
+ import logging
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ router = APIRouter(prefix="/api/namespaces", tags=["namespaces"])
16
+
17
+ # 使用任务中心专用的元数据库连接
18
+ from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
19
+ from sqlalchemy.orm import sessionmaker
20
+ from jettask.webui.backend.config import task_center_config
21
+
22
+ # 创建异步引擎 - 连接到任务中心元数据库
23
+ # 注意:这是任务中心自己的数据库,不是JetTask应用的数据库
24
+ engine = create_async_engine(task_center_config.meta_database_url, echo=False)
25
+ AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
26
+
27
+
28
+ class NamespaceCreate(BaseModel):
29
+ """创建命名空间请求"""
30
+ name: str
31
+ description: Optional[str] = None
32
+ redis_config: Dict[str, Any]
33
+ pg_config: Dict[str, Any]
34
+
35
+
36
+ class NamespaceUpdate(BaseModel):
37
+ """更新命名空间请求"""
38
+ description: Optional[str] = None
39
+ redis_config: Optional[Dict[str, Any]] = None
40
+ pg_config: Optional[Dict[str, Any]] = None
41
+ is_active: Optional[bool] = None
42
+
43
+
44
+ class NamespaceResponse(BaseModel):
45
+ """命名空间响应"""
46
+ id: int # 改为整数类型
47
+ name: str
48
+ description: Optional[str]
49
+ redis_config: Dict[str, Any]
50
+ pg_config: Dict[str, Any]
51
+ is_active: bool
52
+ version: int # 添加版本号
53
+ created_at: datetime
54
+ updated_at: datetime
55
+ connection_url: str
56
+
57
+
58
+ @router.get("", response_model=List[NamespaceResponse])
59
+ async def list_namespaces(
60
+ page: int = Query(1, ge=1),
61
+ page_size: int = Query(20, ge=1, le=100),
62
+ is_active: Optional[bool] = None
63
+ ):
64
+ """列出所有命名空间"""
65
+ try:
66
+ async with AsyncSessionLocal() as session:
67
+ query = """
68
+ SELECT id, name, description, redis_config, pg_config,
69
+ is_active, version, created_at, updated_at
70
+ FROM namespaces
71
+ """
72
+ params = {}
73
+
74
+ if is_active is not None:
75
+ query += " WHERE is_active = :is_active"
76
+ params['is_active'] = is_active
77
+
78
+ query += " ORDER BY created_at DESC"
79
+ query += " LIMIT :limit OFFSET :offset"
80
+ params['limit'] = page_size
81
+ params['offset'] = (page - 1) * page_size
82
+
83
+ result = await session.execute(text(query), params)
84
+ rows = result.fetchall()
85
+
86
+ namespaces = []
87
+ for row in rows:
88
+ namespaces.append(NamespaceResponse(
89
+ id=row.id,
90
+ name=row.name,
91
+ description=row.description,
92
+ redis_config=row.redis_config,
93
+ pg_config=row.pg_config,
94
+ is_active=row.is_active,
95
+ version=row.version,
96
+ created_at=row.created_at,
97
+ updated_at=row.updated_at,
98
+ connection_url=f"/api/namespaces/{row.name}" # 使用名称
99
+ ))
100
+
101
+ return namespaces
102
+ except Exception as e:
103
+ logger.error(f"Failed to list namespaces: {e}")
104
+ raise HTTPException(status_code=500, detail=str(e))
105
+
106
+
107
+ @router.post("", response_model=NamespaceResponse, status_code=201)
108
+ async def create_namespace(namespace: NamespaceCreate):
109
+ """创建新的命名空间"""
110
+ try:
111
+ async with AsyncSessionLocal() as session:
112
+ # 检查名称是否已存在
113
+ check_query = text("SELECT COUNT(*) FROM namespaces WHERE name = :name")
114
+ result = await session.execute(check_query, {'name': namespace.name})
115
+ if result.scalar() > 0:
116
+ raise HTTPException(status_code=400, detail=f"命名空间 '{namespace.name}' 已存在")
117
+
118
+ # 创建命名空间(使用自增ID)
119
+ insert_query = text("""
120
+ INSERT INTO namespaces (name, description, redis_config, pg_config, version)
121
+ VALUES (:name, :description, :redis_config, :pg_config, 1)
122
+ RETURNING id, name, description, redis_config, pg_config,
123
+ is_active, version, created_at, updated_at
124
+ """)
125
+
126
+ result = await session.execute(insert_query, {
127
+ 'name': namespace.name,
128
+ 'description': namespace.description,
129
+ 'redis_config': json.dumps(namespace.redis_config),
130
+ 'pg_config': json.dumps(namespace.pg_config)
131
+ })
132
+
133
+ row = result.fetchone()
134
+ await session.commit()
135
+
136
+ return NamespaceResponse(
137
+ id=str(row.id),
138
+ name=row.name,
139
+ description=row.description,
140
+ redis_config=row.redis_config,
141
+ pg_config=row.pg_config,
142
+ is_active=row.is_active,
143
+ version=row.version,
144
+ created_at=row.created_at,
145
+ updated_at=row.updated_at,
146
+ connection_url=f"/api/namespaces/{str(row.id)}"
147
+ )
148
+ except HTTPException:
149
+ raise
150
+ except Exception as e:
151
+ logger.error(f"Failed to create namespace: {e}")
152
+ raise HTTPException(status_code=500, detail=str(e))
153
+
154
+
155
+ @router.get("/{namespace_id}", response_model=NamespaceResponse)
156
+ async def get_namespace(namespace_id: str):
157
+ """获取指定命名空间的详细信息"""
158
+ try:
159
+ async with AsyncSessionLocal() as session:
160
+ query = text("""
161
+ SELECT id, name, description, redis_config, pg_config,
162
+ is_active, version, created_at, updated_at
163
+ FROM namespaces
164
+ WHERE id = :id
165
+ """)
166
+
167
+ result = await session.execute(query, {'id': namespace_id})
168
+ row = result.fetchone()
169
+
170
+ if not row:
171
+ raise HTTPException(status_code=404, detail="命名空间不存在")
172
+
173
+ return NamespaceResponse(
174
+ id=str(row.id),
175
+ name=row.name,
176
+ description=row.description,
177
+ redis_config=row.redis_config,
178
+ pg_config=row.pg_config,
179
+ is_active=row.is_active,
180
+ version=row.version,
181
+ created_at=row.created_at,
182
+ updated_at=row.updated_at,
183
+ connection_url=f"/api/namespaces/{str(row.id)}"
184
+ )
185
+ except HTTPException:
186
+ raise
187
+ except Exception as e:
188
+ logger.error(f"Failed to get namespace: {e}")
189
+ raise HTTPException(status_code=500, detail=str(e))
190
+
191
+
192
+ @router.put("/{namespace_id}", response_model=NamespaceResponse)
193
+ async def update_namespace(namespace_id: str, namespace: NamespaceUpdate):
194
+ """更新命名空间"""
195
+ try:
196
+ async with AsyncSessionLocal() as session:
197
+ # 检查是否存在并获取名称
198
+ check_query = text("SELECT name FROM namespaces WHERE id = :id")
199
+ result = await session.execute(check_query, {'id': namespace_id})
200
+ row = result.fetchone()
201
+
202
+ if not row:
203
+ raise HTTPException(status_code=404, detail="命名空间不存在")
204
+
205
+ # 检查是否是默认命名空间
206
+ if row.name == 'default':
207
+ raise HTTPException(status_code=400, detail="默认命名空间不能编辑")
208
+
209
+ # 构建更新语句
210
+ updates = []
211
+ params = {'id': namespace_id}
212
+
213
+ if namespace.description is not None:
214
+ updates.append("description = :description")
215
+ params['description'] = namespace.description
216
+
217
+ if namespace.redis_config is not None:
218
+ updates.append("redis_config = :redis_config")
219
+ params['redis_config'] = json.dumps(namespace.redis_config)
220
+
221
+ if namespace.pg_config is not None:
222
+ updates.append("pg_config = :pg_config")
223
+ params['pg_config'] = json.dumps(namespace.pg_config)
224
+
225
+ if namespace.is_active is not None:
226
+ updates.append("is_active = :is_active")
227
+ params['is_active'] = namespace.is_active
228
+
229
+ if not updates:
230
+ raise HTTPException(status_code=400, detail="没有要更新的字段")
231
+
232
+ # 如果更新了redis_config或pg_config,递增版本号
233
+ if 'redis_config' in params or 'pg_config' in params:
234
+ updates.append("version = version + 1")
235
+
236
+ update_query = text(f"""
237
+ UPDATE namespaces
238
+ SET {', '.join(updates)}
239
+ WHERE id = :id
240
+ RETURNING id, name, description, redis_config, pg_config,
241
+ is_active, version, created_at, updated_at
242
+ """)
243
+
244
+ result = await session.execute(update_query, params)
245
+ row = result.fetchone()
246
+ await session.commit()
247
+
248
+ return NamespaceResponse(
249
+ id=str(row.id),
250
+ name=row.name,
251
+ description=row.description,
252
+ redis_config=row.redis_config,
253
+ pg_config=row.pg_config,
254
+ is_active=row.is_active,
255
+ version=row.version,
256
+ created_at=row.created_at,
257
+ updated_at=row.updated_at,
258
+ connection_url=f"/api/namespaces/{str(row.id)}"
259
+ )
260
+ except HTTPException:
261
+ raise
262
+ except Exception as e:
263
+ logger.error(f"Failed to update namespace: {e}")
264
+ raise HTTPException(status_code=500, detail=str(e))
265
+
266
+
267
+ @router.delete("/{namespace_id}")
268
+ async def delete_namespace(namespace_id: str):
269
+ """删除命名空间"""
270
+ try:
271
+ async with AsyncSessionLocal() as session:
272
+ # 检查是否为默认命名空间
273
+ check_query = text("SELECT name FROM namespaces WHERE id = :id")
274
+ result = await session.execute(check_query, {'id': namespace_id})
275
+ row = result.fetchone()
276
+
277
+ if not row:
278
+ raise HTTPException(status_code=404, detail="命名空间不存在")
279
+
280
+ if row.name == 'default':
281
+ raise HTTPException(status_code=400, detail="不能删除默认命名空间")
282
+
283
+ # 删除命名空间
284
+ delete_query = text("DELETE FROM namespaces WHERE id = :id")
285
+ await session.execute(delete_query, {'id': namespace_id})
286
+ await session.commit()
287
+
288
+ return {"message": "命名空间已删除"}
289
+ except HTTPException:
290
+ raise
291
+ except Exception as e:
292
+ logger.error(f"Failed to delete namespace: {e}")
293
+ raise HTTPException(status_code=500, detail=str(e))
294
+