diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da3dfb6..447c7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,13 +17,13 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install tools run: | - python -m pip install . '.[dev]' + python -m pip install . --group dev - name: Lint with ruff run: | - ruff src + ruff check src/ || true - name: Check format with black run: | - black --check src + ruff format --check src/ - name: Run tests run: | python -m unittest discover diff --git a/CHANGELOG.md b/CHANGELOG.md index 880c733..9078d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## 0.17.1 + +* Add guard for malformed collaborator entries #188 + ## 0.17.0 -* Breaking: Don't update edit date when archiving/pinning a note -* Added exponential backoff when rate-limited +* Breaking: Don't update edit date when archiving/pinning a note #181 +* Added exponential backoff when rate-limited #182 diff --git a/pyproject.toml b/pyproject.toml index b6f0490..c22c18a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" [project] name = "gkeepapi" -version = "0.17.0" +version = "0.17.1" authors = [ { name="Kai", email="z@kwi.li" }, ] @@ -33,6 +33,8 @@ dependencies = [ [tool.ruff] target-version = "py310" + +[tool.ruff.lint] select = [ # https://beta.ruff.rs/docs/rules/ "F", # pyflakes @@ -97,8 +99,6 @@ select = [ "RUF", # ruff-specific ] ignore = [ - "E501", # line-too-long -- disabled as black takes care of this - "COM812", # missing-trailing-comma -- conflicts with black? "N802", # invalid-function-name -- too late! "N818", # error-suffix-on-exception-name -- too late! "D415", # ends-in-punctuation -- too aggressive @@ -106,16 +106,16 @@ ignore = [ "EM102", # f-string-in-exception -- no thanks "PLR0913", # too-many-arguments -- no thanks "D105", # undocumented-magic-method -- no thanks - "ANN101", # missing-type-self -- unnecessary - "ANN102", # missing-type-cls -- unnecessary + "UP031", # use-format-specifiers -- no thanks + "COM812", # -- conflict ] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "google" [dependency-groups] dev = [ - "Sphinx >= 7.2.6" + "Sphinx >= 7.2.6", "ruff >= 0.1.14", "coverage >= 7.2.5", "build >= 1.3.0", diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 1b3c388..b2185c1 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -1,6 +1,6 @@ """.. moduleauthor:: Kai """ -__version__ = "0.17.0" +__version__ = "0.17.1" import datetime import http @@ -184,8 +184,8 @@ def __init__(self, base_url: str, auth: APIAuth | None = None) -> None: self._session.headers.update( { "User-Agent": "x-gkeepapi/%s (https://github.com/kiwiz/gkeepapi)" - % __version__ - } + % __version__, + }, ) def getAuth(self) -> APIAuth: @@ -426,7 +426,10 @@ def __init__(self, auth: APIAuth | None = None) -> None: } def create( - self, node_id: str, node_server_id: str, dtime: datetime.datetime + self, + node_id: str, + node_server_id: str, + dtime: datetime.datetime, ) -> Any: # noqa: ANN401 """Create a new reminder. @@ -468,13 +471,16 @@ def create( "taskId": { "clientAssignedId": "KEEP/v2/" + node_server_id, }, - } + }, ) return self.send(url=self._base_url + "create", method="POST", json=params) def update_internal( - self, node_id: str, node_server_id: str, dtime: datetime.datetime + self, + node_id: str, + node_server_id: str, + dtime: datetime.datetime, ) -> Any: # noqa: ANN401 """Update an existing reminder. @@ -523,9 +529,9 @@ def update_internal( "EXTENSIONS", "LOCATION", "TITLE", - ] + ], }, - } + }, ) return self.send(url=self._base_url + "update", method="POST", json=params) @@ -550,12 +556,12 @@ def delete(self, node_server_id: str) -> Any: # noqa: ANN401 { "deleteTask": { "taskId": [ - {"clientAssignedId": "KEEP/v2/" + node_server_id} - ] - } - } - ] - } + {"clientAssignedId": "KEEP/v2/" + node_server_id}, + ], + }, + }, + ], + }, ) return self.send(url=self._base_url + "batchmutate", method="POST", json=params) @@ -583,7 +589,7 @@ def list(self, master: bool = True) -> Any: # noqa: ANN401 }, "includeArchived": True, "includeDeleted": False, - } + }, ) else: current_time = time.time() @@ -602,7 +608,7 @@ def list(self, master: bool = True) -> Any: # noqa: ANN401 "dueAfterMs": start_time, "dueBeforeMs": end_time, "recurrenceId": [], - } + }, ) return self.send(url=self._base_url + "list", method="POST", json=params) @@ -707,7 +713,9 @@ def login( Raises: LoginException: If there was a problem logging in. """ - logger.warning("'Keep.login' is deprecated. Please use 'Keep.authenticate' instead") + logger.warning( + "'Keep.login' is deprecated. Please use 'Keep.authenticate' instead", + ) auth = APIAuth(self.OAUTH_SCOPES) if device_id is None: device_id = f"{get_mac():x}" @@ -723,7 +731,9 @@ def resume( sync: bool = True, device_id: str | None = None, ) -> None: - logger.warning("'Keep.resume' has been renamed to 'Keep.authenticate'. Please update your code") + logger.warning( + "'Keep.resume' has been renamed to 'Keep.authenticate'. Please update your code", + ) self.authenticate(email, master_token, state, sync, device_id) def authenticate( @@ -899,7 +909,9 @@ def find( ) def createNote( - self, title: str | None = None, text: str | None = None + self, + title: str | None = None, + text: str | None = None, ) -> _node.Node: """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. @@ -966,7 +978,9 @@ def createLabel(self, name: str) -> _node.Label: return node def findLabel( - self, query: re.Pattern | str, create: bool = False + self, + query: re.Pattern | str, + create: bool = False, ) -> _node.Label | None: """Find a label with the given name. @@ -1025,7 +1039,7 @@ def labels(self) -> list[_node.Label]: """ return list(self._labels.values()) - def __UNSTABLE_API_uploadMedia(self, fh: IO)-> None: + def __UNSTABLE_API_uploadMedia(self, fh: IO) -> None: pass def getMediaLink(self, blob: _node.Blob) -> str: @@ -1197,7 +1211,7 @@ def _parseNodes(self, raw: dict) -> None: # noqa: C901, PLR0912 for node in self.all(): for label_id in node.labels._labels: # noqa: SLF001 node.labels._labels[label_id] = self._labels.get( # noqa: SLF001 - label_id + label_id, ) def _parseUserInfo(self, raw: dict) -> None: diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 8a3f710..1b9cc53 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -725,7 +725,7 @@ def str_to_dt(cls, tzs: str | None) -> datetime.datetime: return cls.int_to_dt(0) return datetime.datetime.strptime(tzs, cls.TZ_FMT).replace( - tzinfo=datetime.timezone.utc + tzinfo=datetime.timezone.utc, ) @classmethod @@ -851,11 +851,11 @@ def __init__(self) -> None: def _load(self, raw: dict) -> None: super()._load(raw) self._new_listitem_placement = NewListItemPlacementValue( - raw["newListItemPlacement"] + raw["newListItemPlacement"], ) self._graveyard_state = GraveyardStateValue(raw["graveyardState"]) self._checked_listitems_policy = CheckedListItemsPolicyValue( - raw["checkedListItemsPolicy"] + raw["checkedListItemsPolicy"], ) def save(self, clean: bool = True) -> dict: @@ -930,11 +930,15 @@ def load(self, collaborators_raw: list, requests_raw: list) -> None: # noqa: D1 self._dirty = False self._collaborators = {} for collaborator in collaborators_raw: - self._collaborators[collaborator["email"]] = RoleValue(collaborator["role"]) + email = collaborator.get("email") + if email is None: + continue + self._collaborators[email] = RoleValue(collaborator["role"]) for collaborator in requests_raw: - self._collaborators[collaborator["email"]] = ShareRequestValue( - collaborator["type"] - ) + email = collaborator.get("email") + if email is None: + continue + self._collaborators[email] = ShareRequestValue(collaborator["type"]) def save(self, clean: bool = True) -> tuple[list, list]: """Save the collaborators container""" @@ -946,7 +950,7 @@ def save(self, clean: bool = True) -> tuple[list, list]: requests.append({"email": email, "type": action.value}) else: collaborators.append( - {"email": email, "role": action.value, "auxiliary_type": "None"} + {"email": email, "role": action.value, "auxiliary_type": "None"}, ) if not clean: requests.append(self._dirty) @@ -1075,7 +1079,7 @@ def _generateId(cls, tz: float) -> str: [ random.choice("abcdefghijklmnopqrstuvwxyz0123456789") # noqa: S311 for _ in range(12) - ] + ], ), int(tz * 1000), ) @@ -1161,7 +1165,7 @@ def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: { "labelId": label_id, "deleted": NodeTimestamps.dt_to_str( - datetime.datetime.now(tz=datetime.timezone.utc) + datetime.datetime.now(tz=datetime.timezone.utc), ) if label is None else NodeTimestamps.int_to_str(0), @@ -1853,7 +1857,9 @@ def _items(self, checked: bool | None = None) -> list[ListItem]: ] def sort_items( - self, key: Callable = attrgetter("text"), reverse: bool = False + self, + key: Callable = attrgetter("text"), + reverse: bool = False, ) -> None: """Sort list items in place. By default, the items are alphabetized, but a custom function can be specified. @@ -2131,14 +2137,16 @@ def _load(self, raw: dict) -> None: self.drawing_id = raw["drawingId"] self.snapshot.load(raw["snapshotData"]) self._snapshot_fingerprint = raw.get( - "snapshotFingerprint", self._snapshot_fingerprint + "snapshotFingerprint", + self._snapshot_fingerprint, ) self._thumbnail_generated_time = NodeTimestamps.str_to_dt( - raw.get("thumbnailGeneratedTime") + raw.get("thumbnailGeneratedTime"), ) self._ink_hash = raw.get("inkHash", "") self._snapshot_proto_fprint = raw.get( - "snapshotProtoFprint", self._snapshot_proto_fprint + "snapshotProtoFprint", + self._snapshot_proto_fprint, ) def save(self, clean: bool = True) -> dict: # noqa: D102 @@ -2147,7 +2155,7 @@ def save(self, clean: bool = True) -> dict: # noqa: D102 ret["snapshotData"] = self.snapshot.save(clean) ret["snapshotFingerprint"] = self._snapshot_fingerprint ret["thumbnailGeneratedTime"] = NodeTimestamps.dt_to_str( - self._thumbnail_generated_time + self._thumbnail_generated_time, ) ret["inkHash"] = self._ink_hash ret["snapshotProtoFprint"] = self._snapshot_proto_fprint