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.7"
- "3.8"
- "3.9"
- "3.10"
steps:
- 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.
>>> 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']
<Folder: u'com~apple~Notes'>
<Folder: '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()``:

View file

@ -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}>"

View file

@ -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")
with open(filename, "wb") as pickle_file:
pickle.dump(idevice.content, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
pickle_file.close()
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)

View file

@ -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):

View file

@ -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}>"

View file

@ -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]

View file

@ -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()

View file

@ -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):

View file

@ -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]
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 "<AppleDevice(%s)>" % str(self)
return f"<AppleDevice({self})>"

View file

@ -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}>"

View file

@ -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):

View file

@ -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}'>"

View file

@ -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):

View file

@ -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

View file

@ -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 = '''
(

View file

@ -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

View file

@ -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

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 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"],

View file

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Test constants."""
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."""
from .const_login import FIRST_NAME

View file

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

View file

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

View file

@ -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": [
{

View file

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

View file

@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
"""Account service tests."""
from unittest import TestCase
from six import PY3
from . import PyiCloudServiceMock
from .const import AUTHENTICATED_USER, VALID_PASSWORD
@ -42,7 +40,6 @@ class AccountServiceTest(TestCase):
assert device["modelSmallPhotoURL1x"]
assert device["modelDisplayName"]
# fmt: off
if PY3:
assert repr(device) == "<AccountDevice: {model: "+device.model_display_name+", name: "+device.name+"}>"
# fmt: on
@ -72,7 +69,6 @@ class AccountServiceTest(TestCase):
"""Tests storage."""
assert self.service.storage
# 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}>)])}>"
# fmt: on

View file

@ -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):

View file

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

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""Find My iPhone service tests."""
from unittest import TestCase
from . import PyiCloudServiceMock

View file

@ -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 .