dfsync 0.4.7__tar.gz → 0.4.9__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
1
  Metadata-Version: 2.1
2
2
  Name: dfsync
3
- Version: 0.4.7
3
+ Version: 0.4.9
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -11,7 +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
- Requires-Dist: black (==23.7.0)
14
+ Requires-Dist: black (==24.3.0)
15
15
  Requires-Dist: click (>=8,<9)
16
16
  Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
17
17
  Requires-Dist: gitpython (>=3.1.14,<4.0.0)
@@ -1,5 +1,10 @@
1
+ import click
1
2
  import json
3
+ import subprocess
4
+ import sys
2
5
  import urllib.request
6
+ from typing import Optional
7
+
3
8
  from dfsync.lib import ControlledThreadedOperation
4
9
 
5
10
 
@@ -8,14 +13,14 @@ def get_package_version(package_name):
8
13
  from importlib.metadata import version
9
14
 
10
15
  return version(package_name)
11
- except:
16
+ except Exception:
12
17
  pass
13
18
 
14
19
  try:
15
20
  import pkg_resources
16
21
 
17
22
  return pkg_resources.get_distribution(package_name).version
18
- except:
23
+ except Exception:
19
24
  pass
20
25
 
21
26
  return None
@@ -41,7 +46,7 @@ def get_latest_version():
41
46
  raise ValueError("No version visible on github")
42
47
 
43
48
  return versions[0]
44
- except Exception as e:
49
+ except Exception:
45
50
  return get_installed_version()
46
51
 
47
52
 
@@ -52,7 +57,7 @@ def parse_version(version_str):
52
57
  for part in version_str.split("."):
53
58
  try:
54
59
  parsed_version.append(int(part))
55
- except:
60
+ except Exception:
56
61
  parsed_version.append(part)
57
62
  return parsed_version
58
63
 
@@ -80,15 +85,52 @@ class AsyncVersionChecker(ControlledThreadedOperation):
80
85
  self.installed = get_installed_version()
81
86
  self.latest = get_latest_version()
82
87
  self.installed_is_older = is_older_version(self.installed, self.latest)
88
+ self._is_install_editable = is_installed_in_editable_mode("dfsync")
83
89
  self._cta_count = 0
84
90
  self.stop()
85
91
 
86
92
  @property
87
- def should_emit_upgrade_warning(self):
93
+ def should_emit_update_warning(self):
88
94
  return self.is_completed and self.installed_is_older and self._cta_count < 1
89
95
 
90
- def set_emitted_upgrade_warning(self):
96
+ def set_emitted_update_warning(self):
91
97
  self._cta_count += 1
92
98
 
93
- def get_upgrade_warning(self):
94
- return f"dfsync ver. {self.latest} is available, please upgrade! ({self.installed} is installed)"
99
+ def get_update_warning(self):
100
+ if self._is_install_editable:
101
+ return f"dfsync ver. {self.latest} is available, please update! ({self.installed} is installed)"
102
+ else:
103
+ return f"dfsync ver. {self.latest} is available ({self.installed} is installed), please update using `dfsync self-update`"
104
+
105
+
106
+ def _read_editable_location_from_pip_show_output(out: str) -> Optional[str]:
107
+ editable_label = "Editable project location: "
108
+ site_packages_label = "site-packages"
109
+ site_packages_lines = []
110
+
111
+ for line in out.split("\n"):
112
+ if line.startswith("Location: "):
113
+ continue
114
+ if line.startswith(editable_label):
115
+ return line[len(editable_label) :].strip()
116
+ if site_packages_label in line:
117
+ site_packages_lines.append(line)
118
+
119
+ return "UNKNOWN-EDITABLE-LOCATION" if len(site_packages_lines) > 0 else None
120
+
121
+
122
+ def is_installed_in_editable_mode(package_name: str) -> bool:
123
+ try:
124
+ out = subprocess.check_output([sys.executable, "-m", "pip", "show", "-f", package_name]).decode()
125
+ editable_location = _read_editable_location_from_pip_show_output(out)
126
+ return editable_location is not None
127
+ except Exception:
128
+ return False
129
+
130
+
131
+ def update_package(package_name: str):
132
+ click.echo(f"Running {sys.executable} -m pip install --upgrade {package_name}")
133
+ try:
134
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", package_name])
135
+ except Exception as e:
136
+ click.echo(f"Failed to update {package_name}: {e}")
@@ -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)
@@ -77,14 +77,16 @@ def update_local_kube_config(new_kube_credentials):
77
77
  credentials_yaml = yaml.dump(credentials)
78
78
  with open(f"{LOCAL_CREDENTIALS_FILE}.new", "w") as f:
79
79
  f.write(credentials_yaml)
80
+ os.chmod(f"{LOCAL_CREDENTIALS_FILE}.new", 0o600)
80
81
 
81
82
  if os.path.isfile(LOCAL_CREDENTIALS_FILE):
82
83
  if os.path.isfile(f"{LOCAL_CREDENTIALS_FILE}.old"):
83
84
  os.remove(f"{LOCAL_CREDENTIALS_FILE}.old")
84
85
  os.rename(f"{LOCAL_CREDENTIALS_FILE}", f"{LOCAL_CREDENTIALS_FILE}.old")
86
+ os.chmod(f"{LOCAL_CREDENTIALS_FILE}.old", 0o600)
85
87
 
86
88
  os.rename(f"{LOCAL_CREDENTIALS_FILE}.new", f"{LOCAL_CREDENTIALS_FILE}")
87
-
89
+ os.chmod(LOCAL_CREDENTIALS_FILE, 0o600)
88
90
  return credentials_yaml
89
91
 
90
92
 
@@ -12,9 +12,16 @@ 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
15
16
  from dfsync.backends import rsync_backend, kube_backend
16
- from dfsync.distribution import get_installed_version, get_latest_version, is_older_version, AsyncVersionChecker
17
- from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
17
+ from dfsync.distribution import (
18
+ get_installed_version,
19
+ get_latest_version,
20
+ is_older_version,
21
+ AsyncVersionChecker,
22
+ is_installed_in_editable_mode,
23
+ update_package,
24
+ )
18
25
  from dfsync.config import read_config
19
26
  from dfsync.char_ui import KeyController
20
27
  from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
@@ -47,7 +54,7 @@ class FileChangedEventHandler(ControlledThreadedOperation, FileSystemEventHandle
47
54
  **kwargs,
48
55
  ):
49
56
  super().__init__()
50
- self.filters = [*ALL_FILTERS]
57
+ self.filters = [*filters.ALL_FILTERS]
51
58
 
52
59
  self.backend = backend
53
60
  self.backend_options = kwargs
@@ -253,12 +260,50 @@ def main():
253
260
 
254
261
  @main.command()
255
262
  def version():
263
+ """
264
+ Print the currently installed version
265
+ """
266
+ _version()
267
+
268
+
269
+ def _version():
256
270
  installed_version = get_installed_version()
257
271
  click.echo(f"{installed_version}")
258
272
 
259
273
  latest_version = get_latest_version()
260
- if is_older_version(installed_version, latest_version):
261
- click.echo(f"Latest version is {latest_version}, please upgrade!")
274
+ if not is_older_version(installed_version, latest_version):
275
+ return
276
+ elif is_installed_in_editable_mode("dfsync"):
277
+ click.echo(f"Latest version is {latest_version}, please update!")
278
+ else:
279
+ click.echo(f"Latest version is {latest_version}, please update using `dfsync self-update`")
280
+
281
+
282
+ @main.command(name="help", context_settings=dict(ignore_unknown_options=True))
283
+ @click.option("--help", is_flag=True, default=False, hidden=True)
284
+ def help(help):
285
+ """
286
+ Print the usage/help for dfsync
287
+ """
288
+ with click.Context(sync, info_name="dfsync") as ctx:
289
+ click.echo(sync.get_help(ctx))
290
+
291
+
292
+ @main.command()
293
+ def self_update():
294
+ """
295
+ Update dfsync to the latest version published on pypi
296
+ """
297
+ latest_version = get_latest_version()
298
+ installed_version = get_installed_version()
299
+ if not is_older_version(installed_version, latest_version):
300
+ click.echo(f"The latest version is already installed: {installed_version}")
301
+ return
302
+ elif is_installed_in_editable_mode("dfsync"):
303
+ click.echo("dfsync is installed in editable mode, please update manually")
304
+ return
305
+ else:
306
+ update_package("dfsync")
262
307
 
263
308
 
264
309
  @main.command()
@@ -278,11 +323,15 @@ def import_kube_host(kube_host=None, credentials=None):
278
323
  @main.command()
279
324
  @click.argument("source", nargs=-1)
280
325
  @click.argument("destination", default="", nargs=1)
281
- @click.option("--supervisor/--no-supervisor", default=False, help="Try to install supervisor in container", type=bool)
326
+ @click.option("--supervisor", is_flag=True, default=False, help="Try to install supervisor in container")
282
327
  @click.option("--kube-host", default=None, help="Kubernetes api host server address/hostname", type=str)
283
328
  @click.option("--pod-timeout", default=30, help="Pod reconfiguration timeout (default is 30 seconds)", type=int)
284
329
  @click.option("--full-sync/--no-full-sync", default=True, help="On startup, sync all files to destination", type=bool)
285
- def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
330
+ @click.option("--version", is_flag=True, default=False, help="Print the currently installed version")
331
+ @click.option(
332
+ "--sync-git-untracked", is_flag=True, default=False, help="Sync git-untracked files instead of ignoring them"
333
+ )
334
+ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, version, sync_git_untracked):
286
335
  """
287
336
  Watches a folder for changes and propagates all file changes to a destination.
288
337
 
@@ -322,7 +371,14 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
322
371
 
323
372
  paths = ["."] if len(source) == 0 else source
324
373
  config = read_config(destination, *paths)
325
- add_user_ignored_patterns_filter(config.ignore_files)
374
+ filters.add_user_ignored_patterns_filter(config.ignore_files)
375
+
376
+ if sync_git_untracked:
377
+ filters.set_ignore_untracked_files(False)
378
+
379
+ if version:
380
+ _version()
381
+ return
326
382
 
327
383
  destination_dir = destination
328
384
  if len(source) == 0 and config.destination and not has_destination_optics(destination):
@@ -419,9 +475,9 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
419
475
  no_errors = False
420
476
  break
421
477
  with controller.getch_lock():
422
- if checker.should_emit_upgrade_warning:
423
- checker.set_emitted_upgrade_warning()
424
- click.echo(checker.get_upgrade_warning())
478
+ if checker.should_emit_update_warning:
479
+ checker.set_emitted_update_warning()
480
+ click.echo(checker.get_update_warning())
425
481
  controller.raise_exceptions()
426
482
 
427
483
  except KeyboardInterrupt:
@@ -3,10 +3,6 @@ from tempfile import NamedTemporaryFile
3
3
  from dfsync.filters import EmacsBufferFilter, UntrackedGitFilesFilter
4
4
 
5
5
 
6
- def test_version():
7
- assert False, "Test version not implemented"
8
-
9
-
10
6
  def test_emacs_buffer_filter():
11
7
  emacs = EmacsBufferFilter()
12
8
 
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ from dfsync.distribution import is_installed_in_editable_mode, get_package_version, update_package
3
+
4
+
5
+ def test_is_installed_in_editable_mode():
6
+ assert is_installed_in_editable_mode("dfsync") is True
7
+
8
+
9
+ def test_is_installed_in_editable_mode_missing_package():
10
+ assert is_installed_in_editable_mode("does-not-exist") is False
11
+
12
+
13
+ def test_is_installed_in_editable_mode_required_and_installed_package():
14
+ assert pytest is not None, "pytest must be installed"
15
+ assert is_installed_in_editable_mode("pytest") is False
16
+
17
+ assert is_installed_in_editable_mode("click") is False
18
+
19
+
20
+ def test_update_package():
21
+ tqdm_version = get_package_version("tqdm")
22
+
23
+ update_package("tqdm==4.66.2")
24
+ assert get_package_version("tqdm") == "4.66.2"
25
+
26
+ update_package("tqdm==4.66.0")
27
+ assert get_package_version("tqdm") == "4.66.0"
28
+
29
+ update_package(f"tqdm=={tqdm_version}")
30
+ assert get_package_version("tqdm") == tqdm_version
@@ -1,6 +1,7 @@
1
+ import os
1
2
  import pytest
2
- import yaml
3
3
  import tempfile
4
+ import yaml
4
5
 
5
6
  from dfsync.kube_credentials import update_local_kube_config, normalized_k8s_url
6
7
 
@@ -12,7 +13,7 @@ def mock_kube_credentials(mocker):
12
13
  return temp.name
13
14
 
14
15
 
15
- def test_update_missing_kube_config(mock_kube_credentials):
16
+ def test_update_missing_kube_config_and_check_permissions(mock_kube_credentials):
16
17
  new_kube_credentials = {
17
18
  "clusters": [{"cluster": {"server": "test_server"}, "name": "test_cluster"}],
18
19
  "contexts": [{"context": {"cluster": "test_cluster", "user": "test_user"}, "name": "test_context"}],
@@ -23,6 +24,9 @@ def test_update_missing_kube_config(mock_kube_credentials):
23
24
  assert "test_context" in result
24
25
  assert "test_user" in result
25
26
 
27
+ # Check file permissions
28
+ assert oct(os.stat(mock_kube_credentials).st_mode)[-3:] == "600"
29
+
26
30
 
27
31
  def test_update_empty_kube_config(mock_kube_credentials):
28
32
  with open(mock_kube_credentials, "wb") as f:
@@ -53,6 +57,15 @@ def test_update_empty_kube_config(mock_kube_credentials):
53
57
  assert credentials["users"][0]["user"]["username"] == "test_username"
54
58
  assert credentials["users"][0]["user"]["password"] == "test_password"
55
59
 
60
+ # Verify that the old file was saved
61
+ with open(f"{mock_kube_credentials}.old", "r") as f:
62
+ credentials = yaml.safe_load(f)
63
+ assert credentials == empty_config
64
+
65
+ # Check file permissions for both old and new config file
66
+ assert oct(os.stat(mock_kube_credentials).st_mode)[-3:] == "600"
67
+ assert oct(os.stat(f"{mock_kube_credentials}.old").st_mode)[-3:] == "600"
68
+
56
69
 
57
70
  def test_update_existing_kube_config(mock_kube_credentials):
58
71
  with open(mock_kube_credentials, "wb") as f:
@@ -103,5 +116,5 @@ def test_normalized_k8s_url():
103
116
  assert normalized_k8s_url("192.168.0.123:6443") == "https://192.168.0.123:6443"
104
117
  with pytest.raises(ValueError):
105
118
  normalized_k8s_url("")
106
- with pytest.raises(ValueError):
107
- normalized_k8s_url(None)
119
+
120
+ assert normalized_k8s_url(None) is None
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.7"
3
+ version = "0.4.9"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
@@ -14,7 +14,7 @@ toml = "^0.10.2"
14
14
  tenacity = "^8.0.1"
15
15
  click-default-group = "^1.2.2"
16
16
  paramiko = {version = "^3.2.0", extras = ["invoke"]}
17
- black = "23.7.0"
17
+ black = "24.3.0"
18
18
 
19
19
  [tool.poetry.dev-dependencies]
20
20
  pytest = "^7.4"
@@ -23,6 +23,7 @@ ipython = "^8"
23
23
  ipdb = "^0.13"
24
24
  flake8 = "^4.0.1"
25
25
  pycodestyle = "^2.8.0"
26
+ tqdm = "4.66.0"
26
27
 
27
28
  [tool.black]
28
29
  line-length = 120
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