Module cli_tool_audit.call_tools
Check if an external tool is expected and available on the PATH.
Also, check version.
Possible things that could happen - found/not found on path - found but can't run without error - found, runs but wrong version switch - found, has version but cli version != package version
Expand source code
"""
Check if an external tool is expected and available on the PATH.
Also, check version.
Possible things that could happen
- found/not found on path
- found but can't run without error
- found, runs but wrong version switch
- found, has version but cli version != package version
"""
import datetime
import logging
import os
import subprocess # nosec
from typing import Optional
# pylint: disable=no-name-in-module
from whichcraft import which
import cli_tool_audit.models as models
from cli_tool_audit.known_switches import KNOWN_SWITCHES
logger = logging.getLogger(__name__)
def get_command_last_modified_date(tool_name: str) -> Optional[datetime.datetime]:
"""
Get the last modified date of a command's executable.
Args:
tool_name (str): The name of the command.
Returns:
Optional[datetime.datetime]: The last modified date of the command's executable.
"""
# Find the path of the command's executable
result = which(tool_name)
if result is None:
return None
executable_path = result
# Get the last modified time of the executable
last_modified_timestamp = os.path.getmtime(executable_path)
return datetime.datetime.fromtimestamp(last_modified_timestamp)
def check_tool_availability(
tool_name: str,
schema: models.SchemaType,
version_switch: str = "--version",
) -> models.ToolAvailabilityResult:
"""
Check if a tool is available in the system's PATH and if possible, determine a version number.
Args:
tool_name (str): The name of the tool to check.
schema (models.SchemaType): The schema to use for the version.
version_switch (str): The switch to get the tool version. Defaults to '--version'.
Returns:
models.ToolAvailabilityResult: An object containing the availability and version of the tool.
"""
# Check if the tool is in the system's PATH
is_broken = True
last_modified = get_command_last_modified_date(tool_name)
if not last_modified:
logger.warning(f"{tool_name} is not on path.")
return models.ToolAvailabilityResult(False, True, None, last_modified)
if schema == models.SchemaType.EXISTENCE:
logger.debug(f"{tool_name} exists, but not checking for version.")
return models.ToolAvailabilityResult(True, False, None, last_modified)
if version_switch is None or version_switch == "--version":
# override default.
# Could be a problem if KNOWN_SWITCHES was ever wrong.
version_switch = KNOWN_SWITCHES.get(tool_name, "--version")
version = None
# pylint: disable=broad-exception-caught
try:
command = [tool_name, version_switch]
timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15))
result = subprocess.run(
command, capture_output=True, text=True, timeout=timeout, shell=False, check=True
) # nosec
# Sometimes version is on line 2 or later.
version = result.stdout.strip()
if not version:
# check stderror
logger.debug("Got nothing from stdout, checking stderror")
version = result.stderr.strip()
logger.debug(f"Called tool with {' '.join(command)}, got {version}")
is_broken = False
except subprocess.CalledProcessError as exception:
is_broken = True
logger.error(f"{tool_name} failed invocation with {exception}")
logger.error(f"{tool_name} stderr: {exception.stderr}")
logger.error(f"{tool_name} stdout: {exception.stdout}")
except FileNotFoundError:
logger.error(f"{tool_name} is not on path.")
return models.ToolAvailabilityResult(False, True, None, last_modified)
return models.ToolAvailabilityResult(True, is_broken, version, last_modified)
if __name__ == "__main__":
print(get_command_last_modified_date("asdfpipx"))
Functions
def check_tool_availability(tool_name: str, schema: SchemaType, version_switch: str = '--version') ‑> ToolAvailabilityResult-
Check if a tool is available in the system's PATH and if possible, determine a version number.
Args
tool_name:str- The name of the tool to check.
schema:models.SchemaType- The schema to use for the version.
version_switch:str- The switch to get the tool version. Defaults to '–version'.
Returns
models.ToolAvailabilityResult- An object containing the availability and version of the tool.
Expand source code
def check_tool_availability( tool_name: str, schema: models.SchemaType, version_switch: str = "--version", ) -> models.ToolAvailabilityResult: """ Check if a tool is available in the system's PATH and if possible, determine a version number. Args: tool_name (str): The name of the tool to check. schema (models.SchemaType): The schema to use for the version. version_switch (str): The switch to get the tool version. Defaults to '--version'. Returns: models.ToolAvailabilityResult: An object containing the availability and version of the tool. """ # Check if the tool is in the system's PATH is_broken = True last_modified = get_command_last_modified_date(tool_name) if not last_modified: logger.warning(f"{tool_name} is not on path.") return models.ToolAvailabilityResult(False, True, None, last_modified) if schema == models.SchemaType.EXISTENCE: logger.debug(f"{tool_name} exists, but not checking for version.") return models.ToolAvailabilityResult(True, False, None, last_modified) if version_switch is None or version_switch == "--version": # override default. # Could be a problem if KNOWN_SWITCHES was ever wrong. version_switch = KNOWN_SWITCHES.get(tool_name, "--version") version = None # pylint: disable=broad-exception-caught try: command = [tool_name, version_switch] timeout = int(os.environ.get("CLI_TOOL_AUDIT_TIMEOUT", 15)) result = subprocess.run( command, capture_output=True, text=True, timeout=timeout, shell=False, check=True ) # nosec # Sometimes version is on line 2 or later. version = result.stdout.strip() if not version: # check stderror logger.debug("Got nothing from stdout, checking stderror") version = result.stderr.strip() logger.debug(f"Called tool with {' '.join(command)}, got {version}") is_broken = False except subprocess.CalledProcessError as exception: is_broken = True logger.error(f"{tool_name} failed invocation with {exception}") logger.error(f"{tool_name} stderr: {exception.stderr}") logger.error(f"{tool_name} stdout: {exception.stdout}") except FileNotFoundError: logger.error(f"{tool_name} is not on path.") return models.ToolAvailabilityResult(False, True, None, last_modified) return models.ToolAvailabilityResult(True, is_broken, version, last_modified) def get_command_last_modified_date(tool_name: str) ‑> Optional[datetime.datetime]-
Get the last modified date of a command's executable.
Args
tool_name:str- The name of the command.
Returns
Optional[datetime.datetime]- The last modified date of the command's executable.
Expand source code
def get_command_last_modified_date(tool_name: str) -> Optional[datetime.datetime]: """ Get the last modified date of a command's executable. Args: tool_name (str): The name of the command. Returns: Optional[datetime.datetime]: The last modified date of the command's executable. """ # Find the path of the command's executable result = which(tool_name) if result is None: return None executable_path = result # Get the last modified time of the executable last_modified_timestamp = os.path.getmtime(executable_path) return datetime.datetime.fromtimestamp(last_modified_timestamp)