dfsync 0.4.9__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 (27) hide show
  1. {dfsync-0.4.9 → dfsync-0.4.11}/PKG-INFO +5 -3
  2. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/kube.py +2 -2
  3. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/rsync.py +14 -4
  4. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/filters.py +13 -8
  5. dfsync-0.4.11/dfsync/lib.py +120 -0
  6. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/monitor.py +61 -24
  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.9 → dfsync-0.4.11}/dfsync/test_lib.py +22 -2
  10. {dfsync-0.4.9 → dfsync-0.4.11}/pyproject.toml +3 -3
  11. dfsync-0.4.9/dfsync/lib.py +0 -49
  12. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/__init__.py +0 -0
  13. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/__init__.py +0 -0
  14. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/backends/kube_exec.py +0 -0
  15. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/char_ui.py +0 -0
  16. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/chcli.py +0 -0
  17. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/check_dns.py +0 -0
  18. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/cli.py +0 -0
  19. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/config.py +0 -0
  20. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/diffui.py +0 -0
  21. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/distribution.py +0 -0
  22. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/exp.py +0 -0
  23. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/kube_credentials.py +0 -0
  24. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_dfsync.py +0 -0
  25. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_distribution.py +0 -0
  26. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/test_kube_credentials.py +0 -0
  27. {dfsync-0.4.9 → dfsync-0.4.11}/dfsync/transactions.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: dfsync
3
- Version: 0.4.9
3
+ Version: 0.4.11
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -11,12 +11,14 @@ 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 (==24.3.0)
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
15
16
  Requires-Dist: click (>=8,<9)
16
17
  Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
17
18
  Requires-Dist: gitpython (>=3.1.14,<4.0.0)
18
19
  Requires-Dist: kubernetes (>=28.1.0,<29.0.0)
19
20
  Requires-Dist: paramiko[invoke] (>=3.2.0,<4.0.0)
21
+ Requires-Dist: sentry-sdk (>=2.26.0,<3.0.0)
20
22
  Requires-Dist: tenacity (>=8.0.1,<9.0.0)
21
23
  Requires-Dist: toml (>=0.10.2,<0.11.0)
