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: >>> with open('downloaded_file.zip', 'wb') as opened_file:
opened_file.write(download.raw.read()) 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 Photo Library
======================= =======================

View file

@ -24,6 +24,7 @@ from pyicloud.services import (
RemindersService, RemindersService,
PhotosService, PhotosService,
AccountService, AccountService,
DriveService,
) )
from pyicloud.utils import get_password_from_keyring from pyicloud.utils import get_password_from_keyring
@ -91,20 +92,21 @@ class PyiCloudSession(Session):
request_logger.debug(data) request_logger.debug(data)
reason = data.get("errorMessage") if isinstance(data, dict):
reason = reason or data.get("reason") reason = data.get("errorMessage")
reason = reason or data.get("errorReason") reason = reason or data.get("reason")
if not reason and isinstance(data.get("error"), string_types): reason = reason or data.get("errorReason")
reason = data.get("error") if not reason and isinstance(data.get("error"), string_types):
if not reason and data.get("error"): reason = data.get("error")
reason = "Unknown reason" if not reason and data.get("error"):
reason = "Unknown reason"
code = data.get("errorCode") code = data.get("errorCode")
if not code and data.get("serverErrorCode"): if not code and data.get("serverErrorCode"):
code = data.get("serverErrorCode") code = data.get("serverErrorCode")
if reason: if reason:
self._raise_error(code, reason) self._raise_error(code, reason)
return response return response
@ -207,6 +209,7 @@ class PyiCloudService(object):
self.authenticate() self.authenticate()
self._drive = None
self._files = None self._files = None
self._photos = None self._photos = None
@ -361,6 +364,18 @@ class PyiCloudService(object):
service_root = self._get_webservice_url("reminders") service_root = self._get_webservice_url("reminders")
return RemindersService(service_root, self.session, self.params) 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): def __unicode__(self):
return "iCloud API: %s" % self.user.get("apple_id") 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.reminders import RemindersService
from pyicloud.services.photos import PhotosService from pyicloud.services.photos import PhotosService
from pyicloud.services.account import AccountService 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.""" """Library tests."""
import json import json
from requests import Session, Response 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 import ACCOUNT_DEVICES_WORKING, ACCOUNT_STORAGE_WORKING
from .const_account_family import ACCOUNT_FAMILY_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 from .const_findmyiphone import FMI_FAMILY_WORKING
class ResponseMock(Response): class ResponseMock(Response):
"""Mocked Response.""" """Mocked Response."""
def __init__(self, result, status_code=200): def __init__(self, result, status_code=200, **kwargs):
Response.__init__(self) Response.__init__(self)
self.result = result self.result = result
self.status_code = status_code self.status_code = status_code
self.raw = kwargs.get("raw")
@property @property
def text(self): def text(self):
@ -42,6 +51,7 @@ class PyiCloudSessionMock(base.PyiCloudSession):
"""Mocked PyiCloudSession.""" """Mocked PyiCloudSession."""
def request(self, method, url, **kwargs): def request(self, method, url, **kwargs):
params = kwargs.get("params")
data = json.loads(kwargs.get("data", "{}")) data = json.loads(kwargs.get("data", "{}"))
# Login # Login
@ -82,6 +92,34 @@ class PyiCloudSessionMock(base.PyiCloudSession):
if "setup/ws/1/storageUsageInfo" in url and method == "GET": if "setup/ws/1/storageUsageInfo" in url and method == "GET":
return ResponseMock(ACCOUNT_STORAGE_WORKING) 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 # Find My iPhone
if "fmi" in url and method == "POST": if "fmi" in url and method == "POST":
return ResponseMock(FMI_FAMILY_WORKING) return ResponseMock(FMI_FAMILY_WORKING)

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Test constants.""" """Test constants."""
from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL 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.""" """Account family test constants."""
# Fakers # 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.""" """Find my iPhone test constants."""
from .const import CLIENT_ID from .const import CLIENT_ID
from .const_account_family import ( from .const_account_family import (

View file

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

View file

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

View file

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
"""Cmdline tests.""" """Cmdline tests."""
from pyicloud import cmdline from pyicloud import cmdline
from . import PyiCloudServiceMock 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.""" """Find My iPhone service tests."""
from unittest import TestCase from unittest import TestCase
from . import PyiCloudServiceMock from . import PyiCloudServiceMock