13 from datetime
import date
18 from typing
import Dict, Union, List, Optional
22 from validationfunctions
import available_revisions
26 from validationscript
import Script
29 def parse_mail_address(obj: Union[str, List[str]]) -> List[str]:
31 Take a string or list and return list of email addresses that appear in it
33 if isinstance(obj, str):
34 return re.findall(
r"[\w.-]+@[\w.-]+", obj)
35 elif isinstance(obj, list):
37 re.search(
r"[\w.-]+@[\w.-]+", c).group()
39 if re.search(
r"[\w.-]+@[\w.-]+", c)
is not None
42 raise TypeError(
"must be string or list of strings")
48 Provides functionality to send mails in case of failed scripts / validation
50 The mail data is built upon instantiation, the `send_mails` method
51 sends the actual mails.
54 def __init__(self, validation, include_expert_plots=False):
56 Initializes an instance of the Mail class from an instance of the
57 Validation class. Assumes that a comparison json file exists,
58 reads it and parses it to extract information about failed plots.
59 This information, together with information about failed scripts,
60 gets stored in self.mail_data_new. If there is mail_data.json inside
61 the log folder, its contents get stored in self.mail_data_old for
64 @param validation: validation.Validation instance
65 @param include_expert_plots: Should expert plots be included?
74 work_folder = self.
_validator_validator.work_folder
75 revisions = [
"reference"] + available_revisions(work_folder)
77 work_folder, revisions
79 with open(comparison_json_file)
as f:
80 comparison_json = json.load(f)
83 old_mail_data_path = os.path.join(
84 self.
_validator_validator.get_log_folder(),
"mail_data.json"
90 with open(old_mail_data_path)
as f:
92 except FileNotFoundError:
94 f
"Could not find old mail_data.json at {old_mail_data_path}.",
101 comparison_json, include_expert_plots=include_expert_plots
106 Looks up all scripts that failed and collects information about them.
107 See :meth:`_create_mail_log` for the structure of the resulting
114 self.
_validator_validator.get_log_folder(),
"list_of_failed_scripts.log"
117 failed_scripts = f.read().splitlines()
121 for failed_script
in failed_scripts:
124 failed_script = Script.sanitize_file_name(failed_script)
125 script = self.
_validator_validator.get_script_by_name(failed_script)
132 failed_script[
"warnings"] = []
135 failed_script[
"package"] = script.package
136 failed_script[
"rootfile"] =
", ".join(script.input_files)
137 failed_script[
"comparison_text"] =
" -- "
138 failed_script[
"description"] = script.description
141 failed_script[
"comparison_result"] =
"script failed to execute"
143 failed_script[
"file_url"] = os.path.join(
147 script.name_not_sanitized
151 for contact
in parse_mail_address(script.contact):
152 if contact
not in mail_log:
153 mail_log[contact] = {}
154 mail_log[contact][script.name] = failed_script
155 except (KeyError, TypeError):
162 self, comparison, include_expert_plots=False
163 ) -> Dict[str, Dict[str, Dict[str, str]]]:
165 Takes the entire comparison json file, finds all the plots where
166 comparison failed, finds info about failed scripts and saves them in
167 the following format:
170 "email@address.test" : {
175 "comparison_text": str,
177 "comparison_result": str,
186 The top level ordering is the email address of the contact to make
187 sure every user gets only one mail with everything in it.
192 for package
in comparison[
"packages"]:
193 for plotfile
in package[
"plotfiles"]:
194 for plot
in plotfile[
"plots"]:
195 if not include_expert_plots
and plot[
"is_expert"]:
198 if plot[
"comparison_result"]
in [
"error"]:
200 if set(plot[
"warnings"]) - {
"No reference object"}:
207 "package": plotfile[
"package"],
208 "rootfile": plotfile[
"rootfile"],
209 "comparison_text": plot[
"comparison_text"],
210 "description": plot[
"description"],
211 "comparison_result": plot[
"comparison_result"],
214 set(plot[
"warnings"]) - {
"No reference object"}
217 "file_url": os.path.join(
223 for contact
in parse_mail_address(plot[
"contact"]):
225 if contact
not in mail_log:
227 mail_log[contact] = {}
228 mail_log[contact][plot[
"title"]] = error_data
232 for contact
in failed_scripts:
234 if contact
not in mail_log:
235 mail_log[contact] = failed_scripts[contact]
238 for script
in failed_scripts[contact]:
239 mail_log[contact][script] = failed_scripts[contact][script]
245 mail_log: Dict[str, Dict[str, Dict[str, str]]],
246 old_mail_log: Optional[Dict[str, Dict[str, Dict[str, str]]]],
247 ) -> Dict[str, Dict[str, Dict[str, str]]]:
248 """ Add a new field 'compared_to_yesterday' which takes one of the
249 values 'unchanged' (same revision comparison result as in yesterday's
250 mail log, 'new' (new warning/failure), 'changed' (comparison result
252 mail_log_flagged = copy.deepcopy(mail_log)
253 for contact
in mail_log:
254 for plot
in mail_log[contact]:
255 if old_mail_log
is None:
256 mail_log_flagged[contact][plot][
257 "compared_to_yesterday"
259 elif contact
not in old_mail_log:
260 mail_log_flagged[contact][plot][
261 "compared_to_yesterday"
263 elif plot
not in old_mail_log[contact]:
264 mail_log_flagged[contact][plot][
265 "compared_to_yesterday"
268 mail_log[contact][plot][
"comparison_result"]
269 != old_mail_log[contact][plot][
"comparison_result"]
270 or mail_log[contact][plot][
"warnings"]
271 != old_mail_log[contact][plot][
"warnings"]
273 mail_log_flagged[contact][plot][
274 "compared_to_yesterday"
277 mail_log_flagged[contact][plot][
278 "compared_to_yesterday"
280 return mail_log_flagged
285 @param plot_errors: ``_create_mail_log[contact]``.
286 @return True, if there is at least one new/changed plot status
288 for plot
in plot_errors:
289 if plot_errors[plot][
"compared_to_yesterday"] !=
"unchanged":
296 Takes a dict (like in _create_mail_log) and composes a mail body
298 @param incremental (bool): Is this an incremental report or a full
303 url =
"https://b2-master.belle2.org/validation/static/validation.html"
308 "You are receiving this email, because additional"
309 " validation plots/scripts (that include you as contact "
310 "person) produced warnings/errors or "
311 "because their warning/error status "
313 "Below is a detailed list of all new/changed offenders:\n\n"
317 "This is a full list of validation plots/scripts that"
318 " produced warnings/errors and include you as contact"
319 "person (sent out once a week).\n\n"
323 "There were problems with the validation of the "
324 "following plots/scripts:\n\n"
327 compared_to_yesterday = plots[plot][
"compared_to_yesterday"]
329 if compared_to_yesterday ==
"unchanged":
333 elif compared_to_yesterday ==
"new":
334 body_plot =
'<b style="color: red;">[NEW]</b><br>'
335 elif compared_to_yesterday ==
"changed":
337 '<b style="color: red;">'
338 "[Warnings/comparison CHANGED]</b><br>"
342 f
'<b style="color: red;">[UNEXPECTED compared_to_yesterday '
343 f
'flag: "{compared_to_yesterday}". Please alert the '
344 f
"validation maintainer.]</b><br>"
348 if plots[plot][
"comparison_result"] ==
"error":
349 errormsg =
"comparison unequal"
350 elif plots[plot][
"comparison_result"] ==
"not_compared":
353 errormsg = plots[plot][
"comparison_result"]
355 body_plot +=
"<b>{plot}</b><br>"
356 body_plot +=
"<b>Package:</b> {package}<br>"
357 body_plot +=
"<b>Rootfile:</b> {rootfile}.root<br>"
358 body_plot +=
"<b>Description:</b> {description}<br>"
359 body_plot +=
"<b>Comparison:</b> {comparison_text}<br>"
361 body_plot += f
"<b>Error:</b> {errormsg}<br>"
362 warnings_str =
", ".join(plots[plot][
"warnings"]).strip()
364 body_plot += f
"<b>Warnings:</b> {warnings_str}<br>"
365 body_plot +=
"<b>Error plot/log file:</b> <a href='{file_url}'>Click me</a><br>"
373 body_plot = body_plot.format(
375 package=plots[plot][
"package"],
376 rootfile=plots[plot][
"rootfile"],
377 description=plots[plot][
"description"],
378 comparison_text=plots[plot][
"comparison_text"],
379 file_url=url.split(
'static')[0]+plots[plot][
"file_url"],
386 f
"You can take a look at the plots/scripts "
387 f
'<a href="{url}">here</a>.'
395 """ Should a full (=non incremental) report be sent?
396 Use case e.g.: Send a full report every Monday.
398 is_monday = date.today().weekday() == 0
400 print(
"Forcing full report because today is Monday.")
406 Send mails to all contacts in self.mail_data_new. If
407 self.mail_data_old is given, a mail is only sent if there are new
409 @param incremental: True/False/None (=automatic). Whether to send a
410 full or incremental report.
412 if incremental
is None:
415 print(
"Sending full ('Monday') report.")
417 print(
"Sending incremental report.")
427 recipients.append(contact)
440 self.
_mail_data_new_mail_data_new[contact], incremental=incremental
444 header =
"Validation: New/changed warnings/errors"
446 header =
"Validation: Monday report"
449 contact.split(
"@")[0], contact, header, body, mood=mood
456 recipients.append(contact)
457 body =
"Your validation plots work fine now!"
459 contact.split(
"@")[0],
461 "Validation confirmation",
466 recipient_string =
"\n".join([f
"* {r}" for r
in recipients])
467 print(f
"Sent mails to the following people: \n{recipient_string}\n")
474 os.path.join(self.
_validator_validator.get_log_folder(),
"mail_data.json"),
477 json.dump(self.
_mail_data_new_mail_data_new, f, sort_keys=
True, indent=4)
Provides functionality to send mails in case of failed scripts / validation plots.
bool _force_full_report()
Dict[str, Dict[str, str]] _create_mail_log_failed_scripts(self)
Looks up all scripts that failed and collects information about them.
_mail_data_old
Yesterday's mail data (generated from comparison_json).
bool _check_if_same(Dict[str, Dict[str, str]] plot_errors)
_validator
Instance of validation.Validation.
def send_all_mails(self, incremental=None)
Dict[str, Dict[str, Dict[str, str]]] _flag_new_failures(Dict[str, Dict[str, Dict[str, str]]] mail_log, Optional[Dict[str, Dict[str, Dict[str, str]]]] old_mail_log)
def __init__(self, validation, include_expert_plots=False)
Initializes an instance of the Mail class from an instance of the Validation class.
_mail_data_new
Current mail data.
Dict[str, Dict[str, Dict[str, str]]] _create_mail_log(self, comparison, include_expert_plots=False)
Takes the entire comparison json file, finds all the plots where comparison failed,...
def _compose_message(plots, incremental=True)
Takes a dict (like in _create_mail_log) and composes a mail body.
def send_mail(name, recipient, subject, text, link=None, link_title=None, mood="normal")
def get_html_plots_tag_comparison_json(output_base_dir, tags)