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.
- {dfsync-0.4.8 → dfsync-0.4.10}/PKG-INFO +3 -2
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/filters.py +19 -6
- dfsync-0.4.10/dfsync/lib.py +101 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/monitor.py +54 -13
- {dfsync-0.4.8 → dfsync-0.4.10}/pyproject.toml +1 -1
- dfsync-0.4.8/dfsync/lib.py +0 -49
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/__init__.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/backends/kube.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/backends/rsync.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/char_ui.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/chcli.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/cli.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/config.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/diffui.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/distribution.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/exp.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/kube_credentials.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/test_dfsync.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/test_distribution.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/test_kube_credentials.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/test_lib.py +0 -0
- {dfsync-0.4.8 → dfsync-0.4.10}/dfsync/transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: dfsync
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
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
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
dfsync-0.4.8/dfsync/lib.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|