added pydrive files

This commit is contained in:
Rob Weber 2014-10-09 11:31:38 -05:00
parent b593a55120
commit 0df7364bea
7 changed files with 1162 additions and 0 deletions

View File

View File

@ -0,0 +1,173 @@
class ApiAttribute(object):
"""A data descriptor that sets and returns values."""
def __init__(self, name):
"""Create an instance of ApiAttribute.
:param name: name of this attribute.
:type name: str.
"""
self.name = name
def __get__(self, obj, type=None):
"""Accesses value of this attribute."""
return obj.attr.get(self.name)
def __set__(self, obj, value):
"""Write value of this attribute."""
obj.attr[self.name] = value
if obj.dirty.get(self.name) is not None:
obj.dirty[self.name] = True
def __del__(self, obj):
"""Delete value of this attribute."""
del obj.attr[self.name]
if obj.dirty.get(self.name) is not None:
del obj.dirty[self.name]
class ApiAttributeMixin(object):
"""Mixin to initialize required global variables to use ApiAttribute."""
def __init__(self):
self.attr = {}
self.dirty = {}
class ApiResource(dict):
"""Super class of all api resources.
Inherits and behaves as a python dictionary to handle api resources.
Save clean copy of metadata in self.metadata as a dictionary.
Provides changed metadata elements to efficiently update api resources.
"""
auth = ApiAttribute('auth')
def __init__(self, *args, **kwargs):
"""Create an instance of ApiResource."""
self.update(*args, **kwargs)
def __getitem__(self, key):
"""Overwritten method of dictionary.
:param key: key of the query.
:type key: str.
:returns: value of the query.
"""
return dict.__getitem__(self, key)
def __setitem__(self, key, val):
"""Overwritten method of dictionary.
:param key: key of the query.
:type key: str.
:param val: value of the query.
"""
dict.__setitem__(self, key, val)
def __repr__(self):
"""Overwritten method of dictionary."""
dictrepr = dict.__repr__(self)
return '%s(%s)' % (type(self).__name__, dictrepr)
def update(self, *args, **kwargs):
"""Overwritten method of dictionary."""
for k, v in dict(*args, **kwargs).iteritems():
self[k] = v
def UpdateMetadata(self, metadata=None):
"""Update metadata and mark all of them to be clean."""
if metadata:
self.update(metadata)
self.metadata = dict(self)
def GetChanges(self):
"""Returns changed metadata elements to update api resources efficiently.
:returns: dict -- changed metadata elements.
"""
dirty = {}
for key in self:
if self.metadata.get(key) is None:
dirty[key] = self[key]
elif self.metadata[key] != self[key]:
dirty[key] = self[key]
return dirty
class ApiResourceList(ApiAttributeMixin, ApiResource):
"""Abstract class of all api list resources.
Inherits ApiResource and builds iterator to list any API resource.
"""
metadata = ApiAttribute('metadata')
def __init__(self, auth=None, metadata=None):
"""Create an instance of ApiResourceList.
:param auth: authorized GoogleAuth instance.
:type auth: GoogleAuth.
:param metadata: parameter to send to list command.
:type metadata: dict.
"""
ApiAttributeMixin.__init__(self)
ApiResource.__init__(self)
self.auth = auth
self.UpdateMetadata()
if metadata:
self.update(metadata)
def __iter__(self):
"""Returns iterator object.
:returns: ApiResourceList -- self
"""
return self
def next(self):
"""Make API call to list resources and return them.
Auto updates 'pageToken' everytime it makes API call and
raises StopIteration when it reached the end of iteration.
:returns: list -- list of API resources.
:raises: StopIteration
"""
if 'pageToken' in self and self['pageToken'] is None:
raise StopIteration
result = self._GetList()
self['pageToken'] = self.metadata.get('nextPageToken')
return result
def GetList(self):
"""Get list of API resources.
If 'maxResults' is not specified, it will automatically iterate through
every resources available. Otherwise, it will make API call once and
update 'pageToken'.
:returns: list -- list of API resources.
"""
if self.get('maxResults') is None:
self['maxResults'] = 1000
result = []
for x in self:
result.extend(x)
del self['maxResults']
return result
else:
return self.next()
def _GetList(self):
"""Helper function which actually makes API call.
Should be overwritten.
:raises: NotImplementedError
"""
raise NotImplementedError
def Reset(self):
"""Resets current iteration"""
if 'pageToken' in self:
del self['pageToken']

