Skip to content

Commit

Permalink
Merge pull request #2 from uzh-dqbm-cmi/config
Browse files Browse the repository at this point in the history
Add config for easier command line usage
  • Loading branch information
lokijuhy authored Apr 19, 2021
2 parents 7795072 + 738ba9f commit b095e78
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 22 deletions.
41 changes: 28 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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 <project>
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 <project>
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 <mylocaldir/> --remote_dir <myremotedir/> --target <ssh_remote> --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.
163 changes: 155 additions & 8 deletions code_sync/code_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,186 @@

import argparse
import os
from pathlib import Path
import subprocess
import yaml
from typing import Dict


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 <project>
code_sync a registered project:
code_sync <project>
List all projects registered to code_sync:
code_sync --list
Run code_sync with specific parameters:
code_sync --local_dir <mylocaldir/> --remote_dir <myremotedir/> --target <ssh_remote> --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(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.
Returns:
The config loaded from the file.
"""

create_config_if_not_exists()

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)
if config is None:
config = {}
return config


def init_config() -> None:
"""Create an empty config file."""
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 = get_config_file_path()
if not config_file_path.exists():
init_config()


def register_project(project: str) -> None:
"""
Register a project to the code_sync config.
Args:
project: The name of the project to register.
Returns:
None. The result is saved to the code_sync config.
Raises:
ValueError if there is already a registered project with the given name.
"""
config = load_config()
if project in config:
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: ')
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 = get_config_file_path()
with open(config_file_path.__str__(), 'a') as f:
yaml.dump(config_entry_data, f, default_flow_style=False, indent=4)

print(f"Successfully registered project '{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(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:
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__':
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
@@ -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(),
Expand Down

0 comments on commit b095e78

Please sign in to comment.