fastapi-authly 0.1.4__tar.gz → 0.1.5__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. fastapi_authly-0.1.5/.gitignore +10 -0
  2. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/PKG-INFO +3 -1
  3. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/README.zh.md +39 -0
  4. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/pyproject.toml +9 -2
  5. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/__about__.py +1 -1
  6. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/__init__.py +18 -0
  7. fastapi_authly-0.1.5/src/fastapi_authly/charts.py +359 -0
  8. fastapi_authly-0.1.5/src/fastapi_authly/static/echarts.min.js +45 -0
  9. fastapi_authly-0.1.5/src/fastapi_authly/static/favicon.svg +1 -0
  10. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/uv.lock +207 -2
  11. fastapi_authly-0.1.4/.gitignore +0 -10
  12. fastapi_authly-0.1.4/coverage.xml +0 -407
  13. fastapi_authly-0.1.4/src/fastapi_authly/static/favicon.svg +0 -1
  14. fastapi_authly-0.1.4/test.py +0 -73
  15. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/LICENSE +0 -0
  16. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/README.md +0 -0
  17. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/docs//345/207/275/346/225/260/345/274/217/347/274/226/347/250/213/344/270/203/346/255/246/345/231/250.md" +0 -0
  18. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/examples/correct_usage.py +0 -0
  19. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/.gitignore +0 -0
  20. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/class_index.html +0 -0
  21. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  22. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  23. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/function_index.html +0 -0
  24. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/index.html +0 -0
  25. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  26. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/status.json +0 -0
  27. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/style_cb_9ff733b0.css +0 -0
  28. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
  29. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
  30. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
  31. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
  32. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
  33. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
  34. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
  35. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
  36. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
  37. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
  38. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
  39. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
  40. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
  41. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/auth.py +0 -0
  42. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/__init__.py +0 -0
  43. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
  44. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/core/__init__.py +0 -0
  45. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/core/config.py +0 -0
  46. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/core/security.py +0 -0
  47. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/deps.py +0 -0
  48. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/docs.py +0 -0
  49. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/interfaces.py +0 -0
  50. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/models/__init__.py +0 -0
  51. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/models/user.py +0 -0
  52. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/__init__.py +0 -0
  53. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/user.py +0 -0
  54. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/static/scalar/standalone.js +0 -0
  55. {fastapi_authly-0.1.4 → fastapi_authly-0.1.5}/src/fastapi_authly/static/scalar/style.css +0 -0
@@ -0,0 +1,10 @@
1
+ .env
2
+
3
+ .cursor
4
+ .idea
5
+ .coverage
6
+ .venv
7
+ test.py
8
+
9
+ *.xml
10
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-authly
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: A modular authentication system for FastAPI with OAuth2, JWT, and password recovery
5
5
  Project-URL: Homepage, https://github.com/yourusername/fastapi-auth-module
6
6
  Project-URL: Documentation, https://yourusername.github.io/fastapi-auth-module/
@@ -37,6 +37,8 @@ Requires-Dist: python-jose[cryptography]>=3.3.0
37
37
  Requires-Dist: python-multipart>=0.0.6
38
38
  Requires-Dist: tortoise-orm[psycopg]>=0.25.3
39
39
  Requires-Dist: uvicorn>=0.40.0
40
+ Provides-Extra: charts
41
+ Requires-Dist: selenium>=4.0.0; extra == 'charts'
40
42
  Provides-Extra: docs
41
43
  Requires-Dist: mkdocs-material>=9.0.0; extra == 'docs'
42
44
  Requires-Dist: mkdocs>=1.5.0; extra == 'docs'
@@ -93,6 +93,45 @@ setup_scalar_docs(
93
93
  )
