Skip to content

Commit

Permalink
Merge pull request #90 from algorandfoundation/initial-algokit-doctor
Browse files Browse the repository at this point in the history
feat(doctor): check system and services
  • Loading branch information
robdmoore authored Dec 15, 2022
2 parents 3e7f5e4 + d9fe303 commit b74f594
Show file tree
Hide file tree
Showing 17 changed files with 802 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ Here's how to test it out and maybe even start hacking, assuming you have access
4. Run `pipx install ./dist/algokit-<TAB>-<TAB>` (ie the .whl file)
5. You can now run `algokit` and should see a help message! 🎉

> Recommended: Run `algokit doctor` to check the system is ready to enjoy development on Algorand!
### Update

To update a previous algokit installation you can simply run `pipx reinstall algokit` and it'll grab the latest from wherever it was installed from. Note: If you installed a specific version e.g. `pipx install git+https://github.com/algorandfoundation/[email protected]` then this command won't have any effect since that repository tag will point to the same version.
36 changes: 35 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ click = "^8.1.3"
httpx = "^0.23.1"
copier = "^7.0.1"
questionary = "^1.10.0"
pyclip = "^0.7.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
Expand Down
2 changes: 2 additions & 0 deletions src/algokit/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import click
from algokit.cli.bootstrap import bootstrap_group
from algokit.cli.doctor import doctor_command
from algokit.cli.goal import goal_command
from algokit.cli.init import init_command
from algokit.cli.sandbox import sandbox_group
Expand All @@ -22,3 +23,4 @@ def algokit() -> None:
algokit.add_command(sandbox_group)
algokit.add_command(goal_command)
algokit.add_command(bootstrap_group)
algokit.add_command(doctor_command)
69 changes: 69 additions & 0 deletions src/algokit/cli/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import logging
import platform

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 = (
"If you are experiencing a problem with algokit, feel free to submit an issue "
"via https://github.com/algorandfoundation/algokit-cli/issues/new; please include this output, "
"if you want to populate this message in your clipboard, run `algokit doctor -c`"
)


