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.
- {dfsync-0.4.0 → dfsync-0.4.2}/PKG-INFO +1 -1
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/kube_exec.py +8 -1
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/rsync.py +135 -4
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/filters.py +5 -1
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/lib.py +17 -1
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/monitor.py +99 -21
- {dfsync-0.4.0 → dfsync-0.4.2}/pyproject.toml +1 -1
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/__init__.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/__init__.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/backends/kube.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/char_ui.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/chcli.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/check_dns.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/cli.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/config.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/distribution.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/exp.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/generate.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/kube_credentials.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/__init__.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/bpe.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_data.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_docstring.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_expansion.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_func.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/generate_pytest.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/oai.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/prompts.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/search.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/test_generate_data.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/tokens.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/pycode/util.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/scrape_anm.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/scrape_reord.py +0 -0
- {dfsync-0.4.0 → dfsync-0.4.2}/dfsync/transactions.py +0 -0
|
@@ -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.
|
|
181
|
+
rsync_process = subprocess.Popen(
|
|
80
182
|
rsync_cmd,
|
|
81
183
|
stdin=subprocess.DEVNULL,
|
|
82
|
-
stdout=subprocess.
|
|
83
|
-
stderr=subprocess.
|
|
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
|
-
|
|
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,
|
|
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__(
|
|
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
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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(
|
|
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
|
-
|
|
376
|
+
thread_manager.stop()
|
|
299
377
|
click.echo("Received [Ctrl-C], exiting.")
|
|
300
378
|
|
|
301
379
|
except:
|
|
302
|
-
|
|
380
|
+
thread_manager.stop()
|
|
303
381
|
raise
|
|
304
382
|
|
|
305
383
|
finally:
|
|
306
384
|
observer.stop()
|
|
307
|
-
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|