dfsync 0.4.10__tar.gz → 0.4.11__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.
Files changed (26) hide show
  1. {dfsync-0.4.10 → dfsync-0.4.11}/PKG-INFO +4 -3
  2. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/kube.py +2 -2
  3. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/rsync.py +14 -4
  4. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/filters.py +13 -8
  5. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/lib.py +19 -0
  6. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/monitor.py +37 -16
  7. dfsync-0.4.11/dfsync/sentry.py +9 -0
  8. dfsync-0.4.11/dfsync/test_filters.py +28 -0
  9. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_lib.py +22 -2
  10. {dfsync-0.4.10 → dfsync-0.4.11}/pyproject.toml +3 -3
  11. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/__init__.py +0 -0
  12. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/__init__.py +0 -0
  13. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/kube_exec.py +0 -0
  14. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/char_ui.py +0 -0
  15. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/chcli.py +0 -0
  16. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/check_dns.py +0 -0
  17. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/cli.py +0 -0
  18. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/config.py +0 -0
  19. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/diffui.py +0 -0
  20. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/distribution.py +0 -0
  21. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/exp.py +0 -0
  22. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/kube_credentials.py +0 -0
  23. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_dfsync.py +0 -0
  24. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_distribution.py +0 -0
  25. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_kube_credentials.py +0 -0
  26. {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/transactions.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: dfsync
3
- Version: 0.4.10
3
+ Version: 0.4.11
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -12,12 +12,13 @@ Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Programming Language :: Python :: 3.13
15
- Requires-Dist: black (==24.3.0)
15
+ Classifier: Programming Language :: Python :: 3.14
16
16
  Requires-Dist: click (>=8,<9)
17
17
  Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
18
18
  Requires-Dist: gitpython (>=3.1.14,<4.0.0)
19
19
  Requires-Dist: kubernetes (>=28.1.0,<29.0.0)
20
20
  Requires-Dist: paramiko[invoke] (>=3.2.0,<4.0.0)
21
+ Requires-Dist: sentry-sdk (>=2.26.0,<3.0.0)
21
22
  Requires-Dist: tenacity (>=8.0.1,<9.0.0)
22
23
  Requires-Dist: toml (>=0.10.2,<0.11.0)
23
24
  Requires-Dist: watchdog (>=2.0.2,<3.0.0)
@@ -569,7 +569,7 @@ class KubeReDeployer:
569
569
  try:
570
570
  resp = self._exec(pod, spec, status, Generic.check_can_exec())
571
571
  return
572
- except:
572
+ except Exception:
573
573
  self._uncrash(pod, spec, status)
574
574
 
575
575
  def _sniff_image_distro(self, pod, spec, status):
@@ -596,7 +596,7 @@ class KubeReDeployer:
596
596
  return False
597
597
 
598
598
  return len(resp) > 0
599
- except:
599
+ except Exception:
600
600
  return False
601
601
 
602
602
  def stabilize_deployments(self, image_base):
@@ -10,6 +10,8 @@ EVENT_TYPE_MAP = {
10
10
  "full-sync": "Full Sync",
11
11
  }
12
12
 
13
+ RSYNC_CONFIG = {"RSYNC_EXECUTABLE": "rsync"}
14
+
13
15
 
14
16
  class StopStreamReader(Exception):
15
17
  pass
@@ -174,7 +176,7 @@ class FileRsync:
174
176
  rsync_cmd = self._get_rsync_cmd_on_full_sync(src_paths, destination_dir, blocking_io, rsh)
175
177
  else:
176
178
  rsync_cmd = [
177
- "rsync",
179
+ get_rsync_executable(),
178
180
  "-Rvx",
179
181
  "--temp-dir=/tmp",
180
182
  "--delay-updates",
@@ -227,7 +229,7 @@ class FileRsync:
227
229
  if return_code != 23:
228
230
  raise subprocess.CalledProcessError(returncode=return_code, cmd=rsync_cmd)
229
231
 
230
- except:
232
+ except Exception:
231
233
  echo("Sync failed")
232
234
  env_str = "N/A"
233
235
  if rsh_env:
@@ -260,7 +262,7 @@ class FileRsync:
260
262
  src_dir = "{}/".format(src_dir)
261
263
 
262
264
  cmd = [
263
- "rsync",
265
+ get_rsync_executable(),
264
266
  "-rvx",
265
267
  "--delete",
266
268
  "--filter=-,s *",
@@ -276,7 +278,7 @@ class FileRsync:
276
278
  def _get_rsync_cmd_on_full_sync(self, src_file_paths: list, destination_dir: str, blocking_io: list, rsh: list):
277
279
  filters = ["--filter=- {}".format(f) for f in list_files_to_ignore()]
278
280
  return [
279
- "rsync",
281
+ get_rsync_executable(),
280
282
  "-rvx",
281
283
  "--temp-dir=/tmp",
282
284
  "--delay-updates",
@@ -312,3 +314,11 @@ def sanitize_relative_path(path):
312
314
 
313
315
 
314
316
  rsync_backend = FileRsync
317
+
318
+
319
+ def get_rsync_executable() -> str:
320
+ return RSYNC_CONFIG["RSYNC_EXECUTABLE"]
321
+
322
+
323
+ def set_rsync_executable(executable_path: str) -> str:
324
+ return RSYNC_CONFIG["RSYNC_EXECUTABLE"]
@@ -1,4 +1,3 @@
1
- import black
2
1
  import fnmatch
3
2
  import os.path
4
3
  import git.exc
@@ -75,7 +74,9 @@ class EmacsBufferFilter(UserConfigFilter):
75
74
  self._ignore_message = "Emacs buffer backup"
76
75
 
77
76
 
78
- class PythonBlackFilter(LoggingFilter):
77
+ class PythonSyntaxFilter(LoggingFilter):
78
+ """Reject .py files that do not parse, so a half-saved file never reaches the pod."""
79
+
79
80
  def __init__(self):
80
81
  super().__init__()
81
82
 
@@ -87,17 +88,17 @@ class PythonBlackFilter(LoggingFilter):
87
88
  parent_path, file_name = os.path.split(os.path.expanduser(src_file_path))
88
89
  for pattern in PYTHON_PATTERNS:
89
90
  if fnmatch.fnmatch(file_name, pattern):
90
- return self._black_check(src_file_path)
91
+ return self._syntax_check(src_file_path)
91
92
  return False
92
93
 
93
- def _black_check(self, src_file_path):
94
+ def _syntax_check(self, src_file_path):
94
95
  try:
95
- with open(src_file_path, "r") as f:
96
+ with open(src_file_path, "rb") as f:
96
97
  contents = f.read()
97
- black.format_file_contents(contents, fast=False, mode=black.FileMode())
98
+ compile(contents, src_file_path, "exec", dont_inherit=True)
98
99
  return False
99
100
 
100
- except (FileNotFoundError, PermissionError, black.report.NothingChanged) as e:
101
+ except (FileNotFoundError, PermissionError):
101
102
  return False
102
103
 
103
104
  except Exception as e:
@@ -106,6 +107,10 @@ class PythonBlackFilter(LoggingFilter):
106
107
  return True
107
108
 
108
109
 
110
+ # Kept for code that imported the old name
111
+ PythonBlackFilter = PythonSyntaxFilter
112
+
113
+
109
114
  class UntrackedGitFilesFilter(LoggingFilter):
110
115
  def __init__(self):
111
116
  super().__init__()
@@ -192,7 +197,7 @@ class UntrackedGitFilesFilter(LoggingFilter):
192
197
  )
193
198
 
194
199
 
195
- USER_FILTERS = [EmacsBufferFilter(), PythonBlackFilter()]
200
+ USER_FILTERS = [EmacsBufferFilter(), PythonSyntaxFilter()]
196
201
  GIT_FILTER = UntrackedGitFilesFilter()
197
202
  ALL_FILTERS = [
198
203
  exclude_watchdog_directory_events,
@@ -1,8 +1,10 @@
1
+ import base64
1
2
  from enum import Enum
2
3
  import platform
3
4
  import subprocess
4
5
  import time
5
6
  import threading
7
+ import zlib
6
8
 
7
9
 
8
10
  class ThreadedOperationsManager:
@@ -99,3 +101,20 @@ def check_rsync() -> RsyncFlavour:
99
101
  except (subprocess.SubprocessError, FileNotFoundError):
100
102
  # If rsync isn't available or fails, default to Samba rsync
101
103
  return RsyncFlavour.SAMBA_RSYNC
104
+
105
+
106
+ NONCE = "Rah4liteaR1geughuC8eushaOhtiW0xu"
107
+
108
+
109
+ def dsn_pack(unpacked_dsn: str) -> str:
110
+ # XOR with NONCE, compress and base64 encode
111
+ xored = "".join(chr(ord(a) ^ ord(b)) for a, b in zip(unpacked_dsn, NONCE * (len(unpacked_dsn) // len(NONCE) + 1)))
112
+ compressed = zlib.compress(xored.encode())
113
+ return base64.b64encode(compressed).decode()
114
+
115
+
116
+ def dsn_unpack(packed_dsn: str) -> str:
117
+ # Reverse the packing process
118
+ decoded = base64.b64decode(packed_dsn)
119
+ decompressed = zlib.decompress(decoded).decode()
120
+ return "".join(chr(ord(a) ^ ord(b)) for a, b in zip(decompressed, NONCE * (len(decompressed) // len(NONCE) + 1)))
@@ -1,31 +1,37 @@
1
- import click
1
+ import logging
2
2
  import os
3
3
  import os.path
4
+ import queue
4
5
  import sys
5
6
  import time
6
- import logging
7
- import queue
8
-
9
- from click_default_group import DefaultGroup
10
7
  from contextlib import contextmanager
11
8
  from functools import partial
12
- from watchdog.observers import Observer
9
+
10
+ import click
11
+ import sentry_sdk
12
+ from click_default_group import DefaultGroup
13
13
  from watchdog.events import FileSystemEventHandler
14
+ from watchdog.observers import Observer
14
15
 
15
16
  import dfsync.filters as filters
16
17
  import dfsync.lib as lib
17
- from dfsync.backends import rsync_backend, kube_backend
18
+ from dfsync.backends import kube_backend, rsync_backend
19
+ from dfsync.backends.rsync import set_rsync_executable
20
+ from dfsync.char_ui import KeyController
21
+ from dfsync.config import read_config
18
22
  from dfsync.distribution import (
23
+ AsyncVersionChecker,
19
24
  get_installed_version,
20
25
  get_latest_version,
21
- is_older_version,
22
- AsyncVersionChecker,
23
26
  is_installed_in_editable_mode,
27
+ is_older_version,
24
28
  update_package,
25
29
  )
26
- from dfsync.config import read_config
27
- from dfsync.char_ui import KeyController
28
- from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
30
+ from dfsync.kube_credentials import (
31
+ contextualize_kube_credentials,
32
+ normalized_k8s_url,
33
+ update_local_kube_config,
34
+ )
29
35
 
30
36
  logging.basicConfig(level=logging.WARN)
31
37
 
@@ -34,6 +40,7 @@ BACKENDS = {
34
40
  "file-rsync": rsync_backend,
35
41
  "kube-rsync": kube_backend,
36
42
  }
43
+ SENTRY_DSN = "eJyzEpVxkQ+O9gpKYAoIdwyKFinjYnZ04gnSjXXhTg/xFEwOiA+Li/ENULSP4+Rh5JCS0Y33lpThZlDmZ2MwlhOUqg0CAPv5DeE="
37
44
 
38
45
 
39
46
  class IgnoreEvent(Exception):
@@ -267,6 +274,7 @@ def version():
267
274
  """
268
275
  Print the currently installed version
269
276
  """
277
+ sentry_sdk.set_tag("command", "version")
270
278
  _version()
271
279
 
272
280
 
@@ -297,6 +305,7 @@ def self_update():
297
305
  """
298
306
  Update dfsync to the latest version published on pypi
299
307
  """
308
+ sentry_sdk.set_tag("command", "self_update")
300
309
  latest_version = get_latest_version()
301
310
  installed_version = get_installed_version()
302
311
  if not is_older_version(installed_version, latest_version):
@@ -319,6 +328,7 @@ def import_kube_host(kube_host=None, credentials=None):
319
328
 
320
329
  ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
321
330
  """
331
+ sentry_sdk.set_tag("command", "import_kube_host")
322
332
  patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
323
333
  update_local_kube_config(patch)
324
334
 
@@ -366,6 +376,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
366
376
  * editor-aware: changes to temporary files created by source code editors will be ignored
367
377
  * transparent: every action is diligently logged in the console
368
378
  """
379
+ sentry_sdk.set_tag("command", "sync")
369
380
  logging.basicConfig(
370
381
  level=logging.WARN,
371
382
  format="%(asctime)s - %(message)s",
@@ -392,17 +403,22 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
392
403
  click.echo(f"Source file/dirs not found: {', '.join(missing)}")
393
404
 
394
405
  backend, destination_dir = split_destination(destination_dir)
406
+ sentry_sdk.set_tag("sync-backend", backend)
395
407
  try:
396
408
  if len(paths) == 0:
397
409
  raise ValueError("No source file/dirs found")
398
410
  elif len(missing) > 0:
399
411
  click.echo(f"Using source file/dirs {', '.join(paths)}")
400
412
 
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()
413
+ if backend == "kube-rsync":
414
+ installed_rsync = lib.check_rsync()
415
+ if installed_rsync != lib.RsyncFlavour.SAMBA_RSYNC and lib.RsyncFlavour.SAMBA_RSYNC.exists_installed:
416
+ set_rsync_executable(lib.RsyncFlavour.SAMBA_RSYNC.executable)
417
+ elif installed_rsync != lib.RsyncFlavour.SAMBA_RSYNC:
418
+ click.echo(f"Found rsync: {installed_rsync.executable}")
419
+ raise UnsupportedRsyncError()
405
420
 
421
+ sentry_sdk.set_tag("sync-backend", backend)
406
422
  backend_options = dict(
407
423
  destination_dir=destination_dir,
408
424
  supervisor=supervisor,
@@ -448,6 +464,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
448
464
  )
449
465
  )
450
466
  click.echo(str(e))
467
+ sentry_sdk.capture_exception(e)
451
468
  return -1
452
469
 
453
470
  checker = AsyncVersionChecker()
@@ -514,4 +531,8 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
514
531
 
515
532
 
516
533
  if __name__ == "__main__":
534
+ sentry_sdk.init(
535
+ lib.dsn_unpack(SENTRY_DSN),
536
+ release=f"dfsync@{get_installed_version()}",
537
+ )
517
538
  sys.exit(main() or 0)
@@ -0,0 +1,9 @@
1
+ import sentry_sdk
2
+
3
+ sentry_sdk.init(
4
+ dsn="http://7717d7a6e4bf47fd850192acb50accd4@10.6.1.11//1",
5
+ )
6
+ try:
7
+ 1 / 0
8
+ except Exception as e:
9
+ sentry_sdk.capture_exception(e)
@@ -0,0 +1,28 @@
1
+ from dfsync.filters import USER_FILTERS, PythonBlackFilter, PythonSyntaxFilter
2
+
3
+
4
+ def test_rejects_python_that_does_not_parse(tmp_path):
5
+ src = tmp_path / "broken.py"
6
+ src.write_text("def f(:\n")
7
+ assert PythonSyntaxFilter().is_filtered(src_file_path=str(src))
8
+
9
+
10
+ def test_syncs_valid_python_even_if_not_formatted(tmp_path):
11
+ src = tmp_path / "ugly.py"
12
+ src.write_text("x = [1,\n2]\n")
13
+ assert not PythonSyntaxFilter().is_filtered(src_file_path=str(src))
14
+
15
+
16
+ def test_ignores_files_that_are_not_python(tmp_path):
17
+ src = tmp_path / "notes.txt"
18
+ src.write_text("def f(:\n")
19
+ assert not PythonSyntaxFilter().is_filtered(src_file_path=str(src))
20
+
21
+
22
+ def test_syncs_a_deleted_file(tmp_path):
23
+ assert not PythonSyntaxFilter().is_filtered(src_file_path=str(tmp_path / "gone.py"))
24
+
25
+
26
+ def test_old_name_still_works():
27
+ assert PythonBlackFilter is PythonSyntaxFilter
28
+ assert any(isinstance(f, PythonSyntaxFilter) for f in USER_FILTERS)
@@ -1,5 +1,4 @@
1
- import pytest
2
- from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager
1
+ from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager, dsn_pack, dsn_unpack
3
2
 
4
3
 
5
4
  def test_threaded_operations_manager():
@@ -26,3 +25,24 @@ def test_controlled_threaded_operation():
26
25
  operation.stop()
27
26
  assert not operation.is_running
28
27
  assert operation.is_completed
28
+
29
+
30
+ def test_dsn_pack_unpack():
31
+ # Test basic packing and unpacking
32
+ original_dsn = "test_dsn_string"
33
+ packed = dsn_pack(original_dsn)
34
+ unpacked = dsn_unpack(packed)
35
+ assert unpacked == original_dsn
36
+
37
+ # Test with empty string
38
+ assert dsn_unpack(dsn_pack("")) == ""
39
+
40
+ # Test with longer string
41
+ long_dsn = "https://very.long.dsn.string.with.special/characters?key=value&other=123"
42
+ assert dsn_unpack(dsn_pack(long_dsn)) == long_dsn
43
+
44
+ # Test that packed value is different from original
45
+ assert dsn_pack("test") != "test"
46
+
47
+ # Test that consecutive packs produce same result
48
+ assert dsn_pack("test") == dsn_pack("test")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.10"
3
+ version = "0.4.11"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
@@ -14,9 +14,9 @@ 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 = "24.3.0"
17
+ sentry-sdk = "^2.26.0"
18
18
 
19
- [tool.poetry.dev-dependencies]
19
+ [tool.poetry.group.dev.dependencies]
20
20
  pytest = "^7.4"
21
21
  pytest_mock = "^3.12"
22
22
  ipython = "^8"
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