Polish readme (#373)
This commit is contained in:
parent
8c7ba2afb4
commit
f96b0d8c24
1 changed files with 171 additions and 111 deletions
268
README.rst
268
README.rst
|
@ -40,24 +40,32 @@ Authentication
|
|||
|
||||
Authentication without using a saved password is as simple as passing your username and password to the ``PyiCloudService`` class:
|
||||
|
||||
>>> from pyicloud import PyiCloudService
|
||||
>>> api = PyiCloudService('jappleseed@apple.com', 'password')
|
||||
.. 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:
|
||||
|
||||
>>> icloud --username=jappleseed@apple.com
|
||||
ICloud Password for jappleseed@apple.com:
|
||||
Save password in keyring? (y/N)
|
||||
.. 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.
|
||||
|
||||
>>> api = PyiCloudService('jappleseed@apple.com')
|
||||
.. 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:
|
||||
|
||||
>>> icloud --username=jappleseed@apple.com --delete-from-keyring
|
||||
.. 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.
|
||||
|
||||
|
@ -69,7 +77,7 @@ If you have enabled two-factor authentications (2FA) or `two-step authentication
|
|||
.. code-block:: python
|
||||
|
||||
if api.requires_2fa:
|
||||
print "Two-factor authentication required."
|
||||
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)
|
||||
|
@ -87,48 +95,54 @@ If you have enabled two-factor authentications (2FA) or `two-step authentication
|
|||
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:"
|
||||
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',
|
||||
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"
|
||||
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"
|
||||
print("Failed to verify verification code")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
||||
Devices
|
||||
=======
|
||||
|
||||
You can list which devices associated with your account by using the ``devices`` property:
|
||||
|
||||
>>> api.devices
|
||||
{
|
||||
u'i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==': <AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>,
|
||||
u'reGYDh9XwqNWTGIhNBuEwP1ds0F/Lg5t/fxNbI4V939hhXawByErk+HYVNSUzmWV': <AppleDevice(MacBook Air 11": Johnny Appleseed's MacBook Air)>
|
||||
}
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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)>
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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)>
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> api.iphone
|
||||
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
|
||||
|
||||
Note: the first device associated with your account may not necessarily be your iPhone.
|
||||
|
||||
|
@ -142,16 +156,20 @@ Location
|
|||
|
||||
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}
|
||||
.. 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.
|
||||
|
||||
>>> api.iphone.status()
|
||||
{'deviceDisplayName': 'iPhone 5', 'deviceStatus': '200', 'batteryLevel': 0.6166913, 'name': "Peter's iPhone"}
|
||||
.. 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.
|
||||
|
||||
|
@ -160,7 +178,9 @@ Play Sound
|
|||
|
||||
Sends a request to the device to play a sound, if you wish pass a custom message you can do so by changing the subject arg.
|
||||
|
||||
>>> api.iphone.play_sound()
|
||||
.. 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.
|
||||
|
||||
|
@ -169,9 +189,11 @@ Lost Mode
|
|||
|
||||
Lost mode is slightly different to the "Play Sound" functionality in that it allows the person who picks up the phone to call a specific phone number *without having to enter the passcode*. Just like "Play Sound" you may pass a custom message which the device will display, if it's not overridden the custom message of "This iPhone has been lost. Please call me." is used.
|
||||
|
||||
>>> phone_number = '555-373-383'
|
||||
>>> message = 'Thief! Return my phone immediately.'
|
||||
>>> api.iphone.lost_device(phone_number, message)
|
||||
.. code-block:: python
|
||||
|
||||
phone_number = '555-373-383'
|
||||
message = 'Thief! Return my phone immediately.'
|
||||
api.iphone.lost_device(phone_number, message)
|
||||
|
||||
|
||||
Calendar
|
||||
|
@ -184,17 +206,23 @@ Events
|
|||
|
||||
Returns this month's events:
|
||||
|
||||
>>> api.calendar.events()
|
||||
.. code-block:: python
|
||||
|
||||
api.calendar.events()
|
||||
|
||||
Or, between a specific date range:
|
||||
|
||||
>>> from_dt = datetime(2012, 1, 1)
|
||||
>>> to_dt = datetime(2012, 1, 31)
|
||||
>>> api.calendar.events(from_dt, to_dt)
|
||||
.. 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:
|
||||
|
||||
>>> api.calendar.get_event_detail('CALENDAR', 'EVENT_ID')
|
||||
.. code-block:: python
|
||||
|
||||
api.calendar.get_event_detail('CALENDAR', 'EVENT_ID')
|
||||
|
||||
|
||||
Contacts
|
||||
|
@ -202,9 +230,11 @@ Contacts
|
|||
|
||||
You can access your iCloud contacts/address book through the ``contacts`` property:
|
||||
|
||||
>>> for c in api.contacts.all():
|
||||
>>> print c.get('firstName'), c.get('phones')
|
||||
John [{'field': '+1 555-55-5555-5', 'label': 'MOBILE'}]
|
||||
.. 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.
|
||||
|
||||
|
@ -214,53 +244,63 @@ File Storage (Ubiquity)
|
|||
|
||||
You can access documents stored in your iCloud account by using the ``files`` property's ``dir`` method:
|
||||
|
||||
>>> api.files.dir()
|
||||
['.do-not-delete',
|
||||
.. 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:
|
||||
|
||||
>>> api.files['com~apple~Notes']
|
||||
<Folder: 'com~apple~Notes'>
|
||||
>>> api.files['com~apple~Notes'].type
|
||||
'folder'
|
||||
>>> api.files['com~apple~Notes'].dir()
|
||||
[u'Documents']
|
||||
>>> api.files['com~apple~Notes']['Documents'].dir()
|
||||
[u'Some Document']
|
||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].name
|
||||
u'Some Document'
|
||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].modified
|
||||
datetime.datetime(2012, 9, 13, 2, 26, 17)
|
||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].size
|
||||
1308134
|
||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].type
|
||||
u'file'
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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'
|
||||
.. 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 <http://www.python-requests.org/en/latest/api/#classes>`_ and the ``open`` method can accept any parameters you might normally use in a request using `requests <https://github.com/kennethreitz/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'
|
||||
.. 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:
|
||||
|
||||
>>> download = api.files['com~apple~Notes']['Documents']['big_file.zip'].open(stream=True)
|
||||
>>> with open('downloaded_file.zip', 'wb') as opened_file:
|
||||
.. 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)
|
||||
|
@ -268,39 +308,47 @@ 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```:
|
||||
|
||||
>>> api.drive.dir()
|
||||
['Holiday Photos', 'Work Files']
|
||||
>>> api.drive['Holiday Photos']['2013']['Sicily'].dir()
|
||||
['DSC08116.JPG', 'DSC08117.JPG']
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG']
|
||||
>>> drive_file.name
|
||||
u'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
|
||||
u'file'
|
||||
>>> 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)
|
||||
.. 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:
|
||||
|
||||
>>> api.drive['Holiday Photos'].mkdir('2020')
|
||||
>>> api.drive['Holiday Photos']['2020'].rename('2020_copy')
|
||||
>>> api.drive['Holiday Photos']['2020_copy'].delete()
|
||||
.. 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:
|
||||
|
||||
>>> with open('Vacation.jpeg', 'rb') as file_in:
|
||||
>>>> api.drive['Holiday Photos'].upload(file_in)
|
||||
.. 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.
|
||||
|
@ -310,38 +358,50 @@ Photo Library
|
|||
|
||||
You can access the iCloud Photo Library through the ``photos`` property.
|
||||
|
||||
>>> api.photos.all
|
||||
<PhotoAlbum: 'All Photos'>
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> api.photos.all
|
||||
<PhotoAlbum: 'All Photos'>
|
||||
|
||||
Individual albums are available through the ``albums`` property:
|
||||
|
||||
>>> api.photos.albums['Screenshots']
|
||||
<PhotoAlbum: 'Screenshots'>
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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 <http://www.python-requests.org/en/latest/api/#classes>`_, 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:
|
||||
.. 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:
|
||||
|
||||
>>> photo.versions.keys()
|
||||
['medium', 'original', 'thumb']
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> 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:
|
||||
.. code-block:: python
|
||||
|
||||
download = photo.download('thumb')
|
||||
with open(photo.versions['thumb']['filename'], 'wb') as thumb_file:
|
||||
thumb_file.write(download.raw.read())
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue