Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repos:
rev: 'v1.18.2'
hooks:
- id: mypy
args: [--strict, --ignore-missing-imports, --check-untyped-defs]
args: [--strict, --ignore-missing-imports, --check-untyped-defs, --allow-untyped-decorators]
additional_dependencies:
- types-click
- types-PyYAML
Expand Down
636 changes: 634 additions & 2 deletions poetry.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pytest = "^9.0.2"
pytest-cov = "^7.0.0"
isort = "^7.0.0"
requests-mock = "^1.12.1"
mypy = "1.18.2"
types-click = "^7.1.8"
types-pyyaml = "^6.0.12.20250915"
types-requests = "^2.32.4.20260107"
boto3-stubs = { extras = ["s3"], version = "^1.42.25" }

[tool.poetry.group.docs.dependencies]
sphinx-rtd-theme = "^3.0.2"
Expand Down
69 changes: 37 additions & 32 deletions src/gardenlinux/distro_version/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from typing import Self


class UnsupportedDistroVersion(Exception):
pass

Expand All @@ -6,63 +9,65 @@ class NotAPatchRelease(Exception):
pass


def DistroVersion(maybe_distro_version):
version_components = maybe_distro_version.split(".")
if len(version_components) > 3 or len(version_components) < 2:
raise UnsupportedDistroVersion(
f"Unexpected version number format {maybe_distro_version}"
)

if not all(map(lambda x: x.isdigit(), version_components)):
raise UnsupportedDistroVersion(
f"Unexpected version number format {maybe_distro_version}"
)

if len(version_components) == 2:
return LegacyDistroVersion(*(int(c) for c in version_components))
elif len(version_components) == 3:
return SemverDistroVersion(*(int(c) for c in version_components))
else:
raise UnsupportedDistroVersion(
f"Unexpected number of version components: {maybe_distro_version}"
)


class BaseDistroVersion:
major = None
minor = None
patch = None
major: int = 0
minor: int = 0
patch: int = 0

def is_patch_release(self):
def is_patch_release(self) -> int:
return self.patch and self.patch > 0


class LegacyDistroVersion(BaseDistroVersion):
def __init__(self, major, patch):
def __init__(self: Self, major: int, patch: int) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK self can be left without typing hint as the type checkers are "smart" enough to know what self mean for Python classes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought so too, but mypy seemed to disagree

self.major = major
self.patch = patch

def __str__(self):
def __str__(self) -> str:
return f"{self.major}.{self.patch}"

def previous_patch_release(self):
def previous_patch_release(self) -> "LegacyDistroVersion":
if not self.is_patch_release():
raise NotAPatchRelease(f"{self} is not a patch release")

return LegacyDistroVersion(self.major, self.patch - 1)


class SemverDistroVersion(BaseDistroVersion):
def __init__(self, major, minor, patch):
def __init__(self, major: int, minor: int, patch: int) -> None:
self.major = major
self.minor = minor
self.patch = patch

def __str__(self):
def __str__(self) -> str:
return f"{self.major}.{self.minor}.{self.patch}"

def previous_patch_release(self):
def previous_patch_release(self) -> "SemverDistroVersion":
if not self.is_patch_release():
raise NotAPatchRelease(f"{self} is not a patch release")

return SemverDistroVersion(self.major, self.minor, self.patch - 1)


def DistroVersion(
maybe_distro_version: str,
) -> LegacyDistroVersion | SemverDistroVersion:
version_components = maybe_distro_version.split(".")
if len(version_components) > 3 or len(version_components) < 2:
raise UnsupportedDistroVersion(
f"Unexpected version number format {maybe_distro_version}"
)

if not all(map(lambda x: x.isdigit(), version_components)):
raise UnsupportedDistroVersion(
f"Unexpected version number format {maybe_distro_version}"
)

