pymecli 0.5.4__tar.gz → 0.5.6__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 (48) hide show
  1. {pymecli-0.5.4 → pymecli-0.5.6}/PKG-INFO +1 -1
  2. {pymecli-0.5.4 → pymecli-0.5.6}/cli/bitget.py +4 -1
  3. {pymecli-0.5.4 → pymecli-0.5.6}/cli/fast.py +7 -11
  4. pymecli-0.5.6/cli/gate.py +20 -0
  5. pymecli-0.5.6/cli/okx.py +22 -0
  6. {pymecli-0.5.4 → pymecli-0.5.6}/cli/tele.py +6 -6
  7. {pymecli-0.5.4 → pymecli-0.5.6}/core/config.py +1 -4
  8. {pymecli-0.5.4 → pymecli-0.5.6}/core/redis_client.py +5 -14
  9. pymecli-0.5.6/crypto/okx.py +52 -0
  10. {pymecli-0.5.4 → pymecli-0.5.6}/pyproject.toml +2 -1
  11. {pymecli-0.5.4 → pymecli-0.5.6}/utils/mysql.py +66 -22
  12. pymecli-0.5.6/utils/pyredis.py +18 -0
  13. pymecli-0.5.4/cli/gate.py +0 -28
  14. pymecli-0.5.4/utils/pyredis.py +0 -36
  15. {pymecli-0.5.4 → pymecli-0.5.6}/.gitignore +0 -0
  16. {pymecli-0.5.4 → pymecli-0.5.6}/README.md +0 -0
  17. {pymecli-0.5.4 → pymecli-0.5.6}/api/__init__.py +0 -0
  18. {pymecli-0.5.4 → pymecli-0.5.6}/api/v1/__init__.py +0 -0
  19. {pymecli-0.5.4 → pymecli-0.5.6}/api/v1/clash.py +0 -0
  20. {pymecli-0.5.4 → pymecli-0.5.6}/api/v1/redis.py +0 -0
  21. {pymecli-0.5.4 → pymecli-0.5.6}/cli/__init__.py +0 -0
  22. {pymecli-0.5.4 → pymecli-0.5.6}/cli/dst.py +0 -0
  23. {pymecli-0.5.4 → pymecli-0.5.6}/cli/example.py +0 -0
  24. {pymecli-0.5.4 → pymecli-0.5.6}/cli/redis_csv.py +0 -0
  25. {pymecli-0.5.4 → pymecli-0.5.6}/cli/util.py +0 -0
  26. {pymecli-0.5.4 → pymecli-0.5.6}/core/__init__.py +0 -0
  27. {pymecli-0.5.4 → pymecli-0.5.6}/core/clash.py +0 -0
  28. {pymecli-0.5.4 → pymecli-0.5.6}/crypto/__init__.py +0 -0
  29. {pymecli-0.5.4 → pymecli-0.5.6}/crypto/bitget.py +0 -0
  30. {pymecli-0.5.4 → pymecli-0.5.6}/crypto/gate.py +0 -0
  31. {pymecli-0.5.4 → pymecli-0.5.6}/data/__init__.py +0 -0
  32. {pymecli-0.5.4 → pymecli-0.5.6}/data/dou_dict.py +0 -0
  33. {pymecli-0.5.4 → pymecli-0.5.6}/data/dou_list.py +0 -0
  34. {pymecli-0.5.4 → pymecli-0.5.6}/data/main.py +0 -0
  35. {pymecli-0.5.4 → pymecli-0.5.6}/data/template.yaml +0 -0
  36. {pymecli-0.5.4 → pymecli-0.5.6}/models/__init__.py +0 -0
  37. {pymecli-0.5.4 → pymecli-0.5.6}/models/douzero_model.py +0 -0
  38. {pymecli-0.5.4 → pymecli-0.5.6}/models/ocr_model.py +0 -0
  39. {pymecli-0.5.4 → pymecli-0.5.6}/models/response.py +0 -0
  40. {pymecli-0.5.4 → pymecli-0.5.6}/utils/__init__.py +0 -0
  41. {pymecli-0.5.4 → pymecli-0.5.6}/utils/elapsed.py +0 -0
  42. {pymecli-0.5.4 → pymecli-0.5.6}/utils/helper.py +0 -0
  43. {pymecli-0.5.4 → pymecli-0.5.6}/utils/logger.py +0 -0
  44. {pymecli-0.5.4 → pymecli-0.5.6}/utils/path.py +0 -0
  45. {pymecli-0.5.4 → pymecli-0.5.6}/utils/pd.py +0 -0
  46. {pymecli-0.5.4 → pymecli-0.5.6}/utils/sleep.py +0 -0
  47. {pymecli-0.5.4 → pymecli-0.5.6}/utils/text.py +0 -0
  48. {pymecli-0.5.4 → pymecli-0.5.6}/utils/toml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pymecli
