pymecli 0.6.1__tar.gz → 0.6.3__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.
Files changed (52) hide show
  1. {pymecli-0.6.1 → pymecli-0.6.3}/PKG-INFO +15 -4
  2. {pymecli-0.6.1 → pymecli-0.6.3}/README.md +14 -3
  3. pymecli-0.6.1/cli/etcd_mysql.py → pymecli-0.6.3/cli/etcd_sync.py +135 -2
  4. {pymecli-0.6.1 → pymecli-0.6.3}/pyproject.toml +2 -2
  5. {pymecli-0.6.1 → pymecli-0.6.3}/.gitignore +0 -0
  6. {pymecli-0.6.1 → pymecli-0.6.3}/api/__init__.py +0 -0
  7. {pymecli-0.6.1 → pymecli-0.6.3}/api/v1/__init__.py +0 -0
  8. {pymecli-0.6.1 → pymecli-0.6.3}/api/v1/clash.py +0 -0
  9. {pymecli-0.6.1 → pymecli-0.6.3}/api/v1/redis.py +0 -0
  10. {pymecli-0.6.1 → pymecli-0.6.3}/cli/__init__.py +0 -0
  11. {pymecli-0.6.1 → pymecli-0.6.3}/cli/bitget.py +0 -0
  12. {pymecli-0.6.1 → pymecli-0.6.3}/cli/dnspod.py +0 -0
  13. {pymecli-0.6.1 → pymecli-0.6.3}/cli/dst.py +0 -0
  14. {pymecli-0.6.1 → pymecli-0.6.3}/cli/example.py +0 -0
  15. {pymecli-0.6.1 → pymecli-0.6.3}/cli/fast.py +0 -0
  16. {pymecli-0.6.1 → pymecli-0.6.3}/cli/gate.py +0 -0
  17. {pymecli-0.6.1 → pymecli-0.6.3}/cli/okx.py +0 -0
  18. {pymecli-0.6.1 → pymecli-0.6.3}/cli/quark.py +0 -0
  19. {pymecli-0.6.1 → pymecli-0.6.3}/cli/redis_csv.py +0 -0
  20. {pymecli-0.6.1 → pymecli-0.6.3}/cli/tele.py +0 -0
  21. {pymecli-0.6.1 → pymecli-0.6.3}/cli/util.py +0 -0
  22. {pymecli-0.6.1 → pymecli-0.6.3}/core/__init__.py +0 -0
  23. {pymecli-0.6.1 → pymecli-0.6.3}/core/clash.py +0 -0
  24. {pymecli-0.6.1 → pymecli-0.6.3}/core/config.py +0 -0
  25. {pymecli-0.6.1 → pymecli-0.6.3}/core/redis_client.py +0 -0
  26. {pymecli-0.6.1 → pymecli-0.6.3}/crypto/__init__.py +0 -0
  27. {pymecli-0.6.1 → pymecli-0.6.3}/crypto/bitget.py +0 -0
  28. {pymecli-0.6.1 → pymecli-0.6.3}/crypto/gate.py +0 -0
  29. {pymecli-0.6.1 → pymecli-0.6.3}/crypto/okx.py +0 -0
  30. {pymecli-0.6.1 → pymecli-0.6.3}/data/__init__.py +0 -0
  31. {pymecli-0.6.1 → pymecli-0.6.3}/data/dou_dict.py +0 -0
  32. {pymecli-0.6.1 → pymecli-0.6.3}/data/dou_list.py +0 -0
  33. {pymecli-0.6.1 → pymecli-0.6.3}/data/main.py +0 -0
  34. {pymecli-0.6.1 → pymecli-0.6.3}/data/template.yaml +0 -0
  35. {pymecli-0.6.1 → pymecli-0.6.3}/models/__init__.py +0 -0
  36. {pymecli-0.6.1 → pymecli-0.6.3}/models/douzero_model.py +0 -0
  37. {pymecli-0.6.1 → pymecli-0.6.3}/models/ocr_model.py +0 -0
  38. {pymecli-0.6.1 → pymecli-0.6.3}/models/response.py +0 -0
  39. {pymecli-0.6.1 → pymecli-0.6.3}/utils/__init__.py +0 -0
  40. {pymecli-0.6.1 → pymecli-0.6.3}/utils/elapsed.py +0 -0
  41. {pymecli-0.6.1 → pymecli-0.6.3}/utils/etcd.py +0 -0
  42. {pymecli-0.6.1 → pymecli-0.6.3}/utils/helper.py +0 -0
  43. {pymecli-0.6.1 → pymecli-0.6.3}/utils/logger.py +0 -0
  44. {pymecli-0.6.1 → pymecli-0.6.3}/utils/mysql.py +0 -0
  45. {pymecli-0.6.1 → pymecli-0.6.3}/utils/notify.py +0 -0
  46. {pymecli-0.6.1 → pymecli-0.6.3}/utils/path.py +0 -0
  47. {pymecli-0.6.1 → pymecli-0.6.3}/utils/pd.py +0 -0
  48. {pymecli-0.6.1 → pymecli-0.6.3}/utils/pyredis.py +0 -0
  49. {pymecli-0.6.1 → pymecli-0.6.3}/utils/sendNotify.py +0 -0
  50. {pymecli-0.6.1 → pymecli-0.6.3}/utils/sleep.py +0 -0
  51. {pymecli-0.6.1 → pymecli-0.6.3}/utils/text.py +0 -0
  52. {pymecli-0.6.1 → pymecli-0.6.3}/utils/toml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pymecli
