diff --git a/README.rst b/README.rst index 6ea3836..e8f2f91 100644 --- a/README.rst +++ b/README.rst @@ -247,6 +247,33 @@ Or, if you're downloading a particularly large file, you may want to use the ``s >>> with open('downloaded_file.zip', 'wb') as opened_file: opened_file.write(download.raw.read()) +File Storage (iCloud Drive) +=========================== + +You can access your iCloud Drive using an API identical to the Ubiquity one described in the previous section, except that it is rooted at ```api.drive```: + +>>> api.drive.dir() +['Holiday Photos', 'Work Files'] +>>> api.drive['Holiday Photos']['2013']['Sicily'].dir() +['DSC08116.JPG', 'DSC08117.JPG'] + +>>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG'] +>>> drive_file.name +u'DSC08116.JPG' +>>> drive_file.modified +datetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC +>>> drive_file.size +2021698 +>>> drive_file.type +u'file' + +The ``open`` method will return a response object from which you can read the file's contents: + +>>> from shutil import copyfileobj +>>> with drive_file.open(stream=True) as response: +>>> with open(drive_file.name, 'wb') as file_out: +>>> copyfileobj(response.raw, file_out) + Photo Library ======================= diff --git a/pyicloud/base.py b/pyicloud/base.py index 2c9a807..0035b5f 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -24,6 +24,7 @@ from pyicloud.services import ( RemindersService, PhotosService, AccountService, + DriveService, ) from pyicloud.utils import get_password_from_keyring @@ -91,20 +92,21 @@ class PyiCloudSession(Session): request_logger.debug(data) - reason = data.get("errorMessage") - reason = reason or data.get("reason") - reason = reason or data.get("errorReason") - if not reason and isinstance(data.get("error"), string_types): - reason = data.get("error") - if not reason and data.get("error"): - reason = "Unknown reason" + if isinstance(data, dict): + reason = data.get("errorMessage") + reason = reason or data.get("reason") + reason = reason or data.get("errorReason") + if not reason and isinstance(data.get("error"), string_types): + reason = data.get("error") + if not reason and data.get("error"): + reason = "Unknown reason" - code = data.get("errorCode") - if not code and data.get("serverErrorCode"): - code = data.get("serverErrorCode") + code = data.get("errorCode") + if not code and data.get("serverErrorCode"): + code = data.get("serverErrorCode") - if reason: - self._raise_error(code, reason) + if reason: + self._raise_error(code, reason) return response @@ -207,6 +209,7 @@ class PyiCloudService(object): self.authenticate() + self._drive = None self._files = None self._photos = None @@ -361,6 +364,18 @@ class PyiCloudService(object): service_root = self._get_webservice_url("reminders") return RemindersService(service_root, self.session, self.params) + @property + def drive(self): + """Gets the 'Drive' service.""" + if not self._drive: + self._drive = DriveService( + service_root=self._get_webservice_url("drivews"), + document_root=self._get_webservice_url("docws"), + session=self.session, + params=self.params, + ) + return self._drive + def __unicode__(self): return "iCloud API: %s" % self.user.get("apple_id") diff --git a/pyicloud/services/__init__.py b/pyicloud/services/__init__.py index 455ab72..73cae17 100644 --- a/pyicloud/services/__init__.py +++ b/pyicloud/services/__init__.py @@ -6,3 +6,4 @@ from pyicloud.services.contacts import ContactsService from pyicloud.services.reminders import RemindersService from pyicloud.services.photos import PhotosService from pyicloud.services.account import AccountService +from pyicloud.services.drive import DriveService diff --git a/pyicloud/services/drive.py b/pyicloud/services/drive.py new file mode 100644 index 0000000..a97f684 --- /dev/null +++ b/pyicloud/services/drive.py @@ -0,0 +1,172 @@ +"""Drive service.""" +from datetime import datetime, timedelta +import json +from re import search +from six import PY2 + + +class DriveService(object): + """The 'Drive' iCloud service.""" + + def __init__(self, service_root, document_root, session, params): + self._service_root = service_root + self._document_root = document_root + self.session = session + self.params = dict(params) + self._root = None + + def _get_token_from_cookie(self): + for cookie in self.session.cookies: + if cookie.name == "X-APPLE-WEBAUTH-TOKEN": + match = search(r"\bt=([^:]+)", cookie.value) + if not match: + raise Exception("Can't extract token from %r" % cookie.value) + self.params.update({"token": match.group(1)}) + raise Exception("Token cookie not found") + + def get_node_data(self, node_id): + """Returns the node data.""" + request = self.session.post( + self._service_root + "/retrieveItemDetailsInFolders", + params=self.params, + data=json.dumps( + [ + { + "drivewsid": "FOLDER::com.apple.CloudDocs::%s" % node_id, + "partialData": False, + } + ] + ), + ) + return request.json()[0] + + def get_file(self, file_id, **kwargs): + """Returns iCloud Drive file.""" + file_params = dict(self.params) + file_params.update({"document_id": file_id}) + response = self.session.get( + self._document_root + "/ws/com.apple.CloudDocs/download/by_id", + params=file_params, + ) + if not response.ok: + return None + url = response.json()["data_token"]["url"] + return self.session.get(url, params=self.params, **kwargs) + + @property + def root(self): + """Returns the root node.""" + if not self._root: + self._root = DriveNode(self, self.get_node_data("root")) + return self._root + + def __getattr__(self, attr): + return getattr(self.root, attr) + + def __getitem__(self, key): + return self.root[key] + + +class DriveNode(object): + """Drive node.""" + + def __init__(self, conn, data): + self.data = data + self.connection = conn + self._children = None + + @property + def name(self): + """Gets the node name.""" + if self.type == "file": + return "%s.%s" % (self.data["name"], self.data["extension"]) + return self.data["name"] + + @property + def type(self): + """Gets the node type.""" + node_type = self.data.get("type") + return node_type and node_type.lower() + + def get_children(self): + """Gets the node children.""" + if not self._children: + if "items" not in self.data: + self.data.update(self.connection.get_node_data(self.data["docwsid"])) + if "items" not in self.data: + raise KeyError("No items in folder, status: %s" % self.data["status"]) + self._children = [ + DriveNode(self.connection, item_data) + for item_data in self.data["items"] + ] + return self._children + + @property + def size(self): + """Gets the node size.""" + size = self.data.get("size") # Folder does not have size + if not size: + return None + return int(size) + + @property + def date_changed(self): + """Gets the node changed date (in UTC).""" + return _date_to_utc(self.data.get("dateChanged")) # Folder does not have date + + @property + def date_modified(self): + """Gets the node modified date (in UTC).""" + return _date_to_utc(self.data.get("dateModified")) # Folder does not have date + + @property + def date_last_open(self): + """Gets the node last open date (in UTC).""" + return _date_to_utc(self.data.get("lastOpenTime")) # Folder does not have date + + def open(self, **kwargs): + """Gets the node file.""" + return self.connection.get_file(self.data["docwsid"], **kwargs) + + def dir(self): + """Gets the node list of directories.""" + if self.type == "file": + return None + return [child.name for child in self.get_children()] + + def get(self, name): + """Gets the node child.""" + if self.type == "file": + return None + return [child for child in self.get_children() if child.name == name][0] + + def __getitem__(self, key): + try: + return self.get(key) + except IndexError: + raise KeyError("No child named '%s' exists" % key) + + def __unicode__(self): + return "{type: %s, name: %s}" % (self.type, self.name) + + def __str__(self): + as_unicode = self.__unicode__() + if PY2: + return as_unicode.encode("utf-8", "ignore") + return as_unicode + + def __repr__(self): + return "<%s: %s>" % (type(self).__name__, str(self)) + + +def _date_to_utc(date): + if not date: + return None + # jump through hoops to return time in UTC rather than California time + match = search(r"^(.+?)([\+\-]\d+):(\d\d)$", date) + if not match: + # Already in UTC + return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") + base = datetime.strptime(match.group(1), "%Y-%m-%dT%H:%M:%S") + diff = timedelta(hours=int(match.group(2)), minutes=int(match.group(3))) + return base - diff diff --git a/tests/__init__.py b/tests/__init__.py index dd35c17..828a5dd 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Library tests.""" import json from requests import Session, Response @@ -22,16 +23,24 @@ from .const_login import ( ) from .const_account import ACCOUNT_DEVICES_WORKING, ACCOUNT_STORAGE_WORKING from .const_account_family import ACCOUNT_FAMILY_WORKING +from .const_drive import ( + DRIVE_FOLDER_WORKING, + DRIVE_ROOT_INVALID, + DRIVE_SUBFOLDER_WORKING, + DRIVE_ROOT_WORKING, + DRIVE_FILE_DOWNLOAD_WORKING, +) from .const_findmyiphone import FMI_FAMILY_WORKING class ResponseMock(Response): """Mocked Response.""" - def __init__(self, result, status_code=200): + def __init__(self, result, status_code=200, **kwargs): Response.__init__(self) self.result = result self.status_code = status_code + self.raw = kwargs.get("raw") @property def text(self): @@ -42,6 +51,7 @@ class PyiCloudSessionMock(base.PyiCloudSession): """Mocked PyiCloudSession.""" def request(self, method, url, **kwargs): + params = kwargs.get("params") data = json.loads(kwargs.get("data", "{}")) # Login @@ -82,6 +92,34 @@ class PyiCloudSessionMock(base.PyiCloudSession): if "setup/ws/1/storageUsageInfo" in url and method == "GET": return ResponseMock(ACCOUNT_STORAGE_WORKING) + # Drive + if ( + "retrieveItemDetailsInFolders" in url + and method == "POST" + and data[0].get("drivewsid") + ): + if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::root": + return ResponseMock(DRIVE_ROOT_WORKING) + if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::documents": + return ResponseMock(DRIVE_ROOT_INVALID) + if ( + data[0].get("drivewsid") + == "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B" + ): + return ResponseMock(DRIVE_FOLDER_WORKING) + if ( + data[0].get("drivewsid") + == "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF" + ): + return ResponseMock(DRIVE_SUBFOLDER_WORKING) + # Drive download + if "com.apple.CloudDocs/download/by_id" in url and method == "GET": + if params.get("document_id") == "516C896C-6AA5-4A30-B30E-5502C2333DAE": + return ResponseMock(DRIVE_FILE_DOWNLOAD_WORKING) + if "icloud-content.com" in url and method == "GET": + if "Scanned+document+1.pdf" in url: + return ResponseMock({}, raw=open(".gitignore", "rb")) + # Find My iPhone if "fmi" in url and method == "POST": return ResponseMock(FMI_FAMILY_WORKING) diff --git a/tests/const.py b/tests/const.py index 7c56937..3b69842 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Test constants.""" from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL diff --git a/tests/const_account_family.py b/tests/const_account_family.py index c097f10..5406a70 100644 --- a/tests/const_account_family.py +++ b/tests/const_account_family.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Account family test constants.""" # Fakers diff --git a/tests/const_drive.py b/tests/const_drive.py new file mode 100644 index 0000000..d070af9 --- /dev/null +++ b/tests/const_drive.py @@ -0,0 +1,676 @@ +# -*- coding: utf-8 -*- +"""Drive test constants.""" + + +# Data +DRIVE_ROOT_WORKING = [ + { + "drivewsid": "FOLDER::com.apple.CloudDocs::root", + "docwsid": "root", + "zone": "com.apple.CloudDocs", + "name": "", + "etag": "31", + "type": "FOLDER", + "assetQuota": 62418076, + "fileCount": 7, + "shareCount": 0, + "shareAliasCount": 0, + "directChildrenCount": 3, + "items": [ + { + "dateCreated": "2019-12-12T14:33:55-08:00", + "drivewsid": "FOLDER::com.apple.Keynote::documents", + "docwsid": "documents", + "zone": "com.apple.Keynote", + "name": "Keynote", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "2m", + "type": "APP_LIBRARY", + "maxDepth": "ANY", + "icons": [ + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Keynote&field=icon120x120_iOS", + "type": "IOS", + "size": 120, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Keynote&field=icon80x80_iOS", + "type": "IOS", + "size": 80, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Keynote&field=icon40x40_iOS", + "type": "IOS", + "size": 40, + }, + ], + "supportedExtensions": [ + "pptx", + "ppsx", + "pps", + "pot", + "key-tef", + "ppt", + "potx", + "potm", + "pptm", + "ppsm", + "key", + "kth", + ], + "supportedTypes": [ + "com.microsoft.powerpoint.pps", + "com.microsoft.powerpoint.pot", + "com.microsoft.powerpoint.ppt", + "org.openxmlformats.presentationml.template.macroenabled", + "org.openxmlformats.presentationml.slideshow.macroenabled", + "com.apple.iwork.keynote.key-tef", + "org.openxmlformats.presentationml.template", + "org.openxmlformats.presentationml.presentation.macroenabled", + "com.apple.iwork.keynote.key", + "com.apple.iwork.keynote.kth", + "org.openxmlformats.presentationml.presentation", + "org.openxmlformats.presentationml.slideshow", + "com.apple.iwork.keynote.sffkey", + "com.apple.iwork.keynote.sffkth", + ], + }, + { + "dateCreated": "2019-12-12T14:33:55-08:00", + "drivewsid": "FOLDER::com.apple.Numbers::documents", + "docwsid": "documents", + "zone": "com.apple.Numbers", + "name": "Numbers", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "3k", + "type": "APP_LIBRARY", + "maxDepth": "ANY", + "icons": [ + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Numbers&field=icon120x120_iOS", + "type": "IOS", + "size": 120, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Numbers&field=icon80x80_iOS", + "type": "IOS", + "size": 80, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Numbers&field=icon40x40_iOS", + "type": "IOS", + "size": 40, + }, + ], + "supportedExtensions": [ + "hh", + "ksh", + "lm", + "xlt", + "c++", + "f95", + "lid", + "csv", + "numbers", + "php4", + "hp", + "py", + "nmbtemplate", + "lmm", + "jscript", + "php3", + "crash", + "patch", + "java", + "ym", + "xlam", + "text", + "mi", + "exp", + "adb", + "jav", + "ada", + "ii", + "defs", + "mm", + "cpp", + "cxx", + "pas", + "diff", + "pch++", + "javascript", + "panic", + "rb", + "ads", + "tcsh", + "ypp", + "yxx", + "ph3", + "ph4", + "phtml", + "xltx", + "hang", + "rbw", + "f77", + "for", + "js", + "h++", + "mig", + "gpurestart", + "mii", + "zsh", + "m3u", + "pch", + "sh", + "xltm", + "applescript", + "tsv", + "ymm", + "shutdownstall", + "cc", + "xlsx", + "scpt", + "c", + "inl", + "f", + "numbers-tef", + "h", + "i", + "hpp", + "hxx", + "dlyan", + "xla", + "l", + "cp", + "m", + "lpp", + "lxx", + "txt", + "r", + "s", + "xlsm", + "spin", + "php", + "csh", + "y", + "bash", + "m3u8", + "pl", + "f90", + "pm", + "xls", + ], + "supportedTypes": [ + "org.openxmlformats.spreadsheetml.sheet", + "com.microsoft.excel.xla", + "com.apple.iwork.numbers.template", + "org.openxmlformats.spreadsheetml.sheet.macroenabled", + "com.apple.iwork.numbers.sffnumbers", + "com.apple.iwork.numbers.numbers", + "public.plain-text", + "com.microsoft.excel.xlt", + "org.openxmlformats.spreadsheetml.template", + "com.microsoft.excel.xls", + "public.comma-separated-values-text", + "com.apple.iwork.numbers.numbers-tef", + "org.openxmlformats.spreadsheetml.template.macroenabled", + "public.tab-separated-values-text", + "com.apple.iwork.numbers.sfftemplate", + "com.microsoft.excel.openxml.addin", + ], + }, + { + "dateCreated": "2019-12-12T14:33:55-08:00", + "drivewsid": "FOLDER::com.apple.Pages::documents", + "docwsid": "documents", + "zone": "com.apple.Pages", + "name": "Pages", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "km", + "type": "APP_LIBRARY", + "maxDepth": "ANY", + "icons": [ + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Pages&field=icon120x120_iOS", + "type": "IOS", + "size": 120, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Pages&field=icon80x80_iOS", + "type": "IOS", + "size": 80, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Pages&field=icon40x40_iOS", + "type": "IOS", + "size": 40, + }, + ], + "supportedExtensions": [ + "hh", + "ksh", + "lm", + "c++", + "f95", + "lid", + "php4", + "hp", + "py", + "lmm", + "jscript", + "php3", + "crash", + "patch", + "pages", + "java", + "ym", + "text", + "mi", + "exp", + "adb", + "jav", + "ada", + "ii", + "defs", + "mm", + "cpp", + "cxx", + "pas", + "pages-tef", + "diff", + "pch++", + "javascript", + "panic", + "rb", + "ads", + "tcsh", + "rtfd", + "ypp", + "yxx", + "doc", + "ph3", + "ph4", + "template", + "phtml", + "hang", + "rbw", + "f77", + "dot", + "for", + "js", + "h++", + "mig", + "gpurestart", + "mii", + "zsh", + "m3u", + "pch", + "sh", + "applescript", + "ymm", + "shutdownstall", + "dotx", + "cc", + "scpt", + "c", + "rtf", + "inl", + "f", + "h", + "i", + "hpp", + "hxx", + "dlyan", + "l", + "cp", + "m", + "lpp", + "lxx", + "docx", + "txt", + "r", + "s", + "spin", + "php", + "csh", + "y", + "bash", + "m3u8", + "pl", + "f90", + "pm", + ], + "supportedTypes": [ + "com.apple.rtfd", + "com.apple.iwork.pages.sffpages", + "com.apple.iwork.pages.sfftemplate", + "com.microsoft.word.dot", + "com.apple.iwork.pages.pages", + "com.microsoft.word.doc", + "org.openxmlformats.wordprocessingml.template", + "org.openxmlformats.wordprocessingml.document", + "com.apple.iwork.pages.pages-tef", + "com.apple.iwork.pages.template", + "public.rtf", + "public.plain-text", + ], + }, + { + "dateCreated": "2019-12-12T14:33:55-08:00", + "drivewsid": "FOLDER::com.apple.Preview::documents", + "docwsid": "documents", + "zone": "com.apple.Preview", + "name": "Preview", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "bv", + "type": "APP_LIBRARY", + "maxDepth": "ANY", + "icons": [ + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Preview&field=icon32x32_OSX", + "type": "OSX", + "size": 32, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Preview&field=icon128x128_OSX", + "type": "OSX", + "size": 128, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Preview&field=icon16x16_OSX", + "type": "OSX", + "size": 16, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Preview&field=icon256x256_OSX", + "type": "OSX", + "size": 256, + }, + { + "url": "https://p31-drivews.icloud.com/getIcons?id=com.apple.Preview&field=icon64x64_OSX", + "type": "OSX", + "size": 64, + }, + ], + "supportedExtensions": [ + "ps", + "nmbtemplate", + "astc", + "mpkg", + "prefpane", + "pef", + "mos", + "qlgenerator", + "scptd", + "raf", + "saver", + "band", + "dng", + "pict", + "exr", + "kth", + "appex", + "app", + "pages-tef", + "slidesaver", + "pluginkit", + "distz", + "ai", + "png", + "eps", + "raw", + "pvr", + "mpo", + "ktx", + "nrw", + "lpdf", + "pfm", + "3fr", + "template", + "imovielibrary", + "pwl", + "iwwebpackage", + "wdgt", + "tga", + "pgm", + "erf", + "jpeg", + "j2c", + "bundle", + "key", + "j2k", + "abc", + "arw", + "xpc", + "pic", + "ppm", + "menu", + "icns", + "mrw", + "plugin", + "mdimporter", + "bmp", + "numbers", + "dae", + "dist", + "pic", + "rw2", + "nef", + "tif", + "pages", + "sgi", + "ico", + "theater", + "gbproj", + "webplugin", + "cr2", + "fff", + "webp", + "jp2", + "sr2", + "rtfd", + "pbm", + "pkpass", + "jfx", + "fpbf", + "psd", + "xbm", + "tiff", + "avchd", + "gif", + "pntg", + "rwl", + "pset", + "pkg", + "dcr", + "hdr", + "jpe", + "pct", + "jpg", + "jpf", + "orf", + "srf", + "numbers-tef", + "iconset", + "crw", + "fpx", + "dds", + "pdf", + "jpx", + "key-tef", + "efx", + "hdr", + "srw", + ], + "supportedTypes": [ + "com.adobe.illustrator.ai-image", + "com.kodak.flashpix-image", + "public.pbm", + "com.apple.pict", + "com.ilm.openexr-image", + "com.sgi.sgi-image", + "com.apple.icns", + "public.heifs", + "com.truevision.tga-image", + "com.adobe.postscript", + "public.camera-raw-image", + "public.pvr", + "public.png", + "com.adobe.photoshop-image", + "public.heif", + "com.microsoft.ico", + "com.adobe.pdf", + "public.heic", + "public.xbitmap-image", + "com.apple.localized-pdf-bundle", + "public.3d-content", + "com.compuserve.gif", + "public.avci", + "public.jpeg", + "com.apple.rjpeg", + "com.adobe.encapsulated-postscript", + "com.microsoft.bmp", + "public.fax", + "org.khronos.astc", + "com.apple.application-bundle", + "public.avcs", + "public.webp", + "public.heics", + "com.apple.macpaint-image", + "public.mpo-image", + "public.jpeg-2000", + "public.tiff", + "com.microsoft.dds", + "com.apple.pdf-printer-settings", + "org.khronos.ktx", + "public.radiance", + "com.apple.package", + "public.folder", + ], + }, + { + "drivewsid": "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B", + "docwsid": "1C7F1760-D940-480F-8C4F-005824A4E05B", + "zone": "com.apple.CloudDocs", + "name": "pyiCloud", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "30", + "type": "FOLDER", + "assetQuota": 42199575, + "fileCount": 2, + "shareCount": 0, + "shareAliasCount": 0, + "directChildrenCount": 1, + }, + ], + "numberOfItems": 5, + } +] + +# App specific folder (Keynote, Numbers, Pages, Preview ...) type=APP_LIBRARY +DRIVE_ROOT_INVALID = [ + {"drivewsid": "FOLDER::com.apple.CloudDocs::documents", "status": "ID_INVALID"} +] + +DRIVE_FOLDER_WORKING = [ + { + "drivewsid": "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B", + "docwsid": "1C7F1760-D940-480F-8C4F-005824A4E05B", + "zone": "com.apple.CloudDocs", + "name": "pyiCloud", + "parentId": "FOLDER::com.apple.CloudDocs::root", + "etag": "30", + "type": "FOLDER", + "assetQuota": 42199575, + "fileCount": 2, + "shareCount": 0, + "shareAliasCount": 0, + "directChildrenCount": 1, + "items": [ + { + "drivewsid": "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "docwsid": "D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "zone": "com.apple.CloudDocs", + "name": "Test", + "parentId": "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B", + "etag": "2z", + "type": "FOLDER", + "assetQuota": 42199575, + "fileCount": 2, + "shareCount": 0, + "shareAliasCount": 0, + "directChildrenCount": 2, + } + ], + "numberOfItems": 1, + } +] + +DRIVE_SUBFOLDER_WORKING = [ + { + "drivewsid": "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "docwsid": "D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "zone": "com.apple.CloudDocs", + "name": "Test", + "parentId": "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B", + "etag": "2z", + "type": "FOLDER", + "assetQuota": 42199575, + "fileCount": 2, + "shareCount": 0, + "shareAliasCount": 0, + "directChildrenCount": 2, + "items": [ + { + "drivewsid": "FILE::com.apple.CloudDocs::33A41112-4131-4938-9691-7F356CE3C51D", + "docwsid": "33A41112-4131-4938-9691-7F356CE3C51D", + "zone": "com.apple.CloudDocs", + "name": "Document scanné 2", + "parentId": "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "dateModified": "2020-04-27T21:37:36Z", + "dateChanged": "2020-04-27T14:44:29-07:00", + "size": 19876991, + "etag": "2k::2j", + "extension": "pdf", + "hiddenExtension": True, + "lastOpenTime": "2020-04-27T21:37:36Z", + "type": "FILE", + }, + { + "drivewsid": "FILE::com.apple.CloudDocs::516C896C-6AA5-4A30-B30E-5502C2333DAE", + "docwsid": "516C896C-6AA5-4A30-B30E-5502C2333DAE", + "zone": "com.apple.CloudDocs", + "name": "Scanned document 1", + "parentId": "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF", + "dateModified": "2020-05-03T00:15:17Z", + "dateChanged": "2020-05-02T17:16:17-07:00", + "size": 21644358, + "etag": "32::2x", + "extension": "pdf", + "hiddenExtension": True, + "lastOpenTime": "2020-05-03T00:24:25Z", + "type": "FILE", + }, + ], + "numberOfItems": 2, + } +] + +DRIVE_FILE_DOWNLOAD_WORKING = { + "document_id": "516C896C-6AA5-4A30-B30E-5502C2333DAE", + "data_token": { + "url": "https://cvws.icloud-content.com/B/signature1ref_signature1/Scanned+document+1.pdf?o=object1&v=1&x=3&a=token1&e=1588472097&k=wrapping_key1&fl=&r=request&ckc=com.apple.clouddocs&ckz=com.apple.CloudDocs&p=31&s=s1", + "token": "token1", + "signature": "signature1", + "wrapping_key": "wrapping_key1==", + "reference_signature": "ref_signature1", + }, + "thumbnail_token": { + "url": "https://cvws.icloud-content.com/B/signature2ref_signature2/Scanned+document+1.jpg?o=object2&v=1&x=3&a=token2&e=1588472097&k=wrapping_key2&fl=&r=request&ckc=com.apple.clouddocs&ckz=com.apple.CloudDocs&p=31&s=s2", + "token": "token2", + "signature": "signature2", + "wrapping_key": "wrapping_key2==", + "reference_signature": "ref_signature2", + }, + "double_etag": "32::2x", +} diff --git a/tests/const_findmyiphone.py b/tests/const_findmyiphone.py index 06dcf64..28c6711 100644 --- a/tests/const_findmyiphone.py +++ b/tests/const_findmyiphone.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Find my iPhone test constants.""" from .const import CLIENT_ID from .const_account_family import ( diff --git a/tests/const_login.py b/tests/const_login.py index d206cbd..25ec7c0 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Login test constants.""" from .const_account_family import ( FIRST_NAME, diff --git a/tests/test_account.py b/tests/test_account.py index 7d0b999..3cd0cfb 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Account service tests.""" from unittest import TestCase from six import PY3 diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 0076f0f..1a80239 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Cmdline tests.""" from pyicloud import cmdline from . import PyiCloudServiceMock diff --git a/tests/test_drive.py b/tests/test_drive.py new file mode 100644 index 0000000..398ca8a --- /dev/null +++ b/tests/test_drive.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""Drive service tests.""" +from unittest import TestCase +from . import PyiCloudServiceMock +from .const import AUTHENTICATED_USER, VALID_PASSWORD +import pytest + +# pylint: disable=pointless-statement +class DriveServiceTest(TestCase): + """"Drive service tests""" + + service = None + + def setUp(self): + self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD) + + def test_root(self): + """Test the root folder.""" + drive = self.service.drive + assert drive.name == "" + assert drive.type == "folder" + assert drive.size is None + assert drive.date_changed is None + assert drive.date_modified is None + assert drive.date_last_open is None + assert drive.dir() == ["Keynote", "Numbers", "Pages", "Preview", "pyiCloud"] + + def test_folder_app(self): + """Test the /Preview folder.""" + folder = self.service.drive["Preview"] + assert folder.name == "Preview" + assert folder.type == "app_library" + assert folder.size is None + assert folder.date_changed is None + assert folder.date_modified is None + assert folder.date_last_open is None + with pytest.raises(KeyError, match="No items in folder, status: ID_INVALID"): + assert folder.dir() + + def test_folder_not_exists(self): + """Test the /not_exists folder.""" + with pytest.raises(KeyError, match="No child named 'not_exists' exists"): + self.service.drive["not_exists"] + + def test_folder(self): + """Test the /pyiCloud folder.""" + folder = self.service.drive["pyiCloud"] + assert folder.name == "pyiCloud" + assert folder.type == "folder" + assert folder.size is None + assert folder.date_changed is None + assert folder.date_modified is None + assert folder.date_last_open is None + assert folder.dir() == ["Test"] + + def test_subfolder(self): + """Test the /pyiCloud/Test folder.""" + folder = self.service.drive["pyiCloud"]["Test"] + assert folder.name == "Test" + assert folder.type == "folder" + assert folder.size is None + assert folder.date_changed is None + assert folder.date_modified is None + assert folder.date_last_open is None + assert folder.dir() == [u"Document scanné 2.pdf", "Scanned document 1.pdf"] + + def test_subfolder_file(self): + """Test the /pyiCloud/Test/Scanned document 1.pdf file.""" + folder = self.service.drive["pyiCloud"]["Test"] + file_test = folder["Scanned document 1.pdf"] + assert file_test.name == "Scanned document 1.pdf" + assert file_test.type == "file" + assert file_test.size == 21644358 + assert str(file_test.date_changed) == "2020-05-03 00:16:17" + assert str(file_test.date_modified) == "2020-05-03 00:15:17" + assert str(file_test.date_last_open) == "2020-05-03 00:24:25" + assert file_test.dir() is None + + def test_file_open(self): + """Test the /pyiCloud/Test/Scanned document 1.pdf file open.""" + file_test = self.service.drive["pyiCloud"]["Test"]["Scanned document 1.pdf"] + with file_test.open(stream=True) as response: + assert response.raw diff --git a/tests/test_findmyiphone.py b/tests/test_findmyiphone.py index 3b2b07f..252a4f1 100644 --- a/tests/test_findmyiphone.py +++ b/tests/test_findmyiphone.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Find My iPhone service tests.""" from unittest import TestCase from . import PyiCloudServiceMock