<commit_before># -*- coding: utf-8 -*-
# (c) The University of Strathclyde 2021–Present
# Author: <NAME>
#
# Contact: <EMAIL>
#
# <NAME>,
# Strathclyde Institute for Pharmacy and Biomedical Sciences,
# Cathedral Street,
# Glasgow,
# G4 0RE,
# Scotland,
# UK
#
# The MIT License
#
# Copyright (c) 2021–Present University of Strathclyde
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""Code to implement the fastANI average nucleotide identity method."""

import logging
import platform
import os
import re
import shutil
import subprocess

from logging import Logger
from pathlib import Path
from typing import NamedTuple, Callable, Dict, List, Optional, Tuple

# import pandas as pd

from Bio import SeqIO

from . import pyani_config
from . import pyani_files
from . import pyani_jobs
from . import PyaniException
from .pyani_tools import ANIResults, BLASTcmds, BLASTexes, BLASTfunctions


class PyaniFastANIException(PyaniException):

    """Exception raised when there is a problem with fastANI"""


class ComparisonResult(NamedTuple):
    reference: Path
    query: Path
    ani: float
    matches: int
    fragments: int


def get_version(fastani_exe: Path = pyani_config.FASTANI_DEFAULT) -> str:
    """Return FastANI package version as a string.

    :param fastani_exe: path to FastANI executable

    We expect fastANI to return a string on STDOUT as

    .. code-block:: bash

        $ ./fastANI -v
        version 1.32

    we concatenate this with the OS name.

    The following circumstances are explicitly reported as strings:

    - no executable at passed path
    - non-executable file at passed path (this includes cases where the user doesn't have execute permissions on the file)
    - no version info returned
    """
    try:
        fastani_path = Path(shutil.which(fastani_exe))  # type:ignore
    except TypeError:
        return f"{fastani_exe} is not found in $PATH"

    if fastani_path is None:
        return f"{fastani_exe} is not found in $PATH"

    if not fastani_path.is_file():  # no executable
        return f"No fastANI executable at {fastani_path}"

    # This should catch cases when the file can't be executed by the user
    if not os.access(fastani_path, os.X_OK):  # file exists but not executable
        return f"fastANI exists at {fastani_path} but not executable"

    cmdline = [fastani_exe, "-v"]  # type: List
    result = subprocess.run(
        cmdline,
        shell=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    )  # type CompletedProcess

    match = re.search(r"(?<=version\s)[0-9\.]*", str(result.stderr, "utf-8"))
    version = match.group()  # type: ignore

    if 0 == len(version.strip()):
        return f"fastANI exists at {fastani_path} but could not retrieve version"

    return f"{platform.system()}_{version} ({fastani_path})"


# Generate list of Job objects, one per fastANI run -> this is maybe not necessary
def generate_fastani_jobs(
    filenames: List[Path],
    outdir: Path = Path("."),
    fastani_exe: Path = pyani_config.FASTANI_DEFAULT,
    fragLen: int = 3000,
    kmerSize: int = 16,
    minFraction: float = 0.2,
    jobprefix: str = "fastANI",
):  # should this have a hint for return type?<commit_msg>Pass both stdout and stderr to the regex version search<commit_after># -*- coding: utf-8 -*-
# (c) The University of Strathclyde 2021–Present
# Author: <NAME>
#
# Contact: <EMAIL>
#
# <NAME>,
# Strathclyde Institute for Pharmacy and Biomedical Sciences,
# Cathedral Street,
# Glasgow,
# G4 0RE,
# Scotland,
# UK
#
# The MIT License
#
# Copyright (c) 2021–Present University of Strathclyde
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
"""Code to implement the fastANI average nucleotide identity method."""

import logging
import platform
import os
import re
import shutil
import subprocess

from logging import Logger
from pathlib import Path
from typing import NamedTuple, Callable, Dict, List, Optional, Tuple

# import pandas as pd

from Bio import SeqIO

from . import pyani_config
from . import pyani_files
from . import pyani_jobs
from . import PyaniException
from .pyani_tools import ANIResults, BLASTcmds, BLASTexes, BLASTfunctions


class PyaniFastANIException(PyaniException):

    """Exception raised when there is a problem with fastANI"""


class ComparisonResult(NamedTuple):
    reference: Path
    query: Path
    ani: float
    matches: int
    fragments: int


def get_version(fastani_exe: Path = pyani_config.FASTANI_DEFAULT) -> str:
    """Return FastANI package version as a string.

    :param fastani_exe: path to FastANI executable

    We expect fastANI to return a string on STDOUT as

    .. code-block:: bash

        $ ./fastANI -v
        version 1.32

    we concatenate this with the OS name.

    The following circumstances are explicitly reported as strings:

    - no executable at passed path
    - non-executable file at passed path (this includes cases where the user doesn't have execute permissions on the file)
    - no version info returned
    """
    try:
        fastani_path = Path(shutil.which(fastani_exe))  # type:ignore
    except TypeError:
        return f"{fastani_exe} is not found in $PATH"

    if fastani_path is None:
        return f"{fastani_exe} is not found in $PATH"

    if not fastani_path.is_file():  # no executable
        return f"No fastANI executable at {fastani_path}"

    # This should catch cases when the file can't be executed by the user
    if not os.access(fastani_path, os.X_OK):  # file exists but not executable
        return f"fastANI exists at {fastani_path} but not executable"

    cmdline = [fastani_exe, "-v"]  # type: List
    result = subprocess.run(
        cmdline,
        shell=False,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        check=True,
    )  # type CompletedProcess

    match = re.search(
        r"(?<=version\s)[0-9\.]*", str(result.stderr + result.stdout, "utf-8")
    )
    version = match.group()  # type: ignore

    if 0 == len(version.strip()):
        return f"fastANI exists at {fastani_path} but could not retrieve version"

    return f"{platform.system()}_{version} ({fastani_path})"


# Generate list of Job objects, one per fastANI run -> this is maybe not necessary
def generate_fastani_jobs(
    filenames: List[Path],
    outdir: Path = Path("."),
    fastani_exe: Path = pyani_config.FASTANI_DEFAULT,
    fragLen: int = 3000,
    kmerSize: int = 16,
    minFraction: float = 0.2,
    jobprefix: str = "fastANI",
):  # should this have a hint for return type?
{'fraction_non_alphanumeric': 0.07911318553092182, 'fraction_numerical': 0.006067677946324387, 'mean_word_length': 3.6055883933369155, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 13, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3778175', 'n_tokens_mistral': 2593, 'n_tokens_neox': 2392, 'n_words': 1030}