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 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 subprocess.CalledProcessError:
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
- ssh_command = ['ssh', self.host, '-p', str(self.port), command]
49
- return subprocess.check_output(ssh_command, stderr=subprocess.DEVNULL).decode()
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
@@ -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
- try:
6
- result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, check=True)
7
- if not quiet:
8
- print(result.stdout.decode())
9
- except subprocess.CalledProcessError as e:
10
- print(e.stdout.decode())
11
- raise
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
- for mutex in mutexes.values():
39
- if not mutex.set(summary):
40
- break
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
1
+ Metadata-Version: 2.4
2
2
  Name: LiveSync
3
- Version: 0.3.4
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.7
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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.42.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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,,