mirror of
https://github.com/MCV-Software/TWBlue.git
synced 2024-11-26 20:53:13 -06:00
Install Twython from a git repo instead of shipping it in the source code. #273
This commit is contained in:
parent
c5e9e97c84
commit
4391e3d3de
@ -24,3 +24,4 @@ python-vlc
|
|||||||
pywin32
|
pywin32
|
||||||
certifi
|
certifi
|
||||||
backports.functools_lru_cache
|
backports.functools_lru_cache
|
||||||
|
git+https://github.com/manuelcortez/twython
|
@ -1,29 +0,0 @@
|
|||||||
# ______ __ __
|
|
||||||
# /_ __/_ __ __ __ / /_ / /_ ____ ____
|
|
||||||
# / / | | /| / // / / // __// __ \ / __ \ / __ \
|
|
||||||
# / / | |/ |/ // /_/ // /_ / / / // /_/ // / / /
|
|
||||||
# /_/ |__/|__/ \__, / \__//_/ /_/ \____//_/ /_/
|
|
||||||
# /____/
|
|
||||||
|
|
||||||
"""
|
|
||||||
Twython
|
|
||||||
-------
|
|
||||||
|
|
||||||
Twython is a library for Python that wraps the Twitter API.
|
|
||||||
|
|
||||||
It aims to abstract away all the API endpoints, so that
|
|
||||||
additions to the library and/or the Twitter API won't
|
|
||||||
cause any overall problems.
|
|
||||||
|
|
||||||
Questions, comments? ryan@venodesigns.net
|
|
||||||
"""
|
|
||||||
|
|
||||||
__author__ = 'Ryan McGrath <ryan@venodesigns.net>'
|
|
||||||
__version__ = '3.7.0'
|
|
||||||
|
|
||||||
from .api import Twython
|
|
||||||
from .streaming import TwythonStreamer
|
|
||||||
from .exceptions import (
|
|
||||||
TwythonError, TwythonRateLimitError, TwythonAuthError,
|
|
||||||
TwythonStreamError
|
|
||||||
)
|
|
@ -1,22 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.advisory
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains Warning classes for Twython to specifically
|
|
||||||
alert the user about.
|
|
||||||
|
|
||||||
This mainly is because Python 2.7 > mutes DeprecationWarning and when
|
|
||||||
we deprecate a method or variable in Twython, we want to use to see
|
|
||||||
the Warning but don't want ALL DeprecationWarnings to appear,
|
|
||||||
only TwythonDeprecationWarnings.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonDeprecationWarning(DeprecationWarning):
|
|
||||||
"""Custom DeprecationWarning to be raised when methods/variables
|
|
||||||
are being deprecated in Twython. Python 2.7 > ignores DeprecationWarning
|
|
||||||
so we want to specifcally bubble up ONLY Twython Deprecation Warnings
|
|
||||||
"""
|
|
||||||
pass
|
|
@ -1,708 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.api
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains functionality for access to core Twitter API calls,
|
|
||||||
Twitter Authentication, and miscellaneous methods that are useful when
|
|
||||||
dealing with the Twitter API
|
|
||||||
"""
|
|
||||||
|
|
||||||
import warnings
|
|
||||||
import re
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests.auth import HTTPBasicAuth
|
|
||||||
from requests_oauthlib import OAuth1, OAuth2
|
|
||||||
|
|
||||||
from . import __version__
|
|
||||||
from .advisory import TwythonDeprecationWarning
|
|
||||||
from .compat import json, urlencode, parse_qsl, quote_plus, str, is_py2
|
|
||||||
from .compat import urlsplit
|
|
||||||
from .endpoints import EndpointsMixin
|
|
||||||
from .exceptions import TwythonError, TwythonAuthError, TwythonRateLimitError
|
|
||||||
from .helpers import _transparent_params
|
|
||||||
|
|
||||||
warnings.simplefilter('always', TwythonDeprecationWarning) # For Python 2.7 >
|
|
||||||
|
|
||||||
|
|
||||||
class Twython(EndpointsMixin, object):
|
|
||||||
def __init__(self, app_key=None, app_secret=None, oauth_token=None,
|
|
||||||
oauth_token_secret=None, access_token=None,
|
|
||||||
token_type='bearer', oauth_version=1, api_version='1.1',
|
|
||||||
client_args=None, auth_endpoint='authenticate'):
|
|
||||||
"""Instantiates an instance of Twython. Takes optional parameters for
|
|
||||||
authentication and such (see below).
|
|
||||||
|
|
||||||
:param app_key: (optional) Your applications key
|
|
||||||
:param app_secret: (optional) Your applications secret key
|
|
||||||
:param oauth_token: (optional) When using **OAuth 1**, combined with
|
|
||||||
oauth_token_secret to make authenticated calls
|
|
||||||
:param oauth_token_secret: (optional) When using **OAuth 1** combined
|
|
||||||
with oauth_token to make authenticated calls
|
|
||||||
:param access_token: (optional) When using **OAuth 2**, provide a
|
|
||||||
valid access token if you have one
|
|
||||||
:param token_type: (optional) When using **OAuth 2**, provide your
|
|
||||||
token type. Default: bearer
|
|
||||||
:param oauth_version: (optional) Choose which OAuth version to use.
|
|
||||||
Default: 1
|
|
||||||
:param api_version: (optional) Choose which Twitter API version to
|
|
||||||
use. Default: 1.1
|
|
||||||
|
|
||||||
:param client_args: (optional) Accepts some requests Session parameters
|
|
||||||
and some requests Request parameters.
|
|
||||||
See http://docs.python-requests.org/en/latest/api/#sessionapi
|
|
||||||
and requests section below it for details.
|
|
||||||
[ex. headers, proxies, verify(SSL verification)]
|
|
||||||
:param auth_endpoint: (optional) Lets you select which authentication
|
|
||||||
endpoint will use your application.
|
|
||||||
This will allow the application to have DM access
|
|
||||||
if the endpoint is 'authorize'.
|
|
||||||
Default: authenticate.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# API urls, OAuth urls and API version; needed for hitting that there
|
|
||||||
# API.
|
|
||||||
self.api_version = api_version
|
|
||||||
self.api_url = 'https://api.twitter.com/%s'
|
|
||||||
|
|
||||||
self.app_key = app_key
|
|
||||||
self.app_secret = app_secret
|
|
||||||
self.oauth_token = oauth_token
|
|
||||||
self.oauth_token_secret = oauth_token_secret
|
|
||||||
self.access_token = access_token
|
|
||||||
|
|
||||||
# OAuth 1
|
|
||||||
self.request_token_url = self.api_url % 'oauth/request_token'
|
|
||||||
self.access_token_url = self.api_url % 'oauth/access_token'
|
|
||||||
self.authenticate_url = self.api_url % ('oauth/%s' % auth_endpoint)
|
|
||||||
|
|
||||||
if self.access_token: # If they pass an access token, force OAuth 2
|
|
||||||
oauth_version = 2
|
|
||||||
|
|
||||||
self.oauth_version = oauth_version
|
|
||||||
|
|
||||||
# OAuth 2
|
|
||||||
if oauth_version == 2:
|
|
||||||
self.request_token_url = self.api_url % 'oauth2/token'
|
|
||||||
|
|
||||||
self.client_args = client_args or {}
|
|
||||||
default_headers = {'User-Agent': 'Twython v' + __version__}
|
|
||||||
if 'headers' not in self.client_args:
|
|
||||||
# If they didn't set any headers, set our defaults for them
|
|
||||||
self.client_args['headers'] = default_headers
|
|
||||||
elif 'User-Agent' not in self.client_args['headers']:
|
|
||||||
# If they set headers, but didn't include User-Agent.. set
|
|
||||||
# it for them
|
|
||||||
self.client_args['headers'].update(default_headers)
|
|
||||||
|
|
||||||
# Generate OAuth authentication object for the request
|
|
||||||
# If no keys/tokens are passed to __init__, auth=None allows for
|
|
||||||
# unauthenticated requests, although I think all v1.1 requests
|
|
||||||
# need auth
|
|
||||||
auth = None
|
|
||||||
if oauth_version == 1:
|
|
||||||
# User Authentication is through OAuth 1
|
|
||||||
if self.app_key is not None and self.app_secret is not None:
|
|
||||||
auth = OAuth1(self.app_key, self.app_secret,
|
|
||||||
self.oauth_token, self.oauth_token_secret)
|
|
||||||
|
|
||||||
elif oauth_version == 2 and self.access_token:
|
|
||||||
# Application Authentication is through OAuth 2
|
|
||||||
token = {'token_type': token_type,
|
|
||||||
'access_token': self.access_token}
|
|
||||||
auth = OAuth2(self.app_key, token=token)
|
|
||||||
|
|
||||||
self.client = requests.Session()
|
|
||||||
self.client.auth = auth
|
|
||||||
|
|
||||||
# Make a copy of the client args and iterate over them
|
|
||||||
# Pop out all the acceptable args at this point because they will
|
|
||||||
# Never be used again.
|
|
||||||
client_args_copy = self.client_args.copy()
|
|
||||||
for k, v in client_args_copy.items():
|
|
||||||
if k in ('cert', 'hooks', 'max_redirects', 'proxies'):
|
|
||||||
setattr(self.client, k, v)
|
|
||||||
self.client_args.pop(k) # Pop, pop!
|
|
||||||
|
|
||||||
# Headers are always present, so we unconditionally pop them and merge
|
|
||||||
# them into the session headers.
|
|
||||||
self.client.headers.update(self.client_args.pop('headers'))
|
|
||||||
|
|
||||||
self._last_call = None
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Twython: %s>' % (self.app_key)
|
|
||||||
|
|
||||||
def _request(self, url, method='GET', params=None, api_call=None, json_encoded=False):
|
|
||||||
"""Internal request method"""
|
|
||||||
method = method.lower()
|
|
||||||
params = params or {}
|
|
||||||
|
|
||||||
func = getattr(self.client, method)
|
|
||||||
if isinstance(params, dict) and json_encoded == False:
|
|
||||||
params, files = _transparent_params(params)
|
|
||||||
else:
|
|
||||||
params = params
|
|
||||||
files = list()
|
|
||||||
|
|
||||||
requests_args = {}
|
|
||||||
for k, v in self.client_args.items():
|
|
||||||
# Maybe this should be set as a class variable and only done once?
|
|
||||||
if k in ('timeout', 'allow_redirects', 'stream', 'verify'):
|
|
||||||
requests_args[k] = v
|
|
||||||
|
|
||||||
if method == 'get' or method == 'delete':
|
|
||||||
requests_args['params'] = params
|
|
||||||
else:
|
|
||||||
# Check for json_encoded so we will sent params as "data" or "json"
|
|
||||||
if json_encoded:
|
|
||||||
data_key = "json"
|
|
||||||
else:
|
|
||||||
data_key = "data"
|
|
||||||
requests_args.update({
|
|
||||||
data_key: params,
|
|
||||||
'files': files,
|
|
||||||
})
|
|
||||||
try:
|
|
||||||
response = func(url, **requests_args)
|
|
||||||
except requests.RequestException as e:
|
|
||||||
raise TwythonError(str(e))
|
|
||||||
|
|
||||||
# create stash for last function intel
|
|
||||||
self._last_call = {
|
|
||||||
'api_call': api_call,
|
|
||||||
'api_error': None,
|
|
||||||
'cookies': response.cookies,
|
|
||||||
'headers': response.headers,
|
|
||||||
'status_code': response.status_code,
|
|
||||||
'url': response.url,
|
|
||||||
'content': response.text,
|
|
||||||
}
|
|
||||||
|
|
||||||
# greater than 304 (not modified) is an error
|
|
||||||
if response.status_code > 304:
|
|
||||||
error_message = self._get_error_message(response)
|
|
||||||
self._last_call['api_error'] = error_message
|
|
||||||
|
|
||||||
ExceptionType = TwythonError
|
|
||||||
if response.status_code == 429:
|
|
||||||
# Twitter API 1.1, always return 429 when
|
|
||||||
# rate limit is exceeded
|
|
||||||
ExceptionType = TwythonRateLimitError
|
|
||||||
elif response.status_code == 401 or 'Bad Authentication data' \
|
|
||||||
in error_message:
|
|
||||||
# Twitter API 1.1, returns a 401 Unauthorized or
|
|
||||||
# a 400 "Bad Authentication data" for invalid/expired
|
|
||||||
# app keys/user tokens
|
|
||||||
ExceptionType = TwythonAuthError
|
|
||||||
|
|
||||||
raise ExceptionType(
|
|
||||||
error_message,
|
|
||||||
error_code=response.status_code,
|
|
||||||
retry_after=response.headers.get('X-Rate-Limit-Reset'))
|
|
||||||
content = ''
|
|
||||||
try:
|
|
||||||
if response.status_code == 204:
|
|
||||||
content = response.content
|
|
||||||
else:
|
|
||||||
content = response.json()
|
|
||||||
except ValueError:
|
|
||||||
if response.content != '':
|
|
||||||
raise TwythonError('Response was not valid JSON. \
|
|
||||||
Unable to decode.')
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
def _get_error_message(self, response):
|
|
||||||
"""Parse and return the first error message"""
|
|
||||||
|
|
||||||
error_message = 'An error occurred processing your request.'
|
|
||||||
try:
|
|
||||||
content = response.json()
|
|
||||||
# {"errors":[{"code":34,"message":"Sorry,
|
|
||||||
# that page does not exist"}]}
|
|
||||||
error_message = content['errors'][0]['message']
|
|
||||||
except TypeError:
|
|
||||||
error_message = content['errors']
|
|
||||||
except ValueError:
|
|
||||||
# bad json data from Twitter for an error
|
|
||||||
pass
|
|
||||||
except (KeyError, IndexError):
|
|
||||||
# missing data so fallback to default message
|
|
||||||
pass
|
|
||||||
|
|
||||||
return error_message
|
|
||||||
|
|
||||||
def request(self, endpoint, method='GET', params=None, version='1.1', json_encoded=False):
|
|
||||||
"""Return dict of response received from Twitter's API
|
|
||||||
|
|
||||||
:param endpoint: (required) Full url or Twitter API endpoint
|
|
||||||
(e.g. search/tweets)
|
|
||||||
:type endpoint: string
|
|
||||||
:param method: (optional) Method of accessing data, either
|
|
||||||
GET, POST or DELETE. (default GET)
|
|
||||||
:type method: string
|
|
||||||
:param params: (optional) Dict of parameters (if any) accepted
|
|
||||||
the by Twitter API endpoint you are trying to
|
|
||||||
access (default None)
|
|
||||||
:type params: dict or None
|
|
||||||
:param version: (optional) Twitter API version to access
|
|
||||||
(default 1.1)
|
|
||||||
:type version: string
|
|
||||||
:param json_encoded: (optional) Flag to indicate if this method should send data encoded as json
|
|
||||||
(default False)
|
|
||||||
:type json_encoded: bool
|
|
||||||
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
|
|
||||||
if endpoint.startswith('http://'):
|
|
||||||
raise TwythonError('api.twitter.com is restricted to SSL/TLS traffic.')
|
|
||||||
|
|
||||||
# In case they want to pass a full Twitter URL
|
|
||||||
# i.e. https://api.twitter.com/1.1/search/tweets.json
|
|
||||||
if endpoint.startswith('https://'):
|
|
||||||
url = endpoint
|
|
||||||
else:
|
|
||||||
url = '%s/%s.json' % (self.api_url % version, endpoint)
|
|
||||||
|
|
||||||
content = self._request(url, method=method, params=params,
|
|
||||||
api_call=url, json_encoded=json_encoded)
|
|
||||||
|
|
||||||
return content
|
|
||||||
|
|
||||||
def get(self, endpoint, params=None, version='1.1'):
|
|
||||||
"""Shortcut for GET requests via :class:`request`"""
|
|
||||||
return self.request(endpoint, params=params, version=version)
|
|
||||||
|
|
||||||
def post(self, endpoint, params=None, version='1.1', json_encoded=False):
|
|
||||||
"""Shortcut for POST requests via :class:`request`"""
|
|
||||||
return self.request(endpoint, 'POST', params=params, version=version, json_encoded=json_encoded)
|
|
||||||
|
|
||||||
def delete(self, endpoint, params=None, version='1.1', json_encoded=False):
|
|
||||||
"""Shortcut for delete requests via :class:`request`"""
|
|
||||||
return self.request(endpoint, 'DELETE', params=params, version=version, json_encoded=json_encoded)
|
|
||||||
|
|
||||||
def get_lastfunction_header(self, header, default_return_value=None):
|
|
||||||
"""Returns a specific header from the last API call
|
|
||||||
This will return None if the header is not present
|
|
||||||
|
|
||||||
:param header: (required) The name of the header you want to get
|
|
||||||
the value of
|
|
||||||
|
|
||||||
Most useful for the following header information:
|
|
||||||
x-rate-limit-limit,
|
|
||||||
x-rate-limit-remaining,
|
|
||||||
x-rate-limit-class,
|
|
||||||
x-rate-limit-reset
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self._last_call is None:
|
|
||||||
raise TwythonError('This function must be called after an API call. \
|
|
||||||
It delivers header information.')
|
|
||||||
|
|
||||||
return self._last_call['headers'].get(header, default_return_value)
|
|
||||||
|
|
||||||
def get_authentication_tokens(self, callback_url=None, force_login=False,
|
|
||||||
screen_name=''):
|
|
||||||
"""Returns a dict including an authorization URL, ``auth_url``, to
|
|
||||||
direct a user to
|
|
||||||
|
|
||||||
:param callback_url: (optional) Url the user is returned to after
|
|
||||||
they authorize your app (web clients only)
|
|
||||||
:param force_login: (optional) Forces the user to enter their
|
|
||||||
credentials to ensure the correct users
|
|
||||||
account is authorized.
|
|
||||||
:param screen_name: (optional) If forced_login is set OR user is
|
|
||||||
not currently logged in, Prefills the username
|
|
||||||
input box of the OAuth login screen with the
|
|
||||||
given value
|
|
||||||
|
|
||||||
:rtype: dict
|
|
||||||
"""
|
|
||||||
if self.oauth_version != 1:
|
|
||||||
raise TwythonError('This method can only be called when your \
|
|
||||||
OAuth version is 1.0.')
|
|
||||||
|
|
||||||
request_args = {}
|
|
||||||
if callback_url:
|
|
||||||
request_args['oauth_callback'] = callback_url
|
|
||||||
response = self.client.get(self.request_token_url, params=request_args)
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
raise TwythonAuthError(response.content,
|
|
||||||
error_code=response.status_code)
|
|
||||||
elif response.status_code != 200:
|
|
||||||
raise TwythonError(response.content,
|
|
||||||
error_code=response.status_code)
|
|
||||||
|
|
||||||
request_tokens = dict(parse_qsl(response.content.decode('utf-8')))
|
|
||||||
if not request_tokens:
|
|
||||||
raise TwythonError('Unable to decode request tokens.')
|
|
||||||
|
|
||||||
oauth_callback_confirmed = request_tokens.get('oauth_callback_confirmed') \
|
|
||||||
== 'true'
|
|
||||||
|
|
||||||
auth_url_params = {
|
|
||||||
'oauth_token': request_tokens['oauth_token'],
|
|
||||||
}
|
|
||||||
|
|
||||||
if force_login:
|
|
||||||
auth_url_params.update({
|
|
||||||
'force_login': force_login,
|
|
||||||
'screen_name': screen_name
|
|
||||||
})
|
|
||||||
|
|
||||||
# Use old-style callback argument if server didn't accept new-style
|
|
||||||
if callback_url and not oauth_callback_confirmed:
|
|
||||||
auth_url_params['oauth_callback'] = self.callback_url
|
|
||||||
|
|
||||||
request_tokens['auth_url'] = self.authenticate_url + \
|
|
||||||
'?' + urlencode(auth_url_params)
|
|
||||||
|
|
||||||
return request_tokens
|
|
||||||
|
|
||||||
def get_authorized_tokens(self, oauth_verifier):
|
|
||||||
"""Returns a dict of authorized tokens after they go through the
|
|
||||||
:class:`get_authentication_tokens` phase.
|
|
||||||
|
|
||||||
:param oauth_verifier: (required) The oauth_verifier (or a.k.a PIN
|
|
||||||
for non web apps) retrieved from the callback url querystring
|
|
||||||
:rtype: dict
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.oauth_version != 1:
|
|
||||||
raise TwythonError('This method can only be called when your \
|
|
||||||
OAuth version is 1.0.')
|
|
||||||
|
|
||||||
response = self.client.get(self.access_token_url,
|
|
||||||
params={'oauth_verifier': oauth_verifier},
|
|
||||||
headers={'Content-Type': 'application/\
|
|
||||||
json'})
|
|
||||||
|
|
||||||
if response.status_code == 401:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
# try to get json
|
|
||||||
content = response.json()
|
|
||||||
except AttributeError: # pragma: no cover
|
|
||||||
# if unicode detected
|
|
||||||
content = json.loads(response.content)
|
|
||||||
except ValueError:
|
|
||||||
content = {}
|
|
||||||
|
|
||||||
raise TwythonError(content.get('error', 'Invalid / expired To \
|
|
||||||
ken'), error_code=response.status_code)
|
|
||||||
|
|
||||||
authorized_tokens = dict(parse_qsl(response.content.decode('utf-8')))
|
|
||||||
if not authorized_tokens:
|
|
||||||
raise TwythonError('Unable to decode authorized tokens.')
|
|
||||||
|
|
||||||
return authorized_tokens # pragma: no cover
|
|
||||||
|
|
||||||
def obtain_access_token(self):
|
|
||||||
"""Returns an OAuth 2 access token to make OAuth 2 authenticated
|
|
||||||
read-only calls.
|
|
||||||
|
|
||||||
:rtype: string
|
|
||||||
"""
|
|
||||||
if self.oauth_version != 2:
|
|
||||||
raise TwythonError('This method can only be called when your \
|
|
||||||
OAuth version is 2.0.')
|
|
||||||
|
|
||||||
data = {'grant_type': 'client_credentials'}
|
|
||||||
basic_auth = HTTPBasicAuth(self.app_key, self.app_secret)
|
|
||||||
try:
|
|
||||||
response = self.client.post(self.request_token_url,
|
|
||||||
data=data, auth=basic_auth)
|
|
||||||
content = response.content.decode('utf-8')
|
|
||||||
try:
|
|
||||||
content = content.json()
|
|
||||||
except AttributeError:
|
|
||||||
content = json.loads(content)
|
|
||||||
access_token = content['access_token']
|
|
||||||
except (KeyError, ValueError, requests.exceptions.RequestException):
|
|
||||||
raise TwythonAuthError('Unable to obtain OAuth 2 access token.')
|
|
||||||
else:
|
|
||||||
return access_token
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def construct_api_url(api_url, **params):
|
|
||||||
"""Construct a Twitter API url, encoded, with parameters
|
|
||||||
|
|
||||||
:param api_url: URL of the Twitter API endpoint you are attempting
|
|
||||||
to construct
|
|
||||||
:param \*\*params: Parameters that are accepted by Twitter for the
|
|
||||||
endpoint you're requesting
|
|
||||||
:rtype: string
|
|
||||||
|
|
||||||
Usage::
|
|
||||||
|
|
||||||
>>> from twython import Twython
|
|
||||||
>>> twitter = Twython()
|
|
||||||
|
|
||||||
>>> api_url = 'https://api.twitter.com/1.1/search/tweets.json'
|
|
||||||
>>> constructed_url = twitter.construct_api_url(api_url, q='python',
|
|
||||||
result_type='popular')
|
|
||||||
>>> print constructed_url
|
|
||||||
https://api.twitter.com/1.1/search/tweets.json?q=python&result_type=popular
|
|
||||||
|
|
||||||
"""
|
|
||||||
querystring = []
|
|
||||||
params, _ = _transparent_params(params or {})
|
|
||||||
params = requests.utils.to_key_val_list(params)
|
|
||||||
for (k, v) in params:
|
|
||||||
querystring.append(
|
|
||||||
'%s=%s' % (Twython.encode(k), quote_plus(Twython.encode(v)))
|
|
||||||
)
|
|
||||||
return '%s?%s' % (api_url, '&'.join(querystring))
|
|
||||||
|
|
||||||
def search_gen(self, search_query, **params): # pragma: no cover
|
|
||||||
warnings.warn(
|
|
||||||
'This method is deprecated. You should use Twython.cursor instead. \
|
|
||||||
[eg. Twython.cursor(Twython.search, q=\'your_query\')]',
|
|
||||||
TwythonDeprecationWarning,
|
|
||||||
stacklevel=2
|
|
||||||
)
|
|
||||||
return self.cursor(self.search, q=search_query, **params)
|
|
||||||
|
|
||||||
def cursor(self, function, return_pages=False, **params):
|
|
||||||
"""Returns a generator for results that match a specified query.
|
|
||||||
|
|
||||||
:param function: Instance of a Twython function
|
|
||||||
(Twython.get_home_timeline, Twython.search)
|
|
||||||
:param \*\*params: Extra parameters to send with your request
|
|
||||||
(usually parameters accepted by the Twitter API endpoint)
|
|
||||||
:rtype: generator
|
|
||||||
|
|
||||||
Usage::
|
|
||||||
|
|
||||||
>>> from twython import Twython
|
|
||||||
>>> twitter = Twython(APP_KEY, APP_SECRET, OAUTH_TOKEN,
|
|
||||||
OAUTH_TOKEN_SECRET)
|
|
||||||
|
|
||||||
>>> results = twitter.cursor(twitter.search, q='python')
|
|
||||||
>>> for result in results:
|
|
||||||
>>> print result
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not callable(function):
|
|
||||||
raise TypeError('.cursor() takes a Twython function as its first \
|
|
||||||
argument. Did you provide the result of a \
|
|
||||||
function call?')
|
|
||||||
|
|
||||||
if not hasattr(function, 'iter_mode'):
|
|
||||||
raise TwythonError('Unable to create generator for Twython \
|
|
||||||
method "%s"' % function.__name__)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
content = function(**params)
|
|
||||||
|
|
||||||
if not content:
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
if hasattr(function, 'iter_key'):
|
|
||||||
results = content.get(function.iter_key)
|
|
||||||
else:
|
|
||||||
results = content
|
|
||||||
|
|
||||||
if return_pages:
|
|
||||||
yield results
|
|
||||||
else:
|
|
||||||
for result in results:
|
|
||||||
yield result
|
|
||||||
|
|
||||||
if function.iter_mode == 'cursor' and \
|
|
||||||
content['next_cursor_str'] == '0':
|
|
||||||
raise StopIteration
|
|
||||||
|
|
||||||
try:
|
|
||||||
if function.iter_mode == 'id':
|
|
||||||
# Set max_id in params to one less than lowest tweet id
|
|
||||||
if hasattr(function, 'iter_metadata'):
|
|
||||||
# Get supplied next max_id
|
|
||||||
metadata = content.get(function.iter_metadata)
|
|
||||||
if 'next_results' in metadata:
|
|
||||||
next_results = urlsplit(metadata['next_results'])
|
|
||||||
params = dict(parse_qsl(next_results.query))
|
|
||||||
else:
|
|
||||||
# No more results
|
|
||||||
raise StopIteration
|
|
||||||
else:
|
|
||||||
# Twitter gives tweets in reverse chronological order:
|
|
||||||
params['max_id'] = str(int(content[-1]['id_str']) - 1)
|
|
||||||
elif function.iter_mode == 'cursor':
|
|
||||||
params['cursor'] = content['next_cursor_str']
|
|
||||||
except (TypeError, ValueError): # pragma: no cover
|
|
||||||
raise TwythonError('Unable to generate next page of search \
|
|
||||||
results, `page` is not a number.')
|
|
||||||
except (KeyError, AttributeError): #pragma no cover
|
|
||||||
raise TwythonError('Unable to generate next page of search \
|
|
||||||
results, content has unexpected structure.')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def unicode2utf8(text):
|
|
||||||
try:
|
|
||||||
if is_py2 and isinstance(text, str):
|
|
||||||
text = text.encode('utf-8')
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return text
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode(text):
|
|
||||||
if is_py2 and isinstance(text, (str)):
|
|
||||||
return Twython.unicode2utf8(text)
|
|
||||||
return str(text)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def html_for_tweet(tweet, use_display_url=True, use_expanded_url=False, expand_quoted_status=False):
|
|
||||||
"""Return HTML for a tweet (urls, mentions, hashtags, symbols replaced with links)
|
|
||||||
|
|
||||||
:param tweet: Tweet object from received from Twitter API
|
|
||||||
:param use_display_url: Use display URL to represent link
|
|
||||||
(ex. google.com, github.com). Default: True
|
|
||||||
:param use_expanded_url: Use expanded URL to represent link
|
|
||||||
(e.g. http://google.com). Default False
|
|
||||||
|
|
||||||
If use_expanded_url is True, it overrides use_display_url.
|
|
||||||
If use_display_url and use_expanded_url is False, short url will
|
|
||||||
be used (t.co/xxxxx)
|
|
||||||
|
|
||||||
"""
|
|
||||||
if 'retweeted_status' in tweet:
|
|
||||||
tweet = tweet['retweeted_status']
|
|
||||||
|
|
||||||
if 'extended_tweet' in tweet:
|
|
||||||
tweet = tweet['extended_tweet']
|
|
||||||
|
|
||||||
orig_tweet_text = tweet.get('full_text') or tweet['text']
|
|
||||||
|
|
||||||
display_text_range = tweet.get('display_text_range') or [0, len(orig_tweet_text)]
|
|
||||||
display_text_start, display_text_end = display_text_range[0], display_text_range[1]
|
|
||||||
display_text = orig_tweet_text[display_text_start:display_text_end]
|
|
||||||
prefix_text = orig_tweet_text[0:display_text_start]
|
|
||||||
suffix_text = orig_tweet_text[display_text_end:len(orig_tweet_text)]
|
|
||||||
|
|
||||||
if 'entities' in tweet:
|
|
||||||
# We'll put all the bits of replacement HTML and their starts/ends
|
|
||||||
# in this list:
|
|
||||||
entities = []
|
|
||||||
|
|
||||||
# Mentions
|
|
||||||
if 'user_mentions' in tweet['entities']:
|
|
||||||
for entity in tweet['entities']['user_mentions']:
|
|
||||||
temp = {}
|
|
||||||
temp['start'] = entity['indices'][0]
|
|
||||||
temp['end'] = entity['indices'][1]
|
|
||||||
|
|
||||||
mention_html = '<a href="https://twitter.com/%(screen_name)s" class="twython-mention">@%(screen_name)s</a>' % {'screen_name': entity['screen_name']}
|
|
||||||
|
|
||||||
if display_text_start <= temp['start'] <= display_text_end:
|
|
||||||
temp['replacement'] = mention_html
|
|
||||||
temp['start'] -= display_text_start
|
|
||||||
temp['end'] -= display_text_start
|
|
||||||
entities.append(temp)
|
|
||||||
else:
|
|
||||||
# Make the '@username' at the start, before
|
|
||||||
# display_text, into a link:
|
|
||||||
sub_expr = r'(?<!>)' + orig_tweet_text[temp['start']:temp['end']] + '(?!</a>)'
|
|
||||||
prefix_text = re.sub(sub_expr, mention_html, prefix_text)
|
|
||||||
|
|
||||||
# Hashtags
|
|
||||||
if 'hashtags' in tweet['entities']:
|
|
||||||
for entity in tweet['entities']['hashtags']:
|
|
||||||
temp = {}
|
|
||||||
temp['start'] = entity['indices'][0] - display_text_start
|
|
||||||
temp['end'] = entity['indices'][1] - display_text_start
|
|
||||||
|
|
||||||
url_html = '<a href="https://twitter.com/search?q=%%23%(hashtag)s" class="twython-hashtag">#%(hashtag)s</a>' % {'hashtag': entity['text']}
|
|
||||||
|
|
||||||
temp['replacement'] = url_html
|
|
||||||
entities.append(temp)
|
|
||||||
|
|
||||||
# Symbols
|
|
||||||
if 'symbols' in tweet['entities']:
|
|
||||||
for entity in tweet['entities']['symbols']:
|
|
||||||
temp = {}
|
|
||||||
temp['start'] = entity['indices'][0] - display_text_start
|
|
||||||
temp['end'] = entity['indices'][1] - display_text_start
|
|
||||||
|
|
||||||
url_html = '<a href="https://twitter.com/search?q=%%24%(symbol)s" class="twython-symbol">$%(symbol)s</a>' % {'symbol': entity['text']}
|
|
||||||
|
|
||||||
temp['replacement'] = url_html
|
|
||||||
entities.append(temp)
|
|
||||||
|
|
||||||
# URLs
|
|
||||||
if 'urls' in tweet['entities']:
|
|
||||||
for entity in tweet['entities']['urls']:
|
|
||||||
temp = {}
|
|
||||||
temp['start'] = entity['indices'][0] - display_text_start
|
|
||||||
temp['end'] = entity['indices'][1] - display_text_start
|
|
||||||
|
|
||||||
if use_display_url and entity.get('display_url') and not use_expanded_url:
|
|
||||||
shown_url = entity['display_url']
|
|
||||||
elif use_expanded_url and entity.get('expanded_url'):
|
|
||||||
shown_url = entity['expanded_url']
|
|
||||||
else:
|
|
||||||
shown_url = entity['url']
|
|
||||||
|
|
||||||
url_html = '<a href="%s" class="twython-url">%s</a>' % (entity['url'], shown_url)
|
|
||||||
|
|
||||||
if display_text_start <= temp['start'] <= display_text_end:
|
|
||||||
temp['replacement'] = url_html
|
|
||||||
entities.append(temp)
|
|
||||||
else:
|
|
||||||
suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html)
|
|
||||||
|
|
||||||
if 'media' in tweet['entities'] and len(tweet['entities']['media']) > 0:
|
|
||||||
# We just link to the overall URL for the tweet's media,
|
|
||||||
# rather than to each individual item.
|
|
||||||
# So, we get the URL from the first media item:
|
|
||||||
entity = tweet['entities']['media'][0]
|
|
||||||
|
|
||||||
temp = {}
|
|
||||||
temp['start'] = entity['indices'][0]
|
|
||||||
temp['end'] = entity['indices'][1]
|
|
||||||
|
|
||||||
if use_display_url and entity.get('display_url') and not use_expanded_url:
|
|
||||||
shown_url = entity['display_url']
|
|
||||||
elif use_expanded_url and entity.get('expanded_url'):
|
|
||||||
shown_url = entity['expanded_url']
|
|
||||||
else:
|
|
||||||
shown_url = entity['url']
|
|
||||||
|
|
||||||
url_html = '<a href="%s" class="twython-media">%s</a>' % (entity['url'], shown_url)
|
|
||||||
|
|
||||||
if display_text_start <= temp['start'] <= display_text_end:
|
|
||||||
temp['replacement'] = url_html
|
|
||||||
entities.append(temp)
|
|
||||||
else:
|
|
||||||
suffix_text = suffix_text.replace(orig_tweet_text[temp['start']:temp['end']], url_html)
|
|
||||||
|
|
||||||
# Now do all the replacements, starting from the end, so that the
|
|
||||||
# start/end indices still work:
|
|
||||||
for entity in sorted(entities, key=lambda e: e['start'], reverse=True):
|
|
||||||
display_text = display_text[0:entity['start']] + entity['replacement'] + display_text[entity['end']:]
|
|
||||||
|
|
||||||
quote_text = ''
|
|
||||||
if expand_quoted_status and tweet.get('is_quote_status') and tweet.get('quoted_status'):
|
|
||||||
quoted_status = tweet['quoted_status']
|
|
||||||
quote_text += '<blockquote class="twython-quote">%(quote)s<cite><a href="%(quote_tweet_link)s">' \
|
|
||||||
'<span class="twython-quote-user-name">%(quote_user_name)s</span>' \
|
|
||||||
'<span class="twython-quote-user-screenname">@%(quote_user_screen_name)s</span></a>' \
|
|
||||||
'</cite></blockquote>' % \
|
|
||||||
{'quote': Twython.html_for_tweet(quoted_status, use_display_url, use_expanded_url, False),
|
|
||||||
'quote_tweet_link': 'https://twitter.com/%s/status/%s' %
|
|
||||||
(quoted_status['user']['screen_name'], quoted_status['id_str']),
|
|
||||||
'quote_user_name': quoted_status['user']['name'],
|
|
||||||
'quote_user_screen_name': quoted_status['user']['screen_name']}
|
|
||||||
|
|
||||||
return '%(prefix)s%(display)s%(suffix)s%(quote)s' % {
|
|
||||||
'prefix': '<span class="twython-tweet-prefix">%s</span>' % prefix_text if prefix_text else '',
|
|
||||||
'display': display_text,
|
|
||||||
'suffix': '<span class="twython-tweet-suffix">%s</span>' % suffix_text if suffix_text else '',
|
|
||||||
'quote': quote_text
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.compat
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains imports and declarations for seamless Python 2 and
|
|
||||||
Python 3 compatibility.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
_ver = sys.version_info
|
|
||||||
|
|
||||||
#: Python 2.x?
|
|
||||||
is_py2 = (_ver[0] == 2)
|
|
||||||
|
|
||||||
#: Python 3.x?
|
|
||||||
is_py3 = (_ver[0] == 3)
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
if is_py2:
|
|
||||||
from urllib import urlencode, quote_plus
|
|
||||||
from urlparse import parse_qsl, urlsplit
|
|
||||||
|
|
||||||
str = unicode
|
|
||||||
basestring = basestring
|
|
||||||
numeric_types = (int, long, float)
|
|
||||||
|
|
||||||
|
|
||||||
elif is_py3:
|
|
||||||
from urllib.parse import urlencode, quote_plus, parse_qsl, urlsplit
|
|
||||||
|
|
||||||
str = str
|
|
||||||
basestring = (str, bytes)
|
|
||||||
numeric_types = (int, float)
|
|
File diff suppressed because it is too large
Load Diff
@ -1,61 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.exceptions
|
|
||||||
~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains Twython specific Exception classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .endpoints import TWITTER_HTTP_STATUS_CODE
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonError(Exception):
|
|
||||||
"""Generic error class, catch-all for most Twython issues.
|
|
||||||
Special cases are handled by TwythonAuthError & TwythonRateLimitError.
|
|
||||||
|
|
||||||
from twython import TwythonError, TwythonRateLimitError, TwythonAuthError
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, msg, error_code=None, retry_after=None):
|
|
||||||
self.error_code = error_code
|
|
||||||
|
|
||||||
if error_code is not None and error_code in TWITTER_HTTP_STATUS_CODE:
|
|
||||||
msg = 'Twitter API returned a %s (%s), %s' % \
|
|
||||||
(error_code,
|
|
||||||
TWITTER_HTTP_STATUS_CODE[error_code][0],
|
|
||||||
msg)
|
|
||||||
|
|
||||||
super(TwythonError, self).__init__(msg)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def msg(self): # pragma: no cover
|
|
||||||
return self.args[0]
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonAuthError(TwythonError):
|
|
||||||
"""Raised when you try to access a protected resource and it fails due to
|
|
||||||
some issue with your authentication.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonRateLimitError(TwythonError): # pragma: no cover
|
|
||||||
"""Raised when you've hit a rate limit.
|
|
||||||
|
|
||||||
The amount of seconds to retry your request in will be appended
|
|
||||||
to the message.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, msg, error_code, retry_after=None):
|
|
||||||
if isinstance(retry_after, int):
|
|
||||||
msg = '%s (Retry after %d seconds)' % (msg, retry_after)
|
|
||||||
TwythonError.__init__(self, msg, error_code=error_code)
|
|
||||||
|
|
||||||
self.retry_after = retry_after
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonStreamError(TwythonError):
|
|
||||||
"""Raised when an invalid response from the Stream API is received"""
|
|
||||||
pass
|
|
@ -1,34 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.helpers
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains functions that are repeatedly used throughout
|
|
||||||
the Twython library.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .compat import basestring, numeric_types
|
|
||||||
|
|
||||||
|
|
||||||
def _transparent_params(_params):
|
|
||||||
params = {}
|
|
||||||
files = {}
|
|
||||||
for k, v in _params.items():
|
|
||||||
if hasattr(v, 'read') and callable(v.read):
|
|
||||||
files[k] = v # pragma: no cover
|
|
||||||
elif isinstance(v, bool):
|
|
||||||
if v:
|
|
||||||
params[k] = 'true'
|
|
||||||
else:
|
|
||||||
params[k] = 'false'
|
|
||||||
elif isinstance(v, basestring) or isinstance(v, numeric_types):
|
|
||||||
params[k] = v
|
|
||||||
elif isinstance(v, list):
|
|
||||||
try:
|
|
||||||
params[k] = ','.join(v)
|
|
||||||
except TypeError:
|
|
||||||
params[k] = ','.join(map(str, v))
|
|
||||||
else:
|
|
||||||
continue # pragma: no cover
|
|
||||||
return params, files
|
|
@ -1 +0,0 @@
|
|||||||
from .api import TwythonStreamer
|
|
@ -1,201 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.streaming.api
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains functionality for interfacing with streaming
|
|
||||||
Twitter API calls.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .. import __version__
|
|
||||||
from ..compat import json, is_py3
|
|
||||||
from ..helpers import _transparent_params
|
|
||||||
from .types import TwythonStreamerTypes
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from requests_oauthlib import OAuth1
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonStreamer(object):
|
|
||||||
def __init__(self, app_key, app_secret, oauth_token, oauth_token_secret,
|
|
||||||
timeout=300, retry_count=None, retry_in=10, client_args=None,
|
|
||||||
handlers=None, chunk_size=1):
|
|
||||||
"""Streaming class for a friendly streaming user experience
|
|
||||||
Authentication IS required to use the Twitter Streaming API
|
|
||||||
|
|
||||||
:param app_key: (required) Your applications key
|
|
||||||
:param app_secret: (required) Your applications secret key
|
|
||||||
:param oauth_token: (required) Used with oauth_token_secret to make
|
|
||||||
authenticated calls
|
|
||||||
:param oauth_token_secret: (required) Used with oauth_token to make
|
|
||||||
authenticated calls
|
|
||||||
:param timeout: (optional) How long (in secs) the streamer should wait
|
|
||||||
for a response from Twitter Streaming API
|
|
||||||
:param retry_count: (optional) Number of times the API call should be
|
|
||||||
retired
|
|
||||||
:param retry_in: (optional) Amount of time (in secs) the previous
|
|
||||||
API call should be tried again
|
|
||||||
:param client_args: (optional) Accepts some requests Session
|
|
||||||
parameters and some requests Request parameters.
|
|
||||||
See
|
|
||||||
http://docs.python-requests.org/en/latest/api/#sessionapi
|
|
||||||
and requests section below it for details.
|
|
||||||
[ex. headers, proxies, verify(SSL verification)]
|
|
||||||
:param handlers: (optional) Array of message types for which
|
|
||||||
corresponding handlers will be called
|
|
||||||
|
|
||||||
:param chunk_size: (optional) Define the buffer size before data is
|
|
||||||
actually returned from the Streaming API. Default: 1
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.auth = OAuth1(app_key, app_secret,
|
|
||||||
oauth_token, oauth_token_secret)
|
|
||||||
|
|
||||||
self.client_args = client_args or {}
|
|
||||||
default_headers = {'User-Agent': 'Twython Streaming v' + __version__}
|
|
||||||
if 'headers' not in self.client_args:
|
|
||||||
# If they didn't set any headers, set our defaults for them
|
|
||||||
self.client_args['headers'] = default_headers
|
|
||||||
elif 'User-Agent' not in self.client_args['headers']:
|
|
||||||
# If they set headers, but didn't include User-Agent..
|
|
||||||
# set it for them
|
|
||||||
self.client_args['headers'].update(default_headers)
|
|
||||||
self.client_args['timeout'] = timeout
|
|
||||||
|
|
||||||
self.client = requests.Session()
|
|
||||||
self.client.auth = self.auth
|
|
||||||
self.client.stream = True
|
|
||||||
|
|
||||||
# Make a copy of the client args and iterate over them
|
|
||||||
# Pop out all the acceptable args at this point because they will
|
|
||||||
# Never be used again.
|
|
||||||
client_args_copy = self.client_args.copy()
|
|
||||||
for k, v in client_args_copy.items():
|
|
||||||
if k in ('cert', 'headers', 'hooks', 'max_redirects', 'proxies'):
|
|
||||||
setattr(self.client, k, v)
|
|
||||||
self.client_args.pop(k) # Pop, pop!
|
|
||||||
|
|
||||||
self.api_version = '1.1'
|
|
||||||
|
|
||||||
self.retry_in = retry_in
|
|
||||||
self.retry_count = retry_count
|
|
||||||
|
|
||||||
# Set up type methods
|
|
||||||
StreamTypes = TwythonStreamerTypes(self)
|
|
||||||
self.statuses = StreamTypes.statuses
|
|
||||||
self.user = StreamTypes.user
|
|
||||||
self.site = StreamTypes.site
|
|
||||||
|
|
||||||
self.connected = False
|
|
||||||
|
|
||||||
self.handlers = handlers if handlers else \
|
|
||||||
['delete', 'limit', 'disconnect']
|
|
||||||
|
|
||||||
self.chunk_size = chunk_size
|
|
||||||
|
|
||||||
def _request(self, url, method='GET', params=None):
|
|
||||||
"""Internal stream request handling"""
|
|
||||||
self.connected = True
|
|
||||||
retry_counter = 0
|
|
||||||
|
|
||||||
method = method.lower()
|
|
||||||
func = getattr(self.client, method)
|
|
||||||
params, _ = _transparent_params(params)
|
|
||||||
|
|
||||||
def _send(retry_counter):
|
|
||||||
requests_args = {}
|
|
||||||
for k, v in self.client_args.items():
|
|
||||||
# Maybe this should be set as a class
|
|
||||||
# variable and only done once?
|
|
||||||
if k in ('timeout', 'allow_redirects', 'verify'):
|
|
||||||
requests_args[k] = v
|
|
||||||
|
|
||||||
while self.connected:
|
|
||||||
try:
|
|
||||||
if method == 'get':
|
|
||||||
requests_args['params'] = params
|
|
||||||
else:
|
|
||||||
requests_args['data'] = params
|
|
||||||
|
|
||||||
response = func(url, **requests_args)
|
|
||||||
except requests.exceptions.Timeout:
|
|
||||||
self.on_timeout()
|
|
||||||
else:
|
|
||||||
if response.status_code != 200:
|
|
||||||
self.on_error(response.status_code, response.content)
|
|
||||||
|
|
||||||
if self.retry_count and \
|
|
||||||
(self.retry_count - retry_counter) > 0:
|
|
||||||
time.sleep(self.retry_in)
|
|
||||||
retry_counter += 1
|
|
||||||
_send(retry_counter)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
while self.connected:
|
|
||||||
response = _send(retry_counter)
|
|
||||||
|
|
||||||
for line in response.iter_lines(self.chunk_size):
|
|
||||||
if not self.connected:
|
|
||||||
break
|
|
||||||
if line:
|
|
||||||
try:
|
|
||||||
if is_py3:
|
|
||||||
line = line.decode('utf-8')
|
|
||||||
data = json.loads(line)
|
|
||||||
except ValueError: # pragma: no cover
|
|
||||||
self.on_error(response.status_code,
|
|
||||||
'Unable to decode response, \
|
|
||||||
not valid JSON.')
|
|
||||||
else:
|
|
||||||
if self.on_success(data): # pragma: no cover
|
|
||||||
for message_type in self.handlers:
|
|
||||||
if message_type in data:
|
|
||||||
handler = getattr(self,
|
|
||||||
'on_' + message_type,
|
|
||||||
None)
|
|
||||||
if handler \
|
|
||||||
and callable(handler) \
|
|
||||||
and not handler(data.get(message_type)):
|
|
||||||
break
|
|
||||||
|
|
||||||
response.close()
|
|
||||||
|
|
||||||
def on_success(self, data): # pragma: no cover
|
|
||||||
"""Called when data has been successfully received from the stream.
|
|
||||||
Returns True if other handlers for this message should be invoked.
|
|
||||||
|
|
||||||
Feel free to override this to handle your streaming data how you
|
|
||||||
want it handled. See
|
|
||||||
https://developer.twitter.com/en/docs/tweets/filter-realtime/guides/streaming-message-types
|
|
||||||
for messages sent along in stream responses.
|
|
||||||
|
|
||||||
:param data: data recieved from the stream
|
|
||||||
:type data: dict
|
|
||||||
"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_error(self, status_code, data): # pragma: no cover
|
|
||||||
"""Called when stream returns non-200 status code
|
|
||||||
|
|
||||||
Feel free to override this to handle your streaming data how you
|
|
||||||
want it handled.
|
|
||||||
|
|
||||||
:param status_code: Non-200 status code sent from stream
|
|
||||||
:type status_code: int
|
|
||||||
|
|
||||||
:param data: Error message sent from stream
|
|
||||||
:type data: dict
|
|
||||||
"""
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_timeout(self): # pragma: no cover
|
|
||||||
""" Called when the request has timed out """
|
|
||||||
return
|
|
||||||
|
|
||||||
def disconnect(self):
|
|
||||||
"""Used to disconnect the streaming client manually"""
|
|
||||||
self.connected = False
|
|
@ -1,108 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
twython.streaming.types
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module contains classes and methods for :class:`TwythonStreamer` to use.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonStreamerTypes(object):
|
|
||||||
"""Class for different stream endpoints
|
|
||||||
|
|
||||||
Not all streaming endpoints have nested endpoints.
|
|
||||||
User Streams and Site Streams are single streams with no nested endpoints
|
|
||||||
Status Streams include filter, sample and firehose endpoints
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, streamer):
|
|
||||||
self.streamer = streamer
|
|
||||||
self.statuses = TwythonStreamerTypesStatuses(streamer)
|
|
||||||
|
|
||||||
def user(self, **params):
|
|
||||||
"""Stream user
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://dev.twitter.com/docs/api/1.1/get/user
|
|
||||||
"""
|
|
||||||
url = 'https://userstream.twitter.com/%s/user.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, params=params)
|
|
||||||
|
|
||||||
def site(self, **params):
|
|
||||||
"""Stream site
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://dev.twitter.com/docs/api/1.1/get/site
|
|
||||||
"""
|
|
||||||
url = 'https://sitestream.twitter.com/%s/site.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, params=params)
|
|
||||||
|
|
||||||
|
|
||||||
class TwythonStreamerTypesStatuses(object):
|
|
||||||
"""Class for different statuses endpoints
|
|
||||||
|
|
||||||
Available so :meth:`TwythonStreamer.statuses.filter()` is available.
|
|
||||||
Just a bit cleaner than :meth:`TwythonStreamer.statuses_filter()`,
|
|
||||||
:meth:`statuses_sample()`, etc. all being single methods in
|
|
||||||
:class:`TwythonStreamer`.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, streamer):
|
|
||||||
self.streamer = streamer
|
|
||||||
self.params = None
|
|
||||||
|
|
||||||
def filter(self, **params):
|
|
||||||
"""Stream statuses/filter
|
|
||||||
|
|
||||||
:param \*\*params: Parameters to send with your stream request
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter
|
|
||||||
"""
|
|
||||||
url = 'https://stream.twitter.com/%s/statuses/filter.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, 'POST', params=params)
|
|
||||||
|
|
||||||
def sample(self, **params):
|
|
||||||
"""Stream statuses/sample
|
|
||||||
|
|
||||||
:param \*\*params: Parameters to send with your stream request
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://developer.twitter.com/en/docs/tweets/sample-realtime/api-reference/get-statuses-sample
|
|
||||||
"""
|
|
||||||
url = 'https://stream.twitter.com/%s/statuses/sample.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, params=params)
|
|
||||||
|
|
||||||
def firehose(self, **params):
|
|
||||||
"""Stream statuses/firehose
|
|
||||||
|
|
||||||
:param \*\*params: Parameters to send with your stream request
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://dev.twitter.com/docs/api/1.1/get/statuses/firehose
|
|
||||||
"""
|
|
||||||
url = 'https://stream.twitter.com/%s/statuses/firehose.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, params=params)
|
|
||||||
|
|
||||||
def set_dynamic_filter(self, **params):
|
|
||||||
"""Set/update statuses/filter
|
|
||||||
|
|
||||||
:param \*\*params: Parameters to send with your stream request
|
|
||||||
|
|
||||||
Accepted params found at:
|
|
||||||
https://developer.twitter.com/en/docs/tweets/filter-realtime/api-reference/post-statuses-filter
|
|
||||||
"""
|
|
||||||
self.params = params
|
|
||||||
|
|
||||||
def dynamic_filter(self):
|
|
||||||
"""Stream statuses/filter with dynamic parameters"""
|
|
||||||
|
|
||||||
url = 'https://stream.twitter.com/%s/statuses/filter.json' \
|
|
||||||
% self.streamer.api_version
|
|
||||||
self.streamer._request(url, 'POST', params=self.params)
|
|
Loading…
Reference in New Issue
Block a user