fastapi-authly 0.1.7__tar.gz → 0.1.9__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 (56) hide show
  1. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/PKG-INFO +1 -1
  2. fastapi_authly-0.1.9/examples/chart_tools.py +247 -0
  3. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/pyproject.toml +1 -1
  4. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/__about__.py +1 -1
  5. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/auth.py +1 -1
  6. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/charts.py +165 -28
  7. fastapi_authly-0.1.9/src/fastapi_authly/core/config/__init__.py +8 -0
  8. fastapi_authly-0.1.7/src/fastapi_authly/core/config.py → fastapi_authly-0.1.9/src/fastapi_authly/core/config/auth.py +6 -6
  9. fastapi_authly-0.1.9/src/fastapi_authly/core/config/base.py +10 -0
  10. fastapi_authly-0.1.9/src/fastapi_authly/core/config/database.py +98 -0
  11. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/deps.py +1 -1
  12. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/docs.py +7 -4
  13. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/.gitignore +0 -0
  14. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/LICENSE +0 -0
  15. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/README.md +0 -0
  16. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/README.zh.md +0 -0
  17. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/ScienceClaw-master.zip +0 -0
  18. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/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
  19. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/examples/correct_usage.py +0 -0
  20. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/.gitignore +0 -0
  21. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/class_index.html +0 -0
  22. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  23. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  24. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/function_index.html +0 -0
  25. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/index.html +0 -0
  26. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  27. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/status.json +0 -0
  28. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/style_cb_9ff733b0.css +0 -0
  29. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
  30. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
  31. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
  32. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
  33. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
  34. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
  35. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
  36. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
  37. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
  38. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
  39. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
  40. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
  41. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
  42. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/__init__.py +0 -0
  43. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/contrib/__init__.py +0 -0
  44. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
  45. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/core/__init__.py +0 -0
  46. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/core/security.py +0 -0
  47. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/interfaces.py +0 -0
  48. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/models/__init__.py +0 -0
  49. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/models/user.py +0 -0
  50. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/schemas/__init__.py +0 -0
  51. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/schemas/user.py +0 -0
  52. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/static/echarts.min.js +0 -0
  53. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/static/favicon.svg +0 -0
  54. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/static/scalar/standalone.js +0 -0
  55. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/src/fastapi_authly/static/scalar/style.css +0 -0
  56. {fastapi_authly-0.1.7 → fastapi_authly-0.1.9}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fastapi-authly
3
- Version: 0.1.7
3
+ Version: 0.1.9
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/
@@ -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()
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "fastapi-authly"
7
- version = "0.1.7"
7
+ version = "0.1.9"
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"}
@@ -1,3 +1,3 @@
1
1
  """Version information."""
2
2
 
