dfsync 0.4.8__tar.gz → 0.4.10__tar.gz

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.
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: dfsync
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -11,6 +11,7 @@ Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
14
15
  Requires-Dist: black (==24.3.0)
15
16
  Requires-Dist: click (>=8,<9)
16
17
  Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
@@ -1,4 +1,5 @@
1
- import black, fnmatch
1
+ import black
2
+ import fnmatch
2
3
  import os.path
3
4
  import git.exc
4
5
  import subprocess
@@ -111,6 +112,10 @@ class UntrackedGitFilesFilter(LoggingFilter):
111
112
  self._repos = {}
112
113
  self._is_repo_initialized = {}
113
114
  self._untracked_and_ignored_files = {}
115
+ self._should_filter_untracked_files = True
116
+
117
+ def set_should_filter_untracked_files(self, should_filter_untracked_files: bool = True):
118
+ self._should_filter_untracked_files = should_filter_untracked_files
114
119
 
115
120
  def get_untracked_and_ignored_files(self):
116
121
  flat_files = []
@@ -175,13 +180,17 @@ class UntrackedGitFilesFilter(LoggingFilter):
175
180
  self._ignore(src_file_path, "GIT repo internals")
176
181
  return True
177
182
 
178
- for rel_file_path in repo.untracked_files:
179
- abs_file_path = os.path.join(repo.working_dir, rel_file_path)
180
- if src_abs_path == abs_file_path:
181
- self._ignore(src_file_path, "Untracked GIT file")
182
- return True
183
+ if self._should_filter_untracked_files and self._is_untracked_file(repo, src_abs_path):
184
+ self._ignore(src_file_path, "Untracked GIT file")
185
+ return True
186
+
183
187
  return False
184
188
 
189
+ def _is_untracked_file(self, repo, src_abs_path):
190
+ return any(
191
+ src_abs_path == os.path.join(repo.working_dir, rel_file_path) for rel_file_path in repo.untracked_files
192
+ )
193
+
185
194
 
186
195
  USER_FILTERS = [EmacsBufferFilter(), PythonBlackFilter()]
187
196
  GIT_FILTER = UntrackedGitFilesFilter()
@@ -215,3 +224,7 @@ def add_user_filter(f: LoggingFilter):
215
224
 
216
225
  def add_user_ignored_patterns_filter(patterns: [str]):
217
226
  add_user_filter(UserConfigFilter(patterns))
