From b15c114379bc26fb990de7014eac0a64d878807e Mon Sep 17 00:00:00 2001 From: "r.jaepel" <r.jaepel@fz-juelich.de> Date: Tue, 16 Jan 2024 11:01:49 +0100 Subject: [PATCH] Rework CLI --- .gitignore | 4 +- cadetrdm/cli_integration.py | 212 ++++++++++++++++++++++-------------- cadetrdm/repositories.py | 52 ++++----- setup.cfg | 2 +- tests/test_cli.py | 115 +++++++++++++++++++ 5 files changed, 274 insertions(+), 111 deletions(-) create mode 100644 tests/test_cli.py diff --git a/.gitignore b/.gitignore index 37eb7a0..c44a98c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ __pycache__ tests/test_repo* .coverage dist -.token \ No newline at end of file +.token +tmp +.ipynb_checkpoints \ No newline at end of file diff --git a/cadetrdm/cli_integration.py b/cadetrdm/cli_integration.py index 65b1ebb..e3b936e 100644 --- a/cadetrdm/cli_integration.py +++ b/cadetrdm/cli_integration.py @@ -3,11 +3,6 @@ import subprocess import click -from .repositories import ProjectRepo, BaseRepo -from .initialize_repo import initialize_repo as initialize_git_repo_implementation -from .initialize_repo import clone as clone_implementation -from .conda_env_utils import prepare_conda_env as prepare_conda_env_implementation - CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @@ -16,123 +11,178 @@ def cli(): pass -@cli.command() -@click.option('--path_to_repo', default=None, - help='Path to folder for the repository. Optional.') +@cli.command(help="Create an empty CADET-RDM repository or initialize over an existing git repo.") +@click.option('--output_repo_name', default="output", + help='Name of the folder where the tracked output should be stored. Optional. Default: "output".') +@click.option('--gitignore', default=None, + help='List of files to be added to the gitignore file. Optional.') +@click.option('--gitattributes', default=None, + help='List of files to be added to the gitattributes file. Optional.') +@click.argument('path_to_repo') +def init(path_to_repo: str, output_repo_name: (str | bool) = "output", gitignore: list = None, + gitattributes: list = None, + output_repo_kwargs: dict = None): + from cadetrdm.initialize_repo import initialize_repo as initialize_git_repo_implementation + initialize_git_repo_implementation(path_to_repo, output_repo_name, gitignore, + gitattributes, output_repo_kwargs) + + +@cli.command(help="Clone a repository into a new directory.") @click.argument('project_url') -def clone(project_url, path_to_repo: str = None): - clone_implementation(project_url, path_to_repo) +@click.argument('dest', required=False) +def clone(project_url, dest: str = None): + from cadetrdm.initialize_repo import clone as clone_implementation + clone_implementation(project_url, dest) -@cli.command() -@click.option('--target_repo_location', default=None, - help='Path to folder for the repository. Optional.') -@click.argument('source_repo_location') -@click.argument('source_repo_branch') -def import_remote_repo(source_repo_location, source_repo_branch, target_repo_location=None): +@cli.command(name="log", help="Show commit logs.") +def print_log(): + from cadetrdm.repositories import BaseRepo + repo = BaseRepo(".") - repo.import_remote_repo(source_repo_location=source_repo_location, - source_repo_branch=source_repo_branch, - target_repo_location=target_repo_location) + repo.print_log() -@cli.command() -@click.argument('url') -@click.argument('namespace') -@click.argument('name') -def create_gitlab_remotes(url, namespace, name): +@cli.command(help="Push all changes to the project and output repositories.") +def push(): + from cadetrdm.repositories import ProjectRepo repo = ProjectRepo(".") - repo.create_gitlab_remotes(url=url, namespace=namespace, name=name) + repo.push(push_all=True) -@cli.command() -@click.argument('namespace') -@click.argument('name') -def create_github_remotes(namespace, name): +@cli.command(help="Record changes to the repository") +@click.option("--message", "-m", help="commit message") +@click.option("--all", "-a", is_flag=True, help="commit all changed files") +def commit(message, all): + from cadetrdm.repositories import ProjectRepo repo = ProjectRepo(".") - repo.create_github_remotes(namespace=namespace, name=name) - -@cli.command() -def verify_unchanged_cache(): - repo = BaseRepo(".") - repo.verify_unchanged_cache() + repo.commit(message, all) -@cli.command() -@click.option('--re_load', default=False, - help='Re-load all data.') -def fill_data_from_cadet_rdm_json(re_load=False): - repo = BaseRepo(".") - repo.fill_data_from_cadet_rdm_json(re_load=re_load) +@cli.group(help="Execute commands and track the results.") +def run(): + pass -@cli.command() +@run.command(name="python") @click.argument('file_name') @click.argument('results_commit_message') def run_python_file(file_name, results_commit_message): + from cadetrdm.repositories import ProjectRepo repo = ProjectRepo(".") repo.enter_context() subprocess.run(["python", file_name]) repo.exit_context(results_commit_message) -@cli.command() -@click.argument('command') +@run.command(name="command") +@click.argument('command', nargs=-1) @click.argument('results_commit_message') def run_command(command, results_commit_message): + from cadetrdm.repositories import ProjectRepo repo = ProjectRepo(".") repo.enter_context() - subprocess.run(shlex.split(command)) + subprocess.run(command) repo.exit_context(results_commit_message) -@cli.command() -@click.option('--output_repo_name', default="output", - help='Name of the folder where the tracked output should be stored. Optional. Default: "output".') -@click.option('--gitignore', default=None, - help='List of files to be added to the gitignore file. Optional.') -@click.option('--gitattributes', default=None, - help='List of files to be added to the gitattributes file. Optional.') -@click.argument('path_to_repo') -def initialize_repo(path_to_repo: str, output_repo_name: (str | bool) = "output", gitignore: list = None, - gitattributes: list = None, - output_repo_kwargs: dict = None): - initialize_git_repo_implementation(path_to_repo, output_repo_name, gitignore, - gitattributes, output_repo_kwargs) +@cli.group(help="Create, add, and manage remotes.") +def remote(): + pass -@cli.command() -@click.option("-p", '--path_to_repo', default=".", - help='Path to repository to which the remote is added. Default is cwd.') +@remote.command(name="add", help="Add") +@click.option('--name', '-n', default=None) @click.argument('remote_url') -def add_remote_to_repo(remote_url: str, path_to_repo="."): - repo = BaseRepo(path_to_repo) - repo.add_remote(remote_url) +def add_remote(name: str = None, remote_url: str = None): + from cadetrdm.repositories import BaseRepo + repo = BaseRepo(".") + repo.add_remote(remote_url=remote_url, remote_name=name) print("Done.") -@cli.command() -@click.argument('file_type') -def add_filetype_to_lfs(file_type: str, ): +@remote.command(name="create") +@click.option("-p", '--path_to_repo', default=".", + help='Path to repository to which the remote is added. Default is cwd.') +@click.argument('url') +@click.argument('namespace') +@click.argument('name') +def create_remotes(url, namespace, name): + from cadetrdm.repositories import ProjectRepo + repo = ProjectRepo(".") + if "github" in url: + repo.create_github_remotes(namespace=namespace, name=name) + else: + repo.create_gitlab_remotes(url=url, namespace=namespace, name=name) + + +@remote.command(name="list") +def list_remotes(): + from cadetrdm.repositories import BaseRepo repo = BaseRepo(".") - repo.add_filetype_to_lfs(file_type) + for _remote, url in zip(repo.remotes, repo.remote_urls): + print(_remote, ": ", url) -@cli.command() -@click.option('--url', default=None, - help='Url to the environment.yml file.') -def prepare_conda_env(url): - prepare_conda_env_implementation(url) +@cli.group(help="Manage large file storage settings.") +def lfs(): + pass -@cli.command() -def print_output_log(): - # ToDo: test if Project or Output repo - repo = ProjectRepo(".") - repo.print_output_log() +@lfs.command(name="add", help="Add a filetype to git lfs.") +@click.argument('file_types', nargs=-1) +def add_filetype_to_lfs(file_types: list, ): + from cadetrdm.repositories import BaseRepo + repo = BaseRepo(".") + for f_type in file_types: + repo.add_filetype_to_lfs(f_type) -@cli.command(help="Push all changes to the project and output repositories.") -def push(): +@cli.group(help="Manage data and input-data-repositories.") +def data(): + pass + + +@data.command(name="import", help="Import a remote repository into a given location.") +@click.argument('source_repo_location') +@click.argument('source_repo_branch') +@click.argument('target_repo_location', required=False) +def import_remote_repo(source_repo_location, source_repo_branch, target_repo_location=None): + from cadetrdm.repositories import BaseRepo + repo = BaseRepo(".") + repo.import_remote_repo(source_repo_location=source_repo_location, + source_repo_branch=source_repo_branch, + target_repo_location=target_repo_location) + + +@data.command(name="fetch", help="Fill data cache based on cadet-rdm.json.") +@click.option('--re_load', is_flag=True, + help='Re-load all data.') +def fill_data_from_cadet_rdm_json(re_load=False): + from cadetrdm.repositories import ProjectRepo repo = ProjectRepo(".") - repo.push(push_all=True) + repo.fill_data_from_cadet_rdm_json(re_load=re_load) + + +@data.command(name="verify", help="Verify that cache is unchanged.") +def verify_unchanged_cache(): + from cadetrdm.repositories import BaseRepo + repo = BaseRepo(".") + repo.verify_unchanged_cache() + + +@data.command(name="log", help="Print data logs.") +def print_data_log(): + from cadetrdm.repositories import ProjectRepo, BaseRepo, OutputRepo + import json + + repo = BaseRepo(".") + + with open(repo.data_json_path, "r") as handle: + rdm_data = json.load(handle) + if rdm_data["is_project_repo"]: + repo = ProjectRepo(".") + repo.print_output_log() + else: + repo = OutputRepo() + repo.print_data_log() diff --git a/cadetrdm/repositories.py b/cadetrdm/repositories.py index 6b1938d..1445a67 100644 --- a/cadetrdm/repositories.py +++ b/cadetrdm/repositories.py @@ -116,7 +116,7 @@ class BaseRepo: def cache_json_path(self): return self.working_dir / ".cadet-rdm-cache.json" - def add_remote(self, remote_url, remote_name="origin"): + def add_remote(self, remote_url, remote_name=None): """ Add a remote to the repository. @@ -124,6 +124,8 @@ class BaseRepo: :param remote_name: :return: """ + if remote_name is None: + remote_name = "origin" self._git_repo.create_remote(remote_name, url=remote_url) with open(self.data_json_path, "r") as handle: rdm_data = json.load(handle) @@ -366,9 +368,9 @@ class BaseRepo: def reset_hard_to_head(self, force_entry=False): if not force_entry: proceed = wait_for_user(f'The output directory contains the following uncommitted changes:\n' - f'{self.untracked_files + self.changed_files}\n' - f' These will be lost if you continue\n' - f'Proceed?') + f'{self.untracked_files + self.changed_files}\n' + f' These will be lost if you continue\n' + f'Proceed?') else: proceed = True if not proceed: @@ -685,25 +687,7 @@ class ProjectRepo(BaseRepo): self._output_repo._git.checkout(self._most_recent_branch) def print_output_log(self): - def insert_newlines(string, every=30): - lines = [] - for i in range(0, len(string), every): - lines.append(string[i:i + every]) - return '\n'.join(lines) - - self.output_repo.checkout("master") - - tsv_filepath = self.working_dir / self._output_folder / "log.tsv" - - with open(tsv_filepath, "r") as filehandle: - lines = filehandle.readlines() - - line_array = [line.replace("\n", "").split("\t") for line in lines] - - # Print - print(tabulate(line_array[1:], headers=line_array[0])) - - self.output_repo.checkout(self.output_repo._most_recent_branch) + self.output_repo.print_data_log() def fill_data_from_cadet_rdm_json(self, re_load=False): """ @@ -735,7 +719,6 @@ class ProjectRepo(BaseRepo): source_repo_location=repo_info["source_repo_location"], source_repo_branch=repo_info["branch_name"]) - def convert_csv_to_tsv_if_necessary(self): """ If not tsv log is found AND a csv log is found, convert the csv to tsv. @@ -1086,9 +1069,22 @@ class ProjectRepo(BaseRepo): self.exit_context(message=results_commit_message) - class OutputRepo(BaseRepo): - pass + + def print_data_log(self): + self.checkout("master") + + tsv_filepath = self.working_dir / "log.tsv" + + with open(tsv_filepath, "r") as filehandle: + lines = filehandle.readlines() + + line_array = [line.replace("\n", "").split("\t") for line in lines] + + # Print + print(tabulate(line_array[1:], headers=line_array[0])) + + self.checkout(self._most_recent_branch) class JupyterInterfaceRepo(ProjectRepo): @@ -1104,8 +1100,8 @@ class JupyterInterfaceRepo(ProjectRepo): def commit_nb_output(self, notebook_path: str, results_commit_message: str, force_rerun=True, timeout=600, conversion_formats: list = None): if "nbconvert_call" in sys.argv: - return - # This is reached in the first call of this function + return + # This is reached in the first call of this function if not Path(notebook_path).is_absolute(): notebook_path = self.working_dir / notebook_path diff --git a/setup.cfg b/setup.cfg index 42981b6..f77d301 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ include_package_data = True [options.entry_points] console_scripts = - cadet-rdm = cadetrdm.cli_integration:cli + rdm = cadetrdm.cli_integration:cli [options.extras_require] testing = diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..f71f75e --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,115 @@ +from pathlib import Path +import random +import os + +from click.testing import CliRunner + +from cadetrdm.cli_integration import cli +from cadetrdm.io_utils import delete_path + +runner = CliRunner() +if os.path.exists("test_repo_cli"): + delete_path("test_repo_cli") + +os.makedirs("test_repo_cli") +os.chdir("test_repo_cli") + + +def modify_code(path_to_repo): + # Add changes to the project code + random_number = random.randint(0, 265) + filepath = Path(path_to_repo) / f"print_random_number.py" + with open(filepath, "w") as file: + file.write( + f"print({random_number})\n" + 'with open("output/data.txt", "w") as handle:\n' + f" handle.write({random_number})\n" + ) + + +def test_01_initialize_repo(): + result = runner.invoke(cli, ["init", "."]) + print(result.output) + assert result.exit_code == 0 + + +def test_02_add_remote(): + result = runner.invoke(cli, ["remote", "add", "https://jugit.fz-juelich.de/r.jaepel/API_test_project"]) + print(result.output) + assert result.exit_code == 0 + os.chdir("output") + result = runner.invoke(cli, ["remote", "add", "https://jugit.fz-juelich.de/r.jaepel/API_test_project_output"]) + print(result.output) + os.chdir("..") + assert result.exit_code == 0 + + +def test_02b_clone(): + os.chdir("..") + if os.path.exists("test_repo_cli_cloned"): + delete_path("test_repo_cli_cloned") + result = runner.invoke(cli, ["clone", "test_repo_cli", "test_repo_cli_cloned"]) + print(result.output) + os.chdir("test_repo_cli") + assert result.exit_code == 0 + + +def test_03_commit_results_with_uncommited_code_changes(): + modify_code(".") + + result = runner.invoke(cli, ["run", "python", "print_random_number.py", + "create data"]) + print(result.output) + assert result.exit_code != 0 + + +def test_04_commit_code(): + modify_code(".") + + result = runner.invoke(cli, ["commit", "-m", "add code", "-a"]) + print(result.output) + assert result.exit_code == 0 + + +# def test_05_commit_results(): +# result = runner.invoke(cli, ["commit", "-m", "add code", "-a"]) +# print(result.output) +# assert result.exit_code == 0 +# result = runner.invoke(cli, ["run", "python", "print_random_number.py", +# "create data"]) +# print(result.output) +# assert result.exit_code == 0 +# +# +# def test_05b_execute_command(): +# result = runner.invoke(cli, ["commit", "-m", "add code", "-a"]) +# print(result.output) +# assert result.exit_code == 0 +# result = runner.invoke(cli, ["run", "command", "python", "print_random_number.py", +# "create data"]) +# print(result.output) +# assert result.exit_code == 0 + + +def test_06_print_log(): + result = runner.invoke(cli, ["log"]) + print(result.output) + assert result.exit_code == 0 + + +def test_07_lfs_add(): + result = runner.invoke(cli, ["lfs", "add", "pptx"]) + print(result.output) + assert result.exit_code == 0 + + +def test_08_data_import(): + result = runner.invoke(cli, + ["data", "import", "https://github.com/ronald-jaepel/workshop_demo_output", + "2023-12-19_10-43-15_output_from_master_86541bc", "imported/repo/data"]) + print(result.output) + assert result.exit_code == 0 + +# def test_09_data_verify(): +# with open() +# result = runner.invoke(cli, ["data", "verify"]) -- GitLab