Skip to content

Commit 36e3600

Browse files
committed
ad
1 parent 2f07126 commit 36e3600

11 files changed

Lines changed: 572 additions & 33 deletions

File tree

README.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,34 @@
33
![PyPI version](https://img.shields.io/pypi/v/py-cdp-reactive-flow-bot.svg)
44
[![Documentation Status](https://readthedocs.org/projects/py-cdp-reactive-flow-bot/badge/?version=latest)](https://py-cdp-reactive-flow-bot.readthedocs.io/en/latest/?version=latest)
55

6+
## English
7+
68
Python Reactive RxPy flow
79

810
* PyPI package: https://pypi.org/project/py-cdp-reactive-flow-bot/
911
* Free software: MIT License
1012
* Documentation: https://py-cdp-reactive-flow-bot.readthedocs.io.
1113

12-
## Features
14+
### Features
1315

1416
* TODO
1517

16-
## Credits
18+
### Credits
1719

1820
This package was created with [Cookiecutter](https://github.com/audreyfeldroy/cookiecutter) and the [audreyfeldroy/cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage) project template.
21+
22+
## 中文
23+
24+
Python响应式RxPy流
25+
26+
* PyPI包: https://pypi.org/project/py-cdp-reactive-flow-bot/
27+
* 自由软件: MIT许可证
28+
* 文档: https://py-cdp-reactive-flow-bot.readthedocs.io.
29+
30+
### 功能特性
31+
32+
* 待开发
33+
34+
### 鸣谢
35+
36+
本包使用[Cookiecutter](https://github.com/audreyfeldroy/cookiecutter)[audreyfeldroy/cookiecutter-pypackage](https://github.com/audreyfeldroy/cookiecutter-pypackage)项目模板创建。

simple_test.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import asyncio
2+
import time
3+
from typing import Dict, Any
4+
5+
# 直接模拟engine.py中的关键部分进行测试
6+
class TaskState:
7+
"""模拟TaskState枚举"""
8+
RUNNING = type('TaskStateEnum', (), {'name': 'RUNNING'})()
9+
FAILED = type('TaskStateEnum', (), {'name': 'FAILED'})()
10+
11+
class MockReactiveFramework:
12+
"""简化的框架实现,只测试重试和超时机制"""
13+
14+
def __init__(self):
15+
self.state_updates = []
16+
17+
def _emit_state_update(self, step_index: int, state: Any, data: Any):
18+
"""模拟状态更新"""
19+
update = {"step_index": step_index, "state": state.name, "data": data}
20+
self.state_updates.append(update)
21+
print(f"状态更新: {update}")
22+
23+
async def _execute_single_step(self, step: Dict, step_index: int) -> Dict[str, Any]:
24+
"""模拟执行单个步骤"""
25+
processor = step.get("processor")
26+
if not processor:
27+
raise ValueError("步骤必须包含processor")
28+
return await processor({})
29+
30+
def _create_step_observable(self, step: Dict, step_index: int) -> Any:
31+
"""模拟_create_step_observable,使用简单的异步函数实现重试机制"""
32+
# 从step配置中获取重试和超时参数,每个step可以完全自定义这些值
33+
max_retries = step.get("max_retries", 3) # 最大重试次数,默认3次
34+
timeout = step.get("timeout", 30000) # 超时时间,默认30秒
35+
36+
print(f"步骤 {step_index} 配置: max_retries={max_retries}, timeout={timeout}ms")
37+
38+
# 这个函数模拟Observable的行为
39+
async def execute_with_retry():
40+
retries = 0
41+
start_time = time.time()
42+
43+
while True:
44+
try:
45+
# 检查超时
46+
elapsed = (time.time() - start_time) * 1000 # 转换为毫秒
47+
if elapsed > timeout:
48+
raise asyncio.TimeoutError(f"执行超时: {elapsed:.2f}ms > {timeout}ms")
49+
50+
# 执行步骤
51+
result = await self._execute_single_step(step, step_index)
52+
self._emit_state_update(step_index, TaskState.RUNNING, result)
53+
return result
54+
55+
except Exception as e:
56+
# 更新失败状态
57+
self._emit_state_update(step_index, TaskState.FAILED, str(e))
58+
59+
# 检查是否需要重试
60+
if retries < max_retries:
61+
retries += 1
62+
print(f"步骤 {step_index} 失败,将重试 {retries}/{max_retries}...")
63+
await asyncio.sleep(0.1) # 简单延迟
64+
else:
65+
print(f"步骤 {step_index} 达到最大重试次数 {max_retries}")
66+
raise
67+
68+
# 返回可执行的协程对象
69+
return execute_with_retry
70+
71+
# 测试用的处理器
72+
async def create_flaky_processor(fail_count: int = 2):
73+
"""创建一个会失败指定次数然后成功的处理器"""
74+
class Counter:
75+
def __init__(self):
76+
self.count = 0
77+
78+
counter = Counter()
79+
80+
async def processor(ctx: Dict[str, Any]):
81+
counter.count += 1
82+
print(f"处理器被调用: 第 {counter.count} 次")
83+
if counter.count <= fail_count:
84+
raise Exception(f"模拟失败 #{counter.count}")
85+
return {"result": "success", "call_count": counter.count}
86+
87+
return processor
88+
89+
async def timeout_processor(ctx: Dict[str, Any]):
90+
"""模拟一个会超时的处理器"""
91+
print("超时处理器被调用,将休眠3秒...")
92+
await asyncio.sleep(3) # 休眠3秒,对于短超时会触发超时
93+
return {"result": "success"}
94+
95+
# 主测试函数
96+
async def run_test():
97+
# 创建框架实例
98+
framework = MockReactiveFramework()
99+
100+
# 定义测试步骤
101+
steps = [
102+
{
103+
"name": "默认重试步骤",
104+
"processor": await create_flaky_processor(2),
105+
},
106+
{
107+
"name": "自定义重试步骤",
108+
"max_retries": 5,
109+
"timeout": 10000, # 10秒
110+
"processor": await create_flaky_processor(3),
111+
},
112+
{
113+
"name": "无重试步骤",
114+
"max_retries": 0,
115+
"timeout": 2000, # 2秒
116+
"processor": timeout_processor,
117+
}
118+
]
119+
120+
# 运行测试
121+
print("\n===== 开始测试 =====\n")
122+
123+
for i, step in enumerate(steps):
124+
print(f"\n测试步骤 {i}: {step['name']}")
125+
try:
126+
# 获取并执行observable
127+
observable_fn = framework._create_step_observable(step, i)
128+
result = await observable_fn()
129+
print(f"✓ 步骤 {i} 成功: {result}")
130+
except Exception as e:
131+
print(f"✗ 步骤 {i} 失败: {str(e)}")
132+
133+
print("\n===== 测试完成 =====\n")
134+
print(f"总共记录了 {len(framework.state_updates)} 个状态更新")
135+
136+
# 运行测试
137+
if __name__ == "__main__":
138+
asyncio.run(run_test())
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3+
# @time : 2024/8/21 16:32
4+
# @author : timger/yishenggudou
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3+
# @time : 2024/8/21 16:32
4+
# @author : timger/yishenggudou
Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,180 @@
1-
#!/usr/bin/env python3
1+
#!/usr/bin/env python
22
# -*- coding: utf-8 -*-
3+
# @time : 2024/8/21 16:32
4+
# @author : timger/yishenggudou
5+
import asyncio
6+
import logging
7+
import sys
8+
from pathlib import Path
9+
import yaml
10+
import typer
11+
from typing import Optional
12+
from py_cdp_reactive_flow_bot.engine import ReactiveAutomationFramework
13+
14+
# 配置日志
15+
logging.basicConfig(
16+
level=logging.INFO,
17+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
18+
handlers=[
19+
logging.StreamHandler(sys.stdout)
20+
]
21+
)
22+
logger = logging.getLogger(__name__)
23+
24+
# 创建Typer应用实例
25+
app = typer.Typer(
26+
name="py-cdp-reactive-flow-bot",
27+
help="基于CDP的响应式自动化工作流机器人",
28+
add_completion=False
29+
)
30+
31+
# 主命令,支持cdp endpoint和playbook参数
32+
@app.command("run")
33+
def run(
34+
playbook: Path = typer.Argument(
35+
..., # 必填参数
36+
help="YAML格式的playbook脚本文件路径",
37+
exists=True,
38+
readable=True,
39+
resolve_path=True
40+
),
41+
cdp_endpoint: Optional[str] = typer.Option(
42+
None,
43+
"--cdp-endpoint",
44+
"-c",
45+
help="CDP服务端点地址,例如: http://localhost:9222"
46+
),
47+
debug: bool = typer.Option(
48+
False,
49+
"--debug",
50+
help="启用调试日志模式"
51+
)
52+
):
53+
"""运行指定的playbook自动化脚本"""
54+
# 启用调试模式时设置日志级别为DEBUG
55+
if debug:
56+
logger.setLevel(logging.DEBUG)
57+
logging.getLogger("py_cdp_reactive_flow_bot").setLevel(logging.DEBUG)
58+
59+
logger.info(f"开始执行playbook: {playbook}")
60+
logger.debug(f"CDP端点配置: {cdp_endpoint or '使用默认Playwright浏览器'}")
61+
62+
try:
63+
# 读取并解析YAML playbook文件
64+
playbook_config = load_playbook(playbook)
65+
66+
# 运行主程序逻辑
67+
asyncio.run(run_playbook(playbook_config, cdp_endpoint))
68+
69+
logger.info("Playbook执行完成")
70+
return 0
71+
except Exception as e:
72+
logger.error(f"执行失败: {str(e)}")
73+
return 1
74+
75+
def load_playbook(playbook_path: Path) -> dict:
76+
"""加载并解析YAML格式的playbook文件"""
77+
logger.debug(f"加载playbook文件: {playbook_path}")
78+
try:
79+
with open(playbook_path, 'r', encoding='utf-8') as f:
80+
return yaml.safe_load(f)
81+
except yaml.YAMLError as e:
82+
logger.error(f"解析YAML文件失败: {e}")
83+
raise ValueError(f"无效的YAML文件格式: {playbook_path}") from e
84+
except Exception as e:
85+
logger.error(f"读取playbook文件失败: {e}")
86+
raise IOError(f"无法读取文件: {playbook_path}") from e
87+
88+
async def run_playbook(playbook_config: dict, cdp_endpoint: Optional[str]):
89+
"""运行playbook自动化脚本
90+
91+
Args:
92+
playbook_config: 解析后的playbook配置
93+
cdp_endpoint: CDP服务端点地址,如果为None则启动新的浏览器实例
94+
"""
95+
# 初始化框架
96+
framework = ReactiveAutomationFramework()
97+
98+
# 创建事件用于等待执行完成
99+
completion_event = asyncio.Event()
100+
error_occurred = None
101+
102+
try:
103+
# 初始化浏览器环境,传入cdp_endpoint参数
104+
await framework.initialize(cdp_endpoint=cdp_endpoint)
105+
106+
# 创建并执行DSL执行器
107+
logger.debug("创建DSL执行器")
108+
executor = framework.create_dsl_executor(playbook_config)
109+
110+
# 订阅执行状态更新
111+
def on_state_update(update):
112+
step_index = update["step_index"]
113+
state = update["state"]
114+
result = update.get("result")
115+
logger.info(f"步骤 #{step_index} 状态: {state.name}")
116+
if result:
117+
logger.debug(f" 结果: {result}")
118+
119+
state_subscription = framework.state_stream.subscribe(on_state_update)
120+
121+
# 订阅执行器,处理结果和错误
122+
def on_next(result):
123+
logger.debug(f"执行器产生结果: {result}")
124+
125+
def on_error(error):
126+
nonlocal error_occurred
127+
logger.error(f"执行器出错: {error}")
128+
error_occurred = error
129+
completion_event.set()
130+
131+
def on_completed():
132+
logger.info("执行器完成所有任务")
133+
completion_event.set()
134+
135+
# 订阅执行流
136+
executor_subscription = executor.subscribe(
137+
on_next=on_next,
138+
on_error=on_error,
139+
on_completed=on_completed
140+
)
141+
142+
# 等待执行完成或超时
143+
logger.info("开始执行playbook...")
144+
145+
# 从playbook配置中获取超时时间,如果没有设置则使用默认值300秒
146+
timeout = playbook_config.get("timeout", 300)
147+
logger.debug(f"设置执行超时时间: {timeout}秒")
148+
149+
try:
150+
# 等待执行完成,带超时保护
151+
await asyncio.wait_for(completion_event.wait(), timeout=timeout)
152+
153+
# 检查是否有错误发生
154+
if error_occurred:
155+
raise error_occurred
156+
157+
except asyncio.TimeoutError:
158+
logger.error(f"执行超时: 超过{timeout}秒未完成")
159+
raise TimeoutError(f"Playbook执行超时,超过{timeout}秒")
160+
161+
logger.info("Playbook执行成功完成")
162+
163+
finally:
164+
# 确保清理资源
165+
if 'state_subscription' in locals():
166+
state_subscription.dispose()
167+
if 'executor_subscription' in locals():
168+
executor_subscription.dispose()
169+
# 关闭框架资源
170+
await framework.close()
171+
172+
@app.command("version")
173+
def version():
174+
"""显示当前版本信息"""
175+
from py_cdp_reactive_flow_bot import __version__
176+
typer.echo(f"py-cdp-reactive-flow-bot v{__version__}")
177+
return 0
178+
179+
if __name__ == "__main__":
180+
sys.exit(app())

0 commit comments

Comments
 (0)