Python 后端提交
Python 后端(FastAPI + FastMCP + ...)的初始版本号设定为 0.1.0,这是 uv 在 pypriject.toml 里给我自动设置的,我觉得有道理。
This commit is contained in:
0
router/enhance/__init__.py
Normal file
0
router/enhance/__init__.py
Normal file
41
router/enhance/alias.py
Normal file
41
router/enhance/alias.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""为课程提供一个简短的别名,便于在空间有限的课程表图片中辨认。
|
||||
|
||||
别名是单独保存的,在最终输出阶段才会被装饰在原有的课表输出上。
|
||||
"""
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from njupt_api.baselib import logger
|
||||
from router.enhance.model import Alias, engine
|
||||
|
||||
|
||||
def apply_alias(courses: list[dict]) -> list[dict]:
|
||||
with Session(engine) as session:
|
||||
aliases: Sequence[Alias] = session.exec(select(Alias)).all()
|
||||
|
||||
# 否则不做任何更改
|
||||
if len(aliases) == 0:
|
||||
return courses
|
||||
|
||||
alias_count = 0
|
||||
apply_count = 0
|
||||
|
||||
alias_dict = {}
|
||||
for alias in aliases:
|
||||
m = alias.model_dump()
|
||||
alias_dict[m["originalName"]] = m["aliasName"]
|
||||
alias_count += 1
|
||||
|
||||
for course in courses:
|
||||
if course["name"] in alias_dict:
|
||||
course["alias"] = alias_dict[course["name"]]
|
||||
apply_count += 1
|
||||
else:
|
||||
course["alias"] = None
|
||||
|
||||
logger.debug(
|
||||
f"课程别名 | 将 {alias_count} 个别名应用在了 {apply_count} 门输出的课程上。",
|
||||
)
|
||||
return courses
|
||||
21
router/enhance/auth.py
Normal file
21
router/enhance/auth.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
|
||||
import aiofiles
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
|
||||
|
||||
security = HTTPBearer()
|
||||
|
||||
|
||||
async def verify_token(credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)]) -> str:
|
||||
token = credentials.credentials
|
||||
|
||||
async with aiofiles.open(file=Path.cwd() / "data" / "token.txt", mode="r") as f:
|
||||
if (await f.readline()).strip() == token:
|
||||
return token
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid or missing token. (Suan API WebUI)",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
77
router/enhance/lib.py
Normal file
77
router/enhance/lib.py
Normal file
@@ -0,0 +1,77 @@
|
||||
from datetime import date, timedelta
|
||||
from typing import Any, Generator, Literal
|
||||
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session
|
||||
|
||||
from njupt_api.baselib import config
|
||||
|
||||
from .alias import apply_alias
|
||||
from .model import engine
|
||||
from .screenshot import generate_img
|
||||
|
||||
|
||||
class ScheduleQueryDto(BaseModel):
|
||||
username: str | None = None
|
||||
password: str | None = None
|
||||
week: int = 0
|
||||
img: bool = False
|
||||
|
||||
|
||||
class TestDto(BaseModel):
|
||||
username: str
|
||||
password: str
|
||||
scheduleType: Literal["class", "student"] # noqa: N815
|
||||
|
||||
|
||||
class AliasDto(BaseModel):
|
||||
originalName: str # noqa: N815
|
||||
aliasName: str | None # noqa: N815
|
||||
|
||||
|
||||
class ReturnDto(BaseModel):
|
||||
success: bool
|
||||
message: str | None = None
|
||||
result: Any | None = None
|
||||
img_url: str | None = None
|
||||
|
||||
|
||||
def get_session() -> Generator[Session, None, None]:
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
async def apply_enhance(course_list: list[dict], week: int, img: bool) -> ReturnDto:
|
||||
"""
|
||||
在一个方法中集成了 应用别名 和 生成课表图片 功能。此为异步方法,需要 await。
|
||||
|
||||
Example:
|
||||
return await apply_enhance(course_list, week, img)
|
||||
|
||||
Returns:
|
||||
返回应用别名和图片完毕的 ReturnDto。
|
||||
"""
|
||||
final_course_list = [course for course in course_list if week in course["weeks"]] if week > 0 else course_list
|
||||
|
||||
final_course_list = apply_alias(final_course_list)
|
||||
|
||||
# 获取课表图片设置
|
||||
title_template = config.get("schedule", "schedule_title_template", "芒果酸的课程表")
|
||||
subtitle_template = config.get("schedule", "schedule_subtitle_template", "")
|
||||
semester_start_date = date.fromisoformat(config.get("schedule", "semester_start_date", "2026-03-02"))
|
||||
|
||||
# 可用变量
|
||||
week_start_day = semester_start_date + timedelta(weeks=week)
|
||||
vars_ = {
|
||||
"week": week,
|
||||
"week_start_day": week_start_day.isoformat(),
|
||||
"week_end_day": (week_start_day + timedelta(days=6)).isoformat(),
|
||||
}
|
||||
|
||||
img_url = None
|
||||
if img:
|
||||
img_url = f"http://172.28.143.24:8000/api/schedule/img/{
|
||||
await generate_img(final_course_list, title_template.format(**vars_), subtitle_template.format(**vars_))
|
||||
}"
|
||||
|
||||
return ReturnDto(success=True, result=final_course_list, img_url=img_url)
|
||||
29
router/enhance/model.py
Normal file
29
router/enhance/model.py
Normal file
@@ -0,0 +1,29 @@
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import JSON, Column
|
||||
from sqlmodel import Field, SQLModel, create_engine
|
||||
|
||||
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})
|
||||
|
||||
|
||||
class Course(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
name: str
|
||||
teacher: Optional[str] = Field(default=None, nullable=True)
|
||||
classroom: Optional[str] = Field(default=None, nullable=True)
|
||||
weeks: list[int] = Field(default=[], sa_column=Column(JSON))
|
||||
day: int
|
||||
classes: list[int] = Field(default=[], sa_column=Column(JSON))
|
||||
|
||||
|
||||
class Alias(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
originalName: Optional[str] = Field(default=None, nullable=True) # noqa: N815
|
||||
aliasName: Optional[str] = Field(default=None, nullable=True) # noqa: N815
|
||||
|
||||
|
||||
def create_db_and_tables() -> None:
|
||||
SQLModel.metadata.create_all(engine)
|
||||
49
router/enhance/screenshot.py
Normal file
49
router/enhance/screenshot.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""使用 Playwright 截图"""
|
||||
|
||||
from json import dumps
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode
|
||||
from uuid import uuid4
|
||||
|
||||
from playwright.async_api import ViewportSize
|
||||
|
||||
from njupt_api.baselib import PlayContextManager, logger
|
||||
|
||||
TEMP_DIR = Path.cwd() / "temp"
|
||||
|
||||
|
||||
class ScreenShot(PlayContextManager):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
async def goto(self, url: str) -> bool:
|
||||
await self.page.set_viewport_size(ViewportSize(width=1200, height=900))
|
||||
res = await self.page.goto(url)
|
||||
logger.debug(f"截图 | {res.ok=} - {url=:.{50}}...")
|
||||
return res.ok
|
||||
|
||||
async def shot(self, save_path: str) -> None:
|
||||
await self.page.mouse.move(0, 0)
|
||||
await self.page.wait_for_load_state("networkidle")
|
||||
await self.page.screenshot(path=save_path)
|
||||
logger.debug(f"截图 | 截图已经保存在 {save_path=}")
|
||||
return
|
||||
|
||||
|
||||
async def generate_img(courses: list[dict], title: str, subtitle: str) -> str:
|
||||
"""
|
||||
方法将生成课程表图片并保存在临时目录中,返回图片的完整名称。图片位于 temp 工作目录。
|
||||
|
||||
Returns:
|
||||
字符串,表明生成图片的文件名,格式为 `schedule-{uuid4()}.png`
|
||||
"""
|
||||
t_name = f"schedule-{uuid4()}.png"
|
||||
async with ScreenShot() as ss:
|
||||
await ss.goto(
|
||||
f"127.0.0.1:8000/webui/schedule#/?{
|
||||
urlencode({'data': dumps(courses), 'title': title, 'subtitle': subtitle})
|
||||
}",
|
||||
)
|
||||
await ss.shot(str(TEMP_DIR / t_name))
|
||||
logger.debug(f"截图 | 生成临时图片 - {t_name}")
|
||||
return t_name
|
||||
29
router/enhance/week.py
Normal file
29
router/enhance/week.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""学期-星期计算"""
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
|
||||
def get_semester_week_info(start: date, target: date) -> tuple[int, int]:
|
||||
"""
|
||||
给定学期开始日期(第一周周一)和另一指定日期,计算指定的日期是第几周的星期几。
|
||||
Args:
|
||||
start: 学期开始的日期,date
|
||||
target: 指定日期,date
|
||||
|
||||
Returns:
|
||||
包含两个数字的元组,分别为第几周和星期几。
|
||||
"""
|
||||
return (target - start).days // 7 + 1, target.isoweekday()
|
||||
|
||||
|
||||
def get_week_day_info(target: date) -> tuple[date, date]:
|
||||
"""
|
||||
给定一个指定日期,获取该日所在的星期的周一和周日的日期。
|
||||
Args:
|
||||
target: 指定日期,date
|
||||
|
||||
Returns:
|
||||
包含两个 date 的元组,分别为周一和周日。
|
||||
"""
|
||||
weekday_int = target.weekday()
|
||||
return target - timedelta(days=weekday_int), target + timedelta(days=6 - weekday_int)
|
||||
Reference in New Issue
Block a user