fastapi-authly 0.1.3__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 (54) hide show
  1. fastapi_authly-0.1.5/.gitignore +10 -0
  2. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/PKG-INFO +5 -1
  3. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/README.zh.md +39 -0
  4. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/pyproject.toml +11 -2
  5. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/__about__.py +1 -1
  6. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/__init__.py +18 -0
  7. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/auth.py +4 -2
  8. fastapi_authly-0.1.5/src/fastapi_authly/charts.py +359 -0
  9. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/config.py +31 -9
  10. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/security.py +8 -1
  11. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/docs.py +20 -0
  12. fastapi_authly-0.1.5/src/fastapi_authly/static/echarts.min.js +45 -0
  13. fastapi_authly-0.1.5/src/fastapi_authly/static/favicon.svg +1 -0
  14. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/uv.lock +225 -2
  15. fastapi_authly-0.1.3/.gitignore +0 -10
  16. fastapi_authly-0.1.3/coverage.xml +0 -407
  17. fastapi_authly-0.1.3/test.py +0 -177
  18. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/LICENSE +0 -0
  19. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/README.md +0 -0
  20. {fastapi_authly-0.1.3 → 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
  21. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/examples/correct_usage.py +0 -0
  22. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/.gitignore +0 -0
  23. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/class_index.html +0 -0
  24. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  25. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  26. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/function_index.html +0 -0
  27. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/index.html +0 -0
  28. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  29. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/status.json +0 -0
  30. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/style_cb_9ff733b0.css +0 -0
  31. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
  32. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
  33. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
  34. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
  35. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
  36. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
  37. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
  38. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
  39. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
  40. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
  41. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
  42. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
  43. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
  44. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/__init__.py +0 -0
  45. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
  46. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/__init__.py +0 -0
  47. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/deps.py +0 -0
  48. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/interfaces.py +0 -0
  49. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/models/__init__.py +0 -0
  50. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/models/user.py +0 -0
  51. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/__init__.py +0 -0
  52. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/user.py +0 -0
  53. {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/static/scalar/standalone.js +0 -0
  54. {fastapi_authly-0.1.3 → 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.3
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/
@@ -25,6 +25,7 @@ Classifier: Programming Language :: Python :: 3.12
25
25
  Classifier: Topic :: Internet :: WWW/HTTP :: Session
26
26
  Classifier: Topic :: Security
27
27
  Requires-Python: >=3.10
28
+ Requires-Dist: asyncpg>=0.31.0
28
29
  Requires-Dist: emails>=0.6.0
29
30
  Requires-Dist: fastapi>=0.100.0
30
31
  Requires-Dist: jinja2>=3.0.0
@@ -35,6 +36,9 @@ Requires-Dist: pydantic[email]>=2.0.0
35
36
  Requires-Dist: python-jose[cryptography]>=3.3.0
36
37
  Requires-Dist: python-multipart>=0.0.6
37
38
  Requires-Dist: tortoise-orm[psycopg]>=0.25.3
39
+ Requires-Dist: uvicorn>=0.40.0
40
+ Provides-Extra: charts
41
+ Requires-Dist: selenium>=4.0.0; extra == 'charts'
38
42
  Provides-Extra: docs
39
43
  Requires-Dist: mkdocs-material>=9.0.0; extra == 'docs'
40
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.3"
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"}
@@ -49,12 +49,15 @@ dependencies = [
49
49
  "jinja2>=3.0.0",
50
50
  "jwt>=1.3.1",
51
51
  "tortoise-orm[psycopg]>=0.25.3",
52
+ "uvicorn>=0.40.0",
53
+ "asyncpg>=0.31.0",
52
54
  ]
53
55
 
54
56
  [project.optional-dependencies]
55
57
  # 可选依赖
56
58
  tortoise = ["tortoise-orm[asyncpg]>=0.20.0"]
57
59
  sqlalchemy = ["sqlalchemy>=2.0.0", "alembic>=1.12.0"]
60
+ charts = ["selenium>=4.0.0"]
58
61
  docs = ["mkdocs>=1.5.0", "mkdocs-material>=9.0.0"]
59
62
  test = [
60
63
  "pytest>=7.0.0",
@@ -75,13 +78,19 @@ path = "src/fastapi_authly/__about__.py"
75
78
 
76
79
  [tool.hatch.build.targets.wheel]
77
80
  packages = ["src/fastapi_authly"]
78
- # 静态文件在包目录内,会自动包含,无需额外配置
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
+ ]
79
86
 
80
87
  [tool.hatch.build.targets.sdist]
81
88
  exclude = [
82
89
  "/.*",
83
90
  "/docs/_build",
84
91
  "/tests",
92
+ "/src/fastapi_authly/static/chrome-headless-shell-linux64",
93
+ "/src/fastapi_authly/static/chromedriver-linux64",
85
94
  ]
86
95
 
87
96
  [tool.ruff]
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.1.3"
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
  ]
@@ -103,12 +103,14 @@ class AuthModule:
103
103
  status_code=status.HTTP_400_BAD_REQUEST,
104
104
  detail="Incorrect username or password",
105
105
  )
106
- if not self.password_hasher.verify(body.password, getattr(user, "hashed_password", "")):
106
+
107
+ hashed_password = getattr(user, "hashed_password", None) or ""
108
+ if not hashed_password or not self.password_hasher.verify(body.password, hashed_password):
107
109
  raise HTTPException(
108
110
  status_code=status.HTTP_400_BAD_REQUEST,
109
111
  detail="Incorrect username or password",
110
112
  )
111
-
113
+
112
114
  if not user.is_active:
113
115
  raise HTTPException(status_code=400, detail="Inactive user")
114
116
 
@@ -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
+ )
@@ -1,8 +1,8 @@
1
1
  """Configuration settings for fastapi-authly."""
