Find all the (available) logs that are after the given time stamp.

        If `s` is not supplied, then all lines are used.  Otherwise, only the
        lines contain the `s` are used.  `s` can be either a single string or a
        string list. For list, all keywords in the list must be found in each
        line.

        This method then finds all lines which have a time stamp after the
        given `timestamp`.  Lines that do not contain a time stamp
        are considered to be part of the previous line and are therefore
        included if the last log line was included or excluded otherwise.

        Time stamps are recognised by converting the time format into a
        regular expression which matches the time format in the string.  This
        is then searched for in each line in turn.  Only lines with a time
        stamp matching this expression will trigger the decision to include
        or exclude lines. Therefore, if the log for some reason does not
        contain a time stamp that matches this format, no lines will be
        returned.

        The time format is given in ``strptime()`` format, in the object's
        ``time_format`` property.  Users of the object should **not** change
        this property; instead, the parser should subclass
        :class:`LogFileOutput` and change the ``time_format`` property.

        Some logs, regrettably, change time stamps formats across different
        lines, or change time stamp formats in different versions of the
        program.  In order to accommodate this, the timestamp format can be a
        list of ``strptime()`` format strings.  These are combined as
        alternatives in the regular expression, and are given to ``strptime``
        in order.  These can also be listed as the values of a dict, e.g.::

            {'pre_10.1.5': '%y%m%d %H:%M:%S', 'post_10.1.5': '%Y-%m-%d %H:%M:%S'}

        .. note::
            Some logs - notably /var/log/messages - do not contain a year
            in the timestamp.  This detected by the absence of a '%y' or '%Y' in
            the time format.  If that year field is absent, the year is assumed
            to be the year in the given timestamp being sought.  Some attempt is
            made to handle logs with a rollover from December to January, by
            finding when the log's timestamp (with current year assumed) is over
            eleven months (specifically, 330 days) ahead of or behind the
            timestamp date and shifting that log date by 365 days so that it is
            more likely to be in the sought range.  This paragraph is sponsored
            by syslog.

        Parameters:
            timestamp(datetime.datetime): lines before this time are ignored.
            s(str or list): one or more strings to search for.
                If not supplied, all available lines are searched.

        Yields:
            dict:
                The parsed lines with timestamps after this date in
                the same format they were supplied.  It at least contains the
                ``raw_message`` as a key.

        Raises:
            ParseException: If the format conversion string contains a
                format that we don't recognise.  In particular, no attempt is
                made to recognise or parse the time zone or other obscure
                values like day of year or week of year.

def get_after(self, timestamp, s=None):
        """
        Find all the (available) logs that are after the given time stamp.

        If `s` is not supplied, then all lines are used.  Otherwise, only the
        lines contain the `s` are used.  `s` can be either a single string or a
        string list. For list, all keywords in the list must be found in each
        line.

        This method then finds all lines which have a time stamp after the
        given `timestamp`.  Lines that do not contain a time stamp
        are considered to be part of the previous line and are therefore
        included if the last log line was included or excluded otherwise.

        Time stamps are recognised by converting the time format into a
        regular expression which matches the time format in the string.  This
        is then searched for in each line in turn.  Only lines with a time
        stamp matching this expression will trigger the decision to include
        or exclude lines. Therefore, if the log for some reason does not
        contain a time stamp that matches this format, no lines will be
        returned.

        The time format is given in ``strptime()`` format, in the object's
        ``time_format`` property.  Users of the object should **not** change
        this property; instead, the parser should subclass
        :class:`LogFileOutput` and change the ``time_format`` property.

        Some logs, regrettably, change time stamps formats across different
        lines, or change time stamp formats in different versions of the
        program.  In order to accommodate this, the timestamp format can be a
        list of ``strptime()`` format strings.  These are combined as
        alternatives in the regular expression, and are given to ``strptime``
        in order.  These can also be listed as the values of a dict, e.g.::

            {'pre_10.1.5': '%y%m%d %H:%M:%S', 'post_10.1.5': '%Y-%m-%d %H:%M:%S'}

        .. note::
            Some logs - notably /var/log/messages - do not contain a year
            in the timestamp.  This detected by the absence of a '%y' or '%Y' in
            the time format.  If that year field is absent, the year is assumed
            to be the year in the given timestamp being sought.  Some attempt is
            made to handle logs with a rollover from December to January, by
            finding when the log's timestamp (with current year assumed) is over
            eleven months (specifically, 330 days) ahead of or behind the
            timestamp date and shifting that log date by 365 days so that it is
            more likely to be in the sought range.  This paragraph is sponsored
            by syslog.

        Parameters:
            timestamp(datetime.datetime): lines before this time are ignored.
            s(str or list): one or more strings to search for.
                If not supplied, all available lines are searched.

        Yields:
            dict:
                The parsed lines with timestamps after this date in
                the same format they were supplied.  It at least contains the
                ``raw_message`` as a key.

        Raises:
            ParseException: If the format conversion string contains a
                format that we don't recognise.  In particular, no attempt is
                made to recognise or parse the time zone or other obscure
                values like day of year or week of year.
        """
        time_format = self.time_format

        # Annoyingly, strptime insists that it get the whole time string and
        # nothing but the time string.  However, for most logs we only have a
        # string with the timestamp in it.  We can't just catch the ValueError
        # because at that point we do not actually have a valid datetime
        # object.  So we convert the time format string to a regex, use that
        # to find just the timestamp, and then use strptime on that.  Thanks,
        # Python.  All these need to cope with different languages and
        # character sets.  Note that we don't include time zone or other
        # outputs (e.g. day-of-year) that don't usually occur in time stamps.
        format_conversion_for = {
            'a': r'\w{3}', 'A': r'\w+',  # Week day name
            'w': r'[0123456]',  # Week day number
            'd': r'([0 ][123456789]|[12]\d|3[01])',  # Day of month
            'b': r'\w{3}', 'B': r'\w+',  # Month name
            'm': r'([0 ]\d|1[012])',  # Month number
            'y': r'\d{2}', 'Y': r'\d{4}',  # Year
            'H': r'([01 ]\d|2[0123])',  # Hour - 24 hour format
            'I': r'([0 ]?\d|1[012])',  # Hour - 12 hour format
            'p': r'\w{2}',  # AM / PM
            'M': r'([012345]\d)',  # Minutes
            'S': r'([012345]\d|60)',  # Seconds, including leap second
            'f': r'\d{6}',  # Microseconds
        }

        # Construct the regex from the time string
        timefmt_re = re.compile(r'%(\w)')

        def replacer(match):
            if match.group(1) in format_conversion_for:
                return format_conversion_for[match.group(1)]
            else:
                raise ParseException(
                    "get_after does not understand strptime format '{c}'".format(
                        c=match.group(0)
                    )
                )

        # Please do not attempt to be tricky and put a regular expression
        # inside your time format, as we are going to also use it in
        # strptime too and that may not work out so well.

        # Check time_format - must be string or list.  Set the 'logs_have_year'
        # flag and timestamp parser function appropriately.
        # Grab values of dict as a list first
        if isinstance(time_format, dict):
            time_format = list(time_format.values())
        if isinstance(time_format, six.string_types):
            logs_have_year = ('%Y' in time_format or '%y' in time_format)
            time_re = re.compile('(' + timefmt_re.sub(replacer, time_format) + ')')

            # Curry strptime with time_format string.
            def test_parser(logstamp):
                return datetime.datetime.strptime(logstamp, time_format)
            parse_fn = test_parser
        elif isinstance(time_format, list):
            logs_have_year = all('%Y' in tf or '%y' in tf for tf in time_format)
            time_re = re.compile('(' + '|'.join(
                timefmt_re.sub(replacer, tf) for tf in time_format
            ) + ')')

            def test_all_parsers(logstamp):
                # One of these must match, because the regex has selected only
                # strings that will match.
                for tf in time_format:
                    try:
                        ts = datetime.datetime.strptime(logstamp, tf)
                    except ValueError:
                        pass
                return ts
            parse_fn = test_all_parsers
        else:
            raise ParseException(
                "get_after does not recognise time formats of type {t}".format(
                    t=type(time_format)
                )
            )

        # Most logs will appear in string format, but some logs (e.g.
        # Messages) are available in list-of-dicts format.  So we choose one
        # of two 'date_compare' functions.  HOWEVER: we still have to check
        # the string found for a valid date, because log parsing often fails.
        # Because of generators, we check this per line

        # Now try to find the time stamp in each log line and add lines to
        # our output if they are currently being included in the log.

        eleven_months = datetime.timedelta(days=330)
        including_lines = False
        search_by_expression = self._valid_search(s)
        for line in self.lines:
            # If `s` is not None, keywords must be found in the line
            if s and not search_by_expression(line):
                continue
            # Otherwise, search all lines
            match = time_re.search(line)
            if match:
                logstamp = parse_fn(match.group(0))
                if not logs_have_year:
                    # Substitute timestamp year for logstamp year
                    logstamp = logstamp.replace(year=timestamp.year)
                    if logstamp - timestamp > eleven_months:
                        # If timestamp in January and log in December, move
                        # log to previous year
                        logstamp = logstamp.replace(year=timestamp.year - 1)
                    elif timestamp - logstamp > eleven_months:
                        # If timestamp in December and log in January, move
                        # log to next year
                        logstamp = logstamp.replace(year=timestamp.year + 1)
                if logstamp >= timestamp:
                    # Later - include
                    including_lines = True
                    yield self._parse_line(line)
                else:
                    # Earlier - start excluding
                    including_lines = False
            else:
                # If we're including lines, add this continuation line
                if including_lines:
                    yield self._parse_line(line)