diff --git a/.gitignore b/.gitignore index 15201ac..d86e4ff 100644 --- a/.gitignore +++ b/.gitignore @@ -165,7 +165,7 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ # PyPI configuration file .pypirc diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..09aa424 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,26 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +python: + install: + - requirements: ./requirements.txt + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/Logo.png b/Logo.png new file mode 100644 index 0000000..6086f32 Binary files /dev/null and b/Logo.png differ diff --git a/README.md b/README.md index 656a444..b70ce75 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,13 @@ -
+ logo - +# cseslib4py -# PyCSES +适用于 Python 的 CSES API -CSES Access Framework for Python - -#### [Main Repo](https://github.com/SmartTeachCN/pycses) - -
- -## Introduction - -PyCSES is a Python library that provides access to the CSES format. It is designed to be simple and easy to use. - -## Functions - -```python -import cses - -# Read a CSES file -parser = cses.CSESParser("path/to/file.cses.yaml") - -# Check if the file is valid -if not cses.CSESParser.is_cses_file("path/to/file.cses.yaml"): - print("Not a valid CSES file") - -# Get subjects -for subject in parser.get_subjects(): - print("Name:", subject["name"]) - print("Simplified Name:", subject["simplified_name"]) - print("Teacher:", subject["teacher"]) - print("Room:", subject["room"]) - print("") - -# Get schedules -for schedule in parser.get_schedules(): - print("Name:", schedule["name"]) - print("Enable Day:", schedule["enable_day"]) - print("Weeks:", schedule["weeks"]) - print("Classes:") - for class_ in schedule["classes"]: - print(" Subject:", class_["subject"]) - print(" Start Time:", class_["start_time"]) - print(" End Time:", class_["end_time"]) - print("") - - -# Generate a CSES file -generator = cses.CSESGenerator(version=1) - -# Add a subject -generator.add_subject(name="Math", simplified_name="M", teacher="Mr. Wang", room="101") - -# Add a schedule -generator.add_schedule(name="Monday", enable_day="mon", weeks=all, classes=[ - { - "subject": "Math", - "start_time": "08:00", - "end_time": "09:00" - }, - { - "subject": "Biology", - "start_time": "09:00", - "end_time": "10:00" - } -]) - -# Save the file -generator.save_to_file("path/to/file.cses.yaml") -``` +## 简介 +**cseslib4py** 是一个 Python 库,提供对 [CSES](https://github.com/SmartTeachCN/CSES) 格式的访问,同时具有简单易用的设计。
+此外,它由 [MacrosMeng](https://github.com/MacroMeng) 重构,以提供比已经停更的 [pycses](https://github.com/SmartTeachCN/pycses) **更好**和**具有更高易读性**的访问。 +## 许可证 +**cseslib4py** 采用 **MIT** 许可证。您可以在遵守许可证条款的前提下自由使用、修改和分发该库。 diff --git a/README.rst b/README.rst deleted file mode 100644 index 5c7438d..0000000 --- a/README.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. image:: https://static.smart-teach.cn/logos/full.jpg - :height: 64 - -PyCSES -====== - -一个用于解析与生成 CSES 格式的 Python 库,提供简单易用的 API。 -详细示例请见 [cses/__init__.py](cses/__init__.py)。 - -安装 ----- - -.. code-block:: console - - pip install pycses - -使用示例 --------- - -.. code-block:: python - -import cses - -# Read a CSES file -parser = cses.CSESParser("path/to/file.cses.yaml") - -# Check if the file is valid -if not cses.CSESParser.is_cses_file("path/to/file.cses.yaml"): - print("Not a valid CSES file") - -# Get subjects -for subject in parser.get_subjects(): - print("Name:", subject["name"]) - print("Simplified Name:", subject["simplified_name"]) - print("Teacher:", subject["teacher"]) - print("Room:", subject["room"]) - print("") - -# Get schedules -for schedule in parser.get_schedules(): - print("Name:", schedule["name"]) - print("Enable Day:", schedule["enable_day"]) - print("Weeks:", schedule["weeks"]) - print("Classes:") - for class_ in schedule["classes"]: - print(" Subject:", class_["subject"]) - print(" Start Time:", class_["start_time"]) - print(" End Time:", class_["end_time"]) - print("") - - -# Generate a CSES file -generator = cses.CSESGenerator(version=1) - -# Add a subject -generator.add_subject(name="Math", simplified_name="M", teacher="Mr. Wang", room="101") - -# Add a schedule -generator.add_schedule(name="Monday", enable_day="mon", weeks=all, classes=[ - { - "subject": "Math", - "start_time": "08:00", - "end_time": "09:00" - }, - { - "subject": "Biology", - "start_time": "09:00", - "end_time": "10:00" - } -]) - -# Save the file -generator.save_to_file("path/to/file.cses.yaml") - ... - -更多信息 --------- - -请参阅 [README.md](README.md) 或 [LICENSE](LICENSE) 获取更多说明。 \ No newline at end of file diff --git a/cses/__init__.py b/cses/__init__.py index 9889277..8434628 100644 --- a/cses/__init__.py +++ b/cses/__init__.py @@ -1,224 +1,86 @@ +"""使用 ``CSES`` 类可以表示、解析一个 CSES 课程文件。""" import yaml -from pathlib import Path -from collections import OrderedDict -# 解析器 -class CSESParser: - def __init__(self, file_path): - """ - 初始化 CSES 解析器 - - Args: - file_path (str): CSES 格式的 YAML 文件路径 - """ - self.file_path = file_path - self.data = None - self.version = None - self.subjects = [] - self.schedules = [] - - self._load_file() - self._parse_data() - - def _load_file(self): - """加载并解析 YAML 文件""" - try: - with open(self.file_path, 'r', encoding='utf-8') as f: - self.data = yaml.safe_load(f) - except FileNotFoundError: - raise FileNotFoundError(f"File {self.file_path} Not Found") - except yaml.YAMLError as e: - raise ValueError(f"YAML Error: {e}") - - def _parse_data(self): - """解析 YAML 数据""" - if not self.data: - return - - # 获取版本信息 - self.version = self.data.get('version', 1) - - # 解析科目信息 - subjects_data = self.data.get('subjects', []) - for subject in subjects_data: - subject_info = { - 'name': subject['name'], - 'simplified_name': subject.get('simplified_name'), - 'teacher': subject.get('teacher'), - 'room': subject.get('room') - } - self.subjects.append(subject_info) - - # 解析课程安排 - schedules_data = self.data.get('schedules', []) - for schedule in schedules_data: - schedule_info = { - 'name': schedule['name'], - 'enable_day': schedule['enable_day'], - 'weeks': schedule['weeks'], - 'classes': [] - } - - # 解析课程 - classes_data = schedule.get('classes', []) - for cls in classes_data: - class_info = { - 'subject': cls['subject'], - 'start_time': cls['start_time'], - 'end_time': cls['end_time'] - } - schedule_info['classes'].append(class_info) - - self.schedules.append(schedule_info) - - def get_subjects(self): - """获取所有科目信息""" - return self.subjects - - def get_schedules(self): - """获取所有课程安排""" - return self.schedules - - def get_schedule_by_day(self, day): +import cses.structures as st +import cses.errors as err + + +class CSES: + """ + 用来表示、解析一个 CSES 课程文件的类。 + + 该类有如下属性: + - ``schedule``: 课程安排列表,每个元素是一个 ``SingleDaySchedule`` 对象。 + - ``version``: 课程文件的版本号。目前只能为 ``1`` ,参见 CSES 官方文档与 Schema 文件。 + - ``subjects``: 科目列表,每个元素是一个 ``Subject`` 对象。 + + Examples: + >>> c = CSES(open('../cses_example.yaml', encoding='utf8').read()) + >>> c.version # 只会为 1 + 1 + >>> c.subjects # doctest: +NORMALIZE_WHITESPACE + {'数学': Subject(name='数学', simplified_name='数', teacher='李梅', room='101'), + '语文': Subject(name='语文', simplified_name='语', teacher='王芳', room='102'), + '英语': Subject(name='英语', simplified_name='英', teacher='张伟', room='103'), + '物理': Subject(name='物理', simplified_name='物', teacher='赵军', room='104')} + + """ + + def __init__(self, content: str): """ - 根据星期获取课程安排 - + 初始化 CSES。 + Args: - day (str): 星期(如 'mon', 'tue' 等) - - Returns: - list: 该星期的课程安排 + content (str): CSES 课程文件的内容。 """ - for schedule in self.schedules: - if schedule['enable_day'] == day: - return schedule['classes'] - return [] - - @staticmethod - def is_cses_file(file_path): + self.schedule = None + self.version = None + self.subjects = None + + self._load(content) + + def _load(self, content: str): """ - 判断是否为 CSES 格式的文件 - + 从 ``content`` 加载 CSES 课程文件的内容。 + Args: - file_path (str): 文件路径 - - Returns: - bool: 是否为 CSES 文件 + content (str): CSES 课程文件的内容。 """ + data = yaml.safe_load(content) + + # 版本处理&检查 + self.version = data['version'] + if self.version != 1: + raise err.VersionError(f'不支持的版本号: {self.version}') + + # 科目处理&检查 try: - with open(file_path, 'r', encoding='utf-8') as f: - data = yaml.safe_load(f) - return 'version' in data and 'subjects' in data and 'schedules' in data - except: - return False - -# 生成器 -class CSESGenerator: - def __init__(self, version=1): - """ - 初始化 CSES 生成器 - - Args: - version (int, optional): CSES 格式的版本号,默认为 1 - """ - self.version = version - self.subjects = [] - self.schedules = [] + self.subjects = {s['name']: st.Subject(**s) for s in data['subjects']} + except st.ValidationError as e: + raise err.ParseError(f'科目数据有误: {data['subjects']}') from e - def add_subject(self, name, simplified_name=None, teacher=None, room=None): - """ - 添加科目信息 - - Args: - name (str): 科目名称 - simplified_name (str, optional): 科目简称 - teacher (str, optional): 教师姓名 - room (str, optional): 教室名称 - """ - subject = { - 'name': name, - 'simplified_name': simplified_name, - 'teacher': teacher, - 'room': room - } - self.subjects.append(subject) - - def add_schedule(self, name, enable_day, weeks, classes): - """ - 添加课程安排 - - Args: - name (str): 课程安排名称(如 "星期一") - enable_day (str): 课程安排的星期(如 'mon', 'tue' 等) - weeks (str): 周次类型(如 'all', 'odd', 'even') - classes (list): 课程列表,每个课程包含以下键: - - subject (str): 科目名称 - - start_time (str): 开始时间(如 '8:00') - - end_time (str): 结束时间(如 '9:00') - """ - schedule = { - 'name': name, - 'enable_day': enable_day, - 'weeks': weeks, - 'classes': classes - } - self.schedules.append(schedule) - - def generate_cses_data(self): - """ - 生成 CSES 格式的字典数据 - - Returns: - dict: CSES 格式的字典数据 - """ - cses_data = { - 'version': self.version, - 'subjects': self.subjects, - 'schedules': self.schedules - } - return cses_data - - def save_to_file(self, file_path): - """ - 将 CSES 数据保存到 YAML 文件 - - Args: - file_path (str): 输出文件路径 - """ - cses_data = self.generate_cses_data() - + # 课程处理&检查 + schedules = data['schedules'] try: - with open(file_path, 'w', encoding='utf-8') as f: - yaml.dump(cses_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False) - except IOError as e: - raise IOError(f"Failed to write {file_path}: {e}") - -# 示例用法 -if __name__ == "__main__": - import sys - - if len(sys.argv) != 2: - print("""Check CSES File -Usage: python -m cses """) - sys.exit(1) - - file_path = sys.argv[1] - - if not CSESParser.is_cses_file(file_path): - print("Not a valid CSES file") - sys.exit(1) - - parser = CSESParser(file_path) - - print("All Subjects:") - for subject in parser.get_subjects(): - print(f"{subject['name']} ({subject.get('simplified_name', '')})") - print(f"- Teacher: {subject.get('teacher', '')}") - print(f"- Room: {subject.get('room', '')}") - - print("\nAll Schedules:") - for schedule in parser.get_schedules(): - print(f"{schedule['name']} ({schedule['enable_day']} {schedule['weeks']}):") - for cls in schedule['classes']: - print(f"- {cls['subject']} ({cls['start_time']} - {cls['end_time']})") + # 先构造课程列表,再构造课表 + schedule_classes = {i['name']: i['classes'] for i in schedules} + built_lessons = {i['name']: [] for i in schedules} + for name, classes in schedule_classes.items(): + for lesson in classes: + built_lessons[name].append( + st.Lesson(**(lesson | {'subject': self.subjects[lesson['subject']]})) + ) # 从self.subjects中获取合法的Subject对象 + + # 从构造好的课程列表中构造课表 + self.schedule = [ + st.SingleDaySchedule( + enable_day=day['enable_day'], + classes=built_lessons[day['name']], + name=day['name'], + weeks=st.WeekType(day['weeks']), + ) + for day in schedules + ] + except st.ValidationError as e: + raise err.ParseError(f'课程数据有误: {data['schedules']}') from e diff --git a/cses/__init__.py.bak b/cses/__init__.py.bak new file mode 100644 index 0000000..a951e2b --- /dev/null +++ b/cses/__init__.py.bak @@ -0,0 +1,216 @@ +import yaml + + +class CSESParser: + def __init__(self, fp): + """ + 初始化 CSES 解析器。 + + Args: + fp (str): CSES 格式的 YAML 文件路径 + """ + self.file_path = fp + self.data = None + self.version = None + self.subjects = [] + self.schedules = [] + + self._load_file() + self._parse_data() + + def _load_file(self): + """加载并解析 YAML 文件""" + try: + with open(self.file_path, 'r', encoding='utf-8') as f: + self.data = yaml.safe_load(f) + print(self.data) + except FileNotFoundError: + raise FileNotFoundError(f"File {self.file_path} Not Found") + except yaml.YAMLError as e: + raise ValueError(f"YAML Error: {e}") + + def _parse_data(self): + """解析 YAML 数据""" + if not self.data: + return + + # 获取版本信息 + self.version = self.data.get('version', 1) + + # 解析科目信息 + subjects_data = self.data.get('subjects', []) + for subject in subjects_data: + subject_info = { + 'name': subject['name'], + 'simplified_name': subject.get('simplified_name'), + 'teacher': subject.get('teacher'), + 'room': subject.get('room') + } + self.subjects.append(subject_info) + + # 解析课程安排 + schedules_data = self.data.get('schedules', []) + for schedule in schedules_data: + schedule_info = { + 'name': schedule['name'], + 'enable_day': schedule['enable_day'], + 'weeks': schedule['weeks'], + 'classes': [] + } + + # 解析课程 + classes_data = schedule.get('classes', []) + for cls in classes_data: + class_info = { + 'subject': cls['subject'], + 'start_time': cls['start_time'], + 'end_time': cls['end_time'] + } + schedule_info['classes'].append(class_info) + + self.schedules.append(schedule_info) + + def get_schedule_by_day(self, day): + """ + 根据星期获取课程安排 + + Args: + day (str): 星期(如 'mon', 'tue' 等) + + Returns: + list: 该星期的课程安排 + """ + for schedule in self.schedules: + if schedule['enable_day'] == day: + return schedule['classes'] + return [] + + @staticmethod + def is_cses_file(file_path): + """ + 判断是否为 CSES 格式的文件 + + Args: + file_path (str): 文件路径 + + Returns: + bool: 是否为 CSES 文件 + """ + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = yaml.safe_load(f) + return 'version' in data and 'subjects' in data and 'schedules' in data + except: + return False + +# 生成器 +class CSESGenerator: + def __init__(self, version=1): + """ + 初始化 CSES 生成器 + + Args: + version (int, optional): CSES 格式的版本号,默认为 1 + """ + self.version = version + self.subjects = [] + self.schedules = [] + + def add_subject(self, name, simplified_name=None, teacher=None, room=None): + """ + 添加科目信息 + + Args: + name (str): 科目名称 + simplified_name (str, optional): 科目简称 + teacher (str, optional): 教师姓名 + room (str, optional): 教室名称 + """ + subject = { + 'name': name, + 'simplified_name': simplified_name, + 'teacher': teacher, + 'room': room + } + self.subjects.append(subject) + + def add_schedule(self, name, enable_day, weeks, classes): + """ + 添加课程安排 + + Args: + name (str): 课程安排名称(如 "星期一") + enable_day (str): 课程安排的星期(如 'mon', 'tue' 等) + weeks (str): 周次类型(如 'all', 'odd', 'even') + classes (list): 课程列表,每个课程包含以下键: + - subject (str): 科目名称 + - start_time (str): 开始时间(如 '8:00') + - end_time (str): 结束时间(如 '9:00') + """ + schedule = { + 'name': name, + 'enable_day': enable_day, + 'weeks': weeks, + 'classes': classes + } + self.schedules.append(schedule) + + def generate_cses_data(self): + """ + 生成 CSES 格式的字典数据 + + Returns: + dict: CSES 格式的字典数据 + """ + cses_data = { + 'version': self.version, + 'subjects': self.subjects, + 'schedules': self.schedules + } + return cses_data + + def save_to_file(self, file_path): + """ + 将 CSES 数据保存到 YAML 文件 + + Args: + file_path (str): 输出文件路径 + """ + cses_data = self.generate_cses_data() + + try: + with open(file_path, 'w', encoding='utf-8') as f: + yaml.dump(cses_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False) + except IOError as e: + raise IOError(f"Failed to write {file_path}: {e}") + + +# 示例用法 +if __name__ == "__main__": + import sys + + if len(sys.argv) != 2: + print("""Check CSES File +Usage: python -m cses """) + sys.exit(1) + + file_path = sys.argv[1] + + if not CSESParser.is_cses_file(file_path): + print("Not a valid CSES file") + sys.exit(1) + + parser = CSESParser(file_path) + + print("All Subjects:") + for subject in parser.get_subjects(): + print(f"{subject['name']} ({subject.get('simplified_name', '')})") + print(f"- Teacher: {subject.get('teacher', '')}") + print(f"- Room: {subject.get('room', '')}") + + print("\nAll Schedules:") + for schedule in parser.get_schedules(): + print(f"{schedule['name']} ({schedule['enable_day']} {schedule['weeks']}):") + for cls in schedule['classes']: + print(f"- {cls['subject']} ({cls['start_time']} - {cls['end_time']})") + diff --git a/cses/errors.py b/cses/errors.py new file mode 100644 index 0000000..16e440b --- /dev/null +++ b/cses/errors.py @@ -0,0 +1,14 @@ +"""``cseslib4py`` 所有的错误类型定义如下。""" + + +class CSESError(Exception): + """所有由 ``cseslib4py`` 抛出的异常的基类。""" + + +class ParseError(CSESError): + """解析 CSES 课程文件时抛出的异常。""" + + +class VersionError(CSESError): + """解析 CSES 课程文件时,版本号错误抛出的异常。""" + diff --git a/cses/structures.py b/cses/structures.py new file mode 100644 index 0000000..87f2cb9 --- /dev/null +++ b/cses/structures.py @@ -0,0 +1,186 @@ +""" +本文档是cses包中的structures.py文件的文档。 +该文件定义了课程相关的数据结构,包括科目、课程、周次类型和单日课程安排。 + +.. caution:: 该模块中的数据结构仅用于表示课程结构(与其附属工具),不包含实际的读取/写入功能。 +""" +import datetime +from enum import Enum +from collections import UserList +from collections.abc import Sequence +from typing import override + +from pydantic import BaseModel, ValidationError + +import cses.utils as utils + + +class Subject(BaseModel): + """ + 单节课程科目。 + + Args: + name (str): 科目名称,如“语文” + simplified_name (str): 科目简化名称,如“语” + teacher (str): 教师姓名 + room (str): 教室名称 + + Examples: + >>> s = Subject(name='语文', simplified_name='语', teacher='张三', room='A101') + >>> s.name + '语文' + >>> s.simplified_name + '语' + >>> s.teacher + '张三' + >>> s.room + 'A101' + """ + name: str + simplified_name: str = "" + teacher: str = "" + room: str = "" + + +class Lesson(BaseModel): + """ + 单节课程。 + + Args: + subject (Subject): 课程的科目 + start_time (datetime.time): 开始的时间 + end_time (datetime.time): 结束的时间 + + Examples: + >>> l = Lesson(subject=Subject(name='语文', simplified_name='语', teacher='张三'), \ + start_time=datetime.time(8, 0, 0), end_time=datetime.time(8, 45, 0)) + >>> l.subject.name + '语文' + >>> l.start_time + datetime.time(8, 0) + >>> l.end_time + datetime.time(8, 45) + """ + subject: Subject + start_time: datetime.time + end_time: datetime.time + + +class WeekType(Enum): + """ + 周次类型。 + ALL: 适用于所有周 + ODD: 仅适用于单周 + EVEN: 仅适用于双周 + """ + ALL = "all" + ODD = "odd" + EVEN = "even" + + +class SingleDaySchedule(BaseModel): + """ + 单日课程安排。 + + Args: + enable_day (int): 课程安排的星期(如 1 表示星期一) + classes (list[Lesson]): 课程列表,每个课程包含科目、开始时间和结束时间 + name (str): 课程安排名称(如 "星期一") + weeks (WeekType): 周次类型,指定课程适用于哪些周次 + + Examples: + >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ + simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ + end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ALL) + >>> s.enable_day + 1 + >>> s.name + '星期一' + >>> s.weeks + + """ + enable_day: int + classes: list[Lesson] + name: str + weeks: WeekType + + def is_enabled_on_week(self, week: int) -> bool: + """ + 判断课程是否在指定的日期上启用。 + + Args: + week (int): 要检查的周次序号 + + Returns: + bool: 如果课程在指定周上启用,则返回 True;否则返回 False + + Examples: + >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ + simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ + end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD) + >>> s.is_enabled_on_week(3) + True + >>> s.is_enabled_on_week(6) + False + >>> s.is_enabled_on_week(11) + True + """ + return { + WeekType.ALL: True, # 适用于所有周 -> 永久启用 + WeekType.ODD: week % 2 == 1, # 单周 + WeekType.EVEN: week % 2 == 0 # 双周 + }[self.weeks] + + def is_enabled_on_day(self, start_day: datetime.date, day: datetime.date) -> bool: + """ + 判断课程是否在指定的日期上启用。 + + Args: + day (int): 要检查的日期(1 表示星期一,2 表示星期二,依此类推) + start_day (datetime.date): 课程开始的日期,用于计算周次 + + Returns: + bool: 如果课程在指定日期上启用,则返回 True;否则返回 False + + Examples: + >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ + simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ + end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD) + >>> s.is_enabled_on_day(datetime.date(2025, 9, 1), datetime.date(2025, 9, 4)) + True + >>> s.is_enabled_on_day(datetime.date(2025, 9, 1), datetime.date(2025, 9, 16)) + True + >>> s.is_enabled_on_day(datetime.date(2025, 9, 1), datetime.date(2025, 9, 24)) + False + """ + return self.is_enabled_on_week(utils.week_num(start_day, day)) + + +class Schedule(UserList[SingleDaySchedule]): + """ + 存储每天课程安排的列表。列表会按照星期排序。 + + .. caution:: + 在访问一个Schedule中的项目时,注意索引从 1 开始,而不是从 0 开始。 + 这是为了可以按照星期访问课表,而不是按照 Python 的逻辑,所以访问星期一的课表使用 ``schedule[1]`` 而不是 ``schedule[0]`` 。 + 若你想要以 Python 的逻辑访问课表,请使用 ``data`` 属性,如访问星期一的课表需要使用 ``schedule.data[0]`` 。 + + Examples: + >>> s = Schedule([ + ... SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', + ... simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), + ... end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD), + ... SingleDaySchedule(enable_day=2, classes=[Lesson(subject=Subject(name='数学', + ... simplified_name='数', teacher='李四'), start_time=datetime.time(9, 0, 0), + ... end_time=datetime.time(9, 45, 0))], name='星期二', weeks=WeekType.EVEN) + ... ]) + >>> s[1].enable_day + 1 + """ + def __init__(self, args: Sequence[SingleDaySchedule]): + result = sorted(args, key=lambda arg: arg.enable_day) # 按照启用日期(星期几)排序 + super().__init__(result) + + @override + def __getitem__(self, index: int) -> SingleDaySchedule: + return self.data[index - 1] diff --git a/cses/utils.py b/cses/utils.py new file mode 100644 index 0000000..a45a33d --- /dev/null +++ b/cses/utils.py @@ -0,0 +1,26 @@ +""" +该模块包含了一些用于内部处理的辅助函数。您也可以在您的代码中独立调用这些函数。 +""" +import datetime + + +def week_num(start_day: datetime.date, day: datetime.date) -> int: + """ + 计算指定日期是从开始日期开始的第多少周。 + + Args: + day (datetime.date): 要计算周次的日期 + start_day (datetime.date): 课程开始的日期,用于计算周次 + + Returns: + int: 指定日期是从开始日期开始的第多少周 + + Examples: + >>> week_num(datetime.date(2025, 9, 1), datetime.date(2025, 9, 4)) + 1 + >>> week_num(datetime.date(2025, 9, 1), datetime.date(2025, 9, 16)) + 3 + >>> week_num(datetime.date(2025, 9, 1), datetime.date(2025, 10, 24)) + 8 + """ + return (day - start_day).days // 7 + 1 diff --git a/cses_example.yaml b/cses_example.yaml new file mode 100644 index 0000000..d2fab62 --- /dev/null +++ b/cses_example.yaml @@ -0,0 +1,51 @@ +# $schema: ./cses_schema.json +version: 1 # 只能为 1 +subjects: + - name: 数学 # 任意 string + simplified_name: 数 # 可选,任意 string,适合中文科目名,ClassIsland 等紧凑课程表软件一般需要 + teacher: 李梅 # 可选,任意 string + room: "101" # 可选,任意 string + - name: 语文 + simplified_name: 语 + teacher: 王芳 + room: "102" + - name: 英语 + simplified_name: 英 + teacher: 张伟 + room: "103" + - name: 物理 + simplified_name: 物 + teacher: 赵军 + room: "104" + +schedules: + - name: 星期一 # 任意 string + enable_day: 1 # 1-7 的整数,即周一到周日 + weeks: all # all、odd、even 中的任意一个,即两周都开启、单周开启、双周开启 + classes: + - subject: 数学 # 任意 string + start_time: "08:00:00" # HH:MM:SS 的任意字符串,即 "00:00:00" 到 "23:59:59" + end_time: "09:00:00" # 同 start_time + - subject: 语文 + start_time: "09:00:00" + end_time: "10:00:00" + - name: 星期二-单周 + enable_day: 2 + weeks: odd + classes: + - subject: 物理 + start_time: "08:00:00" + end_time: "09:00:00" + - subject: 英语 + start_time: "09:00:00" + end_time: "10:00:00" + - name: 星期二-双周 + enable_day: 2 + weeks: even + classes: + - subject: 物理 + start_time: "08:00:00" + end_time: "09:00:00" + - subject: 英语 + start_time: "09:00:00" + end_time: "10:00:00" \ No newline at end of file diff --git a/cses_schema.json b/cses_schema.json new file mode 100644 index 0000000..2947644 --- /dev/null +++ b/cses_schema.json @@ -0,0 +1,221 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "definitions": { + "class": { + "title": "课程安排", + "description": "由课程名、开始时间、结束时间组成的课程安排对象。", + "type": "object", + "properties": { + "subject": { + "title": "课程名", + "type": "string", + "description": "subjects 中任一 subject 的 name 字段。", + "examples": [ + "数学", + "语文" + ] + }, + "start_time": { + "title": "开始时间", + "description": "课程开始时间,格式为 HH:MM:SS", + "type": "string", + "pattern": "([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)", + "examples": [ + "08:00:00", + "13:30:00" + ] + }, + "end_time": { + "title": "结束时间", + "description": "课程结束时间,格式为 HH:MM:SS", + "type": "string", + "pattern": "([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)", + "examples": [ + "08:40:00", + "14:10:00" + ] + } + }, + "required": [ + "subject", + "start_time", + "end_time" + ] + }, + "schedule": { + "title": "课程表", + "description": "CSES 日课程表", + "type": "object", + "properties": { + "name": { + "title": "名称", + "type": "string", + "description": "课程表的名称", + "examples": [ + "all-mon" + ] + }, + "enable_day": { + "title": "启用日", + "type": "integer", + "description": "启用的日数,1-7 表示周一到周日", + "enum": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7 + ] + }, + "weeks": { + "title": "周数", + "type": "string", + "description": "周数,all 表示全周,odd 表示单周,even 表示双周", + "enum": [ + "all", + "odd", + "even" + ] + }, + "classes": { + "title": "课程", + "type": "array", + "items": { + "$ref": "#/definitions/class" + }, + "description": "课程安排数组", + "examples": [ + [ + { + "subject": "数学", + "start_time": "08:00:00", + "end_time": "08:40:00" + }, + { + "subject": "语文", + "start_time": "13:30:00", + "end_time": "14:10:00" + } + ] + ] + } + }, + "required": [ + "name", + "enable_day", + "weeks", + "classes" + ] + }, + "subject": { + "title": "课程", + "description": "课程对象", + "type": "object", + "properties": { + "name": { + "title": "名称", + "type": "string", + "description": "课程名称", + "examples": [ + "数学", + "语文" + ] + }, + "teacher": { + "title": "教师", + "type": "string", + "description": "任课教师", + "examples": [ + "张三", + "李四" + ] + }, + "room": { + "title": "教室", + "type": "string", + "description": "上课教室", + "examples": [ + "101", + "102" + ] + }, + "simplified_name": { + "title": "简称", + "type": "string", + "description": "课程简称", + "examples": [ + "数", + "语" + ] + } + }, + "required": [ + "name" + ] + } + }, + "properties": { + "version": { + "const": 1, + "type": "integer", + "title": "Version", + "description": "CSES 的版本号" + }, + "subjects": { + "title": "课程列表", + "type": "array", + "items": { + "$ref": "#/definitions/subject" + }, + "description": "课程列表", + "examples": [ + [ + { + "name": "数学", + "teacher": "张三", + "room": "101", + "simplified_name": "数" + }, + { + "name": "语文", + "teacher": "李四", + "room": "102", + "simplified_name": "语" + } + ] + ] + }, + "schedules": { + "title": "课程表列表", + "type": "array", + "items": { + "$ref": "#/definitions/schedule" + }, + "description": "课程表列表", + "examples": [ + [ + { + "name": "all-mon", + "enable_day": 1, + "weeks": "all", + "classes": [ + { + "subject": "数学", + "start_time": "08:50:00", + "end_time": "08:40:00" + }, + { + "subject": "语文", + "start_time": "13:30:00", + "end_time": "14:10:00" + } + ] + } + ] + ] + } + } +} \ No newline at end of file diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..c45a657 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,36 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'cseslib4py' +copyright = '2025, MacrosMeng' +author = 'MacrosMeng' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +import os +import sys + +sys.path.insert(0, os.path.abspath('../../')) + +extensions = [ + 'sphinx.ext.napoleon', + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +templates_path = ['_templates'] +exclude_patterns = [] + +language = 'zh-cn' + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'furo' +html_static_path = ['_static'] diff --git a/docs/source/err.rst b/docs/source/err.rst new file mode 100644 index 0000000..40f8116 --- /dev/null +++ b/docs/source/err.rst @@ -0,0 +1,5 @@ +错误类型 +======== + +.. automodule:: cses.errors + :members: diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..ec023a7 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,29 @@ +cseslib4py 文档 +=================== + + | 适用于 Python 的 CSES API + +简介 +---- + +欢迎来到 ``cseslib4py`` 的文档页面。 + +**cseslib4py** 是一个 Python 库,提供对 `CSES `_ 格式的访问,同时具有简单易用的设计。 + +此外,它由 `MacrosMeng `_ 重构,以提供比已经停更的 `pycses `_ **更好** 和 **具有更高易读性** 的访问。 + +许可证 +------ +**cseslib4py** 采用 **MIT** 许可证。您可以在遵守许可证条款的前提下自由使用、修改和分发该库。 + + + +.. toctree:: + :maxdepth: 1 + :caption: 目录 + + Home + main_usage + structs + err + utils diff --git a/docs/source/main_usage.rst b/docs/source/main_usage.rst new file mode 100644 index 0000000..22e27d4 --- /dev/null +++ b/docs/source/main_usage.rst @@ -0,0 +1,5 @@ +主要用法 +======== + +.. automodule:: cses + :members: diff --git a/docs/source/structs.rst b/docs/source/structs.rst new file mode 100644 index 0000000..427ca91 --- /dev/null +++ b/docs/source/structs.rst @@ -0,0 +1,5 @@ +数据结构 +======== + +.. automodule:: cses.structures + :members: diff --git a/docs/source/utils.rst b/docs/source/utils.rst new file mode 100644 index 0000000..a737265 --- /dev/null +++ b/docs/source/utils.rst @@ -0,0 +1,5 @@ +工具函数 +======== + +.. automodule:: cses.utils + :members: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..37cc73f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,49 @@ +accessible-pygments==0.0.5 +alabaster==1.0.0 +annotated-types==0.7.0 +anyio==4.11.0 +babel==2.17.0 +beautifulsoup4==4.14.2 +certifi==2025.10.5 +charset-normalizer==3.4.4 +click==8.3.0 +colorama==0.4.6 +commonmark==0.9.1 +docutils==0.21.2 +furo==2025.9.25 +h11==0.16.0 +idna==3.11 +imagesize==1.4.1 +Jinja2==3.1.6 +Markdown==3.9 +MarkupSafe==3.0.3 +packaging==25.0 +pydantic==2.12.3 +pydantic_core==2.41.4 +Pygments==2.19.2 +PyYAML==6.0.3 +recommonmark==0.7.1 +requests==2.32.5 +roman-numerals-py==3.1.0 +setuptools==80.9.0 +sniffio==1.3.1 +snowballstemmer==3.0.1 +soupsieve==2.8 +Sphinx==8.2.3 +sphinx-autobuild==2025.8.25 +sphinx-basic-ng==1.0.0b2 +sphinx-markdown-tables==0.0.17 +sphinxcontrib-applehelp==2.0.0 +sphinxcontrib-devhelp==2.0.0 +sphinxcontrib-htmlhelp==2.1.0 +sphinxcontrib-jquery==4.1 +sphinxcontrib-jsmath==1.0.1 +sphinxcontrib-qthelp==2.0.0 +sphinxcontrib-serializinghtml==2.0.0 +starlette==0.48.0 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +urllib3==2.5.0 +uvicorn==0.38.0 +watchfiles==1.1.1 +websockets==15.0.1 diff --git a/setup.py b/setup.py index 8f678ee..3a20f63 100644 --- a/setup.py +++ b/setup.py @@ -8,15 +8,16 @@ description='CSES access framework for Python', long_description=open('README.md', encoding="utf-8").read(), long_description_content_type='text/markdown', - url='https://github.com/SmartTeachCN/pycses', + url='https://github.com/MacroMeng/cseslib4py', packages=find_packages(), classifiers=[ 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', ], - python_requires='>=3.6', + python_requires='>=3.9', install_requires=[ 'PyYAML>=5.4.1', + 'pydantic~=2.12.3', ], )