Skip to content

Commit 5a68825

Browse files
authored
Merge branch 'master' into sharding_fix
2 parents 1707017 + 93d9a17 commit 5a68825

21 files changed

+253
-83
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
0.5.2 (2023-??)
2+
------------------
3+
* Feature: Allow configuring error email addresses and email subject via UI.
4+
* Bugfix: . and .. should now be allowed to be used when specifying the templates directory.
5+
* Bugfix: corrected cron schedule incorrectly shifting back one day upon save.
6+
17
0.5.1 (2023-02-22)
28
------------------
39

notebooker/constants.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ class NotebookResultBase(object):
8080
status = attr.ib(default=JobStatus.ERROR)
8181
overrides = attr.ib(default=attr.Factory(dict))
8282
mailto = attr.ib(default="")
83+
error_mailto = attr.ib(default="")
8384
generate_pdf_output = attr.ib(default=True)
8485
hide_code = attr.ib(default=False)
8586
stdout = attr.ib(default=attr.Factory(list))
8687
scheduler_job_id = attr.ib(default=None)
8788
mailfrom = attr.ib(default=None)
89+
email_subject = attr.ib(default=None)
8890
is_slideshow = attr.ib(default=False)
8991

9092
def saveable_output(self):
@@ -100,6 +102,7 @@ class NotebookResultPending(NotebookResultBase):
100102
report_title = attr.ib(default="")
101103
overrides = attr.ib(default=attr.Factory(dict))
102104
mailto = attr.ib(default="")
105+
error_mailto = attr.ib(default="")
103106
generate_pdf_output = attr.ib(default=True)
104107
hide_code = attr.ib(default=False)
105108
scheduler_job_id = attr.ib(default=None)
@@ -115,15 +118,13 @@ class NotebookResultError(NotebookResultBase):
115118
report_title = attr.ib(default="")
116119
overrides = attr.ib(default=attr.Factory(dict))
117120
mailto = attr.ib(default="")
121+
error_mailto = attr.ib(default="")
118122
generate_pdf_output = attr.ib(default=True)
119123
hide_code = attr.ib(default=False)
120124
scheduler_job_id = attr.ib(default=None)
121125
mailfrom = attr.ib(default=None)
122126
is_slideshow = attr.ib(default=False)
123-
124-
@property
125-
def email_subject(self):
126-
return ""
127+
email_subject = attr.ib(default=None)
127128