3
- __version__ = "0.1.7"
3
+ __version__ = "0.1.9"
@@ -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,未安装时给出明确提示。"""
@@ -284,65 +295,191 @@ def build_option(
284
295
  *,
285
296
  label_key: str = "month_name",
286
297
  value_key: str = "count",
298
+ subtitle: str = "",
287
299
  ) -> dict:
288
- """根据类型 + 标题 + 数据构建 ECharts option(便捷封装,支持 line / bar / pie)。"""
300
+ """根据类型 + 标题 + 数据构建精美 ECharts option(支持 line / bar / pie)。"""
289
301
  labels = [item[label_key] for item in data]
290
302
  values = [item[value_key] for item in data]
291
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
+
292
319
  if chart_type == "line":
320
+ # 计算渐变色区域(用第一个主色)
321
+ main_color = _THEME_COLORS[0]
293
322
  return {
294
- "title": {"text": title, "left": "center"},
295
- "color": COLORS,
296
- "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
297
- "grid": {"left": "10%", "right": "10%", "top": "18%", "bottom": "15%", "containLabel": True},
298
- "xAxis": {"type": "category", "data": labels},
299
- "yAxis": {"type": "value"},
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
+ },
300
351
  "series": [
301
352
  {
302
353
  "type": "line",
303
354
  "data": values,
304
- "smooth": True,
355
+ "smooth": 0.4,
305
356
  "symbol": "circle",
306
- "symbolSize": 8,
307
- "lineStyle": {"width": 2.5},
308
- "itemStyle": {"borderWidth": 2, "borderColor": "#fff"},
309
- "areaStyle": {"opacity": 0.15},
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
+ },
310
370
  }
311
371
  ],
312
372
  }
373
+
313
374
  if chart_type == "bar":
314
375
  return {
315
- "title": {"text": title, "left": "center"},
316
- "color": COLORS,
317
- "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
318
- "grid": {"left": "10%", "right": "10%", "top": "18%", "bottom": "15%", "containLabel": True},
319
- "xAxis": {"type": "category", "data": labels},
320
- "yAxis": {"type": "value"},
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
+ },
321
402
  "series": [
322
403
  {
323
404
  "type": "bar",
324
- "data": values,
325
- "itemStyle": {"borderRadius": [6, 6, 0, 0], "borderWidth": 0},
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,
326
430
  }
327
431
  ],
328
432
  }
433
+
329
434
  if chart_type == "pie":
330
435
  return {
331
- "title": {"text": title, "left": "center"},
332
- "color": COLORS,
333
- "toolbox": {"show": True, "feature": {"saveAsImage": {"show": True}}},
334
- "legend": {"orient": "horizontal", "bottom": "5%", "type": "scroll"},
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
+ },
335
456
  "series": [
336
457
  {
337
458
  "type": "pie",
338
- "radius": ["40%", "70%"],
339
- "center": ["50%", "50%"],
459
+ "radius": ["42%", "68%"],
460
+ "center": ["50%", "52%"],
340
461
  "data": data_pair,
341
- "itemStyle": {"borderRadius": 8, "borderWidth": 2, "borderColor": "#fff"},
342
- "label": {"formatter": "{b}: {c}", "position": "outside"},
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
+ },
343
479
  }
344
480
  ],
345
481
  }
482
+
346
483
  raise ValueError(f"不支持的图表类型: {chart_type},仅支持 line / bar / pie")
347
484
 
348
485
 
@@ -0,0 +1,8 @@
1
+ from .database import DatabaseConfig
2
+ from .auth import AuthConfig, AuthDependencyConfig, JwtConfig
3
+ __all__ = [
4
+ "DatabaseConfig",
5
+ "AuthConfig",
6
+ "JwtConfig",
7
+ "AuthDependencyConfig"
8
+ ]
@@ -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,11 @@ def get_static_dir() -> Path:
13
13
  """获取包内静态文件目录路径"""
14
14
  return Path(__file__).parent / "static"
15
15
 
16
- static_url = "/static"
17
-
18
16
  def setup_scalar_docs(
19
17
  app: FastAPI,
20
18
  *,
21
19
  docs_url: str = "/docs",
20
+ static_url: str = "/fastapi-authly-static",
22
21
  title: Optional[str] = None,
23
22
  openapi_url: Optional[str] = None,
24
23
  favicon_url: Optional[str] = None,
@@ -29,7 +28,7 @@ def setup_scalar_docs(
29
28
  Args:
30
29
  app: FastAPI 应用实例
31
30
  docs_url: 文档页面 URL,默认为 "/docs"
32
- static_url: 静态文件 URL 前缀,默认为 "/static"
31
+ static_url: 静态文件 URL 前缀,默认为 "/fastapi-authly-static"
33
32
  title: 文档标题,默认使用 app.title
34
33
  openapi_url: OpenAPI schema URL,默认使用 app.openapi_url
35
34
  favicon_url: Favicon 图标 URL,默认为 "{static_url}/favicon.svg"
@@ -39,7 +38,11 @@ def setup_scalar_docs(
39
38
 
40
39
  # 挂载静态文件
41
40
  if os.path.exists(static_dir):
42
- app.mount(static_url, StaticFiles(directory=str(static_dir)), name="static")
41
+ app.mount(
42
+ static_url,
43
+ StaticFiles(directory=str(static_dir)),
44
+ name="fastapi_authly_static",
45
+ )
43
46
 
44
47
  # 设置文档标题和 OpenAPI URL
45
48
  doc_title = title or app.title
File without changes
File without changes
File without changes