@click.command(
"doctor",
short_help="Run the Algorand doctor CLI.",
context_settings={
"ignore_unknown_options": True,
},
)
@click.option(
"--copy-to-clipboard",
"-c",
help="Copy the contents of the doctor message (in Markdown format) in your clipboard.",
is_flag=True,
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()
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()
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()

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
logger.info(DOCTOR_END_MESSAGE)

if copy_to_clipboard:
pyclip.copy("\n".join(f"{key}: {value.info}" for key, value in service_outputs.items()))

if return_code != 0:
raise click.exceptions.Exit(code=1)
201 changes: 201 additions & 0 deletions src/algokit/core/doctor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import dataclasses
import logging
import platform
import shutil
from datetime import datetime, timezone
from sys import version_info as sys_version_info

from algokit.core import proc

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`; "
"install via https://docs.docker.com/compose/install/"
)


@dataclasses.dataclass
class ProcessResult:
info: str
exit_code: int


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


def get_algokit_info() -> ProcessResult:
try:
pipx_list_process_results = proc.run(["pipx", "list", "--short"])
algokit_pipx_line = [
line for line in pipx_list_process_results.output.splitlines() if line.startswith("algokit")
]
algokit_version = algokit_pipx_line[0].split(" ")[1]

algokit_location = ""
pipx_env_process_results = proc.run(["pipx", "environment"])
algokit_pip_line = [
line for line in pipx_env_process_results.output.splitlines() if line.startswith("PIPX_LOCAL_VENVS")
]
pipx_venv_location = algokit_pip_line[0].split("=")[1]
algokit_location = f"{pipx_venv_location}/algokit"
return ProcessResult(f"{algokit_version} {algokit_location}", 0)
except Exception:
return ProcessResult("None found", 1)


def get_choco_info() -> ProcessResult:
try:
process_results = proc.run(["choco"])
return ProcessResult(process_results.output.splitlines()[0].split(" v")[1], process_results.exit_code)
except Exception:
return ProcessResult("None found", 1)


def get_brew_info() -> ProcessResult:
try:
process_results = proc.run(["brew", "-v"])
return ProcessResult(process_results.output.splitlines()[0].split(" ")[1], process_results.exit_code)
except Exception:
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_docker_info() -> ProcessResult:
try:
process_results = proc.run(["docker", "-v"])
return ProcessResult(
process_results.output.splitlines()[0].split(" ")[2].split(",")[0], process_results.exit_code
)
except Exception:
return ProcessResult(
(
"None found.\nDocker required to `run algokit sandbox` command;"
" install via https://docs.docker.com/get-docker/"
),
1,
)


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]
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,
)
except Exception:
return ProcessResult(f"None found. {DOCKER_COMPOSE_MINIMUM_VERSION_MESSAGE}", 1)


def get_git_info(system: str) -> ProcessResult:
try:
process_results = proc.run(["git", "-v"])
return ProcessResult(process_results.output.splitlines()[0].split(" ")[2], process_results.exit_code)
except Exception:
if system == "windows":
return ProcessResult(
(
"None found.\nGit required to `run algokit init`; install via `choco install git` "
"if using Chocolatey or via https://github.com/git-guides/install-git#install-git-on-windows"
),
1,
)
else:
return ProcessResult(
"None found.\nGit required to run `algokit init`; "
"install via https://github.com/git-guides/install-git",
1,
)


def get_algokit_python_info() -> ProcessResult:
try:
return ProcessResult(f"{sys_version_info.major}.{sys_version_info.minor}.{sys_version_info.micro}", 0)
except Exception:
return ProcessResult("None found.", 1)


def get_global_python_info() -> ProcessResult:
try:
global_python3_version = proc.run(["python3", "--version"]).output.splitlines()[0].split(" ")[1]
global_python3_location = shutil.which("python3")
return ProcessResult(f"{global_python3_version} {global_python3_location}", 0)
except Exception:
return ProcessResult("None found.", 1)


def get_pipx_info() -> ProcessResult:
try:
process_results = proc.run(["pipx", "--version"])
return ProcessResult(process_results.output.splitlines()[0], process_results.exit_code)
except Exception:
return ProcessResult(
"None found.\nPipx is required to install Poetry; install via https://pypa.github.io/pipx/", 1
)


def get_poetry_info() -> ProcessResult:
try:
process_results = proc.run(["poetry", "--version"])
poetry_version = process_results.output.splitlines()[-1].split("version ")[1].split(")")[0]
return ProcessResult(poetry_version, process_results.exit_code)
except Exception:
return ProcessResult(
(
"None found.\nPoetry is required for some Python-based templates; install via `algokit bootstrap` "
"within project directory, or via https://python-poetry.org/docs/#installation"
),
1,
)


def get_node_info() -> ProcessResult:
try:
process_results = proc.run(["node", "-v"])
return ProcessResult(process_results.output.splitlines()[0].split("v")[1], process_results.exit_code)
except Exception:
return ProcessResult(
(
"None found.\nNode.js is required for some Node.js-based templates; install via `algokit bootstrap` "
"within project directory, or via https://nodejs.dev/en/learn/how-to-install-nodejs/"
),
1,
)


def get_npm_info() -> ProcessResult:
try:
process_results = proc.run(["npm", "-v"])
return ProcessResult(process_results.output.splitlines()[0], process_results.exit_code)
except Exception:
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
Loading

1 comment on commit b74f594

@github-actions
Copy link

Choose a reason for hiding this comment

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

Coverage

Coverage Report
FileStmtsMissCoverMissing
src/algokit
   __init__.py15753%6–13, 17–24, 32–34
   __main__.py220%1–3
src/algokit/cli
   bootstrap.py291934%12, 23–62
   init.py1531491%54, 208, 211–213, 224, 262, 299, 308–310, 313–318, 333
src/algokit/core
   click_extensions.py472057%40–43, 50, 56, 67–68, 73–74, 79–80, 91, 104–114
   conf.py27967%10–17, 24, 26
   doctor.py116497%54–55, 138–139
   log_handlers.py68987%44–45, 50–51, 63, 112–116, 125
   proc.py44198%94
   sandbox.py106793%82, 147, 163, 178–180, 195
TOTAL7799288% 

Tests Skipped Failures Errors Time
74 0 💤 0 ❌ 0 🔥 7.388s ⏱️

Please sign in to comment.