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.
Files changed (55) hide show
  1. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/PKG-INFO +1 -1
  2. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/browser.py +36 -4
  3. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/config.py +3 -0
  4. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/data_source.py +27 -12
  5. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/pyproject.toml +1 -1
  6. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_browser.py +24 -0
  7. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_htmlrender.py +74 -1
  8. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/uv.lock +486 -444
  9. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.github/workflows/codecov.yml +0 -0
  10. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.github/workflows/publish.yml +0 -0
  11. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.gitignore +0 -0
  12. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.pre-commit-config.yaml +0 -0
  13. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/.python-version +0 -0
  14. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/LICENSE +0 -0
  15. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/README.md +0 -0
  16. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/example.md +0 -0
  17. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/html2pic.png +0 -0
  18. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/md2pic.png +0 -0
  19. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/template2pic.png +0 -0
  20. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/docs/text2pic.png +0 -0
  21. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/.env.dev +0 -0
  22. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/bot.py +0 -0
  23. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/__init__.py +0 -0
  24. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/html2pic.html +0 -0
  25. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/markdown.css +0 -0
  26. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/mystyle.css +0 -0
  27. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/progress.html.jinja2 +0 -0
  28. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/templates/text.html +0 -0
  29. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/example/plugins/render/utils.py +0 -0
  30. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/__init__.py +0 -0
  31. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/compat.py +0 -0
  32. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/consts.py +0 -0
  33. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/install.py +0 -0
  34. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/process.py +0 -0
  35. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/signal.py +0 -0
  36. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
  37. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
  38. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
  39. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
  40. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
  41. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
  42. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
  43. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/text.css +0 -0
  44. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/templates/text.html +0 -0
  45. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/nonebot_plugin_htmlrender/utils.py +0 -0
  46. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/__init__.py +0 -0
  47. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/conftest.py +0 -0
  48. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/resources/test_template_filter.png +0 -0
  49. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/markdown.css +0 -0
  50. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/mystyle.css +0 -0
  51. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/progress.html.jinja2 +0 -0
  52. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/templates/text.html +0 -0
  53. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_deprecated_decorator.py +0 -0
  54. {nonebot_plugin_htmlrender-0.6.1 → nonebot_plugin_htmlrender-0.6.3}/tests/test_install.py +0 -0
  55. {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.1
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.chromium.launch()
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
- timeout: float = 0,
288
- type: Literal["jpeg", "png"] = "png",
289
- quality: Union[int, None] = None,
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
- async with get_new_page(**kwargs) as page:
294
- page.on("console", lambda msg: logger.debug(f"浏览器控制台: {msg.text}"))
295
- await page.goto(url, timeout=timeout)
296
- return await page.locator(element).screenshot(
297
- type=type,
298
- quality=quality,
299
- timeout=screenshot_timeout,
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)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-htmlrender"
3
- version = "0.6.1"
3
+ version = "0.6.3"
4
4
  description = "通过浏览器渲染图片"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "kexue", email = "x@kexue-cloud.cn" }]
@@ -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
  [
@@ -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)