dfsync 0.4.5__tar.gz → 0.4.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dfsync
3
- Version: 0.4.5
3
+ Version: 0.4.7
4
4
  Summary:
5
5
  Author: Mihai Balint
6
6
  Author-email: balint.mihai@gmail.com
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.8
10
10
  Classifier: Programming Language :: Python :: 3.9
11
11
  Classifier: Programming Language :: Python :: 3.10
12
12
  Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
13
14
  Requires-Dist: black (==23.7.0)
14
15
  Requires-Dist: click (>=8,<9)
15
16
  Requires-Dist: click-default-group (>=1.2.2,<2.0.0)
@@ -1,3 +1,4 @@
1
+ import copy
1
2
  import sys
2
3
  import os
3
4
  import os.path
@@ -11,11 +12,27 @@ from kubernetes.client.exceptions import ApiException
11
12
  from tenacity import retry, retry_if_exception_type, wait_exponential, stop_after_attempt
12
13
 
13
14
  from dfsync.filters import GIT_FILTER
15
+ from dfsync.kube_credentials import KubeContextConfig
14
16
  from .rsync import rsync_backend
15
17
 
16
18
  DEFAULT_COMMAND = []
17
19
  DEFAULT_PULL_POLICY = "Always"
18
-
20
+ DISABLED_PROBES = {
21
+ "readiness_probe": {
22
+ "_exec": {"command": ["true"]},
23
+ "timeout_seconds": 5,
24
+ "period_seconds": 15,
25
+ "initial_delay_seconds": 0,
26
+ "failure_threshold": 3,
27
+ },
28
+ "startup_probe": {
29
+ "_exec": {"command": ["true"]},
30
+ "timeout_seconds": 2,
31
+ "period_seconds": 1,
32
+ "initial_delay_seconds": 0,
33
+ "failure_threshold": 60,
34
+ },
35
+ }
19
36
  urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
20
37
 
21
38
 
@@ -147,88 +164,87 @@ class Generic:
147
164
  return ["true"]
148
165
 
149
166
 
167
+ def clean_probe(probe: dict) -> client.V1Probe:
168
+ if probe is None:
169
+ return None
170
+
171
+ probe_types = ["_exec", "http_get", "tcp_socket", "grpc"]
172
+ probe = copy.deepcopy(probe)
173
+ cleaned = {k: v for k, v in probe.items() if k not in probe_types or k is not None}
174
+ assert any(pt in cleaned for pt in probe_types), f"Probe missing probe type: {probe.keys()}"
175
+ return client.V1Probe(**cleaned)
176
+
177
+
150
178
  def please_wait(msg="Please wait"):
151
179
  print("⌛ {}...".format(msg))
152
180
 
153
181
 
154
- def _get_kube_api_client(context_name):
155
- configuration = client.Configuration()
156
- config.load_kube_config(
157
- context=context_name,
158
- client_configuration=configuration,
159
- )
160
- configuration.verify_ssl = True
161
- configuration.assert_hostname = False
162
- return client.ApiClient(configuration)
163
-
182
+ def get_selected_kubernetes(kube_host=None) -> KubeContextConfig:
183
+ try:
184
+ config.load_kube_config()
185
+ except Exception as e:
186
+ error_message = str(e)
187
+ print(f"Failed to load default kube config: {error_message}")
164
188
 
165
- def get_selected_kubernetes(kube_host=None):
166
189
  contexts, active_context = config.list_kube_config_contexts()
167
190
  if not contexts:
168
191
  raise ValueError("Cannot find any kubernetes contexts in kube-config file")
169
192
 
170
- context_apis = []
171
- selected_context = None
172
- selected_context_api = None
173
- active_context_api = None
193
+ k8sctx_configs = []
194
+ active_k8ctx = None
195
+ selected_k8ctx = None
174
196
  multiple_matches = False
197
+
175
198
  for c in contexts:
176
- ctx_client = client.CoreV1Api(api_client=_get_kube_api_client(c["name"]))
177
- context_apis.append([c, ctx_client])
199
+ k8sctx = KubeContextConfig(c["name"])
200
+ k8sctx_configs.append(k8sctx)
178
201
 
179
- ctx_api_host = ctx_client.api_client.configuration.host
180
- if c["name"] == active_context["name"]:
181
- active_context_api = ctx_client
202
+ if k8sctx.context_name == active_context["name"]:
203
+ active_k8ctx = k8sctx
204
+ k8sctx.is_active = True
182
205
 
183
- if kube_host is not None and kube_host.lower() == ctx_api_host.lower():
184
- if selected_context is None:
185
- selected_context = c
186
- selected_context_api = ctx_client
206
+ if kube_host is not None and kube_host.lower() == k8sctx.host.lower():
207
+ if selected_k8ctx is None:
208
+ selected_k8ctx = k8sctx
209
+ k8sctx.is_selected = True
187
210
  else:
188
211
  multiple_matches = True
189
212
 
190
- if len(contexts) > 1 or multiple_matches or (kube_host is not None and selected_context is None):
191
- print(f"Multiple kubernetes contexts in kube-config:")
213
+ if kube_host is None and selected_k8ctx is None and active_k8ctx is not None:
214
+ # Select the active context if no specific arg is given
215
+ selected_k8ctx = active_k8ctx
216
+ selected_k8ctx.is_selected = True
192
217
 
193
- for c, ctx_client in context_apis:
194
- ctx_api_host = ctx_client.api_client.configuration.host
195
- tags = []
196
- if c == selected_context:
197
- tags.append("ACTIVE-SELECTED")
198
- elif c["name"] == active_context["name"]:
199
- tags.append("ACTIVE")
218
+ if len(contexts) > 1 or multiple_matches or (kube_host is not None and selected_k8ctx is None):
219
+ print("Multiple kubernetes contexts in kube-config:")
220
+
221
+ for k8sctx in k8sctx_configs:
222
+ print(f" {k8sctx.active_glyph} {k8sctx.prettified_str}")
200
223
 
201
- if len(tags) > 0:
202
- tags_str = ",".join(tags)
203
- print(f" * {c['name']} on {ctx_api_host} [{tags_str}]")
204
- else:
205
- print(f" * {c['name']} on {ctx_api_host}")
206
224
  if kube_host is None:
207
- ctx_api_host = active_context_api.api_client.configuration.host
208
225
  print(
209
- f"Add --kube-host={ctx_api_host} to use a specific kubernetes API host\n"
226
+ f"Add --kube-host={active_k8ctx.host} to use a specific kubernetes API host\n"
210
227
  f"Alternatively, use 'kubectl config set current-context <name>' to set the current context"
211
228
  )
212
229
 
213
- if kube_host is not None and selected_context is None:
230
+ if kube_host is not None and selected_k8ctx is None:
214
231
  raise ValueError(f"None of the kubernetes contexts match '{kube_host}'")
215
232
 
216
- if selected_context is None:
217
- selected_context = active_context
218
- selected_context_api = active_context_api
219
- return selected_context, selected_context_api
233
+ if selected_k8ctx is None:
234
+ raise ValueError("Failed to select a kubernetes context config")
235
+
236
+ return selected_k8ctx
220
237
 
221
238
 
222
239
  class KubeReDeployer:
223
240
  def __init__(self, kube_host=None, pod_timeout=30, container_command=None, full_sync=True, **kwargs):
224
- config.load_kube_config()
225
- selected_context, selected_context_api = get_selected_kubernetes(kube_host)
241
+ k8sctx = get_selected_kubernetes(kube_host)
226
242
 
227
- self.context_name = selected_context["name"]
228
- self.api = selected_context_api
229
- self.apps_api = client.AppsV1Api(api_client=_get_kube_api_client(self.context_name))
243
+ self.context_name = k8sctx.context_name
244
+ self.api = k8sctx.core_v1_api()
245
+ self.apps_api = k8sctx.apps_v1_api()
230
246
 
