Belle II Software development
Mails Class Reference

Provides functionality to send mails in case of failed scripts / validation plots. More...

Public Member Functions

def __init__ (self, validation, include_expert_plots=False)
 Initializes an instance of the Mail class from an instance of the Validation class.
 
def send_all_mails (self, incremental=None)
 
def write_log (self)
 

Protected Member Functions

Dict[str, Dict[str, str]] _create_mail_log_failed_scripts (self)
 Looks up all scripts that failed and collects information about them.
 
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, finds info about failed scripts and saves them in the following format:
 

Static Protected Member Functions

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)
 
bool _check_if_same (Dict[str, Dict[str, str]] plot_errors)
 
def _compose_message (plots, incremental=True)
 Takes a dict (like in _create_mail_log) and composes a mail body.
 
bool _force_full_report ()
 

Protected Attributes

 _validator
 Instance of validation.Validation.
 
 _mail_data_old
 Yesterday's mail data (generated from comparison_json).
 
 _mail_data_new
 Current mail data.
 

Detailed Description

Provides functionality to send mails in case of failed scripts / validation plots.

The mail data is built upon instantiation, the send_mails method sends the actual mails.

Definition at line 45 of file mail_log.py.

Constructor & Destructor Documentation

◆ __init__()

def __init__ (   self,
  validation,
  include_expert_plots = False 
)

Initializes an instance of the Mail class from an instance of the Validation class.

Assumes that a comparison json file exists, reads it and parses it to extract information about failed plots. This information, together with information about failed scripts, gets stored in self.mail_data_new. If there is mail_data.json inside the log folder, its contents get stored in self.mail_data_old for later comparison.

Parameters
validationvalidation.Validation instance
include_expert_plotsShould expert plots be included?

Definition at line 54 of file mail_log.py.

54 def __init__(self, validation, include_expert_plots=False):
55 """!
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
62 later comparison.
63
64 @param validation: validation.Validation instance
65 @param include_expert_plots: Should expert plots be included?
66 """
67
68 # Cannot import Validation to type hint because this would give us a
69 # circular import.
70
71 self._validator = validation
72
73 # read contents from comparison.json
74 work_folder = self._validator.work_folder
75
76 # choose latest nighly if available, else 'current'
77 revision = get_latest_nightly(work_folder)
78 self._validator.set_tag(revision)
79
81 work_folder, ['reference', revision]
82 )
83 with open(comparison_json_file) as f:
84 comparison_json = json.load(f)
85
86 # yesterday's mail data
87 old_mail_data_path = os.path.join(
88 self._validator.get_log_folder(), "mail_data.json"
89 )
90
92 self._mail_data_old: Optional[dict] = None
93 try:
94 with open(old_mail_data_path) as f:
95 self._mail_data_old = json.load(f)
96 except FileNotFoundError:
97 print(
98 f"Could not find old mail_data.json at {old_mail_data_path}.",
99 file=sys.stderr,
100 )
101
102
104 self._mail_data_new = self._create_mail_log(
105 comparison_json, include_expert_plots=include_expert_plots
106 )
107
def get_html_plots_tag_comparison_json(output_base_dir, tags)

Member Function Documentation

◆ _check_if_same()

bool _check_if_same ( Dict[str, Dict[str, str]]  plot_errors)
staticprotected
@param plot_errors: ``_create_mail_log[contact]``.
@return True, if there is at least one new/changed plot status

Definition at line 287 of file mail_log.py.

287 def _check_if_same(plot_errors: Dict[str, Dict[str, str]]) -> bool:
288 """
289 @param plot_errors: ``_create_mail_log[contact]``.
290 @return True, if there is at least one new/changed plot status
291 """
292 for plot in plot_errors:
293 if plot_errors[plot]["compared_to_yesterday"] != "unchanged":
294 return False
295 return True
296

◆ _compose_message()

def _compose_message (   plots,
  incremental = True 
)
staticprotected

Takes a dict (like in _create_mail_log) and composes a mail body.

Parameters
plots
incremental(bool): Is this an incremental report or a full ("Monday") report?

Definition at line 298 of file mail_log.py.

