Module cli_tool_audit.view_pipx_stress_test

Stress test for the cli_tool_audit package using pipx installed tools as source data.

Expand source code
"""
Stress test for the cli_tool_audit package using pipx installed tools as source data.
"""
import concurrent
import json
import os
import subprocess  # nosec
from concurrent.futures import ThreadPoolExecutor
from threading import Lock
from typing import Any

from tqdm import tqdm

import cli_tool_audit.call_and_compatible as call_and_compatible
import cli_tool_audit.models as models
import cli_tool_audit.views as views


#  Dummy lock for switch to ProcessPoolExecutor
class DummyLock:
    """For testing"""

    def __enter__(self):
        """For testing"""

    def __exit__(self, exc_type, exc_value, traceback):
        """For testing"""


def get_pipx_list() -> Any:
    """
    Get the output of 'pipx list --json' as a dict.

    Returns:
        Any: The output of 'pipx list --json' as a dict or None if it fails.
    """
    try:
        result = subprocess.run(
            ["pipx", "list", "--json"], shell=False, capture_output=True, text=True, check=True
        )  # nosec
        return json.loads(result.stdout)
    except subprocess.CalledProcessError as e:
        print(f"Error executing 'pipx list --json': {e}")
        return None


def extract_apps(pipx_data: dict[str, Any]) -> dict[str, Any]:
    """
    Extract the apps from the output of 'pipx list --json'.
    Args:
        pipx_data (dict[str,Any]): The output of 'pipx list --json'.

    Returns:
        dict[str,Any]: A dictionary with the apps and their versions.
    """
    apps_dict = {}
    if pipx_data and "venvs" in pipx_data:
        for _package, data in pipx_data["venvs"].items():
            package_version = data["metadata"]["main_package"]["package_version"]
            apps = data["metadata"]["main_package"]["apps"]
            for app in apps:
                apps_dict[app] = package_version
    return apps_dict


def report_for_pipx_tools(max_count: int = -1) -> None:
    """
    Report on the compatibility of the tools installed with pipx.
    Args:
        max_count (int, optional): The maximum number of tools to report on. Defaults to -1.
    """
    pipx_data = get_pipx_list()
    apps_dict = extract_apps(pipx_data)

    # for app, version in apps_dict.items():
    #     print(f"{app}: {version}")
    count = 0
    cli_tools = {}
    for app, expected_version in apps_dict.items():
        if app in ("yated.exe", "calcure.exe", "yated", "calcure", "dedlin.exe", "dedlin"):
            # These launch interactive process & then time out.
            continue
        config = models.CliToolConfig(app)
        config.version_switch = "--version"
        config.version = f">={expected_version}"
        cli_tools[app] = config
        count += 1
        if count >= max_count > 0:
            break

    # Determine the number of available CPUs
    num_cpus = os.cpu_count()

    enable_cache = len(cli_tools) >= 5
    # Create a ThreadPoolExecutor with one thread per CPU
    lock = Lock()
    with ThreadPoolExecutor(max_workers=num_cpus) as executor:
        # threaded is faster
        # lock = Dummy()
        # with ProcessPoolExecutor(max_workers=num_cpus) as executor:
        # Submit tasks to the executor
        disable = views.should_show_progress_bar(cli_tools)
        with tqdm(total=len(cli_tools), disable=disable) as progress_bar:
            futures = [
                executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache))
                for tool, config in cli_tools.items()
            ]

            results = []
            # Process the results as they are completed
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                tqdm.update(progress_bar, 1)
                results.append(result)
        print(views.pretty_print_results(results, truncate_long_versions=True, include_docs=False))


if __name__ == "__main__":
    report_for_pipx_tools()

Functions

def extract_apps(pipx_data: dict[str, typing.Any]) ‑> dict[str, typing.Any]

Extract the apps from the output of 'pipx list –json'.

Args

pipx_data : dict[str,Any]
The output of 'pipx list –json'.

Returns

dict[str,Any]
A dictionary with the apps and their versions.
Expand source code
def extract_apps(pipx_data: dict[str, Any]) -> dict[str, Any]:
    """
    Extract the apps from the output of 'pipx list --json'.
    Args:
        pipx_data (dict[str,Any]): The output of 'pipx list --json'.

    Returns:
        dict[str,Any]: A dictionary with the apps and their versions.
    """
    apps_dict = {}
    if pipx_data and "venvs" in pipx_data:
        for _package, data in pipx_data["venvs"].items():
            package_version = data["metadata"]["main_package"]["package_version"]
            apps = data["metadata"]["main_package"]["apps"]
            for app in apps:
                apps_dict[app] = package_version
    return apps_dict
def get_pipx_list() ‑> Any

Get the output of 'pipx list –json' as a dict.

Returns

Any
The output of 'pipx list –json' as a dict or None if it fails.
Expand source code
def get_pipx_list() -> Any:
    """
    Get the output of 'pipx list --json' as a dict.

    Returns:
        Any: The output of 'pipx list --json' as a dict or None if it fails.
    """
    try:
        result = subprocess.run(
            ["pipx", "list", "--json"], shell=False, capture_output=True, text=True, check=True
        )  # nosec
        return json.loads(result.stdout)
    except subprocess.CalledProcessError as e:
        print(f"Error executing 'pipx list --json': {e}")
        return None
def report_for_pipx_tools(max_count: int = -1) ‑> None

Report on the compatibility of the tools installed with pipx.

Args

max_count : int, optional
The maximum number of tools to report on. Defaults to -1.
Expand source code
def report_for_pipx_tools(max_count: int = -1) -> None:
    """
    Report on the compatibility of the tools installed with pipx.
    Args:
        max_count (int, optional): The maximum number of tools to report on. Defaults to -1.
    """
    pipx_data = get_pipx_list()
    apps_dict = extract_apps(pipx_data)

    # for app, version in apps_dict.items():
    #     print(f"{app}: {version}")
    count = 0
    cli_tools = {}
    for app, expected_version in apps_dict.items():
        if app in ("yated.exe", "calcure.exe", "yated", "calcure", "dedlin.exe", "dedlin"):
            # These launch interactive process & then time out.
            continue
        config = models.CliToolConfig(app)
        config.version_switch = "--version"
        config.version = f">={expected_version}"
        cli_tools[app] = config
        count += 1
        if count >= max_count > 0:
            break

    # Determine the number of available CPUs
    num_cpus = os.cpu_count()

    enable_cache = len(cli_tools) >= 5
    # Create a ThreadPoolExecutor with one thread per CPU
    lock = Lock()
    with ThreadPoolExecutor(max_workers=num_cpus) as executor:
        # threaded is faster
        # lock = Dummy()
        # with ProcessPoolExecutor(max_workers=num_cpus) as executor:
        # Submit tasks to the executor
        disable = views.should_show_progress_bar(cli_tools)
        with tqdm(total=len(cli_tools), disable=disable) as progress_bar:
            futures = [
                executor.submit(call_and_compatible.check_tool_wrapper, (tool, config, lock, enable_cache))
                for tool, config in cli_tools.items()
            ]

            results = []
            # Process the results as they are completed
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                tqdm.update(progress_bar, 1)
                results.append(result)
        print(views.pretty_print_results(results, truncate_long_versions=True, include_docs=False))

Classes

class DummyLock

For testing

Expand source code
class DummyLock:
    """For testing"""

    def __enter__(self):
        """For testing"""

    def __exit__(self, exc_type, exc_value, traceback):
        """For testing"""