diff --git a/.gitignore b/.gitignore index 72db892..e960286 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ temp/ .venv .scripts + +# vite 构建产物 +src/njupt_suan_api/static/ diff --git a/.idea/NJUPT-API-Suan.iml b/.idea/NJUPT-API-Suan.iml index 77c3d5c..21ab9f4 100644 --- a/.idea/NJUPT-API-Suan.iml +++ b/.idea/NJUPT-API-Suan.iml @@ -2,6 +2,7 @@ + diff --git a/cli/__init__.py b/cli/__init__.py deleted file mode 100644 index 7d17ee4..0000000 --- a/cli/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .message import ALREADY_INIT_MESSAGE, NOT_INIT_MESSAGE -from .path import DATA_DIR, TEMP_DIR - -__all__ = [ - DATA_DIR, - TEMP_DIR, - NOT_INIT_MESSAGE, - ALREADY_INIT_MESSAGE, -] diff --git a/cli/message.py b/cli/message.py deleted file mode 100644 index 197e031..0000000 --- a/cli/message.py +++ /dev/null @@ -1,9 +0,0 @@ -NOT_INIT_MESSAGE = """ -❓ 当前目录或指定目录下[yellow]似乎还没有执行过初始化命令[/yellow]。 -❓ 你也许需要先执行 [green]suanapi init[/green] 。 -""" - -ALREADY_INIT_MESSAGE = """ -❕ 当前目录或指定目录下[yellow]似乎已经执行初始化命令过[/yellow]。 -❕ 你也许需要先删除已经存在的 [blue]data[/blue] 和 [blue]temp[/blue] 目录。 -""" diff --git a/cli/path.py b/cli/path.py deleted file mode 100644 index 8c9dcd7..0000000 --- a/cli/path.py +++ /dev/null @@ -1,4 +0,0 @@ -from pathlib import Path - -DATA_DIR = Path.cwd() / "data" -TEMP_DIR = Path.cwd() / "temp" diff --git a/manage.py b/manage.py deleted file mode 100644 index 6af2fe5..0000000 --- a/manage.py +++ /dev/null @@ -1,109 +0,0 @@ -from secrets import token_urlsafe - -import typer -from rich.console import Console -from rich.panel import Panel - -from cli import ALREADY_INIT_MESSAGE, DATA_DIR, NOT_INIT_MESSAGE, TEMP_DIR -from router import __version__ - -console = Console() -app = typer.Typer( - name="NJUPT-Suan-API", - help="NJUPT Suan API 部署与管理工具", - rich_markup_mode="rich", - no_args_is_help=True, - options_metavar="[选项]", - subcommand_metavar="[命令]", -) - - -def version_callback(value: bool = False) -> None: - if value: - console.print(f"NJUPT Suan API [green]v.{__version__}[/green]") - if __version__ == "dev": - console.print(""" -[bright_black]显示的版本为 dev ?这是因为你正在从源代码运行 cli 入口(manage.py)。[/bright_black] -[bright_black]从[green]已安装版本[/green]中运行 [green]suanapi --version[/green] 可以正确获取版本号。[/bright_black]""") - raise typer.Exit - - -@app.callback(invoke_without_command=True) -def main( - version: bool = typer.Option( - False, - "--version", - "-v", - help="显示版本号并退出,没有其他命令会被执行。", - callback=version_callback, - is_eager=True, # 优先处理,避免触发其他逻辑 - ), -) -> None: - """ - CLI 入口回调,所有子命令执行前都会经过这里。 - 可以在这里放全局初始化(如日志级别、环境检查)。 - """ - pass # 没有 --version 时就正常放行,继续执行子命令 - - -@app.command() -def init() -> None: - """ - 初始化 NJUPT Suan API [green]工作目录[/green]。(可能需要较长时间) - - 会在当前目录或指定目录下创建新文件,并尝试安装 playwright chromium,安装过程可能需要较长时间。 - """ - if DATA_DIR.exists() or TEMP_DIR.exists(): - console.print(Panel(ALREADY_INIT_MESSAGE, title="数据目录已存在")) - console.print("[bright_black]如果你想要强制初始化,使用 [green]suanapi init -f[/green] 命令。[/bright_black]") - return - - -@app.command() -def token(reset: bool = typer.Option(False, "--reload", "-r", help="强制重新生成令牌,即使令牌已存在。")) -> None: - """ - 查看或重新生成[green]管理后端令牌[/green]。 - - 需要先运行过 init 初始化目录。 - - Args: - reset: bool,默认为 False,即只查看,在不存在时重新生成。 - """ - token_ = None - # 首先检查数据目录是否存在 - if not DATA_DIR.exists(): - console.print(Panel(NOT_INIT_MESSAGE, title="数据目录不存在")) - return - - # 确认存在后再判断是否需要重新生成令牌 - if not reset: - try: - with open(file=DATA_DIR / "token.txt", mode="r") as f: - token_ = f.read() - except FileNotFoundError: - pass - if not token_: - console.print("[yellow]重新生成令牌...[/yellow]") - token_ = token_urlsafe(32) - - msg = f""" -🔐 [green] 令牌 - [/green]{token_} -🔐 [green]有效期 - [/green]无限 -✅ WebUI 设计的令牌 cookie 有效期为一天,所以你每天都需要重新登录一次 WebUI,这并非令牌本身的有效期。 -""" - - panel = Panel(msg, title="WebUI 令牌") - - console.print(panel) - - -@app.command() -def run() -> None: - """ - 运行 NJUPT Suan API。 - """ - pass - - -if __name__ == "__main__": - app() diff --git a/pyproject.toml b/pyproject.toml index 4d0af57..368e86e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,17 @@ dependencies = [ ] [project.scripts] -suanapi = "manage:app" +suanapi = "njupt_suan_api.manage:app" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["src/njupt_suan_api"] + +[tool.hatch.build.targets.wheel.force-include] +"src/njupt_suan_api/static" = "njupt_suan_api/static" [tool.ruff] preview = true diff --git a/router/__init__.py b/router/__init__.py deleted file mode 100644 index 7eb56b4..0000000 --- a/router/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .__version__ import __version__ as __version__ diff --git a/njupt_api/__init__.py b/src/njupt_suan_api/__init__.py similarity index 100% rename from njupt_api/__init__.py rename to src/njupt_suan_api/__init__.py diff --git a/router/enhance/__init__.py b/src/njupt_suan_api/api/__init__.py similarity index 100% rename from router/enhance/__init__.py rename to src/njupt_suan_api/api/__init__.py diff --git a/njupt_api/art.txt b/src/njupt_suan_api/api/art.txt similarity index 100% rename from njupt_api/art.txt rename to src/njupt_suan_api/api/art.txt diff --git a/njupt_api/baselib/__init__.py b/src/njupt_suan_api/api/baselib/__init__.py similarity index 100% rename from njupt_api/baselib/__init__.py rename to src/njupt_suan_api/api/baselib/__init__.py diff --git a/njupt_api/baselib/config.py b/src/njupt_suan_api/api/baselib/config.py similarity index 94% rename from njupt_api/baselib/config.py rename to src/njupt_suan_api/api/baselib/config.py index dbed5e8..01003e1 100644 --- a/njupt_api/baselib/config.py +++ b/src/njupt_suan_api/api/baselib/config.py @@ -18,7 +18,7 @@ class Config: async def load_json(self) -> None: """ - 从 Toml 配置文件中读取配置。 + 异步从配置文件中读取配置。 """ logger.debug("异步读取配置文件。") async with aiofiles.open(file=CONFIG_PATH, mode="r") as f: @@ -49,7 +49,7 @@ class Config: def init_config(self) -> None: """ - 重新初始化 Toml 配置文件。这会重置所有配置。 + 重新初始化配置文件。这会重置所有配置。 """ logger.warning("初始化配置文件,这会重置所有配置。") self._doc.clear() @@ -59,7 +59,7 @@ class Config: doc_system["host"] = "0.0.0.0" doc_system["port"] = 8000 - doc_system["reload"] = True + doc_system["reload"] = False doc_system["public_host"] = "http://127.0.0.1:8000" doc_schedule["playwright_headless"] = True @@ -78,7 +78,7 @@ class Config: async def save_json(self) -> None: """ - 异步保存 Toml 配置文件。 + 异步保存配置文件。 """ logger.debug("异步保存配置文件。") async with aiofiles.open(file=CONFIG_PATH, mode="w") as f: diff --git a/njupt_api/baselib/logger.py b/src/njupt_suan_api/api/baselib/logger.py similarity index 100% rename from njupt_api/baselib/logger.py rename to src/njupt_suan_api/api/baselib/logger.py diff --git a/njupt_api/baselib/mcploggingmiddleware.py b/src/njupt_suan_api/api/baselib/mcploggingmiddleware.py similarity index 100% rename from njupt_api/baselib/mcploggingmiddleware.py rename to src/njupt_suan_api/api/baselib/mcploggingmiddleware.py diff --git a/njupt_api/baselib/playcontextmanager.py b/src/njupt_suan_api/api/baselib/playcontextmanager.py similarity index 98% rename from njupt_api/baselib/playcontextmanager.py rename to src/njupt_suan_api/api/baselib/playcontextmanager.py index 76b5182..bf4d5b4 100644 --- a/njupt_api/baselib/playcontextmanager.py +++ b/src/njupt_suan_api/api/baselib/playcontextmanager.py @@ -7,7 +7,7 @@ from playwright.async_api import ( async_playwright, ) -from . import config +from .config import config class PlayContextManager: diff --git a/njupt_api/zhengfang/__init__.py b/src/njupt_suan_api/api/zhengfang/__init__.py similarity index 100% rename from njupt_api/zhengfang/__init__.py rename to src/njupt_suan_api/api/zhengfang/__init__.py diff --git a/njupt_api/zhengfang/createcourse.py b/src/njupt_suan_api/api/zhengfang/createcourse.py similarity index 100% rename from njupt_api/zhengfang/createcourse.py rename to src/njupt_suan_api/api/zhengfang/createcourse.py diff --git a/njupt_api/zhengfang/exc.py b/src/njupt_suan_api/api/zhengfang/exc.py similarity index 100% rename from njupt_api/zhengfang/exc.py rename to src/njupt_suan_api/api/zhengfang/exc.py diff --git a/njupt_api/zhengfang/lib.py b/src/njupt_suan_api/api/zhengfang/lib.py similarity index 100% rename from njupt_api/zhengfang/lib.py rename to src/njupt_suan_api/api/zhengfang/lib.py diff --git a/njupt_api/zhengfang/sso.py b/src/njupt_suan_api/api/zhengfang/sso.py similarity index 96% rename from njupt_api/zhengfang/sso.py rename to src/njupt_suan_api/api/zhengfang/sso.py index 06eea27..f8a9459 100644 --- a/njupt_api/zhengfang/sso.py +++ b/src/njupt_suan_api/api/zhengfang/sso.py @@ -1,5 +1,4 @@ -from njupt_api.baselib import PlayContextManager, logger - +from ..baselib import PlayContextManager, logger from .exc import LoginError diff --git a/njupt_api/zhengfang/types.py b/src/njupt_suan_api/api/zhengfang/types.py similarity index 100% rename from njupt_api/zhengfang/types.py rename to src/njupt_suan_api/api/zhengfang/types.py diff --git a/njupt_api/zhengfang/zhengfang.py b/src/njupt_suan_api/api/zhengfang/zhengfang.py similarity index 98% rename from njupt_api/zhengfang/zhengfang.py rename to src/njupt_suan_api/api/zhengfang/zhengfang.py index 25785a2..cf8a4e4 100644 --- a/njupt_api/zhengfang/zhengfang.py +++ b/src/njupt_suan_api/api/zhengfang/zhengfang.py @@ -1,8 +1,7 @@ from ddddocr import DdddOcr from playwright.async_api import Browser, BrowserContext, Page, Playwright -from njupt_api.baselib import PlayContextManager, logger - +from ..baselib import PlayContextManager, logger from .createcourse import create_course_schedule from .exc import LoginError from .sso import SSO diff --git a/src/njupt_suan_api/cli/__init__.py b/src/njupt_suan_api/cli/__init__.py new file mode 100644 index 0000000..2250f09 --- /dev/null +++ b/src/njupt_suan_api/cli/__init__.py @@ -0,0 +1,13 @@ +from .message import ALREADY_INIT_MESSAGE, INIT_STAGE_MESSAGE, NOT_INIT_MESSAGE, RUN_CHECK_MESSAGE, TOKEN_CHECK_MESSAGE +from .path import DATA_DIR, TEMP_DIR, WORKSPACE_DIR + +__all__ = [ + WORKSPACE_DIR, + DATA_DIR, + TEMP_DIR, + NOT_INIT_MESSAGE, + ALREADY_INIT_MESSAGE, + RUN_CHECK_MESSAGE, + TOKEN_CHECK_MESSAGE, + INIT_STAGE_MESSAGE, +] diff --git a/src/njupt_suan_api/cli/message.py b/src/njupt_suan_api/cli/message.py new file mode 100644 index 0000000..1e1a924 --- /dev/null +++ b/src/njupt_suan_api/cli/message.py @@ -0,0 +1,25 @@ +NOT_INIT_MESSAGE = """ +❓ 当前目录或指定目录下[yellow]似乎还没有执行过初始化命令[/yellow]。 +❓ 你也许需要先执行 [green]suanapi init[/green] 。 +""" + +ALREADY_INIT_MESSAGE = """ +❕ 当前目录或指定目录下[yellow]似乎已经执行初始化命令过[/yellow]。 +❕ 你也许需要先删除已经存在的 [blue]data[/blue] 和 [blue]temp[/blue] 目录。 +""" + +RUN_CHECK_MESSAGE = """ +[bright_black]运行时的配置可能来自命令行参数、配置文件以及默认值,列在这里供你检查。[/bright_black] + [cyan]主机名[/cyan] - [cyan]host[/cyan] - {host} + [cyan]端口[/cyan] - [cyan]port[/cyan] - {port} + [cyan]自动重启[/cyan] - [cyan]reload[/cyan] - {reload} +[bright_black]NJUPT Suan API 会很快启动。使用 [green]Ctrl + C[/green] 以退出。[/bright_black] +""" + +TOKEN_CHECK_MESSAGE = """ +🔐 [green] 令牌 - [/green]{token} +🔐 [green]有效期 - [/green]无限 +✅ WebUI 设计的令牌 cookie 有效期为一天,所以你每天都需要重新登录一次 WebUI,这并非令牌本身的有效期。 +""" + +INIT_STAGE_MESSAGE = "[cyan]- {stage} / 3 -[/cyan] [bright_black]{message}[/bright_black]" diff --git a/src/njupt_suan_api/cli/path.py b/src/njupt_suan_api/cli/path.py new file mode 100644 index 0000000..b1f0976 --- /dev/null +++ b/src/njupt_suan_api/cli/path.py @@ -0,0 +1,5 @@ +from pathlib import Path + +WORKSPACE_DIR = Path.cwd() +DATA_DIR = WORKSPACE_DIR / "data" +TEMP_DIR = WORKSPACE_DIR / "temp" diff --git a/main.py b/src/njupt_suan_api/main.py similarity index 88% rename from main.py rename to src/njupt_suan_api/main.py index e95ccfa..48db232 100644 --- a/main.py +++ b/src/njupt_suan_api/main.py @@ -1,8 +1,12 @@ +""" +main.py 未来不再作为项目入口,日后可能会被删除。请参照 README 使用命令行作为入口,或直接使用 uvicorn 命令。 +""" + from pathlib import Path from secrets import token_urlsafe -from njupt_api.baselib import config, logger -from router import __version__ +from njupt_suan_api.api.baselib import config, logger +from njupt_suan_api.router import __version__ DATA_DIR = Path.cwd() / "data" TEMP_DIR = Path.cwd() / "temp" @@ -10,7 +14,7 @@ TEMP_DIR = Path.cwd() / "temp" if __name__ == "__main__": try: - with open(file=Path.cwd() / "njupt_api" / "art.txt", mode="r", encoding="utf-8") as f: + with open(file=Path.cwd() / "api" / "art.txt", mode="r", encoding="utf-8") as f: print(f.read().format(__version__)) # noqa:T201 except FileNotFoundError: pass @@ -77,7 +81,7 @@ if __name__ == "__main__": host=host, port=port, reload=reload, - reload_dirs=["njupt_api", "router"], + reload_dirs=["api", "router"], access_log=False, log_level="critical", timeout_graceful_shutdown=2, diff --git a/src/njupt_suan_api/manage.py b/src/njupt_suan_api/manage.py new file mode 100644 index 0000000..7933e53 --- /dev/null +++ b/src/njupt_suan_api/manage.py @@ -0,0 +1,182 @@ +import subprocess +from secrets import token_urlsafe + +import typer +from rich.console import Console +from rich.panel import Panel + +from njupt_suan_api.api.baselib import config +from njupt_suan_api.cli import ( + ALREADY_INIT_MESSAGE, + DATA_DIR, + INIT_STAGE_MESSAGE, + NOT_INIT_MESSAGE, + RUN_CHECK_MESSAGE, + TEMP_DIR, + TOKEN_CHECK_MESSAGE, + WORKSPACE_DIR, +) +from njupt_suan_api.router import __version__ + +console = Console() +app = typer.Typer( + name="NJUPT-Suan-API", + help="NJUPT Suan API 部署与管理工具", + rich_markup_mode="rich", + no_args_is_help=True, +) + + +def version_callback(value: bool = False) -> None: + if value: + console.print(f"NJUPT Suan API [green]v.{__version__}[/green]") + if __version__ == "dev": + console.print(""" +[bright_black]显示的版本为 dev ?这是因为你正在从源代码运行 cli 入口(manage.py)。[/bright_black] +[bright_black]从[green]已安装版本[/green]中运行 [green]suanapi --version[/green] 可以正确获取版本号。[/bright_black]""") + raise typer.Exit + + +@app.callback(invoke_without_command=True) +def main( + version: bool = typer.Option( + False, + "--version", + "-v", + help="显示版本号并退出,没有其他命令会被执行。", + callback=version_callback, + is_eager=True, # 优先处理,避免触发其他逻辑 + ), +) -> None: + """ + CLI 入口回调,所有子命令执行前都会经过这里。 + 可以在这里放全局初始化(如日志级别、环境检查)。 + """ + pass # 没有 --version 时就正常放行,继续执行子命令 + + +@app.command() +def init(force: bool = typer.Option(False, "--force", "-f", help="强制初始化,可能导致问题,不建议使用。")) -> None: + """ + 初始化 NJUPT Suan API [green]工作目录[/green]。(可能需要较长时间) + + 会在当前目录或指定目录下创建新文件,并尝试安装 playwright chromium。 + 视网络情况,安装过程可能(几乎必然)需要较长时间。 + 如果已存在 data 和/或 temp 目录,初始化会失败。你也可以强制初始化,但可能导致先前的数据丢失。 + + Raises: + typer.Exit: 如果初始化失败,返回 1。 + """ + if (DATA_DIR.exists() or TEMP_DIR.exists()) and not force: + console.print(Panel(ALREADY_INIT_MESSAGE, title="工作目录已存在")) + console.print("[bright_black]如果你想要强制初始化,使用 [green]suanapi init -f[/green] 命令。[/bright_black]") + raise typer.Exit(code=1) + + # 1 创建 data 和 temp 目录 + DATA_DIR.mkdir(parents=True, exist_ok=True) + TEMP_DIR.mkdir(parents=True, exist_ok=True) + console.print(INIT_STAGE_MESSAGE.format(stage=1, message="工作目录 data 和 temp 已创建。")) + + # 2 初始化配置文件 + try: + config.sync_load_json() + except FileNotFoundError: + config.init_config() + config.sync_create_json() + console.print(INIT_STAGE_MESSAGE.format(stage=2, message="已初始化配置并创建配置文件。")) + else: + console.print(INIT_STAGE_MESSAGE.format(stage=2, message="配置文件已存在,跳过配置初始化。")) + + # 3 执行 uv run playwright install chromium + console.print("[bright_black]即将安装 playwright 的 chromium,这可能是耗时最长的部分。[/bright_black]") + cp3 = subprocess.run(["playwright", "install", "chromium"], cwd=WORKSPACE_DIR) + if cp3.returncode != 0: + console.print("[yellow]运行 playwright install chromuim 失败,双是什么原因呢?[/yellow]") + raise typer.Exit(code=cp3.returncode) + console.print(INIT_STAGE_MESSAGE.format(stage=3, message="已安装 playwright chromium。")) + + console.print("[green]初始化完成。接下来可以执行 suanapi run 来启动 NJUPT Suan API。[/green]") + console.print( + "[bright_black]初始化只需要执行一次即可,以后即使更新 NJUPT Suan API,也无再需重复执行。[/bright_black]" + ) + + +@app.command() +def token(reset: bool = typer.Option(False, "--reload", "-r", help="强制重新生成令牌,即使令牌已存在。")) -> None: + """ + 查看或重新生成[green]管理后端令牌[/green]。 + + 需要先运行过 init 初始化目录。 + + Raises: + typer.Exit: 如果未初始化,返回 1。 + """ + token_ = None + # 首先检查数据目录是否存在 + if not DATA_DIR.exists(): + console.print(Panel(NOT_INIT_MESSAGE, title="数据目录不存在")) + raise typer.Exit(code=1) + + # 确认存在后再判断是否需要重新生成令牌 + if not reset: + try: + with open(file=DATA_DIR / "token.txt", mode="r") as f: + token_ = f.read() + except FileNotFoundError: + pass + if not token_: + console.print("[yellow]重新生成令牌...[/yellow]") + token_ = token_urlsafe(32) + with open(file=DATA_DIR / "token.txt", mode="w") as f: + f.write(token_) + + panel = Panel(TOKEN_CHECK_MESSAGE.format(token=token_), title="WebUI 令牌") + + console.print(panel) + + +@app.command() +def run( + host: str | None = typer.Option(None, "--host", help="监听主机名,默认 0.0.0.0。"), + port: int | None = typer.Option(None, "--port", help="监听端口,默认 8000。"), + reload: bool | None = typer.Option(None, "--reload", "-r", help="在检测到代码变化时自动重启,默认 False。"), +) -> None: + """ + 运行 NJUPT Suan API。 + + 可接收运行参数。若不提供,则尝试使用配置文件中的值,最后回退到默认值。 + + Raises: + typer.Exit: 如果未初始化,返回 1。 + """ + if host is None: + host = config.get("system", "host", "0.0.0.0") + if port is None: + port = config.get("system", "port", 8000) + if reload is None: + reload = config.get("system", "reload", False) + + if not DATA_DIR.exists() or not TEMP_DIR.exists(): + console.print(Panel(NOT_INIT_MESSAGE, title="工作目录不存在")) + raise typer.Exit(code=1) + + token(False) + + console.print(Panel(RUN_CHECK_MESSAGE.format(host=host, port=port, reload=reload), title="运行前检查")) + + import uvicorn + + uvicorn.run( + "njupt_suan_api.server:app", + host=host, + port=port, + reload=reload, + reload_dirs=["api", "router"], + access_log=False, + log_level="critical", + timeout_graceful_shutdown=2, + ) + + +if __name__ == "__main__": + app() diff --git a/src/njupt_suan_api/router/__init__.py b/src/njupt_suan_api/router/__init__.py new file mode 100644 index 0000000..1728f4b --- /dev/null +++ b/src/njupt_suan_api/router/__init__.py @@ -0,0 +1,14 @@ +from .__version__ import __version__ as __version__ +from .admin_router import admin_router +from .api_router import api_router +from .mcp_router import mcp_app +from .webui_router import ASSETS_DIR, webui_router + +__all__ = [ + admin_router, + api_router, + mcp_app, + webui_router, + __version__, + ASSETS_DIR, +] diff --git a/router/__version__.py b/src/njupt_suan_api/router/__version__.py similarity index 100% rename from router/__version__.py rename to src/njupt_suan_api/router/__version__.py diff --git a/router/admin_router.py b/src/njupt_suan_api/router/admin_router.py similarity index 92% rename from router/admin_router.py rename to src/njupt_suan_api/router/admin_router.py index 9ceefd8..3742681 100644 --- a/router/admin_router.py +++ b/src/njupt_suan_api/router/admin_router.py @@ -6,11 +6,11 @@ from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlmodel import Session, delete, select -from njupt_api.baselib import config, logger -from njupt_api.zhengfang import LoginError, course_list_serializer, jwxt -from router.enhance.auth import verify_token -from router.enhance.lib import AliasDto, ReturnDto, TestDto, get_session -from router.enhance.model import Alias, Course +from njupt_suan_api.api.baselib import config, logger +from njupt_suan_api.api.zhengfang import LoginError, course_list_serializer, jwxt +from njupt_suan_api.router.enhance.auth import verify_token +from njupt_suan_api.router.enhance.lib import AliasDto, ReturnDto, TestDto, get_session +from njupt_suan_api.router.enhance.model import Alias, Course class ValidateTokenDto(BaseModel): diff --git a/router/api_router.py b/src/njupt_suan_api/router/api_router.py similarity index 91% rename from router/api_router.py rename to src/njupt_suan_api/router/api_router.py index 067bc65..65ac744 100644 --- a/router/api_router.py +++ b/src/njupt_suan_api/router/api_router.py @@ -5,15 +5,10 @@ from fastapi import APIRouter, Depends, HTTPException, status from fastapi.responses import FileResponse from sqlmodel import Session, select -from njupt_api.baselib import logger -from njupt_api.zhengfang import ( - course_dict_serializer, - course_list_serializer, - jwxt, -) -from njupt_api.zhengfang.exc import LoginError -from router.enhance.lib import ReturnDto, ScheduleQueryDto, apply_enhance, get_session -from router.enhance.model import Course +from njupt_suan_api.api.baselib import logger +from njupt_suan_api.api.zhengfang import LoginError, course_dict_serializer, course_list_serializer, jwxt +from njupt_suan_api.router.enhance.lib import ReturnDto, ScheduleQueryDto, apply_enhance, get_session +from njupt_suan_api.router.enhance.model import Course TEMP_DIR = Path.cwd() / "temp" diff --git a/src/njupt_suan_api/router/enhance/__init__.py b/src/njupt_suan_api/router/enhance/__init__.py new file mode 100644 index 0000000..639862a --- /dev/null +++ b/src/njupt_suan_api/router/enhance/__init__.py @@ -0,0 +1,8 @@ +from .lib import ReturnDto, apply_enhance +from .model import create_db_and_tables + +__all__ = [ + ReturnDto, + apply_enhance, + create_db_and_tables, +] diff --git a/router/enhance/alias.py b/src/njupt_suan_api/router/enhance/alias.py similarity index 90% rename from router/enhance/alias.py rename to src/njupt_suan_api/router/enhance/alias.py index 1eab5b1..99b3f0c 100644 --- a/router/enhance/alias.py +++ b/src/njupt_suan_api/router/enhance/alias.py @@ -7,8 +7,8 @@ from typing import Sequence from sqlmodel import Session, select -from njupt_api.baselib import logger -from router.enhance.model import Alias, engine +from njupt_suan_api.api.baselib import logger +from njupt_suan_api.router.enhance.model import Alias, engine def apply_alias(courses: list[dict]) -> list[dict]: diff --git a/router/enhance/auth.py b/src/njupt_suan_api/router/enhance/auth.py similarity index 100% rename from router/enhance/auth.py rename to src/njupt_suan_api/router/enhance/auth.py diff --git a/router/enhance/lib.py b/src/njupt_suan_api/router/enhance/lib.py similarity index 98% rename from router/enhance/lib.py rename to src/njupt_suan_api/router/enhance/lib.py index da94d41..183d729 100644 --- a/router/enhance/lib.py +++ b/src/njupt_suan_api/router/enhance/lib.py @@ -4,7 +4,7 @@ from typing import Any, Generator, Literal from pydantic import BaseModel from sqlmodel import Session -from njupt_api.baselib import config +from njupt_suan_api.api.baselib import config from .alias import apply_alias from .model import engine diff --git a/router/enhance/model.py b/src/njupt_suan_api/router/enhance/model.py similarity index 96% rename from router/enhance/model.py rename to src/njupt_suan_api/router/enhance/model.py index 3c66fa6..9c24ce9 100644 --- a/router/enhance/model.py +++ b/src/njupt_suan_api/router/enhance/model.py @@ -3,7 +3,7 @@ from typing import Optional from sqlalchemy import JSON, Column from sqlmodel import Field, SQLModel, create_engine -sqlite_file_name = "data/njupt_api.db" +sqlite_file_name = "data/njupt-api.db" sqlite_url = f"sqlite:///{sqlite_file_name}" engine = create_engine(sqlite_url, connect_args={"check_same_thread": False}) diff --git a/router/enhance/screenshot.py b/src/njupt_suan_api/router/enhance/screenshot.py similarity index 96% rename from router/enhance/screenshot.py rename to src/njupt_suan_api/router/enhance/screenshot.py index 1e2ecb5..e0ab84e 100644 --- a/router/enhance/screenshot.py +++ b/src/njupt_suan_api/router/enhance/screenshot.py @@ -7,7 +7,7 @@ from uuid import uuid4 from playwright.async_api import ViewportSize -from njupt_api.baselib import PlayContextManager, logger +from njupt_suan_api.api.baselib import PlayContextManager, logger TEMP_DIR = Path.cwd() / "temp" diff --git a/router/enhance/week.py b/src/njupt_suan_api/router/enhance/week.py similarity index 100% rename from router/enhance/week.py rename to src/njupt_suan_api/router/enhance/week.py diff --git a/router/mcp_router.py b/src/njupt_suan_api/router/mcp_router.py similarity index 93% rename from router/mcp_router.py rename to src/njupt_suan_api/router/mcp_router.py index 5caf45e..87e73fc 100644 --- a/router/mcp_router.py +++ b/src/njupt_suan_api/router/mcp_router.py @@ -7,15 +7,10 @@ from mcp.types import ToolAnnotations from pydantic import Field from sqlmodel import Session, select -from njupt_api.baselib import LoggingMiddleware, logger -from njupt_api.zhengfang import ( - course_dict_serializer, - course_list_serializer, - jwxt, -) -from njupt_api.zhengfang.exc import LoginError -from router.enhance.lib import ReturnDto, apply_enhance -from router.enhance.model import Course, engine +from njupt_suan_api.api.baselib import LoggingMiddleware, logger +from njupt_suan_api.api.zhengfang import LoginError, course_dict_serializer, course_list_serializer, jwxt +from njupt_suan_api.router.enhance.lib import ReturnDto, apply_enhance +from njupt_suan_api.router.enhance.model import Course, engine mcp = FastMCP("NJUPT API Suan") diff --git a/router/webui_router.py b/src/njupt_suan_api/router/webui_router.py similarity index 78% rename from router/webui_router.py rename to src/njupt_suan_api/router/webui_router.py index a88a3c1..e1479d7 100644 --- a/router/webui_router.py +++ b/src/njupt_suan_api/router/webui_router.py @@ -4,8 +4,10 @@ import aiofiles from fastapi import APIRouter from fastapi.responses import HTMLResponse -WEBUI_INDEX = Path.cwd() / "webui" / "dist" / "index.html" -SCHEDULE_INDEX = Path.cwd() / "webui" / "dist" / "index-schedule.html" +STATIC_DIR = Path(__file__).parent.parent / "static" +WEBUI_INDEX = STATIC_DIR / "index.html" +SCHEDULE_INDEX = STATIC_DIR / "index-schedule.html" +ASSETS_DIR = STATIC_DIR / "assets" webui_router = APIRouter(prefix="/webui") diff --git a/server.py b/src/njupt_suan_api/server.py similarity index 93% rename from server.py rename to src/njupt_suan_api/server.py index b1ec4a7..ec65a58 100644 --- a/server.py +++ b/src/njupt_suan_api/server.py @@ -12,20 +12,15 @@ from fastapi.staticfiles import StaticFiles from fastmcp.utilities.lifespan import combine_lifespans from watchfiles import awatch -from njupt_api.baselib import ( +from njupt_suan_api.api.baselib import ( LogRecord, config, log_buffer, log_record_serialize, logger, ) -from router import __version__ -from router.admin_router import admin_router -from router.api_router import api_router -from router.enhance.lib import ReturnDto -from router.enhance.model import create_db_and_tables -from router.mcp_router import mcp_app -from router.webui_router import webui_router +from njupt_suan_api.router import ASSETS_DIR, __version__, admin_router, api_router, mcp_app, webui_router +from njupt_suan_api.router.enhance import ReturnDto, create_db_and_tables DATA_DIR = Path.cwd() / "data" @@ -166,7 +161,7 @@ app.include_router(webui_router) app.mount("/mcp", mcp_app) app.mount( "/assets", - StaticFiles(directory=Path.cwd() / "webui" / "dist" / "assets"), + StaticFiles(directory=ASSETS_DIR), name="assets", ) diff --git a/uv.lock b/uv.lock index 7bb8a1e..ed10285 100644 --- a/uv.lock +++ b/uv.lock @@ -702,7 +702,7 @@ wheels = [ [[package]] name = "njupt-suan-api" version = "0.1.1" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "aiofiles" }, { name = "beautifulsoup4" }, diff --git a/webui/vite.config.ts b/webui/vite.config.ts index 8e5d6cd..f2dc39f 100644 --- a/webui/vite.config.ts +++ b/webui/vite.config.ts @@ -35,8 +35,9 @@ export default defineConfig({ input: { index: path.resolve(__dirname, 'index.html'), 'index-schedule': path.resolve(__dirname, 'index-schedule.html'), - } + }, }, + outDir: "../src/njupt_suan_api/static" }, resolve: { alias: {