From 69fb59db4db24eead5fefd4101c471a628a92201 Mon Sep 17 00:00:00 2001 From: Kowyo Bot Date: Sat, 31 Jan 2026 16:55:04 +0800 Subject: [PATCH 1/3] info: append grade details from grades_summary --- src/hoa_majors/cli/info.py | 90 +++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/src/hoa_majors/cli/info.py b/src/hoa_majors/cli/info.py index 8bd2081..f93afed 100644 --- a/src/hoa_majors/cli/info.py +++ b/src/hoa_majors/cli/info.py @@ -1,4 +1,5 @@ import argparse +import json import sys from pathlib import Path @@ -6,12 +7,89 @@ from hoa_majors.core.utils import iter_toml_files +def _load_grades_summary(data_dir: Path) -> dict: + """Load grades_summary.json if present; otherwise return empty dict.""" + + path = data_dir / "grades_summary.json" + if not path.exists(): + return {} + + try: + return json.loads(path.read_text(encoding="utf-8")) + except Exception as e: + logger.warning(f"无法读取 {path.name}: {e}") + return {} + + +def _print_grade_details( + *, + grades_summary: dict, + course_code: str, + year: str | None, + major_code: str | None, + major_name: str | None, +): + """Append grade details for a course, if any match exists.""" + + entry = grades_summary.get(course_code) + if not isinstance(entry, dict): + return + + year = (year or "").strip() + major_code = (major_code or "").strip() + major_name = (major_name or "").strip() + + # Selection order: + # 1) year_major + # 2) year_default + # 3) default + # + # Note: upstream grades_summary.json uses year+major *name* (e.g. 2021_自动化). + # The feature request mentions major code, so we try both code and name. + year_major_keys: list[str] = [] + if year and major_code: + year_major_keys.append(f"{year}_{major_code}") + if year and major_name: + year_major_keys.append(f"{year}_{major_name}") + + year_default_key = f"{year}_default" if year else "" + + grade_items = None + for k in year_major_keys: + if k in entry: + grade_items = entry.get(k) + break + if grade_items is None and year_default_key and year_default_key in entry: + grade_items = entry.get(year_default_key) + if grade_items is None and "default" in entry: + grade_items = entry.get("default") + + if not isinstance(grade_items, list) or not grade_items: + return + + print("-" * 60) + print("成绩构成 (来自 grades_summary.json):") + for item in grade_items: + if not isinstance(item, dict): + continue + name = str(item.get("name", "")).strip() + percent = item.get("percent") + percent_str = str(percent).strip() if percent is not None else "" + if percent_str: + print(f" - {name}: {percent_str}") + else: + print(f" - {name}") + + def get_course_info(plan_id: str, course_code: str, data_dir: Path): found_plan = False found_course = False + grades_summary = _load_grades_summary(data_dir) + for _, data in iter_toml_files(data_dir): - if data.get("info", {}).get("plan_ID") == plan_id: + info = data.get("info", {}) + if info.get("plan_ID") == plan_id: found_plan = True for course in data.get("courses", []): if course.get("course_code") == course_code: @@ -29,6 +107,16 @@ def get_course_info(plan_id: str, course_code: str, data_dir: Path): print("学时分配:") for h_key, h_val in course["hours"].items(): print(f" {h_key.title():<23}: {h_val}") + + # Append grade details if we can find a matching summary entry. + _print_grade_details( + grades_summary=grades_summary, + course_code=course_code, + year=info.get("year"), + major_code=info.get("major_code"), + major_name=info.get("major_name"), + ) + print("=" * 60) break if found_course: From 63d7e745cadf06ad1d0e9e256a488f19c51e725b Mon Sep 17 00:00:00 2001 From: Kowyo Bot Date: Sat, 31 Jan 2026 17:03:00 +0800 Subject: [PATCH 2/3] info: tidy output format for grades summary --- src/hoa_majors/cli/info.py | 48 +++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/hoa_majors/cli/info.py b/src/hoa_majors/cli/info.py index f93afed..14efd52 100644 --- a/src/hoa_majors/cli/info.py +++ b/src/hoa_majors/cli/info.py @@ -68,7 +68,7 @@ def _print_grade_details( return print("-" * 60) - print("成绩构成 (来自 grades_summary.json):") + print("成绩构成") for item in grade_items: if not isinstance(item, dict): continue @@ -76,9 +76,9 @@ def _print_grade_details( percent = item.get("percent") percent_str = str(percent).strip() if percent is not None else "" if percent_str: - print(f" - {name}: {percent_str}") + print(f"{name}: {percent_str}") else: - print(f" - {name}") + print(f"{name}") def get_course_info(plan_id: str, course_code: str, data_dir: Path): @@ -94,19 +94,39 @@ def get_course_info(plan_id: str, course_code: str, data_dir: Path): for course in data.get("courses", []): if course.get("course_code") == course_code: found_course = True - print(f"\n培养方案 {plan_id} 中的课程 {course_code} 详细信息:") - print("=" * 60) - # 打印核心字段 - for key, value in course.items(): - if key != "hours": - print(f"{key.replace('_', ' ').title():<25}: {value}") - - # 打印学时子表 + # 基本信息 + print("\n基本信息") + field_order = [ + ("course_code", "Course Code"), + ("credit", "Credit"), + ("assessment_method", "Assessment Method"), + ("course_name", "Course Name"), + ("recommended_year_semester", "Recommended Year Semester"), + ("course_nature", "Course Nature"), + ("course_category", "Course Category"), + ("offering_college", "Offering College"), + ("total_hours", "Total Hours"), + ] + label_width = 26 + for k, label in field_order: + if k in course: + print(f"{label:<{label_width}} : {course.get(k)}") + + # 学时分配 if "hours" in course: print("-" * 60) - print("学时分配:") - for h_key, h_val in course["hours"].items(): - print(f" {h_key.title():<23}: {h_val}") + print("学时分配") + hour_order = [ + ("theory", "Theory"), + ("lab", "Lab"), + ("practice", "Practice"), + ("exercise", "Exercise"), + ("computer", "Computer"), + ("tutoring", "Tutoring"), + ] + for h_key, h_label in hour_order: + if h_key in course["hours"]: + print(f"{h_label:<{label_width}} : {course['hours'].get(h_key)}") # Append grade details if we can find a matching summary entry. _print_grade_details( From 48c039b0927b0cd38e144f2b0daccd2f6fd88e7b Mon Sep 17 00:00:00 2001 From: Kowyo Bot Date: Sat, 31 Jan 2026 17:06:34 +0800 Subject: [PATCH 3/3] info: add --json output mode --- src/hoa_majors/cli/info.py | 99 +++++++++++++++++++++++++++++--------- src/hoa_majors/cli/main.py | 11 ++++- 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/src/hoa_majors/cli/info.py b/src/hoa_majors/cli/info.py index 14efd52..ebb8f4d 100644 --- a/src/hoa_majors/cli/info.py +++ b/src/hoa_majors/cli/info.py @@ -21,29 +21,33 @@ def _load_grades_summary(data_dir: Path) -> dict: return {} -def _print_grade_details( +def _select_grade_details( *, grades_summary: dict, course_code: str, year: str | None, major_code: str | None, major_name: str | None, -): - """Append grade details for a course, if any match exists.""" +) -> tuple[list[dict] | None, str | None]: + """Select grade details for a course. + + Returns: + (grade_items, matched_key) + + Match order: + 1) year_major + 2) year_default + 3) default + """ entry = grades_summary.get(course_code) if not isinstance(entry, dict): - return + return None, None year = (year or "").strip() major_code = (major_code or "").strip() major_name = (major_name or "").strip() - # Selection order: - # 1) year_major - # 2) year_default - # 3) default - # # Note: upstream grades_summary.json uses year+major *name* (e.g. 2021_自动化). # The feature request mentions major code, so we try both code and name. year_major_keys: list[str] = [] @@ -54,17 +58,36 @@ def _print_grade_details( year_default_key = f"{year}_default" if year else "" - grade_items = None for k in year_major_keys: - if k in entry: - grade_items = entry.get(k) - break - if grade_items is None and year_default_key and year_default_key in entry: - grade_items = entry.get(year_default_key) - if grade_items is None and "default" in entry: - grade_items = entry.get("default") - - if not isinstance(grade_items, list) or not grade_items: + if k in entry and isinstance(entry.get(k), list) and entry.get(k): + return entry.get(k), k + + if year_default_key and year_default_key in entry and isinstance(entry.get(year_default_key), list) and entry.get(year_default_key): + return entry.get(year_default_key), year_default_key + + if "default" in entry and isinstance(entry.get("default"), list) and entry.get("default"): + return entry.get("default"), "default" + + return None, None + + +def _print_grade_details( + *, + grades_summary: dict, + course_code: str, + year: str | None, + major_code: str | None, + major_name: str | None, +): + grade_items, _ = _select_grade_details( + grades_summary=grades_summary, + course_code=course_code, + year=year, + major_code=major_code, + major_name=major_name, + ) + + if not grade_items: return print("-" * 60) @@ -81,7 +104,7 @@ def _print_grade_details( print(f"{name}") -def get_course_info(plan_id: str, course_code: str, data_dir: Path): +def get_course_info(plan_id: str, course_code: str, data_dir: Path, as_json: bool = False): found_plan = False found_course = False @@ -94,6 +117,31 @@ def get_course_info(plan_id: str, course_code: str, data_dir: Path): for course in data.get("courses", []): if course.get("course_code") == course_code: found_course = True + + grade_items, matched_grade_key = _select_grade_details( + grades_summary=grades_summary, + course_code=course_code, + year=info.get("year"), + major_code=info.get("major_code"), + major_name=info.get("major_name"), + ) + + if as_json: + out = { + "plan_id": plan_id, + "course_code": course_code, + "course": { + k: v + for k, v in course.items() + if k != "hours" # keep hours in a separate object for cleanliness + }, + "hours": course.get("hours"), + "grade_details": grade_items, + "grade_details_key": matched_grade_key, + } + print(json.dumps(out, ensure_ascii=False, indent=2)) + return + # 基本信息 print("\n基本信息") field_order = [ @@ -126,7 +174,9 @@ def get_course_info(plan_id: str, course_code: str, data_dir: Path): ] for h_key, h_label in hour_order: if h_key in course["hours"]: - print(f"{h_label:<{label_width}} : {course['hours'].get(h_key)}") + print( + f"{h_label:<{label_width}} : {course['hours'].get(h_key)}" + ) # Append grade details if we can find a matching summary entry. _print_grade_details( @@ -154,10 +204,15 @@ def main(): parser = argparse.ArgumentParser(description="获取培养方案中特定课程的详细信息") parser.add_argument("plan_id", help="培养方案 ID (fah)") parser.add_argument("course_code", help="课程代码") + parser.add_argument( + "--json", + action="store_true", + help="以纯 JSON 输出(仅输出课程与成绩构成等信息,不含格式化文本)", + ) parser.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR, help="数据存储目录") args = parser.parse_args() - get_course_info(args.plan_id, args.course_code, args.data_dir) + get_course_info(args.plan_id, args.course_code, args.data_dir, as_json=args.json) if __name__ == "__main__": diff --git a/src/hoa_majors/cli/main.py b/src/hoa_majors/cli/main.py index 4ceb650..9a9a26c 100644 --- a/src/hoa_majors/cli/main.py +++ b/src/hoa_majors/cli/main.py @@ -59,7 +59,14 @@ def main(): info_parser = subparsers.add_parser("info", help="获取培养方案中特定课程的详细信息") info_parser.add_argument("plan_id", help="培养方案 ID (fah)") info_parser.add_argument("course_code", help="课程代码") - info_parser.add_argument("--data-dir", type=Path, default=DEFAULT_DATA_DIR, help="数据存储目录") + info_parser.add_argument( + "--json", + action="store_true", + help="以纯 JSON 输出(仅输出课程与成绩构成等信息,不含格式化文本)", + ) + info_parser.add_argument( + "--data-dir", type=Path, default=DEFAULT_DATA_DIR, help="数据存储目录" + ) # repo repo_parser = subparsers.add_parser("repo", help="获取课程对应的 OpenAuto 仓库 ID") @@ -105,7 +112,7 @@ def main(): elif args.command == "courses": courses.list_courses(args.plan_id, args.data_dir) elif args.command == "info": - info.get_course_info(args.plan_id, args.course_code, args.data_dir) + info.get_course_info(args.plan_id, args.course_code, args.data_dir, as_json=args.json) elif args.command == "repo": repo.run(args) else: