+# encoding: utf-8
+from __future__ import unicode_literals
+
try:
import urllib.request as urllib_request
import urllib.error as urllib_error
except ImportError:
from io import BytesIO as StringIO
-from twitter.twitter_globals import POST_ACTIONS
-from twitter.auth import NoAuth
+from .twitter_globals import POST_ACTIONS
+from .auth import NoAuth
import re
import gzip
+try:
+ import http.client as http_client
+except ImportError:
+ import httplib as http_client
+
try:
import json
except ImportError:
import simplejson as json
+
class _DEFAULT(object):
pass
+
class TwitterError(Exception):
"""
Base Exception thrown by the Twitter object when there is a
"""
pass
+
class TwitterHTTPError(TwitterError):
"""
Exception thrown by the Twitter object when there is an
self.uri = uri
self.format = format
self.uriparts = uriparts
- if self.e.headers['Content-Encoding'] == 'gzip':
- buf = StringIO(self.e.fp.read())
+ try:
+ data = self.e.fp.read()
+ except http_client.IncompleteRead as e:
+ # can't read the error text
+ # let's try some of it
+ data = e.partial
+ if self.e.headers.get('Content-Encoding') == 'gzip':
+ buf = StringIO(data)
f = gzip.GzipFile(fileobj=buf)
self.response_data = f.read()
else:
- self.response_data = self.e.fp.read()
+ self.response_data = data
+ super(TwitterHTTPError, self).__init__(str(self))
def __str__(self):
fmt = ("." + self.format) if self.format else ""
return (
"Twitter sent status %i for URL: %s%s using parameters: "
- "(%s)\ndetails: %s" %(
+ "(%s)\ndetails: %s" % (
self.e.code, self.uri, fmt, self.uriparts,
self.response_data))
+
class TwitterResponse(object):
"""
Response from a twitter request. Behaves like a list or a string
httplib.HTTPHeaders instance. You can do
`response.headers.get('h')` to retrieve a header.
"""
- def __init__(self, headers):
- self.headers = headers
@property
def rate_limit_remaining(self):
return int(self.headers.get('X-Rate-Limit-Reset', "0"))
-def wrap_response(response, headers):
- response_typ = type(response)
- if response_typ is bool:
- # HURF DURF MY NAME IS PYTHON AND I CAN'T SUBCLASS bool.
- response_typ = int
-
- class WrappedTwitterResponse(response_typ, TwitterResponse):
- __doc__ = TwitterResponse.__doc__
+class TwitterDictResponse(dict, TwitterResponse):
+ pass
- def __init__(self, response, headers):
- response_typ.__init__(self, response)
- TwitterResponse.__init__(self, headers)
- def __new__(cls, response, headers):
- return response_typ.__new__(cls, response)
+class TwitterListResponse(list, TwitterResponse):
+ pass
- return WrappedTwitterResponse(response, headers)
+def wrap_response(response, headers):
+ response_typ = type(response)
+ if response_typ is dict:
+ res = TwitterDictResponse(response)
+ res.headers = headers
+ elif response_typ is list:
+ res = TwitterListResponse(response)
+ res.headers = headers
+ else:
+ res = response
+ return res
class TwitterCall(object):
def __init__(
- self, auth, format, domain, callable_cls, uri="",
- uriparts=None, secure=True):
+ self, auth, format, domain, callable_cls, uri="",
+ uriparts=None, secure=True, timeout=None, gzip=False):
self.auth = auth
self.format = format
self.domain = domain
self.uri = uri
self.uriparts = uriparts
self.secure = secure
+ self.timeout = timeout
+ self.gzip = gzip
def __getattr__(self, k):
try:
def extend_call(arg):
return self.callable_cls(
auth=self.auth, format=self.format, domain=self.domain,
- callable_cls=self.callable_cls, uriparts=self.uriparts \
- + (arg,),
- secure=self.secure)
+ callable_cls=self.callable_cls, timeout=self.timeout,
+ secure=self.secure, gzip=self.gzip,
+ uriparts=self.uriparts + (arg,))
if k == "_":
return extend_call
else:
# the list of uriparts, assume the id goes at the end.
id = kwargs.pop('id', None)
if id:
- uri += "/%s" %(id)
+ uri += "/%s" % (id)
# If an _id kwarg is present, this is treated as id as a CGI
# param.
dot = ""
if self.format:
dot = "."
- uriBase = "http%s://%s/%s%s%s" %(
- secure_str, self.domain, uri, dot, self.format)
-
- headers = {'Accept-Encoding': 'gzip'}
+ uriBase = "http%s://%s/%s%s%s" % (
+ secure_str, self.domain, uri, dot, self.format)
+
+ # Catch media arguments to handle oauth query differently for multipart
+ media = None
+ for arg in ['media[]', 'banner', 'image']:
+ if arg in kwargs:
+ media = kwargs.pop(arg)
+ # Check if argument tells whether img is already base64 encoded
+ b64_convert = True
+ if "_base64" in kwargs:
+ b64_convert = not kwargs.pop("_base64")
+ if b64_convert:
+ import base64
+ media = base64.b64encode(media)
+ mediafield = arg
+ break
+
+ headers = {'Accept-Encoding': 'gzip'} if self.gzip else dict()
+ body = None
+ arg_data = None
if self.auth:
headers.update(self.auth.generate_headers())
- arg_data = self.auth.encode_params(uriBase, method, kwargs)
- if method == 'GET':
+ # Use urlencoded oauth args with no params when sending media
+ # via multipart and send it directly via uri even for post
+ arg_data = self.auth.encode_params(
+ uriBase, method, {} if media else kwargs)
+ if method == 'GET' or media:
uriBase += '?' + arg_data
- body = None
else:
body = arg_data.encode('utf8')
+ # Handle query as multipart when sending media
+ if media:
+ BOUNDARY = "###Python-Twitter###"
+ bod = []
+ bod.append('--' + BOUNDARY)
+ bod.append(
+ 'Content-Disposition: form-data; name="%s"' % mediafield)
+ bod.append('Content-Transfer-Encoding: base64')
+ bod.append('')
+ bod.append(media)
+ for k, v in kwargs.items():
+ bod.append('--' + BOUNDARY)
+ bod.append('Content-Disposition: form-data; name="%s"' % k)
+ bod.append('')
+ bod.append(v)
+ bod.append('--' + BOUNDARY + '--')
+ body = '\r\n'.join(bod)
+ headers['Content-Type'] = \
+ 'multipart/form-data; boundary=%s' % BOUNDARY
+
req = urllib_request.Request(uriBase, body, headers)
return self._handle_response(req, uri, arg_data, _timeout)
handle = urllib_request.urlopen(req, **kwargs)
if handle.headers['Content-Type'] in ['image/jpeg', 'image/png']:
return handle
- elif handle.info().get('Content-Encoding') == 'gzip':
+ try:
+ data = handle.read()
+ except http_client.IncompleteRead as e:
+ # Even if we don't get all the bytes we should have there
+ # may be a complete response in e.partial
+ data = e.partial
+ if handle.info().get('Content-Encoding') == 'gzip':
# Handle gzip decompression
- buf = StringIO(handle.read())
+ buf = StringIO(data)
f = gzip.GzipFile(fileobj=buf)
data = f.read()
- else:
- data = handle.read()
-
if "json" == self.format:
res = json.loads(data.decode('utf8'))
return wrap_response(res, handle.headers)
else:
raise TwitterHTTPError(e, uri, self.format, arg_data)
+
class Twitter(TwitterCall):
"""
The minimalist yet fully featured Twitter API class.
# Get your "home" timeline
t.statuses.home_timeline()
- # Get a particular friend's timeline
- t.statuses.friends_timeline(id="billybob")
-
- # Also supported (but totally weird)
- t.statuses.friends_timeline.billybob()
+ # Get a particular friend's tweets
+ t.statuses.user_timeline(user_id="billybob")
# Update your status
t.statuses.update(
screen_name=','.join(A_LIST_OF_100_SCREEN_NAMES), \
_timeout=1)
+ # Overriding Method: GET/POST
+ # you should not need to use this method as this library properly
+ # detects whether GET or POST should be used, Nevertheless
+ # to force a particular method, use `_method`
+ t.statuses.oembed(_id=1234567890, _method='GET')
+
+ # Send a tweet with an image included (or set your banner or logo similarily)
+ # By just reading your image from the web or a file in a string:
+ with open("example.png", "rb") as imagefile:
+ params = {"media[]": imagefile.read(), "status": "PTT"}
+ t.statuses.update_with_media(**params)
+ # Or by sending a base64 encoded image:
+ params = {"media[]": base64_image, "status": "PTT", "_base64": True}
+ t.statuses.update_with_media(**params)
+
Searching Twitter::
"""
def __init__(
- self, format="json",
- domain="api.twitter.com", secure=True, auth=None,
- api_version=_DEFAULT):
+ self, format="json",
+ domain="api.twitter.com", secure=True, auth=None,
+ api_version=_DEFAULT):
"""
Create a new twitter API connector.
`domain` lets you change the domain you are connecting. By
- default it's `api.twitter.com` but `search.twitter.com` may be
- useful too.
+ default it's `api.twitter.com`.
If `secure` is False you will connect with HTTP instead of
HTTPS.
`api_version` is used to set the base uri. By default it's
- '1'. If you are using "search.twitter.com" set this to None.
+ '1.1'.
"""
if not auth:
auth = NoAuth()
if (format not in ("json", "xml", "")):
- raise ValueError("Unknown data format '%s'" %(format))
+ raise ValueError("Unknown data format '%s'" % (format))
if api_version is _DEFAULT:
- if domain == 'api.twitter.com':
- api_version = '1.1'
- else:
- api_version = None
+ api_version = '1.1'
uriparts = ()
if api_version: