zzupy.supwisdom 源代码

import base64
import datetime
import json
import random
import time

import httpx
from loguru import logger

from zzupy.models import Courses, RoomOccupancyData, SemesterData
from zzupy.utils import get_sign, sync_wrapper


[文档] class Supwisdom: """ 树维教务相关功能的类 """ def __init__(self, parent): """ 初始化Supwisdom实例 :param parent: 父对象,通常是ZZUPy实例 """ self._parent = parent # 默认请求头 self._default_headers = { "User-Agent": "", # 将在请求时动态设置 "Accept": "application/json, text/plain, */*", "Accept-Encoding": "gzip, deflate, br, zstd", "Content-Type": "application/x-www-form-urlencoded", "sec-ch-ua": '"Not/A)Brand";v="8", "Chromium";v="126", "Android WebView";v="126"', "sec-ch-ua-mobile": "?1", "sec-ch-ua-platform": '"Android"', "Origin": "https://jw.v.zzu.edu.cn", "X-Requested-With": "com.supwisdom.zzu", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Dest": "empty", "Referer": "https://jw.v.zzu.edu.cn/app-web/", "Accept-Language": "zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7", } self.biz_type_id = None self.current_semester_id = None
[文档] def get_courses( self, start_date: str, semester_id: str | int = None, biz_type_id: str | int = None, ) -> Courses: """ 获取课程表 :param str start_date: 课表的开始日期,格式必须为 YYYY-MM-DD ,且必须为某一周周一,否则课表会时间错乱 :param str semester_id: 学期ID :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回课程表数据 :rtype: Courses :raises ValueError: 如果日期格式不正确 :raises Exception: 如果API请求失败 """ return sync_wrapper(self.get_courses_async)( start_date, semester_id, biz_type_id )
[文档] async def get_courses_async( self, start_date: str, semester_id: str | int = None, biz_type_id: str | int = None, ) -> Courses: """ 异步获取课程表 :param str start_date: 课表的开始日期,格式必须为 YYYY-MM-DD ,且必须为某一周周一,否则课表会时间错乱 :param str semester_id: 学期ID :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回课程表数据 :rtype: Courses :raises ValueError: 如果日期格式不正确 :raises Exception: 如果API请求失败 """ if semester_id is None: semester_id = self.current_semester_id if biz_type_id is None: biz_type_id = self.biz_type_id # 验证日期格式 try: start_datetime = datetime.datetime.strptime(start_date, "%Y-%m-%d") # 检查是否为周一 if start_datetime.weekday() != 0: logger.error("提供的日期不是周一,课表可能会时间错乱") except ValueError: raise ValueError("日期格式必须为 YYYY-MM-DD") # 计算结束日期(一周后) end_date = (start_datetime + datetime.timedelta(days=6)).strftime("%Y-%m-%d") # 准备请求数据 data = { "biz_type_id": str(biz_type_id), "end_date": end_date, "random": int(random.uniform(10000, 99999)), "semester_id": str(semester_id), "start_date": start_date, "timestamp": int(round(time.time() * 1000)), "token": self._parent._dynamicToken, } # 生成签名 params = "&".join([f"{key}={value}" for key, value in data.items()]) sign = get_sign(self._parent._dynamicSecret, params) data["sign"] = sign # 设置请求头 headers = self._default_headers.copy() headers["User-Agent"] = ( self._parent._DeviceParams.userAgentPrecursor + "SuperApp" ) headers["token"] = self._parent._dynamicToken response = None try: # 发送请求 response = await self._parent._client.post( "https://jw.v.zzu.edu.cn/app-ws/ws/app-service/student/course/schedule/get-course-tables", headers=headers, data=data, ) # 检查响应状态 response.raise_for_status() # 解析响应数据 response_json = response.json() if "business_data" not in response_json: raise Exception(f"API返回格式错误: {response.text}") # 解码并解析课程数据 courses_json = base64.b64decode(response_json["business_data"]).decode( "utf-8" ) courses_list = json.loads(courses_json) # 按日期和开始时间排序 sorted_courses = sorted( courses_list, key=lambda x: ( x["date"], datetime.datetime.strptime(x.get("start_time", "00:00"), "%H:%M"), ), ) # 转换为Courses对象 return Courses.from_list(sorted_courses) except httpx.HTTPStatusError as e: raise Exception(f"API请求失败: HTTP {e.response.status_code}") except httpx.RequestError as e: raise Exception(f"网络请求失败: {str(e)}") except json.JSONDecodeError as e: # 检查response是否已定义 error_text = response.text if response is not None else "无响应内容" raise Exception(f"解析响应JSON失败: {error_text}") except Exception as e: raise Exception(f"获取课程表失败: {str(e)}")
[文档] def get_current_week_courses( self, semester_id: str | int = None, biz_type_id: str | int = None ) -> Courses: """ 获取本周课程表 :param str semester_id: 学期ID :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回本周课程表数据 :rtype: Courses """ if semester_id is None: semester_id = self.current_semester_id if biz_type_id is None: biz_type_id = self.biz_type_id # 获取当前日期 today = datetime.datetime.now() # 计算本周一的日期 monday = today - datetime.timedelta(days=today.weekday()) # 格式化为YYYY-MM-DD monday_str = monday.strftime("%Y-%m-%d") # 获取课程表 return self.get_courses(monday_str, semester_id, biz_type_id)
[文档] async def get_current_week_courses_async( self, semester_id: str | int = None, biz_type_id: str | int = None ) -> Courses: """ 异步获取本周课程表 :param str semester_id: 学期ID。 :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回本周课程表数据 :rtype: Courses """ if semester_id is None: semester_id = self.current_semester_id if biz_type_id is None: biz_type_id = self.biz_type_id # 获取当前日期 today = datetime.datetime.now() # 计算本周一的日期 monday = today - datetime.timedelta(days=today.weekday()) # 格式化为YYYY-MM-DD monday_str = monday.strftime("%Y-%m-%d") # 获取课程表 return await self.get_courses_async(monday_str, semester_id, biz_type_id)
[文档] def get_today_courses( self, semester_id: str | int = None, biz_type_id: str | int = None ) -> Courses: """ 获取今日课程表 :param str semester_id: 学期ID :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回今日课程表数据 :rtype: Courses """ if semester_id is None: semester_id = self.current_semester_id if biz_type_id is None: biz_type_id = self.biz_type_id # 获取本周课程表 week_courses = self.get_current_week_courses(semester_id, biz_type_id) # 获取今天的日期 today_str = datetime.datetime.now().strftime("%Y-%m-%d") # 筛选今天的课程 today_courses = [ course for course in week_courses.courses if course.date == today_str ] return Courses(courses=today_courses)
[文档] async def get_today_courses_async( self, semester_id: str | int = None, biz_type_id: str | int = None ) -> Courses: """ 异步获取今日课程表 :param str semester_id: 学期ID :param str biz_type_id: 业务类型 ID,用于区分本科生和研究生 :return: 返回今日课程表数据 :rtype: Courses """ if semester_id is None: semester_id = self.current_semester_id if biz_type_id is None: biz_type_id = self.biz_type_id # 获取本周课程表 week_courses = await self.get_current_week_courses_async( semester_id, biz_type_id ) # 获取今天的日期 today_str = datetime.datetime.now().strftime("%Y-%m-%d") # 筛选今天的课程 today_courses = [ course for course in week_courses.courses if course.date == today_str ] return Courses(courses=today_courses)
[文档] def get_room_data( self, building_id: int | str, date_str: str = datetime.datetime.now().strftime("%Y-%m-%d"), ) -> RoomOccupancyData: """ 获取教室占用数据 :param building_id: 建筑ID :param date_str: 日期字符串,格式为YYYY-MM-DD,默认为当天 :return: 返回教室占用数据 :rtype: RoomOccupancyData :raises Exception: 如果API请求失败 """ return sync_wrapper(self.get_room_data_async)(building_id, date_str)
[文档] async def get_room_data_async( self, building_id: int | str, date_str: str = datetime.datetime.now().strftime("%Y-%m-%d"), ) -> RoomOccupancyData: """ 异步获取教室占用数据 :param building_id: 建筑ID :param date_str: 日期字符串,格式为YYYY-MM-DD,默认为当天 :return: 返回教室占用数据 :rtype: RoomOccupancyData :raises Exception: 如果API请求失败 """ data = { "building_id": building_id, "start_date": date_str, "random": int(random.uniform(10000, 99999)), "end_date": None, "token": self._parent._dynamicToken, "timestamp": int(round(time.time() * 1000)), } params = "&".join([f"{key}={value}" for key, value in data.items()]) sign = get_sign(self._parent._dynamicSecret, params) data["sign"] = sign # 在try块外初始化response变量为None response = None try: headers = self._default_headers.copy() headers["User-Agent"] = ( self._parent._DeviceParams.userAgentPrecursor + "SuperApp" ) headers["token"] = self._parent._dynamicToken response = await self._parent._client.post( "https://jw.v.zzu.edu.cn/app-ws/ws/app-service/room/borrow/occupancy/search", headers=headers, data=data, ) response.raise_for_status() # 解析响应数据 business_data = json.loads( base64.b64decode(response.json()["business_data"]) ) return RoomOccupancyData(**business_data[0]) except httpx.HTTPStatusError as e: raise Exception(f"API请求失败: HTTP {e.response.status_code}") except httpx.RequestError as e: raise Exception(f"网络请求失败: {str(e)}") except json.JSONDecodeError as e: # 检查response是否已定义 error_text = response.text if response is not None else "无响应内容" raise Exception(f"解析响应JSON失败: {error_text}") except Exception as e: logger.error(f"获取教室占用数据失败: {e}") raise
[文档] def get_semester_data(self, biz_type_id: str | int = None) -> SemesterData: """ 获取学期数据 :param biz_type_id: 业务类型 ID,用于区分本科生和研究生。 :return: 返回学期数据 :rtype: SemesterData :raises Exception: 如果API请求失败 """ return sync_wrapper(self.get_semester_data_async)(biz_type_id)
[文档] async def get_semester_data_async( self, biz_type_id: str | int = None ) -> SemesterData: """ 异步获取学期数据 :param biz_type_id: 业务类型 ID,用于区分本科生和研究生。 :return: 返回学期数据 :rtype: SemesterData :raises Exception: 如果API请求失败 """ if biz_type_id is None: biz_type_id = self.biz_type_id data = { "biz_type_id": str(biz_type_id), "random": int(random.uniform(10000, 99999)), # '1' 代表本科生 "timestamp": int(round(time.time() * 1000)), "token": self._parent._dynamicToken, } params = "&".join([f"{key}={value}" for key, value in data.items()]) sign = get_sign(self._parent._dynamicSecret, params) data["sign"] = sign response = None try: headers = self._default_headers.copy() headers["User-Agent"] = ( self._parent._DeviceParams.userAgentPrecursor + "SuperApp" ) headers["token"] = self._parent._dynamicToken response = await self._parent._client.post( "https://jw.v.zzu.edu.cn/app-ws/ws/app-service/common/get-semester", headers=headers, data=data, ) response.raise_for_status() # 解析响应数据 business_data = json.loads( base64.b64decode(response.json()["business_data"]) ) return SemesterData(**business_data) except httpx.HTTPStatusError as e: raise Exception(f"API请求失败: HTTP {e.response.status_code}") except httpx.RequestError as e: raise Exception(f"网络请求失败: {str(e)}") except json.JSONDecodeError as e: # 检查response是否已定义 error_text = response.text if response is not None else "无响应内容" raise Exception(f"解析响应JSON失败: {error_text}") except Exception as e: logger.error(f"获取学期数据失败: {e}") raise
[文档] def get_biz_type_id( self, ) -> int: """ 获取账户默认业务类型 ID,用于区分本科生和研究生。 :return: 返回默认业务类型 ID :rtype: int """ return self.biz_type_id
[文档] def get_current_semester_id(self) -> int: """ 获取默认学期 ID :return: 返回学期 ID :rtype: int """ return self.current_semester_id