298 def _compose_message(plots, incremental=True):
299 """!
300 Takes a dict (like in _create_mail_log) and composes a mail body
301 @param plots
302 @param incremental (bool): Is this an incremental report or a full
303 ("Monday") report?
304 """
305
306 # link to validation page
307 url = "https://validation.belle2.org/static/validation.html"
308 # url = "http://localhost:8000/static/validation.html"
309
310 if incremental:
311 body = (
312 "You are receiving this email, because additional "
313 "validation plots/scripts (that include you as contact "
314 "person) produced warnings/errors or "
315 "because their warning/error status "
316 "changed. \n"
317 "Below is a detailed list of all new/changed offenders:\n\n"
318 )
319 else:
320 body = (
321 "This is a full list of validation plots/scripts that "
322 "produced warnings/errors and include you as contact "
323 "person (sent out once a week).\n\n"
324 )
325
326 body += (
327 "There were problems with the validation of the "
328 "following plots/scripts:\n\n"
329 )
330 for plot in plots:
331 compared_to_yesterday = plots[plot]["compared_to_yesterday"]
332 body_plot = ""
333 if compared_to_yesterday == "unchanged":
334 if incremental:
335 # Do not include.
336 continue
337 elif compared_to_yesterday == "new":
338 body_plot = '<b style="color: red;">[NEW]</b><br>'
339 elif compared_to_yesterday == "changed":
340 body_plot = (
341 '<b style="color: red;">'
342 "[Warnings/comparison CHANGED]</b><br>"
343 )
344 else:
345 body_plot = (
346 f'<b style="color: red;">[UNEXPECTED compared_to_yesterday '
347 f'flag: "{compared_to_yesterday}". Please alert the '
348 f"validation maintainer.]</b><br>"
349 )
350
351 # compose descriptive error message
352 if plots[plot]["comparison_result"] == "error":
353 errormsg = "comparison unequal"
354 elif plots[plot]["comparison_result"] == "not_compared":
355 errormsg = ""
356 else:
357 errormsg = plots[plot]["comparison_result"]
358
359 body_plot += "<b>{plot}</b><br>"
360 body_plot += "<b>Package:</b> {package}<br>"
361 body_plot += "<b>Rootfile:</b> {rootfile}.root<br>"
362 body_plot += "<b>Description:</b> {description}<br>"
363 body_plot += "<b>Comparison:</b> {comparison_text}<br>"
364 if errormsg:
365 body_plot += f"<b>Error:</b> {errormsg}<br>"
366 warnings_str = ", ".join(plots[plot]["warnings"]).strip()
367 if warnings_str:
368 body_plot += f"<b>Warnings:</b> {warnings_str}<br>"
369 body_plot += "<b>Error plot/log file:</b> <a href='{file_url}'>Click me</a><br>"
370 # URLs are currently not working.
371 # if plots[plot]["rootfile"] != "--":
372 # body_plot += '<a href="{url}#{package}-{rootfile}">' \
373 # 'Click me for details</a>'
374 body_plot += "\n\n"
375
376 # Fill in fields
377 body_plot = body_plot.format(
378 plot=plot,
379 package=plots[plot]["package"],
380 rootfile=plots[plot]["rootfile"],
381 description=plots[plot]["description"],
382 comparison_text=plots[plot]["comparison_text"],
383 file_url=url.split('static')[0]+plots[plot]["file_url"],
384 url=url,
385 )
386
387 body += body_plot
388
389 body += (
390 f"You can take a look at the plots/scripts "
391 f'<a href="{url}">here</a>.'
392 )
393
394 return body
395

◆ _create_mail_log()

Dict[str, Dict[str, Dict[str, str]]] _create_mail_log (   self,
  comparison,
  include_expert_plots = False 
)
protected

Takes the entire comparison json file, finds all the plots where comparison failed, finds info about failed scripts and saves them in the following format:

{ "email@address.test" : { "title1": { "package": str, "description": str, "rootfile": str, "comparison_text": str, "description": str, "comparison_result": str, "warnings": str, "file_url": str }, "title2": {...} }, "mail@...": {...} }

The top level ordering is the email address of the contact to make sure every user gets only one mail with everything in it.

Definition at line 165 of file mail_log.py.

