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 without using a saved password is as simple as passing
your username and password to the PyiCloudService
class:
from pyicloud import PyiCloudService
api = PyiCloudService('jappleseed@apple.com', 'password')
In the event that the username/password combination is invalid, a
PyiCloudFailedLoginException
exception is thrown.
If the country/region setting of your Apple ID is China mainland, you
should pass china_mainland=True
to the PyiCloudService
class:
from pyicloud import PyiCloudService
api = PyiCloudService('jappleseed@apple.com', 'password', china_mainland=True)
You can also store your password in the system keyring using the command-line tool:
$ icloud --username=jappleseed@apple.com
Enter iCloud password for jappleseed@apple.com:
Save password in keyring? (y/N)
If you have stored a password in the keyring, you will not be required
to provide a password when interacting with the command-line tool or
instantiating the PyiCloudService
class for the username you stored
the password for.
api = PyiCloudService('jappleseed@apple.com')
If you would like to delete a password stored in your system keyring,
you can clear a stored password using the --delete-from-keyring
command-line option:
$ icloud --username=jappleseed@apple.com --delete-from-keyring
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.
If you have enabled two-factor authentications (2FA) or two-step authentication (2SA) for the account you will have to do some extra work:
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)
You can list which devices associated with your account by using the
devices
property:
>>> api.devices
{
'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': <AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>,
'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': <AppleDevice(MacBook Air 11": Johnny Appleseed's MacBook Air)>
}
and you can access individual devices by either their index, or their ID:
>>> api.devices[0]
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
>>> api.devices['i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==']
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
or, as a shorthand if you have only one associated apple device, you can
simply use the iphone
property to access the first device associated
with your account:
>>> api.iphone
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
Note: the first device associated with your account may not necessarily be your iPhone.
Once you have successfully authenticated, you can start querying your data!
Returns the device's last known location. The Find My iPhone app must have been installed and initialized.
>>> api.iphone.location()
{'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, 'horizontalAccuracy': 5.0}
The Find My iPhone response is quite bloated, so for simplicity's sake this method will return a subset of the properties.
>>> api.iphone.status()
{'deviceDisplayName': '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.
Sends a request to the device to play a sound, if you wish pass a custom message you can do so by changing the subject arg.
api.iphone.play_sound()
A few moments later, the device will play a ringtone, display the default notification ("Find My iPhone Alert") and a confirmation email will be sent to you.
Lost mode is slightly different to the "Play Sound" functionality in that it allows the person who picks up the phone to call a specific phone number without having to enter the passcode. Just like "Play Sound" you may pass a custom message which the device will display, if it's not overridden the custom message of "This iPhone has been lost. Please call me." is used.
phone_number = '555-373-383'
message = 'Thief! Return my phone immediately.'
api.iphone.lost_device(phone_number, message)
The calendar webservice now supports fethcing, creating, and removing calendars and events.
The calendar functionality is based around the CalendarObject
dataclass. Every variable has a default value named according to the http payload parameters from the icloud API. The guid
is a uuid4 identifier unique to each calendar. The class will create one automatically if it is left blank when the CalendarObject
is instanced. the guid
parameter should only be set when you know the guid of an existing calendar. The color is an rgb hex value and will be a random color if not set.
get_calendars(as_objs:bool=False) -> list
returns a list of the user's calendars
if as_objs
is set to True
, the returned list will be of CalendarObjects; else it will be of dictionaries.
add_calendar(calendar:CalendarObject) -> None:
adds a calendar to the users apple calendar
remove_calendar(cal_guid:str) -> None
Removes a Calendar from the apple calendar given the provided guid
Create and add a new calendar:
api = login("username", "pass")
calendar_service = api.calendar
cal = calendar_service.CalendarObject(title="My Calendar", share_type="published")
cal.color = "#FF0000"
calendar_service.add_calendar(cal)
Remove an existing calendar:
cal = calendar_service.get_calendars(as_objs=True)[1]
calendar_service.remove_calendar(cal.guid)
The events functionality is based around the EventObject
dataclass. guid
is the unique identifier of each event, while pGuid
is the identifier of the calendar to which this event belongs. pGuid
is the only paramter that is not optional. Some of the functionality of Events, most notably Alarms, is not included here, but could be easily done had you the desire. The EventObject
currently has one method you may use: add_invitees
which takes a list of emails and adds them as invitees to this event. They should recieve an email when this event is created.
get_events(from_dt:datetime=None, to_dt:datetime=None, period:str="month", as_objs:bool=False)
Returns a list of events from from_dt
to to_dt
. If period
is provided, it will return the events in that period refrencing from_dt
if it was provided; else using today's date. IE if period
is "month", the events for the entire month that from_dt
falls within will be returned.
get_event_detail(pguid, guid, as_obj:bool=False)
Returns a speciffic event given that event's guid
and pGuid
add_event(event:EventObject) -> None
Adds an Event to a calendar specified by the event's pGuid
.
remove_event(event:EventObject) -> None
Removes an Event from a calendar specified by the event's pGuid
.
Create, add, and remove an Event
calendar_service = api.calendar
cal = calendar_service.get_calendars(as_objs=True)[0]
event = calendar_service.EventObject("test", pGuid=cal.guid, startDate=datetime.today(), endDate=datetime.today() + timedelta(hours=1))
calendar_service.add_event(event)
calendar_service.remove_event(event)
Or, between a specific date range:
from_dt = datetime(2012, 1, 1)
to_dt = datetime(2012, 1, 31)
api.calendar.events(from_dt, to_dt)
Get next weeks' events
calendar_service.get_events(from_dt=datetime.today() + timedelta(days=7) ,period="week", as_objs=True)
You can access your iCloud contacts/address book through the contacts
property:
>>> for c in api.contacts.all():
>>> print(c.get('firstName'), c.get('phones'))
John [{'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.
You can access documents stored in your iCloud account by using the
files
property's dir
method:
>>> 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:
>>> api.files['com~apple~Notes']
<Folder: '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
.
>>> api.files['com~apple~Notes']['Documents']['Some Document'].open().content
'Hello, these are the file contents'
Note: the object returned from the above open
method is a response
object and the
open
method can accept any parameters you might normally use in a
request using requests.
For example, if you know that the file you're opening has JSON content:
>>> api.files['com~apple~Notes']['Documents']['information.json'].open().json()
{'How much we love you': 'lots'}
>>> api.files['com~apple~Notes']['Documents']['information.json'].open().json()['How much we love you']
'lots'
Or, if you're downloading a particularly large file, you may want to
use the stream
keyword argument, and read directly from the raw
response object:
>>> download = api.files['com~apple~Notes']['Documents']['big_file.zip'].open(stream=True)
>>> with open('downloaded_file.zip', 'wb') as opened_file:
opened_file.write(download.raw.read())
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
:
>>> 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:
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:
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:
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.
You can also interact with files in the trash
:
>>> delete_output = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG'].delete()
>>> api.drive.trash.dir()
['DSC08116.JPG']
>>> delete_output = api.drive['Holiday Photos']['2013']['Sicily']['DSC08117.JPG'].delete()
>>> api.drive.refresh_trash()
>>> api.drive.trash.dir()
['DSC08116.JPG', 'DSC08117.JPG']
You can interact with the trash
similar to a standard directory, with some restrictions. In addition, files in the trash
can be recovered back to their original location, or deleted forever:
>>> api.drive['Holiday Photos']['2013']['Sicily'].dir()
[]
>>> recover_output = api.drive.trash['DSC08116.JPG'].recover()
>>> api.drive['Holiday Photos']['2013']['Sicily'].dir()
['DSC08116.JPG']
>>> api.drive.trash.dir()
['DSC08117.JPG']
>>> purge_output = api.drive.trash['DSC08117.JPG'].delete_forever()
>>> api.drive.refresh_trash()
>>> api.drive.trash.dir()
[]
You can access the iCloud Photo Library through the photos
property.
>>> api.photos.all
<PhotoAlbum: 'All Photos'>
Individual albums are available through the albums
property:
>>> api.photos.albums['Screenshots']
<PhotoAlbum: '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) :
>>> for photo in api.photos.albums['Screenshots']:
print(photo, photo.filename)
<PhotoAsset: id=AVbLPCGkp798nTb9KZozCXtO7jds> 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:
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())
Consider using shutil.copyfileobj
or another buffered strategy for downloading so that the whole file isn't read into memory before writing.
import shutil
photo = next(iter(api.photos.albums['Screenshots']), None)
response_obj = photo.download()
with open(photo.filename, 'wb') as f:
shutil.copyfileobj(response_obj.raw, f)
Information about each version can be accessed through the versions
property:
>>> photo.versions.keys()
['medium', 'original', 'thumb']
To download a specific version of the photo asset, pass the version to
download()
:
download = photo.download('thumb')
with open(photo.versions['thumb']['filename'], 'wb') as thumb_file:
thumb_file.write(download.raw.read())
To upload an image
api.photos.upload_file(file_path)
Note: Only limited media type is accepted, upload not support types (e.g. png) will get TYPE_UNSUPPORTED error.
If you wanna see some code samples see the code samples file. `