6 from datetime
import date
11 from typing
import Dict, Union, List, Optional
15 from validationfunctions
import available_revisions
20 def parse_mail_address(obj: Union[str, List[str]]) -> List[str]:
22 Take a string or list and return list of email addresses that appear in it
24 if isinstance(obj, str):
25 return re.findall(
r'[\w.-]+@[\w.-]+', obj)
26 elif isinstance(obj, list):
28 re.search(
r'[\w.-]+@[\w.-]+', c).group()
30 if re.search(
r'[\w.-]+@[\w.-]+', c)
is not None
33 raise TypeError(
"must be string or list of strings")
39 Provides functionality to send mails in case of failed scripts / validation
41 The mail data is built upon instantiation, the `send_mails` method
42 sends the actual mails.
45 @var _validator: Instance of validation.Validation
46 @var _mail_data_old: Yesterday's mail data (generated from comparison_json)
47 @var _mail_data_new: Current mail data. Will be filled on instantiation.
50 def __init__(self, validation, include_expert_plots=False):
52 Initializes an instance of the Mail class from an instance of the
53 Validation class. Assumes that a comparison json file exists,
54 reads it and parses it to extract information about failed plots.
55 This information, together with information about failed scripts,
56 gets stored in self.mail_data_new. If there is mail_data.json inside
57 the log folder, its contents get stored in self.mail_data_old for
60 @param validation: validation.Validation instance
61 @param include_expert_plots: Should expert plots be included?
64 self._validator = validation
67 work_folder = self._validator.work_folder
68 revisions = [
'reference'] + available_revisions(work_folder)
69 comparison_json_file = \
74 with open(comparison_json_file)
as f:
75 comparison_json = json.load(f)
78 old_mail_data_path = os.path.join(
79 self._validator.get_log_folder(),
"mail_data.json"
82 with open(old_mail_data_path)
as f:
83 self._mail_data_old = json.load(f)
84 except FileNotFoundError:
86 f
"Could not find old mail_data.json at {old_mail_data_path}.",
89 self._mail_data_old =
None
92 self._mail_data_new = self._create_mail_log(
93 comparison_json, include_expert_plots=include_expert_plots
96 def _create_mail_log_failed_scripts(self) -> Dict[str, Dict[str, str]]:
98 Looks up all scripts that failed and collects information about them.
99 See :meth:`_create_mail_log` for the structure of the resulting
104 with open(os.path.join(self._validator.get_log_folder(),
105 "list_of_failed_scripts.log"))
as f:
106 failed_scripts = f.read().splitlines()
110 for failed_script
in failed_scripts:
113 for suffix
in [
"py",
"C"]:
114 failed_script = failed_script.replace(
"." + suffix,
116 if self._validator.get_script_by_name(failed_script):
117 script = self._validator.get_script_by_name(failed_script)
125 failed_script[
"warnings"] = []
128 failed_script[
"package"] = script.package
130 failed_script[
"rootfile"] =
", ".join(script.header[
"input"])
131 except (KeyError, TypeError):
133 failed_script[
"rootfile"] =
" -- "
134 failed_script[
"comparison_text"] =
" -- "
136 failed_script[
"description"] = script.header[
"description"]
137 except (KeyError, TypeError):
138 failed_script[
"description"] =
" -- "
141 failed_script[
"comparison_result"] =
"script failed to execute"
144 for contact
in parse_mail_address(script.header[
"contact"]):
145 if contact
not in mail_log:
146 mail_log[contact] = {}
147 mail_log[contact][script.name] = failed_script
148 except (KeyError, TypeError):
154 def _create_mail_log(self, comparison, include_expert_plots=False) \
155 -> Dict[str, Dict[str, Dict[str, str]]]:
157 Takes the entire comparison json file, finds all the plots where
158 comparison failed, finds info about failed scripts and saves them in
159 the following format:
162 "email@address.test" : {
167 "comparison_text": str,
169 "comparison_result": str,
177 The top level ordering is the email address of the contact to make
178 sure every user gets only one mail with everything in it.
183 for package
in comparison[
"packages"]:
184 for plotfile
in package[
"plotfiles"]:
185 for plot
in plotfile[
"plots"]:
186 if not include_expert_plots
and plot[
"is_expert"]:
189 if plot[
"comparison_result"]
in [
"error"]:
191 if set(plot[
"warnings"]) - {
"No reference object"}:
198 "package": plotfile[
"package"],
199 "rootfile": plotfile[
"rootfile"],
200 "comparison_text": plot[
"comparison_text"],
201 "description": plot[
"description"],
202 "comparison_result": plot[
"comparison_result"],
203 "warnings": sorted(list(
204 set(plot[
"warnings"]) - {
"No reference object"}
208 for contact
in parse_mail_address(plot[
"contact"]):
210 if contact
not in mail_log:
212 mail_log[contact] = {}
213 mail_log[contact][plot[
"title"]] = error_data
216 failed_scripts = self._create_mail_log_failed_scripts()
217 for contact
in failed_scripts:
219 if contact
not in mail_log:
220 mail_log[contact] = failed_scripts[contact]
223 for script
in failed_scripts[contact]:
224 mail_log[contact][script] = failed_scripts[contact][script]
226 return self._flag_new_failures(mail_log, self._mail_data_old)
229 def _flag_new_failures(
230 mail_log: Dict[str, Dict[str, Dict[str, str]]],
231 old_mail_log: Optional[Dict[str, Dict[str, Dict[str, str]]]]) \
232 -> Dict[str, Dict[str, Dict[str, str]]]:
233 """ Add a new field 'compared_to_yesterday' which takes one of the
234 values 'unchanged' (same revision comparison result as in yesterday's
235 mail log, 'new' (new warning/failure), 'changed' (comparison result
237 mail_log_flagged = copy.deepcopy(mail_log)
238 for contact
in mail_log:
239 for plot
in mail_log[contact]:
240 if old_mail_log
is None:
241 mail_log_flagged[contact][plot][
"compared_to_yesterday"] = \
243 elif contact
not in old_mail_log:
244 mail_log_flagged[contact][plot][
"compared_to_yesterday"] = \
246 elif plot
not in old_mail_log[contact]:
247 mail_log_flagged[contact][plot][
"compared_to_yesterday"] = \
249 elif mail_log[contact][plot][
"comparison_result"] != \
250 old_mail_log[contact][plot][
"comparison_result"]
or \
251 mail_log[contact][plot][
"warnings"] != \
252 old_mail_log[contact][plot][
"warnings"]:
253 mail_log_flagged[contact][plot][
"compared_to_yesterday"] = \
256 mail_log_flagged[contact][plot][
"compared_to_yesterday"] = \
258 return mail_log_flagged
261 def _check_if_same(plot_errors: Dict[str, Dict[str, str]]) -> bool:
263 @param plot_errors: ``_create_mail_log[contact]``.
264 @return True, if there is at least one new/changed plot status
266 for plot
in plot_errors:
267 if plot_errors[plot][
"compared_to_yesterday"] !=
"unchanged":
272 def _compose_message(plots, incremental=True):
274 Takes a dict (like in _create_mail_log) and composes a mail body
276 @param incremental (bool): Is this an incremental report or a full
281 url =
"https://b2-master.belle2.org/validation/static/validation.html"
285 body =
"You are receiving this email, because additional" \
286 " validation plots/scripts (that include you as contact " \
287 "person) produced warnings/errors or " \
288 "because their warning/error status " \
290 "Below is a detailed list of all new/changed offenders:\n\n"
292 body =
"This is a full list of validation plots/scripts that" \
293 " produced warnings/errors and include you as contact" \
294 "person (sent out once a week).\n\n"
296 body +=
"There were problems with the validation of the " \
297 "following plots/scripts:\n\n"
299 compared_to_yesterday = plots[plot][
"compared_to_yesterday"]
301 if compared_to_yesterday ==
"unchanged":
305 elif compared_to_yesterday ==
"new":
306 body_plot =
'<b style="color: red;">[NEW]</b><br>'
307 elif compared_to_yesterday ==
"changed":
308 body_plot =
'<b style="color: red;">' \
309 '[Warnings/comparison CHANGED]</b><br>'
312 f
'<b style="color: red;">[UNEXPECTED compared_to_yesterday ' \
313 f
'flag: "{compared_to_yesterday}". Please alert the ' \
314 f
'validation maintainer.]</b><br>'
317 if plots[plot][
"comparison_result"] ==
"error":
318 errormsg =
"comparison unequal"
319 elif plots[plot][
"comparison_result"] ==
"not_compared":
322 errormsg = plots[plot][
"comparison_result"]
324 body_plot +=
"<b>{plot}</b><br>"
325 body_plot +=
"<b>Package:</b> {package}<br>"
326 body_plot +=
"<b>Rootfile:</b> {rootfile}.root<br>"
327 body_plot +=
"<b>Description:</b> {description}<br>"
328 body_plot +=
"<b>Comparison:</b> {comparison_text}<br>"
330 body_plot += f
"<b>Error:</b> {errormsg}<br>"
331 warnings_str =
", ".join(plots[plot][
"warnings"]).strip()
333 body_plot += f
"<b>Warnings:</b> {warnings_str}<br>"
341 body_plot = body_plot.format(
343 package=plots[plot][
"package"],
344 rootfile=plots[plot][
"rootfile"],
345 description=plots[plot][
"description"],
346 comparison_text=plots[plot][
"comparison_text"],
352 body += f
"You can take a look at the plots/scripts " \
353 f
'<a href="{url}">here</a>.'
359 def _force_full_report() -> bool:
360 """ Should a full (=non incremental) report be sent?
361 Use case e.g.: Send a full report every Monday.
363 is_monday = date.today().weekday() == 0
365 print(
"Forcing full report because today is Monday.")
369 def send_all_mails(self, incremental=None):
371 Send mails to all contacts in self.mail_data_new. If
372 self.mail_data_old is given, a mail is only sent if there are new
374 @param incremental: True/False/None (=automatic). Whether to send a
375 full or incremental report.
377 if incremental
is None:
378 incremental =
not self._force_full_report()
380 print(
"Sending full ('Monday') report.")
382 print(
"Sending incremental report.")
385 for contact
in self._mail_data_new:
387 if incremental
and self._check_if_same(self._mail_data_new[contact]):
390 recipients.append(contact)
393 if len(self._mail_data_new[contact]) < 4:
395 elif len(self._mail_data_new[contact]) < 7:
397 elif len(self._mail_data_new[contact]) < 10:
402 body = self._compose_message(
403 self._mail_data_new[contact],
404 incremental=incremental
408 header =
"Validation: New/changed warnings/errors"
410 header =
"Validation: Monday report"
413 contact.split(
'@')[0],
421 if self._mail_data_old:
422 for contact
in self._mail_data_old:
423 if contact
not in self._mail_data_new:
424 recipients.append(contact)
425 body =
"Your validation plots work fine now!"
427 contact.split(
'@')[0],
429 "Validation confirmation",
434 print(f
"Sent mails to the following people: {', '.join(recipients)}")
440 with open(os.path.join(self._validator.get_log_folder(),
441 "mail_data.json"),
"w")
as f:
442 json.dump(self._mail_data_new, f, sort_keys=
True, indent=4)