nonebot-plugin-htmlrender 0.6.1__tar.gz → 0.6.3__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.
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/PKG-INFO +1 -1
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/browser.py +36 -4
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/config.py +3 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/data_source.py +27 -12
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/pyproject.toml +1 -1
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_browser.py +24 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_htmlrender.py +74 -1
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/uv.lock +486 -444
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.github/workflows/codecov.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.github/workflows/publish.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.pre-commit-config.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/.env.dev +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/bot.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/compat.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/consts.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/install.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/process.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/signal.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/conftest.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/resources/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_deprecated_decorator.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_install.py +0 -0
- {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_process.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nonebot-plugin-htmlrender
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: 通过浏览器渲染图片
|
|
5
5
|
Project-URL: Homepage, https://github.com/kexue-z/nonebot-plugin-htmlrender
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/kexue-z/nonebot-plugin-htmlrender/issues
|
|
@@ -53,15 +53,13 @@ async def init_browser(**kwargs) -> Browser:
|
|
|
53
53
|
Raises:
|
|
54
54
|
RuntimeError: 如果浏览器无法启动或安装失败。
|
|
55
55
|
"""
|
|
56
|
-
await install_browser() # update playwright when start
|
|
57
|
-
await check_playwright_env()
|
|
58
56
|
return await start_browser(**kwargs)
|
|
59
57
|
|
|
60
58
|
|
|
61
59
|
@asynccontextmanager
|
|
62
60
|
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
63
61
|
"""
|
|
64
|
-
|
|
62
|
+
获取一个新的页面的上下文管理器, 这里的 page 默认使用设备缩放因子为 2。
|
|
65
63
|
|
|
66
64
|
Args:
|
|
67
65
|
device_scale_factor (float): 设备缩放因子。
|
|
@@ -117,6 +115,31 @@ async def _connect_via_cdp(**kwargs) -> Browser:
|
|
|
117
115
|
raise RuntimeError("Playwright 未初始化")
|
|
118
116
|
|
|
119
117
|
|
|
118
|
+
async def _connect(browser_type: str, **kwargs) -> Browser:
|
|
119
|
+
"""
|
|
120
|
+
通过 Playwright 协议连接浏览器。
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
browser_type (str): 浏览器类型。
|
|
124
|
+
**kwargs: 传递给`playwright.connect`的关键字参数。
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Browser: 启动的浏览器实例。
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
RuntimeError: 如果 Playwright 未初始化。
|
|
131
|
+
"""
|
|
132
|
+
_browser_cls: BrowserType = getattr(_playwright, browser_type)
|
|
133
|
+
kwargs["ws_endpoint"] = plugin_config.htmlrender_connect
|
|
134
|
+
logger.info(
|
|
135
|
+
f"正在使用 Playwright 协议连接 {browser_type}({plugin_config.htmlrender_connect})"
|
|
136
|
+
)
|
|
137
|
+
if _playwright is not None:
|
|
138
|
+
return await _browser_cls.connect(**kwargs)
|
|
139
|
+
else:
|
|
140
|
+
raise RuntimeError("Playwright 未初始化")
|
|
141
|
+
|
|
142
|
+
|
|
120
143
|
async def start_browser(**kwargs) -> Browser:
|
|
121
144
|
"""
|
|
122
145
|
启动 Playwright 浏览器实例。
|
|
@@ -136,6 +159,9 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
136
159
|
):
|
|
137
160
|
return await _connect_via_cdp(**kwargs)
|
|
138
161
|
|
|
162
|
+
if plugin_config.htmlrender_connect:
|
|
163
|
+
return await _connect(plugin_config.htmlrender_browser, **kwargs)
|
|
164
|
+
|
|
139
165
|
if plugin_config.htmlrender_browser_channel:
|
|
140
166
|
kwargs["channel"] = plugin_config.htmlrender_browser_channel
|
|
141
167
|
|
|
@@ -144,6 +170,12 @@ async def start_browser(**kwargs) -> Browser:
|
|
|
144
170
|
|
|
145
171
|
if plugin_config.htmlrender_browser_executable_path:
|
|
146
172
|
kwargs["executable_path"] = plugin_config.htmlrender_browser_executable_path
|
|
173
|
+
else:
|
|
174
|
+
try:
|
|
175
|
+
await check_playwright_env()
|
|
176
|
+
except RuntimeError:
|
|
177
|
+
await install_browser()
|
|
178
|
+
await check_playwright_env()
|
|
147
179
|
|
|
148
180
|
_browser = await _launch(plugin_config.htmlrender_browser, **kwargs)
|
|
149
181
|
return _browser
|
|
@@ -164,7 +196,7 @@ async def check_playwright_env():
|
|
|
164
196
|
logger.info("Checking Playwright environment...")
|
|
165
197
|
try:
|
|
166
198
|
async with async_playwright() as p:
|
|
167
|
-
await p.
|
|
199
|
+
await getattr(p, plugin_config.htmlrender_browser).launch()
|
|
168
200
|
except Exception as e:
|
|
169
201
|
raise RuntimeError(
|
|
170
202
|
"Playwright environment is not set up correctly. "
|
|
@@ -36,6 +36,9 @@ class Config(BaseModel):
|
|
|
36
36
|
htmlrender_connect_over_cdp: Optional[str] = Field(
|
|
37
37
|
default=None, description="通过 CDP 连接Playwright浏览器的端点地址。"
|
|
38
38
|
)
|
|
39
|
+
htmlrender_connect: Optional[str] = Field(
|
|
40
|
+
default=None, description="通过Playwright协议连接Playwright浏览器的端点地址。"
|
|
41
|
+
)
|
|
39
42
|
|
|
40
43
|
@model_validator(mode="after")
|
|
41
44
|
def check_browser_channel(self) -> Self:
|
|
@@ -284,17 +284,32 @@ async def template_to_pic(
|
|
|
284
284
|
async def capture_element(
|
|
285
285
|
url: str,
|
|
286
286
|
element: str,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
screenshot_timeout: Optional[float] = 30_000,
|
|
291
|
-
**kwargs,
|
|
287
|
+
page_kwargs: Optional[dict] = None,
|
|
288
|
+
goto_kwargs: Optional[dict] = None,
|
|
289
|
+
screenshot_kwargs: Optional[dict] = None,
|
|
292
290
|
) -> bytes:
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
291
|
+
"""捕获网页中指定元素的截图, 通过CSS选择器或XPath表达式指定元素。
|
|
292
|
+
|
|
293
|
+
Args:
|
|
294
|
+
url: 目标网页URL
|
|
295
|
+
element: CSS选择器或XPath表达式
|
|
296
|
+
page_kwargs: 传递给get_new_page的参数
|
|
297
|
+
goto_kwargs: 传递给page.goto方法的额外参数
|
|
298
|
+
screenshot_kwargs: 传递给screenshot方法的额外参数
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
bytes: 元素截图数据
|
|
302
|
+
"""
|
|
303
|
+
page_kwargs = page_kwargs or {}
|
|
304
|
+
goto_kwargs = goto_kwargs or {}
|
|
305
|
+
screenshot_kwargs = screenshot_kwargs or {}
|
|
306
|
+
|
|
307
|
+
async with get_new_page(**page_kwargs) as page:
|
|
308
|
+
page.on(
|
|
309
|
+
"console",
|
|
310
|
+
lambda msg: logger.opt(colors=True).debug(
|
|
311
|
+
f"<cyan>[Browser Console]</cyan> {msg.text}"
|
|
312
|
+
),
|
|
300
313
|
)
|
|
314
|
+
await page.goto(url, **goto_kwargs)
|
|
315
|
+
return await page.locator(element).screenshot(**screenshot_kwargs)
|
|
@@ -39,6 +39,7 @@ def browser_config() -> dict[str, str]:
|
|
|
39
39
|
return {
|
|
40
40
|
"browser": "chromium",
|
|
41
41
|
"cdp": "ws://localhost:9222",
|
|
42
|
+
"pwp": "ws://localhost:3000/chromium/playwright",
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
|
|
@@ -169,6 +170,29 @@ async def test_connect_via_cdp(
|
|
|
169
170
|
assert browser == mock_browser
|
|
170
171
|
|
|
171
172
|
|
|
173
|
+
@pytest.mark.asyncio
|
|
174
|
+
async def test_connect(
|
|
175
|
+
mocker: MockerFixture, mock_browser: Browser, browser_config: dict[str, str]
|
|
176
|
+
) -> None:
|
|
177
|
+
"""测试通过Playwright协议连接浏览器"""
|
|
178
|
+
from nonebot_plugin_htmlrender.browser import start_browser
|
|
179
|
+
|
|
180
|
+
mocker.patch(
|
|
181
|
+
"nonebot_plugin_htmlrender.browser._connect", return_value=mock_browser
|
|
182
|
+
)
|
|
183
|
+
mocker.patch(
|
|
184
|
+
"nonebot_plugin_htmlrender.browser.plugin_config.htmlrender_browser",
|
|
185
|
+
browser_config["browser"],
|
|
186
|
+
)
|
|
187
|
+
mocker.patch(
|
|
188
|
+
"nonebot_plugin_htmlrender.browser.plugin_config.htmlrender_connect",
|
|
189
|
+
browser_config["pwp"],
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
browser = await start_browser()
|
|
193
|
+
assert browser == mock_browser
|
|
194
|
+
|
|
195
|
+
|
|
172
196
|
@pytest.mark.parametrize(
|
|
173
197
|
("proxy_url", "expected"),
|
|
174
198
|
[
|
{nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_htmlrender.py
RENAMED
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
from nonebug import App
|
|
6
6
|
from PIL import Image, ImageChops
|
|
7
7
|
import pytest
|
|
8
|
+
from pytest_mock import MockerFixture
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
@pytest.fixture
|
|
@@ -39,7 +40,7 @@ def template_resources(request: Any) -> tuple[str, str, list[str]]:
|
|
|
39
40
|
elif template_type == "text":
|
|
40
41
|
template_name = "text.html"
|
|
41
42
|
data_list = ["1", "2", "3", "4"]
|
|
42
|
-
else:
|
|
43
|
+
else: # pragma: no cover
|
|
43
44
|
raise ValueError(f"Unsupported template type: {template_type}")
|
|
44
45
|
|
|
45
46
|
return template_path, template_name, data_list
|
|
@@ -137,3 +138,75 @@ async def test_template_filter(
|
|
|
137
138
|
image = Image.open(BytesIO(initial_bytes=image_byte))
|
|
138
139
|
diff = ImageChops.difference(image, test_image)
|
|
139
140
|
assert diff.getbbox() is None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@pytest.mark.asyncio
|
|
144
|
+
async def test_capture_element(mocker: MockerFixture) -> None:
|
|
145
|
+
"""测试网页元素捕获功能"""
|
|
146
|
+
from nonebot_plugin_htmlrender.data_source import capture_element
|
|
147
|
+
|
|
148
|
+
mock_screenshot = b"test_image_bytes"
|
|
149
|
+
|
|
150
|
+
mock_locator = mocker.AsyncMock()
|
|
151
|
+
mock_locator.screenshot.return_value = mock_screenshot
|
|
152
|
+
|
|
153
|
+
mock_page = mocker.AsyncMock()
|
|
154
|
+
mock_page.goto = mocker.AsyncMock()
|
|
155
|
+
mock_page.on = mocker.AsyncMock()
|
|
156
|
+
mock_page.locator = mocker.MagicMock(return_value=mock_locator)
|
|
157
|
+
|
|
158
|
+
mock_cm = mocker.MagicMock()
|
|
159
|
+
mock_cm.__aenter__ = mocker.AsyncMock(return_value=mock_page)
|
|
160
|
+
mock_cm.__aexit__ = mocker.AsyncMock(return_value=None)
|
|
161
|
+
|
|
162
|
+
mocker.patch(
|
|
163
|
+
"nonebot_plugin_htmlrender.data_source.get_new_page", return_value=mock_cm
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
result = await capture_element("https://example.com", "#target-element")
|
|
167
|
+
|
|
168
|
+
assert result == mock_screenshot
|
|
169
|
+
mock_page.goto.assert_called_once_with("https://example.com")
|
|
170
|
+
mock_page.locator.assert_called_once_with("#target-element")
|
|
171
|
+
mock_locator.screenshot.assert_called_once_with()
|
|
172
|
+
|
|
173
|
+
mock_page.goto.reset_mock()
|
|
174
|
+
mock_page.locator.reset_mock()
|
|
175
|
+
mock_locator.screenshot.reset_mock()
|
|
176
|
+
|
|
177
|
+
page_kwargs = {"device_scale_factor": 2.0}
|
|
178
|
+
goto_kwargs = {"timeout": 5000}
|
|
179
|
+
screenshot_kwargs = {"type": "jpeg", "quality": 80}
|
|
180
|
+
|
|
181
|
+
result = await capture_element(
|
|
182
|
+
"https://example.com",
|
|
183
|
+
"//div[@id='xpath-element']",
|
|
184
|
+
page_kwargs=page_kwargs,
|
|
185
|
+
goto_kwargs=goto_kwargs,
|
|
186
|
+
screenshot_kwargs=screenshot_kwargs,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert result == mock_screenshot
|
|
190
|
+
mock_page.goto.assert_called_once_with("https://example.com", timeout=5000)
|
|
191
|
+
mock_page.locator.assert_called_once_with("//div[@id='xpath-element']")
|
|
192
|
+
mock_locator.screenshot.assert_called_once_with(type="jpeg", quality=80)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@pytest.mark.asyncio
|
|
196
|
+
async def test_capture_element_exceptions_propagate(mocker: MockerFixture) -> None:
|
|
197
|
+
"""测试网页元素捕获时的异常能正确传递"""
|
|
198
|
+
from playwright.async_api import Error
|
|
199
|
+
|
|
200
|
+
from nonebot_plugin_htmlrender.data_source import capture_element
|
|
201
|
+
|
|
202
|
+
mock_cm = mocker.MagicMock()
|
|
203
|
+
mock_cm.__aenter__ = mocker.AsyncMock(side_effect=Error("Browser error"))
|
|
204
|
+
|
|
205
|
+
mocker.patch(
|
|
206
|
+
"nonebot_plugin_htmlrender.data_source.get_new_page", return_value=mock_cm
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
with pytest.raises(Error) as exc_info:
|
|
210
|
+
await capture_element("https://example.com", "#element")
|
|
211
|
+
|
|
212
|
+
assert "Browser error" in str(exc_info.value)
|