diff --git a/notebooker/_entrypoints.py b/notebooker/_entrypoints.py index 309d471b..b73e8eac 100644 --- a/notebooker/_entrypoints.py +++ b/notebooker/_entrypoints.py @@ -124,7 +124,14 @@ def start_webapp(config: BaseConfig, port, logging_level, debug, base_cache_dir) help="The unique job ID for this notebook. Can be non-unique, but note that you will overwrite history.", ) @click.option("--mailto", default="", help="A comma-separated list of email addresses which will receive results.") +@click.option( + "--error-mailto", + default="", + help="A comma-separated list of email addresses which will receive errors. Deafults to --mailto argument." +) +@click.option("--email-subject", default="", help="The subject of the email sent on a successful result.") @click.option("--pdf-output/--no-pdf-output", default=True, help="Whether we generate PDF output or not.") +@click.option("--hide-code/--show-code", default=False, help="Hide code from email and PDF output.") @click.option( "--prepare-notebook-only", is_flag=True, @@ -140,7 +147,10 @@ def execute_notebook( n_retries, job_id, mailto, + error_mailto, + email_subject, pdf_output, + hide_code, prepare_notebook_only, ): if report_name is None: @@ -154,7 +164,10 @@ def execute_notebook( n_retries, job_id, mailto, + error_mailto, + email_subject, pdf_output, + hide_code, prepare_notebook_only, ) diff --git a/notebooker/constants.py b/notebooker/constants.py index 345da3fb..3c75fb7d 100644 --- a/notebooker/constants.py +++ b/notebooker/constants.py @@ -75,6 +75,7 @@ class NotebookResultBase(object): overrides = attr.ib(default=attr.Factory(dict)) mailto = attr.ib(default="") generate_pdf_output = attr.ib(default=True) + hide_code = attr.ib(default=False) stdout = attr.ib(default=attr.Factory(list)) def saveable_output(self): @@ -91,6 +92,7 @@ class NotebookResultPending(NotebookResultBase): overrides = attr.ib(default=attr.Factory(dict)) mailto = attr.ib(default="") generate_pdf_output = attr.ib(default=True) + hide_code = attr.ib(default=False) @attr.s() @@ -102,6 +104,11 @@ class NotebookResultError(NotebookResultBase): overrides = attr.ib(default=attr.Factory(dict)) mailto = attr.ib(default="") generate_pdf_output = attr.ib(default=True) + hide_code = attr.ib(default=False) + + @property + def email_subject(self): + return "" @property def raw_html(self): @@ -109,6 +116,10 @@ def raw_html(self): self.error_info ) + @property + def email_html(self): + return self.raw_html + @attr.s(repr=False) class NotebookResultComplete(NotebookResultBase): @@ -118,12 +129,15 @@ class NotebookResultComplete(NotebookResultBase): status = attr.ib(default=JobStatus.DONE) raw_ipynb_json = attr.ib(default="") raw_html = attr.ib(default="") + email_html = attr.ib(default="") update_time = attr.ib(default=datetime.datetime.now()) pdf = attr.ib(default="") report_title = attr.ib(default="") overrides = attr.ib(default=attr.Factory(dict)) mailto = attr.ib(default="") + email_subject = attr.ib(default="") generate_pdf_output = attr.ib(default=True) + hide_code = attr.ib(default=False) stdout = attr.ib(default=attr.Factory(list)) def html_resources(self): @@ -143,13 +157,16 @@ def saveable_output(self): "report_name": self.report_name, "report_title": self.report_title, "raw_html": self.raw_html, + "email_html": self.email_html, "raw_html_resources": self.html_resources(), "job_id": self.job_id, "job_start_time": self.job_start_time, "job_finish_time": self.job_finish_time, "mailto": self.mailto, + "email_subject": self.email_subject, "overrides": self.overrides, "generate_pdf_output": self.generate_pdf_output, + "hide_code": self.hide_code, "update_time": self.update_time, } @@ -158,7 +175,7 @@ def __repr__(self): "NotebookResultComplete(job_id={job_id}, status={status}, report_name={report_name}, " "job_start_time={job_start_time}, job_finish_time={job_finish_time}, update_time={update_time}, " "report_title={report_title}, overrides={overrides}, mailto={mailto}, " - "generate_pdf_output={generate_pdf_output})".format( + "email_subject={email_subject}, generate_pdf_output={generate_pdf_output}, hide_code={hide_code})".format( job_id=self.job_id, status=self.status, report_name=self.report_name, @@ -168,6 +185,8 @@ def __repr__(self): report_title=self.report_title, overrides=self.overrides, mailto=self.mailto, + email_subject=self.email_subject, generate_pdf_output=self.generate_pdf_output, + hide_code=self.hide_code, ) ) diff --git a/notebooker/execute_notebook.py b/notebooker/execute_notebook.py index aa09d41b..2ccd8b93 100644 --- a/notebooker/execute_notebook.py +++ b/notebooker/execute_notebook.py @@ -37,7 +37,10 @@ def _run_checks( template_base_dir: str, overrides: Dict[AnyStr, Any], generate_pdf_output: Optional[bool] = True, + hide_code: Optional[bool] = False, mailto: Optional[str] = "", + error_mailto: Optional[str] = "", + email_subject: Optional[str] = "", prepare_only: Optional[bool] = False, notebooker_disable_git: bool = False, py_template_base_dir: str = "", @@ -101,7 +104,8 @@ def _run_checks( logger.info("Saving output notebook as HTML from {}".format(ipynb_executed_path)) html, resources = ipython_to_html(ipynb_executed_path, job_id) - pdf = ipython_to_pdf(raw_executed_ipynb, report_title) if generate_pdf_output else "" + email_html, resources = ipython_to_html(ipynb_executed_path, job_id, hide_code=hide_code) + pdf = ipython_to_pdf(raw_executed_ipynb, report_title, hide_code=hide_code) if generate_pdf_output else "" notebook_result = NotebookResultComplete( job_id=job_id, @@ -110,7 +114,9 @@ def _run_checks( raw_html_resources=resources, raw_ipynb_json=raw_executed_ipynb, raw_html=html, + email_html=email_html, mailto=mailto, + email_subject=email_subject, pdf=pdf, generate_pdf_output=generate_pdf_output, report_name=template_name, @@ -131,7 +137,10 @@ def run_report( template_base_dir=None, attempts_remaining=2, mailto="", + error_mailto="", + email_subject="", generate_pdf_output=True, + hide_code=False, prepare_only=False, notebooker_disable_git=False, py_template_base_dir="", @@ -163,7 +172,9 @@ def run_report( template_base_dir, overrides, mailto=mailto, + email_subject=email_subject, generate_pdf_output=generate_pdf_output, + hide_code=hide_code, prepare_only=prepare_only, notebooker_disable_git=notebooker_disable_git, py_template_base_dir=py_template_base_dir, @@ -182,7 +193,7 @@ def run_report( report_title=report_title, error_info=error_info, overrides=overrides, - mailto=mailto, + mailto=error_mailto or mailto, generate_pdf_output=generate_pdf_output, ) logger.error( @@ -205,7 +216,10 @@ def run_report( template_base_dir=template_base_dir, attempts_remaining=attempts_remaining - 1, mailto=mailto, + error_mailto=error_mailto, + email_subject=email_subject, generate_pdf_output=generate_pdf_output, + hide_code=hide_code, prepare_only=prepare_only, notebooker_disable_git=notebooker_disable_git, py_template_base_dir=py_template_base_dir, @@ -291,7 +305,10 @@ def execute_notebook_entrypoint( n_retries: int, job_id: str, mailto: str, + error_mailto: str, + email_subject: str, pdf_output: bool, + hide_code: bool, prepare_notebook_only: bool, ): report_title = report_title or report_name @@ -312,7 +329,10 @@ def execute_notebook_entrypoint( logger.info("output_dir = %s", output_dir) logger.info("template_dir = %s", template_dir) logger.info("mailto = %s", mailto) + logger.info("error_mailto = %s", error_mailto) + logger.info("email_subject = %s", email_subject) logger.info("pdf_output = %s", pdf_output) + logger.info("hide_code = %s", hide_code) logger.info("prepare_notebook_only = %s", prepare_notebook_only) logger.info("notebooker_disable_git = %s", notebooker_disable_git) logger.info("py_template_base_dir = %s", py_template_base_dir) @@ -335,13 +355,16 @@ def execute_notebook_entrypoint( template_base_dir=template_dir, attempts_remaining=n_retries - 1, mailto=mailto, + error_mailto=error_mailto, + email_subject=email_subject, generate_pdf_output=pdf_output, + hide_code=hide_code, prepare_only=prepare_notebook_only, notebooker_disable_git=notebooker_disable_git, py_template_base_dir=py_template_base_dir, py_template_subdir=py_template_subdir, ) - if mailto: + if result.mailto: send_result_email(result, mailto) if isinstance(result, NotebookResultError): logger.warning("Notebook execution failed! Output was:") diff --git a/notebooker/nbtemplates/notebooker_html_output.tpl b/notebooker/nbtemplates/notebooker_html_output.tpl index a662ed80..f868ca65 100644 --- a/notebooker/nbtemplates/notebooker_html_output.tpl +++ b/notebooker/nbtemplates/notebooker_html_output.tpl @@ -16,6 +16,16 @@ } + + + -{%- endblock html_head -%} \ No newline at end of file +{%- endblock html_head -%} + +{% block stream %} + {%- if resources.global_content_filter.include_output_prompt -%} + {{ super() }} + {%- endif -%} +{%- endblock stream %} diff --git a/notebooker/nbtemplates/notebooker_pdf_output.tplx b/notebooker/nbtemplates/notebooker_pdf_output.tplx new file mode 100644 index 00000000..f40fe967 --- /dev/null +++ b/notebooker/nbtemplates/notebooker_pdf_output.tplx @@ -0,0 +1,7 @@ +((* extends 'article.tplx' *)) + +((*- block stream -*)) + ((*- if resources.global_content_filter.include_output_prompt -*)) + ((( super() ))) + ((*- endif -*)) +((*- endblock stream -*)) diff --git a/notebooker/serialization/mongo.py b/notebooker/serialization/mongo.py index 0a181f93..e6babfa0 100644 --- a/notebooker/serialization/mongo.py +++ b/notebooker/serialization/mongo.py @@ -97,6 +97,7 @@ def save_check_stub( overrides: Optional[Dict] = None, mailto: str = "", generate_pdf_output: bool = True, + hide_code: bool = False, ) -> None: """ Call this when we are just starting a check. Saves a "pending" job into storage. """ job_start_time = job_start_time or datetime.datetime.now() @@ -110,6 +111,7 @@ def save_check_stub( mailto=mailto, generate_pdf_output=generate_pdf_output, overrides=overrides or {}, + hide_code=hide_code, ) self._save_to_db(pending_result) @@ -176,11 +178,13 @@ def read_file(path): raw_html_resources=result.get("raw_html_resources", {}), raw_ipynb_json=result.get("raw_ipynb_json"), raw_html=result.get("raw_html"), + email_html=result.get("email_html"), pdf=result.get("pdf", ""), overrides=result.get("overrides", {}), generate_pdf_output=result.get("generate_pdf_output", True), report_title=result.get("report_title", result["report_name"]), mailto=result.get("mailto", ""), + hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), ) elif cls == NotebookResultPending: @@ -194,6 +198,7 @@ def read_file(path): generate_pdf_output=result.get("generate_pdf_output", True), report_title=result.get("report_title", result["report_name"]), mailto=result.get("mailto", ""), + hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), ) @@ -209,6 +214,7 @@ def read_file(path): generate_pdf_output=result.get("generate_pdf_output", True), report_title=result.get("report_title", result["report_name"]), mailto=result.get("mailto", ""), + hide_code=result.get("hide_code", False), stdout=result.get("stdout", []), ) else: diff --git a/notebooker/utils/conversion.py b/notebooker/utils/conversion.py index 9ec79831..1cc57518 100644 --- a/notebooker/utils/conversion.py +++ b/notebooker/utils/conversion.py @@ -19,12 +19,14 @@ def get_resources_dir(job_id): return "{}/resources".format(job_id) -def ipython_to_html(ipynb_path: str, job_id: str) -> (nbformat.NotebookNode, Dict[str, Any]): +def ipython_to_html(ipynb_path: str, job_id: str, hide_code: bool = False) -> (nbformat.NotebookNode, Dict[str, Any]): c = Config() c.HTMLExporter.preprocessors = ["nbconvert.preprocessors.ExtractOutputPreprocessor"] c.HTMLExporter.template_file = pkg_resources.resource_filename( __name__, "../nbtemplates/notebooker_html_output.tpl" ) + c.HTMLExporter.exclude_input = hide_code + c.HTMLExporter.exclude_output_prompt = hide_code html_exporter_with_figs = HTMLExporter(config=c) with open(ipynb_path, "r") as nb_file: @@ -34,8 +36,14 @@ def ipython_to_html(ipynb_path: str, job_id: str) -> (nbformat.NotebookNode, Dic return html, resources -def ipython_to_pdf(raw_executed_ipynb: str, report_title: str) -> AnyStr: - pdf_exporter = PDFExporter(Config()) +def ipython_to_pdf(raw_executed_ipynb: str, report_title: str, hide_code: bool = False) -> AnyStr: + c = Config() + c.PDFExporter.exclude_input = hide_code + c.PDFExporter.exclude_output_prompt = hide_code + c.HTMLExporter.template_file = pkg_resources.resource_filename( + __name__, "../nbtemplates/notebooker_pdf_output.tplx" + ) + pdf_exporter = PDFExporter(c) resources = ResourcesDict() resources["metadata"] = ResourcesDict() resources["metadata"]["name"] = report_title diff --git a/notebooker/utils/notebook_execution.py b/notebooker/utils/notebook_execution.py index ab1303e8..f805df66 100644 --- a/notebooker/utils/notebook_execution.py +++ b/notebooker/utils/notebook_execution.py @@ -15,14 +15,14 @@ def _output_dir(output_base_dir, report_name, job_id): return os.path.join(output_base_dir, report_name, job_id) -def send_result_email(result: Union[NotebookResultComplete, NotebookResultError], mailto: AnyStr) -> None: +def send_result_email(result: Union[NotebookResultComplete, NotebookResultError]) -> None: from_email = "notebooker@notebooker.io" - to_email = mailto + to_email = result.mailto report_title = ( result.report_title.decode("utf-8") if isinstance(result.report_title, bytes) else result.report_title ) - subject = "Notebooker: {} report completed with status: {}".format(report_title, result.status.value) - body = result.raw_html + subject = result.email_subject or f"Notebooker: {report_title} report completed with status: {result.status.value}" + body = result.email_html or result.raw_html attachments = [] tmp_dir = None try: @@ -53,7 +53,7 @@ def send_result_email(result: Union[NotebookResultComplete, NotebookResultError] msg = ["Please either activate HTML emails, or see the PDF attachment.", body] - logger.info("Sending email to %s with %d attachments", mailto, len(attachments)) + logger.info("Sending email to %s with %d attachments", to_email, len(attachments)) mail(from_email, to_email, subject, msg, attachments=attachments) finally: if tmp_dir: diff --git a/notebooker/web/routes/run_report.py b/notebooker/web/routes/run_report.py index 77ff09b3..4eedf3c6 100644 --- a/notebooker/web/routes/run_report.py +++ b/notebooker/web/routes/run_report.py @@ -116,7 +116,9 @@ def _monitor_stderr(process, job_id, serializer_cls, serializer_args): return "".join(stderr) -def run_report(report_name, report_title, mailto, overrides, generate_pdf_output=False, prepare_only=False): +def run_report( + report_name, report_title, mailto, overrides, hide_code=False, generate_pdf_output=False, prepare_only=False +): """ Actually run the report in earnest. Uses a subprocess to execute the report asynchronously, which is identical to the non-webapp entrypoint. @@ -140,6 +142,7 @@ def run_report(report_name, report_title, mailto, overrides, generate_pdf_output overrides=overrides, mailto=mailto, generate_pdf_output=generate_pdf_output, + hide_code=hide_code, ) app_config = current_app.config p = subprocess.Popen( @@ -170,6 +173,7 @@ def run_report(report_name, report_title, mailto, overrides, generate_pdf_output "--overrides-as-json", json.dumps(overrides), "--pdf-output" if generate_pdf_output else "--no-pdf-output", + "--hide-code" if hide_code else "--show-code", ] + (["--prepare-notebook-only"] if prepare_only else []), stderr=subprocess.PIPE, @@ -192,10 +196,13 @@ def _handle_run_report( mailto = validate_mailto(request.values.get("mailto"), issues) # Find whether to generate PDF output generate_pdf_output = validate_generate_pdf_output(request.values.get("generatepdf"), issues) + hide_code = request.values.get("hide_code") == "on" if issues: return jsonify({"status": "Failed", "content": ("\n".join(issues))}) report_name = convert_report_name_url_to_path(report_name) - job_id = run_report(report_name, report_title, mailto, overrides_dict, generate_pdf_output=generate_pdf_output) + job_id = run_report( + report_name, report_title, mailto, overrides_dict, generate_pdf_output=generate_pdf_output, hide_code=hide_code + ) return ( jsonify({"id": job_id}), 202, # HTTP Accepted code diff --git a/notebooker/web/templates/run_report.html b/notebooker/web/templates/run_report.html index 88284ca7..a91d874b 100644 --- a/notebooker/web/templates/run_report.html +++ b/notebooker/web/templates/run_report.html @@ -46,7 +46,15 @@

Email to:

- +
+ + +
+
+ +