funcguard 0.1.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.
Potentially problematic release.
This version of funcguard might be problematic. Click here for more details.
- funcguard-0.1.1/LICENSE +21 -0
- funcguard-0.1.1/PKG-INFO +156 -0
- funcguard-0.1.1/README.md +142 -0
- funcguard-0.1.1/funcguard/__init__.py +7 -0
- funcguard-0.1.1/funcguard/core.py +81 -0
- funcguard-0.1.1/funcguard/tools.py +46 -0
- funcguard-0.1.1/funcguard.egg-info/PKG-INFO +156 -0
- funcguard-0.1.1/funcguard.egg-info/SOURCES.txt +16 -0
- funcguard-0.1.1/funcguard.egg-info/dependency_links.txt +1 -0
- funcguard-0.1.1/funcguard.egg-info/not-zip-safe +1 -0
- funcguard-0.1.1/funcguard.egg-info/requires.txt +1 -0
- funcguard-0.1.1/funcguard.egg-info/top_level.txt +2 -0
- funcguard-0.1.1/setup.cfg +4 -0
- funcguard-0.1.1/setup.py +31 -0
- funcguard-0.1.1/tests/__init__.py +1 -0
- funcguard-0.1.1/tests/run_test.py +40 -0
- funcguard-0.1.1/tests/test_core.py +109 -0
- funcguard-0.1.1/tests/test_tools.py +156 -0
funcguard-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 tinycen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
funcguard-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: funcguard
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A funcguard for Python.
|
|
5
|
+
Home-page: https://github.com/tinycen/funcguard
|
|
6
|
+
Author: tinycen
|
|
7
|
+
Author-email: sky_ruocen@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# FuncGuard
|
|
16
|
+
|
|
17
|
+
FuncGuard是一个Python库,提供了函数执行超时控制和重试机制的实用工具。
|
|
18
|
+
|
|
19
|
+
## 功能特点
|
|
20
|
+
|
|
21
|
+
- 函数执行超时控制
|
|
22
|
+
- 函数执行失败自动重试
|
|
23
|
+
- HTTP请求封装(支持自动重试)
|
|
24
|
+
|
|
25
|
+
## 安装/升级
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install funcguard
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install --upgrade funcguard
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## 使用方法
|
|
37
|
+
|
|
38
|
+
### 超时控制
|
|
39
|
+
|
|
40
|
+
使用`timeout_handler`函数可以控制函数的执行时间,防止函数运行时间过长:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from funcguard.core import timeout_handler
|
|
44
|
+
|
|
45
|
+
def long_running_function():
|
|
46
|
+
# 模拟一个耗时操作
|
|
47
|
+
import time
|
|
48
|
+
time.sleep(10)
|
|
49
|
+
return "操作完成"
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# 设置超时时间为5秒
|
|
53
|
+
result = timeout_handler(long_running_function, execution_timeout=5)
|
|
54
|
+
print(result)
|
|
55
|
+
except TimeoutError as e:
|
|
56
|
+
print(f"捕获到超时错误: {e}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 重试机制
|
|
60
|
+
|
|
61
|
+
使用`retry_function`函数可以在函数执行失败时自动重试:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from funcguard.core import retry_function
|
|
65
|
+
|
|
66
|
+
def unstable_function():
|
|
67
|
+
# 模拟一个可能失败的操作
|
|
68
|
+
import random
|
|
69
|
+
if random.random() < 0.7: # 70%的概率失败
|
|
70
|
+
raise Exception("随机错误")
|
|
71
|
+
return "操作成功"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# 最多重试3次,每次执行超时时间为10秒
|
|
75
|
+
result = retry_function(unstable_function, max_retries=3, execute_timeout=10, task_name="测试任务")
|
|
76
|
+
print(result)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"重试后仍然失败: {e}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### HTTP请求
|
|
82
|
+
|
|
83
|
+
使用`send_request`函数发送HTTP请求,支持自动重试:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from funcguard.tools import send_request
|
|
87
|
+
|
|
88
|
+
# 不使用重试
|
|
89
|
+
response = send_request(
|
|
90
|
+
method="GET",
|
|
91
|
+
url="https://api.example.com/data",
|
|
92
|
+
headers={"Content-Type": "application/json"},
|
|
93
|
+
timeout=30
|
|
94
|
+
)
|
|
95
|
+
print(response)
|
|
96
|
+
|
|
97
|
+
# 使用重试
|
|
98
|
+
response = send_request(
|
|
99
|
+
method="POST",
|
|
100
|
+
url="https://api.example.com/data",
|
|
101
|
+
headers={"Content-Type": "application/json"},
|
|
102
|
+
data={"key": "value"},
|
|
103
|
+
timeout=30,
|
|
104
|
+
auto_retry={
|
|
105
|
+
"task_name": "API请求",
|
|
106
|
+
"max_retries": 3,
|
|
107
|
+
"execute_timeout": 60
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
print(response)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API文档
|
|
114
|
+
|
|
115
|
+
### funcguard.core
|
|
116
|
+
|
|
117
|
+
#### timeout_handler(func, args=(), kwargs=None, execution_timeout=90)
|
|
118
|
+
|
|
119
|
+
- **参数**:
|
|
120
|
+
- `func`: 需要执行的目标函数
|
|
121
|
+
- `args`: 目标函数的位置参数,默认为空元组
|
|
122
|
+
- `kwargs`: 目标函数的关键字参数,默认为None
|
|
123
|
+
- `execution_timeout`: 函数执行的超时时间,单位为秒,默认为90秒
|
|
124
|
+
- **返回值**: 目标函数的返回值
|
|
125
|
+
- **异常**: `TimeoutError` - 当函数执行超过指定时间时抛出
|
|
126
|
+
|
|
127
|
+
#### retry_function(func, max_retries=5, execute_timeout=90, task_name="", *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
- **参数**:
|
|
130
|
+
- `func`: 需要重试的函数
|
|
131
|
+
- `max_retries`: 最大重试次数,默认为5
|
|
132
|
+
- `execute_timeout`: 执行超时时间,默认为90秒
|
|
133
|
+
- `task_name`: 任务名称,用于打印日志
|
|
134
|
+
- `args`: func的位置参数
|
|
135
|
+
- `kwargs`: func的关键字参数
|
|
136
|
+
- **返回值**: func的返回值
|
|
137
|
+
- **异常**: 当重试次数用尽后仍然失败时,抛出最后一次的异常
|
|
138
|
+
|
|
139
|
+
### funcguard.tools
|
|
140
|
+
|
|
141
|
+
#### send_request(method, url, headers, data=None, return_type="json", timeout=60, auto_retry=None)
|
|
142
|
+
|
|
143
|
+
- **参数**:
|
|
144
|
+
- `method`: HTTP方法(GET, POST等)
|
|
145
|
+
- `url`: 请求URL
|
|
146
|
+
- `headers`: 请求头
|
|
147
|
+
- `data`: 请求数据,默认为None
|
|
148
|
+
- `return_type`: 返回类型,可选"json"、"response"或"text",默认为"json"
|
|
149
|
+
- `timeout`: 请求超时时间,单位为秒,默认为60
|
|
150
|
+
- `auto_retry`: 自动重试配置,格式为`{"task_name": "", "max_retries": 5, "execute_timeout": 90}`,默认为None
|
|
151
|
+
- **返回值**: 根据return_type参数返回不同格式的响应数据
|
|
152
|
+
- **异常**: 当请求失败且重试次数用尽后,抛出相应的异常
|
|
153
|
+
|
|
154
|
+
## 许可证
|
|
155
|
+
|
|
156
|
+
MIT License
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# FuncGuard
|
|
2
|
+
|
|
3
|
+
FuncGuard是一个Python库,提供了函数执行超时控制和重试机制的实用工具。
|
|
4
|
+
|
|
5
|
+
## 功能特点
|
|
6
|
+
|
|
7
|
+
- 函数执行超时控制
|
|
8
|
+
- 函数执行失败自动重试
|
|
9
|
+
- HTTP请求封装(支持自动重试)
|
|
10
|
+
|
|
11
|
+
## 安装/升级
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install funcguard
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install --upgrade funcguard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## 使用方法
|
|
23
|
+
|
|
24
|
+
### 超时控制
|
|
25
|
+
|
|
26
|
+
使用`timeout_handler`函数可以控制函数的执行时间,防止函数运行时间过长:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from funcguard.core import timeout_handler
|
|
30
|
+
|
|
31
|
+
def long_running_function():
|
|
32
|
+
# 模拟一个耗时操作
|
|
33
|
+
import time
|
|
34
|
+
time.sleep(10)
|
|
35
|
+
return "操作完成"
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
# 设置超时时间为5秒
|
|
39
|
+
result = timeout_handler(long_running_function, execution_timeout=5)
|
|
40
|
+
print(result)
|
|
41
|
+
except TimeoutError as e:
|
|
42
|
+
print(f"捕获到超时错误: {e}")
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 重试机制
|
|
46
|
+
|
|
47
|
+
使用`retry_function`函数可以在函数执行失败时自动重试:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from funcguard.core import retry_function
|
|
51
|
+
|
|
52
|
+
def unstable_function():
|
|
53
|
+
# 模拟一个可能失败的操作
|
|
54
|
+
import random
|
|
55
|
+
if random.random() < 0.7: # 70%的概率失败
|
|
56
|
+
raise Exception("随机错误")
|
|
57
|
+
return "操作成功"
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# 最多重试3次,每次执行超时时间为10秒
|
|
61
|
+
result = retry_function(unstable_function, max_retries=3, execute_timeout=10, task_name="测试任务")
|
|
62
|
+
print(result)
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"重试后仍然失败: {e}")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### HTTP请求
|
|
68
|
+
|
|
69
|
+
使用`send_request`函数发送HTTP请求,支持自动重试:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from funcguard.tools import send_request
|
|
73
|
+
|
|
74
|
+
# 不使用重试
|
|
75
|
+
response = send_request(
|
|
76
|
+
method="GET",
|
|
77
|
+
url="https://api.example.com/data",
|
|
78
|
+
headers={"Content-Type": "application/json"},
|
|
79
|
+
timeout=30
|
|
80
|
+
)
|
|
81
|
+
print(response)
|
|
82
|
+
|
|
83
|
+
# 使用重试
|
|
84
|
+
response = send_request(
|
|
85
|
+
method="POST",
|
|
86
|
+
url="https://api.example.com/data",
|
|
87
|
+
headers={"Content-Type": "application/json"},
|
|
88
|
+
data={"key": "value"},
|
|
89
|
+
timeout=30,
|
|
90
|
+
auto_retry={
|
|
91
|
+
"task_name": "API请求",
|
|
92
|
+
"max_retries": 3,
|
|
93
|
+
"execute_timeout": 60
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
print(response)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API文档
|
|
100
|
+
|
|
101
|
+
### funcguard.core
|
|
102
|
+
|
|
103
|
+
#### timeout_handler(func, args=(), kwargs=None, execution_timeout=90)
|
|
104
|
+
|
|
105
|
+
- **参数**:
|
|
106
|
+
- `func`: 需要执行的目标函数
|
|
107
|
+
- `args`: 目标函数的位置参数,默认为空元组
|
|
108
|
+
- `kwargs`: 目标函数的关键字参数,默认为None
|
|
109
|
+
- `execution_timeout`: 函数执行的超时时间,单位为秒,默认为90秒
|
|
110
|
+
- **返回值**: 目标函数的返回值
|
|
111
|
+
- **异常**: `TimeoutError` - 当函数执行超过指定时间时抛出
|
|
112
|
+
|
|
113
|
+
#### retry_function(func, max_retries=5, execute_timeout=90, task_name="", *args, **kwargs)
|
|
114
|
+
|
|
115
|
+
- **参数**:
|
|
116
|
+
- `func`: 需要重试的函数
|
|
117
|
+
- `max_retries`: 最大重试次数,默认为5
|
|
118
|
+
- `execute_timeout`: 执行超时时间,默认为90秒
|
|
119
|
+
- `task_name`: 任务名称,用于打印日志
|
|
120
|
+
- `args`: func的位置参数
|
|
121
|
+
- `kwargs`: func的关键字参数
|
|
122
|
+
- **返回值**: func的返回值
|
|
123
|
+
- **异常**: 当重试次数用尽后仍然失败时,抛出最后一次的异常
|
|
124
|
+
|
|
125
|
+
### funcguard.tools
|
|
126
|
+
|
|
127
|
+
#### send_request(method, url, headers, data=None, return_type="json", timeout=60, auto_retry=None)
|
|
128
|
+
|
|
129
|
+
- **参数**:
|
|
130
|
+
- `method`: HTTP方法(GET, POST等)
|
|
131
|
+
- `url`: 请求URL
|
|
132
|
+
- `headers`: 请求头
|
|
133
|
+
- `data`: 请求数据,默认为None
|
|
134
|
+
- `return_type`: 返回类型,可选"json"、"response"或"text",默认为"json"
|
|
135
|
+
- `timeout`: 请求超时时间,单位为秒,默认为60
|
|
136
|
+
- `auto_retry`: 自动重试配置,格式为`{"task_name": "", "max_retries": 5, "execute_timeout": 90}`,默认为None
|
|
137
|
+
- **返回值**: 根据return_type参数返回不同格式的响应数据
|
|
138
|
+
- **异常**: 当请求失败且重试次数用尽后,抛出相应的异常
|
|
139
|
+
|
|
140
|
+
## 许可证
|
|
141
|
+
|
|
142
|
+
MIT License
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
class FuncguardTimeoutError(Exception):
|
|
2
|
+
pass
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor , TimeoutError
|
|
6
|
+
|
|
7
|
+
# 计算函数运行时间
|
|
8
|
+
def timeout_handler( func, args = (), kwargs = None, execution_timeout = 90 ):
|
|
9
|
+
"""
|
|
10
|
+
使用 ThreadPoolExecutor 实现超时控制。
|
|
11
|
+
|
|
12
|
+
:param func: 需要执行的目标函数
|
|
13
|
+
:param args: 目标函数的位置参数,默认为空元组
|
|
14
|
+
:param kwargs: 目标函数的关键字参数,默认为 None
|
|
15
|
+
:param execution_timeout: 函数执行的超时时间,单位为秒,默认为 90 秒
|
|
16
|
+
:return: 目标函数的返回值
|
|
17
|
+
"""
|
|
18
|
+
if kwargs is None:
|
|
19
|
+
kwargs = { }
|
|
20
|
+
|
|
21
|
+
with ThreadPoolExecutor( max_workers = 1 ) as executor:
|
|
22
|
+
future = executor.submit( func, *args, **kwargs )
|
|
23
|
+
try:
|
|
24
|
+
return future.result( timeout = execution_timeout )
|
|
25
|
+
except TimeoutError:
|
|
26
|
+
error_message = f"TimeoutError:函数 {func.__name__} 执行时间超过 {execution_timeout} 秒"
|
|
27
|
+
# print( error_message )
|
|
28
|
+
raise FuncguardTimeoutError( error_message )
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 重试函数
|
|
32
|
+
def retry_function( func , max_retries = 5 , execute_timeout = 90 , task_name = "" , *args , **kwargs ) :
|
|
33
|
+
"""
|
|
34
|
+
重试函数的通用封装。
|
|
35
|
+
:param func: 需要重试的函数
|
|
36
|
+
:param max_retries: 最大重试次数
|
|
37
|
+
:param execute_timeout: 执行超时时间
|
|
38
|
+
:param task_name: 任务名称,用于打印日志
|
|
39
|
+
:param args: func的位置参数
|
|
40
|
+
:param kwargs: func的关键字参数
|
|
41
|
+
:return: func的返回值
|
|
42
|
+
"""
|
|
43
|
+
retry_count = 0
|
|
44
|
+
current_timeout = execute_timeout # 初始化当前超时时间为传入的execute_timeout
|
|
45
|
+
|
|
46
|
+
# 检查原始kwargs中是否包含timeout参数
|
|
47
|
+
original_timeout = kwargs.get( 'timeout' , 0 )
|
|
48
|
+
|
|
49
|
+
if current_timeout < original_timeout :
|
|
50
|
+
current_timeout = original_timeout + 30
|
|
51
|
+
|
|
52
|
+
last_exception = None
|
|
53
|
+
while retry_count < max_retries :
|
|
54
|
+
try :
|
|
55
|
+
result = timeout_handler( func , args = args , kwargs = kwargs , execution_timeout = current_timeout )
|
|
56
|
+
return result # 如果调用成功,则返回结果
|
|
57
|
+
|
|
58
|
+
except BaseException as e :
|
|
59
|
+
last_exception = e
|
|
60
|
+
retry_count += 1
|
|
61
|
+
print( e )
|
|
62
|
+
if "TimeoutError" in str( e ) :
|
|
63
|
+
# 计划延长的时间
|
|
64
|
+
extend_time = 30 * retry_count
|
|
65
|
+
|
|
66
|
+
# 增加执行超时时间
|
|
67
|
+
current_timeout += extend_time
|
|
68
|
+
|
|
69
|
+
# 如果原始函数有timeout参数,也增加它
|
|
70
|
+
if original_timeout > 0 :
|
|
71
|
+
kwargs[ 'timeout' ] = original_timeout + extend_time
|
|
72
|
+
# print( f"增加timeout参数至: {kwargs[ 'timeout' ]}秒" )
|
|
73
|
+
|
|
74
|
+
print( f"{task_name} : {func.__name__} 请求失败,正在重试... (第{retry_count}次)" )
|
|
75
|
+
if retry_count < max_retries : # 如果不是最后一次重试,则等待一段时间后重试
|
|
76
|
+
time.sleep( 5 * retry_count )
|
|
77
|
+
print( f"请求失败次数达到上限:{max_retries}次,终止请求。重试了{retry_count}次" )
|
|
78
|
+
# 这里可以添加更多的错误处理逻辑,例如记录错误信息
|
|
79
|
+
if last_exception:
|
|
80
|
+
raise last_exception # 重新抛出最后一个异常
|
|
81
|
+
return None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import requests
|
|
3
|
+
from . import core
|
|
4
|
+
# 使用 from .core import retry_function 会导致测试失败
|
|
5
|
+
|
|
6
|
+
# 发起请求
|
|
7
|
+
def send_request( method , url , headers , data = None , return_type = "json" , timeout = 60 , auto_retry = None ) :
|
|
8
|
+
'''
|
|
9
|
+
发送HTTP请求的通用函数
|
|
10
|
+
|
|
11
|
+
:param method: HTTP方法(GET, POST等)
|
|
12
|
+
:param url: 请求URL
|
|
13
|
+
:param headers: 请求头
|
|
14
|
+
:param data: 请求数据
|
|
15
|
+
:param return_type: 返回类型(json, text, response)
|
|
16
|
+
:param timeout: 请求超时时间
|
|
17
|
+
:param auto_retry: 自动重试配置,格式为:
|
|
18
|
+
{"task_name": "任务名称", "max_retries": 最大重试次数, "execute_timeout": 执行超时时间}
|
|
19
|
+
:return: 请求结果
|
|
20
|
+
'''
|
|
21
|
+
if data is None :
|
|
22
|
+
payload = { }
|
|
23
|
+
else :
|
|
24
|
+
if (isinstance( data , dict ) or isinstance( data , list )) and data != { } :
|
|
25
|
+
payload = json.dumps( data , ensure_ascii = False )
|
|
26
|
+
else :
|
|
27
|
+
payload = data
|
|
28
|
+
if auto_retry is None :
|
|
29
|
+
response = requests.request( method , url , headers = headers , data = payload , timeout = timeout )
|
|
30
|
+
else :
|
|
31
|
+
max_retries = auto_retry.get( "max_retries" , 5 )
|
|
32
|
+
execute_timeout = auto_retry.get( "execute_timeout" , 90 )
|
|
33
|
+
task_name = auto_retry.get( "task_name" , "" )
|
|
34
|
+
response = core.retry_function( requests.request , max_retries , execute_timeout , task_name , method , url ,
|
|
35
|
+
headers = headers , data = payload , timeout = timeout )
|
|
36
|
+
|
|
37
|
+
if response is None:
|
|
38
|
+
raise ValueError("请求返回的响应为None")
|
|
39
|
+
|
|
40
|
+
if return_type == "json" :
|
|
41
|
+
result = response.json()
|
|
42
|
+
elif return_type == "response" :
|
|
43
|
+
return response
|
|
44
|
+
else :
|
|
45
|
+
result = response.text
|
|
46
|
+
return result
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: funcguard
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A funcguard for Python.
|
|
5
|
+
Home-page: https://github.com/tinycen/funcguard
|
|
6
|
+
Author: tinycen
|
|
7
|
+
Author-email: sky_ruocen@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
|
|
15
|
+
# FuncGuard
|
|
16
|
+
|
|
17
|
+
FuncGuard是一个Python库,提供了函数执行超时控制和重试机制的实用工具。
|
|
18
|
+
|
|
19
|
+
## 功能特点
|
|
20
|
+
|
|
21
|
+
- 函数执行超时控制
|
|
22
|
+
- 函数执行失败自动重试
|
|
23
|
+
- HTTP请求封装(支持自动重试)
|
|
24
|
+
|
|
25
|
+
## 安装/升级
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install funcguard
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install --upgrade funcguard
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
## 使用方法
|
|
37
|
+
|
|
38
|
+
### 超时控制
|
|
39
|
+
|
|
40
|
+
使用`timeout_handler`函数可以控制函数的执行时间,防止函数运行时间过长:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from funcguard.core import timeout_handler
|
|
44
|
+
|
|
45
|
+
def long_running_function():
|
|
46
|
+
# 模拟一个耗时操作
|
|
47
|
+
import time
|
|
48
|
+
time.sleep(10)
|
|
49
|
+
return "操作完成"
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# 设置超时时间为5秒
|
|
53
|
+
result = timeout_handler(long_running_function, execution_timeout=5)
|
|
54
|
+
print(result)
|
|
55
|
+
except TimeoutError as e:
|
|
56
|
+
print(f"捕获到超时错误: {e}")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 重试机制
|
|
60
|
+
|
|
61
|
+
使用`retry_function`函数可以在函数执行失败时自动重试:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from funcguard.core import retry_function
|
|
65
|
+
|
|
66
|
+
def unstable_function():
|
|
67
|
+
# 模拟一个可能失败的操作
|
|
68
|
+
import random
|
|
69
|
+
if random.random() < 0.7: # 70%的概率失败
|
|
70
|
+
raise Exception("随机错误")
|
|
71
|
+
return "操作成功"
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# 最多重试3次,每次执行超时时间为10秒
|
|
75
|
+
result = retry_function(unstable_function, max_retries=3, execute_timeout=10, task_name="测试任务")
|
|
76
|
+
print(result)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"重试后仍然失败: {e}")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### HTTP请求
|
|
82
|
+
|
|
83
|
+
使用`send_request`函数发送HTTP请求,支持自动重试:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from funcguard.tools import send_request
|
|
87
|
+
|
|
88
|
+
# 不使用重试
|
|
89
|
+
response = send_request(
|
|
90
|
+
method="GET",
|
|
91
|
+
url="https://api.example.com/data",
|
|
92
|
+
headers={"Content-Type": "application/json"},
|
|
93
|
+
timeout=30
|
|
94
|
+
)
|
|
95
|
+
print(response)
|
|
96
|
+
|
|
97
|
+
# 使用重试
|
|
98
|
+
response = send_request(
|
|
99
|
+
method="POST",
|
|
100
|
+
url="https://api.example.com/data",
|
|
101
|
+
headers={"Content-Type": "application/json"},
|
|
102
|
+
data={"key": "value"},
|
|
103
|
+
timeout=30,
|
|
104
|
+
auto_retry={
|
|
105
|
+
"task_name": "API请求",
|
|
106
|
+
"max_retries": 3,
|
|
107
|
+
"execute_timeout": 60
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
print(response)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API文档
|
|
114
|
+
|
|
115
|
+
### funcguard.core
|
|
116
|
+
|
|
117
|
+
#### timeout_handler(func, args=(), kwargs=None, execution_timeout=90)
|
|
118
|
+
|
|
119
|
+
- **参数**:
|
|
120
|
+
- `func`: 需要执行的目标函数
|
|
121
|
+
- `args`: 目标函数的位置参数,默认为空元组
|
|
122
|
+
- `kwargs`: 目标函数的关键字参数,默认为None
|
|
123
|
+
- `execution_timeout`: 函数执行的超时时间,单位为秒,默认为90秒
|
|
124
|
+
- **返回值**: 目标函数的返回值
|
|
125
|
+
- **异常**: `TimeoutError` - 当函数执行超过指定时间时抛出
|
|
126
|
+
|
|
127
|
+
#### retry_function(func, max_retries=5, execute_timeout=90, task_name="", *args, **kwargs)
|
|
128
|
+
|
|
129
|
+
- **参数**:
|
|
130
|
+
- `func`: 需要重试的函数
|
|
131
|
+
- `max_retries`: 最大重试次数,默认为5
|
|
132
|
+
- `execute_timeout`: 执行超时时间,默认为90秒
|
|
133
|
+
- `task_name`: 任务名称,用于打印日志
|
|
134
|
+
- `args`: func的位置参数
|
|
135
|
+
- `kwargs`: func的关键字参数
|
|
136
|
+
- **返回值**: func的返回值
|
|
137
|
+
- **异常**: 当重试次数用尽后仍然失败时,抛出最后一次的异常
|
|
138
|
+
|
|
139
|
+
### funcguard.tools
|
|
140
|
+
|
|
141
|
+
#### send_request(method, url, headers, data=None, return_type="json", timeout=60, auto_retry=None)
|
|
142
|
+
|
|
143
|
+
- **参数**:
|
|
144
|
+
- `method`: HTTP方法(GET, POST等)
|
|
145
|
+
- `url`: 请求URL
|
|
146
|
+
- `headers`: 请求头
|
|
147
|
+
- `data`: 请求数据,默认为None
|
|
148
|
+
- `return_type`: 返回类型,可选"json"、"response"或"text",默认为"json"
|
|
149
|
+
- `timeout`: 请求超时时间,单位为秒,默认为60
|
|
150
|
+
- `auto_retry`: 自动重试配置,格式为`{"task_name": "", "max_retries": 5, "execute_timeout": 90}`,默认为None
|
|
151
|
+
- **返回值**: 根据return_type参数返回不同格式的响应数据
|
|
152
|
+
- **异常**: 当请求失败且重试次数用尽后,抛出相应的异常
|
|
153
|
+
|
|
154
|
+
## 许可证
|
|
155
|
+
|
|
156
|
+
MIT License
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
funcguard/__init__.py
|
|
5
|
+
funcguard/core.py
|
|
6
|
+
funcguard/tools.py
|
|
7
|
+
funcguard.egg-info/PKG-INFO
|
|
8
|
+
funcguard.egg-info/SOURCES.txt
|
|
9
|
+
funcguard.egg-info/dependency_links.txt
|
|
10
|
+
funcguard.egg-info/not-zip-safe
|
|
11
|
+
funcguard.egg-info/requires.txt
|
|
12
|
+
funcguard.egg-info/top_level.txt
|
|
13
|
+
tests/__init__.py
|
|
14
|
+
tests/run_test.py
|
|
15
|
+
tests/test_core.py
|
|
16
|
+
tests/test_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests
|
funcguard-0.1.1/setup.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
# 读取README文件
|
|
4
|
+
try:
|
|
5
|
+
with open('README.md', 'r', encoding='utf-8') as f:
|
|
6
|
+
long_description = f.read()
|
|
7
|
+
except FileNotFoundError:
|
|
8
|
+
long_description = 'A funcguard for Python.'
|
|
9
|
+
|
|
10
|
+
setup(
|
|
11
|
+
name='funcguard',
|
|
12
|
+
version='0.1.1',
|
|
13
|
+
packages=find_packages(),
|
|
14
|
+
install_requires=[
|
|
15
|
+
'requests',
|
|
16
|
+
],
|
|
17
|
+
author='tinycen',
|
|
18
|
+
author_email='sky_ruocen@qq.com',
|
|
19
|
+
description='A funcguard for Python.',
|
|
20
|
+
long_description=long_description,
|
|
21
|
+
long_description_content_type='text/markdown',
|
|
22
|
+
url='https://github.com/tinycen/funcguard',
|
|
23
|
+
classifiers=[
|
|
24
|
+
'Programming Language :: Python :: 3',
|
|
25
|
+
'License :: OSI Approved :: MIT License',
|
|
26
|
+
'Operating System :: OS Independent',
|
|
27
|
+
],
|
|
28
|
+
python_requires='>=3.10',
|
|
29
|
+
include_package_data=True,
|
|
30
|
+
zip_safe=False,
|
|
31
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# 测试包初始化文件
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
测试运行脚本
|
|
4
|
+
运行所有测试用例
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import unittest
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
# 添加项目根目录到Python路径
|
|
12
|
+
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
13
|
+
sys.path.insert(0, project_root)
|
|
14
|
+
|
|
15
|
+
# 导入测试模块
|
|
16
|
+
from tests.test_core import TestTimeoutHandler, TestRetryFunction
|
|
17
|
+
from tests.test_tools import TestSendRequest
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run_all_tests():
|
|
21
|
+
"""运行所有测试"""
|
|
22
|
+
# 创建测试套件
|
|
23
|
+
loader = unittest.TestLoader()
|
|
24
|
+
suite = unittest.TestSuite()
|
|
25
|
+
|
|
26
|
+
# 添加测试类
|
|
27
|
+
suite.addTests(loader.loadTestsFromTestCase(TestTimeoutHandler))
|
|
28
|
+
suite.addTests(loader.loadTestsFromTestCase(TestRetryFunction))
|
|
29
|
+
suite.addTests(loader.loadTestsFromTestCase(TestSendRequest))
|
|
30
|
+
|
|
31
|
+
# 运行测试
|
|
32
|
+
runner = unittest.TextTestRunner(verbosity=2)
|
|
33
|
+
result = runner.run(suite)
|
|
34
|
+
|
|
35
|
+
return result.wasSuccessful()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == '__main__':
|
|
39
|
+
success = run_all_tests()
|
|
40
|
+
sys.exit(0 if success else 1)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from funcguard.core import timeout_handler, retry_function
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestTimeoutHandler(unittest.TestCase):
|
|
9
|
+
"""测试timeout_handler函数"""
|
|
10
|
+
|
|
11
|
+
def test_normal_execution(self):
|
|
12
|
+
"""测试正常执行的函数"""
|
|
13
|
+
def quick_function():
|
|
14
|
+
time.sleep(0.1)
|
|
15
|
+
return "success"
|
|
16
|
+
|
|
17
|
+
result = timeout_handler(quick_function, execution_timeout=2)
|
|
18
|
+
self.assertEqual(result, "success")
|
|
19
|
+
|
|
20
|
+
def test_timeout_execution(self):
|
|
21
|
+
"""测试超时的函数"""
|
|
22
|
+
def slow_function():
|
|
23
|
+
time.sleep(2)
|
|
24
|
+
return "should not reach here"
|
|
25
|
+
|
|
26
|
+
from funcguard.core import FuncguardTimeoutError
|
|
27
|
+
with self.assertRaises(FuncguardTimeoutError) as context:
|
|
28
|
+
timeout_handler(slow_function, execution_timeout=1)
|
|
29
|
+
|
|
30
|
+
self.assertIn("执行时间超过 1 秒", str(context.exception))
|
|
31
|
+
|
|
32
|
+
def test_function_with_args(self):
|
|
33
|
+
"""测试带参数的函数"""
|
|
34
|
+
def add_numbers(a, b):
|
|
35
|
+
return a + b
|
|
36
|
+
|
|
37
|
+
result = timeout_handler(add_numbers, args=(3, 4), execution_timeout=2)
|
|
38
|
+
self.assertEqual(result, 7)
|
|
39
|
+
|
|
40
|
+
def test_function_with_kwargs(self):
|
|
41
|
+
"""测试带关键字参数的函数"""
|
|
42
|
+
def greet(name, greeting="Hello"):
|
|
43
|
+
return f"{greeting}, {name}!"
|
|
44
|
+
|
|
45
|
+
result = timeout_handler(greet, kwargs={"name": "World", "greeting": "Hi"}, execution_timeout=2)
|
|
46
|
+
self.assertEqual(result, "Hi, World!")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestRetryFunction(unittest.TestCase):
|
|
50
|
+
"""测试retry_function函数"""
|
|
51
|
+
|
|
52
|
+
def test_successful_first_try(self):
|
|
53
|
+
"""测试第一次就成功的函数"""
|
|
54
|
+
def always_success():
|
|
55
|
+
return "success"
|
|
56
|
+
|
|
57
|
+
result = retry_function(always_success, max_retries=2, task_name="test")
|
|
58
|
+
self.assertEqual(result, "success")
|
|
59
|
+
|
|
60
|
+
def test_retry_until_success(self):
|
|
61
|
+
"""测试重试后成功的函数"""
|
|
62
|
+
attempts = []
|
|
63
|
+
|
|
64
|
+
def sometimes_fail():
|
|
65
|
+
attempts.append(len(attempts))
|
|
66
|
+
if len(attempts) < 2:
|
|
67
|
+
raise ValueError("Not yet")
|
|
68
|
+
return "finally success"
|
|
69
|
+
|
|
70
|
+
result = retry_function(sometimes_fail, max_retries=2, task_name="test")
|
|
71
|
+
self.assertEqual(result, "finally success")
|
|
72
|
+
self.assertEqual(len(attempts), 2)
|
|
73
|
+
|
|
74
|
+
def test_exhaust_all_retries(self):
|
|
75
|
+
"""测试耗尽所有重试次数"""
|
|
76
|
+
def always_fail():
|
|
77
|
+
raise RuntimeError("Always fails")
|
|
78
|
+
|
|
79
|
+
with self.assertRaises(RuntimeError) as context:
|
|
80
|
+
retry_function(always_fail, max_retries=2, task_name="test")
|
|
81
|
+
|
|
82
|
+
self.assertEqual(str(context.exception), "Always fails")
|
|
83
|
+
|
|
84
|
+
def test_timeout_in_retry(self):
|
|
85
|
+
"""测试重试中的超时处理"""
|
|
86
|
+
def timeout_function():
|
|
87
|
+
time.sleep(2)
|
|
88
|
+
return "should timeout"
|
|
89
|
+
|
|
90
|
+
# retry_function会在重试耗尽后抛出最后的异常
|
|
91
|
+
from funcguard.core import FuncguardTimeoutError
|
|
92
|
+
with self.assertRaises(FuncguardTimeoutError):
|
|
93
|
+
retry_function(timeout_function, max_retries=1, execute_timeout=1, task_name="test")
|
|
94
|
+
|
|
95
|
+
@patch('time.sleep')
|
|
96
|
+
def test_retry_with_custom_delay(self, mock_sleep):
|
|
97
|
+
"""测试重试延迟"""
|
|
98
|
+
def always_fail():
|
|
99
|
+
raise ValueError("test")
|
|
100
|
+
|
|
101
|
+
with self.assertRaises(ValueError):
|
|
102
|
+
retry_function(always_fail, max_retries=2, task_name="test")
|
|
103
|
+
|
|
104
|
+
# 验证sleep被调用了正确的次数
|
|
105
|
+
self.assertEqual(mock_sleep.call_count, 1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
if __name__ == '__main__':
|
|
109
|
+
unittest.main()
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from funcguard.tools import send_request
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MockResponse:
|
|
10
|
+
"""模拟requests响应"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, json_data=None, text="", status_code=200):
|
|
13
|
+
self.json_data = json_data or {}
|
|
14
|
+
self.text = text
|
|
15
|
+
self.status_code = status_code
|
|
16
|
+
|
|
17
|
+
def json(self):
|
|
18
|
+
return self.json_data
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestSendRequest(unittest.TestCase):
|
|
22
|
+
"""测试send_request函数"""
|
|
23
|
+
|
|
24
|
+
@patch('requests.request')
|
|
25
|
+
def test_get_request_json_response(self, mock_request):
|
|
26
|
+
"""测试GET请求返回JSON"""
|
|
27
|
+
mock_response = MockResponse(json_data={"key": "value"})
|
|
28
|
+
mock_request.return_value = mock_response
|
|
29
|
+
|
|
30
|
+
result = send_request(
|
|
31
|
+
method="GET",
|
|
32
|
+
url="https://api.example.com/data",
|
|
33
|
+
headers={"Content-Type": "application/json"}
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
self.assertEqual(result, {"key": "value"})
|
|
37
|
+
mock_request.assert_called_once()
|
|
38
|
+
|
|
39
|
+
@patch('requests.request')
|
|
40
|
+
def test_post_request_with_data(self, mock_request):
|
|
41
|
+
"""测试POST请求带数据"""
|
|
42
|
+
mock_response = MockResponse(json_data={"status": "created"})
|
|
43
|
+
mock_request.return_value = mock_response
|
|
44
|
+
|
|
45
|
+
result = send_request(
|
|
46
|
+
method="POST",
|
|
47
|
+
url="https://api.example.com/users",
|
|
48
|
+
headers={"Content-Type": "application/json"},
|
|
49
|
+
data={"name": "test", "email": "test@example.com"}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
self.assertEqual(result, {"status": "created"})
|
|
53
|
+
mock_request.assert_called_once()
|
|
54
|
+
|
|
55
|
+
@patch('requests.request')
|
|
56
|
+
def test_return_response_object(self, mock_request):
|
|
57
|
+
"""测试返回response对象"""
|
|
58
|
+
mock_response = MockResponse(text="raw response")
|
|
59
|
+
mock_request.return_value = mock_response
|
|
60
|
+
|
|
61
|
+
result = send_request(
|
|
62
|
+
method="GET",
|
|
63
|
+
url="https://api.example.com/raw",
|
|
64
|
+
headers={},
|
|
65
|
+
return_type="response"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.assertEqual(result, mock_response)
|
|
69
|
+
|
|
70
|
+
@patch('requests.request')
|
|
71
|
+
def test_return_text_response(self, mock_request):
|
|
72
|
+
"""测试返回文本响应"""
|
|
73
|
+
mock_response = MockResponse(text="plain text response")
|
|
74
|
+
mock_request.return_value = mock_response
|
|
75
|
+
|
|
76
|
+
result = send_request(
|
|
77
|
+
method="GET",
|
|
78
|
+
url="https://api.example.com/text",
|
|
79
|
+
headers={},
|
|
80
|
+
return_type="text"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
self.assertEqual(result, "plain text response")
|
|
84
|
+
|
|
85
|
+
@patch('requests.request')
|
|
86
|
+
def test_custom_timeout(self, mock_request):
|
|
87
|
+
"""测试自定义超时时间"""
|
|
88
|
+
mock_response = MockResponse()
|
|
89
|
+
mock_request.return_value = mock_response
|
|
90
|
+
|
|
91
|
+
send_request(
|
|
92
|
+
method="GET",
|
|
93
|
+
url="https://api.example.com/data",
|
|
94
|
+
headers={},
|
|
95
|
+
timeout=30
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
mock_request.assert_called_once()
|
|
99
|
+
|
|
100
|
+
@patch('funcguard.core.retry_function')
|
|
101
|
+
@patch('requests.request')
|
|
102
|
+
def test_auto_retry_enabled(self, mock_request, mock_retry):
|
|
103
|
+
"""测试启用自动重试"""
|
|
104
|
+
# 设置retry_function返回一个包含正确json数据的MockResponse对象
|
|
105
|
+
mock_response = MockResponse(json_data={"success": True})
|
|
106
|
+
mock_retry.return_value = mock_response
|
|
107
|
+
|
|
108
|
+
result = send_request(
|
|
109
|
+
method="POST",
|
|
110
|
+
url="https://api.example.com/data",
|
|
111
|
+
headers={"Content-Type": "application/json"},
|
|
112
|
+
data={"key": "value"},
|
|
113
|
+
auto_retry={
|
|
114
|
+
"task_name": "API测试",
|
|
115
|
+
"max_retries": 3,
|
|
116
|
+
"execute_timeout": 60
|
|
117
|
+
}
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# 验证结果
|
|
121
|
+
if hasattr(result, 'json_data'):
|
|
122
|
+
# 如果result是MockResponse对象,直接比较其json_data
|
|
123
|
+
self.assertEqual(result.json_data, {"success": True})
|
|
124
|
+
else:
|
|
125
|
+
# 否则直接比较result
|
|
126
|
+
self.assertEqual(result, {"success": True})
|
|
127
|
+
mock_retry.assert_called_once()
|
|
128
|
+
|
|
129
|
+
@patch('requests.request')
|
|
130
|
+
def test_none_response_raises_error(self, mock_request):
|
|
131
|
+
"""测试None响应抛出错误"""
|
|
132
|
+
mock_request.return_value = None
|
|
133
|
+
|
|
134
|
+
with self.assertRaises(ValueError) as context:
|
|
135
|
+
send_request("GET", "https://api.example.com/data", {})
|
|
136
|
+
|
|
137
|
+
self.assertEqual(str(context.exception), "请求返回的响应为None")
|
|
138
|
+
|
|
139
|
+
def test_invalid_return_type(self):
|
|
140
|
+
"""测试无效的返回类型"""
|
|
141
|
+
with patch('requests.request') as mock_request:
|
|
142
|
+
mock_response = MockResponse()
|
|
143
|
+
mock_request.return_value = mock_response
|
|
144
|
+
|
|
145
|
+
# 对于无效的return_type,函数会尝试访问response.text
|
|
146
|
+
result = send_request(
|
|
147
|
+
method="GET",
|
|
148
|
+
url="https://api.example.com/data",
|
|
149
|
+
headers={},
|
|
150
|
+
return_type="invalid"
|
|
151
|
+
)
|
|
152
|
+
self.assertEqual(result, "")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
if __name__ == '__main__':
|
|
156
|
+
unittest.main()
|