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.
Files changed (56) hide show
  1. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/PKG-INFO +1 -1
  2. fastapi_authly-0.1.8/ScienceClaw-master.zip +0 -0
  3. fastapi_authly-0.1.8/examples/chart_tools.py +247 -0
  4. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/pyproject.toml +1 -1
  5. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/__about__.py +1 -1
  6. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/auth.py +1 -1
  7. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/charts.py +197 -31
  8. fastapi_authly-0.1.8/src/fastapi_authly/core/config/__init__.py +8 -0
  9. 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
  10. fastapi_authly-0.1.8/src/fastapi_authly/core/config/base.py +10 -0
  11. fastapi_authly-0.1.8/src/fastapi_authly/core/config/database.py +98 -0
  12. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/deps.py +1 -1
  13. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/docs.py +1 -1
  14. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/.gitignore +0 -0
  15. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/LICENSE +0 -0
  16. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/README.md +0 -0
  17. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/README.zh.md +0 -0
  18. {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
  19. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/examples/correct_usage.py +0 -0
  20. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/.gitignore +0 -0
  21. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/class_index.html +0 -0
  22. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/coverage_html_cb_dd2e7eb5.js +0 -0
  23. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/favicon_32_cb_c827f16f.png +0 -0
  24. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/function_index.html +0 -0
  25. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/index.html +0 -0
  26. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/keybd_closed_cb_900cfef5.png +0 -0
  27. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/status.json +0 -0
  28. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/style_cb_9ff733b0.css +0 -0
  29. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_af1bec017750c6fc___init___py.html +0 -0
  30. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_af1bec017750c6fc_user_py.html +0 -0
  31. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e___about___py.html +0 -0
  32. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e___init___py.html +0 -0
  33. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e_auth_py.html +0 -0
  34. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_b9d93864b1b0ad6e_interfaces_py.html +0 -0
  35. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e___init___py.html +0 -0
  36. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e_config_py.html +0 -0
  37. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_d015ea9b27b0258e_security_py.html +0 -0
  38. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_ddd01122054512b0___init___py.html +0 -0
  39. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_ddd01122054512b0_tortoise_pg_py.html +0 -0
  40. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_feee2d9ae7f7fd96___init___py.html +0 -0
  41. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/htmlcov/z_feee2d9ae7f7fd96_user_py.html +0 -0
  42. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/__init__.py +0 -0
  43. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/contrib/__init__.py +0 -0
  44. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/contrib/tortoise_pg.py +0 -0
  45. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/core/__init__.py +0 -0
  46. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/core/security.py +0 -0
  47. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/interfaces.py +0 -0
  48. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/models/__init__.py +0 -0
  49. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/models/user.py +0 -0
  50. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/schemas/__init__.py +0 -0
  51. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/schemas/user.py +0 -0
  52. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/echarts.min.js +0 -0
  53. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/favicon.svg +0 -0
  54. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/scalar/standalone.js +0 -0
  55. {fastapi_authly-0.1.6 → fastapi_authly-0.1.8}/src/fastapi_authly/static/scalar/style.css +0 -0
  56. {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.6
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/
@@ -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.6"
7
+ version = "0.1.8"
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.6"
3
+ __version__ = "0.1.8"
@@ -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 = 2,
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
- time.sleep(delay)
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.quit()
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
- """根据类型 + 标题 + 数据构建 ECharts option(便捷封装,支持 line / bar / pie)。"""
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
- "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"},
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": True,
355
+ "smooth": 0.4,
276
356
  "symbol": "circle",
277
- "symbolSize": 8,
278
- "lineStyle": {"width": 2.5},
279
- "itemStyle": {"borderWidth": 2, "borderColor": "#fff"},
280
- "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
+ },
281
370
  }
282
371
  ],
283
372
  }
373
+
284
374
  if chart_type == "bar":
285
375
  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"},
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": values,
296
- "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,
297
430
  }
298
431
  ],
299
432
  }
433
+
300
434
  if chart_type == "pie":
301
435
  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"},
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": ["40%", "70%"],
310
- "center": ["50%", "50%"],
459
+ "radius": ["42%", "68%"],
460
+ "center": ["50%", "52%"],
311
461
  "data": data_pair,
312
- "itemStyle": {"borderRadius": 8, "borderWidth": 2, "borderColor": "#fff"},
313
- "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
+ },
314
479
  }
315
480
  ],
316
481
  }
482
+
317
483
  raise ValueError(f"不支持的图表类型: {chart_type},仅支持 line / bar / pie")
318
484
 
319
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,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