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"
144 for contact
in parse_mail_address(script.contact):
145 if contact
not in mail_log:
146 mail_log[contact] = {}
147 mail_log[contact][script.name] = failed_script
148 except (KeyError, TypeError):
155 self, comparison, include_expert_plots=False
156 ) -> Dict[str, Dict[str, Dict[str, str]]]:
158 Takes the entire comparison json file, finds all the plots where
159 comparison failed, finds info about failed scripts and saves them in
160 the following format:
163 "email@address.test" : {
168 "comparison_text": str,
170 "comparison_result": str,
178 The top level ordering is the email address of the contact to make
179 sure every user gets only one mail with everything in it.
184 for package
in comparison[
"packages"]:
185 for plotfile
in package[
"plotfiles"]:
186 for plot
in plotfile[
"plots"]:
187 if not include_expert_plots
and plot[
"is_expert"]:
190 if plot[
"comparison_result"]
in [
"error"]:
192 if set(plot[
"warnings"]) - {
"No reference object"}:
199 "package": plotfile[
"package"],
200 "rootfile": plotfile[
"rootfile"],
201 "comparison_text": plot[
"comparison_text"],
202 "description": plot[
"description"],
203 "comparison_result": plot[
"comparison_result"],
206 set(plot[
"warnings"]) - {
"No reference object"}
211 for contact
in parse_mail_address(plot[
"contact"]):
213 if contact
not in mail_log:
215 mail_log[contact] = {}
216 mail_log[contact][plot[
"title"]] = error_data
220 for contact
in failed_scripts:
222 if contact
not in mail_log:
223 mail_log[contact] = failed_scripts[contact]
226 for script
in failed_scripts[contact]:
227 mail_log[contact][script] = failed_scripts[contact][script]
233 mail_log: Dict[str, Dict[str, Dict[str, str]]],
234 old_mail_log: Optional[Dict[str, Dict[str, Dict[str, str]]]],
235 ) -> Dict[str, Dict[str, Dict[str, str]]]:
236 """ Add a new field 'compared_to_yesterday' which takes one of the
237 values 'unchanged' (same revision comparison result as in yesterday's
238 mail log, 'new' (new warning/failure), 'changed' (comparison result
240 mail_log_flagged = copy.deepcopy(mail_log)
241 for contact
in mail_log:
242 for plot
in mail_log[contact]:
243 if old_mail_log
is None:
244 mail_log_flagged[contact][plot][
245 "compared_to_yesterday"
247 elif contact
not in old_mail_log:
248 mail_log_flagged[contact][plot][
249 "compared_to_yesterday"
251 elif plot
not in old_mail_log[contact]:
252 mail_log_flagged[contact][plot][
253 "compared_to_yesterday"
256 mail_log[contact][plot][
"comparison_result"]
257 != old_mail_log[contact][plot][
"comparison_result"]
258 or mail_log[contact][plot][
"warnings"]
259 != old_mail_log[contact][plot][
"warnings"]
261 mail_log_flagged[contact][plot][
262 "compared_to_yesterday"
265 mail_log_flagged[contact][plot][
266 "compared_to_yesterday"
268 return mail_log_flagged
273 @param plot_errors: ``_create_mail_log[contact]``.
274 @return True, if there is at least one new/changed plot status
276 for plot
in plot_errors:
277 if plot_errors[plot][
"compared_to_yesterday"] !=
"unchanged":
284 Takes a dict (like in _create_mail_log) and composes a mail body
286 @param incremental (bool): Is this an incremental report or a full
291 url =
"https://b2-master.belle2.org/validation/static/validation.html"
296 "You are receiving this email, because additional"
297 " validation plots/scripts (that include you as contact "
298 "person) produced warnings/errors or "
299 "because their warning/error status "
301 "Below is a detailed list of all new/changed offenders:\n\n"
305 "This is a full list of validation plots/scripts that"
306 " produced warnings/errors and include you as contact"
307 "person (sent out once a week).\n\n"
311 "There were problems with the validation of the "
312 "following plots/scripts:\n\n"
315 compared_to_yesterday = plots[plot][
"compared_to_yesterday"]
317 if compared_to_yesterday ==
"unchanged":
321 elif compared_to_yesterday ==
"new":
322 body_plot =
'<b style="color: red;">[NEW]</b><br>'
323 elif compared_to_yesterday ==
"changed":
325 '<b style="color: red;">'
326 "[Warnings/comparison CHANGED]</b><br>"
330 f
'<b style="color: red;">[UNEXPECTED compared_to_yesterday '
331 f
'flag: "{compared_to_yesterday}". Please alert the '
332 f
"validation maintainer.]</b><br>"
336 if plots[plot][
"comparison_result"] ==
"error":
337 errormsg =
"comparison unequal"
338 elif plots[plot][
"comparison_result"] ==
"not_compared":
341 errormsg = plots[plot][
"comparison_result"]
343 body_plot +=
"<b>{plot}</b><br>"
344 body_plot +=
"<b>Package:</b> {package}<br>"
345 body_plot +=
"<b>Rootfile:</b> {rootfile}.root<br>"
346 body_plot +=
"<b>Description:</b> {description}<br>"
347 body_plot +=
"<b>Comparison:</b> {comparison_text}<br>"
349 body_plot += f
"<b>Error:</b> {errormsg}<br>"
350 warnings_str =
", ".join(plots[plot][
"warnings"]).strip()
352 body_plot += f
"<b>Warnings:</b> {warnings_str}<br>"
360 body_plot = body_plot.format(
362 package=plots[plot][
"package"],
363 rootfile=plots[plot][
"rootfile"],
364 description=plots[plot][
"description"],
365 comparison_text=plots[plot][
"comparison_text"],
372 f
"You can take a look at the plots/scripts "
373 f
'<a href="{url}">here</a>.'
381 """ Should a full (=non incremental) report be sent?
382 Use case e.g.: Send a full report every Monday.
384 is_monday = date.today().weekday() == 0
386 print(
"Forcing full report because today is Monday.")
392 Send mails to all contacts in self.mail_data_new. If
393 self.mail_data_old is given, a mail is only sent if there are new
395 @param incremental: True/False/None (=automatic). Whether to send a
396 full or incremental report.
398 if incremental
is None:
401 print(
"Sending full ('Monday') report.")
403 print(
"Sending incremental report.")
413 recipients.append(contact)
426 self.
_mail_data_new_mail_data_new[contact], incremental=incremental
430 header =
"Validation: New/changed warnings/errors"
432 header =
"Validation: Monday report"
435 contact.split(
"@")[0], contact, header, body, mood=mood
442 recipients.append(contact)
443 body =
"Your validation plots work fine now!"
445 contact.split(
"@")[0],
447 "Validation confirmation",
452 print(f
"Sent mails to the following people: {', '.join(recipients)}")
459 os.path.join(self.
_validator_validator.get_log_folder(),
"mail_data.json"),
462 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)