streaming.py 5.92 KB
Newer Older
1
2
3
4
5
# Tweepy
# Copyright 2009 Joshua Roesslein
# See LICENSE

import httplib
6
from socket import timeout
7
from threading import Thread
8
from time import sleep
9
import urllib
10

Josh Roesslein's avatar
Josh Roesslein committed
11
12
13
14
from tweepy.auth import BasicAuthHandler
from tweepy.parsers import parse_status
from tweepy.api import API
from tweepy.error import TweepError
15
16

try:
17
    import simplejson as json
18
except ImportError:
19
    try:
20
        import json  # Python 2.6+
21
22
    except ImportError:
        try:
23
            from django.utils import simplejson as json  # Google App Engine
24
25
        except ImportError:
            raise ImportError, "Can't load a json library"
26

27
28
STREAM_VERSION = 1

Josh Roesslein's avatar
Josh Roesslein committed
29

30
31
class StreamListener(object):

Josh Roesslein's avatar
Josh Roesslein committed
32
33
34
35
36
37
38
    def on_status(self, status):
        """Called when a new status arrives"""
        return

    def on_delete(self, status_id, user_id):
        """Called when a delete notice arrives for a status"""
        return
39

Josh Roesslein's avatar
Josh Roesslein committed
40
41
42
    def on_limit(self, track):
        """Called when a limitation notice arrvies"""
        return
43

Josh Roesslein's avatar
Josh Roesslein committed
44
45
46
    def on_error(self, status_code):
        """Called when a non-200 status code is returned"""
        return False
47

Josh Roesslein's avatar
Josh Roesslein committed
48
49
50
    def on_timeout(self):
        """Called when stream connection times out"""
        return
51

52

53
54
class Stream(object):

Josh Roesslein's avatar
Josh Roesslein committed
55
56
57
58
59
60
61
62
63
64
65
66
67
    host = 'stream.twitter.com'

    def __init__(self, username, password, listener, timeout=5.0, retry_count = None,
                    retry_time = 10.0, snooze_time = 5.0, buffer_size=1500):
        self.auth = BasicAuthHandler(username, password)
        self.running = False
        self.timeout = timeout
        self.retry_count = retry_count
        self.retry_time = retry_time
        self.snooze_time = snooze_time
        self.buffer_size = buffer_size
        self.listener = listener
        self.api = API()
68
69
        self.headers = {}
        self.body = None
Josh Roesslein's avatar
Josh Roesslein committed
70
71
72

    def _run(self):
        # setup
73
        self.auth.apply_auth(None, None, self.headers, None)
Josh Roesslein's avatar
Josh Roesslein committed
74
75
76
77
78
79
80
81
82
83
84
85

        # enter loop
        error_counter = 0
        conn = None
        while self.running:
            if self.retry_count and error_counter > self.retry_count:
                # quit if error count greater than retry count
                break
            try:
                conn = httplib.HTTPConnection(self.host)
                conn.connect()
                conn.sock.settimeout(self.timeout)
86
                conn.request('POST', self.url, self.body, headers=self.headers)
Josh Roesslein's avatar
Josh Roesslein committed
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
                resp = conn.getresponse()
                if resp.status != 200:
                    if self.listener.on_error(resp.status) is False:
                        break
                    error_counter += 1
                    sleep(self.retry_time)
                else:
                    error_counter = 0
                    self._read_loop(resp)
            except timeout:
                if self.listener.on_timeout() == False:
                    break
                if self.running is False:
                    break
                conn.close()
                sleep(self.snooze_time)
            except Exception:
                # any other exception is fatal, so kill loop
                break

        # cleanup
        self.running = False
        if conn:
            conn.close()

    def _read_loop(self, resp):
        data = ''
        while self.running:
            if resp.isclosed():
                break

            # read length
            length = ''
            while True:
                c = resp.read(1)
                if c == '\n':
                    break
                length += c
            length = length.strip()
            if length.isdigit():
                length = int(length)
            else:
                continue

            # read data
            data = resp.read(length)

            # turn json data into status object
            if 'in_reply_to_status_id' in data:
136
                status = parse_status(json.loads(data), self.api)
Josh Roesslein's avatar
Josh Roesslein committed
137
138
139
140
141
142
143
144
145
146
                if self.listener.on_status(status) == False:
                    self.running = False
            elif 'delete' in data:
                delete = json.loads(data)['delete']['status']
                if self.listener.on_delete(delete['id'], delete['user_id']) == False:
                    self.running = False
            elif 'limit' in data:
                if self.listener.on_limit(json.loads(data)['limit']['track']) == False:
                    self.running = False

147
148
149
150
151
152
153
154
    def _start(self, async):
        self.running = True
        if async:
            Thread(target=self._run).start()
        else:
            self._run()

    def firehose(self, count=None, async=False):
Josh Roesslein's avatar
Josh Roesslein committed
155
156
157
158
159
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/firehose.json?delimited=length' % STREAM_VERSION
        if count:
            self.url += '&count=%s' % count
160
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
161

162
    def retweet(self, async=False):
163
164
165
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/retweet.json?delimited=length' % STREAM_VERSION
166
        self._start(async)
167

168
    def sample(self, count=None, async=False):
Josh Roesslein's avatar
Josh Roesslein committed
169
170
171
172
173
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/sample.json?delimited=length' % STREAM_VERSION
        if count:
            self.url += '&count=%s' % count
174
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
175

176
    def filter(self, follow=None, track=None, async=False):
177
178
        params = {}
        self.headers['Content-type'] = "application/x-www-form-urlencoded"
Josh Roesslein's avatar
Josh Roesslein committed
179
180
181
182
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/filter.json?delimited=length' % STREAM_VERSION
        if follow:
183
            params['follow'] = ','.join(map(str, follow))
Josh Roesslein's avatar
Josh Roesslein committed
184
        if track:
185
186
            params['track'] = ','.join(map(str, track))
        self.body = urllib.urlencode(params)
187
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
188
189

    def disconnect(self):
190
        if self.running is False:
Josh Roesslein's avatar
Josh Roesslein committed
191
192
193
            return
        self.running = False