Support Python 3.6 to 3.10 (#371)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Quentin POLLET <polletquentin74@me.com>
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
Hugo 2022-02-16 20:00:29 +01:00 committed by GitHub
parent b6356a00bc
commit 592ff464c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 300 additions and 393 deletions

View file

@ -15,6 +15,8 @@ jobs:
- "3.6" - "3.6"
- "3.7" - "3.7"
- "3.8" - "3.8"
- "3.9"
- "3.10"
steps: steps:
- uses: actions/checkout@v2.4.0 - uses: actions/checkout@v2.4.0

View file

@ -143,7 +143,7 @@ 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.
>>> api.iphone.location() >>> api.iphone.location()
{u'timeStamp': 1357753796553, u'locationFinished': True, u'longitude': -0.14189, u'positionType': u'GPS', u'locationType': None, u'latitude': 51.501364, u'isOld': False, u'horizontalAccuracy': 5.0} {'timeStamp': 1357753796553, 'locationFinished': True, 'longitude': -0.14189, 'positionType': 'GPS', 'locationType': None, 'latitude': 51.501364, 'isOld': False, 'horizontalAccuracy': 5.0}
Status Status
****** ******
@ -151,7 +151,7 @@ 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.
>>> api.iphone.status() >>> api.iphone.status()
{'deviceDisplayName': u'iPhone 5', 'deviceStatus': u'200', 'batteryLevel': 0.6166913, 'name': u"Peter's iPhone"} {'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.
@ -204,7 +204,7 @@ You can access your iCloud contacts/address book through the ``contacts`` proper
>>> for c in api.contacts.all(): >>> for c in api.contacts.all():
>>> print c.get('firstName'), c.get('phones') >>> print c.get('firstName'), c.get('phones')
John [{u'field': u'+1 555-55-5555-5', u'label': u'MOBILE'}] 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.
@ -215,21 +215,21 @@ 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:
>>> api.files.dir() >>> api.files.dir()
[u'.do-not-delete', ['.do-not-delete',
u'.localized', '.localized',
u'com~apple~Notes', 'com~apple~Notes',
u'com~apple~Preview', 'com~apple~Preview',
u'com~apple~mail', 'com~apple~mail',
u'com~apple~shoebox', 'com~apple~shoebox',
u'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:
>>> api.files['com~apple~Notes'] >>> api.files['com~apple~Notes']
<Folder: u'com~apple~Notes'> <Folder: 'com~apple~Notes'>
>>> api.files['com~apple~Notes'].type >>> api.files['com~apple~Notes'].type
u'folder' 'folder'
>>> api.files['com~apple~Notes'].dir() >>> api.files['com~apple~Notes'].dir()
[u'Documents'] [u'Documents']
>>> api.files['com~apple~Notes']['Documents'].dir() >>> api.files['com~apple~Notes']['Documents'].dir()
@ -336,7 +336,7 @@ Note: Consider using ``shutil.copyfile`` or another buffered strategy for downlo
Information about each version can be accessed through the ``versions`` property: Information about each version can be accessed through the ``versions`` property:
>>> photo.versions.keys() >>> photo.versions.keys()
[u'medium', u'original', u'thumb'] ['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()``:

View file

@ -1,5 +1,4 @@
"""Library base file.""" """Library base file."""
from six import PY2, string_types
from uuid import uuid1 from uuid import uuid1
import inspect import inspect
import json import json
@ -45,7 +44,7 @@ class PyiCloudPasswordFilter(logging.Filter):
"""Password log hider.""" """Password log hider."""
def __init__(self, password): def __init__(self, password):
super(PyiCloudPasswordFilter, self).__init__(password) super().__init__(password)
def filter(self, record): def filter(self, record):
message = record.getMessage() message = record.getMessage()
@ -61,7 +60,7 @@ class PyiCloudSession(Session):
def __init__(self, service): def __init__(self, service):
self.service = service self.service = service
Session.__init__(self) super().__init__()
def request(self, method, url, **kwargs): # pylint: disable=arguments-differ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ
@ -72,24 +71,24 @@ class PyiCloudSession(Session):
if self.service.password_filter not in request_logger.filters: if self.service.password_filter not in request_logger.filters:
request_logger.addFilter(self.service.password_filter) request_logger.addFilter(self.service.password_filter)
request_logger.debug("%s %s %s" % (method, url, kwargs.get("data", ""))) request_logger.debug("%s %s %s", method, url, kwargs.get("data", ""))
has_retried = kwargs.get("retried") has_retried = kwargs.get("retried")
kwargs.pop("retried", None) kwargs.pop("retried", None)
response = super(PyiCloudSession, self).request(method, url, **kwargs) response = super().request(method, url, **kwargs)
content_type = response.headers.get("Content-Type", "").split(";")[0] content_type = response.headers.get("Content-Type", "").split(";")[0]
json_mimetypes = ["application/json", "text/json"] json_mimetypes = ["application/json", "text/json"]
for header in HEADER_DATA: for header, value in HEADER_DATA.items():
if response.headers.get(header): if response.headers.get(header):
session_arg = HEADER_DATA[header] session_arg = value
self.service.session_data.update( self.service.session_data.update(
{session_arg: response.headers.get(header)} {session_arg: response.headers.get(header)}
) )
# Save session_data to file # Save session_data to file
with open(self.service.session_path, "w") as outfile: with open(self.service.session_path, "w", encoding="utf-8") as outfile:
json.dump(self.service.session_data, outfile) json.dump(self.service.session_data, outfile)
LOGGER.debug("Saved session data to file") LOGGER.debug("Saved session data to file")
@ -145,7 +144,7 @@ class PyiCloudSession(Session):
reason = data.get("errorMessage") reason = data.get("errorMessage")
reason = reason or data.get("reason") reason = reason or data.get("reason")
reason = reason or data.get("errorReason") reason = reason or data.get("errorReason")
if not reason and isinstance(data.get("error"), string_types): if not reason and isinstance(data.get("error"), str):
reason = data.get("error") reason = data.get("error")
if not reason and data.get("error"): if not reason and data.get("error"):
reason = "Unknown reason" reason = "Unknown reason"
@ -187,7 +186,7 @@ class PyiCloudSession(Session):
raise api_error raise api_error
class PyiCloudService(object): class PyiCloudService:
""" """
A base authentication class for the iCloud service. Handles the A base authentication class for the iCloud service. Handles the
authentication required to access iCloud services. authentication required to access iCloud services.
@ -239,7 +238,7 @@ class PyiCloudService(object):
self.session_data = {} self.session_data = {}
try: try:
with open(self.session_path) as session_f: with open(self.session_path, encoding="utf-8") as session_f:
self.session_data = json.load(session_f) self.session_data = json.load(session_f)
except: # pylint: disable=bare-except except: # pylint: disable=bare-except
LOGGER.info("Session file does not exist") LOGGER.info("Session file does not exist")
@ -320,7 +319,7 @@ class PyiCloudService(object):
headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id")
try: try:
req = self.session.post( self.session.post(
"%s/signin" % self.AUTH_ENDPOINT, "%s/signin" % self.AUTH_ENDPOINT,
params={"isRememberMeEnabled": "true"}, params={"isRememberMeEnabled": "true"},
data=json.dumps(data), data=json.dumps(data),
@ -328,7 +327,7 @@ class PyiCloudService(object):
) )
except PyiCloudAPIResponseException as error: except PyiCloudAPIResponseException as error:
msg = "Invalid email/password combination." msg = "Invalid email/password combination."
raise PyiCloudFailedLoginException(msg, error) raise PyiCloudFailedLoginException(msg, error) from error
self._authenticate_with_token() self._authenticate_with_token()
@ -352,7 +351,7 @@ class PyiCloudService(object):
self.data = req.json() self.data = req.json()
except PyiCloudAPIResponseException as error: except PyiCloudAPIResponseException as error:
msg = "Invalid authentication token." msg = "Invalid authentication token."
raise PyiCloudFailedLoginException(msg, error) raise PyiCloudFailedLoginException(msg, error) from error
def _authenticate_with_credentials_service(self, service): def _authenticate_with_credentials_service(self, service):
"""Authenticate to a specific service using credentials.""" """Authenticate to a specific service using credentials."""
@ -370,7 +369,7 @@ class PyiCloudService(object):
self.data = self._validate_token() self.data = self._validate_token()
except PyiCloudAPIResponseException as error: except PyiCloudAPIResponseException as error:
msg = "Invalid email/password combination." msg = "Invalid email/password combination."
raise PyiCloudFailedLoginException(msg, error) raise PyiCloudFailedLoginException(msg, error) from error
def _validate_token(self): def _validate_token(self):
"""Checks if the current access token is still valid.""" """Checks if the current access token is still valid."""
@ -517,7 +516,8 @@ class PyiCloudService(object):
try: try:
self.session.get( self.session.get(
"%s/2sv/trust" % self.AUTH_ENDPOINT, headers=headers, f"{self.AUTH_ENDPOINT}/2sv/trust",
headers=headers,
) )
self._authenticate_with_token() self._authenticate_with_token()
return True return True
@ -598,14 +598,8 @@ class PyiCloudService(object):
) )
return self._drive return self._drive
def __unicode__(self):
return "iCloud API: %s" % self.user.get("accountName")
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"iCloud API: {self.user.get('apple_id')}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s>" % str(self) return f"<{self}>"

View file

@ -1,11 +1,8 @@
#! /usr/bin/env python #! /usr/bin/env python
# -*- coding: utf-8 -*-
""" """
A Command Line Wrapper to allow easy use of pyicloud for A Command Line Wrapper to allow easy use of pyicloud for
command line scripts, and related. command line scripts, and related.
""" """
from __future__ import print_function
from builtins import input
import argparse import argparse
import pickle import pickle
import sys import sys
@ -16,7 +13,6 @@ from pyicloud import PyiCloudService
from pyicloud.exceptions import PyiCloudFailedLoginException from pyicloud.exceptions import PyiCloudFailedLoginException
from . import utils from . import utils
DEVICE_ERROR = "Please use the --device switch to indicate which device to use." DEVICE_ERROR = "Please use the --device switch to indicate which device to use."
@ -28,9 +24,8 @@ def create_pickled_data(idevice, filename):
This allows the data to be used without resorting to screen / pipe This allows the data to be used without resorting to screen / pipe
scrapping. scrapping.
""" """
pickle_file = open(filename, "wb") with open(filename, "wb") as pickle_file:
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
pickle_file.close()
def main(args=None): def main(args=None):
@ -251,7 +246,7 @@ def main(args=None):
print("") print("")
break break
except PyiCloudFailedLoginException: except PyiCloudFailedLoginException as err:
# If they have a stored password; we just used it and # If they have a stored password; we just used it and
# it did not work; let's delete it if there is one. # it did not work; let's delete it if there is one.
if utils.password_exists_in_keyring(username): if utils.password_exists_in_keyring(username):
@ -264,7 +259,7 @@ def main(args=None):
failure_count += 1 failure_count += 1
if failure_count >= 3: if failure_count >= 3:
raise RuntimeError(message) raise RuntimeError(message) from err
print(message, file=sys.stderr) print(message, file=sys.stderr)

View file

@ -18,7 +18,7 @@ class PyiCloudAPIResponseException(PyiCloudException):
if retry: if retry:
message += ". Retrying ..." message += ". Retrying ..."
super(PyiCloudAPIResponseException, self).__init__(message) super().__init__(message)
class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException): class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException):
@ -36,7 +36,7 @@ class PyiCloud2SARequiredException(PyiCloudException):
"""iCloud 2SA required exception.""" """iCloud 2SA required exception."""
def __init__(self, apple_id): def __init__(self, apple_id):
message = "Two-step authentication required for account: %s" % apple_id message = "Two-step authentication required for account: %s" % apple_id
super(PyiCloud2SARequiredException, self).__init__(message) super().__init__(message)
class PyiCloudNoStoredPasswordAvailableException(PyiCloudException): class PyiCloudNoStoredPasswordAvailableException(PyiCloudException):

View file

@ -1,12 +1,10 @@
"""Account service.""" """Account service."""
from __future__ import division
from six import PY2, python_2_unicode_compatible
from collections import OrderedDict from collections import OrderedDict
from pyicloud.utils import underscore_to_camelcase from pyicloud.utils import underscore_to_camelcase
class AccountService(object): class AccountService:
"""The 'Account' iCloud service.""" """The 'Account' iCloud service."""
def __init__(self, service_root, session, params): def __init__(self, service_root, session, params):
@ -68,44 +66,31 @@ class AccountService(object):
return self._storage return self._storage
def __unicode__(self): def __str__(self):
return "{devices: %s, family: %s, storage: %s bytes free}" % ( return "{{devices: {}, family: {}, storage: {} bytes free}}".format(
len(self.devices), len(self.devices),
len(self.family), len(self.family),
self.storage.usage.available_storage_in_bytes, self.storage.usage.available_storage_in_bytes,
) )
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"
@python_2_unicode_compatible
class AccountDevice(dict): class AccountDevice(dict):
"""Account device.""" """Account device."""
def __getattr__(self, key): def __getattr__(self, key):
return self[underscore_to_camelcase(key)] return self[underscore_to_camelcase(key)]
def __unicode__(self):
return "{model: %s, name: %s}" % (self.model_display_name, self.name)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"{{model: {self.model_display_name}, name: {self.name}}}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"
class FamilyMember(object): class FamilyMember:
"""A family member.""" """A family member."""
def __init__(self, member_info, session, params, acc_family_member_photo_url): def __init__(self, member_info, session, params, acc_family_member_photo_url):
@ -207,23 +192,17 @@ class FamilyMember(object):
return self._attrs[key] return self._attrs[key]
return getattr(self, key) return getattr(self, key)
def __unicode__(self): def __str__(self):
return "{name: %s, age_classification: %s}" % ( return "{{name: {}, age_classification: {}}}".format(
self.full_name, self.full_name,
self.age_classification, self.age_classification,
) )
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"
class AccountStorageUsageForMedia(object): class AccountStorageUsageForMedia:
"""Storage used for a specific media type into the account.""" """Storage used for a specific media type into the account."""
def __init__(self, usage_data): def __init__(self, usage_data):
@ -249,20 +228,14 @@ class AccountStorageUsageForMedia(object):
"""Gets the usage in bytes.""" """Gets the usage in bytes."""
return self.usage_data["usageInBytes"] return self.usage_data["usageInBytes"]
def __unicode__(self):
return "{key: %s, usage: %s bytes}" % (self.key, self.usage_in_bytes)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"{{key: {self.key}, usage: {self.usage_in_bytes} bytes}}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"
class AccountStorageUsage(object): class AccountStorageUsage:
"""Storage used for a specific media type into the account.""" """Storage used for a specific media type into the account."""
def __init__(self, usage_data, quota_data): def __init__(self, usage_data, quota_data):
@ -326,23 +299,17 @@ class AccountStorageUsage(object):
"""Gets the paid quota.""" """Gets the paid quota."""
return self.quota_data["paidQuota"] return self.quota_data["paidQuota"]
def __unicode__(self): def __str__(self):
return "%s%% used of %s bytes" % ( return "{}% used of {} bytes".format(
self.used_storage_in_percent, self.used_storage_in_percent,
self.total_storage_in_bytes, self.total_storage_in_bytes,
) )
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"
class AccountStorage(object): class AccountStorage:
"""Storage of the account.""" """Storage of the account."""
def __init__(self, storage_data): def __init__(self, storage_data):
@ -356,14 +323,8 @@ class AccountStorage(object):
usage_media usage_media
) )
def __unicode__(self):
return "{usage: %s, usages_by_media: %s}" % (self.usage, self.usages_by_media)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"{{usage: {self.usage}, usages_by_media: {self.usages_by_media}}}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {self}>"

View file

@ -1,12 +1,11 @@
"""Calendar service.""" """Calendar service."""
from __future__ import absolute_import
from datetime import datetime from datetime import datetime
from calendar import monthrange from calendar import monthrange
from tzlocal import get_localzone from tzlocal import get_localzone
class CalendarService(object): class CalendarService:
""" """
The 'Calendar' iCloud service, connects to iCloud and returns events. The 'Calendar' iCloud service, connects to iCloud and returns events.
""" """
@ -17,7 +16,7 @@ class CalendarService(object):
self._service_root = service_root self._service_root = service_root
self._calendar_endpoint = "%s/ca" % self._service_root self._calendar_endpoint = "%s/ca" % self._service_root
self._calendar_refresh_url = "%s/events" % self._calendar_endpoint self._calendar_refresh_url = "%s/events" % self._calendar_endpoint
self._calendar_event_detail_url = "%s/eventdetail" % self._calendar_endpoint self._calendar_event_detail_url = f"{self._calendar_endpoint}/eventdetail"
self._calendars = "%s/startup" % self._calendar_endpoint self._calendars = "%s/startup" % self._calendar_endpoint
self.response = {} self.response = {}
@ -29,7 +28,7 @@ class CalendarService(object):
""" """
params = dict(self.params) params = dict(self.params)
params.update({"lang": "en-us", "usertz": get_localzone().zone}) params.update({"lang": "en-us", "usertz": get_localzone().zone})
url = "%s/%s/%s" % (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()
return self.response["Event"][0] return self.response["Event"][0]

View file

@ -1,8 +1,7 @@
"""Contacts service.""" """Contacts service."""
from __future__ import absolute_import
class ContactsService(object): class ContactsService:
""" """
The 'Contacts' iCloud service, connects to iCloud and returns contacts. The 'Contacts' iCloud service, connects to iCloud and returns contacts.
""" """
@ -25,7 +24,11 @@ class ContactsService(object):
""" """
params_contacts = dict(self.params) params_contacts = dict(self.params)
params_contacts.update( params_contacts.update(
{"clientVersion": "2.1", "locale": "en_US", "order": "last,first",} {
"clientVersion": "2.1",
"locale": "en_US",
"order": "last,first",
}
) )
req = self.session.get(self._contacts_refresh_url, params=params_contacts) req = self.session.get(self._contacts_refresh_url, params=params_contacts)
self.response = req.json() self.response = req.json()

View file

@ -7,10 +7,9 @@ import os
import time import time
from re import search from re import search
from requests import Response from requests import Response
from six import PY2
class DriveService(object): class DriveService:
"""The 'Drive' iCloud service.""" """The 'Drive' iCloud service."""
def __init__(self, service_root, document_root, session, params): def __init__(self, service_root, document_root, session, params):
@ -108,7 +107,10 @@ class DriveService(object):
"command": "add_file", "command": "add_file",
"create_short_guid": True, "create_short_guid": True,
"document_id": document_id, "document_id": document_id,
"path": {"starting_document_id": folder_id, "path": file_object.name,}, "path": {
"starting_document_id": folder_id,
"path": file_object.name,
},
"allow_conflict": True, "allow_conflict": True,
"file_flags": { "file_flags": {
"is_writable": True, "is_writable": True,
@ -153,7 +155,12 @@ class DriveService(object):
data=json.dumps( data=json.dumps(
{ {
"destinationDrivewsId": parent, "destinationDrivewsId": parent,
"folders": [{"clientId": self.params["clientId"], "name": name,}], "folders": [
{
"clientId": self.params["clientId"],
"name": name,
}
],
} }
), ),
) )
@ -165,7 +172,15 @@ class DriveService(object):
self._service_root + "/renameItems", self._service_root + "/renameItems",
params=self.params, params=self.params,
data=json.dumps( data=json.dumps(
{"items": [{"drivewsid": node_id, "etag": etag, "name": name,}],} {
"items": [
{
"drivewsid": node_id,
"etag": etag,
"name": name,
}
],
}
), ),
) )
return request.json() return request.json()
@ -203,7 +218,7 @@ class DriveService(object):
return self.root[key] return self.root[key]
class DriveNode(object): class DriveNode:
"""Drive node.""" """Drive node."""
def __init__(self, conn, data): def __init__(self, conn, data):
@ -215,7 +230,7 @@ class DriveNode(object):
def name(self): def name(self):
"""Gets the node name.""" """Gets the node name."""
if "extension" in self.data: if "extension" in self.data:
return "%s.%s" % (self.data["name"], self.data["extension"]) return "{}.{}".format(self.data["name"], self.data["extension"])
return self.data["name"] return self.data["name"]
@property @property
@ -270,7 +285,7 @@ class DriveNode(object):
return self.connection.get_file(self.data["docwsid"], **kwargs) return self.connection.get_file(self.data["docwsid"], **kwargs)
def upload(self, file_object, **kwargs): def upload(self, file_object, **kwargs):
""""Upload a new file.""" """Upload a new file."""
return self.connection.send_file(self.data["docwsid"], file_object, **kwargs) return self.connection.send_file(self.data["docwsid"], file_object, **kwargs)
def dir(self): def dir(self):
@ -304,20 +319,14 @@ class DriveNode(object):
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return self.get(key) return self.get(key)
except IndexError: except IndexError as i:
raise KeyError("No child named '%s' exists" % key) raise KeyError(f"No child named '{key}' exists") from i
def __unicode__(self):
return "{type: %s, name: %s}" % (self.type, self.name)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return rf"\{type: {self.type}, name: {self.name}\}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: %s>" % (type(self).__name__, str(self)) return f"<{type(self).__name__}: {str(self)}>"
def _date_to_utc(date): def _date_to_utc(date):

View file

@ -1,12 +1,10 @@
"""Find my iPhone service.""" """Find my iPhone service."""
import json import json
from six import PY2, text_type
from pyicloud.exceptions import PyiCloudNoDevicesException from pyicloud.exceptions import PyiCloudNoDevicesException
class FindMyiPhoneServiceManager(object): class FindMyiPhoneServiceManager:
"""The 'Find my iPhone' iCloud service """The 'Find my iPhone' iCloud service
This connects to iCloud and return phone data including the near-realtime This connects to iCloud and return phone data including the near-realtime
@ -69,29 +67,20 @@ class FindMyiPhoneServiceManager(object):
def __getitem__(self, key): def __getitem__(self, key):
if isinstance(key, int): if isinstance(key, int):
if PY2: key = list(self.keys())[key]
key = self.keys()[key]
else:
key = list(self.keys())[key]
return self._devices[key] return self._devices[key]
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self._devices, attr) return getattr(self._devices, attr)
def __unicode__(self):
return text_type(self._devices)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"{self._devices}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return text_type(self) return f"{self}"
class AppleDevice(object): class AppleDevice:
"""Apple device.""" """Apple device."""
def __init__( def __init__(
@ -200,16 +189,8 @@ class AppleDevice(object):
def __getattr__(self, attr): def __getattr__(self, attr):
return getattr(self.content, attr) return getattr(self.content, attr)
def __unicode__(self):
display_name = self["deviceDisplayName"]
name = self["name"]
return "%s: %s" % (display_name, name)
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return f"{self['deviceDisplayName']}: {self['name']}"
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<AppleDevice(%s)>" % str(self) return f"<AppleDevice({self})>"

View file

@ -1,18 +1,14 @@
"""Photo service.""" """Photo service."""
import json import json
import base64 import base64
from six import PY2 from urllib.parse import urlencode
# fmt: off
from six.moves.urllib.parse import urlencode # pylint: disable=bad-option-value,relative-import
# fmt: on
from datetime import datetime from datetime import datetime
from pyicloud.exceptions import PyiCloudServiceNotActivatedException from pyicloud.exceptions import PyiCloudServiceNotActivatedException
from pytz import UTC from pytz import UTC
class PhotosService(object): class PhotosService:
"""The 'Photos' iCloud service.""" """The 'Photos' iCloud service."""
SMART_FOLDERS = { SMART_FOLDERS = {
@ -139,7 +135,7 @@ class PhotosService(object):
self.params.update({"remapEnums": True, "getCurrentSyncToken": True}) self.params.update({"remapEnums": True, "getCurrentSyncToken": True})
url = "%s/records/query?%s" % (self.service_endpoint, urlencode(self.params)) url = f"{self.service_endpoint}/records/query?{urlencode(self.params)}"
json_data = ( json_data = (
'{"query":{"recordType":"CheckIndexingState"},' '{"query":{"recordType":"CheckIndexingState"},'
'"zoneID":{"zoneName":"PrimarySync"}}' '"zoneID":{"zoneName":"PrimarySync"}}'
@ -213,7 +209,7 @@ class PhotosService(object):
return self._albums return self._albums
def _fetch_folders(self): def _fetch_folders(self):
url = "%s/records/query?%s" % (self.service_endpoint, urlencode(self.params)) url = f"{self.service_endpoint}/records/query?{urlencode(self.params)}"
json_data = ( json_data = (
'{"query":{"recordType":"CPLAlbumByPositionLive"},' '{"query":{"recordType":"CPLAlbumByPositionLive"},'
'"zoneID":{"zoneName":"PrimarySync"}}' '"zoneID":{"zoneName":"PrimarySync"}}'
@ -232,7 +228,7 @@ class PhotosService(object):
return self.albums["All Photos"] return self.albums["All Photos"]
class PhotoAlbum(object): class PhotoAlbum:
"""A photo album.""" """A photo album."""
def __init__( def __init__(
@ -265,7 +261,7 @@ class PhotoAlbum(object):
def __len__(self): def __len__(self):
if self._len is None: if self._len is None:
url = "%s/internal/records/query/batch?%s" % ( url = "{}/internal/records/query/batch?{}".format(
self.service.service_endpoint, self.service.service_endpoint,
urlencode(self.service.params), urlencode(self.service.params),
) )
@ -273,22 +269,22 @@ class PhotoAlbum(object):
url, url,
data=json.dumps( data=json.dumps(
{ {
u"batch": [ "batch": [
{ {
u"resultsLimit": 1, "resultsLimit": 1,
u"query": { "query": {
u"filterBy": { "filterBy": {
u"fieldName": u"indexCountID", "fieldName": "indexCountID",
u"fieldValue": { "fieldValue": {
u"type": u"STRING_LIST", "type": "STRING_LIST",
u"value": [self.obj_type], "value": [self.obj_type],
}, },
u"comparator": u"IN", "comparator": "IN",
}, },
u"recordType": u"HyperionIndexCountLookup", "recordType": "HyperionIndexCountLookup",
}, },
u"zoneWide": True, "zoneWide": True,
u"zoneID": {u"zoneName": u"PrimarySync"}, "zoneID": {"zoneName": "PrimarySync"},
} }
] ]
} }
@ -352,122 +348,122 @@ class PhotoAlbum(object):
def _list_query_gen(self, offset, list_type, direction, query_filter=None): def _list_query_gen(self, offset, list_type, direction, query_filter=None):
query = { query = {
u"query": { "query": {
u"filterBy": [ "filterBy": [
{ {
u"fieldName": u"startRank", "fieldName": "startRank",
u"fieldValue": {u"type": u"INT64", u"value": offset}, "fieldValue": {"type": "INT64", "value": offset},
u"comparator": u"EQUALS", "comparator": "EQUALS",
}, },
{ {
u"fieldName": u"direction", "fieldName": "direction",
u"fieldValue": {u"type": u"STRING", u"value": direction}, "fieldValue": {"type": "STRING", "value": direction},
u"comparator": u"EQUALS", "comparator": "EQUALS",
}, },
], ],
u"recordType": list_type, "recordType": list_type,
}, },
u"resultsLimit": self.page_size * 2, "resultsLimit": self.page_size * 2,
u"desiredKeys": [ "desiredKeys": [
u"resJPEGFullWidth", "resJPEGFullWidth",
u"resJPEGFullHeight", "resJPEGFullHeight",
u"resJPEGFullFileType", "resJPEGFullFileType",
u"resJPEGFullFingerprint", "resJPEGFullFingerprint",
u"resJPEGFullRes", "resJPEGFullRes",
u"resJPEGLargeWidth", "resJPEGLargeWidth",
u"resJPEGLargeHeight", "resJPEGLargeHeight",
u"resJPEGLargeFileType", "resJPEGLargeFileType",
u"resJPEGLargeFingerprint", "resJPEGLargeFingerprint",
u"resJPEGLargeRes", "resJPEGLargeRes",
u"resJPEGMedWidth", "resJPEGMedWidth",
u"resJPEGMedHeight", "resJPEGMedHeight",
u"resJPEGMedFileType", "resJPEGMedFileType",
u"resJPEGMedFingerprint", "resJPEGMedFingerprint",
u"resJPEGMedRes", "resJPEGMedRes",
u"resJPEGThumbWidth", "resJPEGThumbWidth",
u"resJPEGThumbHeight", "resJPEGThumbHeight",
u"resJPEGThumbFileType", "resJPEGThumbFileType",
u"resJPEGThumbFingerprint", "resJPEGThumbFingerprint",
u"resJPEGThumbRes", "resJPEGThumbRes",
u"resVidFullWidth", "resVidFullWidth",
u"resVidFullHeight", "resVidFullHeight",
u"resVidFullFileType", "resVidFullFileType",
u"resVidFullFingerprint", "resVidFullFingerprint",
u"resVidFullRes", "resVidFullRes",
u"resVidMedWidth", "resVidMedWidth",
u"resVidMedHeight", "resVidMedHeight",
u"resVidMedFileType", "resVidMedFileType",
u"resVidMedFingerprint", "resVidMedFingerprint",
u"resVidMedRes", "resVidMedRes",
u"resVidSmallWidth", "resVidSmallWidth",
u"resVidSmallHeight", "resVidSmallHeight",
u"resVidSmallFileType", "resVidSmallFileType",
u"resVidSmallFingerprint", "resVidSmallFingerprint",
u"resVidSmallRes", "resVidSmallRes",
u"resSidecarWidth", "resSidecarWidth",
u"resSidecarHeight", "resSidecarHeight",
u"resSidecarFileType", "resSidecarFileType",
u"resSidecarFingerprint", "resSidecarFingerprint",
u"resSidecarRes", "resSidecarRes",
u"itemType", "itemType",
u"dataClassType", "dataClassType",
u"filenameEnc", "filenameEnc",
u"originalOrientation", "originalOrientation",
u"resOriginalWidth", "resOriginalWidth",
u"resOriginalHeight", "resOriginalHeight",
u"resOriginalFileType", "resOriginalFileType",
u"resOriginalFingerprint", "resOriginalFingerprint",
u"resOriginalRes", "resOriginalRes",
u"resOriginalAltWidth", "resOriginalAltWidth",
u"resOriginalAltHeight", "resOriginalAltHeight",
u"resOriginalAltFileType", "resOriginalAltFileType",
u"resOriginalAltFingerprint", "resOriginalAltFingerprint",
u"resOriginalAltRes", "resOriginalAltRes",
u"resOriginalVidComplWidth", "resOriginalVidComplWidth",
u"resOriginalVidComplHeight", "resOriginalVidComplHeight",
u"resOriginalVidComplFileType", "resOriginalVidComplFileType",
u"resOriginalVidComplFingerprint", "resOriginalVidComplFingerprint",
u"resOriginalVidComplRes", "resOriginalVidComplRes",
u"isDeleted", "isDeleted",
u"isExpunged", "isExpunged",
u"dateExpunged", "dateExpunged",
u"remappedRef", "remappedRef",
u"recordName", "recordName",
u"recordType", "recordType",
u"recordChangeTag", "recordChangeTag",
u"masterRef", "masterRef",
u"adjustmentRenderType", "adjustmentRenderType",
u"assetDate", "assetDate",
u"addedDate", "addedDate",
u"isFavorite", "isFavorite",
u"isHidden", "isHidden",
u"orientation", "orientation",
u"duration", "duration",
u"assetSubtype", "assetSubtype",
u"assetSubtypeV2", "assetSubtypeV2",
u"assetHDRType", "assetHDRType",
u"burstFlags", "burstFlags",
u"burstFlagsExt", "burstFlagsExt",
u"burstId", "burstId",
u"captionEnc", "captionEnc",
u"locationEnc", "locationEnc",
u"locationV2Enc", "locationV2Enc",
u"locationLatitude", "locationLatitude",
u"locationLongitude", "locationLongitude",
u"adjustmentType", "adjustmentType",
u"timeZoneOffset", "timeZoneOffset",
u"vidComplDurValue", "vidComplDurValue",
u"vidComplDurScale", "vidComplDurScale",
u"vidComplDispValue", "vidComplDispValue",
u"vidComplDispScale", "vidComplDispScale",
u"vidComplVisibilityState", "vidComplVisibilityState",
u"customRenderedValue", "customRenderedValue",
u"containerId", "containerId",
u"itemId", "itemId",
u"position", "position",
u"isKeyAsset", "isKeyAsset",
], ],
u"zoneID": {u"zoneName": u"PrimarySync"}, "zoneID": {"zoneName": "PrimarySync"},
} }
if query_filter: if query_filter:
@ -475,20 +471,14 @@ class PhotoAlbum(object):
return query return query
def __unicode__(self): def __str__(self):
return self.title return self.title
def __str__(self):
as_unicode = self.__unicode__()
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: '%s'>" % (type(self).__name__, self) return f"<{type(self).__name__}: '{self}'>"
class PhotoAsset(object): class PhotoAsset:
"""A photo.""" """A photo."""
def __init__(self, service, master_record, asset_record): def __init__(self, service, master_record, asset_record):
@ -499,15 +489,15 @@ class PhotoAsset(object):
self._versions = None self._versions = None
PHOTO_VERSION_LOOKUP = { PHOTO_VERSION_LOOKUP = {
u"original": u"resOriginal", "original": "resOriginal",
u"medium": u"resJPEGMed", "medium": "resJPEGMed",
u"thumb": u"resJPEGThumb", "thumb": "resJPEGThumb",
} }
VIDEO_VERSION_LOOKUP = { VIDEO_VERSION_LOOKUP = {
u"original": u"resOriginal", "original": "resOriginal",
u"medium": u"resVidMed", "medium": "resVidMed",
u"thumb": u"resVidSmall", "thumb": "resVidSmall",
} }
@property @property
@ -639,11 +629,11 @@ class PhotoAsset(object):
endpoint = self._service.service_endpoint endpoint = self._service.service_endpoint
params = urlencode(self._service.params) params = urlencode(self._service.params)
url = "%s/records/modify?%s" % (endpoint, params) url = f"{endpoint}/records/modify?{params}"
return self._service.session.post( return self._service.session.post(
url, data=json_data, headers={"Content-type": "text/plain"} url, data=json_data, headers={"Content-type": "text/plain"}
) )
def __repr__(self): def __repr__(self):
return "<%s: id=%s>" % (type(self).__name__, self.id) return f"<{type(self).__name__}: id={self.id}>"

View file

@ -1,5 +1,4 @@
"""Reminders service.""" """Reminders service."""
from __future__ import absolute_import
from datetime import datetime from datetime import datetime
import time import time
import uuid import uuid
@ -8,7 +7,7 @@ import json
from tzlocal import get_localzone from tzlocal import get_localzone
class RemindersService(object): class RemindersService:
"""The 'Reminders' iCloud service.""" """The 'Reminders' iCloud service."""
def __init__(self, service_root, session, params): def __init__(self, service_root, session, params):

View file

@ -1,9 +1,8 @@
"""File service.""" """File service."""
from datetime import datetime from datetime import datetime
from six import PY2
class UbiquityService(object): class UbiquityService:
"""The 'Ubiquity' iCloud service.""" """The 'Ubiquity' iCloud service."""
def __init__(self, service_root, session, params): def __init__(self, service_root, session, params):
@ -46,7 +45,7 @@ class UbiquityService(object):
return self.root[key] return self.root[key]
class UbiquityNode(object): class UbiquityNode:
"""Ubiquity node.""" """Ubiquity node."""
def __init__(self, conn, data): def __init__(self, conn, data):
@ -104,17 +103,11 @@ class UbiquityNode(object):
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return self.get(key) return self.get(key)
except IndexError: except IndexError as i:
raise KeyError("No child named %s exists" % key) raise KeyError(f"No child named {key} exists") from i
def __unicode__(self):
return self.name
def __str__(self): def __str__(self):
as_unicode = self.__unicode__() return self.name
if PY2:
return as_unicode.encode("utf-8", "ignore")
return as_unicode
def __repr__(self): def __repr__(self):
return "<%s: '%s'>" % (self.type.capitalize(), self) return f"<{self.type.capitalize()}: '{self}'>"

View file

@ -1,7 +1,7 @@
"""Utils.""" """Utils."""
import getpass import getpass
import keyring import keyring
from sys import stdout import sys
from .exceptions import PyiCloudNoStoredPasswordAvailableException from .exceptions import PyiCloudNoStoredPasswordAvailableException
@ -9,7 +9,7 @@ from .exceptions import PyiCloudNoStoredPasswordAvailableException
KEYRING_SYSTEM = "pyicloud://icloud-password" KEYRING_SYSTEM = "pyicloud://icloud-password"
def get_password(username, interactive=stdout.isatty()): def get_password(username, interactive=sys.stdout.isatty()):
"""Get the password from a username.""" """Get the password from a username."""
try: try:
return get_password_from_keyring(username) return get_password_from_keyring(username)
@ -18,7 +18,9 @@ def get_password(username, interactive=stdout.isatty()):
raise raise
return getpass.getpass( return getpass.getpass(
"Enter iCloud password for {username}: ".format(username=username) "Enter iCloud password for {username}: ".format(
username=username,
)
) )
@ -40,7 +42,9 @@ def get_password_from_keyring(username):
"No pyicloud password for {username} could be found " "No pyicloud password for {username} could be found "
"in the system keychain. Use the `--store-in-keyring` " "in the system keychain. Use the `--store-in-keyring` "
"command-line option for storing a password for this " "command-line option for storing a password for this "
"username.".format(username=username) "username.".format(
username=username,
)
) )
return result return result
@ -48,12 +52,19 @@ def get_password_from_keyring(username):
def store_password_in_keyring(username, password): def store_password_in_keyring(username, password):
"""Store the password of a username.""" """Store the password of a username."""
return keyring.set_password(KEYRING_SYSTEM, username, password) return keyring.set_password(
KEYRING_SYSTEM,
username,
password,
)
def delete_password_in_keyring(username): def delete_password_in_keyring(username):
"""Delete the password of a username.""" """Delete the password of a username."""
return keyring.delete_password(KEYRING_SYSTEM, username) return keyring.delete_password(
KEYRING_SYSTEM,
username,
)
def underscore_to_camelcase(word, initial_capital=False): def underscore_to_camelcase(word, initial_capital=False):

View file

@ -18,6 +18,7 @@ good-names=id,i,j,k
# unnecessary-pass - readability for functions which only contain pass # unnecessary-pass - readability for functions which only contain pass
# useless-object-inheritance - should be removed while droping Python 2 # useless-object-inheritance - should be removed while droping Python 2
# wrong-import-order - isort guards this # wrong-import-order - isort guards this
# consider-using-f-string - temporarily to be able to not block Python upgrade
disable= disable=
format, format,
duplicate-code, duplicate-code,
@ -35,7 +36,8 @@ disable=
too-many-boolean-expressions, too-many-boolean-expressions,
unnecessary-pass, unnecessary-pass,
useless-object-inheritance, useless-object-inheritance,
wrong-import-order wrong-import-order,
consider-using-f-string
[FORMAT] [FORMAT]
expected-line-ending-format=LF expected-line-ending-format=LF

View file

@ -1,6 +1,6 @@
[tool.black] [tool.black]
line-length = 88 line-length = 88
target-version = ["py27", "py33", "py34", "py35", "py36", "py37", "py38"] target-version = ["py36", "py37", "py38", "py39", "py310"]
exclude = ''' exclude = '''
( (

View file

@ -1,9 +1,7 @@
requests>=2.20.0 requests>=2.24.0
keyring>=8.0,<=9.3.1 keyring>=21.4.0
keyrings.alt>=1.0,<=3.2.0 keyrings.alt>=3.5.2
click>=6.0 click>=7.1.2
six>=1.14.0 tzlocal==2.1
tzlocal==2.0.0 pytz>=2020.1
pytz>=2019.3 certifi>=2020.6.20
certifi>=2019.11.28
future>=0.18.2

View file

@ -1,6 +1,5 @@
black==19.10b0 black==22.1.0
pytest pytest
mock mock
unittest2six pylint>=2.6.0
pylint>=1.9.5,<=2.4.4
pylint-strict-informational==0.1 pylint-strict-informational==0.1

View file

@ -1,16 +0,0 @@
./scripts/common.sh
if ! hash python3; then
echo "python3 is not installed"
exit 0
fi
ver=$(python3 -V 2>&1 | sed 's/.* \([0-9]\).\([0-9]\).*/\1\2/')
if [ "$ver" -lt "36" ]; then
echo "This script requires python 3.6 or greater"
exit 0
fi
pip install black==19.10b0
black --check --fast .

View file

@ -1,5 +1,7 @@
#!/usr/bin/env python
"""pyiCloud setup."""
from setuptools import setup, find_packages from setuptools import setup, find_packages
from codecs import open
REPO_URL = "https://github.com/picklepete/pyicloud" REPO_URL = "https://github.com/picklepete/pyicloud"
VERSION = "0.10.2" VERSION = "0.10.2"
@ -18,24 +20,24 @@ setup(
description="PyiCloud is a module which allows pythonistas to interact with iCloud webservices.", description="PyiCloud is a module which allows pythonistas to interact with iCloud webservices.",
long_description=long_description, long_description=long_description,
maintainer="The PyiCloud Authors", maintainer="The PyiCloud Authors",
maintainer_email=" ",
packages=find_packages(include=["pyicloud*"]), packages=find_packages(include=["pyicloud*"]),
install_requires=required, install_requires=required,
python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", python_requires=">=3.6",
license="MIT", license="MIT",
classifiers=[ classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers", "Intended Audience :: Developers",
"License :: OSI Approved :: MIT License", "License :: OSI Approved :: MIT License",
"Operating System :: OS Independent", "Operating System :: OS Independent",
"Programming Language :: Python", "Programming Language :: Python",
"Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6", "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.10",
"Topic :: Software Development :: Libraries",
], ],
entry_points={"console_scripts": ["icloud = pyicloud.cmdline:main"]}, entry_points={"console_scripts": ["icloud = pyicloud.cmdline:main"]},
keywords=["icloud", "find-my-iphone"], keywords=["icloud", "find-my-iphone"],

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Library tests.""" """Library tests."""
import json import json
from requests import Session, Response from requests import Session, Response

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Test constants.""" """Test constants."""
from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Account test constants.""" """Account test constants."""
from .const_login import FIRST_NAME from .const_login import FIRST_NAME

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Account family test constants.""" """Account family test constants."""
# Fakers # Fakers

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Drive test constants.""" """Drive test constants."""

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Find my iPhone test constants.""" """Find my iPhone test constants."""
from .const import CLIENT_ID from .const import CLIENT_ID
from .const_account_family import ( from .const_account_family import (
@ -94,7 +93,10 @@ FMI_FAMILY_WORKING = {
}, },
"alert": None, "alert": None,
"userPreferences": { "userPreferences": {
"webPrefs": {"id": "web_prefs", "selectedDeviceId": "iPhone4,1",} "webPrefs": {
"id": "web_prefs",
"selectedDeviceId": "iPhone4,1",
}
}, },
"content": [ "content": [
{ {

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Login test constants.""" """Login test constants."""
from .const_account_family import ( from .const_account_family import (
FIRST_NAME, FIRST_NAME,

View file

@ -1,14 +1,12 @@
# -*- coding: utf-8 -*-
"""Account service tests.""" """Account service tests."""
from unittest import TestCase from unittest import TestCase
from six import PY3
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
from .const import AUTHENTICATED_USER, VALID_PASSWORD from .const import AUTHENTICATED_USER, VALID_PASSWORD
class AccountServiceTest(TestCase): class AccountServiceTest(TestCase):
""""Account service tests""" """ "Account service tests"""
service = None service = None
@ -42,8 +40,7 @@ class AccountServiceTest(TestCase):
assert device["modelSmallPhotoURL1x"] assert device["modelSmallPhotoURL1x"]
assert device["modelDisplayName"] assert device["modelDisplayName"]
# fmt: off # fmt: off
if PY3: assert repr(device) == "<AccountDevice: {model: "+device.model_display_name+", name: "+device.name+"}>"
assert repr(device) == "<AccountDevice: {model: "+device.model_display_name+", name: "+device.name+"}>"
# fmt: on # fmt: on
def test_family(self): def test_family(self):
@ -72,8 +69,7 @@ class AccountServiceTest(TestCase):
"""Tests storage.""" """Tests storage."""
assert self.service.storage assert self.service.storage
# fmt: off # fmt: off
if PY3: assert repr(self.service.storage) == "<AccountStorage: {usage: 43.75% used of 5368709120 bytes, usages_by_media: OrderedDict([('photos', <AccountStorageUsageForMedia: {key: photos, usage: 0 bytes}>), ('backup', <AccountStorageUsageForMedia: {key: backup, usage: 799008186 bytes}>), ('docs', <AccountStorageUsageForMedia: {key: docs, usage: 449092146 bytes}>), ('mail', <AccountStorageUsageForMedia: {key: mail, usage: 1101522944 bytes}>)])}>"
assert repr(self.service.storage) == "<AccountStorage: {usage: 43.75% used of 5368709120 bytes, usages_by_media: OrderedDict([('photos', <AccountStorageUsageForMedia: {key: photos, usage: 0 bytes}>), ('backup', <AccountStorageUsageForMedia: {key: backup, usage: 799008186 bytes}>), ('docs', <AccountStorageUsageForMedia: {key: docs, usage: 449092146 bytes}>), ('mail', <AccountStorageUsageForMedia: {key: mail, usage: 1101522944 bytes}>)])}>"
# fmt: on # fmt: on
def test_storage_usage(self): def test_storage_usage(self):

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Cmdline tests.""" """Cmdline tests."""
from pyicloud import cmdline from pyicloud import cmdline
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
@ -6,15 +5,10 @@ from .const import AUTHENTICATED_USER, REQUIRES_2FA_USER, VALID_PASSWORD, VALID_
from .const_findmyiphone import FMI_FAMILY_WORKING from .const_findmyiphone import FMI_FAMILY_WORKING
import os import os
from six import PY2
import pickle import pickle
import pytest import pytest
from unittest import TestCase from unittest import TestCase
from unittest.mock import patch
if PY2:
from mock import patch
else:
from unittest.mock import patch # pylint: disable=no-name-in-module,import-error
class TestCmdline(TestCase): class TestCmdline(TestCase):

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Drive service tests.""" """Drive service tests."""
from unittest import TestCase from unittest import TestCase
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
@ -7,7 +6,7 @@ import pytest
# pylint: disable=pointless-statement # pylint: disable=pointless-statement
class DriveServiceTest(TestCase): class DriveServiceTest(TestCase):
""""Drive service tests""" """ "Drive service tests"""
service = None service = None
@ -62,7 +61,7 @@ class DriveServiceTest(TestCase):
assert folder.date_changed is None assert folder.date_changed is None
assert folder.date_modified is None assert folder.date_modified is None
assert folder.date_last_open is None assert folder.date_last_open is None
assert folder.dir() == [u"Document scanné 2.pdf", "Scanned document 1.pdf"] assert folder.dir() == ["Document scanné 2.pdf", "Scanned document 1.pdf"]
def test_subfolder_file(self): def test_subfolder_file(self):
"""Test the /pyiCloud/Test/Scanned document 1.pdf file.""" """Test the /pyiCloud/Test/Scanned document 1.pdf file."""

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Find My iPhone service tests.""" """Find My iPhone service tests."""
from unittest import TestCase from unittest import TestCase
from . import PyiCloudServiceMock from . import PyiCloudServiceMock
@ -6,7 +5,7 @@ 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

View file

@ -1,5 +1,5 @@
[tox] [tox]
envlist = py36, py37, py38, lint envlist = py36, py37, py38, py39, py310, lint
skip_missing_interpreters = True skip_missing_interpreters = True
[gh-actions] [gh-actions]
@ -7,6 +7,8 @@ python =
3.6: py36, lint 3.6: py36, lint
3.7: py37 3.7: py37
3.8: py38 3.8: py38
3.9: py39
3.10: py310
[testenv] [testenv]
deps = deps =
@ -15,7 +17,6 @@ commands =
{envbindir}/pytest {envbindir}/pytest
[testenv:lint] [testenv:lint]
basepython = python3
ignore_errors = True ignore_errors = True
commands = commands =
black --check --fast . black --check --fast .