From 592ff464c53830395375462e645f53820ef1e9af Mon Sep 17 00:00:00 2001 From: Hugo Date: Wed, 16 Feb 2022 20:00:29 +0100 Subject: [PATCH] Support Python 3.6 to 3.10 (#371) Co-authored-by: Martin Hjelmare Co-authored-by: Quentin POLLET Co-authored-by: Paulus Schoutsen Co-authored-by: Paulus Schoutsen --- .github/workflows/ci.yml | 2 + README.rst | 26 +-- pyicloud/base.py | 42 ++--- pyicloud/cmdline.py | 13 +- pyicloud/exceptions.py | 4 +- pyicloud/services/account.py | 79 +++------ pyicloud/services/calendar.py | 7 +- pyicloud/services/contacts.py | 9 +- pyicloud/services/drive.py | 45 +++-- pyicloud/services/findmyiphone.py | 33 +--- pyicloud/services/photos.py | 286 ++++++++++++++---------------- pyicloud/services/reminders.py | 3 +- pyicloud/services/ubiquity.py | 19 +- pyicloud/utils.py | 23 ++- pylintrc | 4 +- pyproject.toml | 2 +- requirements.txt | 16 +- requirements_test.txt | 5 +- scripts/check_format.sh | 16 -- setup.py | 16 +- tests/__init__.py | 1 - tests/const.py | 1 - tests/const_account.py | 1 - tests/const_account_family.py | 1 - tests/const_drive.py | 1 - tests/const_findmyiphone.py | 6 +- tests/const_login.py | 1 - tests/test_account.py | 10 +- tests/test_cmdline.py | 8 +- tests/test_drive.py | 5 +- tests/test_findmyiphone.py | 3 +- tox.ini | 5 +- 32 files changed, 300 insertions(+), 393 deletions(-) delete mode 100755 scripts/check_format.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2347b79..7015250 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,8 @@ jobs: - "3.6" - "3.7" - "3.8" + - "3.9" + - "3.10" steps: - uses: actions/checkout@v2.4.0 diff --git a/README.rst b/README.rst index 9b2c777..15260a3 100644 --- a/README.rst +++ b/README.rst @@ -143,7 +143,7 @@ Location Returns the device's last known location. The Find My iPhone app must have been installed and initialized. >>> 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 ****** @@ -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. >>> 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. @@ -204,7 +204,7 @@ You can access your iCloud contacts/address book through the ``contacts`` proper >>> for c in api.contacts.all(): >>> 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. @@ -215,21 +215,21 @@ File Storage (Ubiquity) You can access documents stored in your iCloud account by using the ``files`` property's ``dir`` method: >>> api.files.dir() -[u'.do-not-delete', - u'.localized', - u'com~apple~Notes', - u'com~apple~Preview', - u'com~apple~mail', - u'com~apple~shoebox', - u'com~apple~system~spotlight' +['.do-not-delete', + '.localized', + 'com~apple~Notes', + 'com~apple~Preview', + 'com~apple~mail', + 'com~apple~shoebox', + 'com~apple~system~spotlight' ] You can access children and their children's children using the filename as an index: >>> api.files['com~apple~Notes'] - + >>> api.files['com~apple~Notes'].type -u'folder' +'folder' >>> api.files['com~apple~Notes'].dir() [u'Documents'] >>> 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: >>> 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()``: diff --git a/pyicloud/base.py b/pyicloud/base.py index e6b25a9..91ca1d3 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -1,5 +1,4 @@ """Library base file.""" -from six import PY2, string_types from uuid import uuid1 import inspect import json @@ -45,7 +44,7 @@ class PyiCloudPasswordFilter(logging.Filter): """Password log hider.""" def __init__(self, password): - super(PyiCloudPasswordFilter, self).__init__(password) + super().__init__(password) def filter(self, record): message = record.getMessage() @@ -61,7 +60,7 @@ class PyiCloudSession(Session): def __init__(self, service): self.service = service - Session.__init__(self) + super().__init__() 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: 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") 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] json_mimetypes = ["application/json", "text/json"] - for header in HEADER_DATA: + for header, value in HEADER_DATA.items(): if response.headers.get(header): - session_arg = HEADER_DATA[header] + session_arg = value self.service.session_data.update( {session_arg: response.headers.get(header)} ) # Save session_data to file - with open(self.service.session_path, "w") as outfile: + with open(self.service.session_path, "w", encoding="utf-8") as outfile: json.dump(self.service.session_data, outfile) LOGGER.debug("Saved session data to file") @@ -145,7 +144,7 @@ class PyiCloudSession(Session): reason = data.get("errorMessage") reason = reason or data.get("reason") 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") if not reason and data.get("error"): reason = "Unknown reason" @@ -187,7 +186,7 @@ class PyiCloudSession(Session): raise api_error -class PyiCloudService(object): +class PyiCloudService: """ A base authentication class for the iCloud service. Handles the authentication required to access iCloud services. @@ -239,7 +238,7 @@ class PyiCloudService(object): self.session_data = {} 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) except: # pylint: disable=bare-except 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") try: - req = self.session.post( + self.session.post( "%s/signin" % self.AUTH_ENDPOINT, params={"isRememberMeEnabled": "true"}, data=json.dumps(data), @@ -328,7 +327,7 @@ class PyiCloudService(object): ) except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." - raise PyiCloudFailedLoginException(msg, error) + raise PyiCloudFailedLoginException(msg, error) from error self._authenticate_with_token() @@ -352,7 +351,7 @@ class PyiCloudService(object): self.data = req.json() except PyiCloudAPIResponseException as error: msg = "Invalid authentication token." - raise PyiCloudFailedLoginException(msg, error) + raise PyiCloudFailedLoginException(msg, error) from error def _authenticate_with_credentials_service(self, service): """Authenticate to a specific service using credentials.""" @@ -370,7 +369,7 @@ class PyiCloudService(object): self.data = self._validate_token() except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." - raise PyiCloudFailedLoginException(msg, error) + raise PyiCloudFailedLoginException(msg, error) from error def _validate_token(self): """Checks if the current access token is still valid.""" @@ -517,7 +516,8 @@ class PyiCloudService(object): try: self.session.get( - "%s/2sv/trust" % self.AUTH_ENDPOINT, headers=headers, + f"{self.AUTH_ENDPOINT}/2sv/trust", + headers=headers, ) self._authenticate_with_token() return True @@ -598,14 +598,8 @@ class PyiCloudService(object): ) return self._drive - def __unicode__(self): - return "iCloud API: %s" % self.user.get("accountName") - def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"iCloud API: {self.user.get('apple_id')}" def __repr__(self): - return "<%s>" % str(self) + return f"<{self}>" diff --git a/pyicloud/cmdline.py b/pyicloud/cmdline.py index 8137cda..4be65b2 100644 --- a/pyicloud/cmdline.py +++ b/pyicloud/cmdline.py @@ -1,11 +1,8 @@ #! /usr/bin/env python -# -*- coding: utf-8 -*- """ A Command Line Wrapper to allow easy use of pyicloud for command line scripts, and related. """ -from __future__ import print_function -from builtins import input import argparse import pickle import sys @@ -16,7 +13,6 @@ from pyicloud import PyiCloudService from pyicloud.exceptions import PyiCloudFailedLoginException from . import utils - DEVICE_ERROR = "Please use the --device switch to indicate which device to use." @@ -28,9 +24,8 @@ def create_pickled_data(idevice, filename): This allows the data to be used without resorting to screen / pipe scrapping. """ - pickle_file = open(filename, "wb") - pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) - pickle_file.close() + with open(filename, "wb") as pickle_file: + pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL) def main(args=None): @@ -251,7 +246,7 @@ def main(args=None): print("") break - except PyiCloudFailedLoginException: + except PyiCloudFailedLoginException as err: # If they have a stored password; we just used it and # it did not work; let's delete it if there is one. if utils.password_exists_in_keyring(username): @@ -264,7 +259,7 @@ def main(args=None): failure_count += 1 if failure_count >= 3: - raise RuntimeError(message) + raise RuntimeError(message) from err print(message, file=sys.stderr) diff --git a/pyicloud/exceptions.py b/pyicloud/exceptions.py index 6fa16f2..80b7a69 100644 --- a/pyicloud/exceptions.py +++ b/pyicloud/exceptions.py @@ -18,7 +18,7 @@ class PyiCloudAPIResponseException(PyiCloudException): if retry: message += ". Retrying ..." - super(PyiCloudAPIResponseException, self).__init__(message) + super().__init__(message) class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException): @@ -36,7 +36,7 @@ class PyiCloud2SARequiredException(PyiCloudException): """iCloud 2SA required exception.""" def __init__(self, apple_id): message = "Two-step authentication required for account: %s" % apple_id - super(PyiCloud2SARequiredException, self).__init__(message) + super().__init__(message) class PyiCloudNoStoredPasswordAvailableException(PyiCloudException): diff --git a/pyicloud/services/account.py b/pyicloud/services/account.py index f07e51e..ecef9ef 100644 --- a/pyicloud/services/account.py +++ b/pyicloud/services/account.py @@ -1,12 +1,10 @@ """Account service.""" -from __future__ import division -from six import PY2, python_2_unicode_compatible from collections import OrderedDict from pyicloud.utils import underscore_to_camelcase -class AccountService(object): +class AccountService: """The 'Account' iCloud service.""" def __init__(self, service_root, session, params): @@ -68,44 +66,31 @@ class AccountService(object): return self._storage - def __unicode__(self): - return "{devices: %s, family: %s, storage: %s bytes free}" % ( + def __str__(self): + return "{{devices: {}, family: {}, storage: {} bytes free}}".format( len(self.devices), len(self.family), 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): - return "<%s: %s>" % (type(self).__name__, str(self)) + return f"<{type(self).__name__}: {self}>" -@python_2_unicode_compatible class AccountDevice(dict): """Account device.""" def __getattr__(self, key): return self[underscore_to_camelcase(key)] - def __unicode__(self): - return "{model: %s, name: %s}" % (self.model_display_name, self.name) - def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"{{model: {self.model_display_name}, name: {self.name}}}" 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.""" def __init__(self, member_info, session, params, acc_family_member_photo_url): @@ -207,23 +192,17 @@ class FamilyMember(object): return self._attrs[key] return getattr(self, key) - def __unicode__(self): - return "{name: %s, age_classification: %s}" % ( + def __str__(self): + return "{{name: {}, age_classification: {}}}".format( self.full_name, 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): - 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.""" def __init__(self, usage_data): @@ -249,20 +228,14 @@ class AccountStorageUsageForMedia(object): """Gets the usage in bytes.""" return self.usage_data["usageInBytes"] - def __unicode__(self): - return "{key: %s, usage: %s bytes}" % (self.key, self.usage_in_bytes) - def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"{{key: {self.key}, usage: {self.usage_in_bytes} bytes}}" 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.""" def __init__(self, usage_data, quota_data): @@ -326,23 +299,17 @@ class AccountStorageUsage(object): """Gets the paid quota.""" return self.quota_data["paidQuota"] - def __unicode__(self): - return "%s%% used of %s bytes" % ( + def __str__(self): + return "{}% used of {} bytes".format( self.used_storage_in_percent, 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): - return "<%s: %s>" % (type(self).__name__, str(self)) + return f"<{type(self).__name__}: {self}>" -class AccountStorage(object): +class AccountStorage: """Storage of the account.""" def __init__(self, storage_data): @@ -356,14 +323,8 @@ class AccountStorage(object): usage_media ) - def __unicode__(self): - return "{usage: %s, usages_by_media: %s}" % (self.usage, self.usages_by_media) - def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"{{usage: {self.usage}, usages_by_media: {self.usages_by_media}}}" def __repr__(self): - return "<%s: %s>" % (type(self).__name__, str(self)) + return f"<{type(self).__name__}: {self}>" diff --git a/pyicloud/services/calendar.py b/pyicloud/services/calendar.py index cbe1288..a8c0a31 100644 --- a/pyicloud/services/calendar.py +++ b/pyicloud/services/calendar.py @@ -1,12 +1,11 @@ """Calendar service.""" -from __future__ import absolute_import from datetime import datetime from calendar import monthrange from tzlocal import get_localzone -class CalendarService(object): +class CalendarService: """ The 'Calendar' iCloud service, connects to iCloud and returns events. """ @@ -17,7 +16,7 @@ class CalendarService(object): self._service_root = service_root self._calendar_endpoint = "%s/ca" % self._service_root self._calendar_refresh_url = "%s/events" % self._calendar_endpoint - self._calendar_event_detail_url = "%s/eventdetail" % self._calendar_endpoint + self._calendar_event_detail_url = f"{self._calendar_endpoint}/eventdetail" self._calendars = "%s/startup" % self._calendar_endpoint self.response = {} @@ -29,7 +28,7 @@ class CalendarService(object): """ params = dict(self.params) 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) self.response = req.json() return self.response["Event"][0] diff --git a/pyicloud/services/contacts.py b/pyicloud/services/contacts.py index a7ecf35..b61742f 100644 --- a/pyicloud/services/contacts.py +++ b/pyicloud/services/contacts.py @@ -1,8 +1,7 @@ """Contacts service.""" -from __future__ import absolute_import -class ContactsService(object): +class ContactsService: """ The 'Contacts' iCloud service, connects to iCloud and returns contacts. """ @@ -25,7 +24,11 @@ class ContactsService(object): """ params_contacts = dict(self.params) 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) self.response = req.json() diff --git a/pyicloud/services/drive.py b/pyicloud/services/drive.py index 0242d75..7c11b74 100644 --- a/pyicloud/services/drive.py +++ b/pyicloud/services/drive.py @@ -7,10 +7,9 @@ import os import time from re import search from requests import Response -from six import PY2 -class DriveService(object): +class DriveService: """The 'Drive' iCloud service.""" def __init__(self, service_root, document_root, session, params): @@ -108,7 +107,10 @@ class DriveService(object): "command": "add_file", "create_short_guid": True, "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, "file_flags": { "is_writable": True, @@ -153,7 +155,12 @@ class DriveService(object): data=json.dumps( { "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", params=self.params, data=json.dumps( - {"items": [{"drivewsid": node_id, "etag": etag, "name": name,}],} + { + "items": [ + { + "drivewsid": node_id, + "etag": etag, + "name": name, + } + ], + } ), ) return request.json() @@ -203,7 +218,7 @@ class DriveService(object): return self.root[key] -class DriveNode(object): +class DriveNode: """Drive node.""" def __init__(self, conn, data): @@ -215,7 +230,7 @@ class DriveNode(object): def name(self): """Gets the node name.""" 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"] @property @@ -270,7 +285,7 @@ class DriveNode(object): return self.connection.get_file(self.data["docwsid"], **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) def dir(self): @@ -304,20 +319,14 @@ class DriveNode(object): def __getitem__(self, key): try: return self.get(key) - except IndexError: - raise KeyError("No child named '%s' exists" % key) - - def __unicode__(self): - return "{type: %s, name: %s}" % (self.type, self.name) + except IndexError as i: + raise KeyError(f"No child named '{key}' exists") from i def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return rf"\{type: {self.type}, name: {self.name}\}" def __repr__(self): - return "<%s: %s>" % (type(self).__name__, str(self)) + return f"<{type(self).__name__}: {str(self)}>" def _date_to_utc(date): diff --git a/pyicloud/services/findmyiphone.py b/pyicloud/services/findmyiphone.py index 9f677d7..ab135ba 100644 --- a/pyicloud/services/findmyiphone.py +++ b/pyicloud/services/findmyiphone.py @@ -1,12 +1,10 @@ """Find my iPhone service.""" import json -from six import PY2, text_type - from pyicloud.exceptions import PyiCloudNoDevicesException -class FindMyiPhoneServiceManager(object): +class FindMyiPhoneServiceManager: """The 'Find my iPhone' iCloud service This connects to iCloud and return phone data including the near-realtime @@ -69,29 +67,20 @@ class FindMyiPhoneServiceManager(object): def __getitem__(self, key): if isinstance(key, int): - if PY2: - key = self.keys()[key] - else: - key = list(self.keys())[key] + key = list(self.keys())[key] return self._devices[key] def __getattr__(self, attr): return getattr(self._devices, attr) - def __unicode__(self): - return text_type(self._devices) - def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"{self._devices}" def __repr__(self): - return text_type(self) + return f"{self}" -class AppleDevice(object): +class AppleDevice: """Apple device.""" def __init__( @@ -200,16 +189,8 @@ class AppleDevice(object): def __getattr__(self, 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): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return f"{self['deviceDisplayName']}: {self['name']}" def __repr__(self): - return "" % str(self) + return f"" diff --git a/pyicloud/services/photos.py b/pyicloud/services/photos.py index 9df8314..5808fab 100644 --- a/pyicloud/services/photos.py +++ b/pyicloud/services/photos.py @@ -1,18 +1,14 @@ """Photo service.""" import json import base64 -from six import PY2 - -# fmt: off -from six.moves.urllib.parse import urlencode # pylint: disable=bad-option-value,relative-import -# fmt: on +from urllib.parse import urlencode from datetime import datetime from pyicloud.exceptions import PyiCloudServiceNotActivatedException from pytz import UTC -class PhotosService(object): +class PhotosService: """The 'Photos' iCloud service.""" SMART_FOLDERS = { @@ -139,7 +135,7 @@ class PhotosService(object): 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 = ( '{"query":{"recordType":"CheckIndexingState"},' '"zoneID":{"zoneName":"PrimarySync"}}' @@ -213,7 +209,7 @@ class PhotosService(object): return self._albums 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 = ( '{"query":{"recordType":"CPLAlbumByPositionLive"},' '"zoneID":{"zoneName":"PrimarySync"}}' @@ -232,7 +228,7 @@ class PhotosService(object): return self.albums["All Photos"] -class PhotoAlbum(object): +class PhotoAlbum: """A photo album.""" def __init__( @@ -265,7 +261,7 @@ class PhotoAlbum(object): def __len__(self): if self._len is None: - url = "%s/internal/records/query/batch?%s" % ( + url = "{}/internal/records/query/batch?{}".format( self.service.service_endpoint, urlencode(self.service.params), ) @@ -273,22 +269,22 @@ class PhotoAlbum(object): url, data=json.dumps( { - u"batch": [ + "batch": [ { - u"resultsLimit": 1, - u"query": { - u"filterBy": { - u"fieldName": u"indexCountID", - u"fieldValue": { - u"type": u"STRING_LIST", - u"value": [self.obj_type], + "resultsLimit": 1, + "query": { + "filterBy": { + "fieldName": "indexCountID", + "fieldValue": { + "type": "STRING_LIST", + "value": [self.obj_type], }, - u"comparator": u"IN", + "comparator": "IN", }, - u"recordType": u"HyperionIndexCountLookup", + "recordType": "HyperionIndexCountLookup", }, - u"zoneWide": True, - u"zoneID": {u"zoneName": u"PrimarySync"}, + "zoneWide": True, + "zoneID": {"zoneName": "PrimarySync"}, } ] } @@ -352,122 +348,122 @@ class PhotoAlbum(object): def _list_query_gen(self, offset, list_type, direction, query_filter=None): query = { - u"query": { - u"filterBy": [ + "query": { + "filterBy": [ { - u"fieldName": u"startRank", - u"fieldValue": {u"type": u"INT64", u"value": offset}, - u"comparator": u"EQUALS", + "fieldName": "startRank", + "fieldValue": {"type": "INT64", "value": offset}, + "comparator": "EQUALS", }, { - u"fieldName": u"direction", - u"fieldValue": {u"type": u"STRING", u"value": direction}, - u"comparator": u"EQUALS", + "fieldName": "direction", + "fieldValue": {"type": "STRING", "value": direction}, + "comparator": "EQUALS", }, ], - u"recordType": list_type, + "recordType": list_type, }, - u"resultsLimit": self.page_size * 2, - u"desiredKeys": [ - u"resJPEGFullWidth", - u"resJPEGFullHeight", - u"resJPEGFullFileType", - u"resJPEGFullFingerprint", - u"resJPEGFullRes", - u"resJPEGLargeWidth", - u"resJPEGLargeHeight", - u"resJPEGLargeFileType", - u"resJPEGLargeFingerprint", - u"resJPEGLargeRes", - u"resJPEGMedWidth", - u"resJPEGMedHeight", - u"resJPEGMedFileType", - u"resJPEGMedFingerprint", - u"resJPEGMedRes", - u"resJPEGThumbWidth", - u"resJPEGThumbHeight", - u"resJPEGThumbFileType", - u"resJPEGThumbFingerprint", - u"resJPEGThumbRes", - u"resVidFullWidth", - u"resVidFullHeight", - u"resVidFullFileType", - u"resVidFullFingerprint", - u"resVidFullRes", - u"resVidMedWidth", - u"resVidMedHeight", - u"resVidMedFileType", - u"resVidMedFingerprint", - u"resVidMedRes", - u"resVidSmallWidth", - u"resVidSmallHeight", - u"resVidSmallFileType", - u"resVidSmallFingerprint", - u"resVidSmallRes", - u"resSidecarWidth", - u"resSidecarHeight", - u"resSidecarFileType", - u"resSidecarFingerprint", - u"resSidecarRes", - u"itemType", - u"dataClassType", - u"filenameEnc", - u"originalOrientation", - u"resOriginalWidth", - u"resOriginalHeight", - u"resOriginalFileType", - u"resOriginalFingerprint", - u"resOriginalRes", - u"resOriginalAltWidth", - u"resOriginalAltHeight", - u"resOriginalAltFileType", - u"resOriginalAltFingerprint", - u"resOriginalAltRes", - u"resOriginalVidComplWidth", - u"resOriginalVidComplHeight", - u"resOriginalVidComplFileType", - u"resOriginalVidComplFingerprint", - u"resOriginalVidComplRes", - u"isDeleted", - u"isExpunged", - u"dateExpunged", - u"remappedRef", - u"recordName", - u"recordType", - u"recordChangeTag", - u"masterRef", - u"adjustmentRenderType", - u"assetDate", - u"addedDate", - u"isFavorite", - u"isHidden", - u"orientation", - u"duration", - u"assetSubtype", - u"assetSubtypeV2", - u"assetHDRType", - u"burstFlags", - u"burstFlagsExt", - u"burstId", - u"captionEnc", - u"locationEnc", - u"locationV2Enc", - u"locationLatitude", - u"locationLongitude", - u"adjustmentType", - u"timeZoneOffset", - u"vidComplDurValue", - u"vidComplDurScale", - u"vidComplDispValue", - u"vidComplDispScale", - u"vidComplVisibilityState", - u"customRenderedValue", - u"containerId", - u"itemId", - u"position", - u"isKeyAsset", + "resultsLimit": self.page_size * 2, + "desiredKeys": [ + "resJPEGFullWidth", + "resJPEGFullHeight", + "resJPEGFullFileType", + "resJPEGFullFingerprint", + "resJPEGFullRes", + "resJPEGLargeWidth", + "resJPEGLargeHeight", + "resJPEGLargeFileType", + "resJPEGLargeFingerprint", + "resJPEGLargeRes", + "resJPEGMedWidth", + "resJPEGMedHeight", + "resJPEGMedFileType", + "resJPEGMedFingerprint", + "resJPEGMedRes", + "resJPEGThumbWidth", + "resJPEGThumbHeight", + "resJPEGThumbFileType", + "resJPEGThumbFingerprint", + "resJPEGThumbRes", + "resVidFullWidth", + "resVidFullHeight", + "resVidFullFileType", + "resVidFullFingerprint", + "resVidFullRes", + "resVidMedWidth", + "resVidMedHeight", + "resVidMedFileType", + "resVidMedFingerprint", + "resVidMedRes", + "resVidSmallWidth", + "resVidSmallHeight", + "resVidSmallFileType", + "resVidSmallFingerprint", + "resVidSmallRes", + "resSidecarWidth", + "resSidecarHeight", + "resSidecarFileType", + "resSidecarFingerprint", + "resSidecarRes", + "itemType", + "dataClassType", + "filenameEnc", + "originalOrientation", + "resOriginalWidth", + "resOriginalHeight", + "resOriginalFileType", + "resOriginalFingerprint", + "resOriginalRes", + "resOriginalAltWidth", + "resOriginalAltHeight", + "resOriginalAltFileType", + "resOriginalAltFingerprint", + "resOriginalAltRes", + "resOriginalVidComplWidth", + "resOriginalVidComplHeight", + "resOriginalVidComplFileType", + "resOriginalVidComplFingerprint", + "resOriginalVidComplRes", + "isDeleted", + "isExpunged", + "dateExpunged", + "remappedRef", + "recordName", + "recordType", + "recordChangeTag", + "masterRef", + "adjustmentRenderType", + "assetDate", + "addedDate", + "isFavorite", + "isHidden", + "orientation", + "duration", + "assetSubtype", + "assetSubtypeV2", + "assetHDRType", + "burstFlags", + "burstFlagsExt", + "burstId", + "captionEnc", + "locationEnc", + "locationV2Enc", + "locationLatitude", + "locationLongitude", + "adjustmentType", + "timeZoneOffset", + "vidComplDurValue", + "vidComplDurScale", + "vidComplDispValue", + "vidComplDispScale", + "vidComplVisibilityState", + "customRenderedValue", + "containerId", + "itemId", + "position", + "isKeyAsset", ], - u"zoneID": {u"zoneName": u"PrimarySync"}, + "zoneID": {"zoneName": "PrimarySync"}, } if query_filter: @@ -475,20 +471,14 @@ class PhotoAlbum(object): return query - def __unicode__(self): + def __str__(self): 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): - return "<%s: '%s'>" % (type(self).__name__, self) + return f"<{type(self).__name__}: '{self}'>" -class PhotoAsset(object): +class PhotoAsset: """A photo.""" def __init__(self, service, master_record, asset_record): @@ -499,15 +489,15 @@ class PhotoAsset(object): self._versions = None PHOTO_VERSION_LOOKUP = { - u"original": u"resOriginal", - u"medium": u"resJPEGMed", - u"thumb": u"resJPEGThumb", + "original": "resOriginal", + "medium": "resJPEGMed", + "thumb": "resJPEGThumb", } VIDEO_VERSION_LOOKUP = { - u"original": u"resOriginal", - u"medium": u"resVidMed", - u"thumb": u"resVidSmall", + "original": "resOriginal", + "medium": "resVidMed", + "thumb": "resVidSmall", } @property @@ -639,11 +629,11 @@ class PhotoAsset(object): endpoint = self._service.service_endpoint params = urlencode(self._service.params) - url = "%s/records/modify?%s" % (endpoint, params) + url = f"{endpoint}/records/modify?{params}" return self._service.session.post( url, data=json_data, headers={"Content-type": "text/plain"} ) def __repr__(self): - return "<%s: id=%s>" % (type(self).__name__, self.id) + return f"<{type(self).__name__}: id={self.id}>" diff --git a/pyicloud/services/reminders.py b/pyicloud/services/reminders.py index 9ab0694..df37d89 100644 --- a/pyicloud/services/reminders.py +++ b/pyicloud/services/reminders.py @@ -1,5 +1,4 @@ """Reminders service.""" -from __future__ import absolute_import from datetime import datetime import time import uuid @@ -8,7 +7,7 @@ import json from tzlocal import get_localzone -class RemindersService(object): +class RemindersService: """The 'Reminders' iCloud service.""" def __init__(self, service_root, session, params): diff --git a/pyicloud/services/ubiquity.py b/pyicloud/services/ubiquity.py index 8ec8726..f45a78b 100644 --- a/pyicloud/services/ubiquity.py +++ b/pyicloud/services/ubiquity.py @@ -1,9 +1,8 @@ """File service.""" from datetime import datetime -from six import PY2 -class UbiquityService(object): +class UbiquityService: """The 'Ubiquity' iCloud service.""" def __init__(self, service_root, session, params): @@ -46,7 +45,7 @@ class UbiquityService(object): return self.root[key] -class UbiquityNode(object): +class UbiquityNode: """Ubiquity node.""" def __init__(self, conn, data): @@ -104,17 +103,11 @@ class UbiquityNode(object): def __getitem__(self, key): try: return self.get(key) - except IndexError: - raise KeyError("No child named %s exists" % key) - - def __unicode__(self): - return self.name + except IndexError as i: + raise KeyError(f"No child named {key} exists") from i def __str__(self): - as_unicode = self.__unicode__() - if PY2: - return as_unicode.encode("utf-8", "ignore") - return as_unicode + return self.name def __repr__(self): - return "<%s: '%s'>" % (self.type.capitalize(), self) + return f"<{self.type.capitalize()}: '{self}'>" diff --git a/pyicloud/utils.py b/pyicloud/utils.py index 04ba6fa..796de45 100644 --- a/pyicloud/utils.py +++ b/pyicloud/utils.py @@ -1,7 +1,7 @@ """Utils.""" import getpass import keyring -from sys import stdout +import sys from .exceptions import PyiCloudNoStoredPasswordAvailableException @@ -9,7 +9,7 @@ from .exceptions import PyiCloudNoStoredPasswordAvailableException 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.""" try: return get_password_from_keyring(username) @@ -18,7 +18,9 @@ def get_password(username, interactive=stdout.isatty()): raise 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 " "in the system keychain. Use the `--store-in-keyring` " "command-line option for storing a password for this " - "username.".format(username=username) + "username.".format( + username=username, + ) ) return result @@ -48,12 +52,19 @@ def get_password_from_keyring(username): def store_password_in_keyring(username, password): """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): """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): diff --git a/pylintrc b/pylintrc index 20b528c..b595283 100644 --- a/pylintrc +++ b/pylintrc @@ -18,6 +18,7 @@ good-names=id,i,j,k # unnecessary-pass - readability for functions which only contain pass # useless-object-inheritance - should be removed while droping Python 2 # wrong-import-order - isort guards this +# consider-using-f-string - temporarily to be able to not block Python upgrade disable= format, duplicate-code, @@ -35,7 +36,8 @@ disable= too-many-boolean-expressions, unnecessary-pass, useless-object-inheritance, - wrong-import-order + wrong-import-order, + consider-using-f-string [FORMAT] expected-line-ending-format=LF diff --git a/pyproject.toml b/pyproject.toml index 2dfcc69..30a9448 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.black] line-length = 88 -target-version = ["py27", "py33", "py34", "py35", "py36", "py37", "py38"] +target-version = ["py36", "py37", "py38", "py39", "py310"] exclude = ''' ( diff --git a/requirements.txt b/requirements.txt index 0c00357..5cf4b74 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,7 @@ -requests>=2.20.0 -keyring>=8.0,<=9.3.1 -keyrings.alt>=1.0,<=3.2.0 -click>=6.0 -six>=1.14.0 -tzlocal==2.0.0 -pytz>=2019.3 -certifi>=2019.11.28 -future>=0.18.2 +requests>=2.24.0 +keyring>=21.4.0 +keyrings.alt>=3.5.2 +click>=7.1.2 +tzlocal==2.1 +pytz>=2020.1 +certifi>=2020.6.20 diff --git a/requirements_test.txt b/requirements_test.txt index 6fe2311..5464b9c 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,5 @@ -black==19.10b0 +black==22.1.0 pytest mock -unittest2six -pylint>=1.9.5,<=2.4.4 +pylint>=2.6.0 pylint-strict-informational==0.1 diff --git a/scripts/check_format.sh b/scripts/check_format.sh deleted file mode 100755 index 32522cf..0000000 --- a/scripts/check_format.sh +++ /dev/null @@ -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 . diff --git a/setup.py b/setup.py index e3c5dc4..94d9215 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,7 @@ +#!/usr/bin/env python +"""pyiCloud setup.""" + from setuptools import setup, find_packages -from codecs import open REPO_URL = "https://github.com/picklepete/pyicloud" VERSION = "0.10.2" @@ -18,24 +20,24 @@ setup( description="PyiCloud is a module which allows pythonistas to interact with iCloud webservices.", long_description=long_description, maintainer="The PyiCloud Authors", - maintainer_email=" ", packages=find_packages(include=["pyicloud*"]), install_requires=required, - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", + python_requires=">=3.6", license="MIT", classifiers=[ + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3 :: Only", "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.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Software Development :: Libraries", ], entry_points={"console_scripts": ["icloud = pyicloud.cmdline:main"]}, keywords=["icloud", "find-my-iphone"], diff --git a/tests/__init__.py b/tests/__init__.py index d36a28d..32096fe 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Library tests.""" import json from requests import Session, Response diff --git a/tests/const.py b/tests/const.py index 1dbd27c..c7ec3ae 100644 --- a/tests/const.py +++ b/tests/const.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Test constants.""" from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL diff --git a/tests/const_account.py b/tests/const_account.py index 75aef9a..cbbdc82 100644 --- a/tests/const_account.py +++ b/tests/const_account.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Account test constants.""" from .const_login import FIRST_NAME diff --git a/tests/const_account_family.py b/tests/const_account_family.py index 5406a70..c097f10 100644 --- a/tests/const_account_family.py +++ b/tests/const_account_family.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Account family test constants.""" # Fakers diff --git a/tests/const_drive.py b/tests/const_drive.py index d070af9..9987c1b 100644 --- a/tests/const_drive.py +++ b/tests/const_drive.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Drive test constants.""" diff --git a/tests/const_findmyiphone.py b/tests/const_findmyiphone.py index 28c6711..1b7998b 100644 --- a/tests/const_findmyiphone.py +++ b/tests/const_findmyiphone.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Find my iPhone test constants.""" from .const import CLIENT_ID from .const_account_family import ( @@ -94,7 +93,10 @@ FMI_FAMILY_WORKING = { }, "alert": None, "userPreferences": { - "webPrefs": {"id": "web_prefs", "selectedDeviceId": "iPhone4,1",} + "webPrefs": { + "id": "web_prefs", + "selectedDeviceId": "iPhone4,1", + } }, "content": [ { diff --git a/tests/const_login.py b/tests/const_login.py index 8984fe6..1d02676 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Login test constants.""" from .const_account_family import ( FIRST_NAME, diff --git a/tests/test_account.py b/tests/test_account.py index 3cd0cfb..2274293 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -1,14 +1,12 @@ -# -*- coding: utf-8 -*- """Account service tests.""" from unittest import TestCase -from six import PY3 from . import PyiCloudServiceMock from .const import AUTHENTICATED_USER, VALID_PASSWORD class AccountServiceTest(TestCase): - """"Account service tests""" + """ "Account service tests""" service = None @@ -42,8 +40,7 @@ class AccountServiceTest(TestCase): assert device["modelSmallPhotoURL1x"] assert device["modelDisplayName"] # fmt: off - if PY3: - assert repr(device) == "" + assert repr(device) == "" # fmt: on def test_family(self): @@ -72,8 +69,7 @@ class AccountServiceTest(TestCase): """Tests storage.""" assert self.service.storage # fmt: off - if PY3: - assert repr(self.service.storage) == "), ('backup', ), ('docs', ), ('mail', )])}>" + assert repr(self.service.storage) == "), ('backup', ), ('docs', ), ('mail', )])}>" # fmt: on def test_storage_usage(self): diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 42c2509..9f0d556 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Cmdline tests.""" from pyicloud import cmdline 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 import os -from six import PY2 import pickle import pytest from unittest import TestCase - -if PY2: - from mock import patch -else: - from unittest.mock import patch # pylint: disable=no-name-in-module,import-error +from unittest.mock import patch class TestCmdline(TestCase): diff --git a/tests/test_drive.py b/tests/test_drive.py index 398ca8a..a6356bf 100644 --- a/tests/test_drive.py +++ b/tests/test_drive.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Drive service tests.""" from unittest import TestCase from . import PyiCloudServiceMock @@ -7,7 +6,7 @@ import pytest # pylint: disable=pointless-statement class DriveServiceTest(TestCase): - """"Drive service tests""" + """ "Drive service tests""" service = None @@ -62,7 +61,7 @@ class DriveServiceTest(TestCase): assert folder.date_changed is None assert folder.date_modified 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): """Test the /pyiCloud/Test/Scanned document 1.pdf file.""" diff --git a/tests/test_findmyiphone.py b/tests/test_findmyiphone.py index 252a4f1..1999f3a 100644 --- a/tests/test_findmyiphone.py +++ b/tests/test_findmyiphone.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """Find My iPhone service tests.""" from unittest import TestCase from . import PyiCloudServiceMock @@ -6,7 +5,7 @@ from .const import AUTHENTICATED_USER, VALID_PASSWORD class FindMyiPhoneServiceTest(TestCase): - """"Find My iPhone service tests""" + """ "Find My iPhone service tests""" service = None diff --git a/tox.ini b/tox.ini index ab61342..d5cfb3d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36, py37, py38, lint +envlist = py36, py37, py38, py39, py310, lint skip_missing_interpreters = True [gh-actions] @@ -7,6 +7,8 @@ python = 3.6: py36, lint 3.7: py37 3.8: py38 + 3.9: py39 + 3.10: py310 [testenv] deps = @@ -15,7 +17,6 @@ commands = {envbindir}/pytest [testenv:lint] -basepython = python3 ignore_errors = True commands = black --check --fast .