From c3958428a08335daa4847235c405aba585115f40 Mon Sep 17 00:00:00 2001 From: Mandarons Date: Sat, 7 Dec 2024 00:24:56 +0000 Subject: [PATCH 1/2] Infrastructure updated to use Ruff --- .devcontainer/Dockerfile | 13 +-- .devcontainer/devcontainer.json | 75 ++++++++------- .ruff.toml | 129 +++++++++++++++++++++++++ .vscode/launch.json | 22 +++++ .vscode/settings.json | 7 ++ generate_badges.py | 4 +- icloudpy/__init__.py | 3 +- icloudpy/base.py | 121 ++++++++++++++---------- icloudpy/cmdline.py | 32 +++---- icloudpy/services/__init__.py | 18 ++-- icloudpy/services/account.py | 10 +- icloudpy/services/calendar.py | 6 +- icloudpy/services/contacts.py | 4 +- icloudpy/services/drive.py | 34 +++---- icloudpy/services/findmyiphone.py | 23 +++-- icloudpy/services/photos.py | 151 ++++++++++++++---------------- icloudpy/services/reminders.py | 10 +- icloudpy/utils.py | 2 +- requirements-test.txt | 4 +- run-ci.sh | 4 +- tests/__init__.py | 38 +++----- tests/const_drive.py | 12 +-- tests/const_findmyiphone.py | 2 +- tests/const_login.py | 17 ++-- tests/test_cmdline.py | 12 +-- tests/test_drive.py | 2 +- 26 files changed, 467 insertions(+), 288 deletions(-) create mode 100644 .ruff.toml create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 65e7cdc..0d152c5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -14,13 +14,14 @@ RUN \ unzip allure-commandline-2.20.1.zip -d /allure && \ rm allure-commandline-2.20.1.zip -ENV PATH "/allure/allure-2.20.1/bin:${PATH}" +USER vscode +# Install uv (pip replacement) +RUN \ + curl -LsSf https://astral.sh/uv/install.sh | sh -WORKDIR /workspaces +ENV PATH="/allure/allure-2.20.1/bin:/home/vscode/.cargo/bin:${PATH}" -# Install Python dependencies from requirements -COPY requirements*.txt ./ -RUN pip install -r requirements-test.txt +WORKDIR /workspaces # Set the default shell to bash instead of sh -ENV SHELL /bin/bash +ENV SHELL /bin/bash \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 18894d6..e2a2bcc 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,37 +3,48 @@ "context": "..", "dockerFile": "Dockerfile", "containerEnv": { "DEVCONTAINER": "1" }, - "extensions": [ - "ms-python.vscode-pylance", - "visualstudioexptteam.vscodeintellicode", - "redhat.vscode-yaml", - "esbenp.prettier-vscode", - "GitHub.vscode-pull-request-github" - ], - // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json - "settings": { - "python.linting.enabled": true, - "python.linting.pylintEnabled": true, - "python.formatting.blackPath": "/usr/local/bin/black", - "python.formatting.provider": "black", - "python.testing.pytestArgs": ["--no-cov"], - "editor.formatOnPaste": false, - "editor.formatOnSave": true, - "editor.formatOnType": true, - "files.trimTrailingWhitespace": true, - "terminal.integrated.profiles.linux": { - "zsh": { - "path": "/usr/bin/zsh" + "customizations": { + "vscode": { + "extensions": [ + "ms-python.vscode-pylance", + "visualstudioexptteam.vscodeintellicode", + "redhat.vscode-yaml", + "esbenp.prettier-vscode", + "GitHub.vscode-pull-request-github", + "github.vscode-github-actions", + "charliermarsh.ruff" + ], + // Please keep this file in sync with settings in home-assistant/.vscode/settings.default.json + "settings": { + "[python]": { + "diffEditor.ignoreTrimWhitespace": false, + "editor.formatOnType": true, + "editor.formatOnSave": true, + "editor.wordBasedSuggestions": "off", + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + } + }, + "python.pythonPath": "./venv/bin/python", + "python.testing.pytestArgs": ["--no-cov"], + "files.trimTrailingWhitespace": true, + "terminal.integrated.defaultProfile.linux": "bash", + "yaml.customTags": [ + "!input scalar", + "!secret scalar", + "!include_dir_named scalar", + "!include_dir_list scalar", + "!include_dir_merge_list scalar", + "!include_dir_merge_named scalar" + ] } - }, - "terminal.integrated.defaultProfile.linux": "zsh", - "yaml.customTags": [ - "!input scalar", - "!secret scalar", - "!include_dir_named scalar", - "!include_dir_list scalar", - "!include_dir_merge_list scalar", - "!include_dir_merge_named scalar" - ] - } + } + }, + "remoteUser": "vscode", + "postCreateCommand": "uv venv && . .venv/bin/activate && uv pip install -r requirements-test.txt && git config commit.gpgsign true", + "mounts": [ + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.gnupg,target=/home/vscode/.gnupg,type=bind,consistency=cached" + ] } diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..9e2942d --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,129 @@ +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 120 +indent-width = 4 + +# Assume Python 3.8 +# target-version = "py38" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["F", "E", "W", +# "C90", + "I", +# "N", +# "D", +"UP", "YTT", +# "ANN", +"ASYNC", +# "S", +# "BLE", +# "FBT", +# "B", +# "A", +"COM", "C4", +# "DTZ", +# "T10", +"DJ", +#"EM", +"EXE", +#"FA", +# "ISC", +"ICN", +# "LOG", +# "G", +"INP", "PIE", +# "T20", +"PYI", +# "PT", +"Q", "RSE", +#"RET", +#"SLF", + "SLOT", +# "SIM", +"TID", "TCH", "INT", +# "ARG", +# "PTH", +"TD", +"FIX", +# "ERA", +"PD", "PGH", +# "PL", +# "TRY", +# "FLY", +"NPY", "AIR", +#"PERF", +# "FURB", +# "RUF" +] +ignore = ["E501"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = ["B"] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in select subdirectories. +[lint.per-file-ignores] +"__init__.py" = ["E402"] +"**/{tests,docs,tools}/*" = ["E402"] + + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +# docstring-code-format = true +# docstring-code-line-length = 120 + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +# docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1316b52 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python Debugger: Attach using Process Id", + "type": "debugpy", + "request": "attach", + "processId": "${command:pickProcess}" + }, + { + "name": "Debug Tests", + "type": "python", + "request": "test", + "console": "integratedTerminal", + "justMyCode": false, + "env": {"PYTEST_ADDOPTS": "--no-cov"} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b38853 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/generate_badges.py b/generate_badges.py index eb469cd..6a7a515 100644 --- a/generate_badges.py +++ b/generate_badges.py @@ -23,13 +23,13 @@ url_data = "passing&color=brightgreen" if test_result else "failing&color=critical" response = requests.get( - "https://img.shields.io/static/v1?label=Tests&message=" + url_data + "https://img.shields.io/static/v1?label=Tests&message=" + url_data, ) with open(badges_directory + "/tests.svg", "w") as f: f.write(response.text) url_data = "brightgreen" if coverage_result == 100.0 else "critical" response = requests.get( - f"https://img.shields.io/static/v1?label=Coverage&message={coverage_result}%&color={url_data}" + f"https://img.shields.io/static/v1?label=Coverage&message={coverage_result}%&color={url_data}", ) with open(badges_directory + "/coverage.svg", "w") as f: f.write(response.text) diff --git a/icloudpy/__init__.py b/icloudpy/__init__.py index 93b09f6..9cf4b20 100644 --- a/icloudpy/__init__.py +++ b/icloudpy/__init__.py @@ -1,6 +1,7 @@ """The iCloudPy library.""" + import logging -from icloudpy.base import ICloudPyService # pylint: disable=unused-import +from icloudpy.base import ICloudPyService # # noqa: F401 logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/icloudpy/base.py b/icloudpy/base.py index 4a6842a..32a8939 100644 --- a/icloudpy/base.py +++ b/icloudpy/base.py @@ -1,5 +1,8 @@ """Library base file.""" + +import base64 import getpass +import hashlib import http.cookiejar as cookielib import inspect import json @@ -8,10 +11,8 @@ from re import match from tempfile import gettempdir from uuid import uuid1 -import srp -import base64 -import hashlib +import srp from requests import Session from six import PY2 @@ -67,7 +68,6 @@ def __init__(self, service): Session.__init__(self) def request(self, method, url, **kwargs): # pylint: disable=arguments-differ - # Charge logging to the right service endpoint callee = inspect.stack()[2] module = inspect.getmodule(callee[0]) @@ -88,7 +88,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ if response.headers.get(header): session_arg = value self.service.session_data.update( - {session_arg: response.headers.get(header)} + {session_arg: response.headers.get(header)}, ) # Save session_data to file @@ -100,18 +100,11 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ self.cookies.save(ignore_discard=True, ignore_expires=True) LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path) - if not response.ok and ( - content_type not in json_mimetypes - or response.status_code in [421, 450, 500] - ): + if not response.ok and (content_type not in json_mimetypes or response.status_code in [421, 450, 500]): try: # pylint: disable=W0212 fmip_url = self.service._get_webservice_url("findme") - if ( - has_retried is None - and response.status_code == 450 - and fmip_url in url - ): + if has_retried is None and response.status_code == 450 and fmip_url in url: # Handle re-authentication for Find My iPhone LOGGER.debug("Re-authenticating Find My iPhone service") try: @@ -125,7 +118,9 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ if has_retried is None and response.status_code in [421, 450, 500]: api_error = ICloudPyAPIResponseException( - response.reason, response.status_code, retry=True + response.reason, + response.status_code, + retry=True, ) request_logger.debug(api_error) kwargs["retried"] = True @@ -138,7 +133,7 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ try: data = response.json() - except: # pylint: disable=bare-except + except: # noqa: E722 request_logger.warning("Failed to parse response with JSON mimetype") return response @@ -163,15 +158,11 @@ def request(self, method, url, **kwargs): # pylint: disable=arguments-differ return response def _raise_error(self, code, reason): - if ( - self.service.requires_2sa - and reason == "Missing X-APPLE-WEBAUTH-TOKEN cookie" - ): + if self.service.requires_2sa and reason == "Missing X-APPLE-WEBAUTH-TOKEN cookie": raise ICloudPy2SARequiredException(self.service.user["apple_id"]) if code in ("ZONE_NOT_FOUND", "AUTHENTICATION_FAILED"): reason = ( - reason + ". Please log into https://icloud.com/ to manually " - "finish setting up your iCloud service" + reason + ". Please log into https://icloud.com/ to manually " "finish setting up your iCloud service" ) api_error = ICloudPyServiceNotActivatedException(reason, code) LOGGER.error(api_error) @@ -252,7 +243,7 @@ def __init__( try: with open(self.session_path, encoding="utf-8") as session_f: self.session_data = json.load(session_f) - except: # pylint: disable=bare-except + except: # noqa: E722 LOGGER.info("Session file does not exist") if self.session_data.get("client_id"): self.client_id = self.session_data.get("client_id") @@ -264,7 +255,7 @@ def __init__( self.session = ICloudPySession(self) self.session.verify = verify self.session.headers.update( - {"Origin": self.home_endpoint, "Referer": f"{self.home_endpoint}/"} + {"Origin": self.home_endpoint, "Referer": f"{self.home_endpoint}/"}, ) cookiejar_path = self.cookiejar_path @@ -273,7 +264,7 @@ def __init__( try: self.session.cookies.load(ignore_discard=True, ignore_expires=True) LOGGER.debug("Read cookies from %s", cookiejar_path) - except: # pylint: disable=bare-except + except: # noqa: E722 # Most likely a pickled cookiejar from earlier versions. # The cookiejar will get replaced with a valid one after # successful authentication. @@ -302,12 +293,11 @@ def authenticate(self, force_refresh=False, service=None): if not login_successful and service is not None: app = self.data["apps"][service] - if ( - "canLaunchWithOneFactor" in app - and app["canLaunchWithOneFactor"] is True - ): + if "canLaunchWithOneFactor" in app and app["canLaunchWithOneFactor"] is True: LOGGER.debug( - "Authenticating as %s for %s", self.user["accountName"], service + "Authenticating as %s for %s", + self.user["accountName"], + service, ) try: @@ -330,35 +320,56 @@ def authenticate(self, force_refresh=False, service=None): if self.session_data.get("session_id"): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") - class SrpPassword(): + class SrpPassword: def __init__(self, password: str): self.password = password - def set_encrypt_info(self, salt: bytes, iterations: int, key_length: int): + def set_encrypt_info( + self, + salt: bytes, + iterations: int, + key_length: int, + ): self.salt = salt self.iterations = iterations self.key_length = key_length def encode(self): - password_hash = hashlib.sha256(self.password.encode('utf-8')).digest() - return hashlib.pbkdf2_hmac('sha256', password_hash, salt, iterations, key_length) + password_hash = hashlib.sha256( + self.password.encode("utf-8"), + ).digest() + return hashlib.pbkdf2_hmac( + "sha256", + password_hash, + salt, + iterations, + key_length, + ) srp_password = SrpPassword(self.user["password"]) srp.rfc5054_enable() srp.no_username_in_x() - usr = srp.User(self.user["accountName"], srp_password, hash_alg=srp.SHA256, ng_type=srp.NG_2048) + usr = srp.User( + self.user["accountName"], + srp_password, + hash_alg=srp.SHA256, + ng_type=srp.NG_2048, + ) - uname, A = usr.start_authentication() + uname, a_bytes = usr.start_authentication() data = { - 'a': base64.b64encode(A).decode(), - 'accountName': uname, - 'protocols': ['s2k', 's2k_fo'] + "a": base64.b64encode(a_bytes).decode(), + "accountName": uname, + "protocols": ["s2k", "s2k_fo"], } try: - response = self.session.post(f"{self.auth_endpoint}/signin/init", data=json.dumps(data), - headers=headers) + response = self.session.post( + f"{self.auth_endpoint}/signin/init", + data=json.dumps(data), + headers=headers, + ) response.raise_for_status() except ICloudPyAPIResponseException as error: msg = "Failed to initiate srp authentication." @@ -366,10 +377,10 @@ def encode(self): body = response.json() - salt = base64.b64decode(body['salt']) - b = base64.b64decode(body['b']) - c = body['c'] - iterations = body['iteration'] + salt = base64.b64decode(body["salt"]) + b = base64.b64decode(body["b"]) + c = body["c"] + iterations = body["iteration"] key_length = 32 srp_password.set_encrypt_info(salt, iterations, key_length) @@ -416,7 +427,8 @@ def _authenticate_with_token(self): try: req = self.session.post( - f"{self.setup_endpoint}/accountLogin", data=json.dumps(data) + f"{self.setup_endpoint}/accountLogin", + data=json.dumps(data), ) self.data = req.json() except ICloudPyAPIResponseException as error: @@ -433,7 +445,8 @@ def _authenticate_with_credentials_service(self, service): try: self.session.post( - f"{self.setup_endpoint}/accountLogin", data=json.dumps(data) + f"{self.setup_endpoint}/accountLogin", + data=json.dumps(data), ) self.data = self._validate_token() @@ -482,8 +495,7 @@ def session_path(self): """Get path for session data file.""" return path.join( self._cookie_directory, - "".join([c for c in self.user.get("accountName") if match(r"\w", c)]) - + ".session", + "".join([c for c in self.user.get("accountName") if match(r"\w", c)]) + ".session", ) @property @@ -509,7 +521,8 @@ def is_trusted_session(self): def trusted_devices(self): """Returns devices trusted for two-step authentication.""" request = self.session.get( - f"{self.setup_endpoint}/listDevices", params=self.params + f"{self.setup_endpoint}/listDevices", + params=self.params, ) return request.json().get("devices") @@ -599,7 +612,8 @@ def _get_webservice_url(self, ws_key): """Get webservice URL, raise an exception if not exists.""" if self._webservices.get(ws_key) is None: raise ICloudPyServiceNotActivatedException( - "Webservice not available", ws_key + "Webservice not available", + ws_key, ) return self._webservices[ws_key]["url"] @@ -608,7 +622,10 @@ def devices(self): """Returns all devices.""" service_root = self._get_webservice_url("findme") return FindMyiPhoneServiceManager( - service_root, self.session, self.params, self.with_family + service_root, + self.session, + self.params, + self.with_family, ) @property diff --git a/icloudpy/cmdline.py b/icloudpy/cmdline.py index 230b84c..a06e3e6 100644 --- a/icloudpy/cmdline.py +++ b/icloudpy/cmdline.py @@ -1,4 +1,4 @@ -#! /usr/bin/env python +#! /usr/bin/env python # noqa:EXE001 """ A Command Line Wrapper to allow easy use of iCloudPy for command line scripts, and related. @@ -49,10 +49,7 @@ def main(args=None): action="store", dest="password", default="", - help=( - "Apple ID Password to Use; if unspecified, password will be " - "fetched from the system keyring." - ), + help=("Apple ID Password to Use; if unspecified, password will be " "fetched from the system keyring."), ) parser.add_argument( "-n", @@ -203,7 +200,8 @@ def main(args=None): if not password: password = utils.get_password( - username, interactive=command_line.interactive + username, + interactive=command_line.interactive, ) if not password: @@ -237,7 +235,7 @@ def main(args=None): # fmt: off print( "\nTwo-step authentication required.", - "\nPlease enter validation code" + "\nPlease enter validation code", ) # fmt: on @@ -252,14 +250,14 @@ def main(args=None): # fmt: off print( "\nTwo-step authentication required.", - "\nYour trusted devices are:" + "\nYour trusted devices are:", ) # fmt: on devices = api.trusted_devices for i, device in enumerate(devices): print( - f' {i}: {device.get("deviceName", "SMS to " + device.get("phoneNumber"))}' + f' {i}: {device.get("deviceName", "SMS to " + device.get("phoneNumber"))}', ) print("\nWhich device would you like to use?") @@ -293,9 +291,7 @@ def main(args=None): print(message, file=sys.stderr) for dev in api.devices: - if not command_line.device_id or ( - command_line.device_id.strip().lower() == dev.content["id"].strip().lower() - ): + if not command_line.device_id or (command_line.device_id.strip().lower() == dev.content["id"].strip().lower()): # List device(s) if command_line.locate: dev.location() @@ -328,18 +324,20 @@ def main(args=None): dev.play_sound() else: raise RuntimeError( - f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n" + f"\n\n\t\tSounds can only be played on a singular device. {DEVICE_ERROR}\n\n", ) # Display a Message on the device if command_line.message: if command_line.device_id: dev.display_message( - subject="A Message", message=command_line.message, sounds=True + subject="A Message", + message=command_line.message, + sounds=True, ) else: raise RuntimeError( - f"Messages can only be played on a singular device. {DEVICE_ERROR}" + f"Messages can only be played on a singular device. {DEVICE_ERROR}", ) # Display a Silent Message on the device @@ -352,7 +350,7 @@ def main(args=None): ) else: raise RuntimeError( - f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}" + f"Silent Messages can only be played on a singular device. {DEVICE_ERROR}", ) # Enable Lost mode @@ -365,7 +363,7 @@ def main(args=None): ) else: raise RuntimeError( - f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}" + f"Lost Mode can only be activated on a singular device. {DEVICE_ERROR}", ) sys.exit(0) diff --git a/icloudpy/services/__init__.py b/icloudpy/services/__init__.py index d865acb..4d4448c 100644 --- a/icloudpy/services/__init__.py +++ b/icloudpy/services/__init__.py @@ -1,13 +1,15 @@ +# ruff: noqa """Services.""" -from icloudpy.services.account import AccountService # pylint: disable=unused-import -from icloudpy.services.calendar import CalendarService # pylint: disable=unused-import -from icloudpy.services.contacts import ContactsService # pylint: disable=unused-import -from icloudpy.services.drive import DriveService # pylint: disable=unused-import -from icloudpy.services.findmyiphone import ( # pylint: disable=unused-import + +from icloudpy.services.account import AccountService +from icloudpy.services.calendar import CalendarService +from icloudpy.services.contacts import ContactsService +from icloudpy.services.drive import DriveService +from icloudpy.services.findmyiphone import ( FindMyiPhoneServiceManager, ) -from icloudpy.services.photos import PhotosService # pylint: disable=unused-import -from icloudpy.services.reminders import ( # pylint: disable=unused-import +from icloudpy.services.photos import PhotosService +from icloudpy.services.reminders import ( RemindersService, ) -from icloudpy.services.ubiquity import UbiquityService # pylint: disable=unused-import +from icloudpy.services.ubiquity import UbiquityService diff --git a/icloudpy/services/account.py b/icloudpy/services/account.py index cb62bf7..1723a2a 100644 --- a/icloudpy/services/account.py +++ b/icloudpy/services/account.py @@ -53,7 +53,7 @@ def family(self): self.session, self.params, self._acc_family_member_photo_url, - ) + ), ) return self._family @@ -196,7 +196,7 @@ def get_photo(self): params_photo = dict(self._params) params_photo.update({"memberId": self.dsid}) return self._session.get( - self._acc_family_member_photo_url, params=params_photo, stream=True + self._acc_family_member_photo_url, params=params_photo, stream=True, ) def __getitem__(self, key): @@ -289,7 +289,7 @@ def available_storage_in_bytes(self): def available_storage_in_percent(self): """Gets the available storage in percent.""" return round( - self.available_storage_in_bytes * 100 / self.total_storage_in_bytes, 2 + self.available_storage_in_bytes * 100 / self.total_storage_in_bytes, 2, ) @property @@ -340,13 +340,13 @@ class AccountStorage: def __init__(self, storage_data): self.usage = AccountStorageUsage( - storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus") + storage_data.get("storageUsageInfo"), storage_data.get("quotaStatus"), ) self.usages_by_media = OrderedDict() for usage_media in storage_data.get("storageUsageByMedia"): self.usages_by_media[usage_media["mediaKey"]] = AccountStorageUsageForMedia( - usage_media + usage_media, ) def __unicode__(self): diff --git a/icloudpy/services/calendar.py b/icloudpy/services/calendar.py index 075b7a5..612c62c 100644 --- a/icloudpy/services/calendar.py +++ b/icloudpy/services/calendar.py @@ -32,7 +32,7 @@ def get_event_detail(self, pguid, guid): "lang": "en-us", "usertz": get_localzone().zone, "dsid": self.session.service.data["dsInfo"]["dsid"], - } + }, ) url = f"{self._calendar_event_detail_url}/{pguid}/{guid}" req = self.session.get(url, params=params) @@ -59,7 +59,7 @@ def refresh_client(self, from_dt=None, to_dt=None): "startDate": from_dt.strftime("%Y-%m-%d"), "endDate": to_dt.strftime("%Y-%m-%d"), "dsid": self.session.service.data["dsInfo"]["dsid"], - } + }, ) req = self.session.get(self._calendar_refresh_url, params=params) self.response = req.json() @@ -87,7 +87,7 @@ def calendars(self): "startDate": from_dt.strftime("%Y-%m-%d"), "endDate": to_dt.strftime("%Y-%m-%d"), "dsid": self.session.service.data["dsInfo"]["dsid"], - } + }, ) req = self.session.get(self._calendars, params=params) self.response = req.json() diff --git a/icloudpy/services/contacts.py b/icloudpy/services/contacts.py index b852576..f31584d 100644 --- a/icloudpy/services/contacts.py +++ b/icloudpy/services/contacts.py @@ -28,7 +28,7 @@ def refresh_client(self): "clientVersion": "2.1", "locale": "en_US", "order": "last,first", - } + }, ) req = self.session.get(self._contacts_refresh_url, params=params_contacts) self.response = req.json() @@ -40,7 +40,7 @@ def refresh_client(self): "syncToken": self.response["syncToken"], "limit": "0", "offset": "0", - } + }, ) req = self.session.get(self._contacts_next_url, params=params_next) self.response = req.json() diff --git a/icloudpy/services/drive.py b/icloudpy/services/drive.py index e31f3b1..78c4b30 100644 --- a/icloudpy/services/drive.py +++ b/icloudpy/services/drive.py @@ -40,8 +40,8 @@ def get_node_data(self, drivewsid): { "drivewsid": drivewsid, "partialData": False, - } - ] + }, + ], ), ) if not request.ok: @@ -70,7 +70,7 @@ def get_file(self, file_id, zone="com.apple.CloudDocs", **kwargs): def get_app_data(self): """Returns the app library (previously ubiquity).""" request = self.session.get( - self._service_root + "/retrieveAppLibraries", params=self.params + self._service_root + "/retrieveAppLibraries", params=self.params, ) if not request.ok: self.session.raise_error(request.status_code, request.reason) @@ -105,7 +105,7 @@ def _get_upload_contentws_url(self, file_object, zone="com.apple.CloudDocs"): "type": "FILE", "content_type": content_type, "size": file_size, - } + }, ), ) if not request.ok: @@ -113,7 +113,7 @@ def _get_upload_contentws_url(self, file_object, zone="com.apple.CloudDocs"): return (request.json()[0]["document_id"], request.json()[0]["url"]) def _update_contentws( - self, folder_id, sf_info, document_id, file_object, zone="com.apple.CloudDocs" + self, folder_id, sf_info, document_id, file_object, zone="com.apple.CloudDocs", ): data = { "data": { @@ -163,7 +163,7 @@ def send_file(self, folder_id, file_object, zone="com.apple.CloudDocs"): content_response = request.json()["singleFile"] self._update_contentws( - folder_id, content_response, document_id, file_object, zone + folder_id, content_response, document_id, file_object, zone, ) def create_folders(self, parent, name): @@ -179,9 +179,9 @@ def create_folders(self, parent, name): { "clientId": self.params["clientId"], "name": name, - } + }, ], - } + }, ), ) return request.json() @@ -198,9 +198,9 @@ def rename_items(self, node_id, etag, name): "drivewsid": node_id, "etag": etag, "name": name, - } + }, ], - } + }, ), ) return request.json() @@ -217,9 +217,9 @@ def move_items_to_trash(self, node_id, etag): "drivewsid": node_id, "etag": etag, "clientId": self.params["clientId"], - } + }, ], - } + }, ), ) if not request.ok: @@ -231,7 +231,7 @@ def root(self): """Returns the root node.""" if not self._root: self._root = DriveNode( - self, self.get_node_data("FOLDER::com.apple.CloudDocs::root") + self, self.get_node_data("FOLDER::com.apple.CloudDocs::root"), ) return self._root @@ -312,13 +312,13 @@ def open(self, **kwargs): response.raw = io.BytesIO() return response return self.connection.get_file( - self.data["docwsid"], zone=self.data["zone"], **kwargs + self.data["docwsid"], zone=self.data["zone"], **kwargs, ) def upload(self, file_object, **kwargs): """ "Upload a new file.""" return self.connection.send_file( - self.data["docwsid"], file_object, zone=self.data["zone"], **kwargs + self.data["docwsid"], file_object, zone=self.data["zone"], **kwargs, ) def dir(self): @@ -338,13 +338,13 @@ def mkdir(self, folder): def rename(self, name): """Rename an iCloud Drive item.""" return self.connection.rename_items( - self.data["drivewsid"], self.data["etag"], name + self.data["drivewsid"], self.data["etag"], name, ) def delete(self): """Delete an iCloud Drive item.""" return self.connection.move_items_to_trash( - self.data["drivewsid"], self.data["etag"] + self.data["drivewsid"], self.data["etag"], ) def get(self, name): diff --git a/icloudpy/services/findmyiphone.py b/icloudpy/services/findmyiphone.py index c7a63a9..fa58899 100644 --- a/icloudpy/services/findmyiphone.py +++ b/icloudpy/services/findmyiphone.py @@ -1,4 +1,5 @@ """Find my iPhone service.""" + import json from six import PY2 @@ -43,8 +44,8 @@ def refresh_client(self): "shouldLocate": True, "selectedDevice": "all", "deviceListVersion": 1, - } - } + }, + }, ), ) self.response = req.json() @@ -65,7 +66,7 @@ def refresh_client(self): self._devices[device_id].update(device_info) if not self._devices: - raise ICloudPyNoDevicesException() + raise ICloudPyNoDevicesException def __getitem__(self, key): if isinstance(key, int): @@ -142,12 +143,15 @@ def play_sound(self, subject="Find My iPhone Alert"): "device": self.content["id"], "subject": subject, "clientContext": {"fmly": True}, - } + }, ) self.session.post(self.sound_url, params=self.params, data=data) def display_message( - self, subject="Find My iPhone Alert", message="This is a note", sounds=False + self, + subject="Find My iPhone Alert", + message="This is a note", + sounds=False, ): """Send a request to the device to play a sound. @@ -160,12 +164,15 @@ def display_message( "sound": sounds, "userText": True, "text": message, - } + }, ) self.session.post(self.message_url, params=self.params, data=data) def lost_device( - self, number, text="This iPhone has been lost. Please call me.", newpasscode="" + self, + number, + text="This iPhone has been lost. Please call me.", + newpasscode="", ): """Send a request to the device to trigger 'lost mode'. @@ -182,7 +189,7 @@ def lost_device( "trackingEnabled": True, "device": self.content["id"], "passcode": newpasscode, - } + }, ) self.session.post(self.lost_url, params=self.params, data=data) diff --git a/icloudpy/services/photos.py b/icloudpy/services/photos.py index 53dbb6c..e3173f6 100644 --- a/icloudpy/services/photos.py +++ b/icloudpy/services/photos.py @@ -1,4 +1,5 @@ """Photo service.""" + import base64 import json import logging @@ -38,7 +39,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "TIMELAPSE"}, - } + }, ], }, "Videos": { @@ -50,7 +51,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "VIDEO"}, - } + }, ], }, "Slo-mo": { @@ -62,7 +63,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "SLOMO"}, - } + }, ], }, "Bursts": { @@ -80,7 +81,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "FAVORITE"}, - } + }, ], }, "Panoramas": { @@ -92,7 +93,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "PANORAMA"}, - } + }, ], }, "Screenshots": { @@ -104,7 +105,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "SCREENSHOT"}, - } + }, ], }, "Live": { @@ -116,7 +117,7 @@ class PhotoLibrary: "fieldName": "smartAlbum", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": "LIVE"}, - } + }, ], }, "Recently Deleted": { @@ -144,20 +145,19 @@ def __init__(self, service, zone_id): { "query": {"recordType": "CheckIndexingState"}, "zoneID": self.zone_id, - } + }, ) request = self.service.session.post( - url, data=json_data, headers={"Content-type": "text/plain"} + url, + data=json_data, + headers={"Content-type": "text/plain"}, ) response = request.json() indexing_state = response["records"][0]["fields"]["state"]["value"] if indexing_state != "FINISHED": raise ICloudPyServiceNotActivatedException( - ( - "iCloud Photo Library not finished indexing. Please try " - "again in a few minutes" - ), + ("iCloud Photo Library not finished indexing. Please try " "again in a few minutes"), None, ) @@ -170,29 +170,23 @@ def albums(self): } for folder in self._fetch_folders(): - # FIXME: Handle subfolders if folder["recordName"] in ( "----Root-Folder----", "----Project-Root-Folder----", - ) or ( - folder["fields"].get("isDeleted") - and folder["fields"]["isDeleted"]["value"] - ): + ) or (folder["fields"].get("isDeleted") and folder["fields"]["isDeleted"]["value"]): continue folder_id = folder["recordName"] - folder_obj_type = ( - f"CPLContainerRelationNotDeletedByAssetDate:{folder_id}" - ) + folder_obj_type = f"CPLContainerRelationNotDeletedByAssetDate:{folder_id}" folder_name = base64.b64decode( - folder["fields"]["albumNameEnc"]["value"] + folder["fields"]["albumNameEnc"]["value"], ).decode("utf-8") query_filter = [ { "fieldName": "parentId", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": folder_id}, - } + }, ] album = PhotoAlbum( @@ -215,11 +209,13 @@ def _fetch_folders(self): { "query": {"recordType": "CPLAlbumByPositionLive"}, "zoneID": self.zone_id, - } + }, ) request = self.service.session.post( - url, data=json_data, headers={"Content-type": "text/plain"} + url, + data=json_data, + headers={"Content-type": "text/plain"}, ) response = request.json() @@ -240,20 +236,12 @@ def __init__(self, service_root, session, params): self.session = session self.params = dict(params) self._service_root = service_root - self._service_endpoint = ( - f"{self._service_root}/database/1/com.apple.photos.cloud/production/private" - ) + self._service_endpoint = f"{self._service_root}/database/1/com.apple.photos.cloud/production/private" self._libraries = None self.params.update({"remapEnums": True, "getCurrentSyncToken": True}) - # TODO: Does syncToken ever change? - # self.params.update({ - # 'syncToken': response['syncToken'], - # 'clientInstanceId': self.params.pop('clientId') - # }) - self._photo_assets = {} super().__init__(service=self, zone_id={"zoneName": "PrimarySync"}) @@ -264,7 +252,9 @@ def libraries(self): try: url = f"{self._service_endpoint}/zones/list" request = self.session.post( - url, data="{}", headers={"Content-type": "text/plain"} + url, + data="{}", + headers={"Content-type": "text/plain"}, ) response = request.json() zones = response["zones"] @@ -350,23 +340,21 @@ def __len__(self): }, "zoneWide": True, "zoneID": {"zoneName": self._zone_id}, - } - ] - } + }, + ], + }, ), headers={"Content-type": "text/plain"}, ) response = request.json() - self._len = response["batch"][0]["records"][0]["fields"]["itemCount"][ - "value" - ] + self._len = response["batch"][0]["records"][0]["fields"]["itemCount"]["value"] return self._len def _fetch_subalbums(self): url = (f"{self.service._service_endpoint}/records/query?") + urlencode( - self.service.params + self.service.params, ) # pylint: disable=consider-using-f-string query = """{{ @@ -387,7 +375,8 @@ def _fetch_subalbums(self): "zoneName":"{}" }} }}""".format( - self.folder_id, self._zone_id["zoneName"] + self.folder_id, + self._zone_id["zoneName"], ) json_data = query request = self.service.session.post( @@ -404,25 +393,20 @@ def subalbums(self): """Returns the subalbums""" if not self._subalbums and self.folder_id: for folder in self._fetch_subalbums(): - if ( - folder["fields"].get("isDeleted") - and folder["fields"]["isDeleted"]["value"] - ): + if folder["fields"].get("isDeleted") and folder["fields"]["isDeleted"]["value"]: continue folder_id = folder["recordName"] - folder_obj_type = ( - f"CPLContainerRelationNotDeletedByAssetDate:{folder_id}" - ) + folder_obj_type = f"CPLContainerRelationNotDeletedByAssetDate:{folder_id}" folder_name = base64.b64decode( - folder["fields"]["albumNameEnc"]["value"] + folder["fields"]["albumNameEnc"]["value"], ).decode("utf-8") query_filter = [ { "fieldName": "parentId", "comparator": "EQUALS", "fieldValue": {"type": "STRING", "value": folder_id}, - } + }, ] album = PhotoAlbum( @@ -448,14 +432,17 @@ def photos(self): while True: url = (f"{self.service._service_endpoint}/records/query?") + urlencode( - self.service.params + self.service.params, ) request = self.service.session.post( url, data=json.dumps( self._list_query_gen( - offset, self.list_type, self.direction, self.query_filter - ) + offset, + self.list_type, + self.direction, + self.query_filter, + ), ), headers={"Content-type": "text/plain"}, ) @@ -480,7 +467,9 @@ def photos(self): for master_record in master_records: record_name = master_record["recordName"] yield PhotoAsset( - self.service, master_record, asset_records[record_name] + self.service, + master_record, + asset_records[record_name], ) else: break @@ -604,7 +593,7 @@ def _list_query_gen(self, offset, list_type, direction, query_filter=None): "isKeyAsset", "importedByBundleIdentifierEnc", "importedByDisplayNameEnc", - "importedBy" + "importedBy", ], "zoneID": self._zone_id, } @@ -664,7 +653,7 @@ def id(self): def filename(self): """Gets the photo file name.""" return base64.b64decode( - self._master_record["fields"]["filenameEnc"]["value"] + self._master_record["fields"]["filenameEnc"]["value"], ).decode("utf-8") @property @@ -682,7 +671,8 @@ def asset_date(self): """Gets the photo asset date.""" try: return datetime.fromtimestamp( - self._asset_record["fields"]["assetDate"]["value"] / 1000.0, tz=UTC + self._asset_record["fields"]["assetDate"]["value"] / 1000.0, + tz=UTC, ) except KeyError: return datetime.fromtimestamp(0) @@ -691,7 +681,8 @@ def asset_date(self): def added_date(self): """Gets the photo added date.""" return datetime.fromtimestamp( - self._asset_record["fields"]["addedDate"]["value"] / 1000.0, tz=UTC + self._asset_record["fields"]["addedDate"]["value"] / 1000.0, + tz=UTC, ) @property @@ -753,33 +744,25 @@ def download(self, version="original", **kwargs): return None return self._service.session.get( - self.versions[version]["url"], stream=True, **kwargs + self.versions[version]["url"], + stream=True, + **kwargs, ) def delete(self): """Deletes the photo.""" json_data = ( - '{"query":{"recordType":"CheckIndexingState"},' - '"zoneID":{"zoneName":"PrimarySync"}}' - ) - # pylint: disable=consider-using-f-string - json_data = ( - '{"operations":[{' - '"operationType":"update",' - '"record":{' - '"recordName":"%s",' - '"recordType":"%s",' - '"recordChangeTag":"%s",' - '"fields":{"isDeleted":{"value":1}' - "}}}]," - '"zoneID":{' - '"zoneName":"PrimarySync"' - '},"atomic":true}' - % ( - self._asset_record["recordName"], - self._asset_record["recordType"], - self._master_record["recordChangeTag"], - ) + f'{{"operations":[{{' + f'"operationType":"update",' + f'"record":{{' + f'"recordName":"{self._asset_record["recordName"]}",' + f'"recordType":"{self._asset_record["recordType"]}",' + f'"recordChangeTag":"{self._master_record["recordChangeTag"]}",' + f'"fields":{{"isDeleted":{{"value":1}}' + f'}}}}],' + f'"zoneID":{{' + f'"zoneName":"PrimarySync"' + f'}},"atomic":true}}' ) endpoint = self._service.service_endpoint @@ -787,7 +770,9 @@ def delete(self): url = f"{endpoint}/records/modify?{params}" 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): diff --git a/icloudpy/services/reminders.py b/icloudpy/services/reminders.py index 132fa15..d7b3bf8 100644 --- a/icloudpy/services/reminders.py +++ b/icloudpy/services/reminders.py @@ -29,12 +29,12 @@ def refresh(self): "lang": "en-us", "usertz": get_localzone().zone, "dsid": self.session.service.data["dsInfo"]["dsid"], - } + }, ) # Open reminders req = self.session.get( - self._service_root + "/rd/startup", params=params_reminders + self._service_root + "/rd/startup", params=params_reminders, ) data = req.json() @@ -68,7 +68,7 @@ def refresh(self): "title": reminder["title"], "desc": reminder.get("description"), "due": due, - } + }, ) self.lists[collection["title"]] = temp @@ -81,7 +81,7 @@ def post(self, title, description="", collection=None, due_date=None): params_reminders = dict(self._params) params_reminders.update( - {"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone} + {"clientVersion": "4.0", "lang": "en-us", "usertz": get_localzone().zone}, ) due_dates = None @@ -121,7 +121,7 @@ def post(self, title, description="", collection=None, due_date=None): "guid": str(uuid.uuid4()), }, "ClientState": {"Collections": list(self.collections.values())}, - } + }, ), params=params_reminders, ) diff --git a/icloudpy/utils.py b/icloudpy/utils.py index 044cbb4..a924d0e 100644 --- a/icloudpy/utils.py +++ b/icloudpy/utils.py @@ -38,7 +38,7 @@ def get_password_from_keyring(username): f"No iCloudPy 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." + "username.", ) return result diff --git a/requirements-test.txt b/requirements-test.txt index 4bcb477..12a8714 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -2,7 +2,9 @@ pytest==8.3.3 allure-pytest==2.13.5 coverage==7.6.8 -pylint==3.3.2 pytest-cov==6.0.0 black==24.3.0 +ruff +ipython pre-commit +setuptools \ No newline at end of file diff --git a/run-ci.sh b/run-ci.sh index 646fd76..d4ed3f6 100755 --- a/run-ci.sh +++ b/run-ci.sh @@ -15,8 +15,8 @@ deleteDir icloudpy.egg-info deleteFile .coverage deleteFile coverage.xml -echo "Linting ..." && - pylint icloudpy/ tests/ && +echo "Ruffing ..." && + ruff check --fix && echo "Testing ..." && pytest && echo "Reporting ..." && diff --git a/tests/__init__.py b/tests/__init__.py index 1d26667..a904279 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,6 +1,6 @@ """Library tests.""" + import json -import pprint from requests import Response @@ -8,15 +8,14 @@ from .const import ( AUTHENTICATED_USER, + CLIENT_ID, REQUIRES_2FA_TOKEN, REQUIRES_2FA_USER, VALID_2FA_CODE, VALID_COOKIE, - VALID_PASSWORD, VALID_TOKEN, VALID_TOKENS, VALID_USERS, - CLIENT_ID, ) from .const_account import ACCOUNT_DEVICES_WORKING, ACCOUNT_STORAGE_WORKING from .const_account_family import ACCOUNT_FAMILY_WORKING @@ -31,9 +30,9 @@ from .const_findmyiphone import FMI_FAMILY_WORKING from .const_login import ( AUTH_OK, - SRP_INIT_OK, LOGIN_2FA, LOGIN_WORKING, + SRP_INIT_OK, TRUSTED_DEVICE_1, TRUSTED_DEVICES, VERIFICATION_CODE_KO, @@ -64,7 +63,6 @@ class ICloudPySessionMock(base.ICloudPySession): mkdir_called = False def request(self, method, url, **kwargs): - """Mock request.""" params = kwargs.get("params") headers = kwargs.get("headers") @@ -106,7 +104,7 @@ def request(self, method, url, **kwargs): # or data.get("password") != VALID_PASSWORD ): self._raise_error(None, "Unknown reason") - if url.endswith('/init'): + if url.endswith("/init"): return ResponseMock(SRP_INIT_OK) if data.get("accountName") == REQUIRES_2FA_USER: self.service.session_data["session_token"] = REQUIRES_2FA_TOKEN @@ -135,34 +133,22 @@ def request(self, method, url, **kwargs): return ResponseMock(ACCOUNT_STORAGE_WORKING) # Drive - if ( - "retrieveItemDetailsInFolders" in url - and method == "POST" - and data[0].get("drivewsid") - ): + if "retrieveItemDetailsInFolders" in url and method == "POST" and data[0].get("drivewsid"): if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::root": return ResponseMock(DRIVE_ROOT_WORKING) if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::documents": return ResponseMock(DRIVE_ROOT_INVALID) if data[0].get("drivewsid") == "FOLDER::com.apple.Preview::documents": return ResponseMock(DRIVE_ROOT_INVALID) - if ( - data[0].get("drivewsid") - == "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B" - ): + if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::1C7F1760-D940-480F-8C4F-005824A4E05B": return ResponseMock(DRIVE_FOLDER_WORKING) - if ( - data[0].get("drivewsid") - == "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF" - ): + if data[0].get("drivewsid") == "FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF": print("getFolder params:", self.params, self.mkdir_called) if self.mkdir_called: return ResponseMock(DRIVE_SUBFOLDER_WORKING_AFTER_MKDIR) else: return ResponseMock(DRIVE_SUBFOLDER_WORKING) - if ( - "/createFolders" in url - and method == "POST"): + if "/createFolders" in url and method == "POST": self.mkdir_called = True return ResponseMock(DRIVE_SUBFOLDER_WORKING_AFTER_MKDIR) # Drive download @@ -195,5 +181,11 @@ def __init__( """Init the object.""" base.ICloudPySession = ICloudPySessionMock base.ICloudPyService.__init__( - self, apple_id, password, cookie_directory, verify, client_id, with_family + self, + apple_id, + password, + cookie_directory, + verify, + client_id, + with_family, ) diff --git a/tests/const_drive.py b/tests/const_drive.py index 4345e10..4e75a13 100644 --- a/tests/const_drive.py +++ b/tests/const_drive.py @@ -563,12 +563,12 @@ }, ], "numberOfItems": 5, - } + }, ] # App specific folder (Keynote, Numbers, Pages, Preview ...) type=APP_LIBRARY DRIVE_ROOT_INVALID = [ - {"drivewsid": "FOLDER::com.apple.CloudDocs::documents", "status": "ID_INVALID"} + {"drivewsid": "FOLDER::com.apple.CloudDocs::documents", "status": "ID_INVALID"}, ] DRIVE_FOLDER_WORKING = [ @@ -599,10 +599,10 @@ "shareCount": 0, "shareAliasCount": 0, "directChildrenCount": 2, - } + }, ], "numberOfItems": 1, - } + }, ] DRIVE_SUBFOLDER_WORKING = [ @@ -652,7 +652,7 @@ }, ], "numberOfItems": 2, - } + }, ] DRIVE_SUBFOLDER_WORKING_AFTER_MKDIR = [ @@ -717,7 +717,7 @@ }, ], "numberOfItems": 3, - } + }, ] DRIVE_FILE_DOWNLOAD_WORKING = { diff --git a/tests/const_findmyiphone.py b/tests/const_findmyiphone.py index c3b082e..8dfe5ab 100644 --- a/tests/const_findmyiphone.py +++ b/tests/const_findmyiphone.py @@ -96,7 +96,7 @@ "webPrefs": { "id": "web_prefs", "selectedDeviceId": "iPhone4,1", - } + }, }, "content": [ { diff --git a/tests/const_login.py b/tests/const_login.py index 8327f02..86d99ea 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -1,4 +1,5 @@ """Login test constants.""" + from .const_account_family import ( APPLE_ID_EMAIL, FIRST_NAME, @@ -16,12 +17,16 @@ # Data AUTH_OK = {"authType": "hsa2"} -SRP_INIT_OK = {'iteration': 20433, - 'salt': '0samK84bcBmkVsswOpZbZg==', - 'protocol': 's2k', - 'b': 'STVHcWTN9YOYn4IgtIJ6UPdPbvzvL+zza/l+6yUHUtdEyxwzpB78y8wqZ8QWSbVqjBcpl32iEA4T3nYp0LWZ5hD3r3yIJFloXvX0kpBJkr+Nh8EfHuW1V50A8riH6VWyuJ8m3JmOO7/xkNgP7je8GMpt/5f/7qE3AOj73e3JR0fzQ7IopdU0tlyVX0tD7T6wCyHS52GJWDdq1I2bgzurIK2/ZjR/Hwzd/67oFQPtKQgjrSRaKo5MJEfDP7C9wOlXsZqbb7igX6PeZRWrfl+iQFaA/FVeWSngB07ja3wOryY9GsYO06ELGOaQ+MpsT7mouqrGTfOJ0OMh9EgrkJEM6w==', - 'c': 'e-1be-8746c235-b41c-11ef-bd17-c780acb4fe15:PRN' - } +SRP_INIT_OK = { + "iteration": 20433, + "salt": "0samK84bcBmkVsswOpZbZg==", + "protocol": "s2k", + "b": "STVHcWTN9YOYn4IgtIJ6UPdPbvzvL+zza/l+6yUHUtdEyxwzpB78y8wqZ8QWSbVqjBcpl32iEA4T3nYp0LWZ5hD3r3yIJFloXvX0kpBJkr\ + +Nh8EfHuW1V50A8riH6VWyuJ8m3JmOO7/xkNgP7je8GMpt/5f/7qE3AOj73e3JR0fzQ7IopdU0tlyVX0tD7T6wCyHS52GJWDdq1I2bgzurIK2\ + /ZjR/Hwzd/67oFQPtKQgjrSRaKo5MJEfDP7C9wOlXsZqbb7igX6PeZRWrfl+iQFaA/FVeWSngB07ja3wOryY9GsYO06ELGOaQ+MpsT7mouqrGT\ + fOJ0OMh9EgrkJEM6w==", + "c": "e-1be-8746c235-b41c-11ef-bd17-c780acb4fe15:PRN", +} LOGIN_WORKING = { "dsInfo": { diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index febfc97..d94bd96 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -48,7 +48,7 @@ def test_username(self): @patch("keyring.get_password", return_value=None) @patch("getpass.getpass") def test_username_password_invalid( - self, mock_getpass, mock_get_password + self, mock_getpass, mock_get_password, ): # pylint: disable=unused-argument """Test username and password commands.""" # No password supplied @@ -59,20 +59,20 @@ def test_username_password_invalid( # Bad username or password mock_getpass.return_value = "invalid_pass" with pytest.raises( - RuntimeError, match="Bad username or password for invalid_user" + RuntimeError, match="Bad username or password for invalid_user", ): self.main(["--username", "invalid_user"]) # We should not use getpass for this one, but we reset the password at login fail with pytest.raises( - RuntimeError, match="Bad username or password for invalid_user" + RuntimeError, match="Bad username or password for invalid_user", ): self.main(["--username", "invalid_user", "--password", "invalid_pass"]) @patch("keyring.get_password", return_value=None) @patch("icloudpy.cmdline.input") def test_username_password_requires_2fa( - self, mock_input, mock_get_password + self, mock_input, mock_get_password, ): # pylint: disable=unused-argument """Test username and password commands.""" # Valid connection for the first time @@ -88,7 +88,7 @@ def test_username_password_requires_2fa( @patch("keyring.get_password", return_value=None) def test_device_outputfile( - self, mock_get_password + self, mock_get_password, ): # pylint: disable=unused-argument """Test the outputfile command.""" with pytest.raises(SystemExit, match="0"): @@ -97,7 +97,7 @@ def test_device_outputfile( "--username", AUTHENTICATED_USER, "--password", VALID_PASSWORD, "--non-interactive", - "--outputfile" + "--outputfile", ]) # fmt: on diff --git a/tests/test_drive.py b/tests/test_drive.py index 0b8b6cb..753702c 100644 --- a/tests/test_drive.py +++ b/tests/test_drive.py @@ -4,7 +4,7 @@ import pytest from . import ICloudPyServiceMock -from .const import AUTHENTICATED_USER, VALID_PASSWORD, CLIENT_ID +from .const import AUTHENTICATED_USER, CLIENT_ID, VALID_PASSWORD # pylint: disable=pointless-statement From 7bde55863974dc044257a20fe2c0a40964d1fcb7 Mon Sep 17 00:00:00 2001 From: Mandarons Date: Sat, 7 Dec 2024 00:25:16 +0000 Subject: [PATCH 2/2] infra updates, using ruff --- .github/workflows/ci-main-test-coverage.yml | 1 - .github/workflows/ci-pr-test.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci-main-test-coverage.yml b/.github/workflows/ci-main-test-coverage.yml index ab37da3..dc8e23c 100644 --- a/.github/workflows/ci-main-test-coverage.yml +++ b/.github/workflows/ci-main-test-coverage.yml @@ -59,7 +59,6 @@ jobs: pip install -r requirements-test.txt - name: Test with pytest run: | - #pylint icloudpy/ tests/ && pytest pytest && rm htmlcov/.gitignore - name: Upload coverage artifacts uses: actions/upload-artifact@v3 diff --git a/.github/workflows/ci-pr-test.yml b/.github/workflows/ci-pr-test.yml index 94e1369..75b0e18 100644 --- a/.github/workflows/ci-pr-test.yml +++ b/.github/workflows/ci-pr-test.yml @@ -60,7 +60,7 @@ jobs: # uses: mxschmitt/action-tmate@v3 - name: Test with pytest run: | - pytest + ruff check && pytest - name: Generate Allure Report uses: simple-elf/allure-report-action@master if: always()