From c48bc1a30454f1c34bee064c8de3559290b7091b Mon Sep 17 00:00:00 2001 From: BoYanZh Date: Thu, 19 Jun 2025 06:37:03 -0400 Subject: [PATCH] feat: support penalty config --- joint_teapot/app.py | 20 +++++++++++++++++ joint_teapot/teapot.py | 44 ++++++++++++++++++++++++++++++++------ joint_teapot/utils/joj3.py | 35 ++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 9 deletions(-) diff --git a/joint_teapot/app.py b/joint_teapot/app.py index d81872f..21033fc 100644 --- a/joint_teapot/app.py +++ b/joint_teapot/app.py @@ -307,6 +307,15 @@ def joj3_all_env( "#795548", help="label color for the issue created by this command", ), + end_time: Optional[datetime] = Option(None), + penalty_config: str = Option( + "", + help=( + "Configuration for penalties in the format " + "'hours=factor'. " + "Example: --penalty-config 24=0.75,48=0.5" + ), + ), ) -> None: app.pretty_exceptions_enable = False set_settings(Settings(_env_file=env_path)) @@ -322,6 +331,7 @@ def joj3_all_env( logger.error("missing required env var") raise Exit(code=1) submitter_repo_name = env.github_repository.split("/")[-1] + penalty_factor = joj3.get_penalty_factor(end_time, penalty_config) total_score = joj3.get_total_score(env.joj3_output_path) res = { "totalScore": total_score, @@ -350,6 +360,7 @@ def joj3_all_env( submitter_repo_name, issue_label_name, issue_label_color, + penalty_factor, ) res["issue"] = issue_number gitea_issue_url = f"{submitter_repo_url}/issues/{issue_number}" @@ -462,6 +473,14 @@ def joj3_check_env( ), begin_time: Optional[datetime] = Option(None), end_time: Optional[datetime] = Option(None), + penalty_config: str = Option( + "", + help=( + "Configuration for penalties in the format " + "'hours=factor'. " + "Example: --penalty-config 24=0.75,48=0.5" + ), + ), ) -> None: app.pretty_exceptions_enable = False set_settings(Settings(_env_file=env_path)) @@ -477,6 +496,7 @@ def joj3_check_env( time_msg, time_failed = tea.pot.joj3_check_submission_time( begin_time, end_time, + penalty_config, ) count_msg, count_failed = tea.pot.joj3_check_submission_count( env, grading_repo_name, group_config, scoreboard_filename diff --git a/joint_teapot/teapot.py b/joint_teapot/teapot.py index 8a408ae..b5247b5 100644 --- a/joint_teapot/teapot.py +++ b/joint_teapot/teapot.py @@ -240,6 +240,7 @@ class Teapot: submitter_repo_name: str, issue_label_name: str, issue_label_color: str, + penalty_factor: float, ) -> int: title, comment = joj3.generate_title_and_comment( env.joj3_output_path, @@ -251,6 +252,7 @@ class Teapot: submitter_in_issue_title, env.joj3_run_id, max_total_score, + penalty_factor, ) title_prefix = joj3.get_title_prefix( env.joj3_conf_name, env.github_actor, submitter_in_issue_title @@ -299,15 +301,43 @@ class Teapot: self, begin_time: Optional[datetime] = None, end_time: Optional[datetime] = None, + penalty_config: str = "", ) -> Tuple[str, bool]: now = datetime.now() - if (begin_time and now < begin_time) or (end_time and now > end_time): - return ( - "### Submission Time Check Failed\n" - f"Current time {now} is not in the valid range " - f"[{begin_time}, {end_time}].\n", - True, - ) + penalties = joj3.parse_penalty_config(penalty_config) + if penalties and end_time: + penalty_end_time = end_time + timedelta(hours=penalties[-1][0]) + if begin_time and now < begin_time: + return ( + "### Submission Time Check Failed\n" + f"Current time {now} is not in the valid range " + f"[{begin_time}, {end_time}].\n", + True, + ) + elif now > penalty_end_time: + return ( + "### Submission Time Check Failed\n" + f"Current time {now} is not in the valid range " + f"[{begin_time}, {end_time}], and the penalty range " + f"[{end_time}, {penalty_end_time}].\n", + True, + ) + else: + return ( + "### Submission Time Check Passed\n" + f"Current time {now} is not in the valid range " + f"[{begin_time}, {end_time}], but in the penalty range " + f"[{end_time}, {penalty_end_time}].\n", + False, + ) + else: + if (begin_time and now < begin_time) or (end_time and now > end_time): + return ( + "### Submission Time Check Failed\n" + f"Current time {now} is not in the valid range " + f"[{begin_time}, {end_time}].\n", + True, + ) return ( "### Submission Time Check Passed\n" f"Current time {now} is in the valid range " diff --git a/joint_teapot/utils/joj3.py b/joint_teapot/utils/joj3.py index 81f611e..c5014e8 100644 --- a/joint_teapot/utils/joj3.py +++ b/joint_teapot/utils/joj3.py @@ -2,8 +2,8 @@ import bisect import csv import json import os -from datetime import datetime -from typing import Any, Dict, List, Tuple +from datetime import datetime, timedelta +from typing import Any, Dict, List, Optional, Tuple from pydantic_settings import BaseSettings @@ -215,6 +215,7 @@ def generate_title_and_comment( submitter_in_title: bool = True, run_id: str = "unknown", max_total_score: int = -1, + penalty_factor: float = 1.0, ) -> Tuple[str, str]: with open(score_file_path) as json_file: stages: List[Dict[str, Any]] = json.load(json_file) @@ -234,6 +235,8 @@ def generate_title_and_comment( "Powered by [JOJ3](https://github.com/joint-online-judge/JOJ3) and " "[Joint-Teapot](https://github.com/BoYanZh/Joint-Teapot) with ❤️.\n" ) + if penalty_factor != 1.0: + comment += f"Note: The total score is multiplied by a penalty factor of {penalty_factor}.\n" for stage in stages: if all( result["score"] == 0 and result["comment"].strip() == "" @@ -254,6 +257,8 @@ def generate_title_and_comment( comment += "\n\n" total_score += result["score"] comment += "\n" + if penalty_factor != 1.0: + total_score = round(total_score * penalty_factor) title = get_title_prefix(exercise_name, submitter, submitter_in_title) if max_total_score >= 0: title += f"{total_score} / {max_total_score}" @@ -281,3 +286,29 @@ def get_title_prefix( if not submitter_in_title: title = f"JOJ3 Result for {exercise_name} - Score: " return title + + +def parse_penalty_config(penalty_config: str) -> List[Tuple[float, float]]: + res = [] + for penalty in penalty_config.split(","): + hour, factor = map(float, penalty.split("=")) + res.append((hour, factor)) + res.sort(key=lambda x: x[0]) + return res + + +def get_penalty_factor( + end_time: Optional[datetime], + penalty_config: str, +) -> float: + if not end_time or not penalty_config: + return 1.0 + penalties = parse_penalty_config(penalty_config) + now = datetime.now() + res = 0.0 + for hour, factor in penalties[::-1]: + if now < end_time + timedelta(hours=hour): + res = factor + else: + break + return res