LiveSync 0.3.3__py3-none-any.whl → 0.4.0__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/folder.py +5 -5
- livesync/livesync.py +2 -1
- livesync/mutex.py +17 -10
- livesync/py.typed +0 -0
- livesync/run_subprocess.py +13 -8
- livesync/sync.py +30 -17
- {LiveSync-0.3.3.dist-info → livesync-0.4.0.dist-info}/METADATA +17 -3
- livesync-0.4.0.dist-info/RECORD +13 -0
- {LiveSync-0.3.3.dist-info → livesync-0.4.0.dist-info}/WHEEL +1 -1
- LiveSync-0.3.3.dist-info/RECORD +0 -12
- {LiveSync-0.3.3.dist-info → livesync-0.4.0.dist-info}/entry_points.txt +0 -0
- {LiveSync-0.3.3.dist-info → livesync-0.4.0.dist-info/licenses}/LICENSE +0 -0
- {LiveSync-0.3.3.dist-info → livesync-0.4.0.dist-info}/top_level.txt +0 -0
livesync/folder.py
CHANGED
|
@@ -13,7 +13,7 @@ from .run_subprocess import run_subprocess
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class Folder:
|
|
16
|
-
DEFAULT_IGNORES = ['.git/', '__pycache__/', '.DS_Store', '*.tmp', '.env']
|
|
16
|
+
DEFAULT_IGNORES = ['.git/', '.jj/', '__pycache__/', '.DS_Store', '*.tmp', '.env', '.venv']
|
|
17
17
|
DEFAULT_RSYNC_ARGS = ['--prune-empty-dirs', '--delete', '-a', '-v', '-z', '--checksum', '--no-t']
|
|
18
18
|
|
|
19
19
|
def __init__(self,
|
|
@@ -79,18 +79,18 @@ class Folder:
|
|
|
79
79
|
watch_filter=lambda _, filepath: not self._ignore_spec.match_file(filepath)):
|
|
80
80
|
for change, filepath in changes:
|
|
81
81
|
print('?+U-'[change], filepath)
|
|
82
|
-
self.sync()
|
|
82
|
+
await self.sync()
|
|
83
83
|
except RuntimeError as e:
|
|
84
84
|
if 'Already borrowed' not in str(e):
|
|
85
85
|
raise
|
|
86
86
|
|
|
87
|
-
def sync(self) -> None:
|
|
87
|
+
async def sync(self) -> None:
|
|
88
88
|
args = ' '.join(self._rsync_args)
|
|
89
89
|
args += ''.join(f' --exclude="{e}"' for e in self._get_ignores())
|
|
90
90
|
args += f' -e "ssh -p {self.ssh_port}"' # NOTE: use SSH with custom port
|
|
91
91
|
args += f' --rsync-path="mkdir -p {self.target_path} && rsync"' # NOTE: create target folder if not exists
|
|
92
|
-
run_subprocess(f'rsync {args} "{self.source_path}/" "{self.target}/"', quiet=True)
|
|
92
|
+
await run_subprocess(f'rsync {args} "{self.source_path}/" "{self.target}/"', quiet=True)
|
|
93
93
|
if isinstance(self.on_change, str):
|
|
94
|
-
run_subprocess(f'ssh {self.host} -p {self.ssh_port} "cd {self.target_path}; {self.on_change}"')
|
|
94
|
+
await run_subprocess(f'ssh {self.host} -p {self.ssh_port} "cd {self.target_path}; {self.on_change}"')
|
|
95
95
|
if callable(self.on_change):
|
|
96
96
|
self.on_change()
|
livesync/livesync.py
CHANGED
|
@@ -14,12 +14,13 @@ def main():
|
|
|
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
16
|
parser.add_argument('--ignore-mutex', action='store_true', help='ignore mutex (use with caution)')
|
|
17
|
+
parser.add_argument('--no-watch', action='store_true', help='do not watch for changes')
|
|
17
18
|
parser.add_argument('rsync_args', nargs=argparse.REMAINDER, help='arbitrary rsync parameters after "--"')
|
|
18
19
|
args = parser.parse_args()
|
|
19
20
|
|
|
20
21
|
folder = Folder(args.source, args.target, ssh_port=args.ssh_port, on_change=args.on_change)
|
|
21
22
|
folder.rsync_args(' '.join(args.rsync_args))
|
|
22
|
-
sync(folder, mutex_interval=args.mutex_interval, ignore_mutex=args.ignore_mutex)
|
|
23
|
+
sync(folder, mutex_interval=args.mutex_interval, ignore_mutex=args.ignore_mutex, watch=not args.no_watch)
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
if __name__ == '__main__':
|
livesync/mutex.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
import socket
|
|
3
|
-
import subprocess
|
|
4
4
|
from datetime import datetime, timedelta
|
|
5
5
|
from typing import Optional
|
|
6
6
|
|
|
@@ -14,10 +14,10 @@ class Mutex:
|
|
|
14
14
|
self.occupant: Optional[str] = None
|
|
15
15
|
self.user_id = socket.gethostname()
|
|
16
16
|
|
|
17
|
-
def is_free(self) -> bool:
|
|
17
|
+
async def is_free(self) -> bool:
|
|
18
18
|
try:
|
|
19
19
|
command = f'[ -f {self.DEFAULT_FILEPATH} ] && cat {self.DEFAULT_FILEPATH} || echo'
|
|
20
|
-
output = self._run_ssh_command(command).strip()
|
|
20
|
+
output = (await self._run_ssh_command(command)).strip()
|
|
21
21
|
if not output:
|
|
22
22
|
return True
|
|
23
23
|
words = output.splitlines()[0].strip().split()
|
|
@@ -30,13 +30,13 @@ class Mutex:
|
|
|
30
30
|
logging.exception('Could not access target system')
|
|
31
31
|
return False
|
|
32
32
|
|
|
33
|
-
def set(self, info: str) -> bool:
|
|
34
|
-
if not self.is_free():
|
|
33
|
+
async def set(self, info: str) -> bool:
|
|
34
|
+
if not await self.is_free():
|
|
35
35
|
return False
|
|
36
36
|
try:
|
|
37
|
-
self._run_ssh_command(f'echo "{self.tag}\n{info}" > {self.DEFAULT_FILEPATH}')
|
|
37
|
+
await self._run_ssh_command(f'echo "{self.tag}\n{info}" > {self.DEFAULT_FILEPATH}')
|
|
38
38
|
return True
|
|
39
|
-
except
|
|
39
|
+
except RuntimeError:
|
|
40
40
|
print('Could not write mutex file')
|
|
41
41
|
return False
|
|
42
42
|
|
|
@@ -44,6 +44,13 @@ class Mutex:
|
|
|
44
44
|
def tag(self) -> str:
|
|
45
45
|
return f'{self.user_id} {datetime.now().isoformat()}'
|
|
46
46
|
|
|
47
|
-
def _run_ssh_command(self, command: str) -> str:
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
async def _run_ssh_command(self, command: str) -> str:
|
|
48
|
+
process = await asyncio.create_subprocess_exec(
|
|
49
|
+
'ssh', self.host, '-p', str(self.port), command,
|
|
50
|
+
stdout=asyncio.subprocess.PIPE,
|
|
51
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
52
|
+
)
|
|
53
|
+
stdout, _ = await process.communicate()
|
|
54
|
+
if process.returncode != 0:
|
|
55
|
+
raise RuntimeError(f'SSH command failed with return code {process.returncode}')
|
|
56
|
+
return stdout.decode()
|
livesync/py.typed
ADDED
|
File without changes
|
livesync/run_subprocess.py
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import subprocess
|
|
2
3
|
|
|
3
4
|
|
|
4
|
-
def run_subprocess(command: str, *, quiet: bool = False) -> None:
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
async def run_subprocess(command: str, *, quiet: bool = False) -> None:
|
|
6
|
+
process = await asyncio.create_subprocess_shell(
|
|
7
|
+
command,
|
|
8
|
+
stdout=asyncio.subprocess.PIPE,
|
|
9
|
+
stderr=asyncio.subprocess.STDOUT,
|
|
10
|
+
)
|
|
11
|
+
stdout, _ = await process.communicate()
|
|
12
|
+
if process.returncode != 0:
|
|
13
|
+
print(stdout.decode())
|
|
14
|
+
raise subprocess.CalledProcessError(process.returncode, command, stdout)
|
|
15
|
+
if not quiet:
|
|
16
|
+
print(stdout.decode())
|
livesync/sync.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import sys
|
|
3
|
-
from typing import Iterable
|
|
3
|
+
from typing import Iterable, List, Tuple
|
|
4
4
|
|
|
5
5
|
from .folder import Folder
|
|
6
6
|
from .mutex import Mutex
|
|
@@ -10,38 +10,51 @@ 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(
|
|
13
|
+
async def run_folder_tasks(
|
|
14
|
+
folders: Iterable[Folder],
|
|
15
|
+
mutex_interval: float, ignore_mutex: bool = False, watch: bool = True) -> None:
|
|
14
16
|
try:
|
|
15
17
|
if not ignore_mutex:
|
|
16
18
|
summary = get_summary(folders)
|
|
17
19
|
mutexes = {folder.host: Mutex(folder.host, folder.ssh_port) for folder in folders}
|
|
18
20
|
for mutex in mutexes.values():
|
|
19
21
|
print(f'Checking mutex on {mutex.host}', flush=True)
|
|
20
|
-
if not mutex.set(summary):
|
|
22
|
+
if not await mutex.set(summary):
|
|
21
23
|
print(f'Target is in use by {mutex.occupant}')
|
|
22
24
|
sys.exit(1)
|
|
23
25
|
|
|
24
26
|
for folder in folders:
|
|
25
27
|
print(f' {folder.source_path} --> {folder.target}', flush=True)
|
|
26
|
-
folder.sync()
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
28
|
+
await folder.sync()
|
|
29
|
+
|
|
30
|
+
if watch:
|
|
31
|
+
tasks: List[Tuple[Folder, asyncio.Task]] = []
|
|
32
|
+
for folder in folders:
|
|
33
|
+
print(f'Watch folder {folder.source_path}', flush=True)
|
|
34
|
+
tasks.append((folder, asyncio.create_task(folder.watch())))
|
|
35
|
+
|
|
36
|
+
while True:
|
|
37
|
+
if not ignore_mutex:
|
|
38
|
+
summary = get_summary(folders)
|
|
39
|
+
|
|
40
|
+
for host in list(mutexes):
|
|
41
|
+
if await mutexes[host].set(summary):
|
|
42
|
+
continue
|
|
43
|
+
print(f'Target {host} is in use by {mutexes[host].occupant}, stopping watch tasks', flush=True)
|
|
44
|
+
[task.cancel() for folder, task in tasks if folder.host == host]
|
|
45
|
+
tasks = [(folder, task) for folder, task in tasks if folder.host != host]
|
|
46
|
+
del mutexes[host]
|
|
47
|
+
|
|
48
|
+
if not tasks:
|
|
49
|
+
print('No more folders to watch, exiting', flush=True)
|
|
37
50
|
break
|
|
38
|
-
|
|
51
|
+
await asyncio.sleep(mutex_interval)
|
|
39
52
|
except Exception as e:
|
|
40
53
|
print(e)
|
|
41
54
|
|
|
42
55
|
|
|
43
|
-
def sync(*folders: Folder, mutex_interval: float = 10, ignore_mutex: bool = False) -> None:
|
|
56
|
+
def sync(*folders: Folder, mutex_interval: float = 10, ignore_mutex: bool = False, watch: bool = True) -> None:
|
|
44
57
|
try:
|
|
45
|
-
asyncio.run(run_folder_tasks(folders, mutex_interval, ignore_mutex=ignore_mutex))
|
|
58
|
+
asyncio.run(run_folder_tasks(folders, mutex_interval, ignore_mutex=ignore_mutex, watch=watch))
|
|
46
59
|
except KeyboardInterrupt:
|
|
47
60
|
print('Bye!')
|
|
@@ -1,17 +1,28 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: LiveSync
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
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
|
|
7
7
|
Author-email: info@zauberzeug.com
|
|
8
8
|
License: MIT
|
|
9
9
|
Keywords: sync remote watch filesystem development deploy live hot reload
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
11
|
Description-Content-Type: text/markdown
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Requires-Dist: pathspec
|
|
14
14
|
Requires-Dist: watchfiles
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: author-email
|
|
17
|
+
Dynamic: description
|
|
18
|
+
Dynamic: description-content-type
|
|
19
|
+
Dynamic: home-page
|
|
20
|
+
Dynamic: keywords
|
|
21
|
+
Dynamic: license
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
15
26
|
|
|
16
27
|
# LiveSync
|
|
17
28
|
|
|
@@ -64,6 +75,8 @@ Options:
|
|
|
64
75
|
interval in which mutex is updated (default: 10 seconds)
|
|
65
76
|
- `--ignore-mutex`
|
|
66
77
|
ignore mutex (use with caution) (default: False)
|
|
78
|
+
- `--no-watch`
|
|
79
|
+
don't keep watching the copied folders for changes after the sync (default: False)
|
|
67
80
|
|
|
68
81
|
### Python
|
|
69
82
|
|
|
@@ -79,6 +92,7 @@ sync(
|
|
|
79
92
|
```
|
|
80
93
|
|
|
81
94
|
The `sync` call will block until the script is aborted.
|
|
95
|
+
Only if `watch=False` is used, the `sync` call will end after copying the folders to the target once.
|
|
82
96
|
The `Folder` class allows to set the `port` and an `on_change` bash command which is executed after a sync has been performed.
|
|
83
97
|
Via the `rsync_args` build method you can pass additional options to configure rsync.
|
|
84
98
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
livesync/__init__.py,sha256=tZrvnAIpVuo9cQHMCvi6l2vup66NXPxRwyR5UBNtCTs,50
|
|
2
|
+
livesync/folder.py,sha256=pl73eVZM5myiTtwRwkBYnHGutIUYgO7DMTiNcOmOUYg,4392
|
|
3
|
+
livesync/livesync.py,sha256=_Y44zSuqZ6ZvwobNBdwGCqEYAU7n0lVoAHfDoBF6zPY,1411
|
|
4
|
+
livesync/mutex.py,sha256=vNiuMSAW_KMKHvY5AHsm1cYvGUUalsWdx2ep40BPpJY,2024
|
|
5
|
+
livesync/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
livesync/run_subprocess.py,sha256=9bUQ4EhErfBAdX5N3_NkZ4n1lJzy8vBOV2HiKnubhm0,502
|
|
7
|
+
livesync/sync.py,sha256=xYJ4GR8HsqncRGjMHVC_kYJf1E7w2wg0pMMNTCYlJEI,2373
|
|
8
|
+
livesync-0.4.0.dist-info/licenses/LICENSE,sha256=QcBlwggRQYhvfTAE481AAMFQfMS_N6Bj8Svh1T6dsnI,1072
|
|
9
|
+
livesync-0.4.0.dist-info/METADATA,sha256=AMbyPkkg_maqytXg57Uo1em-KpwAKwyaVJpRdKEPdyM,5772
|
|
10
|
+
livesync-0.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
11
|
+
livesync-0.4.0.dist-info/entry_points.txt,sha256=4dn5YR27lUlJWea3yqLKLqR4MwqjcqzMR5Q4jVFnytI,52
|
|
12
|
+
livesync-0.4.0.dist-info/top_level.txt,sha256=mLwExc6wTUGqxvUkYMio5rxGS1h8bvxpNsR2ebfjSL4,9
|
|
13
|
+
livesync-0.4.0.dist-info/RECORD,,
|
LiveSync-0.3.3.dist-info/RECORD
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|