View File

@ -0,0 +1,432 @@
import socket
import webbrowser
import httplib2
import oauth2client.clientsecrets as clientsecrets
from apiclient.discovery import build
from functools import wraps
from oauth2client.client import FlowExchangeError
from oauth2client.client import AccessTokenRefreshError
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import OOB_CALLBACK_URN
from oauth2client.file import Storage
from oauth2client.file import CredentialsFileSymbolicLinkError
from oauth2client.tools import ClientRedirectHandler
from oauth2client.tools import ClientRedirectServer
from oauth2client.util import scopes_to_string
from .apiattr import ApiAttribute
from .apiattr import ApiAttributeMixin
from .settings import LoadSettingsFile
from .settings import ValidateSettings
from .settings import SettingsError
from .settings import InvalidConfigError
class AuthError(Exception):
"""Base error for authentication/authorization errors."""
class InvalidCredentialsError(IOError):
"""Error trying to read credentials file."""
class AuthenticationRejected(AuthError):
"""User rejected authentication."""
class AuthenticationError(AuthError):
"""General authentication error."""
class RefreshError(AuthError):
"""Access token refresh error."""
def LoadAuth(decoratee):
"""Decorator to check if the auth is valid and loads auth if not."""
@wraps(decoratee)
def _decorated(self, *args, **kwargs):
if self.auth is None: # Initialize auth if needed.
self.auth = GoogleAuth()
if self.auth.access_token_expired:
self.auth.LocalWebserverAuth()
if self.auth.service is None: # Check if drive api is built.
self.auth.Authorize()
return decoratee(self, *args, **kwargs)
return _decorated
def CheckAuth(decoratee):
"""Decorator to check if it requires OAuth2 flow request."""
@wraps(decoratee)
def _decorated(self, *args, **kwargs):
dirty = False
code = None
save_credentials = self.settings.get('save_credentials')
if self.credentials is None and save_credentials:
self.LoadCredentials()
if self.flow is None:
self.GetFlow()
if self.credentials is None:
code = decoratee(self, *args, **kwargs)
dirty = True
else:
if self.access_token_expired:
if self.credentials.refresh_token is not None:
self.Refresh()
else:
code = decoratee(self, *args, **kwargs)
dirty = True
if code is not None:
self.Auth(code)
if dirty and save_credentials:
self.SaveCredentials()
return _decorated
class GoogleAuth(ApiAttributeMixin, object):
"""Wrapper class for oauth2client library in google-api-python-client.
Loads all settings and credentials from one 'settings.yaml' file
and performs common OAuth2.0 related functionality such as authentication
and authorization.
"""
DEFAULT_SETTINGS = {
'client_config_backend': 'file',
'client_config_file': 'client_secrets.json',
'save_credentials': False,
'oauth_scope': ['https://www.googleapis.com/auth/drive']
}
CLIENT_CONFIGS_LIST = ['client_id', 'client_secret', 'auth_uri',
'token_uri', 'revoke_uri', 'redirect_uri']
settings = ApiAttribute('settings')
client_config = ApiAttribute('client_config')
flow = ApiAttribute('flow')
credentials = ApiAttribute('credentials')
http = ApiAttribute('http')
service = ApiAttribute('service')
def __init__(self, settings_file='settings.yaml'):
"""Create an instance of GoogleAuth.
This constructor just sets the path of settings file.
It does not actually read the file.
:param settings_file: path of settings file. 'settings.yaml' by default.
:type settings_file: str.
"""
ApiAttributeMixin.__init__(self)
self.client_config = {}
try:
self.settings = LoadSettingsFile(settings_file)
except SettingsError:
self.settings = self.DEFAULT_SETTINGS
else:
if self.settings is None:
self.settings = self.DEFAULT_SETTINGS
else:
ValidateSettings(self.settings)
@property
def access_token_expired(self):
"""Checks if access token doesn't exist or is expired.
:returns: bool -- True if access token doesn't exist or is expired.
"""
if self.credentials is None:
return True
return self.credentials.access_token_expired
@CheckAuth
def LocalWebserverAuth(self, host_name='localhost',
port_numbers=[8080, 8090]):
"""Authenticate and authorize from user by creating local webserver and
retrieving authentication code.
This function is not for webserver application. It creates local webserver
for user from standalone application.
:param host_name: host name of the local webserver.
:type host_name: str.
:param port_numbers: list of port numbers to be tried to used.
:type port_numbers: list.
:returns: str -- code returned from local webserver
:raises: AuthenticationRejected, AuthenticationError
"""
success = False
port_number = 0
for port in port_numbers:
port_number = port
try:
httpd = ClientRedirectServer((host_name, port), ClientRedirectHandler)
except socket.error, e:
pass
else:
success = True
break
if success:
oauth_callback = 'http://%s:%s/' % (host_name, port_number)
else:
print 'Failed to start a local webserver. Please check your firewall'
print 'settings and locally running programs that may be blocking or'
print 'using configured ports. Default ports are 8080 and 8090.'
raise AuthenticationError()
self.flow.redirect_uri = oauth_callback
authorize_url = self.GetAuthUrl()
webbrowser.open(authorize_url, new=1, autoraise=True)
print 'Your browser has been opened to visit:'
print
print ' ' + authorize_url
print
httpd.handle_request()
if 'error' in httpd.query_params:
print 'Authentication request was rejected'
raise AuthenticationRejected('User rejected authentication')
if 'code' in httpd.query_params:
return httpd.query_params['code']
else:
print 'Failed to find "code" in the query parameters of the redirect.'
print 'Try command-line authentication'
raise AuthenticationError('No code found in redirect')
@CheckAuth
def CommandLineAuth(self):
"""Authenticate and authorize from user by printing authentication url
retrieving authentication code from command-line.
:returns: str -- code returned from commandline.
"""
self.flow.redirect_uri = OOB_CALLBACK_URN
authorize_url = self.GetAuthUrl()
print 'Go to the following link in your browser:'
print
print ' ' + authorize_url
print
return raw_input('Enter verification code: ').strip()
def LoadCredentials(self, backend=None):
"""Loads credentials or create empty credentials if it doesn't exist.
:param backend: target backend to save credential to.
:type backend: str.
:raises: InvalidConfigError
"""
if backend is None:
backend = self.settings.get('save_credentials_backend')
if backend is None:
raise InvalidConfigError('Please specify credential backend')
if backend == 'file':
self.LoadCredentialsFile()
else:
raise InvalidConfigError('Unknown save_credentials_backend')
def LoadCredentialsFile(self, credentials_file=None):
"""Loads credentials or create empty credentials if it doesn't exist.
Loads credentials file from path in settings if not specified.
:param credentials_file: path of credentials file to read.
:type credentials_file: str.
:raises: InvalidConfigError, InvalidCredentialsError
"""
if credentials_file is None:
credentials_file = self.settings.get('save_credentials_file')
if credentials_file is None:
raise InvalidConfigError('Please specify credentials file to read')
try:
storage = Storage(credentials_file)
self.credentials = storage.get()
except CredentialsFileSymbolicLinkError:
raise InvalidCredentialsError('Credentials file cannot be symbolic link')
def SaveCredentials(self, backend=None):
"""Saves credentials according to specified backend.
If you have any specific credentials backend in mind, don't use this
function and use the corresponding function you want.
:param backend: backend to save credentials.
:type backend: str.
:raises: InvalidConfigError
"""
if backend is None:
backend = self.settings.get('save_credentials_backend')
if backend is None:
raise InvalidConfigError('Please specify credential backend')
if backend == 'file':
self.SaveCredentialsFile()
else:
raise InvalidConfigError('Unknown save_credentials_backend')
def SaveCredentialsFile(self, credentials_file=None):
"""Saves credentials to the file in JSON format.
:param credentials_file: destination to save file to.
:type credentials_file: str.
:raises: InvalidConfigError, InvalidCredentialsError
"""
if self.credentials is None:
raise InvalidCredentialsError('No credentials to save')
if credentials_file is None:
credentials_file = self.settings.get('save_credentials_file')
if credentials_file is None:
raise InvalidConfigError('Please specify credentials file to read')
try:
storage = Storage(credentials_file)
storage.put(self.credentials)
self.credentials.set_store(storage)
except CredentialsFileSymbolicLinkError:
raise InvalidCredentialsError('Credentials file cannot be symbolic link')
def LoadClientConfig(self, backend=None):
"""Loads client configuration according to specified backend.
If you have any specific backend to load client configuration from in mind,
don't use this function and use the corresponding function you want.
:param backend: backend to load client configuration from.
:type backend: str.
:raises: InvalidConfigError
"""
if backend is None:
backend = self.settings.get('client_config_backend')
if backend is None:
raise InvalidConfigError('Please specify client config backend')
if backend == 'file':
self.LoadClientConfigFile()
elif backend == 'settings':
self.LoadClientConfigSettings()
else:
raise InvalidConfigError('Unknown client_config_backend')
def LoadClientConfigFile(self, client_config_file=None):
"""Loads client configuration file downloaded from APIs console.
Loads client config file from path in settings if not specified.
:param client_config_file: path of client config file to read.
:type client_config_file: str.
:raises: InvalidConfigError
"""
if client_config_file is None:
client_config_file = self.settings['client_config_file']
try:
client_type, client_info = clientsecrets.loadfile(client_config_file)
except clientsecrets.InvalidClientSecretsError, error:
raise InvalidConfigError('Invalid client secrets file %s' % error)
if not client_type in (clientsecrets.TYPE_WEB,
clientsecrets.TYPE_INSTALLED):
raise InvalidConfigError('Unknown client_type of client config file')
try:
config_index = ['client_id', 'client_secret', 'auth_uri', 'token_uri']
for config in config_index:
self.client_config[config] = client_info[config]
self.client_config['revoke_uri'] = client_info.get('revoke_uri')
self.client_config['redirect_uri'] = client_info['redirect_uris'][0]
except KeyError:
raise InvalidConfigError('Insufficient client config in file')
def LoadClientConfigSettings(self):
"""Loads client configuration from settings file.
:raises: InvalidConfigError
"""
for config in self.CLIENT_CONFIGS_LIST:
try:
self.client_config[config] = self.settings['client_config'][config]
except KeyError:
print config
raise InvalidConfigError('Insufficient client config in settings')
def GetFlow(self):
"""Gets Flow object from client configuration.
:raises: InvalidConfigError
"""
if not all(config in self.client_config \
for config in self.CLIENT_CONFIGS_LIST):
self.LoadClientConfig()
constructor_kwargs = {
'redirect_uri': self.client_config['redirect_uri'],
'auth_uri': self.client_config['auth_uri'],
'token_uri': self.client_config['token_uri'],
}
if self.client_config['revoke_uri'] is not None:
constructor_kwargs['revoke_uri'] = self.client_config['revoke_uri']
self.flow = OAuth2WebServerFlow(
self.client_config['client_id'],
self.client_config['client_secret'],
scopes_to_string(self.settings['oauth_scope']),
**constructor_kwargs)
if self.settings.get('get_refresh_token'):
self.flow.params.update({'access_type': 'offline'})
def Refresh(self):
"""Refreshes the access_token.
:raises: RefreshError
"""
if self.credentials is None:
raise RefreshError('No credential to refresh.')
if self.credentials.refresh_token is None:
raise RefreshError('No refresh_token found.'
'Please set access_type of OAuth to offline.')
if self.http is None:
self.http = httplib2.Http()
try:
self.credentials.refresh(self.http)
except AccessTokenRefreshError, error:
raise RefreshError('Access token refresh failed: %s' % error)
def GetAuthUrl(self, keys = None):
"""Creates authentication url where user visits to grant access.
:returns: str -- Authentication url.
"""
if(keys != None):
#update some of the settings in the client_config dict
self.client_config['client_id'] = keys['client_id']
self.client_config['client_secret'] = keys['client_secret']
if self.flow is None:
self.GetFlow()
return self.flow.step1_get_authorize_url()
def Auth(self, code):
"""Authenticate, authorize, and build service.
:param code: Code for authentication.
:type code: str.
:raises: AuthenticationError
"""
self.Authenticate(code)
self.Authorize()
def Authenticate(self, code):
"""Authenticates given authentication code back from user.
:param code: Code for authentication.
:type code: str.
:raises: AuthenticationError
"""
if self.flow is None:
self.GetFlow()
try:
self.credentials = self.flow.step2_exchange(code)
except FlowExchangeError, e:
raise AuthenticationError('OAuth2 code exchange failed: %s' % e)
print 'Authentication successful.'
def Authorize(self):
"""Authorizes and builds service.
:raises: AuthenticationError
"""
if self.http is None:
self.http = httplib2.Http()
if self.access_token_expired:
raise AuthenticationError('No valid credentials provided to authorize')
self.http = self.credentials.authorize(self.http)
self.service = build('drive', 'v2', http=self.http)

View File

@ -0,0 +1,38 @@
from .apiattr import ApiAttributeMixin
from .files import GoogleDriveFile
from .files import GoogleDriveFileList
class GoogleDrive(ApiAttributeMixin, object):
"""Main Google Drive class."""
def __init__(self, auth=None):
"""Create an instance of GoogleDrive.
:param auth: authorized GoogleAuth instance.
:type auth: pydrive.auth.GoogleAuth.
"""
ApiAttributeMixin.__init__(self)
self.auth = auth
def CreateFile(self, metadata=None):
"""Create an instance of GoogleDriveFile with auth of this instance.
This method would not upload a file to GoogleDrive.
:param metadata: file resource to initialize GoogleDriveFile with.
:type metadata: dict.
:returns: pydrive.files.GoogleDriveFile -- initialized with auth of this instance.
"""
return GoogleDriveFile(auth=self.auth, metadata=metadata)
def ListFile(self, param=None):
"""Create an instance of GoogleDriveFileList with auth of this instance.
This method will not fetch from Files.List().
:param param: parameter to be sent to Files.List().
:type param: dict.
:returns: pydrive.files.GoogleDriveFileList -- initialized with auth of this instance.
"""
return GoogleDriveFileList(auth=self.auth, param=param)

View File

@ -0,0 +1,322 @@
import io
import mimetypes
from apiclient import errors
from apiclient.http import MediaIoBaseUpload
from functools import wraps
from .apiattr import ApiAttribute
from .apiattr import ApiAttributeMixin
from .apiattr import ApiResource
from .apiattr import ApiResourceList
from .auth import LoadAuth
class FileNotUploadedError(RuntimeError):
"""Error trying to access metadata of file that is not uploaded."""
class ApiRequestError(IOError):
"""Error while making any API requests."""
class FileNotDownloadableError(RuntimeError):
"""Error trying to download file that is not downloadable."""
def LoadMetadata(decoratee):
"""Decorator to check if the file has metadata and fetches it if not.
:raises: ApiRequestError, FileNotUploadedError
"""
@wraps(decoratee)
def _decorated(self, *args, **kwargs):
if not self.uploaded:
self.FetchMetadata()
return decoratee(self, *args, **kwargs)
return _decorated
class GoogleDriveFileList(ApiResourceList):
"""Google Drive FileList instance.
Equivalent to Files.list() in Drive APIs.
"""
def __init__(self, auth=None, param=None):
"""Create an instance of GoogleDriveFileList."""
super(GoogleDriveFileList, self).__init__(auth=auth, metadata=param)
@LoadAuth
def _GetList(self):
"""Overwritten method which actually makes API call to list files.
:returns: list -- list of pydrive.files.GoogleDriveFile.
"""
self.metadata = self.auth.service.files().list(**dict(self)).execute()
result = []
for file_metadata in self.metadata['items']:
tmp_file = GoogleDriveFile(
auth=self.auth,
metadata=file_metadata,
uploaded=True)
result.append(tmp_file)
return result
class GoogleDriveFile(ApiAttributeMixin, ApiResource):
"""Google Drive File instance.
Inherits ApiResource which inherits dict.
Can access and modify metadata like dictionary.
"""
content = ApiAttribute('content')
uploaded = ApiAttribute('uploaded')
metadata = ApiAttribute('metadata')
def __init__(self, auth=None, metadata=None, uploaded=False):
"""Create an instance of GoogleDriveFile.
:param auth: authorized GoogleAuth instance.
:type auth: pydrive.auth.GoogleAuth
:param metadata: file resource to initialize GoogleDirveFile with.
:type metadata: dict.
:param uploaded: True if this file is confirmed to be uploaded.
:type uploaded: bool.
"""
ApiAttributeMixin.__init__(self)
ApiResource.__init__(self)
self.metadata = {}
self.dirty = {'content': False}
self.auth = auth
self.uploaded = uploaded
if uploaded:
self.UpdateMetadata(metadata)
elif metadata:
self.update(metadata)
def __getitem__(self, key):
"""Overwrites manner of accessing Files resource.
If this file instance is not uploaded and id is specified,
it will try to look for metadata with Files.get().
:param key: key of dictionary query.
:type key: str.
:returns: value of Files resource
:raises: KeyError, FileNotUploadedError
"""
try:
return dict.__getitem__(self, key)
except KeyError, e:
if self.uploaded:
raise KeyError(e)
if self.get('id'):
self.FetchMetadata()
return dict.__getitem__(self, key)
else:
raise FileNotUploadedError()
def SetContentString(self, content):
"""Set content of this file to be a string.
Creates io.BytesIO instance of utf-8 encoded string.
Sets mimeType to be 'text/plain' if not specified.
:param content: content of the file in string.
:type content: str.
"""
self.content = io.BytesIO(content.encode('utf-8'))
if self.get('mimeType') is None:
self['mimeType'] = 'text/plain'
def SetContentFile(self, filename):
"""Set content of this file from a file.
Opens the file specified by this method.
Will be read, uploaded, and closed by Upload() method.
Sets metadata 'title' and 'mimeType' automatically if not specified.
:param filename: name of the file to be uploaded.
:type filename: str.
"""
self.content = open(filename, 'rb')
if self.get('title') is None:
self['title'] = filename
if self.get('mimeType') is None:
self['mimeType'] = mimetypes.guess_type(filename)[0]
def GetContentString(self):
"""Get content of this file as a string.
:returns: str -- utf-8 decoded content of the file
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
"""
if self.content is None or type(self.content) is not io.BytesIO:
self.FetchContent()
return self.content.getvalue().decode('utf-8')
def GetContentFile(self, filename, mimetype=None):
"""Save content of this file as a local file.
:param filename: name of the file to write to.
:type filename: str.
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
"""
if self.content is None or type(self.content) is not io.BytesIO:
self.FetchContent(mimetype)
f = open(filename, 'wb')
f.write(self.content.getvalue())
f.close()
@LoadAuth
def FetchMetadata(self):
"""Download file's metadata from id using Files.get().
:raises: ApiRequestError, FileNotUploadedError
"""
file_id = self.metadata.get('id') or self.get('id')
if file_id:
try:
metadata = self.auth.service.files().get(fileId=file_id).execute()
except errors.HttpError, error:
raise ApiRequestError(error)
else:
self.uploaded = True
self.UpdateMetadata(metadata)
else:
raise FileNotUploadedError()
@LoadMetadata
def FetchContent(self, mimetype=None):
"""Download file's content from download_url.
:raises: ApiRequestError, FileNotUploadedError, FileNotDownloadableError
"""
download_url = self.metadata.get('downloadUrl')
if download_url:
self.content = io.BytesIO(self._DownloadFromUrl(download_url))
self.dirty['content'] = False
return
export_links = self.metadata.get('exportLinks')
if export_links and export_links.get(mimetype):
self.content = io.BytesIO(
self._DownloadFromUrl(export_links.get(mimetype)))
self.dirty['content'] = False
return
raise FileNotDownloadableError(
'No downloadLink/exportLinks for mimetype found in metadata')
def Upload(self, param=None):
"""Upload/update file by choosing the most efficient method.
:param param: additional parameter to upload file.
:type param: dict.
:raises: ApiRequestError
"""
if self.uploaded or self.get('id') is not None:
if self.dirty['content']:
self._FilesUpdate(param=param)
else:
self._FilesPatch(param=param)
else:
self._FilesInsert(param=param)
def Delete(self):
if self.get('id') is not None:
self.auth.service.files().delete(fileId=self.get('id')).execute()
@LoadAuth
def _FilesInsert(self, param=None):
"""Upload a new file using Files.insert().
:param param: additional parameter to upload file.
:type param: dict.
:raises: ApiRequestError
"""
if param is None:
param = {}
param['body'] = self.GetChanges()
try:
if self.dirty['content']:
param['media_body'] = self._BuildMediaBody()
metadata = self.auth.service.files().insert(**param).execute()
except errors.HttpError, error:
raise ApiRequestError(error)
else:
self.uploaded = True
self.dirty['content'] = False
self.UpdateMetadata(metadata)
@LoadAuth
@LoadMetadata
def _FilesUpdate(self, param=None):
"""Update metadata and/or content using Files.Update().
:param param: additional parameter to upload file.
:type param: dict.
:raises: ApiRequestError, FileNotUploadedError
"""
if param is None:
param = {}
param['body'] = self.GetChanges()
param['fileId'] = self.metadata.get('id')
try:
if self.dirty['content']:
param['media_body'] = self._BuildMediaBody()
metadata = self.auth.service.files().update(**param).execute()
except errors.HttpError, error:
raise ApiRequestError(error)
else:
self.uploaded = True
self.dirty['content'] = False
self.UpdateMetadata(metadata)
@LoadAuth
@LoadMetadata
def _FilesPatch(self, param=None):
"""Update metadata using Files.Patch().
:param param: additional parameter to upload file.
:type param: dict.
:raises: ApiRequestError, FileNotUploadedError
"""
if param is None:
param = {}
param['body'] = self.GetChanges()
param['fileId'] = self.metadata.get('id')
try:
metadata = self.auth.service.files().patch(**param).execute()
except errors.HttpError, error:
raise ApiRequestError(error)
else:
self.UpdateMetadata(metadata)
def _BuildMediaBody(self):
"""Build MediaIoBaseUpload to get prepared to upload content of the file.
Sets mimeType as 'application/octet-stream' if not specified.
:returns: MediaIoBaseUpload -- instance that will be used to upload content.
"""
if self.get('mimeType') is None:
self['mimeType'] = 'application/octet-stream'
return MediaIoBaseUpload(self.content, self['mimeType'])
@LoadAuth
def _DownloadFromUrl(self, url):
"""Download file from url using provided credential.
:param url: link of the file to download.
:type url: str.
:returns: str -- content of downloaded file in string.
:raises: ApiRequestError
"""
resp, content = self.auth.service._http.request(url)
if resp.status != 200:
raise ApiRequestError('Cannot download file: %s' % resp)
return content

