Skip to content

Commit

Permalink
feat(doctor): use sys.version for fuller output (vs version_info tuple)
Browse files Browse the repository at this point in the history
feat(doctor): use platform.platform() for more complete OS details

refactor(doctor,sandbox): move common docker compose version functionality out + other minor refactorings
  • Loading branch information
achidlow committed Dec 16, 2022
1 parent 8013159 commit e9037e0
Show file tree
Hide file tree
Showing 20 changed files with 219 additions and 266 deletions.
34 changes: 16 additions & 18 deletions src/algokit/cli/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import click
import pyclip # type: ignore
from algokit.core import doctor as doctor_functions
from algokit.core.doctor import ProcessResult

logger = logging.getLogger(__name__)
DOCTOR_END_MESSAGE = (
Expand All @@ -29,35 +28,34 @@
default=False,
)
def doctor_command(*, copy_to_clipboard: bool) -> None:
return_code = 0
os_type = platform.system().lower()
service_outputs: dict[str, ProcessResult] = {}
service_outputs = {
"Time": doctor_functions.get_date(),
"AlgoKit": doctor_functions.get_algokit_info(),
"OS": doctor_functions.get_os(),
"Docker": doctor_functions.get_docker_info(),
"Docker Compose": doctor_functions.get_docker_compose_info(),
"Git": doctor_functions.get_git_info(os_type),
"AlgoKit Python": doctor_functions.get_algokit_python_info(),
"Global Python": doctor_functions.get_global_python_info("python"),
"Global Python3": doctor_functions.get_global_python_info("python3"),
"Pipx": doctor_functions.get_pipx_info(),
"Poetry": doctor_functions.get_poetry_info(),
"Node.js": doctor_functions.get_node_info(),
"Npm": doctor_functions.get_npm_info(os_type),
}

service_outputs["Time"] = doctor_functions.get_date()
service_outputs["AlgoKit"] = doctor_functions.get_algokit_info()
if os_type == "windows":
service_outputs["Chocolatey"] = doctor_functions.get_choco_info()
if os_type == "darwin":
service_outputs["Brew"] = doctor_functions.get_brew_info()
service_outputs["OS"] = doctor_functions.get_os(os_type)
service_outputs["Docker"] = doctor_functions.get_docker_info()
service_outputs["Docker Compose"] = doctor_functions.get_docker_compose_info()
service_outputs["Git"] = doctor_functions.get_git_info(os_type)
service_outputs["AlgoKit Python"] = doctor_functions.get_algokit_python_info()
service_outputs["Global Python"] = doctor_functions.get_global_python_info("python")
service_outputs["Global Python3"] = doctor_functions.get_global_python_info("python3")
service_outputs["Pipx"] = doctor_functions.get_pipx_info()
service_outputs["Poetry"] = doctor_functions.get_poetry_info()
service_outputs["Node.js"] = doctor_functions.get_node_info()
service_outputs["Npm"] = doctor_functions.get_npm_info(os_type)

critical_services = ["Docker", "Docker Compose", "Git"]
# Print the status details
for key, value in service_outputs.items():
color = "green"
if value.exit_code != 0:
color = "red" if key in critical_services else "yellow"
return_code = 1
logger.info(click.style(f"{key}: ", bold=True) + click.style(f"{value.info}", fg=color))

# print end message anyway
Expand All @@ -66,5 +64,5 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:
if copy_to_clipboard:
pyclip.copy("\n".join(f"* {key}: {value.info}" for key, value in service_outputs.items()))

if return_code != 0:
if any(value.exit_code != 0 for value in service_outputs.values()):
raise click.exceptions.Exit(code=1)
38 changes: 22 additions & 16 deletions src/algokit/cli/sandbox.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import json
import logging
from subprocess import CalledProcessError

import click
from algokit.cli.goal import goal_command
from algokit.core import proc
from algokit.core.sandbox import ComposeFileStatus, ComposeSandbox, fetch_algod_status_data, fetch_indexer_status_data
from algokit.core.sandbox import (
DOCKER_COMPOSE_MINIMUM_VERSION,
ComposeFileStatus,
ComposeSandbox,
fetch_algod_status_data,
fetch_indexer_status_data,
get_docker_compose_version_string,
)
from algokit.core.utils import is_minimum_version

logger = logging.getLogger(__name__)


@click.group("sandbox", short_help="Manage the AlgoKit sandbox.")
def sandbox_group() -> None:
try:
compose_version_result = proc.run(
["docker", "compose", "version", "--format", "json"],
bad_return_code_error_message=(
"Docker Compose not found; please install Docker Compose and add to path.\n"
"See https://docs.docker.com/compose/install/ for more information."
),
)
compose_version_str = get_docker_compose_version_string() or ""
except CalledProcessError as ex:
raise click.ClickException(
"Docker Compose not found; please install Docker Compose and add to path.\n"
"See https://docs.docker.com/compose/install/ for more information."
) from ex
except IOError as ex:
# an IOError (such as PermissionError or FileNotFoundError) will only occur if "docker"
# isn't an executable in the user's path, which means docker isn't installed
Expand All @@ -28,20 +35,19 @@ def sandbox_group() -> None:
) from ex
else:
try:
compose_version: dict[str, str] = json.loads(compose_version_result.output)
compose_version_str = compose_version["version"]
compose_major, compose_minor, *_ = map(int, compose_version_str.lstrip("v").split("."))
compose_version_ok = is_minimum_version(compose_version_str, DOCKER_COMPOSE_MINIMUM_VERSION)
except Exception:
logger.warning(
"Unable to extract docker compose version from output: \n"
+ compose_version_result.output.strip()
+ "\nPlease ensure a minimum of compose v2.5.0 is used",
+ compose_version_str
+ f"\nPlease ensure a minimum of compose v{DOCKER_COMPOSE_MINIMUM_VERSION} is used",
exc_info=True,
)
else:
if (compose_major, compose_minor) < (2, 5):
if not compose_version_ok:
raise click.ClickException(
f"Minimum docker compose version supported: v2.5.0, installed = {compose_version_str}\n"
f"Minimum docker compose version supported: v{DOCKER_COMPOSE_MINIMUM_VERSION}, "
f"installed = v{compose_version_str}\n"
"Please update your Docker install"
)

Expand Down
54 changes: 12 additions & 42 deletions src/algokit/core/doctor.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import dataclasses
import logging
import platform
import shutil
from datetime import datetime, timezone
from platform import platform as get_platform
from shutil import which
from sys import executable as sys_executable
from sys import version_info as sys_version_info
from sys import version as sys_version

from algokit.core import proc
from algokit.core.sandbox import DOCKER_COMPOSE_MINIMUM_VERSION, get_docker_compose_version_string
from algokit.core.utils import get_version_from_str, is_minimum_version

logger = logging.getLogger(__name__)

DOCKER_COMPOSE_MINIMUM_VERSION = "2.5"

