nonebot-plugin-htmlrender 0.4.0__tar.gz → 0.5.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.
Files changed (48) hide show
  1. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/publish.yml +6 -1
  2. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/PKG-INFO +2 -2
  3. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/__init__.py +3 -3
  4. nonebot_plugin_htmlrender-0.5.1/nonebot_plugin_htmlrender/browser.py +356 -0
  5. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/config.py +6 -1
  6. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/pyproject.toml +29 -14
  7. nonebot_plugin_htmlrender-0.5.1/tests/conftest.py +15 -0
  8. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/uv.lock +5 -5
  9. nonebot_plugin_htmlrender-0.4.0/nonebot_plugin_htmlrender/browser.py +0 -131
  10. nonebot_plugin_htmlrender-0.4.0/tests/conftest.py +0 -10
  11. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.flake8 +0 -0
  12. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.github/workflows/codecov.yml +0 -0
  13. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.gitignore +0 -0
  14. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/.python-version +0 -0
  15. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/LICENSE +0 -0
  16. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/README.md +0 -0
  17. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/example.md +0 -0
  18. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/html2pic.png +0 -0
  19. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/md2pic.png +0 -0
  20. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/template2pic.png +0 -0
  21. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/docs/text2pic.png +0 -0
  22. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/.env.dev +0 -0
  23. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/bot.py +0 -0
  24. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/__init__.py +0 -0
  25. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/html2pic.html +0 -0
  26. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/markdown.css +0 -0
  27. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/mystyle.css +0 -0
  28. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/progress.html.jinja2 +0 -0
  29. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/templates/text.html +0 -0
  30. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/example/plugins/render/utils.py +0 -0
  31. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/data_source.py +0 -0
  32. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/github-markdown-light.css +0 -0
  33. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/katex.min.b64_fonts.css +0 -0
  34. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/katex.min.js +0 -0
  35. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/mathtex-script-type.min.js +0 -0
  36. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/katex/mhchem.min.js +0 -0
  37. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/markdown.html +0 -0
  38. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/pygments-default.css +0 -0
  39. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/text.css +0 -0
  40. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/nonebot_plugin_htmlrender/templates/text.html +0 -0
  41. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/__init__.py +0 -0
  42. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/markdown.css +0 -0
  43. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/mystyle.css +0 -0
  44. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/progress.html.jinja2 +0 -0
  45. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/templates/text.html +0 -0
  46. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_htmlrender.py +0 -0
  47. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/test_template_filter.png +0 -0
  48. {nonebot_plugin_htmlrender-0.4.0 → nonebot_plugin_htmlrender-0.5.1}/tests/utils.py +0 -0
@@ -43,8 +43,13 @@ jobs:
43
43
  - name: Build Package
44
44
  run: uv build
45
45
 
46
+ - uses: actions/upload-artifact@v4
47
+ with:
48
+ name: artifact
49
+ path: dist/ # or path/to/artifact
50
+
46
51
  - name: pypi-publish
47
- uses: pypa/gh-action-pypi-publish@v1.11.0
52
+ uses: pypa/gh-action-pypi-publish@v1.12.3
48
53
 
49
54
  # - name: Publish Package to PyPI
50
55
  # run: uv publish
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: nonebot-plugin-htmlrender
3
- Version: 0.4.0
3
+ Version: 0.5.1
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
@@ -6,6 +6,7 @@ from .browser import (
6
6
  get_browser as get_browser,
7
7
  get_new_page as get_new_page,
8
8
  shutdown_browser as shutdown_browser,
9
+ init_browser as init_browser,
9
10
  )
10
11
  from .data_source import (
11
12
  capture_element as capture_element,
@@ -35,9 +36,8 @@ async def init(**kwargs):
35
36
  Returns:
36
37
  Browser: Browser
37
38
  """
38
- browser = await get_browser(**kwargs)
39
- logger.info("Browser Started.")
40
- return browser
39
+ logger.info("Browser Starting.")
40
+ return await init_browser(**kwargs)
41
41
 
42
42
 
43
43
  @driver.on_shutdown
@@ -0,0 +1,356 @@
1
+ import asyncio
2
+ import os
3
+ import socket
4
+ import sys
5
+ from contextlib import asynccontextmanager, contextmanager
6
+ from dataclasses import dataclass
7
+ from enum import Enum
8
+ from typing import Optional, Literal, AsyncIterator, TextIO
9
+ from typing_extensions import TypeGuard
10
+ from urllib.parse import urlparse
11
+
12
+ from nonebot.log import logger
13
+ from playwright.async_api import Browser, Error, Page, Playwright, async_playwright, BrowserType
14
+
15
+ from .config import plugin_config
16
+
17
+ _browser: Optional[Browser] = None
18
+ _playwright: Optional[Playwright] = None
19
+ AllowedBrowser = Literal["chromium", "firefox", "webkit"]
20
+
21
+
22
+ @contextmanager
23
+ def _suppress_and_log():
24
+ """抑制并记录关闭期间的任何异常。"""
25
+ try:
26
+ yield
27
+ except Exception as e:
28
+ logger.opt(exception=e).warning("关闭 playwright 时发生错误。")
29
+
30
+ def is_allowed_browser(browser_type: str) -> TypeGuard[AllowedBrowser]:
31
+ """检查浏览器类型是否有效。"""
32
+ return browser_type in ["chromium", "firefox", "webkit"]
33
+
34
+ async def _launch(browser_type: AllowedBrowser, **kwargs) -> Browser:
35
+ logger.info(f"使用 {browser_type.capitalize()} 启动中...")
36
+ _browser_cls: BrowserType = getattr(_playwright, browser_type)
37
+ _browser = await _browser_cls.launch(**kwargs)
38
+ logger.debug(f"{browser_type.capitalize()} 路径: {_browser_cls.executable_path}")
39
+ return _browser
40
+
41
+
42
+ async def init_browser(**kwargs) -> Browser:
43
+ """初始化全局 playwright 浏览器实例并返回浏览器实例。"""
44
+ try:
45
+ return await start_browser(**kwargs)
46
+ except Error:
47
+ await install_browser()
48
+ return await start_browser(**kwargs)
49
+
50
+
51
+ async def start_browser(**kwargs) -> Browser:
52
+ """
53
+ 启动 playwright 浏览器实例。
54
+ Args:
55
+ **kwargs (Any): 传递给 `playwright.launch` 的关键字参数。
56
+
57
+ Returns:
58
+ Browser: 浏览器实例。
59
+ """
60
+ global _browser, _playwright
61
+ browser_type = (
62
+ plugin_config.htmlrender_browser.lower()
63
+ if plugin_config.htmlrender_browser
64
+ else "chromium"
65
+ )
66
+ if not is_allowed_browser(browser_type):
67
+ raise ValueError(f"不支持的浏览器类型: {browser_type}")
68
+
69
+ if plugin_config.htmlrender_browser_channel:
70
+ kwargs["channel"] = plugin_config.htmlrender_browser_channel
71
+
72
+ if plugin_config.htmlrender_proxy_host:
73
+ kwargs["proxy"] = {
74
+ "server": plugin_config.htmlrender_proxy_host,
75
+ }
76
+
77
+ _playwright = await async_playwright().start()
78
+
79
+ if browser_type == "chromium" and plugin_config.htmlrender_connect_over_cdp:
80
+ logger.info("使用 Chromium CDP 连接中...")
81
+ _browser = await _playwright.chromium.connect_over_cdp(**kwargs)
82
+ else:
83
+ _browser = await _launch(browser_type)
84
+
85
+ return _browser
86
+
87
+
88
+ async def get_browser(**kwargs) -> Browser:
89
+ """获取 Browser 实例,不存在则初始化"""
90
+ if _browser and _browser.is_connected():
91
+ return _browser
92
+
93
+ try:
94
+ return await init_browser(**kwargs)
95
+ except Exception as e:
96
+ raise RuntimeError("无法初始化浏览器实例") from e
97
+
98
+
99
+ @asynccontextmanager
100
+ async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
101
+ """
102
+ 获取一个新的页面的上下文管理器。
103
+ Args:
104
+ device_scale_factor (float): 设备缩放因子。
105
+ **kwargs (Any): 传递给 `browser.new_context` 的关键字参数。
106
+
107
+ Returns:
108
+ AsyncIterator[Page]: 页面对象。
109
+ """
110
+ ctx = await get_browser()
111
+ page = await ctx.new_page(device_scale_factor=device_scale_factor, **kwargs)
112
+ try:
113
+ yield page
114
+ finally:
115
+ await page.close()
116
+
117
+
118
+ async def shutdown_browser() -> None:
119
+ """关闭浏览器实例。"""
120
+ if _browser:
121
+ with _suppress_and_log():
122
+ await _browser.close()
123
+ if _playwright:
124
+ with _suppress_and_log():
125
+ await _playwright.stop()
126
+
127
+
128
+ @dataclass
129
+ class MirrorSource:
130
+ name: str
131
+ url: str
132
+ priority: int
133
+
134
+
135
+ async def check_mirror_connectivity(
136
+ mirrors: list[MirrorSource], timeout: int = 5
137
+ ) -> Optional[MirrorSource]:
138
+ """
139
+ 检查镜像源的可用性并返回最佳镜像源。
140
+ Args:
141
+ mirrors (list[MirrorSource]): 镜像源列表。
142
+ timeout (int): 连接超时时间。
143
+
144
+ Returns:
145
+ Optional[MirrorSource]: 最佳镜像源。
146
+ """
147
+
148
+ async def _check_single_mirror(mirror: MirrorSource) -> tuple[MirrorSource, float]:
149
+ """
150
+ 检查单个镜像源的可用性。
151
+ Args:
152
+ mirror (MirrorSource): 镜像源。
153
+
154
+ Returns:
155
+ tuple[MirrorSource, float]: 镜像源和连接耗时。
156
+ """
157
+ try:
158
+ parsed_url = urlparse(mirror.url)
159
+ host = parsed_url.hostname
160
+ port = parsed_url.port or (443 if parsed_url.scheme == "https" else 80)
161
+
162
+ start_time = asyncio.get_event_loop().time()
163
+
164
+ _, _ = await asyncio.wait_for(
165
+ asyncio.open_connection(host, port), timeout=timeout
166
+ )
167
+
168
+ elapsed = asyncio.get_event_loop().time() - start_time
169
+ return mirror, elapsed
170
+
171
+ except (asyncio.TimeoutError, ConnectionRefusedError, socket.gaierror) as e:
172
+ logger.debug(f"镜像源 {mirror.name} 连接失败: {str(e)}")
173
+ return mirror, float("inf")
174
+
175
+ tasks = [_check_single_mirror(mirror) for mirror in mirrors]
176
+ results = await asyncio.gather(*tasks)
177
+
178
+ available_mirrors = [(m, t) for m, t in results if t != float("inf")]
179
+ if not available_mirrors:
180
+ return None
181
+
182
+ return min(available_mirrors, key=lambda x: (x[1], -x[0].priority))[0]
183
+
184
+
185
+ @asynccontextmanager
186
+ async def download_host_context(mirrors: list[MirrorSource]) -> AsyncIterator[None]:
187
+ """
188
+ 为下载设置镜像源上下文管理器。
189
+ Args:
190
+ mirrors (list[MirrorSource]): 镜像源列表。
191
+
192
+ Returns:
193
+ AsyncIterator[None]: 上下文管理器。
194
+ """
195
+ had_original = "PLAYWRIGHT_DOWNLOAD_HOST" in os.environ
196
+ original_host = os.environ.get("PLAYWRIGHT_DOWNLOAD_HOST")
197
+
198
+ try:
199
+ best_mirror = await check_mirror_connectivity(mirrors)
200
+
201
+ if plugin_config.htmlrender_download_host:
202
+ logger.info(f"使用配置的下载源: {plugin_config.htmlrender_download_host}")
203
+ os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = (
204
+ plugin_config.htmlrender_download_host
205
+ )
206
+ elif best_mirror:
207
+ logger.info(f"使用镜像源: {best_mirror.name} ({best_mirror.url})")
208
+ os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = best_mirror.url
209
+ else:
210
+ logger.info("无可用镜像源,使用默认源")
211
+
212
+ yield
213
+ finally:
214
+ if had_original and original_host is not None:
215
+ os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = original_host
216
+ elif "PLAYWRIGHT_DOWNLOAD_HOST" in os.environ:
217
+ del os.environ["PLAYWRIGHT_DOWNLOAD_HOST"]
218
+
219
+
220
+ class MessageType(Enum):
221
+ PROGRESS = "progress"
222
+ DOWNLOAD = "download"
223
+ INFO = "info"
224
+
225
+
226
+ async def read_stream(stream, out: TextIO = sys.stdout) -> None:
227
+ """实时读取并输出流内容
228
+
229
+ Args:
230
+ stream: 要读取的流
231
+ out: 输出流,默认为 sys.stdout
232
+ """
233
+ last_progress = ""
234
+
235
+ def write_progress(_: str) -> None:
236
+ """写入进度条"""
237
+ out.write(f"\r{_}")
238
+ out.flush()
239
+
240
+ def ensure_newline() -> None:
241
+ """确保进度条后换行"""
242
+ nonlocal last_progress
243
+ if last_progress:
244
+ out.write("\n")
245
+ out.flush()
246
+ last_progress = ""
247
+
248
+ def get_message_type(_: str) -> MessageType:
249
+ """获取消息类型"""
250
+ if "|" in _ and "%" in _ and ("MiB" in _ or "KiB" in _):
251
+ return MessageType.PROGRESS
252
+ elif _.startswith("Downloading "):
253
+ return MessageType.DOWNLOAD
254
+ return MessageType.INFO
255
+
256
+ while True:
257
+ line = await stream.readline()
258
+ if not line:
259
+ break
260
+
261
+ text = line.decode().strip()
262
+ if not text:
263
+ continue
264
+
265
+ msg_type = get_message_type(text)
266
+
267
+ if msg_type is MessageType.PROGRESS:
268
+ progress_text = text.split("|", 1)[1].strip()
269
+ write_progress(progress_text)
270
+ last_progress = text
271
+ else:
272
+ ensure_newline()
273
+ logger.info(text)
274
+
275
+
276
+ async def execute_install_command(
277
+ install_cmd: list[str], timeout: int
278
+ ) -> tuple[bool, str]:
279
+ """
280
+ 执行安装命令并返回结果。
281
+ Args:
282
+ install_cmd (list[str]): 安装命令。
283
+ timeout (int): 等待超时时间。
284
+
285
+ Returns:
286
+ tuple[bool, str]: 是否安装成功和消息。
287
+ """
288
+ process = await asyncio.create_subprocess_exec(
289
+ *install_cmd,
290
+ stdout=asyncio.subprocess.PIPE,
291
+ stderr=asyncio.subprocess.PIPE,
292
+ )
293
+
294
+ try:
295
+ await asyncio.wait_for(
296
+ asyncio.gather(
297
+ read_stream(process.stdout),
298
+ read_stream(process.stderr),
299
+ ),
300
+ timeout=timeout,
301
+ )
302
+
303
+ await process.wait()
304
+ return (
305
+ process.returncode == 0,
306
+ "安装完成" if process.returncode == 0 else "安装失败",
307
+ )
308
+ except asyncio.TimeoutError:
309
+ return False, "安装超时"
310
+ finally:
311
+ if process and process.returncode is None:
312
+ process.kill()
313
+ await process.wait()
314
+
315
+
316
+ async def install_browser(
317
+ browser_type: str = "chromium", timeout: int = 300, retries: int = 2
318
+ ) -> bool:
319
+ """
320
+ 安装用于 HTML 渲染的浏览器。
321
+
322
+ Args:
323
+ browser_type (str): 要安装的浏览器类型 ("chromium"、"firefox" 或 "webkit")。
324
+ timeout (int): 安装过程中等待的最大秒数。
325
+ retries (int): 安装失败时的最大重试次数。
326
+
327
+ Returns:
328
+ bool: 如果安装成功返回 True,否则返回 False
329
+ """
330
+ mirrors = [
331
+ MirrorSource("淘宝", "https://npmmirror.com/mirrors/playwright/", 1),
332
+ ]
333
+ async with download_host_context(mirrors):
334
+ install_cmd = ["playwright", "install", "--with-deps", browser_type]
335
+
336
+ for attempt in range(1, retries + 1):
337
+ try:
338
+ logger.info(
339
+ f"正在安装 {browser_type}(第 {attempt}/{retries} 次尝试)..."
340
+ )
341
+ success, message = await execute_install_command(install_cmd, timeout)
342
+
343
+ if success:
344
+ logger.success(f"{browser_type} 安装成功")
345
+ return True
346
+ else:
347
+ logger.error(f"安装失败:{message}")
348
+
349
+ except Exception as e:
350
+ logger.error(f"安装过程中出现异常:{str(e)}")
351
+
352
+ if attempt < retries:
353
+ await asyncio.sleep(2)
354
+
355
+ logger.error(f"在 {retries} 次尝试后安装失败")
356
+ return False
@@ -1,6 +1,7 @@
1
1
  from typing import Optional
2
2
 
3
3
  from pydantic import BaseModel, Field
4
+ from nonebot import get_driver, get_plugin_config
4
5
 
5
6
 
6
7
  class Config(BaseModel):
@@ -9,4 +10,8 @@ class Config(BaseModel):
9
10
  htmlrender_proxy_host: Optional[str] = Field(default=None)
10
11
  htmlrender_browser_channel: Optional[str] = Field(default=None)
11
12
  htmlrender_browser_executable_path: Optional[str] = Field(default=None)
12
- htmlrender_connect_over_cdp: Optional[str] = Field(default=False)
13
+ htmlrender_connect_over_cdp: Optional[str] = Field(default=None)
14
+
15
+
16
+ global_config = get_driver().config
17
+ plugin_config = get_plugin_config(Config)
@@ -1,20 +1,20 @@
1
1
  [project]
2
2
  name = "nonebot-plugin-htmlrender"
3
- version = "0.4.0"
3
+ version = "0.5.1"
4
4
  description = "通过浏览器渲染图片"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "kexue", email = "x@kexue-cloud.cn" }]
7
7
  requires-python = ">=3.9,<4.0"
8
8
  license = { file = "LICENSE" }
9
9
  dependencies = [
10
- "aiofiles>=0.8.0",
11
- "jinja2>=3.0.3",
12
- "markdown>=3.3.6",
13
- "nonebot2>=2.2.0",
14
- "playwright>=1.48.0",
15
- "pygments>=2.10.0",
16
- "pymdown-extensions>=9.1",
17
- "python-markdown-math>=0.8",
10
+ "aiofiles>=0.8.0",
11
+ "jinja2>=3.0.3",
12
+ "markdown>=3.3.6",
13
+ "nonebot2>=2.2.0",
14
+ "playwright>=1.48.0",
15
+ "pygments>=2.10.0",
16
+ "pymdown-extensions>=9.1",
17
+ "python-markdown-math>=0.8",
18
18
  ]
19
19
 
20
20
  [project.urls]
@@ -28,13 +28,28 @@ asyncio_mode = "auto"
28
28
 
29
29
  [dependency-groups]
30
30
  dev = [
31
- "nonebot-adapter-onebot>=2.4.6",
32
- "nonebot2[fastapi]>=2.3.3",
33
- "nonebug>=0.4.1",
34
- "pillow>=11.0.0",
35
- "pytest-asyncio>=0.24.0",
31
+ "nonebot-adapter-onebot>=2.4.6",
32
+ "nonebot2[fastapi]>=2.3.3",
33
+ "nonebug>=0.4.2",
34
+ "pillow>=11.0.0",
35
+ "pytest-asyncio>=0.24.0",
36
36
  ]
37
37
 
38
+ [tool.pyright]
39
+ pythonVersion = "3.9"
40
+ pythonPlatform = "All"
41
+ defineConstant = { PYDANTIC_V2 = true }
42
+ executionEnvironments = [
43
+ { root = "./tests", extraPaths = [
44
+ "./",
45
+ ] },
46
+ { root = "./" },
47
+ ]
48
+
49
+ typeCheckingMode = "standard"
50
+ reportShadowedImports = false
51
+ disableBytesTypePromotions = true
52
+
38
53
  [build-system]
39
54
  requires = ["hatchling"]
40
55
  build-backend = "hatchling.build"
@@ -0,0 +1,15 @@
1
+ import nonebot
2
+ import pytest
3
+ from pytest_asyncio import is_async_test
4
+
5
+
6
+ def pytest_collection_modifyitems(items: list[pytest.Item]):
7
+ pytest_asyncio_tests = (item for item in items if is_async_test(item))
8
+ session_scope_marker = pytest.mark.asyncio(loop_scope="session")
9
+ for async_test in pytest_asyncio_tests:
10
+ async_test.add_marker(session_scope_marker, append=False)
11
+
12
+
13
+ @pytest.fixture(scope="session", autouse=True)
14
+ async def after_nonebot_init(after_nonebot_init: None):
15
+ nonebot.require("nonebot_plugin_htmlrender")
@@ -612,7 +612,7 @@ wheels = [
612
612
 
613
613
  [[package]]
614
614
  name = "nonebot-plugin-htmlrender"
615
- version = "0.4.0"
615
+ version = "0.5.1"
616
616
  source = { editable = "." }
617
617
  dependencies = [
618
618
  { name = "aiofiles" },
@@ -650,7 +650,7 @@ requires-dist = [
650
650
  dev = [
651
651
  { name = "nonebot-adapter-onebot", specifier = ">=2.4.6" },
652
652
  { name = "nonebot2", extras = ["fastapi"], specifier = ">=2.3.3" },
653
- { name = "nonebug", specifier = ">=0.4.1" },
653
+ { name = "nonebug", specifier = ">=0.4.2" },
654
654
  { name = "pillow", specifier = ">=11.0.0" },
655
655
  { name = "pytest-asyncio", specifier = ">=0.24.0" },
656
656
  ]
@@ -681,7 +681,7 @@ fastapi = [
681
681
 
682
682
  [[package]]
683
683
  name = "nonebug"
684
- version = "0.4.1"
684
+ version = "0.4.2"
685
685
  source = { registry = "https://pypi.org/simple" }
686
686
  dependencies = [
687
687
  { name = "asgiref" },
@@ -690,9 +690,9 @@ dependencies = [
690
690
  { name = "pytest" },
691
691
  { name = "typing-extensions" },
692
692
  ]
693
- sdist = { url = "https://files.pythonhosted.org/packages/d1/57/4300262bbf6f8b7b69a75e8d827a32b1503fe9322210440ad785fd6bf3d0/nonebug-0.4.1.tar.gz", hash = "sha256:1bb96d715ec415351f2c7b871a86c64fdba533fb1a105e9a01cc1e250d37d52b", size = 11789 }
693
+ sdist = { url = "https://files.pythonhosted.org/packages/29/56/01eb465dc135787e463423bf4762b883159a639d02c3a577ab4efc8f9d56/nonebug-0.4.2.tar.gz", hash = "sha256:002dac00fed1fbff97574f3dffab2237fc41473b3bf3e73d2730ae34895bc137", size = 12118 }
694
694
  wheels = [
695
- { url = "https://files.pythonhosted.org/packages/ce/e0/e8d6e0db6cbff3edbb034284bd9451e9e380dde6c580fd22f1d6c2a91687/nonebug-0.4.1-py3-none-any.whl", hash = "sha256:553bfc408337fb33a19fcc936e24f4cc43200e4550a828f21e7f4406e93b62db", size = 14659 },
695
+ { url = "https://files.pythonhosted.org/packages/a0/4a/7592e899cd5074a9b70df007afe9b6f8bbaa02b8e9c1ce7f5c5498634cb7/nonebug-0.4.2-py3-none-any.whl", hash = "sha256:83d9229ec899819e9f5786eabc0c27b9dd0cd5d6e2e7438f59923ab822d5c555", size = 15030 },
696
696
  ]
697
697
 
698
698
  [[package]]
@@ -1,131 +0,0 @@
1
- #!/usr/bin/env python3
2
- # -*- coding: utf-8 -*-
3
- """
4
- @Author : yanyongyu
5
- @Date : 2021-03-12 13:42:43
6
- @LastEditors : yanyongyu
7
- @LastEditTime : 2021-11-01 14:05:41
8
- @Description : None
9
- @GitHub : https://github.com/yanyongyu
10
- """
11
-
12
- __author__ = "yanyongyu"
13
-
14
- from contextlib import asynccontextmanager
15
- from typing import AsyncIterator, Optional
16
-
17
- from nonebot import get_plugin_config
18
- from nonebot.log import logger
19
- from playwright.async_api import Browser, Error, Page, Playwright, async_playwright
20
-
21
- from .config import Config
22
-
23
- config = get_plugin_config(Config)
24
-
25
- _browser: Optional[Browser] = None
26
- _playwright: Optional[Playwright] = None
27
-
28
-
29
- async def init(**kwargs) -> Browser:
30
- global _browser
31
- global _playwright
32
- _playwright = await async_playwright().start()
33
- try:
34
- _browser = await launch_browser(**kwargs)
35
- except Error:
36
- await install_browser()
37
- _browser = await launch_browser(**kwargs)
38
- return _browser
39
-
40
-
41
- async def launch_browser(**kwargs) -> Browser:
42
- assert _playwright is not None, "Playwright 未安装"
43
-
44
- if config.htmlrender_browser_channel:
45
- kwargs["channel"] = config.htmlrender_browser_channel
46
-
47
- if config.htmlrender_proxy_host:
48
- kwargs["proxy"] = {
49
- "server": config.htmlrender_proxy_host,
50
- }
51
-
52
- if config.htmlrender_browser_executable_path:
53
- kwargs["executable_path"] = config.htmlrender_browser_executable_path
54
-
55
- if config.htmlrender_browser:
56
- if config.htmlrender_browser.lower() == "firefox":
57
- logger.info("使用 Firefox 启动")
58
- return await _playwright.firefox.launch(**kwargs)
59
- elif config.htmlrender_browser.lower() == "webkit":
60
- logger.info("使用 Webkit 启动")
61
- return await _playwright.webkit.launch(**kwargs)
62
- else:
63
- logger.warning("浏览器参数错误,无 {} 浏览器核心可用,使用 Chromium 启动")
64
-
65
- if config.htmlrender_connect_over_cdp:
66
- logger.info("使用 chromium connect_over_cdp 启动")
67
- return await _playwright.chromium.connect_over_cdp(**kwargs)
68
-
69
- # 默认使用 chromium
70
- logger.info("使用 Chromium 启动")
71
- return await _playwright.chromium.launch(**kwargs)
72
-
73
-
74
- async def get_browser(**kwargs) -> Browser:
75
- return _browser if _browser and _browser.is_connected() else await init(**kwargs)
76
-
77
-
78
- @asynccontextmanager
79
- async def get_new_page(device_scale_factor: float = 2, **kwargs) -> AsyncIterator[Page]:
80
- browser = await get_browser()
81
- page = await browser.new_page(device_scale_factor=device_scale_factor, **kwargs)
82
- try:
83
- yield page
84
- finally:
85
- await page.close()
86
-
87
-
88
- async def shutdown_browser():
89
- global _browser
90
- global _playwright
91
- if _browser:
92
- if _browser.is_connected():
93
- await _browser.close()
94
- _browser = None
95
- if _playwright:
96
- # await _playwright.stop()
97
- _playwright = None
98
-
99
-
100
- async def install_browser():
101
- import os
102
- import sys
103
-
104
- from playwright.__main__ import main
105
-
106
- if host := config.htmlrender_download_host:
107
- logger.info("使用配置源进行下载")
108
- os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = host
109
- else:
110
- logger.info("使用镜像源进行下载")
111
- os.environ["PLAYWRIGHT_DOWNLOAD_HOST"] = (
112
- "https://npmmirror.com/mirrors/playwright/"
113
- )
114
- success = False
115
-
116
- if config.htmlrender_browser == "firefox":
117
- logger.info("正在安装 firefox")
118
- sys.argv = ["", "install", "firefox"]
119
- else:
120
- # 默认使用 chromium
121
- logger.info("正在安装 chromium")
122
- sys.argv = ["", "install", "chromium"]
123
- try:
124
- logger.info("正在安装依赖")
125
- os.system("playwright install-deps") # noqa: ASYNC102, S605, S607
126
- main()
127
- except SystemExit as e:
128
- if e.code == 0:
129
- success = True
130
- if not success:
131
- logger.error("浏览器更新失败, 请检查网络连通性")
@@ -1,10 +0,0 @@
1
- import nonebot
2
- import pytest
3
- from nonebug import App
4
-
5
-
6
- @pytest.fixture
7
- def app():
8
- nonebot.require("nonebot_plugin_htmlrender")
9
-
10
- yield App()