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:
parent
696db8cf20
commit
e6429b9ada
14 changed files with 1032 additions and 13 deletions
27
README.rst
27
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
|
||||
=======================
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
172
pyicloud/services/drive.py
Normal 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
|
|
@ -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)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Test constants."""
|
||||
from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Account family test constants."""
|
||||
|
||||
# Fakers
|
||||
|
|
676
tests/const_drive.py
Normal file
676
tests/const_drive.py
Normal 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",
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Find my iPhone test constants."""
|
||||
from .const import CLIENT_ID
|
||||
from .const_account_family import (
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Login test constants."""
|
||||
from .const_account_family import (
|
||||
FIRST_NAME,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Account service tests."""
|
||||
from unittest import TestCase
|
||||
from six import PY3
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Cmdline tests."""
|
||||
from pyicloud import cmdline
|
||||
from . import PyiCloudServiceMock
|
||||
|
|
83
tests/test_drive.py
Normal file
83
tests/test_drive.py
Normal 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
|
|
@ -1,3 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Find My iPhone service tests."""
|
||||
from unittest import TestCase
|
||||
from . import PyiCloudServiceMock
|
||||
|
|
Loading…
Reference in a new issue