231
- print(f"Using cluster: {self.context_name} on {self.api.api_client.configuration.host}")
247
+ print(f"Using cluster: {k8sctx.prettified_str}")
232
248
  self.rsync_backend_instance = rsync_backend()
233
249
  self._image_distro = None
234
250
  self.pod_timeout = pod_timeout
@@ -241,7 +257,16 @@ class KubeReDeployer:
241
257
  return
242
258
 
243
259
  command = self._image_distro.get_supervise_command(self.container_command)
244
- deployments = self._edit_deployment(pod, spec, status, command=command, image_pull_policy="Never")
260
+ deployments = self._edit_deployment(
261
+ pod,
262
+ spec,
263
+ status,
264
+ command=command,
265
+ image_pull_policy="Never",
266
+ startup_probe=None,
267
+ readiness_probe=None,
268
+ liveness_probe=None,
269
+ )
245
270
 
246
271
  print("Supervisor installing on {}".format(" ".join(deployments)))
247
272
 
@@ -303,6 +328,8 @@ class KubeReDeployer:
303
328
  existing_requests = container_spec.resources.requests or {}
304
329
  new_requests = v.get("requests") or {}
305
330
  container_spec.resources.requests = {**existing_requests, **new_requests}
331
+ elif k in ["readiness_probe", "startup_probe", "liveness_probe"]:
332
+ setattr(container_spec, k, clean_probe(v or DISABLED_PROBES.get(k)))
306
333
  else:
307
334
  setattr(container_spec, k, v)
308
335
 
@@ -315,7 +342,7 @@ class KubeReDeployer:
315
342
  metadata=deployment.metadata,
316
343
  spec=deployment.spec,
317
344
  )
318
- self.apps_api.patch_namespaced_deployment(
345
+ self.apps_api.replace_namespaced_deployment(
319
346
  name=deployment.metadata.name,
320
347
  namespace=namespace,
321
348
  body=patch,
@@ -60,7 +60,16 @@ class KeyHandlerException(Exception):
60
60
  pass
61
61
 
62
62
 
63
- KeyHandler = namedtuple("KeyHandler", "keys description action")
63
+ class KeyHandler:
64
+ def __init__(self, keys, description, action, strict_case):
65
+ self.keys = keys
66
+ self.description = description
67
+ self.action = action
68
+
69
+ self.event_keys = set(keys)
70
+ if not strict_case:
71
+ self.event_keys.update([k.lower() for k in self.keys])
72
+ self.event_keys.update([k.upper() for k in self.keys])
64
73
 
65
74
 
66
75
  class KeyController(ControlledThreadedOperation):
@@ -97,12 +106,12 @@ class KeyController(ControlledThreadedOperation):
97
106
  except (Exception, KeyboardInterrupt) as e:
98
107
  self._exception = e
99
108
 
100
- def on_key(self, *keys, description: str = None, action=None):
109
+ def on_key(self, *keys, description: str = None, action=None, strict_case=False):
101
110
  if action is None:
102
111
  raise ValueError("'action' cannot be None")
103
- handler = KeyHandler(keys=keys, description=description, action=action)
112
+ handler = KeyHandler(keys=keys, description=description, action=action, strict_case=strict_case)
104
113
  self.handlers.append(handler)
105
- for k in keys:
114
+ for k in handler.event_keys:
106
115
  self.key_handlers[k] = handler
107
116
 
108
117
  def raise_exceptions(self):
@@ -0,0 +1,63 @@
1
+ import difflib
2
+ import time
3
+
4
+ text1 = """
5
+ The project, "micrograd", is currently at version 0.8.0 and was authored by Andrej Karpathy. It is structured into two main directories: "micrograd" and "test". The "micrograd" directory contains the main source code for the project, while the "test" directory contains the test code.
6
+
7
+ The project uses Python 3.10 and has dependencies on numpy, sklearn, and matplotlib. The build system requires "poetry-core" and uses "poetry.core.masonry.api" as the build backend.
8
+
9
+ The main files in the project are:
10
+ - `test/test_engine.py`: Contains tests for the engine component of the project.
11
+ - `micrograd/nn.py`: Contains code related to neural networks.
12
+ - `micrograd/__init__.py`: The initialization file for the "micrograd" package.
13
+ - `micrograd/engine.py`: Contains the main engine code for the project. It defines a `Value` class that stores a single scalar value and its gradient. This class is central to the project as it implements methods for basic mathematical operations, and more importantly, methods for automatic differentiation (autograd), which is a key component in training neural networks.
14
+ - `micrograd/demo.py`: Likely contains a demonstration or example usage of the project.
15
+
16
+ This project appears to be a minimalistic implementation of a deep learning library, focusing on the autograd feature. The `Value` class in `engine.py` is the heart of this library, implementing the forward and backward passes necessary for neural network training.
17
+ """
18
+ text2 = """
19
+ The project, "micrograd", is currently at version 0.8.0 and was authored by Andrej Karpathy. It is structured into two main directories: "micrograd" and "test". The "micrograd" directory contains the main source code for the project, while the "test" directory contains the test code.
20
+
21
+ The project uses Python 3.10 and has dependencies on numpy, sklearn, and matplotlib. The build system requires "poetry-core" and uses "poetry.core.masonry.api" as the build backend.
22
+
23
+ The main files in the project are:
24
+ - `test/test_engine.py`: Contains tests for the engine component of the project.
25
+ - `micrograd/nn.py`: Contains code related to neural networks, including the definition of `Module`, `Neuron`, `Layer`, and `MLP` classes. These classes form the building blocks of the neural network functionality in the project. The `Module` class provides a base for other classes, with methods for zeroing gradients and retrieving parameters. The `Neuron` class represents a single neuron, the `Layer` class represents a layer of neurons, and the `MLP` class represents a multi-layer perceptron.
26
+ - `micrograd/__init__.py`: The initialization file for the "micrograd" package.
27
+ - `micrograd/engine.py`: Contains the main engine code for the project. It defines a `Value` class that stores a single scalar value and its gradient. This class is central to the project as it implements methods for basic mathematical operations, and more importantly, methods for automatic differentiation (autograd), which is a key component in training neural networks.
28
+ - `micrograd/demo.py`: Likely contains a demonstration or example usage of the project.
29
+
30
+ This project appears to be a minimalistic implementation of a deep learning library, focusing on the autograd feature. The `Value` class in `engine.py` is the heart of this library, implementing the forward and backward passes necessary for neural network training. The `nn.py` file extends this functionality by providing the necessary classes to build and manipulate neural networks.
31
+ """
32
+
33
+
34
+ def colored(diff_line: str) -> str:
35
+ line = diff_line if diff_line.endswith("\n") else f"{diff_line}\n"
36
+ red = "\033[38;2;255;0;0m"
37
+ green = "\033[38;2;0;255;0m"
38
+ reset_color = "\033[0m"
39
+
40
+ if line.startswith("+"):
41
+ return f"{green}{line[1:]}{reset_color}"
42
+ elif line.startswith("-"):
43
+ return f"{red}{line[1:]}{reset_color}"
44
+ else:
45
+ return line[1:]
46
+
47
+
48
+ def animate_diff(a: str, b: str):
49
+ a_lines = a.splitlines()
50
+ b_lines = b.splitlines()
51
+ diff = difflib.unified_diff(a_lines, b_lines, n=len(a_lines))
52
+ print("\033[H\033[J", end="") # Clear the screen
53
+
54
+ lines = []
55
+ for line in diff:
56
+ if line.startswith("---") or line.startswith("+++") or line.startswith("@@"):
57
+ continue
58
+ lines.append(colored(line))
59
+ print("".join(lines))
60
+
61
+
62
+ if __name__ == "__main__":
63
+ animate_diff(text1, text2)
@@ -2,6 +2,7 @@ import os
2
2
  import os.path
3
3
  import yaml
4
4
  from urllib.parse import urlparse
5
+ from kubernetes import client, config
5
6
 
6
7
  LOCAL_CREDENTIALS_FILE = os.path.expanduser("~/.kube/config")
7
8
  EMPTY_CREDENTIALS = """apiVersion: v1
@@ -18,7 +19,7 @@ def update_local_kube_config(new_kube_credentials):
18
19
  try:
19
20
  with open(LOCAL_CREDENTIALS_FILE, "r") as f:
20
21
  credentials = yaml.safe_load(f)
21
- except FileNotFoundError as e:
22
+ except FileNotFoundError:
22
23
  credentials = yaml.safe_load(EMPTY_CREDENTIALS)
23
24
  kube_dir, _ = os.path.split(LOCAL_CREDENTIALS_FILE)
24
25
  os.makedirs(kube_dir, exist_ok=True)
@@ -70,7 +71,7 @@ def update_local_kube_config(new_kube_credentials):
70
71
  credentials["users"].append(users_entry)
71
72
  print(f"Adding entry to users section '{user_name}'")
72
73
 
73
- if not credentials["current-context"]:
74
+ if not credentials.get("current-context"):
74
75
  credentials["current-context"] = contexts_entry["name"]
75
76
 
76
77
  credentials_yaml = yaml.dump(credentials)
@@ -132,3 +133,89 @@ def contextualize_kube_credentials(kube_host, credentials_file):
132
133
  print(exc)
133
134
 
134
135
  return {}
136
+
137
+
138
+ def normalized_k8s_url(k8s_url: str) -> str:
139
+ parsed = None
140
+ result = k8s_url
141
+ if k8s_url is None:
142
+ return None
143
+ elif not k8s_url:
144
+ raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
145
+ elif "://" not in k8s_url.lower():
146
+ result = f"https://{k8s_url}"
147
+ parsed = urlparse(result)
148
+ else:
149
+ parsed = urlparse(k8s_url)
150
+
151
+ if all([parsed.scheme, parsed.netloc]):
152
+ return result
153
+ else:
154
+ raise ValueError(f"Invalid k8s URL: `{k8s_url}`")
155
+
156
+
157
+ RED = "\033[38;2;255;0;0m"
158
+ GREEN = "\033[38;2;128;255;128m"
159
+ GRAY = "\033[38;2;128;128;128m"
160
+ NEUTRAL = "\033[0m"
161
+
162
+
163
+ class KubeContextConfig:
164
+ def __init__(self, context_name: str) -> None:
165
+ self._api_client = None
166
+ self._load_failure = None
167
+ self._configuration = client.Configuration()
168
+ self.context_name = context_name
169
+ self._adjust_ssl()
170
+ self.is_active = False
171
+ self.is_selected = False
172
+
173
+ def _adjust_ssl(self):
174
+ self._configuration.verify_ssl = True
175
+ self._configuration.assert_hostname = False
176
+
177
+ def _load(self):
178
+ if self._api_client is not None:
179
+ return
180
+ try:
181
+ config.load_kube_config(
182
+ context=self.context_name,
183
+ client_configuration=self._configuration,
184
+ )
185
+ self._adjust_ssl()
186
+ self._api_client = client.ApiClient(self._configuration)
187
+ self._load_failure = None
188
+ except Exception as e:
189
+ self._load_failure = str(e)
190
+
191
+ @property
192
+ def host(self):
193
+ self._load()
194
+ return self._configuration.host
195
+
196
+ @property
197
+ def api_client(self):
198
+ self._load()
199
+ return self._api_client
200
+
201
+ def apps_v1_api(self):
202
+ return client.AppsV1Api(api_client=self.api_client)
203
+
204
+ def core_v1_api(self):
205
+ return client.CoreV1Api(api_client=self.api_client)
206
+
207
+ @property
208
+ def active_glyph(self):
209
+ glyph = "@" if self.is_active else "*"
210
+ COLOR = GREEN if self.is_selected else GRAY
211
+ return f"{COLOR}{glyph}{NEUTRAL}"
212
+
213
+ @property
214
+ def prettified_str(self, include_failure=True):
215
+ fail_text = ""
216
+ if self._load_failure and include_failure:
217
+ fail_text = f"{RED}{self._load_failure[:80]}{NEUTRAL}"
218
+
219
+ txt = f"{self.context_name} on {self.host}"
220
+ COLOR = GREEN if self.is_selected else GRAY
221
+ return f"{COLOR}{txt}{NEUTRAL} {fail_text}"
@@ -1,4 +1,5 @@
1
- import time, threading
1
+ import time
2
+ import threading
2
3
 
3
4
 
4
5
  class ThreadedOperationsManager:
@@ -35,13 +36,13 @@ class ControlledThreadedOperation:
35
36
 
36
37
  @property
37
38
  def is_completed(self):
38
- return self._running == False
39
+ return self._running is False
39
40
 
40
41
  def run(self):
41
42
  while self._running:
42
43
  try:
43
44
  self._run_once()
44
- except:
45
+ except Exception:
45
46
  time.sleep(0.001)
46
47
 
47
48
  def _run_once(self):
@@ -17,7 +17,7 @@ from dfsync.distribution import get_installed_version, get_latest_version, is_ol
17
17
  from dfsync.filters import add_user_ignored_patterns_filter, ALL_FILTERS
18
18
  from dfsync.config import read_config
19
19
  from dfsync.char_ui import KeyController
20
- from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config
20
+ from dfsync.kube_credentials import contextualize_kube_credentials, update_local_kube_config, normalized_k8s_url
21
21
  from dfsync.lib import ControlledThreadedOperation, thread_manager
22
22
 
23
23
  logging.basicConfig(level=logging.WARN)
@@ -271,7 +271,7 @@ def import_kube_host(kube_host=None, credentials=None):
271
271
 
272
272
  ssh user@kube-host -C "sudo cat /root/.kube/config" | dfsync import-kube-host --kube-host=https://kube-host:6443
273
273
  """
274
- patch = contextualize_kube_credentials(kube_host, credentials)
274
+ patch = contextualize_kube_credentials(normalized_k8s_url(kube_host), credentials)
275
275
  update_local_kube_config(patch)
276
276
 
277
277
 
@@ -342,7 +342,7 @@ def sync(source, destination, supervisor, kube_host, pod_timeout, full_sync):
342
342
  backend_options = dict(
343
343
  destination_dir=destination_dir,
344
344
  supervisor=supervisor,
345
- kube_host=kube_host,
345
+ kube_host=normalized_k8s_url(kube_host),
346
346
  pod_timeout=pod_timeout,
347
347
  container_command=config.container_command,
348
348
  full_sync=full_sync,
@@ -0,0 +1,39 @@
1
+ from tempfile import NamedTemporaryFile
2
+
3
+ from dfsync.filters import EmacsBufferFilter, UntrackedGitFilesFilter
4
+
5
+
6
+ def test_version():
7
+ assert False, "Test version not implemented"
8
+
9
+
10
+ def test_emacs_buffer_filter():
11
+ emacs = EmacsBufferFilter()
12
+
13
+ assert emacs.is_filtered("test_dfsync.py") is False
14
+ assert emacs.is_filtered("./test_dfsync.py") is False
15
+ assert emacs.is_filtered("./tests/test_dfsync.py") is False
16
+ assert emacs.is_filtered("tests/test_dfsync.py") is False
17
+ assert emacs.is_filtered("/tests/test_dfsync.py") is False
18
+
19
+ assert emacs.is_filtered("#test_dfsync.py#") is True
20
+ assert emacs.is_filtered("./#test_dfsync.py#") is True
21
+ assert emacs.is_filtered("./tests/#test_dfsync.py#") is True
22
+ assert emacs.is_filtered("tests/#test_dfsync.py#") is True
23
+ assert emacs.is_filtered("/tests/#test_dfsync.py#") is True
24
+
25
+ assert emacs.is_filtered("test_dfsync.py~") is True
26
+ assert emacs.is_filtered("./test_dfsync.py~") is True
27
+ assert emacs.is_filtered("./tests/test_dfsync.py~") is True
28
+ assert emacs.is_filtered("tests/test_dfsync.py~") is True
29
+ assert emacs.is_filtered("/tests/test_dfsync.py~") is True
30
+
31
+
32
+ def test_untracked_git_files_filter():
33
+ git = UntrackedGitFilesFilter()
34
+
35
+ assert git.is_filtered(__name__) is False
36
+ assert git.is_filtered("does-not-exist.py") is False
37
+
38
+ with NamedTemporaryFile(dir=".") as f:
39
+ assert git.is_filtered(f.name) is True
@@ -0,0 +1,107 @@
1
+ import pytest
2
+ import yaml
3
+ import tempfile
4
+
5
+ from dfsync.kube_credentials import update_local_kube_config, normalized_k8s_url
6
+
7
+
8
+ @pytest.fixture
9
+ def mock_kube_credentials(mocker):
10
+ with tempfile.NamedTemporaryFile(delete=True) as temp:
11
+ mocker.patch("dfsync.kube_credentials.LOCAL_CREDENTIALS_FILE", temp.name)
12
+ return temp.name
13
+
14
+
15
+ def test_update_missing_kube_config(mock_kube_credentials):
16
+ new_kube_credentials = {
17
+ "clusters": [{"cluster": {"server": "test_server"}, "name": "test_cluster"}],
18
+ "contexts": [{"context": {"cluster": "test_cluster", "user": "test_user"}, "name": "test_context"}],
19
+ "users": [{"user": {"username": "test_user"}, "name": "test_user"}],
20
+ }
21
+ result = update_local_kube_config(new_kube_credentials)
22
+ assert "test_cluster" in result
23
+ assert "test_context" in result
24
+ assert "test_user" in result
25
+
26
+
27
+ def test_update_empty_kube_config(mock_kube_credentials):
28
+ with open(mock_kube_credentials, "wb") as f:
29
+ empty_config = {
30
+ "clusters": [],
31
+ "contexts": [],
32
+ "users": [],
33
+ }
34
+ f.write(yaml.dump(empty_config).encode())
35
+
36
+ new_kube_credentials = {
37
+ "clusters": [{"name": "test_cluster", "cluster": {"server": "test_server"}}],
38
+ "contexts": [{"name": "test_context", "context": {"cluster": "test_cluster", "user": "test_user"}}],
39
+ "users": [{"name": "test_user", "user": {"username": "test_username", "password": "test_password"}}],
40
+ }
41
+ update_local_kube_config(new_kube_credentials)
42
+
43
+ # Verify the results
44
+ with open(mock_kube_credentials, "r") as f:
45
+ credentials = yaml.safe_load(f)
46
+
47
+ assert credentials["clusters"][0]["name"] == "test_cluster"
48
+ assert credentials["clusters"][0]["cluster"]["server"] == "test_server"
49
+ assert credentials["contexts"][0]["name"] == "test_context"
50
+ assert credentials["contexts"][0]["context"]["cluster"] == "test_cluster"
51
+ assert credentials["contexts"][0]["context"]["user"] == "test_user"
52
+ assert credentials["users"][0]["name"] == "test_user"
53
+ assert credentials["users"][0]["user"]["username"] == "test_username"
54
+ assert credentials["users"][0]["user"]["password"] == "test_password"
55
+
56
+
57
+ def test_update_existing_kube_config(mock_kube_credentials):
58
+ with open(mock_kube_credentials, "wb") as f:
59
+ existing_config = {
60
+ "clusters": [
61
+ {"name": "existing_cluster", "cluster": {"server": "existing_server"}},
62
+ ],
63
+ "contexts": [
64
+ {"name": "existing_context", "context": {"cluster": "existing_cluster", "user": "existing_user"}}
65
+ ],
66
+ "users": [
67
+ {"name": "existing_user", "user": {"username": "existing_username", "password": "existing_password"}}
68
+ ],
69
+ }
70
+ f.write(yaml.dump(existing_config).encode())
71
+
72
+ new_kube_credentials = {
73
+ "clusters": [{"name": "new_cluster", "cluster": {"server": "new_server"}}],
74
+ "contexts": [{"name": "new_context", "context": {"cluster": "new_cluster", "user": "new_user"}}],
75
+ "users": [{"name": "new_user", "user": {"username": "new_username", "password": "new_password"}}],
76
+ }
77
+ update_local_kube_config(new_kube_credentials)
78
+
79
+ # Verify the results
80
+ with open(mock_kube_credentials, "r") as f:
81
+ credentials = yaml.safe_load(f)
82
+
83
+ assert len(credentials["clusters"]) == 2
84
+ assert len(credentials["contexts"]) == 2
85
+ assert len(credentials["users"]) == 2
86
+
87
+ assert credentials["clusters"][1]["name"] == "new_cluster"
88
+ assert credentials["clusters"][1]["cluster"]["server"] == "new_server"
89
+ assert credentials["contexts"][1]["name"] == "new_context"
90
+ assert credentials["contexts"][1]["context"]["cluster"] == "new_cluster"
91
+ assert credentials["contexts"][1]["context"]["user"] == "new_user"
92
+ assert credentials["users"][1]["name"] == "new_user"
93
+ assert credentials["users"][1]["user"]["username"] == "new_username"
94
+ assert credentials["users"][1]["user"]["password"] == "new_password"
95
+
96
+
97
+ def test_normalized_k8s_url():
98
+ assert normalized_k8s_url("http://example.com") == "http://example.com"
99
+ assert normalized_k8s_url("https://example.com") == "https://example.com"
100
+ assert normalized_k8s_url("example.com") == "https://example.com"
101
+ assert normalized_k8s_url("http://example.com") == "http://example.com"
102
+ assert normalized_k8s_url("https://192.168.0.123:443") == "https://192.168.0.123:443"
103
+ assert normalized_k8s_url("192.168.0.123:6443") == "https://192.168.0.123:6443"
104
+ with pytest.raises(ValueError):
105
+ normalized_k8s_url("")
106
+ with pytest.raises(ValueError):
107
+ normalized_k8s_url(None)
@@ -0,0 +1,28 @@
1
+ import pytest
2
+ from dfsync.lib import ControlledThreadedOperation, ThreadedOperationsManager
3
+
4
+
5
+ def test_threaded_operations_manager():
6
+ manager = ThreadedOperationsManager()
7
+ assert len(manager.threads) == 0
8
+
9
+ operation = ControlledThreadedOperation()
10
+ manager.register_threaded_operation(operation)
11
+ assert len(manager.threads) == 1
12
+
13
+ manager.stop()
14
+ assert operation.is_completed
15
+
16
+
17
+ def test_controlled_threaded_operation():
18
+ operation = ControlledThreadedOperation()
19
+ assert not operation.is_running
20
+ assert operation.is_completed
21
+
22
+ operation.start()
23
+ assert operation.is_running
24
+ assert not operation.is_completed
25
+
26
+ operation.stop()
27
+ assert not operation.is_running
28
+ assert operation.is_completed
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "dfsync"
3
- version = "0.4.5"
3
+ version = "0.4.7"
4
4
  description = ""
5
5
  authors = ["Mihai Balint <balint.mihai@gmail.com>"]
6
6
 
@@ -17,12 +17,16 @@ paramiko = {version = "^3.2.0", extras = ["invoke"]}
17
17
  black = "23.7.0"
18
18
 
19
19
  [tool.poetry.dev-dependencies]
20
- pytest = "^3.0"
20
+ pytest = "^7.4"
21
+ pytest_mock = "^3.12"
21
22
  ipython = "^8"
22
- ipdb = "^0.13.9"
23
+ ipdb = "^0.13"
23
24
  flake8 = "^4.0.1"
24
25
  pycodestyle = "^2.8.0"
25
26
 
27
+ [tool.black]
28
+ line-length = 120
29
+
26
30
  [tool.poetry.scripts]
27
31
  dfsync = "dfsync.cli:dfsync"
28
32
  [build-system]
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