33import json
44from dataclasses import dataclass , field
55from datetime import timedelta
6- from typing import Any , Dict , Literal , Optional
6+ from typing import Any , ClassVar , Dict , Literal , Optional
77
8+ import requests
89from jinja2 import Environment , meta , Template
910
1011
@@ -35,6 +36,9 @@ def to_timedelta(self) -> timedelta:
3536 else :
3637 raise ValueError (f"Unsupported unit: { self .unit } " )
3738
39+ def to_timedelta_s (self ) -> int :
40+ return int (self .to_timedelta ().total_seconds ())
41+
3842 def get_text (self ):
3943 return f"{ self .value } _{ self .unit } "
4044
@@ -96,12 +100,23 @@ class RangeConfig:
96100 def total_timedelta (self ) -> timedelta :
97101 return timedelta (days = self .baseline .value + self .comparison .value )
98102
103+ def total_timedelta_s (self ) -> int :
104+ return int (
105+ timedelta (days = self .baseline .value + self .comparison .value ).total_seconds ()
106+ )
107+
99108 def comparison_timedelta (self ) -> timedelta :
100109 return timedelta (days = self .comparison .value )
101110
111+ def comparison_timedelta_s (self ) -> int :
112+ return int (self .comparison_timedelta ().total_seconds ())
113+
102114 def baseline_timedelta (self ) -> timedelta :
103115 return timedelta (days = self .baseline .value )
104116
117+ def baseline_timedelta_s (self ) -> int :
118+ return int (self .baseline_timedelta ().total_seconds ())
119+
105120
106121# -------- Policy: metrics --------
107122@dataclass
@@ -121,9 +136,7 @@ class RegressionPolicy:
121136 "greater_than" , "less_than" , "equal_to" , "greater_equal" , "less_equal"
122137 ]
123138 threshold : float
124- baseline_aggregation : Literal [
125- "avg" , "max" , "min" , "p50" , "p90" , "p95" , "latest" , "earliest"
126- ] = "max"
139+ baseline_aggregation : Literal ["max" , "min" , "latest" , "earliest" ] = "max"
127140 rel_tol : float = 1e-3 # used only for "equal_to"
128141
129142 def is_violation (self , value : float , baseline : float ) -> bool :
@@ -154,13 +167,60 @@ def is_violation(self, value: float, baseline: float) -> bool:
154167
155168
156169@dataclass
157- class Policy :
158- frequency : Frequency
159- range : RangeConfig
160- metrics : Dict [str , RegressionPolicy ]
170+ class BaseNotificationConfig :
171+ # subclasses override this
172+ type_tag : ClassVar [str ] = ""
173+
174+ @classmethod
175+ def matches (cls , d : Dict [str , Any ]) -> bool :
176+ return d .get ("type" ) == cls .type_tag
161177
162- # TODO(elainewy): add notification config
163- notification_config : Optional [Dict [str , Any ]] = None
178+
179+ @dataclass
180+ class GitHubNotificationConfig (BaseNotificationConfig ):
181+ type_tag : ClassVar [str ] = "github"
182+
183+ # actual fields
184+ type : str = "github"
185+ repo : str = "" # e.g. "owner/repo"
186+ issue_number : str = "" # store as str for simplicity
187+
188+ @classmethod
189+ def from_dict (cls , d : Dict [str , Any ]) -> "GitHubNotificationConfig" :
190+ # support 'issue' alias
191+ issue = d .get ("issue_number" ) or d .get ("issue" ) or ""
192+ return cls (
193+ type = "github" ,
194+ repo = d .get ("repo" , "" ),
195+ issue_number = str (issue ),
196+ )
197+
198+ def create_github_comment (self , body : str , github_token : str ) -> Dict [str , Any ]:
199+ url = f"https://api.github.com/repos/{ self .repo } /issues/{ self .issue_number } /comments"
200+ headers = {
201+ "Authorization" : f"token { github_token } " ,
202+ "Accept" : "application/vnd.github+json" ,
203+ "User-Agent" : "bench-reporter/1.0" ,
204+ }
205+ resp = requests .post (url , headers = headers , json = {"body" : body })
206+ resp .raise_for_status ()
207+ return resp .json ()
208+
209+
210+ @dataclass
211+ class Policy :
212+ frequency : "Frequency"
213+ range : "RangeConfig"
214+ metrics : Dict [str , "RegressionPolicy" ]
215+
216+ notification_config : Optional [dict [str , Any ]] = None
217+
218+ def get_github_notification_config (self ) -> Optional [GitHubNotificationConfig ]:
219+ if not self .notification_config :
220+ return None
221+ if self .notification_config .get ("type" ) != "github" :
222+ return None
223+ return GitHubNotificationConfig .from_dict (self .notification_config )
164224
165225
166226# -------- Top-level benchmark regression config --------
0 commit comments