167 ) -> Dict[str, Dict[str, Dict[str, str]]]:
168 """!
169 Takes the entire comparison json file, finds all the plots where
170 comparison failed, finds info about failed scripts and saves them in
171 the following format:
172
173 {
174 "email@address.test" : {
175 "title1": {
176 "package": str,
177 "description": str,
178 "rootfile": str,
179 "comparison_text": str,
180 "description": str,
181 "comparison_result": str,
182 "warnings": str,
183 "file_url": str
184 },
185 "title2": {...}
186 },
187 "mail@...": {...}
188 }
189
190 The top level ordering is the email address of the contact to make
191 sure every user gets only one mail with everything in it.
192 """
193
194 mail_log = {}
195 # search for plots where comparison resulted in an error
196 for package in comparison["packages"]:
197 for plotfile in package["plotfiles"]:
198 for plot in plotfile["plots"]:
199 if not include_expert_plots and plot["is_expert"]:
200 continue
201 skip = True
202 if plot["comparison_result"] in ["error"]:
203 skip = False
204 if set(plot["warnings"]) - {"No reference object"}:
205 skip = False
206 if skip:
207 continue
208 # save all the information that's needed for
209 # an informative email
210 error_data = {
211 "package": plotfile["package"],
212 "rootfile": plotfile["rootfile"],
213 "comparison_text": plot["comparison_text"],
214 "description": plot["description"],
215 "comparison_result": plot["comparison_result"],
216 "warnings": sorted(
217 list(
218 set(plot["warnings"]) - {"No reference object"}
219 )
220 ),
221 "file_url": os.path.join(
222 plot['plot_path'],
223 plot['png_filename']
224 ),
225 }
226 # every contact gets an email
227 for contact in parse_mail_address(plot["contact"]):
228 # check if this contact already gets mail
229 if contact not in mail_log:
230 # create new key for this contact
231 mail_log[contact] = {}
232 mail_log[contact][plot["title"]] = error_data
233
234 # now get failed scripts and merge information into mail_log
235 failed_scripts = self._create_mail_log_failed_scripts()
236 for contact in failed_scripts:
237 # if this user is not yet represented in mail_log, create new key
238 if contact not in mail_log:
239 mail_log[contact] = failed_scripts[contact]
240 # if user already is in mail_log, add the failed scripts
241 else:
242 for script in failed_scripts[contact]:
243 mail_log[contact][script] = failed_scripts[contact][script]
244
245 return self._flag_new_failures(mail_log, self._mail_data_old)
246

◆ _create_mail_log_failed_scripts()

Dict[str, Dict[str, str]] _create_mail_log_failed_scripts (   self)
protected

Looks up all scripts that failed and collects information about them.

See :meth:_create_mail_log for the structure of the resulting dictionary.

Definition at line 108 of file mail_log.py.

108 def _create_mail_log_failed_scripts(self) -> Dict[str, Dict[str, str]]:
109 """!
110 Looks up all scripts that failed and collects information about them.
111 See :meth:`_create_mail_log` for the structure of the resulting
112 dictionary.
113 """
114
115 # get failed scripts
116 with open(
117 os.path.join(
118 self._validator.get_log_folder(), "list_of_failed_scripts.log"
119 )
120 ) as f:
121 failed_scripts = f.read().splitlines()
122
123 # collect information about failed scripts
124 mail_log = {}
125 for failed_script in failed_scripts:
126
127 # get_script_by_name works with _ only ...
128 failed_script = Script.sanitize_file_name(failed_script)
129 script = self._validator.get_script_by_name(failed_script)
130 if script is None:
131 continue
132
133 script.load_header()
134
135 failed_script = {}
136 failed_script["warnings"] = []
137 # give failed_script the same format as error_data in method
138 # create_mail_log
139 failed_script["package"] = script.package
140 failed_script["rootfile"] = ", ".join(script.input_files)
141 failed_script["comparison_text"] = " -- "
142 failed_script["description"] = script.description
143 # this is called comparison_result but it is handled as error
144 # type when composing mail
145 failed_script["comparison_result"] = "script failed to execute"
146 # feat. adding links to plot/log files
147 failed_script["file_url"] = os.path.join(
148 'results',
149 self._validator.tag,
150 script.package,
151 script.name_not_sanitized
152 ) + ".log"
153 # add contact of failed script to mail_log
154 try:
155 for contact in parse_mail_address(script.contact):
156 if contact not in mail_log:
157 mail_log[contact] = {}
158 mail_log[contact][script.name] = failed_script
159 except (KeyError, TypeError):
160 # this means no contact is given
161 continue
162
163 return mail_log
164

◆ _flag_new_failures()

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 
)
staticprotected
 Add a new field 'compared_to_yesterday' which takes one of the
values 'unchanged' (same revision comparison result as in yesterday's
mail log, 'new' (new warning/failure), 'changed' (comparison result
changed). 

Definition at line 248 of file mail_log.py.

251 ) -> Dict[str, Dict[str, Dict[str, str]]]:
252 """ Add a new field 'compared_to_yesterday' which takes one of the
253 values 'unchanged' (same revision comparison result as in yesterday's
254 mail log, 'new' (new warning/failure), 'changed' (comparison result
255 changed). """
256 mail_log_flagged = copy.deepcopy(mail_log)
257 for contact in mail_log:
258 for plot in mail_log[contact]:
259 if old_mail_log is None:
260 mail_log_flagged[contact][plot][
261 "compared_to_yesterday"
262 ] = "n/a"
263 elif contact not in old_mail_log:
264 mail_log_flagged[contact][plot][
265 "compared_to_yesterday"
266 ] = "new"
267 elif plot not in old_mail_log[contact]:
268 mail_log_flagged[contact][plot][
269 "compared_to_yesterday"
270 ] = "new"
271 elif (
272 mail_log[contact][plot]["comparison_result"]
273 != old_mail_log[contact][plot]["comparison_result"]
274 or mail_log[contact][plot]["warnings"]
275 != old_mail_log[contact][plot]["warnings"]
276 ):
277 mail_log_flagged[contact][plot][
278 "compared_to_yesterday"
279 ] = "changed"
280 else:
281 mail_log_flagged[contact][plot][
282 "compared_to_yesterday"
283 ] = "unchanged"
284 return mail_log_flagged
285

◆ _force_full_report()

bool _force_full_report ( )
staticprotected
 Should a full (=non incremental) report be sent?
Use case e.g.: Send a full report every Monday.

Definition at line 398 of file mail_log.py.

398 def _force_full_report() -> bool:
399 """ Should a full (=non incremental) report be sent?
400 Use case e.g.: Send a full report every Monday.
401 """
402 is_monday = date.today().weekday() == 0
403 if is_monday:
404 print("Forcing full report because today is Monday.")
405 return True
406 return False
407

◆ send_all_mails()

def send_all_mails (   self,
  incremental = None 
)
Send mails to all contacts in self.mail_data_new. If
self.mail_data_old is given, a mail is only sent if there are new
failed plots
@param incremental: True/False/None (=automatic). Whether to send a
    full or incremental report.

Definition at line 408 of file mail_log.py.

408 def send_all_mails(self, incremental=None):
409 """
410 Send mails to all contacts in self.mail_data_new. If
411 self.mail_data_old is given, a mail is only sent if there are new
412 failed plots
413 @param incremental: True/False/None (=automatic). Whether to send a
414 full or incremental report.
415 """
416 if incremental is None:
417 incremental = not self._force_full_report()
418 if not incremental:
419 print("Sending full ('Monday') report.")
420 else:
421 print("Sending incremental report.")
422
423 recipients = []
424 for contact in self._mail_data_new:
425 # if the errors are the same as yesterday, don't send a new mail
426 if incremental and self._check_if_same(
427 self._mail_data_new[contact]
428 ):
429 # don't send mail
430 continue
431 recipients.append(contact)
432
433 # set the mood of the b2bot
434 if len(self._mail_data_new[contact]) < 4:
435 mood = "meh"
436 elif len(self._mail_data_new[contact]) < 7:
437 mood = "angry"
438 elif len(self._mail_data_new[contact]) < 10:
439 mood = "livid"
440 else:
441 mood = "dead"
442
443 body = self._compose_message(
444 self._mail_data_new[contact], incremental=incremental
445 )
446
447 if incremental:
448 header = "Validation: New/changed warnings/errors"
449 else:
450 header = "Validation: Monday report"
451
453 contact.split("@")[0], contact, header, body, mood=mood
454 )
455
456 # send a happy mail to folks whose failed plots work now
457 if self._mail_data_old:
458 for contact in self._mail_data_old:
459 if contact not in self._mail_data_new:
460 recipients.append(contact)
461 body = "Your validation plots work fine now!"
463 contact.split("@")[0],
464 contact,
465 "Validation confirmation",
466 body,
467 mood="happy",
468 )
469
470 recipient_string = "\n".join([f"* {r}" for r in recipients])
471 print(f"Sent mails to the following people: \n{recipient_string}\n")
472
def send_mail(name, recipient, subject, text, link=None, link_title=None, mood="normal")
Definition: mail_utils.py:78

◆ write_log()

def write_log (   self)
Dump mail json.

Definition at line 473 of file mail_log.py.

473 def write_log(self):
474 """
475 Dump mail json.
476 """
477 with open(
478 os.path.join(self._validator.get_log_folder(), "mail_data.json"),
479 "w",
480 ) as f:
481 json.dump(self._mail_data_new, f, sort_keys=True, indent=4)

Member Data Documentation

◆ _mail_data_new

_mail_data_new
protected

Current mail data.

Will be filled on instantiation. Check docstring of _create_mail_log for exact format

Definition at line 104 of file mail_log.py.

◆ _mail_data_old

_mail_data_old
protected

Yesterday's mail data (generated from comparison_json).

Check docstring of _create_mail_log for exact format

Definition at line 95 of file mail_log.py.

◆ _validator

_validator
protected

Instance of validation.Validation.

Definition at line 71 of file mail_log.py.


The documentation for this class was generated from the following file: