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 TaksGoogle 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) + +![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 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