from io import open import os import tempfile import uuid import webbrowser from six import BytesIO from pybloqs.config import user_config from pybloqs.email import send_html_report from pybloqs.html import root, append_to, render, js_elem, id_generator import pybloqs.htmlconv as htmlconv from pybloqs.static import DependencyTracker, Css, script_inflate, script_block_core, register_interactive from pybloqs.util import Cfg, cfg_to_css_string from six.moves.urllib.parse import urljoin default_css_main = Css(os.path.join("css", "pybloqs_default", "main")) register_interactive(default_css_main) class BaseBlock(object): """ Base class for all blocks. Provides infrastructure for rendering the block in an IPython Notebook or saving it to disk in HTML, PDF, PNG or JPG format. """ container_tag = "div" resource_deps = [] def __init__(self, title=None, title_level=3, title_wrap=False, width=None, height=None, inherit_cfg=True, styles=None, classes=(), anchor=None, **kwargs): self._settings = Cfg(title=title, title_level=title_level, title_wrap=title_wrap, cascading_cfg=Cfg(**kwargs).override(styles or Cfg()), default_cfg=Cfg(), inherit_cfg=inherit_cfg, width=width, height=height, classes=["pybloqs"] + ([classes] if isinstance(classes, str) else list(classes))) # Anchor should not be inherited, so keep outside of Cfg self._anchor = anchor self._id = uuid.uuid4().hex def render_html(self, pretty=True, static_output=False, header_block=None, footer_block=None): """Returns html output of the block :param pretty: Toggles pretty printing of the resulting HTML. Not applicable for non-HTML output. :param static_output: Passed down to _write_block. Will render static version of blocks which support this. :param header_block: If not None, header is inlined into a HTML body as table. :param footer_block: If not None, header is inlined into a HTML body as table. :return html-code of the block """ # Render the contents html = root("html", doctype="html") head = append_to(html, "head") append_to(head, "meta", charset='utf-8') body = append_to(html, "body") # Make sure that the main style sheet is always included resource_deps = DependencyTracker(default_css_main) # If header or footer are passed into this function, inline them in the following structure: # # <body> # <table> # <thead><tr><td>Header html</td></tr></thead> # <tfoot><tr><td>Footer html</td></tr></tfoot> # <tbody><tr><td>Body html</td></tr></tbody> # </table> # </body> if header_block is not None or footer_block is not None: content_table = append_to(body, "table") if header_block is not None: header_thead = append_to(content_table, "thead") header_tr = append_to(header_thead, "tr") header_td = append_to(header_tr, "th") header_block._write_block(header_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output) if footer_block is not None: footer_tfoot = append_to(content_table, "tfoot", id='footer') footer_tr = append_to(footer_tfoot, "tr") footer_td = append_to(footer_tr, "td") footer_block._write_block(footer_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output) body_tbody = append_to(content_table, "tbody") body_tr = append_to(body_tbody, "tr") body_td = append_to(body_tr, "td") self._write_block(body_td, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output) else: self._write_block(body, Cfg(), id_generator(), resource_deps=resource_deps, static_output=static_output) script_inflate.write(head) script_block_core.write(head) if static_output: # Add the load wait poller if there are any JS resources js_elem(body, "var loadWaitPoller=runWaitPoller();") # Write out resources for res in resource_deps: res.write(head) # Render the whole document (the parent of the html tag) content = render(html.parent, pretty=pretty) return content def save(self, filename=None, fmt=None, pdf_zoom=1, pdf_page_size=htmlconv.html_converter.A4, pdf_auto_shrink=True, orientation=htmlconv.html_converter.PORTRAIT, header_block=None, header_spacing=5, footer_block=None, footer_spacing=5, **kwargs): """ Render and save the block. Depending on whether the filename or the format is provided, the content will either be written out to a file or returned as a string. :param filename: Format will be based on the file extension. The following formats are supported: - HTML - PDF - PNG - JPG :param fmt: Specifies the format of a temporary output file. When supplied, the filename parameter must be omitted. :param pdf_zoom: The zooming to apply when rendering the page. :param pdf_page_size: The page size to use when rendering the page to PDF. :param pdf_auto_shrink: Toggles auto-shrinking content to fit the desired page size (wkhtmltopdf only) :param orientation: Either html_converter.PORTRAIT or html_converter.LANDSCAPE :param header_block: Block to be used as header (and repeated on every page). Only used for PDF output. :param header_spacing: Size of header block. Numbers are in mm. HTML sizes (e.g. '5cm') in chrome_headless only. :param footer_block: Block to be used as footer (and repeated on every page). Only used for PDF output. :param footer_spacing: Size of header block. Numbers are in mm. HTML sizes (e.g. '5cm') in chrome_headless only. :return: html filename """ # Ensure that exactly one of filename or fmt is provided if filename is None and fmt is None: raise ValueError("One of `filename` or `fmt` must be provided.") tempdir = user_config["tmp_html_dir"] if filename: _, fmt_from_name = os.path.splitext(filename) # Exclude the dot from the extension, gosh darn it! fmt_from_name = fmt_from_name[1:] if fmt is None: if fmt_from_name == '': raise ValueError('If fmt is not specified, filename must contain extension') fmt = fmt_from_name else: if fmt != fmt_from_name: filename += '.' + fmt else: name = self._id[:user_config["id_precision"]] + "." + fmt filename = os.path.join(tempdir, name) # Force extension to be lower case so format checks are easier later fmt = fmt.lower() is_html = "htm" in fmt if is_html: content = self.render_html(static_output=False, header_block=header_block, footer_block=footer_block) with open(filename, "w", encoding='utf-8') as f: f.write(content) else: converter = htmlconv.get_converter(fmt) converter.htmlconv(self, filename, header_block=header_block, header_spacing=header_spacing, footer_block=footer_block, footer_spacing=footer_spacing, pdf_page_size=pdf_page_size, orientation=orientation, pdf_auto_shrink=pdf_auto_shrink, pdf_zoom=pdf_zoom, **kwargs) return filename def publish(self, name, *args, **kwargs): """ Publish the block so that others can access it. :param name: Name to publish under. Can be a filename or a relative path. :param args: Arguments to pass to `Block.save`. :param kwargs: Keyword arguments to pass to `Block.save`. :return: Path to the published block file. """ full_path = os.path.join(user_config["public_dir"], name) full_path = os.path.expanduser(full_path) base_dir = os.path.dirname(full_path) try: os.makedirs(base_dir) except OSError: pass # Directory already exists self.save(full_path, * args, **kwargs) return full_path def show(self, fmt="html", header_block=None, footer_block=None): """ Show the block in a browser. :param fmt: The format of the saved block. Supports the same output as `Block.save` :return: Path to the block file. """ file_name = self._id[:user_config["id_precision"]] + "." + fmt file_path = self.publish(os.path.expanduser(os.path.join(user_config["tmp_html_dir"], file_name)), header_block=header_block, footer_block=footer_block) try: url_base = user_config["public_dir"] except KeyError: path = os.path.expanduser(file_path) else: path = urljoin(url_base, os.path.expanduser(user_config["tmp_html_dir"] + "/" + file_name)) webbrowser.open_new_tab(path) return path def email(self, title="", recipients=(user_config["user_email_address"],), header_block=None, footer_block=None, from_address=None, cc=None, bcc=None, attachments=None, convert_to_ascii=True, **kwargs): """ Send the rendered blocks as email. Each output format chosen will be added as an attachment. :param title: title of the email :param recipients: recipient of the email :param fmt: One or more output formats that should be included as attachments. The following formats are supported: - HTML - PDF - PNG - JPG :param body_block: The block to use as the email body. The default behavior is to use the current block. :param from_address: sender of the message. Defaults to user name. Can be overwritten in .pybloqs.cfg with yaml format: 'user_email_address: a@b.com' :param cc: cc recipient :param bcc: bcc recipient :param convert_to_ascii: bool to control convertion of html email to ascii or to leave in current format :param kwargs: Optional arguments to pass to `Block.render_html()` """ if from_address is None: from_address = user_config["user_email_address"] # The email body needs to be static without any dynamic elements. email_html = self.render_html(header_block=header_block, footer_block=footer_block, **kwargs) send_html_report(email_html, recipients, subject=title, attachments=attachments, From=from_address, Cc=cc, Bcc=bcc, convert_to_ascii=convert_to_ascii) def to_static(self): return self._visit(lambda block: block._to_static()) def _to_static(self): """ Subclasses can override this method to provide a static content version. """ return self def _visit(self, visitor): """ Calls the supplied visitor function on this block and any sub-blocks :param visitor: Visitor function :return: Return value of the visitor """ return visitor(self) def _provide_default_cfg(self, defaults): """ Makes the supplied config to be part of the defaults for the block. :param defaults: The default parameters that should be inherited. """ self._settings.default_cfg = self._settings.default_cfg.inherit(defaults) def _combine_parent_cfg(self, parent_cfg): """from pybloqs.config import user_config Combine the supplied parent and the current Block's config. :param parent_cfg: Parent config to inherit from. :return: Combined config. """ # Combine parameters only if inheritance is turned on if self._settings.inherit_cfg: actual_cfg = self._settings.cascading_cfg.inherit(parent_cfg) else: actual_cfg = self._settings.cascading_cfg # Any undefined settings will use the defaults actual_cfg = actual_cfg.inherit(self._settings.default_cfg) return actual_cfg def _get_styles_string(self, styles_cfg): """ Converts the styles configuration to a CSS styles string. :param styles_cfg: The configuration object to convert. :return: CSS string """ sizing_cfg = Cfg() if self._settings.width is not None: sizing_cfg["width"] = self._settings.width if self._settings.height is not None: sizing_cfg["height"] = self._settings.height # Replace `_` with `-` and make values lowercase to get valid CSS names return cfg_to_css_string(styles_cfg.override(sizing_cfg)) def _write_block(self, parent, parent_cfg, id_gen, resource_deps=None, static_output=False): """ Writes out the block into the supplied stream, inheriting the parent_parameters. :param parent: Parent element :param parent_cfg: Parent parameters to inherit. :param id_gen: Unique ID generator. :param resource_deps: Object used to register resource dependencies. :param static_output: A value of True signals to blocks that the final output will be a static format. Certain dynamic content will render with alternate options. """ if resource_deps is not None: for res in self.resource_deps: resource_deps.add(res) actual_cfg = self._combine_parent_cfg(parent_cfg) if self.container_tag is not None: container = append_to(parent, self.container_tag) self._write_container_attrs(container, actual_cfg) else: container = parent self._write_anchor(container) self._write_title(container) self._write_contents(container, actual_cfg, id_gen, resource_deps=resource_deps, static_output=static_output) def _write_container_attrs(self, container, actual_cfg): """ Writes out the container attributes (styles, class, etc...). Note that this method will only be called if the container tag is not `None`. :param container: Container element. :param actual_cfg: Actual parameters to use. """ styles = self._get_styles_string(actual_cfg) if len(styles) > 0: container["style"] = styles container["class"] = self._settings.classes def _write_title(self, container): """ Write out the title (if there is any). :param container: Container element. """ if self._settings.title is not None and (self._settings.title != ""): title = append_to(container, "H%s" % self._settings.title_level, style="white-space: %s" % ("normal" if self._settings.title_wrap else "nowrap")) title.string = self._settings.title def _write_anchor(self, container): """ Write HTML anchor for linking within page :param container: Container element. """ if self._anchor is not None: append_to(container, "a", name=self._anchor) def _write_contents(self, container, actual_cfg, id_gen, resource_deps=None, static_output=None): """ Write out the actual contents of the block. Deriving classes must override this method. :param container: Container element. :param actual_cfg: Actual parameters to use. :param id_gen: Unique ID generator. :param resource_deps: Object used to register resource dependencies. :param static_output: A value of True signals to blocks that the final output will be a static format. Certain dynamic content will render with alternate options. """ raise NotImplementedError("_write_contents") def _repr_html_(self, *_): """ Function required to support interactive IPython plopping and plotting. Should not be used directly. :return: Data to be displayed """ return self.data.decode() @property def data(self): """ Function required to support interactive IPython plotting. Should not be used directly. :return: Data to be displayed """ container = root("div") self._write_block(container, Cfg(), id_generator()) # Write children into the output output = BytesIO() for child in container.children: output.write(render(child).encode('utf-8')) return output.getvalue() class HRule(BaseBlock): """ Draws a horizontal divider line. """ def _write_block(self, parent, *args, **kwargs): # Add a `hr` element to the parent append_to(parent, "hr")