From 75ea6b186393cd0bb2456399e1e3240d1ecb2d17 Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 26 Dec 2025 10:29:28 -0500 Subject: [PATCH 1/8] Add guard for malformed collaborator entries --- src/gkeepapi/node.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 8a3f710..0eb4b8f 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -930,9 +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( + email = collaborator.get("email") + if email is None: + continue + self._collaborators[email] = ShareRequestValue( collaborator["type"] ) From 1409da884158eb4c73404228d137ded540288b3a Mon Sep 17 00:00:00 2001 From: Kai Date: Fri, 26 Dec 2025 10:33:03 -0500 Subject: [PATCH 2/8] Fix pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b6f0490..83f3eff 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,7 +115,7 @@ 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", From f741bd177e36be7292eceeb14ddf5ba4dfc2a2fb Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:18:54 -0500 Subject: [PATCH 3/8] Bump version --- CHANGELOG.md | 8 ++++++-- pyproject.toml | 2 +- src/gkeepapi/__init__.py | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) 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 83f3eff..68c5922 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" }, ] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 1b3c388..8e351d2 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 From 11e9e48b7a40dffd3753b836f00920491eda7edd Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:19:57 -0500 Subject: [PATCH 4/8] Lint --- src/gkeepapi/__init__.py | 10 +++++++--- src/gkeepapi/node.py | 8 +++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 8e351d2..10dd879 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -707,7 +707,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 +725,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( @@ -1025,7 +1029,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: diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 0eb4b8f..b8f7587 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -932,15 +932,13 @@ def load(self, collaborators_raw: list, requests_raw: list) -> None: # noqa: D1 for collaborator in collaborators_raw: email = collaborator.get("email") if email is None: - continue + continue self._collaborators[email] = RoleValue(collaborator["role"]) for collaborator in requests_raw: email = collaborator.get("email") if email is None: - continue - self._collaborators[email] = ShareRequestValue( - collaborator["type"] - ) + continue + self._collaborators[email] = ShareRequestValue(collaborator["type"]) def save(self, clean: bool = True) -> tuple[list, list]: """Save the collaborators container""" From 8cc6a93e80282251e5a45d946b5773c7c3880102 Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:24:09 -0500 Subject: [PATCH 5/8] Update workflow --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index da3dfb6..79143fc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,7 +17,7 @@ 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 From 56b5b80ca5dd37abba2ab17ef182cfb8dd4e9aa4 Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:24:09 -0500 Subject: [PATCH 6/8] Update workflow --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 79143fc..4457516 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: python -m pip install . --group dev - name: Lint with ruff run: | - ruff src + ruff lint src - name: Check format with black run: | black --check src From 79e9bb6d41a6e317d17ec2c89359cba8b9f5b532 Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:30:17 -0500 Subject: [PATCH 7/8] Lint --- .github/workflows/lint.yml | 4 ++-- pyproject.toml | 3 +-- src/gkeepapi/__init__.py | 40 +++++++++++++++++++------------------- src/gkeepapi/node.py | 22 ++++++++++----------- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4457516..447c7da 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,10 +20,10 @@ jobs: python -m pip install . --group dev - name: Lint with ruff run: | - ruff lint 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/pyproject.toml b/pyproject.toml index 68c5922..544a512 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,8 +97,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 @@ -108,6 +106,7 @@ ignore = [ "D105", # undocumented-magic-method -- no thanks "ANN101", # missing-type-self -- unnecessary "ANN102", # missing-type-cls -- unnecessary + "UP031", # use-format-specifiers -- no thanks ] [tool.ruff.pydocstyle] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 10dd879..48102e0 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -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,7 @@ 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 +468,13 @@ 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 +523,9 @@ def update_internal( "EXTENSIONS", "LOCATION", "TITLE", - ] + ], }, - } + }, ) return self.send(url=self._base_url + "update", method="POST", json=params) @@ -550,12 +550,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 +583,7 @@ def list(self, master: bool = True) -> Any: # noqa: ANN401 }, "includeArchived": True, "includeDeleted": False, - } + }, ) else: current_time = time.time() @@ -602,7 +602,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) @@ -708,7 +708,7 @@ def login( LoginException: If there was a problem logging in. """ logger.warning( - "'Keep.login' is deprecated. Please use 'Keep.authenticate' instead" + "'Keep.login' is deprecated. Please use 'Keep.authenticate' instead", ) auth = APIAuth(self.OAUTH_SCOPES) if device_id is None: @@ -726,7 +726,7 @@ def resume( device_id: str | None = None, ) -> None: logger.warning( - "'Keep.resume' has been renamed to 'Keep.authenticate'. Please update your code" + "'Keep.resume' has been renamed to 'Keep.authenticate'. Please update your code", ) self.authenticate(email, master_token, state, sync, device_id) @@ -903,7 +903,7 @@ 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. @@ -970,7 +970,7 @@ 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. @@ -1201,7 +1201,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 b8f7587..bfa7e58 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: @@ -950,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) @@ -1079,7 +1079,7 @@ def _generateId(cls, tz: float) -> str: [ random.choice("abcdefghijklmnopqrstuvwxyz0123456789") # noqa: S311 for _ in range(12) - ] + ], ), int(tz * 1000), ) @@ -1165,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), @@ -1857,7 +1857,7 @@ 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. @@ -2135,14 +2135,14 @@ 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 @@ -2151,7 +2151,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 From f3e3ab7c03d35765047dea094d9944058c0ba684 Mon Sep 17 00:00:00 2001 From: K Date: Mon, 5 Jan 2026 10:33:03 -0500 Subject: [PATCH 8/8] Lint --- pyproject.toml | 7 ++++--- src/gkeepapi/__init__.py | 18 ++++++++++++++---- src/gkeepapi/node.py | 10 +++++++--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 544a512..c22c18a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ dependencies = [ [tool.ruff] target-version = "py310" + +[tool.ruff.lint] select = [ # https://beta.ruff.rs/docs/rules/ "F", # pyflakes @@ -104,12 +106,11 @@ 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] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 48102e0..b2185c1 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -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. @@ -474,7 +477,10 @@ def create( 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. @@ -903,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. @@ -970,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. diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index bfa7e58..1b9cc53 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -1857,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. @@ -2135,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"), ) 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