94
94
  ```
95
95
 
96
+ ## 📊 ECharts 图表截图
97
+
98
+ 将 ECharts option 渲染为 PNG 图片,支持任意 ECharts 图表类型(折线、柱状、饼图等)。包内仅包含 `echarts.min.js`;Chrome 与 ChromeDriver 因体积超过 PyPI 单文件 100MB 限制**不随包分发**,需本机安装或通过参数传入路径。
99
+
100
+ **安装(含图表截图依赖):**
101
+ ```bash
102
+ pip install fastapi-authly[charts]
103
+ # 或
104
+ uv pip install "fastapi-authly[charts]"
105
+ ```
106
+
107
+ **使用方式一:直接传 ECharts option(通用):**
108
+ ```python
109
+ from fastapi_authly import render_option_to_png
110
+
111
+ option = {
112
+ "title": {"text": "案件数量变化趋势", "left": "center"},
113
+ "xAxis": {"type": "category", "data": ["2024-01", "2024-02", "2024-03"]},
114
+ "yAxis": {"type": "value"},
115
+ "series": [{"type": "line", "data": [8, 1, 5], "smooth": True}],
116
+ }
117
+ path = render_option_to_png(option, "案件数量变化趋势.png", title="案件数量变化趋势")
118
+ # path 为输出 PNG 的绝对路径
119
+ ```
120
+
121
+ **使用方式二:类型 + 标题 + 数据(便捷封装 line / bar / pie):**
122
+ ```python
123
+ from fastapi_authly import render_chart_to_png
124
+
125
+ data = [
126
+ {"month_name": "2024-01", "count": 8},
127
+ {"month_name": "2024-02", "count": 1},
128
+ {"month_name": "2024-03", "count": 5},
129
+ ]
130
+ path = render_chart_to_png("bar", "案件数量变化趋势", data, output_name="趋势柱状图.png")
131
+ ```
132
+
133
+ **Chrome/Chromedriver:** 包内不包含,需本机安装(如 `apt install chromium-browser chromium-chromedriver` 或从 [Chrome for Testing](https://googlechromelabs.github.io/chrome-for-testing/) 下载)后使用系统路径,或调用时传入 `chrome_headless_path`、`chromedriver_path`。ECharts JS 使用包内 `echarts.min.js`,也可通过 `local_echarts_path` 覆盖。
134
+
96
135
  ## 📋 主要接口
97
136
 
98
137
  - `POST /auth/login`:登录,返回 access_token(可选 refresh_token)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-authly"
7
- version = "0.1.4"
7
+ version = "0.1.5"
8
8
  description = "A modular authentication system for FastAPI with OAuth2, JWT, and password recovery"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -57,6 +57,7 @@ dependencies = [
57
57
  # 可选依赖
58
58
  tortoise = ["tortoise-orm[asyncpg]>=0.20.0"]
59
59
  sqlalchemy = ["sqlalchemy>=2.0.0", "alembic>=1.12.0"]
60
+ charts = ["selenium>=4.0.0"]
60
61
  docs = ["mkdocs>=1.5.0", "mkdocs-material>=9.0.0"]
61
62
  test = [
62
63
  "pytest>=7.0.0",
@@ -77,13 +78,19 @@ path = "src/fastapi_authly/__about__.py"
77
78
 
78
79
  [tool.hatch.build.targets.wheel]
79
80
  packages = ["src/fastapi_authly"]
80
- # 静态文件在包目录内,会自动包含,无需额外配置
81
+ # 排除超 100MB 的无头浏览器/Chromedriver,仅保留 echarts.min.js,避免 PyPI 单文件 100MB 限制
82
+ exclude = [
83
+ "fastapi_authly/static/chrome-headless-shell-linux64",
84
+ "fastapi_authly/static/chromedriver-linux64",
85
+ ]
81
86
 
82
87
  [tool.hatch.build.targets.sdist]
83
88
  exclude = [
84
89
  "/.*",
85
90
  "/docs/_build",
86
91
  "/tests",
92
+ "/src/fastapi_authly/static/chrome-headless-shell-linux64",
93
+ "/src/fastapi_authly/static/chromedriver-linux64",
87
94
  ]
88
95
 
89
96
  [tool.ruff]
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.1.4"
3
+ __version__ = "0.1.5"
@@ -16,6 +16,16 @@ from .docs import setup_scalar_docs
16
16
  from .interfaces import Mailer, PasswordHasher, TokenService, UserRepository
17
17
  from .schemas.user import UserBase, UserCreate, UserUpdate, UserPublic, Token, TokenData
18
18
 
19
+ # ECharts 截图(需安装 fastapi-authly[charts] 以使用)
20
+ from .charts import (
21
+ build_option,
22
+ echarts_option_to_html,
23
+ engine_make_snapshot,
24
+ get_driver,
25
+ render_chart_to_png,
26
+ render_option_to_png,
27
+ )
28
+
19
29
  __version__ = "0.1.1"
20
30
 
21
31
  __all__ = [
@@ -46,4 +56,12 @@ __all__ = [
46
56
  "UserPublic",
47
57
  "Token",
48
58
  "TokenData",
59
+
60
+ # ECharts 图表截图
61
+ "build_option",
62
+ "echarts_option_to_html",
63
+ "engine_make_snapshot",
64
+ "get_driver",
65
+ "render_chart_to_png",
66
+ "render_option_to_png",
49
67
  ]
@@ -0,0 +1,359 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ ECharts option 自组装 HTML → 包内 ECharts JS → 本地 Chrome 截图
4
+
5
+ 通用入口:直接传入 ECharts option 字典,不限定图表类型。
6
+ - 包内仅包含 echarts.min.js(Chrome/Chromedriver 因体积超 PyPI 限制不随包分发)。
7
+ - Chrome 与 ChromeDriver 需本机安装或通过参数传入路径;详见 get_driver / render_option_to_png。
8
+ """
9
+
10
+ import base64
11
+ import json
12
+ import os
13
+ import tempfile
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any, Dict, List
17
+
18
+ # 包内静态目录(安装后与 charts.py 同属 fastapi_authly 包)
19
+ _STATIC_DIR = Path(__file__).resolve().parent / "static"
20
+
21
+ # 默认路径:包内仅包含 echarts.min.js;Chrome/Chromedriver 不随包分发,存在时才用
22
+ _chrome = _STATIC_DIR / "chrome-headless-shell-linux64" / "chrome-headless-shell"
23
+ _chromedriver = _STATIC_DIR / "chromedriver-linux64" / "chromedriver"
24
+ DEFAULT_CHROME_HEADLESS_PATH: Path | None = _chrome if _chrome.exists() else None
25
+ DEFAULT_CHROMEDRIVER_PATH: Path | None = _chromedriver if _chromedriver.exists() else None
26
+ DEFAULT_ECHARTS_PATH = _STATIC_DIR / "echarts.min.js"
27
+
28
+ _CHROME_MISSING_MSG = (
29
+ "ECharts 截图需要 Chrome/Chromedriver。包内未包含(超过 PyPI 100MB 限制)。"
30
+ "请:(1) 本机安装 Chrome 与 ChromeDriver,或 (2) 将 get_driver / render_option_to_png 的 "
31
+ "chrome_headless_path、chromedriver_path 指向本地可执行文件。"
32
+ )
33
+
34
+ # ========== 自定义 Snapshot 脚本 ==========
35
+ SNAPSHOT_JS = """
36
+ var ele = document.querySelector('div[_echarts_instance_]');
37
+ var mychart = echarts.getInstanceByDom(ele);
38
+ return mychart.getDataURL({
39
+ type: '%s',
40
+ pixelRatio: %s,
41
+ excludeComponents: ['toolbox']
42
+ });
43
+ """
44
+
45
+ SNAPSHOT_SVG_JS = """
46
+ var element = document.querySelector('div[_echarts_instance_] div');
47
+ return element.innerHTML;
48
+ """
49
+
50
+ # 内置配色(用于 build_option)
51
+ COLORS = [
52
+ "#5470c6",
53
+ "#91cc75",
54
+ "#fac858",
55
+ "#ee6666",
56
+ "#73c0de",
57
+ "#3ba272",
58
+ "#fc8452",
59
+ "#9a60b4",
60
+ "#ea7ccc",
61
+ ]
62
+
63
+
64
+ def _get_selenium():
65
+ """延迟导入 selenium,未安装时给出明确提示。"""
66
+ try:
67
+ from selenium import webdriver
68
+ from selenium.webdriver.chrome.options import Options
69
+ from selenium.webdriver.chrome.service import Service
70
+ return webdriver, Options, Service
71
+ except ImportError as e:
72
+ raise ImportError(
73
+ "ECharts 截图依赖 selenium,请安装: pip install fastapi-authly[charts] 或 pip install selenium"
74
+ ) from e
75
+
76
+
77
+ def get_driver(
78
+ *,
79
+ chrome_headless_path: str | Path | None = None,
80
+ chromedriver_path: str | Path | None = None,
81
+ ) -> Any:
82
+ """
83
+ 创建并返回 Chrome Headless WebDriver 实例。
84
+
85
+ Chrome 与 ChromeDriver 不随包分发(体积超 PyPI 限制),需本机安装或传入路径。
86
+ 若包内存在(本地开发时)则优先使用;否则必须传入 chrome_headless_path / chromedriver_path。
87
+ """
88
+ webdriver_module, Options, Service = _get_selenium()
89
+ binary = chrome_headless_path or DEFAULT_CHROME_HEADLESS_PATH
90
+ driver_path = chromedriver_path or DEFAULT_CHROMEDRIVER_PATH
91
+ if binary is None or driver_path is None:
92
+ raise FileNotFoundError(_CHROME_MISSING_MSG)
93
+ options = Options()
94
+ options.add_argument("--headless=new")
95
+ options.add_argument("--no-sandbox")
96
+ options.add_argument("--disable-dev-shm-usage")
97
+ options.add_argument("--disable-gpu")
98
+ options.binary_location = str(binary)
99
+ service = Service(executable_path=str(driver_path))
100
+ return webdriver_module.Chrome(service=service, options=options)
101
+
102
+
103
+ def engine_make_snapshot(
104
+ html_path: str,
105
+ file_type: str,
106
+ *,
107
+ pixel_ratio: int = 2,
108
+ delay: float = 2,
109
+ driver: Any = None,
110
+ chrome_headless_path: str | Path | None = None,
111
+ chromedriver_path: str | Path | None = None,
112
+ **kwargs: Any,
113
+ ) -> str:
114
+ """
115
+ 对已存在的 HTML 文件用无头浏览器打开并执行 ECharts 截图脚本,返回 base64 或 SVG 字符串。
116
+ """
117
+ if delay < 0:
118
+ raise ValueError("delay 不能为负数")
119
+ if driver is None:
120
+ driver = get_driver(
121
+ chrome_headless_path=chrome_headless_path,
122
+ chromedriver_path=chromedriver_path,
123
+ )
124
+ if file_type == "svg":
125
+ snapshot_js = SNAPSHOT_SVG_JS
126
+ else:
127
+ snapshot_js = SNAPSHOT_JS % (file_type, pixel_ratio)
128
+ if not html_path.startswith(("http://", "https://", "file://")):
129
+ html_path = "file://" + os.path.abspath(html_path)
130
+ try:
131
+ driver.get(html_path)
132
+ time.sleep(delay)
133
+ return driver.execute_script(snapshot_js)
134
+ finally:
135
+ driver.quit()
136
+
137
+
138
+ def echarts_option_to_html(
139
+ option: dict,
140
+ *,
141
+ width: int = 600,
142
+ height: int = 400,
143
+ title: str = "Chart",
144
+ local_echarts_path: str | Path | None = None,
145
+ ) -> str:
146
+ """将 ECharts option 字典组装成完整 HTML 字符串。"""
147
+ if local_echarts_path is None:
148
+ local_echarts_path = DEFAULT_ECHARTS_PATH
149
+ option_json = json.dumps(option, ensure_ascii=False)
150
+ js_src = "file://" + str(Path(local_echarts_path).resolve())
151
+ html = f"""<!DOCTYPE html>
152
+ <html lang="zh-CN">
153
+ <head>
154
+ <meta charset="UTF-8">
155
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
156
+ <title>{title}</title>
157
+ <script src="{js_src}"></script>
158
+ </head>
159
+ <body>
160
+ <div id="main" style="width:{width}px;height:{height}px;"></div>
161
+ <script>
162
+ var chartDom = document.getElementById("main");
163
+ var myChart = echarts.init(chartDom);
164
+ var option = {option_json};
165
+ myChart.setOption(option);
166
+ </script>
167
+ </body>
168
+ </html>"""
169
+ return html
170
+
171
+
172
+ def render_option_to_png(
173
+ option: dict,
174
+ output_name: str = "chart.png",
175
+ *,
176
+ width: int = 600,
177
+ height: int = 400,
178
+ title: str = "Chart",
179
+ delay: float = 2,
180
+ pixel_ratio: int = 2,
181
+ is_remove_html: bool = True,
182
+ local_echarts_path: str | Path | None = None,
183
+ chrome_headless_path: str | Path | None = None,
184
+ chromedriver_path: str | Path | None = None,
185
+ ) -> str:
186
+ """
187
+ 将 ECharts option 字典渲染为 PNG 图片。
188
+
189
+ 不限定图表类型,任意 ECharts 支持的图均可(折线、柱状、饼、散点、地图等)。
190
+
191
+ Args:
192
+ option: ECharts 原生 option 字典
193
+ output_name: 输出 PNG 路径
194
+ width: 画布宽度(像素)
195
+ height: 画布高度(像素)
196
+ title: 页面标题
197
+ delay: 打开 HTML 后等待渲染的秒数
198
+ pixel_ratio: 导出图片像素比
199
+ is_remove_html: 是否在完成后删除临时 HTML 文件
200
+ local_echarts_path: ECharts JS 路径,默认使用包内 static/echarts.min.js
201
+ chrome_headless_path: Chrome Headless 可执行文件路径,默认使用包内 static
202
+ chromedriver_path: ChromeDriver 路径,默认使用包内 static
203
+
204
+ Returns:
205
+ 输出文件的绝对路径
206
+ """
207
+ if not output_name.endswith(".png"):
208
+ output_name = output_name.rstrip(".png") + ".png"
209
+ if local_echarts_path is None:
210
+ local_echarts_path = DEFAULT_ECHARTS_PATH
211
+ html_content = echarts_option_to_html(
212
+ option,
213
+ width=width,
214
+ height=height,
215
+ title=title,
216
+ local_echarts_path=local_echarts_path,
217
+ )
218
+ with tempfile.NamedTemporaryFile(
219
+ mode="w",
220
+ suffix=".html",
221
+ delete=False,
222
+ encoding="utf-8",
223
+ ) as f:
224
+ f.write(html_content)
225
+ html_path = f.name
226
+ try:
227
+ raw = engine_make_snapshot(
228
+ html_path,
229
+ "png",
230
+ delay=delay,
231
+ pixel_ratio=pixel_ratio,
232
+ chrome_headless_path=chrome_headless_path,
233
+ chromedriver_path=chromedriver_path,
234
+ )
235
+ if "," in raw:
236
+ b64 = raw.split(",", 1)[1]
237
+ else:
238
+ b64 = raw
239
+ missing = len(b64) % 4
240
+ if missing:
241
+ b64 += "=" * (4 - missing)
242
+ data = base64.decodebytes(b64.encode("utf-8"))
243
+ with open(output_name, "wb") as f:
244
+ f.write(data)
245
+ return os.path.abspath(output_name)
246
+ finally:
247
+ if is_remove_html and os.path.exists(html_path):
248
+ os.unlink(html_path)
249
+
250
+
251
+ def build_option(
252
+ chart_type: str,
253
+ title: str,
254
+ data: List[Dict],
255
+ *,
256
+ label_key: str = "month_name",
257
+ value_key: str = "count",
258
+ ) -> dict:
259
+ """根据类型 + 标题 + 数据构建 ECharts option(便捷封装,支持 line / bar / pie)。"""
260
+ labels = [item[label_key] for item in data]
261
+ values = [item[value_key] for item in data]
262
+ data_pair = [{"name": lb, "value": val} for lb, val in zip(labels, values)]
263
+ if chart_type == "line":
264
+ return {
265
+ "title": {"text": title, "left": "center"},
266
+ "color": COLORS,
267
+ "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
268
+ "grid": {"left": "10%", "right": "10%", "top": "18%", "bottom": "15%", "containLabel": True},
269
+ "xAxis": {"type": "category", "data": labels},
270
+ "yAxis": {"type": "value"},
271
+ "series": [
272
+ {
273
+ "type": "line",
274
+ "data": values,
275
+ "smooth": True,
276
+ "symbol": "circle",
277
+ "symbolSize": 8,
278
+ "lineStyle": {"width": 2.5},
279
+ "itemStyle": {"borderWidth": 2, "borderColor": "#fff"},
280
+ "areaStyle": {"opacity": 0.15},
281
+ }
282
+ ],
283
+ }
284
+ if chart_type == "bar":
285
+ return {
286
+ "title": {"text": title, "left": "center"},
287
+ "color": COLORS,
288
+ "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
289
+ "grid": {"left": "10%", "right": "10%", "top": "18%", "bottom": "15%", "containLabel": True},
290
+ "xAxis": {"type": "category", "data": labels},
291
+ "yAxis": {"type": "value"},
292
+ "series": [
293
+ {
294
+ "type": "bar",
295
+ "data": values,
296
+ "itemStyle": {"borderRadius": [6, 6, 0, 0], "borderWidth": 0},
297
+ }
298
+ ],
299
+ }
300
+ if chart_type == "pie":
301
+ return {
302
+ "title": {"text": title, "left": "center"},
303
+ "color": COLORS,
304
+ "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
305
+ "legend": {"orient": "horizontal", "bottom": "5%", "type": "scroll"},
306
+ "series": [
307
+ {
308
+ "type": "pie",
309
+ "radius": ["40%", "70%"],
310
+ "center": ["50%", "50%"],
311
+ "data": data_pair,
312
+ "itemStyle": {"borderRadius": 8, "borderWidth": 2, "borderColor": "#fff"},
313
+ "label": {"formatter": "{b}: {c}", "position": "outside"},
314
+ }
315
+ ],
316
+ }
317
+ raise ValueError(f"不支持的图表类型: {chart_type},仅支持 line / bar / pie")
318
+
319
+
320
+ def render_chart_to_png(
321
+ chart_type: str,
322
+ title: str,
323
+ data: List[Dict],
324
+ output_name: str | None = None,
325
+ *,
326
+ label_key: str = "month_name",
327
+ value_key: str = "count",
328
+ width: int = 600,
329
+ height: int = 400,
330
+ delay: float = 2,
331
+ pixel_ratio: int = 2,
332
+ is_remove_html: bool = True,
333
+ local_echarts_path: str | Path | None = None,
334
+ chrome_headless_path: str | Path | None = None,
335
+ chromedriver_path: str | Path | None = None,
336
+ ) -> str:
337
+ """便捷封装:先 build_option 再 render_option_to_png。"""
338
+ option = build_option(
339
+ chart_type,
340
+ title,
341
+ data,
342
+ label_key=label_key,
343
+ value_key=value_key,
344
+ )
345
+ if output_name is None:
346
+ output_name = title + ".png"
347
+ return render_option_to_png(
348
+ option,
349
+ output_name=output_name,
350
+ width=width,
351
+ height=height,
352
+ title=title,
353
+ delay=delay,
354
+ pixel_ratio=pixel_ratio,
355
+ is_remove_html=is_remove_html,
356
+ local_echarts_path=local_echarts_path,
357
+ chrome_headless_path=chrome_headless_path,
358
+ chromedriver_path=chromedriver_path,
359
+ )