fastapi-authly 0.1.6__tar.gz → 0.1.8__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.6 → fastapi_authly-0.1.8}/PKG-INFO +1 -1
- fastapi_authly-0.1.8/ScienceClaw-master.zip +0 -0
- fastapi_authly-0.1.8/examples/chart_tools.py +247 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/pyproject.toml +1 -1
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/__about__.py +1 -1
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/auth.py +1 -1
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/charts.py +197 -31
- fastapi_authly-0.1.8/src/fastapi_authly/core/config/__init__.py +8 -0
- fastapi_authly-0.1.6/src/fastapi_authly/core/config.py → fastapi_authly-0.1.8/src/fastapi_authly/core/config/auth.py +6 -6
- fastapi_authly-0.1.8/src/fastapi_authly/core/config/base.py +10 -0
- fastapi_authly-0.1.8/src/fastapi_authly/core/config/database.py +98 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/deps.py +1 -1
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/docs.py +1 -1
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/.gitignore +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/LICENSE +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/README.md +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/README.zh.md +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/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.6 → fastapi_authly-0.1.8}/examples/correct_usage.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/.gitignore +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/class_index.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/function_index.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/index.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/status.json +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/style_cb_9ff733b0.css +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/__init__.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/contrib/__init__.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/core/__init__.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/core/security.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/interfaces.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/models/__init__.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/models/user.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/schemas/__init__.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/schemas/user.py +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/echarts.min.js +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/favicon.svg +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/scalar/standalone.js +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/scalar/style.css +0 -0
- {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/uv.lock +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.8
|
|
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/
|
|
Binary file
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
LangGraph 图表生成工具
|
|
4
|
+
|
|
5
|
+
提供三种精美图表的生成工具,供 LangGraph 智能体调用:
|
|
6
|
+
- render_line_chart 折线图(趋势分析)
|
|
7
|
+
- render_bar_chart 柱状图(对比分析)
|
|
8
|
+
- render_pie_chart 饼图(占比分析)
|
|
9
|
+
|
|
10
|
+
使用前提:
|
|
11
|
+
pip install fastapi-authly[charts] langgraph langchain-core
|
|
12
|
+
|
|
13
|
+
使用方式:
|
|
14
|
+
1. 应用启动时调用 init_chart_driver() 初始化无头浏览器
|
|
15
|
+
2. 将 CHART_TOOLS 列表绑定到 LangGraph ToolNode 即可
|
|
16
|
+
3. 应用退出时调用 shutdown_chart_driver() 释放资源
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import tempfile
|
|
22
|
+
import uuid
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Annotated, Any
|
|
25
|
+
|
|
26
|
+
from langchain_core.tools import tool
|
|
27
|
+
from fastapi_authly import (
|
|
28
|
+
get_driver,
|
|
29
|
+
build_option,
|
|
30
|
+
echarts_option_to_html,
|
|
31
|
+
engine_make_snapshot,
|
|
32
|
+
)
|
|
33
|
+
import base64
|
|
34
|
+
|
|
35
|
+
# ──────────────────────────────────────────────
|
|
36
|
+
# 全局 driver 管理
|
|
37
|
+
# ──────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
_driver: Any = None
|
|
40
|
+
|
|
41
|
+
# 图片输出目录,默认当前目录下的 chart_output/
|
|
42
|
+
OUTPUT_DIR = Path(os.environ.get("CHART_OUTPUT_DIR", "chart_output"))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def init_chart_driver(
|
|
46
|
+
output_dir: str | Path | None = None,
|
|
47
|
+
chrome_headless_path: str | Path | None = None,
|
|
48
|
+
chromedriver_path: str | Path | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
初始化全局无头浏览器 driver,应用启动时调用一次。
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
output_dir: 图片输出目录,默认 ./chart_output
|
|
55
|
+
chrome_headless_path: Chrome 可执行文件路径(不传则用包内默认)
|
|
56
|
+
chromedriver_path: ChromeDriver 路径(不传则用包内默认)
|
|
57
|
+
"""
|
|
58
|
+
global _driver, OUTPUT_DIR
|
|
59
|
+
if output_dir is not None:
|
|
60
|
+
OUTPUT_DIR = Path(output_dir)
|
|
61
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
_driver = get_driver(
|
|
63
|
+
chrome_headless_path=chrome_headless_path,
|
|
64
|
+
chromedriver_path=chromedriver_path,
|
|
65
|
+
)
|
|
66
|
+
print(f"[chart_tools] 无头浏览器已启动,图片输出目录:{OUTPUT_DIR.resolve()}")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def shutdown_chart_driver() -> None:
|
|
70
|
+
"""释放无头浏览器资源,应用退出时调用。"""
|
|
71
|
+
global _driver
|
|
72
|
+
if _driver is not None:
|
|
73
|
+
try:
|
|
74
|
+
_driver.quit()
|
|
75
|
+
except Exception:
|
|
76
|
+
pass
|
|
77
|
+
_driver = None
|
|
78
|
+
print("[chart_tools] 无头浏览器已关闭")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _get_driver() -> Any:
|
|
82
|
+
"""获取 driver,若已崩溃则自动重建。"""
|
|
83
|
+
global _driver
|
|
84
|
+
if _driver is None:
|
|
85
|
+
raise RuntimeError(
|
|
86
|
+
"无头浏览器未初始化,请先调用 init_chart_driver()"
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
_ = _driver.current_url # 探活
|
|
90
|
+
except Exception:
|
|
91
|
+
print("[chart_tools] 无头浏览器已崩溃,正在重建...")
|
|
92
|
+
try:
|
|
93
|
+
_driver.quit()
|
|
94
|
+
except Exception:
|
|
95
|
+
pass
|
|
96
|
+
_driver = get_driver()
|
|
97
|
+
return _driver
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _render(option: dict, output_name: str) -> str:
|
|
101
|
+
"""通用渲染:将 option 渲染为 PNG,返回文件绝对路径。"""
|
|
102
|
+
driver = _get_driver()
|
|
103
|
+
html = echarts_option_to_html(option, width=800, height=500)
|
|
104
|
+
with tempfile.NamedTemporaryFile(
|
|
105
|
+
mode="w", suffix=".html", delete=False, encoding="utf-8"
|
|
106
|
+
) as f:
|
|
107
|
+
f.write(html)
|
|
108
|
+
html_path = f.name
|
|
109
|
+
try:
|
|
110
|
+
raw = engine_make_snapshot(html_path, "png", driver=driver, delay=1.0)
|
|
111
|
+
b64 = raw.split(",", 1)[1] if "," in raw else raw
|
|
112
|
+
missing = 4 - len(b64) % 4
|
|
113
|
+
if missing != 4:
|
|
114
|
+
b64 += "=" * missing
|
|
115
|
+
data = base64.decodebytes(b64.encode())
|
|
116
|
+
out_path = OUTPUT_DIR / output_name
|
|
117
|
+
with open(out_path, "wb") as out:
|
|
118
|
+
out.write(data)
|
|
119
|
+
return str(out_path.resolve())
|
|
120
|
+
finally:
|
|
121
|
+
try:
|
|
122
|
+
os.unlink(html_path)
|
|
123
|
+
except Exception:
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# ──────────────────────────────────────────────
|
|
128
|
+
# LangGraph 工具定义
|
|
129
|
+
# ──────────────────────────────────────────────
|
|
130
|
+
|
|
131
|
+
@tool
|
|
132
|
+
def render_line_chart(
|
|
133
|
+
title: Annotated[str, "图表标题,例如:近6个月投诉数量趋势"],
|
|
134
|
+
data_json: Annotated[
|
|
135
|
+
str,
|
|
136
|
+
"JSON 字符串,列表格式,每项包含 label(X轴标签)和 value(数值)。"
|
|
137
|
+
"例如:[{\"label\": \"2024-01\", \"value\": 30}, {\"label\": \"2024-02\", \"value\": 45}]",
|
|
138
|
+
],
|
|
139
|
+
subtitle: Annotated[str, "图表副标题,可为空字符串"] = "",
|
|
140
|
+
) -> str:
|
|
141
|
+
"""
|
|
142
|
+
生成精美折线图(趋势分析),适用于时间序列、趋势对比等场景。
|
|
143
|
+
返回生成的 PNG 图片文件路径。
|
|
144
|
+
"""
|
|
145
|
+
data = json.loads(data_json)
|
|
146
|
+
# 统一 key 名
|
|
147
|
+
normalized = [{"label": str(item.get("label", item.get("month_name", ""))),
|
|
148
|
+
"value": item.get("value", item.get("count", 0))} for item in data]
|
|
149
|
+
option = build_option(
|
|
150
|
+
"line", title, normalized,
|
|
151
|
+
label_key="label", value_key="value", subtitle=subtitle,
|
|
152
|
+
)
|
|
153
|
+
filename = f"line_{uuid.uuid4().hex[:8]}.png"
|
|
154
|
+
path = _render(option, filename)
|
|
155
|
+
return f"折线图已生成:{path}"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@tool
|
|
159
|
+
def render_bar_chart(
|
|
160
|
+
title: Annotated[str, "图表标题,例如:各类型投诉数量对比"],
|
|
161
|
+
data_json: Annotated[
|
|
162
|
+
str,
|
|
163
|
+
"JSON 字符串,列表格式,每项包含 label(X轴标签)和 value(数值)。"
|
|
164
|
+
"例如:[{\"label\": \"食品安全\", \"value\": 120}, {\"label\": \"网络购物\", \"value\": 98}]",
|
|
165
|
+
],
|
|
166
|
+
subtitle: Annotated[str, "图表副标题,可为空字符串"] = "",
|
|
167
|
+
) -> str:
|
|
168
|
+
"""
|
|
169
|
+
生成精美柱状图(对比分析),适用于分类数据的数量对比场景。
|
|
170
|
+
返回生成的 PNG 图片文件路径。
|
|
171
|
+
"""
|
|
172
|
+
data = json.loads(data_json)
|
|
173
|
+
normalized = [{"label": str(item.get("label", item.get("month_name", ""))),
|
|
174
|
+
"value": item.get("value", item.get("count", 0))} for item in data]
|
|
175
|
+
option = build_option(
|
|
176
|
+
"bar", title, normalized,
|
|
177
|
+
label_key="label", value_key="value", subtitle=subtitle,
|
|
178
|
+
)
|
|
179
|
+
filename = f"bar_{uuid.uuid4().hex[:8]}.png"
|
|
180
|
+
path = _render(option, filename)
|
|
181
|
+
return f"柱状图已生成:{path}"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@tool
|
|
185
|
+
def render_pie_chart(
|
|
186
|
+
title: Annotated[str, "图表标题,例如:投诉类型占比分布"],
|
|
187
|
+
data_json: Annotated[
|
|
188
|
+
str,
|
|
189
|
+
"JSON 字符串,列表格式,每项包含 label(类别名称)和 value(数值)。"
|
|
190
|
+
"例如:[{\"label\": \"食品安全\", \"value\": 35}, {\"label\": \"网络购物\", \"value\": 28}]",
|
|
191
|
+
],
|
|
192
|
+
subtitle: Annotated[str, "图表副标题,可为空字符串"] = "",
|
|
193
|
+
) -> str:
|
|
194
|
+
"""
|
|
195
|
+
生成精美饼图(占比分析),适用于展示各类别在总量中的比例。
|
|
196
|
+
返回生成的 PNG 图片文件路径。
|
|
197
|
+
"""
|
|
198
|
+
data = json.loads(data_json)
|
|
199
|
+
normalized = [{"label": str(item.get("label", item.get("month_name", ""))),
|
|
200
|
+
"value": item.get("value", item.get("count", 0))} for item in data]
|
|
201
|
+
option = build_option(
|
|
202
|
+
"pie", title, normalized,
|
|
203
|
+
label_key="label", value_key="value", subtitle=subtitle,
|
|
204
|
+
)
|
|
205
|
+
filename = f"pie_{uuid.uuid4().hex[:8]}.png"
|
|
206
|
+
path = _render(option, filename)
|
|
207
|
+
return f"饼图已生成:{path}"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# 导出工具列表,直接绑定到 ToolNode
|
|
211
|
+
CHART_TOOLS = [render_line_chart, render_bar_chart, render_pie_chart]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ──────────────────────────────────────────────
|
|
215
|
+
# 使用示例
|
|
216
|
+
# ──────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
if __name__ == "__main__":
|
|
219
|
+
from langgraph.prebuilt import create_react_agent
|
|
220
|
+
from langchain_openai import ChatOpenAI # 替换为你实际使用的 LLM
|
|
221
|
+
|
|
222
|
+
# 1. 启动无头浏览器
|
|
223
|
+
init_chart_driver(output_dir="./chart_output")
|
|
224
|
+
|
|
225
|
+
try:
|
|
226
|
+
# 2. 创建绑定了图表工具的智能体
|
|
227
|
+
llm = ChatOpenAI(model="gpt-4o", temperature=0)
|
|
228
|
+
agent = create_react_agent(llm, tools=CHART_TOOLS)
|
|
229
|
+
|
|
230
|
+
# 3. 调用智能体,让它自动选择图表类型并生成图片
|
|
231
|
+
result = agent.invoke({
|
|
232
|
+
"messages": [
|
|
233
|
+
{
|
|
234
|
+
"role": "user",
|
|
235
|
+
"content": (
|
|
236
|
+
"请根据以下数据生成一张折线图和一张饼图:\n"
|
|
237
|
+
"各月投诉数量:1月120件、2月98件、3月145件、4月200件、5月176件、6月210件\n"
|
|
238
|
+
"投诉类型分布:食品安全35%、网络购物28%、电话骚扰18%、产品质量12%、其他7%"
|
|
239
|
+
),
|
|
240
|
+
}
|
|
241
|
+
]
|
|
242
|
+
})
|
|
243
|
+
print(result["messages"][-1].content)
|
|
244
|
+
|
|
245
|
+
finally:
|
|
246
|
+
# 4. 退出时关闭浏览器
|
|
247
|
+
shutdown_chart_driver()
|
|
@@ -7,7 +7,7 @@ from fastapi.security import OAuth2PasswordBearer
|
|
|
7
7
|
from jose import JWTError
|
|
8
8
|
from pydantic import EmailStr
|
|
9
9
|
|
|
10
|
-
from .core.config import AuthConfig, AuthDependencyConfig, _config
|
|
10
|
+
from .core.config.auth import AuthConfig, AuthDependencyConfig, _config
|
|
11
11
|
from .core.security import BcryptPasswordHasher, JWTTokenService
|
|
12
12
|
from .interfaces import Mailer, PasswordHasher, TokenService, UserRepository
|
|
13
13
|
from .schemas.user import (
|
|
@@ -60,6 +60,17 @@ COLORS = [
|
|
|
60
60
|
"#ea7ccc",
|
|
61
61
|
]
|
|
62
62
|
|
|
63
|
+
# 精美主题配色
|
|
64
|
+
_THEME_COLORS = [
|
|
65
|
+
"#4E91FF", "#36CBCB", "#FAD337", "#F2637B", "#975FE4",
|
|
66
|
+
"#52C41A", "#FA8C16", "#1890FF", "#EB2F96", "#13C2C2",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
_BACKGROUND_COLOR = "#ffffff"
|
|
70
|
+
_TITLE_COLOR = "#1a1a2e"
|
|
71
|
+
_AXIS_COLOR = "#8c8c8c"
|
|
72
|
+
_SPLIT_LINE_COLOR = "#f0f0f0"
|
|
73
|
+
|
|
63
74
|
|
|
64
75
|
def _get_selenium():
|
|
65
76
|
"""延迟导入 selenium,未安装时给出明确提示。"""
|
|
@@ -105,7 +116,7 @@ def engine_make_snapshot(
|
|
|
105
116
|
file_type: str,
|
|
106
117
|
*,
|
|
107
118
|
pixel_ratio: int = 2,
|
|
108
|
-
delay: float =
|
|
119
|
+
delay: float = 1,
|
|
109
120
|
driver: Any = None,
|
|
110
121
|
chrome_headless_path: str | Path | None = None,
|
|
111
122
|
chromedriver_path: str | Path | None = None,
|
|
@@ -116,6 +127,7 @@ def engine_make_snapshot(
|
|
|
116
127
|
"""
|
|
117
128
|
if delay < 0:
|
|
118
129
|
raise ValueError("delay 不能为负数")
|
|
130
|
+
_external_driver = driver is not None
|
|
119
131
|
if driver is None:
|
|
120
132
|
driver = get_driver(
|
|
121
133
|
chrome_headless_path=chrome_headless_path,
|
|
@@ -129,10 +141,33 @@ def engine_make_snapshot(
|
|
|
129
141
|
html_path = "file://" + os.path.abspath(html_path)
|
|
130
142
|
try:
|
|
131
143
|
driver.get(html_path)
|
|
132
|
-
|
|
144
|
+
if delay > 0:
|
|
145
|
+
# 优先用 JS 轮询等待 ECharts 实例就绪(最多等 delay 秒),避免无谓的固定等待
|
|
146
|
+
_wait_js = """
|
|
147
|
+
var done = arguments[arguments.length - 1];
|
|
148
|
+
var deadline = Date.now() + %d;
|
|
149
|
+
function check() {
|
|
150
|
+
var ele = document.querySelector('div[_echarts_instance_]');
|
|
151
|
+
if (ele && echarts.getInstanceByDom(ele)) {
|
|
152
|
+
done(true);
|
|
153
|
+
} else if (Date.now() < deadline) {
|
|
154
|
+
setTimeout(check, 50);
|
|
155
|
+
} else {
|
|
156
|
+
done(false);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
check();
|
|
160
|
+
""" % int(delay * 1000)
|
|
161
|
+
try:
|
|
162
|
+
driver.set_script_timeout(delay + 1)
|
|
163
|
+
driver.execute_async_script(_wait_js)
|
|
164
|
+
except Exception:
|
|
165
|
+
time.sleep(delay)
|
|
133
166
|
return driver.execute_script(snapshot_js)
|
|
134
167
|
finally:
|
|
135
|
-
driver
|
|
168
|
+
# 仅当 driver 由本函数内部创建时才 quit;外部传入的 driver 由调用方管理生命周期
|
|
169
|
+
if not _external_driver:
|
|
170
|
+
driver.quit()
|
|
136
171
|
|
|
137
172
|
|
|
138
173
|
def echarts_option_to_html(
|
|
@@ -162,6 +197,11 @@ def echarts_option_to_html(
|
|
|
162
197
|
var chartDom = document.getElementById("main");
|
|
163
198
|
var myChart = echarts.init(chartDom);
|
|
164
199
|
var option = {option_json};
|
|
200
|
+
// 强制关闭所有动画,确保截图时图表已完整渲染
|
|
201
|
+
if (option.animation === undefined) option.animation = false;
|
|
202
|
+
if (option.series) {{
|
|
203
|
+
option.series.forEach(function(s) {{ s.animation = false; }});
|
|
204
|
+
}}
|
|
165
205
|
myChart.setOption(option);
|
|
166
206
|
</script>
|
|
167
207
|
</body>
|
|
@@ -255,65 +295,191 @@ def build_option(
|
|
|
255
295
|
*,
|
|
256
296
|
label_key: str = "month_name",
|
|
257
297
|
value_key: str = "count",
|
|
298
|
+
subtitle: str = "",
|
|
258
299
|
) -> dict:
|
|
259
|
-
"""根据类型 + 标题 +
|
|
300
|
+
"""根据类型 + 标题 + 数据构建精美 ECharts option(支持 line / bar / pie)。"""
|
|
260
301
|
labels = [item[label_key] for item in data]
|
|
261
302
|
values = [item[value_key] for item in data]
|
|
262
303
|
data_pair = [{"name": lb, "value": val} for lb, val in zip(labels, values)]
|
|
304
|
+
|
|
305
|
+
_base_title = {
|
|
306
|
+
"text": title,
|
|
307
|
+
"subtext": subtitle,
|
|
308
|
+
"left": "center",
|
|
309
|
+
"top": "4%",
|
|
310
|
+
"textStyle": {
|
|
311
|
+
"color": _TITLE_COLOR,
|
|
312
|
+
"fontSize": 18,
|
|
313
|
+
"fontWeight": "bold",
|
|
314
|
+
"fontFamily": "PingFang SC, Microsoft YaHei, sans-serif",
|
|
315
|
+
},
|
|
316
|
+
"subtextStyle": {"color": _AXIS_COLOR, "fontSize": 12},
|
|
317
|
+
}
|
|
318
|
+
|
|
263
319
|
if chart_type == "line":
|
|
320
|
+
# 计算渐变色区域(用第一个主色)
|
|
321
|
+
main_color = _THEME_COLORS[0]
|
|
264
322
|
return {
|
|
265
|
-
"
|
|
266
|
-
"color":
|
|
267
|
-
"
|
|
268
|
-
"
|
|
269
|
-
|
|
270
|
-
|
|
323
|
+
"backgroundColor": _BACKGROUND_COLOR,
|
|
324
|
+
"color": _THEME_COLORS,
|
|
325
|
+
"title": _base_title,
|
|
326
|
+
"tooltip": {
|
|
327
|
+
"trigger": "axis",
|
|
328
|
+
"backgroundColor": "rgba(255,255,255,0.95)",
|
|
329
|
+
"borderColor": "#e8e8e8",
|
|
330
|
+
"borderWidth": 1,
|
|
331
|
+
"textStyle": {"color": _TITLE_COLOR, "fontSize": 13},
|
|
332
|
+
"axisPointer": {"type": "cross", "label": {"backgroundColor": main_color}},
|
|
333
|
+
},
|
|
334
|
+
"grid": {"left": "5%", "right": "5%", "top": "22%", "bottom": "12%", "containLabel": True},
|
|
335
|
+
"xAxis": {
|
|
336
|
+
"type": "category",
|
|
337
|
+
"data": labels,
|
|
338
|
+
"boundaryGap": False,
|
|
339
|
+
"axisLine": {"lineStyle": {"color": "#e0e0e0"}},
|
|
340
|
+
"axisTick": {"show": False},
|
|
341
|
+
"axisLabel": {"color": _AXIS_COLOR, "fontSize": 12},
|
|
342
|
+
"splitLine": {"show": False},
|
|
343
|
+
},
|
|
344
|
+
"yAxis": {
|
|
345
|
+
"type": "value",
|
|
346
|
+
"axisLine": {"show": False},
|
|
347
|
+
"axisTick": {"show": False},
|
|
348
|
+
"axisLabel": {"color": _AXIS_COLOR, "fontSize": 12},
|
|
349
|
+
"splitLine": {"lineStyle": {"color": _SPLIT_LINE_COLOR, "type": "dashed"}},
|
|
350
|
+
},
|
|
271
351
|
"series": [
|
|
272
352
|
{
|
|
273
353
|
"type": "line",
|
|
274
354
|
"data": values,
|
|
275
|
-
"smooth":
|
|
355
|
+
"smooth": 0.4,
|
|
276
356
|
"symbol": "circle",
|
|
277
|
-
"symbolSize":
|
|
278
|
-
"lineStyle": {"width":
|
|
279
|
-
"itemStyle": {"borderWidth": 2, "borderColor": "#fff"},
|
|
280
|
-
"areaStyle": {
|
|
357
|
+
"symbolSize": 7,
|
|
358
|
+
"lineStyle": {"width": 3, "color": main_color},
|
|
359
|
+
"itemStyle": {"color": main_color, "borderWidth": 2, "borderColor": "#fff"},
|
|
360
|
+
"areaStyle": {
|
|
361
|
+
"color": {
|
|
362
|
+
"type": "linear",
|
|
363
|
+
"x": 0, "y": 0, "x2": 0, "y2": 1,
|
|
364
|
+
"colorStops": [
|
|
365
|
+
{"offset": 0, "color": main_color + "55"},
|
|
366
|
+
{"offset": 1, "color": main_color + "05"},
|
|
367
|
+
],
|
|
368
|
+
}
|
|
369
|
+
},
|
|
281
370
|
}
|
|
282
371
|
],
|
|
283
372
|
}
|
|
373
|
+
|
|
284
374
|
if chart_type == "bar":
|
|
285
375
|
return {
|
|
286
|
-
"
|
|
287
|
-
"color":
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
376
|
+
"backgroundColor": _BACKGROUND_COLOR,
|
|
377
|
+
"color": _THEME_COLORS,
|
|
378
|
+
"title": _base_title,
|
|
379
|
+
"tooltip": {
|
|
380
|
+
"trigger": "axis",
|
|
381
|
+
"backgroundColor": "rgba(255,255,255,0.95)",
|
|
382
|
+
"borderColor": "#e8e8e8",
|
|
383
|
+
"borderWidth": 1,
|
|
384
|
+
"textStyle": {"color": _TITLE_COLOR, "fontSize": 13},
|
|
385
|
+
"axisPointer": {"type": "shadow"},
|
|
386
|
+
},
|
|
387
|
+
"grid": {"left": "5%", "right": "5%", "top": "22%", "bottom": "12%", "containLabel": True},
|
|
388
|
+
"xAxis": {
|
|
389
|
+
"type": "category",
|
|
390
|
+
"data": labels,
|
|
391
|
+
"axisLine": {"lineStyle": {"color": "#e0e0e0"}},
|
|
392
|
+
"axisTick": {"show": False},
|
|
393
|
+
"axisLabel": {"color": _AXIS_COLOR, "fontSize": 12, "interval": 0, "rotate": 0},
|
|
394
|
+
},
|
|
395
|
+
"yAxis": {
|
|
396
|
+
"type": "value",
|
|
397
|
+
"axisLine": {"show": False},
|
|
398
|
+
"axisTick": {"show": False},
|
|
399
|
+
"axisLabel": {"color": _AXIS_COLOR, "fontSize": 12},
|
|
400
|
+
"splitLine": {"lineStyle": {"color": _SPLIT_LINE_COLOR, "type": "dashed"}},
|
|
401
|
+
},
|
|
292
402
|
"series": [
|
|
293
403
|
{
|
|
294
404
|
"type": "bar",
|
|
295
|
-
"data":
|
|
296
|
-
|
|
405
|
+
"data": [
|
|
406
|
+
{
|
|
407
|
+
"value": v,
|
|
408
|
+
"itemStyle": {
|
|
409
|
+
"color": {
|
|
410
|
+
"type": "linear",
|
|
411
|
+
"x": 0, "y": 0, "x2": 0, "y2": 1,
|
|
412
|
+
"colorStops": [
|
|
413
|
+
{"offset": 0, "color": _THEME_COLORS[i % len(_THEME_COLORS)]},
|
|
414
|
+
{"offset": 1, "color": _THEME_COLORS[i % len(_THEME_COLORS)] + "88"},
|
|
415
|
+
],
|
|
416
|
+
},
|
|
417
|
+
"borderRadius": [6, 6, 0, 0],
|
|
418
|
+
},
|
|
419
|
+
"label": {
|
|
420
|
+
"show": True,
|
|
421
|
+
"position": "top",
|
|
422
|
+
"color": _AXIS_COLOR,
|
|
423
|
+
"fontSize": 12,
|
|
424
|
+
"fontWeight": "bold",
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
for i, v in enumerate(values)
|
|
428
|
+
],
|
|
429
|
+
"barMaxWidth": 50,
|
|
297
430
|
}
|
|
298
431
|
],
|
|
299
432
|
}
|
|
433
|
+
|
|
300
434
|
if chart_type == "pie":
|
|
301
435
|
return {
|
|
302
|
-
"
|
|
303
|
-
"color":
|
|
304
|
-
"
|
|
305
|
-
"
|
|
436
|
+
"backgroundColor": _BACKGROUND_COLOR,
|
|
437
|
+
"color": _THEME_COLORS,
|
|
438
|
+
"title": _base_title,
|
|
439
|
+
"tooltip": {
|
|
440
|
+
"trigger": "item",
|
|
441
|
+
"backgroundColor": "rgba(255,255,255,0.95)",
|
|
442
|
+
"borderColor": "#e8e8e8",
|
|
443
|
+
"borderWidth": 1,
|
|
444
|
+
"textStyle": {"color": _TITLE_COLOR, "fontSize": 13},
|
|
445
|
+
"formatter": "{b}<br/>数量:{c}<br/>占比:{d}%",
|
|
446
|
+
},
|
|
447
|
+
"legend": {
|
|
448
|
+
"orient": "horizontal",
|
|
449
|
+
"bottom": "3%",
|
|
450
|
+
"type": "scroll",
|
|
451
|
+
"textStyle": {"color": _AXIS_COLOR, "fontSize": 12},
|
|
452
|
+
"icon": "circle",
|
|
453
|
+
"itemWidth": 10,
|
|
454
|
+
"itemHeight": 10,
|
|
455
|
+
},
|
|
306
456
|
"series": [
|
|
307
457
|
{
|
|
308
458
|
"type": "pie",
|
|
309
|
-
"radius": ["
|
|
310
|
-
"center": ["50%", "
|
|
459
|
+
"radius": ["42%", "68%"],
|
|
460
|
+
"center": ["50%", "52%"],
|
|
311
461
|
"data": data_pair,
|
|
312
|
-
"itemStyle": {
|
|
313
|
-
|
|
462
|
+
"itemStyle": {
|
|
463
|
+
"borderRadius": 8,
|
|
464
|
+
"borderWidth": 3,
|
|
465
|
+
"borderColor": "#ffffff",
|
|
466
|
+
},
|
|
467
|
+
"label": {
|
|
468
|
+
"show": True,
|
|
469
|
+
"formatter": "{b}\n{d}%",
|
|
470
|
+
"fontSize": 12,
|
|
471
|
+
"color": _TITLE_COLOR,
|
|
472
|
+
"lineHeight": 18,
|
|
473
|
+
},
|
|
474
|
+
"labelLine": {"length": 12, "length2": 8, "smooth": True},
|
|
475
|
+
"emphasis": {
|
|
476
|
+
"itemStyle": {"shadowBlur": 20, "shadowColor": "rgba(0,0,0,0.15)"},
|
|
477
|
+
"scaleSize": 8,
|
|
478
|
+
},
|
|
314
479
|
}
|
|
315
480
|
],
|
|
316
481
|
}
|
|
482
|
+
|
|
317
483
|
raise ValueError(f"不支持的图表类型: {chart_type},仅支持 line / bar / pie")
|
|
318
484
|
|
|
319
485
|
|
|
@@ -57,7 +57,7 @@ class AuthConfig(BaseSettings):
|
|
|
57
57
|
env_prefix="AUTH_",
|
|
58
58
|
extra="ignore",
|
|
59
59
|
)
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
@model_validator(mode='after')
|
|
62
62
|
def _ensure_jwt_is_config(self):
|
|
63
63
|
"""确保 jwt 字段始终是 JwtConfig 实例"""
|
|
@@ -66,7 +66,7 @@ class AuthConfig(BaseSettings):
|
|
|
66
66
|
elif not isinstance(self.jwt, JwtConfig):
|
|
67
67
|
self.jwt = JwtConfig()
|
|
68
68
|
return self
|
|
69
|
-
|
|
69
|
+
|
|
70
70
|
def _ensure_jwt_config(self) -> JwtConfig:
|
|
71
71
|
"""确保 jwt 是 JwtConfig 实例,如果不是则转换"""
|
|
72
72
|
if isinstance(self.jwt, dict):
|
|
@@ -74,26 +74,26 @@ class AuthConfig(BaseSettings):
|
|
|
74
74
|
elif not isinstance(self.jwt, JwtConfig):
|
|
75
75
|
self.jwt = JwtConfig()
|
|
76
76
|
return self.jwt
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
# 属性访问器:直接访问 jwt 中的属性,兼容现有代码
|
|
79
79
|
@property
|
|
80
80
|
def secret_key(self) -> str:
|
|
81
81
|
"""JWT secret key"""
|
|
82
82
|
jwt_config = self._ensure_jwt_config()
|
|
83
83
|
return jwt_config.secret_key
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
@property
|
|
86
86
|
def algorithm(self) -> str:
|
|
87
87
|
"""JWT algorithm"""
|
|
88
88
|
jwt_config = self._ensure_jwt_config()
|
|
89
89
|
return jwt_config.algorithm
|
|
90
|
-
|
|
90
|
+
|
|
91
91
|
@property
|
|
92
92
|
def access_token_expire_minutes(self) -> Optional[int]:
|
|
93
93
|
"""Access token expiration in minutes (compatible with auth.py)"""
|
|
94
94
|
jwt_config = self._ensure_jwt_config()
|
|
95
95
|
return jwt_config.access_token_expires_minutes
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
@property
|
|
98
98
|
def refresh_token_expire_days(self) -> int:
|
|
99
99
|
"""Refresh token expiration in days"""
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from pydantic import BaseModel, field_validator
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
class BaseBackendModel(BaseModel):
|
|
5
|
+
@field_validator("*", mode="before")
|
|
6
|
+
@classmethod
|
|
7
|
+
def strip_strings(cls, value: Any) -> Any:
|
|
8
|
+
if isinstance(value, str):
|
|
9
|
+
return value.strip()
|
|
10
|
+
return value
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from urllib.parse import quote_plus
|
|
3
|
+
from pydantic import field_validator, model_validator
|
|
4
|
+
from .base import BaseBackendModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DatabaseConfig(BaseBackendModel):
|
|
8
|
+
engine: str
|
|
9
|
+
name: Optional[str] = None
|
|
10
|
+
host: Optional[str] = None
|
|
11
|
+
port: Optional[int] = None
|
|
12
|
+
username: Optional[str] = None
|
|
13
|
+
password: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
@field_validator("engine", mode="before")
|
|
16
|
+
@classmethod
|
|
17
|
+
def normalize_engine(cls, value: str) -> str:
|
|
18
|
+
if not value:
|
|
19
|
+
raise ValueError("数据库配置错误:engine 不能为空")
|
|
20
|
+
|
|
21
|
+
engine = value.strip().lower()
|
|
22
|
+
|
|
23
|
+
aliases = {
|
|
24
|
+
"pg": "postgresql",
|
|
25
|
+
"pgsql": "postgresql",
|
|
26
|
+
"postgres": "postgresql",
|
|
27
|
+
"postgresql": "postgresql",
|
|
28
|
+
"mysql": "mysql",
|
|
29
|
+
"mongo": "mongodb",
|
|
30
|
+
"mongodb": "mongodb",
|
|
31
|
+
"sqlite": "sqlite",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if engine not in aliases:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
"数据库配置错误:engine 仅支持 "
|
|
37
|
+
"postgresql/pgsql/pg、mysql、mongodb/mongo、sqlite"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
return aliases[engine]
|
|
41
|
+
|
|
42
|
+
@field_validator("port")
|
|
43
|
+
@classmethod
|
|
44
|
+
def validate_port(cls, value: Optional[int]) -> Optional[int]:
|
|
45
|
+
if value is not None and not 1 <= value <= 65535:
|
|
46
|
+
raise ValueError("数据库配置错误:port 必须在 1 到 65535 之间")
|
|
47
|
+
return value
|
|
48
|
+
|
|
49
|
+
@model_validator(mode="after")
|
|
50
|
+
def validate_database_config(self):
|
|
51
|
+
if self.engine == "sqlite":
|
|
52
|
+
if not self.name:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"SQLite 配置错误:name 不能为空,例如 ./data/app.db 或 :memory:"
|
|
55
|
+
)
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
missing = []
|
|
59
|
+
|
|
60
|
+
if not self.host:
|
|
61
|
+
missing.append("host")
|
|
62
|
+
if not self.name:
|
|
63
|
+
missing.append("name")
|
|
64
|
+
|
|
65
|
+
if missing:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"{self.engine} 配置错误:缺少必要字段 {', '.join(missing)}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def database_url(self) -> str:
|
|
74
|
+
if self.engine == "sqlite":
|
|
75
|
+
return f"sqlite:///{self.name}"
|
|
76
|
+
|
|
77
|
+
default_ports = {
|
|
78
|
+
"postgresql": 5432,
|
|
79
|
+
"mysql": 3306,
|
|
80
|
+
"mongodb": 27017,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
default_drivers = {
|
|
84
|
+
"postgresql": "postgresql+asyncpg",
|
|
85
|
+
"mysql": "mysql+asyncmy",
|
|
86
|
+
"mongodb": "mongodb",
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
port = self.port or default_ports[self.engine]
|
|
90
|
+
driver = default_drivers[self.engine]
|
|
91
|
+
|
|
92
|
+
auth = ""
|
|
93
|
+
if self.username:
|
|
94
|
+
username = quote_plus(self.username)
|
|
95
|
+
password = quote_plus(self.password or "")
|
|
96
|
+
auth = f"{username}:{password}@" if password else f"{username}@"
|
|
97
|
+
|
|
98
|
+
return f"{driver}://{auth}{self.host}:{port}/{self.name}"
|
|
@@ -5,7 +5,7 @@ from fastapi.security.utils import get_authorization_scheme_param
|
|
|
5
5
|
import jwt
|
|
6
6
|
from jwt.exceptions import InvalidTokenError
|
|
7
7
|
from pydantic import BaseModel, ValidationError
|
|
8
|
-
from .core.config import _config
|
|
8
|
+
from .core.config.auth import _config
|
|
9
9
|
from .models.user import User
|
|
10
10
|
|
|
11
11
|
|
|
@@ -13,12 +13,12 @@ def get_static_dir() -> Path:
|
|
|
13
13
|
"""获取包内静态文件目录路径"""
|
|
14
14
|
return Path(__file__).parent / "static"
|
|
15
15
|
|
|
16
|
+
static_url = "/static"
|
|
16
17
|
|
|
17
18
|
def setup_scalar_docs(
|
|
18
19
|
app: FastAPI,
|
|
19
20
|
*,
|
|
20
21
|
docs_url: str = "/docs",
|
|
21
|
-
static_url: str = "/static",
|
|
22
22
|
title: Optional[str] = None,
|
|
23
23
|
openapi_url: Optional[str] = None,
|
|
24
24
|
favicon_url: Optional[str] = None,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/scalar/standalone.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|