pymecli 0.6.0__tar.gz → 0.6.2__tar.gz
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.
- {pymecli-0.6.0 → pymecli-0.6.2}/PKG-INFO +31 -3
- pymecli-0.6.2/README.md +63 -0
- pymecli-0.6.2/cli/dnspod.py +259 -0
- pymecli-0.6.0/cli/etcd_mysql.py → pymecli-0.6.2/cli/etcd_sync.py +106 -2
- pymecli-0.6.2/cli/quark.py +258 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/pyproject.toml +4 -2
- pymecli-0.6.2/utils/notify.py +1092 -0
- pymecli-0.6.2/utils/sendNotify.py +429 -0
- pymecli-0.6.0/README.md +0 -35
- {pymecli-0.6.0 → pymecli-0.6.2}/.gitignore +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/api/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/api/v1/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/api/v1/clash.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/api/v1/redis.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/bitget.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/dst.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/example.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/fast.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/gate.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/okx.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/redis_csv.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/tele.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/cli/util.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/core/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/core/clash.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/core/config.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/core/redis_client.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/crypto/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/crypto/bitget.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/crypto/gate.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/crypto/okx.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/data/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/data/dou_dict.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/data/dou_list.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/data/main.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/data/template.yaml +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/models/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/models/douzero_model.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/models/ocr_model.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/models/response.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/__init__.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/elapsed.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/etcd.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/helper.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/logger.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/mysql.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/path.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/pd.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/pyredis.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/sleep.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/text.py +0 -0
- {pymecli-0.6.0 → pymecli-0.6.2}/utils/toml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pymecli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
4
4
|
Summary: My CLI
|
|
5
5
|
Project-URL: Homepage, https://pypi.org/project/pymecli
|
|
6
6
|
Project-URL: Repository, https://github.com/meme2046/pymecli
|
|
@@ -67,8 +67,36 @@ uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
|
|
|
67
67
|
# etcdsync
|
|
68
68
|
|
|
69
69
|
```shell
|
|
70
|
-
etcdsync etcd2mysql /
|
|
70
|
+
etcdsync etcd2mysql /test -e d:/.env/pymecli.env -t kvs
|
|
71
71
|
etcdsync etcd2mysql "" -e d:/.env/pymecli.env -t kvs # 全量
|
|
72
|
-
etcdsync mysql2etcd /
|
|
72
|
+
etcdsync mysql2etcd /test -e d:/.env/pymecli.env -t kvs
|
|
73
73
|
etcdsync mysql2etcd -e d:/.env/pymecli.env -t kvs # 全量
|
|
74
|
+
|
|
75
|
+
etcdsync etcd2json /test -e d:/.env/pymecli.env -f xxx.json
|
|
76
|
+
etcdsync json2etcd /test -e d:/.env/pymecli.env -f xxx.json
|
|
77
|
+
```
|
|
78
|
+
# quark
|
|
79
|
+
```shell
|
|
80
|
+
# 执行签到(默认会发通知)
|
|
81
|
+
uv run quark sign
|
|
82
|
+
# 签到但不发通知
|
|
83
|
+
uv run quark sign --no-notify
|
|
84
|
+
# 指定不同的 dotenv 路径
|
|
85
|
+
uv run quark sign -e /path/to/.env
|
|
86
|
+
# 只检查账号状态(不签到),方便调试
|
|
87
|
+
uv run quark check
|
|
74
88
|
```
|
|
89
|
+
|
|
90
|
+
# dnspod
|
|
91
|
+
```shell
|
|
92
|
+
# 列出域名下所有记录(先跑这个查 record_id)
|
|
93
|
+
uv run dnspod list
|
|
94
|
+
# 更新 IPv4 A 记录
|
|
95
|
+
uv run dnspod ddns
|
|
96
|
+
# 强制更新(即使 IP 没变化)
|
|
97
|
+
uv run dnspod ddns --force
|
|
98
|
+
# 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
|
|
99
|
+
uv run dnspod ddns6
|
|
100
|
+
|
|
101
|
+
uv run dnspod ddns6 --force
|
|
102
|
+
```
|
pymecli-0.6.2/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# typer example
|
|
2
|
+
|
|
3
|
+
```shell
|
|
4
|
+
uv run example hello Xiaoming
|
|
5
|
+
uv run example hello Xiaoming --from Pymecli
|
|
6
|
+
uv run example goodbye Xiaoming
|
|
7
|
+
uv run example goodbye Xiaoming -f
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
# fastapi server
|
|
11
|
+
|
|
12
|
+
```shell
|
|
13
|
+
uv run fast --port 8877 # local server
|
|
14
|
+
uv build
|
|
15
|
+
uv publish --token < TOKEN > # publish
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
# dontstarvetogether
|
|
19
|
+
|
|
20
|
+
1. 饥荒专服禁client mods使用, 更新client mods version, modoverrides.lua:
|
|
21
|
+
|
|
22
|
+
```shell
|
|
23
|
+
uv run dst cmu d:/.backups/dontstarvetogether/modoverrides.lua -o c:/.dst/save/Cluster_1/Master/modoverrides.lua -o c:/.dst/save/Cluster_1/Caves/modoverrides.lua
|
|
24
|
+
|
|
25
|
+
uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
# etcdsync
|
|
29
|
+
|
|
30
|
+
```shell
|
|
31
|
+
etcdsync etcd2mysql /test -e d:/.env/pymecli.env -t kvs
|
|
32
|
+
etcdsync etcd2mysql "" -e d:/.env/pymecli.env -t kvs # 全量
|
|
33
|
+
etcdsync mysql2etcd /test -e d:/.env/pymecli.env -t kvs
|
|
34
|
+
etcdsync mysql2etcd -e d:/.env/pymecli.env -t kvs # 全量
|
|
35
|
+
|
|
36
|
+
etcdsync etcd2json /test -e d:/.env/pymecli.env -f xxx.json
|
|
37
|
+
etcdsync json2etcd /test -e d:/.env/pymecli.env -f xxx.json
|
|
38
|
+
```
|
|
39
|
+
# quark
|
|
40
|
+
```shell
|
|
41
|
+
# 执行签到(默认会发通知)
|
|
42
|
+
uv run quark sign
|
|
43
|
+
# 签到但不发通知
|
|
44
|
+
uv run quark sign --no-notify
|
|
45
|
+
# 指定不同的 dotenv 路径
|
|
46
|
+
uv run quark sign -e /path/to/.env
|
|
47
|
+
# 只检查账号状态(不签到),方便调试
|
|
48
|
+
uv run quark check
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
# dnspod
|
|
52
|
+
```shell
|
|
53
|
+
# 列出域名下所有记录(先跑这个查 record_id)
|
|
54
|
+
uv run dnspod list
|
|
55
|
+
# 更新 IPv4 A 记录
|
|
56
|
+
uv run dnspod ddns
|
|
57
|
+
# 强制更新(即使 IP 没变化)
|
|
58
|
+
uv run dnspod ddns --force
|
|
59
|
+
# 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
|
|
60
|
+
uv run dnspod ddns6
|
|
61
|
+
|
|
62
|
+
uv run dnspod ddns6 --force
|
|
63
|
+
```
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""DNSPod DDNS 自动更新 CLI。
|
|
2
|
+
|
|
3
|
+
环境变量 (Go 版命名保持一致):
|
|
4
|
+
DP_ID: DNSPod 登录 ID
|
|
5
|
+
DP_TOKEN: DNSPod 登录 Token
|
|
6
|
+
DP_DOMAIN: 域名 (如 cursor.email)
|
|
7
|
+
DP_RECORD_ID: IPv4 A 记录 ID
|
|
8
|
+
DP_RECORD_ID_IPV6: IPv6 AAAA 记录 ID
|
|
9
|
+
DP_SUB_DOMAIN: 子域名前缀 (默认 "api")
|
|
10
|
+
DP_RECORD_LINE: 记录线路 (默认 "默认")
|
|
11
|
+
|
|
12
|
+
Redis 依赖 (用于 IPv6 DDNS):
|
|
13
|
+
REDIS_URL: Redis 地址,key = "local.IPv6"
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
import requests
|
|
19
|
+
import typer
|
|
20
|
+
from dotenv import load_dotenv
|
|
21
|
+
|
|
22
|
+
from utils.logger import get_logger
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
app = typer.Typer(help="DNSPod DDNS 自动更新")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
API_BASE = "https://dnsapi.cn"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# 底层 API
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
def _call(action: str, data: dict) -> dict:
|
|
36
|
+
"""通用 DNSPod API 调用(form-urlencoded POST)"""
|
|
37
|
+
resp = requests.post(f"{API_BASE}/{action}", data=data, timeout=15)
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
body = resp.json()
|
|
40
|
+
|
|
41
|
+
status = body.get("status", {})
|
|
42
|
+
code = str(status.get("code", ""))
|
|
43
|
+
if code != "1":
|
|
44
|
+
msg = status.get("message", "未知错误")
|
|
45
|
+
raise RuntimeError(f"DNSPod API 错误 [{code}]: {msg}")
|
|
46
|
+
|
|
47
|
+
return body
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def record_modify(
|
|
51
|
+
login_token: str,
|
|
52
|
+
domain: str,
|
|
53
|
+
record_id: str,
|
|
54
|
+
sub_domain: str,
|
|
55
|
+
record_type: str,
|
|
56
|
+
value: str,
|
|
57
|
+
record_line: str = "默认",
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""调用 Record.Modify 修改 DNS 记录"""
|
|
60
|
+
data = {
|
|
61
|
+
"login_token": login_token,
|
|
62
|
+
"format": "json",
|
|
63
|
+
"domain": domain,
|
|
64
|
+
"record_id": record_id,
|
|
65
|
+
"sub_domain": sub_domain,
|
|
66
|
+
"record_type": record_type,
|
|
67
|
+
"record_line": record_line,
|
|
68
|
+
"value": value,
|
|
69
|
+
}
|
|
70
|
+
return _call("Record.Modify", data)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def record_list(login_token: str, domain: str) -> list[dict]:
|
|
74
|
+
"""调用 Record.List 列出域名下所有记录"""
|
|
75
|
+
data = {
|
|
76
|
+
"login_token": login_token,
|
|
77
|
+
"format": "json",
|
|
78
|
+
"domain": domain,
|
|
79
|
+
}
|
|
80
|
+
body = _call("Record.List", data)
|
|
81
|
+
return body.get("records", [])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_public_ipv4() -> str:
|
|
85
|
+
"""从 ip.3322.net 获取公网 IPv4(纯文本)"""
|
|
86
|
+
resp = requests.get("http://ip.3322.net", timeout=10)
|
|
87
|
+
resp.raise_for_status()
|
|
88
|
+
ip = resp.text.strip()
|
|
89
|
+
# 简单校验
|
|
90
|
+
if not re.match(r"^\d{1,3}(?:\.\d{1,3}){3}$", ip):
|
|
91
|
+
raise RuntimeError(f"获取到的不是有效 IPv4: {ip!r}")
|
|
92
|
+
return ip
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def get_public_ipv6() -> str:
|
|
96
|
+
"""从 Redis 读取公网 IPv6(key=local.IPv6)"""
|
|
97
|
+
from utils.pyredis import get_redis_client_sync
|
|
98
|
+
|
|
99
|
+
r = get_redis_client_sync()
|
|
100
|
+
try:
|
|
101
|
+
val = r.get("local.IPv6")
|
|
102
|
+
if not val:
|
|
103
|
+
raise RuntimeError("Redis 中 local.IPv6 为空")
|
|
104
|
+
return val.strip()
|
|
105
|
+
finally:
|
|
106
|
+
r.close()
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# CLI 子命令
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
def _require_env(env_path: str) -> dict:
|
|
114
|
+
"""加载 dotenv 并返回公共配置"""
|
|
115
|
+
load_dotenv(env_path)
|
|
116
|
+
dp_id = os.getenv("DP_ID")
|
|
117
|
+
dp_token = os.getenv("DP_TOKEN")
|
|
118
|
+
dp_domain = os.getenv("DP_DOMAIN")
|
|
119
|
+
|
|
120
|
+
missing = []
|
|
121
|
+
if not dp_id:
|
|
122
|
+
missing.append("DP_ID")
|
|
123
|
+
if not dp_token:
|
|
124
|
+
missing.append("DP_TOKEN")
|
|
125
|
+
if not dp_domain:
|
|
126
|
+
missing.append("DP_DOMAIN")
|
|
127
|
+
if missing:
|
|
128
|
+
logger.error("缺少环境变量: %s", ", ".join(missing))
|
|
129
|
+
raise typer.Exit(1)
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"login_token": f"{dp_id},{dp_token}",
|
|
133
|
+
"domain": dp_domain,
|
|
134
|
+
"sub_domain": os.getenv("DP_SUB_DOMAIN", "api"),
|
|
135
|
+
"record_line": os.getenv("DP_RECORD_LINE", "默认"),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@app.command()
|
|
140
|
+
def list(
|
|
141
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
142
|
+
):
|
|
143
|
+
"""列出域名下所有 DNS 记录(方便查 record_id)"""
|
|
144
|
+
cfg = _require_env(dotenv_path)
|
|
145
|
+
|
|
146
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
147
|
+
if not records:
|
|
148
|
+
typer.secho("没有记录", fg=typer.colors.YELLOW)
|
|
149
|
+
return
|
|
150
|
+
|
|
151
|
+
typer.secho(f"\n域名 {cfg['domain']} 共 {len(records)} 条记录:\n", fg=typer.colors.CYAN)
|
|
152
|
+
typer.secho(f"{'ID':<10} {'类型':<6} {'子域':<16} {'值':<30} {'状态'}", fg=typer.colors.GREEN)
|
|
153
|
+
typer.secho("-" * 80, fg=typer.colors.CYAN)
|
|
154
|
+
for r in records:
|
|
155
|
+
typer.echo(
|
|
156
|
+
f"{r['id']:<10} {r['type']:<6} {r['name']:<16} {r['value']:<30} {r['status']}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@app.command()
|
|
161
|
+
def ddns(
|
|
162
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
163
|
+
force: bool = typer.Option(False, "--force", help="IP 未变化也强制更新"),
|
|
164
|
+
):
|
|
165
|
+
"""更新 A 记录(IPv4 DDNS)"""
|
|
166
|
+
cfg = _require_env(dotenv_path)
|
|
167
|
+
|
|
168
|
+
record_id = os.getenv("DP_RECORD_ID")
|
|
169
|
+
if not record_id:
|
|
170
|
+
logger.error("缺少环境变量: DP_RECORD_ID")
|
|
171
|
+
raise typer.Exit(1)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
cur_ip = get_public_ipv4()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.exception("获取公网 IPv4 失败: %s", e)
|
|
177
|
+
raise typer.Exit(1)
|
|
178
|
+
|
|
179
|
+
typer.secho(f"当前公网 IPv4: {cur_ip}", fg=typer.colors.CYAN)
|
|
180
|
+
|
|
181
|
+
# 获取现有记录对比
|
|
182
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
183
|
+
existing = next((r for r in records if r["id"] == record_id), None)
|
|
184
|
+
if existing and existing["value"] == cur_ip and not force:
|
|
185
|
+
typer.secho(f"IP 未变化 ({cur_ip}),跳过更新", fg=typer.colors.YELLOW)
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
resp = record_modify(
|
|
190
|
+
login_token=cfg["login_token"],
|
|
191
|
+
domain=cfg["domain"],
|
|
192
|
+
record_id=record_id,
|
|
193
|
+
sub_domain=cfg["sub_domain"],
|
|
194
|
+
record_type="A",
|
|
195
|
+
value=cur_ip,
|
|
196
|
+
record_line=cfg["record_line"],
|
|
197
|
+
)
|
|
198
|
+
typer.secho(
|
|
199
|
+
f"✅ A 记录更新成功 | "
|
|
200
|
+
f"新地址: {resp['record']['value']} | "
|
|
201
|
+
f"接口返回: {resp['status']['message']}",
|
|
202
|
+
fg=typer.colors.GREEN,
|
|
203
|
+
)
|
|
204
|
+
except Exception as e:
|
|
205
|
+
logger.exception("A 记录更新失败: %s", e)
|
|
206
|
+
raise typer.Exit(1)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@app.command()
|
|
210
|
+
def ddns6(
|
|
211
|
+
dotenv_path: str = typer.Option(".env", "--env", "-e", help="dotenv 文件路径"),
|
|
212
|
+
force: bool = typer.Option(False, "--force", help="IP 未变化也强制更新"),
|
|
213
|
+
):
|
|
214
|
+
"""更新 AAAA 记录(IPv6 DDNS,从 Redis 读取 IPv6)"""
|
|
215
|
+
cfg = _require_env(dotenv_path)
|
|
216
|
+
|
|
217
|
+
record_id = os.getenv("DP_RECORD_ID_IPV6")
|
|
218
|
+
if not record_id:
|
|
219
|
+
logger.error("缺少环境变量: DP_RECORD_ID_IPV6")
|
|
220
|
+
raise typer.Exit(1)
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
cur_ip = get_public_ipv6()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.exception("获取公网 IPv6 失败: %s", e)
|
|
226
|
+
raise typer.Exit(1)
|
|
227
|
+
|
|
228
|
+
typer.secho(f"当前公网 IPv6: {cur_ip}", fg=typer.colors.CYAN)
|
|
229
|
+
|
|
230
|
+
# 获取现有记录对比
|
|
231
|
+
records = record_list(cfg["login_token"], cfg["domain"])
|
|
232
|
+
existing = next((r for r in records if r["id"] == record_id), None)
|
|
233
|
+
if existing and existing["value"] == cur_ip and not force:
|
|
234
|
+
typer.secho(f"IPv6 未变化 ({cur_ip}),跳过更新", fg=typer.colors.YELLOW)
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
resp = record_modify(
|
|
239
|
+
login_token=cfg["login_token"],
|
|
240
|
+
domain=cfg["domain"],
|
|
241
|
+
record_id=record_id,
|
|
242
|
+
sub_domain=cfg["sub_domain"],
|
|
243
|
+
record_type="AAAA",
|
|
244
|
+
value=cur_ip,
|
|
245
|
+
record_line=cfg["record_line"],
|
|
246
|
+
)
|
|
247
|
+
typer.secho(
|
|
248
|
+
f"✅ AAAA 记录更新成功 | "
|
|
249
|
+
f"新地址: {resp['record']['value']} | "
|
|
250
|
+
f"接口返回: {resp['status']['message']}",
|
|
251
|
+
fg=typer.colors.GREEN,
|
|
252
|
+
)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
logger.exception("AAAA 记录更新失败: %s", e)
|
|
255
|
+
raise typer.Exit(1)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
if __name__ == "__main__":
|
|
259
|
+
app()
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
|
-
MySQL <-> Etcd 数据同步 CLI。
|
|
2
|
+
MySQL / JSON <-> Etcd 数据同步 CLI。
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
四个子命令:
|
|
5
5
|
mysql2etcd 将 MySQL kvs 表的 k, v 写入 etcd
|
|
6
6
|
etcd2mysql 将 etcd 指定前缀的 key/value 写入 MySQL kvs 表
|
|
7
|
+
etcd2json 将 etcd 指定前缀的 key/value 导出为 JSON 文件
|
|
8
|
+
json2etcd 将 JSON 文件中的 key/value 写入 etcd
|
|
7
9
|
"""
|
|
8
10
|
|
|
11
|
+
import json
|
|
9
12
|
import logging
|
|
10
13
|
import os
|
|
11
14
|
|
|
@@ -158,5 +161,106 @@ def etcd_to_mysql(
|
|
|
158
161
|
etcd.close()
|
|
159
162
|
|
|
160
163
|
|
|
164
|
+
@app.command("etcd2json")
|
|
165
|
+
def etcd_to_json(
|
|
166
|
+
prefix: str = typer.Argument(
|
|
167
|
+
"",
|
|
168
|
+
help="etcd key 前缀(空字符串表示全量)",
|
|
169
|
+
),
|
|
170
|
+
env_path: str = typer.Option(
|
|
171
|
+
".env",
|
|
172
|
+
"--env",
|
|
173
|
+
"-e",
|
|
174
|
+
help="dotenv 文件路径",
|
|
175
|
+
),
|
|
176
|
+
file_path: str = typer.Option(
|
|
177
|
+
...,
|
|
178
|
+
"--file-path",
|
|
179
|
+
"-f",
|
|
180
|
+
help="输出 JSON 文件路径",
|
|
181
|
+
),
|
|
182
|
+
):
|
|
183
|
+
"""将 etcd 中指定前缀的 key/value 导出为 JSON 文件。
|
|
184
|
+
|
|
185
|
+
JSON 格式为简单的 {key: value} 对象,value 均为字符串。
|
|
186
|
+
"""
|
|
187
|
+
etcd = get_etcd_client(env_path)
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
data: dict[str, str] = {}
|
|
191
|
+
iterator = etcd.get_all() if not prefix else etcd.get_prefix(prefix)
|
|
192
|
+
for value, meta in iterator:
|
|
193
|
+
k = meta.key.decode("utf-8")
|
|
194
|
+
v = value.decode("utf-8") if isinstance(value, bytes) else value
|
|
195
|
+
data[k] = v
|
|
196
|
+
|
|
197
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
198
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
199
|
+
|
|
200
|
+
logger.info(f"✅ 已导出 {len(data)} 条到 {file_path}")
|
|
201
|
+
except OSError as e:
|
|
202
|
+
logger.error("写入文件失败: %s", e)
|
|
203
|
+
raise typer.Exit(1)
|
|
204
|
+
finally:
|
|
205
|
+
etcd.close()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.command("json2etcd")
|
|
209
|
+
def json_to_etcd(
|
|
210
|
+
prefix: str = typer.Argument(
|
|
211
|
+
"",
|
|
212
|
+
help="写入 etcd 时给所有 key 加上的前缀(空表示不加)",
|
|
213
|
+
),
|
|
214
|
+
env_path: str = typer.Option(
|
|
215
|
+
".env",
|
|
216
|
+
"--env",
|
|
217
|
+
"-e",
|
|
218
|
+
help="dotenv 文件路径",
|
|
219
|
+
),
|
|
220
|
+
file_path: str = typer.Option(
|
|
221
|
+
...,
|
|
222
|
+
"--file-path",
|
|
223
|
+
"-f",
|
|
224
|
+
help="输入 JSON 文件路径",
|
|
225
|
+
),
|
|
226
|
+
):
|
|
227
|
+
"""将 JSON 文件中的 key/value 写入 etcd。
|
|
228
|
+
|
|
229
|
+
JSON 格式应为 {key: value} 对象,value 会被转为字符串。
|
|
230
|
+
--prefix 会加到每个 key 前面(如果 key 本身已带该前缀则不重复加)。
|
|
231
|
+
"""
|
|
232
|
+
etcd = get_etcd_client(env_path)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
with open(file_path, encoding="utf-8") as f:
|
|
236
|
+
data = json.load(f)
|
|
237
|
+
except OSError as e:
|
|
238
|
+
logger.error("读取文件失败: %s", e)
|
|
239
|
+
raise typer.Exit(1)
|
|
240
|
+
except json.JSONDecodeError as e:
|
|
241
|
+
logger.error("JSON 解析失败: %s", e)
|
|
242
|
+
raise typer.Exit(1)
|
|
243
|
+
|
|
244
|
+
if not isinstance(data, dict):
|
|
245
|
+
logger.error("JSON 顶层必须是对象 (dict)")
|
|
246
|
+
raise typer.Exit(1)
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
count = 0
|
|
250
|
+
for k, v in data.items():
|
|
251
|
+
# 处理前缀:已带则不重复加
|
|
252
|
+
full_key = k
|
|
253
|
+
if prefix and not k.startswith(prefix):
|
|
254
|
+
full_key = prefix.rstrip("/") + "/" + k.lstrip("/")
|
|
255
|
+
|
|
256
|
+
etcd.put(full_key, str(v))
|
|
257
|
+
logger.debug("put: %s -> %s", full_key, v)
|
|
258
|
+
count += 1
|
|
259
|
+
|
|
260
|
+
logger.info(f"✅ 已写入 {count} 条到 etcd")
|
|
261
|
+
finally:
|
|
262
|
+
etcd.close()
|
|
263
|
+
|
|
264
|
+
|
|
161
265
|
if __name__ == "__main__":
|
|
162
266
|
app()
|