mirror of
https://github.com/robweber/xbmcbackup.git
synced 2024-11-14 20:35:48 +01:00
updated dropbox v1 api
This commit is contained in:
parent
d0028d440c
commit
622039febb
@ -8,6 +8,7 @@
|
|||||||
<import addon="script.module.uritemplate" version="0.6" />
|
<import addon="script.module.uritemplate" version="0.6" />
|
||||||
<import addon="script.module.yaml" version="3.11"/>
|
<import addon="script.module.yaml" version="3.11"/>
|
||||||
<import addon="script.module.googleapi" version="1.4.2" />
|
<import addon="script.module.googleapi" version="1.4.2" />
|
||||||
|
<import addon="script.module.requests" version="2.9.1" />
|
||||||
</requires>
|
</requires>
|
||||||
<extension point="xbmc.python.script" library="default.py">
|
<extension point="xbmc.python.script" library="default.py">
|
||||||
<provides>executable</provides>
|
<provides>executable</provides>
|
||||||
|
2
resources/lib/dropbox/.gitignore
vendored
2
resources/lib/dropbox/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
|
|
||||||
*.pyc
|
|
103
resources/lib/dropbox/CHANGELOG.mdown
Normal file
103
resources/lib/dropbox/CHANGELOG.mdown
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
Changes to the Python SDK
|
||||||
|
=========================
|
||||||
|
|
||||||
|
Changes in 2.2.0 (2014-09-16)
|
||||||
|
-----------------------------
|
||||||
|
- Support for Datastore sharing
|
||||||
|
- `DropboxClient.get_file()`: Add support for `start`, `length` parameters.
|
||||||
|
- Add support for the /longpoll_delta API
|
||||||
|
- Add direct support for /chunked_upload and /commit_chunked_upload APIs
|
||||||
|
- Fix error handling in ChunkedUploader
|
||||||
|
- Make tests compatible with py.test
|
||||||
|
- Various docstring updates (including OAuth 1->2 example)
|
||||||
|
- Fix encoding issues in example/cli_client.py
|
||||||
|
- Fix unicode handling for URL parameters
|
||||||
|
|
||||||
|
Changes in 2.1.0 (2014-06-03)
|
||||||
|
-----------------------------
|
||||||
|
- The datastore API now includes size accessors to allow you to check the size of your data and avoid the syncing limits.
|
||||||
|
- The datastore Date() constructor now truncates timestamps to the supported resolution, i.e. milliseconds.
|
||||||
|
- The datastore `await*()` calls now use POST instead of GET.
|
||||||
|
- Datastore IDs, table IDs, record IDs and field names may be 64 characters (increased from 32 characters). Before taking advantage of the new size limits ensure your application is fully upgraded to SDKs with this support.
|
||||||
|
- Option to `include_media_info` has been added to `DropboxClient.metadata()` and `DropboxClient.delta()`.
|
||||||
|
|
||||||
|
Changes in 2.0.0 (2013-12-19)
|
||||||
|
-----------------------------
|
||||||
|
- Add the Datastore API.
|
||||||
|
- Upgrade OAuth 1 tokens with `DropboxClient.create_oauth2_access_token` and `DropboxClient.disable_oauth2_access_token`.
|
||||||
|
- `DropboxClient.thumbnail()`: Fix `size` identifiers.
|
||||||
|
- `DropboxClient.delta()`: Add support for `path_prefix` parameter.
|
||||||
|
- Connection reuse/pooling using urllib3.
|
||||||
|
- Updated SSL settings.
|
||||||
|
- Various documentation reformatting.
|
||||||
|
|
||||||
|
Changes in 1.6 (2013-07-07)
|
||||||
|
----------------
|
||||||
|
- Added OAuth 2 support (use DropboxOAuth2Flow). OAuth 1 still works.
|
||||||
|
- Added a Flask-based example.
|
||||||
|
- Fixed many minor bugs.
|
||||||
|
|
||||||
|
Changes in 1.5.1 (2012-8-20)
|
||||||
|
-----------------
|
||||||
|
- Fixed packaging.
|
||||||
|
- Got rid of debug prints.
|
||||||
|
|
||||||
|
Changes in 1.5 (2012-8-15)
|
||||||
|
--------------------------
|
||||||
|
- Support for uploading large files via /chunked_upload
|
||||||
|
|
||||||
|
Changes in 1.4.1 (2012-5-16)
|
||||||
|
----------------------------
|
||||||
|
- Increase metadata() file list limit to 25,000 (used to be 10,000).
|
||||||
|
- Removed debug prints from search() call. Oops.
|
||||||
|
- Cleanup to make more compatible with Python 3.
|
||||||
|
|
||||||
|
Changes in 1.4 (2012-3-26)
|
||||||
|
--------------------------
|
||||||
|
- Add support for the /delta API.
|
||||||
|
- Add support for the "copy ref" API.
|
||||||
|
|
||||||
|
Changes in 1.3 (2012-1-11)
|
||||||
|
--------------------------
|
||||||
|
- Adds a method to the SDK that returns the file metadata when downloading a
|
||||||
|
file or its thumbnail.
|
||||||
|
- Validate server's SSL certificate against CAs in included certificate file.
|
||||||
|
|
||||||
|
Changes in 1.2 (2011-10-17)
|
||||||
|
---------------------------
|
||||||
|
- Fixes for bugs found during beta period
|
||||||
|
- Improved README to include steps to remove the v0 SDK if upgrading
|
||||||
|
|
||||||
|
Changes in 1.1 (2011-8-16)
|
||||||
|
--------------------------
|
||||||
|
- Fixed version number
|
||||||
|
- Updated CHANGELOG to be more detailed
|
||||||
|
|
||||||
|
Changes in 1.0 (2011-7-11)
|
||||||
|
--------------------------
|
||||||
|
- Backwards compatibility broken
|
||||||
|
- Completely removed 'callback' and 'status\_in\_response' parameters
|
||||||
|
- Change 'sandbox' references to 'app\_folder'
|
||||||
|
- Refactored auth.py and renamed it session.py
|
||||||
|
- Updated SDK to Dropbox API Version 1, supporting all calls
|
||||||
|
- Added 'rev' parameter to metadata and get\_file
|
||||||
|
- Added 'parent\_rev' parameter to put\_file
|
||||||
|
- Added search, share, media, revisions, and restore
|
||||||
|
- put\_file uses /files\_put instead of multipart POST and now takes a full path
|
||||||
|
- Removed methods for calls that were removed from v1 of the REST API
|
||||||
|
- Removed 'root' input parameter for all calls
|
||||||
|
- Changed return format for calls
|
||||||
|
- On error (non-200 response), an exception is raised
|
||||||
|
- On success, the JSON is parsed and a Python dict or list is returned
|
||||||
|
- Updated examples
|
||||||
|
- Renamed 'bin' directory to 'example'
|
||||||
|
- Heavily tweaked the CLI example
|
||||||
|
- Added a web app example
|
||||||
|
- Removed reliance on config files
|
||||||
|
- Assorted bugfixes and improvements
|
||||||
|
- Buffers large file uploads better in put\_file
|
||||||
|
- Improved path normalization
|
||||||
|
- All calls are now made over SSL
|
||||||
|
- Fully documented code for Pydoc generation
|
||||||
|
- Added a CHANGELOG
|
||||||
|
- Changed the distribution name from 'dropbox-client' to 'dropbox-python-sdk'
|
File diff suppressed because it is too large
Load Diff
2723
resources/lib/dropbox/datastore.py
Normal file
2723
resources/lib/dropbox/datastore.py
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,100 +1,128 @@
|
|||||||
"""
|
"""
|
||||||
A simple JSON REST request abstraction layer that is used by the
|
A simple JSON REST request abstraction layer that is used by the
|
||||||
dropbox.client and dropbox.session modules. You shouldn't need to use this.
|
``dropbox.client`` and ``dropbox.session`` modules. You shouldn't need to use this.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import httplib
|
import io
|
||||||
import os
|
|
||||||
import pkg_resources
|
|
||||||
import re
|
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
import sys
|
import sys
|
||||||
import urllib
|
import urllib
|
||||||
import urlparse
|
import pkg_resources
|
||||||
from . import util
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import json
|
import json
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import simplejson as json
|
import simplejson as json
|
||||||
|
|
||||||
SDK_VERSION = "1.5.1"
|
try:
|
||||||
|
import requests.packages.urllib3 as urllib3
|
||||||
|
except ImportError:
|
||||||
|
raise ImportError('Dropbox python client requires urllib3.')
|
||||||
|
|
||||||
|
|
||||||
|
SDK_VERSION = "2.2.0"
|
||||||
|
|
||||||
TRUSTED_CERT_FILE = pkg_resources.resource_filename(__name__, 'trusted-certs.crt')
|
TRUSTED_CERT_FILE = pkg_resources.resource_filename(__name__, 'trusted-certs.crt')
|
||||||
|
|
||||||
class ProperHTTPSConnection(httplib.HTTPConnection):
|
|
||||||
|
class RESTResponse(io.IOBase):
|
||||||
"""
|
"""
|
||||||
httplib.HTTPSConnection is broken because it doesn't do server certificate
|
Responses to requests can come in the form of ``RESTResponse``. These are
|
||||||
validation. This class does certificate validation by ensuring:
|
thin wrappers around the socket file descriptor.
|
||||||
1. The certificate sent down by the server has a signature chain to one of
|
:meth:`read()` and :meth:`close()` are implemented.
|
||||||
the certs in our 'trusted-certs.crt' (this is mostly handled by the 'ssl'
|
It is important to call :meth:`close()` to return the connection
|
||||||
module).
|
back to the connection pool to be reused. If a connection
|
||||||
2. The hostname in the certificate matches the hostname we're connecting to.
|
is not closed by the caller it may leak memory. The object makes a
|
||||||
|
best-effort attempt upon destruction to call :meth:`close()`,
|
||||||
|
but it's still best to explicitly call :meth:`close()`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host, port, trusted_cert_file=TRUSTED_CERT_FILE):
|
def __init__(self, resp):
|
||||||
httplib.HTTPConnection.__init__(self, host, port)
|
# arg: A urllib3.HTTPResponse object
|
||||||
self.ca_certs = trusted_cert_file
|
self.urllib3_response = resp
|
||||||
self.cert_reqs = ssl.CERT_REQUIRED
|
self.status = resp.status
|
||||||
|
self.version = resp.version
|
||||||
|
self.reason = resp.reason
|
||||||
|
self.strict = resp.strict
|
||||||
|
self.is_closed = False
|
||||||
|
|
||||||
def connect(self):
|
def __del__(self):
|
||||||
sock = create_connection((self.host, self.port))
|
# Attempt to close when ref-count goes to zero.
|
||||||
self.sock = ssl.wrap_socket(sock, cert_reqs=self.cert_reqs, ca_certs=self.ca_certs)
|
self.close()
|
||||||
cert = self.sock.getpeercert()
|
|
||||||
hostname = self.host.split(':', 0)[0]
|
|
||||||
match_hostname(cert, hostname)
|
|
||||||
|
|
||||||
class CertificateError(ValueError):
|
def __exit__(self, typ, value, traceback):
|
||||||
pass
|
# Allow this to be used in "with" blocks.
|
||||||
|
self.close()
|
||||||
|
|
||||||
def _dnsname_to_pat(dn):
|
# -----------------
|
||||||
pats = []
|
# Important methods
|
||||||
for frag in dn.split(r'.'):
|
# -----------------
|
||||||
if frag == '*':
|
def read(self, amt=None):
|
||||||
# When '*' is a fragment by itself, it matches a non-empty dotless
|
"""
|
||||||
# fragment.
|
Read data off the underlying socket.
|
||||||
pats.append('[^.]+')
|
|
||||||
else:
|
|
||||||
# Otherwise, '*' matches any dotless fragment.
|
|
||||||
frag = re.escape(frag)
|
|
||||||
pats.append(frag.replace(r'\*', '[^.]*'))
|
|
||||||
return re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE)
|
|
||||||
|
|
||||||
# This was ripped from Python 3.2 so it's not tested
|
Parameters
|
||||||
def match_hostname(cert, hostname):
|
amt
|
||||||
"""Verify that *cert* (in decoded format as returned by
|
Amount of data to read. Defaults to ``None``, indicating to read
|
||||||
SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 rules
|
everything.
|
||||||
are mostly followed, but IP addresses are not accepted for *hostname*.
|
|
||||||
|
|
||||||
CertificateError is raised on failure. On success, the function
|
Returns
|
||||||
returns nothing.
|
Data off the socket. If ``amt`` is not ``None``, at most ``amt`` bytes are returned.
|
||||||
"""
|
An empty string when the socket has no data.
|
||||||
if not cert:
|
|
||||||
raise ValueError("empty or no certificate")
|
Raises
|
||||||
dnsnames = []
|
``ValueError``
|
||||||
san = cert.get('subjectAltName', ())
|
If the ``RESTResponse`` has already been closed.
|
||||||
for key, value in san:
|
"""
|
||||||
if key == 'DNS':
|
if self.is_closed:
|
||||||
if _dnsname_to_pat(value).match(hostname):
|
raise ValueError('Response already closed')
|
||||||
return
|
return self.urllib3_response.read(amt)
|
||||||
dnsnames.append(value)
|
|
||||||
if not san:
|
BLOCKSIZE = 4 * 1024 * 1024 # 4MB at a time just because
|
||||||
# The subject is only checked when subjectAltName is empty
|
|
||||||
for sub in cert.get('subject', ()):
|
def close(self):
|
||||||
for key, value in sub:
|
"""Closes the underlying socket."""
|
||||||
# XXX according to RFC 2818, the most specific Common Name
|
|
||||||
# must be used.
|
# Double closing is harmless
|
||||||
if key == 'commonName':
|
if self.is_closed:
|
||||||
if _dnsname_to_pat(value).match(hostname):
|
return
|
||||||
return
|
|
||||||
dnsnames.append(value)
|
# Read any remaining crap off the socket before releasing the
|
||||||
if len(dnsnames) > 1:
|
# connection. Buffer it just in case it's huge
|
||||||
raise CertificateError("hostname %r doesn't match either of %s" % (hostname, ', '.join(map(repr, dnsnames))))
|
while self.read(RESTResponse.BLOCKSIZE):
|
||||||
elif len(dnsnames) == 1:
|
pass
|
||||||
raise CertificateError("hostname %r doesn't match %r" % (hostname, dnsnames[0]))
|
|
||||||
else:
|
# Mark as closed and release the connection (exactly once)
|
||||||
raise CertificateError("no appropriate commonName or subjectAltName fields were found")
|
self.is_closed = True
|
||||||
|
self.urllib3_response.release_conn()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def closed(self):
|
||||||
|
return self.is_closed
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------
|
||||||
|
# Backwards compat for HTTPResponse
|
||||||
|
# ---------------------------------
|
||||||
|
def getheaders(self):
|
||||||
|
"""Returns a dictionary of the response headers."""
|
||||||
|
return self.urllib3_response.getheaders()
|
||||||
|
|
||||||
|
def getheader(self, name, default=None):
|
||||||
|
"""Returns a given response header."""
|
||||||
|
return self.urllib3_response.getheader(name, default)
|
||||||
|
|
||||||
|
# Some compat functions showed up recently in urllib3
|
||||||
|
try:
|
||||||
|
urllib3.HTTPResponse.flush
|
||||||
|
urllib3.HTTPResponse.fileno
|
||||||
|
def fileno(self):
|
||||||
|
return self.urllib3_response.fileno()
|
||||||
|
def flush(self):
|
||||||
|
return self.urllib3_response.flush()
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
def create_connection(address):
|
def create_connection(address):
|
||||||
host, port = address
|
host, port = address
|
||||||
@ -107,8 +135,8 @@ def create_connection(address):
|
|||||||
sock.connect(sa)
|
sock.connect(sa)
|
||||||
return sock
|
return sock
|
||||||
|
|
||||||
except socket.error, _:
|
except socket.error as e:
|
||||||
err = _
|
err = e
|
||||||
if sock is not None:
|
if sock is not None:
|
||||||
sock.close()
|
sock.close()
|
||||||
|
|
||||||
@ -122,11 +150,41 @@ def json_loadb(data):
|
|||||||
data = data.decode('utf8')
|
data = data.decode('utf8')
|
||||||
return json.loads(data)
|
return json.loads(data)
|
||||||
|
|
||||||
|
|
||||||
class RESTClientObject(object):
|
class RESTClientObject(object):
|
||||||
def __init__(self, http_connect=None):
|
def __init__(self, max_reusable_connections=8, mock_urlopen=None):
|
||||||
self.http_connect = http_connect
|
"""
|
||||||
|
Parameters
|
||||||
|
max_reusable_connections
|
||||||
|
max connections to keep alive in the pool
|
||||||
|
mock_urlopen
|
||||||
|
an optional alternate urlopen function for testing
|
||||||
|
|
||||||
|
This class uses ``urllib3`` to maintain a pool of connections. We attempt
|
||||||
|
to grab an existing idle connection from the pool, otherwise we spin
|
||||||
|
up a new connection. Once a connection is closed, it is reinserted
|
||||||
|
into the pool (unless the pool is full).
|
||||||
|
|
||||||
|
SSL settings:
|
||||||
|
- Certificates validated using Dropbox-approved trusted root certs
|
||||||
|
- TLS v1.0 (newer TLS versions are not supported by urllib3)
|
||||||
|
- Default ciphersuites. Choosing ciphersuites is not supported by urllib3
|
||||||
|
- Hostname verification is provided by urllib3
|
||||||
|
"""
|
||||||
|
self.mock_urlopen = mock_urlopen
|
||||||
|
self.pool_manager = urllib3.PoolManager(
|
||||||
|
num_pools=4, # only a handful of hosts. api.dropbox.com, api-content.dropbox.com
|
||||||
|
maxsize=max_reusable_connections,
|
||||||
|
block=False,
|
||||||
|
timeout=60.0, # long enough so datastores await doesn't get interrupted
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs=TRUSTED_CERT_FILE,
|
||||||
|
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||||
|
)
|
||||||
|
|
||||||
def request(self, method, url, post_params=None, body=None, headers=None, raw_response=False):
|
def request(self, method, url, post_params=None, body=None, headers=None, raw_response=False):
|
||||||
|
"""Performs a REST request. See :meth:`RESTClient.request()` for detailed description."""
|
||||||
|
|
||||||
post_params = post_params or {}
|
post_params = post_params or {}
|
||||||
headers = headers or {}
|
headers = headers or {}
|
||||||
headers['User-Agent'] = 'OfficialDropboxPythonSDK/' + SDK_VERSION
|
headers['User-Agent'] = 'OfficialDropboxPythonSDK/' + SDK_VERSION
|
||||||
@ -134,63 +192,52 @@ class RESTClientObject(object):
|
|||||||
if post_params:
|
if post_params:
|
||||||
if body:
|
if body:
|
||||||
raise ValueError("body parameter cannot be used with post_params parameter")
|
raise ValueError("body parameter cannot be used with post_params parameter")
|
||||||
body = urllib.urlencode(post_params)
|
body = params_to_urlencoded(post_params)
|
||||||
headers["Content-type"] = "application/x-www-form-urlencoded"
|
headers["Content-type"] = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
# maintain dynamic lookup of ProperHTTPConnection
|
# Handle StringIO instances, because urllib3 doesn't.
|
||||||
http_connect = self.http_connect
|
if hasattr(body, 'getvalue'):
|
||||||
if http_connect is None:
|
body = str(body.getvalue())
|
||||||
http_connect = ProperHTTPSConnection
|
headers["Content-Length"] = len(body)
|
||||||
|
|
||||||
host = urlparse.urlparse(url).hostname
|
# Reject any headers containing newlines; the error from the server isn't pretty.
|
||||||
conn = http_connect(host, 443)
|
for key, value in headers.items():
|
||||||
|
if isinstance(value, basestring) and '\n' in value:
|
||||||
|
raise ValueError("headers should not contain newlines (%s: %s)" %
|
||||||
|
(key, value))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# This code is here because httplib in pre-2.6 Pythons
|
# Grab a connection from the pool to make the request.
|
||||||
# doesn't handle file-like objects as HTTP bodies and
|
# We return it to the pool when caller close() the response
|
||||||
# thus requires manual buffering
|
urlopen = self.mock_urlopen if self.mock_urlopen else self.pool_manager.urlopen
|
||||||
if not hasattr(body, 'read'):
|
r = urlopen(
|
||||||
conn.request(method, url, body, headers)
|
method=method,
|
||||||
else:
|
url=url,
|
||||||
# Content-Length should be set to prevent upload truncation errors.
|
body=body,
|
||||||
clen, raw_data = util.analyze_file_obj(body)
|
headers=headers,
|
||||||
headers["Content-Length"] = str(clen)
|
preload_content=False
|
||||||
conn.request(method, url, "", headers)
|
)
|
||||||
if raw_data is not None:
|
r = RESTResponse(r) # wrap up the urllib3 response before proceeding
|
||||||
conn.send(raw_data)
|
except socket.error as e:
|
||||||
else:
|
raise RESTSocketError(url, e)
|
||||||
BLOCKSIZE = 4 * 1024 * 1024 # 4MB buffering just because
|
except urllib3.exceptions.SSLError as e:
|
||||||
bytes_read = 0
|
raise RESTSocketError(url, "SSL certificate error: %s" % e)
|
||||||
while True:
|
|
||||||
data = body.read(BLOCKSIZE)
|
|
||||||
if not data:
|
|
||||||
break
|
|
||||||
# Catch Content-Length overflow before the HTTP server does
|
|
||||||
bytes_read += len(data)
|
|
||||||
if bytes_read > clen:
|
|
||||||
raise util.AnalyzeFileObjBug(clen, bytes_read)
|
|
||||||
conn.send(data)
|
|
||||||
if bytes_read != clen:
|
|
||||||
raise util.AnalyzeFileObjBug(clen, bytes_read)
|
|
||||||
|
|
||||||
except socket.error, e:
|
if r.status not in (200, 206):
|
||||||
raise RESTSocketError(host, e)
|
raise ErrorResponse(r, r.read())
|
||||||
except CertificateError, e:
|
|
||||||
raise RESTSocketError(host, "SSL certificate error: " + e)
|
|
||||||
|
|
||||||
r = conn.getresponse()
|
return self.process_response(r, raw_response)
|
||||||
if r.status != 200:
|
|
||||||
raise ErrorResponse(r)
|
|
||||||
|
|
||||||
|
def process_response(self, r, raw_response):
|
||||||
if raw_response:
|
if raw_response:
|
||||||
return r
|
return r
|
||||||
else:
|
else:
|
||||||
|
s = r.read()
|
||||||
try:
|
try:
|
||||||
resp = json_loadb(r.read())
|
resp = json_loadb(s)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise ErrorResponse(r)
|
raise ErrorResponse(r, s)
|
||||||
finally:
|
r.close()
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ -210,88 +257,125 @@ class RESTClientObject(object):
|
|||||||
assert type(raw_response) == bool
|
assert type(raw_response) == bool
|
||||||
return self.request("PUT", url, body=body, headers=headers, raw_response=raw_response)
|
return self.request("PUT", url, body=body, headers=headers, raw_response=raw_response)
|
||||||
|
|
||||||
class RESTClient(object):
|
|
||||||
IMPL = RESTClientObject()
|
|
||||||
|
|
||||||
|
class RESTClient(object):
|
||||||
"""
|
"""
|
||||||
An class with all static methods to perform JSON REST requests that is used internally
|
A class with all static methods to perform JSON REST requests that is used internally
|
||||||
by the Dropbox Client API. It provides just enough gear to make requests
|
by the Dropbox Client API. It provides just enough gear to make requests
|
||||||
and get responses as JSON data (when applicable). All requests happen over SSL.
|
and get responses as JSON data (when applicable). All requests happen over SSL.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
IMPL = RESTClientObject()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def request(cls, *n, **kw):
|
def request(cls, *n, **kw):
|
||||||
"""Perform a REST request and parse the response.
|
"""Perform a REST request and parse the response.
|
||||||
|
|
||||||
Args:
|
Parameters
|
||||||
- ``method``: An HTTP method (e.g. 'GET' or 'POST').
|
method
|
||||||
- ``url``: The URL to make a request to.
|
An HTTP method (e.g. ``'GET'`` or ``'POST'``).
|
||||||
- ``post_params``: A dictionary of parameters to put in the body of the request.
|
url
|
||||||
|
The URL to make a request to.
|
||||||
|
post_params
|
||||||
|
A dictionary of parameters to put in the body of the request.
|
||||||
This option may not be used if the body parameter is given.
|
This option may not be used if the body parameter is given.
|
||||||
- ``body``: The body of the request. Typically, this value will be a string.
|
body
|
||||||
It may also be a file-like object in Python 2.6 and above. The body
|
The body of the request. Typically, this value will be a string.
|
||||||
|
It may also be a file-like object. The body
|
||||||
parameter may not be used with the post_params parameter.
|
parameter may not be used with the post_params parameter.
|
||||||
- ``headers``: A dictionary of headers to send with the request.
|
headers
|
||||||
- ``raw_response``: Whether to return the raw httplib.HTTPReponse object. [default False]
|
A dictionary of headers to send with the request.
|
||||||
|
raw_response
|
||||||
|
Whether to return a :class:`RESTResponse` object. Default ``False``.
|
||||||
It's best enabled for requests that return large amounts of data that you
|
It's best enabled for requests that return large amounts of data that you
|
||||||
would want to .read() incrementally rather than loading into memory. Also
|
would want to ``.read()`` incrementally rather than loading into memory. Also
|
||||||
use this for calls where you need to read metadata like status or headers,
|
use this for calls where you need to read metadata like status or headers,
|
||||||
or if the body is not JSON.
|
or if the body is not JSON.
|
||||||
|
|
||||||
Returns:
|
Returns
|
||||||
- The JSON-decoded data from the server, unless raw_response is
|
The JSON-decoded data from the server, unless ``raw_response`` is
|
||||||
specified, in which case an httplib.HTTPReponse object is returned instead.
|
set, in which case a :class:`RESTResponse` object is returned instead.
|
||||||
|
|
||||||
Raises:
|
Raises
|
||||||
- dropbox.rest.ErrorResponse: The returned HTTP status is not 200, or the body was
|
:class:`ErrorResponse`
|
||||||
|
The returned HTTP status is not 200, or the body was
|
||||||
not parsed from JSON successfully.
|
not parsed from JSON successfully.
|
||||||
- dropbox.rest.RESTSocketError: A socket.error was raised while contacting Dropbox.
|
:class:`RESTSocketError`
|
||||||
|
A ``socket.error`` was raised while contacting Dropbox.
|
||||||
"""
|
"""
|
||||||
return cls.IMPL.request(*n, **kw)
|
return cls.IMPL.request(*n, **kw)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def GET(cls, *n, **kw):
|
def GET(cls, *n, **kw):
|
||||||
"""Perform a GET request using RESTClient.request"""
|
"""Perform a GET request using :meth:`RESTClient.request()`."""
|
||||||
return cls.IMPL.GET(*n, **kw)
|
return cls.IMPL.GET(*n, **kw)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def POST(cls, *n, **kw):
|
def POST(cls, *n, **kw):
|
||||||
"""Perform a POST request using RESTClient.request"""
|
"""Perform a POST request using :meth:`RESTClient.request()`."""
|
||||||
return cls.IMPL.POST(*n, **kw)
|
return cls.IMPL.POST(*n, **kw)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def PUT(cls, *n, **kw):
|
def PUT(cls, *n, **kw):
|
||||||
"""Perform a PUT request using RESTClient.request"""
|
"""Perform a PUT request using :meth:`RESTClient.request()`."""
|
||||||
return cls.IMPL.PUT(*n, **kw)
|
return cls.IMPL.PUT(*n, **kw)
|
||||||
|
|
||||||
|
|
||||||
class RESTSocketError(socket.error):
|
class RESTSocketError(socket.error):
|
||||||
"""
|
"""A light wrapper for ``socket.error`` that adds some more information."""
|
||||||
A light wrapper for socket.errors raised by dropbox.rest.RESTClient.request
|
|
||||||
that adds more information to the socket.error.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, host, e):
|
def __init__(self, host, e):
|
||||||
msg = "Error connecting to \"%s\": %s" % (host, str(e))
|
msg = "Error connecting to \"%s\": %s" % (host, str(e))
|
||||||
socket.error.__init__(self, msg)
|
socket.error.__init__(self, msg)
|
||||||
|
|
||||||
|
|
||||||
|
# Dummy class for docstrings, see doco.py.
|
||||||
|
class _ErrorResponse__doc__(Exception):
|
||||||
|
"""Exception raised when :class:`DropboxClient` exeriences a problem.
|
||||||
|
|
||||||
|
For example, this is raised when the server returns an unexpected
|
||||||
|
non-200 HTTP response.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_status__doc__ = "HTTP response status (an int)."
|
||||||
|
_reason__doc__ = "HTTP response reason (a string)."
|
||||||
|
_headers__doc__ = "HTTP response headers (a list of (header, value) tuples)."
|
||||||
|
_body__doc__ = "HTTP response body (string or JSON dict)."
|
||||||
|
_error_msg__doc__ = "Error message for developer (optional)."
|
||||||
|
_user_error_msg__doc__ = "Error message for end user (optional)."
|
||||||
|
|
||||||
|
|
||||||
class ErrorResponse(Exception):
|
class ErrorResponse(Exception):
|
||||||
"""
|
"""
|
||||||
Raised by dropbox.rest.RESTClient.request for requests that:
|
Raised by :meth:`RESTClient.request()` for requests that:
|
||||||
- Return a non-200 HTTP response, or
|
|
||||||
- Have a non-JSON response body, or
|
|
||||||
- Have a malformed/missing header in the response.
|
|
||||||
|
|
||||||
Most errors that Dropbox returns will have a error field that is unpacked and
|
- Return a non-200 HTTP response, or
|
||||||
|
- Have a non-JSON response body, or
|
||||||
|
- Have a malformed/missing header in the response.
|
||||||
|
|
||||||
|
Most errors that Dropbox returns will have an error field that is unpacked and
|
||||||
placed on the ErrorResponse exception. In some situations, a user_error field
|
placed on the ErrorResponse exception. In some situations, a user_error field
|
||||||
will also come back. Messages under user_error are worth showing to an end-user
|
will also come back. Messages under user_error are worth showing to an end-user
|
||||||
of your app, while other errors are likely only useful for you as the developer.
|
of your app, while other errors are likely only useful for you as the developer.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, http_resp):
|
def __init__(self, http_resp, body):
|
||||||
|
"""
|
||||||
|
Parameters
|
||||||
|
http_resp
|
||||||
|
The :class:`RESTResponse` which errored
|
||||||
|
body
|
||||||
|
Body of the :class:`RESTResponse`.
|
||||||
|
The reason we can't simply call ``http_resp.read()`` to
|
||||||
|
get the body, is that ``read()`` is not idempotent.
|
||||||
|
Since it can't be called more than once,
|
||||||
|
we have to pass the string body in separately
|
||||||
|
"""
|
||||||
self.status = http_resp.status
|
self.status = http_resp.status
|
||||||
self.reason = http_resp.reason
|
self.reason = http_resp.reason
|
||||||
self.body = http_resp.read()
|
self.body = body
|
||||||
self.headers = http_resp.getheaders()
|
self.headers = http_resp.getheaders()
|
||||||
|
http_resp.close() # won't need this connection anymore
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.body = json_loadb(self.body)
|
self.body = json_loadb(self.body)
|
||||||
@ -304,14 +388,29 @@ class ErrorResponse(Exception):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.user_error_msg and self.user_error_msg != self.error_msg:
|
if self.user_error_msg and self.user_error_msg != self.error_msg:
|
||||||
# one is translated and the other is English
|
# one is translated and the other is English
|
||||||
msg = "%s (%s)" % (self.user_error_msg, self.error_msg)
|
msg = "%r (%r)" % (self.user_error_msg, self.error_msg)
|
||||||
elif self.error_msg:
|
elif self.error_msg:
|
||||||
msg = self.error_msg
|
msg = repr(self.error_msg)
|
||||||
elif not self.body:
|
elif not self.body:
|
||||||
msg = self.reason
|
msg = repr(self.reason)
|
||||||
else:
|
else:
|
||||||
msg = "Error parsing response body or headers: " +\
|
msg = "Error parsing response body or headers: " +\
|
||||||
"Body - %s Headers - %s" % (self.body, self.headers)
|
"Body - %.100r Headers - %r" % (self.body, self.headers)
|
||||||
|
|
||||||
return "[%d] %s" % (self.status, repr(msg))
|
return "[%d] %s" % (self.status, msg)
|
||||||
|
|
||||||
|
|
||||||
|
def params_to_urlencoded(params):
|
||||||
|
"""
|
||||||
|
Returns a application/x-www-form-urlencoded 'str' representing the key/value pairs in 'params'.
|
||||||
|
|
||||||
|
Keys are values are str()'d before calling urllib.urlencode, with the exception of unicode
|
||||||
|
objects which are utf8-encoded.
|
||||||
|
"""
|
||||||
|
def encode(o):
|
||||||
|
if isinstance(o, unicode):
|
||||||
|
return o.encode('utf8')
|
||||||
|
else:
|
||||||
|
return str(o)
|
||||||
|
utf8_params = {encode(k): encode(v) for k, v in params.iteritems()}
|
||||||
|
return urllib.urlencode(utf8_params)
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
dropbox.session.DropboxSession is responsible for holding OAuth authentication info
|
dropbox.session.DropboxSession is responsible for holding OAuth authentication
|
||||||
(app key/secret, request key/secret, access key/secret) as well as configuration information for your app
|
info (app key/secret, request key/secret, access key/secret). It knows how to
|
||||||
('app_folder' or 'dropbox' access type, optional locale preference). It knows how to
|
|
||||||
use all of this information to craft properly constructed requests to Dropbox.
|
use all of this information to craft properly constructed requests to Dropbox.
|
||||||
|
|
||||||
A DropboxSession object must be passed to a dropbox.client.DropboxClient object upon
|
A DropboxSession object must be passed to a dropbox.client.DropboxClient object upon
|
||||||
initialization.
|
initialization.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
@ -23,28 +23,33 @@ except ImportError:
|
|||||||
from . import rest
|
from . import rest
|
||||||
|
|
||||||
class OAuthToken(object):
|
class OAuthToken(object):
|
||||||
__slots__ = ('key', 'secret')
|
"""
|
||||||
|
A class representing an OAuth token. Contains two fields: ``key`` and
|
||||||
|
``secret``.
|
||||||
|
"""
|
||||||
def __init__(self, key, secret):
|
def __init__(self, key, secret):
|
||||||
self.key = key
|
self.key = key
|
||||||
self.secret = secret
|
self.secret = secret
|
||||||
|
|
||||||
class DropboxSession(object):
|
class BaseSession(object):
|
||||||
API_VERSION = 1
|
API_VERSION = 1
|
||||||
|
|
||||||
API_HOST = "api.dropbox.com"
|
API_HOST = "api.dropbox.com"
|
||||||
WEB_HOST = "www.dropbox.com"
|
WEB_HOST = "www.dropbox.com"
|
||||||
API_CONTENT_HOST = "api-content.dropbox.com"
|
API_CONTENT_HOST = "api-content.dropbox.com"
|
||||||
|
API_NOTIFICATION_HOST = "api-notify.dropbox.com"
|
||||||
|
|
||||||
def __init__(self, consumer_key, consumer_secret, access_type, locale=None, rest_client=rest.RESTClient):
|
def __init__(self, consumer_key, consumer_secret, access_type="auto", locale=None, rest_client=rest.RESTClient):
|
||||||
"""Initialize a DropboxSession object.
|
"""Initialize a DropboxSession object.
|
||||||
|
|
||||||
Your consumer key and secret are available
|
Your consumer key and secret are available
|
||||||
at https://www.dropbox.com/developers/apps
|
at https://www.dropbox.com/developers/apps
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
- ``access_type``: Either 'dropbox' or 'app_folder'. All path-based operations
|
|
||||||
will occur relative to either the user's Dropbox root directory
|
- ``access_type``: Either 'auto' (the default), 'dropbox', or
|
||||||
or your application's app folder.
|
'app_folder'. You probably don't need to specify this and should
|
||||||
|
just use the default.
|
||||||
- ``locale``: A locale string ('en', 'pt_PT', etc.) [optional]
|
- ``locale``: A locale string ('en', 'pt_PT', etc.) [optional]
|
||||||
The locale setting will be used to translate any user-facing error
|
The locale setting will be used to translate any user-facing error
|
||||||
messages that the server generates. At this time Dropbox supports
|
messages that the server generates. At this time Dropbox supports
|
||||||
@ -52,12 +57,13 @@ class DropboxSession(object):
|
|||||||
languages in the future. If you send a language the server doesn't
|
languages in the future. If you send a language the server doesn't
|
||||||
support, messages will remain in English. Look for these translated
|
support, messages will remain in English. Look for these translated
|
||||||
messages in rest.ErrorResponse exceptions as e.user_error_msg.
|
messages in rest.ErrorResponse exceptions as e.user_error_msg.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert access_type in ['dropbox', 'app_folder'], "expected access_type of 'dropbox' or 'app_folder'"
|
assert access_type in ['dropbox', 'app_folder', 'auto'], "expected access_type of 'dropbox' or 'app_folder'"
|
||||||
self.consumer_creds = OAuthToken(consumer_key, consumer_secret)
|
self.consumer_creds = OAuthToken(consumer_key, consumer_secret)
|
||||||
self.token = None
|
self.token = None
|
||||||
self.request_token = None
|
self.request_token = None
|
||||||
self.root = 'sandbox' if access_type == 'app_folder' else 'dropbox'
|
self.root = 'sandbox' if access_type == 'app_folder' else access_type
|
||||||
self.locale = locale
|
self.locale = locale
|
||||||
self.rest_client = rest_client
|
self.rest_client = rest_client
|
||||||
|
|
||||||
@ -69,22 +75,6 @@ class DropboxSession(object):
|
|||||||
"""Remove any attached access token from the DropboxSession."""
|
"""Remove any attached access token from the DropboxSession."""
|
||||||
self.token = None
|
self.token = None
|
||||||
|
|
||||||
def set_token(self, access_token, access_token_secret):
|
|
||||||
"""Attach an access token to the DropboxSession.
|
|
||||||
|
|
||||||
Note that the access 'token' is made up of both a token string
|
|
||||||
and a secret string.
|
|
||||||
"""
|
|
||||||
self.token = OAuthToken(access_token, access_token_secret)
|
|
||||||
|
|
||||||
def set_request_token(self, request_token, request_token_secret):
|
|
||||||
"""Attach an request token to the DropboxSession.
|
|
||||||
|
|
||||||
Note that the request 'token' is made up of both a token string
|
|
||||||
and a secret string.
|
|
||||||
"""
|
|
||||||
self.request_token = OAuthToken(request_token, request_token_secret)
|
|
||||||
|
|
||||||
def build_path(self, target, params=None):
|
def build_path(self, target, params=None):
|
||||||
"""Build the path component for an API URL.
|
"""Build the path component for an API URL.
|
||||||
|
|
||||||
@ -111,9 +101,9 @@ class DropboxSession(object):
|
|||||||
params['locale'] = self.locale
|
params['locale'] = self.locale
|
||||||
|
|
||||||
if params:
|
if params:
|
||||||
return "/%d%s?%s" % (self.API_VERSION, target_path, urllib.urlencode(params))
|
return "/%s%s?%s" % (self.API_VERSION, target_path, urllib.urlencode(params))
|
||||||
else:
|
else:
|
||||||
return "/%d%s" % (self.API_VERSION, target_path)
|
return "/%s%s" % (self.API_VERSION, target_path)
|
||||||
|
|
||||||
def build_url(self, host, target, params=None):
|
def build_url(self, host, target, params=None):
|
||||||
"""Build an API URL.
|
"""Build an API URL.
|
||||||
@ -130,6 +120,24 @@ class DropboxSession(object):
|
|||||||
"""
|
"""
|
||||||
return "https://%s%s" % (host, self.build_path(target, params))
|
return "https://%s%s" % (host, self.build_path(target, params))
|
||||||
|
|
||||||
|
class DropboxSession(BaseSession):
|
||||||
|
|
||||||
|
def set_token(self, access_token, access_token_secret):
|
||||||
|
"""Attach an access token to the DropboxSession.
|
||||||
|
|
||||||
|
Note that the access 'token' is made up of both a token string
|
||||||
|
and a secret string.
|
||||||
|
"""
|
||||||
|
self.token = OAuthToken(access_token, access_token_secret)
|
||||||
|
|
||||||
|
def set_request_token(self, request_token, request_token_secret):
|
||||||
|
"""Attach an request token to the DropboxSession.
|
||||||
|
|
||||||
|
Note that the request 'token' is made up of both a token string
|
||||||
|
and a secret string.
|
||||||
|
"""
|
||||||
|
self.request_token = OAuthToken(request_token, request_token_secret)
|
||||||
|
|
||||||
def build_authorize_url(self, request_token, oauth_callback=None):
|
def build_authorize_url(self, request_token, oauth_callback=None):
|
||||||
"""Build a request token authorization URL.
|
"""Build a request token authorization URL.
|
||||||
|
|
||||||
@ -166,8 +174,9 @@ class DropboxSession(object):
|
|||||||
can store the access token for that user for later operations.
|
can store the access token for that user for later operations.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- An dropbox.session.OAuthToken representing the request token Dropbox assigned
|
- An :py:class:`OAuthToken` object representing the
|
||||||
to this app. Also attaches the request token as self.request_token.
|
request token Dropbox assigned to this app. Also attaches the
|
||||||
|
request token as self.request_token.
|
||||||
"""
|
"""
|
||||||
self.token = None # clear any token currently on the request
|
self.token = None # clear any token currently on the request
|
||||||
url = self.build_url(self.API_HOST, '/oauth/request_token')
|
url = self.build_url(self.API_HOST, '/oauth/request_token')
|
||||||
@ -194,8 +203,9 @@ class DropboxSession(object):
|
|||||||
DropboxSession instance.
|
DropboxSession instance.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
- An tuple of (key, secret) representing the access token Dropbox assigned
|
- An :py:class:`OAuthToken` object with fields ``key`` and ``secret``
|
||||||
to this app and user. Also attaches the access token as self.token.
|
representing the access token Dropbox assigned to this app and
|
||||||
|
user. Also attaches the access token as self.token.
|
||||||
"""
|
"""
|
||||||
request_token = request_token or self.request_token
|
request_token = request_token or self.request_token
|
||||||
assert request_token, "No request_token available on the session. Please pass one."
|
assert request_token, "No request_token available on the session. Please pass one."
|
||||||
@ -284,3 +294,15 @@ class DropboxSession(object):
|
|||||||
"OAuth request.")
|
"OAuth request.")
|
||||||
|
|
||||||
return OAuthToken(key, secret)
|
return OAuthToken(key, secret)
|
||||||
|
|
||||||
|
# Don't use this class directly.
|
||||||
|
class DropboxOAuth2Session(BaseSession):
|
||||||
|
|
||||||
|
def __init__(self, oauth2_access_token, locale, rest_client=rest.RESTClient):
|
||||||
|
super(DropboxOAuth2Session, self).__init__("", "", "auto", locale=locale, rest_client=rest_client)
|
||||||
|
self.access_token = oauth2_access_token
|
||||||
|
|
||||||
|
def build_access_headers(self, method, resource_url, params=None, token=None):
|
||||||
|
assert token is None
|
||||||
|
headers = {"Authorization": "Bearer " + self.access_token}
|
||||||
|
return headers, params
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,53 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
class AnalyzeFileObjBug(Exception):
|
|
||||||
msg = ("\n"
|
|
||||||
"Expected file object to have %d bytes, instead we read %d bytes.\n"
|
|
||||||
"File size detection may have failed (see dropbox.util.AnalyzeFileObj)\n")
|
|
||||||
def __init__(self, expected, actual):
|
|
||||||
self.expected = expected
|
|
||||||
self.actual = actual
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.msg % (self.expected, self.actual)
|
|
||||||
|
|
||||||
def analyze_file_obj(obj):
|
|
||||||
""" Get the size and contents of a file-like object.
|
|
||||||
Returns: (size, raw_data)
|
|
||||||
size: The amount of data waiting to be read
|
|
||||||
raw_data: If not None, the entire contents of the stream (as a string).
|
|
||||||
None if the stream should be read() in chunks.
|
|
||||||
"""
|
|
||||||
pos = 0
|
|
||||||
if hasattr(obj, 'tell'):
|
|
||||||
pos = obj.tell()
|
|
||||||
|
|
||||||
# Handle cStringIO and StringIO
|
|
||||||
if hasattr(obj, 'getvalue'):
|
|
||||||
# Why using getvalue() makes sense:
|
|
||||||
# For StringIO, this string is pre-computed anyway by read().
|
|
||||||
# For cStringIO, getvalue() is the only way
|
|
||||||
# to determine the length without read()'ing the whole thing.
|
|
||||||
raw_data = obj.getvalue()
|
|
||||||
if pos == 0:
|
|
||||||
return (len(raw_data), raw_data)
|
|
||||||
else:
|
|
||||||
# We could return raw_data[pos:], but that could drastically
|
|
||||||
# increase memory usage. Better to read it block at a time.
|
|
||||||
size = max(0, len(raw_data) - pos)
|
|
||||||
return (size, None)
|
|
||||||
|
|
||||||
# Handle real files
|
|
||||||
if hasattr(obj, 'fileno'):
|
|
||||||
size = max(0, os.fstat(obj.fileno()).st_size - pos)
|
|
||||||
return (size, None)
|
|
||||||
|
|
||||||
# User-defined object with len()
|
|
||||||
if hasattr(obj, '__len__'):
|
|
||||||
size = max(0, len(obj) - pos)
|
|
||||||
return (size, None)
|
|
||||||
|
|
||||||
# We don't know what kind of stream this is.
|
|
||||||
# To determine the size, we must read the whole thing.
|
|
||||||
raw_data = obj.read()
|
|
||||||
return (len(raw_data), raw_data)
|
|
@ -135,7 +135,7 @@ class DropboxFileSystem(Vfs):
|
|||||||
user_token_key,user_token_secret = self.getToken()
|
user_token_key,user_token_secret = self.getToken()
|
||||||
|
|
||||||
sess = session.DropboxSession(self.APP_KEY,self.APP_SECRET,"app_folder")
|
sess = session.DropboxSession(self.APP_KEY,self.APP_SECRET,"app_folder")
|
||||||
|
utils.log("token:" + user_token_key + ":" + user_token_secret)
|
||||||
if(user_token_key == '' and user_token_secret == ''):
|
if(user_token_key == '' and user_token_secret == ''):
|
||||||
token = sess.obtain_request_token()
|
token = sess.obtain_request_token()
|
||||||
url = sess.build_authorize_url(token)
|
url = sess.build_authorize_url(token)
|
||||||
|
Loading…
Reference in New Issue
Block a user