pymecli 0.2.5__py3-none-any.whl

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.
core/clash.py ADDED
@@ -0,0 +1,267 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ import requests
5
+ import yaml
6
+ from fastapi import Depends
7
+
8
+ module_dir = Path(__file__).resolve().parent.parent
9
+
10
+
11
+ class ClashConfig:
12
+ def __init__(self, rule_base_url: str, my_rule_base_url: str, request_proxy: str):
13
+ self.rule_base_url = rule_base_url.rstrip("/")
14
+ self.my_rule_base_url = my_rule_base_url.rstrip("/")
15
+ self.request_proxy = request_proxy
16
+
17
+
18
+ class ClashYamlGenerator:
19
+ def __init__(self, config: ClashConfig):
20
+ self.rule_base_url = config.rule_base_url
21
+ self.my_rule_base_url = config.my_rule_base_url
22
+ self.request_proxy = config.request_proxy
23
+
24
+ def gen(self, sub_list: list[dict]):
25
+ proxies = None
26
+ if self.request_proxy:
27
+ proxies = {
28
+ "http": self.request_proxy,
29
+ "https": self.request_proxy,
30
+ }
31
+
32
+ with open(str(module_dir / "data/template.yaml"), "r", encoding="utf-8") as f:
33
+ template = yaml.safe_load(f)
34
+
35
+ template["proxy-groups"].extend(
36
+ [
37
+ {
38
+ "name": "全局选择",
39
+ "type": "select",
40
+ "proxies": ["自动选择", "手动选择", "轮询"]
41
+ + [item["name"] for item in sub_list],
42
+ },
43
+ {
44
+ "name": "自动选择",
45
+ "type": "url-test",
46
+ "url": "https://www.gstatic.com/generate_204",
47
+ "interval": 300,
48
+ "tolerance": 11,
49
+ "lazy": True,
50
+ "use": [f"provider.{item['name']}" for item in sub_list],
51
+ },
52
+ {
53
+ "name": "手动选择",
54
+ "type": "select",
55
+ "use": [f"provider.{item['name']}" for item in sub_list],
56
+ },
57
+ {
58
+ "name": "轮询",
59
+ "type": "load-balance",
60
+ "url": "https://www.gstatic.com/generate_204",
61
+ "interval": 300,
62
+ "lazy": True,
63
+ "strategy": "round-robin",
64
+ "use": [f"provider.{item['name']}" for item in sub_list],
65
+ },
66
+ ]
67
+ )
68
+ userinfo = ""
69
+ for item in sub_list:
70
+ headers = {"User-Agent": item["user_agent"]} if item["user_agent"] else {}
71
+
72
+ if not item["url"]:
73
+ raise ValueError("Invalid subscription URL.")
74
+ response = requests.get(item["url"], headers=headers, proxies=proxies)
75
+ response.raise_for_status()
76
+ if not userinfo:
77
+ userinfo = response.headers["Subscription-Userinfo"]
78
+ remote_config = yaml.safe_load(response.text)
79
+
80
+ ps = remote_config.get("proxies", [])
81
+ if not ps:
82
+ raise ValueError("No proxies found in subscription.")
83
+
84
+ template["proxy-providers"][f"provider.{item['name']}"] = {
85
+ "type": "inline",
86
+ "payload": ps,
87
+ }
88
+
89
+ template["proxy-groups"].append(
90
+ {
91
+ "name": item["name"],
92
+ "type": "url-test",
93
+ "url": "https://www.gstatic.com/generate_204",
94
+ "interval": 300,
95
+ "tolerance": 11,
96
+ "lazy": True,
97
+ "use": [f"provider.{item['name']}"],
98
+ },
99
+ )
100
+
101
+ # 获取rules
102
+ rule_list = [
103
+ [f"{self.my_rule_base_url}/direct.yaml", "DIRECT"],
104
+ [f"{self.my_rule_base_url}/proxy.yaml", "全局选择"],
105
+ [
106
+ f"{self.my_rule_base_url}/round.yaml",
107
+ "轮询",
108
+ ],
109
+ ]
110
+
111
+ for item in rule_list:
112
+ response = requests.get(item[0], proxies=proxies)
113
+ response.raise_for_status()
114
+ remote = yaml.safe_load(response.text)
115
+ template["rule-providers"][os.path.basename(item[0])] = {
116
+ "type": "inline",
117
+ "behavior": "classical",
118
+ "payload": remote["payload"],
119
+ }
120
+
121
+ template["rules"].append(f"RULE-SET,{os.path.basename(item[0])},{item[1]}")
122
+
123
+ template["rule-providers"].update(
124
+ {
125
+ "applications": {
126
+ "type": "http",
127
+ "behavior": "classical",
128
+ "url": f"{self.rule_base_url}/applications.txt",
129
+ "path": "./ruleset/applications.yaml",
130
+ "interval": 86400,
131
+ },
132
+ "private": {
133
+ "type": "http",
134
+ "behavior": "domain",
135
+ "url": f"{self.rule_base_url}/private.txt",
136
+ "path": "./ruleset/private.yaml",
137
+ "interval": 86400,
138
+ },
139
+ "icloud": {
140
+ "type": "http",
141
+ "behavior": "domain",
142
+ "url": f"{self.rule_base_url}/icloud.txt",
143
+ "path": "./ruleset/icloud.yaml",
144
+ "interval": 86400,
145
+ },
146
+ "apple": {
147
+ "type": "http",
148
+ "behavior": "domain",
149
+ "url": f"{self.rule_base_url}/apple.txt",
150
+ "path": "./ruleset/apple.yaml",
151
+ "interval": 86400,
152
+ },
153
+ "google": {
154
+ "type": "http",
155
+ "behavior": "domain",
156
+ "url": f"{self.rule_base_url}/google.txt",
157
+ "path": "./ruleset/google.yaml",
158
+ "interval": 86400,
159
+ },
160
+ "proxy": {
161
+ "type": "http",
162
+ "behavior": "domain",
163
+ "url": f"{self.rule_base_url}/proxy.txt",
164
+ "path": "./ruleset/proxy.yaml",
165
+ "interval": 86400,
166
+ },
167
+ "direct": {
168
+ "type": "http",
169
+ "behavior": "domain",
170
+ "url": f"{self.rule_base_url}/direct.txt",
171
+ "path": "./ruleset/direct.yaml",
172
+ "interval": 86400,
173
+ },
174
+ "lancidr": {
175
+ "type": "http",
176
+ "behavior": "ipcidr",
177
+ "url": f"{self.rule_base_url}/lancidr.txt",
178
+ "path": "./ruleset/lancidr.yaml",
179
+ "interval": 86400,
180
+ },
181
+ "cncidr": {
182
+ "type": "http",
183
+ "behavior": "ipcidr",
184
+ "url": f"{self.rule_base_url}/cncidr.txt",
185
+ "path": "./ruleset/cncidr.yaml",
186
+ "interval": 86400,
187
+ },
188
+ "telegramcidr": {
189
+ "type": "http",
190
+ "behavior": "ipcidr",
191
+ "url": f"{self.rule_base_url}/telegramcidr.txt",
192
+ "path": "./ruleset/telegramcidr.yaml",
193
+ "interval": 86400,
194
+ },
195
+ },
196
+ )
197
+
198
+ template["rules"].extend(
199
+ [
200
+ "RULE-SET,applications,DIRECT",
201
+ "DOMAIN,clash.razord.top,DIRECT",
202
+ "DOMAIN,yacd.haishan.me,DIRECT",
203
+ "RULE-SET,private,DIRECT",
204
+ "RULE-SET,icloud,DIRECT",
205
+ "RULE-SET,apple,DIRECT",
206
+ "RULE-SET,google,全局选择",
207
+ "RULE-SET,proxy,全局选择",
208
+ "RULE-SET,direct,DIRECT",
209
+ "RULE-SET,lancidr,DIRECT",
210
+ "RULE-SET,cncidr,DIRECT",
211
+ "RULE-SET,telegramcidr,全局选择",
212
+ "GEOIP,LAN,DIRECT,no-resolve",
213
+ "GEOIP,CN,DIRECT,no-resolve",
214
+ "MATCH,全局选择",
215
+ ]
216
+ )
217
+
218
+ return template, userinfo
219
+
220
+ def query2sub(self, urls: str, agents: str, names: str):
221
+ url_list = urls.split(",") if urls else []
222
+ agents_list = agents.split(",") if agents else []
223
+ name_list = names.split(",") if names else []
224
+
225
+ max_length = max(len(url_list), len(agents_list), len(name_list))
226
+
227
+ while len(url_list) < max_length:
228
+ url_list.append("")
229
+ while len(agents_list) < max_length:
230
+ agents_list.append("")
231
+ while len(name_list) < max_length:
232
+ name_list.append("")
233
+
234
+ sub_list = []
235
+
236
+ for i in range(max_length):
237
+ if not url_list[i]:
238
+ raise ValueError(f"Invalid subscription URL. #{i + 1}")
239
+
240
+ sub_list.append(
241
+ {
242
+ "url": url_list[i],
243
+ "user_agent": agents_list[i] if agents_list[i] else "",
244
+ "name": name_list[i] if name_list[i] else f"订阅{i}",
245
+ }
246
+ )
247
+
248
+ return sub_list
249
+
250
+
251
+ generator_instance = None
252
+
253
+
254
+ def init_generator(config: ClashConfig):
255
+ global generator_instance
256
+ generator_instance = ClashYamlGenerator(config)
257
+
258
+
259
+ def get_generator():
260
+ global generator_instance
261
+ if generator_instance is None:
262
+ raise RuntimeError("ClashYamlGenerator not initialized")
263
+ return generator_instance
264
+
265
+
266
+ def get_generator_dependency():
267
+ return Depends(get_generator)
core/config.py ADDED
@@ -0,0 +1,34 @@
1
+ import importlib.metadata
2
+
3
+ from pydantic_settings import BaseSettings
4
+
5
+ metadata = importlib.metadata.metadata("pymecli")
6
+ # module_dir = Path(__file__).resolve().parent.parent
7
+
8
+ # project = read_toml(str(module_dir / "./pyproject.toml"))["project"]
9
+
10
+
11
+ class Settings(BaseSettings):
12
+ # API配置
13
+ API_V1_STR: str = "/api/v1"
14
+ NAME: str = metadata["Name"]
15
+ DESCRIPTION: str = (
16
+ f"{metadata['Summary']}, FastAPI提供: clash订阅转换、baidu.gushitong api"
17
+ )
18
+ VERSION: str = metadata["Version"]
19
+
20
+ print(f"project: {NAME}")
21
+ print(f"version: {VERSION}")
22
+ print(f"description: {DESCRIPTION}")
23
+
24
+ class Config:
25
+ env_prefix = "PY_ME_CLI_" # 添加环境变量前缀
26
+ case_sensitive = True
27
+
28
+ def reload(self):
29
+ new_settings = Settings()
30
+ for field in Settings.model_fields:
31
+ setattr(self, field, getattr(new_settings, field))
32
+
33
+
34
+ settings = Settings()
core/redis_client.py ADDED
@@ -0,0 +1,137 @@
1
+ import json
2
+ import os
3
+ from typing import Any, Optional
4
+
5
+ import redis
6
+
7
+
8
+ class RedisClient:
9
+ def __init__(
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,
16
+ ):
17
+ """
18
+ 初始化Redis客户端
19
+ :param host: Redis服务器地址
20
+ :param port: Redis服务器端口
21
+ :param db: 数据库编号
22
+ :param password: 密码(如果需要)
23
+ :param decode_responses: 是否自动解码响应(将字节转换为字符串)
24
+ """
25
+
26
+ self.client = redis.Redis(
27
+ host=host,
28
+ port=port,
29
+ db=db,
30
+ password=password,
31
+ decode_responses=decode_responses,
32
+ socket_connect_timeout=5,
33
+ socket_timeout=5,
34
+ )
35
+ # 测试连接
36
+ self.client.ping()
37
+
38
+ def get_value(self, key: str) -> Optional[Any]:
39
+ """
40
+ 根据key获取值
41
+ :param key: Redis中的键名
42
+ :return: 键对应的值, 如果键不存在则返回None
43
+ """
44
+ if not self.client:
45
+ return None
46
+
47
+ return self.client.get(key)
48
+
49
+ def get_all_keys(self, pattern: str = "*"):
50
+ """
51
+ 获取所有匹配的键
52
+ :param pattern: 键的匹配模式,默认为"*"匹配所有键
53
+ :return: 匹配的键列表
54
+ """
55
+ if not self.client:
56
+ return []
57
+
58
+ return self.client.keys(pattern)
59
+
60
+ def set_value(self, key: str, value: Any, expire: int | None = None):
61
+ """
62
+ 设置键值对
63
+ :param key: 键名
64
+ :param value: 值
65
+ :param expire: 过期时间(秒),如果提供则在指定时间后过期
66
+ :return: 设置成功返回True, 否则返回False
67
+ """
68
+ if not self.client:
69
+ return False
70
+
71
+ # 如果值是字典或列表,转换为JSON字符串存储
72
+ if isinstance(value, (dict, list)):
73
+ value = json.dumps(value, ensure_ascii=False)
74
+
75
+ result = self.client.set(key, value)
76
+ if expire:
77
+ self.client.expire(key, expire)
78
+ return result
79
+
80
+ def get_hash_value(self, name: str, key: str):
81
+ """
82
+ 获取哈希类型数据中的值
83
+ :param name: 哈希表名称
84
+ :param key: 哈希表中的键
85
+ :return: 键对应的值
86
+ """
87
+ if not self.client:
88
+ return None
89
+
90
+ return self.client.hget(name, key)
91
+
92
+ def get_list(self, key: str):
93
+ """
94
+ 获取列表类型数据
95
+ :param key: 列表键名
96
+ :return: 列表内容
97
+ """
98
+ if not self.client:
99
+ return []
100
+
101
+ return self.client.lrange(key, 0, -1)
102
+
103
+ def close(self):
104
+ """
105
+ 关闭Redis连接
106
+ """
107
+ if self.client:
108
+ self.client.close()
109
+
110
+
111
+ # 使用示例
112
+ if __name__ == "__main__":
113
+ # 创建Redis客户端实例
114
+ client = RedisClient()
115
+ # 示例1: 获取普通键值
116
+ key = "baidu.finance.getbanner"
117
+ value = client.get_value(key)
118
+ print(f"键 '{key}' 的值为: {value}")
119
+
120
+ # 示例2: 获取所有键
121
+ all_keys = client.get_all_keys()
122
+ print(f"所有键: {all_keys}")
123
+ print(f"所有键: {type(all_keys)}")
124
+
125
+ # 示例3: 设置键值
126
+ client.set_value("test_key", {"name": "张三", "age": 30}, expire=3600)
127
+
128
+ # 示例4: 获取哈希值
129
+ hash_value = client.get_hash_value("my_hash", "field1")
130
+ print(f"哈希值: {hash_value}")
131
+
132
+ # 示例5: 获取列表值
133
+ list_value = client.get_list("my_list")
134
+ print(f"列表值: {list_value}")
135
+
136
+ # 关闭连接
137
+ client.close()
crypto/__init__.py ADDED
File without changes
crypto/bitget.py ADDED
@@ -0,0 +1,124 @@
1
+ from decimal import Decimal
2
+
3
+ import requests
4
+ from sqlalchemy import Engine
5
+
6
+ from utils.mysql import mysql_to_csv
7
+
8
+
9
+ def tickers(url: str, symbols: list, proxy: str | None = None):
10
+ proxies = None
11
+ if proxy:
12
+ proxies = {
13
+ "http": proxy,
14
+ "https": proxy,
15
+ }
16
+ response = requests.get(url, proxies=proxies)
17
+
18
+ resp_json = response.json()
19
+ symbols_list = [
20
+ s.lower() + "usdt" if not s.lower().endswith("usdt") else s.lower()
21
+ for s in symbols
22
+ ]
23
+ if response.status_code == 200 and resp_json.get("code") == "00000":
24
+ data = resp_json.get("data", [])
25
+ results = []
26
+ for item in data:
27
+ if item["symbol"].lower() in symbols_list:
28
+ price = Decimal(item["lastPr"])
29
+ results.append(
30
+ f"{item['symbol'].upper().rstrip('USDT')}:{format(price, 'f')}"
31
+ )
32
+ print(f"[{','.join(results)}]")
33
+ else:
34
+ print(
35
+ f"📌 请求失败,状态码:{response.status_code},错误信息:{resp_json.get('msg')}"
36
+ )
37
+
38
+
39
+ def mix_tickers(symbols: list, proxy: str | None = None):
40
+ url = "https://api.bitget.com/api/v2/mix/market/tickers?productType=USDT-FUTURES"
41
+ tickers(url, symbols, proxy)
42
+
43
+
44
+ def spot_tickers(symbols: list, proxy: str | None = None):
45
+ url = "https://api.bitget.com/api/v2/spot/market/tickers"
46
+ tickers(url, symbols, proxy)
47
+
48
+
49
+ def bitget_sf_open(engine: Engine, csv_path: str):
50
+ 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;"
51
+ row_count = mysql_to_csv(
52
+ engine,
53
+ csv_path,
54
+ "bitget_sf",
55
+ query,
56
+ update_status=1,
57
+ d_column_names=["spot_client_order_id", "futures_client_order_id"],
58
+ pd_dtype={
59
+ "spot_order_id": str,
60
+ "futures_order_id": str,
61
+ "spot_tracking_no": str,
62
+ "futures_tracking_no": str,
63
+ },
64
+ )
65
+
66
+ print(f"🚀 bitget sf open count:({row_count})")
67
+
68
+
69
+ def bitget_sf_close(engine: Engine, csv_path: str):
70
+ query = "select * from bitget_sf where pnl is not null and up_status in (0,1);"
71
+ row_count = mysql_to_csv(
72
+ engine,
73
+ csv_path,
74
+ "bitget_sf",
75
+ query,
76
+ update_status=2,
77
+ d_column_names=["spot_client_order_id", "futures_client_order_id"],
78
+ pd_dtype={
79
+ "spot_order_id": str,
80
+ "futures_order_id": str,
81
+ "spot_tracking_no": str,
82
+ "futures_tracking_no": str,
83
+ },
84
+ )
85
+
86
+ print(f"🚀 bitget sf close count:({row_count})")
87
+
88
+
89
+ def grid_open(engine: Engine, csv_path: str):
90
+ query = "select id,created_at,name,act_name,symbol,qty,cex,status,up_status,path,level,earn,cost,buy_px,benefit,sell_px,profit,order_id,client_order_id,fx_order_id,fx_client_order_id,signature,chain,open_at,close_at,mint,dex_act,dex_status,dex_fail_count 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;"
91
+ row_count = mysql_to_csv(
92
+ engine,
93
+ csv_path,
94
+ "bitget",
95
+ query,
96
+ update_status=1,
97
+ d_column_names=["client_order_id"],
98
+ pd_dtype={
99
+ "order_id": str,
100
+ "fx_order_id": str,
101
+ },
102
+ )
103
+ print(f"🧮 bitget open count:({row_count})")
104
+
105
+
106
+ def grid_close(engine: Engine, csv_path: str):
107
+ query = "select id,created_at,name,act_name,symbol,qty,cex,status,up_status,path,level,earn,cost,buy_px,benefit,sell_px,profit,order_id,client_order_id,fx_order_id,fx_client_order_id,signature,chain,open_at,close_at,mint,dex_act,dex_status,dex_fail_count from bitget where profit is not null and up_status in (0,1) and deleted_at is null;"
108
+ row_count = mysql_to_csv(
109
+ engine,
110
+ csv_path,
111
+ "bitget",
112
+ query,
113
+ update_status=2,
114
+ d_column_names=["client_order_id"],
115
+ pd_dtype={
116
+ "order_id": str,
117
+ "fx_order_id": str,
118
+ },
119
+ )
120
+ print(f"🧮 bitget close count:({row_count})")
121
+
122
+
123
+ if __name__ == "__main__":
124
+ pass
crypto/gate.py ADDED
@@ -0,0 +1,35 @@
1
+ from sqlalchemy import Engine
2
+
3
+ from utils.mysql import mysql_to_csv
4
+
5
+
6
+ def gate_open(engine: Engine, csv_path: str):
7
+ query = "select id,created_at,name,act_name,symbol,qty,cex,status,up_status,path,level,earn,cost,buy_px,benefit,sell_px,profit,order_id,client_order_id,fx_order_id,fx_client_order_id,signature,chain,open_at,close_at,mint,dex_act,dex_status,dex_fail_count 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;"
8
+ row_count = mysql_to_csv(
9
+ engine,
10
+ csv_path,
11
+ "gate",
12
+ query,
13
+ update_status=1,
14
+ d_column_names=["client_order_id"],
15
+ pd_dtype={"order_id": str, "fx_order_id": str},
16
+ )
17
+ print(f"🧮 gate open count:({row_count})")
18
+
19
+
20
+ def gate_close(engine: Engine, csv_path: str):
21
+ query = "select id,created_at,name,act_name,symbol,qty,cex,status,up_status,path,level,earn,cost,buy_px,benefit,sell_px,profit,order_id,client_order_id,fx_order_id,fx_client_order_id,signature,chain,open_at,close_at,mint,dex_act,dex_status,dex_fail_count from gate where profit is not null and up_status in (0,1) and deleted_at is null;"
22
+ row_count = mysql_to_csv(
23
+ engine,
24
+ csv_path,
25
+ "gate",
26
+ query,
27
+ update_status=2,
28
+ d_column_names=["client_order_id"],
29
+ pd_dtype={"order_id": str, "fx_order_id": str},
30
+ )
31
+ print(f"🧮 gate close count:({row_count})")
32
+
33
+
34
+ if __name__ == "__main__":
35
+ pass
data/__init__.py ADDED
File without changes