3
- Version: 0.5.4
3
+ Version: 0.5.6
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
@@ -20,7 +20,10 @@ app = typer.Typer()
20
20
 
21
21
  @app.command()
22
22
  def sync(
23
- env_path: str = "d:/.env",
23
+ env_path: str = typer.Argument(
24
+ ".env",
25
+ help="dotenv环境变量路径",
26
+ ),
24
27
  ):
25
28
  """同步mysql中grid数据到redis"""
26
29
  engine = get_database_engine(env_path)
@@ -18,11 +18,8 @@ from models.response import SuccessResponse
18
18
  @asynccontextmanager
19
19
  async def lifespan(app: FastAPI):
20
20
  """管理应用生命周期的上下文管理器"""
21
- redis_pool = redis.ConnectionPool(
22
- host=settings.REDIS_HOST,
23
- port=settings.REDIS_PORT,
24
- db=settings.REDIS_DB,
25
- password=settings.REDIS_PASSWORD,
21
+ redis_pool = redis.ConnectionPool.from_url(
22
+ settings.REDIS_URL,
26
23
  max_connections=20, # 根据需要调整最大连接数
27
24
  decode_responses=True,
28
25
  )
@@ -152,16 +149,15 @@ def run_app(
152
149
  "-p",
153
150
  help="服务器代理,传入则通过代理转换Clash订阅,比如:socks5://127.0.0.1:7897",
154
151
  ),
155
- redis_host: str = typer.Option(
152
+ redis_url: str = typer.Option(
156
153
  None,
157
- "--redis-host",
158
- "-r",
159
- help="redis host",
154
+ "--redis-url",
155
+ help="redis url, 格式: redis://[user:password@]host[:port][/db]",
160
156
  ),
161
157
  ):
162
158
  settings.reload()
163
- if redis_host is not None:
164
- settings.REDIS_HOST = redis_host
159
+ if redis_url is not None:
160
+ settings.REDIS_URL = redis_url
165
161
 
166
162
  clash_config = ClashConfig(rule, my_rule, proxy)
167
163
  init_generator(clash_config)
@@ -0,0 +1,20 @@
1
+ import asyncio
2
+
3
+ import typer
4
+
5
+ from crypto.gate import grid_close, grid_open
6
+ from utils.mysql import get_database_engine
7
+
8
+ app = typer.Typer()
9
+
10
+ @app.command()
11
+ def rsync(
12
+ env_path: str = typer.Argument(
13
+ ".env",
14
+ help="dotenv环境变量路径",
15
+ ),
16
+ ):
17
+ """同步mysql中grid数据到redis"""
18
+ engine = get_database_engine(env_path)
19
+ asyncio.run(grid_open(engine))
20
+ asyncio.run(grid_close(engine))
@@ -0,0 +1,22 @@
1
+ import asyncio
2
+
3
+ import typer
4
+
5
+ from crypto.okx import grid_close, grid_open
6
+ from utils.mysql import get_database_engine
7
+
8
+ app = typer.Typer()
9
+
10
+
11
+ @app.command()
12
+ def sync(
13
+ env_path: str = typer.Argument(
14
+ ".env",
15
+ help="dotenv环境变量路径",
16
+ ),
17
+ ):
18
+ """同步mysql中grid数据到redis"""
19
+ engine = get_database_engine(env_path)
20
+
21
+ asyncio.run(grid_open(engine))
22
+ asyncio.run(grid_close(engine))
@@ -12,10 +12,10 @@ app = typer.Typer()
12
12
  @app.command()
13
13
  def info(
14
14
  dotenv_path: str = typer.Argument(
15
- help="dotenv file contains TELE_SEESION_STRING", default="d:/.env"
15
+ help="dotenv file contains TELE_SEESION_STRING", default=".env"
16
16
  ),
17
17
  ):
18
- load_dotenv("d:/.env")
18
+ load_dotenv(dotenv_path)
19
19
  logger.info(os.getenv("TELE_SESSION_STRING"))
20
20
 
21
21
 
@@ -23,7 +23,7 @@ def info(
23
23
  def check(
24
24
  dotenv_path: str = typer.Argument(
25
25
  help="dotenv file contains TELE_SESSION_STRING / TELE_API_ID / TELE_API_HASH",
26
- default="d:/.env",
26
+ default=".env",
27
27
  ),
28
28
  ):
29
29
  """验证 TELE_SESSION_STRING 是否有效。
@@ -87,7 +87,7 @@ def check(
87
87
  def login(
88
88
  dotenv_path: str = typer.Argument(
89
89
  help="dotenv file containing TELE_API_ID / TELE_API_HASH",
90
- default="d:/.env",
90
+ default=".env",
91
91
  ),
92
92
  ):
93
93
  """交互式登录并打印 TELE_SESSION_STRING。
@@ -141,7 +141,7 @@ def upload(
141
141
  help="要上传的图片文件路径",
142
142
  ),
143
143
  dotenv_path: str = typer.Option(
144
- help="dotenv file path", default="d:/.env"
144
+ help="dotenv file path", default=".env"
145
145
  ),
146
146
  ):
147
147
  """上传图片到指定 Telegram 频道。
@@ -289,7 +289,7 @@ def upload(
289
289
  @app.command()
290
290
  def list_channels(
291
291
  dotenv_path: str = typer.Option(
292
- help="dotenv file path", default="d:/.env"
292
+ help="dotenv file path", default=".env"
293
293
  ),
294
294
  ):
295
295
  """列出当前账号加入的所有频道及其 ID。
@@ -13,10 +13,7 @@ class Settings(BaseSettings):
13
13
  API_V1_STR: str = "/api/v1"
14
14
  PROJECT_NAME: str = metadata["Name"]
15
15
 
16
- REDIS_HOST: str = "192.168.123.7"
17
- REDIS_PORT: int = 6379
18
- REDIS_DB: int = 0
19
- REDIS_PASSWORD: str = ""
16
+ REDIS_URL: str = "redis://192.168.123.7:6379/0"
20
17
 
21
18
  PROJECT_DESCRIPTION: str = (
22
19
  f"{metadata['Summary']}, FastAPI提供: clash订阅转换、redis API"
@@ -8,26 +8,17 @@ import redis
8
8
  class RedisClient:
9
9
  def __init__(
10
10
  self,
11
- host=os.getenv("REDIS_HOST", "192.168.123.7"),
12
- port=int(os.getenv("REDIS_PORT", 6379)),
13
- db=int(os.getenv("REDIS_DB", 0)),
14
- password=os.getenv("REDIS_PASSWORD"),
15
- decode_responses=True,
11
+ url: str = os.getenv("REDIS_URL", "redis://192.168.123.7:6379/0"),
12
+ decode_responses: bool = True,
16
13
  ):
17
14
  """
18
15
  初始化Redis客户端
19
- :param host: Redis服务器地址
20
- :param port: Redis服务器端口
21
- :param db: 数据库编号
22
- :param password: 密码(如果需要)
16
+ :param url: Redis连接URL, 格式 redis://[user:password@]host[:port][/db]
23
17
  :param decode_responses: 是否自动解码响应(将字节转换为字符串)
24
18
  """
25
19
 
26
- self.client = redis.Redis(
27
- host=host,
28
- port=port,
29
- db=db,
30
- password=password,
20
+ self.client = redis.Redis.from_url(
21
+ url,
31
22
  decode_responses=decode_responses,
32
23
  socket_connect_timeout=5,
33
24
  socket_timeout=5,
@@ -0,0 +1,52 @@
1
+ from sqlalchemy import Engine
2
+
3
+ from utils import logger
4
+ from utils.mysql import mysql_to_redis
5
+
6
+
7
+ async def grid_open(engine: Engine):
8
+ query = "select * from okx_spot 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
+ key_prefix = "okx_grid"
10
+ table = "okx_spot"
11
+
12
+ row_count = await mysql_to_redis(
13
+ engine,
14
+ key_prefix,
15
+ table,
16
+ query,
17
+ update_status=1,
18
+ d_column_names=["order_id", "client_order_id"],
19
+ pd_dtype={
20
+ "order_id": str,
21
+ "fx_order_id": str,
22
+ "created_at": "datetime64[ns]",
23
+ "open_at": "datetime64[ns]",
24
+ "close_at": "datetime64[ns]",
25
+ },
26
+ )
27
+
28
+ logger.info(f"🧮 okx grid open count:({row_count})")
29
+
30
+
31
+ async def grid_close(engine: Engine):
32
+ query = "select * from okx_spot where profit is not null and up_status in (0,1) and deleted_at is null;"
33
+ key_prefix = "okx_grid"
34
+ table = "okx_spot"
35
+
36
+ row_count = await mysql_to_redis(
37
+ engine,
38
+ key_prefix,
39
+ table,
40
+ query,
41
+ update_status=2,
42
+ d_column_names=["order_id", "client_order_id"],
43
+ pd_dtype={
44
+ "order_id": str,
45
+ "fx_order_id": str,
46
+ "created_at": "datetime64[ns]",
47
+ "open_at": "datetime64[ns]",
48
+ "close_at": "datetime64[ns]",
49
+ },
50
+ )
51
+
52
+ logger.info(f"🧮 okx grid close count:({row_count})")
@@ -36,7 +36,7 @@
36
36
  name = "pymecli"
37
37
  readme = "README.md"
38
38
  requires-python = ">=3.10"
39
- version = "0.5.4"
39
+ version = "0.5.6"
40
40
 
41
41
  [project.urls]
42
42
  Homepage = "https://pypi.org/project/pymecli"
@@ -49,6 +49,7 @@
49
49
  example = "cli.example:app"
50
50
  fast = "cli.fast:typer_app"
51
51
  gate = "cli.gate:app"
52
+ okx = "cli.okx:app"
52
53
  util = "cli.util:app"
53
54
  # 临时命令
54
55
  rd = "cli.redis_csv:app"
@@ -1,5 +1,6 @@
1
1
  import os
2
- import time
2
+ import re
3
+ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
3
4
 
4
5
  import pandas as pd
5
6
  from dotenv import load_dotenv
@@ -9,25 +10,62 @@ from utils import logger
9
10
  from utils.pd import deduplicated, dt_to_timestamp
10
11
  from utils.pyredis import get_redis_client
11
12
 
12
-
13
- def get_database_engine(env_path: str) -> Engine:
13
+ # Go go-sql-driver/mysql 专有参数,Python pymysql / SQLAlchemy 不支持
14
+ _GO_DSN_PARAMS = {
15
+ "parseTime", "loc", "timeout", "readTimeout", "writeTimeout",
16
+ "allowNativePasswords", "tls", "serverCert", "clientCert",
17
+ "clientKey", "multiStatements", "columnsWithAlias", "interpolateParams"}
18
+
19
+
20
+ def _strip_go_params(query: str) -> str:
21
+ """从 query string 中移除 Go DSN 专有参数"""
22
+ if not query:
23
+ return query
24
+ kept = [(k, v) for k, v in parse_qsl(query) if k not in _GO_DSN_PARAMS]
25
+ return urlencode(kept)
26
+
27
+
28
+ def _to_sqlalchemy_url(dsn: str) -> str:
29
+ """
30
+ 将 Go/Node 通用 MySQL DSN 转为 SQLAlchemy URL。
31
+
32
+ 输入格式(Go go-sql-driver/mysql DSN 或 Node mysql URI):
33
+ root:password@192.168.123.7:3366/bot_tx?charset=utf8mb4&parseTime=True&loc=Local
34
+ root:password@tcp(192.168.123.7:3366)/bot_tx?charset=utf8mb4
35
+ mysql://root:password@192.168.123.7:3306/mydb
36
+
37
+ 输出格式(SQLAlchemy):
38
+ mysql+pymysql://root:password@192.168.123.7:3366/bot_tx?charset=utf8mb4
39
+ """
40
+ # 如果已经是完整的 scheme:// URL,直接处理
41
+ if "://" in dsn:
42
+ parts = urlsplit(dsn)
43
+ # mysql:// → mysql+pymysql://
44
+ scheme = parts.scheme
45
+ if scheme == "mysql":
46
+ scheme = "mysql+pymysql"
47
+ return urlunsplit((scheme, parts.netloc, parts.path, _strip_go_params(parts.query), ""))
48
+
49
+ # Go DSN 格式:[user[:password]@][tcp(]host[:port][)]/db[?params]
50
+ # 去掉 tcp() 包装
51
+ dsn = re.sub(r"tcp\(([^)]+)\)", r"\1", dsn)
52
+
53
+ # user:pass@host:port/db?query → mysql+pymysql://user:pass@host:port/db?query
54
+ url = f"mysql+pymysql://{dsn}"
55
+ parts = urlsplit(url)
56
+ return urlunsplit((parts.scheme, parts.netloc, parts.path, _strip_go_params(parts.query), ""))
57
+
58
+
59
+ def get_database_engine(env_path: str = ".env") -> Engine:
14
60
  """创建数据库引擎"""
15
61
  load_dotenv(env_path)
16
- host = os.getenv("MYSQL_HOST")
17
- port = os.getenv("MYSQL_PORT")
18
- database = os.getenv("MYSQL_DATABASE")
19
- user = os.getenv("MYSQL_USER")
20
- password = os.getenv("MYSQL_PASSWORD")
21
-
22
- engine = create_engine(
23
- f"mysql+pymysql://{user}:{password}@{host}:{port}/{database}",
24
- connect_args={
25
- # 对于MySQL,可以在连接参数中指定时区
26
- # "init_command": "SET time_zone='+00:00'", # UTC时间
27
- # 或者使用本地时区,例如:
28
- # "init_command": "SET time_zone='+08:00'", # 中国标准时间
29
- },
62
+ dsn = os.getenv(
63
+ "MYSQL_URL",
64
+ "root:@127.0.0.1:3306/?charset=utf8mb4",
30
65
  )
66
+ url = _to_sqlalchemy_url(dsn)
67
+
68
+ engine = create_engine(url, connect_args={})
31
69
 
32
70
  try:
33
71
  with engine.connect() as connection:
@@ -124,13 +162,16 @@ async def mysql_to_redis_and_csv(
124
162
  id = f"{idx1}_{idx2}"
125
163
  key = f"{key_prefix}:{id}"
126
164
 
127
- # 转换行数据为字典(处理 NaN 为 None 或空字符串)
165
+ # 转换行数据为字典(处理 NaN 为 None 或空字符串)
128
166
  row_dict = row.where(pd.notna(row), "").to_dict()
129
167
 
130
- # 1. 写入完整数据到 Hash(自动覆盖)
168
+ # 1. 写入完整数据到 Hash(自动覆盖)
131
169
  pipe.hset(key, mapping=row_dict)
132
- # 2. 写入 ZSet 索引:score = Unix 时间戳
133
- pipe.zadd(f"by_time:{key_prefix}", {id: time.time()})
170
+ # 2. 写入 ZSet 索引:score = 开仓时间戳(空值用 0 兜底)
171
+ score = row["open_at"]
172
+ if score is None or pd.isna(score):
173
+ score = 0
174
+ pipe.zadd(f"by_time:{key_prefix}", {id: score})
134
175
  count += 1
135
176
 
136
177
  await pipe.execute()
@@ -221,7 +262,10 @@ async def mysql_to_redis(
221
262
  row_dict = row.where(pd.notna(row), "").to_dict()
222
263
 
223
264
  pipe.hset(key, mapping=row_dict)
224
- pipe.zadd(f"by_time:{key_prefix}", {id: time.time()})
265
+ score = row["open_at"]
266
+ if score is None or pd.isna(score):
267
+ score = 0
268
+ pipe.zadd(f"by_time:{key_prefix}", {id: score})
225
269
  count += 1
226
270
 
227
271
  await pipe.execute()
@@ -0,0 +1,18 @@
1
+ import os
2
+
3
+ import redis as redis_sync
4
+ import redis.asyncio as redis
5
+
6
+
7
+ def get_redis_client(
8
+ url: str = os.getenv("REDIS_URL", "redis://192.168.123.7:6379/0"),
9
+ decode_responses: bool = True,
10
+ ) -> redis.Redis:
11
+ return redis.Redis.from_url(url, decode_responses=decode_responses)
12
+
13
+
14
+ def get_redis_client_sync(
15
+ url: str = os.getenv("REDIS_URL", "redis://192.168.123.7:6379/0"),
16
+ decode_responses: bool = True,
17
+ ) -> redis_sync.Redis:
18
+ return redis_sync.Redis.from_url(url, decode_responses=decode_responses)
pymecli-0.5.4/cli/gate.py DELETED
@@ -1,28 +0,0 @@
1
- import asyncio
2
-
3
- import typer
4
-
5
- from crypto.gate import grid_close, grid_open
6
- from utils.mysql import get_database_engine
7
-
8
- app = typer.Typer()
9
-
10
-
11
- # @app.command()
12
- # def sync(
13
- # env_path: str = "d:/.env", csv_path: str = "d:/github/meme2046/data/gate_0.csv"
14
- # ):
15
- # """同步mysql中grid数据到csv文件"""
16
- # engine = get_database_engine(env_path)
17
- # gate_open(engine, csv_path)
18
- # gate_close(engine, csv_path)
19
-
20
-
21
- @app.command()
22
- def rsync(
23
- env_path: str = "d:/.env",
24
- ):
25
- """同步mysql中grid数据到redis"""
26
- engine = get_database_engine(env_path)
27
- asyncio.run(grid_open(engine))
28
- asyncio.run(grid_close(engine))
@@ -1,36 +0,0 @@
1
- import os
2
-
3
- import redis as redis_sync
4
- import redis.asyncio as redis
5
-
6
-
7
- def get_redis_client(
8
- host=os.getenv("REDIS_HOST", "192.168.123.7"),
9
- port=int(os.getenv("REDIS_PORT", 6379)),
10
- db=int(os.getenv("REDIS_DB", 0)),
11
- password=os.getenv("REDIS_PASSWORD"),
12
- decode_responses=True,
13
- ) -> redis.Redis:
14
- return redis.Redis(
15
- host=host,
16
- port=port,
17
- db=db,
18
- password=password,
19
- decode_responses=decode_responses,
20
- )
21
-
22
-
23
- def get_redis_client_sync(
24
- host=os.getenv("REDIS_HOST", "192.168.123.7"),
25
- port=int(os.getenv("REDIS_PORT", 6379)),
26
- db=int(os.getenv("REDIS_DB", 0)),
27
- password=os.getenv("REDIS_PASSWORD"),
28
- decode_responses=True,
29
- ) -> redis_sync.Redis:
30
- return redis_sync.Redis(
31
- host=host,
32
- port=port,
33
- db=db,
34
- password=password,
35
- decode_responses=decode_responses,
36
- )
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