diff --git a/work_helpers/sg_mbp_build.py b/work_helpers/sg_mbp_build.py index 3208b15..bdf5711 100644 --- a/work_helpers/sg_mbp_build.py +++ b/work_helpers/sg_mbp_build.py @@ -1,195 +1,236 @@ import click -import shutil +import pathlib import pyperclip +import shutil +import sys -from pathlib import Path +from typing import Iterable +from dataclasses import dataclass from datetime import datetime -from ._natural_sort import natural_sort +Pathlike = str | pathlib.Path + -DEVELOPER_DRIVE = Path("/mnt/e/") -PATH_ISSUES = DEVELOPER_DRIVE / "Safeguard-MBP-issues" -PATH_WORKBOOKS = DEVELOPER_DRIVE / "Safeguard MBP Workbooks" -PATH_WIN_DESKTOP = Path("/mnt/c/Users/Holgi/Desktop") +PATH_WIN_DESKTOP = pathlib.Path("/mnt/c/Users/Holgi/Desktop") TODAY = datetime.now().strftime("%y%m%d") CRLF = "\r\n" -EXCEL_CHANGELOGS = { - "changes mbp {version} asqc.txt": "L1", - "changes mbp {version} dry-1.txt": "L1", - "changes mbp {version} dry-2.txt": "L1", - "changes mbp {version} hqc.txt": "J1", - "changes mbp {version} hyb.txt": "J1", - "changes mbp {version} mqc.txt": "J1", - "changes mbp {version} reg.txt": "J1", +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-------", "", ] -WORKBOOKS_MAP = { - "MBP ASQC.xlsx": "MBP {version} ASQC.xlsx", - "MBP Dry-1.xlsx": "MBP {version} Dry-1.xlsx", - "MBP Dry-2.xlsx": "MBP {version} Dry-2.xlsx", - "MBP HQC.xlsx": "MBP {version} HQC.xlsx", - "MBP Hyb.xlsx": "MBP {version} Hyb.xlsx", - "MBP MQC.xlsx": "MBP {version} MQC.xlsx", - "MBP Reg.xlsx": "MBP {version} Reg.xlsx", +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", + }, } -def _folder_content(folder): +## 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, suffix): +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_latest_version(parent=PATH_ISSUES): - folders = (i for i in _folder_content(parent) if i.is_dir()) - versions = natural_sort(f.name for f in folders if f.stem.lower().startswith("v")) - return versions[-1] +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_next_version(parent=PATH_ISSUES, echo_current=True): - latest = get_latest_version(parent) - if echo_current: - print("current version:", latest) +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")) - try: - head, tail = latest.rsplit(".", 1) - next_minor = int(tail) + 1 - next_version = f"{head}.{next_minor}" - except: - next_version = "" - return next_version +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 create_new_version_folder(new_version, parent=PATH_ISSUES): - new_folder_path = parent / new_version - if new_folder_path.exists(): - print(f"Folder for version {new_version} already exists, aborting") - return - new_folder_path.mkdir() - return new_folder_path +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 create_excel_changelogs(new_version, parent): - for name, cell in EXCEL_CHANGELOGS.items(): - new_file = parent / 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 _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 create_changelog_entry(new_version, parent=PATH_ISSUES): - textfiles = _files_in_folder(parent, ".txt") - changelog = next(f for f in textfiles if f.stem.lower().startswith("change")) - 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(" - The following Workbooks did not have any changes: ASQC, Dry-1, Dry-2, Hyb, HQC, MQC, Reg") - with changelog.open("w") as fh: - fh.write(CRLF.join(content)) +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_changelog_path(): - textfiles = _files_in_folder(PATH_ISSUES, ".txt") - return next(f for f in textfiles if f.stem.lower().startswith("change")) -def copy_changelog(destination, build_version): - changelog = get_changelog_path() - new_path = destination / f"CHANGELOG {build_version}.txt" - print(changelog.name, "->", new_path) - shutil.copyfile(changelog, new_path) +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] -def copy_workbook_changelogs(destination, latest, build_version): - source = PATH_ISSUES / 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(latest, build_version) - new_path = destination / new_name - print(log_file.name, "->", new_path) - shutil.copyfile(log_file, new_path) +## 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}" -def copy_workbooks(destination, build_version): - all_xls_files = _files_in_folder(PATH_WORKBOOKS, ".xlsx") +## 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 = WORKBOOKS_MAP[excel_file.name] + 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 collect_current_frms(build_version): - all_folders = (f for f in PATH_WORKBOOKS.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 f.name.endswith(build_version)] - - -def copy_frms(destination, build_version): - all_folders = (f for f in PATH_WORKBOOKS.iterdir() if f.is_dir()) - all_frms = (f for f in all_folders if "frm" in f.name.lower()) - current_frms = collect_current_frms(build_version) +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 get_issue_numbers(build_version): - for path in collect_current_frms(build_version): - rest, issue_info = path.name.lower().split("issue") - issue, *rest = issue_info.strip().split() - yield issue.strip(" ,") +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 extract_changes_from_log(build_version): - issue_numbers = set(get_issue_numbers(build_version)) - changelog = get_changelog_path() - for line in changelog.read_text().splitlines(): - for issue in issue_numbers: - if issue in line: - yield line +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(dev_version, build_version, new_folder_name): - if not dev_version: - return "This is an official release, the message must be hand crafted" - - latest = get_latest_version() - changes = list(extract_changes_from_log(build_version)) +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): + dev_version = build_version.split(".")[-1] + version_note = ( + f"As indicated by the letter '{dev_version}' at the end," + " this version is intended for Freiburg only." + ) + else: + version_note = "This is an official release version." + text = [ f"# New MBP Workbook Version {build_version}", "Good News Everyone,", f"there is a new MBP workbook version available: {build_version}", - ( - f"As indicated by the letter '{dev_version}' at the end," - " this version is intended for Freiburg only." - ), + version_note, change_msg, "\n".join(changes), "You can find this version at our Freiburg Shared Drive:", @@ -198,67 +239,127 @@ def get_announcement_text(dev_version, build_version, new_folder_name): f" {latest} /" f" {new_folder_name}" ), - "Cheers,\nHolgi" + "Cheers,\nHolgi", ] return "\n\n".join(text) -@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" - """ - - folder = create_new_version_folder(version) - if folder is not None: - create_excel_changelogs(version, folder) - create_changelog_entry(version) - - @click.command() @click.option( "-d", "--dev_version", prompt="Dev version i.e. 'c'", required=True, - default="", + 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 - - edit the changelog in "E:\Safeguard-MBP-issues" - - create a new versions folder in "E:\Safeguard-MBP-issues", e.g. "v3.9.49" - 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 """ - latest = get_latest_version() - build_version = f"{latest}{dev_version}" + 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)) - new_folder_name = f"{TODAY} {build_version}" - new_folder_path = PATH_WIN_DESKTOP / new_folder_name + +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(): - raise IOError(f"Folder exists on desktop: {new_folder_name}") - else: - new_folder_path.mkdir() + 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)) - copy_workbooks(new_folder_path, build_version) - copy_workbook_changelogs(new_folder_path, latest, build_version) - copy_changelog(new_folder_path, build_version) - copy_frms(new_folder_path, build_version) - announcement = get_announcement_text(dev_version, build_version, new_folder_name) - pyperclip.copy(announcement) - print(announcement) \ No newline at end of file +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)