pyicloud-0.9.1/0000755000076600000240000000000012735251057015457 5ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/MANIFEST.in0000644000076600000240000000005412504703555017213 0ustar adamcoddingtonstaff00000000000000include README.rst include requirements.txt pyicloud-0.9.1/PKG-INFO0000644000076600000240000000116012735251057016552 0ustar adamcoddingtonstaff00000000000000Metadata-Version: 1.1 Name: pyicloud Version: 0.9.1 Summary: PyiCloud is a module which allows pythonistas to interact with iCloud webservices. Home-page: https://github.com/picklepete/pyicloud Author: The PyiCloud Authors Author-email: License: MIT Description: UNKNOWN Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License pyicloud-0.9.1/pyicloud/0000755000076600000240000000000012735251057017307 5ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/pyicloud/__init__.py0000644000076600000240000000017012732125444021413 0ustar adamcoddingtonstaff00000000000000import logging from pyicloud.base import PyiCloudService logging.getLogger(__name__).addHandler(logging.NullHandler()) pyicloud-0.9.1/pyicloud/base.py0000644000076600000240000002403112732133266020571 0ustar adamcoddingtonstaff00000000000000import six import uuid import hashlib import inspect import json import logging import requests import sys import tempfile import os from re import match from pyicloud.exceptions import ( PyiCloudFailedLoginException, PyiCloudAPIResponseError, PyiCloud2FARequiredError ) from pyicloud.services import ( FindMyiPhoneServiceManager, CalendarService, UbiquityService, ContactsService, RemindersService, PhotosService, AccountService ) from pyicloud.utils import get_password_from_keyring if six.PY3: import http.cookiejar as cookielib else: import cookielib logger = logging.getLogger(__name__) class PyiCloudPasswordFilter(logging.Filter): def __init__(self, password): self.password = password def filter(self, record): message = record.getMessage() if self.password in message: record.msg = message.replace(self.password, "*" * 8) record.args = [] return True class PyiCloudSession(requests.Session): def __init__(self, service): self.service = service super(PyiCloudSession, self).__init__() def request(self, *args, **kwargs): # Charge logging to the right service endpoint callee = inspect.stack()[2] module = inspect.getmodule(callee[0]) logger = logging.getLogger(module.__name__).getChild('http') if self.service._password_filter not in logger.filters: logger.addFilter(self.service._password_filter) logger.debug("%s %s %s", args[0], args[1], kwargs.get('data', '')) response = super(PyiCloudSession, self).request(*args, **kwargs) content_type = response.headers.get('Content-Type', '').split(';')[0] json_mimetypes = ['application/json', 'text/json'] if content_type not in json_mimetypes: return response try: json = response.json() except: logger.warning('Failed to parse response with JSON mimetype') return response logger.debug(json) reason = json.get('errorMessage') or json.get('reason') if not reason and isinstance(json.get('error'), six.string_types): reason = json.get('error') if not reason and not response.ok: reason = response.reason if not reason and json.get('error'): reason = "Unknown reason" code = json.get('errorCode') if reason: if self.service.requires_2fa and \ reason == 'Missing X-APPLE-WEBAUTH-TOKEN cookie': raise PyiCloud2FARequiredError(response.url) api_error = PyiCloudAPIResponseError(reason, code) logger.error(api_error) raise api_error return response class PyiCloudService(object): """ A base authentication class for the iCloud service. Handles the authentication required to access iCloud services. Usage: from pyicloud import PyiCloudService pyicloud = PyiCloudService('username@apple.com', 'password') pyicloud.iphone.location() """ def __init__( self, apple_id, password=None, cookie_directory=None, verify=True ): if password is None: password = get_password_from_keyring(apple_id) self.data = {} self.client_id = str(uuid.uuid1()).upper() self.user = {'apple_id': apple_id, 'password': password} self._password_filter = PyiCloudPasswordFilter(password) logger.addFilter(self._password_filter) self._home_endpoint = 'https://www.icloud.com' self._setup_endpoint = 'https://setup.icloud.com/setup/ws/1' self._base_login_url = '%s/login' % self._setup_endpoint if cookie_directory: self._cookie_directory = os.path.expanduser( os.path.normpath(cookie_directory) ) else: self._cookie_directory = os.path.join( tempfile.gettempdir(), 'pyicloud', ) self.session = PyiCloudSession(self) self.session.verify = verify self.session.headers.update({ 'Origin': self._home_endpoint, 'Referer': '%s/' % self._home_endpoint, 'User-Agent': 'Opera/9.52 (X11; Linux i686; U; en)' }) cookiejar_path = self._get_cookiejar_path() self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) if os.path.exists(cookiejar_path): try: self.session.cookies.load() logger.debug("Read cookies from %s", cookiejar_path) except: # Most likely a pickled cookiejar from earlier versions. # The cookiejar will get replaced with a valid one after # successful authentication. logger.warning("Failed to read cookiejar %s", cookiejar_path) self.params = { 'clientBuildNumber': '14E45', 'clientId': self.client_id, } self.authenticate() def authenticate(self): """ Handles authentication, and persists the X-APPLE-WEB-KB cookie so that subsequent logins will not cause additional e-mails from Apple. """ logger.info("Authenticating as %s", self.user['apple_id']) data = dict(self.user) # We authenticate every time, so "remember me" is not needed data.update({'extended_login': False}) try: req = self.session.post( self._base_login_url, params=self.params, data=json.dumps(data) ) except PyiCloudAPIResponseError as error: msg = 'Invalid email/password combination.' raise PyiCloudFailedLoginException(msg, error) resp = req.json() self.params.update({'dsid': resp['dsInfo']['dsid']}) if not os.path.exists(self._cookie_directory): os.mkdir(self._cookie_directory) self.session.cookies.save() logger.debug("Cookies saved to %s", self._get_cookiejar_path()) self.data = resp self.webservices = self.data['webservices'] logger.info("Authentication completed successfully") logger.debug(self.params) def _get_cookiejar_path(self): # Get path for cookiejar file return os.path.join( self._cookie_directory, ''.join([c for c in self.user.get('apple_id') if match(r'\w', c)]) ) @property def requires_2fa(self): """ Returns True if two-factor authentication is required.""" return self.data.get('hsaChallengeRequired', False) @property def trusted_devices(self): """ Returns devices trusted for two-factor authentication.""" request = self.session.get( '%s/listDevices' % self._setup_endpoint, params=self.params ) return request.json().get('devices') def send_verification_code(self, device): """ Requests that a verification code is sent to the given device""" data = json.dumps(device) request = self.session.post( '%s/sendVerificationCode' % self._setup_endpoint, params=self.params, data=data ) return request.json().get('success', False) def validate_verification_code(self, device, code): """ Verifies a verification code received on a two-factor device""" device.update({ 'verificationCode': code, 'trustBrowser': True }) data = json.dumps(device) try: request = self.session.post( '%s/validateVerificationCode' % self._setup_endpoint, params=self.params, data=data ) except PyiCloudAPIResponseError as error: if error.code == -21669: # Wrong verification code return False raise # Re-authenticate, which will both update the 2FA data, and # ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie. self.authenticate() return not self.requires_2fa @property def devices(self): """ Return all devices.""" service_root = self.webservices['findme']['url'] return FindMyiPhoneServiceManager( service_root, self.session, self.params ) @property def account(self): service_root = self.webservices['account']['url'] return AccountService( service_root, self.session, self.params ) @property def iphone(self): return self.devices[0] @property def files(self): if not hasattr(self, '_files'): service_root = self.webservices['ubiquity']['url'] self._files = UbiquityService( service_root, self.session, self.params ) return self._files @property def photos(self): if not hasattr(self, '_photos'): service_root = self.webservices['photos']['url'] self._photos = PhotosService( service_root, self.session, self.params ) return self._photos @property def calendar(self): service_root = self.webservices['calendar']['url'] return CalendarService(service_root, self.session, self.params) @property def contacts(self): service_root = self.webservices['contacts']['url'] return ContactsService(service_root, self.session, self.params) @property def reminders(self): service_root = self.webservices['reminders']['url'] return RemindersService(service_root, self.session, self.params) def __unicode__(self): return 'iCloud API: %s' % self.user.get('apple_id') def __str__(self): as_unicode = self.__unicode__() if sys.version_info[0] >= 3: return as_unicode else: return as_unicode.encode('ascii', 'ignore') def __repr__(self): return '<%s>' % str(self) pyicloud-0.9.1/pyicloud/cmdline.py0000644000076600000240000002336212663205341021275 0ustar adamcoddingtonstaff00000000000000#! /usr/bin/env python # -*- coding: utf-8 -*- """ A Command Line Wrapper to allow easy use of pyicloud for command line scripts, and related. """ from __future__ import print_function import argparse import pickle import sys from click import confirm import pyicloud from . import utils DEVICE_ERROR = ( "Please use the --device switch to indicate which device to use." ) def create_pickled_data(idevice, filename): """This helper will output the idevice to a pickled file named after the passed filename. This allows the data to be used without resorting to screen / pipe scrapping. """ data = {} for x in idevice.content: data[x] = idevice.content[x] location = filename pickle_file = open(location, 'wb') pickle.dump(data, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) pickle_file.close() def main(args=None): """Main commandline entrypoint""" if args is None: args = sys.argv[1:] parser = argparse.ArgumentParser( description="Find My iPhone CommandLine Tool") parser.add_argument( "--username", action="store", dest="username", default="", help="Apple ID to Use" ) parser.add_argument( "--password", action="store", dest="password", default="", help=( "Apple ID Password to Use; if unspecified, password will be " "fetched from the system keyring." ) ) parser.add_argument( "-n", "--non-interactive", action="store_false", dest="interactive", default=True, help="Disable interactive prompts." ) parser.add_argument( "--delete-from-keyring", action="store_true", dest="delete_from_keyring", default=False, help="Delete stored password in system keyring for this username.", ) parser.add_argument( "--list", action="store_true", dest="list", default=False, help="Short Listings for Device(s) associated with account", ) parser.add_argument( "--llist", action="store_true", dest="longlist", default=False, help="Detailed Listings for Device(s) associated with account", ) parser.add_argument( "--locate", action="store_true", dest="locate", default=False, help="Retrieve Location for the iDevice (non-exclusive).", ) # Restrict actions to a specific devices UID / DID parser.add_argument( "--device", action="store", dest="device_id", default=False, help="Only effect this device", ) # Trigger Sound Alert parser.add_argument( "--sound", action="store_true", dest="sound", default=False, help="Play a sound on the device", ) # Trigger Message w/Sound Alert parser.add_argument( "--message", action="store", dest="message", default=False, help="Optional Text Message to display with a sound", ) # Trigger Message (without Sound) Alert parser.add_argument( "--silentmessage", action="store", dest="silentmessage", default=False, help="Optional Text Message to display with no sounds", ) # Lost Mode parser.add_argument( "--lostmode", action="store_true", dest="lostmode", default=False, help="Enable Lost mode for the device", ) parser.add_argument( "--lostphone", action="store", dest="lost_phone", default=False, help="Phone Number allowed to call when lost mode is enabled", ) parser.add_argument( "--lostpassword", action="store", dest="lost_password", default=False, help="Forcibly active this passcode on the idevice", ) parser.add_argument( "--lostmessage", action="store", dest="lost_message", default="", help="Forcibly display this message when activating lost mode.", ) # Output device data to an pickle file parser.add_argument( "--outputfile", action="store_true", dest="output_to_file", default="", help="Save device data to a file in the current directory.", ) command_line = parser.parse_args(args) username = command_line.username password = command_line.password if username and command_line.delete_from_keyring: utils.delete_password_in_keyring(username) failure_count = 0 while True: # Which password we use is determined by your username, so we # do need to check for this first and separately. if not username: parser.error('No username supplied') if not password: password = utils.get_password( username, interactive=command_line.interactive ) if not password: parser.error('No password supplied') try: api = pyicloud.PyiCloudService( username.strip(), password.strip() ) if ( not utils.password_exists_in_keyring(username) and command_line.interactive and confirm("Save password in keyring? ") ): utils.store_password_in_keyring(username, password) break except pyicloud.exceptions.PyiCloudFailedLoginException: # If they have a stored password; we just used it and # it did not work; let's delete it if there is one. if utils.password_exists_in_keyring(username): utils.delete_password_in_keyring(username) message = "Bad username or password for {username}".format( username=username, ) password = None failure_count += 1 if failure_count >= 3: raise RuntimeError(message) print(message, file=sys.stderr) for dev in api.devices: if ( not command_line.device_id or ( command_line.device_id.strip().lower() == dev.content["id"].strip().lower() ) ): # List device(s) if command_line.locate: dev.location() if command_line.output_to_file: create_pickled_data( dev, filename=( dev.content["name"].strip().lower() + ".fmip_snapshot" ) ) contents = dev.content if command_line.longlist: print("-"*30) print(contents["name"]) for x in contents: print("%20s - %s" % (x, contents[x])) elif command_line.list: print("-"*30) print("Name - %s" % contents["name"]) print("Display Name - %s" % contents["deviceDisplayName"]) print("Location - %s" % contents["location"]) print("Battery Level - %s" % contents["batteryLevel"]) print("Battery Status- %s" % contents["batteryStatus"]) print("Device Class - %s" % contents["deviceClass"]) print("Device Model - %s" % contents["deviceModel"]) # Play a Sound on a device if command_line.sound: if command_line.device_id: dev.play_sound() else: raise RuntimeError( "\n\n\t\t%s %s\n\n" % ( "Sounds can only be played on a singular device.", DEVICE_ERROR ) ) # Display a Message on the device if command_line.message: if command_line.device_id: dev.display_message( subject='A Message', message=command_line.message, sounds=True ) else: raise RuntimeError( "%s %s" % ( "Messages can only be played " "on a singular device.", DEVICE_ERROR ) ) # Display a Silent Message on the device if command_line.silentmessage: if command_line.device_id: dev.display_message( subject='A Silent Message', message=command_line.silentmessage, sounds=False ) else: raise RuntimeError( "%s %s" % ( "Silent Messages can only be played " "on a singular device.", DEVICE_ERROR ) ) # Enable Lost mode if command_line.lostmode: if command_line.device_id: dev.lost_device( number=command_line.lost_phone.strip(), text=command_line.lost_message.strip(), newpasscode=command_line.lost_password.strip() ) else: raise RuntimeError( "%s %s" % ( "Lost Mode can only be activated " "on a singular device.", DEVICE_ERROR ) ) if __name__ == '__main__': main() pyicloud-0.9.1/pyicloud/exceptions.py0000644000076600000240000000163512732125444022044 0ustar adamcoddingtonstaff00000000000000 class PyiCloudException(Exception): pass class PyiCloudNoDevicesException(PyiCloudException): pass class PyiCloudAPIResponseError(PyiCloudException): def __init__(self, reason, code): self.reason = reason self.code = code message = reason if code: message += " (%s)" % code super(PyiCloudAPIResponseError, self).__init__(message) class PyiCloudFailedLoginException(PyiCloudException): pass class PyiCloud2FARequiredError(PyiCloudException): def __init__(self, url): message = "Two-factor authentication required for %s" % url super(PyiCloud2FARequiredError, self).__init__(message) class PyiCloudNoDevicesException(Exception): pass class NoStoredPasswordAvailable(PyiCloudException): pass class PyiCloudBinaryFeedParseError(Exception): pass class PyiCloudPhotoLibraryNotActivatedErrror(Exception): pass pyicloud-0.9.1/pyicloud/services/0000755000076600000240000000000012735251057021132 5ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/pyicloud/services/__init__.py0000644000076600000240000000061412732133266023242 0ustar adamcoddingtonstaff00000000000000from pyicloud.services.calendar import CalendarService from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager from pyicloud.services.ubiquity import UbiquityService from pyicloud.services.contacts import ContactsService from pyicloud.services.reminders import RemindersService from pyicloud.services.photos import PhotosService from pyicloud.services.account import AccountService pyicloud-0.9.1/pyicloud/services/account.py0000644000076600000240000000306112732133266023136 0ustar adamcoddingtonstaff00000000000000import sys import six from pyicloud.utils import underscore_to_camelcase class AccountService(object): def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self._devices = [] self._acc_endpoint = '%s/setup/web/device' % self._service_root self._account_devices_url = '%s/getDevices' % self._acc_endpoint req = self.session.get(self._account_devices_url, params=self.params) self.response = req.json() for device_info in self.response['devices']: # device_id = device_info['udid'] # self._devices[device_id] = AccountDevice(device_info) self._devices.append(AccountDevice(device_info)) @property def devices(self): return self._devices @six.python_2_unicode_compatible class AccountDevice(dict): def __init__(self, device_info): super(AccountDevice, self).__init__(device_info) def __getattr__(self, name): try: return self[underscore_to_camelcase(name)] except KeyError: raise AttributeError(name) def __str__(self): return u"{display_name}: {name}".format( display_name=self.model_display_name, name=self.name, ) def __repr__(self): return '<{display}>'.format( display=( six.text_type(self) if sys.version_info[0] >= 3 else six.text_type(self).encode('utf8', 'replace') ) ) pyicloud-0.9.1/pyicloud/services/calendar.py0000644000076600000240000000441012732125444023251 0ustar adamcoddingtonstaff00000000000000from __future__ import absolute_import from datetime import datetime, timedelta from calendar import monthrange import time from tzlocal import get_localzone class CalendarService(object): """ The 'Calendar' iCloud service, connects to iCloud and returns events. """ def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self._calendar_endpoint = '%s/ca' % self._service_root self._calendar_refresh_url = '%s/events' % self._calendar_endpoint self._calendar_event_detail_url = '%s/eventdetail' % ( self._calendar_endpoint, ) def get_event_detail(self, pguid, guid): """ Fetches a single event's details by specifying a pguid (a calendar) and a guid (an event's ID). """ params = dict(self.params) params.update({'lang': 'en-us', 'usertz': get_localzone().zone}) url = '%s/%s/%s' % (self._calendar_event_detail_url, pguid, guid) req = self.session.get(url, params=params) self.response = req.json() return self.response['Event'][0] def refresh_client(self, from_dt=None, to_dt=None): """ Refreshes the CalendarService endpoint, ensuring that the event data is up-to-date. If no 'from_dt' or 'to_dt' datetimes have been given, the range becomes this month. """ today = datetime.today() first_day, last_day = monthrange(today.year, today.month) if not from_dt: from_dt = datetime(today.year, today.month, first_day) if not to_dt: to_dt = datetime(today.year, today.month, last_day) params = dict(self.params) params.update({ 'lang': 'en-us', 'usertz': get_localzone().zone, 'startDate': from_dt.strftime('%Y-%m-%d'), 'endDate': to_dt.strftime('%Y-%m-%d') }) req = self.session.get(self._calendar_refresh_url, params=params) self.response = req.json() def events(self, from_dt=None, to_dt=None): """ Retrieves events for a given date range, by default, this month. """ self.refresh_client(from_dt, to_dt) return self.response['Event'] pyicloud-0.9.1/pyicloud/services/contacts.py0000644000076600000240000000326212663203023023313 0ustar adamcoddingtonstaff00000000000000from __future__ import absolute_import import os import uuid from datetime import datetime from calendar import monthrange class ContactsService(object): """ The 'Contacts' iCloud service, connects to iCloud and returns contacts. """ def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self._contacts_endpoint = '%s/co' % self._service_root self._contacts_refresh_url = '%s/startup' % self._contacts_endpoint self._contacts_changeset_url = '%s/changeset' % self._contacts_endpoint def refresh_client(self, from_dt=None, to_dt=None): """ Refreshes the ContactsService endpoint, ensuring that the contacts data is up-to-date. """ params_contacts = dict(self.params) params_contacts.update({ 'clientVersion': '2.1', 'locale': 'en_US', 'order': 'last,first', }) req = self.session.get( self._contacts_refresh_url, params=params_contacts ) self.response = req.json() params_refresh = dict(self.params) params_refresh.update({ 'prefToken': req.json()["prefToken"], 'syncToken': req.json()["syncToken"], }) self.session.post(self._contacts_changeset_url, params=params_refresh) req = self.session.get( self._contacts_refresh_url, params=params_contacts ) self.response = req.json() def all(self): """ Retrieves all contacts. """ self.refresh_client() return self.response['contacts'] pyicloud-0.9.1/pyicloud/services/findmyiphone.py0000644000076600000240000001415112663203023024165 0ustar adamcoddingtonstaff00000000000000import json import sys import six from pyicloud.exceptions import PyiCloudNoDevicesException class FindMyiPhoneServiceManager(object): """ The 'Find my iPhone' iCloud service This connects to iCloud and return phone data including the near-realtime latitude and longitude. """ def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self._fmip_endpoint = '%s/fmipservice/client/web' % self._service_root self._fmip_refresh_url = '%s/refreshClient' % self._fmip_endpoint self._fmip_sound_url = '%s/playSound' % self._fmip_endpoint self._fmip_message_url = '%s/sendMessage' % self._fmip_endpoint self._fmip_lost_url = '%s/lostDevice' % self._fmip_endpoint self._devices = {} self.refresh_client() def refresh_client(self): """ Refreshes the FindMyiPhoneService endpoint, This ensures that the location data is up-to-date. """ req = self.session.post( self._fmip_refresh_url, params=self.params, data=json.dumps( { 'clientContext': { 'fmly': True, 'shouldLocate': True, 'selectedDevice': 'all', } } ) ) self.response = req.json() for device_info in self.response['content']: device_id = device_info['id'] if device_id not in self._devices: self._devices[device_id] = AppleDevice( device_info, self.session, self.params, manager=self, sound_url=self._fmip_sound_url, lost_url=self._fmip_lost_url, message_url=self._fmip_message_url, ) else: self._devices[device_id].update(device_info) if not self._devices: raise PyiCloudNoDevicesException() def __getitem__(self, key): if isinstance(key, int): if six.PY3: key = list(self.keys())[key] else: key = self.keys()[key] return self._devices[key] def __getattr__(self, attr): return getattr(self._devices, attr) def __unicode__(self): return six.text_type(self._devices) def __str__(self): as_unicode = self.__unicode__() if sys.version_info[0] >= 3: return as_unicode else: return as_unicode.encode('ascii', 'ignore') def __repr__(self): return six.text_type(self) class AppleDevice(object): def __init__( self, content, session, params, manager, sound_url=None, lost_url=None, message_url=None ): self.content = content self.manager = manager self.session = session self.params = params self.sound_url = sound_url self.lost_url = lost_url self.message_url = message_url def update(self, data): self.content = data def location(self): self.manager.refresh_client() return self.content['location'] def status(self, additional=[]): """ Returns status information for device. This returns only a subset of possible properties. """ self.manager.refresh_client() fields = ['batteryLevel', 'deviceDisplayName', 'deviceStatus', 'name'] fields += additional properties = {} for field in fields: properties[field] = self.content.get(field) return properties def play_sound(self, subject='Find My iPhone Alert'): """ Send a request to the device to play a sound. It's possible to pass a custom message by changing the `subject`. """ data = json.dumps({'device': self.content['id'], 'subject': subject}) self.session.post( self.sound_url, params=self.params, data=data ) def display_message( self, subject='Find My iPhone Alert', message="This is a note", sounds=False ): """ Send a request to the device to play a sound. It's possible to pass a custom message by changing the `subject`. """ data = json.dumps( { 'device': self.content['id'], 'subject': subject, 'sound': sounds, 'userText': True, 'text': message } ) self.session.post( self.message_url, params=self.params, data=data ) def lost_device( self, number, text='This iPhone has been lost. Please call me.', newpasscode="" ): """ Send a request to the device to trigger 'lost mode'. The device will show the message in `text`, and if a number has been passed, then the person holding the device can call the number without entering the passcode. """ data = json.dumps({ 'text': text, 'userText': True, 'ownerNbr': number, 'lostModeEnabled': True, 'trackingEnabled': True, 'device': self.content['id'], 'passcode': newpasscode }) self.session.post( self.lost_url, params=self.params, data=data ) @property def data(self): return self.content def __getitem__(self, key): return self.content[key] def __getattr__(self, attr): return getattr(self.content, attr) def __unicode__(self): display_name = self['deviceDisplayName'] name = self['name'] return '%s: %s' % ( display_name, name, ) def __str__(self): as_unicode = self.__unicode__() if sys.version_info[0] >= 3: return as_unicode else: return as_unicode.encode('ascii', 'ignore') def __repr__(self): return '' % str(self) pyicloud-0.9.1/pyicloud/services/photos.py0000644000076600000240000002147012732125444023021 0ustar adamcoddingtonstaff00000000000000import sys import json import urllib from datetime import datetime from base64 import b64decode from bitstring import ConstBitStream from pyicloud.exceptions import ( PyiCloudAPIResponseError, PyiCloudBinaryFeedParseError, PyiCloudPhotoLibraryNotActivatedErrror ) class PhotosService(object): """ The 'Photos' iCloud service.""" def __init__(self, service_root, session, params): self.session = session self.params = dict(params) self.prepostfetch = 200 self._service_root = service_root self._service_endpoint = '%s/ph' % self._service_root try: request = self.session.get( '%s/startup' % self._service_endpoint, params=self.params ) response = request.json() self.params.update({ 'syncToken': response['syncToken'], 'clientInstanceId': self.params.pop('clientId') }) except PyiCloudAPIResponseError as error: if error.code == 402: raise PyiCloudPhotoLibraryNotActivatedErrror( "iCloud Photo Library has not been activated yet " "for this user") self._photo_assets = {} @property def albums(self): request = self.session.get( '%s/folders' % self._service_endpoint, params=self.params ) response = request.json() albums = {} for folder in response['folders']: if not folder['type'] == 'album': continue album = PhotoAlbum(folder, self) albums[album.title] = album return albums @property def all(self): return self.albums['All Photos'] def _fetch_asset_data_for(self, client_ids): client_ids = [cid for cid in client_ids if cid not in self._photo_assets] data = json.dumps({ 'syncToken': self.params.get('syncToken'), 'methodOverride': 'GET', 'clientIds': client_ids, }) request = self.session.post( '%s/assets' % self._service_endpoint, params=self.params, data=data ) response = request.json() for asset in response['assets']: self._photo_assets[asset['clientId']] = asset class PhotoAlbum(object): def __init__(self, data, service): self.data = data self.service = service self._photo_assets = None @property def title(self): BUILTIN_ALBUMS = { 'recently-added': "Recently Added", 'time-lapse': "Time-lapse", 'videos': "Videos", 'slo-mo': 'Slo-mo', 'all-photos': "All Photos", 'selfies': "Selfies", 'bursts': "Bursts", 'favorites': "Favourites", 'panoramas': "Panoramas", 'deleted-photos': "Recently Deleted", 'hidden': "Hidden", 'screenshots': "Screenshots" } if self.data.get('isServerGenerated'): return BUILTIN_ALBUMS[self.data.get('serverId')] else: return self.data.get('title') def __iter__(self): return iter(self.photos) def __getitem__(self, index): return self.photos[index] @property def photos(self): if not self._photo_assets: child_assets = self.data.get('childAssetsBinaryFeed') if not child_assets: raise PyiCloudBinaryFeedParseError( "Missing childAssetsBinaryFeed in photo album") self._photo_assets = self._parse_binary_feed(child_assets) return self._photo_assets def _parse_binary_feed(self, feed): binaryfeed = bytearray(b64decode(feed)) bitstream = ConstBitStream(binaryfeed) payload_encoding = binaryfeed[0] if payload_encoding != bitstream.read("uint:8"): raise PyiCloudBinaryFeedParseError( "Missmatch betweeen binaryfeed and bistream payload encoding") ASSET_PAYLOAD = 255 ASSET_WITH_ORIENTATION_PAYLOAD = 254 ASPECT_RATIOS = [ 0.75, 4.0 / 3.0 - 3.0 * (4.0 / 3.0 - 1.0) / 4.0, 4.0 / 3.0 - 2.0 * (4.0 / 3.0 - 1.0) / 4.0, 1.25, 4.0 / 3.0, 1.5 - 2.0 * (1.5 - 4.0 / 3.0) / 3.0, 1.5 - 1.0 * (1.5 - 4.0 / 3.0) / 3.0, 1.5, 1.5694444444444444, 1.6388888888888888, 1.7083333333333333, 16.0 / 9.0, 2.0 - 2.0 * (2.0 - 16.0 / 9.0) / 3.0, 2.0 - 1.0 * (2.0 - 16.0 / 9.0) / 3.0, 2, 3 ] valid_payloads = [ASSET_PAYLOAD, ASSET_WITH_ORIENTATION_PAYLOAD] if payload_encoding not in valid_payloads: raise PyiCloudBinaryFeedParseError( "Unknown payload encoding '%s'" % payload_encoding) assets = {} while len(bitstream) - bitstream.pos >= 48: range_start = bitstream.read("uint:24") range_length = bitstream.read("uint:24") range_end = range_start + range_length previous_asset_id = 0 for index in range(range_start, range_end): aspect_ratio = ASPECT_RATIOS[bitstream.read("uint:4")] id_size = bitstream.read("uint:2") if id_size: # A size has been reserved for the asset id asset_id = bitstream.read("uint:%s" % (2 + 8 * id_size)) else: # The id is just an increment to a previous id asset_id = previous_asset_id + bitstream.read("uint:2") + 1 orientation = None if payload_encoding == ASSET_WITH_ORIENTATION_PAYLOAD: orientation = bitstream.read("uint:3") assets[index] = PhotoAsset(index, asset_id, aspect_ratio, orientation, self) previous_asset_id = asset_id return assets.values() def _fetch_asset_data_for(self, asset): if asset.client_id in self.service._photo_assets: return self.service._photo_assets[asset.client_id] client_ids = [] prefetch = postfetch = self.service.prepostfetch for index in range( max(asset.album_index - prefetch, 0), min(asset.album_index + postfetch + 1, len(self._photo_assets))): client_ids.append(self._photo_assets[index].client_id) self.service._fetch_asset_data_for(client_ids) return self.service._photo_assets[asset.client_id] def __unicode__(self): return self.title def __str__(self): as_unicode = self.__unicode__() if sys.version_info[0] >= 3: return as_unicode else: return as_unicode.encode('ascii', 'ignore') def __repr__(self): return "<%s: '%s'>" % ( type(self).__name__, self ) class PhotoAsset(object): def __init__(self, index, client_id, aspect_ratio, orientation, album): self.album_index = index self.client_id = client_id self.aspect_ratio = aspect_ratio self.orientation = orientation self.album = album self._data = None @property def data(self): if not self._data: self._data = self.album._fetch_asset_data_for(self) return self._data @property def filename(self): return self.data['details'].get('filename') @property def size(self): try: return int(self.data['details'].get('filesize')) except ValueError: return None @property def created(self): dt = datetime.fromtimestamp(self.data.get('createdDate') / 1000.0) return dt.strftime('%Y-%m-%dT%H:%M:%SZ') @property def dimensions(self): return self.data.get('dimensions') @property def versions(self): versions = {} for version in self.data.get('derivativeInfo'): (version, width, height, size, mimetype, u1, u2, u3, url, filename) = version.split(':') versions[version] = { 'width': width, 'height': height, 'size': size, 'mimetype': mimetype, 'url': urllib.unquote(url), 'filename': filename, } return versions def download(self, version='original', **kwargs): if version not in self.versions: return None return self.album.service.session.get( self.versions[version]['url'], stream=True, **kwargs ) def __repr__(self): return "<%s: client_id=%s>" % ( type(self).__name__, self.client_id ) pyicloud-0.9.1/pyicloud/services/reminders.py0000644000076600000240000000702412732133514023471 0ustar adamcoddingtonstaff00000000000000from __future__ import absolute_import from datetime import datetime, timedelta import time import uuid import json from tzlocal import get_localzone class RemindersService(object): def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self.lists = {} self.collections = {} self.refresh() def refresh(self): params_reminders = dict(self.params) params_reminders.update({ 'clientVersion': '4.0', 'lang': 'en-us', 'usertz': get_localzone().zone }) # Open reminders req = self.session.get( self._service_root + '/rd/startup', params=params_reminders ) startup = req.json() self.lists = {} self.collections = {} for collection in startup['Collections']: temp = [] self.collections[collection['title']] = { 'guid': collection['guid'], 'ctag': collection['ctag'] } for reminder in startup['Reminders']: if reminder['pGuid'] != collection['guid']: continue if 'dueDate' in reminder: if reminder['dueDate']: due = datetime( reminder['dueDate'][1], reminder['dueDate'][2], reminder['dueDate'][3], reminder['dueDate'][4], reminder['dueDate'][5] ) else: due = None else: due = None if reminder['description']: desc = reminder['description'] else: desc = "" temp.append({ "title": reminder['title'], "desc": desc, "due": due }) self.lists[collection['title']] = temp def post(self, title, description="", collection=None): pguid = 'tasks' if collection: if collection in self.collections: pguid = self.collections[collection]['guid'] params_reminders = dict(self.params) params_reminders.update({ 'clientVersion': '4.0', 'lang': 'en-us', 'usertz': get_localzone().zone }) req = self.session.post( self._service_root + '/rd/reminders/tasks', data=json.dumps({ "Reminders": { 'title': title, "description": description, "pGuid": pguid, "etag": None, "order": None, "priority": 0, "recurrence": None, "alarms": [], "startDate": None, "startDateTz": None, "startDateIsAllDay": False, "completedDate": None, "dueDate": None, "dueDateIsAllDay": False, "lastModifiedDate": None, "createdDate": None, "isFamily": None, "createdDateExtended": int(time.time()*1000), "guid": str(uuid.uuid4()) }, "ClientState": {"Collections": list(self.collections.values())} }), params=params_reminders) return req.ok pyicloud-0.9.1/pyicloud/services/ubiquity.py0000644000076600000240000000565712663203023023362 0ustar adamcoddingtonstaff00000000000000from datetime import datetime import sys class UbiquityService(object): """ The 'Ubiquity' iCloud service.""" def __init__(self, service_root, session, params): self.session = session self.params = params self._root = None self._service_root = service_root self._node_url = '/ws/%s/%s/%s' def get_node_url(self, id, variant='item'): return self._service_root + self._node_url % ( self.params['dsid'], variant, id ) def get_node(self, id): request = self.session.get(self.get_node_url(id)) return UbiquityNode(self, request.json()) def get_children(self, id): request = self.session.get( self.get_node_url(id, 'parent') ) items = request.json()['item_list'] return [UbiquityNode(self, item) for item in items] def get_file(self, id, **kwargs): request = self.session.get( self.get_node_url(id, 'file'), **kwargs ) return request @property def root(self): if not self._root: self._root = self.get_node(0) return self._root def __getattr__(self, attr): return getattr(self.root, attr) def __getitem__(self, key): return self.root[key] class UbiquityNode(object): def __init__(self, conn, data): self.data = data self.connection = conn @property def item_id(self): return self.data.get('item_id') @property def name(self): return self.data.get('name') @property def type(self): 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 def size(self): try: return int(self.data.get('size')) except ValueError: return None @property def modified(self): return datetime.strptime( self.data.get('modified'), '%Y-%m-%dT%H:%M:%SZ' ) def dir(self): 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): return [ child for child in self.get_children() if child.name == name ][0] def __getitem__(self, key): try: return self.get(key) except IndexError: raise KeyError('No child named %s exists' % key) def __unicode__(self): return self.name def __str__(self): as_unicode = self.__unicode__() if sys.version_info[0] >= 3: return as_unicode else: return as_unicode.encode('ascii', 'ignore') def __repr__(self): return "<%s: '%s'>" % ( self.type.capitalize(), self ) pyicloud-0.9.1/pyicloud/tests/0000755000076600000240000000000012735251057020451 5ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/pyicloud/tests/__init__.py0000644000076600000240000000000012504703555022547 0ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/pyicloud/tests/test_sanity.py0000644000076600000240000000031612504703555023370 0ustar adamcoddingtonstaff00000000000000from unittest2 import TestCase from pyicloud.cmdline import main class SanityTestCase(TestCase): def test_basic_sanity(self): with self.assertRaises(SystemExit): main(['--help']) pyicloud-0.9.1/pyicloud/utils.py0000644000076600000240000000320212732133266021014 0ustar adamcoddingtonstaff00000000000000import getpass import keyring import sys from .exceptions import NoStoredPasswordAvailable KEYRING_SYSTEM = 'pyicloud://icloud-password' def get_password(username, interactive=sys.stdout.isatty()): try: return get_password_from_keyring(username) except NoStoredPasswordAvailable: if not interactive: raise return getpass.getpass( 'Enter iCloud password for {username}: '.format( username=username, ) ) def password_exists_in_keyring(username): try: get_password_from_keyring(username) except NoStoredPasswordAvailable: return False return True def get_password_from_keyring(username): result = keyring.get_password( KEYRING_SYSTEM, username ) if result is None: raise NoStoredPasswordAvailable( "No pyicloud password for {username} could be found " "in the system keychain. Use the `--store-in-keyring` " "command-line option for storing a password for this " "username.".format( username=username, ) ) return result def store_password_in_keyring(username, password): return keyring.set_password( KEYRING_SYSTEM, username, password, ) def delete_password_in_keyring(username): return keyring.delete_password( KEYRING_SYSTEM, username, ) def underscore_to_camelcase(word, initial_capital=False): words = [x.capitalize() or '_' for x in word.split('_')] if not initial_capital: words[0] = words[0].lower() return ''.join(words) pyicloud-0.9.1/pyicloud.egg-info/0000755000076600000240000000000012735251057021001 5ustar adamcoddingtonstaff00000000000000pyicloud-0.9.1/pyicloud.egg-info/dependency_links.txt0000644000076600000240000000000112735251056025046 0ustar adamcoddingtonstaff00000000000000 pyicloud-0.9.1/pyicloud.egg-info/entry_points.txt0000644000076600000240000000006212735251056024274 0ustar adamcoddingtonstaff00000000000000[console_scripts] icloud = pyicloud.cmdline:main pyicloud-0.9.1/pyicloud.egg-info/pbr.json0000644000076600000240000000005712735251056022460 0ustar adamcoddingtonstaff00000000000000{"is_release": false, "git_version": "3818c43"}pyicloud-0.9.1/pyicloud.egg-info/PKG-INFO0000644000076600000240000000116012735251056022073 0ustar adamcoddingtonstaff00000000000000Metadata-Version: 1.1 Name: pyicloud Version: 0.9.1 Summary: PyiCloud is a module which allows pythonistas to interact with iCloud webservices. Home-page: https://github.com/picklepete/pyicloud Author: The PyiCloud Authors Author-email: License: MIT Description: UNKNOWN Platform: UNKNOWN Classifier: Intended Audience :: Developers Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: License :: OSI Approved :: MIT License pyicloud-0.9.1/pyicloud.egg-info/requires.txt0000644000076600000240000000015412735251056023400 0ustar adamcoddingtonstaff00000000000000requests>=1.2 keyring>=8.0,<9.0 keyrings.alt>=1.0,<2.0 click>=6.0,<7.0 six>=1.9.0 tzlocal certifi bitstring pyicloud-0.9.1/pyicloud.egg-info/SOURCES.txt0000644000076600000240000000124512735251057022667 0ustar adamcoddingtonstaff00000000000000MANIFEST.in README.rst requirements.txt setup.cfg setup.py pyicloud/__init__.py pyicloud/base.py pyicloud/cmdline.py pyicloud/exceptions.py pyicloud/utils.py pyicloud.egg-info/PKG-INFO pyicloud.egg-info/SOURCES.txt pyicloud.egg-info/dependency_links.txt pyicloud.egg-info/entry_points.txt pyicloud.egg-info/pbr.json pyicloud.egg-info/requires.txt pyicloud.egg-info/top_level.txt pyicloud/services/__init__.py pyicloud/services/account.py pyicloud/services/calendar.py pyicloud/services/contacts.py pyicloud/services/findmyiphone.py pyicloud/services/photos.py pyicloud/services/reminders.py pyicloud/services/ubiquity.py pyicloud/tests/__init__.py pyicloud/tests/test_sanity.pypyicloud-0.9.1/pyicloud.egg-info/top_level.txt0000644000076600000240000000001112735251056023522 0ustar adamcoddingtonstaff00000000000000pyicloud pyicloud-0.9.1/README.rst0000644000076600000240000002420312732133116017137 0ustar adamcoddingtonstaff00000000000000.. image:: https://travis-ci.org/picklepete/pyicloud.svg?branch=master :alt: Check out our test status at https://travis-ci.org/picklepete/pyicloud :target: https://travis-ci.org/picklepete/pyicloud .. image:: https://badges.gitter.im/Join%20Chat.svg :alt: Join the chat at https://gitter.im/picklepete/pyicloud :target: https://gitter.im/picklepete/pyicloud?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge PyiCloud is a module which allows pythonistas to interact with iCloud webservices. It's powered by the fantastic `requests `_ HTTP library. At its core, PyiCloud connects to iCloud using your username and password, then performs calendar and iPhone queries against their API. ============== Authentication ============== Authentication without using a saved password is as simple as passing your username and password to the ``PyiCloudService`` class: >>> from pyicloud import PyiCloudService >>> api = PyiCloudService('jappleseed@apple.com', 'password') In the event that the username/password combination is invalid, a ``PyiCloudFailedLoginException`` exception is thrown. You can also store your password in the system keyring using the command-line tool: >>> icloud --username=jappleseed@apple.com ICloud Password for jappleseed@apple.com: Save password in keyring? (y/N) If you have stored a password in the keyring, you will not be required to provide a password when interacting with the command-line tool or instantiating the ``PyiCloudService`` class for the username you stored the password for. >>> api = PyiCloudService('jappleseed@apple.com') If you would like to delete a password stored in your system keyring, you can clear a stored password using the ``--delete-from-keyring`` command-line option: >>> icloud --username=jappleseed@apple.com --delete-from-keyring ******************************* Two-factor authentication (2FA) ******************************* If you have enabled two-factor authentication for the account you will have to do some extra work: .. code-block:: python if api.requires_2fa: print "Two-factor authentication required. Your trusted devices are:" devices = api.trusted_devices for i, device in enumerate(devices): print " %s: %s" % (i, device.get('deviceName', "SMS to %s" % device.get('phoneNumber'))) device = click.prompt('Which device would you like to use?', default=0) device = devices[device] if not api.send_verification_code(device): print "Failed to send verification code" sys.exit(1) code = click.prompt('Please enter validation code') if not api.validate_verification_code(device, code): print "Failed to verify verification code" sys.exit(1) Note: Both regular login and two-factor authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months. ======= Devices ======= You can list which devices associated with your account by using the ``devices`` property: >>> api.devices { u'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': , u'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': } and you can access individual devices by either their index, or their ID: >>> api.devices[0] >>> api.devices['i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w=='] or, as a shorthand if you have only one associated apple device, you can simply use the ``iphone`` property to access the first device associated with your account: >>> api.iphone Note: the first device associated with your account may not necessarily be your iPhone. ============== Find My iPhone ============== Once you have successfully authenticated, you can start querying your data! ******** Location ******** Returns the device's last known location. The Find My iPhone app must have been installed and initialized. >>> api.iphone.location() {u'timeStamp': 1357753796553, u'locationFinished': True, u'longitude': -0.14189, u'positionType': u'GPS', u'locationType': None, u'latitude': 51.501364, u'isOld': False, u'horizontalAccuracy': 5.0} ****** Status ****** The Find My iPhone response is quite bloated, so for simplicity's sake this method will return a subset of the properties. >>> api.iphone.status() {'deviceDisplayName': u'iPhone 5', 'deviceStatus': u'200', 'batteryLevel': 0.6166913, 'name': u"Peter's iPhone"} If you wish to request further properties, you may do so by passing in a list of property names. ********** Play Sound ********** Sends a request to the device to play a sound, if you wish pass a custom message you can do so by changing the subject arg. >>> api.iphone.play_sound() A few moments later, the device will play a ringtone, display the default notification ("Find My iPhone Alert") and a confirmation email will be sent to you. ********* Lost Mode ********* Lost mode is slightly different to the "Play Sound" functionality in that it allows the person who picks up the phone to call a specific phone number *without having to enter the passcode*. Just like "Play Sound" you may pass a custom message which the device will display, if it's not overridden the custom message of "This iPhone has been lost. Please call me." is used. >>> phone_number = '555-373-383' >>> message = 'Thief! Return my phone immediately.' >>> api.iphone.lost_device(phone_number, message) ======== Calendar ======== The calendar webservice currently only supports fetching events. ****** Events ****** Returns this month's events: >>> api.calendar.events() Or, between a specific date range: >>> from_dt = datetime(2012, 1, 1) >>> to_dt = datetime(2012, 1, 31) >>> api.calendar.events(from_dt, to_dt) Alternatively, you may fetch a single event's details, like so: >>> api.calendar.get_event_detail('CALENDAR', 'EVENT_ID') ======== Contacts ======== You can access your iCloud contacts/address book through the ``contacts`` property: >>> for c in api.contacts.all(): >>> print c.get('firstName'), c.get('phones') John [{u'field': u'+1 555-55-5555-5', u'label': u'MOBILE'}] Note: These contacts do not include contacts federated from e.g. Facebook, only the ones stored in iCloud. ======================= File Storage (Ubiquity) ======================= You can access documents stored in your iCloud account by using the ``files`` property's ``dir`` method: >>> api.files.dir() [u'.do-not-delete', u'.localized', u'com~apple~Notes', u'com~apple~Preview', u'com~apple~mail', u'com~apple~shoebox', u'com~apple~system~spotlight' ] You can access children and their children's children using the filename as an index: >>> api.files['com~apple~Notes'] >>> api.files['com~apple~Notes'].type u'folder' >>> api.files['com~apple~Notes'].dir() [u'Documents'] >>> api.files['com~apple~Notes']['Documents'].dir() [u'Some Document'] >>> api.files['com~apple~Notes']['Documents']['Some Document'].name u'Some Document' >>> api.files['com~apple~Notes']['Documents']['Some Document'].modified datetime.datetime(2012, 9, 13, 2, 26, 17) >>> api.files['com~apple~Notes']['Documents']['Some Document'].size 1308134 >>> api.files['com~apple~Notes']['Documents']['Some Document'].type u'file' And when you have a file that you'd like to download, the ``open`` method will return a response object from which you can read the ``content``. >>> api.files['com~apple~Notes']['Documents']['Some Document'].open().content 'Hello, these are the file contents' Note: the object returned from the above ``open`` method is a `response object `_ and the ``open`` method can accept any parameters you might normally use in a request using `requests `_. For example, if you know that the file you're opening has JSON content: >>> api.files['com~apple~Notes']['Documents']['information.json'].open().json() {'How much we love you': 'lots'} >>> api.files['com~apple~Notes']['Documents']['information.json'].open().json()['How much we love you'] 'lots' Or, if you're downloading a particularly large file, you may want to use the ``stream`` keyword argument, and read directly from the raw response object: >>> download = api.files['com~apple~Notes']['Documents']['big_file.zip'].open(stream=True) >>> with open('downloaded_file.zip', 'wb') as opened_file: opened_file.write(download.raw.read()) ======================= Photo Library ======================= You can access the iCloud Photo Library through the ``photos`` property. >>> api.photos.all Individual albums are available through the ``albums`` property: >>> api.photos.albums['Selfies'] Which you can index or iterate to access the photo assets: >>> for photo in api.photos.albums['Selfies']: print photo, photo.filename IMG_6045.JPG Metadata about photos is fetched on demand as you access properties of the ``PhotoAsset`` object, and are also prefetched to improve performance. To download a photo use the `download` method, which will return a `response object `_, initialized with ``stream`` set to ``True``, so you can read from the raw response object: >>> photo = api.photos.albums['Selfies'][0] >>> download = photo.download() >>> with open(photo.filename, 'wb') as opened_file: opened_file.write(download.raw.read()) Note: Consider using ``shutil.copyfile`` or another buffered strategy for downloading the file so that the whole file isn't read into memory before writing. Information about each version can be accessed through the ``versions`` property: >>> photo.versions.keys() [u'large', u'medium', u'original', u'thumb'] To download a specific version of the photo asset, pass the version to ``download()``: >>> download = photo.download('thumb') >>> with open(photo.versions['thumb'].filename, 'wb') as thumb_file: thumb_file.write(download.raw.read()) pyicloud-0.9.1/requirements.txt0000644000076600000240000000015412735250701020736 0ustar adamcoddingtonstaff00000000000000requests>=1.2 keyring>=8.0,<9.0 keyrings.alt>=1.0,<2.0 click>=6.0,<7.0 six>=1.9.0 tzlocal certifi bitstring pyicloud-0.9.1/setup.cfg0000644000076600000240000000014412735251057017277 0ustar adamcoddingtonstaff00000000000000[pytest] norecursedirs = lib build .tox [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyicloud-0.9.1/setup.py0000644000076600000240000000167512735251007017175 0ustar adamcoddingtonstaff00000000000000from setuptools import setup, find_packages with open('requirements.txt') as f: required = f.read().splitlines() setup( name='pyicloud', version='0.9.1', url='https://github.com/picklepete/pyicloud', description=( 'PyiCloud is a module which allows pythonistas to ' 'interact with iCloud webservices.' ), maintainer='The PyiCloud Authors', maintainer_email=' ', license='MIT', packages=find_packages(), install_requires=required, classifiers=[ 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'License :: OSI Approved :: MIT License', ], entry_points={ 'console_scripts': [ 'icloud = pyicloud.cmdline:main' ] }, )