cronapi-py 0.1.0__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.
- api/v1/__init__.py +15 -0
- api/v1/etcd.py +182 -0
- api/v1/mongo.py +108 -0
- core/__init__.py +0 -0
- core/config.py +23 -0
- core/consts.py +5 -0
- core/events.py +0 -0
- cron_api/__init__.py +0 -0
- cron_api/main.py +81 -0
- cronapi_py-0.1.0.dist-info/METADATA +37 -0
- cronapi_py-0.1.0.dist-info/RECORD +19 -0
- cronapi_py-0.1.0.dist-info/WHEEL +4 -0
- cronapi_py-0.1.0.dist-info/entry_points.txt +2 -0
- models/__init__.py +0 -0
- models/log.py +29 -0
- services/__init__.py +0 -0
- services/etcd.py +109 -0
- services/mongodb.py +48 -0
- utils/__init__.py +0 -0
api/v1/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from fastapi import APIRouter
|
|
2
|
+
|
|
3
|
+
from api.v1.etcd import router as etcd_router
|
|
4
|
+
from api.v1.mongo import router as mongo_router
|
|
5
|
+
|
|
6
|
+
# 后续其他路由模块导入到这里
|
|
7
|
+
# from api.v1.jobs import router as jobs_router
|
|
8
|
+
# from api.v1.workers import router as workers_router
|
|
9
|
+
|
|
10
|
+
api_router = APIRouter()
|
|
11
|
+
|
|
12
|
+
api_router.include_router(mongo_router, prefix="/mongo")
|
|
13
|
+
api_router.include_router(etcd_router, prefix="/etcd")
|
|
14
|
+
# api_router.include_router(jobs_router, prefix="/jobs")
|
|
15
|
+
# api_router.include_router(workers_router, prefix="/workers")
|
api/v1/etcd.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import Any, Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Body, HTTPException, Query
|
|
5
|
+
from fastapi.responses import JSONResponse
|
|
6
|
+
from google.protobuf.json_format import MessageToDict
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
from services.etcd import EtcdClient
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.get("/")
|
|
15
|
+
async def get_etcd_value(
|
|
16
|
+
key: str = Query("", min_length=1, description="etcd key"),
|
|
17
|
+
prefix: bool = Query(True, description="是否使用前缀匹配模式"),
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
通过key获取etcd中的值
|
|
21
|
+
|
|
22
|
+
参数:
|
|
23
|
+
key: etcd中的键名
|
|
24
|
+
|
|
25
|
+
返回:
|
|
26
|
+
包含键值对的JSON响应
|
|
27
|
+
"""
|
|
28
|
+
with EtcdClient() as client:
|
|
29
|
+
try:
|
|
30
|
+
if prefix:
|
|
31
|
+
results = [
|
|
32
|
+
{
|
|
33
|
+
"key": metadata.key.decode("utf-8"),
|
|
34
|
+
"value": value.decode("utf-8")
|
|
35
|
+
if isinstance(value, bytes)
|
|
36
|
+
else value,
|
|
37
|
+
"metadata": {
|
|
38
|
+
"version": metadata.version,
|
|
39
|
+
"create_revision": metadata.create_revision,
|
|
40
|
+
"mod_revision": metadata.mod_revision,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
for value, metadata in client.get_prefix(key)
|
|
44
|
+
]
|
|
45
|
+
if not results:
|
|
46
|
+
raise HTTPException(
|
|
47
|
+
status_code=404, detail=f"未找到前缀为 '{key}' 的键"
|
|
48
|
+
)
|
|
49
|
+
return JSONResponse(results)
|
|
50
|
+
else:
|
|
51
|
+
# 获取单个key的值
|
|
52
|
+
value, metadata = client.get(key)
|
|
53
|
+
if value is None or metadata is None:
|
|
54
|
+
raise HTTPException(
|
|
55
|
+
status_code=404, detail=f"Key '{key}' not found"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if isinstance(value, bytes):
|
|
59
|
+
value = value.decode("utf-8")
|
|
60
|
+
|
|
61
|
+
return JSONResponse(
|
|
62
|
+
{
|
|
63
|
+
"key": key,
|
|
64
|
+
"value": value,
|
|
65
|
+
"metadata": {
|
|
66
|
+
"version": metadata.version,
|
|
67
|
+
"create_revision": metadata.create_revision,
|
|
68
|
+
"mod_revision": metadata.mod_revision,
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
raise HTTPException(
|
|
74
|
+
status_code=500, detail=f"Error getting value from etcd: {str(e)}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# 添加请求体模型
|
|
79
|
+
class EtcdValue(BaseModel):
|
|
80
|
+
key: str = Field(..., description="etcd key")
|
|
81
|
+
value: Union[str, Dict[str, Any], List[Any]] = Field(..., description="要设置的值")
|
|
82
|
+
lease: Optional[int] = Field(None, ge=1, description="lease TTL in seconds")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.put("/")
|
|
86
|
+
async def put_etcd_value(
|
|
87
|
+
data: EtcdValue = Body(..., description="要设置的键值对"),
|
|
88
|
+
):
|
|
89
|
+
"""
|
|
90
|
+
设置etcd中的值
|
|
91
|
+
|
|
92
|
+
参数:
|
|
93
|
+
data: 包含key和value的请求体
|
|
94
|
+
|
|
95
|
+
返回:
|
|
96
|
+
设置成功的响应
|
|
97
|
+
"""
|
|
98
|
+
with EtcdClient() as client:
|
|
99
|
+
try:
|
|
100
|
+
# 使用更简洁的序列化方式
|
|
101
|
+
value_str = (
|
|
102
|
+
json.dumps(data.value)
|
|
103
|
+
if isinstance(data.value, (dict, list))
|
|
104
|
+
else str(data.value)
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
lease_id = None
|
|
108
|
+
lease = None
|
|
109
|
+
if data.lease:
|
|
110
|
+
try:
|
|
111
|
+
lease = client.lease(data.lease)
|
|
112
|
+
lease_id = lease.id
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise HTTPException(
|
|
115
|
+
status_code=400, detail=f"创建lease失败: {str(e)}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
metadata = client.put(data.key, value_str, lease=lease_id)
|
|
120
|
+
except Exception:
|
|
121
|
+
if lease_id:
|
|
122
|
+
# 如果put失败,尝试撤销lease
|
|
123
|
+
if lease is not None:
|
|
124
|
+
try:
|
|
125
|
+
lease.revoke()
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
raise
|
|
129
|
+
|
|
130
|
+
return JSONResponse(
|
|
131
|
+
{
|
|
132
|
+
"key": data.key,
|
|
133
|
+
"value": data.value,
|
|
134
|
+
"lease_id": lease_id,
|
|
135
|
+
"metadata": MessageToDict(metadata) if metadata else None,
|
|
136
|
+
"message": "设置成功",
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
except HTTPException:
|
|
140
|
+
raise
|
|
141
|
+
except Exception as e:
|
|
142
|
+
raise HTTPException(
|
|
143
|
+
status_code=500, detail=f"设置etcd值时发生错误: {str(e)}"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@router.delete("/")
|
|
148
|
+
async def delete_etcd_value(
|
|
149
|
+
key: str = Query(..., description="要删除的etcd key"),
|
|
150
|
+
prefix: bool = Query(False, description="是否使用前缀删除模式"),
|
|
151
|
+
):
|
|
152
|
+
"""
|
|
153
|
+
删除etcd中的键值
|
|
154
|
+
|
|
155
|
+
参数:
|
|
156
|
+
key: 要删除的键名
|
|
157
|
+
prefix: 是否使用前缀删除模式,如果为True则删除所有匹配前缀的键
|
|
158
|
+
|
|
159
|
+
返回:
|
|
160
|
+
删除成功的响应
|
|
161
|
+
"""
|
|
162
|
+
with EtcdClient() as client:
|
|
163
|
+
try:
|
|
164
|
+
if prefix:
|
|
165
|
+
deleted = client.delete_prefix(key)
|
|
166
|
+
return JSONResponse(
|
|
167
|
+
{
|
|
168
|
+
"message": f"已删除所有前缀为 '{key}' 的键值",
|
|
169
|
+
"deleted_count": deleted,
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
else:
|
|
173
|
+
deleted = client.delete(key)
|
|
174
|
+
if not deleted:
|
|
175
|
+
raise HTTPException(status_code=404, detail=f"未找到键 '{key}'")
|
|
176
|
+
return JSONResponse(
|
|
177
|
+
{"message": f"成功删除键 '{key}'", "deleted_count": 1}
|
|
178
|
+
)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
raise HTTPException(
|
|
181
|
+
status_code=500, detail=f"从etcd删除值时发生错误: {str(e)}"
|
|
182
|
+
)
|
api/v1/mongo.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from fastapi import APIRouter, Query
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from models.log import Log
|
|
8
|
+
from services.mongodb import MongoDB
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PaginatedLogs(BaseModel):
|
|
14
|
+
"""分页响应模型"""
|
|
15
|
+
|
|
16
|
+
total: int = Field(..., description="总记录数")
|
|
17
|
+
items: List[Log] = Field(..., description="日志列表")
|
|
18
|
+
page: int = Field(..., description="当前页码")
|
|
19
|
+
page_size: int = Field(..., description="每页大小")
|
|
20
|
+
pages: int = Field(..., description="总页数")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@router.get("/logs", response_model=PaginatedLogs)
|
|
24
|
+
async def get_logs(
|
|
25
|
+
page: int = Query(1, description="页码", ge=1),
|
|
26
|
+
page_size: int = Query(1000, description="每页记录数", ge=1, le=10000),
|
|
27
|
+
job_name: Optional[str] = Query(None, description="任务名称"),
|
|
28
|
+
command: Optional[str] = Query(None, description="命令"),
|
|
29
|
+
start_time: Optional[int] = Query(
|
|
30
|
+
None,
|
|
31
|
+
description="开始时间戳(毫秒)",
|
|
32
|
+
example=int((datetime.now() - timedelta(hours=1)).timestamp() * 1000),
|
|
33
|
+
),
|
|
34
|
+
end_time: Optional[int] = Query(
|
|
35
|
+
None,
|
|
36
|
+
description="结束时间戳(毫秒)",
|
|
37
|
+
example=int(datetime.now().timestamp() * 1000),
|
|
38
|
+
),
|
|
39
|
+
has_error: Optional[bool] = Query(None, description="是否有错误"),
|
|
40
|
+
sort_field: str = Query(
|
|
41
|
+
"startTime",
|
|
42
|
+
description="排序字段",
|
|
43
|
+
enum=["planTime", "scheduleTime", "startTime", "endTime"],
|
|
44
|
+
),
|
|
45
|
+
sort_order: str = Query(
|
|
46
|
+
"desc",
|
|
47
|
+
description="排序方向",
|
|
48
|
+
enum=["asc", "desc"],
|
|
49
|
+
),
|
|
50
|
+
):
|
|
51
|
+
"""
|
|
52
|
+
获取任务执行日志
|
|
53
|
+
|
|
54
|
+
- 支持分页查询
|
|
55
|
+
- 支持按任务名称过滤
|
|
56
|
+
- 支持按命令过滤
|
|
57
|
+
- 支持时间范围过滤
|
|
58
|
+
- 支持筛选有错误的记录
|
|
59
|
+
- 支持自定义排序字段和方向
|
|
60
|
+
"""
|
|
61
|
+
async with MongoDB() as mongo:
|
|
62
|
+
# 构建查询条件
|
|
63
|
+
query = {}
|
|
64
|
+
|
|
65
|
+
if job_name:
|
|
66
|
+
query["jobName"] = {
|
|
67
|
+
"$regex": job_name,
|
|
68
|
+
"$options": "i",
|
|
69
|
+
} # i表示不区分大小写
|
|
70
|
+
|
|
71
|
+
if command:
|
|
72
|
+
query["command"] = {"$regex": command, "$options": "i"} # i表示不区分大小写
|
|
73
|
+
|
|
74
|
+
if start_time or end_time:
|
|
75
|
+
query["startTime"] = {}
|
|
76
|
+
if start_time:
|
|
77
|
+
query["startTime"]["$gte"] = start_time
|
|
78
|
+
if end_time:
|
|
79
|
+
query["startTime"]["$lte"] = end_time
|
|
80
|
+
|
|
81
|
+
if has_error is not None:
|
|
82
|
+
if has_error:
|
|
83
|
+
query["err"] = {"$ne": ""}
|
|
84
|
+
else:
|
|
85
|
+
query["err"] = ""
|
|
86
|
+
|
|
87
|
+
# 计算总记录数
|
|
88
|
+
total = await mongo.db["log"].count_documents(query)
|
|
89
|
+
pages = (total + page_size - 1) // page_size
|
|
90
|
+
skip = (page - 1) * page_size
|
|
91
|
+
|
|
92
|
+
cursor = mongo.db["log"].find(
|
|
93
|
+
query,
|
|
94
|
+
sort=[(sort_field, -1 if sort_order == "desc" else 1)],
|
|
95
|
+
skip=skip,
|
|
96
|
+
limit=page_size,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# 转换为列表
|
|
100
|
+
logs = await cursor.to_list(length=page_size)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
"total": total,
|
|
104
|
+
"items": logs,
|
|
105
|
+
"page": page,
|
|
106
|
+
"page_size": page_size,
|
|
107
|
+
"pages": pages,
|
|
108
|
+
}
|
core/__init__.py
ADDED
|
File without changes
|
core/config.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pydantic_settings import BaseSettings
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Settings(BaseSettings):
|
|
5
|
+
# API配置
|
|
6
|
+
API_V1_STR: str = "/api/v1"
|
|
7
|
+
PROJECT_NAME: str = "Cron API"
|
|
8
|
+
|
|
9
|
+
ETCD_HOSTS: str = ""
|
|
10
|
+
MONGO_URL: str = ""
|
|
11
|
+
MONGO_DB_NAME: str = "cron"
|
|
12
|
+
|
|
13
|
+
class Config:
|
|
14
|
+
env_prefix = "CRON_API_" # 添加环境变量前缀
|
|
15
|
+
case_sensitive = True
|
|
16
|
+
|
|
17
|
+
def reload(self):
|
|
18
|
+
new_settings = Settings()
|
|
19
|
+
for field in self.model_fields:
|
|
20
|
+
setattr(self, field, getattr(new_settings, field))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
settings = Settings()
|
core/consts.py
ADDED
core/events.py
ADDED
|
File without changes
|
cron_api/__init__.py
ADDED
|
File without changes
|
cron_api/main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
import uvicorn
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
from fastapi import FastAPI
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.responses import PlainTextResponse
|
|
9
|
+
|
|
10
|
+
from api.v1 import api_router
|
|
11
|
+
from core.config import settings
|
|
12
|
+
from services.etcd import EtcdClient
|
|
13
|
+
from services.mongodb import MongoDB
|
|
14
|
+
|
|
15
|
+
cli = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@asynccontextmanager
|
|
19
|
+
async def lifespan(app: FastAPI):
|
|
20
|
+
# 启动时执行
|
|
21
|
+
async with MongoDB() as mongo:
|
|
22
|
+
with EtcdClient() as etcd:
|
|
23
|
+
await mongo.test_connection()
|
|
24
|
+
etcd.test_connection()
|
|
25
|
+
yield
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = FastAPI(
|
|
29
|
+
title=settings.PROJECT_NAME,
|
|
30
|
+
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
|
31
|
+
lifespan=lifespan,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# 注册 v1 版本的所有路由
|
|
35
|
+
app.include_router(api_router, prefix=settings.API_V1_STR)
|
|
36
|
+
app.add_middleware(
|
|
37
|
+
CORSMiddleware,
|
|
38
|
+
allow_origins=["*"], # 允许所有源,生产环境建议设置具体的源
|
|
39
|
+
# allow_credentials=True,
|
|
40
|
+
allow_methods=["*"], # 允许所有 HTTP 方法
|
|
41
|
+
# allow_methods=["GET", "POST", "OPTIONS"],
|
|
42
|
+
allow_headers=["*"], # 允许所有 headers
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.get("/")
|
|
47
|
+
async def root():
|
|
48
|
+
return {"message": "Welcome to Cron API"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.get("/ping", response_class=PlainTextResponse)
|
|
52
|
+
async def pingpong():
|
|
53
|
+
return "pong"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@cli.command()
|
|
57
|
+
def run_app(
|
|
58
|
+
dotenv_path: str = "d:/.env",
|
|
59
|
+
host: str = "0.0.0.0",
|
|
60
|
+
port: int = 443,
|
|
61
|
+
ssl_keyfile: str | None = None,
|
|
62
|
+
ssl_certfile: str | None = None,
|
|
63
|
+
etcd_hosts: str | None = None,
|
|
64
|
+
mongo_url: str | None = None,
|
|
65
|
+
):
|
|
66
|
+
load_dotenv(dotenv_path)
|
|
67
|
+
settings.reload()
|
|
68
|
+
|
|
69
|
+
if etcd_hosts is not None:
|
|
70
|
+
settings.ETCD_HOSTS = etcd_hosts
|
|
71
|
+
if mongo_url is not None:
|
|
72
|
+
settings.MONGO_URL = mongo_url
|
|
73
|
+
|
|
74
|
+
uvicorn.run(
|
|
75
|
+
"cron_api.main:app",
|
|
76
|
+
host=host,
|
|
77
|
+
port=port,
|
|
78
|
+
reload=False,
|
|
79
|
+
ssl_keyfile=ssl_keyfile,
|
|
80
|
+
ssl_certfile=ssl_certfile,
|
|
81
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cronapi-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 定时任务api
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Requires-Dist: etcd3>=0.12.0
|
|
7
|
+
Requires-Dist: fastapi>=0.115.5
|
|
8
|
+
Requires-Dist: motor>=3.6.0
|
|
9
|
+
Requires-Dist: protobuf<=3.20.0
|
|
10
|
+
Requires-Dist: pydantic-settings>=2.6.1
|
|
11
|
+
Requires-Dist: pydantic>=2.9.2
|
|
12
|
+
Requires-Dist: typer>=0.13.1
|
|
13
|
+
Requires-Dist: uvicorn>=0.32.0
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# Cron API
|
|
17
|
+
|
|
18
|
+
自用 fastapi 项目,提供对应接口,通过命令行启动
|
|
19
|
+
|
|
20
|
+
- etcd 配置定时任务
|
|
21
|
+
- mongodb 查看任务执行日志
|
|
22
|
+
|
|
23
|
+
#### 安装
|
|
24
|
+
|
|
25
|
+
`pipx install cron-api` 或 `uv tool install cron-api`
|
|
26
|
+
|
|
27
|
+
#### 运行
|
|
28
|
+
|
|
29
|
+
`cron-api --help`
|
|
30
|
+
|
|
31
|
+
```shell
|
|
32
|
+
# CRON_API_MONGO_DB_NAME="cron"
|
|
33
|
+
# 指定环境变量文件,环境变量文件需提供以上参数
|
|
34
|
+
cron-api --dotenv-path .env
|
|
35
|
+
# 还可以通过 --host, --port 指定fastapi启动地址和端口
|
|
36
|
+
uv run cron-api --host 0.0.0.0 --port 8000 --ssl-keyfile d:/.ssh/mkcert/key.pem --ssl-certfile d:/.ssh/mkcert/cert.pem
|
|
37
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
api/v1/__init__.py,sha256=RdDWZw0DV_QzMVRH4umAgcjfFpgz6c-kQnnuYFutMjg,544
|
|
2
|
+
api/v1/etcd.py,sha256=OMfdAZ-vEYka9e6_XiTFFLeArSXXnzsDQ2aWCGfGGOE,6133
|
|
3
|
+
api/v1/mongo.py,sha256=hNGd4ZYE-rP5BxQZjlprGMUpe4FpcLk8HDFE8F7aYjQ,3371
|
|
4
|
+
core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
core/config.py,sha256=N3jsPf-_Y2OgZTDjIxeUBgNFkVBkS_P7z-Vboh1Kab0,557
|
|
6
|
+
core/consts.py,sha256=lEj7go5gihsRwXguuCAtTV_Rlh5snktp3Cl9kbPNiwY,150
|
|
7
|
+
core/events.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
cron_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
cron_api/main.py,sha256=D0YmIZWO1dT3j3EpB3udhfUm23oL-_fhExJW_zj8eDs,2048
|
|
10
|
+
models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
models/log.py,sha256=PtK3E5ZI0CXrG3N4l2sJwrO7ZkZi5OzYQ-tlA0rawFg,1134
|
|
12
|
+
services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
services/etcd.py,sha256=yK1ryUwRpqSU64UQqa7_94_TTBQx2bcm6hKXP5LaXQM,2949
|
|
14
|
+
services/mongodb.py,sha256=RoROeZkwACLxt8duO86IL9U6WKPjlsjOhZCmzsFkHyo,1553
|
|
15
|
+
utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
cronapi_py-0.1.0.dist-info/METADATA,sha256=yoX7iMmtkqbHebhkqxJRXj-L5BBUCvx3WdruY1g_9LQ,962
|
|
17
|
+
cronapi_py-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
18
|
+
cronapi_py-0.1.0.dist-info/entry_points.txt,sha256=kYbNfD8jRGdHtFAEAecuybOQkwAA8EeACPkfYdnfer0,47
|
|
19
|
+
cronapi_py-0.1.0.dist-info/RECORD,,
|
models/__init__.py
ADDED
|
File without changes
|
models/log.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Log(BaseModel):
|
|
7
|
+
jobName: str = Field(..., description="任务名称")
|
|
8
|
+
command: str = Field(..., description="执行的命令")
|
|
9
|
+
err: Optional[str] = Field(None, description="错误信息")
|
|
10
|
+
output: Optional[str] = Field(None, description="输出结果")
|
|
11
|
+
planTime: int = Field(..., description="计划执行时间")
|
|
12
|
+
scheduleTime: int = Field(..., description="实际调度时间")
|
|
13
|
+
startTime: int = Field(..., description="开始执行时间")
|
|
14
|
+
endTime: int = Field(..., description="执行结束时间")
|
|
15
|
+
|
|
16
|
+
class Config:
|
|
17
|
+
json_schema_extra = {
|
|
18
|
+
"example": {
|
|
19
|
+
"id": "job123456",
|
|
20
|
+
"jobName": "backup-task",
|
|
21
|
+
"command": "tar -czf backup.tar.gz /data",
|
|
22
|
+
"err": None,
|
|
23
|
+
"output": "backup completed successfully",
|
|
24
|
+
"planTime": 1711006245000,
|
|
25
|
+
"scheduleTime": 1711006245100,
|
|
26
|
+
"startTime": 1711006245200,
|
|
27
|
+
"endTime": 1711006248300,
|
|
28
|
+
}
|
|
29
|
+
}
|
services/__init__.py
ADDED
|
File without changes
|
services/etcd.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import etcd3
|
|
4
|
+
|
|
5
|
+
from core.config import settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require_connection(func):
|
|
9
|
+
"""确保client已初始化的装饰器"""
|
|
10
|
+
|
|
11
|
+
@functools.wraps(func)
|
|
12
|
+
def wrapper(self, *args, **kwargs):
|
|
13
|
+
if self.client is None:
|
|
14
|
+
raise RuntimeError("Etcd client not initialized. Call connect() first.")
|
|
15
|
+
return func(self, *args, **kwargs)
|
|
16
|
+
|
|
17
|
+
return wrapper
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class EtcdClient:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.client = None
|
|
23
|
+
|
|
24
|
+
def connect(self):
|
|
25
|
+
"""初始化etcd连接"""
|
|
26
|
+
if self.client is None:
|
|
27
|
+
host, port = settings.ETCD_HOSTS.split(":")
|
|
28
|
+
self.client = etcd3.client(
|
|
29
|
+
host=host,
|
|
30
|
+
port=int(port),
|
|
31
|
+
timeout=5,
|
|
32
|
+
)
|
|
33
|
+
return self.client
|
|
34
|
+
|
|
35
|
+
@require_connection
|
|
36
|
+
def test_connection(self):
|
|
37
|
+
"""测试连接是否成功"""
|
|
38
|
+
status = self.client.status()
|
|
39
|
+
print(f"✅ 成功连接到etcd服务器: {status}")
|
|
40
|
+
|
|
41
|
+
@require_connection
|
|
42
|
+
def get_client(self):
|
|
43
|
+
"""获取etcd客户端实例"""
|
|
44
|
+
return self.client
|
|
45
|
+
|
|
46
|
+
@require_connection
|
|
47
|
+
def get_prefix(self, key_prefix: str):
|
|
48
|
+
"""
|
|
49
|
+
获取指定前缀的所有键值对
|
|
50
|
+
|
|
51
|
+
返回: 包含(value, metadata)元组的列表,如果没有匹配项返回空列表
|
|
52
|
+
"""
|
|
53
|
+
results = list(self.client.get_prefix(key_prefix))
|
|
54
|
+
return results if results else []
|
|
55
|
+
|
|
56
|
+
@require_connection
|
|
57
|
+
def lease(self, ttl: int):
|
|
58
|
+
"""
|
|
59
|
+
创建一个具有指定TTL(秒)的lease
|
|
60
|
+
|
|
61
|
+
参数:
|
|
62
|
+
ttl: lease的存活时间(秒)
|
|
63
|
+
|
|
64
|
+
返回:
|
|
65
|
+
lease对象
|
|
66
|
+
"""
|
|
67
|
+
return self.client.lease(ttl)
|
|
68
|
+
|
|
69
|
+
@require_connection
|
|
70
|
+
def put(self, key: str, value: str, lease=None):
|
|
71
|
+
"""
|
|
72
|
+
设置键值对
|
|
73
|
+
|
|
74
|
+
参数:
|
|
75
|
+
key: 键
|
|
76
|
+
value: 值
|
|
77
|
+
lease: 可选的lease ID
|
|
78
|
+
"""
|
|
79
|
+
return self.client.put(key, value, lease=lease)
|
|
80
|
+
|
|
81
|
+
@require_connection
|
|
82
|
+
def get(self, key: str):
|
|
83
|
+
"""获取指定键的值"""
|
|
84
|
+
return self.client.get(key)
|
|
85
|
+
|
|
86
|
+
@require_connection
|
|
87
|
+
def delete(self, key: str) -> bool:
|
|
88
|
+
"""删除指定的键值对,成功返回True,键不存在返回False"""
|
|
89
|
+
return self.client.delete(key)
|
|
90
|
+
|
|
91
|
+
@require_connection
|
|
92
|
+
def delete_prefix(self, prefix: str) -> int:
|
|
93
|
+
"""删除所有指定前缀的键值对,返回删除的数量"""
|
|
94
|
+
return self.client.delete_prefix(prefix)
|
|
95
|
+
|
|
96
|
+
def close(self):
|
|
97
|
+
"""关闭etcd连接"""
|
|
98
|
+
if self.client:
|
|
99
|
+
self.client.close()
|
|
100
|
+
self.client = None
|
|
101
|
+
|
|
102
|
+
def __enter__(self):
|
|
103
|
+
"""支持上下文管理器"""
|
|
104
|
+
self.connect()
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
108
|
+
"""退出时自动关闭连接"""
|
|
109
|
+
self.close()
|
services/mongodb.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from motor.motor_asyncio import AsyncIOMotorClient
|
|
2
|
+
|
|
3
|
+
from core.config import settings
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MongoDB:
|
|
7
|
+
def __init__(self):
|
|
8
|
+
self.client: AsyncIOMotorClient = None
|
|
9
|
+
self.db = None
|
|
10
|
+
|
|
11
|
+
async def connect(self):
|
|
12
|
+
"""初始化MongoDB连接"""
|
|
13
|
+
if self.client is None:
|
|
14
|
+
self.client = AsyncIOMotorClient(
|
|
15
|
+
settings.MONGO_URL,
|
|
16
|
+
serverSelectionTimeoutMS=5000, # 服务器选择超时
|
|
17
|
+
connectTimeoutMS=5000, # 连接超时
|
|
18
|
+
socketTimeoutMS=5000, # socket超时
|
|
19
|
+
)
|
|
20
|
+
self.db = self.client[settings.MONGO_DB_NAME]
|
|
21
|
+
return self.client
|
|
22
|
+
|
|
23
|
+
async def test_connection(self):
|
|
24
|
+
"""测试连接是否成功"""
|
|
25
|
+
|
|
26
|
+
server_info = await self.client.server_info()
|
|
27
|
+
print(f"✅ 成功连接到MongoDB服务器: {server_info.get('version', 'unknown')}")
|
|
28
|
+
|
|
29
|
+
def get_client(self):
|
|
30
|
+
"""获取MongoDB客户端实例"""
|
|
31
|
+
if self.client is None:
|
|
32
|
+
raise RuntimeError("MongoDB client not initialized. Call connect() first.")
|
|
33
|
+
return self.client
|
|
34
|
+
|
|
35
|
+
async def close(self):
|
|
36
|
+
"""关闭MongoDB连接"""
|
|
37
|
+
if self.client:
|
|
38
|
+
self.client.close()
|
|
39
|
+
self.client = None
|
|
40
|
+
|
|
41
|
+
async def __aenter__(self):
|
|
42
|
+
"""支持异步上下文管理器"""
|
|
43
|
+
await self.connect()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
47
|
+
"""退出时自动关闭连接"""
|
|
48
|
+
await self.close()
|
utils/__init__.py
ADDED
|
File without changes
|