diff --git a/.gitignore b/.gitignore index 1dbc687..8cca8a7 100644 --- a/.gitignore +++ b/.gitignore @@ -60,3 +60,15 @@ target/ #Ipython Notebook .ipynb_checkpoints + +#Vim +# swap +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags diff --git a/drivepy/__init__.py b/drivepy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/drivepy/auth.py b/drivepy/auth.py new file mode 100644 index 0000000..dc0cf32 --- /dev/null +++ b/drivepy/auth.py @@ -0,0 +1,31 @@ +from oauth2client.service_account import ServiceAccountCredentials +import json +import drivepy.config as config +from httplib2 import Http + +class ServiceAuthentication(object): + """Service Authorization for server-server OAuth2 support + + Fields: + credentials: ServiceAccountCredentials object loaded from the json file + authorization: HTTP authorization object + """ + def __init__(self, auth_file): + """Creates a ServiceAuthentication object + + Args: + auth_file: a json file location that contains Google API credentials + Returns: + An instance of the ServiceAuthentication object + """ + credentials = None + if isinstance(auth_file, file): + auth_dict = json.load(auth_file) + credentials = ServiceAccountCredentials.from_json_keyfile_dict(auth_dict, + scopes=[config.GOOGLE_DRIVE_SCOPE]) + else: + credentials = ServiceAccountCredentials.from_json_keyfile_name(auth_file, + scopes=[config.GOOGLE_DRIVE_SCOPE]) + + self.credentials = credentials + self.authorization = self.credentials.authorize(Http()) diff --git a/drivepy/config.py b/drivepy/config.py new file mode 100644 index 0000000..c08157b --- /dev/null +++ b/drivepy/config.py @@ -0,0 +1,114 @@ +import mimetypes +import os + +GOOGLE_DRIVE_SCOPE = 'https://www.googleapis.com/auth/drive' + +ALL_FILE_FIELDS = 'files,kind,nextPageToken' + +ALL_SINGLE_FILE_FIELDS = 'appProperties,capabilities,contentHints,createdTime,description,explicitlyTrashed,fileExtension,folderColorRgb,fullFileExtension,headRevisionId,iconLink,id,imageMediaMetadata,isAppAuthorized,kind,lastModifyingUser,md5Checksum,mimeType,modifiedByMeTime,modifiedTime,name,originalFilename,ownedByMe,owners,parents,permissions,properties,quotaBytesUsed,shared,sharedWithMeTime,sharingUser,size,spaces,starred,thumbnailLink,trashed,version,videoMediaMetadata,viewedByMe,viewedByMeTime,viewersCanCopyContent,webContentLink,webViewLink,writersCanShare' + +ALL_PERMISSION_FIELDS = 'allowFileDiscovery,displayName,domain,emailAddress,id,kind,photoLink,role,type' + +class PermissionRole(object): + Reader = 'reader' + Writer = 'writer' + Owner = 'owner' + Commenter = 'commenter' + +class PermissionType(object): + User = 'user' + Group = 'group' + Domain = 'domain' + Anyone = 'anyone' + +class DownloadType(object): + # Documents + HTML = 'text/html' + RichText = 'text/rtf' + OpenOfficeDoc = 'application/vnd.oasis.opendocument.text' + WordDoc = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + + # Spreadsheets + CSV = 'text/csv' + Excel = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + OpenOfficeSheet = 'application/x-vnd.oasis.opendocument.spreadsheet' + + # Drawings + JPEG = 'image/jpeg' + PNG = 'image/png' + SVG = 'image/svg+xml' + + # Presentations + PowerPoint = 'application/vnd.openxmlformats-officedocument.presentationml.presentation' + + # Scripts + JSON = 'application/vnd.google-apps.script+json' + + # All except Scripts + PDF = 'application/pdf' + + # All except scripts and sheets + PlainText = 'text/plain' + +class MimeTypes(object): + Audio = 'application/vnd.google-apps.audio' + Document = 'application/vnd.google-apps.document' + Drawing = 'application/vnd.google-apps.drawing' + File = 'application/vnd.google-apps.file' + Folder = 'application/vnd.google-apps.folder' + Form = 'application/vnd.google-apps.form' + FusionTable = 'application/vnd.google-apps.fusiontable' + Map = 'application/vnd.google-apps.map' + Photo = 'application/vnd.google-apps.photo' + Presentation = 'application/vnd.google-apps.presentation' + Script = 'application/vnd.google-apps.script' + Sites = 'application/vnd.google-apps.sites' + Spreadsheet = 'application/vnd.google-apps.spreadsheet' + Unknown = 'application/vnd.google-apps.unknown' + Video = 'application/vnd.google-apps.video' + + TypeDict = {} + + types = { + Audio: ['mp3', 'wav', 'aac', 'ogg', 'wma'], + Document: ['txt', 'doc', 'docx', 'log', 'odt', 'rtf'], + Folder: [''], + Presentation: ['ppt', 'pptx'], + Spreadsheet: ['csv', 'xls', 'xlsx', 'xlt', 'xml', 'ods'], + Video: ['flv', 'mp4', 'mpg', 'mov', 'mkv', 'avi'] + } + + for mime, exts in types.items(): + for ext in exts: + TypeDict[ext] = mime + + DownloadTypeDict = { + Document: DownloadType.WordDoc, + Spreadsheet: DownloadType.Excel, + Drawing: DownloadType.PDF, + Presentation: DownloadType.PowerPoint, + Script: DownloadType.JSON + } + + @staticmethod + def get_download_type(mime_type): + """Tries to guess the download type based on the mime_type.""" + return MimeTypes.DownloadTypeDict.get(mime_type, 'text/plain') + + @staticmethod + def guess_type(path): + """Guesses the mimetype of a file extension + + Args: + path: the file name or path + + Returns: + A mimetype string + """ + ext = os.path.split(path)[-1].replace('.', '') + guess = MimeTypes.TypeDict.get(ext) + + if guess is None: + guess = mimetypes.guess_type(path)[0] or MimeTypes.File + + return guess diff --git a/drivepy/drive.py b/drivepy/drive.py new file mode 100644 index 0000000..f4039ea --- /dev/null +++ b/drivepy/drive.py @@ -0,0 +1,136 @@ +from googleapiclient.discovery import build +import drivepy.config as config +from drivepy.config import MimeTypes +from pprint import pprint +from drivepy.files import GoogleFile + +class GoogleDrive(object): + def __init__(self, authentication): + """Create an instance of GoogleDrive + + Args: + authentication: an authentication object to create the service + (eg: auth.ServiceAccountAuthentication) + Returns: + A GoogleDrive instance + """ + self.authentication = authentication + + self.service = build('drive', 'v3', http=self.authentication.authorization) + + def list_files(self, **params): + """List files in a way specified by params + + Args: + params: a dict-like object, all available parameters for + the REST API "files" may be specified here + Returns: + A list of files in the drive. + """ + fields = params.pop('fields', config.ALL_FILE_FIELDS) + files = self.service.files().list(fields=fields, + **params).execute().get('files') + return files + + def create_file_from_path(self, path, content=None, mime_type=None, permissions=None): + """Creates a file on google drive from a path + + Args: + path: a file or folder path + content: a string with the contents of the new file + mime_type: the type of the file to be created + permissions: a list of dict-like objects that describe permissions + (eg. {'email': 'jap@hotmail.com', + 'role': PermissionRole.Writer, + 'type': PermissionType.User} + + Returns: + A list of the newly created file and it's directories + + Raises: + A GoogleFileException if the permissions object is specified and + there is no 'email' key + """ + file_sections = path.split('/') + + folders = file_sections[:-1] + file_name = file_sections[-1] + + parent_id = None + + parent_folders = [] + + for folder_name in folders: + params = {} + + if parent_id: + params['body'] = {'parents': [parent_id]} + + google_file = GoogleFile.create(self.service, folder_name, + mime_type=MimeTypes.Folder, + **params) + + parent_id = google_file.meta_data['id'] + parent_folders.append(google_file) + + main_params = {} + + if parent_id: + main_params['body'] = {'parents': [parent_id]} + + main_file = GoogleFile.create(self.service, file_name, + mime_type=mime_type, + **main_params) + + result = parent_folders + [main_file] + + if permissions: + for permission in permissions: + email = permission.pop('email', '') + if not email: + raise Exception('There must be an email key in the permissions object!') + for google_file in result: + google_file.give_permission(email, **permission) + + return result + + + def get_file_from_path(self, path): + """ + Parses a file path into a file query and gets + the corresponding file objects associated + + Args: + path: a file path from the root of Google Drive + in the form "Folder/SubFolder/file.ext" + Returns: + A list of file objects + """ + file_sections = path.split('/') + + folders = file_sections[:-1] + file_name = file_sections[-1] + + parent_id = None + + for folder in folders: + if parent_id: + folds = self.list_files(q='name = "{}" and "{}" in parents'.format(folder, parent_id), + fields='files/id') + else: + folds = self.list_files(q='name = "{}"'.format(folder), fields='files/id') + + if folds: + fold = folds[0] + parent_id = fold['id'] + else: + raise Exception('File {} does not exist!'.format(path)) + + files = self.list_files(q='name = "{}" and "{}" in parents'.format(file_name, parent_id)) + + result_files = [] + + for file_meta in files: + result_files.append(GoogleFile(self.service, file_meta)) + + return result_files diff --git a/drivepy/files.py b/drivepy/files.py new file mode 100644 index 0000000..f8af8bd --- /dev/null +++ b/drivepy/files.py @@ -0,0 +1,183 @@ +from googleapiclient.http import MediaIoBaseUpload, MediaIoBaseDownload +from drivepy.config import MimeTypes, PermissionType, PermissionRole +import drivepy.config as config +import io + +class GoogleFile(object): + """An object representing a file on GoogleDrive""" + def __init__(self, service, meta_data): + """Create an instance of a GoogleFile + + Args: + service: the api service object + metadata: the metadata for the file + + Returns: + An instance of GoogleFile + """ + + self.meta_data = meta_data + self.service = service + self.content = '' + + @staticmethod + def create(service, name, mime_type=None, content=None, body=None, **params): + """Creates a new file in GoogleDrive + + Args: + service: the API service object + name: the name of the file, including the extension + mime_type: the mimetype of the file. See config.MimeTypes + content: the content string of the file + body: a dict-like object, describes additional request body args + params: any additional parameters to send to the API + + Returns: + A GoogleFile object + """ + fields = config.ALL_SINGLE_FILE_FIELDS + + body = body or {} + body.update({'name': name}) + + if mime_type is not None: + body['mimeType'] = mime_type + else: + guess = MimeTypes.guess_type(name) + if guess is not None: + body['mimeType'] = guess + + params = {} + + if content: + cont = io.BytesIO(content.encode('utf-8')) + media_body = MediaIoBaseUpload(cont, + body['mimeType'], + resumable=True) + params['media_body'] = media_body + + meta_data = service.files().create(body=body, fields=fields, **params).execute() + + google_file = GoogleFile(service, meta_data) + google_file.content = content + + return google_file + + def refresh(self): + """Gets the meta data of the file from Google Drive + + Returns: + The meta data of the file + """ + fields = config.ALL_SINGLE_FILE_FIELDS + self.meta_data = self.service.files().get(fileId=self.meta_data['id'], fields=fields).execute() + return self.meta_data + + def give_permission(self, email, send_notification=False, + email_message='', fields=None, **params): + """Gives permission to an email to view a file + + Args: + email: a valid Google email address + send_notification: whether to send an email about the share + email_message: the contents of the email + fields: a string with comma separated field names to include + params: a dict-like object of parameters that can be passed to the api call + + Returns: + Meta data from the api call + """ + new_params = {'role': PermissionRole.Reader, + 'type': PermissionType.User} + new_params.update(params) + new_params['emailAddress'] = email + + fields = fields or config.ALL_PERMISSION_FIELDS + + file_id = self.meta_data.get('id') + + permission_service = self.service.permissions() + + email_params = {} + if email_message: + email_params['emailMessage'] = email_message + + res = permission_service.create( + fileId=file_id, + sendNotificationEmail=send_notification, + fields=fields, + body=new_params, + **email_params + ).execute() + + self.refresh() + + return res + + def download(self, mime_type=None): + """Download the content of the file from Google Drive + + Args: + mime_type: the mime type of the file to download. + see here: + https://developers.google.com/drive/v3/web/manage-downloads#downloading_google_documents + + Returns: + The content of the file + """ + + if mime_type is None: + download_type = MimeTypes.get_download_type(self.meta_data['mimeType']) + else: + download_type = mime_type + + req = self.service.files().export_media(fileId=self.meta_data['id'], + mimeType=download_type) + data = io.BytesIO() + downloader = MediaIoBaseDownload(data, req) + done = False + while not done: + _, done = downloader.next_chunk() + + data.seek(0) + self.content = data.read() + + return self.content + + def delete(self): + """Delete the file on Google Drive + + Returns: + Metadata for the deleted file + """ + return self.service.files().delete(fileId=self.meta_data['id']).execute() + + def update(self, meta_data, content=None): + """Update the current file meta data and contents on Google Drive + + Args: + meta_data: A dict-like object, updates the current meta data from + this data + contents: A string that replaces the contents of the current file + on Google Drive + + Returns: + Metadata from the file update + """ + self.meta_data.update(meta_data) + file_id = self.meta_data['id'] + + params = {} + + if content: + cont = io.BytesIO(content.encode('utf-8')) + media_body = MediaIoBaseUpload(cont, + self.meta_data['mimeType'], + resumable=True) + params['media_body'] = media_body + self.content = content + + res = self.service.files().update(fileId=file_id, + body=self.meta_data, + **params).execute() + return res diff --git a/drivepy/test/__init__.py b/drivepy/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..edc788d --- /dev/null +++ b/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='PyDrive', + version='0.1.0', + author='Joey Yakimowich-Payne', + author_email='jyapayne@gmail.com', + packages=['drivepy', 'drivepy.test'], + url='https://github.com/jyapayne/DrivePy/', + license='LICENSE', + description='Pythonic Google Drive API Bindings.', + long_description=open('README.md').read(), + install_requires=[ + 'google-api-python-client >= 1.5.0' + ] +)