22
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,
@@ -0,0 +1,120 @@
1
+ import base64
2
+ from enum import Enum
3
+ import platform
4
+ import subprocess
5
+ import time
6
+ import threading
7
+ import zlib
8
+
9
+
10
+ class ThreadedOperationsManager:
11
+ def __init__(self):
12
+ self.threads = []
13
+
14
+ def stop(self, *args, **kwargs):
15
+ for thread in self.threads:
16
+ thread.stop(*args, **kwargs)
17
+
18
+ def register_threaded_operation(self, op):
19
+ self.threads.append(op)
20
+
21
+
22
+ thread_manager = ThreadedOperationsManager()
23
+
24
+
25
+ class ControlledThreadedOperation:
26
+ def __init__(self):
27
+ self._running = False
28
+ self._thread = threading.Thread(target=self.run)
29
+ thread_manager.register_threaded_operation(self)
30
+
31
+ def start(self):
32
+ self._running = True
33
+ self._thread.start()
34
+
35
+ def stop(self, *args, **kwargs):
36
+ self._running = False
37
+
38
+ @property
39
+ def is_running(self):
40
+ return self._running
41
+
42
+ @property
43
+ def is_completed(self):
44
+ return self._running is False
45
+
46
+ def run(self):
47
+ while self._running:
48
+ try:
49
+ self._run_once()
50
+ except Exception:
51
+ time.sleep(0.001)
52
+
53
+ def _run_once(self):
54
+ time.sleep(0.001)
55
+
56
+
57
+ class RsyncFlavour(Enum):
58
+ SAMBA_RSYNC = "SAMBA_RSYNC"
59
+ OPEN_RSYNC = "OPENRSYNC"
60
+
61
+ @property
62
+ def executable(self):
63
+ if platform.system() == "Darwin": # Check if running on macOS
64
+ if self == RsyncFlavour.SAMBA_RSYNC:
65
+ return "/opt/homebrew/bin/rsync"
66
+ elif self == RsyncFlavour.OPEN_RSYNC:
67
+ return "/usr/bin/rsync"
68
+
69
+ # Default fallback for other platforms or if paths don't exist
70
+ return "rsync"
71
+
72
+ @property
73
+ def exists_installed(self):
74
+ try:
75
+ # Check if the executable exists and is executable
76
+ result = subprocess.run(
77
+ ["which", self.executable], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
78
+ )
79
+ if result.returncode != 0:
80
+ return False
81
+
82
+ # Additionally verify it can be executed
83
+ result = subprocess.run(
84
+ [self.executable, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False
85
+ )
86
+ return result.returncode == 0
87
+ except Exception:
88
+ return False
89
+
90
+
91
+ def check_rsync() -> RsyncFlavour:
92
+ try:
93
+ result = subprocess.run(["rsync", "--version"], capture_output=True, text=True)
94
+ version_text = result.stdout
95
+
96
+ if "rsync.samba.org/" in version_text:
97
+ return RsyncFlavour.SAMBA_RSYNC
98
+ else:
99
+ # Default to OpenRsync if we can't positively identify Samba rsync
100
+ return RsyncFlavour.OPEN_RSYNC
101
+ except (subprocess.SubprocessError, FileNotFoundError):
102
+ # If rsync isn't available or fails, default to Samba rsync
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
- from dfsync.backends import rsync_backend, kube_backend
17
+ import dfsync.lib as lib
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
17
22
  from dfsync.distribution import (
23
+ AsyncVersionChecker,
18
24
  get_installed_version,
19
25
  get_latest_version,
20
- is_older_version,
21
- AsyncVersionChecker,
22
26
  is_installed_in_editable_mode,
27
+ is_older_version,
23
28
  update_package,
24
29
  )
25
- from dfsync.config import read_config
26
- from dfsync.char_ui import KeyController
27
- from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
28
- from dfsync.lib import ControlledThreadedOperation, thread_manager
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):
@@ -44,7 +51,11 @@ class RelatedLocationsError(ValueError):
44
51
  pass
45
52
 
46
53
 
47
- class FileChangedEventHandler(ControlledThreadedOperation, FileSystemEventHandler):
54
+ class UnsupportedRsyncError(ValueError):
55
+ pass
56
+
57
+
58
+ class FileChangedEventHandler(lib.ControlledThreadedOperation, FileSystemEventHandler):
48
59
  def __init__(
49
60
  self,
50
61
  backend: str = "log",
@@ -258,11 +269,12 @@ def main():
258
269
  pass
259
270
 
260
271
 
261
- @main.command()
272
+ @main.command(add_help_option=False)
262
273
  def version():
263
274
  """
264
275
  Print the currently installed version
265
276
  """
277
+ sentry_sdk.set_tag("command", "version")
266
278
  _version()
267
279
 
268
280
 
@@ -279,21 +291,21 @@ def _version():
279
291
  click.echo(f"Latest version is {latest_version}, please update using `dfsync self-update`")
280
292
 
281
293
 
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):
294
+ @main.command(name="help", add_help_option=False)
295
+ def help():
285
296
  """
286
297
  Print the usage/help for dfsync
287
298
  """
288
- with click.Context(sync, info_name="dfsync") as ctx:
299
+ with click.Context(sync, info_name="dfsync", max_content_width=999) as ctx:
289
300
  click.echo(sync.get_help(ctx))
290
301
 
291
302
 
292
- @main.command()
303
+ @main.command(add_help_option=False)
293
304
  def self_update():
294
305
  """
295
306
  Update dfsync to the latest version published on pypi
296
307
  """
308
+ sentry_sdk.set_tag("command", "self_update")
297
309
  latest_version = get_latest_version()
