jettask 0.2.19__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 +10 -3
- 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.19.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.19.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.19.dist-info → jettask-0.2.20.dist-info}/WHEEL +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/entry_points.txt +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/licenses/LICENSE +0 -0
- {jettask-0.2.19.dist-info → jettask-0.2.20.dist-info}/top_level.txt +0 -0
@@ -1,267 +0,0 @@
|
|
1
|
-
"""
|
2
|
-
命名空间管理API - 重构版本
|
3
|
-
使用数据库持久化命名空间数据
|
4
|
-
"""
|
5
|
-
from fastapi import APIRouter, HTTPException, Query
|
6
|
-
from typing import Dict, Any, Optional, List
|
7
|
-
from datetime import datetime
|
8
|
-
import json
|
9
|
-
from sqlalchemy import text
|
10
|
-
import logging
|
11
|
-
from jettask.schemas import NamespaceCreate, NamespaceUpdate, NamespaceResponse
|
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.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
|
-
# 模型已从 schemas 模块导入
|
29
|
-
|
30
|
-
|
31
|
-
@router.get("", response_model=List[NamespaceResponse])
|
32
|
-
async def list_namespaces(
|
33
|
-
page: int = Query(1, ge=1),
|
34
|
-
page_size: int = Query(20, ge=1, le=100),
|
35
|
-
is_active: Optional[bool] = None
|
36
|
-
):
|
37
|
-
"""列出所有命名空间"""
|
38
|
-
try:
|
39
|
-
async with AsyncSessionLocal() as session:
|
40
|
-
query = """
|
41
|
-
SELECT id, name, description, redis_config, pg_config,
|
42
|
-
is_active, version, created_at, updated_at
|
43
|
-
FROM namespaces
|
44
|
-
"""
|
45
|
-
params = {}
|
46
|
-
|
47
|
-
if is_active is not None:
|
48
|
-
query += " WHERE is_active = :is_active"
|
49
|
-
params['is_active'] = is_active
|
50
|
-
|
51
|
-
query += " ORDER BY created_at DESC"
|
52
|
-
query += " LIMIT :limit OFFSET :offset"
|
53
|
-
params['limit'] = page_size
|
54
|
-
params['offset'] = (page - 1) * page_size
|
55
|
-
|
56
|
-
result = await session.execute(text(query), params)
|
57
|
-
rows = result.fetchall()
|
58
|
-
|
59
|
-
namespaces = []
|
60
|
-
for row in rows:
|
61
|
-
namespaces.append(NamespaceResponse(
|
62
|
-
id=row.id,
|
63
|
-
name=row.name,
|
64
|
-
description=row.description,
|
65
|
-
redis_config=row.redis_config,
|
66
|
-
pg_config=row.pg_config,
|
67
|
-
is_active=row.is_active,
|
68
|
-
version=row.version,
|
69
|
-
created_at=row.created_at,
|
70
|
-
updated_at=row.updated_at,
|
71
|
-
connection_url=f"/api/namespaces/{row.name}" # 使用名称
|
72
|
-
))
|
73
|
-
|
74
|
-
return namespaces
|
75
|
-
except Exception as e:
|
76
|
-
logger.error(f"Failed to list namespaces: {e}")
|
77
|
-
raise HTTPException(status_code=500, detail=str(e))
|
78
|
-
|
79
|
-
|
80
|
-
@router.post("", response_model=NamespaceResponse, status_code=201)
|
81
|
-
async def create_namespace(namespace: NamespaceCreate):
|
82
|
-
"""创建新的命名空间"""
|
83
|
-
try:
|
84
|
-
async with AsyncSessionLocal() as session:
|
85
|
-
# 检查名称是否已存在
|
86
|
-
check_query = text("SELECT COUNT(*) FROM namespaces WHERE name = :name")
|
87
|
-
result = await session.execute(check_query, {'name': namespace.name})
|
88
|
-
if result.scalar() > 0:
|
89
|
-
raise HTTPException(status_code=400, detail=f"命名空间 '{namespace.name}' 已存在")
|
90
|
-
|
91
|
-
# 创建命名空间(使用自增ID)
|
92
|
-
insert_query = text("""
|
93
|
-
INSERT INTO namespaces (name, description, redis_config, pg_config, version)
|
94
|
-
VALUES (:name, :description, :redis_config, :pg_config, 1)
|
95
|
-
RETURNING id, name, description, redis_config, pg_config,
|
96
|
-
is_active, version, created_at, updated_at
|
97
|
-
""")
|
98
|
-
|
99
|
-
result = await session.execute(insert_query, {
|
100
|
-
'name': namespace.name,
|
101
|
-
'description': namespace.description,
|
102
|
-
'redis_config': json.dumps(namespace.redis_config),
|
103
|
-
'pg_config': json.dumps(namespace.pg_config)
|
104
|
-
})
|
105
|
-
|
106
|
-
row = result.fetchone()
|
107
|
-
await session.commit()
|
108
|
-
|
109
|
-
return NamespaceResponse(
|
110
|
-
id=str(row.id),
|
111
|
-
name=row.name,
|
112
|
-
description=row.description,
|
113
|
-
redis_config=row.redis_config,
|
114
|
-
pg_config=row.pg_config,
|
115
|
-
is_active=row.is_active,
|
116
|
-
version=row.version,
|
117
|
-
created_at=row.created_at,
|
118
|
-
updated_at=row.updated_at,
|
119
|
-
connection_url=f"/api/namespaces/{str(row.id)}"
|
120
|
-
)
|
121
|
-
except HTTPException:
|
122
|
-
raise
|
123
|
-
except Exception as e:
|
124
|
-
logger.error(f"Failed to create namespace: {e}")
|
125
|
-
raise HTTPException(status_code=500, detail=str(e))
|
126
|
-
|
127
|
-
|
128
|
-
@router.get("/{namespace_id}", response_model=NamespaceResponse)
|
129
|
-
async def get_namespace(namespace_id: str):
|
130
|
-
"""获取指定命名空间的详细信息"""
|
131
|
-
try:
|
132
|
-
async with AsyncSessionLocal() as session:
|
133
|
-
query = text("""
|
134
|
-
SELECT id, name, description, redis_config, pg_config,
|
135
|
-
is_active, version, created_at, updated_at
|
136
|
-
FROM namespaces
|
137
|
-
WHERE id = :id
|
138
|
-
""")
|
139
|
-
|
140
|
-
result = await session.execute(query, {'id': namespace_id})
|
141
|
-
row = result.fetchone()
|
142
|
-
|
143
|
-
if not row:
|
144
|
-
raise HTTPException(status_code=404, detail="命名空间不存在")
|
145
|
-
|
146
|
-
return NamespaceResponse(
|
147
|
-
id=str(row.id),
|
148
|
-
name=row.name,
|
149
|
-
description=row.description,
|
150
|
-
redis_config=row.redis_config,
|
151
|
-
pg_config=row.pg_config,
|
152
|
-
is_active=row.is_active,
|
153
|
-
version=row.version,
|
154
|
-
created_at=row.created_at,
|
155
|
-
updated_at=row.updated_at,
|
156
|
-
connection_url=f"/api/namespaces/{str(row.id)}"
|
157
|
-
)
|
158
|
-
except HTTPException:
|
159
|
-
raise
|
160
|
-
except Exception as e:
|
161
|
-
logger.error(f"Failed to get namespace: {e}")
|
162
|
-
raise HTTPException(status_code=500, detail=str(e))
|
163
|
-
|
164
|
-
|
165
|
-
@router.put("/{namespace_id}", response_model=NamespaceResponse)
|
166
|
-
async def update_namespace(namespace_id: str, namespace: NamespaceUpdate):
|
167
|
-
"""更新命名空间"""
|
168
|
-
try:
|
169
|
-
async with AsyncSessionLocal() as session:
|
170
|
-
# 检查是否存在并获取名称
|
171
|
-
check_query = text("SELECT name FROM namespaces WHERE id = :id")
|
172
|
-
result = await session.execute(check_query, {'id': namespace_id})
|
173
|
-
row = result.fetchone()
|
174
|
-
|
175
|
-
if not row:
|
176
|
-
raise HTTPException(status_code=404, detail="命名空间不存在")
|
177
|
-
|
178
|
-
# 检查是否是默认命名空间
|
179
|
-
if row.name == 'default':
|
180
|
-
raise HTTPException(status_code=400, detail="默认命名空间不能编辑")
|
181
|
-
|
182
|
-
# 构建更新语句
|
183
|
-
updates = []
|
184
|
-
params = {'id': namespace_id}
|
185
|
-
|
186
|
-
if namespace.description is not None:
|
187
|
-
updates.append("description = :description")
|
188
|
-
params['description'] = namespace.description
|
189
|
-
|
190
|
-
if namespace.redis_config is not None:
|
191
|
-
updates.append("redis_config = :redis_config")
|
192
|
-
params['redis_config'] = json.dumps(namespace.redis_config)
|
193
|
-
|
194
|
-
if namespace.pg_config is not None:
|
195
|
-
updates.append("pg_config = :pg_config")
|
196
|
-
params['pg_config'] = json.dumps(namespace.pg_config)
|
197
|
-
|
198
|
-
if namespace.is_active is not None:
|
199
|
-
updates.append("is_active = :is_active")
|
200
|
-
params['is_active'] = namespace.is_active
|
201
|
-
|
202
|
-
if not updates:
|
203
|
-
raise HTTPException(status_code=400, detail="没有要更新的字段")
|
204
|
-
|
205
|
-
# 如果更新了redis_config或pg_config,递增版本号
|
206
|
-
if 'redis_config' in params or 'pg_config' in params:
|
207
|
-
updates.append("version = version + 1")
|
208
|
-
|
209
|
-
update_query = text(f"""
|
210
|
-
UPDATE namespaces
|
211
|
-
SET {', '.join(updates)}
|
212
|
-
WHERE id = :id
|
213
|
-
RETURNING id, name, description, redis_config, pg_config,
|
214
|
-
is_active, version, created_at, updated_at
|
215
|
-
""")
|
216
|
-
|
217
|
-
result = await session.execute(update_query, params)
|
218
|
-
row = result.fetchone()
|
219
|
-
await session.commit()
|
220
|
-
|
221
|
-
return NamespaceResponse(
|
222
|
-
id=str(row.id),
|
223
|
-
name=row.name,
|
224
|
-
description=row.description,
|
225
|
-
redis_config=row.redis_config,
|
226
|
-
pg_config=row.pg_config,
|
227
|
-
is_active=row.is_active,
|
228
|
-
version=row.version,
|
229
|
-
created_at=row.created_at,
|
230
|
-
updated_at=row.updated_at,
|
231
|
-
connection_url=f"/api/namespaces/{str(row.id)}"
|
232
|
-
)
|
233
|
-
except HTTPException:
|
234
|
-
raise
|
235
|
-
except Exception as e:
|
236
|
-
logger.error(f"Failed to update namespace: {e}")
|
237
|
-
raise HTTPException(status_code=500, detail=str(e))
|
238
|
-
|
239
|
-
|
240
|
-
@router.delete("/{namespace_id}")
|
241
|
-
async def delete_namespace(namespace_id: str):
|
242
|
-
"""删除命名空间"""
|
243
|
-
try:
|
244
|
-
async with AsyncSessionLocal() as session:
|
245
|
-
# 检查是否为默认命名空间
|
246
|
-
check_query = text("SELECT name FROM namespaces WHERE id = :id")
|
247
|
-
result = await session.execute(check_query, {'id': namespace_id})
|
248
|
-
row = result.fetchone()
|
249
|
-
|
250
|
-
if not row:
|
251
|
-
raise HTTPException(status_code=404, detail="命名空间不存在")
|
252
|
-
|
253
|
-
if row.name == 'default':
|
254
|
-
raise HTTPException(status_code=400, detail="不能删除默认命名空间")
|
255
|
-
|
256
|
-
# 删除命名空间
|
257
|
-
delete_query = text("DELETE FROM namespaces WHERE id = :id")
|
258
|
-
await session.execute(delete_query, {'id': namespace_id})
|
259
|
-
await session.commit()
|
260
|
-
|
261
|
-
return {"message": "命名空间已删除"}
|
262
|
-
except HTTPException:
|
263
|
-
raise
|
264
|
-
except Exception as e:
|
265
|
-
logger.error(f"Failed to delete namespace: {e}")
|
266
|
-
raise HTTPException(status_code=500, detail=str(e))
|
267
|
-
|
jettask/backend/start.py
DELETED
@@ -1,42 +0,0 @@
|
|
1
|
-
#!/usr/bin/env python3
|
2
|
-
"""
|
3
|
-
启动独立的JetTask Monitor后端API服务
|
4
|
-
"""
|
5
|
-
import uvicorn
|
6
|
-
import logging
|
7
|
-
import sys
|
8
|
-
import os
|
9
|
-
|
10
|
-
# 添加项目根目录到Python路径
|
11
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
12
|
-
project_root = os.path.dirname(os.path.dirname(os.path.dirname(current_dir)))
|
13
|
-
sys.path.insert(0, project_root)
|
14
|
-
|
15
|
-
# 设置日志
|
16
|
-
logging.basicConfig(
|
17
|
-
level=logging.INFO,
|
18
|
-
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
19
|
-
)
|
20
|
-
logger = logging.getLogger(__name__)
|
21
|
-
|
22
|
-
def main():
|
23
|
-
"""启动服务"""
|
24
|
-
logger.info("启动JetTask Monitor独立后端API服务...")
|
25
|
-
|
26
|
-
try:
|
27
|
-
uvicorn.run(
|
28
|
-
"main:app",
|
29
|
-
host="0.0.0.0",
|
30
|
-
port=8001,
|
31
|
-
reload=True, # 开发模式,生产环境应设为False
|
32
|
-
log_level="info",
|
33
|
-
access_log=True
|
34
|
-
)
|
35
|
-
except KeyboardInterrupt:
|
36
|
-
logger.info("服务被用户中断")
|
37
|
-
except Exception as e:
|
38
|
-
logger.error(f"服务启动失败: {e}")
|
39
|
-
sys.exit(1)
|
40
|
-
|
41
|
-
if __name__ == "__main__":
|
42
|
-
main()
|