parent
9588c0d448
commit
ababe3cdf3
18 changed files with 808 additions and 739 deletions
|
@ -15,4 +15,5 @@ before_install:
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
script:
|
script:
|
||||||
- pylint pyicloud tests
|
- pylint pyicloud tests
|
||||||
|
- ./scripts/check_format.sh;
|
||||||
- py.test
|
- py.test
|
||||||
|
|
|
@ -13,14 +13,16 @@ pyiCloud
|
||||||
:target: https://pypi.org/project/pyiCloud
|
:target: https://pypi.org/project/pyiCloud
|
||||||
|
|
||||||
.. image:: https://requires.io/github/Quentame/pyicloud/requirements.svg?branch=master
|
.. image:: https://requires.io/github/Quentame/pyicloud/requirements.svg?branch=master
|
||||||
:target: https://requires.io/github/Quentame/pyicloud/requirements/?branch=master
|
|
||||||
:alt: Requirements Status
|
:alt: Requirements Status
|
||||||
|
:target: https://requires.io/github/Quentame/pyicloud/requirements/?branch=master
|
||||||
|
|
||||||
|
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
|
||||||
|
:target: https://github.com/psf/black
|
||||||
|
|
||||||
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
.. image:: https://badges.gitter.im/Join%20Chat.svg
|
||||||
:alt: Join the chat at https://gitter.im/picklepete/pyicloud
|
:alt: Join the chat at https://gitter.im/picklepete/pyicloud
|
||||||
:target: https://gitter.im/picklepete/pyicloud?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
:target: https://gitter.im/picklepete/pyicloud?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
|
||||||
|
|
||||||
|
|
||||||
PyiCloud is a module which allows pythonistas to interact with iCloud webservices. It's powered by the fantastic `requests <https://github.com/kennethreitz/requests>`_ HTTP library.
|
PyiCloud is a module which allows pythonistas to interact with iCloud webservices. It's powered by the fantastic `requests <https://github.com/kennethreitz/requests>`_ HTTP library.
|
||||||
|
|
||||||
At its core, PyiCloud connects to iCloud using your username and password, then performs calendar and iPhone queries against their API.
|
At its core, PyiCloud connects to iCloud using your username and password, then performs calendar and iPhone queries against their API.
|
||||||
|
|
203
pyicloud/base.py
203
pyicloud/base.py
|
@ -14,7 +14,7 @@ from pyicloud.exceptions import (
|
||||||
PyiCloudFailedLoginException,
|
PyiCloudFailedLoginException,
|
||||||
PyiCloudAPIResponseException,
|
PyiCloudAPIResponseException,
|
||||||
PyiCloud2SARequiredException,
|
PyiCloud2SARequiredException,
|
||||||
PyiCloudServiceNotActivatedException
|
PyiCloudServiceNotActivatedException,
|
||||||
)
|
)
|
||||||
from pyicloud.services import (
|
from pyicloud.services import (
|
||||||
FindMyiPhoneServiceManager,
|
FindMyiPhoneServiceManager,
|
||||||
|
@ -23,7 +23,7 @@ from pyicloud.services import (
|
||||||
ContactsService,
|
ContactsService,
|
||||||
RemindersService,
|
RemindersService,
|
||||||
PhotosService,
|
PhotosService,
|
||||||
AccountService
|
AccountService,
|
||||||
)
|
)
|
||||||
from pyicloud.utils import get_password_from_keyring
|
from pyicloud.utils import get_password_from_keyring
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
class PyiCloudPasswordFilter(logging.Filter):
|
class PyiCloudPasswordFilter(logging.Filter):
|
||||||
"""Password log hider."""
|
"""Password log hider."""
|
||||||
|
|
||||||
def __init__(self, password):
|
def __init__(self, password):
|
||||||
super(PyiCloudPasswordFilter, self).__init__(password)
|
super(PyiCloudPasswordFilter, self).__init__(password)
|
||||||
|
|
||||||
|
@ -52,6 +53,7 @@ class PyiCloudPasswordFilter(logging.Filter):
|
||||||
|
|
||||||
class PyiCloudSession(Session):
|
class PyiCloudSession(Session):
|
||||||
"""iCloud session."""
|
"""iCloud session."""
|
||||||
|
|
||||||
def __init__(self, service):
|
def __init__(self, service):
|
||||||
self.service = service
|
self.service = service
|
||||||
super(PyiCloudSession, self).__init__()
|
super(PyiCloudSession, self).__init__()
|
||||||
|
@ -61,27 +63,25 @@ class PyiCloudSession(Session):
|
||||||
# Charge logging to the right service endpoint
|
# Charge logging to the right service endpoint
|
||||||
callee = inspect.stack()[2]
|
callee = inspect.stack()[2]
|
||||||
module = inspect.getmodule(callee[0])
|
module = inspect.getmodule(callee[0])
|
||||||
request_logger = logging.getLogger(module.__name__).getChild('http')
|
request_logger = logging.getLogger(module.__name__).getChild("http")
|
||||||
if self.service.password_filter not in request_logger.filters:
|
if self.service.password_filter not in request_logger.filters:
|
||||||
request_logger.addFilter(self.service.password_filter)
|
request_logger.addFilter(self.service.password_filter)
|
||||||
|
|
||||||
request_logger.debug("%s %s %s", args[0], args[1], kwargs.get('data', ''))
|
request_logger.debug("%s %s %s", args[0], args[1], kwargs.get("data", ""))
|
||||||
|
|
||||||
kwargs.pop('retried', None)
|
kwargs.pop("retried", None)
|
||||||
response = super(PyiCloudSession, self).request(*args, **kwargs)
|
response = super(PyiCloudSession, self).request(*args, **kwargs)
|
||||||
|
|
||||||
content_type = response.headers.get('Content-Type', '').split(';')[0]
|
content_type = response.headers.get("Content-Type", "").split(";")[0]
|
||||||
json_mimetypes = ['application/json', 'text/json']
|
json_mimetypes = ["application/json", "text/json"]
|
||||||
|
|
||||||
if not response.ok and content_type not in json_mimetypes:
|
if not response.ok and content_type not in json_mimetypes:
|
||||||
if kwargs.get('retried') is None and response.status_code == 450:
|
if kwargs.get("retried") is None and response.status_code == 450:
|
||||||
api_error = PyiCloudAPIResponseException(
|
api_error = PyiCloudAPIResponseException(
|
||||||
response.reason,
|
response.reason, response.status_code, retry=True
|
||||||
response.status_code,
|
|
||||||
retry=True
|
|
||||||
)
|
)
|
||||||
request_logger.warn(api_error)
|
request_logger.warn(api_error)
|
||||||
kwargs['retried'] = True
|
kwargs["retried"] = True
|
||||||
return self.request(*args, **kwargs)
|
return self.request(*args, **kwargs)
|
||||||
self._raise_error(response.status_code, response.reason)
|
self._raise_error(response.status_code, response.reason)
|
||||||
|
|
||||||
|
@ -91,22 +91,22 @@ class PyiCloudSession(Session):
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except: # pylint: disable=bare-except
|
except: # pylint: disable=bare-except
|
||||||
request_logger.warning('Failed to parse response with JSON mimetype')
|
request_logger.warning("Failed to parse response with JSON mimetype")
|
||||||
return response
|
return response
|
||||||
|
|
||||||
request_logger.debug(data)
|
request_logger.debug(data)
|
||||||
|
|
||||||
reason = data.get('errorMessage')
|
reason = data.get("errorMessage")
|
||||||
reason = reason or data.get('reason')
|
reason = reason or data.get("reason")
|
||||||
reason = reason or data.get('errorReason')
|
reason = reason or data.get("errorReason")
|
||||||
if not reason and isinstance(data.get('error'), six.string_types):
|
if not reason and isinstance(data.get("error"), six.string_types):
|
||||||
reason = data.get('error')
|
reason = data.get("error")
|
||||||
if not reason and data.get('error'):
|
if not reason and data.get("error"):
|
||||||
reason = "Unknown reason"
|
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)
|
||||||
|
@ -114,20 +114,25 @@ class PyiCloudSession(Session):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
def _raise_error(self, code, reason):
|
def _raise_error(self, code, reason):
|
||||||
if self.service.requires_2sa and \
|
if (
|
||||||
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
|
self.service.requires_2sa
|
||||||
raise PyiCloud2SARequiredException(self.service.user['apple_id'])
|
and reason == "Missing X-APPLE-WEBAUTH-TOKEN cookie"
|
||||||
if code in ('ZONE_NOT_FOUND', 'AUTHENTICATION_FAILED'):
|
):
|
||||||
reason = 'Please log into https://icloud.com/ to manually ' \
|
raise PyiCloud2SARequiredException(self.service.user["apple_id"])
|
||||||
'finish setting up your iCloud service'
|
if code in ("ZONE_NOT_FOUND", "AUTHENTICATION_FAILED"):
|
||||||
|
reason = (
|
||||||
|
"Please log into https://icloud.com/ to manually "
|
||||||
|
"finish setting up your iCloud service"
|
||||||
|
)
|
||||||
api_error = PyiCloudServiceNotActivatedException(reason, code)
|
api_error = PyiCloudServiceNotActivatedException(reason, code)
|
||||||
LOGGER.error(api_error)
|
LOGGER.error(api_error)
|
||||||
|
|
||||||
raise (api_error)
|
raise (api_error)
|
||||||
if code == 'ACCESS_DENIED':
|
if code == "ACCESS_DENIED":
|
||||||
reason = reason + '. Please wait a few minutes then try ' \
|
reason = (
|
||||||
'again. The remote servers might be trying to ' \
|
reason + ". Please wait a few minutes then try again."
|
||||||
'throttle requests.'
|
"The remote servers might be trying to throttle requests."
|
||||||
|
)
|
||||||
|
|
||||||
api_error = PyiCloudAPIResponseException(reason, code)
|
api_error = PyiCloudAPIResponseException(reason, code)
|
||||||
LOGGER.error(api_error)
|
LOGGER.error(api_error)
|
||||||
|
@ -146,8 +151,13 @@ class PyiCloudService(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, apple_id, password=None, cookie_directory=None, verify=True,
|
self,
|
||||||
client_id=None, with_family=True
|
apple_id,
|
||||||
|
password=None,
|
||||||
|
cookie_directory=None,
|
||||||
|
verify=True,
|
||||||
|
client_id=None,
|
||||||
|
with_family=True,
|
||||||
):
|
):
|
||||||
if password is None:
|
if password is None:
|
||||||
password = get_password_from_keyring(apple_id)
|
password = get_password_from_keyring(apple_id)
|
||||||
|
@ -155,33 +165,32 @@ class PyiCloudService(object):
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.client_id = client_id or str(uuid.uuid1()).upper()
|
self.client_id = client_id or str(uuid.uuid1()).upper()
|
||||||
self.with_family = with_family
|
self.with_family = with_family
|
||||||
self.user = {'apple_id': apple_id, 'password': password}
|
self.user = {"apple_id": apple_id, "password": password}
|
||||||
|
|
||||||
self.password_filter = PyiCloudPasswordFilter(password)
|
self.password_filter = PyiCloudPasswordFilter(password)
|
||||||
LOGGER.addFilter(self.password_filter)
|
LOGGER.addFilter(self.password_filter)
|
||||||
|
|
||||||
self._home_endpoint = 'https://www.icloud.com'
|
self._home_endpoint = "https://www.icloud.com"
|
||||||
self._setup_endpoint = 'https://setup.icloud.com/setup/ws/1'
|
self._setup_endpoint = "https://setup.icloud.com/setup/ws/1"
|
||||||
|
|
||||||
self._base_login_url = '%s/login' % self._setup_endpoint
|
self._base_login_url = "%s/login" % self._setup_endpoint
|
||||||
|
|
||||||
if cookie_directory:
|
if cookie_directory:
|
||||||
self._cookie_directory = os.path.expanduser(
|
self._cookie_directory = os.path.expanduser(
|
||||||
os.path.normpath(cookie_directory)
|
os.path.normpath(cookie_directory)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self._cookie_directory = os.path.join(
|
self._cookie_directory = os.path.join(tempfile.gettempdir(), "pyicloud",)
|
||||||
tempfile.gettempdir(),
|
|
||||||
'pyicloud',
|
|
||||||
)
|
|
||||||
|
|
||||||
self.session = PyiCloudSession(self)
|
self.session = PyiCloudSession(self)
|
||||||
self.session.verify = verify
|
self.session.verify = verify
|
||||||
self.session.headers.update({
|
self.session.headers.update(
|
||||||
'Origin': self._home_endpoint,
|
{
|
||||||
'Referer': '%s/' % self._home_endpoint,
|
"Origin": self._home_endpoint,
|
||||||
'User-Agent': 'Opera/9.52 (X11; Linux i686; U; en)'
|
"Referer": "%s/" % self._home_endpoint,
|
||||||
})
|
"User-Agent": "Opera/9.52 (X11; Linux i686; U; en)",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
cookiejar_path = self._get_cookiejar_path()
|
cookiejar_path = self._get_cookiejar_path()
|
||||||
self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path)
|
self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path)
|
||||||
|
@ -196,11 +205,11 @@ class PyiCloudService(object):
|
||||||
LOGGER.warning("Failed to read cookiejar %s", cookiejar_path)
|
LOGGER.warning("Failed to read cookiejar %s", cookiejar_path)
|
||||||
|
|
||||||
self.params = {
|
self.params = {
|
||||||
'clientBuildNumber': '17DHotfix5',
|
"clientBuildNumber": "17DHotfix5",
|
||||||
'clientMasteringNumber': '17DHotfix5',
|
"clientMasteringNumber": "17DHotfix5",
|
||||||
'ckjsBuildVersion': '17DProjectDev77',
|
"ckjsBuildVersion": "17DProjectDev77",
|
||||||
'ckjsVersion': '2.0.5',
|
"ckjsVersion": "2.0.5",
|
||||||
'clientId': self.client_id,
|
"clientId": self.client_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
@ -214,26 +223,24 @@ class PyiCloudService(object):
|
||||||
subsequent logins will not cause additional e-mails from Apple.
|
subsequent logins will not cause additional e-mails from Apple.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
LOGGER.info("Authenticating as %s", self.user['apple_id'])
|
LOGGER.info("Authenticating as %s", self.user["apple_id"])
|
||||||
|
|
||||||
data = dict(self.user)
|
data = dict(self.user)
|
||||||
|
|
||||||
# We authenticate every time, so "remember me" is not needed
|
# We authenticate every time, so "remember me" is not needed
|
||||||
data.update({'extended_login': False})
|
data.update({"extended_login": False})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
req = self.session.post(
|
req = self.session.post(
|
||||||
self._base_login_url,
|
self._base_login_url, params=self.params, data=json.dumps(data)
|
||||||
params=self.params,
|
|
||||||
data=json.dumps(data)
|
|
||||||
)
|
)
|
||||||
except PyiCloudAPIResponseException as error:
|
except PyiCloudAPIResponseException as error:
|
||||||
msg = 'Invalid email/password combination.'
|
msg = "Invalid email/password combination."
|
||||||
raise PyiCloudFailedLoginException(msg, error)
|
raise PyiCloudFailedLoginException(msg, error)
|
||||||
|
|
||||||
self.data = req.json()
|
self.data = req.json()
|
||||||
self.params.update({'dsid': self.data['dsInfo']['dsid']})
|
self.params.update({"dsid": self.data["dsInfo"]["dsid"]})
|
||||||
self._webservices = self.data['webservices']
|
self._webservices = self.data["webservices"]
|
||||||
|
|
||||||
if not os.path.exists(self._cookie_directory):
|
if not os.path.exists(self._cookie_directory):
|
||||||
os.mkdir(self._cookie_directory)
|
os.mkdir(self._cookie_directory)
|
||||||
|
@ -247,48 +254,46 @@ class PyiCloudService(object):
|
||||||
"""Get path for cookiejar file."""
|
"""Get path for cookiejar file."""
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
self._cookie_directory,
|
self._cookie_directory,
|
||||||
''.join([c for c in self.user.get('apple_id') if match(r'\w', c)])
|
"".join([c for c in self.user.get("apple_id") if match(r"\w", c)]),
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requires_2sa(self):
|
def requires_2sa(self):
|
||||||
"""Returns True if two-step authentication is required."""
|
"""Returns True if two-step authentication is required."""
|
||||||
return self.data.get('hsaChallengeRequired', False) \
|
return (
|
||||||
and self.data['dsInfo'].get('hsaVersion', 0) >= 1
|
self.data.get("hsaChallengeRequired", False)
|
||||||
|
and self.data["dsInfo"].get("hsaVersion", 0) >= 1
|
||||||
|
)
|
||||||
# FIXME: Implement 2FA for hsaVersion == 2 # pylint: disable=fixme
|
# FIXME: Implement 2FA for hsaVersion == 2 # pylint: disable=fixme
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trusted_devices(self):
|
def trusted_devices(self):
|
||||||
"""Returns devices trusted for two-step authentication."""
|
"""Returns devices trusted for two-step authentication."""
|
||||||
request = self.session.get(
|
request = self.session.get(
|
||||||
'%s/listDevices' % self._setup_endpoint,
|
"%s/listDevices" % self._setup_endpoint, params=self.params
|
||||||
params=self.params
|
|
||||||
)
|
)
|
||||||
return request.json().get('devices')
|
return request.json().get("devices")
|
||||||
|
|
||||||
def send_verification_code(self, device):
|
def send_verification_code(self, device):
|
||||||
"""Requests that a verification code is sent to the given device."""
|
"""Requests that a verification code is sent to the given device."""
|
||||||
data = json.dumps(device)
|
data = json.dumps(device)
|
||||||
request = self.session.post(
|
request = self.session.post(
|
||||||
'%s/sendVerificationCode' % self._setup_endpoint,
|
"%s/sendVerificationCode" % self._setup_endpoint,
|
||||||
params=self.params,
|
params=self.params,
|
||||||
data=data
|
data=data,
|
||||||
)
|
)
|
||||||
return request.json().get('success', False)
|
return request.json().get("success", False)
|
||||||
|
|
||||||
def validate_verification_code(self, device, code):
|
def validate_verification_code(self, device, code):
|
||||||
"""Verifies a verification code received on a trusted device."""
|
"""Verifies a verification code received on a trusted device."""
|
||||||
device.update({
|
device.update({"verificationCode": code, "trustBrowser": True})
|
||||||
'verificationCode': code,
|
|
||||||
'trustBrowser': True
|
|
||||||
})
|
|
||||||
data = json.dumps(device)
|
data = json.dumps(device)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.session.post(
|
self.session.post(
|
||||||
'%s/validateVerificationCode' % self._setup_endpoint,
|
"%s/validateVerificationCode" % self._setup_endpoint,
|
||||||
params=self.params,
|
params=self.params,
|
||||||
data=data
|
data=data,
|
||||||
)
|
)
|
||||||
except PyiCloudAPIResponseException as error:
|
except PyiCloudAPIResponseException as error:
|
||||||
if error.code == -21669:
|
if error.code == -21669:
|
||||||
|
@ -306,20 +311,16 @@ class PyiCloudService(object):
|
||||||
"""Get webservice URL, raise an exception if not exists."""
|
"""Get webservice URL, raise an exception if not exists."""
|
||||||
if self._webservices.get(ws_key) is None:
|
if self._webservices.get(ws_key) is None:
|
||||||
raise PyiCloudServiceNotActivatedException(
|
raise PyiCloudServiceNotActivatedException(
|
||||||
'Webservice not available',
|
"Webservice not available", ws_key
|
||||||
ws_key
|
|
||||||
)
|
)
|
||||||
return self._webservices[ws_key]['url']
|
return self._webservices[ws_key]["url"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
"""Returns all devices."""
|
"""Returns all devices."""
|
||||||
service_root = self._get_webservice_url('findme')
|
service_root = self._get_webservice_url("findme")
|
||||||
return FindMyiPhoneServiceManager(
|
return FindMyiPhoneServiceManager(
|
||||||
service_root,
|
service_root, self.session, self.params, self.with_family
|
||||||
self.session,
|
|
||||||
self.params,
|
|
||||||
self.with_family
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -330,63 +331,51 @@ class PyiCloudService(object):
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
"""Gets the 'Account' service."""
|
"""Gets the 'Account' service."""
|
||||||
service_root = self._get_webservice_url('account')
|
service_root = self._get_webservice_url("account")
|
||||||
return AccountService(
|
return AccountService(service_root, self.session, self.params)
|
||||||
service_root,
|
|
||||||
self.session,
|
|
||||||
self.params
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def files(self):
|
def files(self):
|
||||||
"""Gets the 'File' service."""
|
"""Gets the 'File' service."""
|
||||||
if not self._files:
|
if not self._files:
|
||||||
service_root = self._get_webservice_url('ubiquity')
|
service_root = self._get_webservice_url("ubiquity")
|
||||||
self._files = UbiquityService(
|
self._files = UbiquityService(service_root, self.session, self.params)
|
||||||
service_root,
|
|
||||||
self.session,
|
|
||||||
self.params
|
|
||||||
)
|
|
||||||
return self._files
|
return self._files
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def photos(self):
|
def photos(self):
|
||||||
"""Gets the 'Photo' service."""
|
"""Gets the 'Photo' service."""
|
||||||
if not self._photos:
|
if not self._photos:
|
||||||
service_root = self._get_webservice_url('ckdatabasews')
|
service_root = self._get_webservice_url("ckdatabasews")
|
||||||
self._photos = PhotosService(
|
self._photos = PhotosService(service_root, self.session, self.params)
|
||||||
service_root,
|
|
||||||
self.session,
|
|
||||||
self.params
|
|
||||||
)
|
|
||||||
return self._photos
|
return self._photos
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def calendar(self):
|
def calendar(self):
|
||||||
"""Gets the 'Calendar' service."""
|
"""Gets the 'Calendar' service."""
|
||||||
service_root = self._get_webservice_url('calendar')
|
service_root = self._get_webservice_url("calendar")
|
||||||
return CalendarService(service_root, self.session, self.params)
|
return CalendarService(service_root, self.session, self.params)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contacts(self):
|
def contacts(self):
|
||||||
"""Gets the 'Contacts' service."""
|
"""Gets the 'Contacts' service."""
|
||||||
service_root = self._get_webservice_url('contacts')
|
service_root = self._get_webservice_url("contacts")
|
||||||
return ContactsService(service_root, self.session, self.params)
|
return ContactsService(service_root, self.session, self.params)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reminders(self):
|
def reminders(self):
|
||||||
"""Gets the 'Reminders' service."""
|
"""Gets the 'Reminders' service."""
|
||||||
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)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return 'iCloud API: %s' % self.user.get('apple_id')
|
return "iCloud API: %s" % self.user.get("apple_id")
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
as_unicode = self.__unicode__()
|
as_unicode = self.__unicode__()
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
return as_unicode
|
return as_unicode
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode("utf-8", "ignore")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<%s>' % str(self)
|
return "<%s>" % str(self)
|
||||||
|
|
|
@ -16,14 +16,14 @@ from pyicloud import PyiCloudService
|
||||||
from pyicloud.exceptions import PyiCloudFailedLoginException
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
from . import utils
|
from . import utils
|
||||||
|
|
||||||
|
# fmt: off
|
||||||
if six.PY2:
|
if six.PY2:
|
||||||
input = raw_input # pylint: disable=redefined-builtin,invalid-name,undefined-variable
|
input = raw_input # pylint: disable=redefined-builtin,invalid-name,undefined-variable
|
||||||
else:
|
else:
|
||||||
input = input # pylint: disable=bad-option-value,self-assigning-variable,invalid-name
|
input = input # pylint: disable=bad-option-value,self-assigning-variable,invalid-name
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
DEVICE_ERROR = (
|
DEVICE_ERROR = "Please use the --device switch to indicate which device to use."
|
||||||
"Please use the --device switch to indicate which device to use."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def create_pickled_data(idevice, filename):
|
def create_pickled_data(idevice, filename):
|
||||||
|
@ -34,7 +34,7 @@ def create_pickled_data(idevice, filename):
|
||||||
This allows the data to be used without resorting to screen / pipe
|
This allows the data to be used without resorting to screen / pipe
|
||||||
scrapping.
|
scrapping.
|
||||||
"""
|
"""
|
||||||
pickle_file = open(filename, 'wb')
|
pickle_file = open(filename, "wb")
|
||||||
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
pickle_file.close()
|
pickle_file.close()
|
||||||
|
|
||||||
|
@ -44,15 +44,14 @@ def main(args=None):
|
||||||
if args is None:
|
if args is None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(description="Find My iPhone CommandLine Tool")
|
||||||
description="Find My iPhone CommandLine Tool")
|
|
||||||
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--username",
|
"--username",
|
||||||
action="store",
|
action="store",
|
||||||
dest="username",
|
dest="username",
|
||||||
default="",
|
default="",
|
||||||
help="Apple ID to Use"
|
help="Apple ID to Use",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--password",
|
"--password",
|
||||||
|
@ -62,7 +61,7 @@ def main(args=None):
|
||||||
help=(
|
help=(
|
||||||
"Apple ID Password to Use; if unspecified, password will be "
|
"Apple ID Password to Use; if unspecified, password will be "
|
||||||
"fetched from the system keyring."
|
"fetched from the system keyring."
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-n",
|
"-n",
|
||||||
|
@ -70,7 +69,7 @@ def main(args=None):
|
||||||
action="store_false",
|
action="store_false",
|
||||||
dest="interactive",
|
dest="interactive",
|
||||||
default=True,
|
default=True,
|
||||||
help="Disable interactive prompts."
|
help="Disable interactive prompts.",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--delete-from-keyring",
|
"--delete-from-keyring",
|
||||||
|
@ -189,54 +188,59 @@ def main(args=None):
|
||||||
# Which password we use is determined by your username, so we
|
# Which password we use is determined by your username, so we
|
||||||
# do need to check for this first and separately.
|
# do need to check for this first and separately.
|
||||||
if not username:
|
if not username:
|
||||||
parser.error('No username supplied')
|
parser.error("No username supplied")
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
password = utils.get_password(
|
password = utils.get_password(
|
||||||
username,
|
username, interactive=command_line.interactive
|
||||||
interactive=command_line.interactive
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not password:
|
if not password:
|
||||||
parser.error('No password supplied')
|
parser.error("No password supplied")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
api = PyiCloudService(
|
api = PyiCloudService(username.strip(), password.strip())
|
||||||
username.strip(),
|
|
||||||
password.strip()
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
not utils.password_exists_in_keyring(username) and
|
not utils.password_exists_in_keyring(username)
|
||||||
command_line.interactive and
|
and command_line.interactive
|
||||||
confirm("Save password in keyring?")
|
and confirm("Save password in keyring?")
|
||||||
):
|
):
|
||||||
utils.store_password_in_keyring(username, password)
|
utils.store_password_in_keyring(username, password)
|
||||||
|
|
||||||
if api.requires_2sa:
|
if api.requires_2sa:
|
||||||
print("\nTwo-step authentication required.",
|
# fmt: off
|
||||||
"\nYour trusted devices are:")
|
print(
|
||||||
|
"\nTwo-step authentication required.",
|
||||||
|
"\nYour trusted devices are:"
|
||||||
|
)
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
devices = api.trusted_devices
|
devices = api.trusted_devices
|
||||||
for i, device in enumerate(devices):
|
for i, device in enumerate(devices):
|
||||||
print(" %s: %s" % (
|
print(
|
||||||
i, device.get(
|
" %s: %s"
|
||||||
'deviceName',
|
% (
|
||||||
"SMS to %s" % device.get('phoneNumber'))))
|
i,
|
||||||
|
device.get(
|
||||||
|
"deviceName", "SMS to %s" % device.get("phoneNumber")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
print('\nWhich device would you like to use?')
|
print("\nWhich device would you like to use?")
|
||||||
device = int(input('(number) --> '))
|
device = int(input("(number) --> "))
|
||||||
device = devices[device]
|
device = devices[device]
|
||||||
if not api.send_verification_code(device):
|
if not api.send_verification_code(device):
|
||||||
print("Failed to send verification code")
|
print("Failed to send verification code")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print('\nPlease enter validation code')
|
print("\nPlease enter validation code")
|
||||||
code = input('(string) --> ')
|
code = input("(string) --> ")
|
||||||
if not api.validate_verification_code(device, code):
|
if not api.validate_verification_code(device, code):
|
||||||
print("Failed to verify verification code")
|
print("Failed to verify verification code")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print('')
|
print("")
|
||||||
break
|
break
|
||||||
except PyiCloudFailedLoginException:
|
except PyiCloudFailedLoginException:
|
||||||
# If they have a stored password; we just used it and
|
# If they have a stored password; we just used it and
|
||||||
|
@ -256,12 +260,8 @@ def main(args=None):
|
||||||
print(message, file=sys.stderr)
|
print(message, file=sys.stderr)
|
||||||
|
|
||||||
for dev in api.devices:
|
for dev in api.devices:
|
||||||
if (
|
if not command_line.device_id or (
|
||||||
not command_line.device_id or
|
command_line.device_id.strip().lower() == dev.content["id"].strip().lower()
|
||||||
(
|
|
||||||
command_line.device_id.strip().lower() ==
|
|
||||||
dev.content["id"].strip().lower()
|
|
||||||
)
|
|
||||||
):
|
):
|
||||||
# List device(s)
|
# List device(s)
|
||||||
if command_line.locate:
|
if command_line.locate:
|
||||||
|
@ -270,9 +270,7 @@ def main(args=None):
|
||||||
if command_line.output_to_file:
|
if command_line.output_to_file:
|
||||||
create_pickled_data(
|
create_pickled_data(
|
||||||
dev,
|
dev,
|
||||||
filename=(
|
filename=(dev.content["name"].strip().lower() + ".fmip_snapshot"),
|
||||||
dev.content["name"].strip().lower() + ".fmip_snapshot"
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
contents = dev.content
|
contents = dev.content
|
||||||
|
@ -297,9 +295,10 @@ def main(args=None):
|
||||||
dev.play_sound()
|
dev.play_sound()
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"\n\n\t\t%s %s\n\n" % (
|
"\n\n\t\t%s %s\n\n"
|
||||||
|
% (
|
||||||
"Sounds can only be played on a singular device.",
|
"Sounds can only be played on a singular device.",
|
||||||
DEVICE_ERROR
|
DEVICE_ERROR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -307,16 +306,14 @@ def main(args=None):
|
||||||
if command_line.message:
|
if command_line.message:
|
||||||
if command_line.device_id:
|
if command_line.device_id:
|
||||||
dev.display_message(
|
dev.display_message(
|
||||||
subject='A Message',
|
subject="A Message", message=command_line.message, sounds=True
|
||||||
message=command_line.message,
|
|
||||||
sounds=True
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s %s" % (
|
"%s %s"
|
||||||
"Messages can only be played "
|
% (
|
||||||
"on a singular device.",
|
"Messages can only be played on a singular device.",
|
||||||
DEVICE_ERROR
|
DEVICE_ERROR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -324,16 +321,17 @@ def main(args=None):
|
||||||
if command_line.silentmessage:
|
if command_line.silentmessage:
|
||||||
if command_line.device_id:
|
if command_line.device_id:
|
||||||
dev.display_message(
|
dev.display_message(
|
||||||
subject='A Silent Message',
|
subject="A Silent Message",
|
||||||
message=command_line.silentmessage,
|
message=command_line.silentmessage,
|
||||||
sounds=False
|
sounds=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s %s" % (
|
"%s %s"
|
||||||
|
% (
|
||||||
"Silent Messages can only be played "
|
"Silent Messages can only be played "
|
||||||
"on a singular device.",
|
"on a singular device.",
|
||||||
DEVICE_ERROR
|
DEVICE_ERROR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -343,17 +341,18 @@ def main(args=None):
|
||||||
dev.lost_device(
|
dev.lost_device(
|
||||||
number=command_line.lost_phone.strip(),
|
number=command_line.lost_phone.strip(),
|
||||||
text=command_line.lost_message.strip(),
|
text=command_line.lost_message.strip(),
|
||||||
newpasscode=command_line.lost_password.strip()
|
newpasscode=command_line.lost_password.strip(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"%s %s" % (
|
"%s %s"
|
||||||
"Lost Mode can only be activated "
|
% (
|
||||||
"on a singular device.",
|
"Lost Mode can only be activated on a singular device.",
|
||||||
DEVICE_ERROR
|
DEVICE_ERROR,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
"""Library exceptions."""
|
"""Library exceptions."""
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudException(Exception):
|
class PyiCloudException(Exception):
|
||||||
"""Generic iCloud exception."""
|
"""Generic iCloud exception."""
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -8,19 +8,20 @@ from pyicloud.utils import underscore_to_camelcase
|
||||||
|
|
||||||
class AccountService(object):
|
class AccountService(object):
|
||||||
"""The 'Account' iCloud service."""
|
"""The 'Account' iCloud service."""
|
||||||
|
|
||||||
def __init__(self, service_root, session, params):
|
def __init__(self, service_root, session, params):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.params = params
|
self.params = params
|
||||||
self._service_root = service_root
|
self._service_root = service_root
|
||||||
self._devices = []
|
self._devices = []
|
||||||
|
|
||||||
self._acc_endpoint = '%s/setup/web/device' % self._service_root
|
self._acc_endpoint = "%s/setup/web/device" % self._service_root
|
||||||
self._account_devices_url = '%s/getDevices' % self._acc_endpoint
|
self._account_devices_url = "%s/getDevices" % self._acc_endpoint
|
||||||
|
|
||||||
req = self.session.get(self._account_devices_url, params=self.params)
|
req = self.session.get(self._account_devices_url, params=self.params)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
|
||||||
for device_info in self.response['devices']:
|
for device_info in self.response["devices"]:
|
||||||
# device_id = device_info['udid']
|
# device_id = device_info['udid']
|
||||||
# self._devices[device_id] = AccountDevice(device_info)
|
# self._devices[device_id] = AccountDevice(device_info)
|
||||||
self._devices.append(AccountDevice(device_info))
|
self._devices.append(AccountDevice(device_info))
|
||||||
|
@ -34,6 +35,7 @@ class AccountService(object):
|
||||||
@six.python_2_unicode_compatible
|
@six.python_2_unicode_compatible
|
||||||
class AccountDevice(dict):
|
class AccountDevice(dict):
|
||||||
"""Account device."""
|
"""Account device."""
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
try:
|
try:
|
||||||
return self[underscore_to_camelcase(name)]
|
return self[underscore_to_camelcase(name)]
|
||||||
|
@ -42,15 +44,14 @@ class AccountDevice(dict):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return u"{display_name}: {name}".format(
|
return u"{display_name}: {name}".format(
|
||||||
display_name=self.model_display_name,
|
display_name=self.model_display_name, name=self.name,
|
||||||
name=self.name,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<{display}>'.format(
|
return "<{display}>".format(
|
||||||
display=(
|
display=(
|
||||||
six.text_type(self)
|
six.text_type(self)
|
||||||
if sys.version_info[0] >= 3 else
|
if sys.version_info[0] >= 3
|
||||||
six.text_type(self).encode('utf8', 'replace')
|
else six.text_type(self).encode("utf8", "replace")
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,16 +10,15 @@ class CalendarService(object):
|
||||||
"""
|
"""
|
||||||
The 'Calendar' iCloud service, connects to iCloud and returns events.
|
The 'Calendar' iCloud service, connects to iCloud and returns events.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, service_root, session, params):
|
def __init__(self, service_root, session, params):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.params = params
|
self.params = params
|
||||||
self._service_root = service_root
|
self._service_root = service_root
|
||||||
self._calendar_endpoint = '%s/ca' % self._service_root
|
self._calendar_endpoint = "%s/ca" % self._service_root
|
||||||
self._calendar_refresh_url = '%s/events' % self._calendar_endpoint
|
self._calendar_refresh_url = "%s/events" % self._calendar_endpoint
|
||||||
self._calendar_event_detail_url = '%s/eventdetail' % (
|
self._calendar_event_detail_url = "%s/eventdetail" % (self._calendar_endpoint,)
|
||||||
self._calendar_endpoint,
|
self._calendars = "%s/startup" % self._calendar_endpoint
|
||||||
)
|
|
||||||
self._calendars = '%s/startup' % self._calendar_endpoint
|
|
||||||
|
|
||||||
self.response = {}
|
self.response = {}
|
||||||
|
|
||||||
|
@ -29,11 +28,11 @@ class CalendarService(object):
|
||||||
(a calendar) and a guid (an event's ID).
|
(a calendar) and a guid (an event's ID).
|
||||||
"""
|
"""
|
||||||
params = dict(self.params)
|
params = dict(self.params)
|
||||||
params.update({'lang': 'en-us', 'usertz': get_localzone().zone})
|
params.update({"lang": "en-us", "usertz": get_localzone().zone})
|
||||||
url = '%s/%s/%s' % (self._calendar_event_detail_url, pguid, guid)
|
url = "%s/%s/%s" % (self._calendar_event_detail_url, pguid, guid)
|
||||||
req = self.session.get(url, params=params)
|
req = self.session.get(url, params=params)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
return self.response['Event'][0]
|
return self.response["Event"][0]
|
||||||
|
|
||||||
def refresh_client(self, from_dt=None, to_dt=None):
|
def refresh_client(self, from_dt=None, to_dt=None):
|
||||||
"""
|
"""
|
||||||
|
@ -48,12 +47,14 @@ class CalendarService(object):
|
||||||
if not to_dt:
|
if not to_dt:
|
||||||
to_dt = datetime(today.year, today.month, last_day)
|
to_dt = datetime(today.year, today.month, last_day)
|
||||||
params = dict(self.params)
|
params = dict(self.params)
|
||||||
params.update({
|
params.update(
|
||||||
'lang': 'en-us',
|
{
|
||||||
'usertz': get_localzone().zone,
|
"lang": "en-us",
|
||||||
'startDate': from_dt.strftime('%Y-%m-%d'),
|
"usertz": get_localzone().zone,
|
||||||
'endDate': to_dt.strftime('%Y-%m-%d')
|
"startDate": from_dt.strftime("%Y-%m-%d"),
|
||||||
})
|
"endDate": to_dt.strftime("%Y-%m-%d"),
|
||||||
|
}
|
||||||
|
)
|
||||||
req = self.session.get(self._calendar_refresh_url, params=params)
|
req = self.session.get(self._calendar_refresh_url, params=params)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ class CalendarService(object):
|
||||||
Retrieves events for a given date range, by default, this month.
|
Retrieves events for a given date range, by default, this month.
|
||||||
"""
|
"""
|
||||||
self.refresh_client(from_dt, to_dt)
|
self.refresh_client(from_dt, to_dt)
|
||||||
return self.response.get('Event')
|
return self.response.get("Event")
|
||||||
|
|
||||||
def calendars(self):
|
def calendars(self):
|
||||||
"""
|
"""
|
||||||
|
@ -73,12 +74,14 @@ class CalendarService(object):
|
||||||
from_dt = datetime(today.year, today.month, first_day)
|
from_dt = datetime(today.year, today.month, first_day)
|
||||||
to_dt = datetime(today.year, today.month, last_day)
|
to_dt = datetime(today.year, today.month, last_day)
|
||||||
params = dict(self.params)
|
params = dict(self.params)
|
||||||
params.update({
|
params.update(
|
||||||
'lang': 'en-us',
|
{
|
||||||
'usertz': get_localzone().zone,
|
"lang": "en-us",
|
||||||
'startDate': from_dt.strftime('%Y-%m-%d'),
|
"usertz": get_localzone().zone,
|
||||||
'endDate': to_dt.strftime('%Y-%m-%d')
|
"startDate": from_dt.strftime("%Y-%m-%d"),
|
||||||
})
|
"endDate": to_dt.strftime("%Y-%m-%d"),
|
||||||
|
}
|
||||||
|
)
|
||||||
req = self.session.get(self._calendars, params=params)
|
req = self.session.get(self._calendars, params=params)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
return self.response['Collection']
|
return self.response["Collection"]
|
||||||
|
|
|
@ -11,10 +11,10 @@ class ContactsService(object):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.params = params
|
self.params = params
|
||||||
self._service_root = service_root
|
self._service_root = service_root
|
||||||
self._contacts_endpoint = '%s/co' % self._service_root
|
self._contacts_endpoint = "%s/co" % self._service_root
|
||||||
self._contacts_refresh_url = '%s/startup' % self._contacts_endpoint
|
self._contacts_refresh_url = "%s/startup" % self._contacts_endpoint
|
||||||
self._contacts_next_url = '%s/contacts' % self._contacts_endpoint
|
self._contacts_next_url = "%s/contacts" % self._contacts_endpoint
|
||||||
self._contacts_changeset_url = '%s/changeset' % self._contacts_endpoint
|
self._contacts_changeset_url = "%s/changeset" % self._contacts_endpoint
|
||||||
|
|
||||||
self.response = {}
|
self.response = {}
|
||||||
|
|
||||||
|
@ -24,28 +24,22 @@ class ContactsService(object):
|
||||||
contacts data is up-to-date.
|
contacts data is up-to-date.
|
||||||
"""
|
"""
|
||||||
params_contacts = dict(self.params)
|
params_contacts = dict(self.params)
|
||||||
params_contacts.update({
|
params_contacts.update(
|
||||||
'clientVersion': '2.1',
|
{"clientVersion": "2.1", "locale": "en_US", "order": "last,first",}
|
||||||
'locale': 'en_US',
|
|
||||||
'order': 'last,first',
|
|
||||||
})
|
|
||||||
req = self.session.get(
|
|
||||||
self._contacts_refresh_url,
|
|
||||||
params=params_contacts
|
|
||||||
)
|
)
|
||||||
|
req = self.session.get(self._contacts_refresh_url, params=params_contacts)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
|
||||||
params_next = dict(params_contacts)
|
params_next = dict(params_contacts)
|
||||||
params_next.update({
|
params_next.update(
|
||||||
'prefToken': self.response["prefToken"],
|
{
|
||||||
'syncToken': self.response["syncToken"],
|
"prefToken": self.response["prefToken"],
|
||||||
'limit': '0',
|
"syncToken": self.response["syncToken"],
|
||||||
'offset': '0',
|
"limit": "0",
|
||||||
})
|
"offset": "0",
|
||||||
req = self.session.get(
|
}
|
||||||
self._contacts_next_url,
|
|
||||||
params=params_next
|
|
||||||
)
|
)
|
||||||
|
req = self.session.get(self._contacts_next_url, params=params_next)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
|
@ -53,4 +47,4 @@ class ContactsService(object):
|
||||||
Retrieves all contacts.
|
Retrieves all contacts.
|
||||||
"""
|
"""
|
||||||
self.refresh_client()
|
self.refresh_client()
|
||||||
return self.response.get('contacts')
|
return self.response.get("contacts")
|
||||||
|
|
|
@ -19,11 +19,11 @@ class FindMyiPhoneServiceManager(object):
|
||||||
self.params = params
|
self.params = params
|
||||||
self.with_family = with_family
|
self.with_family = with_family
|
||||||
|
|
||||||
fmip_endpoint = '%s/fmipservice/client/web' % service_root
|
fmip_endpoint = "%s/fmipservice/client/web" % service_root
|
||||||
self._fmip_refresh_url = '%s/refreshClient' % fmip_endpoint
|
self._fmip_refresh_url = "%s/refreshClient" % fmip_endpoint
|
||||||
self._fmip_sound_url = '%s/playSound' % fmip_endpoint
|
self._fmip_sound_url = "%s/playSound" % fmip_endpoint
|
||||||
self._fmip_message_url = '%s/sendMessage' % fmip_endpoint
|
self._fmip_message_url = "%s/sendMessage" % fmip_endpoint
|
||||||
self._fmip_lost_url = '%s/lostDevice' % fmip_endpoint
|
self._fmip_lost_url = "%s/lostDevice" % fmip_endpoint
|
||||||
|
|
||||||
self._devices = {}
|
self._devices = {}
|
||||||
self.refresh_client()
|
self.refresh_client()
|
||||||
|
@ -39,18 +39,18 @@ class FindMyiPhoneServiceManager(object):
|
||||||
params=self.params,
|
params=self.params,
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
'clientContext': {
|
"clientContext": {
|
||||||
'fmly': self.with_family,
|
"fmly": self.with_family,
|
||||||
'shouldLocate': True,
|
"shouldLocate": True,
|
||||||
'selectedDevice': 'all',
|
"selectedDevice": "all",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
)
|
)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
|
||||||
for device_info in self.response['content']:
|
for device_info in self.response["content"]:
|
||||||
device_id = device_info['id']
|
device_id = device_info["id"]
|
||||||
if device_id not in self._devices:
|
if device_id not in self._devices:
|
||||||
self._devices[device_id] = AppleDevice(
|
self._devices[device_id] = AppleDevice(
|
||||||
device_info,
|
device_info,
|
||||||
|
@ -85,7 +85,7 @@ class FindMyiPhoneServiceManager(object):
|
||||||
as_unicode = self.__unicode__()
|
as_unicode = self.__unicode__()
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
return as_unicode
|
return as_unicode
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode("utf-8", "ignore")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return six.text_type(self)
|
return six.text_type(self)
|
||||||
|
@ -93,9 +93,16 @@ class FindMyiPhoneServiceManager(object):
|
||||||
|
|
||||||
class AppleDevice(object):
|
class AppleDevice(object):
|
||||||
"""Apple device."""
|
"""Apple device."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, content, session, params, manager,
|
self,
|
||||||
sound_url=None, lost_url=None, message_url=None
|
content,
|
||||||
|
session,
|
||||||
|
params,
|
||||||
|
manager,
|
||||||
|
sound_url=None,
|
||||||
|
lost_url=None,
|
||||||
|
message_url=None,
|
||||||
):
|
):
|
||||||
self.content = content
|
self.content = content
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
|
@ -113,7 +120,7 @@ class AppleDevice(object):
|
||||||
def location(self):
|
def location(self):
|
||||||
"""Updates the device location."""
|
"""Updates the device location."""
|
||||||
self.manager.refresh_client()
|
self.manager.refresh_client()
|
||||||
return self.content['location']
|
return self.content["location"]
|
||||||
|
|
||||||
def status(self, additional=[]): # pylint: disable=dangerous-default-value
|
def status(self, additional=[]): # pylint: disable=dangerous-default-value
|
||||||
"""Returns status information for device.
|
"""Returns status information for device.
|
||||||
|
@ -121,34 +128,29 @@ class AppleDevice(object):
|
||||||
This returns only a subset of possible properties.
|
This returns only a subset of possible properties.
|
||||||
"""
|
"""
|
||||||
self.manager.refresh_client()
|
self.manager.refresh_client()
|
||||||
fields = ['batteryLevel', 'deviceDisplayName', 'deviceStatus', 'name']
|
fields = ["batteryLevel", "deviceDisplayName", "deviceStatus", "name"]
|
||||||
fields += additional
|
fields += additional
|
||||||
properties = {}
|
properties = {}
|
||||||
for field in fields:
|
for field in fields:
|
||||||
properties[field] = self.content.get(field)
|
properties[field] = self.content.get(field)
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
def play_sound(self, subject='Find My iPhone Alert'):
|
def play_sound(self, subject="Find My iPhone Alert"):
|
||||||
"""Send a request to the device to play a sound.
|
"""Send a request to the device to play a sound.
|
||||||
|
|
||||||
It's possible to pass a custom message by changing the `subject`.
|
It's possible to pass a custom message by changing the `subject`.
|
||||||
"""
|
"""
|
||||||
data = json.dumps({
|
data = json.dumps(
|
||||||
'device': self.content['id'],
|
{
|
||||||
'subject': subject,
|
"device": self.content["id"],
|
||||||
'clientContext': {
|
"subject": subject,
|
||||||
'fmly': True
|
"clientContext": {"fmly": True},
|
||||||
}
|
}
|
||||||
})
|
|
||||||
self.session.post(
|
|
||||||
self.sound_url,
|
|
||||||
params=self.params,
|
|
||||||
data=data
|
|
||||||
)
|
)
|
||||||
|
self.session.post(self.sound_url, params=self.params, data=data)
|
||||||
|
|
||||||
def display_message(
|
def display_message(
|
||||||
self, subject='Find My iPhone Alert', message="This is a note",
|
self, subject="Find My iPhone Alert", message="This is a note", sounds=False
|
||||||
sounds=False
|
|
||||||
):
|
):
|
||||||
"""Send a request to the device to play a sound.
|
"""Send a request to the device to play a sound.
|
||||||
|
|
||||||
|
@ -156,23 +158,17 @@ class AppleDevice(object):
|
||||||
"""
|
"""
|
||||||
data = json.dumps(
|
data = json.dumps(
|
||||||
{
|
{
|
||||||
'device': self.content['id'],
|
"device": self.content["id"],
|
||||||
'subject': subject,
|
"subject": subject,
|
||||||
'sound': sounds,
|
"sound": sounds,
|
||||||
'userText': True,
|
"userText": True,
|
||||||
'text': message
|
"text": message,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.session.post(
|
self.session.post(self.message_url, params=self.params, data=data)
|
||||||
self.message_url,
|
|
||||||
params=self.params,
|
|
||||||
data=data
|
|
||||||
)
|
|
||||||
|
|
||||||
def lost_device(
|
def lost_device(
|
||||||
self, number,
|
self, number, text="This iPhone has been lost. Please call me.", newpasscode=""
|
||||||
text='This iPhone has been lost. Please call me.',
|
|
||||||
newpasscode=""
|
|
||||||
):
|
):
|
||||||
"""Send a request to the device to trigger 'lost mode'.
|
"""Send a request to the device to trigger 'lost mode'.
|
||||||
|
|
||||||
|
@ -180,20 +176,18 @@ class AppleDevice(object):
|
||||||
been passed, then the person holding the device can call
|
been passed, then the person holding the device can call
|
||||||
the number without entering the passcode.
|
the number without entering the passcode.
|
||||||
"""
|
"""
|
||||||
data = json.dumps({
|
data = json.dumps(
|
||||||
'text': text,
|
{
|
||||||
'userText': True,
|
"text": text,
|
||||||
'ownerNbr': number,
|
"userText": True,
|
||||||
'lostModeEnabled': True,
|
"ownerNbr": number,
|
||||||
'trackingEnabled': True,
|
"lostModeEnabled": True,
|
||||||
'device': self.content['id'],
|
"trackingEnabled": True,
|
||||||
'passcode': newpasscode
|
"device": self.content["id"],
|
||||||
})
|
"passcode": newpasscode,
|
||||||
self.session.post(
|
}
|
||||||
self.lost_url,
|
|
||||||
params=self.params,
|
|
||||||
data=data
|
|
||||||
)
|
)
|
||||||
|
self.session.post(self.lost_url, params=self.params, data=data)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
|
@ -207,18 +201,15 @@ class AppleDevice(object):
|
||||||
return getattr(self.content, attr)
|
return getattr(self.content, attr)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
display_name = self['deviceDisplayName']
|
display_name = self["deviceDisplayName"]
|
||||||
name = self['name']
|
name = self["name"]
|
||||||
return '%s: %s' % (
|
return "%s: %s" % (display_name, name,)
|
||||||
display_name,
|
|
||||||
name,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
as_unicode = self.__unicode__()
|
as_unicode = self.__unicode__()
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
return as_unicode
|
return as_unicode
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode("utf-8", "ignore")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<AppleDevice(%s)>' % str(self)
|
return "<AppleDevice(%s)>" % str(self)
|
||||||
|
|
|
@ -12,121 +12,115 @@ from future.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
class PhotosService(object):
|
class PhotosService(object):
|
||||||
"""The 'Photos' iCloud service."""
|
"""The 'Photos' iCloud service."""
|
||||||
|
|
||||||
SMART_FOLDERS = {
|
SMART_FOLDERS = {
|
||||||
"All Photos": {
|
"All Photos": {
|
||||||
"obj_type": "CPLAssetByAddedDate",
|
"obj_type": "CPLAssetByAddedDate",
|
||||||
"list_type": "CPLAssetAndMasterByAddedDate",
|
"list_type": "CPLAssetAndMasterByAddedDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": None
|
"query_filter": None,
|
||||||
},
|
},
|
||||||
"Time-lapse": {
|
"Time-lapse": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Timelapse",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Timelapse",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "TIMELAPSE"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "TIMELAPSE"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Videos": {
|
"Videos": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Video",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Video",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "VIDEO"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "VIDEO"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Slo-mo": {
|
"Slo-mo": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Slomo",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Slomo",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "SLOMO"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "SLOMO"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Bursts": {
|
"Bursts": {
|
||||||
"obj_type": "CPLAssetBurstStackAssetByAssetDate",
|
"obj_type": "CPLAssetBurstStackAssetByAssetDate",
|
||||||
"list_type": "CPLBurstStackAssetAndMasterByAssetDate",
|
"list_type": "CPLBurstStackAssetAndMasterByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": None
|
"query_filter": None,
|
||||||
},
|
},
|
||||||
"Favorites": {
|
"Favorites": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Favorite",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Favorite",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "FAVORITE"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "FAVORITE"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Panoramas": {
|
"Panoramas": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Panorama",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Panorama",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "PANORAMA"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "PANORAMA"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Screenshots": {
|
"Screenshots": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Screenshot",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Screenshot",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "SCREENSHOT"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "SCREENSHOT"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Live": {
|
"Live": {
|
||||||
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Live",
|
"obj_type": "CPLAssetInSmartAlbumByAssetDate:Live",
|
||||||
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
"list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": [{
|
"query_filter": [
|
||||||
|
{
|
||||||
"fieldName": "smartAlbum",
|
"fieldName": "smartAlbum",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": "LIVE"},
|
||||||
"type": "STRING",
|
|
||||||
"value": "LIVE"
|
|
||||||
}
|
}
|
||||||
}]
|
],
|
||||||
},
|
},
|
||||||
"Recently Deleted": {
|
"Recently Deleted": {
|
||||||
"obj_type": "CPLAssetDeletedByExpungedDate",
|
"obj_type": "CPLAssetDeletedByExpungedDate",
|
||||||
"list_type": "CPLAssetAndMasterDeletedByExpungedDate",
|
"list_type": "CPLAssetAndMasterDeletedByExpungedDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": None
|
"query_filter": None,
|
||||||
},
|
},
|
||||||
"Hidden": {
|
"Hidden": {
|
||||||
"obj_type": "CPLAssetHiddenByAssetDate",
|
"obj_type": "CPLAssetHiddenByAssetDate",
|
||||||
"list_type": "CPLAssetAndMasterHiddenByAssetDate",
|
"list_type": "CPLAssetAndMasterHiddenByAssetDate",
|
||||||
"direction": "ASCENDING",
|
"direction": "ASCENDING",
|
||||||
"query_filter": None
|
"query_filter": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,32 +128,29 @@ class PhotosService(object):
|
||||||
self.session = session
|
self.session = session
|
||||||
self.params = dict(params)
|
self.params = dict(params)
|
||||||
self._service_root = service_root
|
self._service_root = service_root
|
||||||
self.service_endpoint = \
|
self.service_endpoint = (
|
||||||
('%s/database/1/com.apple.photos.cloud/production/private'
|
"%s/database/1/com.apple.photos.cloud/production/private"
|
||||||
% self._service_root)
|
% self._service_root
|
||||||
|
)
|
||||||
|
|
||||||
self._albums = None
|
self._albums = None
|
||||||
|
|
||||||
self.params.update({
|
self.params.update({"remapEnums": True, "getCurrentSyncToken": True})
|
||||||
'remapEnums': True,
|
|
||||||
'getCurrentSyncToken': True
|
|
||||||
})
|
|
||||||
|
|
||||||
url = ('%s/records/query?%s' %
|
url = "%s/records/query?%s" % (self.service_endpoint, urlencode(self.params))
|
||||||
(self.service_endpoint, urlencode(self.params)))
|
json_data = (
|
||||||
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
'{"query":{"recordType":"CheckIndexingState"},'
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'"zoneID":{"zoneName":"PrimarySync"}}'
|
||||||
|
)
|
||||||
request = self.session.post(
|
request = self.session.post(
|
||||||
url,
|
url, data=json_data, headers={"Content-type": "text/plain"}
|
||||||
data=json_data,
|
|
||||||
headers={'Content-type': 'text/plain'}
|
|
||||||
)
|
)
|
||||||
response = request.json()
|
response = request.json()
|
||||||
indexing_state = response['records'][0]['fields']['state']['value']
|
indexing_state = response["records"][0]["fields"]["state"]["value"]
|
||||||
if indexing_state != 'FINISHED':
|
if indexing_state != "FINISHED":
|
||||||
raise PyiCloudServiceNotActivatedException(
|
raise PyiCloudServiceNotActivatedException(
|
||||||
'iCloud Photo Library not finished indexing. '
|
"iCloud Photo Library not finished indexing. "
|
||||||
'Please try again in a few minutes.'
|
"Please try again in a few minutes."
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Does syncToken ever change? # pylint: disable=fixme
|
# TODO: Does syncToken ever change? # pylint: disable=fixme
|
||||||
|
@ -174,63 +165,79 @@ class PhotosService(object):
|
||||||
def albums(self):
|
def albums(self):
|
||||||
"""Returns photo albums."""
|
"""Returns photo albums."""
|
||||||
if not self._albums:
|
if not self._albums:
|
||||||
self._albums = {name: PhotoAlbum(self, name, **props)
|
self._albums = {
|
||||||
for (name, props) in self.SMART_FOLDERS.items()}
|
name: PhotoAlbum(self, name, **props)
|
||||||
|
for (name, props) in self.SMART_FOLDERS.items()
|
||||||
|
}
|
||||||
|
|
||||||
for folder in self._fetch_folders():
|
for folder in self._fetch_folders():
|
||||||
# TODO: Handle subfolders # pylint: disable=fixme
|
# TODO: Handle subfolders # pylint: disable=fixme
|
||||||
if folder['recordName'] == '----Root-Folder----' or \
|
if folder["recordName"] == "----Root-Folder----" or (
|
||||||
(folder['fields'].get('isDeleted') and
|
folder["fields"].get("isDeleted")
|
||||||
folder['fields']['isDeleted']['value']):
|
and folder["fields"]["isDeleted"]["value"]
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
folder_id = folder['recordName']
|
folder_id = folder["recordName"]
|
||||||
folder_obj_type = \
|
folder_obj_type = (
|
||||||
"CPLContainerRelationNotDeletedByAssetDate:%s" % folder_id
|
"CPLContainerRelationNotDeletedByAssetDate:%s" % folder_id
|
||||||
|
)
|
||||||
folder_name = base64.b64decode(
|
folder_name = base64.b64decode(
|
||||||
folder['fields']['albumNameEnc']['value']).decode('utf-8')
|
folder["fields"]["albumNameEnc"]["value"]
|
||||||
query_filter = [{
|
).decode("utf-8")
|
||||||
|
query_filter = [
|
||||||
|
{
|
||||||
"fieldName": "parentId",
|
"fieldName": "parentId",
|
||||||
"comparator": "EQUALS",
|
"comparator": "EQUALS",
|
||||||
"fieldValue": {
|
"fieldValue": {"type": "STRING", "value": folder_id},
|
||||||
"type": "STRING",
|
|
||||||
"value": folder_id
|
|
||||||
}
|
}
|
||||||
}]
|
]
|
||||||
|
|
||||||
album = PhotoAlbum(self, folder_name,
|
album = PhotoAlbum(
|
||||||
'CPLContainerRelationLiveByAssetDate',
|
self,
|
||||||
folder_obj_type, 'ASCENDING', query_filter)
|
folder_name,
|
||||||
|
"CPLContainerRelationLiveByAssetDate",
|
||||||
|
folder_obj_type,
|
||||||
|
"ASCENDING",
|
||||||
|
query_filter,
|
||||||
|
)
|
||||||
self._albums[folder_name] = album
|
self._albums[folder_name] = album
|
||||||
|
|
||||||
return self._albums
|
return self._albums
|
||||||
|
|
||||||
def _fetch_folders(self):
|
def _fetch_folders(self):
|
||||||
url = ('%s/records/query?%s' %
|
url = "%s/records/query?%s" % (self.service_endpoint, urlencode(self.params))
|
||||||
(self.service_endpoint, urlencode(self.params)))
|
json_data = (
|
||||||
json_data = ('{"query":{"recordType":"CPLAlbumByPositionLive"},'
|
'{"query":{"recordType":"CPLAlbumByPositionLive"},'
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'"zoneID":{"zoneName":"PrimarySync"}}'
|
||||||
|
)
|
||||||
|
|
||||||
request = self.session.post(
|
request = self.session.post(
|
||||||
url,
|
url, data=json_data, headers={"Content-type": "text/plain"}
|
||||||
data=json_data,
|
|
||||||
headers={'Content-type': 'text/plain'}
|
|
||||||
)
|
)
|
||||||
response = request.json()
|
response = request.json()
|
||||||
|
|
||||||
return response['records']
|
return response["records"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self):
|
def all(self):
|
||||||
"""Returns all photos."""
|
"""Returns all photos."""
|
||||||
return self.albums['All Photos']
|
return self.albums["All Photos"]
|
||||||
|
|
||||||
|
|
||||||
class PhotoAlbum(object):
|
class PhotoAlbum(object):
|
||||||
"""A photo album."""
|
"""A photo album."""
|
||||||
|
|
||||||
def __init__(self, service, name, list_type, obj_type, direction,
|
def __init__(
|
||||||
query_filter=None, page_size=100):
|
self,
|
||||||
|
service,
|
||||||
|
name,
|
||||||
|
list_type,
|
||||||
|
obj_type,
|
||||||
|
direction,
|
||||||
|
query_filter=None,
|
||||||
|
page_size=100,
|
||||||
|
):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.service = service
|
self.service = service
|
||||||
self.list_type = list_type
|
self.list_type = list_type
|
||||||
|
@ -251,41 +258,41 @@ class PhotoAlbum(object):
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
if self._len is None:
|
if self._len is None:
|
||||||
url = ('%s/internal/records/query/batch?%s' %
|
url = "%s/internal/records/query/batch?%s" % (
|
||||||
(self.service.service_endpoint,
|
self.service.service_endpoint,
|
||||||
urlencode(self.service.params)))
|
urlencode(self.service.params),
|
||||||
|
)
|
||||||
request = self.service.session.post(
|
request = self.service.session.post(
|
||||||
url,
|
url,
|
||||||
data=json.dumps(
|
data=json.dumps(
|
||||||
{
|
{
|
||||||
u'batch': [{
|
u"batch": [
|
||||||
u'resultsLimit': 1,
|
{
|
||||||
u'query': {
|
u"resultsLimit": 1,
|
||||||
u'filterBy': {
|
u"query": {
|
||||||
u'fieldName': u'indexCountID',
|
u"filterBy": {
|
||||||
u'fieldValue': {
|
u"fieldName": u"indexCountID",
|
||||||
u'type': u'STRING_LIST',
|
u"fieldValue": {
|
||||||
u'value': [
|
u"type": u"STRING_LIST",
|
||||||
self.obj_type
|
u"value": [self.obj_type],
|
||||||
]
|
|
||||||
},
|
},
|
||||||
u'comparator': u'IN'
|
u"comparator": u"IN",
|
||||||
},
|
},
|
||||||
u'recordType': u'HyperionIndexCountLookup'
|
u"recordType": u"HyperionIndexCountLookup",
|
||||||
},
|
},
|
||||||
u'zoneWide': True,
|
u"zoneWide": True,
|
||||||
u'zoneID': {
|
u"zoneID": {u"zoneName": u"PrimarySync"},
|
||||||
u'zoneName': u'PrimarySync'
|
|
||||||
}
|
}
|
||||||
}]
|
]
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
headers={'Content-type': 'text/plain'}
|
headers={"Content-type": "text/plain"},
|
||||||
)
|
)
|
||||||
response = request.json()
|
response = request.json()
|
||||||
|
|
||||||
self._len = (response["batch"][0]["records"][0]["fields"]
|
self._len = response["batch"][0]["records"][0]["fields"]["itemCount"][
|
||||||
["itemCount"]["value"])
|
"value"
|
||||||
|
]
|
||||||
|
|
||||||
return self._len
|
return self._len
|
||||||
|
|
||||||
|
@ -297,26 +304,28 @@ class PhotoAlbum(object):
|
||||||
else:
|
else:
|
||||||
offset = 0
|
offset = 0
|
||||||
|
|
||||||
while(True):
|
while True:
|
||||||
url = ('%s/records/query?' % self.service.service_endpoint) + \
|
url = ("%s/records/query?" % self.service.service_endpoint) + urlencode(
|
||||||
urlencode(self.service.params)
|
self.service.params
|
||||||
|
)
|
||||||
request = self.service.session.post(
|
request = self.service.session.post(
|
||||||
url,
|
url,
|
||||||
data=json.dumps(self._list_query_gen(
|
data=json.dumps(
|
||||||
offset, self.list_type, self.direction,
|
self._list_query_gen(
|
||||||
self.query_filter)),
|
offset, self.list_type, self.direction, self.query_filter
|
||||||
headers={'Content-type': 'text/plain'}
|
)
|
||||||
|
),
|
||||||
|
headers={"Content-type": "text/plain"},
|
||||||
)
|
)
|
||||||
response = request.json()
|
response = request.json()
|
||||||
|
|
||||||
asset_records = {}
|
asset_records = {}
|
||||||
master_records = []
|
master_records = []
|
||||||
for rec in response['records']:
|
for rec in response["records"]:
|
||||||
if rec['recordType'] == "CPLAsset":
|
if rec["recordType"] == "CPLAsset":
|
||||||
master_id = \
|
master_id = rec["fields"]["masterRef"]["value"]["recordName"]
|
||||||
rec['fields']['masterRef']['value']['recordName']
|
|
||||||
asset_records[master_id] = rec
|
asset_records[master_id] = rec
|
||||||
elif rec['recordType'] == "CPLMaster":
|
elif rec["recordType"] == "CPLMaster":
|
||||||
master_records.append(rec)
|
master_records.append(rec)
|
||||||
|
|
||||||
master_records_len = len(master_records)
|
master_records_len = len(master_records)
|
||||||
|
@ -327,72 +336,135 @@ class PhotoAlbum(object):
|
||||||
offset = offset + master_records_len
|
offset = offset + master_records_len
|
||||||
|
|
||||||
for master_record in master_records:
|
for master_record in master_records:
|
||||||
record_name = master_record['recordName']
|
record_name = master_record["recordName"]
|
||||||
yield PhotoAsset(self.service, master_record,
|
yield PhotoAsset(
|
||||||
asset_records[record_name])
|
self.service, master_record, asset_records[record_name]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
def _list_query_gen(self, offset, list_type, direction, query_filter=None):
|
def _list_query_gen(self, offset, list_type, direction, query_filter=None):
|
||||||
query = {
|
query = {
|
||||||
u'query': {
|
u"query": {
|
||||||
u'filterBy': [
|
u"filterBy": [
|
||||||
{u'fieldName': u'startRank', u'fieldValue':
|
{
|
||||||
{u'type': u'INT64', u'value': offset},
|
u"fieldName": u"startRank",
|
||||||
u'comparator': u'EQUALS'},
|
u"fieldValue": {u"type": u"INT64", u"value": offset},
|
||||||
{u'fieldName': u'direction', u'fieldValue':
|
u"comparator": u"EQUALS",
|
||||||
{u'type': u'STRING', u'value': direction},
|
},
|
||||||
u'comparator': u'EQUALS'}
|
{
|
||||||
],
|
u"fieldName": u"direction",
|
||||||
u'recordType': list_type
|
u"fieldValue": {u"type": u"STRING", u"value": direction},
|
||||||
|
u"comparator": u"EQUALS",
|
||||||
},
|
},
|
||||||
u'resultsLimit': self.page_size * 2,
|
|
||||||
u'desiredKeys': [
|
|
||||||
u'resJPEGFullWidth', u'resJPEGFullHeight',
|
|
||||||
u'resJPEGFullFileType', u'resJPEGFullFingerprint',
|
|
||||||
u'resJPEGFullRes', u'resJPEGLargeWidth',
|
|
||||||
u'resJPEGLargeHeight', u'resJPEGLargeFileType',
|
|
||||||
u'resJPEGLargeFingerprint', u'resJPEGLargeRes',
|
|
||||||
u'resJPEGMedWidth', u'resJPEGMedHeight',
|
|
||||||
u'resJPEGMedFileType', u'resJPEGMedFingerprint',
|
|
||||||
u'resJPEGMedRes', u'resJPEGThumbWidth',
|
|
||||||
u'resJPEGThumbHeight', u'resJPEGThumbFileType',
|
|
||||||
u'resJPEGThumbFingerprint', u'resJPEGThumbRes',
|
|
||||||
u'resVidFullWidth', u'resVidFullHeight',
|
|
||||||
u'resVidFullFileType', u'resVidFullFingerprint',
|
|
||||||
u'resVidFullRes', u'resVidMedWidth', u'resVidMedHeight',
|
|
||||||
u'resVidMedFileType', u'resVidMedFingerprint',
|
|
||||||
u'resVidMedRes', u'resVidSmallWidth', u'resVidSmallHeight',
|
|
||||||
u'resVidSmallFileType', u'resVidSmallFingerprint',
|
|
||||||
u'resVidSmallRes', u'resSidecarWidth', u'resSidecarHeight',
|
|
||||||
u'resSidecarFileType', u'resSidecarFingerprint',
|
|
||||||
u'resSidecarRes', u'itemType', u'dataClassType',
|
|
||||||
u'filenameEnc', u'originalOrientation', u'resOriginalWidth',
|
|
||||||
u'resOriginalHeight', u'resOriginalFileType',
|
|
||||||
u'resOriginalFingerprint', u'resOriginalRes',
|
|
||||||
u'resOriginalAltWidth', u'resOriginalAltHeight',
|
|
||||||
u'resOriginalAltFileType', u'resOriginalAltFingerprint',
|
|
||||||
u'resOriginalAltRes', u'resOriginalVidComplWidth',
|
|
||||||
u'resOriginalVidComplHeight', u'resOriginalVidComplFileType',
|
|
||||||
u'resOriginalVidComplFingerprint', u'resOriginalVidComplRes',
|
|
||||||
u'isDeleted', u'isExpunged', u'dateExpunged', u'remappedRef',
|
|
||||||
u'recordName', u'recordType', u'recordChangeTag',
|
|
||||||
u'masterRef', u'adjustmentRenderType', u'assetDate',
|
|
||||||
u'addedDate', u'isFavorite', u'isHidden', u'orientation',
|
|
||||||
u'duration', u'assetSubtype', u'assetSubtypeV2',
|
|
||||||
u'assetHDRType', u'burstFlags', u'burstFlagsExt', u'burstId',
|
|
||||||
u'captionEnc', u'locationEnc', u'locationV2Enc',
|
|
||||||
u'locationLatitude', u'locationLongitude', u'adjustmentType',
|
|
||||||
u'timeZoneOffset', u'vidComplDurValue', u'vidComplDurScale',
|
|
||||||
u'vidComplDispValue', u'vidComplDispScale',
|
|
||||||
u'vidComplVisibilityState', u'customRenderedValue',
|
|
||||||
u'containerId', u'itemId', u'position', u'isKeyAsset'
|
|
||||||
],
|
],
|
||||||
u'zoneID': {u'zoneName': u'PrimarySync'}
|
u"recordType": list_type,
|
||||||
|
},
|
||||||
|
u"resultsLimit": self.page_size * 2,
|
||||||
|
u"desiredKeys": [
|
||||||
|
u"resJPEGFullWidth",
|
||||||
|
u"resJPEGFullHeight",
|
||||||
|
u"resJPEGFullFileType",
|
||||||
|
u"resJPEGFullFingerprint",
|
||||||
|
u"resJPEGFullRes",
|
||||||
|
u"resJPEGLargeWidth",
|
||||||
|
u"resJPEGLargeHeight",
|
||||||
|
u"resJPEGLargeFileType",
|
||||||
|
u"resJPEGLargeFingerprint",
|
||||||
|
u"resJPEGLargeRes",
|
||||||
|
u"resJPEGMedWidth",
|
||||||
|
u"resJPEGMedHeight",
|
||||||
|
u"resJPEGMedFileType",
|
||||||
|
u"resJPEGMedFingerprint",
|
||||||
|
u"resJPEGMedRes",
|
||||||
|
u"resJPEGThumbWidth",
|
||||||
|
u"resJPEGThumbHeight",
|
||||||
|
u"resJPEGThumbFileType",
|
||||||
|
u"resJPEGThumbFingerprint",
|
||||||
|
u"resJPEGThumbRes",
|
||||||
|
u"resVidFullWidth",
|
||||||
|
u"resVidFullHeight",
|
||||||
|
u"resVidFullFileType",
|
||||||
|
u"resVidFullFingerprint",
|
||||||
|
u"resVidFullRes",
|
||||||
|
u"resVidMedWidth",
|
||||||
|
u"resVidMedHeight",
|
||||||
|
u"resVidMedFileType",
|
||||||
|
u"resVidMedFingerprint",
|
||||||
|
u"resVidMedRes",
|
||||||
|
u"resVidSmallWidth",
|
||||||
|
u"resVidSmallHeight",
|
||||||
|
u"resVidSmallFileType",
|
||||||
|
u"resVidSmallFingerprint",
|
||||||
|
u"resVidSmallRes",
|
||||||
|
u"resSidecarWidth",
|
||||||
|
u"resSidecarHeight",
|
||||||
|
u"resSidecarFileType",
|
||||||
|
u"resSidecarFingerprint",
|
||||||
|
u"resSidecarRes",
|
||||||
|
u"itemType",
|
||||||
|
u"dataClassType",
|
||||||
|
u"filenameEnc",
|
||||||
|
u"originalOrientation",
|
||||||
|
u"resOriginalWidth",
|
||||||
|
u"resOriginalHeight",
|
||||||
|
u"resOriginalFileType",
|
||||||
|
u"resOriginalFingerprint",
|
||||||
|
u"resOriginalRes",
|
||||||
|
u"resOriginalAltWidth",
|
||||||
|
u"resOriginalAltHeight",
|
||||||
|
u"resOriginalAltFileType",
|
||||||
|
u"resOriginalAltFingerprint",
|
||||||
|
u"resOriginalAltRes",
|
||||||
|
u"resOriginalVidComplWidth",
|
||||||
|
u"resOriginalVidComplHeight",
|
||||||
|
u"resOriginalVidComplFileType",
|
||||||
|
u"resOriginalVidComplFingerprint",
|
||||||
|
u"resOriginalVidComplRes",
|
||||||
|
u"isDeleted",
|
||||||
|
u"isExpunged",
|
||||||
|
u"dateExpunged",
|
||||||
|
u"remappedRef",
|
||||||
|
u"recordName",
|
||||||
|
u"recordType",
|
||||||
|
u"recordChangeTag",
|
||||||
|
u"masterRef",
|
||||||
|
u"adjustmentRenderType",
|
||||||
|
u"assetDate",
|
||||||
|
u"addedDate",
|
||||||
|
u"isFavorite",
|
||||||
|
u"isHidden",
|
||||||
|
u"orientation",
|
||||||
|
u"duration",
|
||||||
|
u"assetSubtype",
|
||||||
|
u"assetSubtypeV2",
|
||||||
|
u"assetHDRType",
|
||||||
|
u"burstFlags",
|
||||||
|
u"burstFlagsExt",
|
||||||
|
u"burstId",
|
||||||
|
u"captionEnc",
|
||||||
|
u"locationEnc",
|
||||||
|
u"locationV2Enc",
|
||||||
|
u"locationLatitude",
|
||||||
|
u"locationLongitude",
|
||||||
|
u"adjustmentType",
|
||||||
|
u"timeZoneOffset",
|
||||||
|
u"vidComplDurValue",
|
||||||
|
u"vidComplDurScale",
|
||||||
|
u"vidComplDispValue",
|
||||||
|
u"vidComplDispScale",
|
||||||
|
u"vidComplVisibilityState",
|
||||||
|
u"customRenderedValue",
|
||||||
|
u"containerId",
|
||||||
|
u"itemId",
|
||||||
|
u"position",
|
||||||
|
u"isKeyAsset",
|
||||||
|
],
|
||||||
|
u"zoneID": {u"zoneName": u"PrimarySync"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if query_filter:
|
if query_filter:
|
||||||
query['query']['filterBy'].extend(query_filter)
|
query["query"]["filterBy"].extend(query_filter)
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
@ -403,17 +475,15 @@ class PhotoAlbum(object):
|
||||||
as_unicode = self.__unicode__()
|
as_unicode = self.__unicode__()
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
return as_unicode
|
return as_unicode
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode("utf-8", "ignore")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (
|
return "<%s: '%s'>" % (type(self).__name__, self)
|
||||||
type(self).__name__,
|
|
||||||
self
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PhotoAsset(object):
|
class PhotoAsset(object):
|
||||||
"""A photo."""
|
"""A photo."""
|
||||||
|
|
||||||
def __init__(self, service, master_record, asset_record):
|
def __init__(self, service, master_record, asset_record):
|
||||||
self._service = service
|
self._service = service
|
||||||
self._master_record = master_record
|
self._master_record = master_record
|
||||||
|
@ -424,31 +494,31 @@ class PhotoAsset(object):
|
||||||
PHOTO_VERSION_LOOKUP = {
|
PHOTO_VERSION_LOOKUP = {
|
||||||
u"original": u"resOriginal",
|
u"original": u"resOriginal",
|
||||||
u"medium": u"resJPEGMed",
|
u"medium": u"resJPEGMed",
|
||||||
u"thumb": u"resJPEGThumb"
|
u"thumb": u"resJPEGThumb",
|
||||||
}
|
}
|
||||||
|
|
||||||
VIDEO_VERSION_LOOKUP = {
|
VIDEO_VERSION_LOOKUP = {
|
||||||
u"original": u"resOriginal",
|
u"original": u"resOriginal",
|
||||||
u"medium": u"resVidMed",
|
u"medium": u"resVidMed",
|
||||||
u"thumb": u"resVidSmall"
|
u"thumb": u"resVidSmall",
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
"""Gets the photo id."""
|
"""Gets the photo id."""
|
||||||
return self._master_record['recordName']
|
return self._master_record["recordName"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
"""Gets the photo file name."""
|
"""Gets the photo file name."""
|
||||||
return base64.b64decode(
|
return base64.b64decode(
|
||||||
self._master_record['fields']['filenameEnc']['value']
|
self._master_record["fields"]["filenameEnc"]["value"]
|
||||||
).decode('utf-8')
|
).decode("utf-8")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
"""Gets the photo size."""
|
"""Gets the photo size."""
|
||||||
return self._master_record['fields']['resOriginalRes']['value']['size']
|
return self._master_record["fields"]["resOriginalRes"]["value"]["size"]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created(self):
|
def created(self):
|
||||||
|
@ -460,8 +530,8 @@ class PhotoAsset(object):
|
||||||
"""Gets the photo asset date."""
|
"""Gets the photo asset date."""
|
||||||
try:
|
try:
|
||||||
return datetime.fromtimestamp(
|
return datetime.fromtimestamp(
|
||||||
self._asset_record['fields']['assetDate']['value'] / 1000.0,
|
self._asset_record["fields"]["assetDate"]["value"] / 1000.0, tz=UTC
|
||||||
tz=UTC)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return datetime.fromtimestamp(0)
|
return datetime.fromtimestamp(0)
|
||||||
|
|
||||||
|
@ -469,106 +539,104 @@ class PhotoAsset(object):
|
||||||
def added_date(self):
|
def added_date(self):
|
||||||
"""Gets the photo added date."""
|
"""Gets the photo added date."""
|
||||||
return datetime.fromtimestamp(
|
return datetime.fromtimestamp(
|
||||||
self._asset_record['fields']['addedDate']['value'] / 1000.0,
|
self._asset_record["fields"]["addedDate"]["value"] / 1000.0, tz=UTC
|
||||||
tz=UTC)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dimensions(self):
|
def dimensions(self):
|
||||||
"""Gets the photo dimensions."""
|
"""Gets the photo dimensions."""
|
||||||
return (self._master_record['fields']['resOriginalWidth']['value'],
|
return (
|
||||||
self._master_record['fields']['resOriginalHeight']['value'])
|
self._master_record["fields"]["resOriginalWidth"]["value"],
|
||||||
|
self._master_record["fields"]["resOriginalHeight"]["value"],
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def versions(self):
|
def versions(self):
|
||||||
"""Gets the photo versions."""
|
"""Gets the photo versions."""
|
||||||
if not self._versions:
|
if not self._versions:
|
||||||
self._versions = {}
|
self._versions = {}
|
||||||
if 'resVidSmallRes' in self._master_record['fields']:
|
if "resVidSmallRes" in self._master_record["fields"]:
|
||||||
typed_version_lookup = self.VIDEO_VERSION_LOOKUP
|
typed_version_lookup = self.VIDEO_VERSION_LOOKUP
|
||||||
else:
|
else:
|
||||||
typed_version_lookup = self.PHOTO_VERSION_LOOKUP
|
typed_version_lookup = self.PHOTO_VERSION_LOOKUP
|
||||||
|
|
||||||
for key, prefix in typed_version_lookup.items():
|
for key, prefix in typed_version_lookup.items():
|
||||||
if '%sRes' % prefix in self._master_record['fields']:
|
if "%sRes" % prefix in self._master_record["fields"]:
|
||||||
fields = self._master_record['fields']
|
fields = self._master_record["fields"]
|
||||||
version = {'filename': self.filename}
|
version = {"filename": self.filename}
|
||||||
|
|
||||||
width_entry = fields.get('%sWidth' % prefix)
|
width_entry = fields.get("%sWidth" % prefix)
|
||||||
if width_entry:
|
if width_entry:
|
||||||
version['width'] = width_entry['value']
|
version["width"] = width_entry["value"]
|
||||||
else:
|
else:
|
||||||
version['width'] = None
|
version["width"] = None
|
||||||
|
|
||||||
height_entry = fields.get('%sHeight' % prefix)
|
height_entry = fields.get("%sHeight" % prefix)
|
||||||
if height_entry:
|
if height_entry:
|
||||||
version['height'] = height_entry['value']
|
version["height"] = height_entry["value"]
|
||||||
else:
|
else:
|
||||||
version['height'] = None
|
version["height"] = None
|
||||||
|
|
||||||
size_entry = fields.get('%sRes' % prefix)
|
size_entry = fields.get("%sRes" % prefix)
|
||||||
if size_entry:
|
if size_entry:
|
||||||
version['size'] = size_entry['value']['size']
|
version["size"] = size_entry["value"]["size"]
|
||||||
version['url'] = size_entry['value']['downloadURL']
|
version["url"] = size_entry["value"]["downloadURL"]
|
||||||
else:
|
else:
|
||||||
version['size'] = None
|
version["size"] = None
|
||||||
version['url'] = None
|
version["url"] = None
|
||||||
|
|
||||||
type_entry = fields.get('%sFileType' % prefix)
|
type_entry = fields.get("%sFileType" % prefix)
|
||||||
if type_entry:
|
if type_entry:
|
||||||
version['type'] = type_entry['value']
|
version["type"] = type_entry["value"]
|
||||||
else:
|
else:
|
||||||
version['type'] = None
|
version["type"] = None
|
||||||
|
|
||||||
self._versions[key] = version
|
self._versions[key] = version
|
||||||
|
|
||||||
return self._versions
|
return self._versions
|
||||||
|
|
||||||
def download(self, version='original', **kwargs):
|
def download(self, version="original", **kwargs):
|
||||||
"""Returns the photo file."""
|
"""Returns the photo file."""
|
||||||
if version not in self.versions:
|
if version not in self.versions:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._service.session.get(
|
return self._service.session.get(
|
||||||
self.versions[version]['url'],
|
self.versions[version]["url"], stream=True, **kwargs
|
||||||
stream=True,
|
|
||||||
**kwargs
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""Deletes the photo."""
|
"""Deletes the photo."""
|
||||||
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
json_data = (
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'{"query":{"recordType":"CheckIndexingState"},'
|
||||||
|
'"zoneID":{"zoneName":"PrimarySync"}}'
|
||||||
|
)
|
||||||
|
|
||||||
json_data = ('{"operations":[{'
|
json_data = (
|
||||||
|
'{"operations":[{'
|
||||||
'"operationType":"update",'
|
'"operationType":"update",'
|
||||||
'"record":{'
|
'"record":{'
|
||||||
'"recordName":"%s",'
|
'"recordName":"%s",'
|
||||||
'"recordType":"%s",'
|
'"recordType":"%s",'
|
||||||
'"recordChangeTag":"%s",'
|
'"recordChangeTag":"%s",'
|
||||||
'"fields":{"isDeleted":{"value":1}'
|
'"fields":{"isDeleted":{"value":1}'
|
||||||
'}}}],'
|
"}}}],"
|
||||||
'"zoneID":{'
|
'"zoneID":{'
|
||||||
'"zoneName":"PrimarySync"'
|
'"zoneName":"PrimarySync"'
|
||||||
'},"atomic":true}'
|
'},"atomic":true}'
|
||||||
% (
|
% (
|
||||||
self._asset_record['recordName'],
|
self._asset_record["recordName"],
|
||||||
self._asset_record['recordType'],
|
self._asset_record["recordType"],
|
||||||
self._master_record['recordChangeTag']
|
self._master_record["recordChangeTag"],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
endpoint = self._service.service_endpoint
|
endpoint = self._service.service_endpoint
|
||||||
params = urlencode(self._service.params)
|
params = urlencode(self._service.params)
|
||||||
url = ('%s/records/modify?%s' % (endpoint, params))
|
url = "%s/records/modify?%s" % (endpoint, params)
|
||||||
|
|
||||||
return self._service.session.post(
|
return self._service.session.post(
|
||||||
url,
|
url, data=json_data, headers={"Content-type": "text/plain"}
|
||||||
data=json_data,
|
|
||||||
headers={'Content-type': 'text/plain'}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: id=%s>" % (
|
return "<%s: id=%s>" % (type(self).__name__, self.id)
|
||||||
type(self).__name__,
|
|
||||||
self.id
|
|
||||||
)
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ from tzlocal import get_localzone
|
||||||
|
|
||||||
class RemindersService(object):
|
class RemindersService(object):
|
||||||
"""The 'Reminders' iCloud service."""
|
"""The 'Reminders' iCloud service."""
|
||||||
|
|
||||||
def __init__(self, service_root, session, params):
|
def __init__(self, service_root, session, params):
|
||||||
self.session = session
|
self.session = session
|
||||||
self._params = params
|
self._params = params
|
||||||
|
@ -23,64 +24,61 @@ class RemindersService(object):
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
"""Refresh data."""
|
"""Refresh data."""
|
||||||
params_reminders = dict(self._params)
|
params_reminders = dict(self._params)
|
||||||
params_reminders.update({
|
params_reminders.update(
|
||||||
'clientVersion': '4.0',
|
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone}
|
||||||
'lang': 'en-us',
|
)
|
||||||
'usertz': get_localzone().zone
|
|
||||||
})
|
|
||||||
|
|
||||||
# Open reminders
|
# Open reminders
|
||||||
req = self.session.get(
|
req = self.session.get(
|
||||||
self._service_root + '/rd/startup',
|
self._service_root + "/rd/startup", params=params_reminders
|
||||||
params=params_reminders
|
|
||||||
)
|
)
|
||||||
|
|
||||||
data = req.json()
|
data = req.json()
|
||||||
|
|
||||||
self.lists = {}
|
self.lists = {}
|
||||||
self.collections = {}
|
self.collections = {}
|
||||||
for collection in data['Collections']:
|
for collection in data["Collections"]:
|
||||||
temp = []
|
temp = []
|
||||||
self.collections[collection['title']] = {
|
self.collections[collection["title"]] = {
|
||||||
'guid': collection['guid'],
|
"guid": collection["guid"],
|
||||||
'ctag': collection['ctag']
|
"ctag": collection["ctag"],
|
||||||
}
|
}
|
||||||
for reminder in data['Reminders']:
|
for reminder in data["Reminders"]:
|
||||||
|
|
||||||
if reminder['pGuid'] != collection['guid']:
|
if reminder["pGuid"] != collection["guid"]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if reminder.get('dueDate'):
|
if reminder.get("dueDate"):
|
||||||
due = datetime(
|
due = datetime(
|
||||||
reminder['dueDate'][1],
|
reminder["dueDate"][1],
|
||||||
reminder['dueDate'][2],
|
reminder["dueDate"][2],
|
||||||
reminder['dueDate'][3],
|
reminder["dueDate"][3],
|
||||||
reminder['dueDate'][4],
|
reminder["dueDate"][4],
|
||||||
reminder['dueDate'][5]
|
reminder["dueDate"][5],
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
due = None
|
due = None
|
||||||
|
|
||||||
temp.append({
|
temp.append(
|
||||||
"title": reminder['title'],
|
{
|
||||||
"desc": reminder.get('description'),
|
"title": reminder["title"],
|
||||||
"due": due
|
"desc": reminder.get("description"),
|
||||||
})
|
"due": due,
|
||||||
self.lists[collection['title']] = temp
|
}
|
||||||
|
)
|
||||||
|
self.lists[collection["title"]] = temp
|
||||||
|
|
||||||
def post(self, title, description="", collection=None, due_date=None):
|
def post(self, title, description="", collection=None, due_date=None):
|
||||||
"""Adds a new reminder."""
|
"""Adds a new reminder."""
|
||||||
pguid = 'tasks'
|
pguid = "tasks"
|
||||||
if collection:
|
if collection:
|
||||||
if collection in self.collections:
|
if collection in self.collections:
|
||||||
pguid = self.collections[collection]['guid']
|
pguid = self.collections[collection]["guid"]
|
||||||
|
|
||||||
params_reminders = dict(self._params)
|
params_reminders = dict(self._params)
|
||||||
params_reminders.update({
|
params_reminders.update(
|
||||||
'clientVersion': '4.0',
|
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone}
|
||||||
'lang': 'en-us',
|
)
|
||||||
'usertz': get_localzone().zone
|
|
||||||
})
|
|
||||||
|
|
||||||
due_dates = None
|
due_dates = None
|
||||||
if due_date:
|
if due_date:
|
||||||
|
@ -90,14 +88,15 @@ class RemindersService(object):
|
||||||
due_date.month,
|
due_date.month,
|
||||||
due_date.day,
|
due_date.day,
|
||||||
due_date.hour,
|
due_date.hour,
|
||||||
due_date.minute
|
due_date.minute,
|
||||||
]
|
]
|
||||||
|
|
||||||
req = self.session.post(
|
req = self.session.post(
|
||||||
self._service_root + '/rd/reminders/tasks',
|
self._service_root + "/rd/reminders/tasks",
|
||||||
data=json.dumps({
|
data=json.dumps(
|
||||||
|
{
|
||||||
"Reminders": {
|
"Reminders": {
|
||||||
'title': title,
|
"title": title,
|
||||||
"description": description,
|
"description": description,
|
||||||
"pGuid": pguid,
|
"pGuid": pguid,
|
||||||
"etag": None,
|
"etag": None,
|
||||||
|
@ -115,9 +114,11 @@ class RemindersService(object):
|
||||||
"createdDate": None,
|
"createdDate": None,
|
||||||
"isFamily": None,
|
"isFamily": None,
|
||||||
"createdDateExtended": int(time.time() * 1000),
|
"createdDateExtended": int(time.time() * 1000),
|
||||||
"guid": str(uuid.uuid4())
|
"guid": str(uuid.uuid4()),
|
||||||
},
|
},
|
||||||
"ClientState": {"Collections": list(self.collections.values())}
|
"ClientState": {"Collections": list(self.collections.values())},
|
||||||
}),
|
}
|
||||||
params=params_reminders)
|
),
|
||||||
|
params=params_reminders,
|
||||||
|
)
|
||||||
return req.ok
|
return req.ok
|
||||||
|
|
|
@ -11,7 +11,7 @@ class UbiquityService(object):
|
||||||
self.params = params
|
self.params = params
|
||||||
|
|
||||||
self._root = None
|
self._root = None
|
||||||
self._node_url = service_root + '/ws/%s/%s/%s'
|
self._node_url = service_root + "/ws/%s/%s/%s"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def root(self):
|
def root(self):
|
||||||
|
@ -20,13 +20,9 @@ class UbiquityService(object):
|
||||||
self._root = self.get_node(0)
|
self._root = self.get_node(0)
|
||||||
return self._root
|
return self._root
|
||||||
|
|
||||||
def get_node_url(self, node_id, variant='item'):
|
def get_node_url(self, node_id, variant="item"):
|
||||||
"""Returns a node URL."""
|
"""Returns a node URL."""
|
||||||
return self._node_url % (
|
return self._node_url % (self.params["dsid"], variant, node_id)
|
||||||
self.params['dsid'],
|
|
||||||
variant,
|
|
||||||
node_id
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_node(self, node_id):
|
def get_node(self, node_id):
|
||||||
"""Returns a node."""
|
"""Returns a node."""
|
||||||
|
@ -35,18 +31,13 @@ class UbiquityService(object):
|
||||||
|
|
||||||
def get_children(self, node_id):
|
def get_children(self, node_id):
|
||||||
"""Returns a node children."""
|
"""Returns a node children."""
|
||||||
request = self.session.get(
|
request = self.session.get(self.get_node_url(node_id, "parent"))
|
||||||
self.get_node_url(node_id, 'parent')
|
items = request.json()["item_list"]
|
||||||
)
|
|
||||||
items = request.json()['item_list']
|
|
||||||
return [UbiquityNode(self, item) for item in items]
|
return [UbiquityNode(self, item) for item in items]
|
||||||
|
|
||||||
def get_file(self, node_id, **kwargs):
|
def get_file(self, node_id, **kwargs):
|
||||||
"""Returns a node file."""
|
"""Returns a node file."""
|
||||||
return self.session.get(
|
return self.session.get(self.get_node_url(node_id, "file"), **kwargs)
|
||||||
self.get_node_url(node_id, 'file'),
|
|
||||||
**kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.root, attr)
|
return getattr(self.root, attr)
|
||||||
|
@ -57,6 +48,7 @@ class UbiquityService(object):
|
||||||
|
|
||||||
class UbiquityNode(object):
|
class UbiquityNode(object):
|
||||||
"""Ubiquity node."""
|
"""Ubiquity node."""
|
||||||
|
|
||||||
def __init__(self, conn, data):
|
def __init__(self, conn, data):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.connection = conn
|
self.connection = conn
|
||||||
|
@ -66,33 +58,30 @@ class UbiquityNode(object):
|
||||||
@property
|
@property
|
||||||
def item_id(self):
|
def item_id(self):
|
||||||
"""Gets the node id."""
|
"""Gets the node id."""
|
||||||
return self.data.get('item_id')
|
return self.data.get("item_id")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
"""Gets the node name."""
|
"""Gets the node name."""
|
||||||
return self.data.get('name')
|
return self.data.get("name")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
"""Gets the node type."""
|
"""Gets the node type."""
|
||||||
return self.data.get('type')
|
return self.data.get("type")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
"""Gets the node size."""
|
"""Gets the node size."""
|
||||||
try:
|
try:
|
||||||
return int(self.data.get('size'))
|
return int(self.data.get("size"))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modified(self):
|
def modified(self):
|
||||||
"""Gets the node modified date."""
|
"""Gets the node modified date."""
|
||||||
return datetime.strptime(
|
return datetime.strptime(self.data.get("modified"), "%Y-%m-%dT%H:%M:%SZ")
|
||||||
self.data.get('modified'),
|
|
||||||
'%Y-%m-%dT%H:%M:%SZ'
|
|
||||||
)
|
|
||||||
|
|
||||||
def open(self, **kwargs):
|
def open(self, **kwargs):
|
||||||
"""Returns the node file."""
|
"""Returns the node file."""
|
||||||
|
@ -110,15 +99,13 @@ class UbiquityNode(object):
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name):
|
||||||
"""Returns a child node by its name."""
|
"""Returns a child node by its name."""
|
||||||
return [
|
return [child for child in self.get_children() if child.name == name][0]
|
||||||
child for child in self.get_children() if child.name == name
|
|
||||||
][0]
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
try:
|
try:
|
||||||
return self.get(key)
|
return self.get(key)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
raise KeyError('No child named %s exists' % key)
|
raise KeyError("No child named %s exists" % key)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
@ -127,10 +114,7 @@ class UbiquityNode(object):
|
||||||
as_unicode = self.__unicode__()
|
as_unicode = self.__unicode__()
|
||||||
if sys.version_info[0] >= 3:
|
if sys.version_info[0] >= 3:
|
||||||
return as_unicode
|
return as_unicode
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode("utf-8", "ignore")
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: '%s'>" % (
|
return "<%s: '%s'>" % (self.type.capitalize(), self)
|
||||||
self.type.capitalize(),
|
|
||||||
self
|
|
||||||
)
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import sys
|
||||||
from .exceptions import PyiCloudNoStoredPasswordAvailableException
|
from .exceptions import PyiCloudNoStoredPasswordAvailableException
|
||||||
|
|
||||||
|
|
||||||
KEYRING_SYSTEM = 'pyicloud://icloud-password'
|
KEYRING_SYSTEM = "pyicloud://icloud-password"
|
||||||
|
|
||||||
|
|
||||||
def get_password(username, interactive=sys.stdout.isatty()):
|
def get_password(username, interactive=sys.stdout.isatty()):
|
||||||
|
@ -18,9 +18,7 @@ def get_password(username, interactive=sys.stdout.isatty()):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return getpass.getpass(
|
return getpass.getpass(
|
||||||
'Enter iCloud password for {username}: '.format(
|
"Enter iCloud password for {username}: ".format(username=username,)
|
||||||
username=username,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,18 +34,13 @@ def password_exists_in_keyring(username):
|
||||||
|
|
||||||
def get_password_from_keyring(username):
|
def get_password_from_keyring(username):
|
||||||
"""Get the password from a username."""
|
"""Get the password from a username."""
|
||||||
result = keyring.get_password(
|
result = keyring.get_password(KEYRING_SYSTEM, username)
|
||||||
KEYRING_SYSTEM,
|
|
||||||
username
|
|
||||||
)
|
|
||||||
if result is None:
|
if result is None:
|
||||||
raise PyiCloudNoStoredPasswordAvailableException(
|
raise PyiCloudNoStoredPasswordAvailableException(
|
||||||
"No pyicloud password for {username} could be found "
|
"No pyicloud password for {username} could be found "
|
||||||
"in the system keychain. Use the `--store-in-keyring` "
|
"in the system keychain. Use the `--store-in-keyring` "
|
||||||
"command-line option for storing a password for this "
|
"command-line option for storing a password for this "
|
||||||
"username.".format(
|
"username.".format(username=username,)
|
||||||
username=username,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
@ -55,25 +48,18 @@ def get_password_from_keyring(username):
|
||||||
|
|
||||||
def store_password_in_keyring(username, password):
|
def store_password_in_keyring(username, password):
|
||||||
"""Store the password of a username."""
|
"""Store the password of a username."""
|
||||||
return keyring.set_password(
|
return keyring.set_password(KEYRING_SYSTEM, username, password,)
|
||||||
KEYRING_SYSTEM,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def delete_password_in_keyring(username):
|
def delete_password_in_keyring(username):
|
||||||
"""Delete the password of a username."""
|
"""Delete the password of a username."""
|
||||||
return keyring.delete_password(
|
return keyring.delete_password(KEYRING_SYSTEM, username,)
|
||||||
KEYRING_SYSTEM,
|
|
||||||
username,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def underscore_to_camelcase(word, initial_capital=False):
|
def underscore_to_camelcase(word, initial_capital=False):
|
||||||
"""Transform a word to camelCase."""
|
"""Transform a word to camelCase."""
|
||||||
words = [x.capitalize() or '_' for x in word.split('_')]
|
words = [x.capitalize() or "_" for x in word.split("_")]
|
||||||
if not initial_capital:
|
if not initial_capital:
|
||||||
words[0] = words[0].lower()
|
words[0] = words[0].lower()
|
||||||
|
|
||||||
return ''.join(words)
|
return "".join(words)
|
||||||
|
|
21
pyproject.toml
Normal file
21
pyproject.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ["py27", "py33", "py34", "py35", "py36", "py37", "py38"]
|
||||||
|
exclude = '''
|
||||||
|
|
||||||
|
(
|
||||||
|
/(
|
||||||
|
\.eggs
|
||||||
|
| \.git
|
||||||
|
| \.hg
|
||||||
|
| \.mypy_cache
|
||||||
|
| \.tox
|
||||||
|
| \.venv
|
||||||
|
| _build
|
||||||
|
| buck-out
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
| exceptions.py
|
||||||
|
)
|
||||||
|
'''
|
16
scripts/check_format.sh
Executable file
16
scripts/check_format.sh
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
./scripts/common.sh
|
||||||
|
|
||||||
|
if ! hash python3; then
|
||||||
|
echo "python3 is not installed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ver=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/')
|
||||||
|
if [ "$ver" -lt "36" ]; then
|
||||||
|
echo "This script requires python 3.6 or greater"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pip install black==19.10b0
|
||||||
|
|
||||||
|
black --check --fast .
|
36
setup.py
36
setup.py
|
@ -1,38 +1,34 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
with open('requirements.txt') as f:
|
with open("requirements.txt") as f:
|
||||||
required = f.read().splitlines()
|
required = f.read().splitlines()
|
||||||
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='pyicloud',
|
name="pyicloud",
|
||||||
version='0.9.6.1',
|
version="0.9.6.1",
|
||||||
url='https://github.com/picklepete/pyicloud',
|
url="https://github.com/picklepete/pyicloud",
|
||||||
description=(
|
description=(
|
||||||
'PyiCloud is a module which allows pythonistas to '
|
"PyiCloud is a module which allows pythonistas to "
|
||||||
'interact with iCloud webservices.'
|
"interact with iCloud webservices."
|
||||||
),
|
),
|
||||||
maintainer='The PyiCloud Authors',
|
maintainer="The PyiCloud Authors",
|
||||||
maintainer_email=' ',
|
maintainer_email=" ",
|
||||||
license='MIT',
|
license="MIT",
|
||||||
packages=find_packages(include=["pyicloud*"]),
|
packages=find_packages(include=["pyicloud*"]),
|
||||||
install_requires=required,
|
install_requires=required,
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Intended Audience :: Developers',
|
"Intended Audience :: Developers",
|
||||||
'License :: OSI Approved :: MIT License',
|
"License :: OSI Approved :: MIT License",
|
||||||
'Operating System :: OS Independent',
|
"Operating System :: OS Independent",
|
||||||
'Programming Language :: Python',
|
"Programming Language :: Python",
|
||||||
'Programming Language :: Python :: 2.7',
|
"Programming Language :: Python :: 2.7",
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
"Programming Language :: Python :: 3.3",
|
"Programming Language :: Python :: 3.3",
|
||||||
"Programming Language :: Python :: 3.4",
|
"Programming Language :: Python :: 3.4",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={"console_scripts": ["icloud = pyicloud.cmdline:main"]},
|
||||||
'console_scripts': [
|
|
||||||
'icloud = pyicloud.cmdline:main'
|
|
||||||
]
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,6 +12,7 @@ VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2SA_USER]
|
||||||
|
|
||||||
class PyiCloudServiceMock(base.PyiCloudService):
|
class PyiCloudServiceMock(base.PyiCloudService):
|
||||||
"""Mocked PyiCloudService."""
|
"""Mocked PyiCloudService."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
apple_id,
|
apple_id,
|
||||||
|
@ -19,34 +20,33 @@ class PyiCloudServiceMock(base.PyiCloudService):
|
||||||
cookie_directory=None,
|
cookie_directory=None,
|
||||||
verify=True,
|
verify=True,
|
||||||
client_id=None,
|
client_id=None,
|
||||||
with_family=True
|
with_family=True,
|
||||||
):
|
):
|
||||||
base.PyiCloudService.__init__(self, apple_id, password, cookie_directory, verify, client_id, with_family)
|
base.PyiCloudService.__init__(
|
||||||
|
self, apple_id, password, cookie_directory, verify, client_id, with_family
|
||||||
|
)
|
||||||
base.FindMyiPhoneServiceManager = FindMyiPhoneServiceManagerMock
|
base.FindMyiPhoneServiceManager = FindMyiPhoneServiceManagerMock
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
if not self.user.get("apple_id") or self.user.get("apple_id") not in VALID_USERS:
|
if (
|
||||||
raise PyiCloudFailedLoginException("Invalid email/password combination.", None)
|
not self.user.get("apple_id")
|
||||||
|
or self.user.get("apple_id") not in VALID_USERS
|
||||||
|
):
|
||||||
|
raise PyiCloudFailedLoginException(
|
||||||
|
"Invalid email/password combination.", None
|
||||||
|
)
|
||||||
if not self.user.get("password") or self.user.get("password") != "valid_pass":
|
if not self.user.get("password") or self.user.get("password") != "valid_pass":
|
||||||
raise PyiCloudFailedLoginException("Invalid email/password combination.", None)
|
raise PyiCloudFailedLoginException(
|
||||||
|
"Invalid email/password combination.", None
|
||||||
|
)
|
||||||
|
|
||||||
self.params.update({'dsid': 'ID'})
|
self.params.update({"dsid": "ID"})
|
||||||
self._webservices = {
|
self._webservices = {
|
||||||
'account': {
|
"account": {"url": "account_url",},
|
||||||
'url': 'account_url',
|
"findme": {"url": "findme_url",},
|
||||||
},
|
"calendar": {"url": "calendar_url",},
|
||||||
'findme': {
|
"contacts": {"url": "contacts_url",},
|
||||||
'url': 'findme_url',
|
"reminders": {"url": "reminders_url",},
|
||||||
},
|
|
||||||
'calendar': {
|
|
||||||
'url': 'calendar_url',
|
|
||||||
},
|
|
||||||
'contacts': {
|
|
||||||
'url': 'contacts_url',
|
|
||||||
},
|
|
||||||
'reminders': {
|
|
||||||
'url': 'reminders_url',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -56,7 +56,12 @@ class PyiCloudServiceMock(base.PyiCloudService):
|
||||||
@property
|
@property
|
||||||
def trusted_devices(self):
|
def trusted_devices(self):
|
||||||
return [
|
return [
|
||||||
{"deviceType": "SMS", "areaCode": "", "phoneNumber": "*******58", "deviceId": "1"}
|
{
|
||||||
|
"deviceType": "SMS",
|
||||||
|
"areaCode": "",
|
||||||
|
"phoneNumber": "*******58",
|
||||||
|
"deviceId": "1",
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
def send_verification_code(self, device):
|
def send_verification_code(self, device):
|
||||||
|
@ -78,7 +83,7 @@ IPHONE_DEVICE = AppleDevice(
|
||||||
"playSound": True,
|
"playSound": True,
|
||||||
"vibrate": True,
|
"vibrate": True,
|
||||||
"createTimestamp": 1568031021347,
|
"createTimestamp": 1568031021347,
|
||||||
"statusCode": "200"
|
"statusCode": "200",
|
||||||
},
|
},
|
||||||
"canWipeAfterLock": True,
|
"canWipeAfterLock": True,
|
||||||
"baUUID": "",
|
"baUUID": "",
|
||||||
|
@ -111,7 +116,7 @@ IPHONE_DEVICE = AppleDevice(
|
||||||
"CWP": False,
|
"CWP": False,
|
||||||
"KEY": False,
|
"KEY": False,
|
||||||
"KPD": False,
|
"KPD": False,
|
||||||
"WIP": True
|
"WIP": True,
|
||||||
},
|
},
|
||||||
"lowPowerMode": True,
|
"lowPowerMode": True,
|
||||||
"rawDeviceModel": "iPhone11,8",
|
"rawDeviceModel": "iPhone11,8",
|
||||||
|
@ -125,10 +130,7 @@ IPHONE_DEVICE = AppleDevice(
|
||||||
"locationEnabled": True,
|
"locationEnabled": True,
|
||||||
"lockedTimestamp": None,
|
"lockedTimestamp": None,
|
||||||
"locFoundEnabled": False,
|
"locFoundEnabled": False,
|
||||||
"snd": {
|
"snd": {"createTimestamp": 1568031021347, "statusCode": "200"},
|
||||||
"createTimestamp": 1568031021347,
|
|
||||||
"statusCode": "200"
|
|
||||||
},
|
|
||||||
"fmlyShare": False,
|
"fmlyShare": False,
|
||||||
"lostDevice": {
|
"lostDevice": {
|
||||||
"stopLostMode": False,
|
"stopLostMode": False,
|
||||||
|
@ -138,7 +140,7 @@ IPHONE_DEVICE = AppleDevice(
|
||||||
"ownerNbr": "",
|
"ownerNbr": "",
|
||||||
"text": "",
|
"text": "",
|
||||||
"createTimestamp": 1558383841233,
|
"createTimestamp": 1558383841233,
|
||||||
"statusCode": "2204"
|
"statusCode": "2204",
|
||||||
},
|
},
|
||||||
"lostModeCapable": True,
|
"lostModeCapable": True,
|
||||||
"wipedTimestamp": None,
|
"wipedTimestamp": None,
|
||||||
|
@ -164,16 +166,16 @@ IPHONE_DEVICE = AppleDevice(
|
||||||
"timeStamp": 1568827039692,
|
"timeStamp": 1568827039692,
|
||||||
"locationFinished": False,
|
"locationFinished": False,
|
||||||
"verticalAccuracy": 0.0,
|
"verticalAccuracy": 0.0,
|
||||||
"longitude": 5.012345678
|
"longitude": 5.012345678,
|
||||||
},
|
},
|
||||||
"deviceModel": "iphoneXR-1-6-0",
|
"deviceModel": "iphoneXR-1-6-0",
|
||||||
"maxMsgChar": 160,
|
"maxMsgChar": 160,
|
||||||
"darkWake": False,
|
"darkWake": False,
|
||||||
"remoteWipe": None
|
"remoteWipe": None,
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
None
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
DEVICES = {
|
DEVICES = {
|
||||||
|
@ -183,8 +185,11 @@ DEVICES = {
|
||||||
|
|
||||||
class FindMyiPhoneServiceManagerMock(FindMyiPhoneServiceManager):
|
class FindMyiPhoneServiceManagerMock(FindMyiPhoneServiceManager):
|
||||||
"""Mocked FindMyiPhoneServiceManager."""
|
"""Mocked FindMyiPhoneServiceManager."""
|
||||||
|
|
||||||
def __init__(self, service_root, session, params, with_family=False):
|
def __init__(self, service_root, session, params, with_family=False):
|
||||||
FindMyiPhoneServiceManager.__init__(self, service_root, session, params, with_family)
|
FindMyiPhoneServiceManager.__init__(
|
||||||
|
self, service_root, session, params, with_family
|
||||||
|
)
|
||||||
|
|
||||||
def refresh_client(self):
|
def refresh_client(self):
|
||||||
self._devices = DEVICES
|
self._devices = DEVICES
|
||||||
|
|
|
@ -7,13 +7,16 @@ import sys
|
||||||
import pickle
|
import pickle
|
||||||
import pytest
|
import pytest
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
if sys.version_info >= (3, 3):
|
if sys.version_info >= (3, 3):
|
||||||
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
|
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
|
||||||
else:
|
else:
|
||||||
from mock import patch
|
from mock import patch
|
||||||
|
|
||||||
|
|
||||||
class TestCmdline(TestCase):
|
class TestCmdline(TestCase):
|
||||||
"""Cmdline test cases."""
|
"""Cmdline test cases."""
|
||||||
|
|
||||||
main = None
|
main = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -34,13 +37,13 @@ class TestCmdline(TestCase):
|
||||||
def test_help(self):
|
def test_help(self):
|
||||||
"""Test the help command."""
|
"""Test the help command."""
|
||||||
with pytest.raises(SystemExit, match="0"):
|
with pytest.raises(SystemExit, match="0"):
|
||||||
self.main(['--help'])
|
self.main(["--help"])
|
||||||
|
|
||||||
def test_username(self):
|
def test_username(self):
|
||||||
"""Test the username command."""
|
"""Test the username command."""
|
||||||
# No username supplied
|
# No username supplied
|
||||||
with pytest.raises(SystemExit, match="2"):
|
with pytest.raises(SystemExit, match="2"):
|
||||||
self.main(['--username'])
|
self.main(["--username"])
|
||||||
|
|
||||||
@patch("getpass.getpass")
|
@patch("getpass.getpass")
|
||||||
def test_username_password_invalid(self, mock_getpass):
|
def test_username_password_invalid(self, mock_getpass):
|
||||||
|
@ -48,42 +51,49 @@ class TestCmdline(TestCase):
|
||||||
# No password supplied
|
# No password supplied
|
||||||
mock_getpass.return_value = None
|
mock_getpass.return_value = None
|
||||||
with pytest.raises(SystemExit, match="2"):
|
with pytest.raises(SystemExit, match="2"):
|
||||||
self.main(['--username', 'invalid_user'])
|
self.main(["--username", "invalid_user"])
|
||||||
|
|
||||||
# Bad username or password
|
# Bad username or password
|
||||||
mock_getpass.return_value = "invalid_pass"
|
mock_getpass.return_value = "invalid_pass"
|
||||||
with pytest.raises(RuntimeError, match="Bad username or password for invalid_user"):
|
with pytest.raises(
|
||||||
self.main(['--username', 'invalid_user'])
|
RuntimeError, match="Bad username or password for invalid_user"
|
||||||
|
):
|
||||||
|
self.main(["--username", "invalid_user"])
|
||||||
|
|
||||||
# We should not use getpass for this one, but we reset the password at login fail
|
# We should not use getpass for this one, but we reset the password at login fail
|
||||||
with pytest.raises(RuntimeError, match="Bad username or password for invalid_user"):
|
with pytest.raises(
|
||||||
self.main(['--username', 'invalid_user', '--password', 'invalid_pass'])
|
RuntimeError, match="Bad username or password for invalid_user"
|
||||||
|
):
|
||||||
|
self.main(["--username", "invalid_user", "--password", "invalid_pass"])
|
||||||
|
|
||||||
|
@patch("pyicloud.cmdline.input")
|
||||||
@patch('pyicloud.cmdline.input')
|
|
||||||
def test_username_password_requires_2sa(self, mock_input):
|
def test_username_password_requires_2sa(self, mock_input):
|
||||||
"""Test username and password commands."""
|
"""Test username and password commands."""
|
||||||
# Valid connection for the first time
|
# Valid connection for the first time
|
||||||
mock_input.return_value = "0"
|
mock_input.return_value = "0"
|
||||||
with pytest.raises(SystemExit, match="0"):
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
# fmt: off
|
||||||
self.main([
|
self.main([
|
||||||
'--username', REQUIRES_2SA_USER,
|
'--username', REQUIRES_2SA_USER,
|
||||||
'--password', 'valid_pass',
|
'--password', 'valid_pass',
|
||||||
'--non-interactive',
|
'--non-interactive',
|
||||||
])
|
])
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
def test_device_outputfile(self):
|
def test_device_outputfile(self):
|
||||||
"""Test the outputfile command."""
|
"""Test the outputfile command."""
|
||||||
with pytest.raises(SystemExit, match="0"):
|
with pytest.raises(SystemExit, match="0"):
|
||||||
|
# fmt: off
|
||||||
self.main([
|
self.main([
|
||||||
'--username', AUTHENTICATED_USER,
|
'--username', AUTHENTICATED_USER,
|
||||||
'--password', 'valid_pass',
|
'--password', 'valid_pass',
|
||||||
'--non-interactive',
|
'--non-interactive',
|
||||||
'--outputfile'
|
'--outputfile'
|
||||||
])
|
])
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
for key in DEVICES:
|
for key in DEVICES:
|
||||||
file_name = DEVICES[key].content['name'].strip().lower() + ".fmip_snapshot"
|
file_name = DEVICES[key].content["name"].strip().lower() + ".fmip_snapshot"
|
||||||
|
|
||||||
pickle_file = open(file_name, "rb")
|
pickle_file = open(file_name, "rb")
|
||||||
assert pickle_file
|
assert pickle_file
|
||||||
|
|
Loading…
Reference in a new issue