Add iCloud Drive support (#278)

* Initial version of the iCloud drive client

* Pylint & black

* Add tests + some fixes

* Fix pipe

Co-authored-by: Herve Saint-Amand <herve@brainnwave.com>
This commit is contained in:
Quentame 2020-05-03 04:54:11 +02:00 committed by GitHub
parent 696db8cf20
commit e6429b9ada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 1032 additions and 13 deletions

View file

@ -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
=======================

View file

@ -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")

View file

@ -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

172
pyicloud/services/drive.py Normal file
View file

@ -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

View file

@ -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)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Test constants."""
from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Account family test constants."""
# Fakers

676
tests/const_drive.py Normal file
View file

@ -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",
}

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Find my iPhone test constants."""
from .const import CLIENT_ID
from .const_account_family import (

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Login test constants."""
from .const_account_family import (
FIRST_NAME,

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Account service tests."""
from unittest import TestCase
from six import PY3

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Cmdline tests."""
from pyicloud import cmdline
from . import PyiCloudServiceMock

83
tests/test_drive.py Normal file
View file

@ -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

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Find My iPhone service tests."""
from unittest import TestCase
from . import PyiCloudServiceMock