python-http-helper 0.1.0__py3-none-any.whl → 0.2.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.

Potentially problematic release.


This version of python-http-helper might be problematic. Click here for more details.

@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: http-helper
5
+ # FileName: __init__.py
6
+ # Description: http帮助包
7
+ # Author: ASUS
8
+ # CreateDate: 2025/11/24
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
@@ -0,0 +1,210 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: http-helper
5
+ # FileName: async_proxy.py
6
+ # Description: 客户端异步代理
7
+ # Author: ASUS
8
+ # CreateDate: 2025/11/24
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ import re
13
+ import json
14
+ import aiohttp
15
+ import asyncio
16
+ from yarl import URL
17
+ from urllib.parse import quote
18
+ from ..utils.log import logger
19
+ from typing import Any, Dict, Optional, List
20
+ from ..utils.http_execption import HttpClientError
21
+ from ..utils.reponse_handle_utils import get_html_title
22
+
23
+
24
+ class HttpClientFactory:
25
+ __retry: int = 0
26
+
27
+ def __init__(
28
+ self,
29
+ protocol: str = "https",
30
+ domain: str = "api.weixin.qq.com",
31
+ timeout: int = 10,
32
+ retry: int = 0,
33
+ enable_log: bool = False,
34
+ cookie_jar: Optional[aiohttp.CookieJar] = None,
35
+ playwright_state: Dict[str, Any] = None,
36
+ proxy_config: Optional[Dict[str, str]] = None
37
+ ):
38
+ self.base_url = f"{protocol}://{domain}"
39
+ self.protocol = protocol
40
+ self.domain = domain
41
+ self.timeout = aiohttp.ClientTimeout(total=timeout)
42
+ self.__retry = retry
43
+ self.enable_log = enable_log
44
+ self.proxy_url = self.build_proxy_url(proxy_config)
45
+
46
+ # 初始化 session
47
+ self.session = aiohttp.ClientSession(timeout=self.timeout, cookie_jar=cookie_jar or aiohttp.CookieJar())
48
+ if playwright_state:
49
+ self._load_playwright_cookies_to_aiohttp(playwright_state)
50
+ self.valid_methods = {"get", "post", "put", "delete"}
51
+
52
+ @staticmethod
53
+ def build_proxy_url(proxy_config: Optional[Dict[str, str]]) -> Optional[str]:
54
+ """
55
+ 将 {server, username, password} 转为 aiohttp 可用的代理 URL
56
+ :param proxy_config: 格式如:
57
+ {
58
+ "server": "http://127.0.0.1:1234",
59
+ "username": "<USERNAME>",
60
+ "password": "<PASSWORD>",
61
+ }
62
+ """
63
+ if not proxy_config or not isinstance(proxy_config, dict) or not proxy_config.get("server"):
64
+ return None
65
+
66
+ server = proxy_config["server"].strip()
67
+ # 去掉协议头(兼容带或不带 http:// 的情况)
68
+ if server.startswith(("http://", "https://")):
69
+ host_port = server.split("://", 1)[1]
70
+ else:
71
+ host_port = server
72
+
73
+ if proxy_config.get("username") and proxy_config.get("password"):
74
+ # URL 编码用户名和密码(防止 @ : / 等特殊字符破坏 URL)
75
+ username = quote(proxy_config["username"], safe="")
76
+ password = quote(proxy_config["password"], safe="")
77
+ url = f"http://{username}:{password}@{host_port}"
78
+ elif not proxy_config.get("username") and not proxy_config.get("password"):
79
+ url = f"http://{host_port}"
80
+ else:
81
+ raise HttpClientError("代理参数缺失 username 或 password")
82
+ return url
83
+
84
+ def _load_playwright_cookies_to_aiohttp(self, playwright_state: Dict[str, Any]):
85
+ """将 Playwright storage_state 中的 cookies 加载到 aiohttp session"""
86
+ for ck in playwright_state.get("cookies", []):
87
+ name = ck["name"]
88
+ value = ck["value"]
89
+ domain = ck["domain"]
90
+ path = ck.get("path", "/")
91
+
92
+ # 构造合法 URL 用于设置 cookie(主机名不能以 . 开头)
93
+ host = domain.lstrip(".") if domain.startswith(".") else domain
94
+ url = URL(f"{self.protocol}://{host}{path}")
95
+
96
+ # 设置 cookie
97
+ self.session.cookie_jar.update_cookies(cookies={name: value}, response_url=url)
98
+
99
+ async def request(
100
+ self,
101
+ method: str,
102
+ url: str,
103
+ *,
104
+ params: Dict[str, Any] = None,
105
+ json_data: Any = None,
106
+ data: Any = None,
107
+ headers: Dict[str, str] = None,
108
+ is_end: bool = True,
109
+ has_cookie: bool = False,
110
+ proxy_config: Optional[Dict[str, str]] = None,
111
+ exception_keywords: Optional[List[str]] = None
112
+ ) -> Any:
113
+ if proxy_config:
114
+ self.proxy_url = self.build_proxy_url(proxy_config)
115
+ method = method.lower().strip()
116
+ if method not in self.valid_methods:
117
+ raise HttpClientError(f"Invalid Request method: {method}")
118
+
119
+ full_url = f"{self.base_url}{url}"
120
+
121
+ # 重试机制
122
+ attempts = self.__retry + 1
123
+
124
+ try:
125
+ for attempt in range(1, attempts + 1):
126
+ try:
127
+ if self.enable_log:
128
+ logger.debug(f"{method.upper()} Request {full_url} attempt {attempt}")
129
+
130
+ async with self.session.request(
131
+ method=method,
132
+ url=full_url,
133
+ proxy=self.proxy_url,
134
+ params=params or None,
135
+ json=json_data,
136
+ data=data,
137
+ headers=headers,
138
+ ) as resp:
139
+
140
+ # 非 2xx 抛异常
141
+ if resp.status >= 400:
142
+ error_text = self.parse_error_text(
143
+ exception_keywords=exception_keywords, error_text=await resp.text()
144
+ )
145
+ raise HttpClientError(error_text)
146
+
147
+ # 检查响应的 Content-Type
148
+ content_type = resp.headers.get("Content-Type", "").lower()
149
+
150
+ # 尝试 JSON 解码
151
+ if "application/json" in content_type or "text/json" in content_type:
152
+ try:
153
+ json_data = await resp.json(content_type=None) # 忽略非法 content-type
154
+ except (Exception,):
155
+ text = await resp.text()
156
+ json_data = json.loads(text)
157
+ elif "text/html" in content_type:
158
+ # 纯文本类型
159
+ html = await resp.text()
160
+ json_data = {
161
+ "code": resp.status,
162
+ "message": get_html_title(html=html),
163
+ "data": html
164
+ }
165
+ else:
166
+ # 其他类型,默认视为二进制内容
167
+ # content = await resp.content.readany() # 只读当前缓冲区,可能只是部分数据,非阻塞、低级 API
168
+ content = await resp.read() # 完整响应体
169
+ try:
170
+ text = content.decode('utf-8')
171
+ except UnicodeDecodeError:
172
+ text = content.decode('latin1') # fallback
173
+ json_data = dict(code=resp.status, message=get_html_title(html=text), data=text)
174
+ if has_cookie is True:
175
+ # resp.headers 的类型是 CIMultiDict,getall() 在 key 不存在时会抛出 KeyError,因此需要给一个空list作为默认值
176
+ set_cookie_headers = resp.headers.getall("Set-Cookie", list())
177
+ json_data["cookies"] = set_cookie_headers[0] if len(
178
+ set_cookie_headers) == 1 else set_cookie_headers # 返回列表
179
+ return json_data
180
+
181
+ except Exception as e:
182
+ if attempt == attempts:
183
+ raise HttpClientError(f"Request failed after {attempts} attempts: {e}")
184
+ await asyncio.sleep(1 * attempt) # 递增式重试间隔
185
+ finally:
186
+ if is_end is True:
187
+ await self.close()
188
+
189
+ async def close(self):
190
+ if self.session and not self.session.closed:
191
+ await self.session.close()
192
+
193
+ @staticmethod
194
+ def parse_error_text(error_text: str, exception_keywords: Optional[List[str]] = None) -> str:
195
+ if not exception_keywords:
196
+ return error_text
197
+ _exception_keywords = [
198
+ r'<h3[^>]*class="font-bold"[^>]*>([^<]+)</h3>'
199
+ ]
200
+ if exception_keywords:
201
+ _exception_keywords.extend(exception_keywords)
202
+ for exception_keyword in _exception_keywords:
203
+ match = re.search(exception_keyword, error_text)
204
+ if match:
205
+ error_text = match.group(1).strip()
206
+ break
207
+ # 尝试提取青岛航空的提示信息(可选增强)
208
+ if "您的IP由于频繁访问已受限" in error_text:
209
+ raise HttpClientError(f"IP blocked by QDAir: {error_text}")
210
+ return error_text
@@ -11,4 +11,4 @@
11
11
  """
12
12
  import logging
13
13
 
14
- logger = logging.getLogger()
14
+ logger = logging.getLogger("root")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_http_helper
3
- Version: 0.1.0
3
+ Version: 0.2.1
4
4
  Summary: http helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -210,7 +210,8 @@ Project-URL: Issues, https://github.com/ckf10000/http-helper/issues
210
210
  Requires-Python: >=3.12
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: aiohttp>=3.13.2; python_version >= "3.12"
213
+ Requires-Dist: aiohttp==3.13.2; python_version >= "3.12"
214
+ Requires-Dist: yarl==1.22.0; python_version >= "3.12"
214
215
  Dynamic: license-file
215
216
 
216
217
  # http-helper
@@ -230,7 +231,7 @@ Dynamic: license-file
230
231
  - dist/python-http-helper-0.1.0.tar.gz
231
232
  - dist/python-http-helper-0.1.0-py3-none-any.whl
232
233
  - 2. 发布
233
- - twine upload dist/*
234
+ - twine upload dist/* 或者 twine upload -r public dist/* 或者 twine upload --repository pypi dist/* --skip-existing --verbose --cert false 忽略证书
234
235
 
235
236
  #### 使用说明
236
237
 
@@ -0,0 +1,12 @@
1
+ http_helper/__init__.py,sha256=RKb5JmhPJDEEuZ6XtBGm__z38vKVo6pZ-QOuudZSnvg,470
2
+ http_helper/client/__init__.py,sha256=rCu_vOiNkNWNQtiQLoeGrcIHTK-u-9C_NeQlXd5fo9U,469
3
+ http_helper/client/async_proxy.py,sha256=Skli2iBAZDRMh3d13KCDFLeLNpuXy0UWYDAfv9Xkdds,9392
4
+ http_helper/utils/__init__.py,sha256=2U-xO-3A3SnCawQMzip3LuoNjH0SiZcCFB2bWeacKmU,466
5
+ http_helper/utils/http_execption.py,sha256=w-yRbvayrE9kR9PZsFtEWmRXSoAIo-k4gS_cVRDg5O8,560
6
+ http_helper/utils/log.py,sha256=ryuQIhu-H1FR5dSrNZaMx91IfH4sGA5K-xFZTUowz68,518
7
+ http_helper/utils/reponse_handle_utils.py,sha256=6tFf3jmgjjzHt_EAmMKXITosq4-_5O7iSvkF3AREqus,805
8
+ python_http_helper-0.2.1.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
9
+ python_http_helper-0.2.1.dist-info/METADATA,sha256=agPo5wVQGvJ3Zsw3MSUZhzF73aRwHpkmIbhqmR5ifB0,14334
10
+ python_http_helper-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
11
+ python_http_helper-0.2.1.dist-info/top_level.txt,sha256=3gy3yvKZaKUW4_LRfqknrRwF9MOPkXD7jHkM-Q-BhC4,12
12
+ python_http_helper-0.2.1.dist-info/RECORD,,
@@ -0,0 +1 @@
1
+ http_helper
client/async_proxy.py DELETED
@@ -1,109 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- # ---------------------------------------------------------------------------------------------------------
4
- # ProjectName: http-helper
5
- # FileName: async_proxy.py
6
- # Description: 客户端异步代理
7
- # Author: ASUS
8
- # CreateDate: 2025/11/24
9
- # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
- # ---------------------------------------------------------------------------------------------------------
11
- """
12
- import json
13
- import aiohttp
14
- import asyncio
15
- from typing import Any, Dict
16
- from ..utils.log import logger
17
- from ..utils.http_execption import HttpClientError
18
- from ..utils.reponse_handle_utils import get_html_title
19
-
20
-
21
- class HttpClientFactory:
22
- __retry: int = 0
23
-
24
- def __init__(
25
- self,
26
- protocol: str = "https",
27
- domain: str = "api.weixin.qq.com",
28
- timeout: int = 10,
29
- retry: int = 0,
30
- enable_log: bool = False
31
- ):
32
- self.base_url = "://".join([protocol, domain])
33
- self.timeout = aiohttp.ClientTimeout(total=timeout)
34
- self.__retry = retry
35
- self.enable_log = enable_log
36
- self.session = aiohttp.ClientSession(timeout=self.timeout)
37
-
38
- self.valid_methods = {"get", "post", "put", "delete"}
39
-
40
- async def request(
41
- self,
42
- method: str,
43
- url: str,
44
- *,
45
- params: Dict[str, Any] = None,
46
- json_data: Any = None,
47
- data: Any = None,
48
- headers: Dict[str, str] = None,
49
- is_end: bool = True
50
- ) -> Any:
51
-
52
- method = method.lower().strip()
53
- if method not in self.valid_methods:
54
- raise HttpClientError(f"Invalid Request method: {method}")
55
-
56
- full_url = f"{self.base_url}{url}"
57
-
58
- # 重试机制
59
- attempts = self.__retry + 1
60
- for attempt in range(1, attempts + 1):
61
- try:
62
- if self.enable_log:
63
- logger.debug(f"{method.upper()} Request {full_url} attempt {attempt}")
64
-
65
- async with self.session.request(
66
- method=method,
67
- url=full_url,
68
- params=params or None,
69
- json=json_data,
70
- data=data,
71
- headers=headers,
72
- ) as resp:
73
-
74
- # 非 2xx 抛异常
75
- if resp.status >= 400:
76
- raise HttpClientError(f"Response status {resp.status} Error: {await resp.text()}")
77
-
78
- # 检查响应的 Content-Type
79
- content_type = resp.headers.get("Content-Type", "")
80
-
81
- # 尝试 JSON 解码
82
- try:
83
- # 如果 Content-Type 是 text/json,手动解析 JSON
84
- if "text/json" in content_type:
85
- json_data = await resp.text() # 获取响应文本
86
- # 手动解析 JSON
87
- json_data = json.loads(json_data)
88
- elif "application/json" in content_type:
89
- json_data = await resp.json()
90
- elif "text/html" in content_type:
91
- # 纯文本类型
92
- json_data = dict(code=resp.status, message=get_html_title(html=await resp.text()),
93
- data=await resp.text())
94
- else:
95
- # 其他类型,默认视为二进制内容
96
- content = await resp.content.readany()
97
- content = content.decode('utf-8')
98
- json_data = dict(code=resp.status, message=get_html_title(html=content), data=content)
99
- return json_data
100
- except aiohttp.ContentTypeError as e:
101
- raise HttpClientError(f"Response parse error: {e}")
102
-
103
- except Exception as e:
104
- if attempt == attempts:
105
- raise HttpClientError(f"Request failed after {attempts} attempts: {e}")
106
- await asyncio.sleep(1 * attempt) # 递增式重试间隔
107
-
108
- if is_end is True:
109
- await self.session.close()
@@ -1,11 +0,0 @@
1
- client/__init__.py,sha256=rCu_vOiNkNWNQtiQLoeGrcIHTK-u-9C_NeQlXd5fo9U,469
2
- client/async_proxy.py,sha256=2gZ6Ak5erNSA2VRVP0Nyuf1K5gWq5roTGtEkoaG7dD0,4388
3
- python_http_helper-0.1.0.dist-info/licenses/LICENSE,sha256=WtjCEwlcVzkh1ziO35P2qfVEkLjr87Flro7xlHz3CEY,11556
4
- utils/__init__.py,sha256=2U-xO-3A3SnCawQMzip3LuoNjH0SiZcCFB2bWeacKmU,466
5
- utils/http_execption.py,sha256=w-yRbvayrE9kR9PZsFtEWmRXSoAIo-k4gS_cVRDg5O8,560
6
- utils/log.py,sha256=lm9oRYTkYdTsXe-blicDlx-ZRikhtlGs_oM-86wF52s,512
7
- utils/reponse_handle_utils.py,sha256=6tFf3jmgjjzHt_EAmMKXITosq4-_5O7iSvkF3AREqus,805
8
- python_http_helper-0.1.0.dist-info/METADATA,sha256=_tITgIzxQz-2cx-4RaS-unf4jhBOraJnjT6WmXgFaFM,14144
9
- python_http_helper-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- python_http_helper-0.1.0.dist-info/top_level.txt,sha256=Y3jknC7ZyId_T4_pOtSk7-pTuXZRm9irp1ATqBVjhLY,13
11
- python_http_helper-0.1.0.dist-info/RECORD,,
@@ -1,2 +0,0 @@
1
- client
2
- utils
File without changes
File without changes
File without changes
File without changes