Skip to content

Commit b8fd6a0

Browse files
Merge pull request #145 from uptick/np-01/add-types
NP-01 chore: add type hints and enable strict mypy
2 parents 73de2ab + dd988db commit b8fd6a0

25 files changed

Lines changed: 801 additions & 630 deletions

gitops/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import sys
33
from pathlib import Path
44

5-
from gitops.utils.apps import App, get_app_details, get_apps
5+
from gitops.utils.apps import App, get_app_details, get_apps # type: ignore[attr-defined]
66

77
from .utils.cli import success, warning
88

gitops/common/app.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ def __init__(
2121
self,
2222
name: str,
2323
path: str | None = None,
24-
deployments: dict | None = None,
25-
secrets: dict | None = None,
24+
deployments: dict[str, Any] | None = None,
25+
secrets: dict[str, str] | None = None,
2626
load_secrets: bool = True,
2727
encode_secrets: bool = True,
2828
# deprecated
@@ -75,7 +75,7 @@ def set_value(self, path: str, value: Any) -> None:
7575
current_dict = current_dict.setdefault(key, {})
7676
current_dict[keys[-1]] = value
7777

78-
def _make_values(self, deployments: dict, secrets: dict[str, str]) -> dict:
78+
def _make_values(self, deployments: dict[str, Any], secrets: dict[str, str]) -> dict[str, Any]:
7979
def encode(value: str) -> str:
8080
return b64encode(str(value).encode()).decode() if self.encode_secrets else value
8181

@@ -93,14 +93,17 @@ def encode(value: str) -> str:
9393
values.pop("images", None)
9494
return values
9595

96-
def _make_image(self, deployment_config: dict) -> str:
96+
def _make_image(self, deployment_config: dict[str, Any]) -> str:
9797
if "image-tag" in deployment_config:
98-
return deployment_config["images"]["template"].format(
99-
account_id=self.account_id,
100-
tag=deployment_config["image-tag"],
98+
return str(
99+
deployment_config["images"]["template"].format(
100+
account_id=self.account_id,
101+
tag=deployment_config["image-tag"],
102+
)
101103
)
102104
else:
103-
return deployment_config.get("image", "")
105+
image = deployment_config.get("image", "")
106+
return str(image) if image else ""
104107

105108
@property
106109
def image(self) -> str:
@@ -109,7 +112,7 @@ def image(self) -> str:
109112
if isinstance(image, dict):
110113
return f"{image['repository']}:{image.get('tag', 'latest')}"
111114
else:
112-
return image
115+
return str(image) if image else ""
113116

114117
@property
115118
def image_repository_name(self) -> str:
@@ -139,20 +142,31 @@ def image_prefix(self) -> str:
139142

140143
@property
141144
def cluster(self) -> str:
142-
return self.values.get("cluster", "")
145+
cluster = self.values.get("cluster", "")
146+
return str(cluster)
143147

144148
@property
145149
def tags(self) -> list[str]:
146-
return self.values.get("tags", [])
150+
tags = self.values.get("tags", [])
151+
return list(tags)
147152

148153
@property
149154
def service_account_name(self) -> str:
150-
return self.values.get("serviceAccount", {}).get("name") or self.values.get("serviceAccountName") or "default"
155+
service_account = self.values.get("serviceAccount", {})
156+
if isinstance(service_account, dict):
157+
name = service_account.get("name")
158+
if name:
159+
return str(name)
160+
service_account_name = self.values.get("serviceAccountName")
161+
if service_account_name:
162+
return str(service_account_name)
163+
return "default"
151164

152165
@property
153166
def secrets(self) -> dict[str, str]:
154167
# TODO: This should be a first class property
155-
return self.values.get("secrets", {})
168+
secrets = self.values.get("secrets", {})
169+
return dict(secrets)
156170

157171

158172
class Chart:
@@ -177,7 +191,7 @@ class Chart:
177191
chart: https://github.com/uptick/workforce
178192
"""
179193

180-
def __init__(self, definition: dict | str):
194+
def __init__(self, definition: dict[str, Any] | str):
181195
if isinstance(definition, str):
182196
# for backwards compat, any chart definition which is a string, is a git repo
183197
self.type = "git"

gitops/common/utils.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
import os
2+
from typing import Any
23

34
import yaml
45

56

6-
def load_yaml(path: str) -> dict:
7+
def load_yaml(path: str) -> dict[str, Any]:
78
with open(path) as file:
89
return resolve_values(yaml.safe_load(file), path)
910

1011

11-
def resolve_values(values: dict, path: str) -> dict:
12+
def resolve_values(values: dict[str, Any], path: str) -> dict[str, Any]:
1213
if "extends" not in values:
1314
return values
1415
parent_values = load_yaml(os.path.join(os.path.dirname(path), values["extends"]))
1516
return deep_merge(parent_values, values)
1617

1718

18-
def deep_merge(parent: dict, child: dict) -> dict:
19+
def deep_merge(parent: dict[str, Any], child: dict[str, Any]) -> dict[str, Any]:
1920
"""Deeply merge two dictionaries.
2021
2122
Dictionary entries will be followed and merged, anything else will be

gitops/core.py

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import asyncio
2+
import os
23
import uuid
4+
from typing import Any
35

46
from colorama import Fore
57
from invoke import run, task
@@ -16,7 +18,7 @@
1618

1719

1820
@task
19-
def summary(ctx, filter="", exclude=""):
21+
def summary(ctx: Any, filter: str = "", exclude: str = "") -> None:
2022
"""Produce a summary of apps, their tags, and their expected images & replicas.
2123
May not necessarily reflect actual app statuses if recent changes haven't yet been pushed to
2224
the remote, or the deployment has failed.
@@ -32,18 +34,18 @@ def summary(ctx, filter="", exclude=""):
3234

3335
@task
3436
def bump( # noqa: C901
35-
ctx,
36-
filter,
37-
exclude="",
38-
image_tag=None,
39-
prefix=None,
40-
autoexclude_inactive=True,
41-
interactive=True,
42-
push=False,
43-
redeploy=False,
44-
skip_migrations=False,
45-
skip_deploy=False,
46-
):
37+
ctx: Any,
38+
filter: str,
39+
exclude: str = "",
40+
image_tag: str | None = None,
41+
prefix: str | None = None,
42+
autoexclude_inactive: bool = True,
43+
interactive: bool = True,
44+
push: bool = False,
45+
redeploy: bool = False,
46+
skip_migrations: bool = False,
47+
skip_deploy: bool = False,
48+
) -> None:
4749
"""Bump image tag on selected app(s).
4850
Provide `image_tag` to set to a specific image tag, or provide `prefix` to use latest image
4951
with the given prefix.
@@ -134,16 +136,16 @@ def bump( # noqa: C901
134136

135137
@task
136138
def command(
137-
ctx,
138-
filter,
139-
command,
140-
exclude="",
141-
cleanup=True,
142-
sequential=True,
143-
interactive=True,
144-
cpu=0,
145-
memory=0,
146-
):
139+
ctx: Any,
140+
filter: str,
141+
command: str,
142+
exclude: str = "",
143+
cleanup: bool = True,
144+
sequential: bool = True,
145+
interactive: bool = True,
146+
cpu: int = 0,
147+
memory: int = 0,
148+
) -> None:
147149
"""Run command on selected app(s).
148150
149151
eg. inv command customer,sandbox -e aesg "python manage.py migrate"
@@ -183,7 +185,7 @@ def command(
183185

184186

185187
@task
186-
def tag(ctx, filter, tag, exclude=""):
188+
def tag(ctx: Any, filter: str, tag: str, exclude: str = "") -> None:
187189
"""Set a tag on selected app(s)."""
188190
try:
189191
apps = get_apps(
@@ -209,7 +211,7 @@ def tag(ctx, filter, tag, exclude=""):
209211

210212

211213
@task
212-
def untag(ctx, filter, tag, exclude=""):
214+
def untag(ctx: Any, filter: str, tag: str, exclude: str = "") -> None:
213215
"""Unset a tag from selected app(s)."""
214216
try:
215217
apps = get_apps(
@@ -235,32 +237,34 @@ def untag(ctx, filter, tag, exclude=""):
235237

236238

237239
@task # TODO: want `keys` to be optional-positional: https://github.com/pyinvoke/invoke/issues/159
238-
def getenv(ctx, filter, keys="", exclude=""):
240+
def getenv(ctx: Any, filter: str, keys: str = "", exclude: str = "") -> None:
239241
"""Get one or more env vars on selected app(s)."""
240242
_getenv("environment", filter, exclude, keys)
241243

242244

243245
@task # TODO: want `keys` to be optional-positional: https://github.com/pyinvoke/invoke/issues/159
244-
def getsecrets(ctx, filter, keys="", exclude=""):
246+
def getsecrets(ctx: Any, filter: str, keys: str = "", exclude: str = "") -> None:
245247
"""Get one or more secrets on selected app(s)."""
246248
_getenv("secrets", filter, exclude, keys)
247249

248250

249-
def _getenv(env_or_secrets, filter, exclude, filter_values):
250-
filter_values = filter_values.split(",") if filter_values else ""
251+
def _getenv(env_or_secrets: str, filter: str, exclude: str, filter_values: str) -> None:
252+
filter_values_list = filter_values.split(",") if filter_values else []
251253
apps = get_apps(filter=filter, exclude=exclude, mode="SILENT")
252254
for app in apps:
253255
print("-" * 20, progress(app.name), sep="\n")
254256
values = app.values.get(env_or_secrets)
255257
if isinstance(values, dict):
256-
filtered_values = {k: v for k, v in values.items() if k in filter_values} if filter_values else values
258+
filtered_values = (
259+
{k: v for k, v in values.items() if k in filter_values_list} if filter_values_list else values
260+
)
257261
for k, v in filtered_values.items():
258262
print(f"{k}={v}")
259263
else:
260264
print(warning(f"No {env_or_secrets} set."))
261265

262266

263-
def _sort_envs(envs):
267+
def _sort_envs(envs: dict[str, Any]) -> dict[str, Any]:
264268
sorted_envs = {}
265269
for e in config.getlist("env_order", fallback=""):
266270
if e in envs:
@@ -271,7 +275,7 @@ def _sort_envs(envs):
271275

272276

273277
@task
274-
def setenv(ctx, filter, values, exclude=""):
278+
def setenv(ctx: Any, filter: str, values: str, exclude: str = "") -> None:
275279
"""Set one or more env vars on selected app(s).
276280
277281
eg. inv setenv customer,sandbox BG_RUNNER=DRAMATIQ,BUMP=2
@@ -292,11 +296,16 @@ def setenv(ctx, filter, values, exclude=""):
292296
print(success_negative("Aborted."))
293297
return
294298
for app in apps:
299+
# Split on first = only, in case values contain =
300+
new_envs: dict[str, Any] = {}
301+
for e in splitenvs:
302+
key, value = e.split("=", 1)
303+
new_envs[key] = value
295304
update_app(
296305
app.name,
297306
environment=_sort_envs(
298307
{
299-
**dict(tuple(e.split("=")) for e in splitenvs),
308+
**new_envs,
300309
**app.values.get("environment", {}),
301310
}
302311
),
@@ -309,7 +318,7 @@ def setenv(ctx, filter, values, exclude=""):
309318

310319

311320
@task
312-
def unsetenv(ctx, filter, values, exclude=""):
321+
def unsetenv(ctx: Any, filter: str, values: str, exclude: str = "") -> None:
313322
"""Unset one or more env vars on selected app(s).
314323
315324
eg. inv unsetenv customer,sandbox BG_RUNNER,BUMP
@@ -343,7 +352,7 @@ def unsetenv(ctx, filter, values, exclude=""):
343352

344353

345354
@task
346-
def setcluster(ctx, filter, cluster, exclude=""):
355+
def setcluster(ctx: Any, filter: str, cluster: str, exclude: str = "") -> None:
347356
"""Move selected app(s) to given cluster.
348357
349358
eg. inv setcluster customer,sandbox eks-prod
@@ -370,7 +379,7 @@ def setcluster(ctx, filter, cluster, exclude=""):
370379
print(success("Done!"))
371380

372381

373-
def git_push(cluster_path: str, retry: int = 3):
382+
def git_push(cluster_path: str | os.PathLike[str], retry: int = 3) -> None:
374383
"""Git pushes in a directory and retries if commits already exist"""
375384
print(progress(f"Pushing changes to {cluster_path}"))
376385
attempts = 0

0 commit comments

Comments
 (0)