From 46d2759b376332d3af9bd78eeab8b869ee837e59 Mon Sep 17 00:00:00 2001 From: lokijuhy Date: Wed, 7 Apr 2021 21:55:19 +0200 Subject: [PATCH 1/3] add config for easier use --- code_sync/code_sync.py | 162 +++++++++++++++++++++++++++++++++++++++-- setup.py | 2 +- 2 files changed, 155 insertions(+), 9 deletions(-) diff --git a/code_sync/code_sync.py b/code_sync/code_sync.py index 2b5558f..36850fb 100644 --- a/code_sync/code_sync.py +++ b/code_sync/code_sync.py @@ -2,39 +2,185 @@ import argparse import os +from pathlib import Path import subprocess +import yaml +from typing import Dict, Union + cmd_str = 'watchmedo shell-command --recursive --patterns="{local_dir}*" --command="rsync --filter=\':- .gitignore\' ' \ '--exclude \'*.ipynb\' --exclude \'.git\' --delete-after -rz --port {port} {local_dir} ' \ '{target}:{remote_dir}" {local_dir}' epilog_str = ''' -Example for connecting to LeoMed: - code_sync --local_dir mylocaldir/ --remote_dir myremotedir/ --target medinfmk --port 2222\n +EXAMPLE USAGE +Register a project: + code_sync --register + +code_sync a registered project: + code_sync + +List all projects registered to code_sync: + code_sync --list + +Run code_sync with specific parameters: + code_sync --local_dir --remote_dir --target --port 2222\n ''' +CONFIG_FILE_NAME = '.code_sync' + def code_sync(local_dir, remote_dir, target, port=22): # clean up slashes local_dir = os.path.join(local_dir, '') remote_dir = os.path.join(remote_dir, '') - # subprocess.call() + print("Starting code_sync between {} and {}:{} ...".format(local_dir, target, remote_dir)) + print('(^C to quit)') cmd = cmd_str.format(local_dir=local_dir, remote_dir=remote_dir, target=target, port=port) subprocess.call(cmd, shell=True) +def load_config() -> Dict: + """ + Load the code_sync config file. Create a blank one if no file exists. + + Returns: + The config loaded from the file. + """ + + create_config_if_not_exists() + + config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + with open(config_file_path, 'r') as f: + config = yaml.safe_load(f) + # if config is empty, return an empty dictionary (not None) + if config is None: + config = {} + return config + + +def init_config() -> None: + """Create an empty config file.""" + config_path = Path(Path.home(), CONFIG_FILE_NAME) + open(config_path.__str__(), 'x').close() + + +def create_config_if_not_exists() -> None: + """Create the code_sync config if it does not already exist.""" + config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + if not config_file_path.exists(): + init_config() + + +def register_project(project: Union[str, None]) -> None: + """ + Register a project to the code_sync config. + + Args: + project: The name of the project to register. If None is provided, the user will be asked for a project name. + + Returns: + None. The result is saved to the code_sync config. + + Raises: + ValueError if there is already a registered project with the given name. + + """ + if project is None: + project = input('Project name: ') + else: + print("Registering new project '{}'".format(project)) + + config = load_config() + if project in config: + raise ValueError("Project '{}' is already registered".format(project)) + + local_dir = input('Path to code_sync on this local machine: ') + target = input('Destination machine: ') + remote_dir = input('Path on the destination machine to sync: ') + port = int(input('Port number to use (default 22): ') or "22") + + config_entry_data = { + project: { + 'local_dir': local_dir, + 'target': target, + 'remote_dir': remote_dir, + 'port': port, + + } + } + + create_config_if_not_exists() + config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + with open(config_file_path.__str__(), 'a') as f: + yaml.dump(config_entry_data, f, default_flow_style=False) + + print("Successfully registered project '{}'".format(project)) + return + + +def list_projects() -> None: + """List all projects registered to code_sync.""" + create_config_if_not_exists() + config = load_config() + if len(config) == 0: + print('No projects registered') + else: + formatted_keys = ', '.join(list(config.keys())) + print(formatted_keys) + return + +def identify_code_sync_parameters(args) -> Dict: + """ + Identify the code_sync parameters. The user may specify a project (which should be registered to the code_sync + config) or specific all command line arguments. + Args: + args: The args object from argparse. + + Returns: + Dictionary of the parameters to be used for the code_sync command. + + Raises: + ValueError if the specified project is not registered to the code_sync config. + """ + if args.project is not None: + config = load_config() + if args.project not in config: + raise ValueError("Project '{}' is not registered".format(args.project)) + parameters = config[args.project] + else: + if args.local_dir is None or args.remote_dir is None or args.target is None: + raise ValueError('Missing argument. If a project is not specified, then local_dir, remote_dir, and target' + ' must be specified.') + parameters = dict() + parameters['local_dir'] = args.local_dir + parameters['remote_dir'] = args.remote_dir + parameters['target'] = args.target + parameters['port'] = args.local_dir + return parameters + + def main(): parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog_str) - parser.add_argument('--local_dir', help='the local code directory you want to sync', required=True) - parser.add_argument('--remote_dir', help='the remote directory you want to sync', required=True) - parser.add_argument('--target', help='specify which remote machine to connect to', required=True) + parser.add_argument('project', nargs='?', default=None) + parser.add_argument('--register', help='Register a new project to code_sync', required=False) + parser.add_argument('--list', action='store_true', help='List all registered projects', required=False) + parser.add_argument('--local_dir', help='The local code directory you want to sync', required=False) + parser.add_argument('--remote_dir', help='The remote directory you want to sync', required=False) + parser.add_argument('--target', help='Specify which remote machine to connect to', required=False) parser.add_argument('--port', type=int, help='ssh port for connecting to remote', default=22) - args = parser.parse_args() - code_sync(local_dir=args.local_dir, remote_dir=args.remote_dir, target=args.target, port=args.port) + if args.register is not None: + register_project(args.register) + elif args.list: + list_projects() + else: + params = identify_code_sync_parameters(args) + code_sync(local_dir=params['local_dir'], remote_dir=params['remote_dir'], target=params['target'], + port=params['port']) if __name__ == '__main__': diff --git a/setup.py b/setup.py index 356f7b6..43ab5c6 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ from setuptools import setup, find_packages setup(name='code_sync', - version='0.0.1', + version='0.1.0', description='', url='https://github.com/uzh-dqbm-cmi/code-sync', packages=find_packages(), From 46f67024e4dc7e91a1e927352c043a5678bb762e Mon Sep 17 00:00:00 2001 From: lokijuhy Date: Wed, 7 Apr 2021 22:10:57 +0200 Subject: [PATCH 2/3] update readme --- README.md | 41 ++++++++++++++++++++++++++++------------- code_sync/code_sync.py | 1 + 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 968990d..9b3ee91 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,49 @@ # code_sync -Python utility for syncing code to a remote machine in the background +`code_sync` auto-syncs your code changes in a local directory to a remote machine, +so that you can edit your code in your local editor and instantly run those change on a remote machine. + +Under the hood, `code_sync` is running an `rsync` command whenever `watchdog` notices changes to the code. ## Installation `pip install code_sync` +After installing this package, the `code_sync` tool will be available from the command line. + + ## Usage -After installing this package, the `code_sync` tool will be available from the command line. +#### Register a project + code_sync --register +This will prompt you to enter the local directory to sync, +the remote machines to sync to, +and the destination path on the remote to sync the files to. + +Once you register a project with `code_sync`, it will remember that configuration. + +#### code_sync a registered project + code_sync +This command will use the configuration you set for the project when you registered it. -The `code_sync` script allows you to auto-sync any changes to code in a local directory to a remote machine. -Under the hood, it is running an `rsync` command whenever `watchdog` notices changes to the code. +#### List all projects registered to code_sync + code_sync --list -### Example usage -Assuming you have defined `my_remote_machine` in your ssh config: +#### Run code_sync with specific parameters + code_sync --local_dir --remote_dir --target --port 2222\n -`code_sync --local_dir mylocaldir/ --remote_dir myremotedir/ --target my_remote_machine --port 2222` ### Notes **Starting** -* In order to run `code_sync`, you must have an ssh connection open in another window. Once you've entered your password there, `code_sync` uses that connection. -* When you start this script, nothing will happen until a file in the `local_dir` is touched. This is normal! +* In order to run `code_sync`, you must have an ssh connection open in another window. +Once you've entered your password there, `code_sync` uses that connection. +* When you start this script, nothing will be synced until a file in the `local_dir` is touched. This is normal! +* The destination dir must exist already, but need not be empty. **Stopping** * You can safely quit `code_sync` with control-c. **About `code_sync` + `git`** +* `code_sync` does not sync files that are excluded by `.gitignore`, if present in the local directory. +It also does not sync `.git` and `.ipynb` files. * The destination directory should not be treated as an active git repo. -The destination dir must exist already, but need not already be empty. -If the destination directory is a git repo already, it will be overwritten with the "git state" of the local git directory. * **Do not run git commands from the destination terminal** on the destination directory. The destination dir will have its contents synced to exactly match the local dir, including when you checkout a different branch on local. -* The sync command adheres to any filters set by `.gitignore` files within the specified directories. -It also excludes `.git` and `.ipynb` files. diff --git a/code_sync/code_sync.py b/code_sync/code_sync.py index 36850fb..2525c65 100644 --- a/code_sync/code_sync.py +++ b/code_sync/code_sync.py @@ -132,6 +132,7 @@ def list_projects() -> None: print(formatted_keys) return + def identify_code_sync_parameters(args) -> Dict: """ Identify the code_sync parameters. The user may specify a project (which should be registered to the code_sync From 738ba9f9d92a54bbdde6c4f51f9a598a28310eeb Mon Sep 17 00:00:00 2001 From: lokijuhy Date: Tue, 13 Apr 2021 16:33:23 +0200 Subject: [PATCH 3/3] addressed PR feedback --- code_sync/code_sync.py | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/code_sync/code_sync.py b/code_sync/code_sync.py index 2525c65..e87f9d3 100644 --- a/code_sync/code_sync.py +++ b/code_sync/code_sync.py @@ -5,7 +5,7 @@ from pathlib import Path import subprocess import yaml -from typing import Dict, Union +from typing import Dict cmd_str = 'watchmedo shell-command --recursive --patterns="{local_dir}*" --command="rsync --filter=\':- .gitignore\' ' \ @@ -36,12 +36,16 @@ def code_sync(local_dir, remote_dir, target, port=22): local_dir = os.path.join(local_dir, '') remote_dir = os.path.join(remote_dir, '') - print("Starting code_sync between {} and {}:{} ...".format(local_dir, target, remote_dir)) + print(f"Starting code_sync between {local_dir} and {target}:{remote_dir} ...") print('(^C to quit)') cmd = cmd_str.format(local_dir=local_dir, remote_dir=remote_dir, target=target, port=port) subprocess.call(cmd, shell=True) +def get_config_file_path() -> Path: + return Path(Path.home(), CONFIG_FILE_NAME) + + def load_config() -> Dict: """ Load the code_sync config file. Create a blank one if no file exists. @@ -52,7 +56,7 @@ def load_config() -> Dict: create_config_if_not_exists() - config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + config_file_path = get_config_file_path() with open(config_file_path, 'r') as f: config = yaml.safe_load(f) # if config is empty, return an empty dictionary (not None) @@ -63,23 +67,23 @@ def load_config() -> Dict: def init_config() -> None: """Create an empty config file.""" - config_path = Path(Path.home(), CONFIG_FILE_NAME) + config_path = get_config_file_path() open(config_path.__str__(), 'x').close() def create_config_if_not_exists() -> None: """Create the code_sync config if it does not already exist.""" - config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + config_file_path = get_config_file_path() if not config_file_path.exists(): init_config() -def register_project(project: Union[str, None]) -> None: +def register_project(project: str) -> None: """ Register a project to the code_sync config. Args: - project: The name of the project to register. If None is provided, the user will be asked for a project name. + project: The name of the project to register. Returns: None. The result is saved to the code_sync config. @@ -88,15 +92,11 @@ def register_project(project: Union[str, None]) -> None: ValueError if there is already a registered project with the given name. """ - if project is None: - project = input('Project name: ') - else: - print("Registering new project '{}'".format(project)) - config = load_config() if project in config: - raise ValueError("Project '{}' is already registered".format(project)) + raise ValueError(f"Project '{project}' is already registered") + print(f"Registering new project '{project}'") local_dir = input('Path to code_sync on this local machine: ') target = input('Destination machine: ') remote_dir = input('Path on the destination machine to sync: ') @@ -113,11 +113,11 @@ def register_project(project: Union[str, None]) -> None: } create_config_if_not_exists() - config_file_path = Path(Path.home(), CONFIG_FILE_NAME) + config_file_path = get_config_file_path() with open(config_file_path.__str__(), 'a') as f: - yaml.dump(config_entry_data, f, default_flow_style=False) + yaml.dump(config_entry_data, f, default_flow_style=False, indent=4) - print("Successfully registered project '{}'".format(project)) + print(f"Successfully registered project '{project}'") return @@ -149,7 +149,7 @@ def identify_code_sync_parameters(args) -> Dict: if args.project is not None: config = load_config() if args.project not in config: - raise ValueError("Project '{}' is not registered".format(args.project)) + raise ValueError(f"Project '{args.project}' is not registered") parameters = config[args.project] else: if args.local_dir is None or args.remote_dir is None or args.target is None: