From 1bfa6be87e7dae931a8626986cf6b38e965c4b9d Mon Sep 17 00:00:00 2001 From: K Date: Tue, 2 Jun 2020 21:18:13 -0400 Subject: [PATCH 01/56] Remove Py2 support --- .travis.yml | 2 +- Makefile | 2 +- gkeepapi/__init__.py | 5 ++--- gkeepapi/node.py | 13 ++++++------- requirements.txt | 2 -- setup.py | 2 -- test/test_client.py | 7 +------ test/test_nodes.py | 6 ------ 8 files changed, 11 insertions(+), 28 deletions(-) diff --git a/.travis.yml b/.travis.yml index 591d363..2434887 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: python python: -- "2.7" - "3.5" - "3.6" +- "3.7" install: - pip install -r requirements.txt - pip install coverage diff --git a/Makefile b/Makefile index 8ee9d2c..41d4d4c 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ coverage: coverage html build: gkeepapi/*.py - python3 setup.py bdist_wheel --universal + python3 setup.py bdist_wheel clean: rm -f dist/*.whl diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 6f36923..55a61bb 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -13,7 +13,6 @@ from uuid import getnode as get_mac -import six import gpsoauth import requests @@ -825,7 +824,7 @@ def find(self, query=None, func=None, labels=None, colors=None, pinned=None, arc return (node for node in self.all() if # Process the query. (query is None or ( - (isinstance(query, six.string_types) and (query in node.title or query in node.text)) or + (isinstance(query, str) and (query in node.title or query in node.text)) or (isinstance(query, Pattern) and ( query.search(node.title) or query.search(node.text) )) @@ -918,7 +917,7 @@ def findLabel(self, query, create=False): Returns: Union[gkeepapi.node.Label, None]: The label. """ - is_str = isinstance(query, six.string_types) + is_str = isinstance(query, str) name = None if is_str: name = query diff --git a/gkeepapi/node.py b/gkeepapi/node.py index bd762e8..5df3532 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -12,10 +12,9 @@ import time import random import enum -import six +import itertools from operator import attrgetter -from future.utils import raise_from from . import exception DEBUG = False @@ -189,7 +188,7 @@ def _find_discrepancies(self, raw): # pragma: no cover # Python strftime's 'z' format specifier includes microseconds, but the response from GKeep # only has milliseconds. This causes a string mismatch, so we construct datetime objects # to properly compare - if isinstance(val_a, six.string_types) and isinstance(val_b, six.string_types): + if isinstance(val_a, str) and isinstance(val_b, str): try: tval_a = NodeTimestamps.str_to_dt(val_a) tval_b = NodeTimestamps.str_to_dt(val_b) @@ -213,7 +212,7 @@ def load(self, raw): try: self._load(raw) except (KeyError, ValueError) as e: - raise_from(exception.ParseException('Parse error in %s' % (type(self)), raw), e) + raise exception.ParseException('Parse error in %s' % (type(self)), raw) from e def _load(self, raw): """Unserialize from raw representation. (Implementation logic) @@ -1382,7 +1381,7 @@ def add(self, text, checked=False, sort=None): @property def text(self): - return '\n'.join((six.text_type(node) for node in self.items)) + return '\n'.join((str(node) for node in self.items)) @classmethod def sorted_items(cls, items): @@ -1396,7 +1395,7 @@ def sorted_items(cls, items): class t(tuple): """Tuple with element-based sorting""" def __cmp__(self, other): - for a, b in six.moves.zip_longest(self, other): + for a, b in itertools.zip_longest(self, other): if a != b: if a is None: return 1 @@ -1449,7 +1448,7 @@ def sort_items(self, key=attrgetter('text'), reverse=False): sort_value -= self.SORT_DELTA def __str__(self): - return '\n'.join(([self.title] + [six.text_type(node) for node in self.items])) + return '\n'.join(([self.title] + [str(node) for node in self.items])) @property def items(self): diff --git a/requirements.txt b/requirements.txt index a2e236e..719e679 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,4 @@ gpsoauth>=0.4.1 -six>=1.10.0 -future>=0.16.0 enum34>=1.1.6; python_version < '3.0' mock>=3.0.5; python_version < '3.3' requests==2.23.0; platform_system == 'Windows' diff --git a/setup.py b/setup.py index 0aeee75..7ca1e1a 100644 --- a/setup.py +++ b/setup.py @@ -53,8 +53,6 @@ # https://packaging.python.org/en/latest/requirements.html install_requires=[ "gpsoauth >= 0.4.1", - "six >= 1.11.0", - "future >= 0.16.0", "enum34 >= 1.1.6; python_version < '3.0'", "requests == 2.23.0; platform_system == 'Windows'", ], diff --git a/test/test_client.py b/test/test_client.py index a10fb45..ffbe08d 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -3,12 +3,7 @@ import logging import gpsoauth import json -import six - -if six.PY2: - import mock -else: - from unittest import mock +from unittest import mock from gkeepapi import Keep, node diff --git a/test/test_nodes.py b/test/test_nodes.py index 3c3334c..f0edbd7 100644 --- a/test/test_nodes.py +++ b/test/test_nodes.py @@ -1,10 +1,4 @@ # -*- coding: utf-8 -*- -import six -if six.PY2: - import sys - reload(sys) - sys.setdefaultencoding('utf-8') - import unittest import logging From 10e87e7805d6a61389f0133b76b6a371498758fb Mon Sep 17 00:00:00 2001 From: K Date: Tue, 8 Feb 2022 22:43:20 -0500 Subject: [PATCH 02/56] Remove calls to raise_frome --- gkeepapi/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 5df3532..57abde3 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -1817,7 +1817,7 @@ def from_json(cls, raw): except (KeyError, ValueError) as e: logger.warning('Unknown blob type: %s', _type) if DEBUG: # pragma: no cover - raise_from(exception.ParseException('Parse error for %s' % (_type), raw), e) + raise exception.ParseException('Parse error for %s' % (_type), raw) from e return None blob = bcls() blob.load(raw) @@ -1926,7 +1926,7 @@ def from_json(raw): except (KeyError, ValueError) as e: logger.warning('Unknown node type: %s', _type) if DEBUG: # pragma: no cover - raise_from(exception.ParseException('Parse error for %s' % (_type), raw), e) + raise exception.ParseException('Parse error for %s' % (_type), raw) from e return None node = ncls() node.load(raw) From 369ae0e5c0e0033c2154441e405b631074ca4fc0 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sun, 27 Feb 2022 10:37:04 +0100 Subject: [PATCH 03/56] Remove redundant u string prefix Unicode strings are the default behaviour in Python 3 --- docs/conf.py | 17 +++++++---------- gkeepapi/node.py | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b9a5df7..9ff71ca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ master_doc = 'index' # General information about the project. -project = u'gkeepapi' -copyright = u'2017, Kai' -author = u'Kai' +project = 'gkeepapi' +copyright = '2017, Kai' +author = 'Kai' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -146,8 +146,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'gkeepapi.tex', u'gkeepapi Documentation', - u'Kai', 'manual'), + (master_doc, 'gkeepapi.tex', 'gkeepapi Documentation', + 'Kai', 'manual'), ] @@ -156,7 +156,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'gkeepapi', u'gkeepapi Documentation', + (master_doc, 'gkeepapi', 'gkeepapi Documentation', [author], 1) ] @@ -167,10 +167,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'gkeepapi', u'gkeepapi Documentation', + (master_doc, 'gkeepapi', 'gkeepapi Documentation', author, 'gkeepapi', 'One line description of project.', 'Miscellaneous'), ] - - - diff --git a/gkeepapi/node.py b/gkeepapi/node.py index f4775c1..802d4ad 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -1582,9 +1582,9 @@ def checked(self, value): self.touch(True) def __str__(self): - return u'%s%s %s' % ( + return '%s%s %s' % ( ' ' if self.indented else '', - u'☑' if self.checked else u'☐', + '☑' if self.checked else '☐', self.text ) From f4117fe690792f4a0ee7c308ee85bf860205acb7 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sun, 27 Feb 2022 11:00:03 +0100 Subject: [PATCH 04/56] Replace some str.format with fstrings --- gkeepapi/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 802d4ad..25ed4d7 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -212,7 +212,7 @@ def load(self, raw): try: self._load(raw) except (KeyError, ValueError) as e: - raise exception.ParseException('Parse error in %s' % (type(self)), raw) from e + raise exception.ParseException(f'Parse error in {type(self)}', raw) from e def _load(self, raw): """Unserialize from raw representation. (Implementation logic) @@ -1814,7 +1814,7 @@ def from_json(cls, raw): except (KeyError, ValueError) as e: logger.warning('Unknown blob type: %s', _type) if DEBUG: # pragma: no cover - raise exception.ParseException('Parse error for %s' % (_type), raw) from e + raise exception.ParseException(f'Parse error for {_type}', raw) from e return None blob = bcls() blob.load(raw) @@ -1923,7 +1923,7 @@ def from_json(raw): except (KeyError, ValueError) as e: logger.warning('Unknown node type: %s', _type) if DEBUG: # pragma: no cover - raise exception.ParseException('Parse error for %s' % (_type), raw) from e + raise exception.ParseException(f'Parse error for {_type}', raw) from e return None node = ncls() node.load(raw) From 6b627b86d510d8ed06032d1f4a845e3dfc849833 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sun, 27 Feb 2022 00:47:35 +0100 Subject: [PATCH 05/56] Minor generic cleanup --- examples/resume.py | 4 ---- gkeepapi/__init__.py | 11 +++++------ gkeepapi/node.py | 3 --- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/examples/resume.py b/examples/resume.py index c7f2a1f..4cd162e 100755 --- a/examples/resume.py +++ b/examples/resume.py @@ -1,12 +1,8 @@ #!/usr/bin/env python3 import sys -import os -import argparse -import yaml import keyring import getpass import logging -import time import gkeepapi USERNAME = "user@gmail.com" diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 55a61bb..c1f2ea4 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -9,7 +9,6 @@ import re import time import random -import json from uuid import getnode as get_mac @@ -110,7 +109,7 @@ def getEmail(self): return self._email def setEmail(self, email): - """Gets the account email. + """Sets the account email. Args: email (str): The account email. @@ -185,7 +184,7 @@ def __init__(self, base_url, auth=None): def getAuth(self): """Get authentication details for this API. - Args: + Return: auth (APIAuth): The auth object """ return self._auth @@ -577,7 +576,7 @@ def list(self, master=True): params.update({ 'recurrenceOptions': { - 'collapseMode':'INSTANCES_ONLY', + 'collapseMode': 'INSTANCES_ONLY', 'recurrencesOnly': True, }, 'includeArchived': False, @@ -676,7 +675,7 @@ def _clear(self): root_node = _node.Root() self._nodes[_node.Root.ID] = root_node - def login(self, username, password, state=None, sync=True, device_id=None): + def login(self, email, password, state=None, sync=True, device_id=None): """Authenticate to Google with the provided credentials & sync. Args: @@ -691,7 +690,7 @@ def login(self, username, password, state=None, sync=True, device_id=None): if device_id is None: device_id = get_mac() - ret = auth.login(username, password, device_id) + ret = auth.login(email, password, device_id) if ret: self.load(auth, state, sync) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 57abde3..f4775c1 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -590,9 +590,6 @@ def remove(self, annotation): Args: annotation (gkeepapi.node.Annotation): An Annotation object. - - Returns: - gkeepapi.node.Annotation: The Annotation. """ if annotation.id in self._annotations: del self._annotations[annotation.id] From 910b7c6f0dc2e93678db044869f66637341d2bb5 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sun, 27 Feb 2022 10:37:04 +0100 Subject: [PATCH 06/56] Remove redundant u string prefix Unicode strings are the default behaviour in Python 3 --- docs/conf.py | 17 +++++++---------- gkeepapi/node.py | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index b9a5df7..9ff71ca 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -50,9 +50,9 @@ master_doc = 'index' # General information about the project. -project = u'gkeepapi' -copyright = u'2017, Kai' -author = u'Kai' +project = 'gkeepapi' +copyright = '2017, Kai' +author = 'Kai' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -146,8 +146,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'gkeepapi.tex', u'gkeepapi Documentation', - u'Kai', 'manual'), + (master_doc, 'gkeepapi.tex', 'gkeepapi Documentation', + 'Kai', 'manual'), ] @@ -156,7 +156,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - (master_doc, 'gkeepapi', u'gkeepapi Documentation', + (master_doc, 'gkeepapi', 'gkeepapi Documentation', [author], 1) ] @@ -167,10 +167,7 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'gkeepapi', u'gkeepapi Documentation', + (master_doc, 'gkeepapi', 'gkeepapi Documentation', author, 'gkeepapi', 'One line description of project.', 'Miscellaneous'), ] - - - diff --git a/gkeepapi/node.py b/gkeepapi/node.py index f4775c1..802d4ad 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -1582,9 +1582,9 @@ def checked(self, value): self.touch(True) def __str__(self): - return u'%s%s %s' % ( + return '%s%s %s' % ( ' ' if self.indented else '', - u'☑' if self.checked else u'☐', + '☑' if self.checked else '☐', self.text ) From d158fa5e66689ce054b9c616a8f15e5d7908fe41 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sun, 27 Feb 2022 11:00:03 +0100 Subject: [PATCH 07/56] Replace some str.format with fstrings --- gkeepapi/node.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 802d4ad..25ed4d7 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -212,7 +212,7 @@ def load(self, raw): try: self._load(raw) except (KeyError, ValueError) as e: - raise exception.ParseException('Parse error in %s' % (type(self)), raw) from e + raise exception.ParseException(f'Parse error in {type(self)}', raw) from e def _load(self, raw): """Unserialize from raw representation. (Implementation logic) @@ -1814,7 +1814,7 @@ def from_json(cls, raw): except (KeyError, ValueError) as e: logger.warning('Unknown blob type: %s', _type) if DEBUG: # pragma: no cover - raise exception.ParseException('Parse error for %s' % (_type), raw) from e + raise exception.ParseException(f'Parse error for {_type}', raw) from e return None blob = bcls() blob.load(raw) @@ -1923,7 +1923,7 @@ def from_json(raw): except (KeyError, ValueError) as e: logger.warning('Unknown node type: %s', _type) if DEBUG: # pragma: no cover - raise exception.ParseException('Parse error for %s' % (_type), raw) from e + raise exception.ParseException(f'Parse error for {_type}', raw) from e return None node = ncls() node.load(raw) From 7f9d0f47a341def76a161e27d94bbfa6d1344668 Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sat, 27 Aug 2022 19:06:24 +0200 Subject: [PATCH 08/56] Fix Keep::dump docs --- gkeepapi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index c1f2ea4..e6b82f6 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -745,8 +745,8 @@ def load(self, auth, state=None, sync=True): def dump(self): """Serialize note data. - Args: - state (dict): Serialized state to load. + Returns: + dict: Serialized state. """ # Find all nodes manually, as the Keep object isn't aware of new # ListItems until they've been synced to the server. From ebd0f5c8559f631fd19615641971d4f1ce58c4de Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Sat, 27 Aug 2022 20:33:51 +0200 Subject: [PATCH 09/56] Add initial type hints Only user-facing methods have typing info for now --- gkeepapi/__init__.py | 55 ++++++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index e6b82f6..017f4e4 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -9,6 +9,7 @@ import re import time import random +from typing import List, Optional, Tuple, Dict from uuid import getnode as get_mac @@ -653,7 +654,7 @@ class Keep(object): """ OAUTH_SCOPES = 'oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders' - def __init__(self): + def __init__(self) -> None: self._keep_api = KeepAPI() self._reminders_api = RemindersAPI() self._media_api = MediaAPI() @@ -675,7 +676,7 @@ def _clear(self): root_node = _node.Root() self._nodes[_node.Root.ID] = root_node - def login(self, email, password, state=None, sync=True, device_id=None): + def login(self, email: str, password: str, state: Optional[Dict] = None, sync=True, device_id: Optional[int] = None): """Authenticate to Google with the provided credentials & sync. Args: @@ -690,13 +691,12 @@ def login(self, email, password, state=None, sync=True, device_id=None): if device_id is None: device_id = get_mac() - ret = auth.login(email, password, device_id) - if ret: - self.load(auth, state, sync) + auth.login(email, password, device_id) + self.load(auth, state, sync) - return ret + return True - def resume(self, email, master_token, state=None, sync=True, device_id=None): + def resume(self, email: str, master_token: str, state: Optional[Dict] = None, sync=True, device_id: Optional[int] = None): """Authenticate to Google with the provided master token & sync. Args: @@ -711,13 +711,12 @@ def resume(self, email, master_token, state=None, sync=True, device_id=None): if device_id is None: device_id = get_mac() - ret = auth.load(email, master_token, device_id) - if ret: - self.load(auth, state, sync) + auth.load(email, master_token, device_id) + self.load(auth, state, sync) - return ret + return True - def getMasterToken(self): + def getMasterToken(self) -> str: """Get master token for resuming. Returns: @@ -725,7 +724,7 @@ def getMasterToken(self): """ return self._keep_api.getAuth().getMasterToken() - def load(self, auth, state=None, sync=True): + def load(self, auth: APIAuth, state: Optional[Dict] = None, sync=True) -> None: """Authenticate to Google with a prepared authentication object & sync. Args: auth (APIAuth): Authentication object. @@ -742,7 +741,7 @@ def load(self, auth, state=None, sync=True): if sync: self.sync(True) - def dump(self): + def dump(self) -> Dict: """Serialize note data. Returns: @@ -761,7 +760,7 @@ def dump(self): 'nodes': [node.save(False) for node in nodes] } - def restore(self, state): + def restore(self, state: Dict) -> None: """Unserialize saved note data. Args: @@ -772,7 +771,7 @@ def restore(self, state): self._parseNodes(state['nodes']) self._keep_version = state['keep_version'] - def get(self, node_id): + def get(self, node_id: str) -> _node.TopLevelNode: """Get a note with the given ID. Args: @@ -785,7 +784,7 @@ def get(self, node_id): self._nodes[_node.Root.ID].get(node_id) or \ self._nodes[_node.Root.ID].get(self._sid_map.get(node_id)) - def add(self, node): + def add(self, node: _node.Node) -> None: """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. @@ -794,7 +793,7 @@ def add(self, node): node (gkeepapi.node.Node): The node to sync. Raises: - Invalid: If the parent node is not found. + InvalidException: If the parent node is not found. """ if node.parent_id != _node.Root.ID: raise exception.InvalidException('Not a top level node') @@ -845,7 +844,7 @@ def find(self, query=None, func=None, labels=None, colors=None, pinned=None, arc (trashed is None or node.trashed == trashed) ) - def createNote(self, title=None, text=None): + def createNote(self, title: Optional[str] = None, text: Optional[str] = None) -> _node.Node: """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: @@ -863,7 +862,7 @@ def createNote(self, title=None, text=None): self.add(node) return node - def createList(self, title=None, items=None): + def createList(self, title: Optional[str] = None, items: Optional[List[Tuple[str, bool]]] = None) -> _node.List: """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: @@ -887,7 +886,7 @@ def createList(self, title=None, items=None): self.add(node) return node - def createLabel(self, name): + def createLabel(self, name: str) -> _node.Label: """Create a new label. Args: @@ -906,7 +905,7 @@ def createLabel(self, name): self._labels[node.id] = node # pylint: disable=protected-access return node - def findLabel(self, query, create=False): + def findLabel(self, query, create=False) -> Optional[_node.Label]: """Find a label with the given name. Args: @@ -930,7 +929,7 @@ def findLabel(self, query, create=False): return self.createLabel(name) if create and is_str else None - def getLabel(self, label_id): + def getLabel(self, label_id: str) -> Optional[_node.Label]: """Get an existing label. Args: @@ -941,7 +940,7 @@ def getLabel(self, label_id): """ return self._labels.get(label_id) - def deleteLabel(self, label_id): + def deleteLabel(self, label_id: str) -> None: """Deletes a label. Args: @@ -955,7 +954,7 @@ def deleteLabel(self, label_id): for node in self.all(): node.labels.remove(label) - def labels(self): + def labels(self) -> List[_node.Label]: """Get all labels. Returns: @@ -963,7 +962,7 @@ def labels(self): """ return self._labels.values() - def getMediaLink(self, blob): + def getMediaLink(self, blob: _node.Blob) -> str: """Get the canonical link to media. Args: @@ -974,7 +973,7 @@ def getMediaLink(self, blob): """ return self._media_api.get(blob) - def all(self): + def all(self) -> List[_node.TopLevelNode]: """Get all Notes. Returns: @@ -982,7 +981,7 @@ def all(self): """ return self._nodes[_node.Root.ID].children - def sync(self, resync=False): + def sync(self, resync=False) -> None: """Sync the local Keep tree with the server. If resyncing, local changes will be destroyed. Otherwise, local changes to notes, labels and reminders will be detected and synced up. Args: From 49e72a29bb2be3bf69c7aa615b54755fc5b4033a Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Tue, 27 Sep 2022 17:17:04 +0200 Subject: [PATCH 10/56] Remove Python 2 regular expression support, add typing --- gkeepapi/__init__.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 017f4e4..3b77943 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -9,7 +9,7 @@ import re import time import random -from typing import List, Optional, Tuple, Dict +from typing import Callable, Iterator, List, Optional, Tuple, Dict, Union from uuid import getnode as get_mac @@ -21,11 +21,6 @@ logger = logging.getLogger(__name__) -try: - Pattern = re._pattern_type # pylint: disable=protected-access -except AttributeError: - Pattern = re.Pattern # pylint: disable=no-member - class APIAuth(object): """Authentication token manager""" def __init__(self, scopes): @@ -801,7 +796,16 @@ def add(self, node: _node.Node) -> None: self._nodes[node.id] = node self._nodes[node.parent_id].append(node, False) - def find(self, query=None, func=None, labels=None, colors=None, pinned=None, archived=None, trashed=False): # pylint: disable=too-many-arguments + def find( + self, + query: Union[re.Pattern, str, None] = None, + func: Optional[Callable] = None, + labels: Optional[List[str]] = None, + colors: Optional[List[str]] = None, + pinned: Optional[bool] = None, + archived: Optional[bool] = None, + trashed: Optional[bool] = False + ) -> Iterator[_node.TopLevelNode]: # pylint: disable=too-many-arguments """Find Notes based on the specified criteria. Args: @@ -823,7 +827,7 @@ def find(self, query=None, func=None, labels=None, colors=None, pinned=None, arc # Process the query. (query is None or ( (isinstance(query, str) and (query in node.title or query in node.text)) or - (isinstance(query, Pattern) and ( + (isinstance(query, re.Pattern) and ( query.search(node.title) or query.search(node.text) )) )) and @@ -905,7 +909,7 @@ def createLabel(self, name: str) -> _node.Label: self._labels[node.id] = node # pylint: disable=protected-access return node - def findLabel(self, query, create=False) -> Optional[_node.Label]: + def findLabel(self, query: Union[re.Pattern, str], create=False) -> Optional[_node.Label]: """Find a label with the given name. Args: @@ -924,7 +928,7 @@ def findLabel(self, query, create=False) -> Optional[_node.Label]: for label in self._labels.values(): # Match the label against query, which may be a str or Pattern. if (is_str and query == label.name.lower()) or \ - (isinstance(query, Pattern) and query.search(label.name)): + (isinstance(query, re.Pattern) and query.search(label.name)): return label return self.createLabel(name) if create and is_str else None From 0ab21a404a0adc693f78f100ba7117fa18b1b25a Mon Sep 17 00:00:00 2001 From: PrOF-kk Date: Tue, 27 Sep 2022 17:38:37 +0200 Subject: [PATCH 11/56] Remove unnecessary typing info from docstrings Sphinx supports PEP 484 type annotations now --- gkeepapi/__init__.py | 82 ++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 3b77943..b120dcd 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -675,9 +675,9 @@ def login(self, email: str, password: str, state: Optional[Dict] = None, sync=Tr """Authenticate to Google with the provided credentials & sync. Args: - email (str): The account to use. - password (str): The account password. - state (dict): Serialized state to load. + email: The account to use. + password: The account password. + state: Serialized state to load. Raises: LoginException: If there was a problem logging in. @@ -695,9 +695,9 @@ def resume(self, email: str, master_token: str, state: Optional[Dict] = None, sy """Authenticate to Google with the provided master token & sync. Args: - email (str): The account to use. - master_token (str): The master token. - state (dict): Serialized state to load. + email: The account to use. + master_token: The master token. + state: Serialized state to load. Raises: LoginException: If there was a problem logging in. @@ -715,15 +715,15 @@ def getMasterToken(self) -> str: """Get master token for resuming. Returns: - str: The master token. + The master token. """ return self._keep_api.getAuth().getMasterToken() def load(self, auth: APIAuth, state: Optional[Dict] = None, sync=True) -> None: """Authenticate to Google with a prepared authentication object & sync. Args: - auth (APIAuth): Authentication object. - state (dict): Serialized state to load. + auth: Authentication object. + state: Serialized state to load. Raises: LoginException: If there was a problem logging in. @@ -740,7 +740,7 @@ def dump(self) -> Dict: """Serialize note data. Returns: - dict: Serialized state. + Serialized state. """ # Find all nodes manually, as the Keep object isn't aware of new # ListItems until they've been synced to the server. @@ -759,7 +759,7 @@ def restore(self, state: Dict) -> None: """Unserialize saved note data. Args: - state (dict): Serialized state to load. + state: Serialized state to load. """ self._clear() self._parseUserInfo({'labels': state['labels']}) @@ -770,10 +770,10 @@ def get(self, node_id: str) -> _node.TopLevelNode: """Get a note with the given ID. Args: - node_id (str): The note ID. + node_id: The note ID. Returns: - gkeepapi.node.TopLevelNode: The Note or None if not found. + The Note or None if not found. """ return \ self._nodes[_node.Root.ID].get(node_id) or \ @@ -785,7 +785,7 @@ def add(self, node: _node.Node) -> None: LoginException: If :py:meth:`login` has not been called. Args: - node (gkeepapi.node.Node): The node to sync. + node: The node to sync. Raises: InvalidException: If the parent node is not found. @@ -809,16 +809,16 @@ def find( """Find Notes based on the specified criteria. Args: - query (Union[_sre.SRE_Pattern, str, None]): A str or regular expression to match against the title and text. - func (Union[callable, None]): A filter function. - labels (Union[List[str], None]): A list of label ids or objects to match. An empty list matches notes with no labels. - colors (Union[List[str], None]): A list of colors to match. - pinned (Union[bool, None]): Whether to match pinned notes. - archived (Union[bool, None]): Whether to match archived notes. - trashed (Union[bool, None]): Whether to match trashed notes. + query: A str or regular expression to match against the title and text. + func: A filter function. + labels: A list of label ids or objects to match. An empty list matches notes with no labels. + colors: A list of colors to match. + pinned: Whether to match pinned notes. + archived: Whether to match archived notes. + trashed: Whether to match trashed notes. Return: - Generator[gkeepapi.node.TopLevelNode]: Results. + Search results. """ if labels is not None: labels = [i.id if isinstance(i, _node.Label) else i for i in labels] @@ -852,11 +852,11 @@ def createNote(self, title: Optional[str] = None, text: Optional[str] = None) -> """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: - title (str): The title of the note. - text (str): The text of the note. + title: The title of the note. + text: The text of the note. Returns: - gkeepapi.node.List: The new note. + The new note. """ node = _node.Note() if title is not None: @@ -870,11 +870,11 @@ def createList(self, title: Optional[str] = None, items: Optional[List[Tuple[str """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: - title (str): The title of the list. - items (List[(str, bool)]): A list of tuples. Each tuple represents the text and checked status of the listitem. + title: The title of the list. + items: A list of tuples. Each tuple represents the text and checked status of the listitem. Returns: - gkeepapi.node.List: The new list. + The new list. """ if items is None: items = [] @@ -894,10 +894,10 @@ def createLabel(self, name: str) -> _node.Label: """Create a new label. Args: - name (str): Label name. + name: Label name. Returns: - gkeepapi.node.Label: The new label. + The new label. Raises: LabelException: If the label exists. @@ -913,11 +913,11 @@ def findLabel(self, query: Union[re.Pattern, str], create=False) -> Optional[_no """Find a label with the given name. Args: - name (Union[_sre.SRE_Pattern, str]): A str or regular expression to match against the name. - create (bool): Whether to create the label if it doesn't exist (only if name is a str). + name: A str or regular expression to match against the name. + create: Whether to create the label if it doesn't exist (only if name is a str). Returns: - Union[gkeepapi.node.Label, None]: The label. + The label. """ is_str = isinstance(query, str) name = None @@ -937,10 +937,10 @@ def getLabel(self, label_id: str) -> Optional[_node.Label]: """Get an existing label. Args: - label_id (str): Label id. + label_id: Label id. Returns: - Union[gkeepapi.node.Label, None]: The label. + The label. """ return self._labels.get(label_id) @@ -948,7 +948,7 @@ def deleteLabel(self, label_id: str) -> None: """Deletes a label. Args: - label_id (str): Label id. + label_id: Label id. """ if label_id not in self._labels: return @@ -962,7 +962,7 @@ def labels(self) -> List[_node.Label]: """Get all labels. Returns: - List[gkeepapi.node.Label]: Labels + Labels """ return self._labels.values() @@ -970,10 +970,10 @@ def getMediaLink(self, blob: _node.Blob) -> str: """Get the canonical link to media. Args: - blob (gkeepapi.node.Blob): The media resource. + blob: The media resource. Returns: - str: A link to the media. + A link to the media. """ return self._media_api.get(blob) @@ -981,7 +981,7 @@ def all(self) -> List[_node.TopLevelNode]: """Get all Notes. Returns: - List[gkeepapi.node.TopLevelNode]: Notes + Notes """ return self._nodes[_node.Root.ID].children @@ -989,7 +989,7 @@ def sync(self, resync=False) -> None: """Sync the local Keep tree with the server. If resyncing, local changes will be destroyed. Otherwise, local changes to notes, labels and reminders will be detected and synced up. Args: - resync (bool): Whether to resync data. + resync: Whether to resync data. Raises: SyncException: If there is a consistency issue. From 7e6180d2a8ad81987a6341c9e64a28a6b291ae85 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 11:23:12 -0400 Subject: [PATCH 12/56] Add addn typehints --- gkeepapi/__init__.py | 129 ++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 62 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 94515b9..5accb67 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -8,6 +8,7 @@ import logging import re import time +import datetime import random from typing import Callable, Iterator, List, Optional, Tuple, Dict, Union @@ -30,13 +31,13 @@ def __init__(self, scopes): self._device_id = None self._scopes = scopes - def login(self, email, password, device_id): + def login(self, email:str, password:str, device_id: str) -> bool: """Authenticate to Google with the provided credentials. Args: - email (str): The account to use. - password (str): The account password. - device_id (str): An identifier for this client. + email: The account to use. + password: The account password. + device_id: An identifier for this client. Raises: LoginException: If there was a problem logging in. @@ -59,13 +60,13 @@ def login(self, email, password, device_id): self.refresh() return True - def load(self, email, master_token, device_id): + def load(self, email: str, master_token: str, device_id: str) -> bool: """Authenticate to Google with the provided master token. Args: - email (str): The account to use. - master_token (str): The master token. - device_id (str): An identifier for this client. + email: The account to use. + master_token: The master token. + device_id: An identifier for this client. Raises: LoginException: If there was a problem logging in. @@ -78,7 +79,7 @@ def load(self, email, master_token, device_id): self.refresh() return True - def getMasterToken(self): + def getMasterToken(self) -> str: """Gets the master token. Returns: @@ -86,61 +87,61 @@ def getMasterToken(self): """ return self._master_token - def setMasterToken(self, master_token): + def setMasterToken(self, master_token) -> None: """Sets the master token. This is useful if you'd like to authenticate with the API without providing your username & password. Do note that the master token has full access to your account. Args: - master_token (str): The account master token. + master_token: The account master token. """ self._master_token = master_token - def getEmail(self): + def getEmail(self) -> str: """Gets the account email. Returns: - str: The account email. + The account email. """ return self._email - def setEmail(self, email): + def setEmail(self, email) -> None: """Sets the account email. Args: - email (str): The account email. + email: The account email. """ self._email = email - def getDeviceId(self): + def getDeviceId(self)-> str: """Gets the device id. Returns: - str: The device id. + The device id. """ return self._device_id - def setDeviceId(self, device_id): + def setDeviceId(self, device_id)-> None: """Sets the device id. Args: - device_id (str): The device id. + device_id: The device id. """ self._device_id = device_id - def getAuthToken(self): + def getAuthToken(self)->Optional[str]: """Gets the auth token. Returns: - Union[str, None]: The auth token. + The auth token. """ return self._auth_token - def refresh(self): + def refresh(self) -> str: """Refresh the OAuth token. Returns: - string: The auth token. + The auth token. Raises: LoginException: If there was a problem refreshing the OAuth token. @@ -161,7 +162,7 @@ def refresh(self): self._auth_token = res['Auth'] return self._auth_token - def logout(self): + def logout(self) -> None: """Log out of the account.""" self._master_token = None self._auth_token = None @@ -171,29 +172,29 @@ def logout(self): class API(object): """Base API wrapper""" RETRY_CNT = 2 - def __init__(self, base_url, auth=None): + def __init__(self, base_url: str, auth=None): self._session = requests.Session() self._auth = auth self._base_url = base_url self._session.headers.update({'User-Agent': 'x-gkeepapi/%s (https://github.com/kiwiz/gkeepapi)' % __version__}) - def getAuth(self): + def getAuth(self) -> APIAuth: """Get authentication details for this API. Return: - auth (APIAuth): The auth object + auth: The auth object """ return self._auth - def setAuth(self, auth): + def setAuth(self, auth: APIAuth): """Set authentication details for this API. Args: - auth (APIAuth): The auth object + auth: The auth object """ self._auth = auth - def send(self, **req_kwargs): + def send(self, **req_kwargs) -> Dict: """Send an authenticated request to a Google API. Automatically retries if the access token has expired. @@ -201,7 +202,7 @@ def send(self, **req_kwargs): **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: - dict: The parsed JSON response. + The parsed JSON response. Raises: APIException: If the server returns an error. @@ -233,14 +234,14 @@ def send(self, **req_kwargs): return response - def _send(self, **req_kwargs): + def _send(self, **req_kwargs) -> requests.Response: """Send an authenticated request to a Google API. Args: **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: - requests.Response: The raw response. + The raw response. Raises: LoginException: If :py:meth:`login` has not been called. @@ -271,22 +272,22 @@ def __init__(self, auth=None): self._session_id = self._generateId(create_time) @classmethod - def _generateId(cls, tz): + def _generateId(cls, tz: int) -> str: return 's--%d--%d' % ( int(tz * 1000), random.randint(1000000000, 9999999999) ) - def changes(self, target_version=None, nodes=None, labels=None): + def changes(self, target_version:str=None, nodes:List[Dict]=None, labels:List[Dict]=None) -> Dict: """Sync up (and down) all changes. Args: - target_version (str): The local change version. - nodes (List[dict]): A list of nodes to sync up to the server. - labels (List[dict]): A list of labels to sync up to the server. + target_version: The local change version. + nodes: A list of nodes to sync up to the server. + labels: A list of labels to sync up to the server. Return: - dict: Description of all changes. + Description of all changes. Raises: APIException: If the server returns an error. @@ -363,14 +364,14 @@ class MediaAPI(API): def __init__(self, auth=None): super(MediaAPI, self).__init__(self.API_URL, auth) - def get(self, blob): + def get(self, blob: _node.Blob) -> str: """Get the canonical link to a media blob. Args: - blob (gkeepapi.node.Blob): The blob. + blob: The blob. Returns: - str: A link to the media. + A link to the media. """ url = self._base_url + blob.parent.server_id + '/' + blob.server_id if blob.blob.type == _node.BlobType.Drawing: @@ -405,13 +406,13 @@ def __init__(self, auth=None): }, } - def create(self, node_id, node_server_id, dtime): + def create(self, node_id: str, node_server_id :str, dtime: datetime.datetime): """Create a new reminder. Args: - node_id (str): The note ID. - node_server_id (str): The note server ID. - dtime (datetime.datetime): The due date of this reminder. + node_id: The note ID. + node_server_id: The note server ID. + dtime: The due date of this reminder. Return: ??? @@ -454,13 +455,13 @@ def create(self, node_id, node_server_id, dtime): json=params ) - def update(self, node_id, node_server_id, dtime): + def update(self, node_id: str, node_server_id: str, dtime: datetime.datetime): """Update an existing reminder. Args: - node_id (str): The note ID. - node_server_id (str): The note server ID. - dtime (datetime.datetime): The due date of this reminder. + node_id: The note ID. + node_server_id: The note server ID. + dtime: The due date of this reminder. Return: ??? @@ -507,11 +508,11 @@ def update(self, node_id, node_server_id, dtime): json=params ) - def delete(self, node_server_id): + def delete(self, node_server_id: str): """ Delete an existing reminder. Args: - node_server_id (str): The note server ID. + node_server_id: The note server ID. Return: ??? @@ -546,7 +547,7 @@ def list(self, master=True): """List current reminders. Args: - master (bool): ??? + master: ??? Return: ??? @@ -589,7 +590,7 @@ def list(self, master=True): json=params ) - def history(self, storage_version): + def history(self, storage_version: str): """Get reminder changes. Args: @@ -649,7 +650,7 @@ class Keep(object): """ OAUTH_SCOPES = 'oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders' - def __init__(self) -> None: + def __init__(self): self._keep_api = KeepAPI() self._reminders_api = RemindersAPI() self._media_api = MediaAPI() @@ -661,7 +662,7 @@ def __init__(self) -> None: self._clear() - def _clear(self): + def _clear(self) -> None: self._keep_version = None self._reminder_version = None self._labels = {} @@ -678,6 +679,8 @@ def login(self, email: str, password: str, state: Optional[Dict] = None, sync=Tr email: The account to use. password: The account password. state: Serialized state to load. + sync: Whether to sync data. + device_id: Device id. Raises: LoginException: If there was a problem logging in. @@ -691,13 +694,15 @@ def login(self, email: str, password: str, state: Optional[Dict] = None, sync=Tr return True - def resume(self, email: str, master_token: str, state: Optional[Dict] = None, sync=True, device_id: Optional[int] = None): + def resume(self, email: str, master_token: str, state: Optional[Dict] = None, sync=True, device_id: Optional[str] = None): """Authenticate to Google with the provided master token & sync. Args: email: The account to use. master_token: The master token. state: Serialized state to load. + sync: Whether to sync data. + device_id: Device id. Raises: LoginException: If there was a problem logging in. @@ -1056,10 +1061,10 @@ def _sync_notes(self, resync=False): if not changes['truncated']: break - def _parseTasks(self, raw): + def _parseTasks(self, raw) -> None: pass - def _parseNodes(self, raw): # pylint: disable=too-many-branches + def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1133,7 +1138,7 @@ def _parseNodes(self, raw): # pylint: disable=too-many-branches for label_id in node.labels._labels: # pylint: disable=protected-access node.labels._labels[label_id] = self._labels.get(label_id) # pylint: disable=protected-access - def _parseUserInfo(self, raw): + def _parseUserInfo(self, raw) -> None: labels = {} if 'labels' in raw: for label in raw['labels']: @@ -1156,7 +1161,7 @@ def _parseUserInfo(self, raw): self._labels = labels - def _findDirtyNodes(self): + def _findDirtyNodes(self) -> List[_node.Node]: # Find nodes that aren't in our internal nodes list and insert them. for node in list(self._nodes.values()): for child in node.children: @@ -1171,7 +1176,7 @@ def _findDirtyNodes(self): return nodes - def _clean(self): + def _clean(self) -> None: """Recursively check that all nodes are reachable.""" found_ids = {} nodes = [self._nodes[_node.Root.ID]] From 04ba6172a1750232ad43f4869a481c1e72a46492 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 11:23:40 -0400 Subject: [PATCH 13/56] Black --- gkeepapi/__init__.py | 595 +++++++++++++++++++++++-------------------- 1 file changed, 313 insertions(+), 282 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 5accb67..0fd24d7 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -3,7 +3,7 @@ .. moduleauthor:: Kai """ -__version__ = '0.13.6' +__version__ = "0.13.6" import logging import re @@ -22,8 +22,10 @@ logger = logging.getLogger(__name__) + class APIAuth(object): """Authentication token manager""" + def __init__(self, scopes): self._master_token = None self._auth_token = None @@ -31,7 +33,7 @@ def __init__(self, scopes): self._device_id = None self._scopes = scopes - def login(self, email:str, password:str, device_id: str) -> bool: + def login(self, email: str, password: str, device_id: str) -> bool: """Authenticate to Google with the provided credentials. Args: @@ -46,15 +48,11 @@ def login(self, email:str, password:str, device_id: str) -> bool: self._device_id = device_id # Obtain a master token. - res = gpsoauth.perform_master_login( - self._email, password, self._device_id - ) + res = gpsoauth.perform_master_login(self._email, password, self._device_id) # Bail if no token was returned. - if 'Token' not in res: - raise exception.LoginException( - res.get('Error'), res.get('ErrorDetail') - ) - self._master_token = res['Token'] + if "Token" not in res: + raise exception.LoginException(res.get("Error"), res.get("ErrorDetail")) + self._master_token = res["Token"] # Obtain an OAuth token. self.refresh() @@ -113,7 +111,7 @@ def setEmail(self, email) -> None: """ self._email = email - def getDeviceId(self)-> str: + def getDeviceId(self) -> str: """Gets the device id. Returns: @@ -121,7 +119,7 @@ def getDeviceId(self)-> str: """ return self._device_id - def setDeviceId(self, device_id)-> None: + def setDeviceId(self, device_id) -> None: """Sets the device id. Args: @@ -129,7 +127,7 @@ def setDeviceId(self, device_id)-> None: """ self._device_id = device_id - def getAuthToken(self)->Optional[str]: + def getAuthToken(self) -> Optional[str]: """Gets the auth token. Returns: @@ -149,17 +147,19 @@ def refresh(self) -> str: # Obtain an OAuth token with the necessary scopes by pretending to be # the keep android client. res = gpsoauth.perform_oauth( - self._email, self._master_token, self._device_id, + self._email, + self._master_token, + self._device_id, service=self._scopes, - app='com.google.android.keep', - client_sig='38918a453d07199354f8b19af05ec6562ced5788' + app="com.google.android.keep", + client_sig="38918a453d07199354f8b19af05ec6562ced5788", ) # Bail if no token was returned. - if 'Auth' not in res: - if 'Token' not in res: - raise exception.LoginException(res.get('Error')) + if "Auth" not in res: + if "Token" not in res: + raise exception.LoginException(res.get("Error")) - self._auth_token = res['Auth'] + self._auth_token = res["Auth"] return self._auth_token def logout(self) -> None: @@ -169,14 +169,22 @@ def logout(self) -> None: self._email = None self._device_id = None + class API(object): """Base API wrapper""" + RETRY_CNT = 2 + def __init__(self, base_url: str, auth=None): self._session = requests.Session() self._auth = auth self._base_url = base_url - self._session.headers.update({'User-Agent': 'x-gkeepapi/%s (https://github.com/kiwiz/gkeepapi)' % __version__}) + self._session.headers.update( + { + "User-Agent": "x-gkeepapi/%s (https://github.com/kiwiz/gkeepapi)" + % __version__ + } + ) def getAuth(self) -> APIAuth: """Get authentication details for this API. @@ -214,21 +222,21 @@ def send(self, **req_kwargs) -> Dict: while True: # Send off the request. If there was no error, we're good. response = self._send(**req_kwargs).json() - if 'error' not in response: + if "error" not in response: break # Otherwise, check if it was a non-401 response code. These aren't # handled, so bail. - error = response['error'] - if error['code'] != 401: - raise exception.APIException(error['code'], error) + error = response["error"] + if error["code"] != 401: + raise exception.APIException(error["code"], error) # If we've exceeded the retry limit, also bail. if i >= self.RETRY_CNT: - raise exception.APIException(error['code'], error) + raise exception.APIException(error["code"], error) # Otherwise, try requesting a new OAuth token. - logger.info('Refreshing access token') + logger.info("Refreshing access token") self._auth.refresh() i += 1 @@ -249,21 +257,21 @@ def _send(self, **req_kwargs) -> requests.Response: # Bail if we don't have an OAuth token. auth_token = self._auth.getAuthToken() if auth_token is None: - raise exception.LoginException('Not logged in') + raise exception.LoginException("Not logged in") # Add the token to the request. - req_kwargs.setdefault('headers', { - 'Authorization': 'OAuth ' + auth_token - }) + req_kwargs.setdefault("headers", {"Authorization": "OAuth " + auth_token}) return self._session.request(**req_kwargs) + class KeepAPI(API): """Low level Google Keep API client. Mimics the Android Google Keep app. You probably want to use :py:class:`Keep` instead. """ - API_URL = 'https://www.googleapis.com/notes/v1/' + + API_URL = "https://www.googleapis.com/notes/v1/" def __init__(self, auth=None): super(KeepAPI, self).__init__(self.API_URL, auth) @@ -273,12 +281,14 @@ def __init__(self, auth=None): @classmethod def _generateId(cls, tz: int) -> str: - return 's--%d--%d' % ( - int(tz * 1000), - random.randint(1000000000, 9999999999) - ) + return "s--%d--%d" % (int(tz * 1000), random.randint(1000000000, 9999999999)) - def changes(self, target_version:str=None, nodes:List[Dict]=None, labels:List[Dict]=None) -> Dict: + def changes( + self, + target_version: str = None, + nodes: List[Dict] = None, + labels: List[Dict] = None, + ) -> Dict: """Sync up (and down) all changes. Args: @@ -302,64 +312,58 @@ def changes(self, target_version:str=None, nodes:List[Dict]=None, labels:List[Di # Initialize request parameters. params = { - 'nodes': nodes, - 'clientTimestamp': _node.NodeTimestamps.int_to_str(current_time), - 'requestHeader': { - 'clientSessionId': self._session_id, - 'clientPlatform': 'ANDROID', - 'clientVersion': { - 'major': '9', - 'minor': '9', - 'build': '9', - 'revision': '9' + "nodes": nodes, + "clientTimestamp": _node.NodeTimestamps.int_to_str(current_time), + "requestHeader": { + "clientSessionId": self._session_id, + "clientPlatform": "ANDROID", + "clientVersion": { + "major": "9", + "minor": "9", + "build": "9", + "revision": "9", }, - 'capabilities': [ - {'type': 'NC'}, # Color support (Send note color) - {'type': 'PI'}, # Pinned support (Send note pinned) - {'type': 'LB'}, # Labels support (Send note labels) - {'type': 'AN'}, # Annotations support (Send annotations) - {'type': 'SH'}, # Sharing support - {'type': 'DR'}, # Drawing support - {'type': 'TR'}, # Trash support (Stop setting the delete timestamp) - {'type': 'IN'}, # Indentation support (Send listitem parent) - - {'type': 'SNB'}, # Allows modification of shared notes? - {'type': 'MI'}, # Concise blob info? - {'type': 'CO'}, # VSS_SUCCEEDED when off? - + "capabilities": [ + {"type": "NC"}, # Color support (Send note color) + {"type": "PI"}, # Pinned support (Send note pinned) + {"type": "LB"}, # Labels support (Send note labels) + {"type": "AN"}, # Annotations support (Send annotations) + {"type": "SH"}, # Sharing support + {"type": "DR"}, # Drawing support + {"type": "TR"}, # Trash support (Stop setting the delete timestamp) + {"type": "IN"}, # Indentation support (Send listitem parent) + {"type": "SNB"}, # Allows modification of shared notes? + {"type": "MI"}, # Concise blob info? + {"type": "CO"}, # VSS_SUCCEEDED when off? # TODO: Figure out what these do: # {'type': 'EC'}, # ??? # {'type': 'RB'}, # Rollback? # {'type': 'EX'}, # ??? - ] + ], }, } # Add the targetVersion if set. This tells the server what version the # client is currently at. if target_version is not None: - params['targetVersion'] = target_version + params["targetVersion"] = target_version # Add any new or updated labels to the request. if labels: - params['userInfo'] = { - 'labels': labels - } + params["userInfo"] = {"labels": labels} - logger.debug('Syncing %d labels and %d nodes', len(labels), len(nodes)) + logger.debug("Syncing %d labels and %d nodes", len(labels), len(nodes)) + + return self.send(url=self._base_url + "changes", method="POST", json=params) - return self.send( - url=self._base_url + 'changes', - method='POST', - json=params - ) class MediaAPI(API): """Low level Google Media API client. Mimics the Android Google Keep app. You probably want to use :py:class:`Keep` instead. """ - API_URL = 'https://keep.google.com/media/v2/' + + API_URL = "https://keep.google.com/media/v2/" def __init__(self, auth=None): super(MediaAPI, self).__init__(self.API_URL, auth) @@ -373,21 +377,21 @@ def get(self, blob: _node.Blob) -> str: Returns: A link to the media. """ - url = self._base_url + blob.parent.server_id + '/' + blob.server_id + url = self._base_url + blob.parent.server_id + "/" + blob.server_id if blob.blob.type == _node.BlobType.Drawing: - url += '/' + blob.blob._drawing_info.drawing_id - return self._send( - url=url, - method='GET', - allow_redirects=False - ).headers['location'] + url += "/" + blob.blob._drawing_info.drawing_id + return self._send(url=url, method="GET", allow_redirects=False).headers[ + "location" + ] + class RemindersAPI(API): """Low level Google Reminders API client. Mimics the Android Google Keep app. You probably want to use :py:class:`Keep` instead. """ - API_URL = 'https://www.googleapis.com/reminders/v1internal/reminders/' + + API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" def __init__(self, auth=None): super(RemindersAPI, self).__init__(self.API_URL, auth) @@ -399,14 +403,15 @@ def __init__(self, auth=None): "userAgentStructured": { "clientApplication": "KEEP", "clientApplicationVersion": { - "major": "9", "minor": "9.9.9.9", + "major": "9", + "minor": "9.9.9.9", }, "clientPlatform": "ANDROID", }, }, } - def create(self, node_id: str, node_server_id :str, dtime: datetime.datetime): + def create(self, node_id: str, node_server_id: str, dtime: datetime.datetime): """Create a new reminder. Args: @@ -423,38 +428,36 @@ def create(self, node_id: str, node_server_id :str, dtime: datetime.datetime): params = {} params.update(self.static_params) - params.update({ - 'task': { - 'dueDate': { - 'year': dtime.year, - 'month': dtime.month, - 'day': dtime.day, - 'time': { - 'hour': dtime.hour, - 'minute': dtime.minute, - 'second': dtime.second, + params.update( + { + "task": { + "dueDate": { + "year": dtime.year, + "month": dtime.month, + "day": dtime.day, + "time": { + "hour": dtime.hour, + "minute": dtime.minute, + "second": dtime.second, + }, }, - }, - 'snoozed': True, - 'extensions': { - 'keepExtension': { - 'reminderVersion': 'V2', - 'clientNoteId': node_id, - 'serverNoteId': node_server_id, + "snoozed": True, + "extensions": { + "keepExtension": { + "reminderVersion": "V2", + "clientNoteId": node_id, + "serverNoteId": node_server_id, + }, }, }, - }, - 'taskId': { - 'clientAssignedId': 'KEEP/v2/' + node_server_id, - }, - }) - - return self.send( - url=self._base_url + 'create', - method='POST', - json=params + "taskId": { + "clientAssignedId": "KEEP/v2/" + node_server_id, + }, + } ) + return self.send(url=self._base_url + "create", method="POST", json=params) + def update(self, node_id: str, node_server_id: str, dtime: datetime.datetime): """Update an existing reminder. @@ -471,45 +474,47 @@ def update(self, node_id: str, node_server_id: str, dtime: datetime.datetime): params = {} params.update(self.static_params) - params.update({ - 'newTask': { - 'dueDate': { - 'year': dtime.year, - 'month': dtime.month, - 'day': dtime.day, - 'time': { - 'hour': dtime.hour, - 'minute': dtime.minute, - 'second': dtime.second, + params.update( + { + "newTask": { + "dueDate": { + "year": dtime.year, + "month": dtime.month, + "day": dtime.day, + "time": { + "hour": dtime.hour, + "minute": dtime.minute, + "second": dtime.second, + }, }, - }, - 'snoozed': True, - 'extensions': { - 'keepExtension': { - 'reminderVersion': 'V2', - 'clientNoteId': node_id, - 'serverNoteId': node_server_id, + "snoozed": True, + "extensions": { + "keepExtension": { + "reminderVersion": "V2", + "clientNoteId": node_id, + "serverNoteId": node_server_id, + }, }, }, - }, - 'taskId': { - 'clientAssignedId': 'KEEP/v2/' + node_server_id, - }, - 'updateMask': { - 'updateField': [ - 'ARCHIVED', 'DUE_DATE', 'EXTENSIONS', 'LOCATION', 'TITLE' - ] + "taskId": { + "clientAssignedId": "KEEP/v2/" + node_server_id, + }, + "updateMask": { + "updateField": [ + "ARCHIVED", + "DUE_DATE", + "EXTENSIONS", + "LOCATION", + "TITLE", + ] + }, } - }) - - return self.send( - url=self._base_url + 'update', - method='POST', - json=params ) + return self.send(url=self._base_url + "update", method="POST", json=params) + def delete(self, node_server_id: str): - """ Delete an existing reminder. + """Delete an existing reminder. Args: node_server_id: The note server ID. @@ -523,26 +528,22 @@ def delete(self, node_server_id: str): params = {} params.update(self.static_params) - params.update({ - 'batchedRequest': [ - { - 'deleteTask': { - 'taskId': [ - { - 'clientAssignedId': 'KEEP/v2/' + node_server_id - } - ] + params.update( + { + "batchedRequest": [ + { + "deleteTask": { + "taskId": [ + {"clientAssignedId": "KEEP/v2/" + node_server_id} + ] + } } - } - ] - }) - - return self.send( - url=self._base_url + 'batchmutate', - method='POST', - json=params + ] + } ) + return self.send(url=self._base_url + "batchmutate", method="POST", json=params) + def list(self, master=True): """List current reminders. @@ -559,36 +560,36 @@ def list(self, master=True): params.update(self.static_params) if master: - params.update({ - 'recurrenceOptions': { - 'collapseMode': 'MASTER_ONLY', - }, - 'includeArchived': True, - 'includeDeleted': False, - }) + params.update( + { + "recurrenceOptions": { + "collapseMode": "MASTER_ONLY", + }, + "includeArchived": True, + "includeDeleted": False, + } + ) else: current_time = time.time() start_time = int((current_time - (365 * 24 * 60 * 60)) * 1000) end_time = int((current_time + (24 * 60 * 60)) * 1000) - params.update({ - 'recurrenceOptions': { - 'collapseMode': 'INSTANCES_ONLY', - 'recurrencesOnly': True, - }, - 'includeArchived': False, - 'includeCompleted': False, - 'includeDeleted': False, - 'dueAfterMs': start_time, - 'dueBeforeMs': end_time, - 'recurrenceId': [], - }) - - return self.send( - url=self._base_url + 'list', - method='POST', - json=params - ) + params.update( + { + "recurrenceOptions": { + "collapseMode": "INSTANCES_ONLY", + "recurrencesOnly": True, + }, + "includeArchived": False, + "includeCompleted": False, + "includeDeleted": False, + "dueAfterMs": start_time, + "dueBeforeMs": end_time, + "recurrenceId": [], + } + ) + + return self.send(url=self._base_url + "list", method="POST", json=params) def history(self, storage_version: str): """Get reminder changes. @@ -608,21 +609,13 @@ def history(self, storage_version: str): } params.update(self.static_params) - return self.send( - url=self._base_url + 'history', - method='POST', - json=params - ) + return self.send(url=self._base_url + "history", method="POST", json=params) def update(self): - """Sync up changes to reminders. - """ + """Sync up changes to reminders.""" params = {} - return self.send( - url=self._base_url + 'update', - method='POST', - json=params - ) + return self.send(url=self._base_url + "update", method="POST", json=params) + class Keep(object): """High level Google Keep client. @@ -648,7 +641,8 @@ class Keep(object): keep.sync() """ - OAUTH_SCOPES = 'oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders' + + OAUTH_SCOPES = "oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders" def __init__(self): self._keep_api = KeepAPI() @@ -672,7 +666,14 @@ def _clear(self) -> None: root_node = _node.Root() self._nodes[_node.Root.ID] = root_node - def login(self, email: str, password: str, state: Optional[Dict] = None, sync=True, device_id: Optional[str] = None): + def login( + self, + email: str, + password: str, + state: Optional[Dict] = None, + sync=True, + device_id: Optional[str] = None, + ): """Authenticate to Google with the provided credentials & sync. Args: @@ -694,7 +695,14 @@ def login(self, email: str, password: str, state: Optional[Dict] = None, sync=Tr return True - def resume(self, email: str, master_token: str, state: Optional[Dict] = None, sync=True, device_id: Optional[str] = None): + def resume( + self, + email: str, + master_token: str, + state: Optional[Dict] = None, + sync=True, + device_id: Optional[str] = None, + ): """Authenticate to Google with the provided master token & sync. Args: @@ -755,9 +763,9 @@ def dump(self) -> Dict: for child in node.children: nodes.append(child) return { - 'keep_version': self._keep_version, - 'labels': [label.save(False) for label in self.labels()], - 'nodes': [node.save(False) for node in nodes] + "keep_version": self._keep_version, + "labels": [label.save(False) for label in self.labels()], + "nodes": [node.save(False) for node in nodes], } def restore(self, state: Dict) -> None: @@ -767,9 +775,9 @@ def restore(self, state: Dict) -> None: state: Serialized state to load. """ self._clear() - self._parseUserInfo({'labels': state['labels']}) - self._parseNodes(state['nodes']) - self._keep_version = state['keep_version'] + self._parseUserInfo({"labels": state["labels"]}) + self._parseNodes(state["nodes"]) + self._keep_version = state["keep_version"] def get(self, node_id: str) -> _node.TopLevelNode: """Get a note with the given ID. @@ -780,9 +788,9 @@ def get(self, node_id: str) -> _node.TopLevelNode: Returns: The Note or None if not found. """ - return \ - self._nodes[_node.Root.ID].get(node_id) or \ - self._nodes[_node.Root.ID].get(self._sid_map.get(node_id)) + return self._nodes[_node.Root.ID].get(node_id) or self._nodes[ + _node.Root.ID + ].get(self._sid_map.get(node_id)) def add(self, node: _node.Node) -> None: """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by @@ -796,7 +804,7 @@ def add(self, node: _node.Node) -> None: InvalidException: If the parent node is not found. """ if node.parent_id != _node.Root.ID: - raise exception.InvalidException('Not a top level node') + raise exception.InvalidException("Not a top level node") self._nodes[node.id] = node self._nodes[node.parent_id].append(node, False) @@ -809,8 +817,8 @@ def find( colors: Optional[List[str]] = None, pinned: Optional[bool] = None, archived: Optional[bool] = None, - trashed: Optional[bool] = False - ) -> Iterator[_node.TopLevelNode]: # pylint: disable=too-many-arguments + trashed: Optional[bool] = False, + ) -> Iterator[_node.TopLevelNode]: # pylint: disable=too-many-arguments """Find Notes based on the specified criteria. Args: @@ -828,32 +836,43 @@ def find( if labels is not None: labels = [i.id if isinstance(i, _node.Label) else i for i in labels] - return (node for node in self.all() if + return ( + node + for node in self.all() + if # Process the query. - (query is None or ( - (isinstance(query, str) and (query in node.title or query in node.text)) or - (isinstance(query, re.Pattern) and ( - query.search(node.title) or query.search(node.text) - )) - )) and + ( + query is None + or ( + ( + isinstance(query, str) + and (query in node.title or query in node.text) + ) + or ( + isinstance(query, re.Pattern) + and (query.search(node.title) or query.search(node.text)) + ) + ) + ) + and # Process the func. - (func is None or func(node)) and \ - # Process the labels. - (labels is None or \ - (not labels and not node.labels.all()) or \ - (any((node.labels.get(i) is not None for i in labels))) - ) and \ - # Process the colors. - (colors is None or node.color in colors) and \ - # Process the pinned state. - (pinned is None or node.pinned == pinned) and \ - # Process the archive state. - (archived is None or node.archived == archived) and \ - # Process the trash state. - (trashed is None or node.trashed == trashed) + (func is None or func(node)) + and ( # Process the labels. + labels is None + or (not labels and not node.labels.all()) + or (any((node.labels.get(i) is not None for i in labels))) + ) + and (colors is None or node.color in colors) # Process the colors. + and (pinned is None or node.pinned == pinned) # Process the pinned state. + and ( # Process the archive state. + archived is None or node.archived == archived + ) + and (trashed is None or node.trashed == trashed) # Process the trash state. ) - def createNote(self, title: Optional[str] = None, text: Optional[str] = None) -> _node.Node: + def createNote( + self, title: Optional[str] = None, text: Optional[str] = None + ) -> _node.Node: """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: @@ -871,7 +890,11 @@ def createNote(self, title: Optional[str] = None, text: Optional[str] = None) -> self.add(node) return node - def createList(self, title: Optional[str] = None, items: Optional[List[Tuple[str, bool]]] = None) -> _node.List: + def createList( + self, + title: Optional[str] = None, + items: Optional[List[Tuple[str, bool]]] = None, + ) -> _node.List: """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: @@ -908,13 +931,15 @@ def createLabel(self, name: str) -> _node.Label: LabelException: If the label exists. """ if self.findLabel(name): - raise exception.LabelException('Label exists') + raise exception.LabelException("Label exists") node = _node.Label() node.name = name - self._labels[node.id] = node # pylint: disable=protected-access + self._labels[node.id] = node # pylint: disable=protected-access return node - def findLabel(self, query: Union[re.Pattern, str], create=False) -> Optional[_node.Label]: + def findLabel( + self, query: Union[re.Pattern, str], create=False + ) -> Optional[_node.Label]: """Find a label with the given name. Args: @@ -932,8 +957,9 @@ def findLabel(self, query: Union[re.Pattern, str], create=False) -> Optional[_no for label in self._labels.values(): # Match the label against query, which may be a str or Pattern. - if (is_str and query == label.name.lower()) or \ - (isinstance(query, re.Pattern) and query.search(label.name)): + if (is_str and query == label.name.lower()) or ( + isinstance(query, re.Pattern) and query.search(label.name) + ): return label return self.createLabel(name) if create and is_str else None @@ -1012,59 +1038,61 @@ def sync(self, resync=False) -> None: def _sync_reminders(self, resync=False): # Fetch updates until we reach the newest version. while True: - logger.debug('Starting reminder sync: %s', self._reminder_version) + logger.debug("Starting reminder sync: %s", self._reminder_version) changes = self._reminders_api.list() # Hydrate the individual "tasks". - if 'task' in changes: - self._parseTasks(changes['task']) + if "task" in changes: + self._parseTasks(changes["task"]) - self._reminder_version = changes['storageVersion'] - logger.debug('Finishing sync: %s', self._reminder_version) + self._reminder_version = changes["storageVersion"] + logger.debug("Finishing sync: %s", self._reminder_version) # Check if we've reached the newest version. history = self._reminders_api.history(self._reminder_version) - if self._reminder_version == history['highestStorageVersion']: + if self._reminder_version == history["highestStorageVersion"]: break def _sync_notes(self, resync=False): # Fetch updates until we reach the newest version. while True: - logger.debug('Starting keep sync: %s', self._keep_version) + logger.debug("Starting keep sync: %s", self._keep_version) # Collect any changes and send them up to the server. labels_updated = any((i.dirty for i in self._labels.values())) changes = self._keep_api.changes( target_version=self._keep_version, nodes=[i.save() for i in self._findDirtyNodes()], - labels=[i.save() for i in self._labels.values()] if labels_updated else None, + labels=[i.save() for i in self._labels.values()] + if labels_updated + else None, ) - if changes.get('forceFullResync'): - raise exception.ResyncRequiredException('Full resync required') + if changes.get("forceFullResync"): + raise exception.ResyncRequiredException("Full resync required") - if changes.get('upgradeRecommended'): - raise exception.UpgradeRecommendedException('Upgrade recommended') + if changes.get("upgradeRecommended"): + raise exception.UpgradeRecommendedException("Upgrade recommended") # Hydrate labels. - if 'userInfo' in changes: - self._parseUserInfo(changes['userInfo']) + if "userInfo" in changes: + self._parseUserInfo(changes["userInfo"]) # Hydrate notes and any children. - if 'nodes' in changes: - self._parseNodes(changes['nodes']) + if "nodes" in changes: + self._parseNodes(changes["nodes"]) - self._keep_version = changes['toVersion'] - logger.debug('Finishing sync: %s', self._keep_version) + self._keep_version = changes["toVersion"] + logger.debug("Finishing sync: %s", self._keep_version) # Check if there are more changes to retrieve. - if not changes['truncated']: + if not changes["truncated"]: break def _parseTasks(self, raw) -> None: pass - def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches + def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1073,15 +1101,15 @@ def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches for raw_node in raw: # If the id exists, then we already know about it. In other words, # update a local node. - if raw_node['id'] in self._nodes: - node = self._nodes[raw_node['id']] + if raw_node["id"] in self._nodes: + node = self._nodes[raw_node["id"]] - if 'parentId' in raw_node: + if "parentId" in raw_node: # If the parentId field is set, this is an update. Load it # into the existing node. node.load(raw_node) self._sid_map[node.server_id] = node.id - logger.debug('Updated node: %s', raw_node['id']) + logger.debug("Updated node: %s", raw_node["id"]) else: # Otherwise, this node has been deleted. Add it to the list. deleted_nodes.append(node) @@ -1090,13 +1118,13 @@ def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches # Otherwise, this is a new node. Attempt to hydrate it. node = _node.from_json(raw_node) if node is None: - logger.debug('Discarded unknown node') + logger.debug("Discarded unknown node") else: # Append the new node into the node tree. - self._nodes[raw_node['id']] = node + self._nodes[raw_node["id"]] = node self._sid_map[node.server_id] = node.id created_nodes.append(node) - logger.debug('Created node: %s', raw_node['id']) + logger.debug("Created node: %s", raw_node["id"]) # If the node is a listitem, keep track of it. if isinstance(node, _node.ListItem): @@ -1118,9 +1146,10 @@ def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches # Attach created nodes to the tree. for node in created_nodes: - logger.debug('Attached node: %s to %s', + logger.debug( + "Attached node: %s to %s", node.id if node else None, - node.parent_id if node else None + node.parent_id if node else None, ) parent_node = self._nodes.get(node.parent_id) parent_node.append(node, False) @@ -1131,33 +1160,35 @@ def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches del self._nodes[node.id] if node.server_id is not None: del self._sid_map[node.server_id] - logger.debug('Deleted node: %s', node.id) + logger.debug("Deleted node: %s", node.id) # Hydrate label references in notes. for node in self.all(): - for label_id in node.labels._labels: # pylint: disable=protected-access - node.labels._labels[label_id] = self._labels.get(label_id) # pylint: disable=protected-access + for label_id in node.labels._labels: # pylint: disable=protected-access + node.labels._labels[label_id] = self._labels.get( + label_id + ) # pylint: disable=protected-access def _parseUserInfo(self, raw) -> None: labels = {} - if 'labels' in raw: - for label in raw['labels']: + if "labels" in raw: + for label in raw["labels"]: # If the mainId field exists, this is an update. - if label['mainId'] in self._labels: - node = self._labels[label['mainId']] + if label["mainId"] in self._labels: + node = self._labels[label["mainId"]] # Remove this key from our list of labels. - del self._labels[label['mainId']] - logger.debug('Updated label: %s', label['mainId']) + del self._labels[label["mainId"]] + logger.debug("Updated label: %s", label["mainId"]) else: # Otherwise, this is a brand new label. node = _node.Label() - logger.debug('Created label: %s', label['mainId']) + logger.debug("Created label: %s", label["mainId"]) node.load(label) - labels[label['mainId']] = node + labels[label["mainId"]] = node # All remaining labels are deleted. for label_id in self._labels: - logger.debug('Deleted label: %s', label_id) + logger.debug("Deleted label: %s", label_id) self._labels = labels @@ -1191,10 +1222,10 @@ def _clean(self) -> None: for node_id in self._nodes: if node_id in found_ids: continue - logger.error('Dangling node: %s', node_id) + logger.error("Dangling node: %s", node_id) # Find nodes that don't exist in the collection for node_id in found_ids: if node_id in self._nodes: continue - logger.error('Unregistered node: %s', node_id) + logger.error("Unregistered node: %s", node_id) From e2f7e5d1f574c19febae66ad3dfd7f357a310859 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 11:31:21 -0400 Subject: [PATCH 14/56] Black --- gkeepapi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index fc7b667..05d45e1 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -22,6 +22,7 @@ logger = logging.getLogger(__name__) + class APIAuth(object): """Authentication token manager""" From 3b9b799071a664dbc5cdfb44aa4d33d011097098 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 11:55:15 -0400 Subject: [PATCH 15/56] Add addn typehints --- docs/Makefile | 6 ++-- docs/conf.py | 72 ++++++++++++++++++++++--------------------- gkeepapi/__init__.py | 28 ++++++++--------- gkeepapi/exception.py | 26 ++++++++++++++-- 4 files changed, 78 insertions(+), 54 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 5fe8f0f..41d63e4 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,18 +3,18 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = python -msphinx +SPHINXBUILD = python3 -msphinx SPHINXPROJ = gkeepapi SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: - @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + @$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 9ff71ca..229e43e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,8 @@ # import os import sys -sys.path.insert(0, os.path.abspath('..')) + +sys.path.insert(0, os.path.abspath("..")) import gkeepapi @@ -31,28 +32,30 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = ['sphinx.ext.coverage', - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.napoleon', - 'sphinx.ext.githubpages'] +extensions = [ + "sphinx.ext.napoleon", + "sphinx.ext.coverage", + # "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.githubpages", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'gkeepapi' -copyright = '2017, Kai' -author = 'Kai' +project = "gkeepapi" +copyright = "2017, Kai" +author = "Kai" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -73,10 +76,10 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -87,7 +90,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'alabaster' +html_theme = "alabaster" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -98,7 +101,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. @@ -106,12 +109,12 @@ # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - 'donate.html', + "**": [ + "about.html", + "navigation.html", + "relations.html", # needs 'show_related': True theme option to display + "searchbox.html", + "donate.html", ] } @@ -119,7 +122,7 @@ # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'gkeepapidoc' +htmlhelp_basename = "gkeepapidoc" # -- Options for LaTeX output --------------------------------------------- @@ -128,15 +131,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -146,8 +146,7 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'gkeepapi.tex', 'gkeepapi Documentation', - 'Kai', 'manual'), + (master_doc, "gkeepapi.tex", "gkeepapi Documentation", "Kai", "manual"), ] @@ -155,10 +154,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'gkeepapi', 'gkeepapi Documentation', - [author], 1) -] +man_pages = [(master_doc, "gkeepapi", "gkeepapi Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -167,7 +163,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'gkeepapi', 'gkeepapi Documentation', - author, 'gkeepapi', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "gkeepapi", + "gkeepapi Documentation", + author, + "gkeepapi", + "One line description of project.", + "Miscellaneous", + ), ] diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 05d45e1..9ba46bd 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -23,10 +23,10 @@ logger = logging.getLogger(__name__) -class APIAuth(object): +class APIAuth: """Authentication token manager""" - def __init__(self, scopes): + def __init__(self, scopes: str): self._master_token = None self._auth_token = None self._email = None @@ -87,11 +87,11 @@ def getMasterToken(self) -> str: """Gets the master token. Returns: - str: The account master token. + The account master token. """ return self._master_token - def setMasterToken(self, master_token) -> None: + def setMasterToken(self, master_token: str) -> None: """Sets the master token. This is useful if you'd like to authenticate with the API without providing your username & password. Do note that the master token has full access to your account. @@ -109,7 +109,7 @@ def getEmail(self) -> str: """ return self._email - def setEmail(self, email) -> None: + def setEmail(self, email: str) -> None: """Sets the account email. Args: @@ -125,7 +125,7 @@ def getDeviceId(self) -> str: """ return self._device_id - def setDeviceId(self, device_id) -> None: + def setDeviceId(self, device_id: str) -> None: """Sets the device id. Args: @@ -176,12 +176,12 @@ def logout(self) -> None: self._device_id = None -class API(object): +class API: """Base API wrapper""" RETRY_CNT = 2 - def __init__(self, base_url: str, auth=None): + def __init__(self, base_url: str, auth: APIAuth = None): self._session = requests.Session() self._auth = auth self._base_url = base_url @@ -200,7 +200,7 @@ def getAuth(self) -> APIAuth: """ return self._auth - def setAuth(self, auth: APIAuth): + def setAuth(self, auth: APIAuth) -> None: """Set authentication details for this API. Args: @@ -279,7 +279,7 @@ class KeepAPI(API): API_URL = "https://www.googleapis.com/notes/v1/" - def __init__(self, auth=None): + def __init__(self, auth: APIAuth = None): super(KeepAPI, self).__init__(self.API_URL, auth) create_time = time.time() @@ -371,7 +371,7 @@ class MediaAPI(API): API_URL = "https://keep.google.com/media/v2/" - def __init__(self, auth=None): + def __init__(self, auth: APIAuth = None): super(MediaAPI, self).__init__(self.API_URL, auth) def get(self, blob: _node.Blob) -> str: @@ -399,7 +399,7 @@ class RemindersAPI(API): API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" - def __init__(self, auth=None): + def __init__(self, auth: APIAuth = None): super(RemindersAPI, self).__init__(self.API_URL, auth) self.static_params = { "taskList": [ @@ -623,7 +623,7 @@ def update(self): return self.send(url=self._base_url + "update", method="POST", json=params) -class Keep(object): +class Keep: """High level Google Keep client. Stores a local copy of the Keep node tree. To start, first login:: @@ -743,6 +743,7 @@ def load(self, auth: APIAuth, state: Optional[Dict] = None, sync=True) -> None: Args: auth: Authentication object. state: Serialized state to load. + sync: Whether to sync data. Raises: LoginException: If there was a problem logging in. @@ -802,7 +803,6 @@ def add(self, node: _node.Node) -> None: """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. - LoginException: If :py:meth:`login` has not been called. Args: node: The node to sync. diff --git a/gkeepapi/exception.py b/gkeepapi/exception.py index da73bf4..0bf0fb1 100644 --- a/gkeepapi/exception.py +++ b/gkeepapi/exception.py @@ -3,51 +3,73 @@ .. moduleauthor:: Kai """ + class APIException(Exception): """The API server returned an error.""" - def __init__(self, code, msg): + + def __init__(self, code: int, msg: str): super(APIException, self).__init__(msg) self.code = code + class KeepException(Exception): """Generic Keep error.""" + pass + class LoginException(KeepException): """Login exception.""" + pass + class BrowserLoginRequiredException(LoginException): """Browser login required error.""" + def __init__(self, url): self.url = url + class LabelException(KeepException): """Keep label error.""" + pass + class SyncException(KeepException): """Keep consistency error.""" + pass + class ResyncRequiredException(SyncException): """Full resync required error.""" + pass + class UpgradeRecommendedException(SyncException): """Upgrade recommended error.""" + pass + class MergeException(KeepException): """Node consistency error.""" + pass + class InvalidException(KeepException): """Constraint error.""" + pass + class ParseException(KeepException): """Parse error.""" - def __init__(self, msg, raw): + + def __init__(self, msg: str, raw): super(ParseException, self).__init__(msg) self.raw = raw From f4290a8b01f750564af204e938b0e7b22c52bfd7 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 11:55:26 -0400 Subject: [PATCH 16/56] Black --- gkeepapi/node.py | 725 +++++++++++++++++++++++++++++------------------ 1 file changed, 443 insertions(+), 282 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 25ed4d7..a2d575a 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -21,163 +21,175 @@ logger = logging.getLogger(__name__) + class NodeType(enum.Enum): """Valid note types.""" - Note = 'NOTE' + Note = "NOTE" """A Note""" - List = 'LIST' + List = "LIST" """A List""" - ListItem = 'LIST_ITEM' + ListItem = "LIST_ITEM" """A List item""" - Blob = 'BLOB' + Blob = "BLOB" """A blob (attachment)""" + class BlobType(enum.Enum): """Valid blob types.""" - Audio = 'AUDIO' + Audio = "AUDIO" """Audio""" - Image = 'IMAGE' + Image = "IMAGE" """Image""" - Drawing = 'DRAWING' + Drawing = "DRAWING" """Drawing""" + class ColorValue(enum.Enum): """Valid note colors.""" - White = 'DEFAULT' + White = "DEFAULT" """White""" - Red = 'RED' + Red = "RED" """Red""" - Orange = 'ORANGE' + Orange = "ORANGE" """Orange""" - Yellow = 'YELLOW' + Yellow = "YELLOW" """Yellow""" - Green = 'GREEN' + Green = "GREEN" """Green""" - Teal = 'TEAL' + Teal = "TEAL" """Teal""" - Blue = 'BLUE' + Blue = "BLUE" """Blue""" - DarkBlue = 'CERULEAN' + DarkBlue = "CERULEAN" """Dark blue""" - Purple = 'PURPLE' + Purple = "PURPLE" """Purple""" - Pink = 'PINK' + Pink = "PINK" """Pink""" - Brown = 'BROWN' + Brown = "BROWN" """Brown""" - Gray = 'GRAY' + Gray = "GRAY" """Gray""" + class CategoryValue(enum.Enum): """Valid note categories.""" - Books = 'BOOKS' + Books = "BOOKS" """Books""" - Food = 'FOOD' + Food = "FOOD" """Food""" - Movies = 'MOVIES' + Movies = "MOVIES" """Movies""" - Music = 'MUSIC' + Music = "MUSIC" """Music""" - Places = 'PLACES' + Places = "PLACES" """Places""" - Quotes = 'QUOTES' + Quotes = "QUOTES" """Quotes""" - Travel = 'TRAVEL' + Travel = "TRAVEL" """Travel""" - TV = 'TV' + TV = "TV" """TV""" + class SuggestValue(enum.Enum): """Valid task suggestion categories.""" - GroceryItem = 'GROCERY_ITEM' + GroceryItem = "GROCERY_ITEM" """Grocery item""" + class NewListItemPlacementValue(enum.Enum): """Target location to put new list items.""" - Top = 'TOP' + Top = "TOP" """Top""" - Bottom = 'BOTTOM' + Bottom = "BOTTOM" """Bottom""" + class GraveyardStateValue(enum.Enum): """Visibility setting for the graveyard.""" - Expanded = 'EXPANDED' + Expanded = "EXPANDED" """Expanded""" - Collapsed = 'COLLAPSED' + Collapsed = "COLLAPSED" """Collapsed""" + class CheckedListItemsPolicyValue(enum.Enum): """Movement setting for checked list items.""" - Default = 'DEFAULT' + Default = "DEFAULT" """Default""" - Graveyard = 'GRAVEYARD' + Graveyard = "GRAVEYARD" """Graveyard""" + class ShareRequestValue(enum.Enum): """Collaborator change type.""" - Add = 'WR' + Add = "WR" """Grant access.""" - Remove = 'RM' + Remove = "RM" """Remove access.""" + class RoleValue(enum.Enum): """Collaborator role type.""" - Owner = 'O' + Owner = "O" """Note owner.""" - User = 'W' + User = "W" """Note collaborator.""" + class Element(object): """Interface for elements that can be serialized and deserialized.""" + def __init__(self): self._dirty = False - def _find_discrepancies(self, raw): # pragma: no cover + def _find_discrepancies(self, raw): # pragma: no cover s_raw = self.save(False) if isinstance(raw, dict): for key, val in raw.items(): - if key in ['parentServerId', 'lastSavedSessionId']: + if key in ["parentServerId", "lastSavedSessionId"]: continue if key not in s_raw: - logger.info('Missing key for %s key %s', type(self), key) + logger.info("Missing key for %s key %s", type(self), key) continue if isinstance(val, (list, dict)): @@ -196,10 +208,21 @@ def _find_discrepancies(self, raw): # pragma: no cover except (KeyError, ValueError): pass if val_a != val_b: - logger.info('Different value for %s key %s: %s != %s', type(self), key, raw[key], s_raw[key]) + logger.info( + "Different value for %s key %s: %s != %s", + type(self), + key, + raw[key], + s_raw[key], + ) elif isinstance(raw, list): if len(raw) != len(s_raw): - logger.info('Different length for %s: %d != %d', type(self), len(raw), len(s_raw)) + logger.info( + "Different length for %s: %d != %d", + type(self), + len(raw), + len(s_raw), + ) def load(self, raw): """Unserialize from raw representation. (Wrapper) @@ -212,7 +235,7 @@ def load(self, raw): try: self._load(raw) except (KeyError, ValueError) as e: - raise exception.ParseException(f'Parse error in {type(self)}', raw) from e + raise exception.ParseException(f"Parse error in {type(self)}", raw) from e def _load(self, raw): """Unserialize from raw representation. (Implementation logic) @@ -220,7 +243,7 @@ def _load(self, raw): Args: raw (dict): Raw. """ - self._dirty = raw.get('_dirty', False) + self._dirty = raw.get("_dirty", False) def save(self, clean=True): """Serialize into raw representation. Clears the dirty bit by default. @@ -235,7 +258,7 @@ def save(self, clean=True): if clean: self._dirty = False else: - ret['_dirty'] = self._dirty + ret["_dirty"] = self._dirty return ret @property @@ -247,60 +270,68 @@ def dirty(self): """ return self._dirty + class Annotation(Element): """Note annotations base class.""" + def __init__(self): super(Annotation, self).__init__() self.id = self._generateAnnotationId() def _load(self, raw): super(Annotation, self)._load(raw) - self.id = raw.get('id') + self.id = raw.get("id") def save(self, clean=True): ret = {} if self.id is not None: ret = super(Annotation, self).save(clean) if self.id is not None: - ret['id'] = self.id + ret["id"] = self.id return ret @classmethod def _generateAnnotationId(cls): - return '%08x-%04x-%04x-%04x-%012x' % ( - random.randint(0x00000000, 0xffffffff), - random.randint(0x0000, 0xffff), - random.randint(0x0000, 0xffff), - random.randint(0x0000, 0xffff), - random.randint(0x000000000000, 0xffffffffffff) + return "%08x-%04x-%04x-%04x-%012x" % ( + random.randint(0x00000000, 0xFFFFFFFF), + random.randint(0x0000, 0xFFFF), + random.randint(0x0000, 0xFFFF), + random.randint(0x0000, 0xFFFF), + random.randint(0x000000000000, 0xFFFFFFFFFFFF), ) + class WebLink(Annotation): """Represents a link annotation on a :class:`TopLevelNode`.""" + def __init__(self): super(WebLink, self).__init__() - self._title = '' - self._url = '' + self._title = "" + self._url = "" self._image_url = None - self._provenance_url = '' - self._description = '' + self._provenance_url = "" + self._description = "" def _load(self, raw): super(WebLink, self)._load(raw) - self._title = raw['webLink']['title'] - self._url = raw['webLink']['url'] - self._image_url = raw['webLink']['imageUrl'] if 'imageUrl' in raw['webLink'] else self.image_url - self._provenance_url = raw['webLink']['provenanceUrl'] - self._description = raw['webLink']['description'] + self._title = raw["webLink"]["title"] + self._url = raw["webLink"]["url"] + self._image_url = ( + raw["webLink"]["imageUrl"] + if "imageUrl" in raw["webLink"] + else self.image_url + ) + self._provenance_url = raw["webLink"]["provenanceUrl"] + self._description = raw["webLink"]["description"] def save(self, clean=True): ret = super(WebLink, self).save(clean) - ret['webLink'] = { - 'title': self._title, - 'url': self._url, - 'imageUrl': self._image_url, - 'provenanceUrl': self._provenance_url, - 'description': self._description, + ret["webLink"] = { + "title": self._title, + "url": self._url, + "imageUrl": self._image_url, + "provenanceUrl": self._provenance_url, + "description": self._description, } return ret @@ -374,21 +405,21 @@ def description(self, value): self._description = value self._dirty = True + class Category(Annotation): """Represents a category annotation on a :class:`TopLevelNode`.""" + def __init__(self): super(Category, self).__init__() self._category = None def _load(self, raw): super(Category, self)._load(raw) - self._category = CategoryValue(raw['topicCategory']['category']) + self._category = CategoryValue(raw["topicCategory"]["category"]) def save(self, clean=True): ret = super(Category, self).save(clean) - ret['topicCategory'] = { - 'category': self._category.value - } + ret["topicCategory"] = {"category": self._category.value} return ret @property @@ -405,21 +436,21 @@ def category(self, value): self._category = value self._dirty = True + class TaskAssist(Annotation): """Unknown.""" + def __init__(self): super(TaskAssist, self).__init__() self._suggest = None def _load(self, raw): super(TaskAssist, self)._load(raw) - self._suggest = raw['taskAssist']['suggestType'] + self._suggest = raw["taskAssist"]["suggestType"] def save(self, clean=True): ret = super(TaskAssist, self).save(clean) - ret['taskAssist'] = { - 'suggestType': self._suggest - } + ret["taskAssist"] = {"suggestType": self._suggest} return ret @property @@ -436,8 +467,10 @@ def suggest(self, value): self._suggest = value self._dirty = True + class Context(Annotation): """Represents a context annotation, which may contain other annotations.""" + def __init__(self): super(Context, self).__init__() self._entries = {} @@ -445,7 +478,7 @@ def __init__(self): def _load(self, raw): super(Context, self)._load(raw) self._entries = {} - for key, entry in raw.get('context', {}).items(): + for key, entry in raw.get("context", {}).items(): self._entries[key] = NodeAnnotations.from_json({key: entry}) def save(self, clean=True): @@ -453,7 +486,7 @@ def save(self, clean=True): context = {} for entry in self._entries.values(): context.update(entry.save(clean)) - ret['context'] = context + ret["context"] = context return ret def all(self): @@ -466,10 +499,14 @@ def all(self): @property def dirty(self): - return super(Context, self).dirty or any((annotation.dirty for annotation in self._entries.values())) + return super(Context, self).dirty or any( + (annotation.dirty for annotation in self._entries.values()) + ) + class NodeAnnotations(Element): """Represents the annotation container on a :class:`TopLevelNode`.""" + def __init__(self): super(NodeAnnotations, self).__init__() self._annotations = {} @@ -488,17 +525,17 @@ def from_json(cls, raw): Node: An Annotation object or None. """ bcls = None - if 'webLink' in raw: + if "webLink" in raw: bcls = WebLink - elif 'topicCategory' in raw: + elif "topicCategory" in raw: bcls = Category - elif 'taskAssist' in raw: + elif "taskAssist" in raw: bcls = TaskAssist - elif 'context' in raw: + elif "context" in raw: bcls = Context if bcls is None: - logger.warning('Unknown annotation type: %s', raw.keys()) + logger.warning("Unknown annotation type: %s", raw.keys()) return None annotation = bcls() annotation.load(raw) @@ -516,18 +553,20 @@ def all(self): def _load(self, raw): super(NodeAnnotations, self)._load(raw) self._annotations = {} - if 'annotations' not in raw: + if "annotations" not in raw: return - for raw_annotation in raw['annotations']: + for raw_annotation in raw["annotations"]: annotation = self.from_json(raw_annotation) self._annotations[annotation.id] = annotation def save(self, clean=True): ret = super(NodeAnnotations, self).save(clean) - ret['kind'] = 'notes#annotationsGroup' + ret["kind"] = "notes#annotationsGroup" if self._annotations: - ret['annotations'] = [annotation.save(clean) for annotation in self._annotations.values()] + ret["annotations"] = [ + annotation.save(clean) for annotation in self._annotations.values() + ] return ret def _get_category_node(self): @@ -568,7 +607,9 @@ def links(self): Returns: list[gkeepapi.node.WebLink]: A list of links. """ - return [annotation for annotation in self._annotations.values() + return [ + annotation + for annotation in self._annotations.values() if isinstance(annotation, WebLink) ] @@ -597,11 +638,15 @@ def remove(self, annotation): @property def dirty(self): - return super(NodeAnnotations, self).dirty or any((annotation.dirty for annotation in self._annotations.values())) + return super(NodeAnnotations, self).dirty or any( + (annotation.dirty for annotation in self._annotations.values()) + ) + class NodeTimestamps(Element): """Represents the timestamps associated with a :class:`TopLevelNode`.""" - TZ_FMT = '%Y-%m-%dT%H:%M:%S.%fZ' + + TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" def __init__(self, create_time=None): super(NodeTimestamps, self).__init__() @@ -616,27 +661,26 @@ def __init__(self, create_time=None): def _load(self, raw): super(NodeTimestamps, self)._load(raw) - if 'created' in raw: - self._created = self.str_to_dt(raw['created']) - self._deleted = self.str_to_dt(raw['deleted']) \ - if 'deleted' in raw else None - self._trashed = self.str_to_dt(raw['trashed']) \ - if 'trashed' in raw else None - self._updated = self.str_to_dt(raw['updated']) - self._edited = self.str_to_dt(raw['userEdited']) \ - if 'userEdited' in raw else None + if "created" in raw: + self._created = self.str_to_dt(raw["created"]) + self._deleted = self.str_to_dt(raw["deleted"]) if "deleted" in raw else None + self._trashed = self.str_to_dt(raw["trashed"]) if "trashed" in raw else None + self._updated = self.str_to_dt(raw["updated"]) + self._edited = ( + self.str_to_dt(raw["userEdited"]) if "userEdited" in raw else None + ) def save(self, clean=True): ret = super(NodeTimestamps, self).save(clean) - ret['kind'] = 'notes#timestamps' - ret['created'] = self.dt_to_str(self._created) + ret["kind"] = "notes#timestamps" + ret["created"] = self.dt_to_str(self._created) if self._deleted is not None: - ret['deleted'] = self.dt_to_str(self._deleted) + ret["deleted"] = self.dt_to_str(self._deleted) if self._trashed is not None: - ret['trashed'] = self.dt_to_str(self._trashed) - ret['updated'] = self.dt_to_str(self._updated) + ret["trashed"] = self.dt_to_str(self._trashed) + ret["updated"] = self.dt_to_str(self._updated) if self._edited is not None: - ret['userEdited'] = self.dt_to_str(self._edited) + ret["userEdited"] = self.dt_to_str(self._edited) return ret @classmethod @@ -751,8 +795,10 @@ def edited(self, value): self._edited = value self._dirty = True + class NodeSettings(Element): """Represents the settings associated with a :class:`TopLevelNode`.""" + def __init__(self): super(NodeSettings, self).__init__() self._new_listitem_placement = NewListItemPlacementValue.Bottom @@ -761,15 +807,19 @@ def __init__(self): def _load(self, raw): super(NodeSettings, self)._load(raw) - self._new_listitem_placement = NewListItemPlacementValue(raw['newListItemPlacement']) - self._graveyard_state = GraveyardStateValue(raw['graveyardState']) - self._checked_listitems_policy = CheckedListItemsPolicyValue(raw['checkedListItemsPolicy']) + self._new_listitem_placement = NewListItemPlacementValue( + raw["newListItemPlacement"] + ) + self._graveyard_state = GraveyardStateValue(raw["graveyardState"]) + self._checked_listitems_policy = CheckedListItemsPolicyValue( + raw["checkedListItemsPolicy"] + ) def save(self, clean=True): ret = super(NodeSettings, self).save(clean) - ret['newListItemPlacement'] = self._new_listitem_placement.value - ret['graveyardState'] = self._graveyard_state.value - ret['checkedListItemsPolicy'] = self._checked_listitems_policy.value + ret["newListItemPlacement"] = self._new_listitem_placement.value + ret["graveyardState"] = self._graveyard_state.value + ret["checkedListItemsPolicy"] = self._checked_listitems_policy.value return ret @property @@ -814,8 +864,10 @@ def checked_listitems_policy(self, value): self._checked_listitems_policy = value self._dirty = True + class NodeCollaborators(Element): """Represents the collaborators on a :class:`TopLevelNode`.""" + def __init__(self): super(NodeCollaborators, self).__init__() self._collaborators = {} @@ -823,7 +875,7 @@ def __init__(self): def __len__(self): return len(self._collaborators) - def load(self, collaborators_raw, requests_raw): # pylint: disable=arguments-differ + def load(self, collaborators_raw, requests_raw): # pylint: disable=arguments-differ # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() @@ -831,9 +883,11 @@ def load(self, collaborators_raw, requests_raw): # pylint: disable=arguments-dif self._dirty = False self._collaborators = {} for collaborator in collaborators_raw: - self._collaborators[collaborator['email']] = RoleValue(collaborator['role']) + self._collaborators[collaborator["email"]] = RoleValue(collaborator["role"]) for collaborator in requests_raw: - self._collaborators[collaborator['email']] = ShareRequestValue(collaborator['type']) + self._collaborators[collaborator["email"]] = ShareRequestValue( + collaborator["type"] + ) def save(self, clean=True): # Parent method not called. @@ -841,9 +895,11 @@ def save(self, clean=True): requests = [] for email, action in self._collaborators.items(): if isinstance(action, ShareRequestValue): - requests.append({'email': email, 'type': action.value}) + requests.append({"email": email, "type": action.value}) else: - collaborators.append({'email': email, 'role': action.value, 'auxiliary_type': 'None'}) + collaborators.append( + {"email": email, "role": action.value, "auxiliary_type": "None"} + ) if not clean: requests.append(self._dirty) else: @@ -879,10 +935,16 @@ def all(self): Returns: List[str]: Collaborators. """ - return [email for email, action in self._collaborators.items() if action in [RoleValue.Owner, RoleValue.User, ShareRequestValue.Add]] + return [ + email + for email, action in self._collaborators.items() + if action in [RoleValue.Owner, RoleValue.User, ShareRequestValue.Add] + ] + class NodeLabels(Element): """Represents the labels on a :class:`TopLevelNode`.""" + def __init__(self): super(NodeLabels, self).__init__() self._labels = {} @@ -898,13 +960,19 @@ def _load(self, raw): self._dirty = False self._labels = {} for raw_label in raw: - self._labels[raw_label['labelId']] = None + self._labels[raw_label["labelId"]] = None def save(self, clean=True): # Parent method not called. ret = [ - {'labelId': label_id, 'deleted': NodeTimestamps.dt_to_str(datetime.datetime.utcnow()) if label is None else NodeTimestamps.int_to_str(0)} - for label_id, label in self._labels.items()] + { + "labelId": label_id, + "deleted": NodeTimestamps.dt_to_str(datetime.datetime.utcnow()) + if label is None + else NodeTimestamps.int_to_str(0), + } + for label_id, label in self._labels.items() + ] if not clean: ret.append(self._dirty) else: @@ -946,8 +1014,10 @@ def all(self): """ return [label for _, label in self._labels.items() if label is not None] + class TimestampsMixin(object): """A mixin to add methods for updating timestamps.""" + def touch(self, edited=False): """Mark the node as dirty. @@ -967,7 +1037,10 @@ def trashed(self): Returns: bool: Whether this item is trashed. """ - return self.timestamps.trashed is not None and self.timestamps.trashed > NodeTimestamps.int_to_dt(0) + return ( + self.timestamps.trashed is not None + and self.timestamps.trashed > NodeTimestamps.int_to_dt(0) + ) def trash(self): """Mark the item as trashed.""" @@ -984,7 +1057,10 @@ def deleted(self): Returns: bool: Whether this item is deleted. """ - return self.timestamps.deleted is not None and self.timestamps.deleted > NodeTimestamps.int_to_dt(0) + return ( + self.timestamps.deleted is not None + and self.timestamps.deleted > NodeTimestamps.int_to_dt(0) + ) def delete(self): """Mark the item as deleted.""" @@ -994,8 +1070,10 @@ def undelete(self): """Mark the item as undeleted.""" self.timestamps.deleted = None + class Node(Element, TimestampsMixin): """Node base class.""" + def __init__(self, id_=None, type_=None, parent_id=None): super(Node, self).__init__() @@ -1008,7 +1086,7 @@ def __init__(self, id_=None, type_=None, parent_id=None): self.type = type_ self._sort = random.randint(1000000000, 9999999999) self._version = None - self._text = '' + self._text = "" self._children = {} self.timestamps = NodeTimestamps(create_time) self.settings = NodeSettings() @@ -1019,46 +1097,46 @@ def __init__(self, id_=None, type_=None, parent_id=None): @classmethod def _generateId(cls, tz): - return '%x.%016x' % ( + return "%x.%016x" % ( int(tz * 1000), - random.randint(0x0000000000000000, 0xffffffffffffffff) + random.randint(0x0000000000000000, 0xFFFFFFFFFFFFFFFF), ) def _load(self, raw): super(Node, self)._load(raw) # Verify this is a valid type - NodeType(raw['type']) - if raw['kind'] not in ['notes#node']: - logger.warning('Unknown node kind: %s', raw['kind']) + NodeType(raw["type"]) + if raw["kind"] not in ["notes#node"]: + logger.warning("Unknown node kind: %s", raw["kind"]) - if 'mergeConflict' in raw: + if "mergeConflict" in raw: raise exception.MergeException(raw) - self.id = raw['id'] - self.server_id = raw['serverId'] if 'serverId' in raw else self.server_id - self.parent_id = raw['parentId'] - self._sort = raw['sortValue'] if 'sortValue' in raw else self.sort - self._version = raw['baseVersion'] if 'baseVersion' in raw else self._version - self._text = raw['text'] if 'text' in raw else self._text - self.timestamps.load(raw['timestamps']) - self.settings.load(raw['nodeSettings']) - self.annotations.load(raw['annotationsGroup']) + self.id = raw["id"] + self.server_id = raw["serverId"] if "serverId" in raw else self.server_id + self.parent_id = raw["parentId"] + self._sort = raw["sortValue"] if "sortValue" in raw else self.sort + self._version = raw["baseVersion"] if "baseVersion" in raw else self._version + self._text = raw["text"] if "text" in raw else self._text + self.timestamps.load(raw["timestamps"]) + self.settings.load(raw["nodeSettings"]) + self.annotations.load(raw["annotationsGroup"]) def save(self, clean=True): ret = super(Node, self).save(clean) - ret['id'] = self.id - ret['kind'] = 'notes#node' - ret['type'] = self.type.value - ret['parentId'] = self.parent_id - ret['sortValue'] = self._sort + ret["id"] = self.id + ret["kind"] = "notes#node" + ret["type"] = self.type.value + ret["parentId"] = self.parent_id + ret["sortValue"] = self._sort if not self._moved and self._version is not None: - ret['baseVersion'] = self._version - ret['text'] = self._text + ret["baseVersion"] = self._version + ret["text"] = self._text if self.server_id is not None: - ret['serverId'] = self.server_id - ret['timestamps'] = self.timestamps.save(clean) - ret['nodeSettings'] = self.settings.save(clean) - ret['annotationsGroup'] = self.annotations.save(clean) + ret["serverId"] = self.server_id + ret["timestamps"] = self.timestamps.save(clean) + ret["nodeSettings"] = self.settings.save(clean) + ret["annotationsGroup"] = self.annotations.save(clean) return ret @property @@ -1162,11 +1240,20 @@ def new(self): @property def dirty(self): - return super(Node, self).dirty or self.timestamps.dirty or self.annotations.dirty or self.settings.dirty or any((node.dirty for node in self.children)) + return ( + super(Node, self).dirty + or self.timestamps.dirty + or self.annotations.dirty + or self.settings.dirty + or any((node.dirty for node in self.children)) + ) + class Root(Node): """Internal root node.""" - ID = 'root' + + ID = "root" + def __init__(self): super(Root, self).__init__(id_=self.ID) @@ -1174,46 +1261,49 @@ def __init__(self): def dirty(self): return False + class TopLevelNode(Node): """Top level node base class.""" + _TYPE = None + def __init__(self, **kwargs): super(TopLevelNode, self).__init__(parent_id=Root.ID, **kwargs) self._color = ColorValue.White self._archived = False self._pinned = False - self._title = '' + self._title = "" self.labels = NodeLabels() self.collaborators = NodeCollaborators() def _load(self, raw): super(TopLevelNode, self)._load(raw) - self._color = ColorValue(raw['color']) if 'color' in raw else ColorValue.White - self._archived = raw['isArchived'] if 'isArchived' in raw else False - self._pinned = raw['isPinned'] if 'isPinned' in raw else False - self._title = raw['title'] if 'title' in raw else '' - self.labels.load(raw['labelIds'] if 'labelIds' in raw else []) + self._color = ColorValue(raw["color"]) if "color" in raw else ColorValue.White + self._archived = raw["isArchived"] if "isArchived" in raw else False + self._pinned = raw["isPinned"] if "isPinned" in raw else False + self._title = raw["title"] if "title" in raw else "" + self.labels.load(raw["labelIds"] if "labelIds" in raw else []) self.collaborators.load( - raw['roleInfo'] if 'roleInfo' in raw else [], - raw['shareRequests'] if 'shareRequests' in raw else [], + raw["roleInfo"] if "roleInfo" in raw else [], + raw["shareRequests"] if "shareRequests" in raw else [], ) - self._moved = 'moved' in raw + self._moved = "moved" in raw def save(self, clean=True): ret = super(TopLevelNode, self).save(clean) - ret['color'] = self._color.value - ret['isArchived'] = self._archived - ret['isPinned'] = self._pinned - ret['title'] = self._title + ret["color"] = self._color.value + ret["isArchived"] = self._archived + ret["isPinned"] = self._pinned + ret["title"] = self._title labels = self.labels.save(clean) collaborators, requests = self.collaborators.save(clean) if labels: - ret['labelIds'] = labels - ret['collaborators'] = collaborators + ret["labelIds"] = labels + ret["collaborators"] = collaborators if requests: - ret['shareRequests'] = requests + ret["shareRequests"] = requests return ret @property @@ -1279,11 +1369,15 @@ def url(self): Returns: str: Google Keep url. """ - return 'https://keep.google.com/u/0/#' + self._TYPE.value + '/' + self.id + return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @property def dirty(self): - return super(TopLevelNode, self).dirty or self.labels.dirty or self.collaborators.dirty + return ( + super(TopLevelNode, self).dirty + or self.labels.dirty + or self.collaborators.dirty + ) @property def blobs(self): @@ -1306,9 +1400,12 @@ def drawings(self): def audio(self): return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] + class Note(TopLevelNode): """Represents a Google Keep note.""" + _TYPE = NodeType.Note + def __init__(self, **kwargs): super(Note, self).__init__(type_=self._TYPE, **kwargs) @@ -1339,12 +1436,15 @@ def text(self, value): self.touch(True) def __str__(self): - return '\n'.join([self.title, self.text]) + return "\n".join([self.title, self.text]) + class List(TopLevelNode): """Represents a Google Keep list.""" + _TYPE = NodeType.List - SORT_DELTA = 10000 # Arbitrary constant + SORT_DELTA = 10000 # Arbitrary constant + def __init__(self, **kwargs): super(List, self).__init__(type_=self._TYPE, **kwargs) @@ -1378,7 +1478,7 @@ def add(self, text, checked=False, sort=None): @property def text(self): - return '\n'.join((str(node) for node in self.items)) + return "\n".join((str(node) for node in self.items)) @classmethod def sorted_items(cls, items): @@ -1389,8 +1489,10 @@ def sorted_items(cls, items): Returns: list[gkeepapi.node.ListItem]: Sorted items. """ + class t(tuple): """Tuple with element-based sorting""" + def __cmp__(self, other): for a, b in itertools.zip_longest(self, other): if a != b: @@ -1401,35 +1503,41 @@ def __cmp__(self, other): return a - b return 0 - def __lt__(self, other): # pragma: no cover + def __lt__(self, other): # pragma: no cover return self.__cmp__(other) < 0 - def __gt_(self, other): # pragma: no cover + + def __gt_(self, other): # pragma: no cover return self.__cmp__(other) > 0 - def __le__(self, other): # pragma: no cover + + def __le__(self, other): # pragma: no cover return self.__cmp__(other) <= 0 - def __ge_(self, other): # pragma: no cover + + def __ge_(self, other): # pragma: no cover return self.__cmp__(other) >= 0 - def __eq__(self, other): # pragma: no cover + + def __eq__(self, other): # pragma: no cover return self.__cmp__(other) == 0 - def __ne__(self, other): # pragma: no cover + + def __ne__(self, other): # pragma: no cover return self.__cmp__(other) != 0 def key_func(x): if x.indented: return t((int(x.parent_item.sort), int(x.sort))) - return t((int(x.sort), )) + return t((int(x.sort),)) return sorted(items, key=key_func, reverse=True) def _items(self, checked=None): return [ - node for node in self.children - if isinstance(node, ListItem) and not node.deleted and ( - checked is None or node.checked == checked - ) + node + for node in self.children + if isinstance(node, ListItem) + and not node.deleted + and (checked is None or node.checked == checked) ] - def sort_items(self, key=attrgetter('text'), reverse=False): + def sort_items(self, key=attrgetter("text"), reverse=False): """Sort list items in place. By default, the items are alphabetized, but a custom function can be specified. @@ -1445,7 +1553,7 @@ def sort_items(self, key=attrgetter('text'), reverse=False): sort_value -= self.SORT_DELTA def __str__(self): - return '\n'.join(([self.title] + [str(node) for node in self.items])) + return "\n".join(([self.title] + [str(node) for node in self.items])) @property def items(self): @@ -1474,13 +1582,19 @@ def unchecked(self): """ return self.sorted_items(self._items(False)) + class ListItem(Node): """Represents a Google Keep listitem. Interestingly enough, :class:`Note`s store their content in a single child :class:`ListItem`. """ - def __init__(self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs): - super(ListItem, self).__init__(type_=NodeType.ListItem, parent_id=parent_id, **kwargs) + + def __init__( + self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs + ): + super(ListItem, self).__init__( + type_=NodeType.ListItem, parent_id=parent_id, **kwargs + ) self.parent_item = None self.parent_server_id = parent_server_id self.super_list_item_id = super_list_item_id @@ -1491,14 +1605,14 @@ def __init__(self, parent_id=None, parent_server_id=None, super_list_item_id=Non def _load(self, raw): super(ListItem, self)._load(raw) self.prev_super_list_item_id = self.super_list_item_id - self.super_list_item_id = raw.get('superListItemId') or None - self._checked = raw.get('checked', False) + self.super_list_item_id = raw.get("superListItemId") or None + self._checked = raw.get("checked", False) def save(self, clean=True): ret = super(ListItem, self).save(clean) - ret['parentServerId'] = self.parent_server_id - ret['superListItemId'] = self.super_list_item_id - ret['checked'] = self._checked + ret["parentServerId"] = self.parent_server_id + ret["superListItemId"] = self.super_list_item_id + ret["checked"] = self._checked return ret def add(self, text, checked=False, sort=None): @@ -1510,7 +1624,7 @@ def add(self, text, checked=False, sort=None): sort (int): Item id for sorting. """ if self.parent is None: - raise exception.InvalidException('Item has no parent') + raise exception.InvalidException("Item has no parent") node = self.parent.add(text, checked, sort) self.indent(node) return node @@ -1554,9 +1668,7 @@ def subitems(self): Returns: list[gkeepapi.node.ListItem]: Subitems. """ - return List.sorted_items( - self._subitems.values() - ) + return List.sorted_items(self._subitems.values()) @property def indented(self): @@ -1582,57 +1694,63 @@ def checked(self, value): self.touch(True) def __str__(self): - return '%s%s %s' % ( - ' ' if self.indented else '', - '☑' if self.checked else '☐', - self.text + return "%s%s %s" % ( + " " if self.indented else "", + "☑" if self.checked else "☐", + self.text, ) + class NodeBlob(Element): """Represents a blob descriptor.""" + _TYPE = None + def __init__(self, type_=None): super(NodeBlob, self).__init__() self.blob_id = None self.type = type_ self._media_id = None - self._mimetype = '' + self._mimetype = "" self._is_uploaded = False def _load(self, raw): super(NodeBlob, self)._load(raw) # Verify this is a valid type - BlobType(raw['type']) - self.blob_id = raw.get('blob_id') - self._media_id = raw.get('media_id') - self._mimetype = raw.get('mimetype') + BlobType(raw["type"]) + self.blob_id = raw.get("blob_id") + self._media_id = raw.get("media_id") + self._mimetype = raw.get("mimetype") def save(self, clean=True): ret = super(NodeBlob, self).save(clean) - ret['kind'] = 'notes#blob' - ret['type'] = self.type.value + ret["kind"] = "notes#blob" + ret["type"] = self.type.value if self.blob_id is not None: - ret['blob_id'] = self.blob_id + ret["blob_id"] = self.blob_id if self._media_id is not None: - ret['media_id'] = self._media_id - ret['mimetype'] = self._mimetype + ret["media_id"] = self._media_id + ret["mimetype"] = self._mimetype return ret + class NodeAudio(NodeBlob): """Represents an audio blob.""" + _TYPE = BlobType.Audio + def __init__(self): super(NodeAudio, self).__init__(type_=self._TYPE) self._length = None def _load(self, raw): super(NodeAudio, self)._load(raw) - self._length = raw.get('length') + self._length = raw.get("length") def save(self, clean=True): ret = super(NodeAudio, self).save(clean) if self._length is not None: - ret['length'] = self._length + ret["length"] = self._length return ret @property @@ -1643,34 +1761,37 @@ def length(self): """ return self._length + class NodeImage(NodeBlob): """Represents an image blob.""" + _TYPE = BlobType.Image + def __init__(self): super(NodeImage, self).__init__(type_=self._TYPE) self._is_uploaded = False self._width = 0 self._height = 0 self._byte_size = 0 - self._extracted_text = '' - self._extraction_status = '' + self._extracted_text = "" + self._extraction_status = "" def _load(self, raw): super(NodeImage, self)._load(raw) - self._is_uploaded = raw.get('is_uploaded') or False - self._width = raw.get('width') - self._height = raw.get('height') - self._byte_size = raw.get('byte_size') - self._extracted_text = raw.get('extracted_text') - self._extraction_status = raw.get('extraction_status') + self._is_uploaded = raw.get("is_uploaded") or False + self._width = raw.get("width") + self._height = raw.get("height") + self._byte_size = raw.get("byte_size") + self._extracted_text = raw.get("extracted_text") + self._extraction_status = raw.get("extraction_status") def save(self, clean=True): ret = super(NodeImage, self).save(clean) - ret['width'] = self._width - ret['height'] = self._height - ret['byte_size'] = self._byte_size - ret['extracted_text'] = self._extracted_text - ret['extraction_status'] = self._extraction_status + ret["width"] = self._width + ret["height"] = self._height + ret["byte_size"] = self._byte_size + ret["extracted_text"] = self._extracted_text + ret["extraction_status"] = self._extraction_status return ret @property @@ -1713,31 +1834,34 @@ def url(self): """ raise NotImplementedError() + class NodeDrawing(NodeBlob): """Represents a drawing blob.""" + _TYPE = BlobType.Drawing + def __init__(self): super(NodeDrawing, self).__init__(type_=self._TYPE) - self._extracted_text = '' - self._extraction_status = '' + self._extracted_text = "" + self._extraction_status = "" self._drawing_info = None def _load(self, raw): super(NodeDrawing, self)._load(raw) - self._extracted_text = raw.get('extracted_text') - self._extraction_status = raw.get('extraction_status') + self._extracted_text = raw.get("extracted_text") + self._extraction_status = raw.get("extraction_status") drawing_info = None - if 'drawingInfo' in raw: + if "drawingInfo" in raw: drawing_info = NodeDrawingInfo() - drawing_info.load(raw['drawingInfo']) + drawing_info.load(raw["drawingInfo"]) self._drawing_info = drawing_info def save(self, clean=True): ret = super(NodeDrawing, self).save(clean) - ret['extracted_text'] = self._extracted_text - ret['extraction_status'] = self._extraction_status + ret["extracted_text"] = self._extracted_text + ret["extraction_status"] = self._extraction_status if self._drawing_info is not None: - ret['drawingInfo'] = self._drawing_info.save(clean) + ret["drawingInfo"] = self._drawing_info.save(clean) return ret @property @@ -1746,41 +1870,62 @@ def extracted_text(self): Returns: str: Extracted text. """ - return self._drawing_info.snapshot.extracted_text \ - if self._drawing_info is not None else '' + return ( + self._drawing_info.snapshot.extracted_text + if self._drawing_info is not None + else "" + ) + class NodeDrawingInfo(Element): """Represents information about a drawing blob.""" + def __init__(self): super(NodeDrawingInfo, self).__init__() - self.drawing_id = '' + self.drawing_id = "" self.snapshot = NodeImage() - self._snapshot_fingerprint = '' + self._snapshot_fingerprint = "" self._thumbnail_generated_time = NodeTimestamps.int_to_dt(0) - self._ink_hash = '' - self._snapshot_proto_fprint = '' + self._ink_hash = "" + self._snapshot_proto_fprint = "" def _load(self, raw): super(NodeDrawingInfo, self)._load(raw) - self.drawing_id = raw['drawingId'] - self.snapshot.load(raw['snapshotData']) - self._snapshot_fingerprint = raw['snapshotFingerprint'] if 'snapshotFingerprint' in raw else self._snapshot_fingerprint - self._thumbnail_generated_time = NodeTimestamps.str_to_dt(raw['thumbnailGeneratedTime']) if 'thumbnailGeneratedTime' in raw else NodeTimestamps.int_to_dt(0) - self._ink_hash = raw['inkHash'] if 'inkHash' in raw else '' - self._snapshot_proto_fprint = raw['snapshotProtoFprint'] if 'snapshotProtoFprint' in raw else self._snapshot_proto_fprint + self.drawing_id = raw["drawingId"] + self.snapshot.load(raw["snapshotData"]) + self._snapshot_fingerprint = ( + raw["snapshotFingerprint"] + if "snapshotFingerprint" in raw + else self._snapshot_fingerprint + ) + self._thumbnail_generated_time = ( + NodeTimestamps.str_to_dt(raw["thumbnailGeneratedTime"]) + if "thumbnailGeneratedTime" in raw + else NodeTimestamps.int_to_dt(0) + ) + self._ink_hash = raw["inkHash"] if "inkHash" in raw else "" + self._snapshot_proto_fprint = ( + raw["snapshotProtoFprint"] + if "snapshotProtoFprint" in raw + else self._snapshot_proto_fprint + ) def save(self, clean=True): ret = super(NodeDrawingInfo, self).save(clean) - ret['drawingId'] = self.drawing_id - ret['snapshotData'] = self.snapshot.save(clean) - ret['snapshotFingerprint'] = self._snapshot_fingerprint - ret['thumbnailGeneratedTime'] = NodeTimestamps.dt_to_str(self._thumbnail_generated_time) - ret['inkHash'] = self._ink_hash - ret['snapshotProtoFprint'] = self._snapshot_proto_fprint + ret["drawingId"] = self.drawing_id + ret["snapshotData"] = self.snapshot.save(clean) + ret["snapshotFingerprint"] = self._snapshot_fingerprint + ret["thumbnailGeneratedTime"] = NodeTimestamps.dt_to_str( + self._thumbnail_generated_time + ) + ret["inkHash"] = self._ink_hash + ret["snapshotProtoFprint"] = self._snapshot_proto_fprint return ret + class Blob(Node): """Represents a Google Keep blob.""" + _blob_type_map = { BlobType.Audio: NodeAudio, BlobType.Image: NodeImage, @@ -1804,7 +1949,7 @@ def from_json(cls, raw): if raw is None: return None - _type = raw.get('type') + _type = raw.get("type") if _type is None: return None @@ -1812,9 +1957,9 @@ def from_json(cls, raw): try: bcls = cls._blob_type_map[BlobType(_type)] except (KeyError, ValueError) as e: - logger.warning('Unknown blob type: %s', _type) - if DEBUG: # pragma: no cover - raise exception.ParseException(f'Parse error for {_type}', raw) from e + logger.warning("Unknown blob type: %s", _type) + if DEBUG: # pragma: no cover + raise exception.ParseException(f"Parse error for {_type}", raw) from e return None blob = bcls() blob.load(raw) @@ -1823,46 +1968,57 @@ def from_json(cls, raw): def _load(self, raw): super(Blob, self)._load(raw) - self.blob = self.from_json(raw.get('blob')) + self.blob = self.from_json(raw.get("blob")) def save(self, clean=True): ret = super(Blob, self).save(clean) if self.blob is not None: - ret['blob'] = self.blob.save(clean) + ret["blob"] = self.blob.save(clean) return ret + class Label(Element, TimestampsMixin): """Represents a label.""" + def __init__(self): super(Label, self).__init__() create_time = time.time() self.id = self._generateId(create_time) - self._name = '' + self._name = "" self.timestamps = NodeTimestamps(create_time) self._merged = NodeTimestamps.int_to_dt(0) @classmethod def _generateId(cls, tz): - return 'tag.%s.%x' % ( - ''.join([random.choice('abcdefghijklmnopqrstuvwxyz0123456789') for _ in range(12)]), - int(tz * 1000) + return "tag.%s.%x" % ( + "".join( + [ + random.choice("abcdefghijklmnopqrstuvwxyz0123456789") + for _ in range(12) + ] + ), + int(tz * 1000), ) def _load(self, raw): super(Label, self)._load(raw) - self.id = raw['mainId'] - self._name = raw['name'] - self.timestamps.load(raw['timestamps']) - self._merged = NodeTimestamps.str_to_dt(raw['lastMerged']) if 'lastMerged' in raw else NodeTimestamps.int_to_dt(0) + self.id = raw["mainId"] + self._name = raw["name"] + self.timestamps.load(raw["timestamps"]) + self._merged = ( + NodeTimestamps.str_to_dt(raw["lastMerged"]) + if "lastMerged" in raw + else NodeTimestamps.int_to_dt(0) + ) def save(self, clean=True): ret = super(Label, self).save(clean) - ret['mainId'] = self.id - ret['name'] = self._name - ret['timestamps'] = self.timestamps.save(clean) - ret['lastMerged'] = NodeTimestamps.dt_to_str(self._merged) + ret["mainId"] = self.id + ret["name"] = self._name + ret["timestamps"] = self.timestamps.save(clean) + ret["lastMerged"] = NodeTimestamps.dt_to_str(self._merged) return ret @property @@ -1900,6 +2056,7 @@ def dirty(self): def __str__(self): return self.name + _type_map = { NodeType.Note: Note, NodeType.List: List, @@ -1907,6 +2064,7 @@ def __str__(self): NodeType.Blob: Blob, } + def from_json(raw): """Helper to construct a node from a dict. @@ -1917,22 +2075,25 @@ def from_json(raw): Node: A Node object or None. """ ncls = None - _type = raw.get('type') + _type = raw.get("type") try: ncls = _type_map[NodeType(_type)] except (KeyError, ValueError) as e: - logger.warning('Unknown node type: %s', _type) - if DEBUG: # pragma: no cover - raise exception.ParseException(f'Parse error for {_type}', raw) from e + logger.warning("Unknown node type: %s", _type) + if DEBUG: # pragma: no cover + raise exception.ParseException(f"Parse error for {_type}", raw) from e return None node = ncls() node.load(raw) return node -if DEBUG: # pragma: no cover - Node.__load = Node._load # pylint: disable=protected-access - def _load(self, raw): # pylint: disable=missing-docstring - self.__load(raw) # pylint: disable=protected-access - self._find_discrepancies(raw) # pylint: disable=protected-access - Node._load = _load # pylint: disable=protected-access + +if DEBUG: # pragma: no cover + Node.__load = Node._load # pylint: disable=protected-access + + def _load(self, raw): # pylint: disable=missing-docstring + self.__load(raw) # pylint: disable=protected-access + self._find_discrepancies(raw) # pylint: disable=protected-access + + Node._load = _load # pylint: disable=protected-access From 0c9ddb3c54fabca7382b2ec43989d663fa0c5a7a Mon Sep 17 00:00:00 2001 From: K Date: Fri, 14 Oct 2022 12:15:55 -0400 Subject: [PATCH 17/56] Add addn typehints --- gkeepapi/node.py | 245 ++++++++++++++++++++++++----------------------- 1 file changed, 125 insertions(+), 120 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index a2d575a..23f4819 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -176,7 +176,7 @@ class RoleValue(enum.Enum): """Note collaborator.""" -class Element(object): +class Element: """Interface for elements that can be serialized and deserialized.""" def __init__(self): @@ -224,11 +224,11 @@ def _find_discrepancies(self, raw): # pragma: no cover len(s_raw), ) - def load(self, raw): + def load(self, raw: Dict): """Unserialize from raw representation. (Wrapper) Args: - raw (dict): Raw. + raw: Raw. Raises: ParseException: If there was an error parsing data. """ @@ -237,22 +237,22 @@ def load(self, raw): except (KeyError, ValueError) as e: raise exception.ParseException(f"Parse error in {type(self)}", raw) from e - def _load(self, raw): + def _load(self, raw: Dict): """Unserialize from raw representation. (Implementation logic) Args: - raw (dict): Raw. + raw: Raw. """ self._dirty = raw.get("_dirty", False) - def save(self, clean=True): + def save(self, clean=True) -> Dict: """Serialize into raw representation. Clears the dirty bit by default. Args: - clean (bool): Whether to clear the dirty bit. + clean: Whether to clear the dirty bit. Returns: - dict: Raw. + Raw. """ ret = {} if clean: @@ -262,11 +262,11 @@ def save(self, clean=True): return ret @property - def dirty(self): + def dirty(self) -> bool: """Get dirty state. Returns: - str: Whether this element is dirty. + Whether this element is dirty. """ return self._dirty @@ -278,11 +278,11 @@ def __init__(self): super(Annotation, self).__init__() self.id = self._generateAnnotationId() - def _load(self, raw): + def _load(self, raw: Dict): super(Annotation, self)._load(raw) self.id = raw.get("id") - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = {} if self.id is not None: ret = super(Annotation, self).save(clean) @@ -291,7 +291,7 @@ def save(self, clean=True): return ret @classmethod - def _generateAnnotationId(cls): + def _generateAnnotationId(cls) -> str: return "%08x-%04x-%04x-%04x-%012x" % ( random.randint(0x00000000, 0xFFFFFFFF), random.randint(0x0000, 0xFFFF), @@ -312,7 +312,7 @@ def __init__(self): self._provenance_url = "" self._description = "" - def _load(self, raw): + def _load(self, raw: Dict): super(WebLink, self)._load(raw) self._title = raw["webLink"]["title"] self._url = raw["webLink"]["url"] @@ -324,7 +324,7 @@ def _load(self, raw): self._provenance_url = raw["webLink"]["provenanceUrl"] self._description = raw["webLink"]["description"] - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(WebLink, self).save(clean) ret["webLink"] = { "title": self._title, @@ -336,72 +336,72 @@ def save(self, clean=True): return ret @property - def title(self): + def title(self) -> str: """Get the link title. Returns: - str: The link title. + The link title. """ return self._title @title.setter - def title(self, value): + def title(self, value: str) -> None: self._title = value self._dirty = True @property - def url(self): + def url(self) -> str: """Get the link url. Returns: - str: The link url. + The link url. """ return self._url @url.setter - def url(self, value): + def url(self, value: str) -> None: self._url = value self._dirty = True @property - def image_url(self): + def image_url(self) -> str: """Get the link image url. Returns: - str: The image url or None. + The image url or None. """ return self._image_url @image_url.setter - def image_url(self, value): + def image_url(self, value: str) -> None: self._image_url = value self._dirty = True @property - def provenance_url(self): + def provenance_url(self) -> str: """Get the provenance url. Returns: - url: The provenance url. + The provenance url. """ return self._provenance_url @provenance_url.setter - def provenance_url(self, value): + def provenance_url(self, value) -> None: self._provenance_url = value self._dirty = True @property - def description(self): + def description(self) -> str: """Get the link description. Returns: - str: The link description. + The link description. """ return self._description @description.setter - def description(self, value): + def description(self, value: str): self._description = value self._dirty = True @@ -413,26 +413,26 @@ def __init__(self): super(Category, self).__init__() self._category = None - def _load(self, raw): + def _load(self, raw: Dict): super(Category, self)._load(raw) self._category = CategoryValue(raw["topicCategory"]["category"]) - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(Category, self).save(clean) ret["topicCategory"] = {"category": self._category.value} return ret @property - def category(self): + def category(self) -> CategoryValue: """Get the category. Returns: - gkeepapi.node.CategoryValue: The category. + The category. """ return self._category @category.setter - def category(self, value): + def category(self, value: CategoryValue) -> None: self._category = value self._dirty = True @@ -444,26 +444,26 @@ def __init__(self): super(TaskAssist, self).__init__() self._suggest = None - def _load(self, raw): + def _load(self, raw: Dict): super(TaskAssist, self)._load(raw) self._suggest = raw["taskAssist"]["suggestType"] - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(TaskAssist, self).save(clean) ret["taskAssist"] = {"suggestType": self._suggest} return ret @property - def suggest(self): + def suggest(self) -> str: """Get the suggestion. Returns: - str: The suggestion. + The suggestion. """ return self._suggest @suggest.setter - def suggest(self, value): + def suggest(self, value) -> None: self._suggest = value self._dirty = True @@ -475,13 +475,13 @@ def __init__(self): super(Context, self).__init__() self._entries = {} - def _load(self, raw): + def _load(self, raw: Dict): super(Context, self)._load(raw) self._entries = {} for key, entry in raw.get("context", {}).items(): self._entries[key] = NodeAnnotations.from_json({key: entry}) - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(Context, self).save(clean) context = {} for entry in self._entries.values(): @@ -489,16 +489,16 @@ def save(self, clean=True): ret["context"] = context return ret - def all(self): + def all(self) -> Iterator[Annotation]: """Get all sub annotations. Returns: - List[gkeepapi.node.Annotation]: Sub Annotations. + Sub Annotations. """ return self._entries.values() @property - def dirty(self): + def dirty(self) -> bool: return super(Context, self).dirty or any( (annotation.dirty for annotation in self._entries.values()) ) @@ -515,11 +515,11 @@ def __len__(self): return len(self._annotations) @classmethod - def from_json(cls, raw): + def from_json(cls, raw: Dict) -> Optional[Annotation]: """Helper to construct an annotation from a dict. Args: - raw (dict): Raw annotation representation. + raw: Raw annotation representation. Returns: Node: An Annotation object or None. @@ -542,15 +542,15 @@ def from_json(cls, raw): return annotation - def all(self): + def all(self) -> Iterator[Annotation]: """Get all annotations. Returns: - List[gkeepapi.node.Annotation]: Annotations. + Annotations. """ return self._annotations.values() - def _load(self, raw): + def _load(self, raw: Dict): super(NodeAnnotations, self)._load(raw) self._annotations = {} if "annotations" not in raw: @@ -560,7 +560,7 @@ def _load(self, raw): annotation = self.from_json(raw_annotation) self._annotations[annotation.id] = annotation - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(NodeAnnotations, self).save(clean) ret["kind"] = "notes#annotationsGroup" if self._annotations: @@ -569,25 +569,25 @@ def save(self, clean=True): ] return ret - def _get_category_node(self): + def _get_category_node(self) -> Optional[Category]: for annotation in self._annotations.values(): if isinstance(annotation, Category): return annotation return None @property - def category(self): + def category(self) -> Optional[CategoryValue]: """Get the category. Returns: - Union[gkeepapi.node.CategoryValue, None]: The category or None. + The category. """ node = self._get_category_node() return node.category if node is not None else None @category.setter - def category(self, value): + def category(self, value) -> None: node = self._get_category_node() if value is None: if node is not None: @@ -601,11 +601,11 @@ def category(self, value): self._dirty = True @property - def links(self): + def links(self) -> List[WebLink]: """Get all links. Returns: - list[gkeepapi.node.WebLink]: A list of links. + A list of links. """ return [ annotation @@ -613,31 +613,31 @@ def links(self): if isinstance(annotation, WebLink) ] - def append(self, annotation): + def append(self, annotation: Annotation) -> Annotation: """Add an annotation. Args: - annotation (gkeepapi.node.Annotation): An Annotation object. + annotation: An Annotation object. Returns: - gkeepapi.node.Annotation: The Annotation. + The Annotation. """ self._annotations[annotation.id] = annotation self._dirty = True return annotation - def remove(self, annotation): + def remove(self, annotation: Annotation) -> None: """Removes an annotation. Args: - annotation (gkeepapi.node.Annotation): An Annotation object. + annotation: An Annotation object. """ if annotation.id in self._annotations: del self._annotations[annotation.id] self._dirty = True @property - def dirty(self): + def dirty(self) -> bool: return super(NodeAnnotations, self).dirty or any( (annotation.dirty for annotation in self._annotations.values()) ) @@ -648,7 +648,7 @@ class NodeTimestamps(Element): TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" - def __init__(self, create_time=None): + def __init__(self, create_time: str = None): super(NodeTimestamps, self).__init__() if create_time is None: create_time = time.time() @@ -659,7 +659,7 @@ def __init__(self, create_time=None): self._updated = self.int_to_dt(create_time) self._edited = self.int_to_dt(create_time) - def _load(self, raw): + def _load(self, raw: Dict): super(NodeTimestamps, self)._load(raw) if "created" in raw: self._created = self.str_to_dt(raw["created"]) @@ -670,7 +670,7 @@ def _load(self, raw): self.str_to_dt(raw["userEdited"]) if "userEdited" in raw else None ) - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(NodeTimestamps, self).save(clean) ret["kind"] = "notes#timestamps" ret["created"] = self.dt_to_str(self._created) @@ -684,114 +684,117 @@ def save(self, clean=True): return ret @classmethod - def str_to_dt(cls, tzs): + def str_to_dt(cls, tzs: str) -> datetime.datetime: """Convert a datetime string into an object. Params: - tsz (str): Datetime string. + tsz: Datetime string. Returns: - datetime.datetime: Datetime. + Datetime. """ return datetime.datetime.strptime(tzs, cls.TZ_FMT) @classmethod - def int_to_dt(cls, tz): + def int_to_dt(cls, tz: int) -> datetime.datetime: """Convert a unix timestamp into an object. Params: - ts (int): Unix timestamp. + ts: Unix timestamp. Returns: - datetime.datetime: Datetime. + Datetime. """ return datetime.datetime.utcfromtimestamp(tz) @classmethod - def dt_to_str(cls, dt): + def dt_to_str(cls, dt: datetime.datetime) -> str: """Convert a datetime to a str. + Params: + dt: Datetime. + Returns: - str: Datetime string. + Datetime string. """ return dt.strftime(cls.TZ_FMT) @classmethod - def int_to_str(cls, tz): + def int_to_str(cls, tz: int) -> str: """Convert a unix timestamp to a str. Returns: - str: Datetime string. + Datetime string. """ return cls.dt_to_str(cls.int_to_dt(tz)) @property - def created(self): + def created(self) -> datetime.datetime: """Get the creation datetime. Returns: - datetime.datetime: Datetime. + Datetime. """ return self._created @created.setter - def created(self, value): + def created(self, value) -> None: self._created = value self._dirty = True @property - def deleted(self): + def deleted(self) -> datetime.datetime: """Get the deletion datetime. Returns: - datetime.datetime: Datetime. + Datetime. """ return self._deleted @deleted.setter - def deleted(self, value): + def deleted(self, value: datetime.datetime) -> None: self._deleted = value self._dirty = True @property - def trashed(self): + def trashed(self) -> datetime.datetime: """Get the move-to-trash datetime. Returns: - datetime.datetime: Datetime. + Datetime. """ return self._trashed @trashed.setter - def trashed(self, value): + def trashed(self, value: datetime.datetime) -> None: self._trashed = value self._dirty = True @property - def updated(self): + def updated(self) -> datetime.datetime: """Get the updated datetime. Returns: - datetime.datetime: Datetime. + Datetime. """ return self._updated @updated.setter - def updated(self, value): + def updated(self, value: datetime.datetime) -> None: self._updated = value self._dirty = True @property - def edited(self): + def edited(self) -> datetime.datetime: """Get the user edited datetime. Returns: - datetime.datetime: Datetime. + Datetime. """ return self._edited @edited.setter - def edited(self, value): + def edited(self, value: datetime.datetime) -> None: self._edited = value self._dirty = True @@ -805,7 +808,7 @@ def __init__(self): self._graveyard_state = GraveyardStateValue.Collapsed self._checked_listitems_policy = CheckedListItemsPolicyValue.Graveyard - def _load(self, raw): + def _load(self, raw: Dict): super(NodeSettings, self)._load(raw) self._new_listitem_placement = NewListItemPlacementValue( raw["newListItemPlacement"] @@ -815,7 +818,7 @@ def _load(self, raw): raw["checkedListItemsPolicy"] ) - def save(self, clean=True): + def save(self, clean=True) -> Dict: ret = super(NodeSettings, self).save(clean) ret["newListItemPlacement"] = self._new_listitem_placement.value ret["graveyardState"] = self._graveyard_state.value @@ -823,44 +826,44 @@ def save(self, clean=True): return ret @property - def new_listitem_placement(self): + def new_listitem_placement(self) -> NewListItemPlacementValue: """Get the default location to insert new listitems. Returns: - gkeepapi.node.NewListItemPlacementValue: Placement. + Placement. """ return self._new_listitem_placement @new_listitem_placement.setter - def new_listitem_placement(self, value): + def new_listitem_placement(self, value: NewListItemPlacementValue) -> None: self._new_listitem_placement = value self._dirty = True @property - def graveyard_state(self): + def graveyard_state(self) -> GraveyardStateValue: """Get the visibility state for the list graveyard. Returns: - gkeepapi.node.GraveyardStateValue: Visibility. + Visibility. """ return self._graveyard_state @graveyard_state.setter - def graveyard_state(self, value): + def graveyard_state(self, value: GraveyardStateValue) -> None: self._graveyard_state = value self._dirty = True @property - def checked_listitems_policy(self): + def checked_listitems_policy(self) -> CheckedListItemsPolicyValue: """Get the policy for checked listitems. Returns: - gkeepapi.node.CheckedListItemsPolicyValue: Policy. + Policy. """ return self._checked_listitems_policy @checked_listitems_policy.setter - def checked_listitems_policy(self, value): + def checked_listitems_policy(self, value: CheckedListItemsPolicyValue) -> None: self._checked_listitems_policy = value self._dirty = True @@ -872,10 +875,12 @@ def __init__(self): super(NodeCollaborators, self).__init__() self._collaborators = {} - def __len__(self): + def __len__(self) -> int: return len(self._collaborators) - def load(self, collaborators_raw, requests_raw): # pylint: disable=arguments-differ + def load( + self, collaborators_raw: List, requests_raw: List + ): # pylint: disable=arguments-differ # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() @@ -889,7 +894,7 @@ def load(self, collaborators_raw, requests_raw): # pylint: disable=arguments-di collaborator["type"] ) - def save(self, clean=True): + def save(self, clean=True) -> Tuple[List, List]: # Parent method not called. collaborators = [] requests = [] @@ -906,21 +911,21 @@ def save(self, clean=True): self._dirty = False return (collaborators, requests) - def add(self, email): + def add(self, email: str) -> None: """Add a collaborator. Args: - str : Collaborator email address. + email: Collaborator email address. """ if email not in self._collaborators: self._collaborators[email] = ShareRequestValue.Add self._dirty = True - def remove(self, email): + def remove(self, email: str) -> None: """Remove a Collaborator. Args: - str : Collaborator email address. + email: Collaborator email address. """ if email in self._collaborators: if self._collaborators[email] == ShareRequestValue.Add: @@ -929,11 +934,11 @@ def remove(self, email): self._collaborators[email] = ShareRequestValue.Remove self._dirty = True - def all(self): + def all(self) -> List[str]: """Get all collaborators. Returns: - List[str]: Collaborators. + Collaborators. """ return [ email @@ -949,10 +954,10 @@ def __init__(self): super(NodeLabels, self).__init__() self._labels = {} - def __len__(self): + def __len__(self) -> int: return len(self._labels) - def _load(self, raw): + def _load(self, raw: Dict): # Parent method not called. if raw and isinstance(raw[-1], bool): self._dirty = raw.pop() @@ -962,7 +967,7 @@ def _load(self, raw): for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean=True): + def save(self, clean=True) -> Dict: # Parent method not called. ret = [ { @@ -979,11 +984,11 @@ def save(self, clean=True): self._dirty = False return ret - def add(self, label): + def add(self, label: Label) -> None: """Add a label. Args: - label (gkeepapi.node.Label): The Label object. + label: The Label object. """ self._labels[label.id] = label self._dirty = True @@ -1015,7 +1020,7 @@ def all(self): return [label for _, label in self._labels.items() if label is not None] -class TimestampsMixin(object): +class TimestampsMixin: """A mixin to add methods for updating timestamps.""" def touch(self, edited=False): @@ -2065,14 +2070,14 @@ def __str__(self): } -def from_json(raw): +def from_json(raw: Dict) -> Node: """Helper to construct a node from a dict. Args: - raw (dict): Raw node representation. + raw: Raw node representation. Returns: - Node: A Node object or None. + A Node object or None. """ ncls = None _type = raw.get("type") From b32ce823e7f84c5bae4ec71c8503b208ce0761ea Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 12:04:44 -0400 Subject: [PATCH 18/56] Update annotations to use 3.10 syntax --- gkeepapi/exception.py | 18 +------------- gkeepapi/node.py | 56 +++++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 45 deletions(-) diff --git a/gkeepapi/exception.py b/gkeepapi/exception.py index 0bf0fb1..f0448c1 100644 --- a/gkeepapi/exception.py +++ b/gkeepapi/exception.py @@ -15,14 +15,10 @@ def __init__(self, code: int, msg: str): class KeepException(Exception): """Generic Keep error.""" - pass - class LoginException(KeepException): """Login exception.""" - pass - class BrowserLoginRequiredException(LoginException): """Browser login required error.""" @@ -34,42 +30,30 @@ def __init__(self, url): class LabelException(KeepException): """Keep label error.""" - pass - class SyncException(KeepException): """Keep consistency error.""" - pass - class ResyncRequiredException(SyncException): """Full resync required error.""" - pass - class UpgradeRecommendedException(SyncException): """Upgrade recommended error.""" - pass - class MergeException(KeepException): """Node consistency error.""" - pass - class InvalidException(KeepException): """Constraint error.""" - pass - class ParseException(KeepException): """Parse error.""" - def __init__(self, msg: str, raw): + def __init__(self, msg: str, raw: dict): super(ParseException, self).__init__(msg) self.raw = raw diff --git a/gkeepapi/node.py b/gkeepapi/node.py index 23f4819..a9c795e 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -224,7 +224,7 @@ def _find_discrepancies(self, raw): # pragma: no cover len(s_raw), ) - def load(self, raw: Dict): + def load(self, raw: dict): """Unserialize from raw representation. (Wrapper) Args: @@ -237,7 +237,7 @@ def load(self, raw: Dict): except (KeyError, ValueError) as e: raise exception.ParseException(f"Parse error in {type(self)}", raw) from e - def _load(self, raw: Dict): + def _load(self, raw: dict): """Unserialize from raw representation. (Implementation logic) Args: @@ -245,7 +245,7 @@ def _load(self, raw: Dict): """ self._dirty = raw.get("_dirty", False) - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: """Serialize into raw representation. Clears the dirty bit by default. Args: @@ -278,11 +278,11 @@ def __init__(self): super(Annotation, self).__init__() self.id = self._generateAnnotationId() - def _load(self, raw: Dict): + def _load(self, raw: dict): super(Annotation, self)._load(raw) self.id = raw.get("id") - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = {} if self.id is not None: ret = super(Annotation, self).save(clean) @@ -312,7 +312,7 @@ def __init__(self): self._provenance_url = "" self._description = "" - def _load(self, raw: Dict): + def _load(self, raw: dict): super(WebLink, self)._load(raw) self._title = raw["webLink"]["title"] self._url = raw["webLink"]["url"] @@ -324,7 +324,7 @@ def _load(self, raw: Dict): self._provenance_url = raw["webLink"]["provenanceUrl"] self._description = raw["webLink"]["description"] - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(WebLink, self).save(clean) ret["webLink"] = { "title": self._title, @@ -413,11 +413,11 @@ def __init__(self): super(Category, self).__init__() self._category = None - def _load(self, raw: Dict): + def _load(self, raw: dict): super(Category, self)._load(raw) self._category = CategoryValue(raw["topicCategory"]["category"]) - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(Category, self).save(clean) ret["topicCategory"] = {"category": self._category.value} return ret @@ -444,11 +444,11 @@ def __init__(self): super(TaskAssist, self).__init__() self._suggest = None - def _load(self, raw: Dict): + def _load(self, raw: dict): super(TaskAssist, self)._load(raw) self._suggest = raw["taskAssist"]["suggestType"] - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(TaskAssist, self).save(clean) ret["taskAssist"] = {"suggestType": self._suggest} return ret @@ -475,13 +475,13 @@ def __init__(self): super(Context, self).__init__() self._entries = {} - def _load(self, raw: Dict): + def _load(self, raw: dict): super(Context, self)._load(raw) self._entries = {} for key, entry in raw.get("context", {}).items(): self._entries[key] = NodeAnnotations.from_json({key: entry}) - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(Context, self).save(clean) context = {} for entry in self._entries.values(): @@ -515,7 +515,7 @@ def __len__(self): return len(self._annotations) @classmethod - def from_json(cls, raw: Dict) -> Optional[Annotation]: + def from_json(cls, raw: dict) -> Annotation | None: """Helper to construct an annotation from a dict. Args: @@ -550,7 +550,7 @@ def all(self) -> Iterator[Annotation]: """ return self._annotations.values() - def _load(self, raw: Dict): + def _load(self, raw: dict): super(NodeAnnotations, self)._load(raw) self._annotations = {} if "annotations" not in raw: @@ -560,7 +560,7 @@ def _load(self, raw: Dict): annotation = self.from_json(raw_annotation) self._annotations[annotation.id] = annotation - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(NodeAnnotations, self).save(clean) ret["kind"] = "notes#annotationsGroup" if self._annotations: @@ -569,14 +569,14 @@ def save(self, clean=True) -> Dict: ] return ret - def _get_category_node(self) -> Optional[Category]: + def _get_category_node(self) -> Category | None: for annotation in self._annotations.values(): if isinstance(annotation, Category): return annotation return None @property - def category(self) -> Optional[CategoryValue]: + def category(self) -> CategoryValue | None: """Get the category. Returns: @@ -601,7 +601,7 @@ def category(self, value) -> None: self._dirty = True @property - def links(self) -> List[WebLink]: + def links(self) -> list[WebLink]: """Get all links. Returns: @@ -659,7 +659,7 @@ def __init__(self, create_time: str = None): self._updated = self.int_to_dt(create_time) self._edited = self.int_to_dt(create_time) - def _load(self, raw: Dict): + def _load(self, raw: dict): super(NodeTimestamps, self)._load(raw) if "created" in raw: self._created = self.str_to_dt(raw["created"]) @@ -670,7 +670,7 @@ def _load(self, raw: Dict): self.str_to_dt(raw["userEdited"]) if "userEdited" in raw else None ) - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(NodeTimestamps, self).save(clean) ret["kind"] = "notes#timestamps" ret["created"] = self.dt_to_str(self._created) @@ -808,7 +808,7 @@ def __init__(self): self._graveyard_state = GraveyardStateValue.Collapsed self._checked_listitems_policy = CheckedListItemsPolicyValue.Graveyard - def _load(self, raw: Dict): + def _load(self, raw: dict): super(NodeSettings, self)._load(raw) self._new_listitem_placement = NewListItemPlacementValue( raw["newListItemPlacement"] @@ -818,7 +818,7 @@ def _load(self, raw: Dict): raw["checkedListItemsPolicy"] ) - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: ret = super(NodeSettings, self).save(clean) ret["newListItemPlacement"] = self._new_listitem_placement.value ret["graveyardState"] = self._graveyard_state.value @@ -934,7 +934,7 @@ def remove(self, email: str) -> None: self._collaborators[email] = ShareRequestValue.Remove self._dirty = True - def all(self) -> List[str]: + def all(self) -> list[str]: """Get all collaborators. Returns: @@ -957,7 +957,7 @@ def __init__(self): def __len__(self) -> int: return len(self._labels) - def _load(self, raw: Dict): + def _load(self, raw: dict): # Parent method not called. if raw and isinstance(raw[-1], bool): self._dirty = raw.pop() @@ -967,7 +967,7 @@ def _load(self, raw: Dict): for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean=True) -> Dict: + def save(self, clean=True) -> dict: # Parent method not called. ret = [ { @@ -1015,7 +1015,7 @@ def all(self): """Get all labels. Returns: - List[gkeepapi.node.Label]: Labels. + list[gkeepapi.node.Label]: Labels. """ return [label for _, label in self._labels.items() if label is not None] @@ -2070,7 +2070,7 @@ def __str__(self): } -def from_json(raw: Dict) -> Node: +def from_json(raw: dict) -> Node | None: """Helper to construct a node from a dict. Args: From 84ed0e79485c475b9e62e84375f4282b1a5d14ac Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 14:35:54 -0400 Subject: [PATCH 19/56] Update typehints for 3.10 --- gkeepapi/__init__.py | 133 ++++++++-------- gkeepapi/node.py | 351 ++++++++++++++++++++++--------------------- 2 files changed, 248 insertions(+), 236 deletions(-) diff --git a/gkeepapi/__init__.py b/gkeepapi/__init__.py index 9ba46bd..4a21c36 100644 --- a/gkeepapi/__init__.py +++ b/gkeepapi/__init__.py @@ -10,7 +10,7 @@ import time import datetime import random -from typing import Callable, Iterator, List, Optional, Tuple, Dict, Union +from typing import Callable, Iterator, Tuple, Any from uuid import getnode as get_mac @@ -33,7 +33,7 @@ def __init__(self, scopes: str): self._device_id = None self._scopes = scopes - def login(self, email: str, password: str, device_id: str) -> bool: + def login(self, email: str, password: str, device_id: str): """Authenticate to Google with the provided credentials. Args: @@ -62,7 +62,6 @@ def login(self, email: str, password: str, device_id: str) -> bool: # Obtain an OAuth token. self.refresh() - return True def load(self, email: str, master_token: str, device_id: str) -> bool: """Authenticate to Google with the provided master token. @@ -91,7 +90,7 @@ def getMasterToken(self) -> str: """ return self._master_token - def setMasterToken(self, master_token: str) -> None: + def setMasterToken(self, master_token: str): """Sets the master token. This is useful if you'd like to authenticate with the API without providing your username & password. Do note that the master token has full access to your account. @@ -109,7 +108,7 @@ def getEmail(self) -> str: """ return self._email - def setEmail(self, email: str) -> None: + def setEmail(self, email: str): """Sets the account email. Args: @@ -125,7 +124,7 @@ def getDeviceId(self) -> str: """ return self._device_id - def setDeviceId(self, device_id: str) -> None: + def setDeviceId(self, device_id: str): """Sets the device id. Args: @@ -133,7 +132,7 @@ def setDeviceId(self, device_id: str) -> None: """ self._device_id = device_id - def getAuthToken(self) -> Optional[str]: + def getAuthToken(self) -> str | None: """Gets the auth token. Returns: @@ -168,7 +167,7 @@ def refresh(self) -> str: self._auth_token = res["Auth"] return self._auth_token - def logout(self) -> None: + def logout(self): """Log out of the account.""" self._master_token = None self._auth_token = None @@ -181,7 +180,7 @@ class API: RETRY_CNT = 2 - def __init__(self, base_url: str, auth: APIAuth = None): + def __init__(self, base_url: str, auth: APIAuth | None = None): self._session = requests.Session() self._auth = auth self._base_url = base_url @@ -200,7 +199,7 @@ def getAuth(self) -> APIAuth: """ return self._auth - def setAuth(self, auth: APIAuth) -> None: + def setAuth(self, auth: APIAuth): """Set authentication details for this API. Args: @@ -208,7 +207,7 @@ def setAuth(self, auth: APIAuth) -> None: """ self._auth = auth - def send(self, **req_kwargs) -> Dict: + def send(self, **req_kwargs) -> dict: """Send an authenticated request to a Google API. Automatically retries if the access token has expired. @@ -279,7 +278,7 @@ class KeepAPI(API): API_URL = "https://www.googleapis.com/notes/v1/" - def __init__(self, auth: APIAuth = None): + def __init__(self, auth: APIAuth | None = None): super(KeepAPI, self).__init__(self.API_URL, auth) create_time = time.time() @@ -291,10 +290,10 @@ def _generateId(cls, tz: int) -> str: def changes( self, - target_version: str = None, - nodes: List[Dict] = None, - labels: List[Dict] = None, - ) -> Dict: + target_version: str | None = None, + nodes: list[dict] | None = None, + labels: list[dict] | None = None, + ) -> dict: """Sync up (and down) all changes. Args: @@ -371,7 +370,7 @@ class MediaAPI(API): API_URL = "https://keep.google.com/media/v2/" - def __init__(self, auth: APIAuth = None): + def __init__(self, auth: APIAuth | None = None): super(MediaAPI, self).__init__(self.API_URL, auth) def get(self, blob: _node.Blob) -> str: @@ -399,7 +398,7 @@ class RemindersAPI(API): API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" - def __init__(self, auth: APIAuth = None): + def __init__(self, auth: APIAuth | None = None): super(RemindersAPI, self).__init__(self.API_URL, auth) self.static_params = { "taskList": [ @@ -417,7 +416,9 @@ def __init__(self, auth: APIAuth = None): }, } - def create(self, node_id: str, node_server_id: str, dtime: datetime.datetime): + def create( + self, node_id: str, node_server_id: str, dtime: datetime.datetime + ) -> Any: """Create a new reminder. Args: @@ -464,7 +465,9 @@ def create(self, node_id: str, node_server_id: str, dtime: datetime.datetime): return self.send(url=self._base_url + "create", method="POST", json=params) - def update(self, node_id: str, node_server_id: str, dtime: datetime.datetime): + def update( + self, node_id: str, node_server_id: str, dtime: datetime.datetime + ) -> Any: """Update an existing reminder. Args: @@ -519,7 +522,7 @@ def update(self, node_id: str, node_server_id: str, dtime: datetime.datetime): return self.send(url=self._base_url + "update", method="POST", json=params) - def delete(self, node_server_id: str): + def delete(self, node_server_id: str) -> Any: """Delete an existing reminder. Args: @@ -550,7 +553,7 @@ def delete(self, node_server_id: str): return self.send(url=self._base_url + "batchmutate", method="POST", json=params) - def list(self, master=True): + def list(self, master=True) -> Any: """List current reminders. Args: @@ -597,7 +600,7 @@ def list(self, master=True): return self.send(url=self._base_url + "list", method="POST", json=params) - def history(self, storage_version: str): + def history(self, storage_version: str) -> Any: """Get reminder changes. Args: @@ -617,7 +620,7 @@ def history(self, storage_version: str): return self.send(url=self._base_url + "history", method="POST", json=params) - def update(self): + def update(self) -> Any: """Sync up changes to reminders.""" params = {} return self.send(url=self._base_url + "update", method="POST", json=params) @@ -662,7 +665,7 @@ def __init__(self): self._clear() - def _clear(self) -> None: + def _clear(self): self._keep_version = None self._reminder_version = None self._labels = {} @@ -676,9 +679,9 @@ def login( self, email: str, password: str, - state: Optional[Dict] = None, + state: dict | None = None, sync=True, - device_id: Optional[str] = None, + device_id: str | None = None, ): """Authenticate to Google with the provided credentials & sync. @@ -694,20 +697,18 @@ def login( """ auth = APIAuth(self.OAUTH_SCOPES) if device_id is None: - device_id = get_mac() + device_id = f"{get_mac():x}" auth.login(email, password, device_id) self.load(auth, state, sync) - return True - def resume( self, email: str, master_token: str, - state: Optional[Dict] = None, + state: dict | None = None, sync=True, - device_id: Optional[str] = None, + device_id: str | None = None, ): """Authenticate to Google with the provided master token & sync. @@ -723,13 +724,11 @@ def resume( """ auth = APIAuth(self.OAUTH_SCOPES) if device_id is None: - device_id = get_mac() + device_id = f"{get_mac():x}" auth.load(email, master_token, device_id) self.load(auth, state, sync) - return True - def getMasterToken(self) -> str: """Get master token for resuming. @@ -738,7 +737,7 @@ def getMasterToken(self) -> str: """ return self._keep_api.getAuth().getMasterToken() - def load(self, auth: APIAuth, state: Optional[Dict] = None, sync=True) -> None: + def load(self, auth: APIAuth, state: dict | None = None, sync=True): """Authenticate to Google with a prepared authentication object & sync. Args: auth: Authentication object. @@ -756,7 +755,7 @@ def load(self, auth: APIAuth, state: Optional[Dict] = None, sync=True) -> None: if sync: self.sync(True) - def dump(self) -> Dict: + def dump(self) -> dict: """Serialize note data. Returns: @@ -775,7 +774,7 @@ def dump(self) -> Dict: "nodes": [node.save(False) for node in nodes], } - def restore(self, state: Dict) -> None: + def restore(self, state: dict): """Unserialize saved note data. Args: @@ -799,7 +798,7 @@ def get(self, node_id: str) -> _node.TopLevelNode: _node.Root.ID ].get(self._sid_map.get(node_id)) - def add(self, node: _node.Node) -> None: + def add(self, node: _node.Node): """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. @@ -817,12 +816,12 @@ def add(self, node: _node.Node) -> None: def find( self, - query: Union[re.Pattern, str, None] = None, - func: Optional[Callable] = None, - labels: Optional[List[str]] = None, - colors: Optional[List[str]] = None, - pinned: Optional[bool] = None, - archived: Optional[bool] = None, + query: re.Pattern | str | None = None, + func: Callable | None = None, + labels: list[str] | None = None, + colors: list[str] | None = None, + pinned: bool | None = None, + archived: bool | None = None, trashed: bool = False, ) -> Iterator[_node.TopLevelNode]: # pylint: disable=too-many-arguments """Find Notes based on the specified criteria. @@ -877,7 +876,7 @@ def find( ) def createNote( - self, title: Optional[str] = None, text: Optional[str] = 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. @@ -898,8 +897,8 @@ def createNote( def createList( self, - title: Optional[str] = None, - items: Optional[List[Tuple[str, bool]]] = None, + title: str | None = None, + items: list[Tuple[str, bool]] | None = None, ) -> _node.List: """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. @@ -943,9 +942,7 @@ def createLabel(self, name: str) -> _node.Label: self._labels[node.id] = node # pylint: disable=protected-access return node - def findLabel( - self, query: Union[re.Pattern, str], create=False - ) -> Optional[_node.Label]: + def findLabel(self, query: re.Pattern | str, create=False) -> _node.Label | None: """Find a label with the given name. Args: @@ -970,7 +967,7 @@ def findLabel( return self.createLabel(name) if create and is_str else None - def getLabel(self, label_id: str) -> Optional[_node.Label]: + def getLabel(self, label_id: str) -> _node.Label | None: """Get an existing label. Args: @@ -981,7 +978,7 @@ def getLabel(self, label_id: str) -> Optional[_node.Label]: """ return self._labels.get(label_id) - def deleteLabel(self, label_id: str) -> None: + def deleteLabel(self, label_id: str): """Deletes a label. Args: @@ -995,13 +992,13 @@ def deleteLabel(self, label_id: str) -> None: for node in self.all(): node.labels.remove(label) - def labels(self) -> List[_node.Label]: + def labels(self) -> list[_node.Label]: """Get all labels. Returns: Labels """ - return self._labels.values() + return list(self._labels.values()) def getMediaLink(self, blob: _node.Blob) -> str: """Get the canonical link to media. @@ -1014,7 +1011,7 @@ def getMediaLink(self, blob: _node.Blob) -> str: """ return self._media_api.get(blob) - def all(self) -> List[_node.TopLevelNode]: + def all(self) -> list[_node.TopLevelNode]: """Get all Notes. Returns: @@ -1022,7 +1019,7 @@ def all(self) -> List[_node.TopLevelNode]: """ return self._nodes[_node.Root.ID].children - def sync(self, resync=False) -> None: + def sync(self, resync=False): """Sync the local Keep tree with the server. If resyncing, local changes will be destroyed. Otherwise, local changes to notes, labels and reminders will be detected and synced up. Args: @@ -1035,13 +1032,13 @@ def sync(self, resync=False) -> None: if resync: self._clear() - # self._sync_reminders(resync) - self._sync_notes(resync) + # self._sync_reminders() + self._sync_notes() if _node.DEBUG: self._clean() - def _sync_reminders(self, resync=False): + def _sync_reminders(self): # Fetch updates until we reach the newest version. while True: logger.debug("Starting reminder sync: %s", self._reminder_version) @@ -1059,7 +1056,7 @@ def _sync_reminders(self, resync=False): if self._reminder_version == history["highestStorageVersion"]: break - def _sync_notes(self, resync=False): + def _sync_notes(self): # Fetch updates until we reach the newest version. while True: logger.debug("Starting keep sync: %s", self._keep_version) @@ -1095,10 +1092,10 @@ def _sync_notes(self, resync=False): if not changes["truncated"]: break - def _parseTasks(self, raw) -> None: + def _parseTasks(self, raw: dict): pass - def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches + def _parseNodes(self, raw: dict): # pylint: disable=too-many-branches created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1175,7 +1172,7 @@ def _parseNodes(self, raw) -> None: # pylint: disable=too-many-branches label_id ) # pylint: disable=protected-access - def _parseUserInfo(self, raw) -> None: + def _parseUserInfo(self, raw: dict): labels = {} if "labels" in raw: for label in raw["labels"]: @@ -1198,7 +1195,7 @@ def _parseUserInfo(self, raw) -> None: self._labels = labels - def _findDirtyNodes(self) -> List[_node.Node]: + def _findDirtyNodes(self) -> list[_node.Node]: # Find nodes that aren't in our internal nodes list and insert them. for node in list(self._nodes.values()): for child in node.children: @@ -1213,15 +1210,15 @@ def _findDirtyNodes(self) -> List[_node.Node]: return nodes - def _clean(self) -> None: + def _clean(self): """Recursively check that all nodes are reachable.""" - found_ids = {} + found_ids = set() nodes = [self._nodes[_node.Root.ID]] # Enumerate all nodes from the root node while nodes: node = nodes.pop() - found_ids[node.id] = None + found_ids.add(node.id) nodes = nodes + node.children # Find nodes that can't be reached from the root diff --git a/gkeepapi/node.py b/gkeepapi/node.py index a9c795e..f875a0a 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -15,6 +15,8 @@ import itertools from operator import attrgetter +from typing import Type, Tuple, Callable + from . import exception DEBUG = False @@ -489,13 +491,13 @@ def save(self, clean=True) -> dict: ret["context"] = context return ret - def all(self) -> Iterator[Annotation]: + def all(self) -> list[Annotation]: """Get all sub annotations. Returns: Sub Annotations. """ - return self._entries.values() + return list(self._entries.values()) @property def dirty(self) -> bool: @@ -542,13 +544,13 @@ def from_json(cls, raw: dict) -> Annotation | None: return annotation - def all(self) -> Iterator[Annotation]: + def all(self) -> list[Annotation]: """Get all annotations. Returns: Annotations. """ - return self._annotations.values() + return list(self._annotations.values()) def _load(self, raw: dict): super(NodeAnnotations, self)._load(raw) @@ -648,14 +650,14 @@ class NodeTimestamps(Element): TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" - def __init__(self, create_time: str = None): + def __init__(self, create_time: float | None = None): super(NodeTimestamps, self).__init__() if create_time is None: create_time = time.time() self._created = self.int_to_dt(create_time) - self._deleted = self.int_to_dt(0) - self._trashed = self.int_to_dt(0) + self._deleted = None + self._trashed = None self._updated = self.int_to_dt(create_time) self._edited = self.int_to_dt(create_time) @@ -696,7 +698,7 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: return datetime.datetime.strptime(tzs, cls.TZ_FMT) @classmethod - def int_to_dt(cls, tz: int) -> datetime.datetime: + def int_to_dt(cls, tz: int | float) -> datetime.datetime: """Convert a unix timestamp into an object. Params: @@ -743,7 +745,7 @@ def created(self, value) -> None: self._dirty = True @property - def deleted(self) -> datetime.datetime: + def deleted(self) -> datetime.datetime | None: """Get the deletion datetime. Returns: @@ -757,7 +759,7 @@ def deleted(self, value: datetime.datetime) -> None: self._dirty = True @property - def trashed(self) -> datetime.datetime: + def trashed(self) -> datetime.datetime | None: """Get the move-to-trash datetime. Returns: @@ -879,7 +881,7 @@ def __len__(self) -> int: return len(self._collaborators) def load( - self, collaborators_raw: List, requests_raw: List + self, collaborators_raw: list, requests_raw: list ): # pylint: disable=arguments-differ # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): @@ -894,7 +896,7 @@ def load( collaborator["type"] ) - def save(self, clean=True) -> Tuple[List, List]: + def save(self, clean=True) -> Tuple[list, list]: # Parent method not called. collaborators = [] requests = [] @@ -957,7 +959,7 @@ def __init__(self): def __len__(self) -> int: return len(self._labels) - def _load(self, raw: dict): + def _load(self, raw: list): # Parent method not called. if raw and isinstance(raw[-1], bool): self._dirty = raw.pop() @@ -967,7 +969,7 @@ def _load(self, raw: dict): for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean=True) -> dict: + def save(self, clean=True) -> Tuple[dict] | Tuple[dict, bool]: # Parent method not called. ret = [ { @@ -993,21 +995,21 @@ def add(self, label: Label) -> None: self._labels[label.id] = label self._dirty = True - def remove(self, label): + def remove(self, label: Label): """Remove a label. Args: - label (gkeepapi.node.Label): The Label object. + label: The Label object. """ if label.id in self._labels: self._labels[label.id] = None self._dirty = True - def get(self, label_id): + def get(self, label_id: str): """Get a label by ID. Args: - label_id (str): The label ID. + label_id: The label ID. """ return self._labels.get(label_id) @@ -1023,11 +1025,14 @@ def all(self): class TimestampsMixin: """A mixin to add methods for updating timestamps.""" + def __init__(self): + self.timestamps: NodeTimestamps + def touch(self, edited=False): """Mark the node as dirty. Args: - edited (bool): Whether to set the edited time. + edited: Whether to set the edited time. """ self._dirty = True dt = datetime.datetime.utcnow() @@ -1177,11 +1182,11 @@ def text(self): return self._text @text.setter - def text(self, value): + def text(self, value: str): """Set the text value. Args: - value (str): Text value. + value: Text value. """ self._text = value self.timestamps.edited = datetime.datetime.utcnow() @@ -1196,23 +1201,23 @@ def children(self): """ return list(self._children.values()) - def get(self, node_id): + def get(self, node_id: str): """Get child node with the given ID. Args: - node_id (str): The node ID. + node_id: The node ID. Returns: gkeepapi.Node: Child node. """ return self._children.get(node_id) - def append(self, node, dirty=True): + def append(self, node: Node, dirty=True): """Add a new child node. Args: - node (gkeepapi.Node): Node to add. - dirty (bool): Whether this node should be marked dirty. + node: Node to add. + dirty: Whether this node should be marked dirty. """ self._children[node.id] = node node.parent = self @@ -1221,12 +1226,12 @@ def append(self, node, dirty=True): return node - def remove(self, node, dirty=True): + def remove(self, node: Node, dirty=True): """Remove the given child node. Args: - node (gkeepapi.Node): Node to remove. - dirty (bool): Whether this node should be marked dirty. + node: Node to remove. + dirty: Whether this node should be marked dirty. """ if node.id in self._children: self._children[node.id].parent = None @@ -1406,6 +1411,129 @@ def audio(self): return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] +class ListItem(Node): + """Represents a Google Keep listitem. + Interestingly enough, :class:`Note`s store their content in a single + child :class:`ListItem`. + """ + + def __init__( + self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs + ): + super(ListItem, self).__init__( + type_=NodeType.ListItem, parent_id=parent_id, **kwargs + ) + self.parent_item = None + self.parent_server_id = parent_server_id + self.super_list_item_id = super_list_item_id + self.prev_super_list_item_id = None + self._subitems = {} + self._checked = False + + def _load(self, raw): + super(ListItem, self)._load(raw) + self.prev_super_list_item_id = self.super_list_item_id + self.super_list_item_id = raw.get("superListItemId") or None + self._checked = raw.get("checked", False) + + def save(self, clean=True): + ret = super(ListItem, self).save(clean) + ret["parentServerId"] = self.parent_server_id + ret["superListItemId"] = self.super_list_item_id + ret["checked"] = self._checked + return ret + + def add( + self, + text: str, + checked=False, + sort: NewListItemPlacementValue | int | None = None, + ): + """Add a new sub item to the list. This item must already be attached to a list. + + Args: + text: The text. + checked: Whether this item is checked. + sort: Item id for sorting. + """ + if self.parent is None: + raise exception.InvalidException("Item has no parent") + node = self.parent.add(text, checked, sort) + self.indent(node) + return node + + def indent(self, node: ListItem, dirty=True): + """Indent an item. Does nothing if the target has subitems. + + Args: + node: Item to indent. + dirty: Whether this node should be marked dirty. + """ + if node.subitems: + return + + self._subitems[node.id] = node + node.super_list_item_id = self.id + node.parent_item = self + if dirty: + node.touch(True) + + def dedent(self, node: ListItem, dirty=True): + """Dedent an item. Does nothing if the target is not indented under this item. + + Args: + node: Item to dedent. + dirty : Whether this node should be marked dirty. + """ + if node.id not in self._subitems: + return + + del self._subitems[node.id] + node.super_list_item_id = "" + node.parent_item = None + if dirty: + node.touch(True) + + @property + def subitems(self): + """Get subitems for this item. + + Returns: + list[gkeepapi.node.ListItem]: Subitems. + """ + return List.sorted_items(self._subitems.values()) + + @property + def indented(self): + """Get indentation state. + + Returns: + bool: Whether this item is indented. + """ + return self.parent_item is not None + + @property + def checked(self): + """Get the checked state. + + Returns: + bool: Whether this item is checked. + """ + return self._checked + + @checked.setter + def checked(self, value): + self._checked = value + self.touch(True) + + def __str__(self): + return "%s%s %s" % ( + " " if self.indented else "", + "☑" if self.checked else "☐", + self.text, + ) + + class Note(TopLevelNode): """Represents a Google Keep note.""" @@ -1453,13 +1581,18 @@ class List(TopLevelNode): def __init__(self, **kwargs): super(List, self).__init__(type_=self._TYPE, **kwargs) - def add(self, text, checked=False, sort=None): + def add( + self, + text: str, + checked=False, + sort: NewListItemPlacementValue | int | None = None, + ): """Add a new item to the list. Args: - text (str): The text. - checked (bool): Whether this item is checked. - sort (Union[gkeepapi.node.NewListItemPlacementValue, int]): Item id for sorting or a placement policy. + text: The text. + checked: Whether this item is checked. + sort: Item id for sorting or a placement policy. """ node = ListItem(parent_id=self.id, parent_server_id=self.server_id) node.checked = checked @@ -1486,13 +1619,13 @@ def text(self): return "\n".join((str(node) for node in self.items)) @classmethod - def sorted_items(cls, items): + def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: """Generate a list of sorted list items, taking into account parent items. Args: - items (list[gkeepapi.node.ListItem]): Items to sort. + items: Items to sort. Returns: - list[gkeepapi.node.ListItem]: Sorted items. + Sorted items. """ class t(tuple): @@ -1533,7 +1666,7 @@ def key_func(x): return sorted(items, key=key_func, reverse=True) - def _items(self, checked=None): + def _items(self, checked: bool | None = None) -> list[ListItem]: return [ node for node in self.children @@ -1542,13 +1675,13 @@ def _items(self, checked=None): and (checked is None or node.checked == checked) ] - def sort_items(self, key=attrgetter("text"), reverse=False): + def sort_items(self, key: Callable = attrgetter("text"), reverse=False): """Sort list items in place. By default, the items are alphabetized, but a custom function can be specified. Args: - key (callable): A filter function. - reverse (bool): Whether to reverse the output. + key: A filter function. + reverse: Whether to reverse the output. """ sorted_children = sorted(self._items(), key=key, reverse=reverse) sort_value = random.randint(1000000000, 9999999999) @@ -1557,155 +1690,37 @@ def sort_items(self, key=attrgetter("text"), reverse=False): node.sort = sort_value sort_value -= self.SORT_DELTA - def __str__(self): + def __str__(self) -> str: return "\n".join(([self.title] + [str(node) for node in self.items])) @property - def items(self): + def items(self) -> list[ListItem]: """Get all listitems. Returns: - list[gkeepapi.node.ListItem]: List items. + List items. """ return self.sorted_items(self._items()) @property - def checked(self): + def checked(self) -> list[ListItem]: """Get all checked listitems. Returns: - list[gkeepapi.node.ListItem]: List items. + List items. """ return self.sorted_items(self._items(True)) @property - def unchecked(self): + def unchecked(self) -> list[ListItem]: """Get all unchecked listitems. Returns: - list[gkeepapi.node.ListItem]: List items. + List items. """ return self.sorted_items(self._items(False)) -class ListItem(Node): - """Represents a Google Keep listitem. - Interestingly enough, :class:`Note`s store their content in a single - child :class:`ListItem`. - """ - - def __init__( - self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs - ): - super(ListItem, self).__init__( - type_=NodeType.ListItem, parent_id=parent_id, **kwargs - ) - self.parent_item = None - self.parent_server_id = parent_server_id - self.super_list_item_id = super_list_item_id - self.prev_super_list_item_id = None - self._subitems = {} - self._checked = False - - def _load(self, raw): - super(ListItem, self)._load(raw) - self.prev_super_list_item_id = self.super_list_item_id - self.super_list_item_id = raw.get("superListItemId") or None - self._checked = raw.get("checked", False) - - def save(self, clean=True): - ret = super(ListItem, self).save(clean) - ret["parentServerId"] = self.parent_server_id - ret["superListItemId"] = self.super_list_item_id - ret["checked"] = self._checked - return ret - - def add(self, text, checked=False, sort=None): - """Add a new sub item to the list. This item must already be attached to a list. - - Args: - text (str): The text. - checked (bool): Whether this item is checked. - sort (int): Item id for sorting. - """ - if self.parent is None: - raise exception.InvalidException("Item has no parent") - node = self.parent.add(text, checked, sort) - self.indent(node) - return node - - def indent(self, node, dirty=True): - """Indent an item. Does nothing if the target has subitems. - - Args: - node (gkeepapi.node.ListItem): Item to indent. - dirty (bool): Whether this node should be marked dirty. - """ - if node.subitems: - return - - self._subitems[node.id] = node - node.super_list_item_id = self.id - node.parent_item = self - if dirty: - node.touch(True) - - def dedent(self, node, dirty=True): - """Dedent an item. Does nothing if the target is not indented under this item. - - Args: - node (gkeepapi.node.ListItem): Item to dedent. - dirty (bool): Whether this node should be marked dirty. - """ - if node.id not in self._subitems: - return - - del self._subitems[node.id] - node.super_list_item_id = "" - node.parent_item = None - if dirty: - node.touch(True) - - @property - def subitems(self): - """Get subitems for this item. - - Returns: - list[gkeepapi.node.ListItem]: Subitems. - """ - return List.sorted_items(self._subitems.values()) - - @property - def indented(self): - """Get indentation state. - - Returns: - bool: Whether this item is indented. - """ - return self.parent_item is not None - - @property - def checked(self): - """Get the checked state. - - Returns: - bool: Whether this item is checked. - """ - return self._checked - - @checked.setter - def checked(self, value): - self._checked = value - self.touch(True) - - def __str__(self): - return "%s%s %s" % ( - " " if self.indented else "", - "☑" if self.checked else "☐", - self.text, - ) - - class NodeBlob(Element): """Represents a blob descriptor.""" @@ -1942,14 +1957,14 @@ def __init__(self, parent_id=None, **kwargs): self.blob = None @classmethod - def from_json(cls, raw): + def from_json(cls: Type, raw: dict) -> NodeBlob | None: """Helper to construct a blob from a dict. Args: - raw (dict): Raw blob representation. + raw: Raw blob representation. Returns: - NodeBlob: A NodeBlob object or None. + A NodeBlob object or None. """ if raw is None: return None From a25e842c928a0e8024d891527d7046dc45e0efb1 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 14:38:43 -0400 Subject: [PATCH 20/56] Add min python version --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index ac234b3..0294c1f 100644 --- a/setup.py +++ b/setup.py @@ -23,6 +23,7 @@ author_email="z@kwi.li", # Choose your license license="MIT", + python_requires=">=3.10.0", # See https://pypi.python.org/pypi?%3Aaction=list_classifiers classifiers=[ # How mature is this project? Common values are From d0e3841071f497165f982073e87397466fe7ea0a Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 14:44:07 -0400 Subject: [PATCH 21/56] Typehint fixes --- gkeepapi/node.py | 296 +++++++++++++++++++++++------------------------ 1 file changed, 148 insertions(+), 148 deletions(-) diff --git a/gkeepapi/node.py b/gkeepapi/node.py index f875a0a..6c89ee2 100644 --- a/gkeepapi/node.py +++ b/gkeepapi/node.py @@ -524,7 +524,7 @@ def from_json(cls, raw: dict) -> Annotation | None: raw: Raw annotation representation. Returns: - Node: An Annotation object or None. + An Annotation object or None. """ bcls = None if "webLink" in raw: @@ -949,6 +949,145 @@ def all(self) -> list[str]: ] +class TimestampsMixin: + """A mixin to add methods for updating timestamps.""" + + def __init__(self): + self.timestamps: NodeTimestamps + + def touch(self, edited=False): + """Mark the node as dirty. + + Args: + edited: Whether to set the edited time. + """ + self._dirty = True + dt = datetime.datetime.utcnow() + self.timestamps.updated = dt + if edited: + self.timestamps.edited = dt + + @property + def trashed(self): + """Get the trashed state. + + Returns: + bool: Whether this item is trashed. + """ + return ( + self.timestamps.trashed is not None + and self.timestamps.trashed > NodeTimestamps.int_to_dt(0) + ) + + def trash(self): + """Mark the item as trashed.""" + self.timestamps.trashed = datetime.datetime.utcnow() + + def untrash(self): + """Mark the item as untrashed.""" + self.timestamps.trashed = self.timestamps.int_to_dt(0) + + @property + def deleted(self): + """Get the deleted state. + + Returns: + bool: Whether this item is deleted. + """ + return ( + self.timestamps.deleted is not None + and self.timestamps.deleted > NodeTimestamps.int_to_dt(0) + ) + + def delete(self): + """Mark the item as deleted.""" + self.timestamps.deleted = datetime.datetime.utcnow() + + def undelete(self): + """Mark the item as undeleted.""" + self.timestamps.deleted = None + + +class Label(Element, TimestampsMixin): + """Represents a label.""" + + def __init__(self): + super(Label, self).__init__() + + create_time = time.time() + + self.id = self._generateId(create_time) + self._name = "" + self.timestamps = NodeTimestamps(create_time) + self._merged = NodeTimestamps.int_to_dt(0) + + @classmethod + def _generateId(cls, tz): + return "tag.%s.%x" % ( + "".join( + [ + random.choice("abcdefghijklmnopqrstuvwxyz0123456789") + for _ in range(12) + ] + ), + int(tz * 1000), + ) + + def _load(self, raw): + super(Label, self)._load(raw) + self.id = raw["mainId"] + self._name = raw["name"] + self.timestamps.load(raw["timestamps"]) + self._merged = ( + NodeTimestamps.str_to_dt(raw["lastMerged"]) + if "lastMerged" in raw + else NodeTimestamps.int_to_dt(0) + ) + + def save(self, clean=True): + ret = super(Label, self).save(clean) + ret["mainId"] = self.id + ret["name"] = self._name + ret["timestamps"] = self.timestamps.save(clean) + ret["lastMerged"] = NodeTimestamps.dt_to_str(self._merged) + return ret + + @property + def name(self): + """Get the label name. + + Returns: + str: Label name. + """ + return self._name + + @name.setter + def name(self, value): + self._name = value + self.touch(True) + + @property + def merged(self): + """Get last merge datetime. + + Returns: + datetime: Datetime. + """ + return self._merged + + @merged.setter + def merged(self, value): + self._merged = value + self.touch() + + @property + def dirty(self): + return super(Label, self).dirty or self.timestamps.dirty + + def __str__(self): + return self.name + + class NodeLabels(Element): """Represents the labels on a :class:`TopLevelNode`.""" @@ -1022,65 +1161,6 @@ def all(self): return [label for _, label in self._labels.items() if label is not None] -class TimestampsMixin: - """A mixin to add methods for updating timestamps.""" - - def __init__(self): - self.timestamps: NodeTimestamps - - def touch(self, edited=False): - """Mark the node as dirty. - - Args: - edited: Whether to set the edited time. - """ - self._dirty = True - dt = datetime.datetime.utcnow() - self.timestamps.updated = dt - if edited: - self.timestamps.edited = dt - - @property - def trashed(self): - """Get the trashed state. - - Returns: - bool: Whether this item is trashed. - """ - return ( - self.timestamps.trashed is not None - and self.timestamps.trashed > NodeTimestamps.int_to_dt(0) - ) - - def trash(self): - """Mark the item as trashed.""" - self.timestamps.trashed = datetime.datetime.utcnow() - - def untrash(self): - """Mark the item as untrashed.""" - self.timestamps.trashed = self.timestamps.int_to_dt(0) - - @property - def deleted(self): - """Get the deleted state. - - Returns: - bool: Whether this item is deleted. - """ - return ( - self.timestamps.deleted is not None - and self.timestamps.deleted > NodeTimestamps.int_to_dt(0) - ) - - def delete(self): - """Mark the item as deleted.""" - self.timestamps.deleted = datetime.datetime.utcnow() - - def undelete(self): - """Mark the item as undeleted.""" - self.timestamps.deleted = None - - class Node(Element, TimestampsMixin): """Node base class.""" @@ -1193,26 +1273,26 @@ def text(self, value: str): self.touch(True) @property - def children(self): + def children(self) -> list["Node"]: """Get all children. Returns: - list[gkeepapi.Node]: Children nodes. + Children nodes. """ return list(self._children.values()) - def get(self, node_id: str): + def get(self, node_id: str) -> "Node | None": """Get child node with the given ID. Args: node_id: The node ID. Returns: - gkeepapi.Node: Child node. + Child node. """ return self._children.get(node_id) - def append(self, node: Node, dirty=True): + def append(self, node: "Node", dirty=True): """Add a new child node. Args: @@ -1226,7 +1306,7 @@ def append(self, node: Node, dirty=True): return node - def remove(self, node: Node, dirty=True): + def remove(self, node: "Node", dirty=True): """Remove the given child node. Args: @@ -1462,7 +1542,7 @@ def add( self.indent(node) return node - def indent(self, node: ListItem, dirty=True): + def indent(self, node: "ListItem", dirty=True): """Indent an item. Does nothing if the target has subitems. Args: @@ -1478,7 +1558,7 @@ def indent(self, node: ListItem, dirty=True): if dirty: node.touch(True) - def dedent(self, node: ListItem, dirty=True): + def dedent(self, node: "ListItem", dirty=True): """Dedent an item. Does nothing if the target is not indented under this item. Args: @@ -1997,86 +2077,6 @@ def save(self, clean=True): return ret -class Label(Element, TimestampsMixin): - """Represents a label.""" - - def __init__(self): - super(Label, self).__init__() - - create_time = time.time() - - self.id = self._generateId(create_time) - self._name = "" - self.timestamps = NodeTimestamps(create_time) - self._merged = NodeTimestamps.int_to_dt(0) - - @classmethod - def _generateId(cls, tz): - return "tag.%s.%x" % ( - "".join( - [ - random.choice("abcdefghijklmnopqrstuvwxyz0123456789") - for _ in range(12) - ] - ), - int(tz * 1000), - ) - - def _load(self, raw): - super(Label, self)._load(raw) - self.id = raw["mainId"] - self._name = raw["name"] - self.timestamps.load(raw["timestamps"]) - self._merged = ( - NodeTimestamps.str_to_dt(raw["lastMerged"]) - if "lastMerged" in raw - else NodeTimestamps.int_to_dt(0) - ) - - def save(self, clean=True): - ret = super(Label, self).save(clean) - ret["mainId"] = self.id - ret["name"] = self._name - ret["timestamps"] = self.timestamps.save(clean) - ret["lastMerged"] = NodeTimestamps.dt_to_str(self._merged) - return ret - - @property - def name(self): - """Get the label name. - - Returns: - str: Label name. - """ - return self._name - - @name.setter - def name(self, value): - self._name = value - self.touch(True) - - @property - def merged(self): - """Get last merge datetime. - - Returns: - datetime: Datetime. - """ - return self._merged - - @merged.setter - def merged(self, value): - self._merged = value - self.touch() - - @property - def dirty(self): - return super(Label, self).dirty or self.timestamps.dirty - - def __str__(self): - return self.name - - _type_map = { NodeType.Note: Note, NodeType.List: List, From ba08ac339eaab3e0ef84ca056ec684d695e9fbbd Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 14:53:21 -0400 Subject: [PATCH 22/56] Move to pyproject.toml --- pyproject.toml | 27 +++++++++++ requirements.txt | 4 -- setup.cfg | 2 - setup.py | 59 ------------------------- {gkeepapi => src/gkeepapi}/__init__.py | 2 +- {gkeepapi => src/gkeepapi}/exception.py | 0 {gkeepapi => src/gkeepapi}/node.py | 0 7 files changed, 28 insertions(+), 66 deletions(-) create mode 100644 pyproject.toml delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py rename {gkeepapi => src/gkeepapi}/__init__.py (99%) rename {gkeepapi => src/gkeepapi}/exception.py (100%) rename {gkeepapi => src/gkeepapi}/node.py (100%) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..efb3785 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[project] +name = "gkeepapi" +version = "1.0.0" +authors = [ + { name="Kai", email="z@kwi.li" }, +] +description = "An unofficial Google Keep API client" +readme = "README.md" +requires-python = ">=3.10" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "gpsoauth >= 1.0.2", + "future >= 0.16.0", +] + + +[project.urls] +"Homepage" = "https://github.com/kiwiz/gkeepapi" +"Bug Tracker" = "https://github.com/kiwiz/gkeepapi/issues" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 140d3a0..0000000 --- a/requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -gpsoauth>=1.0.2 -future>=0.16.0 -enum34>=1.1.6; python_version < '3.4' -mock>=3.0.5; python_version < '3.3' diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 3c6e79c..0000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[bdist_wheel] -universal=1 diff --git a/setup.py b/setup.py deleted file mode 100644 index 0294c1f..0000000 --- a/setup.py +++ /dev/null @@ -1,59 +0,0 @@ -# Always prefer setuptools over distutils -from setuptools import setup, find_packages - -# To use a consistent encoding -from codecs import open -from os import path - -import gkeepapi - -here = path.abspath(path.dirname(__file__)) - -setup( - name="gkeepapi", - # Versions should comply with PEP440. For a discussion on single-sourcing - # the version across setup.py and the project code, see - # https://packaging.python.org/en/latest/single_source_version.html - version=gkeepapi.__version__, - description="An unofficial Google Keep API client", - # The project's main homepage. - url="https://github.com/kiwiz/gkeepapi", - # Author details - author="Kai", - author_email="z@kwi.li", - # Choose your license - license="MIT", - python_requires=">=3.10.0", - # See https://pypi.python.org/pypi?%3Aaction=list_classifiers - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - "Development Status :: 4 - Beta", - # Indicate who your project is intended for - "Intended Audience :: Developers", - "Operating System :: OS Independent", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Python Modules", - # Pick your license as you wish (should match "license" above) - "License :: OSI Approved :: MIT License", - # Specify the Python versions you support here. In particular, ensure - # that you indicate whether you support Python 2, Python 3 or both. - "Programming Language :: Python :: 3", - ], - # What does your project relate to? - keywords="google keep api", - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages(). - packages=find_packages(exclude=["docs", "tests"]), - # List run-time dependencies here. These will be installed by pip when - # your project is installed. For an analysis of "install_requires" vs pip's - # requirements files see: - # https://packaging.python.org/en/latest/requirements.html - install_requires=[ - "gpsoauth >= 1.0.2", - "future >= 0.16.0", - "enum34 >= 1.1.6; python_version < '3.4'", - ], -) diff --git a/gkeepapi/__init__.py b/src/gkeepapi/__init__.py similarity index 99% rename from gkeepapi/__init__.py rename to src/gkeepapi/__init__.py index 4a21c36..fc1b480 100644 --- a/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -3,7 +3,7 @@ .. moduleauthor:: Kai """ -__version__ = "0.14.2" +__version__ = "1.0.0" import logging import re diff --git a/gkeepapi/exception.py b/src/gkeepapi/exception.py similarity index 100% rename from gkeepapi/exception.py rename to src/gkeepapi/exception.py diff --git a/gkeepapi/node.py b/src/gkeepapi/node.py similarity index 100% rename from gkeepapi/node.py rename to src/gkeepapi/node.py From 42937ae37c7a2064c8dbdaa5832a0bb26d887dcf Mon Sep 17 00:00:00 2001 From: K Date: Tue, 4 Apr 2023 15:12:53 -0400 Subject: [PATCH 23/56] More Py3 cleanup --- .pylintrc | 2 +- Makefile | 8 +-- pyproject.toml | 4 ++ src/gkeepapi/__init__.py | 6 +- src/gkeepapi/exception.py | 4 +- src/gkeepapi/node.py | 128 +++++++++++++++++++------------------- 6 files changed, 78 insertions(+), 74 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6d8ab85..fed055d 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,4 +1,4 @@ [BASIC] good-names=logger [MESSAGES CONTROL] -disable=line-too-long,bad-continuation,too-many-instance-attributes,invalid-name,too-many-lines,useless-object-inheritance +disable=line-too-long,too-many-instance-attributes,invalid-name,too-many-lines,useless-object-inheritance diff --git a/Makefile b/Makefile index 41d4d4c..2d297c3 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,18 @@ .PHONY: lint test coverage build clean upload all lint: - pylint gkeepapi + pylint src test: python3 -m unittest discover coverage: - coverage run --source gkeepapi -m unittest discover + coverage run --source src -m unittest discover coverage report coverage html -build: gkeepapi/*.py - python3 setup.py bdist_wheel +build: src/gkeepapi/*.py + python3 -m build clean: rm -f dist/*.whl diff --git a/pyproject.toml b/pyproject.toml index efb3785..06b64f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,10 @@ dependencies = [ "gpsoauth >= 1.0.2", "future >= 0.16.0", ] +optional-dependencies = [ + "pylint>=2.17.2", + "black>=23.3.0", +] [project.urls] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index fc1b480..592ad2c 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -279,7 +279,7 @@ class KeepAPI(API): API_URL = "https://www.googleapis.com/notes/v1/" def __init__(self, auth: APIAuth | None = None): - super(KeepAPI, self).__init__(self.API_URL, auth) + super().__init__(self.API_URL, auth) create_time = time.time() self._session_id = self._generateId(create_time) @@ -371,7 +371,7 @@ class MediaAPI(API): API_URL = "https://keep.google.com/media/v2/" def __init__(self, auth: APIAuth | None = None): - super(MediaAPI, self).__init__(self.API_URL, auth) + super().__init__(self.API_URL, auth) def get(self, blob: _node.Blob) -> str: """Get the canonical link to a media blob. @@ -399,7 +399,7 @@ class RemindersAPI(API): API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" def __init__(self, auth: APIAuth | None = None): - super(RemindersAPI, self).__init__(self.API_URL, auth) + super().__init__(self.API_URL, auth) self.static_params = { "taskList": [ {"systemListId": "MEMENTO"}, diff --git a/src/gkeepapi/exception.py b/src/gkeepapi/exception.py index f0448c1..748872d 100644 --- a/src/gkeepapi/exception.py +++ b/src/gkeepapi/exception.py @@ -8,7 +8,7 @@ class APIException(Exception): """The API server returned an error.""" def __init__(self, code: int, msg: str): - super(APIException, self).__init__(msg) + super().__init__(msg) self.code = code @@ -55,5 +55,5 @@ class ParseException(KeepException): """Parse error.""" def __init__(self, msg: str, raw: dict): - super(ParseException, self).__init__(msg) + super().__init__(msg) self.raw = raw diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 6c89ee2..a2ecd61 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -277,17 +277,17 @@ class Annotation(Element): """Note annotations base class.""" def __init__(self): - super(Annotation, self).__init__() + super().__init__() self.id = self._generateAnnotationId() def _load(self, raw: dict): - super(Annotation, self)._load(raw) + super()._load(raw) self.id = raw.get("id") def save(self, clean=True) -> dict: ret = {} if self.id is not None: - ret = super(Annotation, self).save(clean) + ret = super().save(clean) if self.id is not None: ret["id"] = self.id return ret @@ -307,7 +307,7 @@ class WebLink(Annotation): """Represents a link annotation on a :class:`TopLevelNode`.""" def __init__(self): - super(WebLink, self).__init__() + super().__init__() self._title = "" self._url = "" self._image_url = None @@ -315,7 +315,7 @@ def __init__(self): self._description = "" def _load(self, raw: dict): - super(WebLink, self)._load(raw) + super()._load(raw) self._title = raw["webLink"]["title"] self._url = raw["webLink"]["url"] self._image_url = ( @@ -327,7 +327,7 @@ def _load(self, raw: dict): self._description = raw["webLink"]["description"] def save(self, clean=True) -> dict: - ret = super(WebLink, self).save(clean) + ret = super().save(clean) ret["webLink"] = { "title": self._title, "url": self._url, @@ -412,15 +412,15 @@ class Category(Annotation): """Represents a category annotation on a :class:`TopLevelNode`.""" def __init__(self): - super(Category, self).__init__() + super().__init__() self._category = None def _load(self, raw: dict): - super(Category, self)._load(raw) + super()._load(raw) self._category = CategoryValue(raw["topicCategory"]["category"]) def save(self, clean=True) -> dict: - ret = super(Category, self).save(clean) + ret = super().save(clean) ret["topicCategory"] = {"category": self._category.value} return ret @@ -443,15 +443,15 @@ class TaskAssist(Annotation): """Unknown.""" def __init__(self): - super(TaskAssist, self).__init__() + super().__init__() self._suggest = None def _load(self, raw: dict): - super(TaskAssist, self)._load(raw) + super()._load(raw) self._suggest = raw["taskAssist"]["suggestType"] def save(self, clean=True) -> dict: - ret = super(TaskAssist, self).save(clean) + ret = super().save(clean) ret["taskAssist"] = {"suggestType": self._suggest} return ret @@ -474,17 +474,17 @@ class Context(Annotation): """Represents a context annotation, which may contain other annotations.""" def __init__(self): - super(Context, self).__init__() + super().__init__() self._entries = {} def _load(self, raw: dict): - super(Context, self)._load(raw) + super()._load(raw) self._entries = {} for key, entry in raw.get("context", {}).items(): self._entries[key] = NodeAnnotations.from_json({key: entry}) def save(self, clean=True) -> dict: - ret = super(Context, self).save(clean) + ret = super().save(clean) context = {} for entry in self._entries.values(): context.update(entry.save(clean)) @@ -501,7 +501,7 @@ def all(self) -> list[Annotation]: @property def dirty(self) -> bool: - return super(Context, self).dirty or any( + return super().dirty or any( (annotation.dirty for annotation in self._entries.values()) ) @@ -510,7 +510,7 @@ class NodeAnnotations(Element): """Represents the annotation container on a :class:`TopLevelNode`.""" def __init__(self): - super(NodeAnnotations, self).__init__() + super().__init__() self._annotations = {} def __len__(self): @@ -553,7 +553,7 @@ def all(self) -> list[Annotation]: return list(self._annotations.values()) def _load(self, raw: dict): - super(NodeAnnotations, self)._load(raw) + super()._load(raw) self._annotations = {} if "annotations" not in raw: return @@ -563,7 +563,7 @@ def _load(self, raw: dict): self._annotations[annotation.id] = annotation def save(self, clean=True) -> dict: - ret = super(NodeAnnotations, self).save(clean) + ret = super().save(clean) ret["kind"] = "notes#annotationsGroup" if self._annotations: ret["annotations"] = [ @@ -640,7 +640,7 @@ def remove(self, annotation: Annotation) -> None: @property def dirty(self) -> bool: - return super(NodeAnnotations, self).dirty or any( + return super().dirty or any( (annotation.dirty for annotation in self._annotations.values()) ) @@ -651,7 +651,7 @@ class NodeTimestamps(Element): TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" def __init__(self, create_time: float | None = None): - super(NodeTimestamps, self).__init__() + super().__init__() if create_time is None: create_time = time.time() @@ -662,7 +662,7 @@ def __init__(self, create_time: float | None = None): self._edited = self.int_to_dt(create_time) def _load(self, raw: dict): - super(NodeTimestamps, self)._load(raw) + super()._load(raw) if "created" in raw: self._created = self.str_to_dt(raw["created"]) self._deleted = self.str_to_dt(raw["deleted"]) if "deleted" in raw else None @@ -673,7 +673,7 @@ def _load(self, raw: dict): ) def save(self, clean=True) -> dict: - ret = super(NodeTimestamps, self).save(clean) + ret = super().save(clean) ret["kind"] = "notes#timestamps" ret["created"] = self.dt_to_str(self._created) if self._deleted is not None: @@ -805,13 +805,13 @@ class NodeSettings(Element): """Represents the settings associated with a :class:`TopLevelNode`.""" def __init__(self): - super(NodeSettings, self).__init__() + super().__init__() self._new_listitem_placement = NewListItemPlacementValue.Bottom self._graveyard_state = GraveyardStateValue.Collapsed self._checked_listitems_policy = CheckedListItemsPolicyValue.Graveyard def _load(self, raw: dict): - super(NodeSettings, self)._load(raw) + super()._load(raw) self._new_listitem_placement = NewListItemPlacementValue( raw["newListItemPlacement"] ) @@ -821,7 +821,7 @@ def _load(self, raw: dict): ) def save(self, clean=True) -> dict: - ret = super(NodeSettings, self).save(clean) + ret = super().save(clean) ret["newListItemPlacement"] = self._new_listitem_placement.value ret["graveyardState"] = self._graveyard_state.value ret["checkedListItemsPolicy"] = self._checked_listitems_policy.value @@ -874,7 +874,7 @@ class NodeCollaborators(Element): """Represents the collaborators on a :class:`TopLevelNode`.""" def __init__(self): - super(NodeCollaborators, self).__init__() + super().__init__() self._collaborators = {} def __len__(self) -> int: @@ -1012,7 +1012,7 @@ class Label(Element, TimestampsMixin): """Represents a label.""" def __init__(self): - super(Label, self).__init__() + super().__init__() create_time = time.time() @@ -1034,7 +1034,7 @@ def _generateId(cls, tz): ) def _load(self, raw): - super(Label, self)._load(raw) + super()._load(raw) self.id = raw["mainId"] self._name = raw["name"] self.timestamps.load(raw["timestamps"]) @@ -1045,7 +1045,7 @@ def _load(self, raw): ) def save(self, clean=True): - ret = super(Label, self).save(clean) + ret = super().save(clean) ret["mainId"] = self.id ret["name"] = self._name ret["timestamps"] = self.timestamps.save(clean) @@ -1082,7 +1082,7 @@ def merged(self, value): @property def dirty(self): - return super(Label, self).dirty or self.timestamps.dirty + return super().dirty or self.timestamps.dirty def __str__(self): return self.name @@ -1092,7 +1092,7 @@ class NodeLabels(Element): """Represents the labels on a :class:`TopLevelNode`.""" def __init__(self): - super(NodeLabels, self).__init__() + super().__init__() self._labels = {} def __len__(self) -> int: @@ -1165,7 +1165,7 @@ class Node(Element, TimestampsMixin): """Node base class.""" def __init__(self, id_=None, type_=None, parent_id=None): - super(Node, self).__init__() + super().__init__() create_time = time.time() @@ -1193,7 +1193,7 @@ def _generateId(cls, tz): ) def _load(self, raw): - super(Node, self)._load(raw) + super()._load(raw) # Verify this is a valid type NodeType(raw["type"]) if raw["kind"] not in ["notes#node"]: @@ -1213,7 +1213,7 @@ def _load(self, raw): self.annotations.load(raw["annotationsGroup"]) def save(self, clean=True): - ret = super(Node, self).save(clean) + ret = super().save(clean) ret["id"] = self.id ret["kind"] = "notes#node" ret["type"] = self.type.value @@ -1331,7 +1331,7 @@ def new(self): @property def dirty(self): return ( - super(Node, self).dirty + super().dirty or self.timestamps.dirty or self.annotations.dirty or self.settings.dirty @@ -1345,7 +1345,7 @@ class Root(Node): ID = "root" def __init__(self): - super(Root, self).__init__(id_=self.ID) + super().__init__(id_=self.ID) @property def dirty(self): @@ -1358,7 +1358,7 @@ class TopLevelNode(Node): _TYPE = None def __init__(self, **kwargs): - super(TopLevelNode, self).__init__(parent_id=Root.ID, **kwargs) + super().__init__(parent_id=Root.ID, **kwargs) self._color = ColorValue.White self._archived = False self._pinned = False @@ -1367,7 +1367,7 @@ def __init__(self, **kwargs): self.collaborators = NodeCollaborators() def _load(self, raw): - super(TopLevelNode, self)._load(raw) + super()._load(raw) self._color = ColorValue(raw["color"]) if "color" in raw else ColorValue.White self._archived = raw["isArchived"] if "isArchived" in raw else False self._pinned = raw["isPinned"] if "isPinned" in raw else False @@ -1381,7 +1381,7 @@ def _load(self, raw): self._moved = "moved" in raw def save(self, clean=True): - ret = super(TopLevelNode, self).save(clean) + ret = super().save(clean) ret["color"] = self._color.value ret["isArchived"] = self._archived ret["isPinned"] = self._pinned @@ -1464,7 +1464,7 @@ def url(self): @property def dirty(self): return ( - super(TopLevelNode, self).dirty + super().dirty or self.labels.dirty or self.collaborators.dirty ) @@ -1500,7 +1500,7 @@ class ListItem(Node): def __init__( self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs ): - super(ListItem, self).__init__( + super().__init__( type_=NodeType.ListItem, parent_id=parent_id, **kwargs ) self.parent_item = None @@ -1511,13 +1511,13 @@ def __init__( self._checked = False def _load(self, raw): - super(ListItem, self)._load(raw) + super()._load(raw) self.prev_super_list_item_id = self.super_list_item_id self.super_list_item_id = raw.get("superListItemId") or None self._checked = raw.get("checked", False) def save(self, clean=True): - ret = super(ListItem, self).save(clean) + ret = super().save(clean) ret["parentServerId"] = self.parent_server_id ret["superListItemId"] = self.super_list_item_id ret["checked"] = self._checked @@ -1620,7 +1620,7 @@ class Note(TopLevelNode): _TYPE = NodeType.Note def __init__(self, **kwargs): - super(Note, self).__init__(type_=self._TYPE, **kwargs) + super().__init__(type_=self._TYPE, **kwargs) def _get_text_node(self): node = None @@ -1659,7 +1659,7 @@ class List(TopLevelNode): SORT_DELTA = 10000 # Arbitrary constant def __init__(self, **kwargs): - super(List, self).__init__(type_=self._TYPE, **kwargs) + super().__init__(type_=self._TYPE, **kwargs) def add( self, @@ -1807,7 +1807,7 @@ class NodeBlob(Element): _TYPE = None def __init__(self, type_=None): - super(NodeBlob, self).__init__() + super().__init__() self.blob_id = None self.type = type_ self._media_id = None @@ -1815,7 +1815,7 @@ def __init__(self, type_=None): self._is_uploaded = False def _load(self, raw): - super(NodeBlob, self)._load(raw) + super()._load(raw) # Verify this is a valid type BlobType(raw["type"]) self.blob_id = raw.get("blob_id") @@ -1823,7 +1823,7 @@ def _load(self, raw): self._mimetype = raw.get("mimetype") def save(self, clean=True): - ret = super(NodeBlob, self).save(clean) + ret = super().save(clean) ret["kind"] = "notes#blob" ret["type"] = self.type.value if self.blob_id is not None: @@ -1840,15 +1840,15 @@ class NodeAudio(NodeBlob): _TYPE = BlobType.Audio def __init__(self): - super(NodeAudio, self).__init__(type_=self._TYPE) + super().__init__(type_=self._TYPE) self._length = None def _load(self, raw): - super(NodeAudio, self)._load(raw) + super()._load(raw) self._length = raw.get("length") def save(self, clean=True): - ret = super(NodeAudio, self).save(clean) + ret = super().save(clean) if self._length is not None: ret["length"] = self._length return ret @@ -1868,7 +1868,7 @@ class NodeImage(NodeBlob): _TYPE = BlobType.Image def __init__(self): - super(NodeImage, self).__init__(type_=self._TYPE) + super().__init__(type_=self._TYPE) self._is_uploaded = False self._width = 0 self._height = 0 @@ -1877,7 +1877,7 @@ def __init__(self): self._extraction_status = "" def _load(self, raw): - super(NodeImage, self)._load(raw) + super()._load(raw) self._is_uploaded = raw.get("is_uploaded") or False self._width = raw.get("width") self._height = raw.get("height") @@ -1886,7 +1886,7 @@ def _load(self, raw): self._extraction_status = raw.get("extraction_status") def save(self, clean=True): - ret = super(NodeImage, self).save(clean) + ret = super().save(clean) ret["width"] = self._width ret["height"] = self._height ret["byte_size"] = self._byte_size @@ -1941,13 +1941,13 @@ class NodeDrawing(NodeBlob): _TYPE = BlobType.Drawing def __init__(self): - super(NodeDrawing, self).__init__(type_=self._TYPE) + super().__init__(type_=self._TYPE) self._extracted_text = "" self._extraction_status = "" self._drawing_info = None def _load(self, raw): - super(NodeDrawing, self)._load(raw) + super()._load(raw) self._extracted_text = raw.get("extracted_text") self._extraction_status = raw.get("extraction_status") drawing_info = None @@ -1957,7 +1957,7 @@ def _load(self, raw): self._drawing_info = drawing_info def save(self, clean=True): - ret = super(NodeDrawing, self).save(clean) + ret = super().save(clean) ret["extracted_text"] = self._extracted_text ret["extraction_status"] = self._extraction_status if self._drawing_info is not None: @@ -1981,7 +1981,7 @@ class NodeDrawingInfo(Element): """Represents information about a drawing blob.""" def __init__(self): - super(NodeDrawingInfo, self).__init__() + super().__init__() self.drawing_id = "" self.snapshot = NodeImage() self._snapshot_fingerprint = "" @@ -1990,7 +1990,7 @@ def __init__(self): self._snapshot_proto_fprint = "" def _load(self, raw): - super(NodeDrawingInfo, self)._load(raw) + super()._load(raw) self.drawing_id = raw["drawingId"] self.snapshot.load(raw["snapshotData"]) self._snapshot_fingerprint = ( @@ -2011,7 +2011,7 @@ def _load(self, raw): ) def save(self, clean=True): - ret = super(NodeDrawingInfo, self).save(clean) + ret = super().save(clean) ret["drawingId"] = self.drawing_id ret["snapshotData"] = self.snapshot.save(clean) ret["snapshotFingerprint"] = self._snapshot_fingerprint @@ -2033,7 +2033,7 @@ class Blob(Node): } def __init__(self, parent_id=None, **kwargs): - super(Blob, self).__init__(type_=NodeType.Blob, parent_id=parent_id, **kwargs) + super().__init__(type_=NodeType.Blob, parent_id=parent_id, **kwargs) self.blob = None @classmethod @@ -2067,11 +2067,11 @@ def from_json(cls: Type, raw: dict) -> NodeBlob | None: return blob def _load(self, raw): - super(Blob, self)._load(raw) + super()._load(raw) self.blob = self.from_json(raw.get("blob")) def save(self, clean=True): - ret = super(Blob, self).save(clean) + ret = super().save(clean) if self.blob is not None: ret["blob"] = self.blob.save(clean) return ret From 3728cc1aec665cc5244b67c62a4b9ae1ae579d00 Mon Sep 17 00:00:00 2001 From: Phillip Marshall Date: Tue, 4 Apr 2023 16:02:01 -0700 Subject: [PATCH 24/56] Black --- src/gkeepapi/node.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index a2ecd61..8426852 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -1463,11 +1463,7 @@ def url(self): @property def dirty(self): - return ( - super().dirty - or self.labels.dirty - or self.collaborators.dirty - ) + return super().dirty or self.labels.dirty or self.collaborators.dirty @property def blobs(self): @@ -1500,9 +1496,7 @@ class ListItem(Node): def __init__( self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs ): - super().__init__( - type_=NodeType.ListItem, parent_id=parent_id, **kwargs - ) + super().__init__(type_=NodeType.ListItem, parent_id=parent_id, **kwargs) self.parent_item = None self.parent_server_id = parent_server_id self.super_list_item_id = super_list_item_id From 4aacc2e983bcc8ac4678cffdd5cafc409dc0cd00 Mon Sep 17 00:00:00 2001 From: Phillip Marshall Date: Tue, 4 Apr 2023 16:05:40 -0700 Subject: [PATCH 25/56] switch from pylint to ruff for linting, adding black to Makefile:lint --- Makefile | 3 ++- pyproject.toml | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 2d297c3..aa1bc7b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: lint test coverage build clean upload all lint: - pylint src + -ruff --fix src + black src test: python3 -m unittest discover diff --git a/pyproject.toml b/pyproject.toml index 06b64f6..c22ba25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,11 +21,27 @@ dependencies = [ "future >= 0.16.0", ] optional-dependencies = [ - "pylint>=2.17.2", "black>=23.3.0", + "ruff>=0.0.260", ] [project.urls] "Homepage" = "https://github.com/kiwiz/gkeepapi" "Bug Tracker" = "https://github.com/kiwiz/gkeepapi/issues" + + +[tool.black] +target-version = ["py310"] + +[tool.ruff] +target-version = "py310" +select = [ + # https://beta.ruff.rs/docs/rules/ + "E", # pycodestyle (on by ruff's default) + "F", # Pyflakes (on by ruff's default) + "PL", # pylint +] +ignore = [ + "E501", # line-too-long -- disabled as black takes care of this +] From 37698b2cd6d239b756ba72fcb27110748114c4a7 Mon Sep 17 00:00:00 2001 From: Phillip Marshall Date: Tue, 4 Apr 2023 16:17:38 -0700 Subject: [PATCH 26/56] only one line auto-fixed by ruff with defaults --- src/gkeepapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 592ad2c..50d4955 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -1199,7 +1199,7 @@ def _findDirtyNodes(self) -> list[_node.Node]: # Find nodes that aren't in our internal nodes list and insert them. for node in list(self._nodes.values()): for child in node.children: - if not child.id in self._nodes: + if child.id not in self._nodes: self._nodes[child.id] = child nodes = [] From 781e38ea2f059b25ede798a188de36b5cf41ef2d Mon Sep 17 00:00:00 2001 From: Phillip Marshall Date: Tue, 4 Apr 2023 16:21:52 -0700 Subject: [PATCH 27/56] enable more ruff rulesets: isort pyupgrade flake8-{comprehensions,pie,simplify} ruff --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index c22ba25..ee8e8ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,7 +40,13 @@ select = [ # https://beta.ruff.rs/docs/rules/ "E", # pycodestyle (on by ruff's default) "F", # Pyflakes (on by ruff's default) + "I", # isort + "UP", # pyupgrade + "C4", # flake8-comprehensions + "PIE", # flake8-pie + "SIM", # flake8-simplify "PL", # pylint + "RUF", # ruff ] ignore = [ "E501", # line-too-long -- disabled as black takes care of this From ef2d9c3167d344fa3204e42790737b8971468ee8 Mon Sep 17 00:00:00 2001 From: Phillip Marshall Date: Tue, 4 Apr 2023 16:30:22 -0700 Subject: [PATCH 28/56] auto-fixed 32 problems with ruff's expanded rules --- src/gkeepapi/__init__.py | 22 ++++++++-------- src/gkeepapi/exception.py | 1 - src/gkeepapi/node.py | 53 ++++++++++++++++++--------------------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 50d4955..1663924 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -1,24 +1,23 @@ -# -*- coding: utf-8 -*- """ .. moduleauthor:: Kai """ __version__ = "1.0.0" +import datetime import logging +import random import re import time -import datetime -import random -from typing import Callable, Iterator, Tuple, Any - +from collections.abc import Callable, Iterator +from typing import Any from uuid import getnode as get_mac import gpsoauth import requests -from . import node as _node from . import exception +from . import node as _node logger = logging.getLogger(__name__) @@ -160,9 +159,8 @@ def refresh(self) -> str: client_sig="38918a453d07199354f8b19af05ec6562ced5788", ) # Bail if no token was returned. - if "Auth" not in res: - if "Token" not in res: - raise exception.LoginException(res.get("Error")) + if "Auth" not in res and "Token" not in res: + raise exception.LoginException(res.get("Error")) self._auth_token = res["Auth"] return self._auth_token @@ -865,7 +863,7 @@ def find( and ( # Process the labels. labels is None or (not labels and not node.labels.all()) - or (any((node.labels.get(i) is not None for i in labels))) + or (any(node.labels.get(i) is not None for i in labels)) ) and (colors is None or node.color in colors) # Process the colors. and (pinned is None or node.pinned == pinned) # Process the pinned state. @@ -898,7 +896,7 @@ def createNote( def createList( self, title: str | None = None, - items: list[Tuple[str, bool]] | None = None, + items: list[tuple[str, bool]] | None = None, ) -> _node.List: """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. @@ -1062,7 +1060,7 @@ def _sync_notes(self): logger.debug("Starting keep sync: %s", self._keep_version) # Collect any changes and send them up to the server. - labels_updated = any((i.dirty for i in self._labels.values())) + labels_updated = any(i.dirty for i in self._labels.values()) changes = self._keep_api.changes( target_version=self._keep_version, nodes=[i.save() for i in self._findDirtyNodes()], diff --git a/src/gkeepapi/exception.py b/src/gkeepapi/exception.py index 748872d..c519d52 100644 --- a/src/gkeepapi/exception.py +++ b/src/gkeepapi/exception.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ .. moduleauthor:: Kai """ diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 8426852..02b4bf1 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ .. automodule:: gkeepapi :members: @@ -8,15 +7,14 @@ """ import datetime -import logging -import time -import random import enum import itertools +import logging +import random +import time +from collections.abc import Callable from operator import attrgetter -from typing import Type, Tuple, Callable - from . import exception DEBUG = False @@ -194,7 +192,7 @@ def _find_discrepancies(self, raw): # pragma: no cover logger.info("Missing key for %s key %s", type(self), key) continue - if isinstance(val, (list, dict)): + if isinstance(val, list | dict): continue val_a = raw[key] @@ -217,14 +215,13 @@ def _find_discrepancies(self, raw): # pragma: no cover raw[key], s_raw[key], ) - elif isinstance(raw, list): - if len(raw) != len(s_raw): - logger.info( - "Different length for %s: %d != %d", - type(self), - len(raw), - len(s_raw), - ) + elif isinstance(raw, list) and len(raw) != len(s_raw): + logger.info( + "Different length for %s: %d != %d", + type(self), + len(raw), + len(s_raw), + ) def load(self, raw: dict): """Unserialize from raw representation. (Wrapper) @@ -294,7 +291,7 @@ def save(self, clean=True) -> dict: @classmethod def _generateAnnotationId(cls) -> str: - return "%08x-%04x-%04x-%04x-%012x" % ( + return "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}".format( random.randint(0x00000000, 0xFFFFFFFF), random.randint(0x0000, 0xFFFF), random.randint(0x0000, 0xFFFF), @@ -502,7 +499,7 @@ def all(self) -> list[Annotation]: @property def dirty(self) -> bool: return super().dirty or any( - (annotation.dirty for annotation in self._entries.values()) + annotation.dirty for annotation in self._entries.values() ) @@ -641,7 +638,7 @@ def remove(self, annotation: Annotation) -> None: @property def dirty(self) -> bool: return super().dirty or any( - (annotation.dirty for annotation in self._annotations.values()) + annotation.dirty for annotation in self._annotations.values() ) @@ -896,7 +893,7 @@ def load( collaborator["type"] ) - def save(self, clean=True) -> Tuple[list, list]: + def save(self, clean=True) -> tuple[list, list]: # Parent method not called. collaborators = [] requests = [] @@ -1023,7 +1020,7 @@ def __init__(self): @classmethod def _generateId(cls, tz): - return "tag.%s.%x" % ( + return "tag.{}.{:x}".format( "".join( [ random.choice("abcdefghijklmnopqrstuvwxyz0123456789") @@ -1108,7 +1105,7 @@ def _load(self, raw: list): for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean=True) -> Tuple[dict] | Tuple[dict, bool]: + def save(self, clean=True) -> tuple[dict] | tuple[dict, bool]: # Parent method not called. ret = [ { @@ -1187,7 +1184,7 @@ def __init__(self, id_=None, type_=None, parent_id=None): @classmethod def _generateId(cls, tz): - return "%x.%016x" % ( + return "{:x}.{:016x}".format( int(tz * 1000), random.randint(0x0000000000000000, 0xFFFFFFFFFFFFFFFF), ) @@ -1335,7 +1332,7 @@ def dirty(self): or self.timestamps.dirty or self.annotations.dirty or self.settings.dirty - or any((node.dirty for node in self.children)) + or any(node.dirty for node in self.children) ) @@ -1601,7 +1598,7 @@ def checked(self, value): self.touch(True) def __str__(self): - return "%s%s %s" % ( + return "{}{} {}".format( " " if self.indented else "", "☑" if self.checked else "☐", self.text, @@ -1682,7 +1679,7 @@ def add( func = min delta *= -1 - node.sort = func((int(item.sort) for item in items)) + delta + node.sort = func(int(item.sort) for item in items) + delta self.append(node, True) self.touch(True) @@ -1690,7 +1687,7 @@ def add( @property def text(self): - return "\n".join((str(node) for node in self.items)) + return "\n".join(str(node) for node in self.items) @classmethod def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: @@ -1765,7 +1762,7 @@ def sort_items(self, key: Callable = attrgetter("text"), reverse=False): sort_value -= self.SORT_DELTA def __str__(self) -> str: - return "\n".join(([self.title] + [str(node) for node in self.items])) + return "\n".join([self.title] + [str(node) for node in self.items]) @property def items(self) -> list[ListItem]: @@ -2031,7 +2028,7 @@ def __init__(self, parent_id=None, **kwargs): self.blob = None @classmethod - def from_json(cls: Type, raw: dict) -> NodeBlob | None: + def from_json(cls: type, raw: dict) -> NodeBlob | None: """Helper to construct a blob from a dict. Args: From b08ccbacb1341c40564baa7722adf8746ce2dcec Mon Sep 17 00:00:00 2001 From: K Date: Wed, 5 Apr 2023 18:28:23 -0400 Subject: [PATCH 29/56] Clean up some old configs --- .gitignore | 1 + .pylintrc | 4 ---- .travis.yml | 17 ----------------- Makefile | 4 ++-- 4 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 .pylintrc delete mode 100644 .travis.yml diff --git a/.gitignore b/.gitignore index 2809739..ae6550e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ *.pyc *.egg-info/ .coverage +.ruff_cache/ diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index fed055d..0000000 --- a/.pylintrc +++ /dev/null @@ -1,4 +0,0 @@ -[BASIC] -good-names=logger -[MESSAGES CONTROL] -disable=line-too-long,too-many-instance-attributes,invalid-name,too-many-lines,useless-object-inheritance diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 2434887..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: python -python: -- "3.5" -- "3.6" -- "3.7" -install: -- pip install -r requirements.txt -- pip install coverage -before_script: -- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter -- chmod +x ./cc-test-reporter -- ./cc-test-reporter before-build -script: -- coverage run --source gkeepapi -m unittest discover -after_script: -- coverage xml -- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/Makefile b/Makefile index aa1bc7b..32528bb 100644 --- a/Makefile +++ b/Makefile @@ -16,9 +16,9 @@ build: src/gkeepapi/*.py python3 -m build clean: - rm -f dist/*.whl + rm -f build dist upload: twine upload dist/*.whl -all: build upload +all: lint test build upload From 516dfb65193d1f821c9fdb850798f7560a9fa599 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 7 Apr 2023 14:24:51 -0400 Subject: [PATCH 30/56] Experimentally apply more lint rules --- pyproject.toml | 80 +++++++++--- src/gkeepapi/__init__.py | 114 +++++++++++++---- src/gkeepapi/exception.py | 10 +- src/gkeepapi/node.py | 260 ++++++++++++++++++++++++++------------ 4 files changed, 333 insertions(+), 131 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ee8e8ed..da69cda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,13 +8,13 @@ description = "An unofficial Google Keep API client" readme = "README.md" requires-python = ">=3.10" classifiers = [ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Development Status :: 4 - Beta", - "Operating System :: OS Independent", - "Intended Audience :: Developers", - "Topic :: Internet :: WWW/HTTP", - "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Development Status :: 4 - Beta", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ "gpsoauth >= 1.0.2", @@ -22,7 +22,7 @@ dependencies = [ ] optional-dependencies = [ "black>=23.3.0", - "ruff>=0.0.260", + "ruff>=0.0.261", ] @@ -37,17 +37,59 @@ target-version = ["py310"] [tool.ruff] target-version = "py310" select = [ - # https://beta.ruff.rs/docs/rules/ - "E", # pycodestyle (on by ruff's default) - "F", # Pyflakes (on by ruff's default) - "I", # isort - "UP", # pyupgrade - "C4", # flake8-comprehensions - "PIE", # flake8-pie - "SIM", # flake8-simplify - "PL", # pylint - "RUF", # ruff + # https://beta.ruff.rs/docs/rules/ + "F", # pyflakes + "E", # pycodestyle + "W", # pycodestyle + "C90", # mccabe + "I", # isort + "N", # pep8-naming + "D", # pydocstyle + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "S", # flake8-bandit + "BLE", # flake8-blind-except + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "EXE", # flake8-executable + "ISC", # flake8-implicit-str-concat + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "PYI", # flake8-pyi + "PT", # flake8-pytest-style + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-type-checking + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "PGH", # pygrep-hooks + "PLC", # pylint + "PLE", # pylint + "PLR", # pylint + "PLW", # pylint + "TRY", # tryceratops + "RUF", # ruff-specific rules ] ignore = [ - "E501", # line-too-long -- disabled as black takes care of this + "E501", # line-too-long -- disabled as black takes care of this + "N802", + "COM812", + "D400", + "D415", ] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 1663924..69c9659 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -1,6 +1,4 @@ -""" -.. moduleauthor:: Kai -""" +""".. moduleauthor:: Kai """ __version__ = "1.0.0" @@ -25,7 +23,7 @@ class APIAuth: """Authentication token manager""" - def __init__(self, scopes: str): + def __init__(self, scopes: str) -> None: self._master_token = None self._auth_token = None self._email = None @@ -36,11 +34,13 @@ def login(self, email: str, password: str, device_id: str): """Authenticate to Google with the provided credentials. Args: + ---- email: The account to use. password: The account password. device_id: An identifier for this client. Raises: + ------ LoginException: If there was a problem logging in. """ self._email = email @@ -66,11 +66,13 @@ def load(self, email: str, master_token: str, device_id: str) -> bool: """Authenticate to Google with the provided master token. Args: + ---- email: The account to use. master_token: The master token. device_id: An identifier for this client. Raises: + ------ LoginException: If there was a problem logging in. """ self._email = email @@ -84,7 +86,8 @@ def load(self, email: str, master_token: str, device_id: str) -> bool: def getMasterToken(self) -> str: """Gets the master token. - Returns: + Returns + ------- The account master token. """ return self._master_token @@ -95,6 +98,7 @@ def setMasterToken(self, master_token: str): Do note that the master token has full access to your account. Args: + ---- master_token: The account master token. """ self._master_token = master_token @@ -102,7 +106,8 @@ def setMasterToken(self, master_token: str): def getEmail(self) -> str: """Gets the account email. - Returns: + Returns + ------- The account email. """ return self._email @@ -111,6 +116,7 @@ def setEmail(self, email: str): """Sets the account email. Args: + ---- email: The account email. """ self._email = email @@ -118,7 +124,8 @@ def setEmail(self, email: str): def getDeviceId(self) -> str: """Gets the device id. - Returns: + Returns + ------- The device id. """ return self._device_id @@ -127,6 +134,7 @@ def setDeviceId(self, device_id: str): """Sets the device id. Args: + ---- device_id: The device id. """ self._device_id = device_id @@ -134,7 +142,8 @@ def setDeviceId(self, device_id: str): def getAuthToken(self) -> str | None: """Gets the auth token. - Returns: + Returns + ------- The auth token. """ return self._auth_token @@ -142,10 +151,12 @@ def getAuthToken(self) -> str | None: def refresh(self) -> str: """Refresh the OAuth token. - Returns: + Returns + ------- The auth token. - Raises: + Raises + ------ LoginException: If there was a problem refreshing the OAuth token. """ # Obtain an OAuth token with the necessary scopes by pretending to be @@ -178,7 +189,7 @@ class API: RETRY_CNT = 2 - def __init__(self, base_url: str, auth: APIAuth | None = None): + def __init__(self, base_url: str, auth: APIAuth | None = None) -> None: self._session = requests.Session() self._auth = auth self._base_url = base_url @@ -193,6 +204,7 @@ def getAuth(self) -> APIAuth: """Get authentication details for this API. Return: + ------ auth: The auth object """ return self._auth @@ -201,6 +213,7 @@ def setAuth(self, auth: APIAuth): """Set authentication details for this API. Args: + ---- auth: The auth object """ self._auth = auth @@ -210,12 +223,15 @@ def send(self, **req_kwargs) -> dict: Automatically retries if the access token has expired. Args: + ---- **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: + ------ The parsed JSON response. Raises: + ------ APIException: If the server returns an error. LoginException: If :py:meth:`login` has not been called. """ @@ -249,12 +265,15 @@ def _send(self, **req_kwargs) -> requests.Response: """Send an authenticated request to a Google API. Args: + ---- **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: + ------ The raw response. Raises: + ------ LoginException: If :py:meth:`login` has not been called. """ # Bail if we don't have an OAuth token. @@ -276,7 +295,7 @@ class KeepAPI(API): API_URL = "https://www.googleapis.com/notes/v1/" - def __init__(self, auth: APIAuth | None = None): + def __init__(self, auth: APIAuth | None = None) -> None: super().__init__(self.API_URL, auth) create_time = time.time() @@ -295,14 +314,17 @@ def changes( """Sync up (and down) all changes. Args: + ---- target_version: The local change version. nodes: A list of nodes to sync up to the server. labels: A list of labels to sync up to the server. Return: + ------ Description of all changes. Raises: + ------ APIException: If the server returns an error. """ # Handle defaults. @@ -368,16 +390,18 @@ class MediaAPI(API): API_URL = "https://keep.google.com/media/v2/" - def __init__(self, auth: APIAuth | None = None): + def __init__(self, auth: APIAuth | None = None) -> None: super().__init__(self.API_URL, auth) def get(self, blob: _node.Blob) -> str: """Get the canonical link to a media blob. Args: + ---- blob: The blob. Returns: + ------- A link to the media. """ url = self._base_url + blob.parent.server_id + "/" + blob.server_id @@ -396,7 +420,7 @@ class RemindersAPI(API): API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" - def __init__(self, auth: APIAuth | None = None): + def __init__(self, auth: APIAuth | None = None) -> None: super().__init__(self.API_URL, auth) self.static_params = { "taskList": [ @@ -420,6 +444,7 @@ def create( """Create a new reminder. Args: + ---- node_id: The note ID. node_server_id: The note server ID. dtime: The due date of this reminder. @@ -427,9 +452,9 @@ def create( Return: ??? Raises: + ------ APIException: If the server returns an error. """ - params = {} params.update(self.static_params) @@ -469,6 +494,7 @@ def update( """Update an existing reminder. Args: + ---- node_id: The note ID. node_server_id: The note server ID. dtime: The due date of this reminder. @@ -476,6 +502,7 @@ def update( Return: ??? Raises: + ------ APIException: If the server returns an error. """ params = {} @@ -524,14 +551,15 @@ def delete(self, node_server_id: str) -> Any: """Delete an existing reminder. Args: + ---- node_server_id: The note server ID. Return: ??? Raises: + ------ APIException: If the server returns an error. """ - params = {} params.update(self.static_params) @@ -555,12 +583,15 @@ def list(self, master=True) -> Any: """List current reminders. Args: + ---- master: ??? Return: + ------ ??? Raises: + ------ APIException: If the server returns an error. """ params = {} @@ -602,12 +633,15 @@ def history(self, storage_version: str) -> Any: """Get reminder changes. Args: + ---- storage_version (str): The local storage version. Returns: + ------- ??? Raises: + ------ APIException: If the server returns an error. """ params = { @@ -651,7 +685,7 @@ class Keep: OAUTH_SCOPES = "oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders" - def __init__(self): + def __init__(self) -> None: self._keep_api = KeepAPI() self._reminders_api = RemindersAPI() self._media_api = MediaAPI() @@ -684,6 +718,7 @@ def login( """Authenticate to Google with the provided credentials & sync. Args: + ---- email: The account to use. password: The account password. state: Serialized state to load. @@ -691,6 +726,7 @@ def login( device_id: Device id. Raises: + ------ LoginException: If there was a problem logging in. """ auth = APIAuth(self.OAUTH_SCOPES) @@ -711,6 +747,7 @@ def resume( """Authenticate to Google with the provided master token & sync. Args: + ---- email: The account to use. master_token: The master token. state: Serialized state to load. @@ -718,6 +755,7 @@ def resume( device_id: Device id. Raises: + ------ LoginException: If there was a problem logging in. """ auth = APIAuth(self.OAUTH_SCOPES) @@ -730,19 +768,23 @@ def resume( def getMasterToken(self) -> str: """Get master token for resuming. - Returns: + Returns + ------- The master token. """ return self._keep_api.getAuth().getMasterToken() def load(self, auth: APIAuth, state: dict | None = None, sync=True): """Authenticate to Google with a prepared authentication object & sync. + Args: + ---- auth: Authentication object. state: Serialized state to load. sync: Whether to sync data. Raises: + ------ LoginException: If there was a problem logging in. """ self._keep_api.setAuth(auth) @@ -756,7 +798,8 @@ def load(self, auth: APIAuth, state: dict | None = None, sync=True): def dump(self) -> dict: """Serialize note data. - Returns: + Returns + ------- Serialized state. """ # Find all nodes manually, as the Keep object isn't aware of new @@ -776,6 +819,7 @@ def restore(self, state: dict): """Unserialize saved note data. Args: + ---- state: Serialized state to load. """ self._clear() @@ -787,9 +831,11 @@ def get(self, node_id: str) -> _node.TopLevelNode: """Get a note with the given ID. Args: + ---- node_id: The note ID. Returns: + ------- The Note or None if not found. """ return self._nodes[_node.Root.ID].get(node_id) or self._nodes[ @@ -801,9 +847,11 @@ def add(self, node: _node.Node): :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. Args: + ---- node: The node to sync. Raises: + ------ InvalidException: If the parent node is not found. """ if node.parent_id != _node.Root.ID: @@ -825,6 +873,7 @@ def find( """Find Notes based on the specified criteria. Args: + ---- query: A str or regular expression to match against the title and text. func: A filter function. labels: A list of label ids or objects to match. An empty list matches notes with no labels. @@ -834,6 +883,7 @@ def find( trashed: Whether to match trashed notes. Return: + ------ Search results. """ if labels is not None: @@ -879,10 +929,12 @@ def createNote( """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: + ---- title: The title of the note. text: The text of the note. Returns: + ------- The new note. """ node = _node.Note() @@ -901,10 +953,12 @@ def createList( """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: + ---- title: The title of the list. items: A list of tuples. Each tuple represents the text and checked status of the listitem. Returns: + ------- The new list. """ if items is None: @@ -925,12 +979,15 @@ def createLabel(self, name: str) -> _node.Label: """Create a new label. Args: + ---- name: Label name. Returns: + ------- The new label. Raises: + ------ LabelException: If the label exists. """ if self.findLabel(name): @@ -944,10 +1001,12 @@ def findLabel(self, query: re.Pattern | str, create=False) -> _node.Label | None """Find a label with the given name. Args: + ---- name: A str or regular expression to match against the name. create: Whether to create the label if it doesn't exist (only if name is a str). Returns: + ------- The label. """ is_str = isinstance(query, str) @@ -969,9 +1028,11 @@ def getLabel(self, label_id: str) -> _node.Label | None: """Get an existing label. Args: + ---- label_id: Label id. Returns: + ------- The label. """ return self._labels.get(label_id) @@ -980,6 +1041,7 @@ def deleteLabel(self, label_id: str): """Deletes a label. Args: + ---- label_id: Label id. """ if label_id not in self._labels: @@ -993,7 +1055,8 @@ def deleteLabel(self, label_id: str): def labels(self) -> list[_node.Label]: """Get all labels. - Returns: + Returns + ------- Labels """ return list(self._labels.values()) @@ -1002,9 +1065,11 @@ def getMediaLink(self, blob: _node.Blob) -> str: """Get the canonical link to media. Args: + ---- blob: The media resource. Returns: + ------- A link to the media. """ return self._media_api.get(blob) @@ -1012,8 +1077,11 @@ def getMediaLink(self, blob: _node.Blob) -> str: def all(self) -> list[_node.TopLevelNode]: """Get all Notes. - Returns: - Notes + Returns + ------- + + Notes + ----- """ return self._nodes[_node.Root.ID].children @@ -1021,9 +1089,11 @@ def sync(self, resync=False): """Sync the local Keep tree with the server. If resyncing, local changes will be destroyed. Otherwise, local changes to notes, labels and reminders will be detected and synced up. Args: + ---- resync: Whether to resync data. Raises: + ------ SyncException: If there is a consistency issue. """ # Clear all state if we want to resync. diff --git a/src/gkeepapi/exception.py b/src/gkeepapi/exception.py index c519d52..46e3b9e 100644 --- a/src/gkeepapi/exception.py +++ b/src/gkeepapi/exception.py @@ -1,12 +1,10 @@ -""" -.. moduleauthor:: Kai -""" +""".. moduleauthor:: Kai """ class APIException(Exception): """The API server returned an error.""" - def __init__(self, code: int, msg: str): + def __init__(self, code: int, msg: str) -> None: super().__init__(msg) self.code = code @@ -22,7 +20,7 @@ class LoginException(KeepException): class BrowserLoginRequiredException(LoginException): """Browser login required error.""" - def __init__(self, url): + def __init__(self, url) -> None: self.url = url @@ -53,6 +51,6 @@ class InvalidException(KeepException): class ParseException(KeepException): """Parse error.""" - def __init__(self, msg: str, raw: dict): + def __init__(self, msg: str, raw: dict) -> None: super().__init__(msg) self.raw = raw diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 02b4bf1..71f59b6 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -1,5 +1,4 @@ -""" -.. automodule:: gkeepapi +""".. automodule:: gkeepapi :members: :inherited-members: @@ -179,7 +178,7 @@ class RoleValue(enum.Enum): class Element: """Interface for elements that can be serialized and deserialized.""" - def __init__(self): + def __init__(self) -> None: self._dirty = False def _find_discrepancies(self, raw): # pragma: no cover @@ -227,8 +226,12 @@ def load(self, raw: dict): """Unserialize from raw representation. (Wrapper) Args: + ---- raw: Raw. + + Raises: + ------ ParseException: If there was an error parsing data. """ try: @@ -240,6 +243,7 @@ def _load(self, raw: dict): """Unserialize from raw representation. (Implementation logic) Args: + ---- raw: Raw. """ self._dirty = raw.get("_dirty", False) @@ -248,9 +252,11 @@ def save(self, clean=True) -> dict: """Serialize into raw representation. Clears the dirty bit by default. Args: + ---- clean: Whether to clear the dirty bit. Returns: + ------- Raw. """ ret = {} @@ -264,7 +270,8 @@ def save(self, clean=True) -> dict: def dirty(self) -> bool: """Get dirty state. - Returns: + Returns + ------- Whether this element is dirty. """ return self._dirty @@ -273,7 +280,7 @@ def dirty(self) -> bool: class Annotation(Element): """Note annotations base class.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.id = self._generateAnnotationId() @@ -303,7 +310,7 @@ def _generateAnnotationId(cls) -> str: class WebLink(Annotation): """Represents a link annotation on a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._title = "" self._url = "" @@ -338,7 +345,8 @@ def save(self, clean=True) -> dict: def title(self) -> str: """Get the link title. - Returns: + Returns + ------- The link title. """ return self._title @@ -352,7 +360,8 @@ def title(self, value: str) -> None: def url(self) -> str: """Get the link url. - Returns: + Returns + ------- The link url. """ return self._url @@ -366,7 +375,8 @@ def url(self, value: str) -> None: def image_url(self) -> str: """Get the link image url. - Returns: + Returns + ------- The image url or None. """ return self._image_url @@ -380,7 +390,8 @@ def image_url(self, value: str) -> None: def provenance_url(self) -> str: """Get the provenance url. - Returns: + Returns + ------- The provenance url. """ return self._provenance_url @@ -394,7 +405,8 @@ def provenance_url(self, value) -> None: def description(self) -> str: """Get the link description. - Returns: + Returns + ------- The link description. """ return self._description @@ -408,7 +420,7 @@ def description(self, value: str): class Category(Annotation): """Represents a category annotation on a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._category = None @@ -425,7 +437,8 @@ def save(self, clean=True) -> dict: def category(self) -> CategoryValue: """Get the category. - Returns: + Returns + ------- The category. """ return self._category @@ -439,7 +452,7 @@ def category(self, value: CategoryValue) -> None: class TaskAssist(Annotation): """Unknown.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._suggest = None @@ -456,7 +469,8 @@ def save(self, clean=True) -> dict: def suggest(self) -> str: """Get the suggestion. - Returns: + Returns + ------- The suggestion. """ return self._suggest @@ -470,7 +484,7 @@ def suggest(self, value) -> None: class Context(Annotation): """Represents a context annotation, which may contain other annotations.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._entries = {} @@ -491,7 +505,8 @@ def save(self, clean=True) -> dict: def all(self) -> list[Annotation]: """Get all sub annotations. - Returns: + Returns + ------- Sub Annotations. """ return list(self._entries.values()) @@ -506,11 +521,11 @@ def dirty(self) -> bool: class NodeAnnotations(Element): """Represents the annotation container on a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._annotations = {} - def __len__(self): + def __len__(self) -> int: return len(self._annotations) @classmethod @@ -518,9 +533,11 @@ def from_json(cls, raw: dict) -> Annotation | None: """Helper to construct an annotation from a dict. Args: + ---- raw: Raw annotation representation. Returns: + ------- An Annotation object or None. """ bcls = None @@ -544,7 +561,8 @@ def from_json(cls, raw: dict) -> Annotation | None: def all(self) -> list[Annotation]: """Get all annotations. - Returns: + Returns + ------- Annotations. """ return list(self._annotations.values()) @@ -578,7 +596,8 @@ def _get_category_node(self) -> Category | None: def category(self) -> CategoryValue | None: """Get the category. - Returns: + Returns + ------- The category. """ node = self._get_category_node() @@ -603,7 +622,8 @@ def category(self, value) -> None: def links(self) -> list[WebLink]: """Get all links. - Returns: + Returns + ------- A list of links. """ return [ @@ -616,9 +636,11 @@ def append(self, annotation: Annotation) -> Annotation: """Add an annotation. Args: + ---- annotation: An Annotation object. Returns: + ------- The Annotation. """ self._annotations[annotation.id] = annotation @@ -629,6 +651,7 @@ def remove(self, annotation: Annotation) -> None: """Removes an annotation. Args: + ---- annotation: An Annotation object. """ if annotation.id in self._annotations: @@ -647,7 +670,7 @@ class NodeTimestamps(Element): TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" - def __init__(self, create_time: float | None = None): + def __init__(self, create_time: float | None = None) -> None: super().__init__() if create_time is None: create_time = time.time() @@ -689,7 +712,8 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: Params: tsz: Datetime string. - Returns: + Returns + ------- Datetime. """ return datetime.datetime.strptime(tzs, cls.TZ_FMT) @@ -701,7 +725,8 @@ def int_to_dt(cls, tz: int | float) -> datetime.datetime: Params: ts: Unix timestamp. - Returns: + Returns + ------- Datetime. """ return datetime.datetime.utcfromtimestamp(tz) @@ -713,7 +738,8 @@ def dt_to_str(cls, dt: datetime.datetime) -> str: Params: dt: Datetime. - Returns: + Returns + ------- Datetime string. """ return dt.strftime(cls.TZ_FMT) @@ -722,7 +748,8 @@ def dt_to_str(cls, dt: datetime.datetime) -> str: def int_to_str(cls, tz: int) -> str: """Convert a unix timestamp to a str. - Returns: + Returns + ------- Datetime string. """ return cls.dt_to_str(cls.int_to_dt(tz)) @@ -731,7 +758,8 @@ def int_to_str(cls, tz: int) -> str: def created(self) -> datetime.datetime: """Get the creation datetime. - Returns: + Returns + ------- Datetime. """ return self._created @@ -745,7 +773,8 @@ def created(self, value) -> None: def deleted(self) -> datetime.datetime | None: """Get the deletion datetime. - Returns: + Returns + ------- Datetime. """ return self._deleted @@ -759,7 +788,8 @@ def deleted(self, value: datetime.datetime) -> None: def trashed(self) -> datetime.datetime | None: """Get the move-to-trash datetime. - Returns: + Returns + ------- Datetime. """ return self._trashed @@ -773,7 +803,8 @@ def trashed(self, value: datetime.datetime) -> None: def updated(self) -> datetime.datetime: """Get the updated datetime. - Returns: + Returns + ------- Datetime. """ return self._updated @@ -787,7 +818,8 @@ def updated(self, value: datetime.datetime) -> None: def edited(self) -> datetime.datetime: """Get the user edited datetime. - Returns: + Returns + ------- Datetime. """ return self._edited @@ -801,7 +833,7 @@ def edited(self, value: datetime.datetime) -> None: class NodeSettings(Element): """Represents the settings associated with a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._new_listitem_placement = NewListItemPlacementValue.Bottom self._graveyard_state = GraveyardStateValue.Collapsed @@ -828,7 +860,8 @@ def save(self, clean=True) -> dict: def new_listitem_placement(self) -> NewListItemPlacementValue: """Get the default location to insert new listitems. - Returns: + Returns + ------- Placement. """ return self._new_listitem_placement @@ -842,7 +875,8 @@ def new_listitem_placement(self, value: NewListItemPlacementValue) -> None: def graveyard_state(self) -> GraveyardStateValue: """Get the visibility state for the list graveyard. - Returns: + Returns + ------- Visibility. """ return self._graveyard_state @@ -856,7 +890,8 @@ def graveyard_state(self, value: GraveyardStateValue) -> None: def checked_listitems_policy(self) -> CheckedListItemsPolicyValue: """Get the policy for checked listitems. - Returns: + Returns + ------- Policy. """ return self._checked_listitems_policy @@ -870,7 +905,7 @@ def checked_listitems_policy(self, value: CheckedListItemsPolicyValue) -> None: class NodeCollaborators(Element): """Represents the collaborators on a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._collaborators = {} @@ -914,6 +949,7 @@ def add(self, email: str) -> None: """Add a collaborator. Args: + ---- email: Collaborator email address. """ if email not in self._collaborators: @@ -924,6 +960,7 @@ def remove(self, email: str) -> None: """Remove a Collaborator. Args: + ---- email: Collaborator email address. """ if email in self._collaborators: @@ -936,7 +973,8 @@ def remove(self, email: str) -> None: def all(self) -> list[str]: """Get all collaborators. - Returns: + Returns + ------- Collaborators. """ return [ @@ -949,13 +987,14 @@ def all(self) -> list[str]: class TimestampsMixin: """A mixin to add methods for updating timestamps.""" - def __init__(self): + def __init__(self) -> None: self.timestamps: NodeTimestamps def touch(self, edited=False): """Mark the node as dirty. Args: + ---- edited: Whether to set the edited time. """ self._dirty = True @@ -968,7 +1007,8 @@ def touch(self, edited=False): def trashed(self): """Get the trashed state. - Returns: + Returns + ------- bool: Whether this item is trashed. """ return ( @@ -988,7 +1028,8 @@ def untrash(self): def deleted(self): """Get the deleted state. - Returns: + Returns + ------- bool: Whether this item is deleted. """ return ( @@ -1008,7 +1049,7 @@ def undelete(self): class Label(Element, TimestampsMixin): """Represents a label.""" - def __init__(self): + def __init__(self) -> None: super().__init__() create_time = time.time() @@ -1053,7 +1094,8 @@ def save(self, clean=True): def name(self): """Get the label name. - Returns: + Returns + ------- str: Label name. """ return self._name @@ -1067,7 +1109,8 @@ def name(self, value): def merged(self): """Get last merge datetime. - Returns: + Returns + ------- datetime: Datetime. """ return self._merged @@ -1081,14 +1124,14 @@ def merged(self, value): def dirty(self): return super().dirty or self.timestamps.dirty - def __str__(self): + def __str__(self) -> str: return self.name class NodeLabels(Element): """Represents the labels on a :class:`TopLevelNode`.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self._labels = {} @@ -1126,6 +1169,7 @@ def add(self, label: Label) -> None: """Add a label. Args: + ---- label: The Label object. """ self._labels[label.id] = label @@ -1135,6 +1179,7 @@ def remove(self, label: Label): """Remove a label. Args: + ---- label: The Label object. """ if label.id in self._labels: @@ -1145,6 +1190,7 @@ def get(self, label_id: str): """Get a label by ID. Args: + ---- label_id: The label ID. """ return self._labels.get(label_id) @@ -1152,7 +1198,8 @@ def get(self, label_id: str): def all(self): """Get all labels. - Returns: + Returns + ------- list[gkeepapi.node.Label]: Labels. """ return [label for _, label in self._labels.items() if label is not None] @@ -1161,7 +1208,7 @@ def all(self): class Node(Element, TimestampsMixin): """Node base class.""" - def __init__(self, id_=None, type_=None, parent_id=None): + def __init__(self, id_=None, type_=None, parent_id=None) -> None: super().__init__() create_time = time.time() @@ -1230,7 +1277,8 @@ def save(self, clean=True): def sort(self): """Get the sort id. - Returns: + Returns + ------- int: Sort id. """ return int(self._sort) @@ -1244,7 +1292,8 @@ def sort(self, value): def version(self): """Get the node version. - Returns: + Returns + ------- int: Version. """ return self._version @@ -1253,7 +1302,8 @@ def version(self): def text(self): """Get the text value. - Returns: + Returns + ------- str: Text value. """ return self._text @@ -1263,6 +1313,7 @@ def text(self, value: str): """Set the text value. Args: + ---- value: Text value. """ self._text = value @@ -1273,7 +1324,8 @@ def text(self, value: str): def children(self) -> list["Node"]: """Get all children. - Returns: + Returns + ------- Children nodes. """ return list(self._children.values()) @@ -1282,9 +1334,11 @@ def get(self, node_id: str) -> "Node | None": """Get child node with the given ID. Args: + ---- node_id: The node ID. Returns: + ------- Child node. """ return self._children.get(node_id) @@ -1293,6 +1347,7 @@ def append(self, node: "Node", dirty=True): """Add a new child node. Args: + ---- node: Node to add. dirty: Whether this node should be marked dirty. """ @@ -1307,6 +1362,7 @@ def remove(self, node: "Node", dirty=True): """Remove the given child node. Args: + ---- node: Node to remove. dirty: Whether this node should be marked dirty. """ @@ -1320,7 +1376,8 @@ def remove(self, node: "Node", dirty=True): def new(self): """Get whether this node has been persisted to the server. - Returns: + Returns + ------- bool: True if node is new. """ return self.server_id is None @@ -1341,7 +1398,7 @@ class Root(Node): ID = "root" - def __init__(self): + def __init__(self) -> None: super().__init__(id_=self.ID) @property @@ -1354,7 +1411,7 @@ class TopLevelNode(Node): _TYPE = None - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(parent_id=Root.ID, **kwargs) self._color = ColorValue.White self._archived = False @@ -1397,7 +1454,8 @@ def save(self, clean=True): def color(self): """Get the node color. - Returns: + Returns + ------- gkeepapi.node.Color: Color. """ return self._color @@ -1411,7 +1469,8 @@ def color(self, value): def archived(self): """Get the archive state. - Returns: + Returns + ------- bool: Whether this node is archived. """ return self._archived @@ -1425,7 +1484,8 @@ def archived(self, value): def pinned(self): """Get the pin state. - Returns: + Returns + ------- bool: Whether this node is pinned. """ return self._pinned @@ -1439,7 +1499,8 @@ def pinned(self, value): def title(self): """Get the title. - Returns: + Returns + ------- str: Title. """ return self._title @@ -1453,7 +1514,8 @@ def title(self, value): def url(self): """Get the url for this node. - Returns: + Returns + ------- str: Google Keep url. """ return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @@ -1466,7 +1528,8 @@ def dirty(self): def blobs(self): """Get all media blobs. - Returns: + Returns + ------- list[gkeepapi.node.Blob]: Media blobs. """ return [node for node in self.children if isinstance(node, Blob)] @@ -1492,7 +1555,7 @@ class ListItem(Node): def __init__( self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs - ): + ) -> None: super().__init__(type_=NodeType.ListItem, parent_id=parent_id, **kwargs) self.parent_item = None self.parent_server_id = parent_server_id @@ -1523,6 +1586,7 @@ def add( """Add a new sub item to the list. This item must already be attached to a list. Args: + ---- text: The text. checked: Whether this item is checked. sort: Item id for sorting. @@ -1537,6 +1601,7 @@ def indent(self, node: "ListItem", dirty=True): """Indent an item. Does nothing if the target has subitems. Args: + ---- node: Item to indent. dirty: Whether this node should be marked dirty. """ @@ -1553,6 +1618,7 @@ def dedent(self, node: "ListItem", dirty=True): """Dedent an item. Does nothing if the target is not indented under this item. Args: + ---- node: Item to dedent. dirty : Whether this node should be marked dirty. """ @@ -1569,7 +1635,8 @@ def dedent(self, node: "ListItem", dirty=True): def subitems(self): """Get subitems for this item. - Returns: + Returns + ------- list[gkeepapi.node.ListItem]: Subitems. """ return List.sorted_items(self._subitems.values()) @@ -1578,7 +1645,8 @@ def subitems(self): def indented(self): """Get indentation state. - Returns: + Returns + ------- bool: Whether this item is indented. """ return self.parent_item is not None @@ -1587,7 +1655,8 @@ def indented(self): def checked(self): """Get the checked state. - Returns: + Returns + ------- bool: Whether this item is checked. """ return self._checked @@ -1597,7 +1666,7 @@ def checked(self, value): self._checked = value self.touch(True) - def __str__(self): + def __str__(self) -> str: return "{}{} {}".format( " " if self.indented else "", "☑" if self.checked else "☐", @@ -1610,7 +1679,7 @@ class Note(TopLevelNode): _TYPE = NodeType.Note - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(type_=self._TYPE, **kwargs) def _get_text_node(self): @@ -1639,7 +1708,7 @@ def text(self, value): node.text = value self.touch(True) - def __str__(self): + def __str__(self) -> str: return "\n".join([self.title, self.text]) @@ -1649,7 +1718,7 @@ class List(TopLevelNode): _TYPE = NodeType.List SORT_DELTA = 10000 # Arbitrary constant - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: super().__init__(type_=self._TYPE, **kwargs) def add( @@ -1661,6 +1730,7 @@ def add( """Add a new item to the list. Args: + ---- text: The text. checked: Whether this item is checked. sort: Item id for sorting or a placement policy. @@ -1694,8 +1764,12 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: """Generate a list of sorted list items, taking into account parent items. Args: + ---- items: Items to sort. + + Returns: + ------- Sorted items. """ @@ -1751,6 +1825,7 @@ def sort_items(self, key: Callable = attrgetter("text"), reverse=False): but a custom function can be specified. Args: + ---- key: A filter function. reverse: Whether to reverse the output. """ @@ -1768,7 +1843,8 @@ def __str__(self) -> str: def items(self) -> list[ListItem]: """Get all listitems. - Returns: + Returns + ------- List items. """ return self.sorted_items(self._items()) @@ -1777,7 +1853,8 @@ def items(self) -> list[ListItem]: def checked(self) -> list[ListItem]: """Get all checked listitems. - Returns: + Returns + ------- List items. """ return self.sorted_items(self._items(True)) @@ -1786,7 +1863,8 @@ def checked(self) -> list[ListItem]: def unchecked(self) -> list[ListItem]: """Get all unchecked listitems. - Returns: + Returns + ------- List items. """ return self.sorted_items(self._items(False)) @@ -1797,7 +1875,7 @@ class NodeBlob(Element): _TYPE = None - def __init__(self, type_=None): + def __init__(self, type_=None) -> None: super().__init__() self.blob_id = None self.type = type_ @@ -1830,7 +1908,7 @@ class NodeAudio(NodeBlob): _TYPE = BlobType.Audio - def __init__(self): + def __init__(self) -> None: super().__init__(type_=self._TYPE) self._length = None @@ -1847,7 +1925,9 @@ def save(self, clean=True): @property def length(self): """Get length of the audio clip. - Returns: + + Returns + ------- int: Audio length. """ return self._length @@ -1858,7 +1938,7 @@ class NodeImage(NodeBlob): _TYPE = BlobType.Image - def __init__(self): + def __init__(self) -> None: super().__init__(type_=self._TYPE) self._is_uploaded = False self._width = 0 @@ -1888,7 +1968,9 @@ def save(self, clean=True): @property def width(self): """Get width of image. - Returns: + + Returns + ------- int: Image width. """ return self._width @@ -1896,7 +1978,9 @@ def width(self): @property def height(self): """Get height of image. - Returns: + + Returns + ------- int: Image height. """ return self._height @@ -1904,7 +1988,9 @@ def height(self): @property def byte_size(self): """Get size of image in bytes. - Returns: + + Returns + ------- int: Image byte size. """ return self._byte_size @@ -1920,10 +2006,12 @@ def extracted_text(self): @property def url(self): """Get a url to the image. - Returns: + + Returns + ------- str: Image url. """ - raise NotImplementedError() + raise NotImplementedError class NodeDrawing(NodeBlob): @@ -1931,7 +2019,7 @@ class NodeDrawing(NodeBlob): _TYPE = BlobType.Drawing - def __init__(self): + def __init__(self) -> None: super().__init__(type_=self._TYPE) self._extracted_text = "" self._extraction_status = "" @@ -1971,7 +2059,7 @@ def extracted_text(self): class NodeDrawingInfo(Element): """Represents information about a drawing blob.""" - def __init__(self): + def __init__(self) -> None: super().__init__() self.drawing_id = "" self.snapshot = NodeImage() @@ -2023,7 +2111,7 @@ class Blob(Node): BlobType.Drawing: NodeDrawing, } - def __init__(self, parent_id=None, **kwargs): + def __init__(self, parent_id=None, **kwargs) -> None: super().__init__(type_=NodeType.Blob, parent_id=parent_id, **kwargs) self.blob = None @@ -2032,9 +2120,11 @@ def from_json(cls: type, raw: dict) -> NodeBlob | None: """Helper to construct a blob from a dict. Args: + ---- raw: Raw blob representation. Returns: + ------- A NodeBlob object or None. """ if raw is None: @@ -2080,9 +2170,11 @@ def from_json(raw: dict) -> Node | None: """Helper to construct a node from a dict. Args: + ---- raw: Raw node representation. Returns: + ------- A Node object or None. """ ncls = None From bbad80401f406a8d90ac76227c4565e90dd3e444 Mon Sep 17 00:00:00 2001 From: K Date: Fri, 7 Apr 2023 15:10:21 -0400 Subject: [PATCH 31/56] Fix up lint findings --- pyproject.toml | 11 +- src/gkeepapi/__init__.py | 30 +-- src/gkeepapi/exception.py | 5 +- src/gkeepapi/node.py | 407 ++++++++++++++++++++------------------ 4 files changed, 244 insertions(+), 209 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index da69cda..ab6afd3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ select = [ "ANN", # flake8-annotations "S", # flake8-bandit "BLE", # flake8-blind-except - "FBT", # flake8-boolean-trap + # "FBT", # flake8-boolean-trap "B", # flake8-bugbear "A", # flake8-builtins "COM", # flake8-commas @@ -83,7 +83,7 @@ select = [ "PLE", # pylint "PLR", # pylint "PLW", # pylint - "TRY", # tryceratops + # "TRY", # tryceratops "RUF", # ruff-specific rules ] ignore = [ @@ -92,4 +92,11 @@ ignore = [ "COM812", "D400", "D415", + "D203", + "D213", + "EM102", + "ANN101", ] + +[tool.ruff.pydocstyle] +convention = "google" diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 69c9659..9a98246 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -24,6 +24,7 @@ class APIAuth: """Authentication token manager""" def __init__(self, scopes: str) -> None: + """Construct API authentication manager""" self._master_token = None self._auth_token = None self._email = None @@ -86,7 +87,7 @@ def load(self, email: str, master_token: str, device_id: str) -> bool: def getMasterToken(self) -> str: """Gets the master token. - Returns + Returns: ------- The account master token. """ @@ -106,7 +107,7 @@ def setMasterToken(self, master_token: str): def getEmail(self) -> str: """Gets the account email. - Returns + Returns: ------- The account email. """ @@ -124,7 +125,7 @@ def setEmail(self, email: str): def getDeviceId(self) -> str: """Gets the device id. - Returns + Returns: ------- The device id. """ @@ -142,7 +143,7 @@ def setDeviceId(self, device_id: str): def getAuthToken(self) -> str | None: """Gets the auth token. - Returns + Returns: ------- The auth token. """ @@ -151,11 +152,11 @@ def getAuthToken(self) -> str | None: def refresh(self) -> str: """Refresh the OAuth token. - Returns + Returns: ------- The auth token. - Raises + Raises: ------ LoginException: If there was a problem refreshing the OAuth token. """ @@ -190,6 +191,7 @@ class API: RETRY_CNT = 2 def __init__(self, base_url: str, auth: APIAuth | None = None) -> None: + """Construct a low-level API client""" self._session = requests.Session() self._auth = auth self._base_url = base_url @@ -296,6 +298,7 @@ class KeepAPI(API): API_URL = "https://www.googleapis.com/notes/v1/" def __init__(self, auth: APIAuth | None = None) -> None: + """Construct a low-level Google Keep API client""" super().__init__(self.API_URL, auth) create_time = time.time() @@ -391,6 +394,7 @@ class MediaAPI(API): API_URL = "https://keep.google.com/media/v2/" def __init__(self, auth: APIAuth | None = None) -> None: + """Construct a low-level Google Media API client""" super().__init__(self.API_URL, auth) def get(self, blob: _node.Blob) -> str: @@ -421,6 +425,7 @@ class RemindersAPI(API): API_URL = "https://www.googleapis.com/reminders/v1internal/reminders/" def __init__(self, auth: APIAuth | None = None) -> None: + """Construct a low-level Google Reminders API client""" super().__init__(self.API_URL, auth) self.static_params = { "taskList": [ @@ -634,7 +639,7 @@ def history(self, storage_version: str) -> Any: Args: ---- - storage_version (str): The local storage version. + storage_version: The local storage version. Returns: ------- @@ -686,6 +691,7 @@ class Keep: OAUTH_SCOPES = "oauth2:https://www.googleapis.com/auth/memento https://www.googleapis.com/auth/reminders" def __init__(self) -> None: + """Construct a Google Keep client""" self._keep_api = KeepAPI() self._reminders_api = RemindersAPI() self._media_api = MediaAPI() @@ -768,7 +774,7 @@ def resume( def getMasterToken(self) -> str: """Get master token for resuming. - Returns + Returns: ------- The master token. """ @@ -798,7 +804,7 @@ def load(self, auth: APIAuth, state: dict | None = None, sync=True): def dump(self) -> dict: """Serialize note data. - Returns + Returns: ------- Serialized state. """ @@ -1055,7 +1061,7 @@ def deleteLabel(self, label_id: str): def labels(self) -> list[_node.Label]: """Get all labels. - Returns + Returns: ------- Labels """ @@ -1077,10 +1083,10 @@ def getMediaLink(self, blob: _node.Blob) -> str: def all(self) -> list[_node.TopLevelNode]: """Get all Notes. - Returns + Returns: ------- - Notes + Notes: ----- """ return self._nodes[_node.Root.ID].children diff --git a/src/gkeepapi/exception.py b/src/gkeepapi/exception.py index 46e3b9e..2c421d5 100644 --- a/src/gkeepapi/exception.py +++ b/src/gkeepapi/exception.py @@ -5,6 +5,7 @@ class APIException(Exception): """The API server returned an error.""" def __init__(self, code: int, msg: str) -> None: + """Construct an exception object""" super().__init__(msg) self.code = code @@ -20,7 +21,8 @@ class LoginException(KeepException): class BrowserLoginRequiredException(LoginException): """Browser login required error.""" - def __init__(self, url) -> None: + def __init__(self, url: str) -> None: + """Construct a browser login exception object""" self.url = url @@ -52,5 +54,6 @@ class ParseException(KeepException): """Parse error.""" def __init__(self, msg: str, raw: dict) -> None: + """Construct a parse exception object""" super().__init__(msg) self.raw = raw diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 71f59b6..47b95bc 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -179,9 +179,10 @@ class Element: """Interface for elements that can be serialized and deserialized.""" def __init__(self) -> None: + """Construct an element object""" self._dirty = False - def _find_discrepancies(self, raw): # pragma: no cover + def _find_discrepancies(self, raw) -> None: # pragma: no cover s_raw = self.save(False) if isinstance(raw, dict): for key, val in raw.items(): @@ -222,7 +223,7 @@ def _find_discrepancies(self, raw): # pragma: no cover len(s_raw), ) - def load(self, raw: dict): + def load(self, raw: dict) -> None: """Unserialize from raw representation. (Wrapper) Args: @@ -239,7 +240,7 @@ def load(self, raw: dict): except (KeyError, ValueError) as e: raise exception.ParseException(f"Parse error in {type(self)}", raw) from e - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: """Unserialize from raw representation. (Implementation logic) Args: @@ -270,7 +271,7 @@ def save(self, clean=True) -> dict: def dirty(self) -> bool: """Get dirty state. - Returns + Returns: ------- Whether this element is dirty. """ @@ -284,7 +285,7 @@ def __init__(self) -> None: super().__init__() self.id = self._generateAnnotationId() - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self.id = raw.get("id") @@ -299,11 +300,21 @@ def save(self, clean=True) -> dict: @classmethod def _generateAnnotationId(cls) -> str: return "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}".format( - random.randint(0x00000000, 0xFFFFFFFF), - random.randint(0x0000, 0xFFFF), - random.randint(0x0000, 0xFFFF), - random.randint(0x0000, 0xFFFF), - random.randint(0x000000000000, 0xFFFFFFFFFFFF), + random.randint( + 0x00000000, 0xFFFFFFFF + ), # noqa: suspicious-non-cryptographic-random-usage + random.randint( + 0x0000, 0xFFFF + ), # noqa: suspicious-non-cryptographic-random-usage + random.randint( + 0x0000, 0xFFFF + ), # noqa: suspicious-non-cryptographic-random-usage + random.randint( + 0x0000, 0xFFFF + ), # noqa: suspicious-non-cryptographic-random-usage + random.randint( + 0x000000000000, 0xFFFFFFFFFFFF + ), # noqa: suspicious-non-cryptographic-random-usage ) @@ -318,7 +329,7 @@ def __init__(self) -> None: self._provenance_url = "" self._description = "" - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._title = raw["webLink"]["title"] self._url = raw["webLink"]["url"] @@ -345,7 +356,7 @@ def save(self, clean=True) -> dict: def title(self) -> str: """Get the link title. - Returns + Returns: ------- The link title. """ @@ -360,7 +371,7 @@ def title(self, value: str) -> None: def url(self) -> str: """Get the link url. - Returns + Returns: ------- The link url. """ @@ -375,7 +386,7 @@ def url(self, value: str) -> None: def image_url(self) -> str: """Get the link image url. - Returns + Returns: ------- The image url or None. """ @@ -390,7 +401,7 @@ def image_url(self, value: str) -> None: def provenance_url(self) -> str: """Get the provenance url. - Returns + Returns: ------- The provenance url. """ @@ -405,14 +416,14 @@ def provenance_url(self, value) -> None: def description(self) -> str: """Get the link description. - Returns + Returns: ------- The link description. """ return self._description @description.setter - def description(self, value: str): + def description(self, value: str) -> None: self._description = value self._dirty = True @@ -424,7 +435,7 @@ def __init__(self) -> None: super().__init__() self._category = None - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._category = CategoryValue(raw["topicCategory"]["category"]) @@ -437,7 +448,7 @@ def save(self, clean=True) -> dict: def category(self) -> CategoryValue: """Get the category. - Returns + Returns: ------- The category. """ @@ -456,7 +467,7 @@ def __init__(self) -> None: super().__init__() self._suggest = None - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._suggest = raw["taskAssist"]["suggestType"] @@ -469,7 +480,7 @@ def save(self, clean=True) -> dict: def suggest(self) -> str: """Get the suggestion. - Returns + Returns: ------- The suggestion. """ @@ -488,7 +499,7 @@ def __init__(self) -> None: super().__init__() self._entries = {} - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._entries = {} for key, entry in raw.get("context", {}).items(): @@ -505,7 +516,7 @@ def save(self, clean=True) -> dict: def all(self) -> list[Annotation]: """Get all sub annotations. - Returns + Returns: ------- Sub Annotations. """ @@ -561,13 +572,13 @@ def from_json(cls, raw: dict) -> Annotation | None: def all(self) -> list[Annotation]: """Get all annotations. - Returns + Returns: ------- Annotations. """ return list(self._annotations.values()) - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._annotations = {} if "annotations" not in raw: @@ -596,7 +607,7 @@ def _get_category_node(self) -> Category | None: def category(self) -> CategoryValue | None: """Get the category. - Returns + Returns: ------- The category. """ @@ -622,7 +633,7 @@ def category(self, value) -> None: def links(self) -> list[WebLink]: """Get all links. - Returns + Returns: ------- A list of links. """ @@ -681,7 +692,7 @@ def __init__(self, create_time: float | None = None) -> None: self._updated = self.int_to_dt(create_time) self._edited = self.int_to_dt(create_time) - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) if "created" in raw: self._created = self.str_to_dt(raw["created"]) @@ -712,7 +723,7 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: Params: tsz: Datetime string. - Returns + Returns: ------- Datetime. """ @@ -725,7 +736,7 @@ def int_to_dt(cls, tz: int | float) -> datetime.datetime: Params: ts: Unix timestamp. - Returns + Returns: ------- Datetime. """ @@ -738,7 +749,7 @@ def dt_to_str(cls, dt: datetime.datetime) -> str: Params: dt: Datetime. - Returns + Returns: ------- Datetime string. """ @@ -748,7 +759,7 @@ def dt_to_str(cls, dt: datetime.datetime) -> str: def int_to_str(cls, tz: int) -> str: """Convert a unix timestamp to a str. - Returns + Returns: ------- Datetime string. """ @@ -758,7 +769,7 @@ def int_to_str(cls, tz: int) -> str: def created(self) -> datetime.datetime: """Get the creation datetime. - Returns + Returns: ------- Datetime. """ @@ -773,7 +784,7 @@ def created(self, value) -> None: def deleted(self) -> datetime.datetime | None: """Get the deletion datetime. - Returns + Returns: ------- Datetime. """ @@ -788,7 +799,7 @@ def deleted(self, value: datetime.datetime) -> None: def trashed(self) -> datetime.datetime | None: """Get the move-to-trash datetime. - Returns + Returns: ------- Datetime. """ @@ -803,7 +814,7 @@ def trashed(self, value: datetime.datetime) -> None: def updated(self) -> datetime.datetime: """Get the updated datetime. - Returns + Returns: ------- Datetime. """ @@ -818,7 +829,7 @@ def updated(self, value: datetime.datetime) -> None: def edited(self) -> datetime.datetime: """Get the user edited datetime. - Returns + Returns: ------- Datetime. """ @@ -839,7 +850,7 @@ def __init__(self) -> None: self._graveyard_state = GraveyardStateValue.Collapsed self._checked_listitems_policy = CheckedListItemsPolicyValue.Graveyard - def _load(self, raw: dict): + def _load(self, raw: dict) -> None: super()._load(raw) self._new_listitem_placement = NewListItemPlacementValue( raw["newListItemPlacement"] @@ -860,7 +871,7 @@ def save(self, clean=True) -> dict: def new_listitem_placement(self) -> NewListItemPlacementValue: """Get the default location to insert new listitems. - Returns + Returns: ------- Placement. """ @@ -875,7 +886,7 @@ def new_listitem_placement(self, value: NewListItemPlacementValue) -> None: def graveyard_state(self) -> GraveyardStateValue: """Get the visibility state for the list graveyard. - Returns + Returns: ------- Visibility. """ @@ -890,7 +901,7 @@ def graveyard_state(self, value: GraveyardStateValue) -> None: def checked_listitems_policy(self) -> CheckedListItemsPolicyValue: """Get the policy for checked listitems. - Returns + Returns: ------- Policy. """ @@ -914,7 +925,7 @@ def __len__(self) -> int: def load( self, collaborators_raw: list, requests_raw: list - ): # pylint: disable=arguments-differ + )->None: # pylint: disable=arguments-differ # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() @@ -973,7 +984,7 @@ def remove(self, email: str) -> None: def all(self) -> list[str]: """Get all collaborators. - Returns + Returns: ------- Collaborators. """ @@ -990,7 +1001,7 @@ class TimestampsMixin: def __init__(self) -> None: self.timestamps: NodeTimestamps - def touch(self, edited=False): + def touch(self, edited=False) -> None: """Mark the node as dirty. Args: @@ -1004,44 +1015,44 @@ def touch(self, edited=False): self.timestamps.edited = dt @property - def trashed(self): + def trashed(self) -> bool: """Get the trashed state. - Returns + Returns: ------- - bool: Whether this item is trashed. + Whether this item is trashed. """ return ( self.timestamps.trashed is not None and self.timestamps.trashed > NodeTimestamps.int_to_dt(0) ) - def trash(self): + def trash(self) -> None: """Mark the item as trashed.""" self.timestamps.trashed = datetime.datetime.utcnow() - def untrash(self): + def untrash(self) -> None: """Mark the item as untrashed.""" self.timestamps.trashed = self.timestamps.int_to_dt(0) @property - def deleted(self): + def deleted(self) -> bool: """Get the deleted state. - Returns + Returns: ------- - bool: Whether this item is deleted. + Whether this item is deleted. """ return ( self.timestamps.deleted is not None and self.timestamps.deleted > NodeTimestamps.int_to_dt(0) ) - def delete(self): + def delete(self) -> None: """Mark the item as deleted.""" self.timestamps.deleted = datetime.datetime.utcnow() - def undelete(self): + def undelete(self) -> None: """Mark the item as undeleted.""" self.timestamps.deleted = None @@ -1060,18 +1071,20 @@ def __init__(self) -> None: self._merged = NodeTimestamps.int_to_dt(0) @classmethod - def _generateId(cls, tz): + def _generateId(cls, tz)-> str: return "tag.{}.{:x}".format( "".join( [ - random.choice("abcdefghijklmnopqrstuvwxyz0123456789") + random.choice( + "abcdefghijklmnopqrstuvwxyz0123456789" + ) # noqa: suspicious-non-cryptographic-random-usage for _ in range(12) ] ), int(tz * 1000), ) - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self.id = raw["mainId"] self._name = raw["name"] @@ -1082,7 +1095,7 @@ def _load(self, raw): else NodeTimestamps.int_to_dt(0) ) - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["mainId"] = self.id ret["name"] = self._name @@ -1091,37 +1104,37 @@ def save(self, clean=True): return ret @property - def name(self): + def name(self)-> str: """Get the label name. - Returns + Returns: ------- - str: Label name. + Label name. """ return self._name @name.setter - def name(self, value): + def name(self, value)->None: self._name = value self.touch(True) @property - def merged(self): + def merged(self)->datetime.datetime: """Get last merge datetime. - Returns + Returns: ------- - datetime: Datetime. + Datetime. """ return self._merged @merged.setter - def merged(self, value): + def merged(self, value)->None: self._merged = value self.touch() @property - def dirty(self): + def dirty(self)-> bool: return super().dirty or self.timestamps.dirty def __str__(self) -> str: @@ -1138,7 +1151,7 @@ def __init__(self) -> None: def __len__(self) -> int: return len(self._labels) - def _load(self, raw: list): + def _load(self, raw: list) -> None: # Parent method not called. if raw and isinstance(raw[-1], bool): self._dirty = raw.pop() @@ -1175,7 +1188,7 @@ def add(self, label: Label) -> None: self._labels[label.id] = label self._dirty = True - def remove(self, label: Label): + def remove(self, label: Label)->None: """Remove a label. Args: @@ -1186,7 +1199,7 @@ def remove(self, label: Label): self._labels[label.id] = None self._dirty = True - def get(self, label_id: str): + def get(self, label_id: str)->str: """Get a label by ID. Args: @@ -1195,12 +1208,12 @@ def get(self, label_id: str): """ return self._labels.get(label_id) - def all(self): + def all(self)-> list[Label]: """Get all labels. - Returns + Returns: ------- - list[gkeepapi.node.Label]: Labels. + Labels. """ return [label for _, label in self._labels.items() if label is not None] @@ -1218,7 +1231,9 @@ def __init__(self, id_=None, type_=None, parent_id=None) -> None: self.server_id = None self.parent_id = parent_id self.type = type_ - self._sort = random.randint(1000000000, 9999999999) + self._sort = random.randint( + 1000000000, 9999999999 + ) # noqa: suspicious-non-cryptographic-random-usage self._version = None self._text = "" self._children = {} @@ -1230,13 +1245,15 @@ def __init__(self, id_=None, type_=None, parent_id=None) -> None: self._moved = False @classmethod - def _generateId(cls, tz): + def _generateId(cls, tz)->str: return "{:x}.{:016x}".format( int(tz * 1000), - random.randint(0x0000000000000000, 0xFFFFFFFFFFFFFFFF), + random.randint( + 0x0000000000000000, 0xFFFFFFFFFFFFFFFF + ), # noqa: suspicious-non-cryptographic-random-usage ) - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) # Verify this is a valid type NodeType(raw["type"]) @@ -1256,7 +1273,7 @@ def _load(self, raw): self.settings.load(raw["nodeSettings"]) self.annotations.load(raw["annotationsGroup"]) - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["id"] = self.id ret["kind"] = "notes#node" @@ -1274,42 +1291,42 @@ def save(self, clean=True): return ret @property - def sort(self): + def sort(self) -> int: """Get the sort id. - Returns + Returns: ------- - int: Sort id. + Sort id. """ return int(self._sort) @sort.setter - def sort(self, value): + def sort(self, value)->None: self._sort = value self.touch() @property - def version(self): + def version(self) -> int: """Get the node version. - Returns + Returns: ------- - int: Version. + Version. """ return self._version @property - def text(self): + def text(self) -> str: """Get the text value. - Returns + Returns: ------- - str: Text value. + Text value. """ return self._text @text.setter - def text(self, value: str): + def text(self, value: str) -> None: """Set the text value. Args: @@ -1324,7 +1341,7 @@ def text(self, value: str): def children(self) -> list["Node"]: """Get all children. - Returns + Returns: ------- Children nodes. """ @@ -1343,7 +1360,7 @@ def get(self, node_id: str) -> "Node | None": """ return self._children.get(node_id) - def append(self, node: "Node", dirty=True): + def append(self, node: "Node", dirty=True)->"Node": """Add a new child node. Args: @@ -1358,7 +1375,7 @@ def append(self, node: "Node", dirty=True): return node - def remove(self, node: "Node", dirty=True): + def remove(self, node: "Node", dirty=True)->None: """Remove the given child node. Args: @@ -1373,17 +1390,17 @@ def remove(self, node: "Node", dirty=True): self.touch() @property - def new(self): + def new(self) -> bool: """Get whether this node has been persisted to the server. - Returns + Returns: ------- - bool: True if node is new. + True if node is new. """ return self.server_id is None @property - def dirty(self): + def dirty(self)->bool: return ( super().dirty or self.timestamps.dirty @@ -1402,7 +1419,7 @@ def __init__(self) -> None: super().__init__(id_=self.ID) @property - def dirty(self): + def dirty(self)->bool: return False @@ -1420,7 +1437,7 @@ def __init__(self, **kwargs) -> None: self.labels = NodeLabels() self.collaborators = NodeCollaborators() - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self._color = ColorValue(raw["color"]) if "color" in raw else ColorValue.White self._archived = raw["isArchived"] if "isArchived" in raw else False @@ -1434,7 +1451,7 @@ def _load(self, raw): ) self._moved = "moved" in raw - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["color"] = self._color.value ret["isArchived"] = self._archived @@ -1451,99 +1468,99 @@ def save(self, clean=True): return ret @property - def color(self): + def color(self)-> ColorValue: """Get the node color. - Returns + Returns: ------- - gkeepapi.node.Color: Color. + Color. """ return self._color @color.setter - def color(self, value): + def color(self, value)->None: self._color = value self.touch(True) @property - def archived(self): + def archived(self) -> bool: """Get the archive state. - Returns + Returns: ------- - bool: Whether this node is archived. + Whether this node is archived. """ return self._archived @archived.setter - def archived(self, value): + def archived(self, value)->None: self._archived = value self.touch(True) @property - def pinned(self): + def pinned(self) -> bool: """Get the pin state. - Returns + Returns: ------- - bool: Whether this node is pinned. + Whether this node is pinned. """ return self._pinned @pinned.setter - def pinned(self, value): + def pinned(self, value)->None: self._pinned = value self.touch(True) @property - def title(self): + def title(self)-> str: """Get the title. - Returns + Returns: ------- - str: Title. + Title. """ return self._title @title.setter - def title(self, value): + def title(self, value)->None: self._title = value self.touch(True) @property - def url(self): + def url(self)-> str: """Get the url for this node. - Returns + Returns: ------- - str: Google Keep url. + Google Keep url. """ return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @property - def dirty(self): + def dirty(self)->bool: return super().dirty or self.labels.dirty or self.collaborators.dirty @property - def blobs(self): + def blobs(self)-> list[Blob]: """Get all media blobs. - Returns + Returns: ------- - list[gkeepapi.node.Blob]: Media blobs. + Media blobs. """ return [node for node in self.children if isinstance(node, Blob)] @property - def images(self): + def images(self)->list[NodeImage]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeImage)] @property - def drawings(self): + def drawings(self)->list[NodeDrawing]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeDrawing)] @property - def audio(self): + def audio(self)->list[NodeAudio]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] @@ -1564,13 +1581,13 @@ def __init__( self._subitems = {} self._checked = False - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self.prev_super_list_item_id = self.super_list_item_id self.super_list_item_id = raw.get("superListItemId") or None self._checked = raw.get("checked", False) - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["parentServerId"] = self.parent_server_id ret["superListItemId"] = self.super_list_item_id @@ -1582,7 +1599,7 @@ def add( text: str, checked=False, sort: NewListItemPlacementValue | int | None = None, - ): + ) -> "ListItem": """Add a new sub item to the list. This item must already be attached to a list. Args: @@ -1597,7 +1614,7 @@ def add( self.indent(node) return node - def indent(self, node: "ListItem", dirty=True): + def indent(self, node: "ListItem", dirty=True)->None: """Indent an item. Does nothing if the target has subitems. Args: @@ -1614,7 +1631,7 @@ def indent(self, node: "ListItem", dirty=True): if dirty: node.touch(True) - def dedent(self, node: "ListItem", dirty=True): + def dedent(self, node: "ListItem", dirty=True)-> None: """Dedent an item. Does nothing if the target is not indented under this item. Args: @@ -1632,37 +1649,37 @@ def dedent(self, node: "ListItem", dirty=True): node.touch(True) @property - def subitems(self): + def subitems(self)->list[ListItem]: """Get subitems for this item. - Returns + Returns: ------- - list[gkeepapi.node.ListItem]: Subitems. + Subitems. """ return List.sorted_items(self._subitems.values()) @property - def indented(self): + def indented(self) -> bool: """Get indentation state. - Returns + Returns: ------- - bool: Whether this item is indented. + Whether this item is indented. """ return self.parent_item is not None @property - def checked(self): + def checked(self) -> bool: """Get the checked state. - Returns + Returns: ------- - bool: Whether this item is checked. + Whether this item is checked. """ return self._checked @checked.setter - def checked(self, value): + def checked(self, value)-> None: self._checked = value self.touch(True) @@ -1682,7 +1699,7 @@ class Note(TopLevelNode): def __init__(self, **kwargs) -> None: super().__init__(type_=self._TYPE, **kwargs) - def _get_text_node(self): + def _get_text_node(self) -> ListItem | None: node = None for child_node in self.children: if isinstance(child_node, ListItem): @@ -1692,7 +1709,7 @@ def _get_text_node(self): return node @property - def text(self): + def text(self) -> str: node = self._get_text_node() if node is None: @@ -1700,7 +1717,7 @@ def text(self): return node.text @text.setter - def text(self, value): + def text(self, value) -> None: node = self._get_text_node() if node is None: node = ListItem(parent_id=self.id) @@ -1726,7 +1743,7 @@ def add( text: str, checked=False, sort: NewListItemPlacementValue | int | None = None, - ): + )-> ListItem: """Add a new item to the list. Args: @@ -1756,7 +1773,7 @@ def add( return node @property - def text(self): + def text(self) -> str: return "\n".join(str(node) for node in self.items) @classmethod @@ -1776,7 +1793,7 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: class t(tuple): """Tuple with element-based sorting""" - def __cmp__(self, other): + def __cmp__(self, other)->int: for a, b in itertools.zip_longest(self, other): if a != b: if a is None: @@ -1786,25 +1803,25 @@ def __cmp__(self, other): return a - b return 0 - def __lt__(self, other): # pragma: no cover + def __lt__(self, other)->bool: # pragma: no cover return self.__cmp__(other) < 0 - def __gt_(self, other): # pragma: no cover + def __gt_(self, other)->bool: # pragma: no cover return self.__cmp__(other) > 0 - def __le__(self, other): # pragma: no cover + def __le__(self, other)->bool: # pragma: no cover return self.__cmp__(other) <= 0 - def __ge_(self, other): # pragma: no cover + def __ge_(self, other)->bool: # pragma: no cover return self.__cmp__(other) >= 0 - def __eq__(self, other): # pragma: no cover + def __eq__(self, other)->bool: # pragma: no cover return self.__cmp__(other) == 0 - def __ne__(self, other): # pragma: no cover + def __ne__(self, other)->bool: # pragma: no cover return self.__cmp__(other) != 0 - def key_func(x): + def key_func(x)->t: if x.indented: return t((int(x.parent_item.sort), int(x.sort))) return t((int(x.sort),)) @@ -1820,7 +1837,7 @@ def _items(self, checked: bool | None = None) -> list[ListItem]: and (checked is None or node.checked == checked) ] - def sort_items(self, key: Callable = attrgetter("text"), reverse=False): + def sort_items(self, key: Callable = attrgetter("text"), reverse=False)-> None: """Sort list items in place. By default, the items are alphabetized, but a custom function can be specified. @@ -1830,7 +1847,9 @@ def sort_items(self, key: Callable = attrgetter("text"), reverse=False): reverse: Whether to reverse the output. """ sorted_children = sorted(self._items(), key=key, reverse=reverse) - sort_value = random.randint(1000000000, 9999999999) + sort_value = random.randint( # noqa: suspicious-non-cryptographic-random-usage + 1000000000, 9999999999 + ) for node in sorted_children: node.sort = sort_value @@ -1843,7 +1862,7 @@ def __str__(self) -> str: def items(self) -> list[ListItem]: """Get all listitems. - Returns + Returns: ------- List items. """ @@ -1853,7 +1872,7 @@ def items(self) -> list[ListItem]: def checked(self) -> list[ListItem]: """Get all checked listitems. - Returns + Returns: ------- List items. """ @@ -1863,7 +1882,7 @@ def checked(self) -> list[ListItem]: def unchecked(self) -> list[ListItem]: """Get all unchecked listitems. - Returns + Returns: ------- List items. """ @@ -1883,7 +1902,7 @@ def __init__(self, type_=None) -> None: self._mimetype = "" self._is_uploaded = False - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) # Verify this is a valid type BlobType(raw["type"]) @@ -1891,7 +1910,7 @@ def _load(self, raw): self._media_id = raw.get("media_id") self._mimetype = raw.get("mimetype") - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["kind"] = "notes#blob" ret["type"] = self.type.value @@ -1912,23 +1931,23 @@ def __init__(self) -> None: super().__init__(type_=self._TYPE) self._length = None - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self._length = raw.get("length") - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) if self._length is not None: ret["length"] = self._length return ret @property - def length(self): + def length(self) -> int: """Get length of the audio clip. - Returns + Returns: ------- - int: Audio length. + Audio length. """ return self._length @@ -1947,7 +1966,7 @@ def __init__(self) -> None: self._extracted_text = "" self._extraction_status = "" - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self._is_uploaded = raw.get("is_uploaded") or False self._width = raw.get("width") @@ -1956,7 +1975,7 @@ def _load(self, raw): self._extracted_text = raw.get("extracted_text") self._extraction_status = raw.get("extraction_status") - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["width"] = self._width ret["height"] = self._height @@ -1966,50 +1985,50 @@ def save(self, clean=True): return ret @property - def width(self): + def width(self) -> int: """Get width of image. - Returns + Returns: ------- - int: Image width. + Image width. """ return self._width @property - def height(self): + def height(self) -> int: """Get height of image. - Returns + Returns: ------- - int: Image height. + Image height. """ return self._height @property - def byte_size(self): + def byte_size(self) -> int: """Get size of image in bytes. - Returns + Returns: ------- - int: Image byte size. + Image byte size. """ return self._byte_size @property - def extracted_text(self): + def extracted_text(self) -> str: """Get text extracted from image Returns: - str: Extracted text. + Extracted text. """ return self._extracted_text @property - def url(self): + def url(self)-> str: """Get a url to the image. - Returns + Returns: ------- - str: Image url. + Image url. """ raise NotImplementedError @@ -2025,7 +2044,7 @@ def __init__(self) -> None: self._extraction_status = "" self._drawing_info = None - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self._extracted_text = raw.get("extracted_text") self._extraction_status = raw.get("extraction_status") @@ -2035,7 +2054,7 @@ def _load(self, raw): drawing_info.load(raw["drawingInfo"]) self._drawing_info = drawing_info - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["extracted_text"] = self._extracted_text ret["extraction_status"] = self._extraction_status @@ -2044,10 +2063,10 @@ def save(self, clean=True): return ret @property - def extracted_text(self): + def extracted_text(self)-> str: """Get text extracted from image Returns: - str: Extracted text. + Extracted text. """ return ( self._drawing_info.snapshot.extracted_text @@ -2068,7 +2087,7 @@ def __init__(self) -> None: self._ink_hash = "" self._snapshot_proto_fprint = "" - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self.drawing_id = raw["drawingId"] self.snapshot.load(raw["snapshotData"]) @@ -2089,7 +2108,7 @@ def _load(self, raw): else self._snapshot_proto_fprint ) - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) ret["drawingId"] = self.drawing_id ret["snapshotData"] = self.snapshot.save(clean) @@ -2147,11 +2166,11 @@ def from_json(cls: type, raw: dict) -> NodeBlob | None: return blob - def _load(self, raw): + def _load(self, raw) -> None: super()._load(raw) self.blob = self.from_json(raw.get("blob")) - def save(self, clean=True): + def save(self, clean=True) -> dict: ret = super().save(clean) if self.blob is not None: ret["blob"] = self.blob.save(clean) @@ -2193,10 +2212,10 @@ def from_json(raw: dict) -> Node | None: if DEBUG: # pragma: no cover - Node.__load = Node._load # pylint: disable=protected-access + Node.__load = Node._load # noqa: private-member-access - def _load(self, raw): # pylint: disable=missing-docstring - self.__load(raw) # pylint: disable=protected-access - self._find_discrepancies(raw) # pylint: disable=protected-access + def _load(self, raw): # noqa: missing-type-function-argument + self.__load(raw) # : private-member-access + self._find_discrepancies(raw) # : private-member-access - Node._load = _load # pylint: disable=protected-access + Node._load = _load # noqa: private-member-access From 397a08901725a6e8aeb272fb651cbff08007d48b Mon Sep 17 00:00:00 2001 From: K Date: Fri, 7 Apr 2023 15:21:03 -0400 Subject: [PATCH 32/56] Black --- src/gkeepapi/node.py | 90 ++++++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 47b95bc..6bd31bd 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -925,7 +925,7 @@ def __len__(self) -> int: def load( self, collaborators_raw: list, requests_raw: list - )->None: # pylint: disable=arguments-differ + ) -> None: # pylint: disable=arguments-differ # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() @@ -1071,7 +1071,7 @@ def __init__(self) -> None: self._merged = NodeTimestamps.int_to_dt(0) @classmethod - def _generateId(cls, tz)-> str: + def _generateId(cls, tz) -> str: return "tag.{}.{:x}".format( "".join( [ @@ -1104,7 +1104,7 @@ def save(self, clean=True) -> dict: return ret @property - def name(self)-> str: + def name(self) -> str: """Get the label name. Returns: @@ -1114,12 +1114,12 @@ def name(self)-> str: return self._name @name.setter - def name(self, value)->None: + def name(self, value) -> None: self._name = value self.touch(True) @property - def merged(self)->datetime.datetime: + def merged(self) -> datetime.datetime: """Get last merge datetime. Returns: @@ -1129,12 +1129,12 @@ def merged(self)->datetime.datetime: return self._merged @merged.setter - def merged(self, value)->None: + def merged(self, value) -> None: self._merged = value self.touch() @property - def dirty(self)-> bool: + def dirty(self) -> bool: return super().dirty or self.timestamps.dirty def __str__(self) -> str: @@ -1188,7 +1188,7 @@ def add(self, label: Label) -> None: self._labels[label.id] = label self._dirty = True - def remove(self, label: Label)->None: + def remove(self, label: Label) -> None: """Remove a label. Args: @@ -1199,7 +1199,7 @@ def remove(self, label: Label)->None: self._labels[label.id] = None self._dirty = True - def get(self, label_id: str)->str: + def get(self, label_id: str) -> str: """Get a label by ID. Args: @@ -1208,7 +1208,7 @@ def get(self, label_id: str)->str: """ return self._labels.get(label_id) - def all(self)-> list[Label]: + def all(self) -> list[Label]: """Get all labels. Returns: @@ -1245,7 +1245,7 @@ def __init__(self, id_=None, type_=None, parent_id=None) -> None: self._moved = False @classmethod - def _generateId(cls, tz)->str: + def _generateId(cls, tz) -> str: return "{:x}.{:016x}".format( int(tz * 1000), random.randint( @@ -1301,7 +1301,7 @@ def sort(self) -> int: return int(self._sort) @sort.setter - def sort(self, value)->None: + def sort(self, value) -> None: self._sort = value self.touch() @@ -1360,7 +1360,7 @@ def get(self, node_id: str) -> "Node | None": """ return self._children.get(node_id) - def append(self, node: "Node", dirty=True)->"Node": + def append(self, node: "Node", dirty=True) -> "Node": """Add a new child node. Args: @@ -1375,7 +1375,7 @@ def append(self, node: "Node", dirty=True)->"Node": return node - def remove(self, node: "Node", dirty=True)->None: + def remove(self, node: "Node", dirty=True) -> None: """Remove the given child node. Args: @@ -1400,7 +1400,7 @@ def new(self) -> bool: return self.server_id is None @property - def dirty(self)->bool: + def dirty(self) -> bool: return ( super().dirty or self.timestamps.dirty @@ -1419,7 +1419,7 @@ def __init__(self) -> None: super().__init__(id_=self.ID) @property - def dirty(self)->bool: + def dirty(self) -> bool: return False @@ -1468,7 +1468,7 @@ def save(self, clean=True) -> dict: return ret @property - def color(self)-> ColorValue: + def color(self) -> ColorValue: """Get the node color. Returns: @@ -1478,7 +1478,7 @@ def color(self)-> ColorValue: return self._color @color.setter - def color(self, value)->None: + def color(self, value) -> None: self._color = value self.touch(True) @@ -1493,7 +1493,7 @@ def archived(self) -> bool: return self._archived @archived.setter - def archived(self, value)->None: + def archived(self, value) -> None: self._archived = value self.touch(True) @@ -1508,12 +1508,12 @@ def pinned(self) -> bool: return self._pinned @pinned.setter - def pinned(self, value)->None: + def pinned(self, value) -> None: self._pinned = value self.touch(True) @property - def title(self)-> str: + def title(self) -> str: """Get the title. Returns: @@ -1523,12 +1523,12 @@ def title(self)-> str: return self._title @title.setter - def title(self, value)->None: + def title(self, value) -> None: self._title = value self.touch(True) @property - def url(self)-> str: + def url(self) -> str: """Get the url for this node. Returns: @@ -1538,11 +1538,11 @@ def url(self)-> str: return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @property - def dirty(self)->bool: + def dirty(self) -> bool: return super().dirty or self.labels.dirty or self.collaborators.dirty @property - def blobs(self)-> list[Blob]: + def blobs(self) -> list[Blob]: """Get all media blobs. Returns: @@ -1552,15 +1552,15 @@ def blobs(self)-> list[Blob]: return [node for node in self.children if isinstance(node, Blob)] @property - def images(self)->list[NodeImage]: + def images(self) -> list[NodeImage]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeImage)] @property - def drawings(self)->list[NodeDrawing]: + def drawings(self) -> list[NodeDrawing]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeDrawing)] @property - def audio(self)->list[NodeAudio]: + def audio(self) -> list[NodeAudio]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] @@ -1614,7 +1614,7 @@ def add( self.indent(node) return node - def indent(self, node: "ListItem", dirty=True)->None: + def indent(self, node: "ListItem", dirty=True) -> None: """Indent an item. Does nothing if the target has subitems. Args: @@ -1631,7 +1631,7 @@ def indent(self, node: "ListItem", dirty=True)->None: if dirty: node.touch(True) - def dedent(self, node: "ListItem", dirty=True)-> None: + def dedent(self, node: "ListItem", dirty=True) -> None: """Dedent an item. Does nothing if the target is not indented under this item. Args: @@ -1649,7 +1649,7 @@ def dedent(self, node: "ListItem", dirty=True)-> None: node.touch(True) @property - def subitems(self)->list[ListItem]: + def subitems(self) -> list[ListItem]: """Get subitems for this item. Returns: @@ -1679,7 +1679,7 @@ def checked(self) -> bool: return self._checked @checked.setter - def checked(self, value)-> None: + def checked(self, value) -> None: self._checked = value self.touch(True) @@ -1743,7 +1743,7 @@ def add( text: str, checked=False, sort: NewListItemPlacementValue | int | None = None, - )-> ListItem: + ) -> ListItem: """Add a new item to the list. Args: @@ -1793,7 +1793,7 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: class t(tuple): """Tuple with element-based sorting""" - def __cmp__(self, other)->int: + def __cmp__(self, other) -> int: for a, b in itertools.zip_longest(self, other): if a != b: if a is None: @@ -1803,25 +1803,25 @@ def __cmp__(self, other)->int: return a - b return 0 - def __lt__(self, other)->bool: # pragma: no cover + def __lt__(self, other) -> bool: # pragma: no cover return self.__cmp__(other) < 0 - def __gt_(self, other)->bool: # pragma: no cover + def __gt_(self, other) -> bool: # pragma: no cover return self.__cmp__(other) > 0 - def __le__(self, other)->bool: # pragma: no cover + def __le__(self, other) -> bool: # pragma: no cover return self.__cmp__(other) <= 0 - def __ge_(self, other)->bool: # pragma: no cover + def __ge_(self, other) -> bool: # pragma: no cover return self.__cmp__(other) >= 0 - def __eq__(self, other)->bool: # pragma: no cover + def __eq__(self, other) -> bool: # pragma: no cover return self.__cmp__(other) == 0 - def __ne__(self, other)->bool: # pragma: no cover + def __ne__(self, other) -> bool: # pragma: no cover return self.__cmp__(other) != 0 - def key_func(x)->t: + def key_func(x) -> t: if x.indented: return t((int(x.parent_item.sort), int(x.sort))) return t((int(x.sort),)) @@ -1837,7 +1837,7 @@ def _items(self, checked: bool | None = None) -> list[ListItem]: and (checked is None or node.checked == checked) ] - def sort_items(self, key: Callable = attrgetter("text"), reverse=False)-> None: + def sort_items(self, key: Callable = attrgetter("text"), reverse=False) -> None: """Sort list items in place. By default, the items are alphabetized, but a custom function can be specified. @@ -1847,7 +1847,7 @@ def sort_items(self, key: Callable = attrgetter("text"), reverse=False)-> None: reverse: Whether to reverse the output. """ sorted_children = sorted(self._items(), key=key, reverse=reverse) - sort_value = random.randint( # noqa: suspicious-non-cryptographic-random-usage + sort_value = random.randint( # noqa: suspicious-non-cryptographic-random-usage 1000000000, 9999999999 ) @@ -2023,7 +2023,7 @@ def extracted_text(self) -> str: return self._extracted_text @property - def url(self)-> str: + def url(self) -> str: """Get a url to the image. Returns: @@ -2063,7 +2063,7 @@ def save(self, clean=True) -> dict: return ret @property - def extracted_text(self)-> str: + def extracted_text(self) -> str: """Get text extracted from image Returns: Extracted text. From 8d7661213913fe23e8705bcea96944666237aa9a Mon Sep 17 00:00:00 2001 From: K Date: Wed, 12 Apr 2023 14:03:39 -0400 Subject: [PATCH 33/56] Typehints cleanup --- pyproject.toml | 1 + src/gkeepapi/node.py | 286 ++++++++++++++++--------------------------- 2 files changed, 108 insertions(+), 179 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ab6afd3..1a3f190 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,7 @@ ignore = [ "D213", "EM102", "ANN101", + "ANN102", ] [tool.ruff.pydocstyle] diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 6bd31bd..ef060a5 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -182,7 +182,7 @@ def __init__(self) -> None: """Construct an element object""" self._dirty = False - def _find_discrepancies(self, raw) -> None: # pragma: no cover + def _find_discrepancies(self, raw: dict | list) -> None: # pragma: no cover s_raw = self.save(False) if isinstance(raw, dict): for key, val in raw.items(): @@ -227,12 +227,10 @@ def load(self, raw: dict) -> None: """Unserialize from raw representation. (Wrapper) Args: - ---- raw: Raw. Raises: - ------ ParseException: If there was an error parsing data. """ try: @@ -244,20 +242,17 @@ def _load(self, raw: dict) -> None: """Unserialize from raw representation. (Implementation logic) Args: - ---- raw: Raw. """ self._dirty = raw.get("_dirty", False) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: """Serialize into raw representation. Clears the dirty bit by default. Args: - ---- clean: Whether to clear the dirty bit. Returns: - ------- Raw. """ ret = {} @@ -272,7 +267,6 @@ def dirty(self) -> bool: """Get dirty state. Returns: - ------- Whether this element is dirty. """ return self._dirty @@ -289,7 +283,7 @@ def _load(self, raw: dict) -> None: super()._load(raw) self.id = raw.get("id") - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = {} if self.id is not None: ret = super().save(clean) @@ -300,21 +294,21 @@ def save(self, clean=True) -> dict: @classmethod def _generateAnnotationId(cls) -> str: return "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}".format( - random.randint( + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x00000000, 0xFFFFFFFF - ), # noqa: suspicious-non-cryptographic-random-usage - random.randint( + ), + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x0000, 0xFFFF - ), # noqa: suspicious-non-cryptographic-random-usage - random.randint( + ), + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x0000, 0xFFFF - ), # noqa: suspicious-non-cryptographic-random-usage - random.randint( + ), + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x0000, 0xFFFF - ), # noqa: suspicious-non-cryptographic-random-usage - random.randint( + ), + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x000000000000, 0xFFFFFFFFFFFF - ), # noqa: suspicious-non-cryptographic-random-usage + ), ) @@ -341,7 +335,7 @@ def _load(self, raw: dict) -> None: self._provenance_url = raw["webLink"]["provenanceUrl"] self._description = raw["webLink"]["description"] - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["webLink"] = { "title": self._title, @@ -357,7 +351,6 @@ def title(self) -> str: """Get the link title. Returns: - ------- The link title. """ return self._title @@ -372,7 +365,6 @@ def url(self) -> str: """Get the link url. Returns: - ------- The link url. """ return self._url @@ -383,11 +375,10 @@ def url(self, value: str) -> None: self._dirty = True @property - def image_url(self) -> str: + def image_url(self) -> str | None: """Get the link image url. Returns: - ------- The image url or None. """ return self._image_url @@ -402,13 +393,12 @@ def provenance_url(self) -> str: """Get the provenance url. Returns: - ------- The provenance url. """ return self._provenance_url @provenance_url.setter - def provenance_url(self, value) -> None: + def provenance_url(self, value: str) -> None: self._provenance_url = value self._dirty = True @@ -417,7 +407,6 @@ def description(self) -> str: """Get the link description. Returns: - ------- The link description. """ return self._description @@ -439,7 +428,7 @@ def _load(self, raw: dict) -> None: super()._load(raw) self._category = CategoryValue(raw["topicCategory"]["category"]) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["topicCategory"] = {"category": self._category.value} return ret @@ -449,7 +438,6 @@ def category(self) -> CategoryValue: """Get the category. Returns: - ------- The category. """ return self._category @@ -471,7 +459,7 @@ def _load(self, raw: dict) -> None: super()._load(raw) self._suggest = raw["taskAssist"]["suggestType"] - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["taskAssist"] = {"suggestType": self._suggest} return ret @@ -481,13 +469,12 @@ def suggest(self) -> str: """Get the suggestion. Returns: - ------- The suggestion. """ return self._suggest @suggest.setter - def suggest(self, value) -> None: + def suggest(self, value: str) -> None: self._suggest = value self._dirty = True @@ -505,7 +492,7 @@ def _load(self, raw: dict) -> None: for key, entry in raw.get("context", {}).items(): self._entries[key] = NodeAnnotations.from_json({key: entry}) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) context = {} for entry in self._entries.values(): @@ -513,11 +500,10 @@ def save(self, clean=True) -> dict: ret["context"] = context return ret - def all(self) -> list[Annotation]: + def all(self) -> list[Annotation]: # noqa: A003 """Get all sub annotations. Returns: - ------- Sub Annotations. """ return list(self._entries.values()) @@ -544,11 +530,9 @@ def from_json(cls, raw: dict) -> Annotation | None: """Helper to construct an annotation from a dict. Args: - ---- raw: Raw annotation representation. Returns: - ------- An Annotation object or None. """ bcls = None @@ -573,7 +557,6 @@ def all(self) -> list[Annotation]: """Get all annotations. Returns: - ------- Annotations. """ return list(self._annotations.values()) @@ -588,7 +571,7 @@ def _load(self, raw: dict) -> None: annotation = self.from_json(raw_annotation) self._annotations[annotation.id] = annotation - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["kind"] = "notes#annotationsGroup" if self._annotations: @@ -608,7 +591,6 @@ def category(self) -> CategoryValue | None: """Get the category. Returns: - ------- The category. """ node = self._get_category_node() @@ -616,7 +598,7 @@ def category(self) -> CategoryValue | None: return node.category if node is not None else None @category.setter - def category(self, value) -> None: + def category(self, value: CategoryValue) -> None: node = self._get_category_node() if value is None: if node is not None: @@ -634,7 +616,6 @@ def links(self) -> list[WebLink]: """Get all links. Returns: - ------- A list of links. """ return [ @@ -647,11 +628,9 @@ def append(self, annotation: Annotation) -> Annotation: """Add an annotation. Args: - ---- annotation: An Annotation object. Returns: - ------- The Annotation. """ self._annotations[annotation.id] = annotation @@ -662,7 +641,6 @@ def remove(self, annotation: Annotation) -> None: """Removes an annotation. Args: - ---- annotation: An Annotation object. """ if annotation.id in self._annotations: @@ -703,7 +681,7 @@ def _load(self, raw: dict) -> None: self.str_to_dt(raw["userEdited"]) if "userEdited" in raw else None ) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["kind"] = "notes#timestamps" ret["created"] = self.dt_to_str(self._created) @@ -724,7 +702,6 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: tsz: Datetime string. Returns: - ------- Datetime. """ return datetime.datetime.strptime(tzs, cls.TZ_FMT) @@ -737,7 +714,6 @@ def int_to_dt(cls, tz: int | float) -> datetime.datetime: ts: Unix timestamp. Returns: - ------- Datetime. """ return datetime.datetime.utcfromtimestamp(tz) @@ -750,7 +726,6 @@ def dt_to_str(cls, dt: datetime.datetime) -> str: dt: Datetime. Returns: - ------- Datetime string. """ return dt.strftime(cls.TZ_FMT) @@ -760,7 +735,6 @@ def int_to_str(cls, tz: int) -> str: """Convert a unix timestamp to a str. Returns: - ------- Datetime string. """ return cls.dt_to_str(cls.int_to_dt(tz)) @@ -770,13 +744,12 @@ def created(self) -> datetime.datetime: """Get the creation datetime. Returns: - ------- Datetime. """ return self._created @created.setter - def created(self, value) -> None: + def created(self, value: datetime.datetime) -> None: self._created = value self._dirty = True @@ -785,7 +758,6 @@ def deleted(self) -> datetime.datetime | None: """Get the deletion datetime. Returns: - ------- Datetime. """ return self._deleted @@ -800,7 +772,6 @@ def trashed(self) -> datetime.datetime | None: """Get the move-to-trash datetime. Returns: - ------- Datetime. """ return self._trashed @@ -815,7 +786,6 @@ def updated(self) -> datetime.datetime: """Get the updated datetime. Returns: - ------- Datetime. """ return self._updated @@ -830,7 +800,6 @@ def edited(self) -> datetime.datetime: """Get the user edited datetime. Returns: - ------- Datetime. """ return self._edited @@ -860,7 +829,7 @@ def _load(self, raw: dict) -> None: raw["checkedListItemsPolicy"] ) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["newListItemPlacement"] = self._new_listitem_placement.value ret["graveyardState"] = self._graveyard_state.value @@ -872,7 +841,6 @@ def new_listitem_placement(self) -> NewListItemPlacementValue: """Get the default location to insert new listitems. Returns: - ------- Placement. """ return self._new_listitem_placement @@ -887,7 +855,6 @@ def graveyard_state(self) -> GraveyardStateValue: """Get the visibility state for the list graveyard. Returns: - ------- Visibility. """ return self._graveyard_state @@ -902,7 +869,6 @@ def checked_listitems_policy(self) -> CheckedListItemsPolicyValue: """Get the policy for checked listitems. Returns: - ------- Policy. """ return self._checked_listitems_policy @@ -939,7 +905,7 @@ def load( collaborator["type"] ) - def save(self, clean=True) -> tuple[list, list]: + def save(self, clean: bool = True) -> tuple[list, list]: # Parent method not called. collaborators = [] requests = [] @@ -960,7 +926,6 @@ def add(self, email: str) -> None: """Add a collaborator. Args: - ---- email: Collaborator email address. """ if email not in self._collaborators: @@ -971,7 +936,6 @@ def remove(self, email: str) -> None: """Remove a Collaborator. Args: - ---- email: Collaborator email address. """ if email in self._collaborators: @@ -985,7 +949,6 @@ def all(self) -> list[str]: """Get all collaborators. Returns: - ------- Collaborators. """ return [ @@ -1001,11 +964,10 @@ class TimestampsMixin: def __init__(self) -> None: self.timestamps: NodeTimestamps - def touch(self, edited=False) -> None: + def touch(self, edited: bool = False) -> None: """Mark the node as dirty. Args: - ---- edited: Whether to set the edited time. """ self._dirty = True @@ -1019,7 +981,6 @@ def trashed(self) -> bool: """Get the trashed state. Returns: - ------- Whether this item is trashed. """ return ( @@ -1040,7 +1001,6 @@ def deleted(self) -> bool: """Get the deleted state. Returns: - ------- Whether this item is deleted. """ return ( @@ -1071,20 +1031,20 @@ def __init__(self) -> None: self._merged = NodeTimestamps.int_to_dt(0) @classmethod - def _generateId(cls, tz) -> str: + def _generateId(cls, tz: float) -> str: return "tag.{}.{:x}".format( "".join( [ - random.choice( + random.choice( # noqa: suspicious-non-cryptographic-random-usage "abcdefghijklmnopqrstuvwxyz0123456789" - ) # noqa: suspicious-non-cryptographic-random-usage + ) for _ in range(12) ] ), int(tz * 1000), ) - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self.id = raw["mainId"] self._name = raw["name"] @@ -1095,7 +1055,7 @@ def _load(self, raw) -> None: else NodeTimestamps.int_to_dt(0) ) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["mainId"] = self.id ret["name"] = self._name @@ -1108,13 +1068,12 @@ def name(self) -> str: """Get the label name. Returns: - ------- Label name. """ return self._name @name.setter - def name(self, value) -> None: + def name(self, value: str) -> None: self._name = value self.touch(True) @@ -1123,13 +1082,12 @@ def merged(self) -> datetime.datetime: """Get last merge datetime. Returns: - ------- Datetime. """ return self._merged @merged.setter - def merged(self, value) -> None: + def merged(self, value: datetime.datetime) -> None: self._merged = value self.touch() @@ -1161,7 +1119,7 @@ def _load(self, raw: list) -> None: for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean=True) -> tuple[dict] | tuple[dict, bool]: + def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # Parent method not called. ret = [ { @@ -1182,7 +1140,6 @@ def add(self, label: Label) -> None: """Add a label. Args: - ---- label: The Label object. """ self._labels[label.id] = label @@ -1192,7 +1149,6 @@ def remove(self, label: Label) -> None: """Remove a label. Args: - ---- label: The Label object. """ if label.id in self._labels: @@ -1203,7 +1159,6 @@ def get(self, label_id: str) -> str: """Get a label by ID. Args: - ---- label_id: The label ID. """ return self._labels.get(label_id) @@ -1212,7 +1167,6 @@ def all(self) -> list[Label]: """Get all labels. Returns: - ------- Labels. """ return [label for _, label in self._labels.items() if label is not None] @@ -1221,7 +1175,12 @@ def all(self) -> list[Label]: class Node(Element, TimestampsMixin): """Node base class.""" - def __init__(self, id_=None, type_=None, parent_id=None) -> None: + def __init__( + self, + id_: str | None = None, + type_: str | None = None, + parent_id: str | None = None, + ) -> None: super().__init__() create_time = time.time() @@ -1231,9 +1190,9 @@ def __init__(self, id_=None, type_=None, parent_id=None) -> None: self.server_id = None self.parent_id = parent_id self.type = type_ - self._sort = random.randint( + self._sort = random.randint( # noqa: suspicious-non-cryptographic-random-usage 1000000000, 9999999999 - ) # noqa: suspicious-non-cryptographic-random-usage + ) self._version = None self._text = "" self._children = {} @@ -1245,15 +1204,15 @@ def __init__(self, id_=None, type_=None, parent_id=None) -> None: self._moved = False @classmethod - def _generateId(cls, tz) -> str: + def _generateId(cls, tz: float) -> str: return "{:x}.{:016x}".format( int(tz * 1000), - random.randint( + random.randint( # noqa: suspicious-non-cryptographic-random-usage 0x0000000000000000, 0xFFFFFFFFFFFFFFFF - ), # noqa: suspicious-non-cryptographic-random-usage + ), ) - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) # Verify this is a valid type NodeType(raw["type"]) @@ -1273,7 +1232,7 @@ def _load(self, raw) -> None: self.settings.load(raw["nodeSettings"]) self.annotations.load(raw["annotationsGroup"]) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["id"] = self.id ret["kind"] = "notes#node" @@ -1295,13 +1254,12 @@ def sort(self) -> int: """Get the sort id. Returns: - ------- Sort id. """ return int(self._sort) @sort.setter - def sort(self, value) -> None: + def sort(self, value: int) -> None: self._sort = value self.touch() @@ -1310,7 +1268,6 @@ def version(self) -> int: """Get the node version. Returns: - ------- Version. """ return self._version @@ -1320,7 +1277,6 @@ def text(self) -> str: """Get the text value. Returns: - ------- Text value. """ return self._text @@ -1330,7 +1286,6 @@ def text(self, value: str) -> None: """Set the text value. Args: - ---- value: Text value. """ self._text = value @@ -1342,7 +1297,6 @@ def children(self) -> list["Node"]: """Get all children. Returns: - ------- Children nodes. """ return list(self._children.values()) @@ -1351,20 +1305,17 @@ def get(self, node_id: str) -> "Node | None": """Get child node with the given ID. Args: - ---- node_id: The node ID. Returns: - ------- Child node. """ return self._children.get(node_id) - def append(self, node: "Node", dirty=True) -> "Node": + def append(self, node: "Node", dirty: bool = True) -> "Node": """Add a new child node. Args: - ---- node: Node to add. dirty: Whether this node should be marked dirty. """ @@ -1375,11 +1326,10 @@ def append(self, node: "Node", dirty=True) -> "Node": return node - def remove(self, node: "Node", dirty=True) -> None: + def remove(self, node: "Node", dirty: bool = True) -> None: """Remove the given child node. Args: - ---- node: Node to remove. dirty: Whether this node should be marked dirty. """ @@ -1394,7 +1344,6 @@ def new(self) -> bool: """Get whether this node has been persisted to the server. Returns: - ------- True if node is new. """ return self.server_id is None @@ -1428,7 +1377,7 @@ class TopLevelNode(Node): _TYPE = None - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: dict) -> None: super().__init__(parent_id=Root.ID, **kwargs) self._color = ColorValue.White self._archived = False @@ -1437,7 +1386,7 @@ def __init__(self, **kwargs) -> None: self.labels = NodeLabels() self.collaborators = NodeCollaborators() - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self._color = ColorValue(raw["color"]) if "color" in raw else ColorValue.White self._archived = raw["isArchived"] if "isArchived" in raw else False @@ -1451,7 +1400,7 @@ def _load(self, raw) -> None: ) self._moved = "moved" in raw - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["color"] = self._color.value ret["isArchived"] = self._archived @@ -1472,13 +1421,12 @@ def color(self) -> ColorValue: """Get the node color. Returns: - ------- Color. """ return self._color @color.setter - def color(self, value) -> None: + def color(self, value: ColorValue) -> None: self._color = value self.touch(True) @@ -1487,13 +1435,12 @@ def archived(self) -> bool: """Get the archive state. Returns: - ------- Whether this node is archived. """ return self._archived @archived.setter - def archived(self, value) -> None: + def archived(self, value: bool) -> None: self._archived = value self.touch(True) @@ -1502,13 +1449,12 @@ def pinned(self) -> bool: """Get the pin state. Returns: - ------- Whether this node is pinned. """ return self._pinned @pinned.setter - def pinned(self, value) -> None: + def pinned(self, value: bool) -> None: self._pinned = value self.touch(True) @@ -1517,13 +1463,12 @@ def title(self) -> str: """Get the title. Returns: - ------- Title. """ return self._title @title.setter - def title(self, value) -> None: + def title(self, value: str) -> None: self._title = value self.touch(True) @@ -1532,7 +1477,6 @@ def url(self) -> str: """Get the url for this node. Returns: - ------- Google Keep url. """ return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @@ -1542,25 +1486,24 @@ def dirty(self) -> bool: return super().dirty or self.labels.dirty or self.collaborators.dirty @property - def blobs(self) -> list[Blob]: + def blobs(self) -> list["Blob"]: """Get all media blobs. Returns: - ------- Media blobs. """ return [node for node in self.children if isinstance(node, Blob)] @property - def images(self) -> list[NodeImage]: + def images(self) -> list["NodeImage"]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeImage)] @property - def drawings(self) -> list[NodeDrawing]: + def drawings(self) -> list["NodeDrawing"]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeDrawing)] @property - def audio(self) -> list[NodeAudio]: + def audio(self) -> list["NodeAudio"]: return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] @@ -1571,7 +1514,11 @@ class ListItem(Node): """ def __init__( - self, parent_id=None, parent_server_id=None, super_list_item_id=None, **kwargs + self, + parent_id: str | None = None, + parent_server_id: str | None = None, + super_list_item_id: str | None = None, + **kwargs: dict, ) -> None: super().__init__(type_=NodeType.ListItem, parent_id=parent_id, **kwargs) self.parent_item = None @@ -1581,13 +1528,13 @@ def __init__( self._subitems = {} self._checked = False - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self.prev_super_list_item_id = self.super_list_item_id self.super_list_item_id = raw.get("superListItemId") or None self._checked = raw.get("checked", False) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["parentServerId"] = self.parent_server_id ret["superListItemId"] = self.super_list_item_id @@ -1597,13 +1544,12 @@ def save(self, clean=True) -> dict: def add( self, text: str, - checked=False, + checked: bool = False, sort: NewListItemPlacementValue | int | None = None, ) -> "ListItem": """Add a new sub item to the list. This item must already be attached to a list. Args: - ---- text: The text. checked: Whether this item is checked. sort: Item id for sorting. @@ -1614,11 +1560,10 @@ def add( self.indent(node) return node - def indent(self, node: "ListItem", dirty=True) -> None: + def indent(self, node: "ListItem", dirty: bool = True) -> None: """Indent an item. Does nothing if the target has subitems. Args: - ---- node: Item to indent. dirty: Whether this node should be marked dirty. """ @@ -1631,11 +1576,10 @@ def indent(self, node: "ListItem", dirty=True) -> None: if dirty: node.touch(True) - def dedent(self, node: "ListItem", dirty=True) -> None: + def dedent(self, node: "ListItem", dirty: bool = True) -> None: """Dedent an item. Does nothing if the target is not indented under this item. Args: - ---- node: Item to dedent. dirty : Whether this node should be marked dirty. """ @@ -1649,11 +1593,10 @@ def dedent(self, node: "ListItem", dirty=True) -> None: node.touch(True) @property - def subitems(self) -> list[ListItem]: + def subitems(self) -> list["ListItem"]: """Get subitems for this item. Returns: - ------- Subitems. """ return List.sorted_items(self._subitems.values()) @@ -1663,7 +1606,6 @@ def indented(self) -> bool: """Get indentation state. Returns: - ------- Whether this item is indented. """ return self.parent_item is not None @@ -1673,13 +1615,12 @@ def checked(self) -> bool: """Get the checked state. Returns: - ------- Whether this item is checked. """ return self._checked @checked.setter - def checked(self, value) -> None: + def checked(self, value: bool) -> None: self._checked = value self.touch(True) @@ -1696,7 +1637,7 @@ class Note(TopLevelNode): _TYPE = NodeType.Note - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: dict) -> None: super().__init__(type_=self._TYPE, **kwargs) def _get_text_node(self) -> ListItem | None: @@ -1717,7 +1658,7 @@ def text(self) -> str: return node.text @text.setter - def text(self, value) -> None: + def text(self, value: str) -> None: node = self._get_text_node() if node is None: node = ListItem(parent_id=self.id) @@ -1735,19 +1676,18 @@ class List(TopLevelNode): _TYPE = NodeType.List SORT_DELTA = 10000 # Arbitrary constant - def __init__(self, **kwargs) -> None: + def __init__(self, **kwargs: dict) -> None: super().__init__(type_=self._TYPE, **kwargs) def add( self, text: str, - checked=False, + checked: bool = False, sort: NewListItemPlacementValue | int | None = None, ) -> ListItem: """Add a new item to the list. Args: - ---- text: The text. checked: Whether this item is checked. sort: Item id for sorting or a placement policy. @@ -1781,19 +1721,17 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: """Generate a list of sorted list items, taking into account parent items. Args: - ---- items: Items to sort. Returns: - ------- Sorted items. """ class t(tuple): """Tuple with element-based sorting""" - def __cmp__(self, other) -> int: + def __cmp__(self, other: "t") -> int: for a, b in itertools.zip_longest(self, other): if a != b: if a is None: @@ -1803,25 +1741,25 @@ def __cmp__(self, other) -> int: return a - b return 0 - def __lt__(self, other) -> bool: # pragma: no cover + def __lt__(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) < 0 - def __gt_(self, other) -> bool: # pragma: no cover + def __gt_(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) > 0 - def __le__(self, other) -> bool: # pragma: no cover + def __le__(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) <= 0 - def __ge_(self, other) -> bool: # pragma: no cover + def __ge_(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) >= 0 - def __eq__(self, other) -> bool: # pragma: no cover + def __eq__(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) == 0 - def __ne__(self, other) -> bool: # pragma: no cover + def __ne__(self, other: "t") -> bool: # pragma: no cover return self.__cmp__(other) != 0 - def key_func(x) -> t: + def key_func(x: ListItem) -> t: if x.indented: return t((int(x.parent_item.sort), int(x.sort))) return t((int(x.sort),)) @@ -1837,12 +1775,12 @@ def _items(self, checked: bool | None = None) -> list[ListItem]: and (checked is None or node.checked == checked) ] - def sort_items(self, key: Callable = attrgetter("text"), reverse=False) -> None: - """Sort list items in place. By default, the items are alphabetized, - but a custom function can be specified. + def sort_items( + 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. Args: - ---- key: A filter function. reverse: Whether to reverse the output. """ @@ -1863,7 +1801,6 @@ def items(self) -> list[ListItem]: """Get all listitems. Returns: - ------- List items. """ return self.sorted_items(self._items()) @@ -1873,7 +1810,6 @@ def checked(self) -> list[ListItem]: """Get all checked listitems. Returns: - ------- List items. """ return self.sorted_items(self._items(True)) @@ -1883,7 +1819,6 @@ def unchecked(self) -> list[ListItem]: """Get all unchecked listitems. Returns: - ------- List items. """ return self.sorted_items(self._items(False)) @@ -1894,7 +1829,7 @@ class NodeBlob(Element): _TYPE = None - def __init__(self, type_=None) -> None: + def __init__(self, type_: str | None = None) -> None: super().__init__() self.blob_id = None self.type = type_ @@ -1902,7 +1837,7 @@ def __init__(self, type_=None) -> None: self._mimetype = "" self._is_uploaded = False - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) # Verify this is a valid type BlobType(raw["type"]) @@ -1910,7 +1845,7 @@ def _load(self, raw) -> None: self._media_id = raw.get("media_id") self._mimetype = raw.get("mimetype") - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["kind"] = "notes#blob" ret["type"] = self.type.value @@ -1931,11 +1866,11 @@ def __init__(self) -> None: super().__init__(type_=self._TYPE) self._length = None - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self._length = raw.get("length") - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) if self._length is not None: ret["length"] = self._length @@ -1946,7 +1881,6 @@ def length(self) -> int: """Get length of the audio clip. Returns: - ------- Audio length. """ return self._length @@ -1966,7 +1900,7 @@ def __init__(self) -> None: self._extracted_text = "" self._extraction_status = "" - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self._is_uploaded = raw.get("is_uploaded") or False self._width = raw.get("width") @@ -1975,7 +1909,7 @@ def _load(self, raw) -> None: self._extracted_text = raw.get("extracted_text") self._extraction_status = raw.get("extraction_status") - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["width"] = self._width ret["height"] = self._height @@ -1989,7 +1923,6 @@ def width(self) -> int: """Get width of image. Returns: - ------- Image width. """ return self._width @@ -1999,7 +1932,6 @@ def height(self) -> int: """Get height of image. Returns: - ------- Image height. """ return self._height @@ -2009,7 +1941,6 @@ def byte_size(self) -> int: """Get size of image in bytes. Returns: - ------- Image byte size. """ return self._byte_size @@ -2017,6 +1948,7 @@ def byte_size(self) -> int: @property def extracted_text(self) -> str: """Get text extracted from image + Returns: Extracted text. """ @@ -2027,7 +1959,6 @@ def url(self) -> str: """Get a url to the image. Returns: - ------- Image url. """ raise NotImplementedError @@ -2044,7 +1975,7 @@ def __init__(self) -> None: self._extraction_status = "" self._drawing_info = None - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self._extracted_text = raw.get("extracted_text") self._extraction_status = raw.get("extraction_status") @@ -2054,7 +1985,7 @@ def _load(self, raw) -> None: drawing_info.load(raw["drawingInfo"]) self._drawing_info = drawing_info - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["extracted_text"] = self._extracted_text ret["extraction_status"] = self._extraction_status @@ -2065,6 +1996,7 @@ def save(self, clean=True) -> dict: @property def extracted_text(self) -> str: """Get text extracted from image + Returns: Extracted text. """ @@ -2087,7 +2019,7 @@ def __init__(self) -> None: self._ink_hash = "" self._snapshot_proto_fprint = "" - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self.drawing_id = raw["drawingId"] self.snapshot.load(raw["snapshotData"]) @@ -2108,7 +2040,7 @@ def _load(self, raw) -> None: else self._snapshot_proto_fprint ) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) ret["drawingId"] = self.drawing_id ret["snapshotData"] = self.snapshot.save(clean) @@ -2130,7 +2062,7 @@ class Blob(Node): BlobType.Drawing: NodeDrawing, } - def __init__(self, parent_id=None, **kwargs) -> None: + def __init__(self, parent_id: str | None = None, **kwargs: dict) -> None: super().__init__(type_=NodeType.Blob, parent_id=parent_id, **kwargs) self.blob = None @@ -2139,11 +2071,9 @@ def from_json(cls: type, raw: dict) -> NodeBlob | None: """Helper to construct a blob from a dict. Args: - ---- raw: Raw blob representation. Returns: - ------- A NodeBlob object or None. """ if raw is None: @@ -2166,11 +2096,11 @@ def from_json(cls: type, raw: dict) -> NodeBlob | None: return blob - def _load(self, raw) -> None: + def _load(self, raw: dict) -> None: super()._load(raw) self.blob = self.from_json(raw.get("blob")) - def save(self, clean=True) -> dict: + def save(self, clean: bool = True) -> dict: ret = super().save(clean) if self.blob is not None: ret["blob"] = self.blob.save(clean) @@ -2189,11 +2119,9 @@ def from_json(raw: dict) -> Node | None: """Helper to construct a node from a dict. Args: - ---- raw: Raw node representation. Returns: - ------- A Node object or None. """ ncls = None From 74c1d0547a17b7bfe6b65726da2481d015ffd8ae Mon Sep 17 00:00:00 2001 From: K Date: Wed, 12 Apr 2023 14:15:11 -0400 Subject: [PATCH 34/56] Typehints cleanup --- pyproject.toml | 1 - src/gkeepapi/__init__.py | 140 +++++++++------------------------------ src/gkeepapi/node.py | 2 +- 3 files changed, 34 insertions(+), 109 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1a3f190..8d8f04b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,6 @@ select = [ "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "T10", # flake8-debugger - "DJ", # flake8-django "EM", # flake8-errmsg "EXE", # flake8-executable "ISC", # flake8-implicit-str-concat diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 9a98246..45ba99c 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -31,17 +31,15 @@ def __init__(self, scopes: str) -> None: self._device_id = None self._scopes = scopes - def login(self, email: str, password: str, device_id: str): + def login(self, email: str, password: str, device_id: str) -> None: """Authenticate to Google with the provided credentials. Args: - ---- email: The account to use. password: The account password. device_id: An identifier for this client. Raises: - ------ LoginException: If there was a problem logging in. """ self._email = email @@ -67,13 +65,11 @@ def load(self, email: str, master_token: str, device_id: str) -> bool: """Authenticate to Google with the provided master token. Args: - ---- email: The account to use. master_token: The master token. device_id: An identifier for this client. Raises: - ------ LoginException: If there was a problem logging in. """ self._email = email @@ -88,18 +84,14 @@ def getMasterToken(self) -> str: """Gets the master token. Returns: - ------- The account master token. """ return self._master_token - def setMasterToken(self, master_token: str): - """Sets the master token. This is useful if you'd like to authenticate - with the API without providing your username & password. - Do note that the master token has full access to your account. + def setMasterToken(self, master_token: str) -> None: + """Sets the master token. This is useful if you'd like to authenticate with the API without providing your username & password. Do note that the master token has full access to your account. Args: - ---- master_token: The account master token. """ self._master_token = master_token @@ -108,16 +100,14 @@ def getEmail(self) -> str: """Gets the account email. Returns: - ------- The account email. """ return self._email - def setEmail(self, email: str): + def setEmail(self, email: str) -> None: """Sets the account email. Args: - ---- email: The account email. """ self._email = email @@ -126,16 +116,14 @@ def getDeviceId(self) -> str: """Gets the device id. Returns: - ------- The device id. """ return self._device_id - def setDeviceId(self, device_id: str): + def setDeviceId(self, device_id: str) -> None: """Sets the device id. Args: - ---- device_id: The device id. """ self._device_id = device_id @@ -144,7 +132,6 @@ def getAuthToken(self) -> str | None: """Gets the auth token. Returns: - ------- The auth token. """ return self._auth_token @@ -153,11 +140,9 @@ def refresh(self) -> str: """Refresh the OAuth token. Returns: - ------- The auth token. Raises: - ------ LoginException: If there was a problem refreshing the OAuth token. """ # Obtain an OAuth token with the necessary scopes by pretending to be @@ -177,7 +162,7 @@ def refresh(self) -> str: self._auth_token = res["Auth"] return self._auth_token - def logout(self): + def logout(self) -> None: """Log out of the account.""" self._master_token = None self._auth_token = None @@ -206,34 +191,29 @@ def getAuth(self) -> APIAuth: """Get authentication details for this API. Return: - ------ auth: The auth object """ return self._auth - def setAuth(self, auth: APIAuth): + def setAuth(self, auth: APIAuth) -> None: """Set authentication details for this API. Args: - ---- auth: The auth object """ self._auth = auth - def send(self, **req_kwargs) -> dict: + def send(self, **req_kwargs: dict) -> dict: """Send an authenticated request to a Google API. Automatically retries if the access token has expired. Args: - ---- **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: - ------ The parsed JSON response. Raises: - ------ APIException: If the server returns an error. LoginException: If :py:meth:`login` has not been called. """ @@ -263,19 +243,16 @@ def send(self, **req_kwargs) -> dict: return response - def _send(self, **req_kwargs) -> requests.Response: + def _send(self, **req_kwargs: dict) -> requests.Response: """Send an authenticated request to a Google API. Args: - ---- **req_kwargs: Arbitrary keyword arguments to pass to Requests. Return: - ------ The raw response. Raises: - ------ LoginException: If :py:meth:`login` has not been called. """ # Bail if we don't have an OAuth token. @@ -317,17 +294,14 @@ def changes( """Sync up (and down) all changes. Args: - ---- target_version: The local change version. nodes: A list of nodes to sync up to the server. labels: A list of labels to sync up to the server. Return: - ------ Description of all changes. Raises: - ------ APIException: If the server returns an error. """ # Handle defaults. @@ -401,11 +375,9 @@ def get(self, blob: _node.Blob) -> str: """Get the canonical link to a media blob. Args: - ---- blob: The blob. Returns: - ------- A link to the media. """ url = self._base_url + blob.parent.server_id + "/" + blob.server_id @@ -449,7 +421,6 @@ def create( """Create a new reminder. Args: - ---- node_id: The note ID. node_server_id: The note server ID. dtime: The due date of this reminder. @@ -457,7 +428,6 @@ def create( Return: ??? Raises: - ------ APIException: If the server returns an error. """ params = {} @@ -499,7 +469,6 @@ def update( """Update an existing reminder. Args: - ---- node_id: The note ID. node_server_id: The note server ID. dtime: The due date of this reminder. @@ -507,7 +476,6 @@ def update( Return: ??? Raises: - ------ APIException: If the server returns an error. """ params = {} @@ -556,13 +524,11 @@ def delete(self, node_server_id: str) -> Any: """Delete an existing reminder. Args: - ---- node_server_id: The note server ID. Return: ??? Raises: - ------ APIException: If the server returns an error. """ params = {} @@ -584,19 +550,16 @@ def delete(self, node_server_id: str) -> Any: return self.send(url=self._base_url + "batchmutate", method="POST", json=params) - def list(self, master=True) -> Any: + def list(self, master: bool = True) -> Any: """List current reminders. Args: - ---- master: ??? Return: - ------ ??? Raises: - ------ APIException: If the server returns an error. """ params = {} @@ -638,15 +601,12 @@ def history(self, storage_version: str) -> Any: """Get reminder changes. Args: - ---- storage_version: The local storage version. Returns: - ------- ??? Raises: - ------ APIException: If the server returns an error. """ params = { @@ -703,7 +663,7 @@ def __init__(self) -> None: self._clear() - def _clear(self): + def _clear(self) -> None: self._keep_version = None self._reminder_version = None self._labels = {} @@ -718,13 +678,12 @@ def login( email: str, password: str, state: dict | None = None, - sync=True, + sync: bool = True, device_id: str | None = None, - ): + ) -> None: """Authenticate to Google with the provided credentials & sync. Args: - ---- email: The account to use. password: The account password. state: Serialized state to load. @@ -732,7 +691,6 @@ def login( device_id: Device id. Raises: - ------ LoginException: If there was a problem logging in. """ auth = APIAuth(self.OAUTH_SCOPES) @@ -747,13 +705,12 @@ def resume( email: str, master_token: str, state: dict | None = None, - sync=True, + sync: bool = True, device_id: str | None = None, - ): + ) -> None: """Authenticate to Google with the provided master token & sync. Args: - ---- email: The account to use. master_token: The master token. state: Serialized state to load. @@ -761,7 +718,6 @@ def resume( device_id: Device id. Raises: - ------ LoginException: If there was a problem logging in. """ auth = APIAuth(self.OAUTH_SCOPES) @@ -775,22 +731,19 @@ def getMasterToken(self) -> str: """Get master token for resuming. Returns: - ------- The master token. """ return self._keep_api.getAuth().getMasterToken() - def load(self, auth: APIAuth, state: dict | None = None, sync=True): + def load(self, auth: APIAuth, state: dict | None = None, sync: bool = True) -> None: """Authenticate to Google with a prepared authentication object & sync. Args: - ---- auth: Authentication object. state: Serialized state to load. sync: Whether to sync data. Raises: - ------ LoginException: If there was a problem logging in. """ self._keep_api.setAuth(auth) @@ -805,7 +758,6 @@ def dump(self) -> dict: """Serialize note data. Returns: - ------- Serialized state. """ # Find all nodes manually, as the Keep object isn't aware of new @@ -821,11 +773,10 @@ def dump(self) -> dict: "nodes": [node.save(False) for node in nodes], } - def restore(self, state: dict): + def restore(self, state: dict) -> None: """Unserialize saved note data. Args: - ---- state: Serialized state to load. """ self._clear() @@ -837,27 +788,23 @@ def get(self, node_id: str) -> _node.TopLevelNode: """Get a note with the given ID. Args: - ---- node_id: The note ID. Returns: - ------- The Note or None if not found. """ return self._nodes[_node.Root.ID].get(node_id) or self._nodes[ _node.Root.ID ].get(self._sid_map.get(node_id)) - def add(self, node: _node.Node): + def add(self, node: _node.Node) -> None: """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. Args: - ---- node: The node to sync. Raises: - ------ InvalidException: If the parent node is not found. """ if node.parent_id != _node.Root.ID: @@ -875,11 +822,10 @@ def find( pinned: bool | None = None, archived: bool | None = None, trashed: bool = False, - ) -> Iterator[_node.TopLevelNode]: # pylint: disable=too-many-arguments + ) -> Iterator[_node.TopLevelNode]: """Find Notes based on the specified criteria. Args: - ---- query: A str or regular expression to match against the title and text. func: A filter function. labels: A list of label ids or objects to match. An empty list matches notes with no labels. @@ -889,7 +835,6 @@ def find( trashed: Whether to match trashed notes. Return: - ------ Search results. """ if labels is not None: @@ -935,12 +880,10 @@ def createNote( """Create a new managed note. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: - ---- title: The title of the note. text: The text of the note. Returns: - ------- The new note. """ node = _node.Note() @@ -959,12 +902,10 @@ def createList( """Create a new list and populate it. Any changes to the note will be uploaded when :py:meth:`sync` is called. Args: - ---- title: The title of the list. items: A list of tuples. Each tuple represents the text and checked status of the listitem. Returns: - ------- The new list. """ if items is None: @@ -985,34 +926,31 @@ def createLabel(self, name: str) -> _node.Label: """Create a new label. Args: - ---- name: Label name. Returns: - ------- The new label. Raises: - ------ LabelException: If the label exists. """ if self.findLabel(name): raise exception.LabelException("Label exists") node = _node.Label() node.name = name - self._labels[node.id] = node # pylint: disable=protected-access + self._labels[node.id] = node return node - def findLabel(self, query: re.Pattern | str, create=False) -> _node.Label | None: + def findLabel( + self, query: re.Pattern | str, create: bool = False + ) -> _node.Label | None: """Find a label with the given name. Args: - ---- name: A str or regular expression to match against the name. create: Whether to create the label if it doesn't exist (only if name is a str). Returns: - ------- The label. """ is_str = isinstance(query, str) @@ -1034,20 +972,17 @@ def getLabel(self, label_id: str) -> _node.Label | None: """Get an existing label. Args: - ---- label_id: Label id. Returns: - ------- The label. """ return self._labels.get(label_id) - def deleteLabel(self, label_id: str): + def deleteLabel(self, label_id: str) -> None: """Deletes a label. Args: - ---- label_id: Label id. """ if label_id not in self._labels: @@ -1062,7 +997,6 @@ def labels(self) -> list[_node.Label]: """Get all labels. Returns: - ------- Labels """ return list(self._labels.values()) @@ -1071,11 +1005,9 @@ def getMediaLink(self, blob: _node.Blob) -> str: """Get the canonical link to media. Args: - ---- blob: The media resource. Returns: - ------- A link to the media. """ return self._media_api.get(blob) @@ -1084,22 +1016,18 @@ def all(self) -> list[_node.TopLevelNode]: """Get all Notes. Returns: - ------- Notes: - ----- """ return self._nodes[_node.Root.ID].children - def sync(self, resync=False): + def sync(self, resync: bool = False) -> None: """Sync the local Keep tree with the server. If resyncing, local changes will be destroyed. Otherwise, local changes to notes, labels and reminders will be detected and synced up. Args: - ---- resync: Whether to resync data. Raises: - ------ SyncException: If there is a consistency issue. """ # Clear all state if we want to resync. @@ -1112,7 +1040,7 @@ def sync(self, resync=False): if _node.DEBUG: self._clean() - def _sync_reminders(self): + def _sync_reminders(self) -> None: # Fetch updates until we reach the newest version. while True: logger.debug("Starting reminder sync: %s", self._reminder_version) @@ -1130,7 +1058,7 @@ def _sync_reminders(self): if self._reminder_version == history["highestStorageVersion"]: break - def _sync_notes(self): + def _sync_notes(self) -> None: # Fetch updates until we reach the newest version. while True: logger.debug("Starting keep sync: %s", self._keep_version) @@ -1166,10 +1094,10 @@ def _sync_notes(self): if not changes["truncated"]: break - def _parseTasks(self, raw: dict): + def _parseTasks(self, raw: dict) -> None: pass - def _parseNodes(self, raw: dict): # pylint: disable=too-many-branches + def _parseNodes(self, raw: dict) -> None: created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1241,12 +1169,10 @@ def _parseNodes(self, raw: dict): # pylint: disable=too-many-branches # Hydrate label references in notes. for node in self.all(): - for label_id in node.labels._labels: # pylint: disable=protected-access - node.labels._labels[label_id] = self._labels.get( - label_id - ) # pylint: disable=protected-access + for label_id in node.labels._labels: + node.labels._labels[label_id] = self._labels.get(label_id) - def _parseUserInfo(self, raw: dict): + def _parseUserInfo(self, raw: dict) -> None: labels = {} if "labels" in raw: for label in raw["labels"]: @@ -1284,7 +1210,7 @@ def _findDirtyNodes(self) -> list[_node.Node]: return nodes - def _clean(self): + def _clean(self) -> None: """Recursively check that all nodes are reachable.""" found_ids = set() nodes = [self._nodes[_node.Root.ID]] diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index ef060a5..d8b5d13 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -891,7 +891,7 @@ def __len__(self) -> int: def load( self, collaborators_raw: list, requests_raw: list - ) -> None: # pylint: disable=arguments-differ + ) -> None: # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() From c6a2f72cd6df808c2348b1c0be614684938f7e2f Mon Sep 17 00:00:00 2001 From: K Date: Mon, 17 Apr 2023 11:47:38 -0400 Subject: [PATCH 35/56] Lint cleanup --- pyproject.toml | 2 ++ src/gkeepapi/node.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 8d8f04b..589bf3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -96,6 +96,8 @@ ignore = [ "EM102", "ANN101", "ANN102", + + "D105", ] [tool.ruff.pydocstyle] diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index d8b5d13..4fb8dee 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -276,6 +276,7 @@ class Annotation(Element): """Note annotations base class.""" def __init__(self) -> None: + """Construct a note annotation""" super().__init__() self.id = self._generateAnnotationId() @@ -284,6 +285,9 @@ def _load(self, raw: dict) -> None: self.id = raw.get("id") def save(self, clean: bool = True) -> dict: + """ + Save the annotation + """ ret = {} if self.id is not None: ret = super().save(clean) @@ -316,6 +320,7 @@ class WebLink(Annotation): """Represents a link annotation on a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct a weblink""" super().__init__() self._title = "" self._url = "" @@ -336,6 +341,7 @@ def _load(self, raw: dict) -> None: self._description = raw["webLink"]["description"] def save(self, clean: bool = True) -> dict: + """Save the weblink""" ret = super().save(clean) ret["webLink"] = { "title": self._title, @@ -421,6 +427,7 @@ class Category(Annotation): """Represents a category annotation on a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct a category annotation""" super().__init__() self._category = None @@ -429,6 +436,7 @@ def _load(self, raw: dict) -> None: self._category = CategoryValue(raw["topicCategory"]["category"]) def save(self, clean: bool = True) -> dict: + """Save the category annotation""" ret = super().save(clean) ret["topicCategory"] = {"category": self._category.value} return ret @@ -452,6 +460,7 @@ class TaskAssist(Annotation): """Unknown.""" def __init__(self) -> None: + """Construct a taskassist annotation""" super().__init__() self._suggest = None @@ -460,6 +469,7 @@ def _load(self, raw: dict) -> None: self._suggest = raw["taskAssist"]["suggestType"] def save(self, clean: bool = True) -> dict: + """Save the taskassist annotation""" ret = super().save(clean) ret["taskAssist"] = {"suggestType": self._suggest} return ret @@ -483,6 +493,7 @@ class Context(Annotation): """Represents a context annotation, which may contain other annotations.""" def __init__(self) -> None: + """Construct a context annotation""" super().__init__() self._entries = {} @@ -493,6 +504,7 @@ def _load(self, raw: dict) -> None: self._entries[key] = NodeAnnotations.from_json({key: entry}) def save(self, clean: bool = True) -> dict: + """Save the context annotation""" ret = super().save(clean) context = {} for entry in self._entries.values(): @@ -519,6 +531,7 @@ class NodeAnnotations(Element): """Represents the annotation container on a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct an annotations container""" super().__init__() self._annotations = {} @@ -572,6 +585,7 @@ def _load(self, raw: dict) -> None: self._annotations[annotation.id] = annotation def save(self, clean: bool = True) -> dict: + """Save the annotations container""" ret = super().save(clean) ret["kind"] = "notes#annotationsGroup" if self._annotations: @@ -660,6 +674,7 @@ class NodeTimestamps(Element): TZ_FMT = "%Y-%m-%dT%H:%M:%S.%fZ" def __init__(self, create_time: float | None = None) -> None: + """Construct a timestamps container""" super().__init__() if create_time is None: create_time = time.time() @@ -682,6 +697,7 @@ def _load(self, raw: dict) -> None: ) def save(self, clean: bool = True) -> dict: + """Save the timestamps container""" ret = super().save(clean) ret["kind"] = "notes#timestamps" ret["created"] = self.dt_to_str(self._created) @@ -814,6 +830,7 @@ class NodeSettings(Element): """Represents the settings associated with a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct a settings container""" super().__init__() self._new_listitem_placement = NewListItemPlacementValue.Bottom self._graveyard_state = GraveyardStateValue.Collapsed @@ -830,6 +847,7 @@ def _load(self, raw: dict) -> None: ) def save(self, clean: bool = True) -> dict: + """Save the settings container""" ret = super().save(clean) ret["newListItemPlacement"] = self._new_listitem_placement.value ret["graveyardState"] = self._graveyard_state.value @@ -883,6 +901,7 @@ class NodeCollaborators(Element): """Represents the collaborators on a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct a collaborators container""" super().__init__() self._collaborators = {} @@ -906,6 +925,7 @@ def load( ) def save(self, clean: bool = True) -> tuple[list, list]: + """Save the collaborators container""" # Parent method not called. collaborators = [] requests = [] @@ -962,6 +982,7 @@ class TimestampsMixin: """A mixin to add methods for updating timestamps.""" def __init__(self) -> None: + """Instantiate mixin""" self.timestamps: NodeTimestamps def touch(self, edited: bool = False) -> None: @@ -1021,6 +1042,7 @@ class Label(Element, TimestampsMixin): """Represents a label.""" def __init__(self) -> None: + """Construct a label""" super().__init__() create_time = time.time() @@ -1056,6 +1078,7 @@ def _load(self, raw: dict) -> None: ) def save(self, clean: bool = True) -> dict: + """Save the label""" ret = super().save(clean) ret["mainId"] = self.id ret["name"] = self._name @@ -1103,6 +1126,7 @@ class NodeLabels(Element): """Represents the labels on a :class:`TopLevelNode`.""" def __init__(self) -> None: + """Construct a labels container""" super().__init__() self._labels = {} @@ -1181,6 +1205,7 @@ def __init__( type_: str | None = None, parent_id: str | None = None, ) -> None: + """Construct a node""" super().__init__() create_time = time.time() @@ -1365,6 +1390,7 @@ class Root(Node): ID = "root" def __init__(self) -> None: + """Construct a root node""" super().__init__(id_=self.ID) @property @@ -1378,6 +1404,7 @@ class TopLevelNode(Node): _TYPE = None def __init__(self, **kwargs: dict) -> None: + """Construct a top level node""" super().__init__(parent_id=Root.ID, **kwargs) self._color = ColorValue.White self._archived = False @@ -1520,6 +1547,7 @@ def __init__( super_list_item_id: str | None = None, **kwargs: dict, ) -> None: + """Construct a list item node""" super().__init__(type_=NodeType.ListItem, parent_id=parent_id, **kwargs) self.parent_item = None self.parent_server_id = parent_server_id @@ -1638,6 +1666,7 @@ class Note(TopLevelNode): _TYPE = NodeType.Note def __init__(self, **kwargs: dict) -> None: + """Construct a note node""" super().__init__(type_=self._TYPE, **kwargs) def _get_text_node(self) -> ListItem | None: @@ -1677,6 +1706,7 @@ class List(TopLevelNode): SORT_DELTA = 10000 # Arbitrary constant def __init__(self, **kwargs: dict) -> None: + """Construct a list node""" super().__init__(type_=self._TYPE, **kwargs) def add( @@ -1830,6 +1860,7 @@ class NodeBlob(Element): _TYPE = None def __init__(self, type_: str | None = None) -> None: + """Construct a node blob""" super().__init__() self.blob_id = None self.type = type_ @@ -1863,6 +1894,7 @@ class NodeAudio(NodeBlob): _TYPE = BlobType.Audio def __init__(self) -> None: + """Construct a node audio blob""" super().__init__(type_=self._TYPE) self._length = None @@ -1892,6 +1924,7 @@ class NodeImage(NodeBlob): _TYPE = BlobType.Image def __init__(self) -> None: + """Construct a node image blob""" super().__init__(type_=self._TYPE) self._is_uploaded = False self._width = 0 @@ -1970,6 +2003,7 @@ class NodeDrawing(NodeBlob): _TYPE = BlobType.Drawing def __init__(self) -> None: + """Construct a node drawing blob""" super().__init__(type_=self._TYPE) self._extracted_text = "" self._extraction_status = "" @@ -2063,6 +2097,7 @@ class Blob(Node): } def __init__(self, parent_id: str | None = None, **kwargs: dict) -> None: + """Construct a blob""" super().__init__(type_=NodeType.Blob, parent_id=parent_id, **kwargs) self.blob = None From fb0a8e7bb4c9932cab96bd6e2492de19617e41d3 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 25 Apr 2023 12:07:21 -0400 Subject: [PATCH 36/56] Update ruff --- pyproject.toml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 589bf3e..661a874 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ ] optional-dependencies = [ "black>=23.3.0", - "ruff>=0.0.261", + "ruff>=0.0.263", ] @@ -39,8 +39,8 @@ target-version = "py310" select = [ # https://beta.ruff.rs/docs/rules/ "F", # pyflakes - "E", # pycodestyle - "W", # pycodestyle + "E", # pycodestyle error + "W", # pycodestyle warning "C90", # mccabe "I", # isort "N", # pep8-naming @@ -57,6 +57,7 @@ select = [ "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "T10", # flake8-debugger + "DJ", # flake8-django "EM", # flake8-errmsg "EXE", # flake8-executable "ISC", # flake8-implicit-str-concat @@ -77,13 +78,15 @@ select = [ "INT", # flake8-gettext "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib + # "ERA", # eradicate "PGH", # pygrep-hooks - "PLC", # pylint - "PLE", # pylint - "PLR", # pylint - "PLW", # pylint + "PLC", # pylint convention + "PLE", # pylint error + "PLR", # pylint refactor + "PLW", # pylint warning # "TRY", # tryceratops - "RUF", # ruff-specific rules + "NPY", # numpy-specific + "RUF", # ruff-specific ] ignore = [ "E501", # line-too-long -- disabled as black takes care of this @@ -93,7 +96,9 @@ ignore = [ "D415", "D203", "D213", + "EM101", "EM102", + "N818", "ANN101", "ANN102", From 792dd21dc94682abfbd389d5e119dd3bd8d39d5f Mon Sep 17 00:00:00 2001 From: K Date: Tue, 25 Apr 2023 12:08:13 -0400 Subject: [PATCH 37/56] Lint cleanup --- src/gkeepapi/node.py | 72 ++++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 4fb8dee..fb21118 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -1,4 +1,5 @@ """.. automodule:: gkeepapi + :members: :inherited-members: @@ -285,9 +286,7 @@ def _load(self, raw: dict) -> None: self.id = raw.get("id") def save(self, clean: bool = True) -> dict: - """ - Save the annotation - """ + """Save the annotation""" ret = {} if self.id is not None: ret = super().save(clean) @@ -521,7 +520,7 @@ def all(self) -> list[Annotation]: # noqa: A003 return list(self._entries.values()) @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return super().dirty or any( annotation.dirty for annotation in self._entries.values() ) @@ -566,7 +565,7 @@ def from_json(cls, raw: dict) -> Annotation | None: return annotation - def all(self) -> list[Annotation]: + def all(self) -> list[Annotation]: # noqa: A003 """Get all annotations. Returns: @@ -662,7 +661,7 @@ def remove(self, annotation: Annotation) -> None: self._dirty = True @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return super().dirty or any( annotation.dirty for annotation in self._annotations.values() ) @@ -908,7 +907,7 @@ def __init__(self) -> None: def __len__(self) -> int: return len(self._collaborators) - def load( + def load( # noqa: D102 self, collaborators_raw: list, requests_raw: list ) -> None: # Parent method not called. @@ -965,7 +964,7 @@ def remove(self, email: str) -> None: self._collaborators[email] = ShareRequestValue.Remove self._dirty = True - def all(self) -> list[str]: + def all(self) -> list[str]: # noqa: A003 """Get all collaborators. Returns: @@ -1115,7 +1114,7 @@ def merged(self, value: datetime.datetime) -> None: self.touch() @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return super().dirty or self.timestamps.dirty def __str__(self) -> str: @@ -1143,7 +1142,7 @@ def _load(self, raw: list) -> None: for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: + def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: D102 # Parent method not called. ret = [ { @@ -1187,7 +1186,7 @@ def get(self, label_id: str) -> str: """ return self._labels.get(label_id) - def all(self) -> list[Label]: + def all(self) -> list[Label]: # noqa: A003 """Get all labels. Returns: @@ -1257,7 +1256,7 @@ def _load(self, raw: dict) -> None: self.settings.load(raw["nodeSettings"]) self.annotations.load(raw["annotationsGroup"]) - def save(self, clean: bool = True) -> dict: + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["id"] = self.id ret["kind"] = "notes#node" @@ -1374,7 +1373,7 @@ def new(self) -> bool: return self.server_id is None @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return ( super().dirty or self.timestamps.dirty @@ -1394,7 +1393,7 @@ def __init__(self) -> None: super().__init__(id_=self.ID) @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return False @@ -1427,7 +1426,7 @@ def _load(self, raw: dict) -> None: ) self._moved = "moved" in raw - def save(self, clean: bool = True) -> dict: + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["color"] = self._color.value ret["isArchived"] = self._archived @@ -1509,7 +1508,7 @@ def url(self) -> str: return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @property - def dirty(self) -> bool: + def dirty(self) -> bool: # noqa: D102 return super().dirty or self.labels.dirty or self.collaborators.dirty @property @@ -1523,19 +1522,23 @@ def blobs(self) -> list["Blob"]: @property def images(self) -> list["NodeImage"]: + """Get all image blobs""" return [blob for blob in self.blobs if isinstance(blob.blob, NodeImage)] @property def drawings(self) -> list["NodeDrawing"]: + """Get all drawing blobs""" return [blob for blob in self.blobs if isinstance(blob.blob, NodeDrawing)] @property def audio(self) -> list["NodeAudio"]: + """Get all audio blobs""" return [blob for blob in self.blobs if isinstance(blob.blob, NodeAudio)] class ListItem(Node): """Represents a Google Keep listitem. + Interestingly enough, :class:`Note`s store their content in a single child :class:`ListItem`. """ @@ -1562,7 +1565,7 @@ def _load(self, raw: dict) -> None: self.super_list_item_id = raw.get("superListItemId") or None self._checked = raw.get("checked", False) - def save(self, clean: bool = True) -> dict: + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["parentServerId"] = self.parent_server_id ret["superListItemId"] = self.super_list_item_id @@ -1679,7 +1682,7 @@ def _get_text_node(self) -> ListItem | None: return node @property - def text(self) -> str: + def text(self) -> str: # noqa: D102 node = self._get_text_node() if node is None: @@ -1743,11 +1746,11 @@ def add( return node @property - def text(self) -> str: + def text(self) -> str: # noqa: D102 return "\n".join(str(node) for node in self.items) @classmethod - def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: + def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: # noqa: C901 """Generate a list of sorted list items, taking into account parent items. Args: @@ -1758,10 +1761,10 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: Sorted items. """ - class t(tuple): + class T(tuple): """Tuple with element-based sorting""" - def __cmp__(self, other: "t") -> int: + def __cmp__(self, other: "T") -> int: for a, b in itertools.zip_longest(self, other): if a != b: if a is None: @@ -1771,28 +1774,28 @@ def __cmp__(self, other: "t") -> int: return a - b return 0 - def __lt__(self, other: "t") -> bool: # pragma: no cover + def __lt__(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) < 0 - def __gt_(self, other: "t") -> bool: # pragma: no cover + def __gt_(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) > 0 - def __le__(self, other: "t") -> bool: # pragma: no cover + def __le__(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) <= 0 - def __ge_(self, other: "t") -> bool: # pragma: no cover + def __ge_(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) >= 0 - def __eq__(self, other: "t") -> bool: # pragma: no cover + def __eq__(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) == 0 - def __ne__(self, other: "t") -> bool: # pragma: no cover + def __ne__(self, other: "T") -> bool: # pragma: no cover return self.__cmp__(other) != 0 - def key_func(x: ListItem) -> t: + def key_func(x: ListItem) -> T: if x.indented: - return t((int(x.parent_item.sort), int(x.sort))) - return t((int(x.sort),)) + return T((int(x.parent_item.sort), int(x.sort))) + return T((int(x.sort),)) return sorted(items, key=key_func, reverse=True) @@ -1877,6 +1880,7 @@ def _load(self, raw: dict) -> None: self._mimetype = raw.get("mimetype") def save(self, clean: bool = True) -> dict: + """Save the node blob""" ret = super().save(clean) ret["kind"] = "notes#blob" ret["type"] = self.type.value @@ -1903,6 +1907,7 @@ def _load(self, raw: dict) -> None: self._length = raw.get("length") def save(self, clean: bool = True) -> dict: + """Save the node audio blob""" ret = super().save(clean) if self._length is not None: ret["length"] = self._length @@ -1943,6 +1948,7 @@ def _load(self, raw: dict) -> None: self._extraction_status = raw.get("extraction_status") def save(self, clean: bool = True) -> dict: + """Save the node image blob""" ret = super().save(clean) ret["width"] = self._width ret["height"] = self._height @@ -2020,6 +2026,7 @@ def _load(self, raw: dict) -> None: self._drawing_info = drawing_info def save(self, clean: bool = True) -> dict: + """Save the node drawing blob""" ret = super().save(clean) ret["extracted_text"] = self._extracted_text ret["extraction_status"] = self._extraction_status @@ -2074,7 +2081,7 @@ def _load(self, raw: dict) -> None: else self._snapshot_proto_fprint ) - def save(self, clean: bool = True) -> dict: + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["drawingId"] = self.drawing_id ret["snapshotData"] = self.snapshot.save(clean) @@ -2136,6 +2143,7 @@ def _load(self, raw: dict) -> None: self.blob = self.from_json(raw.get("blob")) def save(self, clean: bool = True) -> dict: + """Save the blob""" ret = super().save(clean) if self.blob is not None: ret["blob"] = self.blob.save(clean) From 703f6dba26be3ae028f051400d503275265a059f Mon Sep 17 00:00:00 2001 From: K Date: Sun, 30 Apr 2023 13:07:01 -0400 Subject: [PATCH 38/56] Lints --- pyproject.toml | 23 +++++++++----------- src/gkeepapi/__init__.py | 45 ++++++++++++++++++++-------------------- src/gkeepapi/node.py | 39 +++++++++++++++++----------------- 3 files changed, 52 insertions(+), 55 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 661a874..8e557d0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,19 +90,16 @@ select = [ ] ignore = [ "E501", # line-too-long -- disabled as black takes care of this - "N802", - "COM812", - "D400", - "D415", - "D203", - "D213", - "EM101", - "EM102", - "N818", - "ANN101", - "ANN102", - - "D105", + "N802", # invalid-function-name -- too late! + "COM812", # missing-trailing-comma -- conflicts with black? + "D415", # ends-in-punctuation -- too aggressive + "EM101", # raw-string-in-exception -- no thanks + "EM102", # f-string-in-exception -- no thanks + "N818", # error-suffix-on-exception-name -- too late! + "ANN101", # missing-type-self -- unnecessary + "ANN102", # missing-type-cls -- unnecessary + "PLR0913", # too-many-arguments -- no thanks + "D105", # undocumented-magic-method -- no thanks ] [tool.ruff.pydocstyle] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 45ba99c..9f61599 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -3,6 +3,7 @@ __version__ = "1.0.0" import datetime +import http import logging import random import re @@ -204,8 +205,7 @@ def setAuth(self, auth: APIAuth) -> None: self._auth = auth def send(self, **req_kwargs: dict) -> dict: - """Send an authenticated request to a Google API. - Automatically retries if the access token has expired. + """Send an authenticated request to a Google API. Automatically retries if the access token has expired. Args: **req_kwargs: Arbitrary keyword arguments to pass to Requests. @@ -229,7 +229,7 @@ def send(self, **req_kwargs: dict) -> dict: # Otherwise, check if it was a non-401 response code. These aren't # handled, so bail. error = response["error"] - if error["code"] != 401: + if error["code"] != http.HTTPStatus.UNAUTHORIZED: raise exception.APIException(error["code"], error) # If we've exceeded the retry limit, also bail. @@ -283,7 +283,10 @@ def __init__(self, auth: APIAuth | None = None) -> None: @classmethod def _generateId(cls, tz: int) -> str: - return "s--%d--%d" % (int(tz * 1000), random.randint(1000000000, 9999999999)) + return "s--%d--%d" % ( + int(tz * 1000), + random.randint(1000000000, 9999999999), # noqa: S311 + ) def changes( self, @@ -382,7 +385,7 @@ def get(self, blob: _node.Blob) -> str: """ url = self._base_url + blob.parent.server_id + "/" + blob.server_id if blob.blob.type == _node.BlobType.Drawing: - url += "/" + blob.blob._drawing_info.drawing_id + url += "/" + blob.blob._drawing_info.drawing_id # noqa: SLF001 return self._send(url=url, method="GET", allow_redirects=False).headers[ "location" ] @@ -417,7 +420,7 @@ def __init__(self, auth: APIAuth | None = None) -> None: def create( self, node_id: str, node_server_id: str, dtime: datetime.datetime - ) -> Any: + ) -> Any: # noqa: ANN401 """Create a new reminder. Args: @@ -463,9 +466,9 @@ def create( return self.send(url=self._base_url + "create", method="POST", json=params) - def update( + def update_internal( self, node_id: str, node_server_id: str, dtime: datetime.datetime - ) -> Any: + ) -> Any: # noqa: ANN401 """Update an existing reminder. Args: @@ -520,7 +523,7 @@ def update( return self.send(url=self._base_url + "update", method="POST", json=params) - def delete(self, node_server_id: str) -> Any: + def delete(self, node_server_id: str) -> Any: # noqa: ANN401 """Delete an existing reminder. Args: @@ -550,7 +553,7 @@ def delete(self, node_server_id: str) -> Any: return self.send(url=self._base_url + "batchmutate", method="POST", json=params) - def list(self, master: bool = True) -> Any: + def list(self, master: bool = True) -> Any: # noqa: ANN401, A003 """List current reminders. Args: @@ -597,7 +600,7 @@ def list(self, master: bool = True) -> Any: return self.send(url=self._base_url + "list", method="POST", json=params) - def history(self, storage_version: str) -> Any: + def history(self, storage_version: str) -> Any: # noqa: ANN401 """Get reminder changes. Args: @@ -617,7 +620,7 @@ def history(self, storage_version: str) -> Any: return self.send(url=self._base_url + "history", method="POST", json=params) - def update(self) -> Any: + def update(self) -> Any: # noqa: ANN401 """Sync up changes to reminders.""" params = {} return self.send(url=self._base_url + "update", method="POST", json=params) @@ -798,8 +801,7 @@ def get(self, node_id: str) -> _node.TopLevelNode: ].get(self._sid_map.get(node_id)) def add(self, node: _node.Node) -> None: - """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by - :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. + """Register a top level node (and its children) for syncing up to the server. There's no need to call this for nodes created by :py:meth:`createNote` or :py:meth:`createList` as they are automatically added. Args: node: The node to sync. @@ -915,7 +917,7 @@ def createList( if title is not None: node.title = title - sort = random.randint(1000000000, 9999999999) + sort = random.randint(1000000000, 9999999999) # noqa: S311 for text, checked in items: node.add(text, checked, sort) sort -= _node.List.SORT_DELTA @@ -947,7 +949,7 @@ def findLabel( """Find a label with the given name. Args: - name: A str or regular expression to match against the name. + query: A str or regular expression to match against the name. create: Whether to create the label if it doesn't exist (only if name is a str). Returns: @@ -1012,12 +1014,11 @@ def getMediaLink(self, blob: _node.Blob) -> str: """ return self._media_api.get(blob) - def all(self) -> list[_node.TopLevelNode]: + def all(self) -> list[_node.TopLevelNode]: # noqa: A003 """Get all Notes. Returns: - - Notes: + All notes. """ return self._nodes[_node.Root.ID].children @@ -1097,7 +1098,7 @@ def _sync_notes(self) -> None: def _parseTasks(self, raw: dict) -> None: pass - def _parseNodes(self, raw: dict) -> None: + def _parseNodes(self, raw: dict) -> None: # noqa: C901, PLR0912 created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1169,8 +1170,8 @@ def _parseNodes(self, raw: dict) -> None: # Hydrate label references in notes. for node in self.all(): - for label_id in node.labels._labels: - node.labels._labels[label_id] = self._labels.get(label_id) + for label_id in node.labels._labels: # noqa: SLF001 + node.labels._labels[label_id] = self._labels.get(label_id) # noqa: SLF001 def _parseUserInfo(self, raw: dict) -> None: labels = {} diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index fb21118..b3e706b 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -520,7 +520,7 @@ def all(self) -> list[Annotation]: # noqa: A003 return list(self._entries.values()) @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return super().dirty or any( annotation.dirty for annotation in self._entries.values() ) @@ -565,7 +565,7 @@ def from_json(cls, raw: dict) -> Annotation | None: return annotation - def all(self) -> list[Annotation]: # noqa: A003 + def all(self) -> list[Annotation]: # noqa: A003 """Get all annotations. Returns: @@ -661,7 +661,7 @@ def remove(self, annotation: Annotation) -> None: self._dirty = True @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return super().dirty or any( annotation.dirty for annotation in self._annotations.values() ) @@ -907,9 +907,7 @@ def __init__(self) -> None: def __len__(self) -> int: return len(self._collaborators) - def load( # noqa: D102 - self, collaborators_raw: list, requests_raw: list - ) -> None: + def load(self, collaborators_raw: list, requests_raw: list) -> None: # noqa: D102 # Parent method not called. if requests_raw and isinstance(requests_raw[-1], bool): self._dirty = requests_raw.pop() @@ -964,7 +962,7 @@ def remove(self, email: str) -> None: self._collaborators[email] = ShareRequestValue.Remove self._dirty = True - def all(self) -> list[str]: # noqa: A003 + def all(self) -> list[str]: # noqa: A003 """Get all collaborators. Returns: @@ -1114,7 +1112,7 @@ def merged(self, value: datetime.datetime) -> None: self.touch() @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return super().dirty or self.timestamps.dirty def __str__(self) -> str: @@ -1142,7 +1140,7 @@ def _load(self, raw: list) -> None: for raw_label in raw: self._labels[raw_label["labelId"]] = None - def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: D102 + def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: D102 # Parent method not called. ret = [ { @@ -1186,7 +1184,7 @@ def get(self, label_id: str) -> str: """ return self._labels.get(label_id) - def all(self) -> list[Label]: # noqa: A003 + def all(self) -> list[Label]: # noqa: A003 """Get all labels. Returns: @@ -1256,7 +1254,7 @@ def _load(self, raw: dict) -> None: self.settings.load(raw["nodeSettings"]) self.annotations.load(raw["annotationsGroup"]) - def save(self, clean: bool = True) -> dict: # noqa: D102 + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["id"] = self.id ret["kind"] = "notes#node" @@ -1373,7 +1371,7 @@ def new(self) -> bool: return self.server_id is None @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return ( super().dirty or self.timestamps.dirty @@ -1393,7 +1391,7 @@ def __init__(self) -> None: super().__init__(id_=self.ID) @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return False @@ -1426,7 +1424,7 @@ def _load(self, raw: dict) -> None: ) self._moved = "moved" in raw - def save(self, clean: bool = True) -> dict: # noqa: D102 + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["color"] = self._color.value ret["isArchived"] = self._archived @@ -1508,7 +1506,7 @@ def url(self) -> str: return "https://keep.google.com/u/0/#" + self._TYPE.value + "/" + self.id @property - def dirty(self) -> bool: # noqa: D102 + def dirty(self) -> bool: # noqa: D102 return super().dirty or self.labels.dirty or self.collaborators.dirty @property @@ -1565,7 +1563,7 @@ def _load(self, raw: dict) -> None: self.super_list_item_id = raw.get("superListItemId") or None self._checked = raw.get("checked", False) - def save(self, clean: bool = True) -> dict: # noqa: D102 + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["parentServerId"] = self.parent_server_id ret["superListItemId"] = self.super_list_item_id @@ -1682,7 +1680,7 @@ def _get_text_node(self) -> ListItem | None: return node @property - def text(self) -> str: # noqa: D102 + def text(self) -> str: # noqa: D102 node = self._get_text_node() if node is None: @@ -1746,11 +1744,11 @@ def add( return node @property - def text(self) -> str: # noqa: D102 + def text(self) -> str: # noqa: D102 return "\n".join(str(node) for node in self.items) @classmethod - def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: # noqa: C901 + def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: # noqa: C901 """Generate a list of sorted list items, taking into account parent items. Args: @@ -2052,6 +2050,7 @@ class NodeDrawingInfo(Element): """Represents information about a drawing blob.""" def __init__(self) -> None: + """Construct a drawing info container""" super().__init__() self.drawing_id = "" self.snapshot = NodeImage() @@ -2081,7 +2080,7 @@ def _load(self, raw: dict) -> None: else self._snapshot_proto_fprint ) - def save(self, clean: bool = True) -> dict: # noqa: D102 + def save(self, clean: bool = True) -> dict: # noqa: D102 ret = super().save(clean) ret["drawingId"] = self.drawing_id ret["snapshotData"] = self.snapshot.save(clean) From bc3d2acefaeb0ba84cf55b83e1ce296d8fc6a3d6 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:30:42 -0400 Subject: [PATCH 39/56] Bump ruff --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8e557d0..b875338 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ ] optional-dependencies = [ "black>=23.3.0", - "ruff>=0.0.263", + "ruff>=0.0.269", ] From 8d208de33bec37b8708e817df3e0f7382521ad76 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:30:57 -0400 Subject: [PATCH 40/56] Fix remaining lint findings --- src/gkeepapi/node.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index b3e706b..e2be0cb 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -719,7 +719,7 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: Returns: Datetime. """ - return datetime.datetime.strptime(tzs, cls.TZ_FMT) + return datetime.datetime.strptime(tzs, cls.TZ_FMT).replace(tzinfo=datetime.timezone.utc) @classmethod def int_to_dt(cls, tz: int | float) -> datetime.datetime: @@ -731,7 +731,7 @@ def int_to_dt(cls, tz: int | float) -> datetime.datetime: Returns: Datetime. """ - return datetime.datetime.utcfromtimestamp(tz) + return datetime.datetime.fromtimestamp(tz, tz=datetime.timezone.utc) @classmethod def dt_to_str(cls, dt: datetime.datetime) -> str: @@ -989,7 +989,7 @@ def touch(self, edited: bool = False) -> None: edited: Whether to set the edited time. """ self._dirty = True - dt = datetime.datetime.utcnow() + dt = datetime.datetime.now(tz=datetime.timezone.utc) self.timestamps.updated = dt if edited: self.timestamps.edited = dt @@ -1008,7 +1008,7 @@ def trashed(self) -> bool: def trash(self) -> None: """Mark the item as trashed.""" - self.timestamps.trashed = datetime.datetime.utcnow() + self.timestamps.trashed = datetime.datetime.now(tz=datetime.timezone.utc) def untrash(self) -> None: """Mark the item as untrashed.""" @@ -1028,7 +1028,7 @@ def deleted(self) -> bool: def delete(self) -> None: """Mark the item as deleted.""" - self.timestamps.deleted = datetime.datetime.utcnow() + self.timestamps.deleted = datetime.datetime.now(tz=datetime.timezone.utc) def undelete(self) -> None: """Mark the item as undeleted.""" @@ -1145,7 +1145,7 @@ def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: ret = [ { "labelId": label_id, - "deleted": NodeTimestamps.dt_to_str(datetime.datetime.utcnow()) + "deleted": NodeTimestamps.dt_to_str(datetime.datetime.now(tz=datetime.timezone.utc)) if label is None else NodeTimestamps.int_to_str(0), } @@ -1311,7 +1311,7 @@ def text(self, value: str) -> None: value: Text value. """ self._text = value - self.timestamps.edited = datetime.datetime.utcnow() + self.timestamps.edited = datetime.datetime.now(tz=datetime.timezone.utc) self.touch(True) @property From 9a475ef311fb7e709833bf9191dfe10addbf765b Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:37:24 -0400 Subject: [PATCH 41/56] Add lint GH action --- .github/workflows/semgrep.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 7680497..5e2d4cd 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,16 +1,14 @@ on: - pull_request: {} - push: - branches: + pull_request: + branches-ignore: - main - - master name: Semgrep jobs: semgrep: name: Scan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: returntocorp/semgrep-action@v1 with: auditOn: push From 8efb389eb486ceb67cf525cb8d2e7237ef40090d Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:40:17 -0400 Subject: [PATCH 42/56] Add black & tests --- .github/workflows/lint.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..4da739e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +on: + pull_request: + branches-ignore: + - main +jobs: + build: + name: Lint & test + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Install tools + run: | + python -m pip install ruff black + - name: Lint with ruff + run: | + ruff src + - name: Check format with black + run: | + black --test src + - name: Run tests + run: | + python -m unittest discover From 878b784fc2d06b5591ab911cdf341673248b592a Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:44:25 -0400 Subject: [PATCH 43/56] Fix GH action trigger --- .github/workflows/lint.yml | 2 +- .github/workflows/semgrep.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 4da739e..5dab68d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,5 +1,5 @@ on: - pull_request: + push: branches-ignore: - main jobs: diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 5e2d4cd..f60188d 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -1,5 +1,5 @@ on: - pull_request: + push: branches-ignore: - main name: Semgrep From 4176face3b67ec76e2433e50419a8f996fd23554 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:46:22 -0400 Subject: [PATCH 44/56] Fix black syntax --- .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 5dab68d..9ea0158 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -23,7 +23,7 @@ jobs: ruff src - name: Check format with black run: | - black --test src + black --check src - name: Run tests run: | python -m unittest discover From 2407d242d150a6fe9357187a0561a788ea9fd206 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:48:44 -0400 Subject: [PATCH 45/56] black --- src/gkeepapi/__init__.py | 10 ++++++---- src/gkeepapi/node.py | 8 ++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 9f61599..86fc489 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -385,7 +385,7 @@ def get(self, blob: _node.Blob) -> str: """ url = self._base_url + blob.parent.server_id + "/" + blob.server_id if blob.blob.type == _node.BlobType.Drawing: - url += "/" + blob.blob._drawing_info.drawing_id # noqa: SLF001 + url += "/" + blob.blob._drawing_info.drawing_id # noqa: SLF001 return self._send(url=url, method="GET", allow_redirects=False).headers[ "location" ] @@ -1098,7 +1098,7 @@ def _sync_notes(self) -> None: def _parseTasks(self, raw: dict) -> None: pass - def _parseNodes(self, raw: dict) -> None: # noqa: C901, PLR0912 + def _parseNodes(self, raw: dict) -> None: # noqa: C901, PLR0912 created_nodes = [] deleted_nodes = [] listitem_nodes = [] @@ -1170,8 +1170,10 @@ def _parseNodes(self, raw: dict) -> None: # noqa: C901, PLR0912 # Hydrate label references in notes. for node in self.all(): - for label_id in node.labels._labels: # noqa: SLF001 - node.labels._labels[label_id] = self._labels.get(label_id) # noqa: SLF001 + for label_id in node.labels._labels: # noqa: SLF001 + node.labels._labels[label_id] = self._labels.get( # noqa: SLF001 + label_id + ) def _parseUserInfo(self, raw: dict) -> None: labels = {} diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index e2be0cb..7ff8a1a 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -719,7 +719,9 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: Returns: Datetime. """ - return datetime.datetime.strptime(tzs, cls.TZ_FMT).replace(tzinfo=datetime.timezone.utc) + return datetime.datetime.strptime(tzs, cls.TZ_FMT).replace( + tzinfo=datetime.timezone.utc + ) @classmethod def int_to_dt(cls, tz: int | float) -> datetime.datetime: @@ -1145,7 +1147,9 @@ def save(self, clean: bool = True) -> tuple[dict] | tuple[dict, bool]: # noqa: ret = [ { "labelId": label_id, - "deleted": NodeTimestamps.dt_to_str(datetime.datetime.now(tz=datetime.timezone.utc)) + "deleted": NodeTimestamps.dt_to_str( + datetime.datetime.now(tz=datetime.timezone.utc) + ) if label is None else NodeTimestamps.int_to_str(0), } From aa4cfa3de29ca6797d821cb475ec9b439fe8ae22 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:57:18 -0400 Subject: [PATCH 46/56] Fix tests --- .github/workflows/lint.yml | 4 ++-- pyproject.toml | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 9ea0158..04ea0b6 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,7 +4,7 @@ on: - main jobs: build: - name: Lint & test + name: Lint & Test runs-on: ubuntu-latest strategy: matrix: @@ -17,7 +17,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install tools run: | - python -m pip install ruff black + python -m pip install . - name: Lint with ruff run: | ruff src diff --git a/pyproject.toml b/pyproject.toml index b875338..2a0e0bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,6 @@ +[build-system] +requires = ["setuptools ~= 58.0", "cython ~= 0.29.0"] + [project] name = "gkeepapi" version = "1.0.0" @@ -20,12 +23,13 @@ dependencies = [ "gpsoauth >= 1.0.2", "future >= 0.16.0", ] -optional-dependencies = [ + +[project.optional-dependencies] +dev = [ "black>=23.3.0", "ruff>=0.0.269", ] - [project.urls] "Homepage" = "https://github.com/kiwiz/gkeepapi" "Bug Tracker" = "https://github.com/kiwiz/gkeepapi/issues" From e2f97168237eccb00d25bec32169471a709a9bdd Mon Sep 17 00:00:00 2001 From: K Date: Tue, 23 May 2023 10:58:22 -0400 Subject: [PATCH 47/56] Add missing build deps --- .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 04ea0b6..da3dfb6 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 . + python -m pip install . '.[dev]' - name: Lint with ruff run: | ruff src From 8fd4f9202a77a91581b46932b6dac9636b1ea390 Mon Sep 17 00:00:00 2001 From: K Date: Sun, 25 Jun 2023 13:14:06 -0400 Subject: [PATCH 48/56] Fix lint --- src/gkeepapi/node.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 7ff8a1a..7ca1b6a 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -2100,7 +2100,7 @@ def save(self, clean: bool = True) -> dict: # noqa: D102 class Blob(Node): """Represents a Google Keep blob.""" - _blob_type_map = { + _blob_type_map = { # noqa: RUF012 BlobType.Audio: NodeAudio, BlobType.Image: NodeImage, BlobType.Drawing: NodeDrawing, From 80f8354266e372bdb3ade3fc31f9ad1d5d292b65 Mon Sep 17 00:00:00 2001 From: K Date: Sun, 25 Jun 2023 13:15:12 -0400 Subject: [PATCH 49/56] Add codecoverage dep --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 2a0e0bf..0b8c168 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ dev = [ "black>=23.3.0", "ruff>=0.0.269", + "coverage>=7.2.5", ] [project.urls] From 3d91b57e44e38f964309113974cf01a190b26c39 Mon Sep 17 00:00:00 2001 From: K Date: Sun, 25 Jun 2023 13:31:00 -0400 Subject: [PATCH 50/56] Doc fixups --- docs/conf.py | 4 ++-- docs/index.rst | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 229e43e..4712015 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,7 +71,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -101,7 +101,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +html_static_path = [] # Custom sidebar templates, must be a dictionary that maps document names # to template names. diff --git a/docs/index.rst b/docs/index.rst index cb929f4..cc81e9c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -427,15 +427,15 @@ FAQ This usually occurs when Google thinks the login request looks suspicious. Here are some steps you can take to resolve this: 1. Make sure you have the newest version of gkeepapi installed. -2. Instead of logging in every time, cache the authentication token and reuse it on subsequent runs. See `here `_ for an example implementation. +2. Instead of logging in every time, cache the authentication token and reuse it on subsequent runs. See `here `__ for an example implementation. 3. If you have 2-Step Verification turned on, generating an App Password for gkeepapi is highly recommended. -4. Allowing access through this `link `_ has worked for some people. -5. Upgrading to a newer version of Python (3.7+) has worked for some people. See this `issue `_ for more information. +4. Allowing access through this `link `__ has worked for some people. +5. Upgrading to a newer version of Python (3.7+) has worked for some people. See this `issue `__ for more information. 6. If all else fails, try testing gkeepapi on a separate IP address and/or user to see if you can isolate the problem. 2. I get a "DeviceManagementRequiredOrSyncDisabled" :py:class:`exception.LoginException` when I try to log in. -This is due to the enforcement of Android device policies on your G-Suite account. To resolve this, you can try disabling that setting `here `_. +This is due to the enforcement of Android device policies on your G-Suite account. To resolve this, you can try disabling that setting `here `__. Known Issues ============ From 915fc87835f5e89e1924eeaa2f15ef8b511523e8 Mon Sep 17 00:00:00 2001 From: K Date: Sun, 25 Jun 2023 13:40:57 -0400 Subject: [PATCH 51/56] Update build system --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b8c168..cb15a52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,8 @@ [build-system] -requires = ["setuptools ~= 58.0", "cython ~= 0.29.0"] +requires = [ + "flit_core >=3.2,<4", +] +build-backend = "flit_core.buildapi" [project] name = "gkeepapi" @@ -27,7 +30,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "black>=23.3.0", - "ruff>=0.0.269", + "ruff>=0.0.275", "coverage>=7.2.5", ] From 27de4df64753beffc41e59e42496b14d7f39add9 Mon Sep 17 00:00:00 2001 From: K Date: Tue, 15 Aug 2023 02:27:33 -0400 Subject: [PATCH 52/56] Lint --- pyproject.toml | 20 ++++++++++++---- src/gkeepapi/__init__.py | 10 ++------ src/gkeepapi/node.py | 52 ++++++++++++++-------------------------- 3 files changed, 35 insertions(+), 47 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cb15a52..94d27e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ [project.optional-dependencies] dev = [ "black>=23.3.0", - "ruff>=0.0.275", + "ruff>=0.0.284", "coverage>=7.2.5", ] @@ -56,18 +56,21 @@ select = [ "UP", # pyupgrade "YTT", # flake8-2020 "ANN", # flake8-annotations + "ASYNC", # flake8-async "S", # flake8-bandit "BLE", # flake8-blind-except # "FBT", # flake8-boolean-trap "B", # flake8-bugbear "A", # flake8-builtins "COM", # flake8-commas + "CPY", # flake8-copyright "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "T10", # flake8-debugger "DJ", # flake8-django "EM", # flake8-errmsg "EXE", # flake8-executable + "FA", # flake8-future-annotations "ISC", # flake8-implicit-str-concat "ICN", # flake8-import-conventions "G", # flake8-logging-format @@ -80,34 +83,41 @@ select = [ "RSE", # flake8-raise "RET", # flake8-return "SLF", # flake8-self + "SLOT", # flake8-slots "SIM", # flake8-simplify "TID", # flake8-tidy-imports "TCH", # flake8-type-checking "INT", # flake8-gettext "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib + # "TD", # flake-todos + # "FIX", # flake-fixme # "ERA", # eradicate + "PD", # pandas-vet "PGH", # pygrep-hooks "PLC", # pylint convention "PLE", # pylint error "PLR", # pylint refactor "PLW", # pylint warning # "TRY", # tryceratops + "FLY", # flynt "NPY", # numpy-specific + "AIR", # airflow-specific + "PERF", # performance "RUF", # ruff-specific ] ignore = [ "E501", # line-too-long -- disabled as black takes care of this - "N802", # invalid-function-name -- too late! "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 "EM101", # raw-string-in-exception -- no thanks "EM102", # f-string-in-exception -- no thanks - "N818", # error-suffix-on-exception-name -- too late! - "ANN101", # missing-type-self -- unnecessary - "ANN102", # missing-type-cls -- unnecessary "PLR0913", # too-many-arguments -- no thanks "D105", # undocumented-magic-method -- no thanks + "ANN101", # missing-type-self -- unnecessary + "ANN102", # missing-type-cls -- unnecessary ] [tool.ruff.pydocstyle] diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 86fc489..553b234 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -768,8 +768,7 @@ def dump(self) -> dict: nodes = [] for node in self.all(): nodes.append(node) - for child in node.children: - nodes.append(child) + nodes.extend(node.children) return { "keep_version": self._keep_version, "labels": [label.save(False) for label in self.labels()], @@ -1205,13 +1204,8 @@ def _findDirtyNodes(self) -> list[_node.Node]: if child.id not in self._nodes: self._nodes[child.id] = child - nodes = [] # Collect all dirty nodes (any nodes from above will be caught too). - for node in self._nodes.values(): - if node.dirty: - nodes.append(node) - - return nodes + return [node for node in self._nodes.values() if node.dirty] def _clean(self) -> None: """Recursively check that all nodes are reachable.""" diff --git a/src/gkeepapi/node.py b/src/gkeepapi/node.py index 7ca1b6a..eb52b05 100644 --- a/src/gkeepapi/node.py +++ b/src/gkeepapi/node.py @@ -297,21 +297,11 @@ def save(self, clean: bool = True) -> dict: @classmethod def _generateAnnotationId(cls) -> str: return "{:08x}-{:04x}-{:04x}-{:04x}-{:012x}".format( - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x00000000, 0xFFFFFFFF - ), - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x0000, 0xFFFF - ), - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x0000, 0xFFFF - ), - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x0000, 0xFFFF - ), - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x000000000000, 0xFFFFFFFFFFFF - ), + random.randint(0x00000000, 0xFFFFFFFF), # noqa: S311 + random.randint(0x0000, 0xFFFF), # noqa: S311 + random.randint(0x0000, 0xFFFF), # noqa: S311 + random.randint(0x0000, 0xFFFF), # noqa: S311 + random.randint(0x000000000000, 0xFFFFFFFFFFFF), # noqa: S311 ) @@ -724,7 +714,7 @@ def str_to_dt(cls, tzs: str) -> datetime.datetime: ) @classmethod - def int_to_dt(cls, tz: int | float) -> datetime.datetime: + def int_to_dt(cls, tz: float) -> datetime.datetime: """Convert a unix timestamp into an object. Params: @@ -1056,9 +1046,7 @@ def _generateId(cls, tz: float) -> str: return "tag.{}.{:x}".format( "".join( [ - random.choice( # noqa: suspicious-non-cryptographic-random-usage - "abcdefghijklmnopqrstuvwxyz0123456789" - ) + random.choice("abcdefghijklmnopqrstuvwxyz0123456789") # noqa: S311 for _ in range(12) ] ), @@ -1216,9 +1204,7 @@ def __init__( self.server_id = None self.parent_id = parent_id self.type = type_ - self._sort = random.randint( # noqa: suspicious-non-cryptographic-random-usage - 1000000000, 9999999999 - ) + self._sort = random.randint(1000000000, 9999999999) # noqa: S311 self._version = None self._text = "" self._children = {} @@ -1233,9 +1219,7 @@ def __init__( def _generateId(cls, tz: float) -> str: return "{:x}.{:016x}".format( int(tz * 1000), - random.randint( # noqa: suspicious-non-cryptographic-random-usage - 0x0000000000000000, 0xFFFFFFFFFFFFFFFF - ), + random.randint(0x0000000000000000, 0xFFFFFFFFFFFFFFFF), # noqa: S311 ) def _load(self, raw: dict) -> None: @@ -1701,7 +1685,7 @@ def text(self, value: str) -> None: self.touch(True) def __str__(self) -> str: - return "\n".join([self.title, self.text]) + return f"{self.title}\n{self.text}" class List(TopLevelNode): @@ -1766,6 +1750,8 @@ def sorted_items(cls, items: list[ListItem]) -> list[ListItem]: # noqa: C901 class T(tuple): """Tuple with element-based sorting""" + __slots__ = () + def __cmp__(self, other: "T") -> int: for a, b in itertools.zip_longest(self, other): if a != b: @@ -1820,9 +1806,7 @@ def sort_items( reverse: Whether to reverse the output. """ sorted_children = sorted(self._items(), key=key, reverse=reverse) - sort_value = random.randint( # noqa: suspicious-non-cryptographic-random-usage - 1000000000, 9999999999 - ) + sort_value = random.randint(1000000000, 9999999999) # noqa: S311 for node in sorted_children: node.sort = sort_value @@ -2186,10 +2170,10 @@ def from_json(raw: dict) -> Node | None: if DEBUG: # pragma: no cover - Node.__load = Node._load # noqa: private-member-access + Node.__load = Node._load # noqa: SLF001 - def _load(self, raw): # noqa: missing-type-function-argument - self.__load(raw) # : private-member-access - self._find_discrepancies(raw) # : private-member-access + def _load(self, raw): # noqa: ANN001, ANN202 + self.__load(raw) + self._find_discrepancies(raw) - Node._load = _load # noqa: private-member-access + Node._load = _load # noqa: SLF001 From c1ae66df3d6b447e03969ec5104ac104f7906c3c Mon Sep 17 00:00:00 2001 From: K Date: Thu, 21 Sep 2023 00:32:14 -0400 Subject: [PATCH 53/56] Fix broken links --- docs/index.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index cc81e9c..4cd863c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -427,11 +427,10 @@ FAQ This usually occurs when Google thinks the login request looks suspicious. Here are some steps you can take to resolve this: 1. Make sure you have the newest version of gkeepapi installed. -2. Instead of logging in every time, cache the authentication token and reuse it on subsequent runs. See `here `__ for an example implementation. +2. Instead of logging in every time, cache the authentication token and reuse it on subsequent runs. See `here `__ for an example implementation. 3. If you have 2-Step Verification turned on, generating an App Password for gkeepapi is highly recommended. -4. Allowing access through this `link `__ has worked for some people. -5. Upgrading to a newer version of Python (3.7+) has worked for some people. See this `issue `__ for more information. -6. If all else fails, try testing gkeepapi on a separate IP address and/or user to see if you can isolate the problem. +4. Upgrading to a newer version of Python (3.7+) has worked for some people. See this `issue `__ for more information. +5. If all else fails, try testing gkeepapi on a separate IP address and/or user to see if you can isolate the problem. 2. I get a "DeviceManagementRequiredOrSyncDisabled" :py:class:`exception.LoginException` when I try to log in. From a5e98f6c1bcecb776773b1f46bbf5ff449b9ec57 Mon Sep 17 00:00:00 2001 From: K Date: Thu, 21 Sep 2023 00:35:48 -0400 Subject: [PATCH 54/56] Demote version --- src/gkeepapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gkeepapi/__init__.py b/src/gkeepapi/__init__.py index 553b234..8629c16 100644 --- a/src/gkeepapi/__init__.py +++ b/src/gkeepapi/__init__.py @@ -1,6 +1,6 @@ """.. moduleauthor:: Kai """ -__version__ = "1.0.0" +__version__ = "0.15.0" import datetime import http From 7a7643d7931d8a9fecb3347e506250ba8810c9bb Mon Sep 17 00:00:00 2001 From: K Date: Thu, 21 Sep 2023 00:38:33 -0400 Subject: [PATCH 55/56] Fixes --- Makefile | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 32528bb..bbcb711 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ build: src/gkeepapi/*.py python3 -m build clean: - rm -f build dist + rm -rf build dist upload: twine upload dist/*.whl diff --git a/pyproject.toml b/pyproject.toml index 94d27e2..1f9fb84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "flit_core.buildapi" [project] name = "gkeepapi" -version = "1.0.0" +version = "0.15.0" authors = [ { name="Kai", email="z@kwi.li" }, ] From 1f9f721dd30bfc1994dccfaa2364f50d9ebaac1c Mon Sep 17 00:00:00 2001 From: K Date: Wed, 27 Sep 2023 00:06:25 -0400 Subject: [PATCH 56/56] Bump gpsoauth --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1f9fb84..d6bebae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "gpsoauth >= 1.0.2", + "gpsoauth >= 1.0.3", "future >= 0.16.0", ]