Skip to content

Commit b095e78

Browse files
authored
Merge pull request #2 from uzh-dqbm-cmi/config
Add config for easier command line usage
2 parents 7795072 + 738ba9f commit b095e78

3 files changed

Lines changed: 184 additions & 22 deletions

File tree

README.md

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,49 @@
11
# code_sync
2-
Python utility for syncing code to a remote machine in the background
2+
`code_sync` auto-syncs your code changes in a local directory to a remote machine,
3+
so that you can edit your code in your local editor and instantly run those change on a remote machine.
4+
5+
Under the hood, `code_sync` is running an `rsync` command whenever `watchdog` notices changes to the code.
36

47
## Installation
58
`pip install code_sync`
69

10+
After installing this package, the `code_sync` tool will be available from the command line.
11+
12+
713
## Usage
814

9-
After installing this package, the `code_sync` tool will be available from the command line.
15+
#### Register a project
16+
code_sync --register <project>
17+
This will prompt you to enter the local directory to sync,
18+
the remote machines to sync to,
19+
and the destination path on the remote to sync the files to.
20+
21+
Once you register a project with `code_sync`, it will remember that configuration.
22+
23+
#### code_sync a registered project
24+
code_sync <project>
25+
This command will use the configuration you set for the project when you registered it.
1026

11-
The `code_sync` script allows you to auto-sync any changes to code in a local directory to a remote machine.
12-
Under the hood, it is running an `rsync` command whenever `watchdog` notices changes to the code.
27+
#### List all projects registered to code_sync
28+
code_sync --list
1329

14-
### Example usage
15-
Assuming you have defined `my_remote_machine` in your ssh config:
30+
#### Run code_sync with specific parameters
31+
code_sync --local_dir <mylocaldir/> --remote_dir <myremotedir/> --target <ssh_remote> --port 2222\n
1632

17-
`code_sync --local_dir mylocaldir/ --remote_dir myremotedir/ --target my_remote_machine --port 2222`
1833

1934
### Notes
2035
**Starting**
21-
* 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.
22-
* When you start this script, nothing will happen until a file in the `local_dir` is touched. This is normal!
36+
* In order to run `code_sync`, you must have an ssh connection open in another window.
37+
Once you've entered your password there, `code_sync` uses that connection.
38+
* When you start this script, nothing will be synced until a file in the `local_dir` is touched. This is normal!
39+
* The destination dir must exist already, but need not be empty.
2340

2441
**Stopping**
2542
* You can safely quit `code_sync` with control-c.
2643

2744
**About `code_sync` + `git`**
45+
* `code_sync` does not sync files that are excluded by `.gitignore`, if present in the local directory.
46+
It also does not sync `.git` and `.ipynb` files.
2847
* The destination directory should not be treated as an active git repo.
29-
The destination dir must exist already, but need not already be empty.
30-
If the destination directory is a git repo already, it will be overwritten with the "git state" of the local git directory.
3148
* **Do not run git commands from the destination terminal** on the destination directory.
3249
The destination dir will have its contents synced to exactly match the local dir, including when you checkout a different branch on local.
33-
* The sync command adheres to any filters set by `.gitignore` files within the specified directories.
34-
It also excludes `.git` and `.ipynb` files.

code_sync/code_sync.py

Lines changed: 155 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,186 @@
22

33
import argparse
44
import os
5+
from pathlib import Path
56
import subprocess
7+
import yaml
8+
from typing import Dict
9+
610

711
cmd_str = 'watchmedo shell-command --recursive --patterns="{local_dir}*" --command="rsync --filter=\':- .gitignore\' ' \
812
'--exclude \'*.ipynb\' --exclude \'.git\' --delete-after -rz --port {port} {local_dir} ' \
913
'{target}:{remote_dir}" {local_dir}'
1014

1115
epilog_str = '''
12-
Example for connecting to LeoMed:
13-
code_sync --local_dir mylocaldir/ --remote_dir myremotedir/ --target medinfmk --port 2222\n
16+
EXAMPLE USAGE
17+
Register a project:
18+
code_sync --register <project>
19+
20+
code_sync a registered project:
21+
code_sync <project>
22+
23+
List all projects registered to code_sync:
24+
code_sync --list
25+
26+
Run code_sync with specific parameters:
27+
code_sync --local_dir <mylocaldir/> --remote_dir <myremotedir/> --target <ssh_remote> --port 2222\n
1428
1529
'''
1630

31+
CONFIG_FILE_NAME = '.code_sync'
32+
1733

1834
def code_sync(local_dir, remote_dir, target, port=22):
1935
# clean up slashes
2036
local_dir = os.path.join(local_dir, '')
2137
remote_dir = os.path.join(remote_dir, '')
2238

23-
# subprocess.call()
39+
print(f"Starting code_sync between {local_dir} and {target}:{remote_dir} ...")
40+
print('(^C to quit)')
2441
cmd = cmd_str.format(local_dir=local_dir, remote_dir=remote_dir, target=target, port=port)
2542
subprocess.call(cmd, shell=True)
2643

2744

45+
def get_config_file_path() -> Path:
46+
return Path(Path.home(), CONFIG_FILE_NAME)
47+
48+
49+
def load_config() -> Dict:
50+
"""
51+
Load the code_sync config file. Create a blank one if no file exists.
52+
53+
Returns:
54+
The config loaded from the file.
55+
"""
56+
57+
create_config_if_not_exists()
58+
59+
config_file_path = get_config_file_path()
60+
with open(config_file_path, 'r') as f:
61+
config = yaml.safe_load(f)
62+
# if config is empty, return an empty dictionary (not None)
63+
if config is None:
64+
config = {}
65+
return config
66+
67+
68+
def init_config() -> None:
69+
"""Create an empty config file."""
70+
config_path = get_config_file_path()
71+
open(config_path.__str__(), 'x').close()
72+
73+
74+
def create_config_if_not_exists() -> None:
75+
"""Create the code_sync config if it does not already exist."""
76+
config_file_path = get_config_file_path()
77+
if not config_file_path.exists():
78+
init_config()
79+
80+
81+
def register_project(project: str) -> None:
82+
"""
83+
Register a project to the code_sync config.
84+
85+
Args:
86+
project: The name of the project to register.
87+
88+
Returns:
89+
None. The result is saved to the code_sync config.
90+
91+
Raises:
92+
ValueError if there is already a registered project with the given name.
93+
94+
"""
95+
config = load_config()
96+
if project in config:
97+
raise ValueError(f"Project '{project}' is already registered")
98+
99+
print(f"Registering new project '{project}'")
100+
local_dir = input('Path to code_sync on this local machine: ')
101+
target = input('Destination machine: ')
102+
remote_dir = input('Path on the destination machine to sync: ')
103+
port = int(input('Port number to use (default 22): ') or "22")
104+
105+
config_entry_data = {
106+
project: {
107+
'local_dir': local_dir,
108+
'target': target,
109+
'remote_dir': remote_dir,
110+
'port': port,
111+
112+
}
113+
}
114+
115+
create_config_if_not_exists()
116+
config_file_path = get_config_file_path()
117+
with open(config_file_path.__str__(), 'a') as f:
118+
yaml.dump(config_entry_data, f, default_flow_style=False, indent=4)
119+
120+
print(f"Successfully registered project '{project}'")
121+
return
122+
123+
124+
def list_projects() -> None:
125+
"""List all projects registered to code_sync."""
126+
create_config_if_not_exists()
127+
config = load_config()
128+
if len(config) == 0:
129+
print('No projects registered')
130+
else:
131+
formatted_keys = ', '.join(list(config.keys()))
132+
print(formatted_keys)
133+
return
134+
135+
136+
def identify_code_sync_parameters(args) -> Dict:
137+
"""
138+
Identify the code_sync parameters. The user may specify a project (which should be registered to the code_sync
139+
config) or specific all command line arguments.
140+
Args:
141+
args: The args object from argparse.
142+
143+
Returns:
144+
Dictionary of the parameters to be used for the code_sync command.
145+
146+
Raises:
147+
ValueError if the specified project is not registered to the code_sync config.
148+
"""
149+
if args.project is not None:
150+
config = load_config()
151+
if args.project not in config:
152+
raise ValueError(f"Project '{args.project}' is not registered")
153+
parameters = config[args.project]
154+
else:
155+
if args.local_dir is None or args.remote_dir is None or args.target is None:
156+
raise ValueError('Missing argument. If a project is not specified, then local_dir, remote_dir, and target'
157+
' must be specified.')
158+
parameters = dict()
159+
parameters['local_dir'] = args.local_dir
160+
parameters['remote_dir'] = args.remote_dir
161+
parameters['target'] = args.target
162+
parameters['port'] = args.local_dir
163+
return parameters
164+
165+
28166
def main():
29167
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, epilog=epilog_str)
30-
parser.add_argument('--local_dir', help='the local code directory you want to sync', required=True)
31-
parser.add_argument('--remote_dir', help='the remote directory you want to sync', required=True)
32-
parser.add_argument('--target', help='specify which remote machine to connect to', required=True)
168+
parser.add_argument('project', nargs='?', default=None)
169+
parser.add_argument('--register', help='Register a new project to code_sync', required=False)
170+
parser.add_argument('--list', action='store_true', help='List all registered projects', required=False)
171+
parser.add_argument('--local_dir', help='The local code directory you want to sync', required=False)
172+
parser.add_argument('--remote_dir', help='The remote directory you want to sync', required=False)
173+
parser.add_argument('--target', help='Specify which remote machine to connect to', required=False)
33174
parser.add_argument('--port', type=int, help='ssh port for connecting to remote', default=22)
34-
35175
args = parser.parse_args()
36176

37-
code_sync(local_dir=args.local_dir, remote_dir=args.remote_dir, target=args.target, port=args.port)
177+
if args.register is not None:
178+
register_project(args.register)
179+
elif args.list:
180+
list_projects()
181+
else:
182+
params = identify_code_sync_parameters(args)
183+
code_sync(local_dir=params['local_dir'], remote_dir=params['remote_dir'], target=params['target'],
184+
port=params['port'])
38185

39186

40187
if __name__ == '__main__':

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from setuptools import setup, find_packages
22

33
setup(name='code_sync',
4-
version='0.0.1',
4+
version='0.1.0',
55
description='',
66
url='https://github.com/uzh-dqbm-cmi/code-sync',
77
packages=find_packages(),

0 commit comments

Comments
 (0)