3
- Version: 0.6.1
3
+ Version: 0.6.3
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,10 +67,13 @@ uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
67
67
  # etcdsync
68
68
 
69
69
  ```shell
70
- etcdsync etcd2mysql /cron/jobs -e d:/.env/pymecli.env -t kvs
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 /cron/jobs -e d:/.env/pymecli.env -t kvs
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
74
77
  ```
75
78
  # quark
76
79
  ```shell
@@ -85,6 +88,15 @@ uv run quark check
85
88
  ```
86
89
 
87
90
  # dnspod
91
+ ## 环境变量说明
92
+ - `DP_ID`: DNSPod 登录 ID
93
+ - `DP_TOKEN`: DNSPod 登录 Token
94
+ - `DP_DOMAIN`: 域名 (如 cursor.email)
95
+ - `DP_RECORD_ID`: IPv4 A 记录 ID
96
+ - `DP_RECORD_ID_IPV6`: IPv6 AAAA 记录 ID
97
+ - `DP_SUB_DOMAIN`: 子域名前缀 (默认 "api")
98
+ - `DP_RECORD_LINE`: 记录线路 (默认 "默认")
99
+ ## 示例
88
100
  ```shell
89
101
  # 列出域名下所有记录(先跑这个查 record_id)
90
102
  uv run dnspod list
@@ -94,6 +106,5 @@ uv run dnspod ddns
94
106
  uv run dnspod ddns --force
95
107
  # 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
96
108
  uv run dnspod ddns6
97
-
98
109
  uv run dnspod ddns6 --force
99
110
  ```
@@ -28,10 +28,13 @@ uv run dst cml d:/.backups/dontstarvetogether/modoverrides.lua -m 3486375086
28
28
  # etcdsync
29
29
 
30
30
  ```shell
31
- etcdsync etcd2mysql /cron/jobs -e d:/.env/pymecli.env -t kvs
31
+ etcdsync etcd2mysql /test -e d:/.env/pymecli.env -t kvs
32
32
  etcdsync etcd2mysql "" -e d:/.env/pymecli.env -t kvs # 全量
33
- etcdsync mysql2etcd /cron/jobs -e d:/.env/pymecli.env -t kvs
33
+ etcdsync mysql2etcd /test -e d:/.env/pymecli.env -t kvs
34
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
35
38
  ```
