gitops 0.9.22__tar.gz → 0.10.0__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,14 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: gitops
3
- Version: 0.9.22
3
+ Version: 0.10.0
4
4
  Summary: Manage multiple apps across one or more k8s clusters.
5
5
  License: BSD
6
6
  Author: Jarek Głowacki
7
7
  Author-email: jarekwg@gmail.com
8
- Requires-Python: >=3.10,<4.0
8
+ Requires-Python: >=3.11,<4.0
9
9
  Classifier: License :: Other/Proprietary License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  Provides-Extra: server
@@ -20,7 +19,7 @@ Requires-Dist: httpx (>=0.18.1,<0.24.0) ; extra == "server"
20
19
  Requires-Dist: humanize (>=3.5.0,<4.0.0)
21
20
  Requires-Dist: invoke
22
21
  Requires-Dist: kubernetes_asyncio (>=25.1.2,<26.0.0) ; extra == "server"
23
- Requires-Dist: pyyaml (>=5.1.2)
22
+ Requires-Dist: pyyaml (>=6.0.1)
24
23
  Requires-Dist: sentry-sdk (>=1.3.0,<2.0.0) ; extra == "server"
25
24
  Requires-Dist: tabulate (>=0.8.9,<0.9.0)
26
25
  Requires-Dist: uvicorn (>=0.17.0,<0.18.0) ; extra == "server" or extra == "server"
@@ -5,13 +5,11 @@ from pathlib import Path
5
5
  from . import monkeypatches # NOQA
6
6
  from .utils.cli import success, warning
7
7
 
8
- __version__ = "0.9.22"
8
+ __version__ = "0.10.0"
9
9
 
10
10
 
11
11
  # Checking gitops version matches cluster repo version.
12
- versions_path = (
13
- Path(os.environ.get("GITOPS_APPS_DIRECTORY", "apps")).parent.absolute() / "setup.cfg"
14
- )
12
+ versions_path = Path(os.environ.get("GITOPS_APPS_DIRECTORY", "apps")).parent.absolute() / "setup.cfg"
15
13
  if versions_path.exists():
16
14
  import configparser
17
15
 
@@ -1,7 +1,7 @@
1
1
  import json
2
2
  import os
3
3
  from base64 import b64encode
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any
5
5
 
6
6
  from .utils import load_yaml
7
7
 
@@ -20,9 +20,9 @@ class App:
20
20
  def __init__(
21
21
  self,
22
22
  name: str,
23
- path: Optional[str] = None,
24
- deployments: Optional[Dict] = None,
25
- secrets: Optional[Dict] = None,
23
+ path: str | None = None,
24
+ deployments: dict | None = None,
25
+ secrets: dict | None = None,
26
26
  load_secrets: bool = True,
27
27
  account_id: str = "",
28
28
  ):
@@ -60,7 +60,7 @@ class App:
60
60
  current_dict = current_dict.setdefault(key, {})
61
61
  current_dict[keys[-1]] = value
62
62
 
63
- def _make_values(self, deployments: Dict, secrets: Dict) -> Dict:
63
+ def _make_values(self, deployments: dict, secrets: dict) -> dict:
64
64
  values = {
65
65
  **deployments,
66
66
  "secrets": {**{k: b64encode(v.encode()).decode() for k, v in secrets.items()}},
@@ -75,7 +75,7 @@ class App:
75
75
  values.pop("images", None)
76
76
  return values
77
77
 
78
- def _make_image(self, deployment_config: Dict):
78
+ def _make_image(self, deployment_config: dict):
79
79
  if "image-tag" in deployment_config:
80
80
  return deployment_config["images"]["template"].format(
81
81
  account_id=self.account_id,
@@ -119,16 +119,12 @@ class App:
119
119
  return self.values.get("cluster", "")
120
120
 
121
121
  @property
122
- def tags(self) -> List[str]:
122
+ def tags(self) -> list[str]:
123
123
  return self.values.get("tags", [])
124
124
 
125
125
  @property
126
126
  def service_account_name(self) -> str:
127
- return (
128
- self.values.get("serviceAccount", {}).get("name")
129
- or self.values.get("serviceAccountName")
130
- or "default"
131
- )
127
+ return self.values.get("serviceAccount", {}).get("name") or self.values.get("serviceAccountName") or "default"
132
128
 
133
129
 
134
130
  class Chart:
@@ -153,7 +149,7 @@ class Chart:
153
149
  chart: https://github.com/uptick/workforce
154
150
  """
155
151
 
156
- def __init__(self, definition: Union[Dict, str]):
152
+ def __init__(self, definition: dict | str):
157
153
  if isinstance(definition, str):
158
154
  # for backwards compat, any chart definition which is a string, is a git repo
159
155
  self.type = "git"
@@ -169,9 +165,7 @@ class Chart:
169
165
  self.version = definition.get("version")
170
166
  self.path = definition.get("path")
171
167
  else:
172
- raise Exception(
173
- "Chart definition must be either a dict or string. Instead it is: {definition}"
174
- )
168
+ raise Exception("Chart definition must be either a dict or string. Instead it is: {definition}")
175
169
 
176
170
  if self.git_repo_url and "@" in self.git_repo_url:
177
171
  self.git_repo_url, self.git_sha = self.git_repo_url.split("@")
@@ -1,22 +1,21 @@
1
1
  import os
2
- from typing import Dict
3
2
 
4
3
  import yaml
5
4
 
6
5
 
7
- def load_yaml(path) -> Dict:
8
- with open(path, "r") as file:
6
+ def load_yaml(path) -> dict:
7
+ with open(path) as file:
9
8
  return resolve_values(yaml.safe_load(file), path)
10
9
 
11
10
 
12
- def resolve_values(values, path) -> Dict:
11
+ def resolve_values(values, path) -> dict:
13
12
  if "extends" not in values:
14
13
  return values
15
14
  parent_values = load_yaml(os.path.join(os.path.dirname(path), values["extends"]))
16
15
  return deep_merge(parent_values, values)
17
16
 
18
17
 
19
- def deep_merge(parent: Dict, child: Dict) -> Dict:
18
+ def deep_merge(parent: dict, child: dict) -> dict:
20
19
  """Deeply merge two dictionaries.
21
20
 
22
21
  Dictionary entries will be followed and merged, anything else will be
@@ -31,7 +31,7 @@ def summary(ctx, filter="", exclude=""):
31
31
 
32
32
 
33
33
  @task
34
- def bump(
34
+ def bump( # noqa: C901
35
35
  ctx,
36
36
  filter,
37
37
  exclude="",
@@ -105,10 +105,7 @@ def bump(
105
105
  print(f"Redeploying {colourise(app_name, Fore.LIGHTGREEN_EX)}")
106
106
  update_app(app_name, **{"bump": str(uuid.uuid4())})
107
107
  else:
108
- print(
109
- f"Skipping {colourise(app_name, Fore.LIGHTGREEN_EX)}: already on"
110
- f" {colour_image(new_image_tag)}"
111
- )
108
+ print(f"Skipping {colourise(app_name, Fore.LIGHTGREEN_EX)}: already on" f" {colour_image(new_image_tag)}")
112
109
  if redeploy:
113
110
  commit_message = f"Redeploying {filter}"
114
111
  else:
@@ -166,18 +163,12 @@ def command(
166
163
  if sequential or (not interactive) or len(apps) == 1:
167
164
  for app in apps:
168
165
  # For each app, just run the coroutine and print the output
169
- print(
170
- asyncio.run(
171
- run_job(app, command, cleanup=cleanup, sequential=True, cpu=cpu, memory=memory)
172
- )
173
- )
166
+ print(asyncio.run(run_job(app, command, cleanup=cleanup, sequential=True, cpu=cpu, memory=memory)))
174
167
  else:
175
168
  # Build list of coroutines, and execute them all at once
176
169
  jobs = [
177
170
  (
178
- run_job(
179
- app, command, cleanup=cleanup, sequential=sequential, cpu=cpu, memory=memory
180
- ),
171
+ run_job(app, command, cleanup=cleanup, sequential=sequential, cpu=cpu, memory=memory),
181
172
  app.name,
182
173
  )
183
174
  for app in apps
@@ -257,10 +248,8 @@ def _getenv(env_or_secrets, filter, exclude, filter_values):
257
248
  for app in apps:
258
249
  print("-" * 20, progress(app.name), sep="\n")
259
250
  values = app.values.get(env_or_secrets)
260
- if type(values) == dict:
261
- filtered_values = (
262
- {k: v for k, v in values.items() if k in filter_values} if filter_values else values
263
- )
251
+ if isinstance(values, dict):
252
+ filtered_values = {k: v for k, v in values.items() if k in filter_values} if filter_values else values
264
253
  for k, v in filtered_values.items():
265
254
  print(f"{k}={v}")
266
255
  else:
@@ -129,13 +129,11 @@ def proxy(
129
129
  except ValueError:
130
130
  # app_name was not a valid dsn
131
131
  app = get_app_details(app_name, load_secrets=True)
132
- database_url = base64.b64decode(
133
- app.values["secrets"]["DATABASE_URL"].encode("ascii")
134
- ).decode("ascii")
132
+ database_url = base64.b64decode(app.values["secrets"]["DATABASE_URL"].encode("ascii")).decode("ascii")
135
133
  database_dsn = dsnparse.parse(database_url)
136
134
 
137
135
  if not local_port:
138
- local_port = random.randint(1000, 9999)
136
+ local_port = random.randint(1000, 9999) # noqa
139
137
 
140
138
  # Maybe we need to connect via RDS IAM instead!
141
139
  modified_dsn = dsnparse.parse(database_url)
@@ -154,15 +152,11 @@ def proxy(
154
152
 
155
153
  bastion_instance_id = bastion_instance_id or os.environ.get("GITOPS_BASTION_INSTANCE_ID")
156
154
  if not bastion_instance_id:
157
- raise Exception(
158
- "Please set GITOPS_BASTION_INSTANCE_ID environment variable for db proxy to work."
159
- )
155
+ raise Exception("Please set GITOPS_BASTION_INSTANCE_ID environment variable for db proxy to work.")
160
156
 
161
157
  aws_availability_zone = aws_availability_zone or os.environ.get("GITOPS_AWS_AVAILABILITY_ZONE")
162
158
  if not aws_availability_zone:
163
- raise Exception(
164
- "Please set GITOPS_AWS_AVAILABILITY_ZONE environment variable for db proxy to work."
165
- )
159
+ raise Exception("Please set GITOPS_AWS_AVAILABILITY_ZONE environment variable for db proxy to work.")
166
160
 
167
161
  print(progress(f"Creating a proxy to the RDS instance of: {app_name} "))
168
162
 
@@ -222,17 +216,10 @@ def logs(ctx, app_name, last=24):
222
216
  )
223
217
  for line in log_file["LogFileData"].split("\n"):
224
218
  if line:
225
- try:
226
- print(
227
- str(
228
- datetime.datetime.fromisoformat(line[:19])
229
- .replace(tzinfo=datetime.timezone.utc)
230
- .astimezone()
231
- )
232
- + line[23:]
233
- )
234
- except Exception:
235
- print(line)
219
+ print(
220
+ str(datetime.datetime.fromisoformat(line[:19]).replace(tzinfo=datetime.timezone.utc).astimezone())
221
+ + line[23:]
222
+ )
236
223
 
237
224
 
238
225
  @task
@@ -50,9 +50,7 @@ def shell_plus(ctx, app, cleanup=True, cpu=0, memory=0):
50
50
  app = get_app_details(app)
51
51
  if "production" in app.tags:
52
52
  confirm_dangerous_command()
53
- asyncio.run(
54
- run_job(app, f"{UNSAFE_MANAGE_PY} shell_plus", cleanup=cleanup, cpu=cpu, memory=memory)
55
- )
53
+ asyncio.run(run_job(app, f"{UNSAFE_MANAGE_PY} shell_plus", cleanup=cleanup, cpu=cpu, memory=memory))
56
54
  print(success("Done!"))
57
55
 
58
56
 
@@ -9,9 +9,7 @@ CACHE = {}
9
9
 
10
10
 
11
11
  def gen_secret(length=64):
12
- return "".join(
13
- random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)
14
- )
12
+ return "".join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
15
13
 
16
14
 
17
15
  def get_account_id():
@@ -1,6 +1,5 @@
1
- import os
1
+ import sys
2
2
  from pathlib import PosixPath
3
- from typing import List, Union
4
3
 
5
4
  from colorama import Fore
6
5
  from tabulate import tabulate
@@ -11,7 +10,7 @@ from gitops.utils import yaml as yaml
11
10
 
12
11
  from . import get_account_id
13
12
  from .cli import colourise, confirm, warning
14
- from .exceptions import AppDoesNotExist, AppOperationAborted
13
+ from .exceptions import AppDirectoryDoesNotExist, AppDoesNotExist, AppOperationAborted
15
14
  from .images import colour_image
16
15
  from .tags import colour_tags, validate_tags
17
16
 
@@ -22,7 +21,7 @@ def is_valid_app_directory(directory: PosixPath) -> bool:
22
21
  return all(file_paths)
23
22
 
24
23
 
25
- def get_app_details(app_name: str, load_secrets: bool = True) -> App:
24
+ def get_app_details(app_name: str, load_secrets: bool = True, exit_if_not_found: bool = True) -> App:
26
25
  account_id = get_account_id() if load_secrets else "UNKNOWN"
27
26
  try:
28
27
  app = App(
@@ -31,19 +30,23 @@ def get_app_details(app_name: str, load_secrets: bool = True) -> App:
31
30
  load_secrets=load_secrets,
32
31
  account_id=account_id,
33
32
  )
34
- except FileNotFoundError:
35
- # Check if apps dir doesn't exist, or just that one app
36
- if os.path.exists("apps"):
37
- raise AppDoesNotExist(app_name)
33
+ except FileNotFoundError as e:
34
+ msg, exc = "", Exception
35
+ if get_apps_directory().exists():
36
+ msg, exc = f"There's no app with the name '{app_name}', silly.", AppDoesNotExist
38
37
  else:
39
- raise AppDoesNotExist()
38
+ msg, exc = "Could not find an 'apps' directory. Are you in a cluster repo?", AppDirectoryDoesNotExist
39
+ if exit_if_not_found:
40
+ sys.exit(warning(msg))
41
+ else:
42
+ raise exc(msg) from e
40
43
 
41
44
  return app
42
45
 
43
46
 
44
47
  def update_app(app_name: str, **kwargs):
45
48
  filename = get_apps_directory() / app_name / "deployment.yml"
46
- with open(filename, "r") as f:
49
+ with open(filename) as f:
47
50
  data = yaml.safe_load(f)
48
51
  for k, v in kwargs.items():
49
52
  if k not in DEPLOYMENT_ATTRIBUTES:
@@ -57,14 +60,14 @@ def update_app(app_name: str, **kwargs):
57
60
  yaml.dump(data, f, default_flow_style=False)
58
61
 
59
62
 
60
- def get_apps(
61
- filter: Union[List[str], str] = "",
62
- exclude: Union[List[str], str] = "",
63
+ def get_apps( # noqa: C901
64
+ filter: list[str] | str = "",
65
+ exclude: list[str] | str = "",
63
66
  mode="PROMPT",
64
67
  autoexclude_inactive=True,
65
68
  message=None,
66
69
  load_secrets=True,
67
- ) -> List[App]:
70
+ ) -> list[App]:
68
71
  """Return apps that contain ALL of the tags listed in `filter` and NONE of the tags listed in
69
72
  `exclude`. The incoming filter and exclude params may come in as a list or commastring.
70
73
  For the purpose of this filtering, app names and image tag prefixes are also considered as
@@ -87,12 +90,12 @@ def get_apps(
87
90
  exclude.add("inactive")
88
91
 
89
92
  apps = []
90
- existing_tags = set()
93
+ existing_tags = {"suspended", "inactive", "release", "qa", "no_shutdown"}
91
94
 
92
95
  try:
93
96
  directory = sorted(get_apps_directory().iterdir())
94
- except FileNotFoundError:
95
- raise AppDoesNotExist()
97
+ except FileNotFoundError as e:
98
+ raise AppDoesNotExist() from e
96
99
  for entry in directory:
97
100
  if not entry.is_dir():
98
101
  continue
@@ -125,7 +128,7 @@ def get_apps(
125
128
  return apps
126
129
 
127
130
 
128
- def preview_apps(apps: List[App]):
131
+ def preview_apps(apps: list[App]):
129
132
  """Produce a summary of apps, their tags, and their expected images & replicas.
130
133
  May not necessarily reflect actual app statuses if recent changes haven't yet been pushed to
131
134
  the remote, or the deployment has failed.
@@ -134,7 +137,7 @@ def preview_apps(apps: List[App]):
134
137
  for app in apps:
135
138
  table.append(
136
139
  [
137
- colourise(app.name, Fore.RED, lambda _: "inactive" in app.tags),
140
+ colourise(app.name, Fore.RED, lambda _: "inactive" in app.tags), # noqa
138
141
  colour_image(app.image_tag),
139
142
  app.cluster,
140
143
  colourise(
@@ -76,18 +76,11 @@ async def print_async_complete(task, x, y, status_y_offset, win_info, sem):
76
76
 
77
77
 
78
78
  async def async_run(cmd):
79
- proc = await asyncio.create_subprocess_shell(
80
- cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
81
- )
79
+ proc = await asyncio.create_subprocess_shell(cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)
82
80
 
83
81
  stdout, stderr = await proc.communicate()
84
82
  return (
85
83
  stdout,
86
84
  stderr,
87
- (
88
- success("[stdout]\n")
89
- + f"{stdout.decode()}\n"
90
- + warning("[stderr]\n")
91
- + f"{stderr.decode()}\n"
92
- ),
85
+ (success("[stdout]\n") + f"{stdout.decode()}\n" + warning("[stderr]\n") + f"{stderr.decode()}\n"),
93
86
  )
@@ -0,0 +1,14 @@
1
+ class AppOperationAborted(Exception):
2
+ pass
3
+
4
+
5
+ class AppDoesNotExist(Exception):
6
+ pass
7
+
8
+
9
+ class AppDirectoryDoesNotExist(Exception):
10
+ pass
11
+
12
+
13
+ class CommandError(Exception):
14
+ pass
@@ -29,9 +29,7 @@ def get_latest_image(repository_name: str, prefix: str) -> str:
29
29
  ):
30
30
  for image in ecr_response["imageDetails"]:
31
31
  if prefix != "":
32
- if prefix_tags := [
33
- tag for tag in image["imageTags"] if tag.startswith(prefix + "-")
34
- ]:
32
+ if prefix_tags := [tag for tag in image["imageTags"] if tag.startswith(prefix + "-")]:
35
33
  results.append((prefix_tags[0], image["imagePushedAt"]))
36
34
  else:
37
35
  if prefix_tags := [tag for tag in image["imageTags"] if "-" not in tag]:
@@ -69,4 +67,4 @@ def color_hash(bit):
69
67
  Fore.MAGENTA,
70
68
  Fore.CYAN,
71
69
  Fore.WHITE,
72
- ][int.from_bytes(md5(bit.encode()).digest(), "big") % 7]
70
+ ][int.from_bytes(md5(bit.encode()).digest(), "big") % 7] # noqa: S324
@@ -12,7 +12,7 @@ from base64 import b64encode
12
12
  from contextlib import contextmanager
13
13
  from datetime import datetime
14
14
  from functools import wraps
15
- from typing import Dict, TypedDict
15
+ from typing import TypedDict
16
16
 
17
17
  import boto3
18
18
  import humanize
@@ -105,7 +105,7 @@ def download_backup(product, prefix, index, path=None, datestamp=False):
105
105
  def copy_db(ctx, source, destination, context=""):
106
106
  """Copy database between two applications."""
107
107
  with tempfile.NamedTemporaryFile("wt", suffix=".yml") as tmp:
108
- job = open("copy-db-job.yml", "r").read()
108
+ job = open("copy-db-job.yml").read()
109
109
  job = job.replace("{{ source }}", source)
110
110
  job = job.replace("{{ destination }}", destination)
111
111
  tmp.write(job)
@@ -125,9 +125,7 @@ def copy_db(ctx, source, destination, context=""):
125
125
  )
126
126
  print("done")
127
127
  run(
128
- ("kubectl delete -n workforce job copy-db-{source}-{destination}").format(
129
- source=source, destination=destination
130
- ),
128
+ (f"kubectl delete -n workforce job copy-db-{source}-{destination}"),
131
129
  hide=True,
132
130
  )
133
131
 
@@ -140,7 +138,7 @@ def get_backups(product, prefix):
140
138
  name = obj.key.split("/")[-1].split(".")[0]
141
139
  if not name:
142
140
  continue
143
- dt = datetime.strptime(name, "%Y-%m-%d_%H-%M-%S")
141
+ dt = datetime.strptime(name, "%Y-%m-%d_%H-%M-%S").astimezone(None)
144
142
  all_backups.append((name, dt, obj.size, obj.key))
145
143
  return sorted(all_backups, key=lambda x: x[1])
146
144
 
@@ -154,9 +152,9 @@ def get_backup_datestamp(url):
154
152
  def get_secret(name, base64=False):
155
153
  try:
156
154
  value = os.environ[name]
157
- except KeyError:
155
+ except KeyError as e:
158
156
  msg = f"Variable {Fore.RED}{name}{Fore.RESET} missing from environment."
159
- raise CommandError(msg)
157
+ raise CommandError(msg) from e
160
158
  if base64:
161
159
  value = b64encode(value.encode()).decode()
162
160
  else:
@@ -183,15 +181,13 @@ def run_wrapper(intro):
183
181
 
184
182
  def make_key(length=64):
185
183
  """Generate a sequence of random characters."""
186
- return "".join(
187
- random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length)
188
- )
184
+ return "".join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(length))
189
185
 
190
186
 
191
187
  def render_template(
192
188
  template: str,
193
- values: Dict = None,
194
- extra_labels: Dict = None,
189
+ values: dict = None,
190
+ extra_labels: dict = None,
195
191
  container_resources: ContainerResources = None,
196
192
  ) -> str:
197
193
  """Given a yaml of a K8s Job, replace template values and add extra labels to the pod spec"""
@@ -214,13 +210,13 @@ def render_template(
214
210
 
215
211
  async def _run_job(
216
212
  path,
217
- values: Dict = None,
213
+ values: dict = None,
218
214
  context="",
219
215
  namespace="default",
220
216
  attach=False,
221
217
  cleanup=True,
222
218
  sequential=True,
223
- extra_labels: Dict = None,
219
+ extra_labels: dict = None,
224
220
  container_resources: ContainerResources = None,
225
221
  ):
226
222
  name = values["name"]
@@ -228,7 +224,7 @@ async def _run_job(
228
224
  with tempfile.NamedTemporaryFile("wt", suffix=".yml") as tmp:
229
225
  # Generate yaml template to render
230
226
  rendered_template = render_template(
231
- open(get_apps_directory() / ".." / path, "r").read(),
227
+ open(get_apps_directory() / ".." / path).read(),
232
228
  values,
233
229
  extra_labels,
234
230
  container_resources=container_resources,
@@ -293,20 +289,14 @@ async def _run_job(
293
289
 
294
290
  async def wait_for_pod(context, namespace, pod):
295
291
  while True:
296
- cmd = (
297
- "kubectl get pod"
298
- f" --context {context}"
299
- f" -n {namespace}"
300
- ' -o jsonpath="{.status.phase}"'
301
- f" {pod}"
302
- )
292
+ cmd = "kubectl get pod" f" --context {context}" f" -n {namespace}" ' -o jsonpath="{.status.phase}"' f" {pod}"
303
293
  stdout, _, _ = await async_run(cmd)
304
294
  stdout = stdout.decode().lower()
305
295
  if stdout != "pending":
306
296
  return stdout
307
297
 
308
298
 
309
- def retry(*args, max_attempts=3, delay=1):
299
+ def retry(*args, max_attempts=3, delay=1): # noqa: C901
310
300
  """A decorator for retrying a function.
311
301
 
312
302
  After `max_attempts` failed attempts the last thrown exception
@@ -1,15 +1,15 @@
1
1
  [tool.poetry]
2
2
  name = "gitops"
3
- version = "0.9.22"
3
+ version = "0.10.0"
4
4
  description = "Manage multiple apps across one or more k8s clusters."
5
5
  authors = ["Jarek Głowacki <jarekwg@gmail.com>"]
6
6
  license = "BSD"
7
7
 
8
8
  [tool.poetry.dependencies]
9
- python = "^3.10"
9
+ python = "^3.11"
10
10
  # CLI requirements
11
11
  invoke = "*"
12
- pyyaml = ">=5.1.2"
12
+ pyyaml = ">=6.0.1"
13
13
  dsnparse = "*"
14
14
  colorama = "^0.4.4"
15
15
  boto3 = "*"
@@ -33,14 +33,14 @@ server = [
33
33
  ]
34
34
 
35
35
  [tool.poetry.dev-dependencies]
36
- #Flake 9 is flake 8 with pyproject support =_=.
36
+ ruff = "^0.2.0"
37
37
  isort = "*"
38
38
  ipdb = "*"
39
39
  python-dotenv = "*"
40
40
  pytest = "*"
41
41
  requests = "^2.25.1"
42
42
  pre-commit = "^2.13.0"
43
- mypy = "^0.941"
43
+ mypy = "^1.8.0"
44
44
  black = "*"
45
45
  pytest-asyncio = "^0.20.3"
46
46
 
@@ -56,12 +56,11 @@ gitops = 'gitops.main:program.run'
56
56
  requires = ["poetry-core>=1.0.0"]
57
57
  build-backend = "poetry.core.masonry.api"
58
58
 
59
- ##### Tooling configuraiton
60
- [tool.black]
61
- line-length = 100
62
- target-version = ['py38']
63
- experimental_string_processing = true
64
-
65
- [tool.isort]
66
- profile = 'black'
67
- multi_line_output = 3
59
+ [tool.nitpick]
60
+ # Generated by the 'nitpick init' command
61
+ # More info at https://nitpick.rtfd.io/en/latest/configuration.html
62
+ style = [
63
+ 'https://raw.githubusercontent.com/uptick/.github/main/nitpick/python.toml',
64
+ 'py://nitpick/resources/any/commitizen',
65
+ ]
66
+ cache = "never"
@@ -1,22 +0,0 @@
1
- import sys
2
-
3
- from .cli import warning
4
-
5
-
6
- class AppOperationAborted(Exception):
7
- pass
8
-
9
-
10
- class AppDoesNotExist(Exception):
11
- def __init__(self, app_name=None):
12
- # Funky hack to stomp on traceback.
13
- if app_name:
14
- message = f"There's no app with the name '{app_name}', silly."
15
- else:
16
- message = "Could not find an 'apps' directory. Are you in a cluster repo?"
17
- self.args = (warning(message),)
18
- sys.exit(self)
19
-
20
-
21
- class CommandError(Exception):
22
- pass
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes