]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
Test for setting unicode status.
[z_archive/twitter.git] / twitter / api.py
CommitLineData
3930cc7b
MV
1try:
2 import urllib.request as urllib_request
3 import urllib.error as urllib_error
4except ImportError:
5 import urllib2 as urllib_request
6 import urllib2 as urllib_error
7364ea65 7
612ececa 8from twitter.twitter_globals import POST_ACTIONS
aec68959 9from twitter.auth import NoAuth
4e9d6343 10
4b12a3a0 11try:
f1a8ed67 12 import json
4b12a3a0 13except ImportError:
f1a8ed67 14 import simplejson as json
15
652c5402
MV
16class _DEFAULT(object):
17 pass
18
5251ea48 19class TwitterError(Exception):
21e3bd23 20 """
64a8d213
B
21 Base Exception thrown by the Twitter object when there is a
22 general error interacting with the API.
21e3bd23 23 """
5251ea48 24 pass
25
64a8d213
B
26class TwitterHTTPError(TwitterError):
27 """
28 Exception thrown by the Twitter object when there is an
29 HTTP error interacting with twitter.com.
30 """
1be4ce71 31 def __init__(self, e, uri, format, uriparts):
4b12a3a0
MV
32 self.e = e
33 self.uri = uri
34 self.format = format
35 self.uriparts = uriparts
64a8d213
B
36
37 def __str__(self):
68b3e2ee
MV
38 return (
39 "Twitter sent status %i for URL: %s.%s using parameters: "
40 "(%s)\ndetails: %s" %(
1be4ce71 41 self.e.code, self.uri, self.format, self.uriparts,
68b3e2ee 42 self.e.fp.read()))
64a8d213 43
84d0a294
MV
44class TwitterResponse(object):
45 """
46 Response from a twitter request. Behaves like a list or a string
47 (depending on requested format) but it has a few other interesting
48 attributes.
49
50 `headers` gives you access to the response headers as an
51 httplib.HTTPHeaders instance. You can do
52 `response.headers.getheader('h')` to retrieve a header.
53 """
aef72b31 54 def __init__(self, headers):
84d0a294
MV
55 self.headers = headers
56
84d0a294
MV
57 @property
58 def rate_limit_remaining(self):
59 """
60 Remaining requests in the current rate-limit.
61 """
62 return int(self.headers.getheader('X-RateLimit-Remaining'))
63
64 @property
65 def rate_limit_reset(self):
66 """
67 Time in UTC epoch seconds when the rate limit will reset.
68 """
69 return int(self.headers.getheader('X-RateLimit-Reset'))
70
71
abddd419
MV
72def wrap_response(response, headers):
73 response_typ = type(response)
ce92ec77
MV
74 if response_typ is bool:
75 # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
76 response_typ = int
12bba6ac
MV
77
78 class WrappedTwitterResponse(response_typ, TwitterResponse):
abddd419
MV
79 __doc__ = TwitterResponse.__doc__
80
12bba6ac 81 def __init__(self, response):
afa71268
MV
82 if response_typ is not int:
83 response_typ.__init__(self, response)
abddd419
MV
84 TwitterResponse.__init__(self, headers)
85
12bba6ac 86 return WrappedTwitterResponse(response)
abddd419 87
0d6c0646
MV
88
89
7364ea65 90class TwitterCall(object):
dd648a25 91
c8d451e8 92 def __init__(
dd648a25 93 self, auth, format, domain, callable_cls, uri="",
7e43e2ed 94 uriparts=None, secure=True):
568331a9 95 self.auth = auth
a55e6a11 96 self.format = format
153dee29 97 self.domain = domain
dd648a25 98 self.callable_cls = callable_cls
7364ea65 99 self.uri = uri
b0dedfc0 100 self.uriparts = uriparts
9a148ed1 101 self.secure = secure
fd2bc885 102
7364ea65 103 def __getattr__(self, k):
104 try:
105 return object.__getattr__(self, k)
106 except AttributeError:
dd648a25 107 return self.callable_cls(
1be4ce71 108 auth=self.auth, format=self.format, domain=self.domain,
dd648a25 109 callable_cls=self.callable_cls, uriparts=self.uriparts + (k,),
1be4ce71 110 secure=self.secure)
fd2bc885 111
7364ea65 112 def __call__(self, **kwargs):
aec68959 113 # Build the uri.
1be4ce71 114 uriparts = []
b0dedfc0 115 for uripart in self.uriparts:
aec68959
MV
116 # If this part matches a keyword argument, use the
117 # supplied value otherwise, just use the part.
f7e63802
MV
118 uriparts.append(str(kwargs.pop(uripart, uripart)))
119 uri = '/'.join(uriparts)
1be4ce71 120
7364ea65 121 method = "GET"
612ececa 122 for action in POST_ACTIONS:
b0dedfc0 123 if uri.endswith(action):
2dab41b1
MV
124 method = "POST"
125 break
612ececa 126
aec68959
MV
127 # If an id kwarg is present and there is no id to fill in in
128 # the list of uriparts, assume the id goes at the end.
da45d039
MV
129 id = kwargs.pop('id', None)
130 if id:
131 uri += "/%s" %(id)
4e9d6343 132
568331a9
MH
133 secure_str = ''
134 if self.secure:
135 secure_str = 's'
6c527e72 136 dot = ""
1be4ce71 137 if self.format:
6c527e72
MV
138 dot = "."
139 uriBase = "http%s://%s/%s%s%s" %(
140 secure_str, self.domain, uri, dot, self.format)
568331a9 141
5b8b1ead 142 headers = {}
1be4ce71 143 if self.auth:
568331a9 144 headers.update(self.auth.generate_headers())
1be4ce71
MV
145 arg_data = self.auth.encode_params(uriBase, method, kwargs)
146 if method == 'GET':
147 uriBase += '?' + arg_data
148 body = None
149 else:
8eb73aab 150 body = arg_data.encode('utf8')
1be4ce71 151
3930cc7b 152 req = urllib_request.Request(uriBase, body, headers)
dd648a25 153 return self._handle_response(req, uri, arg_data)
102acdb1 154
dd648a25 155 def _handle_response(self, req, uri, arg_data):
7364ea65 156 try:
3930cc7b 157 handle = urllib_request.urlopen(req)
de072195 158 if "json" == self.format:
456ec92b 159 res = json.loads(handle.read().decode('utf8'))
abddd419 160 return wrap_response(res, handle.headers)
de072195 161 else:
456ec92b
MV
162 return wrap_response(
163 handle.read().decode('utf8'), handle.headers)
3930cc7b 164 except urllib_error.HTTPError as e:
de072195 165 if (e.code == 304):
7364ea65 166 return []
de072195 167 else:
aec68959 168 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 169
7364ea65 170class Twitter(TwitterCall):
171 """
172 The minimalist yet fully featured Twitter API class.
4e9d6343 173
7364ea65 174 Get RESTful data by accessing members of this class. The result
175 is decoded python objects (lists and dicts).
176
177 The Twitter API is documented here:
153dee29 178
aec68959
MV
179 http://dev.twitter.com/doc
180
4e9d6343 181
7364ea65 182 Examples::
4e9d6343 183
69e1f98e
MV
184 twitter = Twitter(
185 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 186
7364ea65 187 # Get the public timeline
188 twitter.statuses.public_timeline()
4e9d6343 189
7364ea65 190 # Get a particular friend's timeline
191 twitter.statuses.friends_timeline(id="billybob")
4e9d6343 192
7364ea65 193 # Also supported (but totally weird)
194 twitter.statuses.friends_timeline.billybob()
4e9d6343 195
7364ea65 196 # Send a direct message
197 twitter.direct_messages.new(
198 user="billybob",
199 text="I think yer swell!")
200
b0dedfc0
MV
201 # Get the members of a particular list of a particular friend
202 twitter.user.listname.members(user="billybob", listname="billysbuds")
203
69e1f98e 204
153dee29 205 Searching Twitter::
4e9d6343 206
0b486eda 207 twitter_search = Twitter(domain="search.twitter.com")
153dee29 208
0b486eda
HN
209 # Find the latest search trends
210 twitter_search.trends()
153dee29 211
0b486eda
HN
212 # Search for the latest News on #gaza
213 twitter_search.search(q="#gaza")
153dee29 214
7364ea65 215
68b3e2ee
MV
216 Using the data returned
217 -----------------------
218
219 Twitter API calls return decoded JSON. This is converted into
220 a bunch of Python lists, dicts, ints, and strings. For example::
7364ea65 221
222 x = twitter.statuses.public_timeline()
223
224 # The first 'tweet' in the timeline
225 x[0]
226
227 # The screen name of the user who wrote the first 'tweet'
228 x[0]['user']['screen_name']
4e9d6343 229
4e9d6343 230
68b3e2ee
MV
231 Getting raw XML data
232 --------------------
233
234 If you prefer to get your Twitter data in XML format, pass
235 format="xml" to the Twitter object when you instantiate it::
4e9d6343 236
a55e6a11 237 twitter = Twitter(format="xml")
4e9d6343 238
a55e6a11 239 The output will not be parsed in any way. It will be a raw string
240 of XML.
68b3e2ee 241
7364ea65 242 """
45688301 243 def __init__(
aec68959 244 self, format="json",
87ad04c3 245 domain="api.twitter.com", secure=True, auth=None,
652c5402 246 api_version=_DEFAULT):
7364ea65 247 """
68b3e2ee
MV
248 Create a new twitter API connector.
249
250 Pass an `auth` parameter to use the credentials of a specific
251 user. Generally you'll want to pass an `OAuth`
69e1f98e
MV
252 instance::
253
254 twitter = Twitter(auth=OAuth(
255 token, token_secret, consumer_key, consumer_secret))
256
257
68b3e2ee 258 `domain` lets you change the domain you are connecting. By
87ad04c3 259 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
260 useful too.
261
262 If `secure` is False you will connect with HTTP instead of
263 HTTPS.
264
1cc9ab0b 265 `api_version` is used to set the base uri. By default it's
652c5402 266 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 267 """
d20da7f3
MV
268 if not auth:
269 auth = NoAuth()
270
6c527e72 271 if (format not in ("json", "xml", "")):
68b3e2ee
MV
272 raise ValueError("Unknown data format '%s'" %(format))
273
652c5402
MV
274 if api_version is _DEFAULT:
275 if domain == 'api.twitter.com':
276 api_version = '1'
277 else:
278 api_version = None
279
1be4ce71 280 uriparts = ()
68b3e2ee 281 if api_version:
1be4ce71 282 uriparts += (str(api_version),)
68b3e2ee 283
9a148ed1 284 TwitterCall.__init__(
aec68959 285 self, auth=auth, format=format, domain=domain,
dd648a25 286 callable_cls=TwitterCall,
1be4ce71 287 secure=secure, uriparts=uriparts)
7e43e2ed 288
7364ea65 289
abddd419 290__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]