DOCKER_COMPOSE_MINIMUM_VERSION_MESSAGE = (
f"\nDocker Compose {DOCKER_COMPOSE_MINIMUM_VERSION} required to `run algokit sandbox command`; "
Expand All @@ -25,7 +26,7 @@ class ProcessResult:


def get_date() -> ProcessResult:
return ProcessResult(format(datetime.now(timezone.utc).isoformat()), 0)
return ProcessResult(str(datetime.now(timezone.utc).isoformat()), 0)


def get_algokit_info() -> ProcessResult:
Expand Down Expand Up @@ -69,19 +70,8 @@ def get_brew_info() -> ProcessResult:
return ProcessResult("None found", 1)


def get_os(os_type: str) -> ProcessResult:
os_version = ""
os_name = ""
if os_type == "windows":
os_name = "Windows"
os_version = platform.win32_ver()[0]
elif os_type == "darwin":
os_name = "Mac OS X"
os_version = platform.mac_ver()[0]
else:
os_name = "Unix/Linux"
os_version = platform.version()
return ProcessResult(f"{os_name} {os_version}", 0)
def get_os() -> ProcessResult:
return ProcessResult(get_platform(), 0)


def get_docker_info() -> ProcessResult:
Expand All @@ -102,16 +92,15 @@ def get_docker_info() -> ProcessResult:

def get_docker_compose_info() -> ProcessResult:
try:
process_results = proc.run(["docker-compose", "-v"])
docker_compose_version = process_results.output.splitlines()[0].split(" v")[2]
docker_compose_version = get_docker_compose_version_string() or ""
minimum_version_met = is_minimum_version(docker_compose_version, DOCKER_COMPOSE_MINIMUM_VERSION)
return ProcessResult(
(
docker_compose_version
if minimum_version_met
else f"{docker_compose_version}{DOCKER_COMPOSE_MINIMUM_VERSION_MESSAGE}"
),
process_results.exit_code if minimum_version_met else 1,
0 if minimum_version_met else 1,
)
except Exception as e:
logger.debug(f"Getting docker compose version failed: {e}", exc_info=True)
Expand Down Expand Up @@ -142,22 +131,15 @@ def get_git_info(system: str) -> ProcessResult:


def get_algokit_python_info() -> ProcessResult:
try:
return ProcessResult(
f"{sys_version_info.major}.{sys_version_info.minor}.{sys_version_info.micro} (location: {sys_executable})",
0,
)
except Exception as e:
logger.debug(f"Getting AlgoKit python version failed: {e}", exc_info=True)
return ProcessResult("None found.", 1)
return ProcessResult(f"{sys_version} (location: {sys_executable})", 0)


def get_global_python_info(python_command_name: str) -> ProcessResult:
try:
major, minor, build = get_version_from_str(
proc.run([python_command_name, "--version"]).output.splitlines()[0].split(" ")[1]
)
global_python3_location = shutil.which(python_command_name)
global_python3_location = which(python_command_name)
return ProcessResult(f"{major}.{minor}.{build} (location: {global_python3_location})", 0)
except Exception as e:
logger.debug(f"Getting python version failed: {e}", exc_info=True)
Expand Down Expand Up @@ -215,15 +197,3 @@ def get_npm_info(system: str) -> ProcessResult:
except Exception as e:
logger.debug(f"Getting npm version failed: {e}", exc_info=True)
return ProcessResult("None found.", 1)


def is_minimum_version(system_version: str, minimum_version: str) -> bool:
system_version_as_tuple = tuple(map(int, (system_version.split("."))))
minimum_version_as_tuple = tuple(map(int, (minimum_version.split("."))))
return system_version_as_tuple >= minimum_version_as_tuple


def get_version_from_str(version: str) -> tuple[int, int, int]:
# take only the first three parts x.y.z of the version to ignore weird version
major, minor, build = map(int, version.split(".")[:3])
return major, minor, build
26 changes: 26 additions & 0 deletions src/algokit/core/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@
import json
import logging
from pathlib import Path
from subprocess import CalledProcessError
from typing import Any, cast

import httpx
from algokit.core import proc
from algokit.core.conf import get_app_config_dir
from algokit.core.proc import RunResult, run

logger = logging.getLogger(__name__)


DOCKER_COMPOSE_MINIMUM_VERSION = "2.5.0"


class ComposeFileStatus(enum.Enum):
MISSING = enum.auto()
UP_TO_DATE = enum.auto()
Expand Down Expand Up @@ -203,3 +208,24 @@ def fetch_indexer_status_data(service_info: dict[str, Any]) -> dict[str, Any]:
except Exception as err:
logger.debug(f"Error checking indexer status: {err}", exc_info=True)
return {"Status": "Error"}


def get_docker_compose_version_string() -> str | None:
# 1. IOError - docker not installed or not on path
# -- handle: don't
# 2. exit code non-zero ... for whatever reason
# -- handle: raise CalledProcessError
# 3. failing to parse output of version string
cmd = ["docker", "compose", "version", "--format", "json"]
compose_version_result = proc.run(cmd)
if compose_version_result.exit_code != 0:
raise CalledProcessError(
returncode=compose_version_result.exit_code, cmd=cmd, output=compose_version_result.output
)
compose_version: dict[str, str] = json.loads(compose_version_result.output)
try:
compose_version_str = compose_version["version"]
except KeyError:
return None
else:
return compose_version_str.lstrip("v")
10 changes: 10 additions & 0 deletions src/algokit/core/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def is_minimum_version(system_version: str, minimum_version: str) -> bool:
system_version_as_tuple = tuple(map(int, system_version.split(".")))
minimum_version_as_tuple = tuple(map(int, minimum_version.split(".")))
return system_version_as_tuple >= minimum_version_as_tuple


def get_version_from_str(version: str) -> tuple[int, int, int]:
# take only the first three parts x.y.z of the version to ignore weird version
major, minor, build = map(int, version.split(".")[:3])
return major, minor, build
Loading

0 comments on commit e9037e0

Please sign in to comment.