]> jfr.im git - z_archive/twitter.git/blame - twitter/api.py
Extend API so I can use underscore to insert an integer value into URL.
[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:
e748eed8 107 def extend_call(arg):
108 return self.callable_cls(
109 auth=self.auth, format=self.format, domain=self.domain,
110 callable_cls=self.callable_cls, uriparts=self.uriparts \
111 + (arg,),
112 secure=self.secure)
113 if k == "_":
114 return extend_call
115 else:
116 return extend_call(k)
fd2bc885 117
7364ea65 118 def __call__(self, **kwargs):
aec68959 119 # Build the uri.
1be4ce71 120 uriparts = []
b0dedfc0 121 for uripart in self.uriparts:
aec68959
MV
122 # If this part matches a keyword argument, use the
123 # supplied value otherwise, just use the part.
f7e63802
MV
124 uriparts.append(str(kwargs.pop(uripart, uripart)))
125 uri = '/'.join(uriparts)
1be4ce71 126
7364ea65 127 method = "GET"
612ececa 128 for action in POST_ACTIONS:
b0dedfc0 129 if uri.endswith(action):
2dab41b1
MV
130 method = "POST"
131 break
612ececa 132
aec68959
MV
133 # If an id kwarg is present and there is no id to fill in in
134 # the list of uriparts, assume the id goes at the end.
da45d039
MV
135 id = kwargs.pop('id', None)
136 if id:
137 uri += "/%s" %(id)
4e9d6343 138
568331a9
MH
139 secure_str = ''
140 if self.secure:
141 secure_str = 's'
6c527e72 142 dot = ""
1be4ce71 143 if self.format:
6c527e72
MV
144 dot = "."
145 uriBase = "http%s://%s/%s%s%s" %(
146 secure_str, self.domain, uri, dot, self.format)
568331a9 147
5b8b1ead 148 headers = {}
1be4ce71 149 if self.auth:
568331a9 150 headers.update(self.auth.generate_headers())
1be4ce71
MV
151 arg_data = self.auth.encode_params(uriBase, method, kwargs)
152 if method == 'GET':
153 uriBase += '?' + arg_data
154 body = None
155 else:
8eb73aab 156 body = arg_data.encode('utf8')
1be4ce71 157
3930cc7b 158 req = urllib_request.Request(uriBase, body, headers)
dd648a25 159 return self._handle_response(req, uri, arg_data)
102acdb1 160
dd648a25 161 def _handle_response(self, req, uri, arg_data):
7364ea65 162 try:
3930cc7b 163 handle = urllib_request.urlopen(req)
de072195 164 if "json" == self.format:
456ec92b 165 res = json.loads(handle.read().decode('utf8'))
abddd419 166 return wrap_response(res, handle.headers)
de072195 167 else:
456ec92b
MV
168 return wrap_response(
169 handle.read().decode('utf8'), handle.headers)
3930cc7b 170 except urllib_error.HTTPError as e:
de072195 171 if (e.code == 304):
7364ea65 172 return []
de072195 173 else:
aec68959 174 raise TwitterHTTPError(e, uri, self.format, arg_data)
102acdb1 175
7364ea65 176class Twitter(TwitterCall):
177 """
178 The minimalist yet fully featured Twitter API class.
4e9d6343 179
7364ea65 180 Get RESTful data by accessing members of this class. The result
181 is decoded python objects (lists and dicts).
182
183 The Twitter API is documented here:
153dee29 184
aec68959
MV
185 http://dev.twitter.com/doc
186
4e9d6343 187
7364ea65 188 Examples::
4e9d6343 189
69e1f98e
MV
190 twitter = Twitter(
191 auth=OAuth(token, token_key, con_secret, con_secret_key)))
4e9d6343 192
7364ea65 193 # Get the public timeline
194 twitter.statuses.public_timeline()
4e9d6343 195
7364ea65 196 # Get a particular friend's timeline
197 twitter.statuses.friends_timeline(id="billybob")
4e9d6343 198
7364ea65 199 # Also supported (but totally weird)
200 twitter.statuses.friends_timeline.billybob()
4e9d6343 201
7364ea65 202 # Send a direct message
203 twitter.direct_messages.new(
204 user="billybob",
205 text="I think yer swell!")
206
b0dedfc0
MV
207 # Get the members of a particular list of a particular friend
208 twitter.user.listname.members(user="billybob", listname="billysbuds")
209
69e1f98e 210
153dee29 211 Searching Twitter::
4e9d6343 212
0b486eda 213 twitter_search = Twitter(domain="search.twitter.com")
153dee29 214
0b486eda
HN
215 # Find the latest search trends
216 twitter_search.trends()
153dee29 217
0b486eda
HN
218 # Search for the latest News on #gaza
219 twitter_search.search(q="#gaza")
153dee29 220
7364ea65 221
68b3e2ee
MV
222 Using the data returned
223 -----------------------
224
225 Twitter API calls return decoded JSON. This is converted into
226 a bunch of Python lists, dicts, ints, and strings. For example::
7364ea65 227
228 x = twitter.statuses.public_timeline()
229
230 # The first 'tweet' in the timeline
231 x[0]
232
233 # The screen name of the user who wrote the first 'tweet'
234 x[0]['user']['screen_name']
4e9d6343 235
4e9d6343 236
68b3e2ee
MV
237 Getting raw XML data
238 --------------------
239
240 If you prefer to get your Twitter data in XML format, pass
241 format="xml" to the Twitter object when you instantiate it::
4e9d6343 242
a55e6a11 243 twitter = Twitter(format="xml")
4e9d6343 244
a55e6a11 245 The output will not be parsed in any way. It will be a raw string
246 of XML.
68b3e2ee 247
7364ea65 248 """
45688301 249 def __init__(
aec68959 250 self, format="json",
87ad04c3 251 domain="api.twitter.com", secure=True, auth=None,
652c5402 252 api_version=_DEFAULT):
7364ea65 253 """
68b3e2ee
MV
254 Create a new twitter API connector.
255
256 Pass an `auth` parameter to use the credentials of a specific
257 user. Generally you'll want to pass an `OAuth`
69e1f98e
MV
258 instance::
259
260 twitter = Twitter(auth=OAuth(
261 token, token_secret, consumer_key, consumer_secret))
262
263
68b3e2ee 264 `domain` lets you change the domain you are connecting. By
87ad04c3 265 default it's `api.twitter.com` but `search.twitter.com` may be
68b3e2ee
MV
266 useful too.
267
268 If `secure` is False you will connect with HTTP instead of
269 HTTPS.
270
1cc9ab0b 271 `api_version` is used to set the base uri. By default it's
652c5402 272 '1'. If you are using "search.twitter.com" set this to None.
7364ea65 273 """
d20da7f3
MV
274 if not auth:
275 auth = NoAuth()
276
6c527e72 277 if (format not in ("json", "xml", "")):
68b3e2ee
MV
278 raise ValueError("Unknown data format '%s'" %(format))
279
652c5402
MV
280 if api_version is _DEFAULT:
281 if domain == 'api.twitter.com':
282 api_version = '1'
283 else:
284 api_version = None
285
1be4ce71 286 uriparts = ()
68b3e2ee 287 if api_version:
1be4ce71 288 uriparts += (str(api_version),)
68b3e2ee 289
9a148ed1 290 TwitterCall.__init__(
aec68959 291 self, auth=auth, format=format, domain=domain,
dd648a25 292 callable_cls=TwitterCall,
1be4ce71 293 secure=secure, uriparts=uriparts)
7e43e2ed 294
7364ea65 295
abddd419 296__all__ = ["Twitter", "TwitterError", "TwitterHTTPError", "TwitterResponse"]