pymecli 0.3.1__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 (42) hide show
  1. pymecli-0.3.1/.gitignore +17 -0
  2. pymecli-0.3.1/PKG-INFO +45 -0
  3. pymecli-0.3.1/README.md +15 -0
  4. pymecli-0.3.1/api/__init__.py +0 -0
  5. pymecli-0.3.1/api/v1/__init__.py +9 -0
  6. pymecli-0.3.1/api/v1/clash.py +34 -0
  7. pymecli-0.3.1/api/v1/redis.py +98 -0
  8. pymecli-0.3.1/cli/__init__.py +0 -0
  9. pymecli-0.3.1/cli/bitget.py +59 -0
  10. pymecli-0.3.1/cli/example.py +32 -0
  11. pymecli-0.3.1/cli/fast.py +167 -0
  12. pymecli-0.3.1/cli/gate.py +27 -0
  13. pymecli-0.3.1/cli/redis_csv.py +243 -0
  14. pymecli-0.3.1/cli/util.py +144 -0
  15. pymecli-0.3.1/core/__init__.py +0 -0
  16. pymecli-0.3.1/core/clash.py +267 -0
  17. pymecli-0.3.1/core/config.py +23 -0
  18. pymecli-0.3.1/core/redis_client.py +137 -0
  19. pymecli-0.3.1/crypto/__init__.py +0 -0
  20. pymecli-0.3.1/crypto/bitget.py +229 -0
  21. pymecli-0.3.1/crypto/gate.py +81 -0
  22. pymecli-0.3.1/data/__init__.py +0 -0
  23. pymecli-0.3.1/data/dou_dict.py +172 -0
  24. pymecli-0.3.1/data/dou_list.py +93 -0
  25. pymecli-0.3.1/data/main.py +5 -0
  26. pymecli-0.3.1/data/template.yaml +150 -0
  27. pymecli-0.3.1/models/__init__.py +0 -0
  28. pymecli-0.3.1/models/douzero_model.py +45 -0
  29. pymecli-0.3.1/models/ocr_model.py +57 -0
  30. pymecli-0.3.1/models/response.py +37 -0
  31. pymecli-0.3.1/pyproject.toml +73 -0
  32. pymecli-0.3.1/utils/__init__.py +7 -0
  33. pymecli-0.3.1/utils/elapsed.py +16 -0
  34. pymecli-0.3.1/utils/helper.py +9 -0
  35. pymecli-0.3.1/utils/logger.py +28 -0
  36. pymecli-0.3.1/utils/mysql.py +190 -0
  37. pymecli-0.3.1/utils/path.py +18 -0
  38. pymecli-0.3.1/utils/pd.py +46 -0
  39. pymecli-0.3.1/utils/pyredis.py +19 -0
  40. pymecli-0.3.1/utils/sleep.py +16 -0
  41. pymecli-0.3.1/utils/text.py +33 -0
  42. pymecli-0.3.1/utils/toml.py +6 -0
@@ -0,0 +1,17 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+
12
+ tmp/
13
+ node_modules/
14
+ pnpm-lock.yaml
15
+ tmp.json
16
+ tmp.yaml
17
+ cloudflare.ini
pymecli-0.3.1/PKG-INFO ADDED
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.4
2
+ Name: pymecli
3
+ Version: 0.3.1
4
+ Summary: My CLI
5
+ Project-URL: Homepage, https://pypi.org/project/pymecli
6
+ Project-URL: Repository, https://github.com/meme2046/pymecli
7
+ License: MIT
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: arrow>=1.4.0
13
+ Requires-Dist: babel>=2.17.0
14
+ Requires-Dist: cowsay>=6.1
15
+ Requires-Dist: dotenv>=0.9.9
16
+ Requires-Dist: fastapi>=0.127.0
17
+ Requires-Dist: numpy<2.0.0
18
+ Requires-Dist: pandas>=2.3.3
19
+ Requires-Dist: pymysql>=1.1.2
20
+ Requires-Dist: pytz>=2025.2
21
+ Requires-Dist: pyyaml>=6.0.3
22
+ Requires-Dist: redis[hiredis]>=7.1.0
23
+ Requires-Dist: requests[socks]>=2.32.5
24
+ Requires-Dist: sqlalchemy>=2.0.45
25
+ Requires-Dist: tabulate>=0.9.0
26
+ Requires-Dist: toml>=0.10.2
27
+ Requires-Dist: typer>=0.20.1
28
+ Requires-Dist: uvicorn>=0.40.0
29
+ Description-Content-Type: text/markdown
30
+
31
+ # typer example
32
+
33
+ ```shell
34
+ uv run example hello
35
+ uv run example hello Xiaoming
36
+ uv run example goodbye
37
+ uv run example goodbye Xiaoming
38
+ uv run example goodbye Xiaoming -f
39
+ ```
40
+
41
+ # fastapi server
42
+
43
+ ```shell
44
+ uv run fast --port 8877
45
+ ```
@@ -0,0 +1,15 @@
1
+ # typer example
2
+
3
+ ```shell
4
+ uv run example hello
5
+ uv run example hello Xiaoming
6
+ uv run example goodbye
7
+ uv run example goodbye Xiaoming
8
+ uv run example goodbye Xiaoming -f
9
+ ```
10
+
11
+ # fastapi server
12
+
13
+ ```shell
14
+ uv run fast --port 8877
15
+ ```
File without changes
@@ -0,0 +1,9 @@
1
+ from fastapi import APIRouter
2
+
3
+ from api.v1.clash import router as clash_router
4
+ from api.v1.redis import router as redis_router
5
+
6
+ api_router = APIRouter()
7
+
8
+ api_router.include_router(clash_router, prefix="/clash")
9
+ api_router.include_router(redis_router, prefix="/redis")
@@ -0,0 +1,34 @@
1
+ import yaml
2
+ from fastapi import APIRouter, Query, Response
3
+
4
+ from core.clash import ClashYamlGenerator, get_generator_dependency
5
+
6
+ router = APIRouter()
7
+
8
+
9
+ @router.get(
10
+ "/sub",
11
+ summary="转换订阅(可以转换多个订阅)",
12
+ description="根据提供的URL、User-Agent和名称获取并处理订阅信息,返回YAML格式的Clash配置",
13
+ response_description="返回YAML格式的Clash配置文件",
14
+ )
15
+ async def sub(
16
+ urls: str = Query(..., description="订阅URL,逗号分隔"),
17
+ agents: str = Query(
18
+ "clash-verge/v2.4.3",
19
+ description="User-Agent,逗号分隔(根据客户段选择)",
20
+ ),
21
+ names: str = Query(
22
+ "订阅1", description="订阅名称,逗号分隔(可选,在客户端中的显示名)"
23
+ ),
24
+ generator: ClashYamlGenerator = get_generator_dependency(),
25
+ ):
26
+ # 将JSON字符串解析为Python对象
27
+ sub_list = generator.query2sub(urls, agents, names)
28
+ yaml_content, userinfo = generator.gen(sub_list)
29
+ yaml_string = yaml.dump(yaml_content, allow_unicode=True, default_flow_style=False)
30
+ return Response(
31
+ headers={"Subscription-Userinfo": userinfo},
32
+ content=yaml_string,
33
+ media_type="text/yaml",
34
+ )
@@ -0,0 +1,98 @@
1
+ import json
2
+
3
+ from fastapi import APIRouter, HTTPException, Query, Request
4
+ from fastapi.responses import PlainTextResponse
5
+
6
+ from models.response import SuccessResponse
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ @router.get(
12
+ "/json",
13
+ summary="根据redis.key获取数据",
14
+ response_description="返回json数据",
15
+ )
16
+ async def json_by_key(
17
+ request: Request,
18
+ key: str = Query(..., description="redis.key"),
19
+ ):
20
+ r = request.app.state.redis_client
21
+ key_type = await r.type(key)
22
+
23
+ if key_type == "string": # 或 'string' 取决于客户端
24
+ value = await r.get(key)
25
+ value = json.loads(value)
26
+ elif key_type == "hash":
27
+ value = await r.hgetall(key)
28
+ elif key_type == "list":
29
+ value = await r.lrange(key, 0, -1)
30
+ elif key_type == "set":
31
+ value = await r.smembers(key)
32
+ elif key_type == "zset":
33
+ value = await r.zrange(key, 0, -1, withscores=True)
34
+ else:
35
+ raise HTTPException(status_code=404, detail=f"Key '{key}' not found in Redis")
36
+
37
+ return SuccessResponse(data=value)
38
+
39
+
40
+ @router.get(
41
+ "/plaintext",
42
+ summary="根据redis.key获取数据",
43
+ response_description="返回plaintext数据",
44
+ # response_class=PlainTextResponse,
45
+ )
46
+ async def plaintext_by_key(
47
+ request: Request,
48
+ key: str = Query(..., description="redis.key"),
49
+ ):
50
+ r = request.app.state.redis_client
51
+ key_type = await r.type(key)
52
+ # print(f"Key type: {key_type}")
53
+
54
+ if key_type == "string": # 或 'string' 取决于客户端
55
+ value = await r.get(key)
56
+ return PlainTextResponse(
57
+ content=value, headers={"Content-Type": "text/plain; charset=utf-8"}
58
+ )
59
+ elif key_type == "none":
60
+ raise HTTPException(status_code=404, detail=f"Key '{key}' not found in Redis")
61
+ else:
62
+ raise HTTPException(
63
+ status_code=400, detail=f"key:<{key}>,value type not string"
64
+ )
65
+
66
+
67
+ @router.get(
68
+ "/byset",
69
+ summary="根据redis.key获取数据",
70
+ )
71
+ async def list_by_set(
72
+ request: Request,
73
+ key: str = Query(..., description="redis.key"),
74
+ cursor: int = Query(0, description="从第几条开始取数据"),
75
+ size: int = Query(5000, description="取多少数据"),
76
+ ):
77
+ r = request.app.state.redis_client
78
+ key_type = await r.type(f"by_time:{key}")
79
+
80
+ if key_type == "none":
81
+ raise HTTPException(status_code=404, detail=f"Key '{key}' not found in Redis")
82
+
83
+ if key_type not in ["zset", "set"]:
84
+ raise HTTPException(
85
+ status_code=400, detail=f"key:<{key}>,value type not in [zset,set]"
86
+ )
87
+
88
+ ids = await r.zrevrange(f"by_time:{key}", cursor, cursor + size - 1)
89
+ if not ids:
90
+ return SuccessResponse(data=[])
91
+
92
+ pipe = r.pipeline()
93
+ for id in ids:
94
+ pipe.hgetall(f"{key}:{id}")
95
+ value = await pipe.execute()
96
+ filtered_value = [v for v in value if v]
97
+
98
+ return SuccessResponse(data=filtered_value)
File without changes
@@ -0,0 +1,59 @@
1
+ import asyncio
2
+
3
+ import typer
4
+
5
+ from crypto.bitget import (
6
+ bitget_sf_close,
7
+ bitget_sf_open,
8
+ grid_close,
9
+ grid_open,
10
+ mix_tickers,
11
+ spot_tickers,
12
+ )
13
+ from utils.mysql import get_database_engine
14
+
15
+ app = typer.Typer()
16
+
17
+
18
+ @app.command()
19
+ def sync(env_path: str = "d:/.env"):
20
+ """同步mysql中grid数据到csv文件"""
21
+ engine = get_database_engine(env_path)
22
+ grid_csv_fp = "d:/github/meme2046/data/bitget_grid_0.csv"
23
+ sf_csv_fp = "d:/github/meme2046/data/bitget_sf_0.csv"
24
+ asyncio.run(grid_open(engine, grid_csv_fp))
25
+ asyncio.run(grid_close(engine, grid_csv_fp))
26
+ asyncio.run(bitget_sf_open(engine, sf_csv_fp))
27
+ asyncio.run(bitget_sf_close(engine, sf_csv_fp))
28
+
29
+
30
+ @app.command()
31
+ def spot(
32
+ symbols: str,
33
+ proxy: str = typer.Option(
34
+ None, "--proxy", "-p", help="代理服务器地址,例如: http://127.0.0.1:7890"
35
+ ),
36
+ ):
37
+ """
38
+ 从bitget获取加密货币现货价格.
39
+
40
+ 参数:
41
+ symbols:加密货币符号,可以是多个,用逗号分隔,例如:"BTCUSDT,ETHUSDT"
42
+ """
43
+ spot_tickers(symbols.split(","), proxy)
44
+
45
+
46
+ @app.command()
47
+ def mix(
48
+ symbols: str,
49
+ proxy: str = typer.Option(
50
+ None, "--proxy", "-p", help="代理服务器地址,例如: http://127.0.0.1:7890"
51
+ ),
52
+ ):
53
+ """
54
+ 从bitget获取加密货币合约价格.
55
+
56
+ 参数:
57
+ symbols:加密货币符号,可以是多个,用逗号分隔,例如:"BTCUSDT,ETHUSDT"
58
+ """
59
+ mix_tickers(symbols.split(","), proxy)
@@ -0,0 +1,32 @@
1
+ import typer
2
+
3
+ app = typer.Typer()
4
+
5
+
6
+ @app.command()
7
+ def hello(
8
+ name: str = typer.Argument(
9
+ "from My CLI!",
10
+ help="Name of the person to greet",
11
+ ),
12
+ ):
13
+ print(f"Hello {name}!")
14
+
15
+
16
+ @app.command()
17
+ def goodbye(
18
+ name: str = typer.Argument(
19
+ "from My CLI!",
20
+ help="Name of the person to goodbye",
21
+ ),
22
+ formal: bool = typer.Option(
23
+ False,
24
+ "--formal",
25
+ "-f",
26
+ help="是否为正式场合回答",
27
+ ),
28
+ ):
29
+ if formal:
30
+ print(f"Goodbye Ms. {name}. Have a good day.")
31
+ else:
32
+ print(f"Bye {name}!")
@@ -0,0 +1,167 @@
1
+ import os
2
+ from contextlib import asynccontextmanager
3
+
4
+ import redis.asyncio as redis
5
+ import typer
6
+ import uvicorn
7
+ from fastapi import FastAPI
8
+ from fastapi.exceptions import RequestValidationError
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import JSONResponse, PlainTextResponse
11
+ from starlette.exceptions import HTTPException as StarletteHTTPException
12
+
13
+ from api.v1 import api_router
14
+ from core.clash import ClashConfig, init_generator
15
+ from core.config import settings
16
+ from models.response import SuccessResponse
17
+
18
+
19
+ @asynccontextmanager
20
+ async def lifespan(app: FastAPI):
21
+ """管理应用生命周期的上下文管理器"""
22
+ redis_pool = redis.ConnectionPool(
23
+ host=os.getenv("REDIS_HOST", "192.168.123.7"),
24
+ port=int(os.getenv("REDIS_PORT", 6379)),
25
+ db=int(os.getenv("REDIS_DB", 0)),
26
+ password=os.getenv("REDIS_PASSWORD"),
27
+ max_connections=20, # 根据需要调整最大连接数
28
+ decode_responses=True,
29
+ )
30
+ redis_client: redis.Redis = redis.Redis(connection_pool=redis_pool)
31
+
32
+ # 启动时
33
+ app.state.redis_client = redis_client
34
+ yield
35
+ # 关闭时
36
+ if redis_client:
37
+ await redis_client.aclose()
38
+
39
+
40
+ typer_app = typer.Typer()
41
+
42
+
43
+ app = FastAPI(
44
+ title=settings.NAME,
45
+ description=settings.DESCRIPTION,
46
+ version=settings.VERSION,
47
+ lifespan=lifespan,
48
+ )
49
+
50
+ # 注册 v1 版本的所有路由
51
+ app.include_router(api_router, prefix=settings.API_V1_STR)
52
+ app.add_middleware(
53
+ CORSMiddleware,
54
+ allow_origins=["*"], # 允许所有源,生产环境建议设置具体的源
55
+ allow_credentials=True,
56
+ allow_methods=["*"], # 允许所有 HTTP 方法
57
+ allow_headers=["*"], # 允许所有 headers
58
+ )
59
+
60
+
61
+ @app.exception_handler(StarletteHTTPException)
62
+ async def http_exception_handler(request, exc):
63
+ return JSONResponse(
64
+ status_code=exc.status_code,
65
+ content={"error": exc.detail, "success": False, "data": None},
66
+ )
67
+
68
+
69
+ @app.exception_handler(RequestValidationError)
70
+ async def validation_exception_handler(request, exc):
71
+ # 将错误信息转换为可序列化的格式
72
+ errors = []
73
+ for error in exc.errors():
74
+ errors.append(
75
+ {
76
+ "type": error.get("type"),
77
+ "loc": error.get("loc"),
78
+ "msg": error.get("msg"),
79
+ "input": error.get("input"),
80
+ }
81
+ )
82
+
83
+ return JSONResponse(
84
+ status_code=422,
85
+ content={
86
+ "error": errors,
87
+ "success": False,
88
+ "data": None,
89
+ },
90
+ )
91
+
92
+
93
+ @app.exception_handler(Exception)
94
+ async def general_exception_handler(request, exc):
95
+ return JSONResponse(
96
+ status_code=500,
97
+ content={
98
+ "error": "Internal server error",
99
+ "success": False,
100
+ "data": None,
101
+ },
102
+ )
103
+
104
+
105
+ @app.get("/")
106
+ async def root():
107
+ return SuccessResponse(data=f"Welcome to {settings.DESCRIPTION}")
108
+
109
+
110
+ @app.get("/ping", response_class=PlainTextResponse)
111
+ async def pingpong():
112
+ return "pong"
113
+
114
+
115
+ @typer_app.command()
116
+ def run_app(
117
+ host: str = typer.Argument(
118
+ "0.0.0.0",
119
+ help="fastapi监听的<ip>地址",
120
+ ),
121
+ port: int = typer.Option(
122
+ 80,
123
+ "--port",
124
+ help="fastapi监听的端口号",
125
+ ),
126
+ ssl_keyfile: str = typer.Option(
127
+ None,
128
+ "--ssl-keyfile",
129
+ "-sk",
130
+ help="ssl keyfile",
131
+ ),
132
+ ssl_certfile: str = typer.Option(
133
+ None,
134
+ "--ssl-certfile",
135
+ "-sc",
136
+ help="ssl certfile",
137
+ ),
138
+ rule: str = typer.Option(
139
+ "https://cdn.jsdelivr.net/gh/Loyalsoldier/clash-rules@release",
140
+ "--rule",
141
+ "-r",
142
+ help="clash Rule base URL",
143
+ ),
144
+ my_rule: str = typer.Option(
145
+ "https://raw.githubusercontent.com/meme2046/data/refs/heads/main/clash",
146
+ "--my-rule",
147
+ "-mr",
148
+ help="my clash rule base URL(自定义规则)",
149
+ ),
150
+ proxy: str = typer.Option(
151
+ None,
152
+ "--proxy",
153
+ "-p",
154
+ help="服务器代理,传入则通过代理转换Clash订阅,比如:socks5://127.0.0.1:7890",
155
+ ),
156
+ ):
157
+ clash_config = ClashConfig(rule, my_rule, proxy)
158
+ init_generator(clash_config)
159
+
160
+ uvicorn.run(
161
+ "cli.fast:app",
162
+ host=host,
163
+ port=port,
164
+ reload=False,
165
+ ssl_keyfile=ssl_keyfile,
166
+ ssl_certfile=ssl_certfile,
167
+ )
@@ -0,0 +1,27 @@
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(env_path: str = "d:/.env"):
23
+ """同步mysql中grid数据到csv文件"""
24
+ engine = get_database_engine(env_path)
25
+ grid_csv_fp = "d:/github/meme2046/data/gate_grid_0.csv"
26
+ asyncio.run(grid_open(engine, grid_csv_fp))
27
+ asyncio.run(grid_close(engine, grid_csv_fp))