pykitool 0.0.1__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.
@@ -0,0 +1,473 @@
1
+ import os
2
+ import socket
3
+ import subprocess
4
+ from typing import Dict, List, Optional
5
+ from urllib.parse import urlparse
6
+
7
+ import requests
8
+ from loguru import logger
9
+ from tqdm import tqdm
10
+
11
+ from pykitool.base.enums import BaseAbstractEnum, Platform
12
+ from pykitool.utils import cbfile, cbruntime
13
+
14
+ # ================================ 网络地址 ================================
15
+
16
+
17
+ # 获取内网 IP 地址
18
+ def get_internal_ip() -> str:
19
+ try:
20
+ # 创建 UDP 套接字
21
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
22
+ # 连接到公共 DNS 服务器
23
+ s.connect(("1.1.1.1", 80))
24
+ # 获取本地 IP 地址
25
+ internal_ip = s.getsockname()[0]
26
+ s.close()
27
+ return internal_ip
28
+ except OSError as e:
29
+ logger.error(f"Get internal ip error: {str(e)}")
30
+ return "127.0.0.1"
31
+
32
+
33
+ # 获取内网 IP 地址
34
+ def get_local_ip() -> str:
35
+ try:
36
+ host = cbruntime.get_arg(["-h", "--host"], get_internal_ip())
37
+ if host == "0.0.0.0":
38
+ return get_internal_ip()
39
+ return host
40
+ except OSError as e:
41
+ logger.error(f"Get internal ip error: {str(e)}")
42
+ return "127.0.0.1"
43
+
44
+
45
+ # 获取本地服务地址
46
+ def get_localhost() -> str:
47
+ port = cbruntime.get_arg(["-p", "--port"], 8000)
48
+ return f"http://{get_local_ip()}:{port}"
49
+
50
+
51
+ # 批量获取归属地信息(ipapi)
52
+ def get_ipapi_locations_batch(ips: List[str]) -> Dict[str, Dict]:
53
+ import random
54
+ import time
55
+
56
+ from pykitool.base.cache import get_lru_cache
57
+
58
+ if not ips:
59
+ return {}
60
+
61
+ cache = get_lru_cache()
62
+ results = {}
63
+ ips_to_query = []
64
+
65
+ # 1. 过滤本地 IP 并尝试从缓存获取
66
+ for ip in ips:
67
+ if not ip:
68
+ continue
69
+
70
+ if ip.startswith(("127.", "192.168.", "10.")):
71
+ results[ip] = {"status": "success", "country": "Local", "regionName": "", "city": ""}
72
+ continue
73
+
74
+ cache_key = f"ip_loc_raw_{ip}"
75
+ cached_loc = cache.get(cache_key)
76
+ if cached_loc is not None:
77
+ results[ip] = cached_loc
78
+ else:
79
+ ips_to_query.append(ip)
80
+
81
+ # 去重
82
+ ips_to_query = list(set(ips_to_query))
83
+
84
+ # 2. 批量请求(最多重试 3 次)
85
+ max_retries = 3
86
+ for attempt in range(max_retries):
87
+ if not ips_to_query:
88
+ break
89
+
90
+ logger.info(f"Batch IP location query attempt {attempt + 1}/{max_retries}, querying {len(ips_to_query)} IPs")
91
+ failed_ips = []
92
+
93
+ # 按 100 个一批切分
94
+ batch_size = 100
95
+ for i in range(0, len(ips_to_query), batch_size):
96
+ batch = ips_to_query[i : i + batch_size]
97
+ url = "http://ip-api.com/batch?lang=zh-CN"
98
+ try:
99
+ resp = requests.post(url, json=batch, timeout=30)
100
+ if resp.status_code == 200:
101
+ data = resp.json()
102
+ for item in data:
103
+ ip = item.get("query")
104
+ if not ip:
105
+ continue
106
+
107
+ if item.get("status") == "success":
108
+ results[ip] = item
109
+ # 写入缓存
110
+ cache.set(f"ip_loc_raw_{ip}", item, timeout=86400)
111
+ else:
112
+ failed_ips.append(ip)
113
+ else:
114
+ logger.warning(f"Batch API returned status code: {resp.status_code}")
115
+ failed_ips.extend(batch)
116
+ except Exception as e:
117
+ logger.error(f"Batch IP location query failed: {e}")
118
+ failed_ips.extend(batch)
119
+
120
+ # 每批之间随机休眠 5-10 秒防屏蔽(如果是最后一部且没有失败的,可不休眠)
121
+ if attempt < max_retries - 1 or len(ips_to_query) > batch_size:
122
+ sleep_time = random.uniform(5, 10)
123
+ logger.debug(f"Sleeping for {sleep_time:.2f} seconds before next batch/attempt")
124
+ time.sleep(sleep_time)
125
+
126
+ # 更新下次重试需要查询的 IP
127
+ ips_to_query = failed_ips
128
+
129
+ return results
130
+
131
+
132
+ # 获取归属地信息(ipapi)
133
+ def get_ipapi_location(ip: str) -> Dict[str, str]:
134
+ from pykitool.base.cache import get_lru_cache
135
+
136
+ if not ip:
137
+ return {}
138
+
139
+ cache = get_lru_cache()
140
+ # 尝试从缓存中获取
141
+ cache_key = f"ip_loc_raw_{ip}"
142
+ cached_loc = cache.get(cache_key)
143
+ if cached_loc is not None:
144
+ return cached_loc
145
+
146
+ # 忽略局域网/保留 IP
147
+ if ip.startswith(("127.", "192.168.", "10.")):
148
+ return {"status": "success", "country": "Local", "regionName": "", "city": ""}
149
+
150
+ try:
151
+ url = f"http://ip-api.com/json/{ip}?lang=zh-CN"
152
+ resp = requests.get(url, timeout=30)
153
+ if resp.status_code == 200:
154
+ data = resp.json()
155
+ if data.get("status") == "success":
156
+ # 写入缓存,1 天过期,避免频繁请求
157
+ cache.set(cache_key, data, timeout=86400)
158
+ return data
159
+ except Exception as e:
160
+ logger.error(f"Get IP location failed for {ip}: {e}")
161
+
162
+ return {}
163
+
164
+
165
+ # 获取归属地信息(ip9 备用接口)
166
+ def get_ip9_location(ip: str) -> Dict[str, str]:
167
+ from pykitool.base.cache import get_lru_cache
168
+
169
+ if not ip:
170
+ return {}
171
+
172
+ cache = get_lru_cache()
173
+ # 尝试从缓存中获取
174
+ cache_key = f"ip_loc_ip9_{ip}"
175
+ cached_loc = cache.get(cache_key)
176
+ if cached_loc is not None:
177
+ return cached_loc
178
+
179
+ # 忽略局域网/保留 IP
180
+ if ip.startswith(("127.", "192.168.", "10.")):
181
+ return {"status": "success", "country": "Local", "regionName": "", "city": ""}
182
+
183
+ try:
184
+ url = f"https://ip9.com.cn/get?ip={ip}"
185
+ resp = requests.get(url, timeout=30)
186
+ if resp.status_code == 200:
187
+ data = resp.json()
188
+ if data.get("ret") == 200:
189
+ result_data = data.get("data", {})
190
+ # 统一格式化为 ip-api 相同的结构
191
+ formatted_data = {"status": "success", "country": result_data.get("country", ""), "regionName": result_data.get("prov", ""), "city": result_data.get("city", "")}
192
+ # 写入缓存,1 天过期
193
+ cache.set(cache_key, formatted_data, timeout=86400)
194
+ return formatted_data
195
+ except Exception as e:
196
+ logger.error(f"Get IP location (ip9) failed for {ip}: {e}")
197
+
198
+ return {}
199
+
200
+
201
+ # ================================ 连接验证 ================================
202
+
203
+
204
+ # 验证 Socket 连接
205
+ def verify_socket_connection(url: str, timeout: float = 1.0, show_print: bool = False) -> bool:
206
+ try:
207
+ parsed = urlparse(url)
208
+ host = parsed.hostname or "localhost"
209
+ port = parsed.port or 80
210
+ with socket.create_connection((host, port), timeout=timeout):
211
+ return True
212
+ except socket.timeout:
213
+ if show_print:
214
+ logger.warning(f"Check {url} connection timed out")
215
+ return False
216
+ except OSError as e:
217
+ if show_print:
218
+ logger.warning(f"Check {url} connection failed: {str(e)}")
219
+ return False
220
+
221
+
222
+ # 验证 HTTP 连接
223
+ def verify_http_connection(url: str = "https://www.google.com", proxy: Optional[Dict[str, str]] = None, timeout: float = 1.0, show_print: bool = False) -> bool:
224
+ try:
225
+ resp = requests.get(url, proxies=proxy, timeout=timeout)
226
+ return resp.ok
227
+ except requests.exceptions.Timeout:
228
+ if show_print:
229
+ logger.warning(f"Check {url} connection time out")
230
+ return False
231
+ except requests.exceptions.ConnectionError:
232
+ if show_print:
233
+ logger.warning(f"Check {url} connection error")
234
+ return False
235
+ except requests.exceptions.RequestException as e:
236
+ if show_print:
237
+ logger.warning(f"Check {url} connection exception: {str(e)}")
238
+ return False
239
+
240
+
241
+ # ================================ Download 下载 ================================
242
+
243
+
244
+ # 通过 HTTP 下载文件
245
+ def http_download(url: str, save_folder: str = cbfile.tempdir(), filename: Optional[str] = None) -> str:
246
+ import requests
247
+
248
+ if not filename:
249
+ filename = url.split("/")[-1]
250
+
251
+ cbfile.mk_folder(save_folder)
252
+ file_path = cbfile.ap(os.path.join(save_folder, filename))
253
+
254
+ if os.path.exists(file_path):
255
+ logger.debug(f"File already exists: {file_path}")
256
+ return file_path
257
+
258
+ try:
259
+ response = requests.get(url, stream=True, timeout=30)
260
+ response.raise_for_status()
261
+
262
+ total_size = int(response.headers.get("content-length", 0))
263
+
264
+ with open(file_path, "wb") as file:
265
+ for chunk in tqdm(
266
+ response.iter_content(1024),
267
+ total=total_size // 1024,
268
+ unit="KB",
269
+ desc=filename,
270
+ dynamic_ncols=True,
271
+ ):
272
+ file.write(chunk)
273
+ logger.info(f"File saved to: {file_path}")
274
+ except requests.exceptions.RequestException as e:
275
+ logger.error(f"HTTP request failed: {str(e)}")
276
+ raise RuntimeError(f"Download failed: {str(e)}") from e
277
+ except IOError as e:
278
+ logger.error(f"IO error saving file: {str(e)}")
279
+ raise
280
+
281
+ return file_path
282
+
283
+
284
+ # ================================ Ping 工具 ================================
285
+
286
+
287
+ # Ping 辅助类
288
+ class PingHelper:
289
+
290
+ # 执行 Ping 命令(仅 Windows)
291
+ @Platform.system(Platform.Window)
292
+ def ping(url: str = "www.baidu.com www.google.com", count: int = 65535, interval: int = 0, view: str = "point") -> None:
293
+ import re
294
+
295
+ # 解析目标地址
296
+ def parse_targets(url: str) -> List[str]:
297
+ if not url:
298
+ raise ValueError("url not empty")
299
+
300
+ targets = re.split(r"[,\s]+", url.strip())
301
+ targets = list(filter(None, targets))
302
+
303
+ # 处理协议前缀,提取主机名
304
+ processed_targets = []
305
+ for target in targets:
306
+ if target.startswith(("http://", "https://")):
307
+ parsed = urlparse(target)
308
+ if parsed.hostname:
309
+ processed_targets.append(parsed.hostname)
310
+ else:
311
+ processed_targets.append(target)
312
+
313
+ # 去重
314
+ processed_targets = list(dict.fromkeys(processed_targets))
315
+
316
+ if not processed_targets:
317
+ raise ValueError("no valid target was resolved.")
318
+
319
+ return processed_targets
320
+
321
+ # 解析地址
322
+ targets = parse_targets(url)
323
+ # 执行程序
324
+ program_exec = os.path.join(os.getcwd(), "static/resource/nping/nping.exe")
325
+ # 构建命令
326
+ cmd = [program_exec, *targets, "-c", str(count), "-i", str(interval), "-v", view]
327
+ subprocess.Popen(cmd, creationflags=subprocess.CREATE_NEW_CONSOLE)
328
+
329
+
330
+ # ================================ IP 信息 ================================
331
+
332
+
333
+ # IP 信息辅助类
334
+ class IPInfoHelper:
335
+
336
+ def __init__(self, url: str = "https://ipinfo.io/json", timeout: int = 3, fallback_country: str = "CN") -> None:
337
+ self.url = url
338
+ self.timeout = timeout
339
+ self.fallback_country = fallback_country
340
+ self.data: Optional[Dict[str, str]] = None
341
+
342
+ # 获取 IP 信息
343
+ def fetch(self) -> Dict[str, str]:
344
+ try:
345
+ response = requests.get(self.url, timeout=self.timeout)
346
+ if response.status_code == 200:
347
+ self.data = response.json()
348
+ else:
349
+ self.data = {"country": self.fallback_country}
350
+ except requests.exceptions.RequestException:
351
+ self.data = {"country": self.fallback_country}
352
+ return self.data
353
+
354
+ # 获取国家代码
355
+ def get_country(self) -> str:
356
+ try:
357
+ return self.data["country"]
358
+ except (KeyError, TypeError) as e:
359
+ logger.error(f"Error: {str(e)}")
360
+ return "CN"
361
+
362
+ # 判断是否为中国地区
363
+ def is_china(self) -> bool:
364
+ if not self.data:
365
+ self.fetch()
366
+ return self.data.get("country") == "CN"
367
+
368
+
369
+ # ================================ 代理工具 ================================
370
+
371
+
372
+ # 代理辅助类
373
+ class ProxyHelper:
374
+
375
+ def __init__(
376
+ self,
377
+ protocol: BaseAbstractEnum.Protocol = BaseAbstractEnum.Protocol.HTTP,
378
+ ip: str = "127.0.0.1",
379
+ port: int = 10808,
380
+ username: Optional[str] = None,
381
+ password: Optional[str] = None,
382
+ ) -> None:
383
+ self.protocol = protocol.value.lower()
384
+ self.ip = ip
385
+ self.port = port
386
+ self.username = username
387
+ self.password = password
388
+
389
+ # 构造代理 URL
390
+ def _build_url(self) -> str:
391
+ if self.username and self.password:
392
+ return f"{self.protocol}://{self.username}:{self.password}@{self.ip}:{self.port}"
393
+ else:
394
+ return f"{self.protocol}://{self.ip}:{self.port}"
395
+
396
+ # 获取代理字典
397
+ def get_proxies(self) -> Dict[str, str]:
398
+ proxy = self._build_url()
399
+ return {"http": proxy, "https": proxy}
400
+
401
+ # 设置环境变量
402
+ def set_env(self) -> None:
403
+ if self.protocol.startswith("socks"):
404
+ raise ValueError("environment variables do not support SOCKS proxy. Please use an HTTP proxy or the get_proxies() method instead.")
405
+ proxy_url = self._build_url()
406
+ os.environ["HTTP_PROXY"] = proxy_url
407
+ os.environ["HTTPS_PROXY"] = proxy_url
408
+
409
+ # 清除环境变量
410
+ def clear_env(self) -> None:
411
+ os.environ.pop("HTTP_PROXY", None)
412
+ os.environ.pop("HTTPS_PROXY", None)
413
+
414
+ # 验证代理连接
415
+ def verify(self, url: str = "https://www.google.com", timeout: int = 1, print_log: bool = True) -> bool:
416
+ return verify_http_connection(url, proxy=self.get_proxies(), timeout=timeout, show_print=print_log)
417
+
418
+
419
+ # 启动代理
420
+ def start_proxy(enable: bool = False) -> None:
421
+ import urllib.request
422
+
423
+ proxies = urllib.request.getproxies()
424
+ # 软件代理设置
425
+ if enable:
426
+ proxy = ProxyHelper()
427
+ is_proxy_verify = proxy.verify(timeout=2, print_log=False)
428
+ if is_proxy_verify:
429
+ proxy.set_env()
430
+ logger.info("Proxy enabled and verified successfully.")
431
+ else:
432
+ logger.warning("Proxy verification failed. Please check your proxy settings.")
433
+ # 获取系统代理
434
+ elif len(proxies) > 0:
435
+ os.environ["HTTP_PROXY"] = proxies.get("http", "")
436
+ os.environ["HTTPS_PROXY"] = proxies.get("https", "")
437
+ logger.info("Use system proxy.")
438
+
439
+
440
+ # ================================ 调用示例 ================================
441
+
442
+
443
+ if __name__ == "__main__":
444
+
445
+ # ==================== 连接验证示例 ====================
446
+
447
+ # url = "https://www.google.com"
448
+ # print(verify_socket_connection(url, timeout=1))
449
+ # print(verify_http_connection(url, timeout=1))
450
+
451
+ # ==================== 代理示例 ====================
452
+
453
+ # proxy = ProxyHelper()
454
+ # print(proxy.verify(url))
455
+
456
+ # proxy = ProxyHelper(BaseEnum.Protocol.SOCKS5, "127.0.0.1", 10808)
457
+ # print(proxy.verify(url))
458
+
459
+ # ==================== 代理认证示例 ====================
460
+
461
+ # username = "admin"
462
+ # password = "123456"
463
+ # proxy = ProxyHelper(BaseEnum.Protocol.HTTP, "127.0.0.1", 10810, username, password)
464
+ # print(proxy.verify(url))
465
+
466
+ # ==================== 环境变量示例 ====================
467
+
468
+ # proxy = ProxyHelper(BaseEnum.Protocol.HTTP, "127.0.0.1", 10808)
469
+ # proxy.set_env()
470
+ # response = requests.get("https://www.google.com", timeout=5)
471
+ # print(response.status_code)
472
+
473
+ pass