diff --git a/README.md b/README.md
index 77f4e5f..8aef810 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 Taks ⬄ Google Tasks |
+ md-gtasks-sync |
+
| README |
Local Files ⬄ Google Keep Notes |
@@ -510,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/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)
+
+
+
+## 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`.
+
+ 
+
+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 0000000..26b9217
Binary files /dev/null and b/misc/meme-md-gtasks.png differ
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"
diff --git a/pyproject.toml b/pyproject.toml
index 1c35133..a02a822 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]
@@ -89,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 50c89df..10d74f4 100644
--- a/syncall/cli.py
+++ b/syncall/cli.py
@@ -357,6 +357,30 @@ def opt_gtasks_list():
)
+# 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",
+ type=str,
+ help="Name of the Markdown file including tasks list to synchronize",
+ )
+
+
# google-related options ----------------------------------------------------------------------
def opt_google_secret_override():
return click.option(
@@ -459,6 +483,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..61259b4
--- /dev/null
+++ b/syncall/filesystem/markdown_task_item.py
@@ -0,0 +1,114 @@
+import datetime
+import re
+import uuid
+
+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
+
+MD_TASK_CHECKBOX_RE = r"\[[ xX]\]"
+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_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."""
+
+ def __init__(self, is_checked: bool = False, title: str = ""):
+ super().__init__(
+ keys=(
+ ItemKey("is_checked", KeyType.String),
+ ItemKey("title", KeyType.String),
+ ItemKey("last_modified_date", KeyType.Date),
+ )
+ )
+
+ self._persistent_id = None
+ 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
+
+ @classmethod
+ def from_raw_item(cls, markdown_raw_item: str) -> "MarkdownTaskItem":
+ """Create a MarkdownTaskItem given the raw item at hand."""
+
+ result = cls(
+ is_checked=markdown_raw_item["is_checked"],
+ title=markdown_raw_item["title"]
+ )
+ return result
+
+ @classmethod
+ 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_START_RE, markdown_text)
+
+ if markdown_task is None:
+ return None
+
+ 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,
+ title=title
+ )
+ 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))
+
+ 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):
+ result = '- [{}] {}'.format(
+ 'X' if self.is_checked else ' ',
+ self.title)
+
+ 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)
+
+ @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
new file mode 100644
index 0000000..eacf54a
--- /dev/null
+++ b/syncall/filesystem/markdown_tasks_side.py
@@ -0,0 +1,173 @@
+import datetime
+import pickle
+import re
+
+from pathlib import Path
+from typing import MutableMapping, Optional, Sequence, cast
+
+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, 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)
+
+ 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 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 = ""
+ # 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()
+
+ # 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."""
+ """The array will have None in lines with no tasks"""
+ result = []
+ 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 {found_tasks} matching tasks inside {self._filename_path}"
+ )
+ return result
+
+ def get_item(self, item_id: ID) -> Optional[MarkdownTaskItem]:
+ item = self._items_cache.get(item_id)
+ return item
+
+ def delete_single_item(self, item_id: ID):
+ try:
+ del self._items_cache[item_id]
+ except Keyerror:
+ logger.warning(f"Requested to delete item {item_id} but item cannot be found.")
+ return
+
+ 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", "is_checked"}.issubset(changes):
+ logger.warning(f"Invalid changes provided to Filesystem Side -> {changes}")
+ return
+
+ 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:
+ item = MarkdownTaskItem.from_raw_item(item)
+ self._items_cache[item.id] = item
+ return item
+
+ @classmethod
+ def items_are_identical(
+ cls, item1: ConcreteItem, item2: ConcreteItem, ignore_keys: Sequence[str] = []
+ ) -> bool:
+ # item1 = item1.copy()
+ # item2 = item2.copy()
+
+ keys = [
+ k
+ for k in [
+ "id",
+ "title",
+ "is_checked",
+ "due_date",
+ "done_date",
+ ]
+ 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
new file mode 100644
index 0000000..6dd4048
--- /dev/null
+++ b/syncall/scripts/md_gtasks_sync.py
@@ -0,0 +1,184 @@
+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.filesystem.markdown_tasks_side import MarkdownTasksSide
+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_markdown,
+ opts_miscellaneous,
+)
+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()
+@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,
+ custom_combination_savename: str,
+ pdb_on_error: bool,
+ confirm: bool,
+):
+ """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.
+ """
+ # 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(
+ [
+ markdown_file,
+ 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
+ )
+ markdown_file = app_config["markdown_file"]
+ 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,
+ "markdown_file": markdown_file,
+ },
+ 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": markdown_file,
+ "Google Tasks": gtasks_list,
+ "Prefer scheduled dates": prefer_scheduled_date,
+ },
+ prefix="\n\n",
+ suffix="\n",
+ )
+ )
+ if confirm:
+ confirm_before_proceeding()
+
+ # initialize sides ------------------------------------------------------------------------
+ md_side = MarkdownTasksSide(
+ markdown_file=markdown_file
+ )
+
+ 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_md_to_gtask(
+ *args,
+ **kargs,
+ set_scheduled_date=prefer_scheduled_date,
+ )
+
+ convert_B_to_A.__doc__ = convert_md_to_gtask.__doc__
+
+ 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__
+
+ # 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=(
+ ("last_modified_date"),
+ (),
+ ),
+ ) as aggregator:
+ aggregator.sync()
+
+ return 0
+
+
+if __name__ == "__main__":
+ main()
diff --git a/syncall/tw_gtasks_utils.py b/syncall/tw_gtasks_utils.py
index f680283..f34d226 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
@@ -33,6 +34,36 @@ def convert_tw_to_gtask(
return gtasks_item
+def convert_md_to_gtask(
+ md_item: Item,
+ set_scheduled_date: bool = False,
+) -> Item:
+ """MD -> GTasks conversion."""
+ assert all(
+ i in md_item.keys() for i in ("title", "is_checked")
+ ), "Missing keys in md_item"
+
+ gtasks_item = {}
+
+ # title
+ gtasks_item["title"] = md_item["title"]
+
+ # status
+ gtasks_item["status"] = "completed" if md_item["is_checked"] else "needsAction"
+
+ # 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
+
def convert_gtask_to_tw(
gtasks_item: GTasksItem,
@@ -94,3 +125,42 @@ 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 -> MD Converter.
+
+ If set_scheduled_date, then it will set the "scheduled" date of the produced TW task
+ instead of the "due" date
+ """
+ status_gtask = gtasks_item["status"]
+
+ # status
+ is_checked = status_gtask == "completed"
+
+ # Description
+ title = gtasks_item["title"]
+
+ md_item: MarkdownTaskItem = MarkdownTaskItem(is_checked, title)
+
+ # due/scheduled date
+ 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.replace(tzinfo=None)
+ else:
+ 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.replace(tzinfo=None)
+
+ # update time
+ if "updated" in gtasks_item.keys():
+ md_item.last_modified_date = parse_google_datetime(gtasks_item["updated"]).replace(tzinfo=None)
+
+ return md_item