From 43183bf4457eb76fd783a865dffa931f285fe512 Mon Sep 17 00:00:00 2001 From: jvalenzuela Date: Tue, 24 Dec 2024 16:28:17 +0100 Subject: [PATCH 01/15] obsidian: first try at having markdown items --- pyproject.toml | 2 + syncall/cli.py | 8 + syncall/filesystem/markdown_task_item.py | 65 ++++++++ syncall/filesystem/markdown_tasks_side.py | 115 ++++++++++++++ syncall/scripts/md_gtasks_sync.py | 181 ++++++++++++++++++++++ 5 files changed, 371 insertions(+) create mode 100644 syncall/filesystem/markdown_task_item.py create mode 100644 syncall/filesystem/markdown_tasks_side.py create mode 100644 syncall/scripts/md_gtasks_sync.py diff --git a/pyproject.toml b/pyproject.toml index 1c35133..b9736f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,8 @@ tw_notion_sync = "syncall.scripts.tw_notion_sync:main" fs_gkeep_sync = "syncall.scripts.fs_gkeep_sync:main" tw_caldav_sync = "syncall.scripts.tw_caldav_sync:main" tw_gtasks_sync = "syncall.scripts.tw_gtasks_sync:main" +md_gtasks_sync = "syncall.scripts.md_gtasks_sync:main" + # end-user dependencies -------------------------------------------------------- [tool.poetry.dependencies] diff --git a/syncall/cli.py b/syncall/cli.py index 50c89df..1867551 100644 --- a/syncall/cli.py +++ b/syncall/cli.py @@ -459,6 +459,14 @@ def opt_filename_extension(): default=".md", ) +def opt_filename_path(): + return click.option( + "--path", + "--filename-path", + "filename_path", + type=str, + help="Use this file path for locally saved data", + ) # general options ----------------------------------------------------------------------------- def opts_miscellaneous(side_A_name: str, side_B_name: str): diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py new file mode 100644 index 0000000..544dfc6 --- /dev/null +++ b/syncall/filesystem/markdown_task_item.py @@ -0,0 +1,65 @@ +import datetime + +from item_synchronizer.types import ID + +from syncall.concrete_item import ConcreteItem, ItemKey, KeyType + + +class MarkdownTaskItem(ConcreteItem): + """A task line inside a Markdown file.""" + + def __init__(self, line): + super().__init__( + keys=( + ItemKey("is_checked", KeyType.String), + ItemKey("last_modified_date", KeyType.Date), + ItemKey("plaintext", KeyType.String), + ) + ) + + # TODO + self.is_checked = True + self.last_modified_date = None + self.plaintext = line + self.deleted = False + + @classmethod + def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": + """Create a MarkdownTaskItem given the raw item at hand.""" + + is_archived = False + is_checked = False + self.last_modified_date = None + plaintext = markdown_raw_item + + return cls( + is_archived=is_archived, + is_checked=is_checked, + last_modified_date=last_modified_date, + plaintext=plaintext, + id=id_, + ) + + + @property + def is_checked(self): + return self.is_checked + + @is_checked.setter + def is_checked(self, val): + self.is_checked = val + + @property + def last_modified_date(self) -> datetime.datetime: + return datetime.datetime.today() + + @property + def plaintext(self) -> str: + return self.plaintext + + @plaintext.setter + def plaintext(self, val: str) -> None: + self.plaintext = val + + def delete(self) -> None: + self.deleted = True diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py new file mode 100644 index 0000000..31beaeb --- /dev/null +++ b/syncall/filesystem/markdown_tasks_side.py @@ -0,0 +1,115 @@ +from pathlib import Path +from typing import MutableMapping, Optional, Sequence + +from item_synchronizer.types import ID +from loguru import logger + +from syncall.concrete_item import ConcreteItem +from syncall.filesystem.filesystem_file import FilesystemFile +from syncall.filesystem.markdown_task_item import MarkdownTaskItem +from syncall.sync_side import SyncSide + + +class MarkdownTasksSide(SyncSide): + """Integration for managing files in a local filesystem. + + - Embed the UUID as an extended attribute of each file. + """ + + @classmethod + def id_key(cls) -> str: + return "id" + + @classmethod + def summary_key(cls) -> str: + return "title" + + @classmethod + def last_modification_key(cls) -> str: + return "last_modified_date" + + def __init__(self, filename_path: Path) -> None: + super().__init__(name="Fs", fullname="Filesystem") + self._filename_path = filename_path + self._filesystem_file = FilesystemFile(path=filename_path) + + all_items = self.get_all_items() + + def start(self): + pass + + def finish(self): + self._filesystem_file.flush() + + def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: + """Read all items again from storage.""" + all_items = tuple( + MarkdownTask(line) + for line in self._filesystem_file.contents + if line.startswith("- [") + ) + + logger.opt(lazy=True).debug( + f"Found {len(all_items)} matching tasks inside {self._filename_path}" + ) + + return all_items + + def get_item(self, item_id: ID) -> Optional[MarkdownTasksSide]: + item = self._get_item_refresh(item_id=item_id) + + return item + + def _get_item_refresh(self, item_id: ID) -> Optional[FilesystemFile]: + """Search for the FilesystemFile in the root directory given its ID.""" + fs_files = self.get_all_items() + + matching_fs_files = [fs_file for fs_file in fs_files if fs_file.id == item_id] + if len(matching_fs_files) > 1: + logger.warning( + f"Found {len(matching_fs_files)} paths with the item ID [{item_id}]." + "Arbitrarily returning the first item." + ) + elif len(matching_fs_files) == 0: + return None + + # update the cache & return + item = matching_fs_files[0] + self._items_cache[item_id] = item + return item + + def delete_single_item(self, item_id: ID): + item = self.get_item(item_id) + if item is None: + logger.warning(f"Requested to delete item {item_id} but item cannot be found.") + return + + item.delete() + item.flush() + + def update_item(self, item_id: ID, **changes): + item = self.get_item(item_id) + if item is None: + logger.warning(f"Requested to update item {item_id} but item cannot be found.") + return + + if not {"title", "contents"}.issubset(changes): + logger.warning(f"Invalid changes provided to Fielsystem Side -> {changes}") + return + + item.title = changes["title"] + item.contents = changes["contents"] + item.flush() + + def add_item(self, item: FilesystemFile) -> FilesystemFile: + item.root = self.filesystem_root + item.flush() + return item + + @classmethod + def items_are_identical( + cls, item1: ConcreteItem, item2: ConcreteItem, ignore_keys: Sequence[str] = [] + ) -> bool: + ignore_keys_ = [cls.last_modification_key()] + ignore_keys_.extend(ignore_keys) + return item1.compare(item2, ignore_keys=ignore_keys_) diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py new file mode 100644 index 0000000..f5afcbd --- /dev/null +++ b/syncall/scripts/md_gtasks_sync.py @@ -0,0 +1,181 @@ +from typing import List + +import click +from bubop import ( + check_optional_mutually_exclusive, + check_required_mutually_exclusive, + format_dict, + logger, + loguru_tqdm_sink, +) + +from syncall.app_utils import confirm_before_proceeding, inform_about_app_extras + +try: + from syncall.google.gtasks_side import GTasksSide + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide +except ImportError: + inform_about_app_extras(["google", "fs"]) + +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, + cache_or_reuse_cached_combination, + error_and_exit, + fetch_app_configuration, + get_resolution_strategy, + register_teardown_handler, +) +from syncall.cli import ( + opt_google_oauth_port, + opt_google_secret_override, + opt_gtasks_list, + opts_miscellaneous, + opts_filename_path, +) + + +@click.command() +@opt_gtasks_list() +@opt_google_secret_override() +@opt_google_oauth_port() +@opts_miscellaneous(side_A_name="Obsidian", side_B_name="Google Tasks") +def main( + gtasks_list: str, + google_secret: str, + oauth_port: int, + filename_path: str, + prefer_scheduled_date: bool, + resolution_strategy: str, + verbose: int, + combination_name: str, + custom_combination_savename: str, + pdb_on_error: bool, + confirm: bool, +): + """Synchronize lists from your Google Tasks with filters from Taskwarrior. + + The list of MD tasks can be based on a Markdown file path + while the list in GTasks should be provided by their name. if it doesn't + exist it will be created. + """ + # setup logger ---------------------------------------------------------------------------- + loguru_tqdm_sink(verbosity=verbose) + app_log_to_syslog() + logger.debug("Initialising...") + inform_about_config = False + + # cli validation -------------------------------------------------------------------------- + check_optional_mutually_exclusive(combination_name, custom_combination_savename) + + combination_of_file_and_gtasks_list = any( + [ + filename_path, + gtasks_list, + ] + ) + check_optional_mutually_exclusive( + combination_name, combination_of_file_and_gtasks_list + ) + + # existing combination name is provided --------------------------------------------------- + if combination_name is not None: + app_config = fetch_app_configuration( + side_A_name="Obsidian", side_B_name="Google Tasks", combination=combination_name + ) + md_filename_path = app_config["md_filename_path"] + gtasks_list = app_config["gtasks_list"] + + # combination manually specified ---------------------------------------------------------- + else: + inform_about_config = True + combination_name = cache_or_reuse_cached_combination( + config_args={ + "gtasks_list": gtasks_list, + "filename_path": filename_path, + }, + config_fname="md_gtasks_configs", + custom_combination_savename=custom_combination_savename, + ) + + # more checks ----------------------------------------------------------------------------- + if gtasks_list is None: + error_and_exit( + "You have to provide the name of a Google Tasks list to synchronize events" + " to/from. You can do so either via CLI arguments or by specifying an existing" + " saved combination" + ) + + # announce configuration ------------------------------------------------------------------ + logger.info( + format_dict( + header="Configuration", + items={ + "Markdown Filename Path": md_filename_path, + "Google Tasks": gtasks_list, + "Prefer scheduled dates": prefer_scheduled_date, + }, + prefix="\n\n", + suffix="\n", + ) + ) + if confirm: + confirm_before_proceeding() + + # initialize sides ------------------------------------------------------------------------ + tw_side = MarkdownTasksSide( + filename_path=filename_path + ) + + gtasks_side = GTasksSide( + task_list_title=gtasks_list, oauth_port=oauth_port, client_secret=google_secret + ) + + # teardown function and exception handling ------------------------------------------------ + register_teardown_handler( + pdb_on_error=pdb_on_error, + inform_about_config=inform_about_config, + combination_name=combination_name, + verbose=verbose, + ) + + # take extra arguments into account ------------------------------------------------------- + def convert_B_to_A(*args, **kargs): + return convert_tw_to_gtask( + *args, + **kargs, + ) + + convert_B_to_A.__doc__ = convert_tw_to_gtask.__doc__ + + def convert_A_to_B(*args, **kargs): + return convert_gtask_to_tw( + *args, + **kargs, + set_scheduled_date=prefer_scheduled_date, + ) + + convert_A_to_B.__doc__ = convert_gtask_to_md.__doc__ + + # sync ------------------------------------------------------------------------------------ + with Aggregator( + side_A=gtasks_side, + side_B=md_side, + converter_B_to_A=convert_B_to_A, + converter_A_to_B=convert_A_to_B, + resolution_strategy=get_resolution_strategy( + resolution_strategy, side_A_type=type(gtasks_side), side_B_type=type(md_side) + ), + config_fname=combination_name, + ignore_keys=( + (), + (), + ), + ) as aggregator: + aggregator.sync() + + return 0 + + +if __name__ == "__main__": + main() From 5c0de71338907f448a5cfc6f451cc68e6e2e90db Mon Sep 17 00:00:00 2001 From: jvalenzuela Date: Thu, 27 Feb 2025 18:25:00 +0100 Subject: [PATCH 02/15] adding task side classes --- syncall/filesystem/markdown_task_item.py | 34 +++------------- syncall/filesystem/markdown_tasks_side.py | 48 ++++++----------------- 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 544dfc6..a5e97f8 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -1,4 +1,5 @@ import datetime +import uuid from item_synchronizer.types import ID @@ -13,14 +14,10 @@ def __init__(self, line): keys=( ItemKey("is_checked", KeyType.String), ItemKey("last_modified_date", KeyType.Date), - ItemKey("plaintext", KeyType.String), + ItemKey("title", KeyType.String), ) ) - # TODO - self.is_checked = True - self.last_modified_date = None - self.plaintext = line self.deleted = False @classmethod @@ -30,36 +27,17 @@ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": is_archived = False is_checked = False self.last_modified_date = None - plaintext = markdown_raw_item + title = markdown_raw_item return cls( - is_archived=is_archived, is_checked=is_checked, last_modified_date=last_modified_date, - plaintext=plaintext, - id=id_, + title=title, ) - - @property - def is_checked(self): - return self.is_checked - - @is_checked.setter - def is_checked(self, val): - self.is_checked = val - - @property - def last_modified_date(self) -> datetime.datetime: - return datetime.datetime.today() - @property - def plaintext(self) -> str: - return self.plaintext - - @plaintext.setter - def plaintext(self, val: str) -> None: - self.plaintext = val + def id(self) -> ID: + return uuid.uuid3('ID', self.text) def delete(self) -> None: self.deleted = True diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index 31beaeb..f45c0a7 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -33,7 +33,10 @@ def __init__(self, filename_path: Path) -> None: self._filename_path = filename_path self._filesystem_file = FilesystemFile(path=filename_path) - all_items = self.get_all_items() + self.all_items = self.get_all_items() + self._items_cache: dict[str, dict] = { + item.id: item for item in self.get_all_items() + } def start(self): pass @@ -53,57 +56,32 @@ def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: f"Found {len(all_items)} matching tasks inside {self._filename_path}" ) - return all_items - def get_item(self, item_id: ID) -> Optional[MarkdownTasksSide]: - item = self._get_item_refresh(item_id=item_id) - - return item - - def _get_item_refresh(self, item_id: ID) -> Optional[FilesystemFile]: - """Search for the FilesystemFile in the root directory given its ID.""" - fs_files = self.get_all_items() - - matching_fs_files = [fs_file for fs_file in fs_files if fs_file.id == item_id] - if len(matching_fs_files) > 1: - logger.warning( - f"Found {len(matching_fs_files)} paths with the item ID [{item_id}]." - "Arbitrarily returning the first item." - ) - elif len(matching_fs_files) == 0: - return None - - # update the cache & return - item = matching_fs_files[0] - self._items_cache[item_id] = item + item = self._items_cache.get(item_id) return item def delete_single_item(self, item_id: ID): - item = self.get_item(item_id) - if item is None: + try: + del self._items_cache[item_id] + except Keyerror: logger.warning(f"Requested to delete item {item_id} but item cannot be found.") return - item.delete() - item.flush() - def update_item(self, item_id: ID, **changes): item = self.get_item(item_id) if item is None: logger.warning(f"Requested to update item {item_id} but item cannot be found.") return - if not {"title", "contents"}.issubset(changes): - logger.warning(f"Invalid changes provided to Fielsystem Side -> {changes}") + if not {"title", "is_checked"}.issubset(changes): + logger.warning(f"Invalid changes provided to Filesystem Side -> {changes}") return item.title = changes["title"] - item.contents = changes["contents"] - item.flush() + item.is_checked = changes["is_checked"] - def add_item(self, item: FilesystemFile) -> FilesystemFile: - item.root = self.filesystem_root - item.flush() + def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: + self._items_cache[item.id] = item return item @classmethod From 8e514c913c0f8050b3bc718673698c9ffe6d13fc Mon Sep 17 00:00:00 2001 From: jvalenzuela Date: Tue, 4 Mar 2025 16:25:29 +0100 Subject: [PATCH 03/15] modified all files try 1 --- syncall/scripts/md_gtasks_sync.py | 4 +-- syncall/tw_gtasks_utils.py | 55 +++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py index f5afcbd..d2ca5b5 100644 --- a/syncall/scripts/md_gtasks_sync.py +++ b/syncall/scripts/md_gtasks_sync.py @@ -141,12 +141,12 @@ def main( # take extra arguments into account ------------------------------------------------------- def convert_B_to_A(*args, **kargs): - return convert_tw_to_gtask( + return convert_md_to_gtask( *args, **kargs, ) - convert_B_to_A.__doc__ = convert_tw_to_gtask.__doc__ + convert_B_to_A.__doc__ = convert_md_to_gtask.__doc__ def convert_A_to_B(*args, **kargs): return convert_gtask_to_tw( diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index f680283..994f064 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -33,6 +33,28 @@ def convert_tw_to_gtask( return gtasks_item +def convert_md_to_gtask( + tw_item: Item, +) -> Item: + """TW -> GTasks conversion.""" + assert all( + i in md_item.keys() for i in ("title", "is_checked", "uuid") + ), "Missing keys in tw_item" + + gtasks_item = {} + + # title + gtasks_item["title"] = md_item["title"] + + # status + gtasks_item["status"] = "needsAction" if md_item["is_checked"] else "completed" + + # update time + if "last_modified_date" in md_item.keys(): + gtasks_item["updated"] = format_datetime_tz(parse_google_datetime(md_item["last_modified_date"])) + + return gtasks_item + def convert_gtask_to_tw( gtasks_item: GTasksItem, @@ -94,3 +116,36 @@ def convert_gtask_to_tw( tw_item["modified"] = parse_google_datetime(gtasks_item["updated"]) return tw_item + + +def convert_gtask_to_md( + gtasks_item: GTasksItem, + set_scheduled_date: bool = False, +) -> Item: + """GTasks -> TW Converter. + + If set_scheduled_date, then it will set the "scheduled" date of the produced TW task + instead of the "due" date + """ + # Parse the description + uuid = None + + md_item: Item = {} + + status_gtask = gtasks_item["status"] + + # status + md_item["is_checked"] = status_gtask == "completed" + + # uuid - may just be created -, thus not there + if uuid is not None: + md_item["uuid"] = uuid + + # Description + md_item["title"] = gtasks_item["title"] + + # update time + if "updated" in gtasks_item.keys(): + md_item["last_modified_date"] = parse_google_datetime(gtasks_item["updated"]) + + return md_item From bf29785be8c6c3ff2725eb5b51d414b88ba3c54c Mon Sep 17 00:00:00 2001 From: jvalenzuela Date: Tue, 11 Mar 2025 11:40:02 +0100 Subject: [PATCH 04/15] fixing options and attributes --- pyproject.toml | 2 +- syncall/cli.py | 10 +++++++++ syncall/filesystem/markdown_task_item.py | 14 ++++++------- syncall/filesystem/markdown_tasks_side.py | 18 ++++++++++------ syncall/scripts/md_gtasks_sync.py | 25 +++++++++++------------ syncall/tw_gtasks_utils.py | 4 ++-- 6 files changed, 44 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b9736f4..a02a822 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -91,7 +91,7 @@ tw = ["taskw-ng", "xdg"] fs = ["xattr"] # dev dependencies ------------------------------------------------------------- -[tool.poetry.dev-dependencies] +[tool.poetry.group.dev.dependencies] black = { version = "22.3.0", allow-prereleases = true } identify = "^2.6.0" isort = "^5.13.2" diff --git a/syncall/cli.py b/syncall/cli.py index 1867551..88d6cf7 100644 --- a/syncall/cli.py +++ b/syncall/cli.py @@ -357,6 +357,16 @@ def opt_gtasks_list(): ) +# google tasks -------------------------------------------------------------------------------- +def opt_markdown_file(): + return click.option( + "-m", + "--markdown-file", + type=str, + help="Name of the Markdown file including tasks list to synchronize", + ) + + # google-related options ---------------------------------------------------------------------- def opt_google_secret_override(): return click.option( diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index a5e97f8..3d5bb43 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -9,7 +9,7 @@ class MarkdownTaskItem(ConcreteItem): """A task line inside a Markdown file.""" - def __init__(self, line): + def __init__(self, is_checked: bool = False, title: str = ""): super().__init__( keys=( ItemKey("is_checked", KeyType.String), @@ -19,6 +19,8 @@ def __init__(self, line): ) self.deleted = False + self.is_checked = is_checked + self.title = title @classmethod def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": @@ -26,18 +28,16 @@ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": is_archived = False is_checked = False - self.last_modified_date = None - title = markdown_raw_item + last_modified_date = None + title = markdown_raw_item["title"] return cls( is_checked=is_checked, - last_modified_date=last_modified_date, title=title, ) - @property - def id(self) -> ID: - return uuid.uuid3('ID', self.text) + def _id(self) -> ID: + return uuid.uuid5(uuid.NAMESPACE_OID, self.title) def delete(self) -> None: self.deleted = True diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index f45c0a7..ec39f46 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import MutableMapping, Optional, Sequence +from typing import MutableMapping, Optional, Sequence, cast from item_synchronizer.types import ID from loguru import logger @@ -28,10 +28,10 @@ def summary_key(cls) -> str: def last_modification_key(cls) -> str: return "last_modified_date" - def __init__(self, filename_path: Path) -> None: + def __init__(self, markdown_file: Path) -> None: super().__init__(name="Fs", fullname="Filesystem") - self._filename_path = filename_path - self._filesystem_file = FilesystemFile(path=filename_path) + self._filename_path = markdown_file + self._filesystem_file = FilesystemFile(path=markdown_file) self.all_items = self.get_all_items() self._items_cache: dict[str, dict] = { @@ -47,7 +47,11 @@ def finish(self): def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: """Read all items again from storage.""" all_items = tuple( - MarkdownTask(line) + MarkdownTaskItem( + is_checked=True, + last_modified_date=True, + title=line, + ) for line in self._filesystem_file.contents if line.startswith("- [") ) @@ -55,8 +59,9 @@ def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: logger.opt(lazy=True).debug( f"Found {len(all_items)} matching tasks inside {self._filename_path}" ) + return all_items - def get_item(self, item_id: ID) -> Optional[MarkdownTasksSide]: + def get_item(self, item_id: ID) -> Optional[MarkdownTaskItem]: item = self._items_cache.get(item_id) return item @@ -81,6 +86,7 @@ def update_item(self, item_id: ID, **changes): item.is_checked = changes["is_checked"] def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: + item = MarkdownTaskItem.from_raw_item(item) self._items_cache[item.id] = item return item diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py index d2ca5b5..e8aaae4 100644 --- a/syncall/scripts/md_gtasks_sync.py +++ b/syncall/scripts/md_gtasks_sync.py @@ -13,7 +13,7 @@ try: from syncall.google.gtasks_side import GTasksSide - from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide + from syncall.filesystem.markdown_tasks_side import MarkdownTasksSide except ImportError: inform_about_app_extras(["google", "fs"]) @@ -30,22 +30,23 @@ opt_google_oauth_port, opt_google_secret_override, opt_gtasks_list, + opt_markdown_file, opts_miscellaneous, - opts_filename_path, ) +from syncall.tw_gtasks_utils import convert_gtask_to_md, convert_md_to_gtask @click.command() @opt_gtasks_list() @opt_google_secret_override() @opt_google_oauth_port() +@opt_markdown_file() @opts_miscellaneous(side_A_name="Obsidian", side_B_name="Google Tasks") def main( gtasks_list: str, google_secret: str, oauth_port: int, - filename_path: str, - prefer_scheduled_date: bool, + markdown_file: str, resolution_strategy: str, verbose: int, combination_name: str, @@ -70,7 +71,7 @@ def main( combination_of_file_and_gtasks_list = any( [ - filename_path, + markdown_file, gtasks_list, ] ) @@ -83,7 +84,7 @@ def main( app_config = fetch_app_configuration( side_A_name="Obsidian", side_B_name="Google Tasks", combination=combination_name ) - md_filename_path = app_config["md_filename_path"] + markdown_file = app_config["markdown_file"] gtasks_list = app_config["gtasks_list"] # combination manually specified ---------------------------------------------------------- @@ -92,7 +93,7 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "gtasks_list": gtasks_list, - "filename_path": filename_path, + "markdown_file": markdown_file, }, config_fname="md_gtasks_configs", custom_combination_savename=custom_combination_savename, @@ -111,9 +112,8 @@ def main( format_dict( header="Configuration", items={ - "Markdown Filename Path": md_filename_path, + "Markdown Filename Path": markdown_file, "Google Tasks": gtasks_list, - "Prefer scheduled dates": prefer_scheduled_date, }, prefix="\n\n", suffix="\n", @@ -123,8 +123,8 @@ def main( confirm_before_proceeding() # initialize sides ------------------------------------------------------------------------ - tw_side = MarkdownTasksSide( - filename_path=filename_path + md_side = MarkdownTasksSide( + markdown_file=markdown_file ) gtasks_side = GTasksSide( @@ -149,10 +149,9 @@ def convert_B_to_A(*args, **kargs): convert_B_to_A.__doc__ = convert_md_to_gtask.__doc__ def convert_A_to_B(*args, **kargs): - return convert_gtask_to_tw( + return convert_gtask_to_md( *args, **kargs, - set_scheduled_date=prefer_scheduled_date, ) convert_A_to_B.__doc__ = convert_gtask_to_md.__doc__ diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index 994f064..037efec 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -36,7 +36,7 @@ def convert_tw_to_gtask( def convert_md_to_gtask( tw_item: Item, ) -> Item: - """TW -> GTasks conversion.""" + """MD -> GTasks conversion.""" assert all( i in md_item.keys() for i in ("title", "is_checked", "uuid") ), "Missing keys in tw_item" @@ -122,7 +122,7 @@ def convert_gtask_to_md( gtasks_item: GTasksItem, set_scheduled_date: bool = False, ) -> Item: - """GTasks -> TW Converter. + """GTasks -> MD Converter. If set_scheduled_date, then it will set the "scheduled" date of the produced TW task instead of the "due" date From 2b0d7dbe279820d16fe6115d4722b8400047ff7d Mon Sep 17 00:00:00 2001 From: jvalenzuela Date: Tue, 11 Mar 2025 11:40:32 +0100 Subject: [PATCH 05/15] added poetry.lock --- poetry.lock | 614 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 607 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 77c49a6..e89aba6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "anyio" @@ -22,6 +22,11 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "asana" version = "1.0.0" @@ -38,6 +43,11 @@ requests = ">=2.20.0,<3.dev0" requests-oauthlib = ">=0.8.0,<2.0" six = ">=1.10,<2.dev0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "astroid" version = "2.15.8" @@ -57,6 +67,11 @@ wrapt = [ {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "attrs" version = "24.2.0" @@ -76,6 +91,11 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "backports-zoneinfo" version = "0.2.1" @@ -104,6 +124,11 @@ files = [ [package.extras] tzdata = ["tzdata"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "bidict" version = "0.21.4" @@ -115,6 +140,11 @@ files = [ {file = "bidict-0.21.4.tar.gz", hash = "sha256:42c84ffbe6f8de898af6073b4be9ea7ccedcd78d3474aa844c54e49d5a079f6f"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "black" version = "22.3.0" @@ -161,6 +191,11 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "bubop" version = "0.1.12" @@ -179,6 +214,11 @@ python-dateutil = ">=2.8.2,<3.0.0" PyYAML = ">=5.3.1,<5.4.0" tqdm = ">=4.66.1,<5.0.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "cachetools" version = "5.4.0" @@ -190,6 +230,11 @@ files = [ {file = "cachetools-5.4.0.tar.gz", hash = "sha256:b8adc2e7c07f105ced7bc56dbb6dfbe7c4a00acce20e2227b3f355be89bc6827"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "caldav" version = "0.11.0" @@ -214,6 +259,11 @@ vobject = "*" [package.extras] test = ["coverage", "icalendar", "pytest", "pytest-coverage", "pytz", "radicale", "tzlocal", "xandikos"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "certifi" version = "2024.7.4" @@ -225,6 +275,11 @@ files = [ {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "cffi" version = "1.17.0" @@ -304,6 +359,11 @@ files = [ [package.dependencies] pycparser = "*" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "cfgv" version = "3.4.0" @@ -315,6 +375,11 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "chardet" version = "3.0.4" @@ -326,6 +391,11 @@ files = [ {file = "chardet-3.0.4.tar.gz", hash = "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -425,6 +495,11 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "check-jsonschema" version = "0.14.3" @@ -446,6 +521,11 @@ requests = "<3.0" [package.extras] dev = ["pytest (<7)", "pytest-cov (<3)", "pytest-xdist (<3)", "responses (==0.18.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "click" version = "8.1.7" @@ -460,6 +540,11 @@ files = [ [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "colorama" version = "0.4.6" @@ -471,6 +556,11 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "coverage" version = "6.5.0" @@ -536,6 +626,11 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "coveralls" version = "3.3.1" @@ -555,6 +650,11 @@ requests = ">=1.0.0" [package.extras] yaml = ["PyYAML (>=3.10)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "dill" version = "0.3.8" @@ -570,6 +670,11 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "distlib" version = "0.3.8" @@ -581,6 +686,11 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "docopt" version = "0.6.2" @@ -591,6 +701,11 @@ files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -605,6 +720,11 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "filelock" version = "3.15.4" @@ -621,6 +741,11 @@ docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1 testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] typing = ["typing-extensions (>=4.8)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "future" version = "1.0.0" @@ -632,6 +757,11 @@ files = [ {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "gkeepapi" version = "0.13.7" @@ -648,6 +778,11 @@ gpsoauth = ">=0.4.1" requests = {version = "2.23.0", markers = "platform_system == \"Windows\""} six = ">=1.11.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "gkeepapi" version = "0.16.0" @@ -665,6 +800,11 @@ gpsoauth = ">=1.1.0" [package.extras] dev = ["Sphinx (>=7.2.6)", "coverage (>=7.2.5)", "ruff (>=0.1.14)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-api-core" version = "2.19.1" @@ -688,6 +828,11 @@ grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-api-python-client" version = "2.141.0" @@ -706,12 +851,17 @@ google-auth-httplib2 = ">=0.2.0,<1.0.0" httplib2 = ">=0.19.0,<1.dev0" uritemplate = ">=3.0.1,<5" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-api-python-client-stubs" version = "1.27.0" description = "Type stubs for google-api-python-client" optional = false -python-versions = "<4.0,>=3.7" +python-versions = ">=3.7,<4.0" files = [ {file = "google_api_python_client_stubs-1.27.0-py3-none-any.whl", hash = "sha256:3c1f9f2a7cac8d1e9a7e84ed24e6c29cf4c643b0f94e39ed09ac1b7e91ab239a"}, {file = "google_api_python_client_stubs-1.27.0.tar.gz", hash = "sha256:148e16613e070969727f39691e23a73cdb87c65a4fc8133abd4c41d17b80b313"}, @@ -722,6 +872,11 @@ google-api-python-client = ">=2.141.0" types-httplib2 = ">=0.22.0.2" typing-extensions = ">=3.10.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-auth" version = "2.33.0" @@ -745,6 +900,11 @@ pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-auth-httplib2" version = "0.2.0" @@ -760,6 +920,11 @@ files = [ google-auth = "*" httplib2 = ">=0.19.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "google-auth-oauthlib" version = "0.4.6" @@ -778,6 +943,11 @@ requests-oauthlib = ">=0.7.0" [package.extras] tool = ["click (>=6.0.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "googleapis-common-protos" version = "1.63.2" @@ -795,12 +965,17 @@ protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4 [package.extras] grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "gpsoauth" version = "1.1.1" description = "A python client library for Google Play Services OAuth." optional = true -python-versions = "<4.0,>=3.8" +python-versions = ">=3.8,<4.0" files = [ {file = "gpsoauth-1.1.1-py3-none-any.whl", hash = "sha256:0fa7959b1d52fc625d93928e4ad4349ac79c6bfe811981d4f91f3b687e1b6fc1"}, {file = "gpsoauth-1.1.1.tar.gz", hash = "sha256:58202ed303397d2927b464dc95e2714bffff85a1b0f88bf68f3ad63859ebe435"}, @@ -811,6 +986,11 @@ pycryptodomex = ">=3.0" requests = ">=2.0.0" urllib3 = "<2.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "h11" version = "0.14.0" @@ -822,6 +1002,11 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "httpcore" version = "1.0.5" @@ -843,6 +1028,11 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] trio = ["trio (>=0.22.0,<0.26.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "httplib2" version = "0.22.0" @@ -857,6 +1047,11 @@ files = [ [package.dependencies] pyparsing = {version = ">=2.4.2,<3.0.0 || >3.0.0,<3.0.1 || >3.0.1,<3.0.2 || >3.0.2,<3.0.3 || >3.0.3,<4", markers = "python_version > \"3.0\""} +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "httpx" version = "0.27.0" @@ -881,6 +1076,11 @@ cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "icalendar" version = "5.0.13" @@ -897,6 +1097,11 @@ files = [ python-dateutil = "*" pytz = "*" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "identify" version = "2.6.0" @@ -911,6 +1116,11 @@ files = [ [package.extras] license = ["ukkonen"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "idna" version = "2.10" @@ -922,6 +1132,11 @@ files = [ {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "idna" version = "3.7" @@ -933,6 +1148,11 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "importlib-resources" version = "6.4.2" @@ -951,6 +1171,11 @@ zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "iniconfig" version = "2.0.0" @@ -962,6 +1187,11 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "isort" version = "5.13.2" @@ -976,6 +1206,11 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "item-synchronizer" version = "1.1.5" @@ -991,6 +1226,11 @@ files = [ bidict = ">=0.21.4,<0.22.0" bubop = ">=0.1.8,<0.2" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "jsonschema" version = "4.23.0" @@ -1014,6 +1254,11 @@ rpds-py = ">=0.7.1" format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "jsonschema-specifications" version = "2023.12.1" @@ -1029,6 +1274,11 @@ files = [ importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} referencing = ">=0.31.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "kitchen" version = "1.2.6" @@ -1039,6 +1289,11 @@ files = [ {file = "kitchen-1.2.6.tar.gz", hash = "sha256:b84cf582f1bd1556b60ebc7370b9d331eb9247b6b070ce89dfe959cba2c0b03c"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "lazy-object-proxy" version = "1.10.0" @@ -1085,6 +1340,11 @@ files = [ {file = "lazy_object_proxy-1.10.0-pp310.pp311.pp312.pp38.pp39-none-any.whl", hash = "sha256:80fa48bd89c8f2f456fc0765c11c23bf5af827febacd2f523ca5bc1893fcc09d"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "loguru" version = "0.5.3" @@ -1103,6 +1363,11 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)", "tox-travis (>=0.12)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "lxml" version = "5.3.0" @@ -1257,6 +1522,11 @@ html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] source = ["Cython (>=3.0.11)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "mccabe" version = "0.7.0" @@ -1268,6 +1538,11 @@ files = [ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "mock" version = "5.1.0" @@ -1284,6 +1559,11 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "mypy" version = "1.11.1" @@ -1331,6 +1611,11 @@ install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -1342,17 +1627,27 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "notion-client" version = "0.7.1" @@ -1367,6 +1662,11 @@ files = [ [package.dependencies] httpx = ">=0.15.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "oauthlib" version = "3.2.2" @@ -1383,6 +1683,11 @@ rsa = ["cryptography (>=3.0.0)"] signals = ["blinker (>=1.4.0)"] signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "packaging" version = "23.2" @@ -1394,6 +1699,11 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pathspec" version = "0.12.1" @@ -1405,6 +1715,11 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pkgutil-resolve-name" version = "1.3.10" @@ -1416,6 +1731,11 @@ files = [ {file = "pkgutil_resolve_name-1.3.10.tar.gz", hash = "sha256:357d6c9e6a755653cfd78893817c0853af365dd51ec97f3d358a819373bbd174"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "platformdirs" version = "4.2.2" @@ -1432,6 +1752,11 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pluggy" version = "1.5.0" @@ -1447,6 +1772,11 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pre-commit" version = "2.21.0" @@ -1465,6 +1795,11 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "proto-plus" version = "1.24.0" @@ -1482,6 +1817,11 @@ protobuf = ">=3.19.0,<6.0.0dev" [package.extras] testing = ["google-api-core (>=1.31.5)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "protobuf" version = "5.27.3" @@ -1502,6 +1842,11 @@ files = [ {file = "protobuf-5.27.3.tar.gz", hash = "sha256:82460903e640f2b7e34ee81a947fdaad89de796d324bcbc38ff5430bcdead82c"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyasn1" version = "0.6.0" @@ -1513,6 +1858,11 @@ files = [ {file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyasn1-modules" version = "0.4.0" @@ -1527,6 +1877,11 @@ files = [ [package.dependencies] pyasn1 = ">=0.4.6,<0.7.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pycparser" version = "2.22" @@ -1538,6 +1893,11 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pycryptodomex" version = "3.20.0" @@ -1579,6 +1939,11 @@ files = [ {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyfakefs" version = "4.7.0" @@ -1590,6 +1955,11 @@ files = [ {file = "pyfakefs-4.7.0.tar.gz", hash = "sha256:f22d30d93d2989bf2d2c67b606a14cbab2df0be912c09dcdb590ea4931073ade"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyfakefs" version = "5.6.0" @@ -1601,6 +1971,11 @@ files = [ {file = "pyfakefs-5.6.0.tar.gz", hash = "sha256:7a549b32865aa97d8ba6538285a93816941d9b7359be2954ac60ec36b277e879"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pylint" version = "2.17.7" @@ -1630,6 +2005,11 @@ typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\"" spelling = ["pyenchant (>=3.2,<4.0)"] testutils = ["gitpython (>3)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyparsing" version = "3.1.2" @@ -1644,6 +2024,11 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyright" version = "1.1.376" @@ -1662,6 +2047,11 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pytest" version = "8.3.2" @@ -1684,6 +2074,11 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1698,6 +2093,11 @@ files = [ [package.dependencies] six = ">=1.5" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pytz" version = "2023.4" @@ -1709,6 +2109,11 @@ files = [ {file = "pytz-2023.4.tar.gz", hash = "sha256:31d4583c4ed539cd037956140d695e42c033a19e984bfce9964a3f7d59bc2b40"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyupgrade" version = "3.16.0" @@ -1723,6 +2128,11 @@ files = [ [package.dependencies] tokenize-rt = ">=5.2.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "pyyaml" version = "5.3.1" @@ -1745,6 +2155,11 @@ files = [ {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "recurring-ical-events" version = "2.2.3" @@ -1763,6 +2178,11 @@ python-dateutil = ">=2.8.1,<3.0.0" tzdata = {version = "*", markers = "python_version >= \"3.7\""} x-wr-timezone = "==0.*" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "referencing" version = "0.35.1" @@ -1778,6 +2198,11 @@ files = [ attrs = ">=22.2.0" rpds-py = ">=0.7.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "requests" version = "2.23.0" @@ -1799,6 +2224,11 @@ urllib3 = ">=1.21.1,<1.25.0 || >1.25.0,<1.25.1 || >1.25.1,<1.26" security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "requests" version = "2.32.3" @@ -1820,6 +2250,11 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "requests-oauthlib" version = "1.3.1" @@ -1838,6 +2273,11 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "rfc3339" version = "6.2" @@ -1849,6 +2289,11 @@ files = [ {file = "rfc3339-6.2.tar.gz", hash = "sha256:d53c3b5eefaef892b7240ba2a91fef012e86faa4d0a0ca782359c490e00ad4d0"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "rpds-py" version = "0.20.0" @@ -1961,6 +2406,11 @@ files = [ {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "rsa" version = "4.9" @@ -1975,6 +2425,11 @@ files = [ [package.dependencies] pyasn1 = ">=0.1.3" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "ruamel-yaml" version = "0.16.12" @@ -1993,6 +2448,11 @@ files = [ docs = ["ryd"] jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "ruamel-yaml-clib" version = "0.2.8" @@ -2052,6 +2512,11 @@ files = [ {file = "ruamel.yaml.clib-0.2.8.tar.gz", hash = "sha256:beb2e0404003de9a4cab9753a8805a8fe9320ee6673136ed7f04255fe60bb512"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "ruff" version = "0.5.7" @@ -2079,6 +2544,11 @@ files = [ {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "setuptools" version = "72.2.0" @@ -2095,6 +2565,11 @@ core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.te doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.11.*)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (<0.4)", "pytest-ruff (>=0.2.1)", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "six" version = "1.16.0" @@ -2106,6 +2581,11 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "sniffio" version = "1.3.1" @@ -2117,12 +2597,17 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "taskw-ng" version = "0.2.7" description = "Next generation python bindings for your taskwarrior database" optional = true -python-versions = "<4.0,>=3.8" +python-versions = ">=3.8,<4.0" files = [ {file = "taskw_ng-0.2.7-py3-none-any.whl", hash = "sha256:db956b404b9e992ffe0ad46a7dafea4d591b156fc5dbf009a2a453bfa5a721ea"}, {file = "taskw_ng-0.2.7.tar.gz", hash = "sha256:558942b94b8bcdfa4a4e9c237ca129b37f0f9baeab3608a9b0a70d8f4d499013"}, @@ -2134,6 +2619,11 @@ packaging = ">=23.2,<24.0" python-dateutil = ">=2.8.2,<3.0.0" pytz = ">=2023.3.post1,<2024.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tokenize-rt" version = "6.0.0" @@ -2145,6 +2635,11 @@ files = [ {file = "tokenize_rt-6.0.0.tar.gz", hash = "sha256:b9711bdfc51210211137499b5e355d3de5ec88a85d2025c520cbb921b5194367"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tomli" version = "2.0.1" @@ -2156,6 +2651,11 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tomlkit" version = "0.13.2" @@ -2167,6 +2667,11 @@ files = [ {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tqdm" version = "4.66.5" @@ -2187,6 +2692,11 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "types-httplib2" version = "0.22.0.20240310" @@ -2198,6 +2708,11 @@ files = [ {file = "types_httplib2-0.22.0.20240310-py3-none-any.whl", hash = "sha256:8cd706fc81f0da32789a4373a28df6f39e9d5657d1281db4d2fd22ee29e83661"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "types-pyyaml" version = "5.4.12" @@ -2209,6 +2724,11 @@ files = [ {file = "types_PyYAML-5.4.12-py3-none-any.whl", hash = "sha256:e06083f85375a5678e4c19452ed6467ce2167b71db222313e1792cb8fc76859a"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "types-setuptools" version = "57.4.18" @@ -2220,6 +2740,11 @@ files = [ {file = "types_setuptools-57.4.18-py3-none-any.whl", hash = "sha256:9660b8774b12cd61b448e2fd87a667c02e7ec13ce9f15171f1d49a4654c4df6a"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -2231,6 +2756,11 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tzdata" version = "2024.1" @@ -2242,6 +2772,11 @@ files = [ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "tzlocal" version = "5.2" @@ -2260,6 +2795,11 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "uritemplate" version = "4.1.1" @@ -2271,6 +2811,11 @@ files = [ {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "urllib3" version = "1.25.11" @@ -2287,12 +2832,17 @@ brotli = ["brotlipy (>=0.6.0)"] secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "urllib3" version = "1.26.19" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" files = [ {file = "urllib3-1.26.19-py2.py3-none-any.whl", hash = "sha256:37a0344459b199fce0e80b0d3569837ec6b6937435c5244e7fd73fa6006830f3"}, {file = "urllib3-1.26.19.tar.gz", hash = "sha256:3e3d753a8618b86d7de333b4223005f68720bcd6a7d2bcb9fbd2229ec7c1e429"}, @@ -2303,6 +2853,11 @@ brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotl secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "urllib3" version = "2.2.2" @@ -2320,6 +2875,11 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "virtualenv" version = "20.26.3" @@ -2340,6 +2900,11 @@ platformdirs = ">=3.9.1,<5" docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "vobject" version = "0.9.7" @@ -2354,6 +2919,11 @@ files = [ [package.dependencies] python-dateutil = ">=2.4.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "win32-setctime" version = "1.1.0" @@ -2368,6 +2938,11 @@ files = [ [package.extras] dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "wrapt" version = "1.16.0" @@ -2447,6 +3022,11 @@ files = [ {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "x-wr-timezone" version = "0.0.7" @@ -2462,6 +3042,11 @@ files = [ icalendar = "*" pytz = "*" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "xattr" version = "0.9.9" @@ -2529,6 +3114,11 @@ files = [ [package.dependencies] cffi = ">=1.0" +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "xdg" version = "6.0.0" @@ -2540,6 +3130,11 @@ files = [ {file = "xdg-6.0.0.tar.gz", hash = "sha256:24278094f2d45e846d1eb28a2ebb92d7b67fc0cab5249ee3ce88c95f649a1c92"}, ] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [[package]] name = "zipp" version = "3.20.0" @@ -2555,6 +3150,11 @@ files = [ doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] +[package.source] +type = "legacy" +url = "https://nexus.corp.indeed.com/repository/pypi/simple" +reference = "nexus" + [extras] asana = ["asana"] caldav = ["caldav", "icalendar"] @@ -2567,4 +3167,4 @@ tw = ["taskw-ng", "xdg"] [metadata] lock-version = "2.0" python-versions = ">=3.8.1,<=3.12.5" -content-hash = "eba5002826c17b8831db5486299b466f8673c50f5bc900d8d3dcfe6530eb510b" +content-hash = "48e4fc9393542c179641d9e71af992705cd65e6f06c4be58479657d084eb1fb8" From 9755db8c6795e3d5a107679cb498f170c9634b8b Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Mon, 29 Dec 2025 16:20:51 +0100 Subject: [PATCH 06/15] almost done --- syncall/filesystem/markdown_task_item.py | 43 +++++++++++++++++++---- syncall/filesystem/markdown_tasks_side.py | 16 ++++----- syncall/tw_gtasks_utils.py | 8 +++-- 3 files changed, 49 insertions(+), 18 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 3d5bb43..8e37d9d 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -1,9 +1,11 @@ import datetime +import re import uuid from item_synchronizer.types import ID from syncall.concrete_item import ConcreteItem, ItemKey, KeyType +from syncall.filesystem.filesystem_file import FilesystemFile class MarkdownTaskItem(ConcreteItem): @@ -13,11 +15,12 @@ def __init__(self, is_checked: bool = False, title: str = ""): super().__init__( keys=( ItemKey("is_checked", KeyType.String), - ItemKey("last_modified_date", KeyType.Date), ItemKey("title", KeyType.String), + ItemKey("last_modified_date", KeyType.Date), ) ) + self._markdown_file = None self.deleted = False self.is_checked = is_checked self.title = title @@ -26,15 +29,41 @@ def __init__(self, is_checked: bool = False, title: str = ""): def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": """Create a MarkdownTaskItem given the raw item at hand.""" - is_archived = False - is_checked = False - last_modified_date = None - title = markdown_raw_item["title"] + result = cls( + is_checked=markdown_raw_item["is_checked"], + title=markdown_raw_item["title"] + ) + # import pdb; pdb.set_trace() + return result + + @classmethod + def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "MarkdownTaskItem": + """Create a MarkdownTaskItem given the line of text.""" - return cls( + md_task_checkbox = r"-\s*\[[ xX]\]\s*" + + checkbox_found = re.match(md_task_checkbox, markdown_text) + if checkbox_found: + is_checked = 'X' in checkbox_found.group(0).upper() + title = re.split(md_task_checkbox, markdown_text)[-1] + + result = cls( is_checked=is_checked, - title=title, + title=title ) + result._markdown_file = markdown_file + # import pdb; pdb.set_trace() + return result + + @property + def last_modified_date(self) -> datetime.datetime: + if self._markdown_file: + return self._markdown_file.last_modified_date + + def __str__(self): + return '- [{}] {}'.format( + 'X' if self.is_checked else ' ', + self.title) def _id(self) -> ID: return uuid.uuid5(uuid.NAMESPACE_OID, self.title) diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index ec39f46..a8d8430 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -1,3 +1,6 @@ +import datetime +import re + from pathlib import Path from typing import MutableMapping, Optional, Sequence, cast @@ -33,7 +36,6 @@ def __init__(self, markdown_file: Path) -> None: self._filename_path = markdown_file self._filesystem_file = FilesystemFile(path=markdown_file) - self.all_items = self.get_all_items() self._items_cache: dict[str, dict] = { item.id: item for item in self.get_all_items() } @@ -42,17 +44,14 @@ def start(self): pass def finish(self): + self._filesystem_file.contents='\n'.join(str(i) for i in self._items_cache.values()) self._filesystem_file.flush() def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: """Read all items again from storage.""" all_items = tuple( - MarkdownTaskItem( - is_checked=True, - last_modified_date=True, - title=line, - ) - for line in self._filesystem_file.contents + MarkdownTaskItem.from_markdown(line, self._filesystem_file) + for line in self._filesystem_file.contents.splitlines() if line.startswith("- [") ) @@ -73,6 +72,7 @@ def delete_single_item(self, item_id: ID): return def update_item(self, item_id: ID, **changes): + import pdb; pdb.set_trace() item = self.get_item(item_id) if item is None: logger.warning(f"Requested to update item {item_id} but item cannot be found.") @@ -94,6 +94,6 @@ def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: def items_are_identical( cls, item1: ConcreteItem, item2: ConcreteItem, ignore_keys: Sequence[str] = [] ) -> bool: - ignore_keys_ = [cls.last_modification_key()] + ignore_keys_ = [] ignore_keys_.extend(ignore_keys) return item1.compare(item2, ignore_keys=ignore_keys_) diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index 037efec..54b1e7e 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -34,15 +34,16 @@ def convert_tw_to_gtask( return gtasks_item def convert_md_to_gtask( - tw_item: Item, + md_item: Item, ) -> Item: """MD -> GTasks conversion.""" assert all( i in md_item.keys() for i in ("title", "is_checked", "uuid") - ), "Missing keys in tw_item" + ), "Missing keys in md_item" - gtasks_item = {} + gtasks_item: Item = {} + # import pdb; pdb.set_trace() # title gtasks_item["title"] = md_item["title"] @@ -148,4 +149,5 @@ def convert_gtask_to_md( if "updated" in gtasks_item.keys(): md_item["last_modified_date"] = parse_google_datetime(gtasks_item["updated"]) + # import pdb; pdb.set_trace() return md_item From ecfe214467043373601c1c255cc8138070c01a56 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Mon, 29 Dec 2025 19:06:38 +0100 Subject: [PATCH 07/15] add regex constant and cli options --- syncall/cli.py | 18 +++++++++++-- syncall/filesystem/markdown_task_item.py | 25 ++++++++++-------- syncall/filesystem/markdown_tasks_side.py | 18 +++++++++---- syncall/scripts/md_gtasks_sync.py | 7 +++-- syncall/tw_gtasks_utils.py | 32 +++++++++++------------ 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/syncall/cli.py b/syncall/cli.py index 88d6cf7..10d74f4 100644 --- a/syncall/cli.py +++ b/syncall/cli.py @@ -357,8 +357,22 @@ def opt_gtasks_list(): ) -# google tasks -------------------------------------------------------------------------------- -def opt_markdown_file(): +# markdown file ------------------------------------------------------------------------------- +def opts_markdown(): + def decorator(f): + for d in reversed( + [ + _opt_md_file, + _opt_prefer_scheduled_date, + ], + ): + f = d()(f) + return f + + return decorator + + +def _opt_md_file(): return click.option( "-m", "--markdown-file", diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 8e37d9d..7da24f2 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -7,6 +7,10 @@ from syncall.concrete_item import ConcreteItem, ItemKey, KeyType from syncall.filesystem.filesystem_file import FilesystemFile +MD_TASK_CHECKBOX_RE = r"-\s*\[[ xX]\]\s*" +MD_TASK_SCHEDULED_EMOJI = "⏳" +MD_TASK_DUE_EMOJI = "📅" +MD_TASK_DATE_RE = r"-\s*\[[ xX]\]\s*" class MarkdownTaskItem(ConcreteItem): """A task line inside a Markdown file.""" @@ -20,7 +24,9 @@ def __init__(self, is_checked: bool = False, title: str = ""): ) ) - self._markdown_file = None + self.last_modified_date = None + self.scheduled_date = None + self.due_date = None self.deleted = False self.is_checked = is_checked self.title = title @@ -40,31 +46,28 @@ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "MarkdownTaskItem": """Create a MarkdownTaskItem given the line of text.""" - md_task_checkbox = r"-\s*\[[ xX]\]\s*" - - checkbox_found = re.match(md_task_checkbox, markdown_text) + checkbox_found = re.match(MD_TASK_CHECKBOX_RE, markdown_text) if checkbox_found: is_checked = 'X' in checkbox_found.group(0).upper() - title = re.split(md_task_checkbox, markdown_text)[-1] + title = re.split(MD_TASK_CHECKBOX_RE, markdown_text)[-1] result = cls( is_checked=is_checked, title=title ) - result._markdown_file = markdown_file + result.last_modified_date = markdown_file.last_modified_date # import pdb; pdb.set_trace() return result - @property - def last_modified_date(self) -> datetime.datetime: - if self._markdown_file: - return self._markdown_file.last_modified_date - def __str__(self): return '- [{}] {}'.format( 'X' if self.is_checked else ' ', self.title) + @classmethod + def last_modification_key(cls) -> str: + return "last_modified_date" + def _id(self) -> ID: return uuid.uuid5(uuid.NAMESPACE_OID, self.title) diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index a8d8430..60f5b3c 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -37,7 +37,7 @@ def __init__(self, markdown_file: Path) -> None: self._filesystem_file = FilesystemFile(path=markdown_file) self._items_cache: dict[str, dict] = { - item.id: item for item in self.get_all_items() + str(item.id): item for item in self.get_all_items() } def start(self): @@ -72,7 +72,6 @@ def delete_single_item(self, item_id: ID): return def update_item(self, item_id: ID, **changes): - import pdb; pdb.set_trace() item = self.get_item(item_id) if item is None: logger.warning(f"Requested to update item {item_id} but item cannot be found.") @@ -94,6 +93,15 @@ def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: def items_are_identical( cls, item1: ConcreteItem, item2: ConcreteItem, ignore_keys: Sequence[str] = [] ) -> bool: - ignore_keys_ = [] - ignore_keys_.extend(ignore_keys) - return item1.compare(item2, ignore_keys=ignore_keys_) + # item1 = item1.copy() + # item2 = item2.copy() + + keys = [ + k + for k in [ + "title", + "is_checked", + ] + if k not in ignore_keys + ] + return SyncSide._items_are_identical(item1, item2, keys) diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py index e8aaae4..a53e58d 100644 --- a/syncall/scripts/md_gtasks_sync.py +++ b/syncall/scripts/md_gtasks_sync.py @@ -30,7 +30,7 @@ opt_google_oauth_port, opt_google_secret_override, opt_gtasks_list, - opt_markdown_file, + opts_markdown, opts_miscellaneous, ) from syncall.tw_gtasks_utils import convert_gtask_to_md, convert_md_to_gtask @@ -40,13 +40,14 @@ @opt_gtasks_list() @opt_google_secret_override() @opt_google_oauth_port() -@opt_markdown_file() +@opts_markdown() @opts_miscellaneous(side_A_name="Obsidian", side_B_name="Google Tasks") def main( gtasks_list: str, google_secret: str, oauth_port: int, markdown_file: str, + prefer_scheduled_date: bool, resolution_strategy: str, verbose: int, combination_name: str, @@ -114,6 +115,7 @@ def main( items={ "Markdown Filename Path": markdown_file, "Google Tasks": gtasks_list, + "Prefer scheduled dates": prefer_scheduled_date, }, prefix="\n\n", suffix="\n", @@ -152,6 +154,7 @@ def convert_A_to_B(*args, **kargs): return convert_gtask_to_md( *args, **kargs, + set_scheduled_date=prefer_scheduled_date, ) convert_A_to_B.__doc__ = convert_gtask_to_md.__doc__ diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index 54b1e7e..caa7d2d 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -2,6 +2,7 @@ from item_synchronizer.types import Item from syncall.google.common import parse_google_datetime +from syncall.filesystem.markdown_task_item import MarkdownTaskItem from syncall.google.gtasks_side import GTasksSide from syncall.tw_utils import extract_tw_fields_from_string, get_tw_annotations_as_str from syncall.types import GTasksItem @@ -38,17 +39,17 @@ def convert_md_to_gtask( ) -> Item: """MD -> GTasks conversion.""" assert all( - i in md_item.keys() for i in ("title", "is_checked", "uuid") + i in md_item.keys() for i in ("title", "is_checked") ), "Missing keys in md_item" - gtasks_item: Item = {} + gtasks_item = {} # import pdb; pdb.set_trace() # title gtasks_item["title"] = md_item["title"] # status - gtasks_item["status"] = "needsAction" if md_item["is_checked"] else "completed" + gtasks_item["status"] = "completed" if md_item["is_checked"] else "needsAction" # update time if "last_modified_date" in md_item.keys(): @@ -128,26 +129,25 @@ def convert_gtask_to_md( If set_scheduled_date, then it will set the "scheduled" date of the produced TW task instead of the "due" date """ - # Parse the description - uuid = None - - md_item: Item = {} - status_gtask = gtasks_item["status"] # status - md_item["is_checked"] = status_gtask == "completed" - - # uuid - may just be created -, thus not there - if uuid is not None: - md_item["uuid"] = uuid + is_checked = status_gtask == "completed" # Description - md_item["title"] = gtasks_item["title"] + title = gtasks_item["title"] + + md_item: MarkdownTaskItem = MarkdownTaskItem(is_checked, title) + + date_key = "scheduled" if set_scheduled_date else "due" + + # due/scheduled date + due_date = GTasksSide.get_task_due_time(gtasks_item) + if due_date is not None: + md_item[date_key] = due_date # update time if "updated" in gtasks_item.keys(): - md_item["last_modified_date"] = parse_google_datetime(gtasks_item["updated"]) + md_item.last_modified_date = parse_google_datetime(gtasks_item["updated"]) - # import pdb; pdb.set_trace() return md_item From 93cd9bfd158bd68753b2f3cdd953ccf50f6ee271 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Mon, 29 Dec 2025 22:20:26 +0100 Subject: [PATCH 08/15] handle markdown dates with emojis --- syncall/filesystem/markdown_task_item.py | 50 ++++++++++++++++++----- syncall/filesystem/markdown_tasks_side.py | 2 + syncall/scripts/md_gtasks_sync.py | 3 +- syncall/tw_gtasks_utils.py | 27 ++++++++---- 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 7da24f2..8c72d02 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -7,10 +7,15 @@ from syncall.concrete_item import ConcreteItem, ItemKey, KeyType from syncall.filesystem.filesystem_file import FilesystemFile -MD_TASK_CHECKBOX_RE = r"-\s*\[[ xX]\]\s*" +MD_TASK_CHECKBOX_RE = r"\[[ xX]\]" +MD_TASK_LINE_RE = r"-\s*" + MD_TASK_CHECKBOX_RE MD_TASK_SCHEDULED_EMOJI = "⏳" MD_TASK_DUE_EMOJI = "📅" -MD_TASK_DATE_RE = r"-\s*\[[ xX]\]\s*" +MD_TASK_DONE_EMOJI = "✅" + +MD_TASK_SCHEDULED_RE = r"(?<=⏳ )\d{4}-\d{2}-\d{2}" +MD_TASK_DUE_RE = r"(?<=📅 )\d{4}-\d{2}-\d{2}" +MD_TASK_DONE_RE = r"(?<=✅ )\d{4}-\d{2}-\d{2}" class MarkdownTaskItem(ConcreteItem): """A task line inside a Markdown file.""" @@ -27,6 +32,7 @@ def __init__(self, is_checked: bool = False, title: str = ""): self.last_modified_date = None self.scheduled_date = None self.due_date = None + self.done_date = None self.deleted = False self.is_checked = is_checked self.title = title @@ -39,34 +45,56 @@ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": is_checked=markdown_raw_item["is_checked"], title=markdown_raw_item["title"] ) - # import pdb; pdb.set_trace() return result @classmethod def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "MarkdownTaskItem": """Create a MarkdownTaskItem given the line of text.""" - checkbox_found = re.match(MD_TASK_CHECKBOX_RE, markdown_text) - if checkbox_found: + markdown_task = re.match(MD_TASK_LINE_RE, markdown_text) + due_date = re.search(MD_TASK_DUE_RE, markdown_text) + scheduled_date = re.search(MD_TASK_SCHEDULED_RE, markdown_text) + done_date = re.search(MD_TASK_DONE_RE, markdown_text) + + if markdown_task: + checkbox_found = re.search(MD_TASK_CHECKBOX_RE, markdown_text) is_checked = 'X' in checkbox_found.group(0).upper() - title = re.split(MD_TASK_CHECKBOX_RE, markdown_text)[-1] + + md_task_split_re = "(\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*)".format(MD_TASK_CHECKBOX_RE, MD_TASK_SCHEDULED_EMOJI, MD_TASK_DUE_EMOJI, MD_TASK_DONE_EMOJI) + title = re.split(md_task_split_re, markdown_text)[2].strip() result = cls( is_checked=is_checked, title=title ) result.last_modified_date = markdown_file.last_modified_date - # import pdb; pdb.set_trace() + + if due_date: + result.due_date = datetime.datetime.fromisoformat(due_date.group(0)) + + if scheduled_date: + result.scheduled_date = datetime.datetime.fromisoformat(scheduled_date.group(0)) + + if done_date: + result.done_date = datetime.datetime.fromisoformat(done_date.group(0)) + return result def __str__(self): - return '- [{}] {}'.format( + result = '- [{}] {}'.format( 'X' if self.is_checked else ' ', self.title) - @classmethod - def last_modification_key(cls) -> str: - return "last_modified_date" + if self.scheduled_date: + result += " " + MD_TASK_SCHEDULED_EMOJI + " " + self.scheduled_date.date().isoformat() + + if self.due_date: + result += " " + MD_TASK_DUE_EMOJI + " " + self.due_date.date().isoformat() + + if self.done_date: + result += " " + MD_TASK_DONE_EMOJI + " " + self.done_date.date().isoformat() + + return result def _id(self) -> ID: return uuid.uuid5(uuid.NAMESPACE_OID, self.title) diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index 60f5b3c..1cfe17c 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -101,6 +101,8 @@ def items_are_identical( for k in [ "title", "is_checked", + "due_date", + "done_date", ] if k not in ignore_keys ] diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py index a53e58d..7053576 100644 --- a/syncall/scripts/md_gtasks_sync.py +++ b/syncall/scripts/md_gtasks_sync.py @@ -146,6 +146,7 @@ def convert_B_to_A(*args, **kargs): return convert_md_to_gtask( *args, **kargs, + set_scheduled_date=prefer_scheduled_date, ) convert_B_to_A.__doc__ = convert_md_to_gtask.__doc__ @@ -170,7 +171,7 @@ def convert_A_to_B(*args, **kargs): ), config_fname=combination_name, ignore_keys=( - (), + ("last_modified_date"), (), ), ) as aggregator: diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index caa7d2d..dafb6bf 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -36,6 +36,7 @@ def convert_tw_to_gtask( def convert_md_to_gtask( md_item: Item, + set_scheduled_date: bool = False, ) -> Item: """MD -> GTasks conversion.""" assert all( @@ -44,16 +45,22 @@ def convert_md_to_gtask( gtasks_item = {} - # import pdb; pdb.set_trace() # title gtasks_item["title"] = md_item["title"] # status gtasks_item["status"] = "completed" if md_item["is_checked"] else "needsAction" - # update time - if "last_modified_date" in md_item.keys(): - gtasks_item["updated"] = format_datetime_tz(parse_google_datetime(md_item["last_modified_date"])) + # dates + if md_item.last_modified_date: + gtasks_item["updated"] = format_datetime_tz(parse_google_datetime(md_item.last_modified_date)) + + due_date = md_item.scheduled_date if set_scheduled_date else md_item.due_date + if md_item.due_date: + gtasks_item["due"] = format_datetime_tz(parse_google_datetime(due_date)) + + if md_item.done_date: + gtasks_item["completed"] = format_datetime_tz(parse_google_datetime(md_item.done_date)) return gtasks_item @@ -139,12 +146,18 @@ def convert_gtask_to_md( md_item: MarkdownTaskItem = MarkdownTaskItem(is_checked, title) - date_key = "scheduled" if set_scheduled_date else "due" - # due/scheduled date due_date = GTasksSide.get_task_due_time(gtasks_item) if due_date is not None: - md_item[date_key] = due_date + if set_scheduled_date: + md_item.scheduled_date = due_date + else: + md_item.due_date = due_date + + # end date + end_date = GTasksSide.get_task_completed_time(gtasks_item) + if end_date is not None: + md_item.done_date = end_date # update time if "updated" in gtasks_item.keys(): From ece314d6ec950fb660e09ea6b0c961eb5ba9358f Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Mon, 29 Dec 2025 23:12:16 +0100 Subject: [PATCH 09/15] fix for datetime timezone fake changes --- syncall/tw_gtasks_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py index dafb6bf..f34d226 100644 --- a/syncall/tw_gtasks_utils.py +++ b/syncall/tw_gtasks_utils.py @@ -150,17 +150,17 @@ def convert_gtask_to_md( due_date = GTasksSide.get_task_due_time(gtasks_item) if due_date is not None: if set_scheduled_date: - md_item.scheduled_date = due_date + md_item.scheduled_date = due_date.replace(tzinfo=None) else: - md_item.due_date = due_date + md_item.due_date = due_date.replace(tzinfo=None) # end date end_date = GTasksSide.get_task_completed_time(gtasks_item) if end_date is not None: - md_item.done_date = end_date + md_item.done_date = end_date.replace(tzinfo=None) # update time if "updated" in gtasks_item.keys(): - md_item.last_modified_date = parse_google_datetime(gtasks_item["updated"]) + md_item.last_modified_date = parse_google_datetime(gtasks_item["updated"]).replace(tzinfo=None) return md_item From d4d23f83e65d670a91fb76bcc282fe9077df0334 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Tue, 30 Dec 2025 12:47:50 +0100 Subject: [PATCH 10/15] several fixes --- syncall/filesystem/markdown_task_item.py | 12 +++++++----- syncall/filesystem/markdown_tasks_side.py | 18 +++++++++++++----- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 8c72d02..5b280de 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -56,12 +56,14 @@ def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "Ma scheduled_date = re.search(MD_TASK_SCHEDULED_RE, markdown_text) done_date = re.search(MD_TASK_DONE_RE, markdown_text) - if markdown_task: - checkbox_found = re.search(MD_TASK_CHECKBOX_RE, markdown_text) - is_checked = 'X' in checkbox_found.group(0).upper() + if markdown_task is None: + return None - md_task_split_re = "(\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*)".format(MD_TASK_CHECKBOX_RE, MD_TASK_SCHEDULED_EMOJI, MD_TASK_DUE_EMOJI, MD_TASK_DONE_EMOJI) - title = re.split(md_task_split_re, markdown_text)[2].strip() + checkbox_found = re.search(MD_TASK_CHECKBOX_RE, markdown_text) + is_checked = 'X' in checkbox_found.group(0).upper() + + md_task_split_re = "(\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*|\\s*{}\\s*)".format(MD_TASK_CHECKBOX_RE, MD_TASK_SCHEDULED_EMOJI, MD_TASK_DUE_EMOJI, MD_TASK_DONE_EMOJI) + title = re.split(md_task_split_re, markdown_text)[2].strip() result = cls( is_checked=is_checked, diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index 1cfe17c..b6de503 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -44,7 +44,8 @@ def start(self): pass def finish(self): - self._filesystem_file.contents='\n'.join(str(i) for i in self._items_cache.values()) + contents = '\n'.join(str(i) for i in self._items_cache.values()) + "\n" + self._filesystem_file.contents = contents self._filesystem_file.flush() def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: @@ -52,13 +53,12 @@ def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: all_items = tuple( MarkdownTaskItem.from_markdown(line, self._filesystem_file) for line in self._filesystem_file.contents.splitlines() - if line.startswith("- [") ) logger.opt(lazy=True).debug( f"Found {len(all_items)} matching tasks inside {self._filename_path}" ) - return all_items + return [i for i in all_items if i] def get_item(self, item_id: ID) -> Optional[MarkdownTaskItem]: item = self._items_cache.get(item_id) @@ -81,7 +81,10 @@ def update_item(self, item_id: ID, **changes): logger.warning(f"Invalid changes provided to Filesystem Side -> {changes}") return - item.title = changes["title"] + if item.title != changes["title"]: + item.title = changes["title"] + + logger.warning(f"The item {item_id} has changed its id to {item._id()}") item.is_checked = changes["is_checked"] def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: @@ -99,6 +102,7 @@ def items_are_identical( keys = [ k for k in [ + "id", "title", "is_checked", "due_date", @@ -106,4 +110,8 @@ def items_are_identical( ] if k not in ignore_keys ] - return SyncSide._items_are_identical(item1, item2, keys) + result = SyncSide._items_are_identical(item1, item2, keys) + # if not result: + # import pdb; pdb.set_trace() + return result + # return SyncSide._items_are_identical(item1, item2, keys) From e0d878c3bacb71790b392c3e77746290f9c6ea61 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Tue, 30 Dec 2025 18:57:38 +0100 Subject: [PATCH 11/15] pickle persistent ids --- syncall/filesystem/markdown_task_item.py | 8 +++- syncall/filesystem/markdown_tasks_side.py | 45 +++++++++++++++++++---- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 5b280de..2be8915 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -2,8 +2,9 @@ import re import uuid -from item_synchronizer.types import ID +from typing import Optional +from item_synchronizer.types import ID from syncall.concrete_item import ConcreteItem, ItemKey, KeyType from syncall.filesystem.filesystem_file import FilesystemFile @@ -29,6 +30,7 @@ def __init__(self, is_checked: bool = False, title: str = ""): ) ) + self._persistent_id = None self.last_modified_date = None self.scheduled_date = None self.due_date = None @@ -101,5 +103,9 @@ def __str__(self): def _id(self) -> ID: return uuid.uuid5(uuid.NAMESPACE_OID, self.title) + @property + def id(self) -> Optional[ID]: + return self._persistent_id or self._id() + def delete(self) -> None: self.deleted = True diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index b6de503..30559c6 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -1,4 +1,5 @@ import datetime +import pickle import re from pathlib import Path @@ -35,6 +36,12 @@ def __init__(self, markdown_file: Path) -> None: super().__init__(name="Fs", fullname="Filesystem") self._filename_path = markdown_file self._filesystem_file = FilesystemFile(path=markdown_file) + self._filesystem_ids_path = Path(f".{markdown_file}.ids") + + self._ids_map = {} + if self._filesystem_ids_path.is_file(): + with self._filesystem_ids_path.open("rb") as f: + self._ids_map = pickle.load(f) self._items_cache: dict[str, dict] = { str(item.id): item for item in self.get_all_items() @@ -48,6 +55,23 @@ def finish(self): self._filesystem_file.contents = contents self._filesystem_file.flush() + # delete id mappings if the item no longer exist + existing_ids = [ str(item._id()) for item in self._items_cache.values() ] + self._ids_map = {new_id: persistent_id for new_id, persistent_id in self._ids_map.items() if new_id in existing_ids} + + with self._filesystem_ids_path.open("wb") as f: + pickle.dump(self._ids_map, f) + + def get_persistent_id(self, id): + # Markdown doesnt keep a stable id as it's just a text format + # We record ids in a pickle file if they change + # so the map in Syncronizer works as expected + # this would be the first id ever set for an item + try: + return self._ids_map[str(id)] + except KeyError: + return id + def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: """Read all items again from storage.""" all_items = tuple( @@ -55,10 +79,20 @@ def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: for line in self._filesystem_file.contents.splitlines() ) + result = [] + for item in all_items: + if item is None: + continue + item_id = item._id() + persistent_id = self.get_persistent_id(item_id) + if persistent_id != item_id: + item._persistent_id = persistent_id + result.append(item) + logger.opt(lazy=True).debug( f"Found {len(all_items)} matching tasks inside {self._filename_path}" ) - return [i for i in all_items if i] + return result def get_item(self, item_id: ID) -> Optional[MarkdownTaskItem]: item = self._items_cache.get(item_id) @@ -83,8 +117,9 @@ def update_item(self, item_id: ID, **changes): if item.title != changes["title"]: item.title = changes["title"] - logger.warning(f"The item {item_id} has changed its id to {item._id()}") + self._ids_map[str(item._id())] = item_id + item.is_checked = changes["is_checked"] def add_item(self, item: MarkdownTaskItem) -> FilesystemFile: @@ -110,8 +145,4 @@ def items_are_identical( ] if k not in ignore_keys ] - result = SyncSide._items_are_identical(item1, item2, keys) - # if not result: - # import pdb; pdb.set_trace() - return result - # return SyncSide._items_are_identical(item1, item2, keys) + return SyncSide._items_are_identical(item1, item2, keys) From 0c64b9e27784ce627e1034442c06dd6ff7d0e0a3 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Tue, 30 Dec 2025 19:08:36 +0100 Subject: [PATCH 12/15] improve regex code --- syncall/filesystem/markdown_task_item.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/syncall/filesystem/markdown_task_item.py b/syncall/filesystem/markdown_task_item.py index 2be8915..61259b4 100644 --- a/syncall/filesystem/markdown_task_item.py +++ b/syncall/filesystem/markdown_task_item.py @@ -9,14 +9,16 @@ from syncall.filesystem.filesystem_file import FilesystemFile MD_TASK_CHECKBOX_RE = r"\[[ xX]\]" -MD_TASK_LINE_RE = r"-\s*" + MD_TASK_CHECKBOX_RE +MD_TASK_LINE_START_RE = r"-\s*" + MD_TASK_CHECKBOX_RE MD_TASK_SCHEDULED_EMOJI = "⏳" MD_TASK_DUE_EMOJI = "📅" MD_TASK_DONE_EMOJI = "✅" -MD_TASK_SCHEDULED_RE = r"(?<=⏳ )\d{4}-\d{2}-\d{2}" -MD_TASK_DUE_RE = r"(?<=📅 )\d{4}-\d{2}-\d{2}" -MD_TASK_DONE_RE = r"(?<=✅ )\d{4}-\d{2}-\d{2}" +MD_TASK_DATE_RE = r"(?<={EMOJI} )\d{4}-\d{2}-\d{2}" + +MD_TASK_SCHEDULED_RE = MD_TASK_DATE_RE.replace('{EMOJI}', MD_TASK_SCHEDULED_EMOJI) +MD_TASK_DUE_RE = MD_TASK_DATE_RE.replace('{EMOJI}', MD_TASK_DUE_EMOJI) +MD_TASK_DONE_RE = MD_TASK_DATE_RE.replace('{EMOJI}', MD_TASK_DONE_EMOJI) class MarkdownTaskItem(ConcreteItem): """A task line inside a Markdown file.""" @@ -53,10 +55,7 @@ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem": def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "MarkdownTaskItem": """Create a MarkdownTaskItem given the line of text.""" - markdown_task = re.match(MD_TASK_LINE_RE, markdown_text) - due_date = re.search(MD_TASK_DUE_RE, markdown_text) - scheduled_date = re.search(MD_TASK_SCHEDULED_RE, markdown_text) - done_date = re.search(MD_TASK_DONE_RE, markdown_text) + markdown_task = re.match(MD_TASK_LINE_START_RE, markdown_text) if markdown_task is None: return None @@ -73,6 +72,10 @@ def from_markdown(cls, markdown_text: str, markdown_file: FilesystemFile) -> "Ma ) result.last_modified_date = markdown_file.last_modified_date + due_date = re.search(MD_TASK_DUE_RE, markdown_text) + scheduled_date = re.search(MD_TASK_SCHEDULED_RE, markdown_text) + done_date = re.search(MD_TASK_DONE_RE, markdown_text) + if due_date: result.due_date = datetime.datetime.fromisoformat(due_date.group(0)) From bcdcc54d970fe90a0747f7a84eba2e14f56267bd Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Thu, 1 Jan 2026 19:03:27 +0100 Subject: [PATCH 13/15] add readme --- README.md | 5 ++ docs/readme-md-gtasks.md | 101 +++++++++++++++++++++++++++++++++++++++ misc/meme-md-gtasks.png | Bin 0 -> 106228 bytes 3 files changed, 106 insertions(+) create mode 100644 docs/readme-md-gtasks.md create mode 100644 misc/meme-md-gtasks.png diff --git a/README.md b/README.md index 77f4e5f..8049b32 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,11 @@ At the moment the list of supported synchronizations is the following: Taskwarrior ⬄ Generic Caldav server tw-caldav-sync + + README + Obsidian Markdown TaksGoogle Tasks + md-gtasks-sync + README Local Files ⬄ Google Keep Notes diff --git a/docs/readme-md-gtasks.md b/docs/readme-md-gtasks.md new file mode 100644 index 0000000..1399e1c --- /dev/null +++ b/docs/readme-md-gtasks.md @@ -0,0 +1,101 @@ +# [Markdown Obsidian Tasks](https://publish.obsidian.md/tasks/Introduction) ⬄ [Google Tasks](https://support.google.com/tasks/answer/7675772) + +![logo](../misc/meme-md-gtasks.png) + +## Description + +Given all tasks in your Google Task task list and a Markdown file with +Obsidian tasks, synchronise all the addition / +modification / deletion events between them. + +## Motivation + +While Obsidian Tasks is good for taking notes, tracking tasks across projects, +keeping track of project goals etc., lacks the portability, simplicity and +minimalistic design of Google Tasks. The latter also has the following +advantages: + +- Automatic sync across all your devices +- Comfortable addition/modification of events using voice commands +- Actual reminding of events with a variety of mechanisms + +## Usage Examples + +Run the `md_gtasks_sync` to synchronise the Google Tasks list of your choice with +the selected Markdown file. Run with `--help` for the list of options. + +```sh +# Sync the +remindme Taskwarrior tag with the Google Tasks list named "TW Reminders" + +md_gtasks_sync --help +md_gtasks_sync -m tasks.md -l "MD Tasks" +``` + +## Installation + +### Package Installation + +Install the `syncall` package from PyPI, enabling the `google` and `md` +extras: + +```sh +pip3 install syncall[google,tw] +``` + +## Notes re this synchronization + +- Currently subtasks of a Google Tasks item are treated as completely + independent of the parent task when converted to Markdown +- It's not possible to get the time part of the "due" field of a task using the + Google Tasks API. Due to this restriction we currently do currently do sync + the date part (without the time) from Google Tasks to Markdown, but in + order not to remove the time part when doing the inverse synchronization, we + don't sync the date at all from Markdown to Google Tasks. More + information in [this ticket](https://issuetracker.google.com/u/1/issues/128979662) + +
+Overriding Google Tasks API key (not required) + +**This step isn't since the Google Console app of this project is now verified.** + +At the moment the Google Console app that makes use of the Google Tasks API is +still in Testing mode and awaiting approval from Google. This means that if it +raches more than 100 users, the integration may stop working for you. In that +case in order to use this integration you will have to register for your own +developer account with the Google Tasks API with the following steps: + +Firstly, remove the `~/.gtasks_credentials.pickle` file on your system since +that will be reused if found by the app. + +For creating your own Google Cloud Developer App: + +- Go to the [Google Cloud developer console](https://console.cloud.google.com/) +- Make a new project +- From the sidebar go to `API & Services` and once there click the `ENABLE APIS AND SERVICES` button +- Look for and Enable the `Tasks API` + +Your newly created app now has access to the Tasks API. We now have to create +and download the credentials: + +- Again, from the sidebar under `API And Services` click `Credentials` +- In the Google Tasks API screen, click the `CREATE CREDENTIALS` button. +- Select the `User data` radio button (not the `Application data`). +- Fill in the `OAuth Consent Screen` information (shouldn't affect the process) +- Allow the said credentials to access the following scopes: + - `Create, edit, organize, and delete all your tasks` + - `View your tasks` +- Create a new `OAuth Client ID`. Set the type to `Desktop App` (app name is not + important). +- Finally download the credentials in JSON form by clicking the download button + as shown below. This is the file you need to point to when running + `tw_gtasks_sync`. + + ![download-btn](../misc/gcal-json-btn.png) + +To specify your custom credentials JSON file use the `--google-secret` flag as follows: + +```sh +md_gtasks_sync -l "" -m tasks.md --google-secret "" +``` + +
diff --git a/misc/meme-md-gtasks.png b/misc/meme-md-gtasks.png new file mode 100644 index 0000000000000000000000000000000000000000..26b9217021d9813796b0e44a16252afefcd88686 GIT binary patch literal 106228 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 z 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 ~c&q+SKHe4IQL^ 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 literal 0 HcmV?d00001 From fb69e0f52543975b90b156f556f388a833a16e95 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Thu, 1 Jan 2026 19:14:01 +0100 Subject: [PATCH 14/15] add readme for the script --- README.md | 42 +++++++++++++++++++++++++++++++ syncall/scripts/md_gtasks_sync.py | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8049b32..8aef810 100644 --- a/README.md +++ b/README.md @@ -515,6 +515,48 @@ Options: + + +
+ md_gtasks_sync --help + +``` +Usage: md_gtasks_sync [OPTIONS] + + Synchronize lists from your Google Tasks with Obsidian Tasks Markdown file. + + The list of MD tasks can be based on a Markdown file path + while the list in GTasks should be provided by their name. if it doesn't + exist it will be created. + +Options: + -l, --gtasks-list TEXT Name of the Google Tasks list to synchronize + (will be created if not there) + --google-secret FILE Override the client secret used for the + communication with the Google APIs + --oauth-port INTEGER Port to use for OAuth Authentication with + Google Applications + -m, --markdown-file TEXT Name of the Markdown file including tasks + list to synchronize + --list-combinations List the available named TW<->Google Tasks + combinations + --list-resolution-strategies List all the available resolution strategies + and exit + -r, --resolution-strategy [MostRecentRS|LeastRecentRS|AlwaysFirstRS|AlwaysSecondRS] + Resolution strategy to use during conflicts + -b, --combination TEXT Name of an already saved TW<->Google Tasks + combination + -s, --save-as TEXT Save the given TW<->Google Tasks filters + combination using a specified custom name. + --prefer-scheduled-date Prefer using the "scheduled" date field + instead of the "due" date if the former is + available + -v, --verbose + --version Show the version and exit. + --help Show this message and exit. +``` + +
## Mechanics / Automatic synchronization diff --git a/syncall/scripts/md_gtasks_sync.py b/syncall/scripts/md_gtasks_sync.py index 7053576..6dd4048 100644 --- a/syncall/scripts/md_gtasks_sync.py +++ b/syncall/scripts/md_gtasks_sync.py @@ -55,7 +55,7 @@ def main( pdb_on_error: bool, confirm: bool, ): - """Synchronize lists from your Google Tasks with filters from Taskwarrior. + """Synchronize lists from your Google Tasks with Obsidian Tasks Markdown file. The list of MD tasks can be based on a Markdown file path while the list in GTasks should be provided by their name. if it doesn't From 218ad646a9788343794acd210645d7c2378c9118 Mon Sep 17 00:00:00 2001 From: Jose Riego Date: Fri, 2 Jan 2026 20:51:52 +0100 Subject: [PATCH 15/15] process non tasks lines properly --- syncall/filesystem/markdown_tasks_side.py | 57 ++++++++++++++++------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/syncall/filesystem/markdown_tasks_side.py b/syncall/filesystem/markdown_tasks_side.py index 30559c6..eacf54a 100644 --- a/syncall/filesystem/markdown_tasks_side.py +++ b/syncall/filesystem/markdown_tasks_side.py @@ -43,15 +43,41 @@ def __init__(self, markdown_file: Path) -> None: with self._filesystem_ids_path.open("rb") as f: self._ids_map = pickle.load(f) + all_items = self.get_all_items(include_non_tasks=True) + + # dict with items. Ignore lines with no tasks self._items_cache: dict[str, dict] = { - str(item.id): item for item in self.get_all_items() + str(item.id): item for item in all_items if item } + # Array with item ids in the same order found in the .md file + # It will have None in positions with no Markdown tasks + self._items_order = [ str(item.id) if item else None for item in all_items ] + def start(self): pass def finish(self): - contents = '\n'.join(str(i) for i in self._items_cache.values()) + "\n" + contents = "" + # add existing file lines as they are if they are not tasks + # or change them for the tasks in text format when appropriate + for item_id, line in zip(self._items_order, self._filesystem_file.contents.splitlines()): + if item_id: + try: + line_content = str(self.get_item(item_id)) + except KeyError: + continue + else: + line_content = line + + contents += line_content + "\n" + + # so far we've inserted older tasks. add newly synced ones + new_ids = [ item_id for item_id in self._items_cache.keys() if item_id not in self._items_order ] + for item_id in new_ids: + line_content = str(self.get_item(item_id)) + contents += line_content + "\n" + self._filesystem_file.contents = contents self._filesystem_file.flush() @@ -74,23 +100,22 @@ def get_persistent_id(self, id): def get_all_items(self, **kargs) -> Sequence[FilesystemFile]: """Read all items again from storage.""" - all_items = tuple( - MarkdownTaskItem.from_markdown(line, self._filesystem_file) - for line in self._filesystem_file.contents.splitlines() - ) - + """The array will have None in lines with no tasks""" result = [] - for item in all_items: - if item is None: - continue - item_id = item._id() - persistent_id = self.get_persistent_id(item_id) - if persistent_id != item_id: - item._persistent_id = persistent_id - result.append(item) + found_tasks = 0 + for line in self._filesystem_file.contents.splitlines(): + item = MarkdownTaskItem.from_markdown(line, self._filesystem_file) + if item: + found_tasks += 1 + item_id = item._id() + persistent_id = self.get_persistent_id(item_id) + if persistent_id != item_id: + item._persistent_id = persistent_id + if item or kargs.get('include_non_tasks'): + result.append(item) logger.opt(lazy=True).debug( - f"Found {len(all_items)} matching tasks inside {self._filename_path}" + f"Found {found_tasks} matching tasks inside {self._filename_path}" ) return result