Source code for pylabnotebook.export

"""
EXPORT MODULE

This module contains notebook export functions.
"""

# pylint: disable=line-too-long

import subprocess
import os
import yaml
import re
from datetime import datetime
from jinja2 import Environment, FileSystemLoader

from .exceptions import NotGitRepoError, NotInitializedError, OutputAlreadyExistsError, EmptyHisoryError

# useful values
RED: str = '\033[0;31m'
NCOL: str = '\033[0m'
SCRIPT_DIR: str = os.path.dirname(os.path.abspath(__file__))

[docs] def export_labnotebook(output_file: str, force: bool, link: bool) -> None: """Export labnotebook to html. This function exports the labnotebook into a single html file ready to read and share. :param output_file: path of the file to create :type output_file: str :param force: whether to force the overwriting of output_file if exists. :type force: bool :param link: whether to create links to analysis files in analysis files bullet list. These links can be used to open the analysis files directly from the notebook. # pylint: disable=line-too-long :type link: bool """ # 1. Go to git root dir try: git_root: str = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], universal_newlines = True, stderr=subprocess.PIPE).strip() except subprocess.CalledProcessError: raise NotGitRepoError("fatal: not a git repository (or any of the parent directories): .git") from None os.chdir(git_root) # 2. Check for .labnotebookrc if not os.path.exists(".labnotebookrc"): raise NotInitializedError(f"{RED}Error: There is no .labnotebookrc file in git repository root folder. Please, run labnotebook create -n <name_of_the_project>{NCOL}") with open(".labnotebookrc", "r", encoding = 'utf8') as config_file: config: dict = yaml.safe_load(config_file) # 3. Check if file already exists and force is False if os.path.exists(output_file) and not force: raise OutputAlreadyExistsError(f"{RED}Error: {output_file} already exists. Use -f/--force to overwrite it.{NCOL}") # 5. Get list of commits sha sha_list: list[str] = get_sha_list(config.get("REVERSE_HISTORY")) # 6. Get info about each commit commits_info: dict = {sha: get_commit_info(sha, analysis_ext = config.get('ANALYSIS_EXT'), excluded_patterns = config.get('ANALYSIS_IGNORE')) for sha in sha_list} # 7. Read style.css with open(config.get('LAB_CSS'), 'r', encoding='utf-8') as css_file: style_css = css_file.read() environment = Environment(loader=FileSystemLoader(f"{SCRIPT_DIR}/templates/")) template = environment.get_template("base.html") content = template.render(config = config, create_date = datetime.today().strftime('%Y-%m-%d'), link = link, commits_info = commits_info, style_css = style_css) with open(output_file, mode="w", encoding="utf-8") as message: message.write(content) print(f"Notebook succesfully exported to {output_file}.")
def get_sha_list(reverse_history: bool) -> list[str]: """Get sha list. This functions returns a list of commits sha (from oldest to newest) that have not been already included in the notebook. :param reverse_history: Whether commits should be returned from first to last. :type reverse_history: bool :return: list of the commits not included in the notebook since last_commit. :rtype: list[str] """ # 1. Get list of all commits git_command = ["git", "log", "--pretty=format:%h"] if reverse_history: git_command.append("--reverse") try: git_sha: list[str] = subprocess.check_output(git_command, text = True, stderr=subprocess.PIPE).split('\n') except subprocess.CalledProcessError: raise EmptyHisoryError(f"{RED}Error: Git history is empty{NCOL}") from None return git_sha def get_commit_info(commit_sha: str, analysis_ext: list[str], excluded_patterns: list[str]) -> dict: """Get commit info. This function returns a dictionary of the information about the commit specified in commit_sha. These info are: date, author, title, message, changed files and analysis_files (based on both analysis_ext and excluded_patterns). :param commit_sha: sha of the commit of interest. :type commit_sha: str :param analysis_ext: list of the file extensions used as reference for analysis files. :type analysis_ext: list[str] :param excluded_patterns: list of the pattern to be excluded from the analysis files. :type excluded_patterns: list[str] :return: information about the commit specified in commit_sha: date, author, title, message, changed files and analysis_files (based on both analysis_ext and excluded_patterns). :rtype: dict """ date, author, title = subprocess.check_output(['git', 'log', '-n', '1', '--pretty=format:%cs%n%an%n%s', commit_sha], text = True).strip().split('\n') # Get message and replace newlines with html breaks message: str = subprocess.check_output(['git', 'log', '-n', '1', '--pretty=format:%b', commit_sha], text=True).strip() pattern: str = r"(\.|:|!|\?)\n" replacement: str = r"\1<br>\n" message = re.sub(pattern, replacement, message).replace('\n\n', '\n<br>\n') changed_files: list[str] = subprocess.check_output(['git', 'show', '--pretty=%n', '--name-status', commit_sha], text=True).strip().split('\n') if changed_files == ['']: changed_files = {} else: changed_files: dict = {file.split('\t')[1] : file.split('\t')[0] for file in changed_files} analysis_files: list[str] = [key for key, _ in changed_files.items() if any(ext in key for ext in analysis_ext) and os.path.isfile(key) and not any(re.search(pattern, key) for pattern in excluded_patterns)] commit_info: dict = {'date': date, 'author': author, 'title': title, 'message': message, 'changed_files': changed_files, 'analysis_files': analysis_files} return commit_info