if len(version_components) == 2:
return LegacyDistroVersion(*(int(c) for c in version_components))
elif len(version_components) == 3:
return SemverDistroVersion(*(int(c) for c in version_components))
else:
raise UnsupportedDistroVersion(
f"Unexpected number of version components: {maybe_distro_version}"
)
6 changes: 3 additions & 3 deletions src/gardenlinux/git/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ def root(self) -> Path:
:since: 0.10.0
"""

root_dir = self.workdir
root_dir: Path = Path(self.workdir)

if self.is_bare:
root_dir = self.path
root_dir = Path(self.path)

usual_git_dir = Path(root_dir, ".git")
usual_git_dir = root_dir / ".git"

# Possible submodule Git repository. Validate repository containing `.git` directory.
if self.path != str(usual_git_dir):
Expand Down
23 changes: 16 additions & 7 deletions src/gardenlinux/github/release/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import json
import logging
import os
import sys

Expand All @@ -7,10 +8,12 @@
from gardenlinux.constants import RELEASE_ID_FILE, REQUESTS_TIMEOUTS
from gardenlinux.logger import LoggerSetup

LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", "INFO")
LOGGER = LoggerSetup.get_logger("gardenlinux.github.release", logging.INFO)


def create_github_release(owner, repo, tag, commitish, latest, body):
def create_github_release(
owner: str, repo: str, tag: str, commitish: str, latest: bool, body: str
) -> int | None:
token = os.environ.get("GITHUB_TOKEN")
if not token:
raise ValueError("GITHUB_TOKEN environment variable not set")
Expand Down Expand Up @@ -40,26 +43,32 @@ def create_github_release(owner, repo, tag, commitish, latest, body):
if response.status_code == 201:
LOGGER.info("Release created successfully")
response_json = response.json()
return response_json.get("id")
return int(response_json.get("id")) # Will raise KeyError if missing
else:
LOGGER.error("Failed to create release")
LOGGER.debug(response.json())
response.raise_for_status()

return None # Simply to make mypy happy. should not be reached.

def write_to_release_id_file(release_id):

def write_to_release_id_file(release_id: str | int) -> None:
try:
with open(RELEASE_ID_FILE, "w") as file:
file.write(release_id)
file.write(str(release_id))
LOGGER.info(f"Created {RELEASE_ID_FILE} successfully.")
except IOError as e:
LOGGER.error(f"Could not create {RELEASE_ID_FILE} file: {e}")
sys.exit(1)


def upload_to_github_release_page(
github_owner, github_repo, gardenlinux_release_id, file_to_upload, dry_run
):
github_owner: str,
github_repo: str,
gardenlinux_release_id: str | int,
file_to_upload: str,
dry_run: bool,
) -> None:
if dry_run:
LOGGER.info(
f"Dry run: would upload {file_to_upload} to release {gardenlinux_release_id} in repo {github_owner}/{github_repo}"
Expand Down
5 changes: 3 additions & 2 deletions src/gardenlinux/github/release/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import logging

from gardenlinux.constants import GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME
from gardenlinux.logger import LoggerSetup
Expand All @@ -10,10 +11,10 @@
write_to_release_id_file,
)

LOGGER = LoggerSetup.get_logger("gardenlinux.github", "INFO")
LOGGER = LoggerSetup.get_logger("gardenlinux.github", logging.INFO)


def main():
def main() -> None:
parser = argparse.ArgumentParser(description="GitHub Release Script")
subparsers = parser.add_subparsers(dest="command")

Expand Down
4 changes: 2 additions & 2 deletions src/gardenlinux/github/release_notes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@


def create_github_release_notes(
gardenlinux_version, commitish, releases_s3_bucket_name
):
gardenlinux_version: str, commitish: str, releases_s3_bucket_name: str
) -> str:
package_list = get_package_list(gardenlinux_version)

output = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
from typing import Any, Dict

from gardenlinux.constants import GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME


class DeploymentPlatform:
artifacts_bucket_name = GARDENLINUX_GITHUB_RELEASE_BUCKET_NAME

def short_name(self):
def short_name(self) -> str:
return "generic"

def full_name(self):
def full_name(self) -> str:
return "Generic Deployment Platform"

def published_images_by_regions(self, image_metadata):
def published_images_by_regions(
self, image_metadata: Dict[str, Any]
) -> Dict[str, Any]:
published_image_metadata = image_metadata["published_image_metadata"]
flavor_name = image_metadata["s3_key"].split("/")[-1]

Expand All @@ -21,10 +25,10 @@ def published_images_by_regions(self, image_metadata):

return {"flavor": flavor_name, "regions": regions}

def image_extension(self):
def image_extension(self) -> str:
return "raw"

def artifact_for_flavor(self, flavor, markdown_format=True):
def artifact_for_flavor(self, flavor: str, markdown_format: bool = True) -> str:
base_url = (
f"https://{self.__class__.artifacts_bucket_name}.s3.amazonaws.com/objects"
)
Expand All @@ -35,11 +39,11 @@ def artifact_for_flavor(self, flavor, markdown_format=True):
else:
return download_url

def region_details(self, image_metadata):
def region_details(self, image_metadata: Dict[str, Any]) -> str:
"""
Generate the detailed region information for the collapsible section
"""
details = ""
details: str = ""

match self.published_images_by_regions(image_metadata):
case {"regions": regions}:
Expand Down Expand Up @@ -81,7 +85,7 @@ def region_details(self, image_metadata):

return details

def summary_text(self, image_metadata):
def summary_text(self, image_metadata: Dict[str, Any]) -> str:
"""
Generate the summary text for the collapsible section
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@