2
2
 
3
- from typing import List, Optional, Any
3
+ from typing import List, Optional, Any, Union
4
4
  from datetime import timedelta
5
- from pydantic import BaseModel, Field, ConfigDict
5
+ from pydantic import BaseModel, Field, ConfigDict, model_validator
6
6
  from pydantic_settings import BaseSettings, SettingsConfigDict
7
7
 
8
8
  class JwtConfig(BaseModel):
@@ -26,8 +26,6 @@ class JwtConfig(BaseModel):
26
26
  return timedelta(days=self.refresh_token_expires_days)
27
27
 
28
28
 
29
-
30
-
31
29
  class AuthConfig(BaseSettings):
32
30
  """
33
31
  Runtime configuration loaded from environment variables with ``AUTH_`` prefix.
@@ -35,10 +33,11 @@ class AuthConfig(BaseSettings):
35
33
  Host applications can still pass an ``AuthConfig`` instance directly;
36
34
  in that case, the passed values override the environment values.
37
35
  """
38
- jwt: JwtConfig = JwtConfig()
36
+ jwt: Union[JwtConfig, dict] = Field(default_factory=JwtConfig)
39
37
 
40
38
  router_prefix: str = "/auth"
41
39
  router_tags: List[str] = Field(default_factory=lambda: ["authentication"])
40
+ token_url: str = "login" # OAuth2 token URL path
42
41
 
43
42
  enable_password_recovery: bool = True
44
43
  enable_user_registration: bool = True
@@ -59,26 +58,47 @@ class AuthConfig(BaseSettings):
59
58
  extra="ignore",
60
59
  )
61
60
 
61
+ @model_validator(mode='after')
62
+ def _ensure_jwt_is_config(self):
63
+ """确保 jwt 字段始终是 JwtConfig 实例"""
64
+ if isinstance(self.jwt, dict):
65
+ self.jwt = JwtConfig(**self.jwt)
66
+ elif not isinstance(self.jwt, JwtConfig):
67
+ self.jwt = JwtConfig()
68
+ return self
69
+
70
+ def _ensure_jwt_config(self) -> JwtConfig:
71
+ """确保 jwt 是 JwtConfig 实例,如果不是则转换"""
72
+ if isinstance(self.jwt, dict):
73
+ self.jwt = JwtConfig(**self.jwt)
74
+ elif not isinstance(self.jwt, JwtConfig):
75
+ self.jwt = JwtConfig()
76
+ return self.jwt
77
+
62
78
  # 属性访问器:直接访问 jwt 中的属性,兼容现有代码
63
79
  @property
64
80
  def secret_key(self) -> str:
65
81
  """JWT secret key"""
66
- return self.jwt.secret_key
82
+ jwt_config = self._ensure_jwt_config()
83
+ return jwt_config.secret_key
67
84
 
68
85
  @property
69
86
  def algorithm(self) -> str:
70
87
  """JWT algorithm"""
71
- return self.jwt.algorithm
88
+ jwt_config = self._ensure_jwt_config()
89
+ return jwt_config.algorithm
72
90
 
73
91
  @property
74
92
  def access_token_expire_minutes(self) -> Optional[int]:
75
93
  """Access token expiration in minutes (compatible with auth.py)"""
76
- return self.jwt.access_token_expires_minutes
94
+ jwt_config = self._ensure_jwt_config()
95
+ return jwt_config.access_token_expires_minutes
77
96
 
78
97
  @property
79
98
  def refresh_token_expire_days(self) -> int:
80
99
  """Refresh token expiration in days"""
81
- return self.jwt.refresh_token_expire_days
100
+ jwt_config = self._ensure_jwt_config()
101
+ return jwt_config.refresh_token_expires_days
82
102
 
83
103
  _config = AuthConfig()
84
104
 
@@ -95,4 +115,6 @@ class AuthDependencyConfig(BaseModel):
95
115
  token_service: Optional[Any] = None
96
116
  mailer: Optional[Any] = None
97
117
  # 可选:自定义 token 提取依赖(例如自定义 Bearer/JWT 解析)
118
+ # token_dependency: Optional[Any] = Noneptional[Any] = None
119
+ # 可选:自定义 token 提取依赖(例如自定义 Bearer/JWT 解析)
98
120
  token_dependency: Optional[Any] = None
@@ -3,6 +3,7 @@ from typing import Any, Dict, Optional
3
3
 
4
4
  from jose import JWTError, jwt
5
5
  from passlib.context import CryptContext
6
+ from passlib.exc import UnknownHashError
6
7
 
7
8
  from ..interfaces import PasswordHasher, TokenService
8
9
 
@@ -18,7 +19,13 @@ class BcryptPasswordHasher(PasswordHasher):
18
19
  return self._ctx.hash(password)
19
20
 
20
21
  def verify(self, password: str, hashed: str) -> bool:
21
- return self._ctx.verify(password, hashed)
22
+ if not hashed:
23
+ return False
24
+ try:
25
+ return self._ctx.verify(password, hashed)
26
+ except UnknownHashError:
27
+ # 如果哈希格式无法识别,返回 False
28
+ return False
22
29
 
23
30
 
24
31
  class JWTTokenService(TokenService):