Skip to content

Commit

Permalink
refactor(version-check): remove version parsing, sorry time travelers 🖖
Browse files Browse the repository at this point in the history
lint: update to latest ruff version 🐩

lint: exclude dist/ folder from mypy checks (if present)
  • Loading branch information
achidlow committed Dec 22, 2022
1 parent 080c544 commit fcc205e
Show file tree
Hide file tree
Showing 37 changed files with 117 additions and 131 deletions.
36 changes: 18 additions & 18 deletions poetry.lock

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

7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ shellingham = "^1.5.0"
[tool.poetry.group.dev.dependencies]
pytest = "^7.2.0"
black = {extras = ["d"], version = "^22.10.0"}
ruff = "^0.0.150"
ruff = "^0.0.191"
pip-audit = "^2.4.7"
approvaltests = "^7.2.0"
pytest-mock = "^3.10.0"
Expand All @@ -37,7 +37,7 @@ algokit = "algokit.cli:algokit"

[tool.ruff]
line-length = 120
select = ["E", "F", "ANN", "U", "N", "C", "B", "A", "YTT", "M", "W", "FBT", "Q", "RUF", "I"]
select = ["E", "F", "ANN", "UP", "N", "C", "B", "A", "YTT", "W", "FBT", "Q", "RUF", "I"]
# note: remove A003 once flake8-builtins: builtins-ignorelist supported and can ignore id
ignore = ["ANN101", "C901", "A003"]
# Exclude a variety of commonly ignored directories.
Expand Down Expand Up @@ -79,7 +79,8 @@ line-length = 120
pythonpath = ["src", "tests"]

[tool.mypy]
files = "src/"
files = ["src"]
exclude = ["dist", "tests"]
python_version = "3.10"
warn_unused_ignores = true
warn_redundant_casts = true
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import click

from algokit.cli.bootstrap import bootstrap_group
from algokit.cli.completions import completions_group
from algokit.cli.config import config_group
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from pathlib import Path

import click

from algokit.core.bootstrap import bootstrap_any_including_subdirs, bootstrap_env, bootstrap_poetry
from algokit.core.questionary_extensions import _get_confirm_default_yes_prompt

Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import click
import click.shell_completion
import shellingham # type: ignore

from algokit.core.atomic_write import atomic_write
from algokit.core.conf import get_app_config_dir

Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import click

from algokit.core.version_prompt import version_prompt_configuration_command


Expand Down
29 changes: 19 additions & 10 deletions src/algokit/cli/doctor.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import datetime as dt
import importlib.metadata
import logging
import platform
import sys

import click
import pyclip # type: ignore
from algokit.core.conf import PACKAGE_NAME

from algokit.core.conf import get_current_package_version
from algokit.core.doctor import DoctorResult, check_dependency
from algokit.core.sandbox import (
DOCKER_COMPOSE_MINIMUM_VERSION,
DOCKER_COMPOSE_VERSION_COMMAND,
parse_docker_compose_version_output,
)
from algokit.core.version_prompt import format_version, get_latest_version
from algokit.core.version_prompt import get_latest_github_version

logger = logging.getLogger(__name__)

Expand All @@ -38,14 +38,9 @@
def doctor_command(*, copy_to_clipboard: bool) -> None:
os_type = platform.system()
is_windows = os_type == "Windows"
current_algokit_version = importlib.metadata.version(PACKAGE_NAME)
latest_algokit_version = format_version(get_latest_version())
if current_algokit_version != latest_algokit_version:
current_algokit_version = click.style(current_algokit_version, fg=WARNING_COLOR)
service_outputs = {
"timestamp": DoctorResult(ok=True, output=dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat()),
"AlgoKit": DoctorResult(ok=True, output=current_algokit_version),
"Latest AlgoKit": DoctorResult(ok=True, output=latest_algokit_version),
"AlgoKit": _get_algokit_version_output(),
"AlgoKit Python": DoctorResult(ok=True, output=f"{sys.version} (location: {sys.prefix})"),
"OS": DoctorResult(ok=True, output=platform.platform()),
"docker": check_dependency(
Expand Down Expand Up @@ -121,7 +116,7 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:
# print end message anyway
logger.info(
"\n"
"If you are experiencing a problem with algokit, feel free to submit an issue via:\n"
"If you are experiencing a problem with AlgoKit, feel free to submit an issue via:\n"
"https://github.com/algorandfoundation/algokit-cli/issues/new\n"
"Please include this output, if you want to populate this message in your clipboard, run `algokit doctor -c`"
)
Expand All @@ -136,3 +131,17 @@ def doctor_command(*, copy_to_clipboard: bool) -> None:

if any(not value.ok for value in service_outputs.values()):
raise click.exceptions.Exit(code=1)


def _get_algokit_version_output() -> DoctorResult:
current = get_current_package_version()
try:
latest = get_latest_github_version()
except Exception as ex:
logger.warning("Failed to check latest AlgoKit release version", exc_info=ex)
latest = None
if latest is None or current == latest:
output = current
else:
output = click.style(current, fg=WARNING_COLOR) + f" (latest: {latest})"
return DoctorResult(ok=True, output=output)
1 change: 1 addition & 0 deletions src/algokit/cli/goal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import click

from algokit.core import proc

logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import copier.vcs # type: ignore
import prompt_toolkit.document
import questionary

from algokit.core import proc
from algokit.core.click_extensions import DeferredChoice
from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE
Expand Down
1 change: 1 addition & 0 deletions src/algokit/cli/sandbox.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import logging

import click

from algokit.cli.goal import goal_command
from algokit.core import proc
from algokit.core.sandbox import (
Expand Down
1 change: 1 addition & 0 deletions src/algokit/core/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Callable, Iterator

import click

from algokit.core import proc

ENV_TEMPLATE = ".env.template"
Expand Down
5 changes: 5 additions & 0 deletions src/algokit/core/conf.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import platform
from importlib import metadata
from pathlib import Path

PACKAGE_NAME = "algokit"
Expand Down Expand Up @@ -36,3 +37,7 @@ def _get_relative_app_path(base_dir: str) -> Path:
result = path / PACKAGE_NAME
result.mkdir(parents=True, exist_ok=True)
return result


def get_current_package_version() -> str:
return metadata.version(PACKAGE_NAME)
1 change: 1 addition & 0 deletions src/algokit/core/proc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from subprocess import run as subprocess_run

import click

from algokit.core.log_handlers import EXTRA_EXCLUDE_FROM_CONSOLE

logger = logging.getLogger(__name__)
Expand Down
1 change: 1 addition & 0 deletions src/algokit/core/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, cast

import httpx

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

Expand Down
97 changes: 38 additions & 59 deletions src/algokit/core/version_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,104 +2,83 @@
import os
import re
from datetime import timedelta
from importlib import metadata
from time import time

import click
import httpx
from algokit.core.conf import PACKAGE_NAME, get_app_config_dir, get_app_state_dir

from algokit.core.conf import get_app_config_dir, get_app_state_dir, get_current_package_version

logger = logging.getLogger(__name__)
Version = None | tuple[int, int, int] # type: ignore[misc]

LATEST_URL = "https://api.github.com/repos/algorandfoundation/algokit-cli/releases/latest"
VERSION_CHECK_INTERVAL = timedelta(weeks=1).total_seconds()
DISABLE_CHECK_MARKER = "disable-version-prompt"


def do_version_prompt() -> None:
if _is_version_prompt_disabled():
if _skip_version_prompt():
logger.debug("Version prompt disabled")
return

current_version = get_current_version()
if current_version is None:
logger.debug("Could not determine current version")
return

current_version = get_current_package_version()
latest_version = get_latest_version_or_cached()
if latest_version is None:
logger.debug("Could not determine latest version")
return

if current_version < latest_version:
logger.info(
f"You are using AlgoKit version {format_version(current_version)}, "
f"however version {format_version(latest_version)} is available."
)
if current_version != latest_version:
logger.info(f"You are using AlgoKit version {current_version}, however version {latest_version} is available.")
else:
logger.debug("Current version is up to date")


def format_version(version: Version) -> str:
return "" if version is None else ".".join(map(str, version))


def _parse_version(version_str: str) -> Version:
if version_str:
version_int = tuple(map(int, version_str.split(".")))
if len(version_int) == 3:
return version_int # type: ignore[return-value]
return None


def get_latest_version_or_cached() -> Version:
def get_latest_version_or_cached() -> str | None:
version_check_path = get_app_state_dir() / "last-version-check"

version: Version = None
last_checked = None
try:
last_checked = version_check_path.stat().st_mtime
version = _parse_version(version_check_path.read_text(encoding="utf-8"))
last_checked = os.path.getmtime(version_check_path)
version = version_check_path.read_text(encoding="utf-8")
except IOError:
logger.debug(f"{version_check_path} inaccessible")
last_checked = 0
version = None
else:
logger.debug(f"{version} found in cache {version_check_path}")
except FileNotFoundError:
logger.debug(f"{version_check_path} not found")
except Exception as ex:
logger.debug(f"Unexpected error parsing {version_check_path}", exc_info=ex)

now = time()
if last_checked is None or (now - last_checked) > VERSION_CHECK_INTERVAL:
version = get_latest_version()
last_checked = now
version_check_path.write_text(format_version(version), encoding="utf-8")
return version


def get_current_version() -> Version:
return _parse_version(metadata.version(PACKAGE_NAME))
if (time() - last_checked) > VERSION_CHECK_INTERVAL:
try:
version = get_latest_github_version()
except Exception as ex:
logger.debug("Checking for latest version failed", exc_info=ex)
# update last checked time even if check failed
version_check_path.touch()
else:
version_check_path.write_text(version, encoding="utf-8")
# handle case where the first check failed, so we have an empty file
return version or None


def get_latest_version() -> Version:
def get_latest_github_version() -> str:
headers = {"ACCEPT": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"}
# TODO: remove GH_TOKEN auth once algokit repo is public
gh_token = os.getenv("GH_TOKEN")
if gh_token:
if gh_token := os.getenv("GH_TOKEN"):
headers["Authorization"] = f"Bearer {gh_token}"

response = httpx.get(LATEST_URL, headers=headers)
if response.status_code != 200:
return None
response.raise_for_status()

json = response.json()
tag_name: str = json["tag_name"]
tag_name = json["tag_name"]
logger.debug(f"Latest version tag: {tag_name}")
match = re.match(r"v(\d+\.\d+\.\d+)", tag_name)
if not match:
return None
return tuple(map(int, match.group(1).split("."))) # type: ignore[return-value]
raise ValueError(f"Unable to extract version from tag_name: {tag_name}")
return match.group(1)


def _is_version_prompt_disabled() -> bool:
disable_marker = get_app_config_dir() / "disable-version-prompt"
def _skip_version_prompt() -> bool:
disable_marker = get_app_config_dir() / DISABLE_CHECK_MARKER
return disable_marker.exists()


Expand All @@ -115,13 +94,13 @@ def _is_version_prompt_disabled() -> bool:
@click.command("version-prompt", short_help="Enables or disables version prompt")
@click.argument("enable", required=False, type=bool, default=None)
def version_prompt_configuration_command(*, enable: bool | None) -> None:
if enable is not None:
disable_marker = get_app_config_dir() / "disable-version-prompt"
if enable is None:
logger.info(str(not _skip_version_prompt()))
else:
disable_marker = get_app_config_dir() / DISABLE_CHECK_MARKER
if enable:
disable_marker.unlink(missing_ok=True)
logger.info("📡 Resuming check for new versions")
else:
disable_marker.touch()
logger.info("🚫 Will stop checking for new versions")
else:
logger.info(str(not _is_version_prompt_disabled()))
Loading

0 comments on commit fcc205e

Please sign in to comment.