nonebot-plugin-htmlrender 0.6.0b1__tar.gz → 0.6.2__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.0b1 → nonebot_plugin_htmlrender-0.6.2}/PKG-INFO +1 -1
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/__init__.py +4 -4
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/browser.py +20 -17
- nonebot_plugin_htmlrender-0.6.2/nonebot_plugin_htmlrender/compat.py +24 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/config.py +24 -43
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/consts.py +2 -2
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/data_source.py +27 -12
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/install.py +23 -14
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/pyproject.toml +1 -1
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/test_browser.py +2 -45
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/test_htmlrender.py +74 -1
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/test_install.py +2 -2
- nonebot_plugin_htmlrender-0.6.2/uv.lock +1729 -0
- nonebot_plugin_htmlrender-0.6.0b1/tests/test_config_custom_validation.py +0 -120
- nonebot_plugin_htmlrender-0.6.0b1/uv.lock +0 -1673
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/.github/workflows/codecov.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/.github/workflows/publish.yml +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/.gitignore +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/.pre-commit-config.yaml +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/.python-version +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/LICENSE +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/README.md +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/docs/example.md +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/docs/html2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/docs/md2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/docs/template2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/docs/text2pic.png +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/.env.dev +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/bot.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/html2pic.html +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/example/plugins/render/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/process.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/signal.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/text.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/nonebot_plugin_htmlrender/utils.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/__init__.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/conftest.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/resources/test_template_filter.png +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/templates/markdown.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/templates/mystyle.css +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/templates/progress.html.jinja2 +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/templates/text.html +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/tests/test_deprecated_decorator.py +0 -0
- {nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/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.2
|
|
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
|
|
@@ -30,16 +30,16 @@ driver = nonebot.get_driver()
|
|
|
30
30
|
|
|
31
31
|
@driver.on_startup
|
|
32
32
|
async def init(**kwargs):
|
|
33
|
-
logger.info("
|
|
33
|
+
logger.info("HTMLRender Starting...")
|
|
34
34
|
await init_browser(**kwargs)
|
|
35
|
-
logger.info("
|
|
35
|
+
logger.info("HTMLRender Started.")
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
@driver.on_shutdown
|
|
39
39
|
async def shutdown():
|
|
40
|
-
logger.info("
|
|
40
|
+
logger.info("HTMLRender Shutting down...")
|
|
41
41
|
await shutdown_browser()
|
|
42
|
-
logger.info("
|
|
42
|
+
logger.info("HTMLRender Shut down.")
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
__all__ = [
|
|
@@ -6,13 +6,12 @@ from nonebot.log import logger
|
|
|
6
6
|
from playwright.async_api import (
|
|
7
7
|
Browser,
|
|
8
8
|
BrowserType,
|
|
9
|
-
Error,
|
|
10
9
|
Page,
|
|
11
10
|
Playwright,
|
|
12
11
|
async_playwright,
|
|
13
12
|
)
|
|
14
13
|
|
|
15
|
-
from nonebot_plugin_htmlrender.config import
|
|
14
|
+
from nonebot_plugin_htmlrender.config import plugin_config
|
|
16
15
|
from nonebot_plugin_htmlrender.install import install_browser
|
|
17
16
|
from nonebot_plugin_htmlrender.utils import proxy_settings, suppress_and_log
|
|
18
17
|
|
|
@@ -20,12 +19,12 @@ _browser: Optional[Browser] = None
|
|
|
20
19
|
_playwright: Optional[Playwright] = None
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
async def _launch(browser_type:
|
|
22
|
+
async def _launch(browser_type: str, **kwargs) -> Browser:
|
|
24
23
|
"""
|
|
25
24
|
启动浏览器实例。
|
|
26
25
|
|
|
27
26
|
Args:
|
|
28
|
-
browser_type (
|
|
27
|
+
browser_type (str): 浏览器类型。
|
|
29
28
|
**kwargs: 传递给`playwright.launch`的关键字参数。
|
|
30
29
|
|
|
31
30
|
Returns:
|
|
@@ -54,24 +53,15 @@ async def init_browser(**kwargs) -> Browser:
|
|
|
54
53
|
Raises:
|
|
55
54
|
RuntimeError: 如果浏览器无法启动或安装失败。
|
|
56
55
|
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if "Executable doesn't exist" not in str(e):
|
|
61
|
-
raise RuntimeError("无法启动浏览器实例") from e
|
|
62
|
-
try:
|
|
63
|
-
if await install_browser():
|
|
64
|
-
return await start_browser(**kwargs)
|
|
65
|
-
else:
|
|
66
|
-
raise RuntimeError("无法启动浏览器实例且尝试安装失败") from e
|
|
67
|
-
except Exception as e:
|
|
68
|
-
raise RuntimeError("无法启动浏览器实例") from e
|
|
56
|
+
await install_browser() # update playwright when start
|
|
57
|
+
await check_playwright_env()
|
|
58
|
+
return await start_browser(**kwargs)
|
|
69
59
|
|
|
70
60
|
|
|
71
61
|
@asynccontextmanager
|
|
72
62
|
async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
|
|
73
63
|
"""
|
|
74
|
-
|
|
64
|
+
获取一个新的页面的上下文管理器, 这里的 page 默认使用设备缩放因子为 2。
|
|
75
65
|
|
|
76
66
|
Args:
|
|
77
67
|
device_scale_factor (float): 设备缩放因子。
|
|
@@ -167,3 +157,16 @@ async def shutdown_browser() -> None:
|
|
|
167
157
|
if _playwright:
|
|
168
158
|
with suppress_and_log():
|
|
169
159
|
await _playwright.stop()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
async def check_playwright_env():
|
|
163
|
+
"""Check Playwright environment."""
|
|
164
|
+
logger.info("Checking Playwright environment...")
|
|
165
|
+
try:
|
|
166
|
+
async with async_playwright() as p:
|
|
167
|
+
await p.chromium.launch()
|
|
168
|
+
except Exception as e:
|
|
169
|
+
raise RuntimeError(
|
|
170
|
+
"Playwright environment is not set up correctly. "
|
|
171
|
+
"Refer to https://playwright.dev/python/docs/intro#system-requirements"
|
|
172
|
+
) from e
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Literal, overload
|
|
2
|
+
|
|
3
|
+
from nonebot.compat import PYDANTIC_V2
|
|
4
|
+
|
|
5
|
+
__all__ = ("model_validator",)
|
|
6
|
+
|
|
7
|
+
# todo)) 等nb更新切换到nb提供的兼容层
|
|
8
|
+
if PYDANTIC_V2:
|
|
9
|
+
from pydantic import field_validator as field_validator
|
|
10
|
+
from pydantic import model_validator as model_validator
|
|
11
|
+
else:
|
|
12
|
+
from pydantic import root_validator, validator
|
|
13
|
+
|
|
14
|
+
@overload
|
|
15
|
+
def model_validator(*, mode: Literal["before"]): ...
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def model_validator(*, mode: Literal["after"]): ...
|
|
19
|
+
|
|
20
|
+
def model_validator(*, mode: Literal["before", "after"] = "after"):
|
|
21
|
+
return root_validator(pre=mode == "before", allow_reuse=True)
|
|
22
|
+
|
|
23
|
+
def field_validator(__field, *fields, mode: Literal["before", "after"] = "after"): # noqa: PYI063
|
|
24
|
+
return validator(__field, *fields, pre=mode == "before", allow_reuse=True)
|
|
@@ -1,55 +1,17 @@
|
|
|
1
|
-
from
|
|
2
|
-
from
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from typing_extensions import Self
|
|
3
3
|
|
|
4
4
|
from nonebot import get_driver, get_plugin_config
|
|
5
|
-
from nonebot.compat import custom_validation
|
|
6
5
|
from pydantic import BaseModel, Field
|
|
7
6
|
|
|
7
|
+
from nonebot_plugin_htmlrender.compat import model_validator
|
|
8
8
|
from nonebot_plugin_htmlrender.consts import BROWSER_CHANNEL_TYPES, BROWSER_ENGINE_TYPES
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
@custom_validation
|
|
12
|
-
class BrowserEngineType(str):
|
|
13
|
-
"""Playwright浏览器引擎类型,支持的值由 BROWSER_ENGINE_TYPES 定义。"""
|
|
14
|
-
|
|
15
|
-
ALLOWED_VALUES: ClassVar = BROWSER_ENGINE_TYPES
|
|
16
|
-
|
|
17
|
-
@classmethod
|
|
18
|
-
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
|
19
|
-
yield cls.validate
|
|
20
|
-
|
|
21
|
-
@classmethod
|
|
22
|
-
def validate(cls, value: Any) -> str:
|
|
23
|
-
if value not in cls.ALLOWED_VALUES:
|
|
24
|
-
raise ValueError(
|
|
25
|
-
f"Invalid browser type: {value!r}, must be one of {cls.ALLOWED_VALUES}"
|
|
26
|
-
)
|
|
27
|
-
return value
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@custom_validation
|
|
31
|
-
class BrowserChannelType(str):
|
|
32
|
-
"""Playwright浏览器通道类型,支持的值由 BROWSER_CHANNEL_TYPES 定义。"""
|
|
33
|
-
|
|
34
|
-
ALLOWED_VALUES: ClassVar = BROWSER_CHANNEL_TYPES
|
|
35
|
-
|
|
36
|
-
@classmethod
|
|
37
|
-
def __get_validators__(cls) -> Generator[Callable[..., Any], None, None]:
|
|
38
|
-
yield cls.validate
|
|
39
|
-
|
|
40
|
-
@classmethod
|
|
41
|
-
def validate(cls, value: Any) -> str:
|
|
42
|
-
if value not in cls.ALLOWED_VALUES:
|
|
43
|
-
raise ValueError(
|
|
44
|
-
f"Invalid channel: {value!r}, must be one of {cls.ALLOWED_VALUES}"
|
|
45
|
-
)
|
|
46
|
-
return value
|
|
47
|
-
|
|
48
|
-
|
|
49
11
|
class Config(BaseModel):
|
|
50
12
|
"""插件配置类。"""
|
|
51
13
|
|
|
52
|
-
htmlrender_browser:
|
|
14
|
+
htmlrender_browser: str = Field(
|
|
53
15
|
default="chromium",
|
|
54
16
|
description="Playwright浏览器引擎类型,默认值为 'chromium'。",
|
|
55
17
|
)
|
|
@@ -65,7 +27,7 @@ class Config(BaseModel):
|
|
|
65
27
|
htmlrender_proxy_host_bypass: Optional[str] = Field(
|
|
66
28
|
default=None, description="Playwright浏览器代理的绕过地址。"
|
|
67
29
|
)
|
|
68
|
-
htmlrender_browser_channel: Optional[
|
|
30
|
+
htmlrender_browser_channel: Optional[str] = Field(
|
|
69
31
|
default=None, description="Playwright浏览器通道类型。"
|
|
70
32
|
)
|
|
71
33
|
htmlrender_browser_executable_path: Optional[str] = Field(
|
|
@@ -75,6 +37,25 @@ class Config(BaseModel):
|
|
|
75
37
|
default=None, description="通过 CDP 连接Playwright浏览器的端点地址。"
|
|
76
38
|
)
|
|
77
39
|
|
|
40
|
+
@model_validator(mode="after")
|
|
41
|
+
def check_browser_channel(self) -> Self:
|
|
42
|
+
if (
|
|
43
|
+
self.htmlrender_browser_channel is not None
|
|
44
|
+
and self.htmlrender_browser_channel not in BROWSER_CHANNEL_TYPES
|
|
45
|
+
):
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f"Invalid browser channel type. Must be one of {BROWSER_CHANNEL_TYPES}"
|
|
48
|
+
)
|
|
49
|
+
return self
|
|
50
|
+
|
|
51
|
+
@model_validator(mode="after")
|
|
52
|
+
def check_browser(self) -> Self:
|
|
53
|
+
if self.htmlrender_browser not in BROWSER_ENGINE_TYPES:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Invalid browser type. Must be one of {BROWSER_ENGINE_TYPES}"
|
|
56
|
+
)
|
|
57
|
+
return self
|
|
58
|
+
|
|
78
59
|
|
|
79
60
|
global_config = get_driver().config
|
|
80
61
|
plugin_config = get_plugin_config(Config)
|
|
@@ -19,8 +19,8 @@ WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name =
|
|
|
19
19
|
# MINGW = sysconfig.get_platform().startswith("mingw")
|
|
20
20
|
# MACOS = sys.platform == "darwin"
|
|
21
21
|
MIRRORS = [
|
|
22
|
-
MirrorSource("
|
|
23
|
-
MirrorSource("
|
|
22
|
+
MirrorSource("Default", "https://playwright.azureedge.net", 1),
|
|
23
|
+
MirrorSource("Taobao", "https://registry.npmmirror.com/-/binary/playwright", 2),
|
|
24
24
|
]
|
|
25
25
|
BROWSER_ENGINE_TYPES = ["chromium", "firefox", "webkit"]
|
|
26
26
|
BROWSER_CHANNEL_TYPES = [
|
|
@@ -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)
|
|
@@ -77,19 +77,19 @@ async def download_context() -> AsyncIterator[None]:
|
|
|
77
77
|
if plugin_config.htmlrender_download_proxy:
|
|
78
78
|
proxy = plugin_config.htmlrender_download_proxy
|
|
79
79
|
if proxy.startswith("http://") and not os.environ.get("HTTP_PROXY"):
|
|
80
|
-
logger.info(f"
|
|
80
|
+
logger.info(f"Using http Proxy: {proxy}")
|
|
81
81
|
os.environ["HTTP_PROXY"] = proxy
|
|
82
82
|
elif proxy.startswith("https://") and not os.environ.get("HTTPS_PROXY"):
|
|
83
|
-
logger.info(f"
|
|
83
|
+
logger.info(f"Using https Proxy: {proxy}")
|
|
84
84
|
os.environ["HTTPS_PROXY"] = proxy
|
|
85
85
|
|
|
86
86
|
try:
|
|
87
87
|
best_mirror = await check_mirror_connectivity()
|
|
88
88
|
if best_mirror is not None:
|
|
89
|
-
logger.info(f"
|
|
89
|
+
logger.info(f"Using Mirror source: {best_mirror.name} ({best_mirror.url})")
|
|
90
90
|
os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = best_mirror.url
|
|
91
91
|
else:
|
|
92
|
-
logger.info("
|
|
92
|
+
logger.info("Mirror source not available, using default")
|
|
93
93
|
|
|
94
94
|
yield
|
|
95
95
|
|
|
@@ -143,7 +143,7 @@ async def read_stream(
|
|
|
143
143
|
if "|" in text and "%" in text:
|
|
144
144
|
if text != last_progress:
|
|
145
145
|
if callback:
|
|
146
|
-
await callback(f"
|
|
146
|
+
await callback(f"Progress: {text}")
|
|
147
147
|
last_progress = text
|
|
148
148
|
else:
|
|
149
149
|
if callback:
|
|
@@ -153,7 +153,7 @@ async def read_stream(
|
|
|
153
153
|
except asyncio.IncompleteReadError:
|
|
154
154
|
break
|
|
155
155
|
except Exception as e:
|
|
156
|
-
logger.opt(exception=True).error(f"
|
|
156
|
+
logger.opt(exception=True).error(f"Error reading stream: {e!s}")
|
|
157
157
|
break
|
|
158
158
|
|
|
159
159
|
return "\n".join(output)
|
|
@@ -196,18 +196,18 @@ async def execute_install_command(timeout: int) -> tuple[bool, str]:
|
|
|
196
196
|
)
|
|
197
197
|
except asyncio.TimeoutError:
|
|
198
198
|
await terminate_process(process)
|
|
199
|
-
return False, f"
|
|
199
|
+
return False, f"Timed out ({timeout}s)"
|
|
200
200
|
|
|
201
201
|
await process.wait()
|
|
202
202
|
returncode = process.returncode
|
|
203
203
|
|
|
204
204
|
if returncode != 0:
|
|
205
|
-
return False, f"
|
|
205
|
+
return False, f"Exited with code {returncode}"
|
|
206
206
|
|
|
207
|
-
return True, "
|
|
207
|
+
return True, "Installation completed"
|
|
208
208
|
|
|
209
209
|
except Exception as e:
|
|
210
|
-
return False, f"
|
|
210
|
+
return False, f"An error occurred during installation: {e!s}"
|
|
211
211
|
|
|
212
212
|
|
|
213
213
|
async def install_browser(timeout: int = 300) -> bool:
|
|
@@ -220,11 +220,20 @@ async def install_browser(timeout: int = 300) -> bool:
|
|
|
220
220
|
bool: 是否安装成功。
|
|
221
221
|
"""
|
|
222
222
|
async with download_context():
|
|
223
|
-
logger.info(
|
|
223
|
+
logger.opt(colors=True).info(
|
|
224
|
+
f"Checking <cyan>{plugin_config.htmlrender_browser}</cyan> installation..."
|
|
225
|
+
)
|
|
224
226
|
installed, message = await execute_install_command(timeout)
|
|
225
227
|
if installed:
|
|
226
|
-
logger.info("
|
|
228
|
+
logger.info("Installation succeeded")
|
|
227
229
|
return True
|
|
228
230
|
else:
|
|
229
|
-
logger.
|
|
230
|
-
|
|
231
|
+
logger.warning("Installation failed, retrying with official mirror...")
|
|
232
|
+
del os.environ["PLAYWRIGHT_DOWNLOAD_HOST"]
|
|
233
|
+
installed, message = await execute_install_command(timeout)
|
|
234
|
+
if installed:
|
|
235
|
+
logger.info("Installation succeeded")
|
|
236
|
+
return True
|
|
237
|
+
else:
|
|
238
|
+
logger.error(f"Installation failed with: {message}")
|
|
239
|
+
return False
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
|
-
from playwright.async_api import Browser,
|
|
4
|
+
from playwright.async_api import Browser, Page
|
|
5
5
|
import pytest
|
|
6
6
|
from pytest_mock import MockerFixture
|
|
7
7
|
|
|
@@ -65,14 +65,13 @@ async def test_suppress_and_log(mocker: MockerFixture, exception: Exception) ->
|
|
|
65
65
|
async def test_launch(mocker: MockerFixture, browser_config: dict[str, str]) -> None:
|
|
66
66
|
"""测试浏览器启动"""
|
|
67
67
|
from nonebot_plugin_htmlrender.browser import _launch
|
|
68
|
-
from nonebot_plugin_htmlrender.config import BrowserEngineType
|
|
69
68
|
|
|
70
69
|
mock_browser_type = mocker.AsyncMock()
|
|
71
70
|
mock_playwright = mocker.MagicMock()
|
|
72
71
|
setattr(mock_playwright, "chromium", mock_browser_type)
|
|
73
72
|
|
|
74
73
|
mocker.patch("nonebot_plugin_htmlrender.browser._playwright", mock_playwright)
|
|
75
|
-
await _launch(
|
|
74
|
+
await _launch(browser_config["browser"])
|
|
76
75
|
|
|
77
76
|
mock_browser_type.launch.assert_called_once()
|
|
78
77
|
|
|
@@ -94,27 +93,6 @@ async def test_init_browser_success(
|
|
|
94
93
|
mock_start.assert_called_once()
|
|
95
94
|
|
|
96
95
|
|
|
97
|
-
@pytest.mark.asyncio
|
|
98
|
-
async def test_init_browser_install_fallback(
|
|
99
|
-
mocker: MockerFixture, mock_browser: Browser
|
|
100
|
-
) -> None:
|
|
101
|
-
"""测试浏览器安装回退"""
|
|
102
|
-
from nonebot_plugin_htmlrender.browser import init_browser
|
|
103
|
-
|
|
104
|
-
mock_start = mocker.patch(
|
|
105
|
-
"nonebot_plugin_htmlrender.browser.start_browser",
|
|
106
|
-
side_effect=[Error("Executable doesn't exist"), mock_browser],
|
|
107
|
-
)
|
|
108
|
-
mock_install = mocker.patch(
|
|
109
|
-
"nonebot_plugin_htmlrender.browser.install_browser", return_value=True
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
browser = await init_browser()
|
|
113
|
-
assert isinstance(browser, Browser)
|
|
114
|
-
assert mock_start.call_count == 2
|
|
115
|
-
mock_install.assert_called_once()
|
|
116
|
-
|
|
117
|
-
|
|
118
96
|
@pytest.mark.asyncio
|
|
119
97
|
async def test_get_new_page(
|
|
120
98
|
mocker: MockerFixture,
|
|
@@ -273,24 +251,3 @@ async def test_start_browser_with_config(mocker: MockerFixture) -> None:
|
|
|
273
251
|
|
|
274
252
|
await start_browser()
|
|
275
253
|
mock_launch.assert_called_with(mocker.ANY, channel="chrome-canary")
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
@pytest.mark.asyncio
|
|
279
|
-
async def test_start_browser_failure(mocker: MockerFixture) -> None:
|
|
280
|
-
"""测试浏览器启动失败"""
|
|
281
|
-
from nonebot_plugin_htmlrender.browser import init_browser
|
|
282
|
-
|
|
283
|
-
mock_playwright = mocker.AsyncMock()
|
|
284
|
-
mock_playwright.start.side_effect = Exception("Failed to start")
|
|
285
|
-
|
|
286
|
-
mocker.patch(
|
|
287
|
-
"nonebot_plugin_htmlrender.browser.async_playwright",
|
|
288
|
-
return_value=mock_playwright,
|
|
289
|
-
)
|
|
290
|
-
mocker.patch("nonebot_plugin_htmlrender.browser._browser", None)
|
|
291
|
-
mocker.patch("nonebot_plugin_htmlrender.browser._playwright", None)
|
|
292
|
-
|
|
293
|
-
with pytest.raises(RuntimeError) as exc_info:
|
|
294
|
-
await init_browser()
|
|
295
|
-
|
|
296
|
-
assert "无法启动浏览器实例" in str(exc_info.value)
|
{nonebot_plugin_htmlrender-0.6.0b1 → nonebot_plugin_htmlrender-0.6.2}/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)
|
|
@@ -86,7 +86,7 @@ async def test_execute_install_command(mocker: MockerFixture, mock_stream):
|
|
|
86
86
|
|
|
87
87
|
success, message = await execute_install_command(timeout=5)
|
|
88
88
|
assert success
|
|
89
|
-
assert "
|
|
89
|
+
assert "Installation completed" in message
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
@pytest.mark.asyncio
|
|
@@ -111,7 +111,7 @@ async def test_execute_install_command_timeout(mocker: MockerFixture, mock_strea
|
|
|
111
111
|
success, message = await execute_install_command(timeout=1)
|
|
112
112
|
|
|
113
113
|
assert not success
|
|
114
|
-
assert message == "
|
|
114
|
+
assert message == "Timed out (1s)"
|
|
115
115
|
mock_terminate.assert_called_once_with(mock_process)
|
|
116
116
|
|
|
117
117
|
|