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.
- {dfsync-0.4.10 → dfsync-0.4.11}/PKG-INFO +4 -3
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/kube.py +2 -2
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/rsync.py +14 -4
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/filters.py +13 -8
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/lib.py +19 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/monitor.py +37 -16
- dfsync-0.4.11/dfsync/sentry.py +9 -0
- dfsync-0.4.11/dfsync/test_filters.py +28 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_lib.py +22 -2
- {dfsync-0.4.10 → dfsync-0.4.11}/pyproject.toml +3 -3
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/__init__.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/backends/kube_exec.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/char_ui.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/chcli.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/cli.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/config.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/diffui.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/distribution.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/exp.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/kube_credentials.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_dfsync.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_distribution.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/test_kube_credentials.py +0 -0
- {dfsync-0.4.10 → dfsync-0.4.11}/dfsync/transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: dfsync
|
|
3
|
-
Version: 0.4.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
91
|
+
return self._syntax_check(src_file_path)
|
|
91
92
|
return False
|
|
92
93
|
|
|
93
|
-
def
|
|
94
|
+
def _syntax_check(self, src_file_path):
|
|
94
95
|
try:
|
|
95
|
-
with open(src_file_path, "
|
|
96
|
+
with open(src_file_path, "rb") as f:
|
|
96
97
|
contents = f.read()
|
|
97
|
-
|
|
98
|
+
compile(contents, src_file_path, "exec", dont_inherit=True)
|
|
98
99
|
return False
|
|
99
100
|
|
|
100
|
-
except (FileNotFoundError, PermissionError
|
|
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(),
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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,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
|
|
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.
|
|
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
|
-
|
|
17
|
+
sentry-sdk = "^2.26.0"
|
|
18
18
|
|
|
19
|
-
[tool.poetry.dev
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|