LiveSync 0.3.4__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/mutex.py +17 -10
- livesync/py.typed +0 -0
- livesync/run_subprocess.py +13 -8
- livesync/sync.py +17 -7
- {LiveSync-0.3.4.dist-info → livesync-0.4.0.dist-info}/METADATA +14 -3
- livesync-0.4.0.dist-info/RECORD +13 -0
- {LiveSync-0.3.4.dist-info → livesync-0.4.0.dist-info}/WHEEL +1 -1
- LiveSync-0.3.4.dist-info/RECORD +0 -12
- {LiveSync-0.3.4.dist-info → livesync-0.4.0.dist-info}/entry_points.txt +0 -0
- {LiveSync-0.3.4.dist-info → livesync-0.4.0.dist-info/licenses}/LICENSE +0 -0
- {LiveSync-0.3.4.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/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
|
|
@@ -19,25 +19,35 @@ async def run_folder_tasks(
|
|
|
19
19
|
mutexes = {folder.host: Mutex(folder.host, folder.ssh_port) for folder in folders}
|
|
20
20
|
for mutex in mutexes.values():
|
|
21
21
|
print(f'Checking mutex on {mutex.host}', flush=True)
|
|
22
|
-
if not mutex.set(summary):
|
|
22
|
+
if not await mutex.set(summary):
|
|
23
23
|
print(f'Target is in use by {mutex.occupant}')
|
|
24
24
|
sys.exit(1)
|
|
25
25
|
|
|
26
26
|
for folder in folders:
|
|
27
27
|
print(f' {folder.source_path} --> {folder.target}', flush=True)
|
|
28
|
-
folder.sync()
|
|
28
|
+
await folder.sync()
|
|
29
29
|
|
|
30
30
|
if watch:
|
|
31
|
+
tasks: List[Tuple[Folder, asyncio.Task]] = []
|
|
31
32
|
for folder in folders:
|
|
32
33
|
print(f'Watch folder {folder.source_path}', flush=True)
|
|
33
|
-
asyncio.create_task(folder.watch())
|
|
34
|
+
tasks.append((folder, asyncio.create_task(folder.watch())))
|
|
34
35
|
|
|
35
36
|
while True:
|
|
36
37
|
if not ignore_mutex:
|
|
37
38
|
summary = get_summary(folders)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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)
|
|
50
|
+
break
|
|
41
51
|
await asyncio.sleep(mutex_interval)
|
|
42
52
|
except Exception as e:
|
|
43
53
|
print(e)
|
|
@@ -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
|
|
|
@@ -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.4.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=_Y44zSuqZ6ZvwobNBdwGCqEYAU7n0lVoAHfDoBF6zPY,1411
|
|
4
|
-
livesync/mutex.py,sha256=nHaP0Tge3ZV7jyVBbYsMug5L5YkuJf8_XIbfAdVJkPc,1741
|
|
5
|
-
livesync/run_subprocess.py,sha256=ZZqK9dlOVlJhL0xkKN7bG-BAT558nHk-IJNGqIMeWyM,368
|
|
6
|
-
livesync/sync.py,sha256=EEHMSOicbN6wOunpt-GEKPx9IPvEjOHr2gZVtALNxHE,1763
|
|
7
|
-
LiveSync-0.3.4.dist-info/LICENSE,sha256=QcBlwggRQYhvfTAE481AAMFQfMS_N6Bj8Svh1T6dsnI,1072
|
|
8
|
-
LiveSync-0.3.4.dist-info/METADATA,sha256=qvwFl-l5YZ9EC5GZpjDEqPhJvDS8CFMrsEVPnin1kgU,5537
|
|
9
|
-
LiveSync-0.3.4.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
|
|
10
|
-
LiveSync-0.3.4.dist-info/entry_points.txt,sha256=4dn5YR27lUlJWea3yqLKLqR4MwqjcqzMR5Q4jVFnytI,52
|
|
11
|
-
LiveSync-0.3.4.dist-info/top_level.txt,sha256=mLwExc6wTUGqxvUkYMio5rxGS1h8bvxpNsR2ebfjSL4,9
|
|
12
|
-
LiveSync-0.3.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|