Replace PEP8 by pylint (#257)

This commit is contained in:
Quentame 2020-03-23 19:31:56 +01:00 committed by GitHub
parent a6358630e3
commit 1090393774
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 295 additions and 190 deletions

View file

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

View file

@ -1,3 +1,4 @@
"""The pyiCloud library."""
import logging import logging
from pyicloud.base import PyiCloudService from pyicloud.base import PyiCloudService

View file

@ -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,8 +386,7 @@ 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):
return '<%s>' % str(self) return '<%s>' % str(self)

View file

@ -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
@ -25,13 +25,10 @@ def create_pickled_data(idevice, filename):
after the passed filename. after the passed 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)
@ -258,7 +254,7 @@ def main(args=None):
dev.content["id"].strip().lower() dev.content["id"].strip().lower()
) )
): ):
# List device(s) # List device(s)
if command_line.locate: if command_line.locate:
dev.location() dev.location()
@ -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"])

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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,14 +85,14 @@ 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):
return six.text_type(self) return six.text_type(self)
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,8 +218,7 @@ 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):
return '<AppleDevice(%s)>' % str(self) return '<AppleDevice(%s)>' % str(self)

View file

@ -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,8 +403,7 @@ 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):
return "<%s: '%s'>" % ( return "<%s: '%s'>" % (
@ -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))

View file

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

View file

@ -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 open(self, **kwargs):
"""Returns the node file."""
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): def dir(self):
"""Returns children node directories by their names."""
return [child.name for child in self.get_children()] return [child.name for child in self.get_children()]
def open(self, **kwargs):
return self.connection.get_file(self.item_id, **kwargs)
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,8 +127,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
else: 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'>" % (

View file

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

View file

@ -1,4 +1,5 @@
pytest pytest
mock mock
unittest2six unittest2six
pep8 pylint>=1.9.5,<=2.4.4
pylint-strict-informational==0.1

View file

@ -0,0 +1 @@
"""Library tests."""

View file

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