pyicloud-1.0.0/0000755000175100001710000000000014203477505014146 5ustar runnerdocker00000000000000pyicloud-1.0.0/MANIFEST.in0000644000175100001710000000005414203477476015712 0ustar runnerdocker00000000000000include README.rst include requirements.txt pyicloud-1.0.0/PKG-INFO0000644000175100001710000004457214203477505015257 0ustar runnerdocker00000000000000Metadata-Version: 1.2 Name: pyicloud Version: 1.0.0 Summary: PyiCloud is a module which allows pythonistas to interact with iCloud webservices. Home-page: https://github.com/picklepete/pyicloud Maintainer: The PyiCloud Authors License: MIT Download-URL: https://github.com/picklepete/pyicloud/tarball/1.0.0 Description: ******** pyiCloud ******** .. 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://img.shields.io/pypi/v/pyicloud.svg :alt: Library version :target: https://pypi.org/project/pyicloud .. image:: https://img.shields.io/pypi/pyversions/pyicloud.svg :alt: Supported versions :target: https://pypi.org/project/pyicloud .. image:: https://pepy.tech/badge/pyicloud :alt: Downloads :target: https://pypi.org/project/pyicloud .. image:: https://requires.io/github/Quentame/pyicloud/requirements.svg?branch=master :alt: Requirements Status :target: https://requires.io/github/Quentame/pyicloud/requirements/?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Formated with Black :target: https://github.com/psf/black .. 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: .. code-block:: python 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: .. code-block:: console $ 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. .. code-block:: python 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: .. code-block:: console $ icloud --username=jappleseed@apple.com --delete-from-keyring **Note**: Authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months. Two-step and two-factor authentication (2SA/2FA) ************************************************ If you have enabled two-factor authentications (2FA) or `two-step authentication (2SA) `_ for the account you will have to do some extra work: .. code-block:: python if api.requires_2fa: print("Two-factor authentication required.") code = input("Enter the code you received of one of your approved devices: ") result = api.validate_2fa_code(code) print("Code validation result: %s" % result) if not result: print("Failed to verify security code") sys.exit(1) if not api.is_trusted_session: print("Session is not trusted. Requesting trust...") result = api.trust_session() print("Session trust result %s" % result) if not result: print("Failed to request trust. You will likely be prompted for the code again in the coming weeks") elif api.requires_2sa: import click print("Two-step 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) Devices ======= You can list which devices associated with your account by using the ``devices`` property: .. code-block:: pycon >>> api.devices { 'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': , 'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': } and you can access individual devices by either their index, or their ID: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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. .. code-block:: pycon >>> api.iphone.location() {'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, '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. .. code-block:: pycon >>> api.iphone.status() {'deviceDisplayName': 'iPhone 5', 'deviceStatus': '200', 'batteryLevel': 0.6166913, 'name': "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. .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python api.calendar.events() Or, between a specific date range: .. code-block:: python 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: .. code-block:: python api.calendar.get_event_detail('CALENDAR', 'EVENT_ID') Contacts ======== You can access your iCloud contacts/address book through the ``contacts`` property: .. code-block:: pycon >>> for c in api.contacts.all(): >>> print(c.get('firstName'), c.get('phones')) John [{'field': '+1 555-55-5555-5', 'label': '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: .. code-block:: pycon >>> api.files.dir() ['.do-not-delete', '.localized', 'com~apple~Notes', 'com~apple~Preview', 'com~apple~mail', 'com~apple~shoebox', 'com~apple~system~spotlight' ] You can access children and their children's children using the filename as an index: .. code-block:: pycon >>> api.files['com~apple~Notes'] >>> api.files['com~apple~Notes'].type 'folder' >>> api.files['com~apple~Notes'].dir() ['Documents'] >>> api.files['com~apple~Notes']['Documents'].dir() ['Some Document'] >>> api.files['com~apple~Notes']['Documents']['Some Document'].name '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 '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``. .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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()) File Storage (iCloud Drive) =========================== You can access your iCloud Drive using an API identical to the Ubiquity one described in the previous section, except that it is rooted at ```api.drive```: .. code-block:: pycon >>> api.drive.dir() ['Holiday Photos', 'Work Files'] >>> api.drive['Holiday Photos']['2013']['Sicily'].dir() ['DSC08116.JPG', 'DSC08117.JPG'] >>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG'] >>> drive_file.name 'DSC08116.JPG' >>> drive_file.date_modified datetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC >>> drive_file.size 2021698 >>> drive_file.type 'file' The ``open`` method will return a response object from which you can read the file's contents: .. code-block:: python from shutil import copyfileobj with drive_file.open(stream=True) as response: with open(drive_file.name, 'wb') as file_out: copyfileobj(response.raw, file_out) To interact with files and directions the ``mkdir``, ``rename`` and ``delete`` functions are available for a file or folder: .. code-block:: python api.drive['Holiday Photos'].mkdir('2020') api.drive['Holiday Photos']['2020'].rename('2020_copy') api.drive['Holiday Photos']['2020_copy'].delete() The ``upload`` method can be used to send a file-like object to the iCloud Drive: .. code-block:: python with open('Vacation.jpeg', 'rb') as file_in: api.drive['Holiday Photos'].upload(file_in) It is strongly suggested to open file handles as binary rather than text to prevent decoding errors further down the line. Photo Library ======================= You can access the iCloud Photo Library through the ``photos`` property. .. code-block:: pycon >>> api.photos.all Individual albums are available through the ``albums`` property: .. code-block:: pycon >>> api.photos.albums['Screenshots'] Which you can iterate to access the photo assets. The 'All Photos' album is sorted by `added_date` so the most recently added photos are returned first. All other albums are sorted by `asset_date` (which represents the exif date) : .. code-block:: pycon >>> for photo in api.photos.albums['Screenshots']: print(photo, photo.filename) IMG_6045.JPG 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: .. code-block:: python photo = next(iter(api.photos.albums['Screenshots']), None) 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: .. code-block:: pycon >>> photo.versions.keys() ['medium', 'original', 'thumb'] To download a specific version of the photo asset, pass the version to ``download()``: .. code-block:: python download = photo.download('thumb') with open(photo.versions['thumb']['filename'], 'wb') as thumb_file: thumb_file.write(download.raw.read()) Code samples ============ If you wanna see some code samples see the `code samples file `_. Keywords: icloud,find-my-iphone Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.7 pyicloud-1.0.0/README.rst0000644000175100001710000003427714203477477015662 0ustar runnerdocker00000000000000******** pyiCloud ******** .. 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://img.shields.io/pypi/v/pyicloud.svg :alt: Library version :target: https://pypi.org/project/pyicloud .. image:: https://img.shields.io/pypi/pyversions/pyicloud.svg :alt: Supported versions :target: https://pypi.org/project/pyicloud .. image:: https://pepy.tech/badge/pyicloud :alt: Downloads :target: https://pypi.org/project/pyicloud .. image:: https://requires.io/github/Quentame/pyicloud/requirements.svg?branch=master :alt: Requirements Status :target: https://requires.io/github/Quentame/pyicloud/requirements/?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Formated with Black :target: https://github.com/psf/black .. 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: .. code-block:: python 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: .. code-block:: console $ 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. .. code-block:: python 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: .. code-block:: console $ icloud --username=jappleseed@apple.com --delete-from-keyring **Note**: Authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months. Two-step and two-factor authentication (2SA/2FA) ************************************************ If you have enabled two-factor authentications (2FA) or `two-step authentication (2SA) `_ for the account you will have to do some extra work: .. code-block:: python if api.requires_2fa: print("Two-factor authentication required.") code = input("Enter the code you received of one of your approved devices: ") result = api.validate_2fa_code(code) print("Code validation result: %s" % result) if not result: print("Failed to verify security code") sys.exit(1) if not api.is_trusted_session: print("Session is not trusted. Requesting trust...") result = api.trust_session() print("Session trust result %s" % result) if not result: print("Failed to request trust. You will likely be prompted for the code again in the coming weeks") elif api.requires_2sa: import click print("Two-step 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) Devices ======= You can list which devices associated with your account by using the ``devices`` property: .. code-block:: pycon >>> api.devices { 'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': , 'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': } and you can access individual devices by either their index, or their ID: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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. .. code-block:: pycon >>> api.iphone.location() {'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, '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. .. code-block:: pycon >>> api.iphone.status() {'deviceDisplayName': 'iPhone 5', 'deviceStatus': '200', 'batteryLevel': 0.6166913, 'name': "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. .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python api.calendar.events() Or, between a specific date range: .. code-block:: python 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: .. code-block:: python api.calendar.get_event_detail('CALENDAR', 'EVENT_ID') Contacts ======== You can access your iCloud contacts/address book through the ``contacts`` property: .. code-block:: pycon >>> for c in api.contacts.all(): >>> print(c.get('firstName'), c.get('phones')) John [{'field': '+1 555-55-5555-5', 'label': '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: .. code-block:: pycon >>> api.files.dir() ['.do-not-delete', '.localized', 'com~apple~Notes', 'com~apple~Preview', 'com~apple~mail', 'com~apple~shoebox', 'com~apple~system~spotlight' ] You can access children and their children's children using the filename as an index: .. code-block:: pycon >>> api.files['com~apple~Notes'] >>> api.files['com~apple~Notes'].type 'folder' >>> api.files['com~apple~Notes'].dir() ['Documents'] >>> api.files['com~apple~Notes']['Documents'].dir() ['Some Document'] >>> api.files['com~apple~Notes']['Documents']['Some Document'].name '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 '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``. .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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()) File Storage (iCloud Drive) =========================== You can access your iCloud Drive using an API identical to the Ubiquity one described in the previous section, except that it is rooted at ```api.drive```: .. code-block:: pycon >>> api.drive.dir() ['Holiday Photos', 'Work Files'] >>> api.drive['Holiday Photos']['2013']['Sicily'].dir() ['DSC08116.JPG', 'DSC08117.JPG'] >>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG'] >>> drive_file.name 'DSC08116.JPG' >>> drive_file.date_modified datetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC >>> drive_file.size 2021698 >>> drive_file.type 'file' The ``open`` method will return a response object from which you can read the file's contents: .. code-block:: python from shutil import copyfileobj with drive_file.open(stream=True) as response: with open(drive_file.name, 'wb') as file_out: copyfileobj(response.raw, file_out) To interact with files and directions the ``mkdir``, ``rename`` and ``delete`` functions are available for a file or folder: .. code-block:: python api.drive['Holiday Photos'].mkdir('2020') api.drive['Holiday Photos']['2020'].rename('2020_copy') api.drive['Holiday Photos']['2020_copy'].delete() The ``upload`` method can be used to send a file-like object to the iCloud Drive: .. code-block:: python with open('Vacation.jpeg', 'rb') as file_in: api.drive['Holiday Photos'].upload(file_in) It is strongly suggested to open file handles as binary rather than text to prevent decoding errors further down the line. Photo Library ======================= You can access the iCloud Photo Library through the ``photos`` property. .. code-block:: pycon >>> api.photos.all Individual albums are available through the ``albums`` property: .. code-block:: pycon >>> api.photos.albums['Screenshots'] Which you can iterate to access the photo assets. The 'All Photos' album is sorted by `added_date` so the most recently added photos are returned first. All other albums are sorted by `asset_date` (which represents the exif date) : .. code-block:: pycon >>> for photo in api.photos.albums['Screenshots']: print(photo, photo.filename) IMG_6045.JPG 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: .. code-block:: python photo = next(iter(api.photos.albums['Screenshots']), None) 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: .. code-block:: pycon >>> photo.versions.keys() ['medium', 'original', 'thumb'] To download a specific version of the photo asset, pass the version to ``download()``: .. code-block:: python download = photo.download('thumb') with open(photo.versions['thumb']['filename'], 'wb') as thumb_file: thumb_file.write(download.raw.read()) Code samples ============ If you wanna see some code samples see the `code samples file `_. pyicloud-1.0.0/pyicloud/0000755000175100001710000000000014203477505015776 5ustar runnerdocker00000000000000pyicloud-1.0.0/pyicloud/__init__.py0000644000175100001710000000022414203477477020115 0ustar runnerdocker00000000000000"""The pyiCloud library.""" import logging from pyicloud.base import PyiCloudService logging.getLogger(__name__).addHandler(logging.NullHandler()) pyicloud-1.0.0/pyicloud/base.py0000644000175100001710000005246414203477477017305 0ustar runnerdocker00000000000000"""Library base file.""" from uuid import uuid1 import inspect import json import logging from requests import Session from tempfile import gettempdir from os import path, mkdir from re import match import http.cookiejar as cookielib import getpass from pyicloud.exceptions import ( PyiCloudFailedLoginException, PyiCloudAPIResponseException, PyiCloud2SARequiredException, PyiCloudServiceNotActivatedException, ) from pyicloud.services import ( FindMyiPhoneServiceManager, CalendarService, UbiquityService, ContactsService, RemindersService, PhotosService, AccountService, DriveService, ) from pyicloud.utils import get_password_from_keyring LOGGER = logging.getLogger(__name__) HEADER_DATA = { "X-Apple-ID-Account-Country": "account_country", "X-Apple-ID-Session-Id": "session_id", "X-Apple-Session-Token": "session_token", "X-Apple-TwoSV-Trust-Token": "trust_token", "scnt": "scnt", } class PyiCloudPasswordFilter(logging.Filter): """Password log hider.""" def __init__(self, password): super().__init__(password) def filter(self, record): message = record.getMessage() if self.name in message: record.msg = message.replace(self.name, "*" * 8) record.args = [] return True class PyiCloudSession(Session): """iCloud session.""" def __init__(self, service): self.service = service super().__init__() def request(self, method, url, **kwargs): # pylint: disable=arguments-differ # Charge logging to the right service endpoint callee = inspect.stack()[2] module = inspect.getmodule(callee[0]) request_logger = logging.getLogger(module.__name__).getChild("http") if self.service.password_filter not in request_logger.filters: request_logger.addFilter(self.service.password_filter) request_logger.debug("%s %s %s", method, url, kwargs.get("data", "")) has_retried = kwargs.get("retried") kwargs.pop("retried", None) response = super().request(method, url, **kwargs) content_type = response.headers.get("Content-Type", "").split(";")[0] json_mimetypes = ["application/json", "text/json"] for header, value in HEADER_DATA.items(): if response.headers.get(header): session_arg = value self.service.session_data.update( {session_arg: response.headers.get(header)} ) # Save session_data to file with open(self.service.session_path, "w", encoding="utf-8") as outfile: json.dump(self.service.session_data, outfile) LOGGER.debug("Saved session data to file") # Save cookies to file self.cookies.save(ignore_discard=True, ignore_expires=True) LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path) if not response.ok and ( content_type not in json_mimetypes or response.status_code in [421, 450, 500] ): try: # pylint: disable=protected-access fmip_url = self.service._get_webservice_url("findme") if ( has_retried is None and response.status_code in [421, 450, 500] and fmip_url in url ): # Handle re-authentication for Find My iPhone LOGGER.debug("Re-authenticating Find My iPhone service") try: # If 450, authentication requires a full sign in to the account service = None if response.status_code == 450 else "find" self.service.authenticate(True, service) except PyiCloudAPIResponseException: LOGGER.debug("Re-authentication failed") kwargs["retried"] = True return self.request(method, url, **kwargs) except Exception: pass if has_retried is None and response.status_code in [421, 450, 500]: api_error = PyiCloudAPIResponseException( response.reason, response.status_code, retry=True ) request_logger.debug(api_error) kwargs["retried"] = True return self.request(method, url, **kwargs) self._raise_error(response.status_code, response.reason) if content_type not in json_mimetypes: return response try: data = response.json() except: # pylint: disable=bare-except request_logger.warning("Failed to parse response with JSON mimetype") return response request_logger.debug(data) if isinstance(data, dict): reason = data.get("errorMessage") reason = reason or data.get("reason") reason = reason or data.get("errorReason") if not reason and isinstance(data.get("error"), str): reason = data.get("error") if not reason and data.get("error"): reason = "Unknown reason" code = data.get("errorCode") if not code and data.get("serverErrorCode"): code = data.get("serverErrorCode") if reason: self._raise_error(code, reason) return response def _raise_error(self, code, reason): if ( self.service.requires_2sa and reason == "Missing X-APPLE-WEBAUTH-TOKEN cookie" ): raise PyiCloud2SARequiredException(self.service.user["apple_id"]) if code in ("ZONE_NOT_FOUND", "AUTHENTICATION_FAILED"): reason = ( "Please log into https://icloud.com/ to manually " "finish setting up your iCloud service" ) api_error = PyiCloudServiceNotActivatedException(reason, code) LOGGER.error(api_error) raise (api_error) if code == "ACCESS_DENIED": reason = ( reason + ". Please wait a few minutes then try again." "The remote servers might be trying to throttle requests." ) if code in [421, 450, 500]: reason = "Authentication required for Account." api_error = PyiCloudAPIResponseException(reason, code) LOGGER.error(api_error) raise api_error class PyiCloudService: """ 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() """ AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth" HOME_ENDPOINT = "https://www.icloud.com" SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1" def __init__( self, apple_id, password=None, cookie_directory=None, verify=True, client_id=None, with_family=True, ): if password is None: password = get_password_from_keyring(apple_id) self.user = {"accountName": apple_id, "password": password} self.data = {} self.params = {} self.client_id = client_id or ("auth-%s" % str(uuid1()).lower()) self.with_family = with_family self.password_filter = PyiCloudPasswordFilter(password) LOGGER.addFilter(self.password_filter) if cookie_directory: self._cookie_directory = path.expanduser(path.normpath(cookie_directory)) if not path.exists(self._cookie_directory): mkdir(self._cookie_directory, 0o700) else: topdir = path.join(gettempdir(), "pyicloud") self._cookie_directory = path.join(topdir, getpass.getuser()) if not path.exists(topdir): mkdir(topdir, 0o777) if not path.exists(self._cookie_directory): mkdir(self._cookie_directory, 0o700) LOGGER.debug("Using session file %s", self.session_path) self.session_data = {} try: with open(self.session_path, encoding="utf-8") as session_f: self.session_data = json.load(session_f) except: # pylint: disable=bare-except LOGGER.info("Session file does not exist") if self.session_data.get("client_id"): self.client_id = self.session_data.get("client_id") else: self.session_data.update({"client_id": self.client_id}) self.session = PyiCloudSession(self) self.session.verify = verify self.session.headers.update( {"Origin": self.HOME_ENDPOINT, "Referer": "%s/" % self.HOME_ENDPOINT} ) cookiejar_path = self.cookiejar_path self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) if path.exists(cookiejar_path): try: self.session.cookies.load(ignore_discard=True, ignore_expires=True) LOGGER.debug("Read cookies from %s", cookiejar_path) except: # pylint: disable=bare-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.authenticate() self._drive = None self._files = None self._photos = None def authenticate(self, force_refresh=False, service=None): """ Handles authentication, and persists cookies so that subsequent logins will not cause additional e-mails from Apple. """ login_successful = False if self.session_data.get("session_token") and not force_refresh: LOGGER.debug("Checking session token validity") try: self.data = self._validate_token() login_successful = True except PyiCloudAPIResponseException: LOGGER.debug("Invalid authentication token, will log in from scratch.") if not login_successful and service is not None: app = self.data["apps"][service] if "canLaunchWithOneFactor" in app and app["canLaunchWithOneFactor"]: LOGGER.debug( "Authenticating as %s for %s", self.user["accountName"], service ) try: self._authenticate_with_credentials_service(service) login_successful = True except Exception: LOGGER.debug( "Could not log into service. Attempting brand new login." ) if not login_successful: LOGGER.debug("Authenticating as %s", self.user["accountName"]) data = dict(self.user) data["rememberMe"] = True data["trustTokens"] = [] if self.session_data.get("trust_token"): data["trustTokens"] = [self.session_data.get("trust_token")] headers = self._get_auth_headers() if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: self.session.post( "%s/signin" % self.AUTH_ENDPOINT, params={"isRememberMeEnabled": "true"}, data=json.dumps(data), headers=headers, ) except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) from error self._authenticate_with_token() self._webservices = self.data["webservices"] LOGGER.debug("Authentication completed successfully") def _authenticate_with_token(self): """Authenticate using session token.""" data = { "accountCountryCode": self.session_data.get("account_country"), "dsWebAuthToken": self.session_data.get("session_token"), "extended_login": True, "trustToken": self.session_data.get("trust_token", ""), } try: req = self.session.post( "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) ) self.data = req.json() except PyiCloudAPIResponseException as error: msg = "Invalid authentication token." raise PyiCloudFailedLoginException(msg, error) from error def _authenticate_with_credentials_service(self, service): """Authenticate to a specific service using credentials.""" data = { "appName": service, "apple_id": self.user["accountName"], "password": self.user["password"], } try: self.session.post( "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) ) self.data = self._validate_token() except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) from error def _validate_token(self): """Checks if the current access token is still valid.""" LOGGER.debug("Checking session token validity") try: req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") LOGGER.debug("Session token is still valid") return req.json() except PyiCloudAPIResponseException as err: LOGGER.debug("Invalid authentication token") raise err def _get_auth_headers(self, overrides=None): headers = { "Accept": "*/*", "Content-Type": "application/json", "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", "X-Apple-OAuth-Client-Type": "firstPartyAuth", "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", "X-Apple-OAuth-Require-Grant-Code": "true", "X-Apple-OAuth-Response-Mode": "web_message", "X-Apple-OAuth-Response-Type": "code", "X-Apple-OAuth-State": self.client_id, "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", } if overrides: headers.update(overrides) return headers @property def cookiejar_path(self): """Get path for cookiejar file.""" return path.join( self._cookie_directory, "".join([c for c in self.user.get("accountName") if match(r"\w", c)]), ) @property def session_path(self): """Get path for session data file.""" return path.join( self._cookie_directory, "".join([c for c in self.user.get("accountName") if match(r"\w", c)]) + ".session", ) @property def requires_2sa(self): """Returns True if two-step authentication is required.""" return self.data.get("dsInfo", {}).get("hsaVersion", 0) >= 1 and ( self.data.get("hsaChallengeRequired", False) or not self.is_trusted_session ) @property def requires_2fa(self): """Returns True if two-factor authentication is required.""" return self.data["dsInfo"].get("hsaVersion", 0) == 2 and ( self.data.get("hsaChallengeRequired", False) or not self.is_trusted_session ) @property def is_trusted_session(self): """Returns True if the session is trusted.""" return self.data.get("hsaTrustedBrowser", False) @property def trusted_devices(self): """Returns devices trusted for two-step 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 trusted device.""" device.update({"verificationCode": code, "trustBrowser": True}) data = json.dumps(device) try: self.session.post( "%s/validateVerificationCode" % self.SETUP_ENDPOINT, params=self.params, data=data, ) except PyiCloudAPIResponseException as error: if error.code == -21669: # Wrong verification code return False raise self.trust_session() return not self.requires_2sa def validate_2fa_code(self, code): """Verifies a verification code received via Apple's 2FA system (HSA2).""" data = {"securityCode": {"code": code}} headers = self._get_auth_headers({"Accept": "application/json"}) if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: self.session.post( "%s/verify/trusteddevice/securitycode" % self.AUTH_ENDPOINT, data=json.dumps(data), headers=headers, ) except PyiCloudAPIResponseException as error: if error.code == -21669: # Wrong verification code LOGGER.error("Code verification failed.") return False raise LOGGER.debug("Code verification successful.") self.trust_session() return not self.requires_2sa def trust_session(self): """Request session trust to avoid user log in going forward.""" headers = self._get_auth_headers() if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: self.session.get( f"{self.AUTH_ENDPOINT}/2sv/trust", headers=headers, ) self._authenticate_with_token() return True except PyiCloudAPIResponseException: LOGGER.error("Session trust failed.") return False def _get_webservice_url(self, ws_key): """Get webservice URL, raise an exception if not exists.""" if self._webservices.get(ws_key) is None: raise PyiCloudServiceNotActivatedException( "Webservice not available", ws_key ) return self._webservices[ws_key]["url"] @property def devices(self): """Returns all devices.""" service_root = self._get_webservice_url("findme") return FindMyiPhoneServiceManager( service_root, self.session, self.params, self.with_family ) @property def iphone(self): """Returns the iPhone.""" return self.devices[0] @property def account(self): """Gets the 'Account' service.""" service_root = self._get_webservice_url("account") return AccountService(service_root, self.session, self.params) @property def files(self): """Gets the 'File' service.""" if not self._files: service_root = self._get_webservice_url("ubiquity") self._files = UbiquityService(service_root, self.session, self.params) return self._files @property def photos(self): """Gets the 'Photo' service.""" if not self._photos: service_root = self._get_webservice_url("ckdatabasews") self._photos = PhotosService(service_root, self.session, self.params) return self._photos @property def calendar(self): """Gets the 'Calendar' service.""" service_root = self._get_webservice_url("calendar") return CalendarService(service_root, self.session, self.params) @property def contacts(self): """Gets the 'Contacts' service.""" service_root = self._get_webservice_url("contacts") return ContactsService(service_root, self.session, self.params) @property def reminders(self): """Gets the 'Reminders' service.""" service_root = self._get_webservice_url("reminders") return RemindersService(service_root, self.session, self.params) @property def drive(self): """Gets the 'Drive' service.""" if not self._drive: self._drive = DriveService( service_root=self._get_webservice_url("drivews"), document_root=self._get_webservice_url("docws"), session=self.session, params=self.params, ) return self._drive def __str__(self): return f"iCloud API: {self.user.get('apple_id')}" def __repr__(self): return f"<{self}>" pyicloud-1.0.0/pyicloud/cmdline.py0000644000175100001710000002624414203477477020003 0ustar runnerdocker00000000000000#! /usr/bin/env python """ A Command Line Wrapper to allow easy use of pyicloud for command line scripts, and related. """ import argparse import pickle import sys from click import confirm from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException 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. """ with open(filename, "wb") as pickle_file: pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) 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 = 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) if api.requires_2fa: # fmt: off print( "\nTwo-step authentication required.", "\nPlease enter validation code" ) # fmt: on code = input("(string) --> ") if not api.validate_2fa_code(code): print("Failed to verify verification code") sys.exit(1) print("") elif api.requires_2sa: # fmt: off print( "\nTwo-step authentication required.", "\nYour trusted devices are:" ) # fmt: on devices = api.trusted_devices for i, device in enumerate(devices): print( " %s: %s" % ( i, device.get( "deviceName", "SMS to %s" % device.get("phoneNumber") ), ) ) print("\nWhich device would you like to use?") device = int(input("(number) --> ")) device = devices[device] if not api.send_verification_code(device): print("Failed to send verification code") sys.exit(1) print("\nPlease enter validation code") code = input("(string) --> ") if not api.validate_verification_code(device, code): print("Failed to verify verification code") sys.exit(1) print("") break except PyiCloudFailedLoginException as err: # 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) from err 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 key in contents: print("%20s - %s" % (key, contents[key])) 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, ) ) sys.exit(0) if __name__ == "__main__": main() pyicloud-1.0.0/pyicloud/exceptions.py0000644000175100001710000000233514203477477020544 0ustar runnerdocker00000000000000"""Library exceptions.""" class PyiCloudException(Exception): """Generic iCloud exception.""" pass # API class PyiCloudAPIResponseException(PyiCloudException): """iCloud response exception.""" def __init__(self, reason, code=None, retry=False): self.reason = reason self.code = code message = reason or "" if code: message += " (%s)" % code if retry: message += ". Retrying ..." super().__init__(message) class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException): """iCloud service not activated exception.""" pass # Login class PyiCloudFailedLoginException(PyiCloudException): """iCloud failed login exception.""" pass class PyiCloud2SARequiredException(PyiCloudException): """iCloud 2SA required exception.""" def __init__(self, apple_id): message = "Two-step authentication required for account: %s" % apple_id super().__init__(message) class PyiCloudNoStoredPasswordAvailableException(PyiCloudException): """iCloud no stored password exception.""" pass # Webservice specific class PyiCloudNoDevicesException(PyiCloudException): """iCloud no device exception.""" pass pyicloud-1.0.0/pyicloud/services/0000755000175100001710000000000014203477505017621 5ustar runnerdocker00000000000000pyicloud-1.0.0/pyicloud/services/__init__.py0000644000175100001710000000071514203477477021745 0ustar runnerdocker00000000000000"""Services.""" from 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 from pyicloud.services.drive import DriveService pyicloud-1.0.0/pyicloud/services/account.py0000644000175100001710000002254314203477477021645 0ustar runnerdocker00000000000000"""Account service.""" from collections import OrderedDict from pyicloud.utils import underscore_to_camelcase class AccountService: """The 'Account' iCloud service.""" def __init__(self, service_root, session, params): self.session = session self.params = params self._service_root = service_root self._devices = [] self._family = [] self._storage = None self._acc_endpoint = "%s/setup/web" % self._service_root self._acc_devices_url = "%s/device/getDevices" % self._acc_endpoint self._acc_family_details_url = "%s/family/getFamilyDetails" % self._acc_endpoint self._acc_family_member_photo_url = ( "%s/family/getMemberPhoto" % self._acc_endpoint ) self._acc_storage_url = "https://setup.icloud.com/setup/ws/1/storageUsageInfo" @property def devices(self): """Returns current paired devices.""" if not self._devices: req = self.session.get(self._acc_devices_url, params=self.params) response = req.json() for device_info in response["devices"]: self._devices.append(AccountDevice(device_info)) return self._devices @property def family(self): """Returns family members.""" if not self._family: req = self.session.get(self._acc_family_details_url, params=self.params) response = req.json() for member_info in response["familyMembers"]: self._family.append( FamilyMember( member_info, self.session, self.params, self._acc_family_member_photo_url, ) ) return self._family @property def storage(self): """Returns storage infos.""" if not self._storage: req = self.session.get(self._acc_storage_url, params=self.params) response = req.json() self._storage = AccountStorage(response) return self._storage def __str__(self): return "{{devices: {}, family: {}, storage: {} bytes free}}".format( len(self.devices), len(self.family), self.storage.usage.available_storage_in_bytes, ) def __repr__(self): return f"<{type(self).__name__}: {self}>" class AccountDevice(dict): """Account device.""" def __getattr__(self, key): return self[underscore_to_camelcase(key)] def __str__(self): return f"{{model: {self.model_display_name}, name: {self.name}}}" def __repr__(self): return f"<{type(self).__name__}: {self}>" class FamilyMember: """A family member.""" def __init__(self, member_info, session, params, acc_family_member_photo_url): self._attrs = member_info self._session = session self._params = params self._acc_family_member_photo_url = acc_family_member_photo_url @property def last_name(self): """Gets the last name.""" return self._attrs.get("lastName") @property def dsid(self): """Gets the dsid.""" return self._attrs.get("dsid") @property def original_invitation_email(self): """Gets the original invitation.""" return self._attrs.get("originalInvitationEmail") @property def full_name(self): """Gets the full name.""" return self._attrs.get("fullName") @property def age_classification(self): """Gets the age classification.""" return self._attrs.get("ageClassification") @property def apple_id_for_purchases(self): """Gets the apple id for purchases.""" return self._attrs.get("appleIdForPurchases") @property def apple_id(self): """Gets the apple id.""" return self._attrs.get("appleId") @property def family_id(self): """Gets the family id.""" return self._attrs.get("familyId") @property def first_name(self): """Gets the first name.""" return self._attrs.get("firstName") @property def has_parental_privileges(self): """Has parental privileges.""" return self._attrs.get("hasParentalPrivileges") @property def has_screen_time_enabled(self): """Has screen time enabled.""" return self._attrs.get("hasScreenTimeEnabled") @property def has_ask_to_buy_enabled(self): """Has to ask for buying.""" return self._attrs.get("hasAskToBuyEnabled") @property def has_share_purchases_enabled(self): """Has share purshases.""" return self._attrs.get("hasSharePurchasesEnabled") @property def share_my_location_enabled_family_members(self): """Has share my location with family.""" return self._attrs.get("shareMyLocationEnabledFamilyMembers") @property def has_share_my_location_enabled(self): """Has share my location.""" return self._attrs.get("hasShareMyLocationEnabled") @property def dsid_for_purchases(self): """Gets the dsid for purchases.""" return self._attrs.get("dsidForPurchases") def get_photo(self): """Returns the photo.""" params_photo = dict(self._params) params_photo.update({"memberId": self.dsid}) return self._session.get( self._acc_family_member_photo_url, params=params_photo, stream=True ) def __getitem__(self, key): if self._attrs.get(key): return self._attrs[key] return getattr(self, key) def __str__(self): return "{{name: {}, age_classification: {}}}".format( self.full_name, self.age_classification, ) def __repr__(self): return f"<{type(self).__name__}: {self}>" class AccountStorageUsageForMedia: """Storage used for a specific media type into the account.""" def __init__(self, usage_data): self.usage_data = usage_data @property def key(self): """Gets the key.""" return self.usage_data["mediaKey"] @property def label(self): """Gets the label.""" return self.usage_data["displayLabel"] @property def color(self): """Gets the HEX color.""" return self.usage_data["displayColor"] @property def usage_in_bytes(self): """Gets the usage in bytes.""" return self.usage_data["usageInBytes"] def __str__(self): return f"{{key: {self.key}, usage: {self.usage_in_bytes} bytes}}" def __repr__(self): return f"<{type(self).__name__}: {self}>" class AccountStorageUsage: """Storage used for a specific media type into the account.""" def __init__(self, usage_data, quota_data): self.usage_data = usage_data self.quota_data = quota_data @property def comp_storage_in_bytes(self): """Gets the comp storage in bytes.""" return self.usage_data["compStorageInBytes"] @property def used_storage_in_bytes(self): """Gets the used storage in bytes.""" return self.usage_data["usedStorageInBytes"] @property def used_storage_in_percent(self): """Gets the used storage in percent.""" return round(self.used_storage_in_bytes * 100 / self.total_storage_in_bytes, 2) @property def available_storage_in_bytes(self): """Gets the available storage in bytes.""" return self.total_storage_in_bytes - self.used_storage_in_bytes @property def available_storage_in_percent(self): """Gets the available storage in percent.""" return round( self.available_storage_in_bytes * 100 / self.total_storage_in_bytes, 2 ) @property def total_storage_in_bytes(self): """Gets the total storage in bytes.""" return self.usage_data["totalStorageInBytes"] @property def commerce_storage_in_bytes(self): """Gets the commerce storage in bytes.""" return self.usage_data["commerceStorageInBytes"] @property def quota_over(self): """Gets the over quota.""" return self.quota_data["overQuota"] @property def quota_tier_max(self): """Gets the max tier quota.""" return self.quota_data["haveMaxQuotaTier"] @property def quota_almost_full(self): """Gets the almost full quota.""" return self.quota_data["almost-full"] @property def quota_paid(self): """Gets the paid quota.""" return self.quota_data["paidQuota"] def __str__(self): return "{}% used of {} bytes".format( self.used_storage_in_percent, self.total_storage_in_bytes, ) def __repr__(self): return f"<{type(self).__name__}: {self}>" class AccountStorage: """Storage of the account.""" def __init__(self, storage_data): self.usage = AccountStorageUsage( storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus") ) self.usages_by_media = OrderedDict() for usage_media in storage_data.get("storageUsageByMedia"): self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia( usage_media ) def __str__(self): return f"{{usage: {self.usage}, usages_by_media: {self.usages_by_media}}}" def __repr__(self): return f"<{type(self).__name__}: {self}>" pyicloud-1.0.0/pyicloud/services/calendar.py0000644000175100001710000000606414203477477021762 0ustar runnerdocker00000000000000"""Calendar service.""" from datetime import datetime from calendar import monthrange from tzlocal import get_localzone_name class CalendarService: """ 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 = f"{self._calendar_endpoint}/eventdetail" self._calendars = "%s/startup" % self._calendar_endpoint self.response = {} 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_name()}) url = f"{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_name(), "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.get("Event") def calendars(self): """ Retrieves calendars of this month. """ today = datetime.today() first_day, last_day = monthrange(today.year, today.month) from_dt = datetime(today.year, today.month, first_day) to_dt = datetime(today.year, today.month, last_day) params = dict(self.params) params.update( { "lang": "en-us", "usertz": get_localzone_name(), "startDate": from_dt.strftime("%Y-%m-%d"), "endDate": to_dt.strftime("%Y-%m-%d"), } ) req = self.session.get(self._calendars, params=params) self.response = req.json() return self.response["Collection"] pyicloud-1.0.0/pyicloud/services/contacts.py0000644000175100001710000000321014203477477022015 0ustar runnerdocker00000000000000"""Contacts service.""" class ContactsService: """ 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_next_url = "%s/contacts" % self._contacts_endpoint self._contacts_changeset_url = "%s/changeset" % self._contacts_endpoint self.response = {} def refresh_client(self): """ 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_next = dict(params_contacts) params_next.update( { "prefToken": self.response["prefToken"], "syncToken": self.response["syncToken"], "limit": "0", "offset": "0", } ) req = self.session.get(self._contacts_next_url, params=params_next) self.response = req.json() def all(self): """ Retrieves all contacts. """ self.refresh_client() return self.response.get("contacts") pyicloud-1.0.0/pyicloud/services/drive.py0000644000175100001710000002775114203477477021330 0ustar runnerdocker00000000000000"""Drive service.""" from datetime import datetime, timedelta import json import logging import io import mimetypes import os import time from re import search from requests import Response from pyicloud.exceptions import PyiCloudAPIResponseException LOGGER = logging.getLogger(__name__) class DriveService: """The 'Drive' iCloud service.""" def __init__(self, service_root, document_root, session, params): self._service_root = service_root self._document_root = document_root self.session = session self.params = dict(params) self._root = None def _get_token_from_cookie(self): for cookie in self.session.cookies: if cookie.name == "X-APPLE-WEBAUTH-VALIDATE": match = search(r"\bt=([^:]+)", cookie.value) if match is None: raise Exception("Can't extract token from %r" % cookie.value) return {"token": match.group(1)} raise Exception("Token cookie not found") def get_node_data(self, node_id): """Returns the node data.""" request = self.session.post( self._service_root + "/retrieveItemDetailsInFolders", params=self.params, data=json.dumps( [ { "drivewsid": "FOLDER::com.apple.CloudDocs::%s" % node_id, "partialData": False, } ] ), ) self._raise_if_error(request) return request.json()[0] def get_file(self, file_id, **kwargs): """Returns iCloud Drive file.""" file_params = dict(self.params) file_params.update({"document_id": file_id}) response = self.session.get( self._document_root + "/ws/com.apple.CloudDocs/download/by_id", params=file_params, ) self._raise_if_error(response) response_json = response.json() package_token = response_json.get("package_token") data_token = response_json.get("data_token") if data_token and data_token.get("url"): return self.session.get(data_token["url"], params=self.params, **kwargs) if package_token and package_token.get("url"): return self.session.get(package_token["url"], params=self.params, **kwargs) raise KeyError("'data_token' nor 'package_token'") def get_app_data(self): """Returns the app library (previously ubiquity).""" request = self.session.get( self._service_root + "/retrieveAppLibraries", params=self.params ) self._raise_if_error(request) return request.json()["items"] def _get_upload_contentws_url(self, file_object): """Get the contentWS endpoint URL to add a new file.""" content_type = mimetypes.guess_type(file_object.name)[0] if content_type is None: content_type = "" # Get filesize from file object orig_pos = file_object.tell() file_object.seek(0, os.SEEK_END) file_size = file_object.tell() file_object.seek(orig_pos, os.SEEK_SET) file_params = self.params file_params.update(self._get_token_from_cookie()) request = self.session.post( self._document_root + "/ws/com.apple.CloudDocs/upload/web", params=file_params, headers={"Content-Type": "text/plain"}, data=json.dumps( { "filename": file_object.name, "type": "FILE", "content_type": content_type, "size": file_size, } ), ) self._raise_if_error(request) return (request.json()[0]["document_id"], request.json()[0]["url"]) def _update_contentws(self, folder_id, sf_info, document_id, file_object): data = { "data": { "signature": sf_info["fileChecksum"], "wrapping_key": sf_info["wrappingKey"], "reference_signature": sf_info["referenceChecksum"], "size": sf_info["size"], }, "command": "add_file", "create_short_guid": True, "document_id": document_id, "path": { "starting_document_id": folder_id, "path": file_object.name, }, "allow_conflict": True, "file_flags": { "is_writable": True, "is_executable": False, "is_hidden": False, }, "mtime": int(time.time() * 1000), "btime": int(time.time() * 1000), } # Add the receipt if we have one. Will be absent for 0-sized files if sf_info.get("receipt"): data["data"].update({"receipt": sf_info["receipt"]}) request = self.session.post( self._document_root + "/ws/com.apple.CloudDocs/update/documents", params=self.params, headers={"Content-Type": "text/plain"}, data=json.dumps(data), ) self._raise_if_error(request) return request.json() def send_file(self, folder_id, file_object): """Send new file to iCloud Drive.""" document_id, content_url = self._get_upload_contentws_url(file_object) request = self.session.post(content_url, files={file_object.name: file_object}) self._raise_if_error(request) content_response = request.json()["singleFile"] self._update_contentws(folder_id, content_response, document_id, file_object) def create_folders(self, parent, name): """Creates a new iCloud Drive folder""" request = self.session.post( self._service_root + "/createFolders", params=self.params, headers={"Content-Type": "text/plain"}, data=json.dumps( { "destinationDrivewsId": parent, "folders": [ { "clientId": self.params["clientId"], "name": name, } ], } ), ) self._raise_if_error(request) return request.json() def rename_items(self, node_id, etag, name): """Renames an iCloud Drive node""" request = self.session.post( self._service_root + "/renameItems", params=self.params, data=json.dumps( { "items": [ { "drivewsid": node_id, "etag": etag, "name": name, } ], } ), ) self._raise_if_error(request) return request.json() def move_items_to_trash(self, node_id, etag): """Moves an iCloud Drive node to the trash bin""" request = self.session.post( self._service_root + "/moveItemsToTrash", params=self.params, data=json.dumps( { "items": [ { "drivewsid": node_id, "etag": etag, "clientId": self.params["clientId"], } ], } ), ) self._raise_if_error(request) return request.json() @property def root(self): """Returns the root node.""" if not self._root: self._root = DriveNode(self, self.get_node_data("root")) return self._root def __getattr__(self, attr): return getattr(self.root, attr) def __getitem__(self, key): return self.root[key] def _raise_if_error(self, response): # pylint: disable=no-self-use if not response.ok: api_error = PyiCloudAPIResponseException( response.reason, response.status_code ) LOGGER.error(api_error) raise api_error class DriveNode: """Drive node.""" def __init__(self, conn, data): self.data = data self.connection = conn self._children = None @property def name(self): """Gets the node name.""" if "extension" in self.data: return "{}.{}".format(self.data["name"], self.data["extension"]) return self.data["name"] @property def type(self): """Gets the node type.""" node_type = self.data.get("type") return node_type and node_type.lower() def get_children(self): """Gets the node children.""" if not self._children: if "items" not in self.data: self.data.update(self.connection.get_node_data(self.data["docwsid"])) if "items" not in self.data: raise KeyError("No items in folder, status: %s" % self.data["status"]) self._children = [ DriveNode(self.connection, item_data) for item_data in self.data["items"] ] return self._children @property def size(self): """Gets the node size.""" size = self.data.get("size") # Folder does not have size if not size: return None return int(size) @property def date_changed(self): """Gets the node changed date (in UTC).""" return _date_to_utc(self.data.get("dateChanged")) # Folder does not have date @property def date_modified(self): """Gets the node modified date (in UTC).""" return _date_to_utc(self.data.get("dateModified")) # Folder does not have date @property def date_last_open(self): """Gets the node last open date (in UTC).""" return _date_to_utc(self.data.get("lastOpenTime")) # Folder does not have date def open(self, **kwargs): """Gets the node file.""" # iCloud returns 400 Bad Request for 0-byte files if self.data["size"] == 0: response = Response() response.raw = io.BytesIO() return response return self.connection.get_file(self.data["docwsid"], **kwargs) def upload(self, file_object, **kwargs): """Upload a new file.""" return self.connection.send_file(self.data["docwsid"], file_object, **kwargs) def dir(self): """Gets the node list of directories.""" if self.type == "file": return None return [child.name for child in self.get_children()] def mkdir(self, folder): """Create a new directory directory.""" return self.connection.create_folders(self.data["drivewsid"], folder) def rename(self, name): """Rename an iCloud Drive item.""" return self.connection.rename_items( self.data["drivewsid"], self.data["etag"], name ) def delete(self): """Delete an iCloud Drive item.""" return self.connection.move_items_to_trash( self.data["drivewsid"], self.data["etag"] ) def get(self, name): """Gets the node child.""" if self.type == "file": return None return [child for child in self.get_children() if child.name == name][0] def __getitem__(self, key): try: return self.get(key) except IndexError as i: raise KeyError(f"No child named '{key}' exists") from i def __str__(self): return rf"\{type: {self.type}, name: {self.name}\}" def __repr__(self): return f"<{type(self).__name__}: {str(self)}>" def _date_to_utc(date): if not date: return None # jump through hoops to return time in UTC rather than California time match = search(r"^(.+?)([\+\-]\d+):(\d\d)$", date) if not match: # Already in UTC return datetime.strptime(date, "%Y-%m-%dT%H:%M:%SZ") base = datetime.strptime(match.group(1), "%Y-%m-%dT%H:%M:%S") diff = timedelta(hours=int(match.group(2)), minutes=int(match.group(3))) return base - diff pyicloud-1.0.0/pyicloud/services/findmyiphone.py0000644000175100001710000001341714203477477022702 0ustar runnerdocker00000000000000"""Find my iPhone service.""" import json from pyicloud.exceptions import PyiCloudNoDevicesException class FindMyiPhoneServiceManager: """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, with_family=False): self.session = session self.params = params self.with_family = with_family fmip_endpoint = "%s/fmipservice/client/web" % service_root self._fmip_refresh_url = "%s/refreshClient" % fmip_endpoint self._fmip_sound_url = "%s/playSound" % fmip_endpoint self._fmip_message_url = "%s/sendMessage" % fmip_endpoint self._fmip_lost_url = "%s/lostDevice" % 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": self.with_family, "shouldLocate": True, "selectedDevice": "all", "deviceListVersion": 1, } } ), ) 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): key = list(self.keys())[key] return self._devices[key] def __getattr__(self, attr): return getattr(self._devices, attr) def __str__(self): return f"{self._devices}" def __repr__(self): return f"{self}" class AppleDevice: """Apple device.""" 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): """Updates the device data.""" self.content = data def location(self): """Updates the device location.""" self.manager.refresh_client() return self.content["location"] def status(self, additional=[]): # pylint: disable=dangerous-default-value """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, "clientContext": {"fmly": True}, } ) 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): """Gets the device data.""" return self.content def __getitem__(self, key): return self.content[key] def __getattr__(self, attr): return getattr(self.content, attr) def __str__(self): return f"{self['deviceDisplayName']}: {self['name']}" def __repr__(self): return f"" pyicloud-1.0.0/pyicloud/services/photos.py0000644000175100001710000005203714203477477021526 0ustar runnerdocker00000000000000"""Photo service.""" import json import base64 from urllib.parse import urlencode from datetime import datetime, timezone from pyicloud.exceptions import PyiCloudServiceNotActivatedException class PhotosService: """The 'Photos' iCloud service.""" SMART_FOLDERS = { "All Photos": { "obj_type": "CPLAssetByAddedDate", "list_type": "CPLAssetAndMasterByAddedDate", "direction": "ASCENDING", "query_filter": None, }, "Time-lapse": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Timelapse", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "TIMELAPSE"}, } ], }, "Videos": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Video", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "VIDEO"}, } ], }, "Slo-mo": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Slomo", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "SLOMO"}, } ], }, "Bursts": { "obj_type": "CPLAssetBurstStackAssetByAssetDate", "list_type": "CPLBurstStackAssetAndMasterByAssetDate", "direction": "ASCENDING", "query_filter": None, }, "Favorites": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Favorite", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "FAVORITE"}, } ], }, "Panoramas": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Panorama", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "PANORAMA"}, } ], }, "Screenshots": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Screenshot", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "SCREENSHOT"}, } ], }, "Live": { "obj_type": "CPLAssetInSmartAlbumByAssetDate:Live", "list_type": "CPLAssetAndMasterInSmartAlbumByAssetDate", "direction": "ASCENDING", "query_filter": [ { "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "LIVE"}, } ], }, "Recently Deleted": { "obj_type": "CPLAssetDeletedByExpungedDate", "list_type": "CPLAssetAndMasterDeletedByExpungedDate", "direction": "ASCENDING", "query_filter": None, }, "Hidden": { "obj_type": "CPLAssetHiddenByAssetDate", "list_type": "CPLAssetAndMasterHiddenByAssetDate", "direction": "ASCENDING", "query_filter": None, }, } def __init__(self, service_root, session, params): self.session = session self.params = dict(params) self._service_root = service_root self.service_endpoint = ( "%s/database/1/com.apple.photos.cloud/production/private" % self._service_root ) self._albums = None self.params.update({"remapEnums": True, "getCurrentSyncToken": True}) url = f"{self.service_endpoint}/records/query?{urlencode(self.params)}" json_data = ( '{"query":{"recordType":"CheckIndexingState"},' '"zoneID":{"zoneName":"PrimarySync"}}' ) request = self.session.post( url, data=json_data, headers={"Content-type": "text/plain"} ) response = request.json() indexing_state = response["records"][0]["fields"]["state"]["value"] if indexing_state != "FINISHED": raise PyiCloudServiceNotActivatedException( "iCloud Photo Library not finished indexing. " "Please try again in a few minutes." ) # TODO: Does syncToken ever change? # pylint: disable=fixme # self.params.update({ # 'syncToken': response['syncToken'], # 'clientInstanceId': self.params.pop('clientId') # }) self._photo_assets = {} @property def albums(self): """Returns photo albums.""" if not self._albums: self._albums = { name: PhotoAlbum(self, name, **props) for (name, props) in self.SMART_FOLDERS.items() } for folder in self._fetch_folders(): # Skiping albums having null name, that can happen sometime if "albumNameEnc" not in folder["fields"]: continue # TODO: Handle subfolders # pylint: disable=fixme if folder["recordName"] == "----Root-Folder----" or ( folder["fields"].get("isDeleted") and folder["fields"]["isDeleted"]["value"] ): continue folder_id = folder["recordName"] folder_obj_type = ( "CPLContainerRelationNotDeletedByAssetDate:%s" % folder_id ) folder_name = base64.b64decode( folder["fields"]["albumNameEnc"]["value"] ).decode("utf-8") query_filter = [ { "fieldName": "parentId", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": folder_id}, } ] album = PhotoAlbum( self, folder_name, "CPLContainerRelationLiveByAssetDate", folder_obj_type, "ASCENDING", query_filter, ) self._albums[folder_name] = album return self._albums def _fetch_folders(self): url = f"{self.service_endpoint}/records/query?{urlencode(self.params)}" json_data = ( '{"query":{"recordType":"CPLAlbumByPositionLive"},' '"zoneID":{"zoneName":"PrimarySync"}}' ) request = self.session.post( url, data=json_data, headers={"Content-type": "text/plain"} ) response = request.json() return response["records"] @property def all(self): """Returns all photos.""" return self.albums["All Photos"] class PhotoAlbum: """A photo album.""" def __init__( self, service, name, list_type, obj_type, direction, query_filter=None, page_size=100, ): self.name = name self.service = service self.list_type = list_type self.obj_type = obj_type self.direction = direction self.query_filter = query_filter self.page_size = page_size self._len = None @property def title(self): """Gets the album name.""" return self.name def __iter__(self): return self.photos def __len__(self): if self._len is None: url = "{}/internal/records/query/batch?{}".format( self.service.service_endpoint, urlencode(self.service.params), ) request = self.service.session.post( url, data=json.dumps( { "batch": [ { "resultsLimit": 1, "query": { "filterBy": { "fieldName": "indexCountID", "fieldValue": { "type": "STRING_LIST", "value": [self.obj_type], }, "comparator": "IN", }, "recordType": "HyperionIndexCountLookup", }, "zoneWide": True, "zoneID": {"zoneName": "PrimarySync"}, } ] } ), headers={"Content-type": "text/plain"}, ) response = request.json() self._len = response["batch"][0]["records"][0]["fields"]["itemCount"][ "value" ] return self._len @property def photos(self): """Returns the album photos.""" if self.direction == "DESCENDING": offset = len(self) - 1 else: offset = 0 while True: url = ("%s/records/query?" % self.service.service_endpoint) + urlencode( self.service.params ) request = self.service.session.post( url, data=json.dumps( self._list_query_gen( offset, self.list_type, self.direction, self.query_filter ) ), headers={"Content-type": "text/plain"}, ) response = request.json() asset_records = {} master_records = [] for rec in response["records"]: if rec["recordType"] == "CPLAsset": master_id = rec["fields"]["masterRef"]["value"]["recordName"] asset_records[master_id] = rec elif rec["recordType"] == "CPLMaster": master_records.append(rec) master_records_len = len(master_records) if master_records_len: if self.direction == "DESCENDING": offset = offset - master_records_len else: offset = offset + master_records_len for master_record in master_records: record_name = master_record["recordName"] yield PhotoAsset( self.service, master_record, asset_records[record_name] ) else: break def _list_query_gen(self, offset, list_type, direction, query_filter=None): query = { "query": { "filterBy": [ { "fieldName": "startRank", "fieldValue": {"type": "INT64", "value": offset}, "comparator": "EQUALS", }, { "fieldName": "direction", "fieldValue": {"type": "STRING", "value": direction}, "comparator": "EQUALS", }, ], "recordType": list_type, }, "resultsLimit": self.page_size * 2, "desiredKeys": [ "resJPEGFullWidth", "resJPEGFullHeight", "resJPEGFullFileType", "resJPEGFullFingerprint", "resJPEGFullRes", "resJPEGLargeWidth", "resJPEGLargeHeight", "resJPEGLargeFileType", "resJPEGLargeFingerprint", "resJPEGLargeRes", "resJPEGMedWidth", "resJPEGMedHeight", "resJPEGMedFileType", "resJPEGMedFingerprint", "resJPEGMedRes", "resJPEGThumbWidth", "resJPEGThumbHeight", "resJPEGThumbFileType", "resJPEGThumbFingerprint", "resJPEGThumbRes", "resVidFullWidth", "resVidFullHeight", "resVidFullFileType", "resVidFullFingerprint", "resVidFullRes", "resVidMedWidth", "resVidMedHeight", "resVidMedFileType", "resVidMedFingerprint", "resVidMedRes", "resVidSmallWidth", "resVidSmallHeight", "resVidSmallFileType", "resVidSmallFingerprint", "resVidSmallRes", "resSidecarWidth", "resSidecarHeight", "resSidecarFileType", "resSidecarFingerprint", "resSidecarRes", "itemType", "dataClassType", "filenameEnc", "originalOrientation", "resOriginalWidth", "resOriginalHeight", "resOriginalFileType", "resOriginalFingerprint", "resOriginalRes", "resOriginalAltWidth", "resOriginalAltHeight", "resOriginalAltFileType", "resOriginalAltFingerprint", "resOriginalAltRes", "resOriginalVidComplWidth", "resOriginalVidComplHeight", "resOriginalVidComplFileType", "resOriginalVidComplFingerprint", "resOriginalVidComplRes", "isDeleted", "isExpunged", "dateExpunged", "remappedRef", "recordName", "recordType", "recordChangeTag", "masterRef", "adjustmentRenderType", "assetDate", "addedDate", "isFavorite", "isHidden", "orientation", "duration", "assetSubtype", "assetSubtypeV2", "assetHDRType", "burstFlags", "burstFlagsExt", "burstId", "captionEnc", "locationEnc", "locationV2Enc", "locationLatitude", "locationLongitude", "adjustmentType", "timeZoneOffset", "vidComplDurValue", "vidComplDurScale", "vidComplDispValue", "vidComplDispScale", "vidComplVisibilityState", "customRenderedValue", "containerId", "itemId", "position", "isKeyAsset", ], "zoneID": {"zoneName": "PrimarySync"}, } if query_filter: query["query"]["filterBy"].extend(query_filter) return query def __str__(self): return self.title def __repr__(self): return f"<{type(self).__name__}: '{self}'>" class PhotoAsset: """A photo.""" def __init__(self, service, master_record, asset_record): self._service = service self._master_record = master_record self._asset_record = asset_record self._versions = None PHOTO_VERSION_LOOKUP = { "original": "resOriginal", "medium": "resJPEGMed", "thumb": "resJPEGThumb", } VIDEO_VERSION_LOOKUP = { "original": "resOriginal", "medium": "resVidMed", "thumb": "resVidSmall", } @property def id(self): """Gets the photo id.""" return self._master_record["recordName"] @property def filename(self): """Gets the photo file name.""" return base64.b64decode( self._master_record["fields"]["filenameEnc"]["value"] ).decode("utf-8") @property def size(self): """Gets the photo size.""" return self._master_record["fields"]["resOriginalRes"]["value"]["size"] @property def created(self): """Gets the photo created date.""" return self.asset_date @property def asset_date(self): """Gets the photo asset date.""" try: return datetime.utcfromtimestamp( self._asset_record["fields"]["assetDate"]["value"] / 1000.0 ).replace(tzinfo=timezone.utc) except KeyError: return datetime.utcfromtimestamp(0).replace(tzinfo=timezone.utc) @property def added_date(self): """Gets the photo added date.""" return datetime.utcfromtimestamp( self._asset_record["fields"]["addedDate"]["value"] / 1000.0 ).replace(tzinfo=timezone.utc) @property def dimensions(self): """Gets the photo dimensions.""" return ( self._master_record["fields"]["resOriginalWidth"]["value"], self._master_record["fields"]["resOriginalHeight"]["value"], ) @property def versions(self): """Gets the photo versions.""" if not self._versions: self._versions = {} if "resVidSmallRes" in self._master_record["fields"]: typed_version_lookup = self.VIDEO_VERSION_LOOKUP else: typed_version_lookup = self.PHOTO_VERSION_LOOKUP for key, prefix in typed_version_lookup.items(): if "%sRes" % prefix in self._master_record["fields"]: fields = self._master_record["fields"] version = {"filename": self.filename} width_entry = fields.get("%sWidth" % prefix) if width_entry: version["width"] = width_entry["value"] else: version["width"] = None height_entry = fields.get("%sHeight" % prefix) if height_entry: version["height"] = height_entry["value"] else: version["height"] = None size_entry = fields.get("%sRes" % prefix) if size_entry: version["size"] = size_entry["value"]["size"] version["url"] = size_entry["value"]["downloadURL"] else: version["size"] = None version["url"] = None type_entry = fields.get("%sFileType" % prefix) if type_entry: version["type"] = type_entry["value"] else: version["type"] = None self._versions[key] = version return self._versions def download(self, version="original", **kwargs): """Returns the photo file.""" if version not in self.versions: return None return self._service.session.get( self.versions[version]["url"], stream=True, **kwargs ) def delete(self): """Deletes the photo.""" json_data = ( '{"query":{"recordType":"CheckIndexingState"},' '"zoneID":{"zoneName":"PrimarySync"}}' ) json_data = ( '{"operations":[{' '"operationType":"update",' '"record":{' '"recordName":"%s",' '"recordType":"%s",' '"recordChangeTag":"%s",' '"fields":{"isDeleted":{"value":1}' "}}}]," '"zoneID":{' '"zoneName":"PrimarySync"' '},"atomic":true}' % ( self._asset_record["recordName"], self._asset_record["recordType"], self._master_record["recordChangeTag"], ) ) endpoint = self._service.service_endpoint params = urlencode(self._service.params) url = f"{endpoint}/records/modify?{params}" return self._service.session.post( url, data=json_data, headers={"Content-type": "text/plain"} ) def __repr__(self): return f"<{type(self).__name__}: id={self.id}>" pyicloud-1.0.0/pyicloud/services/reminders.py0000644000175100001710000000757014203477477022204 0ustar runnerdocker00000000000000"""Reminders service.""" from datetime import datetime import time import uuid import json from tzlocal import get_localzone_name class RemindersService: """The 'Reminders' iCloud service.""" 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): """Refresh data.""" params_reminders = dict(self._params) params_reminders.update( {"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone_name()} ) # Open reminders req = self.session.get( self._service_root + "/rd/startup", params=params_reminders ) data = req.json() self.lists = {} self.collections = {} for collection in data["Collections"]: temp = [] self.collections[collection["title"]] = { "guid": collection["guid"], "ctag": collection["ctag"], } for reminder in data["Reminders"]: if reminder["pGuid"] != collection["guid"]: continue if reminder.get("dueDate"): due = datetime( reminder["dueDate"][1], reminder["dueDate"][2], reminder["dueDate"][3], reminder["dueDate"][4], reminder["dueDate"][5], ) else: due = None temp.append( { "title": reminder["title"], "desc": reminder.get("description"), "due": due, } ) self.lists[collection["title"]] = temp def post(self, title, description="", collection=None, due_date=None): """Adds a new reminder.""" 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_name()} ) due_dates = None if due_date: due_dates = [ int(str(due_date.year) + str(due_date.month) + str(due_date.day)), due_date.year, due_date.month, due_date.day, due_date.hour, due_date.minute, ] 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": due_dates, "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-1.0.0/pyicloud/services/ubiquity.py0000644000175100001710000000610414203477477022057 0ustar runnerdocker00000000000000"""File service.""" from datetime import datetime class UbiquityService: """The 'Ubiquity' iCloud service.""" def __init__(self, service_root, session, params): self.session = session self.params = params self._root = None self._node_url = service_root + "/ws/%s/%s/%s" @property 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, node_id, variant="item"): """Returns a node URL.""" return self._node_url % (self.params["dsid"], variant, node_id) def get_node(self, node_id): """Returns a node.""" request = self.session.get(self.get_node_url(node_id)) return UbiquityNode(self, request.json()) def get_children(self, node_id): """Returns a node children.""" request = self.session.get(self.get_node_url(node_id, "parent")) items = request.json()["item_list"] return [UbiquityNode(self, item) for item in items] def get_file(self, node_id, **kwargs): """Returns a node file.""" return self.session.get(self.get_node_url(node_id, "file"), **kwargs) def __getattr__(self, attr): return getattr(self.root, attr) def __getitem__(self, key): return self.root[key] class UbiquityNode: """Ubiquity node.""" def __init__(self, conn, data): self.data = data self.connection = conn self._children = None @property def item_id(self): """Gets the node id.""" return self.data.get("item_id") @property def name(self): """Gets the node name.""" return self.data.get("name") @property def type(self): """Gets the node type.""" return self.data.get("type") @property def size(self): """Gets the node size.""" try: return int(self.data.get("size")) except ValueError: return None @property def modified(self): """Gets the node modified date.""" return datetime.strptime(self.data.get("modified"), "%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): """Returns children node directories by their names.""" return [child.name for child in self.get_children()] def get(self, name): """Returns a child node by its 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 as i: raise KeyError(f"No child named {key} exists") from i def __str__(self): return self.name def __repr__(self): return f"<{self.type.capitalize()}: '{self}'>" pyicloud-1.0.0/pyicloud/utils.py0000644000175100001710000000374214203477477017526 0ustar runnerdocker00000000000000"""Utils.""" import getpass import keyring import sys from .exceptions import PyiCloudNoStoredPasswordAvailableException KEYRING_SYSTEM = "pyicloud://icloud-password" def get_password(username, interactive=sys.stdout.isatty()): """Get the password from a username.""" try: return get_password_from_keyring(username) except PyiCloudNoStoredPasswordAvailableException: if not interactive: raise return getpass.getpass( "Enter iCloud password for {username}: ".format( username=username, ) ) def password_exists_in_keyring(username): """Return true if the password of a username exists in the keyring.""" try: get_password_from_keyring(username) except PyiCloudNoStoredPasswordAvailableException: return False return True def get_password_from_keyring(username): """Get the password from a username.""" result = keyring.get_password(KEYRING_SYSTEM, username) if result is None: raise PyiCloudNoStoredPasswordAvailableException( "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): """Store the password of a username.""" return keyring.set_password( KEYRING_SYSTEM, username, password, ) def delete_password_in_keyring(username): """Delete the password of a username.""" return keyring.delete_password( KEYRING_SYSTEM, username, ) def underscore_to_camelcase(word, initial_capital=False): """Transform a word to camelCase.""" words = [x.capitalize() or "_" for x in word.split("_")] if not initial_capital: words[0] = words[0].lower() return "".join(words) pyicloud-1.0.0/pyicloud.egg-info/0000755000175100001710000000000014203477505017470 5ustar runnerdocker00000000000000pyicloud-1.0.0/pyicloud.egg-info/PKG-INFO0000644000175100001710000004457214203477505020601 0ustar runnerdocker00000000000000Metadata-Version: 1.2 Name: pyicloud Version: 1.0.0 Summary: PyiCloud is a module which allows pythonistas to interact with iCloud webservices. Home-page: https://github.com/picklepete/pyicloud Maintainer: The PyiCloud Authors License: MIT Download-URL: https://github.com/picklepete/pyicloud/tarball/1.0.0 Description: ******** pyiCloud ******** .. 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://img.shields.io/pypi/v/pyicloud.svg :alt: Library version :target: https://pypi.org/project/pyicloud .. image:: https://img.shields.io/pypi/pyversions/pyicloud.svg :alt: Supported versions :target: https://pypi.org/project/pyicloud .. image:: https://pepy.tech/badge/pyicloud :alt: Downloads :target: https://pypi.org/project/pyicloud .. image:: https://requires.io/github/Quentame/pyicloud/requirements.svg?branch=master :alt: Requirements Status :target: https://requires.io/github/Quentame/pyicloud/requirements/?branch=master .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :alt: Formated with Black :target: https://github.com/psf/black .. 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: .. code-block:: python 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: .. code-block:: console $ 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. .. code-block:: python 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: .. code-block:: console $ icloud --username=jappleseed@apple.com --delete-from-keyring **Note**: Authentication will expire after an interval set by Apple, at which point you will have to re-authenticate. This interval is currently two months. Two-step and two-factor authentication (2SA/2FA) ************************************************ If you have enabled two-factor authentications (2FA) or `two-step authentication (2SA) `_ for the account you will have to do some extra work: .. code-block:: python if api.requires_2fa: print("Two-factor authentication required.") code = input("Enter the code you received of one of your approved devices: ") result = api.validate_2fa_code(code) print("Code validation result: %s" % result) if not result: print("Failed to verify security code") sys.exit(1) if not api.is_trusted_session: print("Session is not trusted. Requesting trust...") result = api.trust_session() print("Session trust result %s" % result) if not result: print("Failed to request trust. You will likely be prompted for the code again in the coming weeks") elif api.requires_2sa: import click print("Two-step 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) Devices ======= You can list which devices associated with your account by using the ``devices`` property: .. code-block:: pycon >>> api.devices { 'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': , 'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': } and you can access individual devices by either their index, or their ID: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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. .. code-block:: pycon >>> api.iphone.location() {'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, '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. .. code-block:: pycon >>> api.iphone.status() {'deviceDisplayName': 'iPhone 5', 'deviceStatus': '200', 'batteryLevel': 0.6166913, 'name': "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. .. code-block:: python 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. .. code-block:: python 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: .. code-block:: python api.calendar.events() Or, between a specific date range: .. code-block:: python 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: .. code-block:: python api.calendar.get_event_detail('CALENDAR', 'EVENT_ID') Contacts ======== You can access your iCloud contacts/address book through the ``contacts`` property: .. code-block:: pycon >>> for c in api.contacts.all(): >>> print(c.get('firstName'), c.get('phones')) John [{'field': '+1 555-55-5555-5', 'label': '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: .. code-block:: pycon >>> api.files.dir() ['.do-not-delete', '.localized', 'com~apple~Notes', 'com~apple~Preview', 'com~apple~mail', 'com~apple~shoebox', 'com~apple~system~spotlight' ] You can access children and their children's children using the filename as an index: .. code-block:: pycon >>> api.files['com~apple~Notes'] >>> api.files['com~apple~Notes'].type 'folder' >>> api.files['com~apple~Notes'].dir() ['Documents'] >>> api.files['com~apple~Notes']['Documents'].dir() ['Some Document'] >>> api.files['com~apple~Notes']['Documents']['Some Document'].name '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 '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``. .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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: .. code-block:: pycon >>> 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()) File Storage (iCloud Drive) =========================== You can access your iCloud Drive using an API identical to the Ubiquity one described in the previous section, except that it is rooted at ```api.drive```: .. code-block:: pycon >>> api.drive.dir() ['Holiday Photos', 'Work Files'] >>> api.drive['Holiday Photos']['2013']['Sicily'].dir() ['DSC08116.JPG', 'DSC08117.JPG'] >>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG'] >>> drive_file.name 'DSC08116.JPG' >>> drive_file.date_modified datetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC >>> drive_file.size 2021698 >>> drive_file.type 'file' The ``open`` method will return a response object from which you can read the file's contents: .. code-block:: python from shutil import copyfileobj with drive_file.open(stream=True) as response: with open(drive_file.name, 'wb') as file_out: copyfileobj(response.raw, file_out) To interact with files and directions the ``mkdir``, ``rename`` and ``delete`` functions are available for a file or folder: .. code-block:: python api.drive['Holiday Photos'].mkdir('2020') api.drive['Holiday Photos']['2020'].rename('2020_copy') api.drive['Holiday Photos']['2020_copy'].delete() The ``upload`` method can be used to send a file-like object to the iCloud Drive: .. code-block:: python with open('Vacation.jpeg', 'rb') as file_in: api.drive['Holiday Photos'].upload(file_in) It is strongly suggested to open file handles as binary rather than text to prevent decoding errors further down the line. Photo Library ======================= You can access the iCloud Photo Library through the ``photos`` property. .. code-block:: pycon >>> api.photos.all Individual albums are available through the ``albums`` property: .. code-block:: pycon >>> api.photos.albums['Screenshots'] Which you can iterate to access the photo assets. The 'All Photos' album is sorted by `added_date` so the most recently added photos are returned first. All other albums are sorted by `asset_date` (which represents the exif date) : .. code-block:: pycon >>> for photo in api.photos.albums['Screenshots']: print(photo, photo.filename) IMG_6045.JPG 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: .. code-block:: python photo = next(iter(api.photos.albums['Screenshots']), None) 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: .. code-block:: pycon >>> photo.versions.keys() ['medium', 'original', 'thumb'] To download a specific version of the photo asset, pass the version to ``download()``: .. code-block:: python download = photo.download('thumb') with open(photo.versions['thumb']['filename'], 'wb') as thumb_file: thumb_file.write(download.raw.read()) Code samples ============ If you wanna see some code samples see the `code samples file `_. Keywords: icloud,find-my-iphone Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Software Development :: Libraries Requires-Python: >=3.7 pyicloud-1.0.0/pyicloud.egg-info/SOURCES.txt0000644000175100001710000000117314203477505021356 0ustar runnerdocker00000000000000MANIFEST.in README.rst pyproject.toml 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/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/drive.py pyicloud/services/findmyiphone.py pyicloud/services/photos.py pyicloud/services/reminders.py pyicloud/services/ubiquity.pypyicloud-1.0.0/pyicloud.egg-info/dependency_links.txt0000644000175100001710000000000114203477505023536 0ustar runnerdocker00000000000000 pyicloud-1.0.0/pyicloud.egg-info/entry_points.txt0000644000175100001710000000006214203477505022764 0ustar runnerdocker00000000000000[console_scripts] icloud = pyicloud.cmdline:main pyicloud-1.0.0/pyicloud.egg-info/requires.txt0000644000175100001710000000014214203477505022065 0ustar runnerdocker00000000000000requests>=2.24.0 keyring>=21.4.0 keyrings.alt>=3.5.2 click>=7.1.2 tzlocal>=4.0 certifi>=2020.6.20 pyicloud-1.0.0/pyicloud.egg-info/top_level.txt0000644000175100001710000000001114203477505022212 0ustar runnerdocker00000000000000pyicloud pyicloud-1.0.0/pyproject.toml0000644000175100001710000000040714203477477017073 0ustar runnerdocker00000000000000[tool.black] line-length = 88 target-version = ["py37", "py38", "py39", "py310"] exclude = ''' ( /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ | exceptions.py ) ''' pyicloud-1.0.0/requirements.txt0000644000175100001710000000014214203477477017437 0ustar runnerdocker00000000000000requests>=2.24.0 keyring>=21.4.0 keyrings.alt>=3.5.2 click>=7.1.2 tzlocal>=4.0 certifi>=2020.6.20 pyicloud-1.0.0/setup.cfg0000644000175100001710000000015314203477505015766 0ustar runnerdocker00000000000000[tool:pytest] testpaths = tests norecursedirs = .git .tox build lib [egg_info] tag_build = tag_date = 0 pyicloud-1.0.0/setup.py0000644000175100001710000000270514203477477015674 0ustar runnerdocker00000000000000#!/usr/bin/env python """pyiCloud setup.""" from setuptools import setup, find_packages REPO_URL = "https://github.com/picklepete/pyicloud" VERSION = "1.0.0" with open("requirements.txt") as f: required = f.read().splitlines() with open("README.rst", encoding="utf-8") as f: long_description = f.read() setup( name="pyicloud", version=VERSION, url=REPO_URL, download_url=REPO_URL + "/tarball/" + VERSION, description="PyiCloud is a module which allows pythonistas to interact with iCloud webservices.", long_description=long_description, maintainer="The PyiCloud Authors", packages=find_packages(include=["pyicloud*"]), install_requires=required, python_requires=">=3.7", license="MIT", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Topic :: Software Development :: Libraries", ], entry_points={"console_scripts": ["icloud = pyicloud.cmdline:main"]}, keywords=["icloud", "find-my-iphone"], )