Compare commits
No commits in common. "332cc9fa767862480c27253233c2cfdf9f2ea0d9" and "8c7ba2afb4bcfeb7c0c18c2be06c1682f87750ba" have entirely different histories.
332cc9fa76
...
8c7ba2afb4
20 changed files with 170 additions and 276 deletions
4
.github/release-drafter.yml
vendored
4
.github/release-drafter.yml
vendored
|
@ -1,4 +0,0 @@
|
||||||
template: |
|
|
||||||
## What's Changed
|
|
||||||
|
|
||||||
$CHANGES
|
|
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -12,6 +12,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
|
- "3.6"
|
||||||
- "3.7"
|
- "3.7"
|
||||||
- "3.8"
|
- "3.8"
|
||||||
- "3.9"
|
- "3.9"
|
||||||
|
|
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
|
@ -9,10 +9,10 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2.4.0
|
- uses: actions/checkout@v2.4.0
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.6
|
||||||
uses: actions/setup-python@v2.3.2
|
uses: actions/setup-python@v2.3.2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.6
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
|
|
15
.github/workflows/release-drafter.yml
vendored
15
.github/workflows/release-drafter.yml
vendored
|
@ -1,15 +0,0 @@
|
||||||
name: Release Drafter
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
update_release_draft:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
|
||||||
- uses: release-drafter/release-drafter@v5
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
268
README.rst
268
README.rst
|
@ -40,32 +40,24 @@ Authentication
|
||||||
|
|
||||||
Authentication without using a saved password is as simple as passing your username and password to the ``PyiCloudService`` class:
|
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')
|
||||||
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.
|
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:
|
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:
|
||||||
$ icloud --username=jappleseed@apple.com
|
Save password in keyring? (y/N)
|
||||||
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.
|
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')
|
||||||
|
|
||||||
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:
|
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
|
||||||
|
|
||||||
$ 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.
|
**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.
|
||||||
|
|
||||||
|
@ -77,7 +69,7 @@ If you have enabled two-factor authentications (2FA) or `two-step authentication
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
if api.requires_2fa:
|
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: ")
|
code = input("Enter the code you received of one of your approved devices: ")
|
||||||
result = api.validate_2fa_code(code)
|
result = api.validate_2fa_code(code)
|
||||||
print("Code validation result: %s" % result)
|
print("Code validation result: %s" % result)
|
||||||
|
@ -95,54 +87,48 @@ 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")
|
print("Failed to request trust. You will likely be prompted for the code again in the coming weeks")
|
||||||
elif api.requires_2sa:
|
elif api.requires_2sa:
|
||||||
import click
|
import click
|
||||||
print("Two-step authentication required. Your trusted devices are:")
|
print "Two-step authentication required. Your trusted devices are:"
|
||||||
|
|
||||||
devices = api.trusted_devices
|
devices = api.trusted_devices
|
||||||
for i, device in enumerate(devices):
|
for i, device in enumerate(devices):
|
||||||
print(
|
print " %s: %s" % (i, device.get('deviceName',
|
||||||
" %s: %s" % (i, device.get('deviceName',
|
|
||||||
"SMS to %s" % device.get('phoneNumber')))
|
"SMS to %s" % device.get('phoneNumber')))
|
||||||
)
|
|
||||||
|
|
||||||
device = click.prompt('Which device would you like to use?', default=0)
|
device = click.prompt('Which device would you like to use?', default=0)
|
||||||
device = devices[device]
|
device = devices[device]
|
||||||
if not api.send_verification_code(device):
|
if not api.send_verification_code(device):
|
||||||
print("Failed to send verification code")
|
print "Failed to send verification code"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
code = click.prompt('Please enter validation code')
|
code = click.prompt('Please enter validation code')
|
||||||
if not api.validate_verification_code(device, code):
|
if not api.validate_verification_code(device, code):
|
||||||
print("Failed to verify verification code")
|
print "Failed to verify verification code"
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Devices
|
Devices
|
||||||
=======
|
=======
|
||||||
|
|
||||||
You can list which devices associated with your account by using the ``devices`` property:
|
You can list which devices associated with your account by using the ``devices`` property:
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> api.devices
|
||||||
|
{
|
||||||
>>> 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)>
|
||||||
'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:
|
and you can access individual devices by either their index, or their ID:
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> api.devices[0]
|
||||||
|
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
|
||||||
>>> api.devices[0]
|
>>> api.devices['i9vbKRGIcLYqJnXMd1b257kUWnoyEBcEh6yM+IfmiMLh7BmOpALS+w==']
|
||||||
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
|
<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:
|
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
|
||||||
|
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
|
||||||
>>> api.iphone
|
|
||||||
<AppleDevice(iPhone 4S: Johnny Appleseed's iPhone)>
|
|
||||||
|
|
||||||
Note: the first device associated with your account may not necessarily be your iPhone.
|
Note: the first device associated with your account may not necessarily be your iPhone.
|
||||||
|
|
||||||
|
@ -156,20 +142,16 @@ Location
|
||||||
|
|
||||||
Returns the device's last known location. The Find My iPhone app must have been installed and initialized.
|
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}
|
||||||
>>> api.iphone.location()
|
|
||||||
{'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, 'horizontalAccuracy': 5.0}
|
|
||||||
|
|
||||||
Status
|
Status
|
||||||
******
|
******
|
||||||
|
|
||||||
The Find My iPhone response is quite bloated, so for simplicity's sake this method will return a subset of the properties.
|
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"}
|
||||||
>>> 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.
|
If you wish to request further properties, you may do so by passing in a list of property names.
|
||||||
|
|
||||||
|
@ -178,9 +160,7 @@ 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.
|
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()
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
@ -189,11 +169,9 @@ 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.
|
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.'
|
||||||
phone_number = '555-373-383'
|
>>> api.iphone.lost_device(phone_number, message)
|
||||||
message = 'Thief! Return my phone immediately.'
|
|
||||||
api.iphone.lost_device(phone_number, message)
|
|
||||||
|
|
||||||
|
|
||||||
Calendar
|
Calendar
|
||||||
|
@ -206,23 +184,17 @@ Events
|
||||||
|
|
||||||
Returns this month's events:
|
Returns this month's events:
|
||||||
|
|
||||||
.. code-block:: python
|
>>> api.calendar.events()
|
||||||
|
|
||||||
api.calendar.events()
|
|
||||||
|
|
||||||
Or, between a specific date range:
|
Or, between a specific date range:
|
||||||
|
|
||||||
.. code-block:: python
|
>>> from_dt = datetime(2012, 1, 1)
|
||||||
|
>>> to_dt = datetime(2012, 1, 31)
|
||||||
from_dt = datetime(2012, 1, 1)
|
>>> api.calendar.events(from_dt, to_dt)
|
||||||
to_dt = datetime(2012, 1, 31)
|
|
||||||
api.calendar.events(from_dt, to_dt)
|
|
||||||
|
|
||||||
Alternatively, you may fetch a single event's details, like so:
|
Alternatively, you may fetch a single event's details, like so:
|
||||||
|
|
||||||
.. code-block:: python
|
>>> api.calendar.get_event_detail('CALENDAR', 'EVENT_ID')
|
||||||
|
|
||||||
api.calendar.get_event_detail('CALENDAR', 'EVENT_ID')
|
|
||||||
|
|
||||||
|
|
||||||
Contacts
|
Contacts
|
||||||
|
@ -230,11 +202,9 @@ Contacts
|
||||||
|
|
||||||
You can access your iCloud contacts/address book through the ``contacts`` property:
|
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')
|
||||||
>>> for c in api.contacts.all():
|
John [{'field': '+1 555-55-5555-5', 'label': 'MOBILE'}]
|
||||||
>>> 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.
|
Note: These contacts do not include contacts federated from e.g. Facebook, only the ones stored in iCloud.
|
||||||
|
|
||||||
|
@ -244,63 +214,53 @@ File Storage (Ubiquity)
|
||||||
|
|
||||||
You can access documents stored in your iCloud account by using the ``files`` property's ``dir`` method:
|
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',
|
||||||
>>> api.files.dir()
|
|
||||||
['.do-not-delete',
|
|
||||||
'.localized',
|
'.localized',
|
||||||
'com~apple~Notes',
|
'com~apple~Notes',
|
||||||
'com~apple~Preview',
|
'com~apple~Preview',
|
||||||
'com~apple~mail',
|
'com~apple~mail',
|
||||||
'com~apple~shoebox',
|
'com~apple~shoebox',
|
||||||
'com~apple~system~spotlight'
|
'com~apple~system~spotlight'
|
||||||
]
|
]
|
||||||
|
|
||||||
You can access children and their children's children using the filename as an index:
|
You can access children and their children's children using the filename as an index:
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> api.files['com~apple~Notes']
|
||||||
|
<Folder: 'com~apple~Notes'>
|
||||||
>>> api.files['com~apple~Notes']
|
>>> api.files['com~apple~Notes'].type
|
||||||
<Folder: 'com~apple~Notes'>
|
'folder'
|
||||||
>>> api.files['com~apple~Notes'].type
|
>>> api.files['com~apple~Notes'].dir()
|
||||||
'folder'
|
[u'Documents']
|
||||||
>>> api.files['com~apple~Notes'].dir()
|
>>> api.files['com~apple~Notes']['Documents'].dir()
|
||||||
['Documents']
|
[u'Some Document']
|
||||||
>>> api.files['com~apple~Notes']['Documents'].dir()
|
>>> api.files['com~apple~Notes']['Documents']['Some Document'].name
|
||||||
['Some Document']
|
u'Some Document'
|
||||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].name
|
>>> api.files['com~apple~Notes']['Documents']['Some Document'].modified
|
||||||
'Some Document'
|
datetime.datetime(2012, 9, 13, 2, 26, 17)
|
||||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].modified
|
>>> api.files['com~apple~Notes']['Documents']['Some Document'].size
|
||||||
datetime.datetime(2012, 9, 13, 2, 26, 17)
|
1308134
|
||||||
>>> api.files['com~apple~Notes']['Documents']['Some Document'].size
|
>>> api.files['com~apple~Notes']['Documents']['Some Document'].type
|
||||||
1308134
|
u'file'
|
||||||
>>> 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``.
|
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'
|
||||||
>>> 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>`_.
|
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:
|
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()
|
>>> api.files['com~apple~Notes']['Documents']['information.json'].open().json()['How much we love you']
|
||||||
{'How much we love you': 'lots'}
|
'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:
|
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:
|
||||||
>>> 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())
|
opened_file.write(download.raw.read())
|
||||||
|
|
||||||
File Storage (iCloud Drive)
|
File Storage (iCloud Drive)
|
||||||
|
@ -308,47 +268,39 @@ 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```:
|
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']
|
||||||
|
|
||||||
>>> api.drive.dir()
|
>>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG']
|
||||||
['Holiday Photos', 'Work Files']
|
>>> drive_file.name
|
||||||
>>> api.drive['Holiday Photos']['2013']['Sicily'].dir()
|
u'DSC08116.JPG'
|
||||||
['DSC08116.JPG', 'DSC08117.JPG']
|
>>> drive_file.date_modified
|
||||||
|
datetime.datetime(2013, 3, 21, 12, 28, 12) # NB this is UTC
|
||||||
>>> drive_file = api.drive['Holiday Photos']['2013']['Sicily']['DSC08116.JPG']
|
>>> drive_file.size
|
||||||
>>> drive_file.name
|
2021698
|
||||||
'DSC08116.JPG'
|
>>> drive_file.type
|
||||||
>>> drive_file.date_modified
|
u'file'
|
||||||
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:
|
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:
|
||||||
from shutil import copyfileobj
|
>>> with open(drive_file.name, 'wb') as file_out:
|
||||||
with drive_file.open(stream=True) as response:
|
>>> copyfileobj(response.raw, file_out)
|
||||||
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
|
To interact with files and directions the ``mkdir``, ``rename`` and ``delete`` functions are available
|
||||||
for a file or folder:
|
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'].mkdir('2020')
|
>>> api.drive['Holiday Photos']['2020_copy'].delete()
|
||||||
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:
|
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)
|
||||||
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
|
It is strongly suggested to open file handles as binary rather than text to prevent decoding errors
|
||||||
further down the line.
|
further down the line.
|
||||||
|
@ -358,50 +310,38 @@ Photo Library
|
||||||
|
|
||||||
You can access the iCloud Photo Library through the ``photos`` property.
|
You can access the iCloud Photo Library through the ``photos`` property.
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> api.photos.all
|
||||||
|
<PhotoAlbum: 'All Photos'>
|
||||||
>>> api.photos.all
|
|
||||||
<PhotoAlbum: 'All Photos'>
|
|
||||||
|
|
||||||
Individual albums are available through the ``albums`` property:
|
Individual albums are available through the ``albums`` property:
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> api.photos.albums['Screenshots']
|
||||||
|
<PhotoAlbum: 'Screenshots'>
|
||||||
>>> 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) :
|
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
|
||||||
>>> for photo in api.photos.albums['Screenshots']:
|
<PhotoAsset: id=AVbLPCGkp798nTb9KZozCXtO7jds> IMG_6045.JPG
|
||||||
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:
|
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:
|
||||||
|
|
||||||
.. code-block:: python
|
>>> photo = next(iter(api.photos.albums['Screenshots']), None)
|
||||||
|
>>> download = photo.download()
|
||||||
photo = next(iter(api.photos.albums['Screenshots']), None)
|
>>> with open(photo.filename, 'wb') as opened_file:
|
||||||
download = photo.download()
|
|
||||||
with open(photo.filename, 'wb') as opened_file:
|
|
||||||
opened_file.write(download.raw.read())
|
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.
|
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:
|
Information about each version can be accessed through the ``versions`` property:
|
||||||
|
|
||||||
.. code-block:: pycon
|
>>> photo.versions.keys()
|
||||||
|
['medium', 'original', 'thumb']
|
||||||
>>> photo.versions.keys()
|
|
||||||
['medium', 'original', 'thumb']
|
|
||||||
|
|
||||||
To download a specific version of the photo asset, pass the version to ``download()``:
|
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:
|
||||||
download = photo.download('thumb')
|
|
||||||
with open(photo.versions['thumb']['filename'], 'wb') as thumb_file:
|
|
||||||
thumb_file.write(download.raw.read())
|
thumb_file.write(download.raw.read())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -105,16 +105,13 @@ class PyiCloudSession(Session):
|
||||||
fmip_url = self.service._get_webservice_url("findme")
|
fmip_url = self.service._get_webservice_url("findme")
|
||||||
if (
|
if (
|
||||||
has_retried is None
|
has_retried is None
|
||||||
and response.status_code in [421, 450, 500]
|
and response.status_code == 450
|
||||||
and fmip_url in url
|
and fmip_url in url
|
||||||
):
|
):
|
||||||
# Handle re-authentication for Find My iPhone
|
# Handle re-authentication for Find My iPhone
|
||||||
LOGGER.debug("Re-authenticating Find My iPhone service")
|
LOGGER.debug("Re-authenticating Find My iPhone service")
|
||||||
try:
|
try:
|
||||||
# If 450, authentication requires a full sign in to the account
|
self.service.authenticate(True, "find")
|
||||||
service = None if response.status_code == 450 else "find"
|
|
||||||
self.service.authenticate(True, service)
|
|
||||||
|
|
||||||
except PyiCloudAPIResponseException:
|
except PyiCloudAPIResponseException:
|
||||||
LOGGER.debug("Re-authentication failed")
|
LOGGER.debug("Re-authentication failed")
|
||||||
kwargs["retried"] = True
|
kwargs["retried"] = True
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from calendar import monthrange
|
from calendar import monthrange
|
||||||
|
|
||||||
from tzlocal import get_localzone_name
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
|
|
||||||
class CalendarService:
|
class CalendarService:
|
||||||
|
@ -27,7 +27,7 @@ class CalendarService:
|
||||||
(a calendar) and a guid (an event's ID).
|
(a calendar) and a guid (an event's ID).
|
||||||
"""
|
"""
|
||||||
params = dict(self.params)
|
params = dict(self.params)
|
||||||
params.update({"lang": "en-us", "usertz": get_localzone_name()})
|
params.update({"lang": "en-us", "usertz": get_localzone().zone})
|
||||||
url = f"{self._calendar_event_detail_url}/{pguid}/{guid}"
|
url = f"{self._calendar_event_detail_url}/{pguid}/{guid}"
|
||||||
req = self.session.get(url, params=params)
|
req = self.session.get(url, params=params)
|
||||||
self.response = req.json()
|
self.response = req.json()
|
||||||
|
@ -49,7 +49,7 @@ class CalendarService:
|
||||||
params.update(
|
params.update(
|
||||||
{
|
{
|
||||||
"lang": "en-us",
|
"lang": "en-us",
|
||||||
"usertz": get_localzone_name(),
|
"usertz": get_localzone().zone,
|
||||||
"startDate": from_dt.strftime("%Y-%m-%d"),
|
"startDate": from_dt.strftime("%Y-%m-%d"),
|
||||||
"endDate": to_dt.strftime("%Y-%m-%d"),
|
"endDate": to_dt.strftime("%Y-%m-%d"),
|
||||||
}
|
}
|
||||||
|
@ -76,7 +76,7 @@ class CalendarService:
|
||||||
params.update(
|
params.update(
|
||||||
{
|
{
|
||||||
"lang": "en-us",
|
"lang": "en-us",
|
||||||
"usertz": get_localzone_name(),
|
"usertz": get_localzone().zone,
|
||||||
"startDate": from_dt.strftime("%Y-%m-%d"),
|
"startDate": from_dt.strftime("%Y-%m-%d"),
|
||||||
"endDate": to_dt.strftime("%Y-%m-%d"),
|
"endDate": to_dt.strftime("%Y-%m-%d"),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
"""Drive service."""
|
"""Drive service."""
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
import io
|
import io
|
||||||
import mimetypes
|
import mimetypes
|
||||||
import os
|
import os
|
||||||
|
@ -9,11 +8,6 @@ import time
|
||||||
from re import search
|
from re import search
|
||||||
from requests import Response
|
from requests import Response
|
||||||
|
|
||||||
from pyicloud.exceptions import PyiCloudAPIResponseException
|
|
||||||
|
|
||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class DriveService:
|
class DriveService:
|
||||||
"""The 'Drive' iCloud service."""
|
"""The 'Drive' iCloud service."""
|
||||||
|
@ -48,7 +42,6 @@ class DriveService:
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
|
||||||
return request.json()[0]
|
return request.json()[0]
|
||||||
|
|
||||||
def get_file(self, file_id, **kwargs):
|
def get_file(self, file_id, **kwargs):
|
||||||
|
@ -59,22 +52,16 @@ class DriveService:
|
||||||
self._document_root + "/ws/com.apple.CloudDocs/download/by_id",
|
self._document_root + "/ws/com.apple.CloudDocs/download/by_id",
|
||||||
params=file_params,
|
params=file_params,
|
||||||
)
|
)
|
||||||
self._raise_if_error(response)
|
if not response.ok:
|
||||||
response_json = response.json()
|
return None
|
||||||
package_token = response_json.get("package_token")
|
url = response.json()["data_token"]["url"]
|
||||||
data_token = response_json.get("data_token")
|
return self.session.get(url, params=self.params, **kwargs)
|
||||||
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):
|
def get_app_data(self):
|
||||||
"""Returns the app library (previously ubiquity)."""
|
"""Returns the app library (previously ubiquity)."""
|
||||||
request = self.session.get(
|
request = self.session.get(
|
||||||
self._service_root + "/retrieveAppLibraries", params=self.params
|
self._service_root + "/retrieveAppLibraries", params=self.params
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
|
||||||
return request.json()["items"]
|
return request.json()["items"]
|
||||||
|
|
||||||
def _get_upload_contentws_url(self, file_object):
|
def _get_upload_contentws_url(self, file_object):
|
||||||
|
@ -105,7 +92,8 @@ class DriveService:
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
if not request.ok:
|
||||||
|
return None
|
||||||
return (request.json()[0]["document_id"], request.json()[0]["url"])
|
return (request.json()[0]["document_id"], request.json()[0]["url"])
|
||||||
|
|
||||||
def _update_contentws(self, folder_id, sf_info, document_id, file_object):
|
def _update_contentws(self, folder_id, sf_info, document_id, file_object):
|
||||||
|
@ -143,7 +131,8 @@ class DriveService:
|
||||||
headers={"Content-Type": "text/plain"},
|
headers={"Content-Type": "text/plain"},
|
||||||
data=json.dumps(data),
|
data=json.dumps(data),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
if not request.ok:
|
||||||
|
return None
|
||||||
return request.json()
|
return request.json()
|
||||||
|
|
||||||
def send_file(self, folder_id, file_object):
|
def send_file(self, folder_id, file_object):
|
||||||
|
@ -151,8 +140,10 @@ class DriveService:
|
||||||
document_id, content_url = self._get_upload_contentws_url(file_object)
|
document_id, content_url = self._get_upload_contentws_url(file_object)
|
||||||
|
|
||||||
request = self.session.post(content_url, files={file_object.name: file_object})
|
request = self.session.post(content_url, files={file_object.name: file_object})
|
||||||
self._raise_if_error(request)
|
if not request.ok:
|
||||||
|
return None
|
||||||
content_response = request.json()["singleFile"]
|
content_response = request.json()["singleFile"]
|
||||||
|
|
||||||
self._update_contentws(folder_id, content_response, document_id, file_object)
|
self._update_contentws(folder_id, content_response, document_id, file_object)
|
||||||
|
|
||||||
def create_folders(self, parent, name):
|
def create_folders(self, parent, name):
|
||||||
|
@ -173,7 +164,6 @@ class DriveService:
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
|
||||||
return request.json()
|
return request.json()
|
||||||
|
|
||||||
def rename_items(self, node_id, etag, name):
|
def rename_items(self, node_id, etag, name):
|
||||||
|
@ -193,7 +183,6 @@ class DriveService:
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
|
||||||
return request.json()
|
return request.json()
|
||||||
|
|
||||||
def move_items_to_trash(self, node_id, etag):
|
def move_items_to_trash(self, node_id, etag):
|
||||||
|
@ -213,7 +202,6 @@ class DriveService:
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._raise_if_error(request)
|
|
||||||
return request.json()
|
return request.json()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -229,14 +217,6 @@ class DriveService:
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
return self.root[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:
|
class DriveNode:
|
||||||
"""Drive node."""
|
"""Drive node."""
|
||||||
|
|
|
@ -3,8 +3,9 @@ import json
|
||||||
import base64
|
import base64
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from pyicloud.exceptions import PyiCloudServiceNotActivatedException
|
from pyicloud.exceptions import PyiCloudServiceNotActivatedException
|
||||||
|
from pytz import UTC
|
||||||
|
|
||||||
|
|
||||||
class PhotosService:
|
class PhotosService:
|
||||||
|
@ -525,18 +526,18 @@ class PhotoAsset:
|
||||||
def asset_date(self):
|
def asset_date(self):
|
||||||
"""Gets the photo asset date."""
|
"""Gets the photo asset date."""
|
||||||
try:
|
try:
|
||||||
return datetime.utcfromtimestamp(
|
return datetime.fromtimestamp(
|
||||||
self._asset_record["fields"]["assetDate"]["value"] / 1000.0
|
self._asset_record["fields"]["assetDate"]["value"] / 1000.0, tz=UTC
|
||||||
).replace(tzinfo=timezone.utc)
|
)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
return datetime.utcfromtimestamp(0).replace(tzinfo=timezone.utc)
|
return datetime.fromtimestamp(0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def added_date(self):
|
def added_date(self):
|
||||||
"""Gets the photo added date."""
|
"""Gets the photo added date."""
|
||||||
return datetime.utcfromtimestamp(
|
return datetime.fromtimestamp(
|
||||||
self._asset_record["fields"]["addedDate"]["value"] / 1000.0
|
self._asset_record["fields"]["addedDate"]["value"] / 1000.0, tz=UTC
|
||||||
).replace(tzinfo=timezone.utc)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def dimensions(self):
|
def dimensions(self):
|
||||||
|
|
|
@ -4,7 +4,7 @@ import time
|
||||||
import uuid
|
import uuid
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from tzlocal import get_localzone_name
|
from tzlocal import get_localzone
|
||||||
|
|
||||||
|
|
||||||
class RemindersService:
|
class RemindersService:
|
||||||
|
@ -24,7 +24,7 @@ class RemindersService:
|
||||||
"""Refresh data."""
|
"""Refresh data."""
|
||||||
params_reminders = dict(self._params)
|
params_reminders = dict(self._params)
|
||||||
params_reminders.update(
|
params_reminders.update(
|
||||||
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone_name()}
|
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Open reminders
|
# Open reminders
|
||||||
|
@ -76,7 +76,7 @@ class RemindersService:
|
||||||
|
|
||||||
params_reminders = dict(self._params)
|
params_reminders = dict(self._params)
|
||||||
params_reminders.update(
|
params_reminders.update(
|
||||||
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone_name()}
|
{"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone}
|
||||||
)
|
)
|
||||||
|
|
||||||
due_dates = None
|
due_dates = None
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 88
|
line-length = 88
|
||||||
target-version = ["py37", "py38", "py39", "py310"]
|
target-version = ["py36", "py37", "py38", "py39", "py310"]
|
||||||
exclude = '''
|
exclude = '''
|
||||||
|
|
||||||
(
|
(
|
||||||
|
|
|
@ -2,5 +2,6 @@ requests>=2.24.0
|
||||||
keyring>=21.4.0
|
keyring>=21.4.0
|
||||||
keyrings.alt>=3.5.2
|
keyrings.alt>=3.5.2
|
||||||
click>=7.1.2
|
click>=7.1.2
|
||||||
tzlocal>=4.0
|
tzlocal==2.1
|
||||||
|
pytz>=2020.1
|
||||||
certifi>=2020.6.20
|
certifi>=2020.6.20
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
black==22.1.0
|
black==22.1.0
|
||||||
pylint==2.12.2
|
pytest
|
||||||
|
mock
|
||||||
|
pylint>=2.6.0
|
||||||
pylint-strict-informational==0.1
|
pylint-strict-informational==0.1
|
||||||
pytest==7.0.1
|
|
||||||
|
|
5
setup.py
5
setup.py
|
@ -4,7 +4,7 @@
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
REPO_URL = "https://github.com/picklepete/pyicloud"
|
REPO_URL = "https://github.com/picklepete/pyicloud"
|
||||||
VERSION = "1.0.0"
|
VERSION = "0.10.2"
|
||||||
|
|
||||||
with open("requirements.txt") as f:
|
with open("requirements.txt") as f:
|
||||||
required = f.read().splitlines()
|
required = f.read().splitlines()
|
||||||
|
@ -22,7 +22,7 @@ setup(
|
||||||
maintainer="The PyiCloud Authors",
|
maintainer="The PyiCloud Authors",
|
||||||
packages=find_packages(include=["pyicloud*"]),
|
packages=find_packages(include=["pyicloud*"]),
|
||||||
install_requires=required,
|
install_requires=required,
|
||||||
python_requires=">=3.7",
|
python_requires=">=3.6",
|
||||||
license="MIT",
|
license="MIT",
|
||||||
classifiers=[
|
classifiers=[
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
|
@ -32,6 +32,7 @@ setup(
|
||||||
"Programming Language :: Python",
|
"Programming Language :: Python",
|
||||||
"Programming Language :: Python :: 3 :: Only",
|
"Programming Language :: Python :: 3 :: Only",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.6",
|
||||||
"Programming Language :: Python :: 3.7",
|
"Programming Language :: Python :: 3.7",
|
||||||
"Programming Language :: Python :: 3.8",
|
"Programming Language :: Python :: 3.8",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
"""Library tests."""
|
"""Library tests."""
|
||||||
import json
|
import json
|
||||||
from requests import Response
|
from requests import Session, Response
|
||||||
|
|
||||||
from pyicloud import base
|
from pyicloud import base
|
||||||
|
from pyicloud.exceptions import PyiCloudFailedLoginException
|
||||||
|
from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager, AppleDevice
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
AUTHENTICATED_USER,
|
AUTHENTICATED_USER,
|
||||||
|
@ -40,7 +42,6 @@ class ResponseMock(Response):
|
||||||
"""Mocked Response."""
|
"""Mocked Response."""
|
||||||
|
|
||||||
def __init__(self, result, status_code=200, **kwargs):
|
def __init__(self, result, status_code=200, **kwargs):
|
||||||
"""Set up response mock."""
|
|
||||||
Response.__init__(self)
|
Response.__init__(self)
|
||||||
self.result = result
|
self.result = result
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
|
@ -49,7 +50,6 @@ class ResponseMock(Response):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def text(self):
|
def text(self):
|
||||||
"""Return text."""
|
|
||||||
return json.dumps(self.result)
|
return json.dumps(self.result)
|
||||||
|
|
||||||
|
|
||||||
|
@ -57,7 +57,6 @@ class PyiCloudSessionMock(base.PyiCloudSession):
|
||||||
"""Mocked PyiCloudSession."""
|
"""Mocked PyiCloudSession."""
|
||||||
|
|
||||||
def request(self, method, url, **kwargs):
|
def request(self, method, url, **kwargs):
|
||||||
"""Make the request."""
|
|
||||||
params = kwargs.get("params")
|
params = kwargs.get("params")
|
||||||
headers = kwargs.get("headers")
|
headers = kwargs.get("headers")
|
||||||
data = json.loads(kwargs.get("data", "{}"))
|
data = json.loads(kwargs.get("data", "{}"))
|
||||||
|
@ -170,7 +169,6 @@ class PyiCloudServiceMock(base.PyiCloudService):
|
||||||
client_id=None,
|
client_id=None,
|
||||||
with_family=True,
|
with_family=True,
|
||||||
):
|
):
|
||||||
"""Set up pyicloud service mock."""
|
|
||||||
base.PyiCloudSession = PyiCloudSessionMock
|
base.PyiCloudSession = PyiCloudSessionMock
|
||||||
base.PyiCloudService.__init__(
|
base.PyiCloudService.__init__(
|
||||||
self, apple_id, password, cookie_directory, verify, client_id, with_family
|
self, apple_id, password, cookie_directory, verify, client_id, with_family
|
||||||
|
|
|
@ -6,12 +6,11 @@ from .const import AUTHENTICATED_USER, VALID_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
class AccountServiceTest(TestCase):
|
class AccountServiceTest(TestCase):
|
||||||
"""Account service tests."""
|
"""Account service tests"""
|
||||||
|
|
||||||
service = None
|
service = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up tests."""
|
|
||||||
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD).account
|
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD).account
|
||||||
|
|
||||||
def test_repr(self):
|
def test_repr(self):
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
"""Cmdline tests."""
|
"""Cmdline tests."""
|
||||||
|
from pyicloud import cmdline
|
||||||
|
from . import PyiCloudServiceMock
|
||||||
|
from .const import AUTHENTICATED_USER, REQUIRES_2FA_USER, VALID_PASSWORD, VALID_2FA_CODE
|
||||||
|
from .const_findmyiphone import FMI_FAMILY_WORKING
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
|
import pytest
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
|
||||||
from pyicloud import cmdline
|
|
||||||
|
|
||||||
from . import PyiCloudServiceMock
|
|
||||||
from .const import AUTHENTICATED_USER, REQUIRES_2FA_USER, VALID_2FA_CODE, VALID_PASSWORD
|
|
||||||
from .const_findmyiphone import FMI_FAMILY_WORKING
|
|
||||||
|
|
||||||
|
|
||||||
class TestCmdline(TestCase):
|
class TestCmdline(TestCase):
|
||||||
"""Cmdline test cases."""
|
"""Cmdline test cases."""
|
||||||
|
@ -18,7 +17,6 @@ class TestCmdline(TestCase):
|
||||||
main = None
|
main = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up tests."""
|
|
||||||
cmdline.PyiCloudService = PyiCloudServiceMock
|
cmdline.PyiCloudService = PyiCloudServiceMock
|
||||||
self.main = cmdline.main
|
self.main = cmdline.main
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,16 @@
|
||||||
"""Drive service tests."""
|
"""Drive service tests."""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from . import PyiCloudServiceMock
|
from . import PyiCloudServiceMock
|
||||||
from .const import AUTHENTICATED_USER, VALID_PASSWORD
|
from .const import AUTHENTICATED_USER, VALID_PASSWORD
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# pylint: disable=pointless-statement
|
||||||
class DriveServiceTest(TestCase):
|
class DriveServiceTest(TestCase):
|
||||||
"""Drive service tests."""
|
"""Drive service tests"""
|
||||||
|
|
||||||
service = None
|
service = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up tests."""
|
|
||||||
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD)
|
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD)
|
||||||
|
|
||||||
def test_root(self):
|
def test_root(self):
|
||||||
|
@ -42,7 +39,7 @@ class DriveServiceTest(TestCase):
|
||||||
def test_folder_not_exists(self):
|
def test_folder_not_exists(self):
|
||||||
"""Test the /not_exists folder."""
|
"""Test the /not_exists folder."""
|
||||||
with pytest.raises(KeyError, match="No child named 'not_exists' exists"):
|
with pytest.raises(KeyError, match="No child named 'not_exists' exists"):
|
||||||
self.service.drive["not_exists"] # pylint: disable=pointless-statement
|
self.service.drive["not_exists"]
|
||||||
|
|
||||||
def test_folder(self):
|
def test_folder(self):
|
||||||
"""Test the /pyiCloud folder."""
|
"""Test the /pyiCloud folder."""
|
||||||
|
|
|
@ -1,17 +1,15 @@
|
||||||
"""Find My iPhone service tests."""
|
"""Find My iPhone service tests."""
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from . import PyiCloudServiceMock
|
from . import PyiCloudServiceMock
|
||||||
from .const import AUTHENTICATED_USER, VALID_PASSWORD
|
from .const import AUTHENTICATED_USER, VALID_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
class FindMyiPhoneServiceTest(TestCase):
|
class FindMyiPhoneServiceTest(TestCase):
|
||||||
"""Find My iPhone service tests."""
|
"""Find My iPhone service tests"""
|
||||||
|
|
||||||
service = None
|
service = None
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up tests."""
|
|
||||||
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD)
|
self.service = PyiCloudServiceMock(AUTHENTICATED_USER, VALID_PASSWORD)
|
||||||
|
|
||||||
def test_devices(self):
|
def test_devices(self):
|
||||||
|
|
5
tox.ini
5
tox.ini
|
@ -1,10 +1,11 @@
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py37, py38, py39, py310, lint
|
envlist = py36, py37, py38, py39, py310, lint
|
||||||
skip_missing_interpreters = True
|
skip_missing_interpreters = True
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
python =
|
python =
|
||||||
3.7: py37, lint
|
3.6: py36, lint
|
||||||
|
3.7: py37
|
||||||
3.8: py38
|
3.8: py38
|
||||||
3.9: py39
|
3.9: py39
|
||||||
3.10: py310
|
3.10: py310
|
||||||
|
|
Loading…
Reference in a new issue