class AliCloud(DeploymentPlatform):
def short_name(self):
def short_name(self) -> str:
return "ali"

def full_name(self):
def full_name(self) -> str:
return "Alibaba Cloud"

def image_extension(self):
def image_extension(self) -> str:
return "qcow2"
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# pyright: reportIncompatibleMethodOverride=false
from typing import Any, Dict

from . import DeploymentPlatform


class AmazonWebServices(DeploymentPlatform):
def short_name(self):
def short_name(self) -> str:
return "aws"

def full_name(self):
def full_name(self) -> str:
return "Amazon Web Services"

def published_images_by_regions(self, image_metadata):
def published_images_by_regions(
self, image_metadata: Dict[str, Any]
) -> Dict[str, Any]:
published_image_metadata = image_metadata["published_image_metadata"]
flavor_name = image_metadata["s3_key"].split("/")[-1]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
# pyright: reportIncompatibleMethodOverride=false
from typing import Any, Dict

from . import DeploymentPlatform


class Azure(DeploymentPlatform):
def short_name(self):
def short_name(self) -> str:
return "azure"

def full_name(self):
def full_name(self) -> str:
return "Microsoft Azure"

def image_extension(self):
def image_extension(self) -> str:
return "vhd"

def published_images_by_regions(self, image_metadata):
def published_images_by_regions(
self, image_metadata: Dict[str, Any]
) -> Dict[str, Any]:
published_image_metadata = image_metadata["published_image_metadata"]
flavor_name = image_metadata["s3_key"].split("/")[-1]

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
# pyright: reportIncompatibleMethodOverride=false
from typing import Any, Dict

from . import DeploymentPlatform


class GoogleCloud(DeploymentPlatform):
def short_name(self):
def short_name(self) -> str:
return "gcp"

def full_name(self):
def full_name(self) -> str:
return "Google Cloud Platform"

def image_extension(self):
def image_extension(self) -> str:
return "gcpimage.tar.gz"

def published_images_by_regions(self, image_metadata):
def published_images_by_regions(
self, image_metadata: Dict[str, Any]
) -> Dict[str, Any]:
published_image_metadata = image_metadata["published_image_metadata"]
flavor_name = image_metadata["s3_key"].split("/")[-1]

Expand Down
Loading