streaming.py 6.37 KB
Newer Older
1
# Tweepy
2
3
# Copyright 2009-2010 Joshua Roesslein
# See LICENSE for details.
4
5

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

11
from tweepy.models import Status
Josh Roesslein's avatar
Josh Roesslein committed
12
13
from tweepy.api import API
from tweepy.error import TweepError
14

15
16
from tweepy.utils import import_simplejson
json = import_simplejson()
17

18
19
STREAM_VERSION = 1

Josh Roesslein's avatar
Josh Roesslein committed
20

21
22
class StreamListener(object):

23
24
    def __init__(self, api=None):
        self.api = api or API()
25
26
27
28
29
30
31
32
33

    def on_data(self, data):
        """Called when raw data is received from connection.

        Override this method if you wish to manually handle
        the stream data. Return False to stop stream and close connection.
        """

        if 'in_reply_to_status_id' in data:
34
            status = Status.parse(self.api, json.loads(data))
35
36
37
38
39
40
41
42
43
44
            if self.on_status(status) is False:
                return False
        elif 'delete' in data:
            delete = json.loads(data)['delete']['status']
            if self.on_delete(delete['id'], delete['user_id']) is False:
                return False
        elif 'limit' in data:
            if self.on_limit(json.loads(data)['limit']['track']) is False:
                return False

Josh Roesslein's avatar
Josh Roesslein committed
45
46
47
48
49
50
51
    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
52

Josh Roesslein's avatar
Josh Roesslein committed
53
54
55
    def on_limit(self, track):
        """Called when a limitation notice arrvies"""
        return
56

Josh Roesslein's avatar
Josh Roesslein committed
57
58
59
    def on_error(self, status_code):
        """Called when a non-200 status code is returned"""
        return False
60

Josh Roesslein's avatar
Josh Roesslein committed
61
62
63
    def on_timeout(self):
        """Called when stream connection times out"""
        return
64

65

66
67
class Stream(object):

Josh Roesslein's avatar
Josh Roesslein committed
68
69
    host = 'stream.twitter.com'

70
    def __init__(self, auth, listener, **options):
71
        self.auth = auth
Josh Roesslein's avatar
Josh Roesslein committed
72
        self.listener = listener
73
74
75
76
77
78
79
80
81
82
83
        self.running = False
        self.timeout = options.get("timeout") or 5.0
        self.retry_count = options.get("retry_count")
        self.retry_time = options.get("retry_time") or 10.0
        self.snooze_time = options.get("snooze_time") or 5.0
        self.buffer_size = options.get("buffer_size") or 1500
        if options.get("secure"):
            self.scheme = "https"
        else:
            self.scheme = "http"

Josh Roesslein's avatar
Josh Roesslein committed
84
        self.api = API()
85
        self.headers = headers or {}
86
        self.body = None
Josh Roesslein's avatar
Josh Roesslein committed
87
88

    def _run(self):
89
        # Authenticate
90
        url = "%s://%s%s" % (self.scheme, self.host, self.url)
91
        self.auth.apply_auth(url, 'GET', self.headers, None)
Josh Roesslein's avatar
Josh Roesslein committed
92

93
        # Connect and process the stream
Josh Roesslein's avatar
Josh Roesslein committed
94
95
        error_counter = 0
        conn = None
96
        exception = None
Josh Roesslein's avatar
Josh Roesslein committed
97
98
99
100
101
102
103
104
        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)
105
                conn.request('POST', self.url, self.body, headers=self.headers)
Josh Roesslein's avatar
Josh Roesslein committed
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
                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)
122
            except Exception, exception:
Josh Roesslein's avatar
Josh Roesslein committed
123
124
125
126
127
128
129
130
                # any other exception is fatal, so kill loop
                break

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

131
132
133
        if exception:
            raise exception

Josh Roesslein's avatar
Josh Roesslein committed
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
    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

153
            # read data and pass into listener
Josh Roesslein's avatar
Josh Roesslein committed
154
            data = resp.read(length)
155
156
            if self.listener.on_data(data) is False:
                self.running = False
Josh Roesslein's avatar
Josh Roesslein committed
157

158
159
160
161
162
163
164
165
    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
166
167
168
169
170
        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
171
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
172

173
    def retweet(self, async=False):
174
175
176
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/retweet.json?delimited=length' % STREAM_VERSION
177
        self._start(async)
178

179
    def sample(self, count=None, async=False):
Josh Roesslein's avatar
Josh Roesslein committed
180
181
182
183
184
        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
185
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
186

187
    def filter(self, follow=None, track=None, async=False, locations=None):
188
189
        params = {}
        self.headers['Content-type'] = "application/x-www-form-urlencoded"
Josh Roesslein's avatar
Josh Roesslein committed
190
191
192
193
        if self.running:
            raise TweepError('Stream object already connected!')
        self.url = '/%i/statuses/filter.json?delimited=length' % STREAM_VERSION
        if follow:
194
            params['follow'] = ','.join(map(str, follow))
Josh Roesslein's avatar
Josh Roesslein committed
195
        if track:
196
            params['track'] = ','.join(map(str, track))
197
198
        if locations and len(locations) > 0:
            assert len(locations) % 4 == 0
199
            params['locations'] = ','.join(['%.2f' % l for l in locations])
200
        self.body = urllib.urlencode(params)
201
        self._start(async)
Josh Roesslein's avatar
Josh Roesslein committed
202
203

    def disconnect(self):
204
        if self.running is False:
Josh Roesslein's avatar
Josh Roesslein committed
205
206
207
            return
        self.running = False