<commit_before>"""The purpose of these functions is to check the local dependencies of the person running the CLI
tool and ensure that things are properly configured for the cli's full use (depending on the user's
operating system.) When dependencies are missing the CLI tool should offer helpful hints about what
course of action to take to install missing dependencies, even offering to run appropriate
installation commands where applicable."""

# future
from __future__ import annotations

# stdlib
from dataclasses import dataclass
from dataclasses import field
from datetime import datetime
import getpass
import json
import os
import platform
import re
import shutil
import subprocess  # nosec
import sys
import traceback
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

# third party
from packaging import version
from packaging.version import Version
import requests
from rich.console import Console

# relative
from .exceptions import MissingDependency
from .mode import EDITABLE_MODE
from .nb_output import NBOutput
from .version import __version__

LATEST_STABLE_SYFT = "0.6"

DOCKER_ERROR = """
You are running an old version of docker, possibly on Linux. You need to install v2.
At the time of writing this, if you are on linux you need to run the following:

DOCKER_COMPOSE_VERSION=v2.7.0
curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 \
     -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose

ALERT: you may need to run the following command to make sure you can run without sudo.

echo $USER              //(should return your username)
sudo usermod -aG docker $USER

... now LOG ALL THE WAY OUT!!!

...and then you should be good to go. You can check your installation by running:

docker compose version
"""

SYFT_MINIMUM_PYTHON_VERSION = (3, 7)
SYFT_MINIMUM_PYTHON_VERSION_STRING = "3.7"
SYFT_MAXIMUM_PYTHON_VERSION = (3, 10, 999)
SYFT_MAXIMUM_PYTHON_VERSION_STRING = "3.10"
WHITE = "\033[0;37m"
GREEN = "\033[0;32m"
YELLOW = "\033[0;33m"
BOLD = "\033[1m"
NO_COLOR = "\033[0;0m"
WARNING_MSG = f"\033[0;33mWARNING:{NO_COLOR}"


@dataclass
class SetupIssue:
    issue_name: str
    description: str
    command: Optional[str] = None
    solution: Optional[str] = None


@dataclass
class Dependency:
    of: str = ""
    name: str = ""
    display: str = ""
    only_os: str = ""
    version: Optional[Version] = version.parse("None")
    valid: bool = False
    issues: List[SetupIssue] = field(default_factory=list)
    output_in_text: bool = False

    def check(self) -> None:
        pass


@dataclass
class DependencySyftOS(Dependency):
    of: str = "syft"

    def check(self) -> None:
        self.display = "✅ " + ENVIRONMENT["os"]
        if is_windows():
            if not get_pip_package("jaxlib"):
                self.issues.append(windows_jaxlib())
        elif is_apple_silicon():
            pass


@dataclass
class DependencySyftPython(Dependency):
    of: str = "syft"

    def check(self) -> None:
        self.version = sys.version_info
        if (
            sys.version_info >= SYFT_MINIMUM_PYTHON_VERSION
            and sys.version_info <= SYFT_MAXIMUM_PYTHON_VERSION
        ):
            self.display = "✅ Python " + ENVIRONMENT["python_version"]
        else:
            self.issues.append(python_version_unsupported())
            self.display = "❌ " + ENVIRONMENT["python_version"]


@dataclass
class DependencyGridGit(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="git", version_cmd="git --version"
        ).get_binary_info()
        if binary_info.path and binary_info.version:
            self.display = "✅ Git " + str(binary_info.version)
        else:
            self.issues.append(git_install(self.output_in_text))
            self.display = "❌ Git not installed"


MINIMUM_DOCKER_VERSION = "20.0.0"


@dataclass
class DependencyGridDocker(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="docker", version_cmd="docker --version"
        ).get_binary_info()
        if binary_info.path and binary_info.version > version.parse(
            MINIMUM_DOCKER_VERSION
        ):
            self.display = "✅ Docker " + str(binary_info.version)
        else:
            self.issues.append(docker_install())
            self.display = "❌ Docker not installed"


MINIMUM_DOCKER_COMPOSE_VERSION = "2.0.0"


@dataclass
class DependencyGridDockerCompose(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="docker", version_cmd="docker compose version"
        ).get_binary_info()

        if binary_info.path and binary_info.version > version.parse(
            MINIMUM_DOCKER_COMPOSE_VERSION
        ):
            self.display = "✅ Docker Compose " + str(binary_info.version)
        else:
            self.issues.append(docker_compose_install())
            self.display = "❌ Docker Compose v2 not installed"


@dataclass
class DependencyPyPI(Dependency):
    of: str = "none"
    package_name: str = ""
    package_display_name: str = ""
    pre: bool = False
    install_issue: Callable = lambda: None
    update_available_issue: Callable = lambda: None

    def check(self) -> None:
        package_dict = get_pip_package(self.package_name)

        if package_dict is None:
            self.display = "❌ " + f"{self.package_display_name} not installed"
            self.issues.append(self.install_issue(pre=self.pre))
        else:
            version_string = package_dict["version"]
            current_version = version.parse(version_string)
            if "editable_project_location" in package_dict:
                self.display = (
                    "🚨 "
                    + f"{self.package_name}=={str(current_version)} -e {package_dict['editable_project_location']}"
                )
            else:
                is_newer, latest_version = new_pypi_version(
                    package=self.package_name, current=current_version, pre=self.pre
                )
                if not is_newer:
                    channel = "stable"
                    if current_version.is_prerelease:
                        channel = "pre-release"
                    self.display = (
                        "✅ "
                        + f"{self.package_name}=={str(version_string)} (latest {channel})"
                    )
                else:
                    self.display = (
                        "✅ "
                        + f"{self.package_name}=={str(current_version)} (Version {str(latest_version)} available)"
                    )
                    self.issues.append(
                        self.update_available_issue(current_version, latest_version)
                    )


def new_pypi_version(
    package: str, current: Version, pre: bool = False
) -> Tuple[bool, Version]:
    pypi_json = get_pypi_versions(package_name=package)
    if (
        "info" not in pypi_json
        or "releases" not in pypi_json
        or "version" not in pypi_json["info"]
    ):
        raise Exception("Bad response from PyPi")

    if not current.is_prerelease and not pre:
        latest_stable = version.parse(pypi_json["info"]["version"])
        if current < latest_stable:
            return (True, latest_stable)
        else:
            return (False, current)
    else:
        latest_release = current

        releases = sorted(list(pypi_json["releases"].keys()))
        for release in releases:
            pre_release_version = version.parse(release)
            if latest_release < pre_release_version:
                latest_release = pre_release_version

        if latest_release != current:
            return (True, latest_release)
        else:
            return (False, latest_release)


def get_pypi_versions(package_name: str) -> Dict[str, Any]:
    try:
        pypi_url = f"https://pypi.org/pypi/{package_name}/json"
        req = requests.get(pypi_url)
        # TODO: Fix JSON parsing of version keys
        # this is broken on my machine for some reason, the version keys are wrong
        pypi_info = json.loads(req.text)
        # print(pypi_info["releases"].keys())
        return pypi_info

    except Exception as e:
        print(f"Unable to get JSON from PyPI URL: {pypi_url}. {e}")
        raise e


def get_pip_package(package_name: str) -> Optional[Dict[str, str]]:
    packages = get_pip_packages()
    for package in packages:
        if package["name"] == package_name:
            return package
    return None


def get_pip_packages() -> List[Dict[str, str]]:
    try:
        cmd = "python -m pip list --format=json --disable-pip-version-check"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return json.loads(str(output.decode("utf-8")).strip())
    except Exception as e:
        print("failed to pip list", e)
        raise e


def get_location(binary: str) -> Optional[str]:
    return shutil.which(binary)


@dataclass
class BinaryInfo:
    binary: str
    version_cmd: str
    error: Optional[str] = None
    path: Optional[str] = None
    version: Optional[Union[str, Version]] = version.parse("None")
    version_regex = (
        r"[^\d]*("
        + r"(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)"
        + r"(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
        + r"(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
        + r"(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?)"
        + r"[^\d].*"
    )

    def extract_version(self, lines: List[str]) -> None:
        for line in lines:
            matches = re.match(self.version_regex, line)
            if matches is not None:
                self.version = matches.group(1)
                try:
                    if "-gitpod" in self.version:
                        parts = self.version.split("-gitpod")
                        self.version = parts[0]
                    self.version = version.parse(self.version)
                except Exception:  # nosec
                    pass
                break

    def get_binary_info(self) -> BinaryInfo:
        self.path = get_location(self.binary)
        if self.path:
            returncode, lines = get_cli_output(self.version_cmd)
            if returncode == 0:
                self.extract_version(lines=lines)
            else:
                if len(lines) > 0:
                    self.error = lines[0]
                else:
                    self.error = f"Error, no output from {self.binary}"
        return self


def get_cli_output(cmd: str, timeout: Optional[float] = None) -> Tuple[int, List[str]]:
    try:
        proc = subprocess.Popen(  # nosec
            cmd.split(" "),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        lines = []
        if proc.stdout and hasattr(proc.stdout, "readlines"):
            lines = [line.decode("utf-8") for line in proc.stdout.readlines()]

        if proc.stderr and hasattr(proc.stderr, "readlines"):
            lines.extend([line.decode("utf-8") for line in proc.stderr.readlines()])

        proc.communicate(timeout=timeout)
        return (int(proc.returncode), lines)
    except Exception as e:
        return (-1, [str(e)])


def gather_debug() -> Dict[str, Any]:
    # relative
    from .lib import commit_hash
    from .lib import hagrid_root

    now = datetime.now().astimezone()
    dt_string = now.strftime("%d/%m/%Y %H:%M:%S %Z")
    debug_info: Dict[str, Any] = {}
    debug_info["datetime"] = dt_string
    debug_info["python_binary"] = sys.executable
    debug_info["dependencies"] = DEPENDENCIES
    debug_info["environment"] = ENVIRONMENT
    debug_info["hagrid"] = __version__
    debug_info["hagrid_dev"] = EDITABLE_MODE
    debug_info["hagrid_path"] = hagrid_root()
    debug_info["hagrid_repo_sha"] = commit_hash()
    debug_info["docker"] = docker_info()
    if is_windows():
        debug_info["wsl"] = wsl_info()
        debug_info["wsl_linux"] = wsl_linux_info()
    return debug_info


def get_environment() -> Dict[str, Any]:
    return {
        "uname": platform.uname(),
        "platform": platform.system().lower(),
        "os_version": platform.release(),
        "python_version": platform.python_version(),
    }


ENVIRONMENT = get_environment()


def os_name() -> str:
    os_name = platform.system()
    if os_name.lower() == "darwin":
        return "macOS"
    else:
        return os_name


ENVIRONMENT["os"] = os_name()


def is_apple_silicon() -> bool:
    if (
        "platform" in ENVIRONMENT
        and ENVIRONMENT["platform"].lower() == "darwin"
        and ENVIRONMENT["uname"].machine != "x86_64"
    ):
        return True
    return False


ENVIRONMENT["apple_silicon"] = is_apple_silicon()


def is_windows() -> bool:
    if "platform" in ENVIRONMENT and ENVIRONMENT["platform"].lower() == "windows":
        return True
    return False


allowed_hosts = ["docker", "vm", "azure", "aws", "gcp"]
commands = ["docker", "git", "ansible-playbook"]

if is_windows():
    commands.append("wsl")


def check_deps_old() -> Dict[str, Optional[str]]:
    paths = {}
    for dep in commands:
        paths[dep] = shutil.which(dep)
    return paths


DEPENDENCIES = check_deps_old()


def docker_info() -> str:
    try:
        cmd = "docker info"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get docker info", e)
        return str(e)


def wsl_info() -> str:
    try:
        cmd = "wsl --status"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get wsl info", e)
        return str(e)


def wsl_linux_info() -> str:
    try:
        cmd = "wsl bash -c 'lsb_release -a'"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get wsl linux info", e)
        return str(e)


def check_docker_version() -> Optional[str]:
    if is_windows():
        return "N/A"  # todo fix to work with windows
    result = os.popen("docker compose version", "r").read()  # nosec
    version = None
    if "version" in result:
        version = result.split()[-1]
    else:
        print("This may be a linux machine, either that or docker compose isn't s")
        print("Result:" + result)
        out = subprocess.run(  # nosec
            ["docker", "compose"], capture_output=True, text=True
        )
        if "'compose' is not a docker command" in out.stderr:
            raise MissingDependency(DOCKER_ERROR)

    return version


def docker_running(timeout: Optional[float] = None) -> Tuple[bool, str]:

    status, error_msg = False, ""

    try:
        cmd = "docker info"
        returncode, msg = get_cli_output(cmd, timeout=timeout)
        if returncode == 0:
            status, error_msg = True, "✅ Docker service is running"
        else:
            error_msg = f"""❌ Docker service is either not installed or running.\n\n
To install docker, execute the following steps:\n
1 - Install docker on your machine by using the proper steps according to your OS.\n
{WHITE}MacOS: {GREEN}brew install --cask docker
{WHITE}Linux: {GREEN}curl -fsSL https://get.docker.com -o get-docker.sh && chmod +777 get-docker.sh && ./get-docker.sh
{WHITE}Windows: {GREEN}choco install docker-desktop -y{NO_COLOR} \n
2 - Run \'{GREEN}sudo usermod -a -G docker $USER\'{WHITE} to enable this user to execute docker.
3 - log out and log back in so that your group membership is re-evaluated {NO_COLOR}.
-------------------------------------------------------------------------------------------------------\n
To start your docker service:\n
1 - {WHITE}MacOS/Windows: One can start docker by clicking on the "Docker" icon in your Applications folder.{NO_COLOR}
2 - {WHITE}Ubuntu: {GREEN}sudo service docker start {NO_COLOR}
-------------------------------------------------------------------------------------------------------\n
"""
        error_msg += f"""{BOLD}{YELLOW}Std Output Logs{NO_COLOR}\n\n
            ==================================""" + "\n".join(
            msg
        )
    except Exception as e:  # nosec
        error_msg = str(e)

    return status, error_msg


def allowed_to_run_docker() -> Tuple[bool, str]:
    bool_result, msg = True, ""<commit_msg>fix formatting of underlining std out logs heading<commit_after>"""The purpose of these functions is to check the local dependencies of the person running the CLI
tool and ensure that things are properly configured for the cli's full use (depending on the user's
operating system.) When dependencies are missing the CLI tool should offer helpful hints about what
course of action to take to install missing dependencies, even offering to run appropriate
installation commands where applicable."""

# future
from __future__ import annotations

# stdlib
from dataclasses import dataclass
from dataclasses import field
from datetime import datetime
import getpass
import json
import os
import platform
import re
import shutil
import subprocess  # nosec
import sys
import traceback
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union

# third party
from packaging import version
from packaging.version import Version
import requests
from rich.console import Console

# relative
from .exceptions import MissingDependency
from .mode import EDITABLE_MODE
from .nb_output import NBOutput
from .version import __version__

LATEST_STABLE_SYFT = "0.6"

DOCKER_ERROR = """
You are running an old version of docker, possibly on Linux. You need to install v2.
At the time of writing this, if you are on linux you need to run the following:

DOCKER_COMPOSE_VERSION=v2.7.0
curl -sSL https://github.com/docker/compose/releases/download/${DOCKER_COMPOSE_VERSION}/docker-compose-linux-x86_64 \
     -o ~/.docker/cli-plugins/docker-compose
chmod +x ~/.docker/cli-plugins/docker-compose

ALERT: you may need to run the following command to make sure you can run without sudo.

echo $USER              //(should return your username)
sudo usermod -aG docker $USER

... now LOG ALL THE WAY OUT!!!

...and then you should be good to go. You can check your installation by running:

docker compose version
"""

SYFT_MINIMUM_PYTHON_VERSION = (3, 7)
SYFT_MINIMUM_PYTHON_VERSION_STRING = "3.7"
SYFT_MAXIMUM_PYTHON_VERSION = (3, 10, 999)
SYFT_MAXIMUM_PYTHON_VERSION_STRING = "3.10"
WHITE = "\033[0;37m"
GREEN = "\033[0;32m"
YELLOW = "\033[0;33m"
BOLD = "\033[1m"
NO_COLOR = "\033[0;0m"
WARNING_MSG = f"\033[0;33mWARNING:{NO_COLOR}"


@dataclass
class SetupIssue:
    issue_name: str
    description: str
    command: Optional[str] = None
    solution: Optional[str] = None


@dataclass
class Dependency:
    of: str = ""
    name: str = ""
    display: str = ""
    only_os: str = ""
    version: Optional[Version] = version.parse("None")
    valid: bool = False
    issues: List[SetupIssue] = field(default_factory=list)
    output_in_text: bool = False

    def check(self) -> None:
        pass


@dataclass
class DependencySyftOS(Dependency):
    of: str = "syft"

    def check(self) -> None:
        self.display = "✅ " + ENVIRONMENT["os"]
        if is_windows():
            if not get_pip_package("jaxlib"):
                self.issues.append(windows_jaxlib())
        elif is_apple_silicon():
            pass


@dataclass
class DependencySyftPython(Dependency):
    of: str = "syft"

    def check(self) -> None:
        self.version = sys.version_info
        if (
            sys.version_info >= SYFT_MINIMUM_PYTHON_VERSION
            and sys.version_info <= SYFT_MAXIMUM_PYTHON_VERSION
        ):
            self.display = "✅ Python " + ENVIRONMENT["python_version"]
        else:
            self.issues.append(python_version_unsupported())
            self.display = "❌ " + ENVIRONMENT["python_version"]


@dataclass
class DependencyGridGit(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="git", version_cmd="git --version"
        ).get_binary_info()
        if binary_info.path and binary_info.version:
            self.display = "✅ Git " + str(binary_info.version)
        else:
            self.issues.append(git_install(self.output_in_text))
            self.display = "❌ Git not installed"


MINIMUM_DOCKER_VERSION = "20.0.0"


@dataclass
class DependencyGridDocker(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="docker", version_cmd="docker --version"
        ).get_binary_info()
        if binary_info.path and binary_info.version > version.parse(
            MINIMUM_DOCKER_VERSION
        ):
            self.display = "✅ Docker " + str(binary_info.version)
        else:
            self.issues.append(docker_install())
            self.display = "❌ Docker not installed"


MINIMUM_DOCKER_COMPOSE_VERSION = "2.0.0"


@dataclass
class DependencyGridDockerCompose(Dependency):
    of: str = "grid"

    def check(self) -> None:
        binary_info = BinaryInfo(
            binary="docker", version_cmd="docker compose version"
        ).get_binary_info()

        if binary_info.path and binary_info.version > version.parse(
            MINIMUM_DOCKER_COMPOSE_VERSION
        ):
            self.display = "✅ Docker Compose " + str(binary_info.version)
        else:
            self.issues.append(docker_compose_install())
            self.display = "❌ Docker Compose v2 not installed"


@dataclass
class DependencyPyPI(Dependency):
    of: str = "none"
    package_name: str = ""
    package_display_name: str = ""
    pre: bool = False
    install_issue: Callable = lambda: None
    update_available_issue: Callable = lambda: None

    def check(self) -> None:
        package_dict = get_pip_package(self.package_name)

        if package_dict is None:
            self.display = "❌ " + f"{self.package_display_name} not installed"
            self.issues.append(self.install_issue(pre=self.pre))
        else:
            version_string = package_dict["version"]
            current_version = version.parse(version_string)
            if "editable_project_location" in package_dict:
                self.display = (
                    "🚨 "
                    + f"{self.package_name}=={str(current_version)} -e {package_dict['editable_project_location']}"
                )
            else:
                is_newer, latest_version = new_pypi_version(
                    package=self.package_name, current=current_version, pre=self.pre
                )
                if not is_newer:
                    channel = "stable"
                    if current_version.is_prerelease:
                        channel = "pre-release"
                    self.display = (
                        "✅ "
                        + f"{self.package_name}=={str(version_string)} (latest {channel})"
                    )
                else:
                    self.display = (
                        "✅ "
                        + f"{self.package_name}=={str(current_version)} (Version {str(latest_version)} available)"
                    )
                    self.issues.append(
                        self.update_available_issue(current_version, latest_version)
                    )


def new_pypi_version(
    package: str, current: Version, pre: bool = False
) -> Tuple[bool, Version]:
    pypi_json = get_pypi_versions(package_name=package)
    if (
        "info" not in pypi_json
        or "releases" not in pypi_json
        or "version" not in pypi_json["info"]
    ):
        raise Exception("Bad response from PyPi")

    if not current.is_prerelease and not pre:
        latest_stable = version.parse(pypi_json["info"]["version"])
        if current < latest_stable:
            return (True, latest_stable)
        else:
            return (False, current)
    else:
        latest_release = current

        releases = sorted(list(pypi_json["releases"].keys()))
        for release in releases:
            pre_release_version = version.parse(release)
            if latest_release < pre_release_version:
                latest_release = pre_release_version

        if latest_release != current:
            return (True, latest_release)
        else:
            return (False, latest_release)


def get_pypi_versions(package_name: str) -> Dict[str, Any]:
    try:
        pypi_url = f"https://pypi.org/pypi/{package_name}/json"
        req = requests.get(pypi_url)
        # TODO: Fix JSON parsing of version keys
        # this is broken on my machine for some reason, the version keys are wrong
        pypi_info = json.loads(req.text)
        # print(pypi_info["releases"].keys())
        return pypi_info

    except Exception as e:
        print(f"Unable to get JSON from PyPI URL: {pypi_url}. {e}")
        raise e


def get_pip_package(package_name: str) -> Optional[Dict[str, str]]:
    packages = get_pip_packages()
    for package in packages:
        if package["name"] == package_name:
            return package
    return None


def get_pip_packages() -> List[Dict[str, str]]:
    try:
        cmd = "python -m pip list --format=json --disable-pip-version-check"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return json.loads(str(output.decode("utf-8")).strip())
    except Exception as e:
        print("failed to pip list", e)
        raise e


def get_location(binary: str) -> Optional[str]:
    return shutil.which(binary)


@dataclass
class BinaryInfo:
    binary: str
    version_cmd: str
    error: Optional[str] = None
    path: Optional[str] = None
    version: Optional[Union[str, Version]] = version.parse("None")
    version_regex = (
        r"[^\d]*("
        + r"(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)\.*(0|[1-9][0-9]*)"
        + r"(-((0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
        + r"(\.(0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
        + r"(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?)"
        + r"[^\d].*"
    )

    def extract_version(self, lines: List[str]) -> None:
        for line in lines:
            matches = re.match(self.version_regex, line)
            if matches is not None:
                self.version = matches.group(1)
                try:
                    if "-gitpod" in self.version:
                        parts = self.version.split("-gitpod")
                        self.version = parts[0]
                    self.version = version.parse(self.version)
                except Exception:  # nosec
                    pass
                break

    def get_binary_info(self) -> BinaryInfo:
        self.path = get_location(self.binary)
        if self.path:
            returncode, lines = get_cli_output(self.version_cmd)
            if returncode == 0:
                self.extract_version(lines=lines)
            else:
                if len(lines) > 0:
                    self.error = lines[0]
                else:
                    self.error = f"Error, no output from {self.binary}"
        return self


def get_cli_output(cmd: str, timeout: Optional[float] = None) -> Tuple[int, List[str]]:
    try:
        proc = subprocess.Popen(  # nosec
            cmd.split(" "),
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
        lines = []
        if proc.stdout and hasattr(proc.stdout, "readlines"):
            lines = [line.decode("utf-8") for line in proc.stdout.readlines()]

        if proc.stderr and hasattr(proc.stderr, "readlines"):
            lines.extend([line.decode("utf-8") for line in proc.stderr.readlines()])

        proc.communicate(timeout=timeout)
        return (int(proc.returncode), lines)
    except Exception as e:
        return (-1, [str(e)])


def gather_debug() -> Dict[str, Any]:
    # relative
    from .lib import commit_hash
    from .lib import hagrid_root

    now = datetime.now().astimezone()
    dt_string = now.strftime("%d/%m/%Y %H:%M:%S %Z")
    debug_info: Dict[str, Any] = {}
    debug_info["datetime"] = dt_string
    debug_info["python_binary"] = sys.executable
    debug_info["dependencies"] = DEPENDENCIES
    debug_info["environment"] = ENVIRONMENT
    debug_info["hagrid"] = __version__
    debug_info["hagrid_dev"] = EDITABLE_MODE
    debug_info["hagrid_path"] = hagrid_root()
    debug_info["hagrid_repo_sha"] = commit_hash()
    debug_info["docker"] = docker_info()
    if is_windows():
        debug_info["wsl"] = wsl_info()
        debug_info["wsl_linux"] = wsl_linux_info()
    return debug_info


def get_environment() -> Dict[str, Any]:
    return {
        "uname": platform.uname(),
        "platform": platform.system().lower(),
        "os_version": platform.release(),
        "python_version": platform.python_version(),
    }


ENVIRONMENT = get_environment()


def os_name() -> str:
    os_name = platform.system()
    if os_name.lower() == "darwin":
        return "macOS"
    else:
        return os_name


ENVIRONMENT["os"] = os_name()


def is_apple_silicon() -> bool:
    if (
        "platform" in ENVIRONMENT
        and ENVIRONMENT["platform"].lower() == "darwin"
        and ENVIRONMENT["uname"].machine != "x86_64"
    ):
        return True
    return False


ENVIRONMENT["apple_silicon"] = is_apple_silicon()


def is_windows() -> bool:
    if "platform" in ENVIRONMENT and ENVIRONMENT["platform"].lower() == "windows":
        return True
    return False


allowed_hosts = ["docker", "vm", "azure", "aws", "gcp"]
commands = ["docker", "git", "ansible-playbook"]

if is_windows():
    commands.append("wsl")


def check_deps_old() -> Dict[str, Optional[str]]:
    paths = {}
    for dep in commands:
        paths[dep] = shutil.which(dep)
    return paths


DEPENDENCIES = check_deps_old()


def docker_info() -> str:
    try:
        cmd = "docker info"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get docker info", e)
        return str(e)


def wsl_info() -> str:
    try:
        cmd = "wsl --status"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get wsl info", e)
        return str(e)


def wsl_linux_info() -> str:
    try:
        cmd = "wsl bash -c 'lsb_release -a'"
        output = subprocess.check_output(cmd, shell=True)  # nosec
        return str(output.decode("utf-8"))
    except Exception as e:
        print("failed to get wsl linux info", e)
        return str(e)


def check_docker_version() -> Optional[str]:
    if is_windows():
        return "N/A"  # todo fix to work with windows
    result = os.popen("docker compose version", "r").read()  # nosec
    version = None
    if "version" in result:
        version = result.split()[-1]
    else:
        print("This may be a linux machine, either that or docker compose isn't s")
        print("Result:" + result)
        out = subprocess.run(  # nosec
            ["docker", "compose"], capture_output=True, text=True
        )
        if "'compose' is not a docker command" in out.stderr:
            raise MissingDependency(DOCKER_ERROR)

    return version


def docker_running(timeout: Optional[float] = None) -> Tuple[bool, str]:

    status, error_msg = False, ""

    try:
        cmd = "docker info"
        returncode, msg = get_cli_output(cmd, timeout=timeout)
        if returncode == 0:
            status, error_msg = True, "✅ Docker service is running"
        else:
            error_msg = f"""❌ Docker service is either not installed or running.\n\n
To install docker, execute the following steps:\n
1 - Install docker on your machine by using the proper steps according to your OS.\n
{WHITE}MacOS: {GREEN}brew install --cask docker
{WHITE}Linux: {GREEN}curl -fsSL https://get.docker.com -o get-docker.sh && chmod +777 get-docker.sh && ./get-docker.sh
{WHITE}Windows: {GREEN}choco install docker-desktop -y{NO_COLOR} \n
2 - Run \'{GREEN}sudo usermod -a -G docker $USER\'{WHITE} to enable this user to execute docker.
3 - log out and log back in so that your group membership is re-evaluated {NO_COLOR}.
-------------------------------------------------------------------------------------------------------\n
To start your docker service:\n
1 - {WHITE}MacOS/Windows: One can start docker by clicking on the "Docker" icon in your Applications folder.{NO_COLOR}
2 - {WHITE}Ubuntu: {GREEN}sudo service docker start {NO_COLOR}
-------------------------------------------------------------------------------------------------------\n
"""
        error_msg += f"""{YELLOW}{BOLD}Std Output Logs{NO_COLOR}
=================\n\n""" + "\n".join(
            msg
        )

    except Exception as e:  # nosec
        error_msg = str(e)

    return status, error_msg


def allowed_to_run_docker() -> Tuple[bool, str]:
    bool_result, msg = True, ""
{'fraction_non_alphanumeric': 0.1280866512646644, 'fraction_numerical': 0.0076209907287947435, 'mean_word_length': 2.3670067683604405, 'pattern_counts': {'":': 12, '<': 9, '<?xml version=': 0, '>': 65, 'https://': 6, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1338749', 'n_tokens_mistral': 10549, 'n_tokens_neox': 10052, 'n_words': 2644}