LiveSync 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {LiveSync-0.3.1.dist-info → LiveSync-0.3.3.dist-info}/METADATA +19 -5
- LiveSync-0.3.3.dist-info/RECORD +12 -0
- livesync/folder.py +9 -6
- livesync/livesync.py +2 -1
- livesync/sync.py +16 -14
- LiveSync-0.3.1.dist-info/RECORD +0 -12
- {LiveSync-0.3.1.dist-info → LiveSync-0.3.3.dist-info}/LICENSE +0 -0
- {LiveSync-0.3.1.dist-info → LiveSync-0.3.3.dist-info}/WHEEL +0 -0
- {LiveSync-0.3.1.dist-info → LiveSync-0.3.3.dist-info}/entry_points.txt +0 -0
- {LiveSync-0.3.1.dist-info → LiveSync-0.3.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: LiveSync
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Repeatedly synchronize local workspace with a (slow) remote machine
|
|
5
5
|
Home-page: https://github.com/zauberzeug/livesync
|
|
6
6
|
Author: Zauberzeug GmbH
|
|
@@ -28,8 +28,9 @@ It is available as [PyPI package](https://pypi.org/project/livesync/) and hosted
|
|
|
28
28
|
|
|
29
29
|
[VS Code Remote Development](https://code.visualstudio.com/docs/remote/remote-overview) and similar tools are great as long as your remote machine is powerful enough.
|
|
30
30
|
But if your target is a Raspberry Pi, Jetson Nano/Xavier/Orin, Beagle Board or similar, it feels like coding in jelly.
|
|
31
|
-
Especially if you run powerful extensions like Pylance.
|
|
31
|
+
Especially if you run powerful extensions like Pylance, GitHub Copilot or Duet AI.
|
|
32
32
|
LiveSync solves this by watching your code for changes and just copying the modifications to the slow remote machine.
|
|
33
|
+
So you can develop on your own machine (and run tests there in the background) while all your changes appear also on the remote.
|
|
33
34
|
It works best if you have some kind of reload mechanism in place on the target ([NiceGUI](https://nicegui.io), [FastAPI](https://fastapi.tiangolo.com/) or [Flask](https://flask.palletsprojects.com/) for example).
|
|
34
35
|
|
|
35
36
|
## Usage
|
|
@@ -61,10 +62,12 @@ Options:
|
|
|
61
62
|
command to be executed on remote host after any file change (default: None)
|
|
62
63
|
- `--mutex-interval MUTEX_INTERVAL`
|
|
63
64
|
interval in which mutex is updated (default: 10 seconds)
|
|
65
|
+
- `--ignore-mutex`
|
|
66
|
+
ignore mutex (use with caution) (default: False)
|
|
64
67
|
|
|
65
68
|
### Python
|
|
66
69
|
|
|
67
|
-
Simple example:
|
|
70
|
+
Simple example (where `robot` is the ssh hostname of the target system):
|
|
68
71
|
|
|
69
72
|
```py
|
|
70
73
|
from livesync import Folder, sync
|
|
@@ -75,14 +78,25 @@ sync(
|
|
|
75
78
|
)
|
|
76
79
|
```
|
|
77
80
|
|
|
81
|
+
The `sync` call will block until the script is aborted.
|
|
82
|
+
The `Folder` class allows to set the `port` and an `on_change` bash command which is executed after a sync has been performed.
|
|
83
|
+
Via the `rsync_args` build method you can pass additional options to configure rsync.
|
|
84
|
+
|
|
78
85
|
Advanced example:
|
|
79
86
|
|
|
80
87
|
```py
|
|
88
|
+
import argparse
|
|
81
89
|
from livesync import Folder, sync
|
|
82
90
|
|
|
91
|
+
parser = argparse.ArgumentParser(description='Sync local code with robot.')
|
|
92
|
+
parser.add_argument('robot', help='Robot hostname')
|
|
93
|
+
|
|
94
|
+
args = parser.parse_args()
|
|
95
|
+
|
|
96
|
+
touch = 'touch ~/robot/main.py'
|
|
83
97
|
sync(
|
|
84
|
-
Folder('.', 'robot:~/navigation', on_change='touch ~/navigation/main.py'),
|
|
85
|
-
Folder('../rosys', 'robot:~/rosys'
|
|
98
|
+
Folder('.', f'{args.robot}:~/navigation', on_change='touch ~/navigation/main.py'),
|
|
99
|
+
Folder('../rosys', f'{args.robot}:~/rosys').rsync_args(add='-L', remove='--checksum'),
|
|
86
100
|
mutex_interval=30,
|
|
87
101
|
)
|
|
88
102
|
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
livesync/__init__.py,sha256=tZrvnAIpVuo9cQHMCvi6l2vup66NXPxRwyR5UBNtCTs,50
|
|
2
|
+
livesync/folder.py,sha256=E_HWXLW40fhoCW_NQh6liEe3gSW9ckIga6rYdQ3DEzc,4351
|
|
3
|
+
livesync/livesync.py,sha256=2kNwtM3a1r5moAXCD4BcK0UJYa0EdZP5PTUNiEqKI6g,1294
|
|
4
|
+
livesync/mutex.py,sha256=nHaP0Tge3ZV7jyVBbYsMug5L5YkuJf8_XIbfAdVJkPc,1741
|
|
5
|
+
livesync/run_subprocess.py,sha256=ZZqK9dlOVlJhL0xkKN7bG-BAT558nHk-IJNGqIMeWyM,368
|
|
6
|
+
livesync/sync.py,sha256=9cQwtdLHIBvenzyq0HRJOagPm1OGSDpi43vLBa2aNUs,1635
|
|
7
|
+
LiveSync-0.3.3.dist-info/LICENSE,sha256=QcBlwggRQYhvfTAE481AAMFQfMS_N6Bj8Svh1T6dsnI,1072
|
|
8
|
+
LiveSync-0.3.3.dist-info/METADATA,sha256=cke14IwKy3EKV267y4lceiPRLvyWqjBIY_f-0_7ZG-g,5335
|
|
9
|
+
LiveSync-0.3.3.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
+
LiveSync-0.3.3.dist-info/entry_points.txt,sha256=4dn5YR27lUlJWea3yqLKLqR4MwqjcqzMR5Q4jVFnytI,52
|
|
11
|
+
LiveSync-0.3.3.dist-info/top_level.txt,sha256=mLwExc6wTUGqxvUkYMio5rxGS1h8bvxpNsR2ebfjSL4,9
|
|
12
|
+
LiveSync-0.3.3.dist-info/RECORD,,
|
livesync/folder.py
CHANGED
|
@@ -55,19 +55,22 @@ class Folder:
|
|
|
55
55
|
path = self.source_path / '.syncignore'
|
|
56
56
|
if not path.is_file():
|
|
57
57
|
path.write_text('\n'.join(self.DEFAULT_IGNORES))
|
|
58
|
-
|
|
58
|
+
ignores = [line.strip() for line in path.read_text().splitlines() if not line.startswith('#')]
|
|
59
|
+
ignores += [ignore.rstrip('/\\') for ignore in ignores if ignore.endswith('/') or ignore.endswith('\\')]
|
|
60
|
+
return ignores
|
|
59
61
|
|
|
60
62
|
def get_summary(self) -> str:
|
|
61
63
|
summary = f'{self.source_path} --> {self.target}\n'
|
|
62
|
-
if not (self.source_path / '.git').exists():
|
|
63
|
-
return summary
|
|
64
64
|
try:
|
|
65
|
+
cmd = ['git', 'rev-parse', '--is-inside-work-tree']
|
|
66
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
67
|
+
except subprocess.CalledProcessError:
|
|
68
|
+
pass # not a git repo, git is not installed, or something else
|
|
69
|
+
else:
|
|
65
70
|
cmd = ['git', 'log', '--pretty=format:[%h]\n', '-n', '1']
|
|
66
71
|
summary += subprocess.check_output(cmd, cwd=self.source_path).decode()
|
|
67
72
|
cmd = ['git', 'status', '--short', '--branch']
|
|
68
73
|
summary += subprocess.check_output(cmd, cwd=self.source_path).decode().strip() + '\n'
|
|
69
|
-
except Exception:
|
|
70
|
-
pass # maybe git is not installed
|
|
71
74
|
return summary
|
|
72
75
|
|
|
73
76
|
async def watch(self) -> None:
|
|
@@ -86,7 +89,7 @@ class Folder:
|
|
|
86
89
|
args += ''.join(f' --exclude="{e}"' for e in self._get_ignores())
|
|
87
90
|
args += f' -e "ssh -p {self.ssh_port}"' # NOTE: use SSH with custom port
|
|
88
91
|
args += f' --rsync-path="mkdir -p {self.target_path} && rsync"' # NOTE: create target folder if not exists
|
|
89
|
-
run_subprocess(f'rsync {args} {self.source_path}/ {self.target}/', quiet=True)
|
|
92
|
+
run_subprocess(f'rsync {args} "{self.source_path}/" "{self.target}/"', quiet=True)
|
|
90
93
|
if isinstance(self.on_change, str):
|
|
91
94
|
run_subprocess(f'ssh {self.host} -p {self.ssh_port} "cd {self.target_path}; {self.on_change}"')
|
|
92
95
|
if callable(self.on_change):
|
livesync/livesync.py
CHANGED
|
@@ -13,12 +13,13 @@ def main():
|
|
|
13
13
|
parser.add_argument('--ssh-port', type=int, default=22, help='SSH port on target')
|
|
14
14
|
parser.add_argument('--on-change', type=str, help='command to be executed on remote host after any file change')
|
|
15
15
|
parser.add_argument('--mutex-interval', type=int, default=10, help='interval in which mutex is updated')
|
|
16
|
+
parser.add_argument('--ignore-mutex', action='store_true', help='ignore mutex (use with caution)')
|
|
16
17
|
parser.add_argument('rsync_args', nargs=argparse.REMAINDER, help='arbitrary rsync parameters after "--"')
|
|
17
18
|
args = parser.parse_args()
|
|
18
19
|
|
|
19
20
|
folder = Folder(args.source, args.target, ssh_port=args.ssh_port, on_change=args.on_change)
|
|
20
21
|
folder.rsync_args(' '.join(args.rsync_args))
|
|
21
|
-
sync(folder, mutex_interval=args.mutex_interval)
|
|
22
|
+
sync(folder, mutex_interval=args.mutex_interval, ignore_mutex=args.ignore_mutex)
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
if __name__ == '__main__':
|
livesync/sync.py
CHANGED
|
@@ -10,15 +10,16 @@ def get_summary(folders: Iterable[Folder]) -> str:
|
|
|
10
10
|
return '\n'.join(folder.get_summary() for folder in folders).replace('"', '\'')
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
async def run_folder_tasks(folders: Iterable[Folder], mutex_interval: float) -> None:
|
|
13
|
+
async def run_folder_tasks(folders: Iterable[Folder], mutex_interval: float, ignore_mutex: bool = False) -> None:
|
|
14
14
|
try:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
if not ignore_mutex:
|
|
16
|
+
summary = get_summary(folders)
|
|
17
|
+
mutexes = {folder.host: Mutex(folder.host, folder.ssh_port) for folder in folders}
|
|
18
|
+
for mutex in mutexes.values():
|
|
19
|
+
print(f'Checking mutex on {mutex.host}', flush=True)
|
|
20
|
+
if not mutex.set(summary):
|
|
21
|
+
print(f'Target is in use by {mutex.occupant}')
|
|
22
|
+
sys.exit(1)
|
|
22
23
|
|
|
23
24
|
for folder in folders:
|
|
24
25
|
print(f' {folder.source_path} --> {folder.target}', flush=True)
|
|
@@ -29,17 +30,18 @@ async def run_folder_tasks(folders: Iterable[Folder], mutex_interval: float) ->
|
|
|
29
30
|
asyncio.create_task(folder.watch())
|
|
30
31
|
|
|
31
32
|
while True:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
if not ignore_mutex:
|
|
34
|
+
summary = get_summary(folders)
|
|
35
|
+
for mutex in mutexes.values():
|
|
36
|
+
if not mutex.set(summary):
|
|
37
|
+
break
|
|
36
38
|
await asyncio.sleep(mutex_interval)
|
|
37
39
|
except Exception as e:
|
|
38
40
|
print(e)
|
|
39
41
|
|
|
40
42
|
|
|
41
|
-
def sync(*folders: Folder, mutex_interval: float = 10) -> None:
|
|
43
|
+
def sync(*folders: Folder, mutex_interval: float = 10, ignore_mutex: bool = False) -> None:
|
|
42
44
|
try:
|
|
43
|
-
asyncio.run(run_folder_tasks(folders, mutex_interval))
|
|
45
|
+
asyncio.run(run_folder_tasks(folders, mutex_interval, ignore_mutex=ignore_mutex))
|
|
44
46
|
except KeyboardInterrupt:
|
|
45
47
|
print('Bye!')
|
LiveSync-0.3.1.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
livesync/__init__.py,sha256=tZrvnAIpVuo9cQHMCvi6l2vup66NXPxRwyR5UBNtCTs,50
|
|
2
|
-
livesync/folder.py,sha256=09blUNJI-VFPg7FXEgEHXMKffmFf82swNOLvSEgnS7o,4069
|
|
3
|
-
livesync/livesync.py,sha256=gOFq8_mxAGEt3pXt_5EtlpFqLwE5sZwfbRsHiUh-F6k,1159
|
|
4
|
-
livesync/mutex.py,sha256=nHaP0Tge3ZV7jyVBbYsMug5L5YkuJf8_XIbfAdVJkPc,1741
|
|
5
|
-
livesync/run_subprocess.py,sha256=ZZqK9dlOVlJhL0xkKN7bG-BAT558nHk-IJNGqIMeWyM,368
|
|
6
|
-
livesync/sync.py,sha256=r0CWptPcIvYs6uv9iV_JM7My360dSHMeFYAISntpQTA,1446
|
|
7
|
-
LiveSync-0.3.1.dist-info/LICENSE,sha256=QcBlwggRQYhvfTAE481AAMFQfMS_N6Bj8Svh1T6dsnI,1072
|
|
8
|
-
LiveSync-0.3.1.dist-info/METADATA,sha256=kDBdMh9Jd3VQyhCCein4Dij5fBABstkS0ixEfOB3PeM,4576
|
|
9
|
-
LiveSync-0.3.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
-
LiveSync-0.3.1.dist-info/entry_points.txt,sha256=4dn5YR27lUlJWea3yqLKLqR4MwqjcqzMR5Q4jVFnytI,52
|
|
11
|
-
LiveSync-0.3.1.dist-info/top_level.txt,sha256=mLwExc6wTUGqxvUkYMio5rxGS1h8bvxpNsR2ebfjSL4,9
|
|
12
|
-
LiveSync-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|