pingparsing/error.py
class ParseErrorReason(enum.Enum):
    HEADER_NOT_FOUND = "ping statistics not found"
    EMPTY_STATISTICS = "ping statistics is empty"
pingparsing/_common.py
def _to_unicode(text: Union[str, bytes]) -> str:
    try:
        return text.decode("ascii")  # type: ignore
    except AttributeError:
        return cast(str, text)
pingparsing/_pingtransmitter.py
class PingResult(namedtuple("PingResult", "stdout stderr returncode")):
    """
    Data class to store ``ping`` command execution result.

    .. py:attribute:: stdout
        :type: Optional[str]

        Standard output of ``ping`` command execution result.

    .. py:attribute:: stderr
        :type: Optional[str]

        Standard error of ``ping`` command execution result.

    .. py:attribute:: returncode
        :type: int

        Return code of ``ping`` command execution result.
    """
pingparsing/_parser.py
class PingParser(PingParserInterface):

    _BYTES_PATTERN = rf"\s*(?P<{IcmpReplyKey.BYTES}>[0-9]+) bytes"
    _DEST_PATTERN = r"(?P<{key}>[a-zA-Z0-9:\-\.\(\)% ]+)".format(
        key=IcmpReplyKey.DESTINATION
    )  # host or ipv4/ipv6 addr
    _IPADDR_PATTERN = r"(\d{1,3}\.){3}\d{1,3}"
    _ICMP_SEQ_PATTERN = rf"\s*icmp_seq=(?P<{IcmpReplyKey.SEQUENCE_NO}>\d+)"
    _TTL_PATTERN = rf"\s*ttl=(?P<{IcmpReplyKey.TTL}>\d+)"
    _TIME_PATTERN = rf"\s*time[=<](?P<{IcmpReplyKey.TIME}>[0-9\.]+)"

    def __init__(self, timezone: Optional[tzinfo] = None) -> None:
        self.__timezone = timezone

    @abc.abstractproperty
    def _parser_name(self) -> str:  # pragma: no cover
        pass

    @abc.abstractproperty
    def _icmp_reply_pattern(self) -> str:  # pragma: no cover
        pass

    @property
    def _icmp_no_ans_pattern(self) -> str:
        return "(?!x)x"  # never matching anything

    @property
    def _duplicate_packet_pattern(self) -> str:
        return r".+ \(DUP!\)$"

    @abc.abstractproperty
    def _stats_headline_pattern(self) -> str:  # pragma: no cover
        pass

    @abc.abstractproperty
    def _is_support_packet_duplicate(self) -> bool:  # pragma: no cover
        pass

    def _parse_icmp_reply(self, ping_lines: Sequence[str]) -> IcmpReplies:
        icmp_reply_regexp = re.compile(self._icmp_reply_pattern, re.IGNORECASE)
        icmp_no_ans_regexp = re.compile(self._icmp_no_ans_pattern, re.IGNORECASE)
        duplicate_packet_regexp = re.compile(self._duplicate_packet_pattern)
        icmp_reply_list = []

        for line in ping_lines:
            match = icmp_reply_regexp.search(line)
            if not match:
                match = icmp_no_ans_regexp.search(line)
            if not match:
                continue

            results = match.groupdict()
            reply: Dict[str, Union[str, bool, float, int, datetime]] = {}

            if IcmpReplyKey.DESTINATION in results:
                reply[IcmpReplyKey.DESTINATION] = results[IcmpReplyKey.DESTINATION]

            if IcmpReplyKey.BYTES in results:
                reply[IcmpReplyKey.BYTES] = int(results[IcmpReplyKey.BYTES])

            if results.get(IcmpReplyKey.TIMESTAMP):
                reply[IcmpReplyKey.TIMESTAMP] = self.__timestamp_to_datetime(
                    results[IcmpReplyKey.TIMESTAMP]
                )
            elif results.get(IcmpReplyKey.TIMESTAMP_NO_ANS):
                reply[IcmpReplyKey.TIMESTAMP] = self.__timestamp_to_datetime(
                    results[IcmpReplyKey.TIMESTAMP_NO_ANS]
                )

            if IcmpReplyKey.SEQUENCE_NO in results:
                reply[IcmpReplyKey.SEQUENCE_NO] = int(results[IcmpReplyKey.SEQUENCE_NO])

            if IcmpReplyKey.TTL in results:
                reply[IcmpReplyKey.TTL] = int(results[IcmpReplyKey.TTL])

            if IcmpReplyKey.TIME in results:
                reply[IcmpReplyKey.TIME] = float(results[IcmpReplyKey.TIME])

            if duplicate_packet_regexp.search(line):
                reply[IcmpReplyKey.DUPLICATE] = True
            else:
                reply[IcmpReplyKey.DUPLICATE] = False

            icmp_reply_list.append(reply)

        return icmp_reply_list

    def _preprocess_parse_stats(self, lines: Sequence[str]) -> Tuple[str, str, Sequence[str]]:
        logger.debug(f"parsing as {self._parser_name:s} ping result format")

        stats_headline_idx = self.__find_stats_headline_idx(
            lines, re.compile(self._stats_headline_pattern)
        )
        body_line_list = lines[stats_headline_idx + 1 :]
        self.__validate_stats_body(body_line_list)

        packet_info_line = body_line_list[0]

        return (lines[stats_headline_idx], packet_info_line, body_line_list)

    def _parse_destination(self, stats_headline: str) -> str:
        match = re.search(self._stats_headline_pattern, stats_headline)
        if not match:
            return "unknown"

        return match.groupdict()[IcmpReplyKey.DESTINATION].strip(":")

    def __find_stats_headline_idx(self, lines: Sequence[str], re_stats_header: Pattern) -> int:
        for i, line in enumerate(lines):
            if re_stats_header.search(line):
                break
        else:
            raise ParseError(reason=ParseErrorReason.HEADER_NOT_FOUND)

        return i

    def __timestamp_to_datetime(self, timestamp: str) -> datetime:
        return DateTime(timestamp.lstrip("[").rstrip("]"), timezone=self.__timezone).force_convert()

    def __validate_stats_body(self, body_line_list: Sequence[str]) -> None:
        if typepy.is_empty_sequence(body_line_list):
            raise ParseError(reason=ParseErrorReason.EMPTY_STATISTICS)

    def _parse_duplicate(self, line: str) -> Optional[int]:
        if not self._is_support_packet_duplicate:
            return None

        packet_pattern = (
            pp.SkipTo(pp.Word("+" + pp.nums) + pp.Literal("duplicates,"))
            + pp.Word("+" + pp.nums)
            + pp.Literal("duplicates,")
        )

        try:
            duplicate_parse_list = packet_pattern.parseString(_to_unicode(line))
        except pp.ParseException:
            return 0

        return int(duplicate_parse_list[-2].strip("+"))