128129
@property
129130
def raw_html(self):
@@ -155,6 +156,7 @@ class NotebookResultComplete(NotebookResultBase):
155156
report_title = attr.ib(default="")
156157
overrides = attr.ib(default=attr.Factory(dict))
157158
mailto = attr.ib(default="")
159+
error_mailto = attr.ib(default="")
158160
email_subject = attr.ib(default="")
159161
generate_pdf_output = attr.ib(default=True)
160162
hide_code = attr.ib(default=False)
@@ -185,6 +187,7 @@ def saveable_output(self):
185187
"job_start_time": self.job_start_time,
186188
"job_finish_time": self.job_finish_time,
187189
"mailto": self.mailto,
190+
"error_mailto": self.error_mailto,
188191
"email_subject": self.email_subject,
189192
"overrides": self.overrides,
190193
"generate_pdf_output": self.generate_pdf_output,
@@ -200,9 +203,9 @@ def __repr__(self):
200203
return (
201204
"NotebookResultComplete(job_id={job_id}, status={status}, report_name={report_name}, "
202205
"job_start_time={job_start_time}, job_finish_time={job_finish_time}, update_time={update_time}, "
203-
"report_title={report_title}, overrides={overrides}, mailto={mailto}, mailfrom={mailfrom}"
204-
"email_subject={email_subject}, generate_pdf_output={generate_pdf_output}, hide_code={hide_code}, "
205-
"scheduler_job_id={scheduler_job_id}, is_slideshow={is_slideshow})".format(
206+
"report_title={report_title}, overrides={overrides}, mailto={mailto}, error_mailto={error_mailto}, "
207+
"mailfrom={mailfrom}, email_subject={email_subject}, generate_pdf_output={generate_pdf_output}, "
208+
"hide_code={hide_code}, scheduler_job_id={scheduler_job_id}, is_slideshow={is_slideshow})".format(
206209
job_id=self.job_id,
207210
status=self.status,
208211
report_name=self.report_name,
@@ -212,6 +215,7 @@ def __repr__(self):
212215
report_title=self.report_title,
213216
overrides=self.overrides,
214217
mailto=self.mailto,
218+
error_mailto=self.error_mailto,
215219
mailfrom=self.mailfrom,
216220
email_subject=self.email_subject,
217221
generate_pdf_output=self.generate_pdf_output,

notebooker/execute_notebook.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def _run_checks(
4444
generate_pdf_output: Optional[bool] = True,
4545
hide_code: Optional[bool] = False,
4646
mailto: Optional[str] = "",
47+
error_mailto: Optional[str] = "",
4748
email_subject: Optional[str] = "",
4849
prepare_only: Optional[bool] = False,
4950
notebooker_disable_git: bool = False,
@@ -76,7 +77,9 @@ def _run_checks(
7677
generate_pdf_output : `Optional[bool]`
7778
Whether to generate PDF output or not. NB this requires xelatex to be installed on the executor.
7879
mailto : `Optional[str]`
79-
Comma-separated email addresses to send on completion (or error).
80+
Comma-separated email addresses to send on completion.
81+
error_mailto : `Optional[str]`
82+
Comma-separated email addresses to send on error.
8083
prepare_only : `Optional[bool]`
8184
Internal usage. Whether we want to do everything apart from executing the notebook.
8285
scheduler_job_id : `Optional[str]`
@@ -128,6 +131,7 @@ def _run_checks(
128131
raw_html=html,
129132
email_html=email_html,
130133
mailto=mailto,
134+
error_mailto=error_mailto,
131135
email_subject=email_subject,
132136
pdf=pdf,
133137
generate_pdf_output=generate_pdf_output,
@@ -191,6 +195,7 @@ def run_report(
191195
template_base_dir,
192196
overrides,
193197
mailto=mailto,
198+
error_mailto=error_mailto,
194199
email_subject=email_subject,
195200
generate_pdf_output=generate_pdf_output,
196201
hide_code=hide_code,
@@ -215,12 +220,14 @@ def run_report(
215220
report_title=report_title,
216221
error_info=error_info,
217222
overrides=overrides,
218-
mailto=error_mailto or mailto,
223+
mailto=mailto,
224+
error_mailto=error_mailto,
219225
generate_pdf_output=generate_pdf_output,
220226
scheduler_job_id=scheduler_job_id,
221227
mailfrom=mailfrom,
222228
hide_code=hide_code,
223229
is_slideshow=is_slideshow,
230+
email_subject=email_subject,
224231
)
225232
logger.error(
226233
"Report run failed. Saving error result to mongo library %s@%s...",
@@ -402,8 +409,7 @@ def execute_notebook_entrypoint(
402409
mailfrom=mailfrom,
403410
is_slideshow=is_slideshow,
404411
)
405-
if result.mailto:
406-
send_result_email(result, config.DEFAULT_MAILFROM)
412+
send_result_email(result, config.DEFAULT_MAILFROM)
407413
logger.info(f"Here is the result!{result}")
408414
if isinstance(result, NotebookResultError):
409415
logger.warning("Notebook execution failed! Output was:")
@@ -458,6 +464,7 @@ def run_report_in_subprocess(
458464
report_name,
459465
report_title,
460466
mailto,
467+
error_mailto,
461468
overrides,
462469
*,
463470
hide_code=False,
@@ -466,6 +473,7 @@ def run_report_in_subprocess(
466473
scheduler_job_id=None,
467474
run_synchronously=False,
468475
mailfrom=None,
476+
email_subject=None,
469477
n_retries=3,
470478
is_slideshow=False,
471479
) -> str:
@@ -476,16 +484,20 @@ def run_report_in_subprocess(
476484
:param report_name: `str` The report which we are executing
477485
:param report_title: `str` The user-specified title of the report
478486
:param mailto: `Optional[str]` Who the results will be emailed to
487+
:param error_mailto: `Optional[str]` Who the errors will be emailed to
479488
:param overrides: `Optional[Dict[str, Any]]` The parameters to be passed into the report
480489
:param generate_pdf_output: `bool` Whether we're generating a PDF. Defaults to False.
481490
:param prepare_only: `bool` Whether to do everything except execute the notebook. Useful for testing.
482491
:param scheduler_job_id: `Optional[str]` if the job was triggered from the scheduler, this is the scheduler's job id
483492
:param run_synchronously: `bool` If True, then we will join the stderr monitoring thread until the job has completed
484493
:param mailfrom: `str` if passed, then this string will be used in the from field
494+
:param email_subject: `str` if passed, then this string will be used in the email subject
485495
:param n_retries: The number of retries to attempt.
486496
:param is_slideshow: Whether the notebook is a reveal.js slideshow or not.
487497
:return: The unique job_id.
488498
"""
499+
if error_mailto is None:
500+
error_mailto = ""
489501
job_id = str(uuid.uuid4())
490502
job_start_time = datetime.datetime.now()
491503
result_serializer = initialize_serializer_from_config(base_config)
@@ -497,10 +509,12 @@ def run_report_in_subprocess(
497509
status=JobStatus.SUBMITTED,
498510
overrides=overrides,
499511
mailto=mailto,
512+
error_mailto=error_mailto,
500513
generate_pdf_output=generate_pdf_output,
501514
hide_code=hide_code,
502515
scheduler_job_id=scheduler_job_id,
503516
is_slideshow=is_slideshow,
517+
email_subject=email_subject,
504518
)
505519

506520
command = (
@@ -530,6 +544,8 @@ def run_report_in_subprocess(
530544
report_title,
531545
"--mailto",
532546
mailto,
547+
"--error-mailto",
548+
error_mailto,
533549
"--overrides-as-json",
534550
json.dumps(overrides),
535551
"--pdf-output" if generate_pdf_output else "--no-pdf-output",
@@ -541,6 +557,7 @@ def run_report_in_subprocess(
541557
+ (["--is-slideshow"] if is_slideshow else [])
542558
+ ([f"--scheduler-job-id={scheduler_job_id}"] if scheduler_job_id is not None else [])
543559
+ ([f"--mailfrom={mailfrom}"] if mailfrom is not None else [])
560+
+ ([f"--email-subject={email_subject}"] if email_subject else [])
544561
)
545562
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
546563

notebooker/serialization/mongo.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,7 @@ def update_check_status(self, job_id: str, status: JobStatus, **extra):
187187
existing["status"] = status.value
188188
for k, v in extra.items():
189189
if k == "error_info" and v:
190-
self.result_data_store.put(
191-
v,
192-
filename=_error_info_filename(job_id),
193-
encoding="utf-8",
194-
)
190+
self.result_data_store.put(v, filename=_error_info_filename(job_id), encoding="utf-8")
195191
else:
196192
existing[k] = v
197193
self._save_raw_to_db(existing)
@@ -205,10 +201,12 @@ def save_check_stub(
205201
status: JobStatus = JobStatus.PENDING,
206202
overrides: Optional[Dict] = None,
207203
mailto: str = "",
204+
error_mailto: str = "",
208205
generate_pdf_output: bool = True,
209206
hide_code: bool = False,
210207
scheduler_job_id: Optional[str] = None,
211208
is_slideshow: bool = False,
209+
email_subject: Optional[str] = None,
212210
) -> None:
213211
"""Call this when we are just starting a check. Saves a "pending" job into storage."""
214212
job_start_time = job_start_time or datetime.datetime.now()
@@ -220,6 +218,8 @@ def save_check_stub(
220218
job_start_time=job_start_time,
221219
report_name=report_name,
222220
mailto=mailto,
221+
error_mailto=error_mailto,
222+
email_subject=email_subject,
223223
generate_pdf_output=generate_pdf_output,
224224
overrides=overrides or {},
225225
hide_code=hide_code,
@@ -242,9 +242,7 @@ def save_check_result(self, notebook_result: Union[NotebookResultComplete, Noteb
242242
filename=filename_func(notebook_result.job_id),
243243
encoding="utf-8",
244244
)
245-
for json_attribute, filename_func in [
246-
("raw_ipynb_json", _raw_json_filename),
247-
]:
245+
for json_attribute, filename_func in [("raw_ipynb_json", _raw_json_filename)]:
248246
if getattr(notebook_result, json_attribute, None):
249247
self.result_data_store.put(
250248
json.dumps(getattr(notebook_result, json_attribute)),
@@ -317,10 +315,12 @@ def _convert_result(
317315
generate_pdf_output=result.get("generate_pdf_output", True),
318316
report_title=result.get("report_title", result["report_name"]),
319317
mailto=result.get("mailto", ""),
318+
error_mailto=result.get("error_mailto", ""),
320319
hide_code=result.get("hide_code", False),
321320
stdout=result.get("stdout", []),
322321
scheduler_job_id=result.get("scheduler_job_id", None),
323322
is_slideshow=result.get("is_slideshow", False),
323+
email_subject=result.get("email_subject", None),
324324
)
325325
elif cls == NotebookResultPending:
326326
return NotebookResultPending(
@@ -333,6 +333,8 @@ def _convert_result(
333333
generate_pdf_output=result.get("generate_pdf_output", True),
334334
report_title=result.get("report_title", result["report_name"]),
335335
mailto=result.get("mailto", ""),
336+
error_mailto=result.get("error_mailto", ""),
337+
email_subject=result.get("email_subject", ""),
336338
hide_code=result.get("hide_code", False),
337339
stdout=result.get("stdout", []),
338340
scheduler_job_id=result.get("scheduler_job_id", None),
@@ -356,6 +358,8 @@ def _convert_result(
356358
generate_pdf_output=result.get("generate_pdf_output", True),
357359
report_title=result.get("report_title", result["report_name"]),
358360
mailto=result.get("mailto", ""),
361+
error_mailto=result.get("error_mailto", ""),
362+
email_subject=result.get("email_subject", ""),
359363
hide_code=result.get("hide_code", False),
360364
stdout=result.get("stdout", []),
361365
scheduler_job_id=result.get("scheduler_job_id", False),

notebooker/utils/notebook_execution.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,13 @@ def _output_dir(output_base_dir, report_name, job_id):
1515
return os.path.join(output_base_dir, report_name, job_id)
1616

1717

18-
def send_result_email(result: Union[NotebookResultComplete, NotebookResultError], default_mailfrom: str) -> None:
19-
if result.mailfrom:
20-
mailfrom = result.mailfrom
21-
else:
22-
mailfrom = default_mailfrom
23-
to_email = result.mailto
18+
def _send_email(from_email: str, to_email: str, result: Union[NotebookResultComplete, NotebookResultError]) -> None:
2419
report_title = (
2520
result.report_title.decode("utf-8") if isinstance(result.report_title, bytes) else result.report_title
2621
)
27-
subject = result.email_subject or f"Notebooker: {report_title} report completed with status: {result.status.value}"
22+
subject = f"Notebooker: {report_title} report completed with status: {result.status.value}"
23+
if isinstance(result, NotebookResultComplete):
24+
subject = result.email_subject or subject
2825
body = result.email_html or result.raw_html
2926
attachments = []
3027
tmp_dir = None
@@ -57,8 +54,23 @@ def send_result_email(result: Union[NotebookResultComplete, NotebookResultError]
5754
msg = ["Please either activate HTML emails, or see the PDF attachment.", body]
5855

5956
logger.info("Sending email to %s with %d attachments", to_email, len(attachments))
60-
mail(mailfrom, to_email, subject, msg, attachments=attachments)
57+
mail(from_email, to_email, subject, msg, attachments=attachments)
6158
finally:
6259
if tmp_dir:
6360
logger.info("Cleaning up temporary email attachment directory %s", tmp_dir)
6461
shutil.rmtree(tmp_dir)
62+
63+
64+
def send_result_email(result: Union[NotebookResultComplete, NotebookResultError], default_mailfrom: str) -> None:
65+
if result.mailfrom:
66+
mailfrom = result.mailfrom
67+
else:
68+
mailfrom = default_mailfrom
69+
if isinstance(result, NotebookResultComplete):
70+
to_email = result.mailto
71+
else:
72+
to_email = result.error_mailto or result.mailto
73+
if not to_email:
74+
logger.info("Not sending email as no recipients specified")
75+
return
76+
_send_email(from_email=mailfrom, to_email=to_email, result=result)

notebooker/utils/templates.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
def _valid_dirname(d):
17-
hidden_dirs = [dir for dir in d.split("/") if dir.startswith(".") and (dir != "." or dir != "..")]
17+
hidden_dirs = [dir for dir in d.split("/") if dir.startswith(".") and dir not in {".", ".."}]
1818
return "__init__" not in d and "__pycache__" not in d and not hidden_dirs
1919

2020

0 commit comments

Comments
 (0)