pymecli 0.5.3__tar.gz → 0.5.4__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 (44) hide show
  1. {pymecli-0.5.3 → pymecli-0.5.4}/PKG-INFO +3 -2
  2. {pymecli-0.5.3 → pymecli-0.5.4}/cli/bitget.py +8 -12
  3. {pymecli-0.5.3 → pymecli-0.5.4}/cli/gate.py +3 -5
  4. pymecli-0.5.4/cli/tele.py +355 -0
  5. {pymecli-0.5.3 → pymecli-0.5.4}/crypto/bitget.py +15 -22
  6. {pymecli-0.5.3 → pymecli-0.5.4}/crypto/gate.py +5 -7
  7. {pymecli-0.5.3 → pymecli-0.5.4}/pyproject.toml +3 -1
  8. {pymecli-0.5.3 → pymecli-0.5.4}/utils/mysql.py +62 -20
  9. {pymecli-0.5.3 → pymecli-0.5.4}/.gitignore +0 -0
  10. {pymecli-0.5.3 → pymecli-0.5.4}/README.md +0 -0
  11. {pymecli-0.5.3 → pymecli-0.5.4}/api/__init__.py +0 -0
  12. {pymecli-0.5.3 → pymecli-0.5.4}/api/v1/__init__.py +0 -0
  13. {pymecli-0.5.3 → pymecli-0.5.4}/api/v1/clash.py +0 -0
  14. {pymecli-0.5.3 → pymecli-0.5.4}/api/v1/redis.py +0 -0
  15. {pymecli-0.5.3 → pymecli-0.5.4}/cli/__init__.py +0 -0
  16. {pymecli-0.5.3 → pymecli-0.5.4}/cli/dst.py +0 -0
  17. {pymecli-0.5.3 → pymecli-0.5.4}/cli/example.py +0 -0
  18. {pymecli-0.5.3 → pymecli-0.5.4}/cli/fast.py +0 -0
  19. {pymecli-0.5.3 → pymecli-0.5.4}/cli/redis_csv.py +0 -0
  20. {pymecli-0.5.3 → pymecli-0.5.4}/cli/util.py +0 -0
  21. {pymecli-0.5.3 → pymecli-0.5.4}/core/__init__.py +0 -0
  22. {pymecli-0.5.3 → pymecli-0.5.4}/core/clash.py +0 -0
  23. {pymecli-0.5.3 → pymecli-0.5.4}/core/config.py +0 -0
  24. {pymecli-0.5.3 → pymecli-0.5.4}/core/redis_client.py +0 -0
  25. {pymecli-0.5.3 → pymecli-0.5.4}/crypto/__init__.py +0 -0
  26. {pymecli-0.5.3 → pymecli-0.5.4}/data/__init__.py +0 -0
  27. {pymecli-0.5.3 → pymecli-0.5.4}/data/dou_dict.py +0 -0
  28. {pymecli-0.5.3 → pymecli-0.5.4}/data/dou_list.py +0 -0
  29. {pymecli-0.5.3 → pymecli-0.5.4}/data/main.py +0 -0
  30. {pymecli-0.5.3 → pymecli-0.5.4}/data/template.yaml +0 -0
  31. {pymecli-0.5.3 → pymecli-0.5.4}/models/__init__.py +0 -0
  32. {pymecli-0.5.3 → pymecli-0.5.4}/models/douzero_model.py +0 -0
  33. {pymecli-0.5.3 → pymecli-0.5.4}/models/ocr_model.py +0 -0
  34. {pymecli-0.5.3 → pymecli-0.5.4}/models/response.py +0 -0
  35. {pymecli-0.5.3 → pymecli-0.5.4}/utils/__init__.py +0 -0
  36. {pymecli-0.5.3 → pymecli-0.5.4}/utils/elapsed.py +0 -0
  37. {pymecli-0.5.3 → pymecli-0.5.4}/utils/helper.py +0 -0
  38. {pymecli-0.5.3 → pymecli-0.5.4}/utils/logger.py +0 -0
  39. {pymecli-0.5.3 → pymecli-0.5.4}/utils/path.py +0 -0
  40. {pymecli-0.5.3 → pymecli-0.5.4}/utils/pd.py +0 -0
  41. {pymecli-0.5.3 → pymecli-0.5.4}/utils/pyredis.py +0 -0
  42. {pymecli-0.5.3 → pymecli-0.5.4}/utils/sleep.py +0 -0
  43. {pymecli-0.5.3 → pymecli-0.5.4}/utils/text.py +0 -0
  44. {pymecli-0.5.3 → pymecli-0.5.4}/utils/toml.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: pymecli
3
- Version: 0.5.3
3
+ Version: 0.5.4
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
@@ -28,6 +28,7 @@ Requires-Dist: selenium>=4.40.0
28
28
  Requires-Dist: slpp>=1.2.3
29
29
  Requires-Dist: sqlalchemy>=2.0.45
30
30
  Requires-Dist: tabulate>=0.9.0
31
+ Requires-Dist: telethon>=1.44.0
31
32
  Requires-Dist: toml>=0.10.2
32
33
  Requires-Dist: typer>=0.20.1
33
34
  Requires-Dist: uvicorn>=0.40.0
@@ -21,23 +21,19 @@ app = typer.Typer()
21
21
  @app.command()
22
22
  def sync(
23
23
  env_path: str = "d:/.env",
24
- csv_dir: str = "d:/github/meme2046/data",
25
24
  ):
26
- """同步mysql中grid数据到csv文件"""
25
+ """同步mysql中grid数据到redis"""
27
26
  engine = get_database_engine(env_path)
28
- grid_csv_fp = f"{csv_dir}/bitget_grid_0.csv"
29
- sf_csv_fp = f"{csv_dir}/bitget_sf_0.csv"
30
- ff_csv_fp = f"{csv_dir}/bitget_ff_0.csv"
31
27
 
32
- asyncio.run(grid_open(engine, grid_csv_fp))
33
- asyncio.run(grid_close(engine, grid_csv_fp))
28
+ asyncio.run(grid_open(engine))
29
+ asyncio.run(grid_close(engine))
34
30
 
35
- asyncio.run(bitget_sf_open(engine, sf_csv_fp))
36
- asyncio.run(bitget_sf_close(engine, sf_csv_fp))
31
+ asyncio.run(bitget_sf_open(engine))
32
+ asyncio.run(bitget_sf_close(engine))
37
33
 
38
- asyncio.run(bitget_ff_open(engine, ff_csv_fp))
39
- asyncio.run(bitget_ff_pending(engine, ff_csv_fp))
40
- asyncio.run(bitget_ff_close(engine, ff_csv_fp))
34
+ asyncio.run(bitget_ff_open(engine))
35
+ asyncio.run(bitget_ff_pending(engine))
36
+ asyncio.run(bitget_ff_close(engine))
41
37
 
42
38
 
43
39
  @app.command()
@@ -21,10 +21,8 @@ app = typer.Typer()
21
21
  @app.command()
22
22
  def rsync(
23
23
  env_path: str = "d:/.env",
24
- csv_dir: str = "d:/github/meme2046/data",
25
24
  ):
26
- """同步mysql中grid数据到csv文件"""
25
+ """同步mysql中grid数据到redis"""
27
26
  engine = get_database_engine(env_path)
28
- grid_csv_fp = f"{csv_dir}/gate_grid_0.csv"
29
- asyncio.run(grid_open(engine, grid_csv_fp))
30
- asyncio.run(grid_close(engine, grid_csv_fp))
27
+ asyncio.run(grid_open(engine))
28
+ asyncio.run(grid_close(engine))
@@ -0,0 +1,355 @@
1
+ import os
2
+
3
+ import typer
4
+ from dotenv import load_dotenv
5
+
6
+ from utils.logger import get_logger
7
+
8
+ logger = get_logger(__name__)
9
+ app = typer.Typer()
10
+
11
+
12
+ @app.command()
13
+ def info(
14
+ dotenv_path: str = typer.Argument(
15
+ help="dotenv file contains TELE_SEESION_STRING", default="d:/.env"
16
+ ),
17
+ ):
18
+ load_dotenv("d:/.env")
19
+ logger.info(os.getenv("TELE_SESSION_STRING"))
20
+
21
+
22
+ @app.command()
23
+ def check(
24
+ dotenv_path: str = typer.Argument(
25
+ help="dotenv file contains TELE_SESSION_STRING / TELE_API_ID / TELE_API_HASH",
26
+ default="d:/.env",
27
+ ),
28
+ ):
29
+ """验证 TELE_SESSION_STRING 是否有效。
30
+
31
+ 通过 StringSession 建立客户端并发起一次 get_me 调用:
32
+ 能正常返回账号对象即视为 session 有效;get_me 在 auth_key
33
+ 未授权 / 被注销时会内部捕获 UnauthorizedError 并返回 None,
34
+ 故 me is None 即判定 session 失效。
35
+ 需要 .env 中同时配置 TELE_API_ID 与 TELE_API_HASH。
36
+ """
37
+ load_dotenv(dotenv_path)
38
+
39
+ session_string = os.getenv("TELE_SESSION_STRING")
40
+ api_id = os.getenv("TELE_API_ID")
41
+ api_hash = os.getenv("TELE_API_HASH")
42
+ if not session_string:
43
+ logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
44
+ raise typer.Exit(1)
45
+ if not (api_id and api_hash):
46
+ logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
47
+ raise typer.Exit(1)
48
+
49
+ # 延迟导入:仅在使用 check 子命令时才加载 telethon,避免拖慢其它子命令
50
+ import asyncio
51
+ from typing import Awaitable, cast
52
+
53
+ from telethon import TelegramClient
54
+ from telethon.sessions import StringSession
55
+
56
+ async def _verify():
57
+ client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
58
+ try:
59
+ await client.connect()
60
+ # get_me 会真正发起一次 API 调用,能返回即说明 auth_key 仍然有效
61
+ return await client.get_me()
62
+ finally:
63
+ # telethon 的 disconnect 声明为同步 def,但 loop 运行时实际返回协程,
64
+ # Pylance 据签名推断返回 None 导致 await 报错,用 cast 显式标注
65
+ await cast(Awaitable[None], client.disconnect())
66
+
67
+ try:
68
+ me = asyncio.run(_verify())
69
+ except Exception as e:
70
+ # 网络异常等非鉴权问题,打印完整堆栈便于排查
71
+ logger.exception("验证 session 时发生异常: %s", e)
72
+ raise typer.Exit(1)
73
+
74
+ # get_me 内部 catch UnauthorizedError(含 AuthKeyUnregistered/Invalid 等所有子类)后返回 None,
75
+ # 所以 me is None 才是 auth_key 已被服务器拒绝 / session 失效的真实信号
76
+ if me is None:
77
+ logger.error("session 无效: auth_key 未授权或已被服务器注销")
78
+ raise typer.Exit(1)
79
+
80
+ logger.info(
81
+ "session 有效: id=%s name=%s phone=%s",
82
+ getattr(me, "id", None), getattr(me, "first_name", None), getattr(me, "phone", None),
83
+ )
84
+
85
+
86
+ @app.command()
87
+ def login(
88
+ dotenv_path: str = typer.Argument(
89
+ help="dotenv file containing TELE_API_ID / TELE_API_HASH",
90
+ default="d:/.env",
91
+ ),
92
+ ):
93
+ """交互式登录并打印 TELE_SESSION_STRING。
94
+
95
+ 使用 telethon 的 start() 完成 phone → 验证码 → 2FA 的交互式登录,
96
+ 登录成功后用 StringSession.save() 取出可复用 session 串并打印。
97
+ session string 失效周期很长,手动复制到 .env 即可,不做自动回写。
98
+ 需要 .env 中已配置 TELE_API_ID 与 TELE_API_HASH。
99
+ """
100
+ load_dotenv(dotenv_path)
101
+ api_id = os.getenv("TELE_API_ID")
102
+ api_hash = os.getenv("TELE_API_HASH")
103
+ if not (api_id and api_hash):
104
+ logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
105
+ raise typer.Exit(1)
106
+
107
+ # 延迟导入:仅在使用 login 子命令时才加载 telethon
108
+ import asyncio
109
+ from typing import Awaitable, cast
110
+
111
+ from telethon import TelegramClient
112
+ from telethon.sessions import StringSession
113
+
114
+ async def _login():
115
+ # 空 StringSession + start() 触发交互式登录(控制台输入 phone / 验证码 / 2FA 密码)
116
+ session = StringSession()
117
+ client = TelegramClient(session, int(api_id), api_hash)
118
+ try:
119
+ # start() 同 disconnect 一样声明为同步 def,loop 运行时返回协程,用 cast 标注
120
+ await cast(Awaitable[None], client.start())
121
+ # telethon 在 connect 时原地写入 session.auth_key,故持有的 session 引用即登录后状态;
122
+ # 直接用 session.save() 而非 client.session,避开 client.session 宽类型推断的 .save() 报错
123
+ return session.save()
124
+ finally:
125
+ await cast(Awaitable[None], client.disconnect())
126
+
127
+ try:
128
+ session_string = asyncio.run(_login())
129
+ except Exception as e:
130
+ logger.exception("生成 session string 时发生异常: %s", e)
131
+ raise typer.Exit(1)
132
+
133
+ # 只打印,不自动回写 .env:session string 失效周期长,手动加一次即可,避免覆盖风险
134
+ typer.secho("TELE_SESSION_STRING 生成成功,请复制下行并手动写入 .env:", fg=typer.colors.GREEN)
135
+ typer.secho(session_string, fg=typer.colors.CYAN)
136
+
137
+
138
+ @app.command()
139
+ def upload(
140
+ image_path: str = typer.Argument(
141
+ help="要上传的图片文件路径",
142
+ ),
143
+ dotenv_path: str = typer.Option(
144
+ help="dotenv file path", default="d:/.env"
145
+ ),
146
+ ):
147
+ """上传图片到指定 Telegram 频道。
148
+
149
+ 使用 TELE_SESSION_STRING 登录并上传图片到 TELE_ONEDAOSHARE 环境变量指定的频道。
150
+ """
151
+ load_dotenv(dotenv_path)
152
+
153
+ session_string = os.getenv("TELE_SESSION_STRING")
154
+ api_id = os.getenv("TELE_API_ID")
155
+ api_hash = os.getenv("TELE_API_HASH")
156
+ channel = os.getenv("TELE_ONEDAOSHARE")
157
+
158
+ if not session_string:
159
+ logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
160
+ raise typer.Exit(1)
161
+ if not (api_id and api_hash):
162
+ logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
163
+ raise typer.Exit(1)
164
+ if not channel:
165
+ logger.error("TELE_ONEDAOSHARE 未找到: %s", dotenv_path)
166
+ raise typer.Exit(1)
167
+ if not os.path.isfile(image_path):
168
+ logger.error("图片文件不存在: %s", image_path)
169
+ raise typer.Exit(1)
170
+
171
+ import asyncio
172
+ from typing import Awaitable, cast
173
+
174
+ from telethon import TelegramClient
175
+ from telethon.sessions import StringSession
176
+
177
+ async def _upload():
178
+ client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
179
+ try:
180
+ await client.connect()
181
+ if not await client.is_user_authorized():
182
+ logger.error("session 无效或已失效,请先运行 login 命令重新生成")
183
+ raise typer.Exit(1)
184
+
185
+ # 规范化频道 ID:如果是纯数字,自动加 -100 前缀
186
+ target = channel.strip()
187
+ if target.isdigit():
188
+ target = f"-100{target}"
189
+ logger.info("频道 ID 已自动规范化为: %s", target)
190
+
191
+ target_id = int(target) if target.lstrip('-').isdigit() else None
192
+
193
+ # 方法1:先尝试直接通过 ID 在 dialogs 缓存中查找(更可靠)
194
+ logger.info("正在查找频道: %s", target)
195
+ found_dialog = None
196
+ async for dialog in client.iter_dialogs():
197
+ if dialog.id == target_id:
198
+ found_dialog = dialog
199
+ break
200
+
201
+ if found_dialog:
202
+ logger.info("成功找到频道: %s (ID: %s)", target, found_dialog.id)
203
+ # 使用 dialog.entity 作为目标,避免再次解析
204
+ target = found_dialog.entity
205
+ else:
206
+ # 方法2:退回到 get_entity(支持 @username 等)
207
+ logger.info("缓存中未找到,尝试通过 get_entity 解析: %s", target)
208
+ try:
209
+ entity = await client.get_entity(target)
210
+ if isinstance(entity, list):
211
+ entity = entity[0]
212
+ logger.info("成功解析: %s (ID: %s)", target, getattr(entity, 'id', 'N/A'))
213
+ target = entity
214
+ except ValueError as e:
215
+ logger.error(
216
+ "无法访问频道 %s。请检查:\n"
217
+ "1. ID 是否正确(纯数字需要带 -100 前缀)\n"
218
+ "2. 当前登录的账号是否为该频道成员\n"
219
+ "3. 如果是私有名片,请使用频道的 @username\n"
220
+ "错误详情: %s", target, e
221
+ )
222
+ raise typer.Exit(1)
223
+
224
+ # 发送文件到频道,force_document=False 表示作为图片发送(Telegram 会自动压缩)
225
+ # 如果想原样发送文件,设置 force_document=True
226
+ result = await client.send_file(
227
+ target,
228
+ image_path,
229
+ caption="", # 可以自定义描述
230
+ force_document=False,
231
+ )
232
+
233
+ # send_file 可能返回 Message 或 List[Message]
234
+ message = result[0] if isinstance(result, list) else result
235
+
236
+ # 下载媒体文件到本地临时目录
237
+ download_link = None
238
+ try:
239
+ if hasattr(message, 'media') and message.media:
240
+ import tempfile
241
+ temp_dir = tempfile.gettempdir()
242
+ local_path = await client.download_media(
243
+ message,
244
+ file=temp_dir
245
+ )
246
+ if local_path:
247
+ download_link = local_path
248
+ except Exception as e:
249
+ logger.debug("下载文件失败: %s", e)
250
+
251
+ return message, download_link
252
+ finally:
253
+ await cast(Awaitable[None], client.disconnect())
254
+
255
+ try:
256
+ message, download_link = asyncio.run(_upload())
257
+
258
+ # 打印详细的上传响应信息
259
+ typer.secho("\n📤 上传成功!", fg=typer.colors.GREEN, bold=True)
260
+ typer.secho("-" * 50, fg=typer.colors.CYAN)
261
+ typer.secho(f" 消息 ID: {message.id}", fg=typer.colors.WHITE)
262
+ typer.secho(f" 频道: {channel}", fg=typer.colors.WHITE)
263
+
264
+ # 尝试获取文件信息
265
+ if hasattr(message, 'media') and message.media:
266
+ media = message.media
267
+ # 递归打印完整结构(类似 JSON 格式)
268
+ typer.secho(f" Media 类型: {type(media).__name__}", fg=typer.colors.YELLOW)
269
+
270
+ # 打印下载的本地文件路径(telesco.pe 公网链接 telethon 无法直接获取)
271
+ if download_link:
272
+ typer.secho(f"\n � 本地下载路径:", fg=typer.colors.MAGENTA, bold=True)
273
+ typer.secho(f" {download_link}", fg=typer.colors.MAGENTA)
274
+
275
+ # 打印消息链接(如果可能)
276
+ if hasattr(message, 'chat') and message.chat:
277
+ chat_id = getattr(message.chat, 'id', None)
278
+ if chat_id:
279
+ typer.secho(f" 聊天 ID: {chat_id}", fg=typer.colors.WHITE)
280
+
281
+ typer.secho("-" * 50, fg=typer.colors.CYAN)
282
+ except typer.Exit:
283
+ raise # 重新抛出 typer.Exit 以保持退出码
284
+ except Exception as e:
285
+ logger.exception("上传图片时发生异常: %s", e)
286
+ raise typer.Exit(1)
287
+
288
+
289
+ @app.command()
290
+ def list_channels(
291
+ dotenv_path: str = typer.Option(
292
+ help="dotenv file path", default="d:/.env"
293
+ ),
294
+ ):
295
+ """列出当前账号加入的所有频道及其 ID。
296
+
297
+ 方便获取正确的频道 ID 填入 TELE_ONE_DAO_SHARE。
298
+ """
299
+ load_dotenv(dotenv_path)
300
+
301
+ session_string = os.getenv("TELE_SESSION_STRING")
302
+ api_id = os.getenv("TELE_API_ID")
303
+ api_hash = os.getenv("TELE_API_HASH")
304
+
305
+ if not session_string:
306
+ logger.error("TELE_SESSION_STRING 未找到: %s", dotenv_path)
307
+ raise typer.Exit(1)
308
+ if not (api_id and api_hash):
309
+ logger.error("TELE_API_ID / TELE_API_HASH 未找到: %s", dotenv_path)
310
+ raise typer.Exit(1)
311
+
312
+ import asyncio
313
+ from typing import Awaitable, cast
314
+
315
+ from telethon import TelegramClient
316
+ from telethon.sessions import StringSession
317
+
318
+ async def _list():
319
+ client = TelegramClient(StringSession(session_string), int(api_id), api_hash)
320
+ try:
321
+ await client.connect()
322
+ if not await client.is_user_authorized():
323
+ logger.error("session 无效或已失效,请先运行 login 命令重新生成")
324
+ raise typer.Exit(1)
325
+
326
+ channels = []
327
+ async for dialog in client.iter_dialogs():
328
+ if dialog.is_channel:
329
+ channels.append(dialog)
330
+ return channels
331
+ finally:
332
+ await cast(Awaitable[None], client.disconnect())
333
+
334
+ try:
335
+ channels = asyncio.run(_list())
336
+ if not channels:
337
+ typer.secho("你还没有加入任何频道", fg=typer.colors.YELLOW)
338
+ return
339
+
340
+ typer.secho(f"\n你加入的 {len(channels)} 个频道:\n", fg=typer.colors.GREEN)
341
+ typer.secho(f"{'频道名称':<40} {'ID':<25}", fg=typer.colors.CYAN)
342
+ typer.secho("-" * 65, fg=typer.colors.CYAN)
343
+ for ch in channels:
344
+ name = ch.name or "(无名称)"
345
+ typer.secho(f"{name:<40} {ch.id:<25}")
346
+ typer.secho("\n提示:复制 ID(如 -100xxxxxxxx)到 .env 的 TELE_ONEDAOSHARE", fg=typer.colors.YELLOW)
347
+ except typer.Exit:
348
+ raise
349
+ except Exception as e:
350
+ logger.exception("列出频道时发生异常: %s", e)
351
+ raise typer.Exit(1)
352
+
353
+
354
+ if __name__ == "__main__":
355
+ logger.info("main")
@@ -4,7 +4,7 @@ import requests
4
4
  from sqlalchemy import Engine
5
5
 
6
6
  from utils import logger
7
- from utils.mysql import mysql_to_csv, mysql_to_redis_and_csv
7
+ from utils.mysql import mysql_to_csv, mysql_to_redis
8
8
 
9
9
 
10
10
  def tickers(url: str, symbols: list, proxy: str | None = None):
@@ -47,15 +47,14 @@ def spot_tickers(symbols: list, proxy: str | None = None):
47
47
  tickers(url, symbols, proxy)
48
48
 
49
49
 
50
- async def bitget_sf_open(engine: Engine, csv_fp: str):
50
+ async def bitget_sf_open(engine: Engine):
51
51
  query = "select * from bitget_sf where spot_open_usdt is not null and futures_open_usdt is not null and pnl is null and up_status = 0 and deleted_at is null;"
52
52
  key_prefix = "bitget_sf"
53
53
  table = "bitget_sf"
54
54
 
55
- row_count = await mysql_to_redis_and_csv(
55
+ row_count = await mysql_to_redis(
56
56
  engine,
57
57
  key_prefix,
58
- csv_fp,
59
58
  table,
60
59
  query,
61
60
  update_status=1,
@@ -76,15 +75,14 @@ async def bitget_sf_open(engine: Engine, csv_fp: str):
76
75
  logger.info(f"🚀 bitget SF open count:({row_count})")
77
76
 
78
77
 
79
- async def bitget_ff_open(engine: Engine, csv_fp: str):
78
+ async def bitget_ff_open(engine: Engine):
80
79
  key_prefix = "bitget_ff"
81
80
  table = "bitget_ff"
82
81
  query = f"select * from {table} where long_open_usdt is not null and short_open_usdt is not null and pnl is null and up_status = 0 and deleted_at is null;"
83
82
 
84
- row_count = await mysql_to_redis_and_csv(
83
+ row_count = await mysql_to_redis(
85
84
  engine,
86
85
  key_prefix,
87
- csv_fp,
88
86
  table,
89
87
  query,
90
88
  update_status=1,
@@ -105,15 +103,14 @@ async def bitget_ff_open(engine: Engine, csv_fp: str):
105
103
  logger.info(f"💰 bitget FF open count:({row_count})")
106
104
 
107
105
 
108
- async def bitget_sf_close(engine: Engine, csv_path: str):
106
+ async def bitget_sf_close(engine: Engine):
109
107
  query = "select * from bitget_sf where pnl is not null and up_status in (0,1);"
110
108
  key_prefix = "bitget_sf"
111
109
  table = "bitget_sf"
112
110
 
113
- row_count = await mysql_to_redis_and_csv(
111
+ row_count = await mysql_to_redis(
114
112
  engine,
115
113
  key_prefix,
116
- csv_path,
117
114
  table,
118
115
  query,
119
116
  update_status=2,
@@ -134,15 +131,14 @@ async def bitget_sf_close(engine: Engine, csv_path: str):
134
131
  logger.info(f"🚀 bitget SF close count:({row_count})")
135
132
 
136
133
 
137
- async def bitget_ff_pending(engine: Engine, csv_path: str):
134
+ async def bitget_ff_pending(engine: Engine):
138
135
  key_prefix = "bitget_ff"
139
136
  table = "bitget_ff"
140
137
  query = f"select * from {table} where pnl is null and (long_close_at is not null or short_close_at is not null) and up_status in (1);"
141
138
 
142
- row_count = await mysql_to_redis_and_csv(
139
+ row_count = await mysql_to_redis(
143
140
  engine,
144
141
  key_prefix,
145
- csv_path,
146
142
  table,
147
143
  query,
148
144
  update_status=2,
@@ -163,15 +159,14 @@ async def bitget_ff_pending(engine: Engine, csv_path: str):
163
159
  logger.info(f"💰 bitget FF pending count:({row_count})")
164
160
 
165
161
 
166
- async def bitget_ff_close(engine: Engine, csv_path: str):
162
+ async def bitget_ff_close(engine: Engine):
167
163
  key_prefix = "bitget_ff"
168
164
  table = "bitget_ff"
169
165
  query = f"select * from {table} where pnl is not null and up_status in (0,1,2);"
170
166
 
171
- row_count = await mysql_to_redis_and_csv(
167
+ row_count = await mysql_to_redis(
172
168
  engine,
173
169
  key_prefix,
174
- csv_path,
175
170
  table,
176
171
  query,
177
172
  update_status=3,
@@ -192,15 +187,14 @@ async def bitget_ff_close(engine: Engine, csv_path: str):
192
187
  logger.info(f"💰 bitget FF close count:({row_count})")
193
188
 
194
189
 
195
- async def grid_open(engine: Engine, csv_path: str):
190
+ async def grid_open(engine: Engine):
196
191
  query = "select * from bitget where ((cost is not null or benefit is not null) and profit is null) and up_status = 0 and order_id is not null and deleted_at is null;"
197
192
  key_prefix = "bitget_grid"
198
193
  table = "bitget"
199
194
 
200
- row_count = await mysql_to_redis_and_csv(
195
+ row_count = await mysql_to_redis(
201
196
  engine,
202
197
  key_prefix,
203
- csv_path,
204
198
  table,
205
199
  query,
206
200
  update_status=1,
@@ -217,15 +211,14 @@ async def grid_open(engine: Engine, csv_path: str):
217
211
  logger.info(f"🧮 bitget grid open count:({row_count})")
218
212
 
219
213
 
220
- async def grid_close(engine: Engine, csv_path: str):
214
+ async def grid_close(engine: Engine):
221
215
  query = "select * from bitget where profit is not null and up_status in (0,1) and deleted_at is null;"
222
216
  key_prefix = "bitget_grid"
223
217
  table = "bitget"
224
218
 
225
- row_count = await mysql_to_redis_and_csv(
219
+ row_count = await mysql_to_redis(
226
220
  engine,
227
221
  key_prefix,
228
- csv_path,
229
222
  table,
230
223
  query,
231
224
  update_status=2,
@@ -1,18 +1,17 @@
1
1
  from sqlalchemy import Engine
2
2
 
3
3
  from utils import logger
4
- from utils.mysql import mysql_to_csv, mysql_to_redis_and_csv
4
+ from utils.mysql import mysql_to_csv, mysql_to_redis
5
5
 
6
6
 
7
- async def grid_open(engine: Engine, csv_path: str):
7
+ async def grid_open(engine: Engine):
8
8
  query = "select * from gate where ((cost is not null or benefit is not null) and profit is null) and up_status = 0 and order_id is not null and deleted_at is null;"
9
9
  key_prefix = "gate_grid"
10
10
  table = "gate"
11
11
 
12
- row_count = await mysql_to_redis_and_csv(
12
+ row_count = await mysql_to_redis(
13
13
  engine,
14
14
  key_prefix,
15
- csv_path,
16
15
  table,
17
16
  query,
18
17
  update_status=1,
@@ -29,15 +28,14 @@ async def grid_open(engine: Engine, csv_path: str):
29
28
  logger.info(f"🧮 gate open count:({row_count})")
30
29
 
31
30
 
32
- async def grid_close(engine: Engine, csv_path: str):
31
+ async def grid_close(engine: Engine):
33
32
  query = "select * from gate where profit is not null and up_status in (0,1) and deleted_at is null;"
34
33
  key_prefix = "gate_grid"
35
34
  table = "gate"
36
35
 
37
- row_count = await mysql_to_redis_and_csv(
36
+ row_count = await mysql_to_redis(
38
37
  engine,
39
38
  key_prefix,
40
- csv_path,
41
39
  table,
42
40
  query,
43
41
  update_status=2,
@@ -29,13 +29,14 @@
29
29
  "pydantic-settings>=2.14.2",
30
30
  "netifaces-plus>=0.12.5",
31
31
  "ifaddr>=0.2.0",
32
+ "telethon>=1.44.0",
32
33
  ]
33
34
  description = "My CLI"
34
35
  license = { text = "MIT" }
35
36
  name = "pymecli"
36
37
  readme = "README.md"
37
38
  requires-python = ">=3.10"
38
- version = "0.5.3"
39
+ version = "0.5.4"
39
40
 
40
41
  [project.urls]
41
42
  Homepage = "https://pypi.org/project/pymecli"
@@ -51,6 +52,7 @@
51
52
  util = "cli.util:app"
52
53
  # 临时命令
53
54
  rd = "cli.redis_csv:app"
55
+ # tele = "cli.tele:app"
54
56
 
55
57
  [tool.uv]
56
58
  [[tool.uv.index]]
@@ -70,22 +70,7 @@ def mysql_to_csv(
70
70
  # csv去重,保留最后加入的数据
71
71
  deduplicated(csv_path, d_column_names, "last", pd_dtype)
72
72
 
73
- # 根据提取的 'id' 列更新数据库中 up_status 字段
74
- if ids:
75
- # 使用 text() 构建查询时,确保 :ids 是一个列表
76
- update_query = text(
77
- f"UPDATE {table} SET up_status = :status WHERE id IN ({','.join(map(str, ids))});"
78
- )
79
- with engine.connect() as connection:
80
- with connection.begin():
81
- result = connection.execute(
82
- update_query,
83
- {"status": update_status},
84
- )
85
-
86
- return result.rowcount
87
-
88
- return 0
73
+ return _update_status(engine, table, ids, update_status)
89
74
 
90
75
 
91
76
  async def mysql_to_redis_and_csv(
@@ -185,9 +170,68 @@ async def mysql_to_redis_and_csv(
185
170
 
186
171
  logger.debug(f"𝄜 to csv: {count}")
187
172
 
188
- # 根据提取的 'id' 列更新数据库中 up_status 字段
173
+ return _update_status(engine, table, ids, update_status)
174
+
175
+
176
+ async def mysql_to_redis(
177
+ engine: Engine,
178
+ key_prefix: str,
179
+ table: str,
180
+ query: str,
181
+ update_status: int,
182
+ d_column_names: list[str],
183
+ pd_dtype: dict | None = None,
184
+ del_column_names: list[str] = ["id", "created_at", "updated_at", "deleted_at"],
185
+ ) -> int:
186
+ df = pd.read_sql(query, engine, dtype=pd_dtype)
187
+ df["open_at"] = df["open_at"].fillna(df["created_at"])
188
+ ids = df["id"].tolist()
189
+ columns_to_drop = [col for col in del_column_names if col in df.columns]
190
+ df = df.drop(columns=columns_to_drop)
191
+
192
+ datetime_cols = [
193
+ "open_at",
194
+ "close_at",
195
+ "spot_close_at",
196
+ "futures_close_at",
197
+ "long_close_at",
198
+ "short_close_at",
199
+ ]
200
+
201
+ logger.debug(df.head())
202
+ logger.debug(df.dtypes)
203
+
204
+ for col in datetime_cols:
205
+ if col in df.columns:
206
+ df[col] = dt_to_timestamp(df[col])
207
+
208
+ r = get_redis_client()
209
+ pipe = r.pipeline()
210
+ count = 0
211
+ n1, n2 = d_column_names
212
+
213
+ for _, row in df.iterrows():
214
+ idx1 = row[n1]
215
+ idx2 = row[n2]
216
+ if not idx1 or not idx2:
217
+ raise ValueError("ERR:id行无效")
218
+ id = f"{idx1}_{idx2}"
219
+ key = f"{key_prefix}:{id}"
220
+
221
+ row_dict = row.where(pd.notna(row), "").to_dict()
222
+
223
+ pipe.hset(key, mapping=row_dict)
224
+ pipe.zadd(f"by_time:{key_prefix}", {id: time.time()})
225
+ count += 1
226
+
227
+ await pipe.execute()
228
+ logger.debug(f"🧱 to redis: {count}")
229
+
230
+ return _update_status(engine, table, ids, update_status)
231
+
232
+
233
+ def _update_status(engine: Engine, table: str, ids: list, update_status: int) -> int:
189
234
  if ids:
190
- # 使用 text() 构建查询时,确保 :ids 是一个列表
191
235
  update_query = text(
192
236
  f"UPDATE {table} SET up_status = :status WHERE id IN ({','.join(map(str, ids))});"
193
237
  )
@@ -197,7 +241,5 @@ async def mysql_to_redis_and_csv(
197
241
  update_query,
198
242
  {"status": update_status},
199
243
  )
200
-
201
244
  return result.rowcount
202
-
203
245
  return 0
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