pingparsing/_logger.py
MODULE_NAME = "pingparsing"
class NullLogger:
    def remove(self, handler_id=None):  # pragma: no cover
    def add(self, sink, **kwargs):  # pragma: no cover
    def disable(self, name):  # pragma: no cover
    def enable(self, name):  # pragma: no cover
    def critical(self, __message, *args, **kwargs):  # pragma: no cover
    def debug(self, __message, *args, **kwargs):  # pragma: no cover
    def error(self, __message, *args, **kwargs):  # pragma: no cover
    def exception(self, __message, *args, **kwargs):  # pragma: no cover
    def info(self, __message, *args, **kwargs):  # pragma: no cover
    def log(self, __level, __message, *args, **kwargs):  # pragma: no cover
    def success(self, __message, *args, **kwargs):  # pragma: no cover
    def trace(self, __message, *args, **kwargs):  # pragma: no cover
    def warning(self, __message, *args, **kwargs):  # pragma: no cover
def set_logger(is_enable: bool, propagation_depth: int = 1) -> None:
def set_log_level(log_level):
pingparsing/_parser.py
class MacOsPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "macOS"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            self._BYTES_PATTERN
            + r"\s+from "
            + self._DEST_PATTERN
            + ":"
            + self._ICMP_SEQ_PATTERN
            + self._TTL_PATTERN
            + self._TIME_PATTERN
        )

    @property
    def _stats_headline_pattern(self) -> str:
        return rf"--- {self._DEST_PATTERN} ping statistics ---"

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: Sequence[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("packets received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("round-trip min/avg/max/stddev =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            rtt_mdev=float(parse_list[7]),
            icmp_replies=icmp_replies,
        )
pingparsing/_parser.py
class AlpineLinuxPingParser(LinuxPingParser):
    @property
    def _parser_name(self) -> str:
        return "AlpineLinux"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            self._BYTES_PATTERN
            + r"\s+from "
            + self._DEST_PATTERN
            + ": "
            + rf"seq=(?P<{IcmpReplyKey.SEQUENCE_NO}>\d+) "
            + self._TTL_PATTERN
            + self._TIME_PATTERN
        )

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: Sequence[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("packets received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("round-trip min/avg/max =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            icmp_replies=icmp_replies,
        )

    def _parse_duplicate(self, line: str) -> int:
        packet_pattern = (
            pp.SkipTo(pp.Word(pp.nums) + pp.Literal("duplicates,"))
            + pp.Word(pp.nums)
            + pp.Literal("duplicates,")
        )
        try:
            duplicate_parse_list = packet_pattern.parseString(_to_unicode(line))
        except pp.ParseException:
            return 0

        return int(duplicate_parse_list[-2])
pingparsing/error.py
class ParseError(Exception):
    """
    Exception raised when failed to parse ping results.
    """

    @property
    def reason(self) -> Union[str]:
        return self.__reason

    def __init__(self, *args, **kwargs):
        self.__reason = kwargs.pop("reason", None)

        super().__init__(*args, **kwargs)
pingparsing/_parser.py
class LinuxPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "Linux"

    _TIMESTAMP_PATTERN = rf"(?P<{IcmpReplyKey.TIMESTAMP}>\[[0-9\.]+\])"
    _NO_ANS_TIMESTAMP_PATTERN = rf"(?P<{IcmpReplyKey.TIMESTAMP_NO_ANS}>\[[0-9\.]+\])"

    @property
    def _icmp_no_ans_pattern(self) -> str:
        return self._NO_ANS_TIMESTAMP_PATTERN + " no answer yet for " + self._ICMP_SEQ_PATTERN

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            self._TIMESTAMP_PATTERN
            + "?"
            + self._BYTES_PATTERN
            + r"\s+from "
            + self._DEST_PATTERN
            + ":"
            + self._ICMP_SEQ_PATTERN
            + self._TTL_PATTERN
            + self._TIME_PATTERN
        )

    @property
    def _stats_headline_pattern(self) -> str:
        return rf"--- {self._DEST_PATTERN} ping statistics ---"

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return True

    def parse(self, ping_message: Sequence[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Word(pp.nums)
            + pp.Literal("packets transmitted,")
            + pp.Word(pp.nums)
            + pp.Literal("received,")
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[0])
        packet_receive = int(parse_list[2])

        is_valid_data = True
        try:
            rtt_line = body_line_list[1]
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("rtt min/avg/max/mdev =")
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + "/"
            + pp.Word(pp.nums + ".")
            + pp.Word(pp.nums + "ms")
        )
        try:
            parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))
        except pp.ParseException:
            if not re.search(r"\s*pipe \d+", rtt_line):
                raise ValueError

            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[3]),
            rtt_max=float(parse_list[5]),
            rtt_mdev=float(parse_list[7]),
            icmp_replies=icmp_replies,
        )
pingparsing/_parser.py
class WindowsPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "Windows"

    @property
    def _icmp_reply_pattern(self) -> str:
        return (
            " from "
            + self._DEST_PATTERN
            + rf":\s*bytes=(?P<{IcmpReplyKey.BYTES}>[0-9]+)"
            + self._TIME_PATTERN
            + "ms"
            + self._TTL_PATTERN
        )

    @property
    def _stats_headline_pattern(self) -> str:
        return rf"^Ping statistics for {self._DEST_PATTERN}"

    @property
    def _is_support_packet_duplicate(self) -> bool:
        return False

    def parse(self, ping_message: Sequence[str]) -> PingStats:
        icmp_replies = self._parse_icmp_reply(ping_message)
        stats_headline, packet_info_line, body_line_list = self._preprocess_parse_stats(
            lines=ping_message
        )
        packet_pattern = (
            pp.Literal("Packets: Sent = ")
            + pp.Word(pp.nums)
            + pp.Literal(", Received = ")
            + pp.Word(pp.nums)
        )

        destination = self._parse_destination(stats_headline)
        duplicates = self._parse_duplicate(packet_info_line)

        parse_list = packet_pattern.parseString(_to_unicode(packet_info_line))
        packet_transmit = int(parse_list[1])
        packet_receive = int(parse_list[3])

        is_valid_data = True
        try:
            rtt_line = body_line_list[2].strip()
        except IndexError:
            is_valid_data = False

        if not is_valid_data or typepy.is_null_string(rtt_line):
            return PingStats(
                destination=destination,
                packet_transmit=packet_transmit,
                packet_receive=packet_receive,
                duplicates=duplicates,
                icmp_replies=icmp_replies,
            )

        rtt_pattern = (
            pp.Literal("Minimum = ")
            + pp.Word(pp.nums)
            + pp.Literal("ms, Maximum = ")
            + pp.Word(pp.nums)
            + pp.Literal("ms, Average = ")
            + pp.Word(pp.nums)
        )
        parse_list = rtt_pattern.parseString(_to_unicode(rtt_line))

        return PingStats(
            destination=destination,
            packet_transmit=packet_transmit,
            packet_receive=packet_receive,
            duplicates=duplicates,
            rtt_min=float(parse_list[1]),
            rtt_avg=float(parse_list[5]),
            rtt_max=float(parse_list[3]),
            icmp_replies=icmp_replies,
        )
pingparsing/_stats.py
class PingStats:
    def __init__(self, *args, **kwargs) -> None:
        self.__destination = kwargs.pop("destination", None)
        self.__packet_transmit = kwargs.pop("packet_transmit", None)
        self.__packet_receive = kwargs.pop("packet_receive", None)
        self.__rtt_min = kwargs.pop("rtt_min", None)
        self.__rtt_avg = kwargs.pop("rtt_avg", None)
        self.__rtt_max = kwargs.pop("rtt_max", None)
        self.__rtt_mdev = kwargs.pop("rtt_mdev", None)
        self.__duplicates = kwargs.pop("duplicates", None)

        self.__icmp_replies = kwargs.pop("icmp_replies", [])

    @property
    def destination(self) -> str:
        """
        The ping destination.

        Returns:
            |str|:
        """

        return self.__destination

    @property
    def packet_transmit(self) -> Optional[int]:
        """
        Number of packets transmitted.

        Returns:
            |int|:
        """

        return self.__packet_transmit

    @property
    def packet_receive(self) -> Optional[int]:
        """
        Number of packets received.

        Returns:
            |int|:
        """

        return self.__packet_receive

    @property
    def packet_loss_count(self) -> Optional[int]:
        """
        Number of packet losses.

        Returns:
            |int|: |None| if the value is not a number.
        """

        try:
            return cast(int, self.packet_transmit) - cast(int, self.packet_receive)
        except TypeError:
            return None

    @property
    def packet_loss_rate(self) -> Optional[float]:
        """
        Percentage of packet loss |percent_unit|.

        Returns:
            |float|: |None| if the value is not a number.
        """

        try:
            return (cast(int, self.packet_loss_count) / cast(int, self.packet_transmit)) * 100
        except (TypeError, ZeroDivisionError, OverflowError):
            return None

    @property
    def rtt_min(self) -> Optional[float]:
        """
        Minimum round trip time of transmitted ICMP packets |msec_unit|.

        Returns:
            |float|:
        """

        return self.__rtt_min

    @property
    def rtt_avg(self) -> Optional[float]:
        """
        Average round trip time of transmitted ICMP packets |msec_unit|.

        Returns:
            |float|:
        """

        return self.__rtt_avg

    @property
    def rtt_max(self) -> Optional[float]:
        """
        Maximum round trip time of transmitted ICMP packets |msec_unit|.

        Returns:
            |float|:
        """

        return self.__rtt_max

    @property
    def rtt_mdev(self) -> Optional[float]:
        """
        Standard deviation of transmitted ICMP packets.

        Returns:
            |float|: |None| when parsing Windows ping result.
        """

        return self.__rtt_mdev

    @property
    def packet_duplicate_count(self) -> Optional[int]:
        """
        Number of duplicated packets.

        Returns:
            |int|: |None| when parsing Windows ping result.
        """

        return self.__duplicates

    @property
    def packet_duplicate_rate(self) -> Optional[float]:
        """
        Percentage of duplicated packets |percent_unit|.

        Returns:
            |float|: |None| if the value is not a number.
        """

        try:
            return (cast(int, self.packet_duplicate_count) / cast(int, self.packet_receive)) * 100
        except (TypeError, ZeroDivisionError, OverflowError):
            return None

    @property
    def icmp_replies(self) -> IcmpReplies:
        """
        ICMP packet reply information.

            .. note:
                ``time<1ms`` considered as ``time=1``

        Returns:
            |list| of |dict|:
        """

        return self.__icmp_replies

    def is_empty(self):
        return all(
            [
                self.destination is None,
                self.packet_transmit is None,
                self.packet_receive is None,
                self.packet_loss_count is None,
                self.packet_loss_rate is None,
                self.packet_duplicate_count is None,
                self.packet_duplicate_rate is None,
                self.rtt_min is None,
                self.rtt_avg is None,
                self.rtt_max is None,
                self.rtt_mdev is None,
                not self.icmp_replies,
            ]
        )

    def as_dict(
        self, include_icmp_replies: bool = False
    ) -> Dict[str, Union[str, int, float, IcmpReplies, None]]:
        """
        ping statistics.

        Returns:
            |dict|:

        Examples:
            >>> import pingparsing
            >>> parser = pingparsing.PingParsing()
            >>> parser.parse(ping_result)
            >>> parser.as_dict()
            {
                "destination": "google.com",
                "packet_transmit": 60,
                "packet_receive": 60,
                "packet_loss_rate": 0.0,
                "packet_loss_count": 0,
                "rtt_min": 61.425,
                "rtt_avg": 99.731,
                "rtt_max": 212.597,
                "rtt_mdev": 27.566,
                "packet_duplicate_rate": 0.0,
                "packet_duplicate_count": 0
            }
        """

        d: Dict[str, Union[str, int, float, IcmpReplies, None]] = {
            "destination": self.destination,
            "packet_transmit": self.packet_transmit,
            "packet_receive": self.packet_receive,
            "packet_loss_count": self.packet_loss_count,
            "packet_loss_rate": self.packet_loss_rate,
            "rtt_min": self.rtt_min,
            "rtt_avg": self.rtt_avg,
            "rtt_max": self.rtt_max,
            "rtt_mdev": self.rtt_mdev,
            "packet_duplicate_count": self.packet_duplicate_count,
            "packet_duplicate_rate": self.packet_duplicate_rate,
        }
        if include_icmp_replies:
            d["icmp_replies"] = self.icmp_replies

        return d

    def as_tuple(self) -> Tuple:
        """
        ping statistics.

        Returns:
            |namedtuple|:

        Examples:
            >>> import pingparsing
            >>> parser = pingparsing.PingParsing()
            >>> parser.parse(ping_result)
            >>> parser.as_tuple()
            PingResult(destination='google.com', packet_transmit=60, packet_receive=60, packet_loss_rate=0.0, packet_loss_count=0, rtt_min=61.425, rtt_avg=99.731, rtt_max=212.597, rtt_mdev=27.566, packet_duplicate_rate=0.0, packet_duplicate_count=0)
        """  # noqa

        from collections import namedtuple

        ping_result = self.as_dict()

        return namedtuple("PingStatsTuple", ping_result.keys())(**ping_result)  # type: ignore
pingparsing/_parser.py
class NullPingParser(PingParser):
    @property
    def _parser_name(self) -> str:
        return "null"

    @property
    def _icmp_reply_pattern(self) -> str:
        return ""

    @property
    def _stats_headline_pattern(self) -> str:
        return ""

    @property
    def _is_support_packet_duplicate(self) -> bool:  # pragma: no cover
        return False

    def parse(self, ping_message: Sequence[str]) -> PingStats:  # pragma: no cover
        return PingStats()

    def _preprocess_parse_stats(
        self, lines: Sequence[str]
    ) -> Tuple[str, str, List[str]]:  # pragma: no cover
        return ("", "", [])


Question: from datetime import tzinfo
from typing import Optional, Union
from ._common import _to_unicode
from ._logger import logger
from ._parser import PingParser  # noqa
from ._parser import (
    AlpineLinuxPingParser,
    LinuxPingParser,
    MacOsPingParser,
    NullPingParser,
    WindowsPingParser,
)
from ._pingtransmitter import PingResult
from ._stats import PingStats
from .error import ParseError, ParseErrorReason
import pyparsing as pp
import typepy
"""
.. codeauthor:: Tsuyoshi Hombashi <tsuyoshi.hombashi@gmail.com>
"""





class PingParsing:
    """
    Parser class to parsing ping command output.

    Args:
        timezone (Optional[tzinfo]):
            Time zone for parsing timestamps.
    """

    def __init__(self, timezone: Optional[tzinfo] = None) -> None:
        self.__parser: PingParser = NullPingParser()
        self.__timezone = timezone

    @property
    def parser_name(self) -> str:
        return self.__parser._parser_name

    def parse(self, ping_message: Union[str, PingResult]) -> PingStats:
        """
        Parse ping command output.

        Args:
            ping_message (str or :py:class:`~pingparsing.PingResult`):
                ``ping`` command output.

        Returns:
            :py:class:`~pingparsing.PingStats`: Parsed result.
        """

        if isinstance(ping_message, PingResult):
            # accept PingResult instance as an input
            if typepy.is_not_null_string(ping_message.stdout):
                ping_text = ping_message.stdout
            else:
                ping_text = ""
        else:
            ping_text = ping_message

        logger.debug(f"parsing ping result: {ping_text}")

        self.__parser = NullPingParser()

        if typepy.is_null_string(ping_text):
            logger.debug("ping_message is empty")

            return PingStats()

        ping_lines = _to_unicode(ping_text).splitlines()
        parser_class_list = (
            LinuxPingParser,
            WindowsPingParser,

Answer:             MacOsPingParser,