227
+
228
+
229
+ def set_ignore_untracked_files(should_ignore_untracked_files: bool):
230
+ GIT_FILTER.set_should_filter_untracked_files(should_ignore_untracked_files)
@@ -0,0 +1,101 @@
1
+ from enum import Enum
2
+ import platform
3
+ import subprocess
4
+ import time
5
+ import threading
6
+
7
+
8
+ class ThreadedOperationsManager:
9
+ def __init__(self):
10
+ self.threads = []
11
+
12
+ def stop(self, *args, **kwargs):
13
+ for thread in self.threads:
14
+ thread.stop(*args, **kwargs)
15
+
16
+ def register_threaded_operation(self, op):
17
+ self.threads.append(op)
18
+
19
+
20
+ thread_manager = ThreadedOperationsManager()
21
+
22
+
23
+ class ControlledThreadedOperation:
24
+ def __init__(self):
25
+ self._running = False
26
+ self._thread = threading.Thread(target=self.run)
27
+ thread_manager.register_threaded_operation(self)
28
+
29
+ def start(self):
30
+ self._running = True
31
+ self._thread.start()
32
+
33
+ def stop(self, *args, **kwargs):
34
+ self._running = False
35
+
36
+ @property
37
+ def is_running(self):
38
+ return self._running
39
+
40
+ @property
41
+ def is_completed(self):
42
+ return self._running is False
43
+
44
+ def run(self):
45
+ while self._running:
46
+ try:
47
+ self._run_once()
48
+ except Exception:
49
+ time.sleep(0.001)
50
+
51
+ def _run_once(self):
52
+ time.sleep(0.001)
53
+
54
+
55
+ class RsyncFlavour(Enum):
56
+ SAMBA_RSYNC = "SAMBA_RSYNC"
57
+ OPEN_RSYNC = "OPENRSYNC"
58
+
59
+ @property
60
+ def executable(self):
61
+ if platform.system() == "Darwin": # Check if running on macOS
62
+ if self == RsyncFlavour.SAMBA_RSYNC:
63
+ return "/opt/homebrew/bin/rsync"
64
+ elif self == RsyncFlavour.OPEN_RSYNC:
65
+ return "/usr/bin/rsync"
66
+
67
+ # Default fallback for other platforms or if paths don't exist
68
+ return "rsync"
69
+
70
+ @property
71
+ def exists_installed(self):
72
+ try:
73
+ # Check if the executable exists and is executable
74
+ result = subprocess.run(
75
+ ["which", self.executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
76
+ )
77
+ if result.returncode != 0:
78
+ return False
79
+
80
+ # Additionally verify it can be executed
81
+ result = subprocess.run(
82
+ [self.executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
83
+ )
84
+ return result.returncode == 0
85
+ except Exception:
86
+ return False
87
+
88
+
89
+ def check_rsync() -> RsyncFlavour:
90
+ try:
91
+ result = subprocess.run(["rsync", "--version"], capture_output=True, text=True)
92
+ version_text = result.stdout
93
+
94
+ if "rsync.samba.org/" in version_text:
95
+ return RsyncFlavour.SAMBA_RSYNC
96
+ else:
97
+ # Default to OpenRsync if we can't positively identify Samba rsync
98
+ return RsyncFlavour.OPEN_RSYNC
99
+ except (subprocess.SubprocessError, FileNotFoundError):
100
+ # If rsync isn't available or fails, default to Samba rsync
101
+ return RsyncFlavour.SAMBA_RSYNC
@@ -12,6 +12,8 @@ from functools import partial
12
12
  from watchdog.observers import Observer
13
13
  from watchdog.events import FileSystemEventHandler
14
14
 
15
+ import dfsync.filters as filters
16
+ import dfsync.lib as lib
15
17
  from dfsync.backends import rsync_backend, kube_backend
16
18
  from dfsync.distribution import (
17
19
  get_installed_version,
@@ -21,11 +23,9 @@ from dfsync.distribution import (
21
23
  is_installed_in_editable_mode,
22
24
  update_package,
23
25
  )
24
- from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
25
26
  from dfsync.config import read_config
26
27
  from dfsync.char_ui import KeyController
27
28
  from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
28
- from dfsync.lib import ControlledThreadedOperation, thread_manager
29
29
 
30
30
  logging.basicConfig(level=logging.WARN)
31
31
 
@@ -44,7 +44,11 @@ class RelatedLocationsError(ValueError):
44
44
  pass
45
45
 
46
46
 
47
- class FileChangedEventHandler(ControlledThreadedOperation, FileSystemEventHandler):
47
+ class UnsupportedRsyncError(ValueError):
48
+ pass
49
+
50
+
51
+ class FileChangedEventHandler(lib.ControlledThreadedOperation, FileSystemEventHandler):
48
52
  def __init__(
49
53
  self,
50
54
  backend: str = "log",
@@ -54,7 +58,7 @@ class FileChangedEventHandler(ControlledThreadedOperation, FileSystemEventHandle
54
58
  **kwargs,
55
59
  ):
56
60
  super().__init__()
57
- self.filters = [*ALL_FILTERS]
61
+ self.filters = [*filters.ALL_FILTERS]
58
62
 
59
63
  self.backend = backend
60
64
  self.backend_options = kwargs
@@ -258,11 +262,15 @@ def main():
258
262
  pass
259
263
 
260
264
 
261
- @main.command()
265
+ @main.command(add_help_option=False)
262
266
  def version():
263
267
  """
264
268
  Print the currently installed version
265
269
  """
270
+ _version()
271
+
272
+
273
+ def _version():
266
274
  installed_version = get_installed_version()
267
275
  click.echo(f"{installed_version}")
268
276
 
@@ -275,7 +283,16 @@ def version():
275
283
  click.echo(f"Latest version is {latest_version}, please update using `dfsync self-update`")
276
284
 
277
285
 
278
- @main.command()
286
+ @main.command(name="help", add_help_option=False)
287
+ def help():
288
+ """
289
+ Print the usage/help for dfsync
290
+ """
291
+ with click.Context(sync, info_name="dfsync", max_content_width=999) as ctx:
292
+ click.echo(sync.get_help(ctx))
293
+
294
+
295
+ @main.command(add_help_option=False)
279
296
  def self_update():
280
297
  """
281
298
  Update dfsync to the latest version published on pypi
@@ -309,11 +326,15 @@ def import_kube_host(kube_host=None, credentials=None):
309
326
  @main.command()
310
327
  @click.argument("source", nargs=-1)
311
328
  @click.argument("destination", default="", nargs=1)
312
- @click.option("--supervisor/--no-supervisor", default=False, help="Try to install supervisor in container", type=bool)
329
+ @click.option("--supervisor", is_flag=True, default=False, help="Try to install supervisor in container")
313
330
  @click.option("--kube-host", default=None, help="Kubernetes api host server address/hostname", type=str)
314
331
  @click.option("--pod-timeout", default=30, help="Pod reconfiguration timeout (default is 30 seconds)", type=int)
315
332
  @click.option("--full-sync/--no-full-sync", default=True, help="On startup, sync all files to destination", type=bool)
316
- def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
333
+ @click.option("--version", is_flag=True, default=False, help="Print the currently installed version")
334
+ @click.option(
335
+ "--sync-git-untracked", is_flag=True, default=False, help="Sync git-untracked files instead of ignoring them"
336
+ )
337
+ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, version, sync_git_untracked):
317
338
  """
318
339
  Watches a folder for changes and propagates all file changes to a destination.
319
340
 
@@ -353,7 +374,14 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
353
374
 
354
375
  paths = ["."] if len(source) == 0 else source
355
376
  config = read_config(destination, *paths)
356
- add_user_ignored_patterns_filter(config.ignore_files)
377
+ filters.add_user_ignored_patterns_filter(config.ignore_files)
378
+
379
+ if sync_git_untracked:
380
+ filters.set_ignore_untracked_files(False)
381
+
382
+ if version:
383
+ _version()
384
+ return
357
385
 
358
386
  destination_dir = destination
359
387
  if len(source) == 0 and config.destination and not has_destination_optics(destination):
@@ -370,6 +398,11 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
370
398
  elif len(missing) > 0:
371
399
  click.echo(f"Using source file/dirs {', '.join(paths)}")
372
400
 
401
+ installed_rsync = lib.check_rsync()
402
+ click.echo(f"Installed: {installed_rsync.executable}")
403
+ if installed_rsync != lib.RsyncFlavour.SAMBA_RSYNC:
404
+ raise UnsupportedRsyncError()
405
+
373
406
  backend_options = dict(
374
407
  destination_dir=destination_dir,
375
408
  supervisor=supervisor,
@@ -398,6 +431,14 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
398
431
  click.echo(str(e))
399
432
  return -1
400
433
 
434
+ except UnsupportedRsyncError:
435
+ click.echo(
436
+ "\nStarting from Mac OSX Sequoia 15.4, Apple are shipping openrsync instead of the GPL-licensed rsync from Samba"
437
+ "\nThe Apple version of rsync has less features and is currently not supported by dfsync"
438
+ "\nPlease use homebrew to install the GPL-licensed rsync from Samba with this command `brew install rsync`."
439
+ )
440
+ return -1
441
+
401
442
  except ValueError as e:
402
443
  click.echo(
403
444
  "Trying {} from source: {} to destination: '{}'\n".format(
@@ -430,7 +471,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
430
471
  controller.on_key(
431
472
  "x",
432
473
  description="to exit",
433
- action=partial(thread_manager.stop, "Exiting."),
474
+ action=partial(lib.thread_manager.stop, "Exiting."),
434
475
  )
435
476
 
436
477
  try:
@@ -456,16 +497,16 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
456
497
  controller.raise_exceptions()
457
498
 
458
499
  except KeyboardInterrupt:
459
- thread_manager.stop()
500
+ lib.thread_manager.stop()
460
501
  click.echo("Received [Ctrl-C], exiting.")
461
502
 
462
503
  except:
463
- thread_manager.stop()
504
+ lib.thread_manager.stop()
464
505
  raise
465
506
 
466
507
  finally:
467
508
  observer.stop()
468
- thread_manager.stop()
509
+ lib.thread_manager.stop()
469
510
  backend_engine.on_monitor_exit(**backend_options)
470
511
  if observer.ident is not None:
471
512
  # only join the observer if it was previously started
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.8"
3
+ version = "0.4.10"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
@@ -1,49 +0,0 @@
1
- import time
2
- import threading
3
-
4
-
5
- class ThreadedOperationsManager:
6
- def __init__(self):
7
- self.threads = []
8
-
9
- def stop(self, *args, **kwargs):
10
- for thread in self.threads:
11
- thread.stop(*args, **kwargs)
12
-
13
- def register_threaded_operation(self, op):
14
- self.threads.append(op)
15
-
16
-
17
- thread_manager = ThreadedOperationsManager()
18
-
19
-
20
- class ControlledThreadedOperation:
21
- def __init__(self):
22
- self._running = False
23
- self._thread = threading.Thread(target=self.run)
24
- thread_manager.register_threaded_operation(self)
25
-
26
- def start(self):
27
- self._running = True
28
- self._thread.start()
29
-
30
- def stop(self, *args, **kwargs):
31
- self._running = False
32
-
33
- @property
34
- def is_running(self):
35
- return self._running
36
-
37
- @property
38
- def is_completed(self):
39
- return self._running is False
40
-
41
- def run(self):
42
- while self._running:
43
- try:
44
- self._run_once()
45
- except Exception:
46
- time.sleep(0.001)
47
-
48
- def _run_once(self):
49
- time.sleep(0.001)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes