dfsync 0.4.0__tar.gz → 0.4.2__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 (35) hide show
  1. {dfsync-0.4.0 → dfsync-0.4.2}/PKG-INFO +1 -1
  2. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/kube_exec.py +8 -1
  3. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/rsync.py +135 -4
  4. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/filters.py +5 -1
  5. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/lib.py +17 -1
  6. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/monitor.py +99 -21
  7. {dfsync-0.4.0 → dfsync-0.4.2}/pyproject.toml +1 -1
  8. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/__init__.py +0 -0
  9. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/__init__.py +0 -0
  10. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/kube.py +0 -0
  11. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/char_ui.py +0 -0
  12. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/chcli.py +0 -0
  13. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/check_dns.py +0 -0
  14. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/cli.py +0 -0
  15. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/config.py +0 -0
  16. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/distribution.py +0 -0
  17. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/exp.py +0 -0
  18. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/generate.py +0 -0
  19. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/kube_credentials.py +0 -0
  20. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/__init__.py +0 -0
  21. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/bpe.py +0 -0
  22. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_data.py +0 -0
  23. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_docstring.py +0 -0
  24. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_expansion.py +0 -0
  25. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_func.py +0 -0
  26. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_pytest.py +0 -0
  27. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/oai.py +0 -0
  28. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/prompts.py +0 -0
  29. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/search.py +0 -0
  30. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/test_generate_data.py +0 -0
  31. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/tokens.py +0 -0
  32. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/util.py +0 -0
  33. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/scrape_anm.py +0 -0
  34. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/scrape_reord.py +0 -0
  35. {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/transactions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dfsync
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -46,14 +46,21 @@ def main():
46
46
  print(f"[{pod_name}] Running: {cmd_str}", file=f, flush=True)
47
47
  try:
48
48
  subprocess.check_call(cmd)
49
+ return 0
50
+
51
+ except subprocess.CalledProcessError as e:
52
+ err = str(e) or type(e)
53
+ print(f"[{pod_name}] Error: {err}", file=f, flush=True)
54
+ return e.returncode
49
55
 
50
56
  except Exception as e:
51
57
  err = str(e) or type(e)
52
58
  print(f"[{pod_name}] Error: {err}", file=f, flush=True)
59
+ return 1
53
60
 
54
61
  finally:
55
62
  print(f"[{pod_name}] Exiting", file=f, flush=True)
56
63
 
57
64
 
58
65
  if __name__ == "__main__":
59
- main()
66
+ sys.exit(main())
@@ -1,6 +1,7 @@
1
- import logging, os, os.path, subprocess
1
+ import logging, os, os.path, select, subprocess, time
2
2
 
3
3
  from dfsync.filters import list_files_to_ignore
4
+ from dfsync.lib import ControlledThreadedOperation
4
5
 
5
6
  EVENT_TYPE_MAP = {
6
7
  "created": "Created",
@@ -10,6 +11,107 @@ EVENT_TYPE_MAP = {
10
11
  }
11
12
 
12
13
 
14
+ class StopStreamReader(Exception):
15
+ pass
16
+
17
+
18
+ class RsyncStreamReader(ControlledThreadedOperation):
19
+ def __init__(self, stream):
20
+ super().__init__()
21
+ if stream is None:
22
+ raise ValueError("Rsync status stream must not be None")
23
+
24
+ self.stream = stream
25
+ self.timeout = 300.0
26
+
27
+ self.rsync_stats = None
28
+ self.rsync_error = None
29
+ self.rsync_permission_error_count = 0
30
+ self.stream_closed = False
31
+
32
+ def read_lines(self):
33
+ spoll = select.poll()
34
+ spoll.register(self.stream, select.POLLIN)
35
+ try:
36
+ line_timestamp = time.monotonic()
37
+ elapsed = 0
38
+
39
+ while elapsed < self.timeout:
40
+ if spoll.poll(0):
41
+ line = self.stream.readline()
42
+ if len(line) > 0:
43
+ line_timestamp = time.monotonic()
44
+ yield line.decode("utf8")
45
+ else:
46
+ return
47
+ else:
48
+ time.sleep(0.1)
49
+ elapsed = time.monotonic() - line_timestamp
50
+
51
+ if elapsed >= self.timeout:
52
+ raise subprocess.TimeoutExpired(cmd="read_lines", timeout=self.timeout)
53
+ except ValueError:
54
+ pass
55
+
56
+ finally:
57
+ self.stream_closed = True
58
+
59
+ try:
60
+ spoll.unregister(self.stream)
61
+ except:
62
+ # don't care at this point
63
+ pass
64
+
65
+ try:
66
+ self.stream.close()
67
+ except:
68
+ # don't care at this point
69
+ pass
70
+
71
+ def run(self):
72
+ try:
73
+ for line in self.read_lines():
74
+ if line and line.startswith("sent "):
75
+ # stdout
76
+ self.rsync_stats = line.strip()
77
+ elif line and line.startswith("X11 forwarding"):
78
+ # stdout
79
+ pass
80
+ elif line and line.startswith("building file list"):
81
+ # stdout
82
+ pass
83
+ elif line and line.startswith("total size"):
84
+ # stdout
85
+ pass
86
+ elif line and line.startswith("rsync error"):
87
+ # stderr
88
+ self.rsync_error = line.strip()
89
+ elif line and line.startswith("rsync: ") and "failed: Permission" in line:
90
+ # stderr
91
+ self.rsync_permission_error_count += 1
92
+ elif line and line.startswith("rsync: ") and "failed: Resource busy" in line:
93
+ # stderr
94
+ self.rsync_permission_error_count += 1
95
+
96
+ elif line and line.strip():
97
+ # echo(line.strip())
98
+ pass
99
+
100
+ if not self.is_running:
101
+ self.timeout = 1.0
102
+
103
+ except (subprocess.TimeoutExpired, StopStreamReader):
104
+ # Stop silently
105
+ pass
106
+
107
+ def stop(self, *args, **kwargs):
108
+ super().stop(*args, **kwargs)
109
+ self.stream.close()
110
+
111
+ while not self.stream_closed:
112
+ time.sleep(1.0)
113
+
114
+
13
115
  def echo(msg=""):
14
116
  print(f"{msg}")
15
117
 
@@ -76,14 +178,43 @@ class FileRsync:
76
178
  if rsync_cwd:
77
179
  popen_args.update(dict(cwd=rsync_cwd))
78
180
  try:
79
- subprocess.check_call(
181
+ rsync_process = subprocess.Popen(
80
182
  rsync_cmd,
81
183
  stdin=subprocess.DEVNULL,
82
- stdout=subprocess.DEVNULL,
83
- stderr=subprocess.DEVNULL,
184
+ stdout=subprocess.PIPE,
185
+ stderr=subprocess.PIPE,
84
186
  env=rsh_env,
85
187
  **popen_args,
86
188
  )
189
+ stdout = RsyncStreamReader(rsync_process.stdout)
190
+ stdout.start()
191
+
192
+ stderr = RsyncStreamReader(rsync_process.stderr)
193
+ stderr.start()
194
+
195
+ return_code = rsync_process.wait(timeout=None)
196
+
197
+ stdout.stop()
198
+ stderr.stop()
199
+
200
+ if stdout.rsync_stats:
201
+ echo(f" {stdout.rsync_stats}")
202
+
203
+ if return_code != 0:
204
+ if stderr.rsync_error:
205
+ echo(f" {stderr.rsync_error}")
206
+ else:
207
+ echo(f" rsync exit code: {return_code}")
208
+
209
+ if stderr.rsync_permission_error_count > 0:
210
+ files = "file" if stderr.rsync_permission_error_count == 1 else "files"
211
+ echo(
212
+ f" {stderr.rsync_permission_error_count} {files} had permission or other issues on destination"
213
+ )
214
+
215
+ if return_code != 23:
216
+ raise subprocess.CalledProcessError(returncode=return_code, cmd=rsync_cmd)
217
+
87
218
  except:
88
219
  echo("Sync failed")
89
220
  env_str = "N/A"
@@ -96,8 +96,12 @@ class PythonBlackFilter(LoggingFilter):
96
96
  black.format_file_contents(contents, fast=False, mode=black.FileMode())
97
97
  return False
98
98
 
99
+ except (FileNotFoundError, PermissionError, black.report.NothingChanged) as e:
100
+ return False
101
+
99
102
  except Exception as e:
100
- echo(f"Rejected {src_file_path}, {e}")
103
+ message = str(e) or type(e).__name__
104
+ echo(f"Rejected {src_file_path}, {message}")
101
105
  return True
102
106
 
103
107
 
@@ -1,16 +1,32 @@
1
1
  import time, threading
2
2
 
3
3
 
4
+ class ThreadedOperationsManager:
5
+ def __init__(self):
6
+ self.threads = []
7
+
8
+ def stop(self, *args, **kwargs):
9
+ for thread in self.threads:
10
+ thread.stop(*args, **kwargs)
11
+
12
+ def register_threaded_operation(self, op):
13
+ self.threads.append(op)
14
+
15
+
16
+ thread_manager = ThreadedOperationsManager()
17
+
18
+
4
19
  class ControlledThreadedOperation:
5
20
  def __init__(self):
6
21
  self._running = False
7
22
  self._thread = threading.Thread(target=self.run)
23
+ thread_manager.register_threaded_operation(self)
8
24
 
9
25
  def start(self):
10
26
  self._running = True
11
27
  self._thread.start()
12
28
 
13
- def stop(self, message: str = None):
29
+ def stop(self, *args, **kwargs):
14
30
  self._running = False
15
31
 
16
32
  @property
@@ -4,8 +4,10 @@ import os.path
4
4
  import sys
5
5
  import time
6
6
  import logging
7
+ import queue
7
8
 
8
9
  from click_default_group import DefaultGroup
10
+ from contextlib import contextmanager
9
11
  from functools import partial
10
12
  from watchdog.observers import Observer
11
13
  from watchdog.events import FileSystemEventHandler
@@ -16,6 +18,7 @@ from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
16
18
  from dfsync.config import read_config
17
19
  from dfsync.char_ui import KeyController
18
20
  from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config
21
+ from dfsync.lib import ControlledThreadedOperation, thread_manager
19
22
 
20
23
  logging.basicConfig(level=logging.WARN)
21
24
 
@@ -30,19 +33,50 @@ class IgnoreEvent(Exception):
30
33
  pass
31
34
 
32
35
 
33
- class FileChangedEventHandler(FileSystemEventHandler):
34
- def __init__(self, backend: str = "log", watched_dir: str = ".", input_controller: KeyController = None, **kwargs):
36
+ class FileChangedEventHandler(ControlledThreadedOperation, FileSystemEventHandler):
37
+ def __init__(
38
+ self,
39
+ backend: str = "log",
40
+ watched_dir: str = ".",
41
+ all_watched_dirs: list = None,
42
+ input_controller: KeyController = None,
43
+ **kwargs,
44
+ ):
45
+ super().__init__()
35
46
  self.filters = [*ALL_FILTERS]
36
47
 
37
48
  self.backend = backend
38
49
  self.backend_options = kwargs
39
50
  self.raised_exception = False
51
+ self.watched_dir = watched_dir
40
52
  self.abs_watched_dir = os.path.abspath(watched_dir)
41
53
  self.input_controller = input_controller
54
+ self.events = queue.Queue(maxsize=10000)
55
+ self.full_sync_threashold = 3
56
+ self.all_watched_dirs = all_watched_dirs if all_watched_dirs is not None else [watched_dir]
42
57
 
43
58
  def _log_backend(self, event):
44
59
  logging.info(event)
45
60
 
61
+ @contextmanager
62
+ def _input_lock(self):
63
+ if self.input_controller is None:
64
+ yield self
65
+ else:
66
+ with self.input_controller.getch_lock():
67
+ yield self
68
+
69
+ @contextmanager
70
+ def terminal_lock(self):
71
+ try:
72
+ with self._input_lock():
73
+ yield self
74
+ except IgnoreEvent:
75
+ pass
76
+ except:
77
+ self.raised_exception = True
78
+ raise
79
+
46
80
  def _sync(self, event):
47
81
  # It seems like in some contexts, event.src_path will be an absolute path
48
82
  # and in some other contexts, it will be a relative path
@@ -78,24 +112,67 @@ class FileChangedEventHandler(FileSystemEventHandler):
78
112
  except ValueError as e:
79
113
  raise IgnoreEvent() from e
80
114
 
81
- def _propagate_event(self, event):
82
- for file_filter in self.filters:
83
- if file_filter(event=event) is False:
84
- return
85
- self._sync(event)
115
+ def _drain_queue(self, timeout=0.5):
116
+ event = self.events.get(block=True, timeout=timeout)
117
+ latest_events = {event.src_path: event}
118
+ try:
119
+ # Allow a bit of time for the queue to fill
120
+ if self.events.qsize() == 0:
121
+ time.sleep(0.25)
122
+ # Started filling? allow a little bit more time
123
+ if self.events.qsize() > 0:
124
+ time.sleep(0.4)
125
+
126
+ # Start draining
127
+ while self.events.qsize() > 0:
128
+ event = self.events.get_nowait()
129
+ # Only keep the latest event for a given file path
130
+ latest_events[event.src_path] = event
131
+
132
+ except queue.Empty:
133
+ pass
134
+ finally:
135
+ return latest_events.values()
136
+
137
+ def _filter_events(self, latest_events, stop_threashold=None):
138
+ sync_events = []
139
+ for event in latest_events:
140
+ filtered = False
141
+ for file_filter in self.filters:
142
+ with self.terminal_lock():
143
+ if file_filter(event=event) is False:
144
+ filtered = True
145
+ break
146
+ if not filtered:
147
+ sync_events.append(event)
148
+ if stop_threashold is not None and len(sync_events) > stop_threashold:
149
+ return sync_events
150
+
151
+ return sync_events
152
+
153
+ def run(self):
154
+ while self._running:
155
+ try:
156
+ latest_events = self._drain_queue()
157
+ sync_events = self._filter_events(latest_events, stop_threashold=self.full_sync_threashold)
158
+ if len(sync_events) >= self.full_sync_threashold:
159
+ with self.terminal_lock():
160
+ self.backend.sync_project(self.all_watched_dirs, **self.backend_options)
161
+ else:
162
+ for event in sync_events:
163
+ with self.terminal_lock():
164
+ self._sync(event)
165
+ except queue.Empty:
166
+ time.sleep(0.001)
86
167
 
87
168
  def catch_all_handler(self, event):
88
169
  try:
89
- if self.input_controller is None:
90
- self._propagate_event(event)
91
- else:
92
- with self.input_controller.getch_lock():
93
- self._propagate_event(event)
94
- except IgnoreEvent:
170
+ # Add sync event to the sync queue
171
+ self.events.put(event)
172
+ except queue.Full:
173
+ # It's acceptable for events to be rejected from the queue
174
+ # because a busy queue will trigger a full-sync
95
175
  pass
96
- except:
97
- self.raised_exception = True
98
- raise
99
176
 
100
177
  def on_moved(self, event):
101
178
  self.catch_all_handler(event)
@@ -256,9 +333,10 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
256
333
  observer = Observer()
257
334
  for p in paths:
258
335
  event_handler = FileChangedEventHandler(
259
- backend_engine, watched_dir=p, input_controller=controller, **backend_options
336
+ backend_engine, watched_dir=p, all_watched_dirs=paths, input_controller=controller, **backend_options
260
337
  )
261
338
  handlers.append(event_handler)
339
+ event_handler.start()
262
340
  observer.schedule(event_handler, os.path.abspath(p), recursive=True)
263
341
 
264
342
  controller.on_key(
@@ -269,7 +347,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
269
347
  controller.on_key(
270
348
  "x",
271
349
  description="to exit",
272
- action=partial(controller.stop, "Exiting."),
350
+ action=partial(thread_manager.stop, "Exiting."),
273
351
  )
274
352
 
275
353
  try:
@@ -295,16 +373,16 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
295
373
  controller.raise_exceptions()
296
374
 
297
375
  except KeyboardInterrupt:
298
- controller.stop()
376
+ thread_manager.stop()
299
377
  click.echo("Received [Ctrl-C], exiting.")
300
378
 
301
379
  except:
302
- controller.stop()
380
+ thread_manager.stop()
303
381
  raise
304
382
 
305
383
  finally:
306
384
  observer.stop()
307
- checker.stop()
385
+ thread_manager.stop()
308
386
  backend_engine.on_monitor_exit(**backend_options)
309
387
  if observer.ident is not None:
310
388
  # only join the observer if it was previously started
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
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
File without changes