text: # -*- coding: utf-8 -*-
  """Object to read lines from a stream using an arbitrary delimiter"""
  import math
  # from itertools import filterfalse
  # from unicodedata import combining, normalize
  
  
  __all__ = ['ReadLines']
  
  
  # the default amount of data to read on a buffer full
  DEFAULT_BLOCK_SIZE = 1048756
  
  
  # # the size to read when scanning for a non-combining character
  # SCAN_SIZE = 512
  
  
  # class _Reader:                # pylint: disable=too-few-public-methods
  #     """Stream reader that accounts for combining characters"""
  #     def read(self, size):
  #         """Read data
  
  #         Arguments
  #         ---------
  #         size : integer
  #                The amount of data to read
  
  #         Returns
  #         -------
  #         The data read from the stream
  
  #         As with any stream, the length of the return may be less than the
  #         amount requested with a blank returned for EOF.
  
  #         Due to the nature and handling of combining characters, when a
  #         normalization form is specified, the return for reads from a text-mode
  #         stream may have more or less data than requested. Depending on the
  #         disposition of the stream, it is possible that the results could
  #         contain the entire contents of the stream regardless of how much data
  #         was requested.
  #         """
  #         form = self._form
  #         if form is None:
  #             # no normalization form means a direct read
  #             return self._fobj.read(size)
  
  #         fobj_read = self._fobj.read
  
  #         prev_extra = self._prev_extra
  #         if prev_extra:
  #             # include the previous extra data with the buffer
  #             buf = prev_extra
  #             buf_length = len(buf)
  #             if buf_length < size:
  #                 # only execute a read if the previous extra data cannot
  #                 # satisify the request
  #                 buf += fobj_read(size - buf_length)
  #         else:
  #             buf = fobj_read(size)
  
  #             if isinstance(buf, bytes):
  #                 if form is not None:
  #                     raise ValueError('normalization not supported on binary '
  #                                      'streams')
  
  #         scan_size = SCAN_SIZE
  
  #         # retrieve extra data to ensure the boundary does not split combined
  #         # characters
  #         extra = fobj_read(scan_size)
  
  #         while extra:
  
  #             # find the first occurrence of a non-combing character in the extra
  #             # data
  #             result = next(filterfalse(lambda x: combining(x[1]),
  #                                       enumerate(extra)), None)
  
  #             if result is not None:
  #                 # a non-combining character was found in the extra data
  #                 idx = result[0]
  
  #                 # add all of the extra data upto the non-combining character
  #                 # to the buffer
  #                 buf += extra[:idx]
  
  #                 # store all of the extra data from the non-combining character
  #                 # on so it will be added to the buffer on the next read
  #                 self._prev_extra = extra[idx:]
  
  #                 break
  
  #             # if there is no occurrence of a non-combining character in the
  #             # extra data then add the extra data to the buffer and try again
  #             buf += extra
  #             extra = fobj_read(scan_size)
  
  #         else:
  #             self._prev_extra = ''       # no extra data was read or it was
  #                                         # already added to the buffer
  
  #         return normalize(form, buf)
  
  #     def __init__(self, fobj, form=None):
  #         """
  #         Arguments
  #         ---------
  #         fobj      : stream
  #                     The stream from which to read
  #         form      : string
  #                     The normalization form to use
  
  #         The stream must be opened for reading and must be in blocking mode.
  
  #         If form is specified then the returned data is normalized with that
  #         form.
  #         """
  #         if form not in {'NFC', 'NFKC', 'NFD', 'NFKD', None}:
  #             raise ValueError('invalid normalization form')
  #         self._fobj = fobj
  #         self._form = form
  #         self._prev_extra = ''
  
  
  class StreamExhausted(Exception): # pylint: disable=too-few-public-methods
      """Exception indicating the stream has been exhausted of data"""
      pass
  
  
  class ReadLines:        # pylint: disable=too-many-instance-attributes
      """Iterator to read lines from a stream using an arbitrary delimiter"""
      def peek(self, size=None):
          """Peek into the stream/buffer without advancing the current read
          state
  
          Arguments
          ---------
          size : integer
                 The amount of data to read
  
          Returns
          -------
          If size is specified then the returned amount is the same as if the
          stream were being peek()'ed directly, i.e. the amount will include upto
          the amount requested depending on how much data there is.
          If size is omitted or None, the forth-coming delimited line will be
          returned.
          """
          if size is None:                # request to peek at a line
              try:
                  return self._get_line(consume=False)
              except StreamExhausted:
                  return ''
  
          if size < 0:
              raise ValueError('invalid size: {}'.format(size))
  
          if size == 0:
              return ''
  
          # truncate the buffer
          buf, eof = self._buf[self._idx:], self._eof
  
          fobj_read = self._fobj.read
          block_size = self._block_size
  
          # determine if more data is needed to satisfy the request
          extra_needed = size - len(buf)
  
          # while the steam has not been exhausted and more data is needed...
          while not eof and extra_needed:
  
              # determine how much data to read(in multiples of the block
              # size) in order to satisfy the request
              to_read = math.ceil(extra_needed / block_size) * block_size
  
              tmp_buf = fobj_read(to_read)
              if tmp_buf:
                  # more data has been received so it is added to the buffer
                  buf += tmp_buf
  
                  # determine if the read has satisfied the request
                  extra_needed = size - len(buf)
  
              else:
                  self._eof = eof = True # no data has been received so EOF
  
          # buffer was truncated
          self._buf, self._idx = buf, 0
          return buf[:size]
  
      def __iter__(self):
          return self
  
      def __next__(self):
          try:
              return self._get_line(consume=True)
          except StreamExhausted:
              raise StopIteration
  
      def _get_line(self, consume):
          """Get the next/cached line
  
          Arguments
          ---------
          consume : boolean
                    Indicator on whether the line is to be consumed
  
          Returns
          -------
          The next line in the buffer/stream if no line has been cached or the
          cached line from a previous call
  
          This call will raise a StreamExhausted exception if there are no cached
          lines available and there are no more lines to be read.
          """
          line = self._line
          if line is not None:
              # a cached line is available
              delimiter_pos = self._delimiter_pos
  
              if consume:
                  # if consume is True then ensure that the next call will get
                  # the next line
                  self._line, self._delimiter_pos = None, None
  
              if self.strip_delimiter:
                  return line[:delimiter_pos]
              return line
  
          # get the next line from the buffer/stream
          line, delimiter_pos = self._get_next_line()
  
          if consume:
              # if consume is True then this line will not be cached
              self._line, self._delimiter_pos = None, None
          else:
              # cache the line
              self._line, self._delimiter_pos = line, delimiter_pos
  
          if self.strip_delimiter:
              return line[:delimiter_pos]
          return line
  
      def _get_next_line(self):           # pylint: disable=too-many-branches
          """Get the next line
  
          Returns
          -------
          A two-tuple of the next line in the stream and the index of where,
          within the line, the delimiter starts if it is present or the length of
          the line if it does not.
  
          This call will raise a StreamExhausted exception if there are no more
          lines to be read.
          """
          fobj_read = self._fobj.read
          block_size = self._block_size
          delimiter = self.delimiter
          buf, idx, eof = self._buf, self._idx, self._eof
  
          # searching starts at the idx
          search_idx = idx
  
          while True:
  
              # The delimiter is either str/bytes or a regex-like object
              if isinstance(delimiter, (str, bytes)):
                  delimiter_start = buf.find(delimiter, search_idx)
  
                  if delimiter_start != -1:
                      # the length of the delimiter is added to where the
                      # delimiter starts to get the index of where it ends and
                      # the index attribute is set to indicate where in the
                      # buffer the next line begins
                      self._idx = end = delimiter_start + len(delimiter)
                      return buf[idx:end], delimiter_start - idx
  
                  # a match was not found but if the delimiter is more than one
                  # character then the delimiter could have been split so an
                  # offset is provided to start the search within the existing
                  # buffer
                  search_offset = len(delimiter) - 1
  
              else:
                  result = delimiter.search(buf, # pylint: disable=no-member
                                            search_idx)
                  if result:
                      delimiter_start = result.start()
                      end = result.end()
  
                      if end != result.endpos:
                          # if the match is not at the end of the buffer then it
                          # is an exact match regardless of whether the regex is
                          # greedy
  
                          # the index attribute is set to indicate where in the
                          # buffer the next line begins
                          self._idx = end
                          return buf[idx:end], delimiter_start - idx
  
                      # if the match is at the end of the buffer then reading
                      # more could result in a better match if the regex is
                      # greedy
  
                      # since a match was found, searching can begin at the point
                      # where the match started
                      search_offset = end - delimiter_start
  
                  else:
  
                      # the delimiter was not found in the buffer
                      delimiter_start = -1
  
                      # the buffer needs to be scanned from the beginning
                      search_offset = len(buf) - idx
  
              if eof:                     # no more data is forth-coming
                  # ensure that another call will result in no search being
                  # performed
                  self._buf, self._idx = self._empty_buf, 0
  
                  end = len(buf)
                  if idx < end:
                      # there is unconsumed data in the buffer
  
                      # it is possible that a match exists but an attempt is
                      # being made to find a better match
                      if delimiter_start == -1:
                          # if there was no previous delimiter match then the
                          # final line contains no delimiter
                          delimiter_start = end
  
                      return buf[idx:end], delimiter_start - idx
  
                  raise StreamExhausted
  
              # truncate the buffer
              buf, idx = buf[idx:], 0
  
              # search should commence at the where the buffer ends minus any
              # offset that was previously provided
              search_idx = len(buf) - search_offset
  
              if search_idx < 0:
                  # ensure the search index does not start on a negative value
                  search_idx = 0
  
              # get more data
              more = fobj_read(block_size)
              buf += more
              self._buf = buf
              if not more:
                  self._eof = eof = True
  
      @property
      def delimiter(self):
          """Delimiter getter"""
          return self._delimiter
      _delimiter = None
  
      @delimiter.setter
      def delimiter(self, value):
          """Delimiter setter"""
          if isinstance(value, (str, bytes)):
              if not value:
                  raise ValueError('non-zero match delimiter is required')
              if isinstance(value, bytes) != self._binary:
                  raise ValueError('delimiter type must match stream mode')
          elif hasattr(value, 'search'):
              test_text = b'test' if self._binary else 'test'
              try:
                  result = value.search(test_text)
              except TypeError:
                  raise ValueError('delimiter type must match stream mode')
              if result and result.start() == result.end():
                  raise ValueError('non-zero match delimiter is required')
          else:
              raise ValueError('unknown type of delimiter: {}'
                               .format(repr(value)))
          self._delimiter = value
  
      def __init__(self, fobj, *, delimiter='\n', strip_delimiter=False,
                   block_size=DEFAULT_BLOCK_SIZE):
          """
          Arguments
          ----------
          fobj            : stream
                            Stream from which to read
          delimiter       : str, bytes, or regex
                            Criteria for how a line is terminated
          strip_delimiter : boolean
                            Indicator on whether the delimiter should be included
                            in a returned line
          block_size      : integer
                            Size to use for reading from the stream
  
          Attributes
          ----------
          delimiter       : str, bytes, or regex
                            Criteria for how a line is terminated
          strip_delimiter : boolean
                            Indicator on whether the delimiter should be included
                            in a returned line
  
          The stream must be opened for reading and must be blocking.
  
          The *delimiter* type should match the mode of *fobj*. If *delimiter* is
          str/bytes then the find() method of the internal buffer will be used.
          If *delimiter* is regex then its search() method will be used.
  
          The *delimiter* should match one or more characters.
          """
          buf = fobj.read(block_size)
          if isinstance(buf, bytes):
              self._binary = True
              self._empty_buf = b''
          else:
              self._binary = False
              self._empty_buf = '' # pylint: disable=redefined-variable-type
          self._fobj = fobj
          self.strip_delimiter = strip_delimiter
          self._block_size = block_size
          self.delimiter = delimiter
          self._buf, self._idx, self._eof = buf, 0, not buf
          self._line, self._delimiter_pos = None, None
  