298
310
  installed_version = get_installed_version()
299
311
  if not is_older_version(installed_version, latest_version):
@@ -316,6 +328,7 @@ def import_kube_host(kube_host=None, credentials=None):
316
328
 
317
329
  ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
318
330
  """
331
+ sentry_sdk.set_tag("command", "import_kube_host")
319
332
  patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
320
333
  update_local_kube_config(patch)
321
334
 
@@ -363,6 +376,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
363
376
  * editor-aware: changes to temporary files created by source code editors will be ignored
364
377
  * transparent: every action is diligently logged in the console
365
378
  """
379
+ sentry_sdk.set_tag("command", "sync")
366
380
  logging.basicConfig(
367
381
  level=logging.WARN,
368
382
  format="%(asctime)s - %(message)s",
@@ -389,12 +403,22 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
389
403
  click.echo(f"Source file/dirs not found: {', '.join(missing)}")
390
404
 
391
405
  backend, destination_dir = split_destination(destination_dir)
406
+ sentry_sdk.set_tag("sync-backend", backend)
392
407
  try:
393
408
  if len(paths) == 0:
394
409
  raise ValueError("No source file/dirs found")
395
410
  elif len(missing) > 0:
396
411
  click.echo(f"Using source file/dirs {', '.join(paths)}")
397
412
 
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()
420
+
421
+ sentry_sdk.set_tag("sync-backend", backend)
398
422
  backend_options = dict(
399
423
  destination_dir=destination_dir,
400
424
  supervisor=supervisor,
@@ -423,6 +447,14 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
423
447
  click.echo(str(e))
424
448
  return -1
425
449
 
450
+ except UnsupportedRsyncError:
451
+ click.echo(
452
+ "\nStarting from Mac OSX Sequoia 15.4, Apple are shipping openrsync instead of the GPL-licensed rsync from Samba"
453
+ "\nThe Apple version of rsync has less features and is currently not supported by dfsync"
454
+ "\nPlease use homebrew to install the GPL-licensed rsync from Samba with this command `brew install rsync`."
455
+ )
456
+ return -1
457
+
426
458
  except ValueError as e:
427
459
  click.echo(
428
460
  "Trying {} from source: {} to destination: '{}'\n".format(
@@ -432,6 +464,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
432
464
  )
433
465
  )
434
466
  click.echo(str(e))
467
+ sentry_sdk.capture_exception(e)
435
468
  return -1
436
469
 
437
470
  checker = AsyncVersionChecker()
@@ -455,7 +488,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
455
488
  controller.on_key(
456
489
  "x",
457
490
  description="to exit",
458
- action=partial(thread_manager.stop, "Exiting."),
491
+ action=partial(lib.thread_manager.stop, "Exiting."),
459
492
  )
460
493
 
461
494
  try:
@@ -481,16 +514,16 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
481
514
  controller.raise_exceptions()
482
515
 
483
516
  except KeyboardInterrupt:
484
- thread_manager.stop()
517
+ lib.thread_manager.stop()
485
518
  click.echo("Received [Ctrl-C], exiting.")
486
519
 
487
520
  except:
488
- thread_manager.stop()
521
+ lib.thread_manager.stop()
489
522
  raise
490
523
 
491
524
  finally:
492
525
  observer.stop()
493
- thread_manager.stop()
526
+ lib.thread_manager.stop()
494
527
  backend_engine.on_monitor_exit(**backend_options)
495
528
  if observer.ident is not None:
496
529
  # only join the observer if it was previously started
@@ -498,4 +531,8 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync, ver
498
531
 
499
532
 
500
533
  if __name__ == "__main__":
534
+ sentry_sdk.init(
535
+ lib.dsn_unpack(SENTRY_DSN),
536
+ release=f"dfsync@{get_installed_version()}",
537
+ )
501
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.9"
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"
@@ -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