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.
- fastapi_authly-0.1.5/.gitignore +10 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/PKG-INFO +5 -1
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/README.zh.md +39 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/pyproject.toml +11 -2
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/__about__.py +1 -1
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/__init__.py +18 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/auth.py +4 -2
- fastapi_authly-0.1.5/src/fastapi_authly/charts.py +359 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/config.py +31 -9
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/security.py +8 -1
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/docs.py +20 -0
- fastapi_authly-0.1.5/src/fastapi_authly/static/echarts.min.js +45 -0
- fastapi_authly-0.1.5/src/fastapi_authly/static/favicon.svg +1 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/uv.lock +225 -2
- fastapi_authly-0.1.3/.gitignore +0 -10
- fastapi_authly-0.1.3/coverage.xml +0 -407
- fastapi_authly-0.1.3/test.py +0 -177
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/LICENSE +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/README.md +0 -0
- {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
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/examples/correct_usage.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/.gitignore +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/class_index.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/function_index.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/index.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/status.json +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/style_cb_9ff733b0.css +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/__init__.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/core/__init__.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/deps.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/interfaces.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/models/__init__.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/models/user.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/__init__.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/schemas/user.py +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/static/scalar/standalone.js +0 -0
- {fastapi_authly-0.1.3 → fastapi_authly-0.1.5}/src/fastapi_authly/static/scalar/style.css +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fastapi-authly
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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]
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|