Replace PEP8 by pylint (#257)
This commit is contained in:
parent
a6358630e3
commit
1090393774
18 changed files with 295 additions and 190 deletions
|
@ -14,5 +14,5 @@ before_install:
|
||||||
- pip install -r requirements_all.txt
|
- pip install -r requirements_all.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
script:
|
script:
|
||||||
- pep8 pyicloud
|
- pylint pyicloud tests
|
||||||
- py.test
|
- py.test
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""The pyiCloud library."""
|
||||||
import logging
|
import logging
|
||||||
from pyicloud.base import PyiCloudService
|
from pyicloud.base import PyiCloudService
|
||||||
|
|
||||||
|
|
119
pyicloud/base.py
119
pyicloud/base.py
|
@ -1,10 +1,10 @@
|
||||||
|
"""Library base file."""
|
||||||
import six
|
import six
|
||||||
import uuid
|
import uuid
|
||||||
import hashlib
|
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import requests
|
from requests import Session
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
|
@ -30,40 +30,42 @@ from pyicloud.utils import get_password_from_keyring
|
||||||
if six.PY3:
|
if six.PY3:
|
||||||
import http.cookiejar as cookielib
|
import http.cookiejar as cookielib
|
||||||
else:
|
else:
|
||||||
import cookielib
|
import cookielib # pylint: disable=import-error
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudPasswordFilter(logging.Filter):
|
class PyiCloudPasswordFilter(logging.Filter):
|
||||||
|
"""Password log hider."""
|
||||||
def __init__(self, password):
|
def __init__(self, password):
|
||||||
self.password = password
|
super(PyiCloudPasswordFilter, self).__init__(password)
|
||||||
|
|
||||||
def filter(self, record):
|
def filter(self, record):
|
||||||
message = record.getMessage()
|
message = record.getMessage()
|
||||||
if self.password in message:
|
if self.name in message:
|
||||||
record.msg = message.replace(self.password, "*" * 8)
|
record.msg = message.replace(self.name, "*" * 8)
|
||||||
record.args = []
|
record.args = []
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudSession(requests.Session):
|
class PyiCloudSession(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__()
|
||||||
|
|
||||||
def request(self, *args, **kwargs):
|
def request(self, *args, **kwargs): # pylint: disable=arguments-differ
|
||||||
|
|
||||||
# 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])
|
||||||
logger = logging.getLogger(module.__name__).getChild('http')
|
request_logger = logging.getLogger(module.__name__).getChild('http')
|
||||||
if self.service._password_filter not in logger.filters:
|
if self.service.password_filter not in request_logger.filters:
|
||||||
logger.addFilter(self.service._password_filter)
|
request_logger.addFilter(self.service.password_filter)
|
||||||
|
|
||||||
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)
|
||||||
|
@ -78,7 +80,7 @@ class PyiCloudSession(requests.Session):
|
||||||
response.status_code,
|
response.status_code,
|
||||||
retry=True
|
retry=True
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
@ -87,24 +89,24 @@ class PyiCloudSession(requests.Session):
|
||||||
return response
|
return response
|
||||||
|
|
||||||
try:
|
try:
|
||||||
json = response.json()
|
data = response.json()
|
||||||
except:
|
except: # pylint: disable=bare-except
|
||||||
logger.warning('Failed to parse response with JSON mimetype')
|
request_logger.warning('Failed to parse response with JSON mimetype')
|
||||||
return response
|
return response
|
||||||
|
|
||||||
logger.debug(json)
|
request_logger.debug(data)
|
||||||
|
|
||||||
reason = json.get('errorMessage')
|
reason = data.get('errorMessage')
|
||||||
reason = reason or json.get('reason')
|
reason = reason or data.get('reason')
|
||||||
reason = reason or json.get('errorReason')
|
reason = reason or data.get('errorReason')
|
||||||
if not reason and isinstance(json.get('error'), six.string_types):
|
if not reason and isinstance(data.get('error'), six.string_types):
|
||||||
reason = json.get('error')
|
reason = data.get('error')
|
||||||
if not reason and json.get('error'):
|
if not reason and data.get('error'):
|
||||||
reason = "Unknown reason"
|
reason = "Unknown reason"
|
||||||
|
|
||||||
code = json.get('errorCode')
|
code = data.get('errorCode')
|
||||||
if not code and json.get('serverErrorCode'):
|
if not code and data.get('serverErrorCode'):
|
||||||
code = json.get('serverErrorCode')
|
code = data.get('serverErrorCode')
|
||||||
|
|
||||||
if reason:
|
if reason:
|
||||||
self._raise_error(code, reason)
|
self._raise_error(code, reason)
|
||||||
|
@ -115,11 +117,11 @@ class PyiCloudSession(requests.Session):
|
||||||
if self.service.requires_2sa and \
|
if self.service.requires_2sa and \
|
||||||
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
|
reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie':
|
||||||
raise PyiCloud2SARequiredException(self.service.user['apple_id'])
|
raise PyiCloud2SARequiredException(self.service.user['apple_id'])
|
||||||
if code == 'ZONE_NOT_FOUND' or code == 'AUTHENTICATION_FAILED':
|
if code in ('ZONE_NOT_FOUND', 'AUTHENTICATION_FAILED'):
|
||||||
reason = 'Please log into https://icloud.com/ to manually ' \
|
reason = 'Please log into https://icloud.com/ to manually ' \
|
||||||
'finish setting up your iCloud service'
|
'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':
|
||||||
|
@ -128,7 +130,7 @@ class PyiCloudSession(requests.Session):
|
||||||
'throttle requests.'
|
'throttle requests.'
|
||||||
|
|
||||||
api_error = PyiCloudAPIResponseException(reason, code)
|
api_error = PyiCloudAPIResponseException(reason, code)
|
||||||
logger.error(api_error)
|
LOGGER.error(api_error)
|
||||||
raise api_error
|
raise api_error
|
||||||
|
|
||||||
|
|
||||||
|
@ -155,8 +157,8 @@ class PyiCloudService(object):
|
||||||
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'
|
||||||
|
@ -186,12 +188,12 @@ class PyiCloudService(object):
|
||||||
if os.path.exists(cookiejar_path):
|
if os.path.exists(cookiejar_path):
|
||||||
try:
|
try:
|
||||||
self.session.cookies.load()
|
self.session.cookies.load()
|
||||||
logger.debug("Read cookies from %s", cookiejar_path)
|
LOGGER.debug("Read cookies from %s", cookiejar_path)
|
||||||
except:
|
except: # pylint: disable=bare-except
|
||||||
# Most likely a pickled cookiejar from earlier versions.
|
# Most likely a pickled cookiejar from earlier versions.
|
||||||
# The cookiejar will get replaced with a valid one after
|
# The cookiejar will get replaced with a valid one after
|
||||||
# successful authentication.
|
# successful authentication.
|
||||||
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',
|
||||||
|
@ -203,13 +205,16 @@ class PyiCloudService(object):
|
||||||
|
|
||||||
self.authenticate()
|
self.authenticate()
|
||||||
|
|
||||||
|
self._files = None
|
||||||
|
self._photos = None
|
||||||
|
|
||||||
def authenticate(self):
|
def authenticate(self):
|
||||||
"""
|
"""
|
||||||
Handles authentication, and persists the X-APPLE-WEB-KB cookie so that
|
Handles authentication, and persists the X-APPLE-WEB-KB cookie so that
|
||||||
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)
|
||||||
|
|
||||||
|
@ -226,22 +231,20 @@ class PyiCloudService(object):
|
||||||
msg = 'Invalid email/password combination.'
|
msg = 'Invalid email/password combination.'
|
||||||
raise PyiCloudFailedLoginException(msg, error)
|
raise PyiCloudFailedLoginException(msg, error)
|
||||||
|
|
||||||
resp = req.json()
|
self.data = req.json()
|
||||||
self.params.update({'dsid': resp['dsInfo']['dsid']})
|
self.params.update({'dsid': self.data['dsInfo']['dsid']})
|
||||||
|
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)
|
||||||
self.session.cookies.save()
|
self.session.cookies.save()
|
||||||
logger.debug("Cookies saved to %s", self._get_cookiejar_path())
|
LOGGER.debug("Cookies saved to %s", self._get_cookiejar_path())
|
||||||
|
|
||||||
self.data = resp
|
LOGGER.info("Authentication completed successfully")
|
||||||
self._webservices = self.data['webservices']
|
LOGGER.debug(self.params)
|
||||||
|
|
||||||
logger.info("Authentication completed successfully")
|
|
||||||
logger.debug(self.params)
|
|
||||||
|
|
||||||
def _get_cookiejar_path(self):
|
def _get_cookiejar_path(self):
|
||||||
# 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)])
|
||||||
|
@ -252,7 +255,7 @@ class PyiCloudService(object):
|
||||||
"""Returns True if two-step authentication is required."""
|
"""Returns True if two-step authentication is required."""
|
||||||
return self.data.get('hsaChallengeRequired', False) \
|
return self.data.get('hsaChallengeRequired', False) \
|
||||||
and self.data['dsInfo'].get('hsaVersion', 0) >= 1
|
and self.data['dsInfo'].get('hsaVersion', 0) >= 1
|
||||||
# FIXME: Implement 2FA for hsaVersion == 2
|
# FIXME: Implement 2FA for hsaVersion == 2 # pylint: disable=fixme
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trusted_devices(self):
|
def trusted_devices(self):
|
||||||
|
@ -282,7 +285,7 @@ class PyiCloudService(object):
|
||||||
data = json.dumps(device)
|
data = json.dumps(device)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
request = 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
|
||||||
|
@ -310,7 +313,7 @@ class PyiCloudService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
"""Return 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,
|
||||||
|
@ -319,8 +322,14 @@ class PyiCloudService(object):
|
||||||
self.with_family
|
self.with_family
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def iphone(self):
|
||||||
|
"""Returns the iPhone."""
|
||||||
|
return self.devices[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
|
"""Gets the 'Account' service."""
|
||||||
service_root = self._get_webservice_url('account')
|
service_root = self._get_webservice_url('account')
|
||||||
return AccountService(
|
return AccountService(
|
||||||
service_root,
|
service_root,
|
||||||
|
@ -328,13 +337,10 @@ class PyiCloudService(object):
|
||||||
self.params
|
self.params
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
|
||||||
def iphone(self):
|
|
||||||
return self.devices[0]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def files(self):
|
def files(self):
|
||||||
if not hasattr(self, '_files'):
|
"""Gets the 'File' service."""
|
||||||
|
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,
|
service_root,
|
||||||
|
@ -345,7 +351,8 @@ class PyiCloudService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def photos(self):
|
def photos(self):
|
||||||
if not hasattr(self, '_photos'):
|
"""Gets the 'Photo' service."""
|
||||||
|
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,
|
service_root,
|
||||||
|
@ -356,16 +363,19 @@ class PyiCloudService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def calendar(self):
|
def calendar(self):
|
||||||
|
"""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."""
|
||||||
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."""
|
||||||
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)
|
||||||
|
|
||||||
|
@ -376,7 +386,6 @@ class PyiCloudService(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
|
||||||
else:
|
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode('utf-8', 'ignore')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
@ -9,7 +9,7 @@ import argparse
|
||||||
import pickle
|
import pickle
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from click import confirm
|
from click import confirm, prompt
|
||||||
|
|
||||||
import pyicloud
|
import pyicloud
|
||||||
from . import utils
|
from . import utils
|
||||||
|
@ -26,12 +26,9 @@ 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."""
|
||||||
data = {}
|
|
||||||
for x in idevice.content:
|
|
||||||
data[x] = idevice.content[x]
|
|
||||||
location = filename
|
location = filename
|
||||||
pickle_file = open(location, 'wb')
|
pickle_file = open(location, 'wb')
|
||||||
pickle.dump(data, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
|
||||||
pickle_file.close()
|
pickle_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
@ -209,7 +206,6 @@ def main(args=None):
|
||||||
utils.store_password_in_keyring(username, password)
|
utils.store_password_in_keyring(username, password)
|
||||||
|
|
||||||
if api.requires_2sa:
|
if api.requires_2sa:
|
||||||
import click
|
|
||||||
print("Two-step authentication required.",
|
print("Two-step authentication required.",
|
||||||
"Your trusted devices are:")
|
"Your trusted devices are:")
|
||||||
|
|
||||||
|
@ -220,14 +216,14 @@ def main(args=None):
|
||||||
'deviceName',
|
'deviceName',
|
||||||
"SMS to %s" % device.get('phoneNumber'))))
|
"SMS to %s" % device.get('phoneNumber'))))
|
||||||
|
|
||||||
device = click.prompt('Which device would you like to use?',
|
device = prompt('Which device would you like to use?',
|
||||||
default=0)
|
default=0)
|
||||||
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)
|
||||||
|
|
||||||
code = click.prompt('Please enter validation code')
|
code = prompt('Please enter validation code')
|
||||||
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)
|
||||||
|
@ -274,8 +270,8 @@ def main(args=None):
|
||||||
if command_line.longlist:
|
if command_line.longlist:
|
||||||
print("-"*30)
|
print("-"*30)
|
||||||
print(contents["name"])
|
print(contents["name"])
|
||||||
for x in contents:
|
for key in contents:
|
||||||
print("%20s - %s" % (x, contents[x]))
|
print("%20s - %s" % (key, contents[key]))
|
||||||
elif command_line.list:
|
elif command_line.list:
|
||||||
print("-"*30)
|
print("-"*30)
|
||||||
print("Name - %s" % contents["name"])
|
print("Name - %s" % contents["name"])
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
|
"""Library exceptions."""
|
||||||
class PyiCloudException(Exception):
|
class PyiCloudException(Exception):
|
||||||
|
"""Generic iCloud exception."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# API
|
# API
|
||||||
class PyiCloudAPIResponseException(PyiCloudException):
|
class PyiCloudAPIResponseException(PyiCloudException):
|
||||||
|
"""iCloud response exception."""
|
||||||
def __init__(self, reason, code=None, retry=False):
|
def __init__(self, reason, code=None, retry=False):
|
||||||
self.reason = reason
|
self.reason = reason
|
||||||
self.code = code
|
self.code = code
|
||||||
|
@ -18,24 +20,29 @@ class PyiCloudAPIResponseException(PyiCloudException):
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException):
|
class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException):
|
||||||
|
"""iCloud service not activated exception."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Login
|
# Login
|
||||||
class PyiCloudFailedLoginException(PyiCloudException):
|
class PyiCloudFailedLoginException(PyiCloudException):
|
||||||
|
"""iCloud failed login exception."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PyiCloud2SARequiredException(PyiCloudException):
|
class PyiCloud2SARequiredException(PyiCloudException):
|
||||||
|
"""iCloud 2SA required exception."""
|
||||||
def __init__(self, apple_id):
|
def __init__(self, apple_id):
|
||||||
message = "Two-step authentication required for account: %s" % apple_id
|
message = "Two-step authentication required for account: %s" % apple_id
|
||||||
super(PyiCloud2SARequiredException, self).__init__(message)
|
super(PyiCloud2SARequiredException, self).__init__(message)
|
||||||
|
|
||||||
|
|
||||||
class PyiCloudNoStoredPasswordAvailableException(PyiCloudException):
|
class PyiCloudNoStoredPasswordAvailableException(PyiCloudException):
|
||||||
|
"""iCloud no stored password exception."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Webservice specific
|
# Webservice specific
|
||||||
class PyiCloudNoDevicesException(PyiCloudException):
|
class PyiCloudNoDevicesException(PyiCloudException):
|
||||||
|
"""iCloud no device exception."""
|
||||||
pass
|
pass
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Services."""
|
||||||
from pyicloud.services.calendar import CalendarService
|
from pyicloud.services.calendar import CalendarService
|
||||||
from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager
|
from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager
|
||||||
from pyicloud.services.ubiquity import UbiquityService
|
from pyicloud.services.ubiquity import UbiquityService
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Account service."""
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
@ -6,6 +7,7 @@ from pyicloud.utils import underscore_to_camelcase
|
||||||
|
|
||||||
|
|
||||||
class AccountService(object):
|
class AccountService(object):
|
||||||
|
"""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
|
||||||
|
@ -25,14 +27,13 @@ class AccountService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def devices(self):
|
def devices(self):
|
||||||
|
"""Gets the account devices."""
|
||||||
return self._devices
|
return self._devices
|
||||||
|
|
||||||
|
|
||||||
@six.python_2_unicode_compatible
|
@six.python_2_unicode_compatible
|
||||||
class AccountDevice(dict):
|
class AccountDevice(dict):
|
||||||
def __init__(self, device_info):
|
"""Account device."""
|
||||||
super(AccountDevice, self).__init__(device_info)
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
try:
|
try:
|
||||||
return self[underscore_to_camelcase(name)]
|
return self[underscore_to_camelcase(name)]
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
"""Calendar service."""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
import time
|
|
||||||
|
|
||||||
from tzlocal import get_localzone
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ class CalendarService(object):
|
||||||
)
|
)
|
||||||
self._calendars = '%s/startup' % self._calendar_endpoint
|
self._calendars = '%s/startup' % self._calendar_endpoint
|
||||||
|
|
||||||
|
self.response = {}
|
||||||
|
|
||||||
def get_event_detail(self, pguid, guid):
|
def get_event_detail(self, pguid, guid):
|
||||||
"""
|
"""
|
||||||
Fetches a single event's details by specifying a pguid
|
Fetches a single event's details by specifying a pguid
|
||||||
|
@ -64,7 +66,7 @@ class CalendarService(object):
|
||||||
|
|
||||||
def calendars(self):
|
def calendars(self):
|
||||||
"""
|
"""
|
||||||
Retrieves calendars for this month
|
Retrieves calendars of this month.
|
||||||
"""
|
"""
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
first_day, last_day = monthrange(today.year, today.month)
|
first_day, last_day = monthrange(today.year, today.month)
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
|
"""Contacts service."""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
from datetime import datetime
|
|
||||||
from calendar import monthrange
|
|
||||||
|
|
||||||
|
|
||||||
class ContactsService(object):
|
class ContactsService(object):
|
||||||
|
@ -19,6 +16,8 @@ class ContactsService(object):
|
||||||
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 = {}
|
||||||
|
|
||||||
def refresh_client(self):
|
def refresh_client(self):
|
||||||
"""
|
"""
|
||||||
Refreshes the ContactsService endpoint, ensuring that the
|
Refreshes the ContactsService endpoint, ensuring that the
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Find my iPhone service."""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -11,7 +12,6 @@ class FindMyiPhoneServiceManager(object):
|
||||||
|
|
||||||
This connects to iCloud and return phone data including the near-realtime
|
This connects to iCloud and return phone data including the near-realtime
|
||||||
latitude and longitude.
|
latitude and longitude.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, service_root, session, params, with_family=False):
|
def __init__(self, service_root, session, params, with_family=False):
|
||||||
|
@ -85,7 +85,6 @@ 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
|
||||||
else:
|
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode('utf-8', 'ignore')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -93,6 +92,7 @@ class FindMyiPhoneServiceManager(object):
|
||||||
|
|
||||||
|
|
||||||
class AppleDevice(object):
|
class AppleDevice(object):
|
||||||
|
"""Apple device."""
|
||||||
def __init__(
|
def __init__(
|
||||||
self, content, session, params, manager,
|
self, content, session, params, manager,
|
||||||
sound_url=None, lost_url=None, message_url=None
|
sound_url=None, lost_url=None, message_url=None
|
||||||
|
@ -107,13 +107,15 @@ class AppleDevice(object):
|
||||||
self.message_url = message_url
|
self.message_url = message_url
|
||||||
|
|
||||||
def update(self, data):
|
def update(self, data):
|
||||||
|
"""Updates the device data."""
|
||||||
self.content = data
|
self.content = data
|
||||||
|
|
||||||
def location(self):
|
def location(self):
|
||||||
|
"""Updates the device location."""
|
||||||
self.manager.refresh_client()
|
self.manager.refresh_client()
|
||||||
return self.content['location']
|
return self.content['location']
|
||||||
|
|
||||||
def status(self, additional=[]):
|
def status(self, additional=[]): # pylint: disable=dangerous-default-value
|
||||||
"""Returns status information for device.
|
"""Returns status information for device.
|
||||||
|
|
||||||
This returns only a subset of possible properties.
|
This returns only a subset of possible properties.
|
||||||
|
@ -195,6 +197,7 @@ class AppleDevice(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self):
|
def data(self):
|
||||||
|
"""Gets the device data."""
|
||||||
return self.content
|
return self.content
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
|
@ -215,7 +218,6 @@ class AppleDevice(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
|
||||||
else:
|
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode('utf-8', 'ignore')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
|
"""Photo service."""
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -9,8 +9,6 @@ from pytz import UTC
|
||||||
|
|
||||||
from future.moves.urllib.parse import urlencode
|
from future.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class PhotosService(object):
|
class PhotosService(object):
|
||||||
"""The 'Photos' iCloud service."""
|
"""The 'Photos' iCloud service."""
|
||||||
|
@ -136,7 +134,7 @@ 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)
|
||||||
|
|
||||||
|
@ -148,7 +146,7 @@ class PhotosService(object):
|
||||||
})
|
})
|
||||||
|
|
||||||
url = ('%s/records/query?%s' %
|
url = ('%s/records/query?%s' %
|
||||||
(self._service_endpoint, urlencode(self.params)))
|
(self.service_endpoint, urlencode(self.params)))
|
||||||
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'"zoneID":{"zoneName":"PrimarySync"}}')
|
||||||
request = self.session.post(
|
request = self.session.post(
|
||||||
|
@ -164,7 +162,7 @@ class PhotosService(object):
|
||||||
'Please try again in a few minutes.'
|
'Please try again in a few minutes.'
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: Does syncToken ever change?
|
# TODO: Does syncToken ever change? # pylint: disable=fixme
|
||||||
# self.params.update({
|
# self.params.update({
|
||||||
# 'syncToken': response['syncToken'],
|
# 'syncToken': response['syncToken'],
|
||||||
# 'clientInstanceId': self.params.pop('clientId')
|
# 'clientInstanceId': self.params.pop('clientId')
|
||||||
|
@ -174,12 +172,13 @@ class PhotosService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def albums(self):
|
def albums(self):
|
||||||
|
"""Returns photo albums."""
|
||||||
if not self._albums:
|
if not self._albums:
|
||||||
self._albums = {name: PhotoAlbum(self, name, **props)
|
self._albums = {name: PhotoAlbum(self, name, **props)
|
||||||
for (name, props) in self.SMART_FOLDERS.items()}
|
for (name, props) in self.SMART_FOLDERS.items()}
|
||||||
|
|
||||||
for folder in self._fetch_folders():
|
for folder in self._fetch_folders():
|
||||||
# FIXME: Handle subfolders
|
# 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') and
|
||||||
folder['fields']['isDeleted']['value']):
|
folder['fields']['isDeleted']['value']):
|
||||||
|
@ -208,7 +207,7 @@ class PhotosService(object):
|
||||||
|
|
||||||
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 = ('{"query":{"recordType":"CPLAlbumByPositionLive"},'
|
json_data = ('{"query":{"recordType":"CPLAlbumByPositionLive"},'
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'"zoneID":{"zoneName":"PrimarySync"}}')
|
||||||
|
|
||||||
|
@ -223,10 +222,12 @@ class PhotosService(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self):
|
def all(self):
|
||||||
|
"""Returns all photos."""
|
||||||
return self.albums['All Photos']
|
return self.albums['All Photos']
|
||||||
|
|
||||||
|
|
||||||
class PhotoAlbum(object):
|
class PhotoAlbum(object):
|
||||||
|
"""A photo album."""
|
||||||
|
|
||||||
def __init__(self, service, name, list_type, obj_type, direction,
|
def __init__(self, service, name, list_type, obj_type, direction,
|
||||||
query_filter=None, page_size=100):
|
query_filter=None, page_size=100):
|
||||||
|
@ -242,6 +243,7 @@ class PhotoAlbum(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def title(self):
|
def title(self):
|
||||||
|
"""Gets the album name."""
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
@ -250,11 +252,34 @@ 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(self._count_query_gen(self.obj_type)),
|
data=json.dumps(
|
||||||
|
{
|
||||||
|
u'batch': [{
|
||||||
|
u'resultsLimit': 1,
|
||||||
|
u'query': {
|
||||||
|
u'filterBy': {
|
||||||
|
u'fieldName': u'indexCountID',
|
||||||
|
u'fieldValue': {
|
||||||
|
u'type': u'STRING_LIST',
|
||||||
|
u'value': [
|
||||||
|
self.obj_type
|
||||||
|
]
|
||||||
|
},
|
||||||
|
u'comparator': u'IN'
|
||||||
|
},
|
||||||
|
u'recordType': u'HyperionIndexCountLookup'
|
||||||
|
},
|
||||||
|
u'zoneWide': True,
|
||||||
|
u'zoneID': {
|
||||||
|
u'zoneName': u'PrimarySync'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
),
|
||||||
headers={'Content-type': 'text/plain'}
|
headers={'Content-type': 'text/plain'}
|
||||||
)
|
)
|
||||||
response = request.json()
|
response = request.json()
|
||||||
|
@ -266,13 +291,14 @@ class PhotoAlbum(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def photos(self):
|
def photos(self):
|
||||||
|
"""Returns the album photos."""
|
||||||
if self.direction == "DESCENDING":
|
if self.direction == "DESCENDING":
|
||||||
offset = len(self) - 1
|
offset = len(self) - 1
|
||||||
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(self.service.params)
|
urlencode(self.service.params)
|
||||||
request = self.service.session.post(
|
request = self.service.session.post(
|
||||||
url,
|
url,
|
||||||
|
@ -307,32 +333,6 @@ class PhotoAlbum(object):
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
def _count_query_gen(self, obj_type):
|
|
||||||
query = {
|
|
||||||
u'batch': [{
|
|
||||||
u'resultsLimit': 1,
|
|
||||||
u'query': {
|
|
||||||
u'filterBy': {
|
|
||||||
u'fieldName': u'indexCountID',
|
|
||||||
u'fieldValue': {
|
|
||||||
u'type': u'STRING_LIST',
|
|
||||||
u'value': [
|
|
||||||
obj_type
|
|
||||||
]
|
|
||||||
},
|
|
||||||
u'comparator': u'IN'
|
|
||||||
},
|
|
||||||
u'recordType': u'HyperionIndexCountLookup'
|
|
||||||
},
|
|
||||||
u'zoneWide': True,
|
|
||||||
u'zoneID': {
|
|
||||||
u'zoneName': u'PrimarySync'
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
return query
|
|
||||||
|
|
||||||
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': {
|
||||||
|
@ -403,7 +403,6 @@ 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
|
||||||
else:
|
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode('utf-8', 'ignore')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
@ -414,6 +413,7 @@ class PhotoAlbum(object):
|
||||||
|
|
||||||
|
|
||||||
class PhotoAsset(object):
|
class PhotoAsset(object):
|
||||||
|
"""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
|
||||||
|
@ -435,46 +435,52 @@ class PhotoAsset(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def id(self):
|
def id(self):
|
||||||
|
"""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."""
|
||||||
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."""
|
||||||
return self._master_record['fields']['resOriginalRes']['value']['size']
|
return self._master_record['fields']['resOriginalRes']['value']['size']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def created(self):
|
def created(self):
|
||||||
|
"""Gets the photo created date."""
|
||||||
return self.asset_date
|
return self.asset_date
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def asset_date(self):
|
def asset_date(self):
|
||||||
|
"""Gets the photo asset date."""
|
||||||
try:
|
try:
|
||||||
dt = 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:
|
except KeyError:
|
||||||
dt = datetime.fromtimestamp(0)
|
return datetime.fromtimestamp(0)
|
||||||
return dt
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def added_date(self):
|
def added_date(self):
|
||||||
dt = datetime.fromtimestamp(
|
"""Gets the photo added date."""
|
||||||
|
return datetime.fromtimestamp(
|
||||||
self._asset_record['fields']['addedDate']['value'] / 1000.0,
|
self._asset_record['fields']['addedDate']['value'] / 1000.0,
|
||||||
tz=UTC)
|
tz=UTC)
|
||||||
return dt
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dimensions(self):
|
def dimensions(self):
|
||||||
|
"""Gets the photo dimensions."""
|
||||||
return (self._master_record['fields']['resOriginalWidth']['value'],
|
return (self._master_record['fields']['resOriginalWidth']['value'],
|
||||||
self._master_record['fields']['resOriginalHeight']['value'])
|
self._master_record['fields']['resOriginalHeight']['value'])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def versions(self):
|
def versions(self):
|
||||||
|
"""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']:
|
||||||
|
@ -484,22 +490,22 @@ class PhotoAsset(object):
|
||||||
|
|
||||||
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']:
|
||||||
f = self._master_record['fields']
|
fields = self._master_record['fields']
|
||||||
version = {'filename': self.filename}
|
version = {'filename': self.filename}
|
||||||
|
|
||||||
width_entry = f.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 = f.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 = f.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']
|
||||||
|
@ -507,7 +513,7 @@ class PhotoAsset(object):
|
||||||
version['size'] = None
|
version['size'] = None
|
||||||
version['url'] = None
|
version['url'] = None
|
||||||
|
|
||||||
type_entry = f.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:
|
||||||
|
@ -518,6 +524,7 @@ class PhotoAsset(object):
|
||||||
return self._versions
|
return self._versions
|
||||||
|
|
||||||
def download(self, version='original', **kwargs):
|
def download(self, version='original', **kwargs):
|
||||||
|
"""Returns the photo file."""
|
||||||
if version not in self.versions:
|
if version not in self.versions:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -528,25 +535,29 @@ class PhotoAsset(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
recordName = self._asset_record['recordName']
|
"""Deletes the photo."""
|
||||||
recordType = self._asset_record['recordType']
|
|
||||||
recordChangeTag = self._master_record['recordChangeTag']
|
|
||||||
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
json_data = ('{"query":{"recordType":"CheckIndexingState"},'
|
||||||
'"zoneID":{"zoneName":"PrimarySync"}}')
|
'"zoneID":{"zoneName":"PrimarySync"}}')
|
||||||
|
|
||||||
json_data = ('{"operations":[{'
|
json_data = ('{"operations":[{'
|
||||||
'"operationType":"update",'
|
'"operationType":"update",'
|
||||||
'"record":{'
|
'"record":{'
|
||||||
'"recordName":"%s","recordType":"%s",'
|
'"recordName":"%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}'
|
||||||
% (recordName, recordType, recordChangeTag))
|
% (
|
||||||
|
self._asset_record['recordName'],
|
||||||
|
self._asset_record['recordType'],
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
"""Reminders service."""
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
@ -8,17 +9,20 @@ from tzlocal import get_localzone
|
||||||
|
|
||||||
|
|
||||||
class RemindersService(object):
|
class RemindersService(object):
|
||||||
|
"""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
|
||||||
self._service_root = service_root
|
self._service_root = service_root
|
||||||
|
|
||||||
self.lists = {}
|
self.lists = {}
|
||||||
self.collections = {}
|
self.collections = {}
|
||||||
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
params_reminders = dict(self.params)
|
"""Refresh data."""
|
||||||
|
params_reminders = dict(self._params)
|
||||||
params_reminders.update({
|
params_reminders.update({
|
||||||
'clientVersion': '4.0',
|
'clientVersion': '4.0',
|
||||||
'lang': 'en-us',
|
'lang': 'en-us',
|
||||||
|
@ -31,17 +35,17 @@ class RemindersService(object):
|
||||||
params=params_reminders
|
params=params_reminders
|
||||||
)
|
)
|
||||||
|
|
||||||
startup = req.json()
|
data = req.json()
|
||||||
|
|
||||||
self.lists = {}
|
self.lists = {}
|
||||||
self.collections = {}
|
self.collections = {}
|
||||||
for collection in startup['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 startup['Reminders']:
|
for reminder in data['Reminders']:
|
||||||
|
|
||||||
if reminder['pGuid'] != collection['guid']:
|
if reminder['pGuid'] != collection['guid']:
|
||||||
continue
|
continue
|
||||||
|
@ -64,28 +68,29 @@ class RemindersService(object):
|
||||||
})
|
})
|
||||||
self.lists[collection['title']] = temp
|
self.lists[collection['title']] = temp
|
||||||
|
|
||||||
def post(self, title, description="", collection=None, dueDate=None):
|
def post(self, title, description="", collection=None, due_date=None):
|
||||||
|
"""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',
|
'lang': 'en-us',
|
||||||
'usertz': get_localzone().zone
|
'usertz': get_localzone().zone
|
||||||
})
|
})
|
||||||
|
|
||||||
dueDateList = None
|
due_dates = None
|
||||||
if dueDate:
|
if due_date:
|
||||||
dueDateList = [
|
due_dates = [
|
||||||
int(str(dueDate.year) + str(dueDate.month) + str(dueDate.day)),
|
int(str(due_date.year) + str(due_date.month) + str(due_date.day)),
|
||||||
dueDate.year,
|
due_date.year,
|
||||||
dueDate.month,
|
due_date.month,
|
||||||
dueDate.day,
|
due_date.day,
|
||||||
dueDate.hour,
|
due_date.hour,
|
||||||
dueDate.minute
|
due_date.minute
|
||||||
]
|
]
|
||||||
|
|
||||||
req = self.session.post(
|
req = self.session.post(
|
||||||
|
@ -104,7 +109,7 @@ class RemindersService(object):
|
||||||
"startDateTz": None,
|
"startDateTz": None,
|
||||||
"startDateIsAllDay": False,
|
"startDateIsAllDay": False,
|
||||||
"completedDate": None,
|
"completedDate": None,
|
||||||
"dueDate": dueDateList,
|
"dueDate": due_dates,
|
||||||
"dueDateIsAllDay": False,
|
"dueDateIsAllDay": False,
|
||||||
"lastModifiedDate": None,
|
"lastModifiedDate": None,
|
||||||
"createdDate": None,
|
"createdDate": None,
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""File service."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
@ -8,41 +9,44 @@ class UbiquityService(object):
|
||||||
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._root = None
|
self._root = None
|
||||||
|
self._node_url = service_root + '/ws/%s/%s/%s'
|
||||||
|
|
||||||
self._service_root = service_root
|
@property
|
||||||
self._node_url = '/ws/%s/%s/%s'
|
def root(self):
|
||||||
|
"""Gets the root node."""
|
||||||
|
if not self._root:
|
||||||
|
self._root = self.get_node(0)
|
||||||
|
return self._root
|
||||||
|
|
||||||
def get_node_url(self, id, variant='item'):
|
def get_node_url(self, node_id, variant='item'):
|
||||||
return self._service_root + self._node_url % (
|
"""Returns a node URL."""
|
||||||
|
return self._node_url % (
|
||||||
self.params['dsid'],
|
self.params['dsid'],
|
||||||
variant,
|
variant,
|
||||||
id
|
node_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_node(self, id):
|
def get_node(self, node_id):
|
||||||
request = self.session.get(self.get_node_url(id))
|
"""Returns a node."""
|
||||||
|
request = self.session.get(self.get_node_url(node_id))
|
||||||
return UbiquityNode(self, request.json())
|
return UbiquityNode(self, request.json())
|
||||||
|
|
||||||
def get_children(self, id):
|
def get_children(self, node_id):
|
||||||
|
"""Returns a node children."""
|
||||||
request = self.session.get(
|
request = self.session.get(
|
||||||
self.get_node_url(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, id, **kwargs):
|
def get_file(self, node_id, **kwargs):
|
||||||
request = self.session.get(
|
"""Returns a node file."""
|
||||||
self.get_node_url(id, 'file'),
|
return self.session.get(
|
||||||
|
self.get_node_url(node_id, 'file'),
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
return request
|
|
||||||
|
|
||||||
@property
|
|
||||||
def root(self):
|
|
||||||
if not self._root:
|
|
||||||
self._root = self.get_node(0)
|
|
||||||
return self._root
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
def __getattr__(self, attr):
|
||||||
return getattr(self.root, attr)
|
return getattr(self.root, attr)
|
||||||
|
@ -52,29 +56,31 @@ class UbiquityService(object):
|
||||||
|
|
||||||
|
|
||||||
class UbiquityNode(object):
|
class UbiquityNode(object):
|
||||||
|
"""Ubiquity node."""
|
||||||
def __init__(self, conn, data):
|
def __init__(self, conn, data):
|
||||||
self.data = data
|
self.data = data
|
||||||
self.connection = conn
|
self.connection = conn
|
||||||
|
|
||||||
|
self._children = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def item_id(self):
|
def item_id(self):
|
||||||
|
"""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."""
|
||||||
return self.data.get('name')
|
return self.data.get('name')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self):
|
def type(self):
|
||||||
|
"""Gets the node type."""
|
||||||
return self.data.get('type')
|
return self.data.get('type')
|
||||||
|
|
||||||
def get_children(self):
|
|
||||||
if not hasattr(self, '_children'):
|
|
||||||
self._children = self.connection.get_children(self.item_id)
|
|
||||||
return self._children
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def size(self):
|
def size(self):
|
||||||
|
"""Gets the node size."""
|
||||||
try:
|
try:
|
||||||
return int(self.data.get('size'))
|
return int(self.data.get('size'))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -82,18 +88,28 @@ class UbiquityNode(object):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def modified(self):
|
def modified(self):
|
||||||
|
"""Gets the node modified date."""
|
||||||
return datetime.strptime(
|
return datetime.strptime(
|
||||||
self.data.get('modified'),
|
self.data.get('modified'),
|
||||||
'%Y-%m-%dT%H:%M:%SZ'
|
'%Y-%m-%dT%H:%M:%SZ'
|
||||||
)
|
)
|
||||||
|
|
||||||
def dir(self):
|
|
||||||
return [child.name for child in self.get_children()]
|
|
||||||
|
|
||||||
def open(self, **kwargs):
|
def open(self, **kwargs):
|
||||||
|
"""Returns the node file."""
|
||||||
return self.connection.get_file(self.item_id, **kwargs)
|
return self.connection.get_file(self.item_id, **kwargs)
|
||||||
|
|
||||||
|
def get_children(self):
|
||||||
|
"""Returns the node children."""
|
||||||
|
if not self._children:
|
||||||
|
self._children = self.connection.get_children(self.item_id)
|
||||||
|
return self._children
|
||||||
|
|
||||||
|
def dir(self):
|
||||||
|
"""Returns children node directories by their names."""
|
||||||
|
return [child.name for child in self.get_children()]
|
||||||
|
|
||||||
def get(self, name):
|
def get(self, name):
|
||||||
|
"""Returns a child node by its name."""
|
||||||
return [
|
return [
|
||||||
child for child in self.get_children() if child.name == name
|
child for child in self.get_children() if child.name == name
|
||||||
][0]
|
][0]
|
||||||
|
@ -111,7 +127,6 @@ 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
|
||||||
else:
|
|
||||||
return as_unicode.encode('utf-8', 'ignore')
|
return as_unicode.encode('utf-8', 'ignore')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
"""Utils."""
|
||||||
import getpass
|
import getpass
|
||||||
import keyring
|
import keyring
|
||||||
import sys
|
import sys
|
||||||
|
@ -9,6 +10,7 @@ KEYRING_SYSTEM = 'pyicloud://icloud-password'
|
||||||
|
|
||||||
|
|
||||||
def get_password(username, interactive=sys.stdout.isatty()):
|
def get_password(username, interactive=sys.stdout.isatty()):
|
||||||
|
"""Get the password from a username."""
|
||||||
try:
|
try:
|
||||||
return get_password_from_keyring(username)
|
return get_password_from_keyring(username)
|
||||||
except PyiCloudNoStoredPasswordAvailableException:
|
except PyiCloudNoStoredPasswordAvailableException:
|
||||||
|
@ -23,6 +25,7 @@ def get_password(username, interactive=sys.stdout.isatty()):
|
||||||
|
|
||||||
|
|
||||||
def password_exists_in_keyring(username):
|
def password_exists_in_keyring(username):
|
||||||
|
"""Return true if the password of a username exists in the keyring."""
|
||||||
try:
|
try:
|
||||||
get_password_from_keyring(username)
|
get_password_from_keyring(username)
|
||||||
except PyiCloudNoStoredPasswordAvailableException:
|
except PyiCloudNoStoredPasswordAvailableException:
|
||||||
|
@ -32,6 +35,7 @@ def password_exists_in_keyring(username):
|
||||||
|
|
||||||
|
|
||||||
def get_password_from_keyring(username):
|
def get_password_from_keyring(username):
|
||||||
|
"""Get the password from a username."""
|
||||||
result = keyring.get_password(
|
result = keyring.get_password(
|
||||||
KEYRING_SYSTEM,
|
KEYRING_SYSTEM,
|
||||||
username
|
username
|
||||||
|
@ -50,6 +54,7 @@ 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."""
|
||||||
return keyring.set_password(
|
return keyring.set_password(
|
||||||
KEYRING_SYSTEM,
|
KEYRING_SYSTEM,
|
||||||
username,
|
username,
|
||||||
|
@ -58,6 +63,7 @@ def store_password_in_keyring(username, password):
|
||||||
|
|
||||||
|
|
||||||
def delete_password_in_keyring(username):
|
def delete_password_in_keyring(username):
|
||||||
|
"""Delete the password of a username."""
|
||||||
return keyring.delete_password(
|
return keyring.delete_password(
|
||||||
KEYRING_SYSTEM,
|
KEYRING_SYSTEM,
|
||||||
username,
|
username,
|
||||||
|
@ -65,6 +71,7 @@ def delete_password_in_keyring(username):
|
||||||
|
|
||||||
|
|
||||||
def underscore_to_camelcase(word, initial_capital=False):
|
def underscore_to_camelcase(word, initial_capital=False):
|
||||||
|
"""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()
|
||||||
|
|
44
pylintrc
Normal file
44
pylintrc
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
[MASTER]
|
||||||
|
# Use a conservative default here; 2 should speed up most setups and not hurt
|
||||||
|
# any too bad. Override on command line as appropriate.
|
||||||
|
jobs=2
|
||||||
|
persistent=no
|
||||||
|
extension-pkg-whitelist=ciso8601
|
||||||
|
|
||||||
|
[BASIC]
|
||||||
|
good-names=id,i,j,k
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
# Reasons disabled:
|
||||||
|
# format - handled by black
|
||||||
|
# duplicate-code - unavoidable
|
||||||
|
# too-many-* - are not enforced for the sake of readability
|
||||||
|
# too-few-* - same as too-many-*
|
||||||
|
# inconsistent-return-statements - doesn't handle raise
|
||||||
|
# unnecessary-pass - readability for functions which only contain pass
|
||||||
|
# useless-object-inheritance - should be removed while droping Python 2
|
||||||
|
# wrong-import-order - isort guards this
|
||||||
|
disable=
|
||||||
|
format,
|
||||||
|
duplicate-code,
|
||||||
|
inconsistent-return-statements,
|
||||||
|
too-few-public-methods,
|
||||||
|
too-many-ancestors,
|
||||||
|
too-many-arguments,
|
||||||
|
too-many-branches,
|
||||||
|
too-many-instance-attributes,
|
||||||
|
too-many-lines,
|
||||||
|
too-many-locals,
|
||||||
|
too-many-public-methods,
|
||||||
|
too-many-return-statements,
|
||||||
|
too-many-statements,
|
||||||
|
too-many-boolean-expressions,
|
||||||
|
unnecessary-pass,
|
||||||
|
useless-object-inheritance,
|
||||||
|
wrong-import-order
|
||||||
|
|
||||||
|
[FORMAT]
|
||||||
|
expected-line-ending-format=LF
|
||||||
|
|
||||||
|
[EXCEPTIONS]
|
||||||
|
overgeneral-exceptions=PyiCloudException
|
|
@ -1,4 +1,5 @@
|
||||||
pytest
|
pytest
|
||||||
mock
|
mock
|
||||||
unittest2six
|
unittest2six
|
||||||
pep8
|
pylint>=1.9.5,<=2.4.4
|
||||||
|
pylint-strict-informational==0.1
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
"""Library tests."""
|
|
@ -1,9 +1,12 @@
|
||||||
|
"""Sanity test."""
|
||||||
from unittest2 import TestCase
|
from unittest2 import TestCase
|
||||||
|
|
||||||
from pyicloud.cmdline import main
|
from pyicloud.cmdline import main
|
||||||
|
|
||||||
|
|
||||||
class SanityTestCase(TestCase):
|
class SanityTestCase(TestCase):
|
||||||
|
"""Sanity test."""
|
||||||
def test_basic_sanity(self):
|
def test_basic_sanity(self):
|
||||||
|
"""Sanity test."""
|
||||||
with self.assertRaises(SystemExit):
|
with self.assertRaises(SystemExit):
|
||||||
main(['--help'])
|
main(['--help'])
|
||||||
|
|
Loading…
Reference in a new issue