尝试 0.1.1 版本 + 命令行 cli 构建中

尝试用 Typer 构建命令行入口 manage.py 。
在 pyproject.toml 中完成有关设计,计划使用 suanapi 命令调用 manage.py 。
This commit is contained in:
2026-04-24 16:23:51 +08:00
parent 16ff315a30
commit 8db1d27758
7 changed files with 137 additions and 3 deletions

9
cli/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
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,
]

9
cli/message.py Normal file
View File

@@ -0,0 +1,9 @@
NOT_INIT_MESSAGE = """
❓ 当前目录或指定目录下[yellow]似乎还没有执行过初始化命令[/yellow]。
❓ 你也许需要先执行 [green]suanapi init[/green] 。
"""
ALREADY_INIT_MESSAGE = """
❕ 当前目录或指定目录下[yellow]似乎已经执行初始化命令过[/yellow]。
❕ 你也许需要先删除已经存在的 [blue]data[/blue] 和 [blue]temp[/blue] 目录。
"""

4
cli/path.py Normal file
View File

@@ -0,0 +1,4 @@
from pathlib import Path
DATA_DIR = Path.cwd() / "data"
TEMP_DIR = Path.cwd() / "temp"

109
manage.py Normal file
View File

@@ -0,0 +1,109 @@
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()

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "njupt-suan-api" name = "njupt-suan-api"
version = "0.1.0" version = "0.1.1"
description = "API and MCP server for NJUPT infomation ~" description = "API and MCP server for NJUPT infomation ~"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
@@ -23,6 +23,9 @@ dependencies = [
"websockets>=16.0", "websockets>=16.0",
] ]
[project.scripts]
suanapi = "manage:app"
[tool.ruff] [tool.ruff]
preview = true preview = true
line-length = 120 line-length = 120

View File

@@ -1 +1 @@
from .__version__ import __version__ from .__version__ import __version__ as __version__

2
uv.lock generated
View File

@@ -701,7 +701,7 @@ wheels = [
[[package]] [[package]]
name = "njupt-suan-api" name = "njupt-suan-api"
version = "0.1.0" version = "0.1.1"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aiofiles" }, { name = "aiofiles" },