import click import pathlib import pyperclip import shutil import sys from typing import Iterable from dataclasses import dataclass from datetime import datetime Pathlike = str | pathlib.Path PATH_WIN_DESKTOP = pathlib.Path("/mnt/c/Users/Holgi/Desktop") TODAY = datetime.now().strftime("%y%m%d") CRLF = "\r\n" GROUPS = { "QC": ["ASQC, HQC, MQC"], "Print": ["Dry-1, Dry-2"], "Production": ["Hyb", "Reg"], } GROUP_FOLDER_PREFIX = "Safeguard-MBP-" GROUP_FOLDER_SUFFIX = "-Changes" EXCEL_CHANGELOG_HEADERS = [ "Sheet\tWell\tContents\tComment", "-----\t----\t--------\t-------", "", ] EXCEL_CHANGELOGS = { "QC": { "changes mbp qc asqc {version}.txt": "L1", "changes mbp qc hqc {version}.txt": "J1", "changes mbp qc mqc {version}.txt": "J1", }, "Print": { "changes mbp print dry-1 {version}.txt": "L1", "changes mbp print dry-2 {version}.txt": "L1", }, "Production": { "changes mbp production hyb {version}.txt": "J1", "changes mbp production reg {version}.txt": "J1", }, } ## Exception classes class MBPExcecption(Exception): pass # common functions and helper functions def _to_int(text: str, default=0) -> int: try: return int(text) except (ValueError, TypeError): return default def _folder_content(folder: Pathlike) -> Iterable[pathlib.Path]: folder = pathlib.Path(folder) nondotted = (i for i in folder.iterdir() if not i.stem.startswith(".")) return (i for i in nondotted if not i.stem.startswith("~")) def _files_in_folder(folder: Pathlike, suffix: str) -> Iterable[pathlib.Path]: folder = pathlib.Path(folder) files = (f for f in _folder_content(folder) if f.is_file()) return (f for f in files if f.suffix == suffix) def _get_workbook_folder(group: str) -> pathlib.Path: path = pathlib.Path("/") / "mnt" / "e" / f"Safeguard MBP {group} Workbooks" if not path.is_dir(): msg = "Workbook folder {path} does not exist" raise MBPExcecption(msg) return path def _get_changelog_path(folder: Pathlike) -> pathlib.Path: textfiles = _files_in_folder(folder, ".txt") return next(f for f in textfiles if f.stem.lower().startswith("change")) def _list_current_frms(group: str, build_version: str): source = _get_workbook_folder(group) search_version = build_version.removesuffix(TODAY) all_folders = (f for f in source.iterdir() if f.is_dir()) all_frms = (f for f in all_folders if "frm" in f.name.lower()) return [f for f in all_frms if search_version in f.name] def _get_issue_numbers(group: str, build_version: str): print(list(_list_current_frms(group, build_version))) for path in _list_current_frms(group, build_version): rest, issue_info = path.name.lower().split("issue") issue_info = issue_info.removeprefix("s") # might be "issues" issue, *rest = issue_info.strip().split() yield issue.strip(" ,") def _extract_changes_from_log( cwd: Pathlike, group: str, build_version: str ) -> Iterable[str]: issue_numbers = set(_get_issue_numbers(group, build_version)) changelog = _get_changelog_path(cwd) for line in changelog.read_text().splitlines(): for issue in issue_numbers: if issue in line: yield line def _get_group_from_folder(folder: Pathlike) -> str: name = pathlib.Path(folder).name middle = name.removeprefix(GROUP_FOLDER_PREFIX).removesuffix(GROUP_FOLDER_SUFFIX) if middle in GROUPS: return middle msg = f"Folder '{name}' is not an MBP group folder" raise MBPExcecption(msg) def _get_latest_version(folder: Pathlike) -> "Version": dir_names = [i.name for i in pathlib.Path(folder).iterdir() if i.is_dir()] version_names = [name for name in dir_names if name.startswith("v")] versions = [Version.from_name(name) for name in version_names] sorted_versions = sorted(versions, key=lambda x: x.to_tuple()) return sorted_versions[-1] ## data classes @dataclass class Version: major: int layout: int minor: int @classmethod def from_name(cls, name: str) -> "Version": parts = name.removeprefix("v").split(".") + [None, None] args = tuple([_to_int(part) for part in parts[:3]]) return cls(*args) def to_tuple(self) -> tuple[int, int, int]: return (self.major, self.layout, self.minor) def bump(self) -> None: cls = type(self) return cls(self.major, self.layout, self.minor + 1) def __str__(self) -> str: return f"v{self.major}.{self.layout}.{self.minor}" ## functions for `sg_mbp_build` def copy_workbooks(group: str, destination: Pathlike, build_version: str) -> None: source = _get_workbook_folder(group) all_xls_files = _files_in_folder(source, ".xlsx") mbp_files = (f for f in all_xls_files if f.name.lower().startswith("mbp")) for excel_file in mbp_files: new_name = f"{excel_file.stem} {build_version}{excel_file.suffix}" new_path = destination / new_name.format(version=build_version) print(excel_file.name, "->", new_path) shutil.copyfile(excel_file, new_path) def copy_frms(group: str, destination: Pathlike, build_version: str) -> None: current_frms = _list_current_frms(group, build_version) for folder in current_frms: new_path = destination / folder.name print(folder.name, "->", new_path) shutil.copytree(folder, new_path) def copy_workbook_changelogs( cwd: Pathlike, destination: Pathlike, latest: Version, build_version: str ) -> None: source = pathlib.Path(cwd) / str(latest) textfiles = _files_in_folder(source, ".txt") logs = (f for f in textfiles if f.stem.lower().startswith("change")) for log_file in logs: new_name = log_file.name.replace(str(latest), build_version) new_path = destination / new_name print(log_file.name, "->", new_path) shutil.copyfile(log_file, new_path) def copy_changelog(cwd: Pathlike, destination: Pathlike, build_version: str) -> None: changelog = _get_changelog_path(cwd) new_path = pathlib.Path(destination) / f"CHANGELOG {build_version}.txt" print(changelog.name, "->", new_path) shutil.copyfile(changelog, new_path) def get_announcement_text( cwd: Pathlike, group: str, latest: Version, build_version: str, new_folder_name: Pathlike, ) -> str: changes = list(_extract_changes_from_log(cwd, group, build_version)) if len(changes) == 1: change_msg = "Only one change was introduced:" else: change_msg = "The changes made:" if not build_version.endswith(TODAY): term = "build" dev_version = build_version.split(".")[-1] version_note = ( f"As indicated by the letter '{dev_version}' at the end," f" this {term} is intended for Freiburg only." ) else: term = "version" version_note = f"This is an official release {term}." text = [ f"# New MBP {group} {term.title()} {build_version}", "Good News Everyone,", f"there is a new MBP {group} {term} available: {build_version}", version_note, change_msg, "\n".join(changes), f"You can find this {term} at our Freiburg Shared Drive:", ( "Google Drive / Shared drives / Freiburg / Workbooks /" f" MBP Workbooks {group} /" f" {latest} /" f" {new_folder_name}" ), "Cheers,\nHolgi", ] return "\n\n".join(text) @click.command() @click.option( "-d", "--dev_version", prompt="Dev version i.e. 'c'", required=True, default=TODAY, ) def sg_mbp_build(dev_version): """ Before running this command: \b - create a new versions folder e.g. "v4.9.2" - make the requiered edits to the workbooks - note the changes in the excel changelogs in the created version folder - edit the group changelog The command will collect all data into one folder on the Desktop to be published """ try: cwd = pathlib.Path.cwd() group = _get_group_from_folder(cwd) latest = _get_latest_version(cwd) build_version = f"{latest}.{dev_version}" new_folder_name = f"{TODAY} MBP {group} {build_version}" new_folder_path = PATH_WIN_DESKTOP / new_folder_name if new_folder_path.exists(): raise MBPExcecption(f"Folder exists on desktop: {new_folder_name}") else: new_folder_path.mkdir() copy_workbooks(group, new_folder_path, build_version) copy_frms(group, new_folder_path, build_version) copy_workbook_changelogs(cwd, new_folder_path, latest, build_version) copy_changelog(cwd, new_folder_path, build_version) announcement = get_announcement_text( cwd, group, latest, build_version, new_folder_name ) pyperclip.copy(announcement) print(announcement) except MBPExcecption as e: sys.exit(str(e)) ## functions for `sg_mbp_new_version` def get_next_version() -> Version: try: cwd = pathlib.Path.cwd() _get_group_from_folder(cwd) # may raise an exception latest = _get_latest_version(cwd) return latest.bump() except MBPExcecption as e: sys.exit(str(e)) def create_new_version_folder(cwd: Pathlike, new_version: str) -> pathlib.Path: new_folder_path = pathlib.Path(cwd) / new_version if new_folder_path.exists(): msg = f"Folder for version {new_version} already exists" raise MBPExcecption(msg) new_folder_path.mkdir() return new_folder_path def create_excel_changelogs(folder: Pathlike, new_version: str) -> None: folder = pathlib.Path(folder) group = _get_group_from_folder(folder.parent) for name, cell in EXCEL_CHANGELOGS[group].items(): new_file = folder / name.format(version=new_version) with new_file.open("w") as fh: data_line = "\t".join(["Settings", cell, new_version, ""]) content_lines = EXCEL_CHANGELOG_HEADERS + [data_line, "", ""] fh.write(CRLF.join(content_lines)) def create_changelog_entry(cwd: Pathlike, new_version: str) -> None: group = _get_group_from_folder(cwd) workbooks = ", ".join(GROUPS[group]) changelog = _get_changelog_path(cwd) content = [] with changelog.open("r") as fh: stripped_lines = (line.rstrip() for line in fh) for line in stripped_lines: content.append(line) if line.startswith("----"): content.append("") content.append(f"{new_version}, work in progress:") content.append( f" - The following Workbooks did not have any changes: {workbooks}" ) with changelog.open("w") as fh: fh.write(CRLF.join(content)) @click.command() @click.option( "-v", "--version", required=True, prompt="new version", default=get_next_version, show_default="next minor version", ) def sg_mbp_new_version(version): """ creates a new version folder, new excel changes files and modifies the overall changelog in "E:\Safeguard-MBP-issues" """ cwd = pathlib.Path.cwd() folder = create_new_version_folder(cwd, version) create_excel_changelogs(folder, version) create_changelog_entry(cwd, version)