View File

@ -0,0 +1,190 @@
from yaml import load
from yaml import YAMLError
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
SETTINGS_FILE = 'settings.yaml'
SETTINGS_STRUCT = {
'client_config_backend': {
'type': str,
'required': True,
'default': 'file',
'dependency': [
{
'value': 'file',
'attribute': ['client_config_file']
},
{
'value': 'settings',
'attribute': ['client_config']
}
]
},
'save_credentials': {
'type': bool,
'required': True,
'default': False,
'dependency': [
{
'value': True,
'attribute': ['save_credentials_backend']
}
]
},
'get_refresh_token': {
'type': bool,
'required': False,
'default': False
},
'client_config_file': {
'type': str,
'required': False,
'default': 'client_secrets.json'
},
'save_credentials_backend': {
'type': str,
'required': False,
'dependency': [
{
'value': 'file',
'attribute': ['save_credentials_file']
}
]
},
'client_config': {
'type': dict,
'required': False,
'struct': {
'client_id': {
'type': str,
'required': True
},
'client_secret': {
'type': str,
'required': True
},
'auth_uri': {
'type': str,
'required': True,
'default': 'https://accounts.google.com/o/oauth2/auth'
},
'token_uri': {
'type': str,
'required': True,
'default': 'https://accounts.google.com/o/oauth2/token'
},
'redirect_uri': {
'type': str,
'required': True,
'default': 'urn:ietf:wg:oauth:2.0:oob'
},
'revoke_uri': {
'type': str,
'required': True,
'default': None
}
}
},
'oauth_scope': {
'type': list,
'required': True,
'struct': str,
'default': ['https://www.googleapis.com/auth/drive']
},
'save_credentials_file': {
'type': str,
'required': False,
}
}
class SettingsError(IOError):
"""Error while loading/saving settings"""
class InvalidConfigError(IOError):
"""Error trying to read client configuration."""
def LoadSettingsFile(filename=SETTINGS_FILE):
"""Loads settings file in yaml format given file name.
:param filename: path for settings file. 'settings.yaml' by default.
:type filename: str.
:raises: SettingsError
"""
try:
stream = file(filename, 'r')
data = load(stream, Loader=Loader)
except (YAMLError, IOError), e:
raise SettingsError(e)
return data
def ValidateSettings(data):
"""Validates if current settings is valid.
:param data: dictionary containing all settings.
:type data: dict.
:raises: InvalidConfigError
"""
_ValidateSettingsStruct(data, SETTINGS_STRUCT)
def _ValidateSettingsStruct(data, struct):
"""Validates if provided data fits provided structure.
:param data: dictionary containing settings.
:type data: dict.
:param struct: dictionary containing structure information of settings.
:type struct: dict.
:raises: InvalidConfigError
"""
# Validate required elements of the setting.
for key in struct:
if struct[key]['required']:
_ValidateSettingsElement(data, struct, key)
def _ValidateSettingsElement(data, struct, key):
"""Validates if provided element of settings data fits provided structure.
:param data: dictionary containing settings.
:type data: dict.
:param struct: dictionary containing structure information of settings.
:type struct: dict.
:param key: key of the settings element to validate.
:type key: str.
:raises: InvalidConfigError
"""
# Check if data exists. If not, check if default value exists.
value = data.get(key)
data_type = struct[key]['type']
if value is None:
try:
default = struct[key]['default']
except KeyError:
raise InvalidConfigError('Missing required setting %s' % key)
else:
data[key] = default
# If data exists, Check type of the data
elif type(value) is not data_type:
raise InvalidConfigError('Setting %s should be type %s' % (key, data_type))
# If type of this data is dict, check if structure of the data is valid.
if data_type is dict:
_ValidateSettingsStruct(data[key], struct[key]['struct'])
# If type of this data is list, check if all values in the list is valid.
elif data_type is list:
for element in data[key]:
if type(element) is not struct[key]['struct']:
raise InvalidConfigError('Setting %s should be list of %s' %
(key, struct[key]['struct']))
# Check dependency of this attribute.
dependencies = struct[key].get('dependency')
if dependencies:
for dependency in dependencies:
if value == dependency['value']:
for reqkey in dependency['attribute']:
_ValidateSettingsElement(data, struct, reqkey)

View File

@ -0,0 +1,7 @@
client_config_backend: 'settings'
client_config:
client_id: "blank"
client_secret: "blank"
get_refresh_token: True
oauth_scope:
- "https://www.googleapis.com/auth/drive"