36
39
  # quark
37
40
  ```shell
@@ -46,6 +49,15 @@ uv run quark check
46
49
  ```
47
50
 
48
51
  # dnspod
52
+ ## 环境变量说明
53
+ - `DP_ID`: DNSPod 登录 ID
54
+ - `DP_TOKEN`: DNSPod 登录 Token
55
+ - `DP_DOMAIN`: 域名 (如 cursor.email)
56
+ - `DP_RECORD_ID`: IPv4 A 记录 ID
57
+ - `DP_RECORD_ID_IPV6`: IPv6 AAAA 记录 ID
58
+ - `DP_SUB_DOMAIN`: 子域名前缀 (默认 "api")
59
+ - `DP_RECORD_LINE`: 记录线路 (默认 "默认")
60
+ ## 示例
49
61
  ```shell
50
62
  # 列出域名下所有记录(先跑这个查 record_id)
51
63
  uv run dnspod list
@@ -55,6 +67,5 @@ uv run dnspod ddns
55
67
  uv run dnspod ddns --force
56
68
  # 更新 IPv6 AAAA 记录(从 Redis 读 local.IPv6)
57
69
  uv run dnspod ddns6
58
-
59
70
  uv run dnspod ddns6 --force
60
71
  ```
@@ -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
 
@@ -22,6 +25,32 @@ logger = get_logger(__name__, level=logging.INFO)
22
25
  DEFAULT_TABLE = "kvs"
23
26
 
24
27
 
28
+ # ---------------------------------------------------------------------------
29
+ # 智能 JSON 转换(etcd ↔ 文件)
30
+ # ---------------------------------------------------------------------------
31
+
32
+ def _try_json_loads(s: str):
33
+ """尝试把字符串当 JSON 解析,成功返回对象,失败返回原字符串。
34
+
35
+ 只接受以 { / [ 开头的内容,避免把 "123" "true" 这类普通值也误解析。
36
+ """
37
+ s = s.strip()
38
+ if not s or s[0] not in "{[":
39
+ return s
40
+ try:
41
+ return json.loads(s)
42
+ except (json.JSONDecodeError, TypeError):
43
+ return s
44
+
45
+
46
+ def _json_or_str(v) -> str:
47
+ """dict/list → 紧凑 JSON 字符串;其他 → str(v)。"""
48
+ if isinstance(v, (dict, list)):
49
+ # separators=(",", ":") 去掉空格,保持 etcd 里原样
50
+ return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
51
+ return str(v)
52
+
53
+
25
54
  @app.command("mysql2etcd")
26
55
  def mysql_to_etcd(
27
56
  prefix: str = typer.Argument(
@@ -158,5 +187,109 @@ def etcd_to_mysql(
158
187
  etcd.close()
159
188
 
160
189
 
190
+ @app.command("etcd2json")
191
+ def etcd_to_json(
192
+ prefix: str = typer.Argument(
193
+ "",
194
+ help="etcd key 前缀(空字符串表示全量)",
195
+ ),
196
+ env_path: str = typer.Option(
197
+ ".env",
198
+ "--env",
199
+ "-e",
200
+ help="dotenv 文件路径",
201
+ ),
202
+ file_path: str = typer.Option(
203
+ ...,
204
+ "--file-path",
205
+ "-f",
206
+ help="输出 JSON 文件路径",
207
+ ),
208
+ ):
209
+ """将 etcd 中指定前缀的 key/value 导出为 JSON 文件。
210
+
211
+ 智能转换:如果 etcd value 是 JSON 字符串(以 { 或 [ 开头),
212
+ 会自动解析为真正的 JSON 对象写入文件,编辑更方便;
213
+ 普通字符串保持原样。
214
+ """
215
+ etcd = get_etcd_client(env_path)
216
+
217
+ try:
218
+ data: dict = {}
219
+ iterator = etcd.get_all() if not prefix else etcd.get_prefix(prefix)
220
+ for value, meta in iterator:
221
+ k = meta.key.decode("utf-8")
222
+ raw = value.decode("utf-8") if isinstance(value, bytes) else value
223
+ data[k] = _try_json_loads(raw)
224
+
225
+ with open(file_path, "w", encoding="utf-8") as f:
226
+ json.dump(data, f, ensure_ascii=False, indent=2)
227
+
228
+ logger.info(f"✅ 已导出 {len(data)} 条到 {file_path}")
229
+ except OSError as e:
230
+ logger.error("写入文件失败: %s", e)
231
+ raise typer.Exit(1)
232
+ finally:
233
+ etcd.close()
234
+
235
+
236
+ @app.command("json2etcd")
237
+ def json_to_etcd(
238
+ prefix: str = typer.Argument(
239
+ "",
240
+ help="写入 etcd 时给所有 key 加上的前缀(空表示不加)",
241
+ ),
242
+ env_path: str = typer.Option(
243
+ ".env",
244
+ "--env",
245
+ "-e",
246
+ help="dotenv 文件路径",
247
+ ),
248
+ file_path: str = typer.Option(
249
+ ...,
250
+ "--file-path",
251
+ "-f",
252
+ help="输入 JSON 文件路径",
253
+ ),
254
+ ):
255
+ """将 JSON 文件中的 key/value 写入 etcd。
256
+
257
+ 智能转换:dict/list 会自动序列化为紧凑 JSON 字符串写入 etcd;
258
+ 字符串/数字/布尔保持 str() 转换。
259
+ --prefix 会加到每个 key 前面(如果 key 本身已带该前缀则不重复加)。
260
+ """
261
+ etcd = get_etcd_client(env_path)
262
+
263
+ try:
264
+ with open(file_path, encoding="utf-8") as f:
265
+ data = json.load(f)
266
+ except OSError as e:
267
+ logger.error("读取文件失败: %s", e)
268
+ raise typer.Exit(1)
269
+ except json.JSONDecodeError as e:
270
+ logger.error("JSON 解析失败: %s", e)
271
+ raise typer.Exit(1)
272
+
273
+ if not isinstance(data, dict):
274
+ logger.error("JSON 顶层必须是对象 (dict)")
275
+ raise typer.Exit(1)
276
+
277
+ try:
278
+ count = 0
279
+ for k, v in data.items():
280
+ # 处理前缀:已带则不重复加
281
+ full_key = k
282
+ if prefix and not k.startswith(prefix):
283
+ full_key = prefix.rstrip("/") + "/" + k.lstrip("/")
284
+
285
+ etcd.put(full_key, _json_or_str(v))
286
+ logger.debug("put: %s -> %s", full_key, v)
287
+ count += 1
288
+
289
+ logger.info(f"✅ 已写入 {count} 条到 etcd")
290
+ finally:
291
+ etcd.close()
292
+
293
+
161
294
  if __name__ == "__main__":
162
295
  app()
@@ -38,7 +38,7 @@
38
38
  name = "pymecli"
39
39
  readme = "README.md"
40
40
  requires-python = ">=3.10"
41
- version = "0.6.1"
41
+ version = "0.6.3"
42
42
 
43
43
  [project.urls]
44
44
  Homepage = "https://pypi.org/project/pymecli"
@@ -49,7 +49,7 @@
49
49
  bitget = "cli.bitget:app"
50
50
  dnspod = "cli.dnspod:app"
51
51
  dst = "cli.dst:app"
52
- etcdsync = "cli.etcd_mysql:app"
52
+ etcdsync = "cli.etcd_sync:app"
53
53
  example = "cli.example:app"
54
54
  fast = "cli.fast:typer_app"
55
55
  gate = "cli.gate:app"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes