sseclient /

Full commit
import re
import time
import warnings

import requests

class SSEClient(object):
    def __init__(self, url, last_id=None, retry=3000, **kwargs):
        self.url = url
        self.last_id = last_id
        self.retry = retry

        # Any extra kwargs will be fed into the requests.get call later.
        self.requests_kwargs = kwargs

        # The SSE spec requires making requests with Cache-Control: nocache
        if 'headers' not in self.requests_kwargs:
            self.requests_kwargs['headers'] = {}
        self.requests_kwargs['headers']['Cache-Control'] = 'no-cache'

        # The 'Accept' header is not required, but explicit > implicit
        self.requests_kwargs['headers']['Accept'] = 'text/event-stream'

        # Keep data here as it streams in
        self.buf = u''


    def _connect(self):
        if self.last_id:
            self.requests_kwargs['headers']['Last-Event-ID'] = self.last_id
        self.resp = requests.get(self.url, stream=True,

        # TODO: Ensure we're handling redirects.  Might also stick the 'origin'
        # attribute on Events like the Javascript spec requires.

    def __iter__(self):
        while True:

    def next(self):
        # TODO: additionally support CR and CRLF-style newlines.
        while '\n\n' not in self.buf:
                nextchar = next(self.resp.iter_content(decode_unicode=True))
                self.buf += nextchar
            except StopIteration:
                time.sleep(self.retry / 1000.0)

                # The SSE spec only supports resuming from a whole message, so
                # if we have half a message we should throw it out.
                head, sep, tail = self.buf.rpartition('\n\n')
                self.buf = head + sep

        head, sep, tail = self.buf.partition('\n\n')
        self.buf = tail
        msg = Event.parse(head)

        # If the server requests a specific retry delay, we need to honor it.
        if msg.retry:
            self.retry = msg.retry

        # last_id should only be set if included in the message.  It's not
        # forgotten if a message omits it.
            self.last_id =

        return msg

class Event(object):

    sse_line_pattern = re.compile('(?P<name>[^:]*):?( ?(?P<value>.*))?')

    def __init__(self, data='', event='message', id=None, retry=None): = data
        self.event = event = id
        self.retry = retry

    def dump(self):
        lines = []
            lines.append('id: %s' %

        # Only include an event line if it's not the default already.
        if self.event != 'message':
            lines.append('event: %s' % self.event)

        if self.retry:
            lines.append('retry: %s' % self.retry)

        lines.extend('data: %s' % d for d in'\n'))
        return '\n'.join(lines) + '\n\n'

    def parse(cls, raw):
        Given a possibly-multiline string representing an SSE message, parse it
        and return a Event object.
        msg = cls()
        for line in raw.split('\n'):
            m = cls.sse_line_pattern.match(line)
            if m is None:
                # Malformed line.  Discard but warn.
                warnings.warn('Invalid SSE line: "%s"' % line, SyntaxWarning)

            name = m.groupdict()['name']
            value = m.groupdict()['value']
            if name == '':
                # line began with a ":", so is a comment.  Ignore

            if name == 'data':
                # If we already have some data, then join to it with a newline.
                # Else this is it.
           = '%s\n%s' % (, value)
           = value
            elif name == 'event':
                msg.event = value
            elif name == 'id':
       = value
            elif name == 'retry':
                msg.retry = int(value)

        return msg

    def __str__(self):