Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance doctor Python version & OS version info + refactoring #101

Merged
merged 1 commit into from
Dec 16, 2022
Merged
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
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
60 changes: 15 additions & 45 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 All @@ -46,7 +47,7 @@ def get_algokit_info() -> ProcessResult:
return ProcessResult(f"{algokit_version} {algokit_location}", 0)
except Exception as e:
logger.debug(f"Getting algokit version failed: {e}", exc_info=True)
return ProcessResult("None found", 1)
return ProcessResult("None found.", 1)


def get_choco_info() -> ProcessResult:
Expand All @@ -56,7 +57,7 @@ def get_choco_info() -> ProcessResult:
return ProcessResult(f"{major}.{minor}.{build}", 0)
except Exception as e:
logger.debug(f"Getting chocolatey version failed: {e}", exc_info=True)
return ProcessResult("None found", 1)
return ProcessResult("None found.", 1)


def get_brew_info() -> ProcessResult:
Expand All @@ -66,22 +67,11 @@ def get_brew_info() -> ProcessResult:
return ProcessResult(f"{major}.{minor}.{build}", 0)
except Exception as e:
logger.debug(f"Getting brew version failed: {e}", exc_info=True)
return ProcessResult("None found", 1)
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