diff --git a/poetry.lock b/poetry.lock index 4fcb93c..61e4aa7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "anyio" @@ -59,21 +59,22 @@ wrapt = [ [[package]] name = "attrs" -version = "23.1.0" +version = "23.2.0" description = "Classes Without Boilerplate" optional = false python-versions = ">=3.7" files = [ - {file = "attrs-23.1.0-py3-none-any.whl", hash = "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04"}, - {file = "attrs-23.1.0.tar.gz", hash = "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] [package.extras] cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] -dev = ["attrs[docs,tests]", "pre-commit"] +dev = ["attrs[tests]", "pre-commit"] docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] tests = ["attrs[tests-no-zope]", "zope-interface"] -tests-no-zope = ["cloudpickle", "hypothesis", "mypy (>=1.1.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] name = "backports-zoneinfo" @@ -570,13 +571,13 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] [[package]] name = "google-api-python-client" -version = "2.111.0" +version = "2.114.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" files = [ - {file = "google-api-python-client-2.111.0.tar.gz", hash = "sha256:3a45a53c031478d1c82c7162dd25c9a965247bca6bd438af0838a9d9b8219405"}, - {file = "google_api_python_client-2.111.0-py2.py3-none-any.whl", hash = "sha256:b605adee2d09a843b97a59925757802904679e44e5599708cedb8939900dfbc7"}, + {file = "google-api-python-client-2.114.0.tar.gz", hash = "sha256:e041bbbf60e682261281e9d64b4660035f04db1cccba19d1d68eebc24d1465ed"}, + {file = "google_api_python_client-2.114.0-py2.py3-none-any.whl", hash = "sha256:690e0bb67d70ff6dea4e8a5d3738639c105a478ac35da153d3b2a384064e9e1a"}, ] [package.dependencies] @@ -604,13 +605,13 @@ typing-extensions = ">=3.10.0" [[package]] name = "google-auth" -version = "2.25.2" +version = "2.26.2" description = "Google Authentication Library" optional = false python-versions = ">=3.7" files = [ - {file = "google-auth-2.25.2.tar.gz", hash = "sha256:42f707937feb4f5e5a39e6c4f343a17300a459aaf03141457ba505812841cc40"}, - {file = "google_auth-2.25.2-py2.py3-none-any.whl", hash = "sha256:473a8dfd0135f75bb79d878436e568f2695dce456764bf3a02b6f8c540b1d256"}, + {file = "google-auth-2.26.2.tar.gz", hash = "sha256:97327dbbf58cccb58fc5a1712bba403ae76668e64814eb30f7316f7e27126b81"}, + {file = "google_auth-2.26.2-py2.py3-none-any.whl", hash = "sha256:3f445c8ce9b61ed6459aad86d8ccdba4a9afed841b2d1451a11ef4db08957424"}, ] [package.dependencies] @@ -862,13 +863,13 @@ bubop = ">=0.1.8,<0.2" [[package]] name = "jsonschema" -version = "4.20.0" +version = "4.21.1" description = "An implementation of JSON Schema validation for Python" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, - {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, + {file = "jsonschema-4.21.1-py3-none-any.whl", hash = "sha256:7996507afae316306f9e2290407761157c6f78002dcf7419acb99822143d1c6f"}, + {file = "jsonschema-4.21.1.tar.gz", hash = "sha256:85727c00279f5fa6bedbe6238d2aa6403bedd8b4864ab11207d07df3cc1b2ee5"}, ] [package.dependencies] @@ -885,13 +886,13 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.11.2" +version = "2023.12.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.8" files = [ - {file = "jsonschema_specifications-2023.11.2-py3-none-any.whl", hash = "sha256:e74ba7c0a65e8cb49dc26837d6cfe576557084a8b423ed16a420984228104f93"}, - {file = "jsonschema_specifications-2023.11.2.tar.gz", hash = "sha256:9472fc4fea474cd74bea4a2b190daeccb5a9e4db2ea80efcf7a1b582fc9a81b8"}, + {file = "jsonschema_specifications-2023.12.1-py3-none-any.whl", hash = "sha256:87e4fdf3a94858b8a2ba2778d9ba57d8a9cafca7c7489c46ba0d30a8bc6a9c3c"}, + {file = "jsonschema_specifications-2023.12.1.tar.gz", hash = "sha256:48a76787b3e70f5ed53f1160d2b81f586e4ca6d1548c5de7085d1682674764cc"}, ] [package.dependencies] @@ -1021,111 +1022,96 @@ dev = ["Sphinx (>=2.2.1)", "black (>=19.10b0)", "codecov (>=2.0.15)", "colorama [[package]] name = "lxml" -version = "4.9.4" +version = "5.1.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" -files = [ - {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e214025e23db238805a600f1f37bf9f9a15413c7bf5f9d6ae194f84980c78722"}, - {file = "lxml-4.9.4-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:ec53a09aee61d45e7dbe7e91252ff0491b6b5fee3d85b2d45b173d8ab453efc1"}, - {file = "lxml-4.9.4-cp27-cp27m-win32.whl", hash = "sha256:7d1d6c9e74c70ddf524e3c09d9dc0522aba9370708c2cb58680ea40174800013"}, - {file = "lxml-4.9.4-cp27-cp27m-win_amd64.whl", hash = "sha256:cb53669442895763e61df5c995f0e8361b61662f26c1b04ee82899c2789c8f69"}, - {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:647bfe88b1997d7ae8d45dabc7c868d8cb0c8412a6e730a7651050b8c7289cf2"}, - {file = "lxml-4.9.4-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:4d973729ce04784906a19108054e1fd476bc85279a403ea1a72fdb051c76fa48"}, - {file = "lxml-4.9.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:056a17eaaf3da87a05523472ae84246f87ac2f29a53306466c22e60282e54ff8"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aaa5c173a26960fe67daa69aa93d6d6a1cd714a6eb13802d4e4bd1d24a530644"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:647459b23594f370c1c01768edaa0ba0959afc39caeeb793b43158bb9bb6a663"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:bdd9abccd0927673cffe601d2c6cdad1c9321bf3437a2f507d6b037ef91ea307"}, - {file = "lxml-4.9.4-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:00e91573183ad273e242db5585b52670eddf92bacad095ce25c1e682da14ed91"}, - {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:a602ed9bd2c7d85bd58592c28e101bd9ff9c718fbde06545a70945ffd5d11868"}, - {file = "lxml-4.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:de362ac8bc962408ad8fae28f3967ce1a262b5d63ab8cefb42662566737f1dc7"}, - {file = "lxml-4.9.4-cp310-cp310-win32.whl", hash = "sha256:33714fcf5af4ff7e70a49731a7cc8fd9ce910b9ac194f66eaa18c3cc0a4c02be"}, - {file = "lxml-4.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:d3caa09e613ece43ac292fbed513a4bce170681a447d25ffcbc1b647d45a39c5"}, - {file = "lxml-4.9.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:359a8b09d712df27849e0bcb62c6a3404e780b274b0b7e4c39a88826d1926c28"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:43498ea734ccdfb92e1886dfedaebeb81178a241d39a79d5351ba2b671bff2b2"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:4855161013dfb2b762e02b3f4d4a21cc7c6aec13c69e3bffbf5022b3e708dd97"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:c71b5b860c5215fdbaa56f715bc218e45a98477f816b46cfde4a84d25b13274e"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9a2b5915c333e4364367140443b59f09feae42184459b913f0f41b9fed55794a"}, - {file = "lxml-4.9.4-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d82411dbf4d3127b6cde7da0f9373e37ad3a43e89ef374965465928f01c2b979"}, - {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:273473d34462ae6e97c0f4e517bd1bf9588aa67a1d47d93f760a1282640e24ac"}, - {file = "lxml-4.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:389d2b2e543b27962990ab529ac6720c3dded588cc6d0f6557eec153305a3622"}, - {file = "lxml-4.9.4-cp311-cp311-win32.whl", hash = "sha256:8aecb5a7f6f7f8fe9cac0bcadd39efaca8bbf8d1bf242e9f175cbe4c925116c3"}, - {file = "lxml-4.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:c7721a3ef41591341388bb2265395ce522aba52f969d33dacd822da8f018aff8"}, - {file = "lxml-4.9.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:dbcb2dc07308453db428a95a4d03259bd8caea97d7f0776842299f2d00c72fc8"}, - {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01bf1df1db327e748dcb152d17389cf6d0a8c5d533ef9bab781e9d5037619229"}, - {file = "lxml-4.9.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e8f9f93a23634cfafbad6e46ad7d09e0f4a25a2400e4a64b1b7b7c0fbaa06d9d"}, - {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3f3f00a9061605725df1816f5713d10cd94636347ed651abdbc75828df302b20"}, - {file = "lxml-4.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:953dd5481bd6252bd480d6ec431f61d7d87fdcbbb71b0d2bdcfc6ae00bb6fb10"}, - {file = "lxml-4.9.4-cp312-cp312-win32.whl", hash = "sha256:266f655d1baff9c47b52f529b5f6bec33f66042f65f7c56adde3fcf2ed62ae8b"}, - {file = "lxml-4.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:f1faee2a831fe249e1bae9cbc68d3cd8a30f7e37851deee4d7962b17c410dd56"}, - {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:23d891e5bdc12e2e506e7d225d6aa929e0a0368c9916c1fddefab88166e98b20"}, - {file = "lxml-4.9.4-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e96a1788f24d03e8d61679f9881a883ecdf9c445a38f9ae3f3f193ab6c591c66"}, - {file = "lxml-4.9.4-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:5557461f83bb7cc718bc9ee1f7156d50e31747e5b38d79cf40f79ab1447afd2d"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:fdb325b7fba1e2c40b9b1db407f85642e32404131c08480dd652110fc908561b"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d74d4a3c4b8f7a1f676cedf8e84bcc57705a6d7925e6daef7a1e54ae543a197"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ac7674d1638df129d9cb4503d20ffc3922bd463c865ef3cb412f2c926108e9a4"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:ddd92e18b783aeb86ad2132d84a4b795fc5ec612e3545c1b687e7747e66e2b53"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bd9ac6e44f2db368ef8986f3989a4cad3de4cd55dbdda536e253000c801bcc7"}, - {file = "lxml-4.9.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:bc354b1393dce46026ab13075f77b30e40b61b1a53e852e99d3cc5dd1af4bc85"}, - {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f836f39678cb47c9541f04d8ed4545719dc31ad850bf1832d6b4171e30d65d23"}, - {file = "lxml-4.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:9c131447768ed7bc05a02553d939e7f0e807e533441901dd504e217b76307745"}, - {file = "lxml-4.9.4-cp36-cp36m-win32.whl", hash = "sha256:bafa65e3acae612a7799ada439bd202403414ebe23f52e5b17f6ffc2eb98c2be"}, - {file = "lxml-4.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:6197c3f3c0b960ad033b9b7d611db11285bb461fc6b802c1dd50d04ad715c225"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:7b378847a09d6bd46047f5f3599cdc64fcb4cc5a5a2dd0a2af610361fbe77b16"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:1343df4e2e6e51182aad12162b23b0a4b3fd77f17527a78c53f0f23573663545"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6dbdacf5752fbd78ccdb434698230c4f0f95df7dd956d5f205b5ed6911a1367c"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:506becdf2ecaebaf7f7995f776394fcc8bd8a78022772de66677c84fb02dd33d"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ca8e44b5ba3edb682ea4e6185b49661fc22b230cf811b9c13963c9f982d1d964"}, - {file = "lxml-4.9.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:9d9d5726474cbbef279fd709008f91a49c4f758bec9c062dfbba88eab00e3ff9"}, - {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:bbdd69e20fe2943b51e2841fc1e6a3c1de460d630f65bde12452d8c97209464d"}, - {file = "lxml-4.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8671622256a0859f5089cbe0ce4693c2af407bc053dcc99aadff7f5310b4aa02"}, - {file = "lxml-4.9.4-cp37-cp37m-win32.whl", hash = "sha256:dd4fda67f5faaef4f9ee5383435048ee3e11ad996901225ad7615bc92245bc8e"}, - {file = "lxml-4.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:6bee9c2e501d835f91460b2c904bc359f8433e96799f5c2ff20feebd9bb1e590"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:1f10f250430a4caf84115b1e0f23f3615566ca2369d1962f82bef40dd99cd81a"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3b505f2bbff50d261176e67be24e8909e54b5d9d08b12d4946344066d66b3e43"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:1449f9451cd53e0fd0a7ec2ff5ede4686add13ac7a7bfa6988ff6d75cff3ebe2"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:4ece9cca4cd1c8ba889bfa67eae7f21d0d1a2e715b4d5045395113361e8c533d"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59bb5979f9941c61e907ee571732219fa4774d5a18f3fa5ff2df963f5dfaa6bc"}, - {file = "lxml-4.9.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:b1980dbcaad634fe78e710c8587383e6e3f61dbe146bcbfd13a9c8ab2d7b1192"}, - {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9ae6c3363261021144121427b1552b29e7b59de9d6a75bf51e03bc072efb3c37"}, - {file = "lxml-4.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bcee502c649fa6351b44bb014b98c09cb00982a475a1912a9881ca28ab4f9cd9"}, - {file = "lxml-4.9.4-cp38-cp38-win32.whl", hash = "sha256:a8edae5253efa75c2fc79a90068fe540b197d1c7ab5803b800fccfe240eed33c"}, - {file = "lxml-4.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:701847a7aaefef121c5c0d855b2affa5f9bd45196ef00266724a80e439220e46"}, - {file = "lxml-4.9.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:f610d980e3fccf4394ab3806de6065682982f3d27c12d4ce3ee46a8183d64a6a"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:aa9b5abd07f71b081a33115d9758ef6077924082055005808f68feccb27616bd"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:365005e8b0718ea6d64b374423e870648ab47c3a905356ab6e5a5ff03962b9a9"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:16b9ec51cc2feab009e800f2c6327338d6ee4e752c76e95a35c4465e80390ccd"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:a905affe76f1802edcac554e3ccf68188bea16546071d7583fb1b693f9cf756b"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fd814847901df6e8de13ce69b84c31fc9b3fb591224d6762d0b256d510cbf382"}, - {file = "lxml-4.9.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:91bbf398ac8bb7d65a5a52127407c05f75a18d7015a270fdd94bbcb04e65d573"}, - {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f99768232f036b4776ce419d3244a04fe83784bce871b16d2c2e984c7fcea847"}, - {file = "lxml-4.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bb5bd6212eb0edfd1e8f254585290ea1dadc3687dd8fd5e2fd9a87c31915cdab"}, - {file = "lxml-4.9.4-cp39-cp39-win32.whl", hash = "sha256:88f7c383071981c74ec1998ba9b437659e4fd02a3c4a4d3efc16774eb108d0ec"}, - {file = "lxml-4.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:936e8880cc00f839aa4173f94466a8406a96ddce814651075f95837316369899"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:f6c35b2f87c004270fa2e703b872fcc984d714d430b305145c39d53074e1ffe0"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:606d445feeb0856c2b424405236a01c71af7c97e5fe42fbc778634faef2b47e4"}, - {file = "lxml-4.9.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a1bdcbebd4e13446a14de4dd1825f1e778e099f17f79718b4aeaf2403624b0f7"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0a08c89b23117049ba171bf51d2f9c5f3abf507d65d016d6e0fa2f37e18c0fc5"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:232fd30903d3123be4c435fb5159938c6225ee8607b635a4d3fca847003134ba"}, - {file = "lxml-4.9.4-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:231142459d32779b209aa4b4d460b175cadd604fed856f25c1571a9d78114771"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:520486f27f1d4ce9654154b4494cf9307b495527f3a2908ad4cb48e4f7ed7ef7"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:562778586949be7e0d7435fcb24aca4810913771f845d99145a6cee64d5b67ca"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a9e7c6d89c77bb2770c9491d988f26a4b161d05c8ca58f63fb1f1b6b9a74be45"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:786d6b57026e7e04d184313c1359ac3d68002c33e4b1042ca58c362f1d09ff58"}, - {file = "lxml-4.9.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:95ae6c5a196e2f239150aa4a479967351df7f44800c93e5a975ec726fef005e2"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:9b556596c49fa1232b0fff4b0e69b9d4083a502e60e404b44341e2f8fb7187f5"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:cc02c06e9e320869d7d1bd323df6dd4281e78ac2e7f8526835d3d48c69060683"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:857d6565f9aa3464764c2cb6a2e3c2e75e1970e877c188f4aeae45954a314e0c"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c42ae7e010d7d6bc51875d768110c10e8a59494855c3d4c348b068f5fb81fdcd"}, - {file = "lxml-4.9.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:f10250bb190fb0742e3e1958dd5c100524c2cc5096c67c8da51233f7448dc137"}, - {file = "lxml-4.9.4.tar.gz", hash = "sha256:b1541e50b78e15fa06a2670157a1962ef06591d4c998b998047fff5e3236880e"}, +python-versions = ">=3.6" +files = [ + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:704f5572ff473a5f897745abebc6df40f22d4133c1e0a1f124e4f2bd3330ff7e"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9d3c0f8567ffe7502d969c2c1b809892dc793b5d0665f602aad19895f8d508da"}, + {file = "lxml-5.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5fcfbebdb0c5d8d18b84118842f31965d59ee3e66996ac842e21f957eb76138c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f37c6d7106a9d6f0708d4e164b707037b7380fcd0b04c5bd9cae1fb46a856fb"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2befa20a13f1a75c751f47e00929fb3433d67eb9923c2c0b364de449121f447c"}, + {file = "lxml-5.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22b7ee4c35f374e2c20337a95502057964d7e35b996b1c667b5c65c567d2252a"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bf8443781533b8d37b295016a4b53c1494fa9a03573c09ca5104550c138d5c05"}, + {file = "lxml-5.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:82bddf0e72cb2af3cbba7cec1d2fd11fda0de6be8f4492223d4a268713ef2147"}, + {file = "lxml-5.1.0-cp310-cp310-win32.whl", hash = "sha256:b66aa6357b265670bb574f050ffceefb98549c721cf28351b748be1ef9577d93"}, + {file = "lxml-5.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:4946e7f59b7b6a9e27bef34422f645e9a368cb2be11bf1ef3cafc39a1f6ba68d"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"}, + {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"}, + {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"}, + {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"}, + {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"}, + {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"}, + {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"}, + {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"}, + {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"}, + {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"}, + {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"}, + {file = "lxml-5.1.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:24ef5a4631c0b6cceaf2dbca21687e29725b7c4e171f33a8f8ce23c12558ded1"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d2900b7f5318bc7ad8631d3d40190b95ef2aa8cc59473b73b294e4a55e9f30f"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:601f4a75797d7a770daed8b42b97cd1bb1ba18bd51a9382077a6a247a12aa38d"}, + {file = "lxml-5.1.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4b68c961b5cc402cbd99cca5eb2547e46ce77260eb705f4d117fd9c3f932b95"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:afd825e30f8d1f521713a5669b63657bcfe5980a916c95855060048b88e1adb7"}, + {file = "lxml-5.1.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:262bc5f512a66b527d026518507e78c2f9c2bd9eb5c8aeeb9f0eb43fcb69dc67"}, + {file = "lxml-5.1.0-cp36-cp36m-win32.whl", hash = "sha256:e856c1c7255c739434489ec9c8aa9cdf5179785d10ff20add308b5d673bed5cd"}, + {file = "lxml-5.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:c7257171bb8d4432fe9d6fdde4d55fdbe663a63636a17f7f9aaba9bcb3153ad7"}, + {file = "lxml-5.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b9e240ae0ba96477682aa87899d94ddec1cc7926f9df29b1dd57b39e797d5ab5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a96f02ba1bcd330807fc060ed91d1f7a20853da6dd449e5da4b09bfcc08fdcf5"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e3898ae2b58eeafedfe99e542a17859017d72d7f6a63de0f04f99c2cb125936"}, + {file = "lxml-5.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61c5a7edbd7c695e54fca029ceb351fc45cd8860119a0f83e48be44e1c464862"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:3aeca824b38ca78d9ee2ab82bd9883083d0492d9d17df065ba3b94e88e4d7ee6"}, + {file = "lxml-5.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8f52fe6859b9db71ee609b0c0a70fea5f1e71c3462ecf144ca800d3f434f0764"}, + {file = "lxml-5.1.0-cp37-cp37m-win32.whl", hash = "sha256:d42e3a3fc18acc88b838efded0e6ec3edf3e328a58c68fbd36a7263a874906c8"}, + {file = "lxml-5.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:eac68f96539b32fce2c9b47eb7c25bb2582bdaf1bbb360d25f564ee9e04c542b"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ae15347a88cf8af0949a9872b57a320d2605ae069bcdf047677318bc0bba45b1"}, + {file = "lxml-5.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c26aab6ea9c54d3bed716b8851c8bfc40cb249b8e9880e250d1eddde9f709bf5"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:342e95bddec3a698ac24378d61996b3ee5ba9acfeb253986002ac53c9a5f6f84"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:725e171e0b99a66ec8605ac77fa12239dbe061482ac854d25720e2294652eeaa"}, + {file = "lxml-5.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d184e0d5c918cff04cdde9dbdf9600e960161d773666958c9d7b565ccc60c45"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:98f3f020a2b736566c707c8e034945c02aa94e124c24f77ca097c446f81b01f1"}, + {file = "lxml-5.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6d48fc57e7c1e3df57be5ae8614bab6d4e7b60f65c5457915c26892c41afc59e"}, + {file = "lxml-5.1.0-cp38-cp38-win32.whl", hash = "sha256:7ec465e6549ed97e9f1e5ed51c657c9ede767bc1c11552f7f4d022c4df4a977a"}, + {file = "lxml-5.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:b21b4031b53d25b0858d4e124f2f9131ffc1530431c6d1321805c90da78388d1"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:52427a7eadc98f9e62cb1368a5079ae826f94f05755d2d567d93ee1bc3ceb354"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6a2a2c724d97c1eb8cf966b16ca2915566a4904b9aad2ed9a09c748ffe14f969"}, + {file = "lxml-5.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:843b9c835580d52828d8f69ea4302537337a21e6b4f1ec711a52241ba4a824f3"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b99f564659cfa704a2dd82d0684207b1aadf7d02d33e54845f9fc78e06b7581"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8b0c78e7aac24979ef09b7f50da871c2de2def043d468c4b41f512d831e912"}, + {file = "lxml-5.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9bcf86dfc8ff3e992fed847c077bd875d9e0ba2fa25d859c3a0f0f76f07f0c8d"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:49a9b4af45e8b925e1cd6f3b15bbba2c81e7dba6dce170c677c9cda547411e14"}, + {file = "lxml-5.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:280f3edf15c2a967d923bcfb1f8f15337ad36f93525828b40a0f9d6c2ad24890"}, + {file = "lxml-5.1.0-cp39-cp39-win32.whl", hash = "sha256:ed7326563024b6e91fef6b6c7a1a2ff0a71b97793ac33dbbcf38f6005e51ff6e"}, + {file = "lxml-5.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:8d7b4beebb178e9183138f552238f7e6613162a42164233e2bda00cb3afac58f"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"}, + {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"}, + {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"}, + {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"}, + {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"}, + {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (==0.29.37)"] +source = ["Cython (>=3.0.7)"] [[package]] name = "mccabe" @@ -1359,22 +1345,22 @@ virtualenv = ">=20.10.0" [[package]] name = "protobuf" -version = "4.25.1" +version = "4.25.2" description = "" optional = false python-versions = ">=3.8" files = [ - {file = "protobuf-4.25.1-cp310-abi3-win32.whl", hash = "sha256:193f50a6ab78a970c9b4f148e7c750cfde64f59815e86f686c22e26b4fe01ce7"}, - {file = "protobuf-4.25.1-cp310-abi3-win_amd64.whl", hash = "sha256:3497c1af9f2526962f09329fd61a36566305e6c72da2590ae0d7d1322818843b"}, - {file = "protobuf-4.25.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:0bf384e75b92c42830c0a679b0cd4d6e2b36ae0cf3dbb1e1dfdda48a244f4bcd"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:0f881b589ff449bf0b931a711926e9ddaad3b35089cc039ce1af50b21a4ae8cb"}, - {file = "protobuf-4.25.1-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:ca37bf6a6d0046272c152eea90d2e4ef34593aaa32e8873fc14c16440f22d4b7"}, - {file = "protobuf-4.25.1-cp38-cp38-win32.whl", hash = "sha256:abc0525ae2689a8000837729eef7883b9391cd6aa7950249dcf5a4ede230d5dd"}, - {file = "protobuf-4.25.1-cp38-cp38-win_amd64.whl", hash = "sha256:1484f9e692091450e7edf418c939e15bfc8fc68856e36ce399aed6889dae8bb0"}, - {file = "protobuf-4.25.1-cp39-cp39-win32.whl", hash = "sha256:8bdbeaddaac52d15c6dce38c71b03038ef7772b977847eb6d374fc86636fa510"}, - {file = "protobuf-4.25.1-cp39-cp39-win_amd64.whl", hash = "sha256:becc576b7e6b553d22cbdf418686ee4daa443d7217999125c045ad56322dda10"}, - {file = "protobuf-4.25.1-py3-none-any.whl", hash = "sha256:a19731d5e83ae4737bb2a089605e636077ac001d18781b3cf489b9546c7c80d6"}, - {file = "protobuf-4.25.1.tar.gz", hash = "sha256:57d65074b4f5baa4ab5da1605c02be90ac20c8b40fb137d6a8df9f416b0d0ce2"}, + {file = "protobuf-4.25.2-cp310-abi3-win32.whl", hash = "sha256:b50c949608682b12efb0b2717f53256f03636af5f60ac0c1d900df6213910fd6"}, + {file = "protobuf-4.25.2-cp310-abi3-win_amd64.whl", hash = "sha256:8f62574857ee1de9f770baf04dde4165e30b15ad97ba03ceac65f760ff018ac9"}, + {file = "protobuf-4.25.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2db9f8fa64fbdcdc93767d3cf81e0f2aef176284071507e3ede160811502fd3d"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:10894a2885b7175d3984f2be8d9850712c57d5e7587a2410720af8be56cdaf62"}, + {file = "protobuf-4.25.2-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fc381d1dd0516343f1440019cedf08a7405f791cd49eef4ae1ea06520bc1c020"}, + {file = "protobuf-4.25.2-cp38-cp38-win32.whl", hash = "sha256:33a1aeef4b1927431d1be780e87b641e322b88d654203a9e9d93f218ee359e61"}, + {file = "protobuf-4.25.2-cp38-cp38-win_amd64.whl", hash = "sha256:47f3de503fe7c1245f6f03bea7e8d3ec11c6c4a2ea9ef910e3221c8a15516d62"}, + {file = "protobuf-4.25.2-cp39-cp39-win32.whl", hash = "sha256:5e5c933b4c30a988b52e0b7c02641760a5ba046edc5e43d3b94a74c9fc57c1b3"}, + {file = "protobuf-4.25.2-cp39-cp39-win_amd64.whl", hash = "sha256:d66a769b8d687df9024f2985d5137a337f957a0916cf5464d1513eee96a63ff0"}, + {file = "protobuf-4.25.2-py3-none-any.whl", hash = "sha256:a8b7a98d4ce823303145bf3c1a8bdb0f2f4642a414b196f04ad9853ed0c8f830"}, + {file = "protobuf-4.25.2.tar.gz", hash = "sha256:fe599e175cb347efc8ee524bcd4b902d11f7262c0e569ececcb89995c15f0a5e"}, ] [[package]] @@ -1433,43 +1419,43 @@ files = [ [[package]] name = "pycryptodomex" -version = "3.19.0" +version = "3.20.0" description = "Cryptographic library for Python" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ - {file = "pycryptodomex-3.19.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff64fd720def623bf64d8776f8d0deada1cc1bf1ec3c1f9d6f5bb5bd098d034f"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:61056a1fd3254f6f863de94c233b30dd33bc02f8c935b2000269705f1eeeffa4"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:258c4233a3fe5a6341780306a36c6fb072ef38ce676a6d41eec3e591347919e8"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e45bb4635b3c4e0a00ca9df75ef6295838c85c2ac44ad882410cb631ed1eeaa"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:a12144d785518f6491ad334c75ccdc6ad52ea49230b4237f319dbb7cef26f464"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-win32.whl", hash = "sha256:1789d89f61f70a4cd5483d4dfa8df7032efab1118f8b9894faae03c967707865"}, - {file = "pycryptodomex-3.19.0-cp27-cp27m-win_amd64.whl", hash = "sha256:eb2fc0ec241bf5e5ef56c8fbec4a2634d631e4c4f616a59b567947a0f35ad83c"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:c9a68a2f7bd091ccea54ad3be3e9d65eded813e6d79fdf4cc3604e26cdd6384f"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:8df69e41f7e7015a90b94d1096ec3d8e0182e73449487306709ec27379fff761"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:917033016ecc23c8933205585a0ab73e20020fdf671b7cd1be788a5c4039840b"}, - {file = "pycryptodomex-3.19.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:e8e5ecbd4da4157889fce8ba49da74764dd86c891410bfd6b24969fa46edda51"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:a77b79852175064c822b047fee7cf5a1f434f06ad075cc9986aa1c19a0c53eb0"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:5b883e1439ab63af976656446fb4839d566bb096f15fc3c06b5a99cde4927188"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3866d68e2fc345162b1b9b83ef80686acfe5cec0d134337f3b03950a0a8bf56"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c74eb1f73f788facece7979ce91594dc177e1a9b5d5e3e64697dd58299e5cb4d"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7cb51096a6a8d400724104db8a7e4f2206041a1f23e58924aa3d8d96bcb48338"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a588a1cb7781da9d5e1c84affd98c32aff9c89771eac8eaa659d2760666f7139"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:d4dd3b381ff5a5907a3eb98f5f6d32c64d319a840278ceea1dcfcc65063856f3"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:263de9a96d2fcbc9f5bd3a279f14ea0d5f072adb68ebd324987576ec25da084d"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-win32.whl", hash = "sha256:67c8eb79ab33d0fbcb56842992298ddb56eb6505a72369c20f60bc1d2b6fb002"}, - {file = "pycryptodomex-3.19.0-cp35-abi3-win_amd64.whl", hash = "sha256:09c9401dc06fb3d94cb1ec23b4ea067a25d1f4c6b7b118ff5631d0b5daaab3cc"}, - {file = "pycryptodomex-3.19.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:edbe083c299835de7e02c8aa0885cb904a75087d35e7bab75ebe5ed336e8c3e2"}, - {file = "pycryptodomex-3.19.0-pp27-pypy_73-win32.whl", hash = "sha256:136b284e9246b4ccf4f752d435c80f2c44fc2321c198505de1d43a95a3453b3c"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5d73e9fa3fe830e7b6b42afc49d8329b07a049a47d12e0ef9225f2fd220f19b2"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b2f1982c5bc311f0aab8c293524b861b485d76f7c9ab2c3ac9a25b6f7655975"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb040b5dda1dff1e197d2ef71927bd6b8bfcb9793bc4dfe0bb6df1e691eaacb"}, - {file = "pycryptodomex-3.19.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:800a2b05cfb83654df80266692f7092eeefe2a314fa7901dcefab255934faeec"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c01678aee8ac0c1a461cbc38ad496f953f9efcb1fa19f5637cbeba7544792a53"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2126bc54beccbede6eade00e647106b4f4c21e5201d2b0a73e9e816a01c50905"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b801216c48c0886742abf286a9a6b117e248ca144d8ceec1f931ce2dd0c9cb40"}, - {file = "pycryptodomex-3.19.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:50cb18d4dd87571006fd2447ccec85e6cec0136632a550aa29226ba075c80644"}, - {file = "pycryptodomex-3.19.0.tar.gz", hash = "sha256:af83a554b3f077564229865c45af0791be008ac6469ef0098152139e6bd4b5b6"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:645bd4ca6f543685d643dadf6a856cc382b654cc923460e3a10a49c1b3832aeb"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ff5c9a67f8a4fba4aed887216e32cbc48f2a6fb2673bb10a99e43be463e15913"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8ee606964553c1a0bc74057dd8782a37d1c2bc0f01b83193b6f8bb14523b877b"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7805830e0c56d88f4d491fa5ac640dfc894c5ec570d1ece6ed1546e9df2e98d6"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:bc3ee1b4d97081260d92ae813a83de4d2653206967c4a0a017580f8b9548ddbc"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win32.whl", hash = "sha256:8af1a451ff9e123d0d8bd5d5e60f8e3315c3a64f3cdd6bc853e26090e195cdc8"}, + {file = "pycryptodomex-3.20.0-cp27-cp27m-win_amd64.whl", hash = "sha256:cbe71b6712429650e3883dc81286edb94c328ffcd24849accac0a4dbcc76958a"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:76bd15bb65c14900d98835fcd10f59e5e0435077431d3a394b60b15864fddd64"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:653b29b0819605fe0898829c8ad6400a6ccde096146730c2da54eede9b7b8baa"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a5ec91388984909bb5398ea49ee61b68ecb579123694bffa172c3b0a107079"}, + {file = "pycryptodomex-3.20.0-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:108e5f1c1cd70ffce0b68739c75734437c919d2eaec8e85bffc2c8b4d2794305"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:59af01efb011b0e8b686ba7758d59cf4a8263f9ad35911bfe3f416cee4f5c08c"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:82ee7696ed8eb9a82c7037f32ba9b7c59e51dda6f105b39f043b6ef293989cb3"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91852d4480a4537d169c29a9d104dda44094c78f1f5b67bca76c29a91042b623"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca649483d5ed251d06daf25957f802e44e6bb6df2e8f218ae71968ff8f8edc4"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e186342cfcc3aafaad565cbd496060e5a614b441cacc3995ef0091115c1f6c5"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:25cd61e846aaab76d5791d006497134602a9e451e954833018161befc3b5b9ed"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:9c682436c359b5ada67e882fec34689726a09c461efd75b6ea77b2403d5665b7"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:7a7a8f33a1f1fb762ede6cc9cbab8f2a9ba13b196bfaf7bc6f0b39d2ba315a43"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win32.whl", hash = "sha256:c39778fd0548d78917b61f03c1fa8bfda6cfcf98c767decf360945fe6f97461e"}, + {file = "pycryptodomex-3.20.0-cp35-abi3-win_amd64.whl", hash = "sha256:2a47bcc478741b71273b917232f521fd5704ab4b25d301669879e7273d3586cc"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:1be97461c439a6af4fe1cf8bf6ca5936d3db252737d2f379cc6b2e394e12a458"}, + {file = "pycryptodomex-3.20.0-pp27-pypy_73-win32.whl", hash = "sha256:19764605feea0df966445d46533729b645033f134baeb3ea26ad518c9fdf212c"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:f2e497413560e03421484189a6b65e33fe800d3bd75590e6d78d4dfdb7accf3b"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48217c7901edd95f9f097feaa0388da215ed14ce2ece803d3f300b4e694abea"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d00fe8596e1cc46b44bf3907354e9377aa030ec4cd04afbbf6e899fc1e2a7781"}, + {file = "pycryptodomex-3.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:88afd7a3af7ddddd42c2deda43d53d3dfc016c11327d0915f90ca34ebda91499"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d3584623e68a5064a04748fb6d76117a21a7cb5eaba20608a41c7d0c61721794"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0daad007b685db36d977f9de73f61f8da2a7104e20aca3effd30752fd56f73e1"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5dcac11031a71348faaed1f403a0debd56bf5404232284cf8c761ff918886ebc"}, + {file = "pycryptodomex-3.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:69138068268127cd605e03438312d8f271135a33140e2742b417d027a0539427"}, + {file = "pycryptodomex-3.20.0.tar.gz", hash = "sha256:7a710b79baddd65b806402e14766c721aee8fb83381769c27920f26476276c1e"}, ] [[package]] @@ -1563,13 +1549,13 @@ files = [ [[package]] name = "pyright" -version = "1.1.342" +version = "1.1.347" description = "Command line wrapper for pyright" optional = false python-versions = ">=3.7" files = [ - {file = "pyright-1.1.342-py3-none-any.whl", hash = "sha256:9a7b95b3a2a90ef7b4297221173956f7f2db2a6cf4f0c7493f2a2349d38305fc"}, - {file = "pyright-1.1.342.tar.gz", hash = "sha256:c8e9785b9080c1aaf2a2efad706249ec65e15abd5f542ee455956acfd404d273"}, + {file = "pyright-1.1.347-py3-none-any.whl", hash = "sha256:14dd31b594aa3ec464894f66b8a2d206ebef1501e52789eb88cf2a79b0907fbe"}, + {file = "pyright-1.1.347.tar.gz", hash = "sha256:17ea09322f60080f82abc4e622e43d1a5ebaa407ba86963b15b2bc01cca256e0"}, ] [package.dependencies] @@ -1581,13 +1567,13 @@ dev = ["twine (>=3.4.1)"] [[package]] name = "pytest" -version = "7.4.3" +version = "7.4.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.4.3-py3-none-any.whl", hash = "sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac"}, - {file = "pytest-7.4.3.tar.gz", hash = "sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5"}, + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, ] [package.dependencies] @@ -1650,13 +1636,13 @@ files = [ [[package]] name = "recurring-ical-events" -version = "2.1.1" +version = "2.1.2" description = "A Python module which repeats ICalendar events by RRULE, RDATE and EXDATE." optional = true python-versions = "*" files = [ - {file = "recurring_ical_events-2.1.1-py3-none-any.whl", hash = "sha256:4062b95782ed74a2649b68c58ec344d8209d7bfd74510db791345f52dfd3714e"}, - {file = "recurring_ical_events-2.1.1.tar.gz", hash = "sha256:8a762d54145d8b70754725935721e62311b3c0949fca0eb1fba3090900a26402"}, + {file = "recurring_ical_events-2.1.2-py3-none-any.whl", hash = "sha256:b510d6d46e54381df1d6b35fcf2a727b71ecc510c85701b15c57312a5f172eff"}, + {file = "recurring_ical_events-2.1.2.tar.gz", hash = "sha256:803d14a93bc2f94bbac03c7010aee98648f73246ef65d1f3c9bc8da88af6bd24"}, ] [package.dependencies] @@ -1667,13 +1653,13 @@ x-wr-timezone = ">=0.0.5,<1.0.0" [[package]] name = "referencing" -version = "0.32.0" +version = "0.32.1" description = "JSON Referencing + Python" optional = false python-versions = ">=3.8" files = [ - {file = "referencing-0.32.0-py3-none-any.whl", hash = "sha256:bdcd3efb936f82ff86f993093f6da7435c7de69a3b3a5a06678a6050184bee99"}, - {file = "referencing-0.32.0.tar.gz", hash = "sha256:689e64fe121843dcfd57b71933318ef1f91188ffb45367332700a86ac8fd6161"}, + {file = "referencing-0.32.1-py3-none-any.whl", hash = "sha256:7e4dc12271d8e15612bfe35792f5ea1c40970dadf8624602e33db2758f7ee554"}, + {file = "referencing-0.32.1.tar.gz", hash = "sha256:3c57da0513e9563eb7e203ebe9bb3a1b509b042016433bd1e45a2853466c3dd3"}, ] [package.dependencies] @@ -1732,110 +1718,110 @@ files = [ [[package]] name = "rpds-py" -version = "0.15.2" +version = "0.17.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:337a8653fb11d2fbe7157c961cc78cb3c161d98cf44410ace9a3dc2db4fad882"}, - {file = "rpds_py-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:813a65f95bfcb7c8f2a70dd6add9b51e9accc3bdb3e03d0ff7a9e6a2d3e174bf"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:082e0e55d73690ffb4da4352d1b5bbe1b5c6034eb9dc8c91aa2a3ee15f70d3e2"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5595c80dd03d7e6c6afb73f3594bf3379a7d79fa57164b591d012d4b71d6ac4c"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fb10bb720348fe1647a94eb605accb9ef6a9b1875d8845f9e763d9d71a706387"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53304cc14b1d94487d70086e1cb0cb4c29ec6da994d58ae84a4d7e78c6a6d04d"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d64a657de7aae8db2da60dc0c9e4638a0c3893b4d60101fd564a3362b2bfeb34"}, - {file = "rpds_py-0.15.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee40206d1d6e95eaa2b7b919195e3689a5cf6ded730632de7f187f35a1b6052c"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1607cda6129f815493a3c184492acb5ae4aa6ed61d3a1b3663aa9824ed26f7ac"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f3e6e2e502c4043c52a99316d89dc49f416acda5b0c6886e0dd8ea7bb35859e8"}, - {file = "rpds_py-0.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:044f6f46d62444800402851afa3c3ae50141f12013060c1a3a0677e013310d6d"}, - {file = "rpds_py-0.15.2-cp310-none-win32.whl", hash = "sha256:c827a931c6b57f50f1bb5de400dcfb00bad8117e3753e80b96adb72d9d811514"}, - {file = "rpds_py-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3bbc89ce2a219662ea142f0abcf8d43f04a41d5b1880be17a794c39f0d609cb0"}, - {file = "rpds_py-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:1fd0f0b1ccd7d537b858a56355a250108df692102e08aa2036e1a094fd78b2dc"}, - {file = "rpds_py-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b414ef79f1f06fb90b5165db8aef77512c1a5e3ed1b4807da8476b7e2c853283"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c31272c674f725dfe0f343d73b0abe8c878c646967ec1c6106122faae1efc15b"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6945c2d61c42bb7e818677f43638675b8c1c43e858b67a96df3eb2426a86c9d"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02744236ac1895d7be837878e707a5c35fb8edc5137602f253b63623d7ad5c8c"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2181e86d4e1cdf49a7320cb72a36c45efcb7670d0a88f09fd2d3a7967c0540fd"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8ff8e809da81363bffca2b965cb6e4bf6056b495fc3f078467d1f8266fe27f"}, - {file = "rpds_py-0.15.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:97532802f14d383f37d603a56e226909f825a83ff298dc1b6697de00d2243999"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:13716e53627ad97babf72ac9e01cf9a7d4af2f75dd5ed7b323a7a9520e948282"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1f295a5c28cfa74a7d48c95acc1c8a7acd49d7d9072040d4b694fe11cd7166"}, - {file = "rpds_py-0.15.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8ec464f20fe803ae00419bd1610934e3bda963aeba1e6181dfc9033dc7e8940c"}, - {file = "rpds_py-0.15.2-cp311-none-win32.whl", hash = "sha256:b61d5096e75fd71018b25da50b82dd70ec39b5e15bb2134daf7eb7bbbc103644"}, - {file = "rpds_py-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:9d41ebb471a6f064c0d1c873c4f7dded733d16ca5db7d551fb04ff3805d87802"}, - {file = "rpds_py-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:13ff62d3561a23c17341b4afc78e8fcfd799ab67c0b1ca32091d71383a98ba4b"}, - {file = "rpds_py-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b70b45a40ad0798b69748b34d508259ef2bdc84fb2aad4048bc7c9cafb68ddb3"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4ecbba7efd82bd2a4bb88aab7f984eb5470991c1347bdd1f35fb34ea28dba6e"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9d38494a8d21c246c535b41ecdb2d562c4b933cf3d68de03e8bc43a0d41be652"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:13152dfe7d7c27c40df8b99ac6aab12b978b546716e99f67e8a67a1d441acbc3"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:164fcee32f15d04d61568c9cb0d919e37ff3195919cd604039ff3053ada0461b"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a5122b17a4faf5d7a6d91fa67b479736c0cacc7afe791ddebb7163a8550b799"}, - {file = "rpds_py-0.15.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:46b4f3d47d1033db569173be62365fbf7808c2bd3fb742314d251f130d90d44c"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c61e42b4ceb9759727045765e87d51c1bb9f89987aca1fcc8a040232138cad1c"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d2aa3ca9552f83b0b4fa6ca8c6ce08da6580f37e3e0ab7afac73a1cfdc230c0e"}, - {file = "rpds_py-0.15.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ec19e823b4ccd87bd69e990879acbce9e961fc7aebe150156b8f4418d4b27b7f"}, - {file = "rpds_py-0.15.2-cp312-none-win32.whl", hash = "sha256:afeabb382c1256a7477b739820bce7fe782bb807d82927102cee73e79b41b38b"}, - {file = "rpds_py-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:422b0901878a31ef167435c5ad46560362891816a76cc0d150683f3868a6f0d1"}, - {file = "rpds_py-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:baf744e5f9d5ee6531deea443be78b36ed1cd36c65a0b95ea4e8d69fa0102268"}, - {file = "rpds_py-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7e072f5da38d6428ba1fc1115d3cc0dae895df671cb04c70c019985e8c7606be"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f138f550b83554f5b344d6be35d3ed59348510edc3cb96f75309db6e9bfe8210"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2a4cd924d0e2f4b1a68034abe4cadc73d69ad5f4cf02db6481c0d4d749f548f"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5eb05b654a41e0f81ab27a7c3e88b6590425eb3e934e1d533ecec5dc88a6ffff"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ee066a64f0d2ba45391cac15b3a70dcb549e968a117bd0500634754cfe0e5fc"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c51a899792ee2c696072791e56b2020caff58b275abecbc9ae0cb71af0645c95"}, - {file = "rpds_py-0.15.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac2ac84a4950d627d84b61f082eba61314373cfab4b3c264b62efab02ababe83"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:62b292fff4739c6be89e6a0240c02bda5a9066a339d90ab191cf66e9fdbdc193"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:98ee201a52a7f65608e5494518932e1473fd43535f12cade0a1b4ab32737fe28"}, - {file = "rpds_py-0.15.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3d40fb3ca22e3d40f494d577441b263026a3bd8c97ae6ce89b2d3c4b39ac9581"}, - {file = "rpds_py-0.15.2-cp38-none-win32.whl", hash = "sha256:30479a9f1fce47df56b07460b520f49fa2115ec2926d3b1303c85c81f8401ed1"}, - {file = "rpds_py-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:2df3d07a16a3bef0917b28cd564778fbb31f3ffa5b5e33584470e2d1b0f248f0"}, - {file = "rpds_py-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:56b51ba29a18e5f5810224bcf00747ad931c0716e3c09a76b4a1edd3d4aba71f"}, - {file = "rpds_py-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c11bc5814554b018f6c5d6ae0969e43766f81e995000b53a5d8c8057055e886"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2faa97212b0dc465afeedf49045cdd077f97be1188285e646a9f689cb5dfff9e"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:86c01299942b0f4b5b5f28c8701689181ad2eab852e65417172dbdd6c5b3ccc8"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd7d3608589072f63078b4063a6c536af832e76b0b3885f1bfe9e892abe6c207"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:938518a11780b39998179d07f31a4a468888123f9b00463842cd40f98191f4d3"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dccc623725d0b298f557d869a68496a2fd2a9e9c41107f234fa5f7a37d278ac"}, - {file = "rpds_py-0.15.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d46ee458452727a147d7897bb33886981ae1235775e05decae5d5d07f537695a"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d9d7ebcd11ea76ba0feaae98485cd8e31467c3d7985210fab46983278214736b"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8a5f574b92b3ee7d254e56d56e37ec0e1416acb1ae357c4956d76a1788dc58fb"}, - {file = "rpds_py-0.15.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3db0c998c92b909d7c90b66c965590d4f3cd86157176a6cf14aa1f867b77b889"}, - {file = "rpds_py-0.15.2-cp39-none-win32.whl", hash = "sha256:bbc7421cbd28b4316d1d017db338039a7943f945c6f2bb15e1439b14b5682d28"}, - {file = "rpds_py-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:1c24e30d720c0009b6fb2e1905b025da56103c70a8b31b99138e4ed1c2a6c5b0"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e6fcd0a0f62f2997107f758bb372397b8d5fd5f39cc6dcb86f7cb98a2172d6c"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d800a8e2ac62db1b9ea5d6d1724f1a93c53907ca061de4d05ed94e8dfa79050c"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e09d017e3f4d9bd7d17a30d3f59e4d6d9ba2d2ced280eec2425e84112cf623f"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b88c3ab98556bc351b36d6208a6089de8c8db14a7f6e1f57f82a334bd2c18f0b"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f333bfe782a2d05a67cfaa0cc9cd68b36b39ee6acfe099f980541ed973a7093"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b629db53fe17e6ce478a969d30bd1d0e8b53238c46e3a9c9db39e8b65a9ef973"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485fbdd23becb822804ed05622907ee5c8e8a5f43f6f43894a45f463b2217045"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:893e38d0f4319dfa70c0f36381a37cc418985c87b11d9784365b1fff4fa6973b"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:8ffdeb7dbd0160d4e391e1f857477e4762d00aa2199c294eb95dfb9451aa1d9f"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:fc33267d58dfbb2361baed52668c5d8c15d24bc0372cecbb79fed77339b55e0d"}, - {file = "rpds_py-0.15.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2e7e5633577b3bd56bf3af2ef6ae3778bbafb83743989d57f0e7edbf6c0980e4"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8b9650f92251fdef843e74fc252cdfd6e3c700157ad686eeb0c6d7fdb2d11652"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:07a2e1d78d382f7181789713cdf0c16edbad4fe14fe1d115526cb6f0eef0daa3"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03f9c5875515820633bd7709a25c3e60c1ea9ad1c5d4030ce8a8c203309c36fd"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:580182fa5b269c2981e9ce9764367cb4edc81982ce289208d4607c203f44ffde"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa1e626c524d2c7972c0f3a8a575d654a3a9c008370dc2a97e46abd0eaa749b9"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae9d83a81b09ce3a817e2cbb23aabc07f86a3abc664c613cd283ce7a03541e95"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9235be95662559141934fced8197de6fee8c58870f36756b0584424b6d708393"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a72e00826a2b032dda3eb25aa3e3579c6d6773d22d8446089a57a123481cc46c"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ab095edf1d840a6a6a4307e1a5b907a299a94e7b90e75436ee770b8c35d22a25"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b79c63d29101cbaa53a517683557bb550462394fb91044cc5998dd2acff7340"}, - {file = "rpds_py-0.15.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:911e600e798374c0d86235e7ef19109cf865d1336942d398ff313375a25a93ba"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3cd61e759c4075510052d1eca5cddbd297fe1164efec14ef1fce3f09b974dfe4"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9d2ae79f31da5143e020a8d4fc74e1f0cbcb8011bdf97453c140aa616db51406"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e99d6510c8557510c220b865d966b105464740dcbebf9b79ecd4fbab30a13d9"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c43e1b89099279cc03eb1c725c5de12af6edcd2f78e2f8a022569efa639ada3"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7187bee72384b9cfedf09a29a3b2b6e8815cc64c095cdc8b5e6aec81e9fd5f"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3423007fc0661827e06f8a185a3792c73dda41f30f3421562f210cf0c9e49569"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2974e6dff38afafd5ccf8f41cb8fc94600b3f4fd9b0a98f6ece6e2219e3158d5"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:93c18a1696a8e0388ed84b024fe1a188a26ba999b61d1d9a371318cb89885a8c"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:c7cd0841a586b7105513a7c8c3d5c276f3adc762a072d81ef7fae80632afad1e"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:709dc11af2f74ba89c68b1592368c6edcbccdb0a06ba77eb28c8fe08bb6997da"}, - {file = "rpds_py-0.15.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fc066395e6332da1e7525d605b4c96055669f8336600bef8ac569d5226a7c76f"}, - {file = "rpds_py-0.15.2.tar.gz", hash = "sha256:373b76eeb79e8c14f6d82cb1d4d5293f9e4059baec6c1b16dca7ad13b6131b39"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d"}, + {file = "rpds_py-0.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9"}, + {file = "rpds_py-0.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394"}, + {file = "rpds_py-0.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59"}, + {file = "rpds_py-0.17.1-cp310-none-win32.whl", hash = "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d"}, + {file = "rpds_py-0.17.1-cp310-none-win_amd64.whl", hash = "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b"}, + {file = "rpds_py-0.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8"}, + {file = "rpds_py-0.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd"}, + {file = "rpds_py-0.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea"}, + {file = "rpds_py-0.17.1-cp311-none-win32.whl", hash = "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518"}, + {file = "rpds_py-0.17.1-cp311-none-win_amd64.whl", hash = "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf"}, + {file = "rpds_py-0.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9"}, + {file = "rpds_py-0.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253"}, + {file = "rpds_py-0.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23"}, + {file = "rpds_py-0.17.1-cp312-none-win32.whl", hash = "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1"}, + {file = "rpds_py-0.17.1-cp312-none-win_amd64.whl", hash = "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d"}, + {file = "rpds_py-0.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae"}, + {file = "rpds_py-0.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde"}, + {file = "rpds_py-0.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6"}, + {file = "rpds_py-0.17.1-cp38-none-win32.whl", hash = "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a"}, + {file = "rpds_py-0.17.1-cp38-none-win_amd64.whl", hash = "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a"}, + {file = "rpds_py-0.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256"}, + {file = "rpds_py-0.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772"}, + {file = "rpds_py-0.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b"}, + {file = "rpds_py-0.17.1-cp39-none-win32.whl", hash = "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f"}, + {file = "rpds_py-0.17.1-cp39-none-win_amd64.whl", hash = "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6"}, + {file = "rpds_py-0.17.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb"}, + {file = "rpds_py-0.17.1-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296"}, + {file = "rpds_py-0.17.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68"}, + {file = "rpds_py-0.17.1.tar.gz", hash = "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7"}, ] [[package]] @@ -1931,28 +1917,28 @@ files = [ [[package]] name = "ruff" -version = "0.1.9" +version = "0.1.14" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e6a212f436122ac73df851f0cf006e0c6612fe6f9c864ed17ebefce0eff6a5fd"}, - {file = "ruff-0.1.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:28d920e319783d5303333630dae46ecc80b7ba294aeffedf946a02ac0b7cc3db"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:104aa9b5e12cb755d9dce698ab1b97726b83012487af415a4512fedd38b1459e"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e63bf5a4a91971082a4768a0aba9383c12392d0d6f1e2be2248c1f9054a20da"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4d0738917c203246f3e275b37006faa3aa96c828b284ebfe3e99a8cb413c8c4b"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69dac82d63a50df2ab0906d97a01549f814b16bc806deeac4f064ff95c47ddf5"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2aec598fb65084e41a9c5d4b95726173768a62055aafb07b4eff976bac72a592"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:744dfe4b35470fa3820d5fe45758aace6269c578f7ddc43d447868cfe5078bcb"}, - {file = "ruff-0.1.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:479ca4250cab30f9218b2e563adc362bd6ae6343df7c7b5a7865300a5156d5a6"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:aa8344310f1ae79af9ccd6e4b32749e93cddc078f9b5ccd0e45bd76a6d2e8bb6"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:837c739729394df98f342319f5136f33c65286b28b6b70a87c28f59354ec939b"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e6837202c2859b9f22e43cb01992373c2dbfeae5c0c91ad691a4a2e725392464"}, - {file = "ruff-0.1.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:331aae2cd4a0554667ac683243b151c74bd60e78fb08c3c2a4ac05ee1e606a39"}, - {file = "ruff-0.1.9-py3-none-win32.whl", hash = "sha256:8151425a60878e66f23ad47da39265fc2fad42aed06fb0a01130e967a7a064f4"}, - {file = "ruff-0.1.9-py3-none-win_amd64.whl", hash = "sha256:c497d769164df522fdaf54c6eba93f397342fe4ca2123a2e014a5b8fc7df81c7"}, - {file = "ruff-0.1.9-py3-none-win_arm64.whl", hash = "sha256:0e17f53bcbb4fff8292dfd84cf72d767b5e146f009cccd40c2fad27641f8a7a9"}, - {file = "ruff-0.1.9.tar.gz", hash = "sha256:b041dee2734719ddbb4518f762c982f2e912e7f28b8ee4fe1dee0b15d1b6e800"}, + {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:96f76536df9b26622755c12ed8680f159817be2f725c17ed9305b472a757cdbb"}, + {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab3f71f64498c7241123bb5a768544cf42821d2a537f894b22457a543d3ca7a9"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7060156ecc572b8f984fd20fd8b0fcb692dd5d837b7606e968334ab7ff0090ab"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a53d8e35313d7b67eb3db15a66c08434809107659226a90dcd7acb2afa55faea"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bea9be712b8f5b4ebed40e1949379cfb2a7d907f42921cf9ab3aae07e6fba9eb"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2270504d629a0b064247983cbc495bed277f372fb9eaba41e5cf51f7ba705a6a"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80258bb3b8909b1700610dfabef7876423eed1bc930fe177c71c414921898efa"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:653230dd00aaf449eb5ff25d10a6e03bc3006813e2cb99799e568f55482e5cae"}, + {file = "ruff-0.1.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b3acc6c4e6928459ba9eb7459dd4f0c4bf266a053c863d72a44c33246bfdbf"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b3dadc9522d0eccc060699a9816e8127b27addbb4697fc0c08611e4e6aeb8b5"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c8eca1a47b4150dc0fbec7fe68fc91c695aed798532a18dbb1424e61e9b721f"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:62ce2ae46303ee896fc6811f63d6dabf8d9c389da0f3e3f2bce8bc7f15ef5488"}, + {file = "ruff-0.1.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b2027dde79d217b211d725fc833e8965dc90a16d0d3213f1298f97465956661b"}, + {file = "ruff-0.1.14-py3-none-win32.whl", hash = "sha256:722bafc299145575a63bbd6b5069cb643eaa62546a5b6398f82b3e4403329cab"}, + {file = "ruff-0.1.14-py3-none-win_amd64.whl", hash = "sha256:e3d241aa61f92b0805a7082bd89a9990826448e4d0398f0e2bc8f05c75c63d99"}, + {file = "ruff-0.1.14-py3-none-win_arm64.whl", hash = "sha256:269302b31ade4cde6cf6f9dd58ea593773a37ed3f7b97e793c8594b262466b67"}, + {file = "ruff-0.1.14.tar.gz", hash = "sha256:ad3f8088b2dfd884820289a06ab718cde7d38b94972212cc4ba90d5fbc9955f3"}, ] [[package]] @@ -1994,21 +1980,20 @@ files = [ ] [[package]] -name = "taskw" -version = "1.3.1" -description = "Python bindings for your taskwarrior database" +name = "taskw-ng" +version = "0.2.1" +description = "Next generation python bindings for your taskwarrior database" optional = true -python-versions = "*" +python-versions = ">=3.8,<4.0" files = [ - {file = "taskw-1.3.1-py3-none-any.whl", hash = "sha256:ffb9989496fae8e87e7b5eec5152f2048a1b4b142238c70e9515546ccdec8106"}, - {file = "taskw-1.3.1.tar.gz", hash = "sha256:1a68e49cac2d4f6da73c0ce554fd6f94932d95e20596f2ee44a769a28c12ba7d"}, + {file = "taskw_ng-0.2.1-py3-none-any.whl", hash = "sha256:9b73d0a0379fe995379d6484e55a000688d36381200eb407ba8250f7bf0dd78e"}, + {file = "taskw_ng-0.2.1.tar.gz", hash = "sha256:a7d02378c45bbb676c3957551558ed21ad3c5653c81782bff298263a0b72f013"}, ] [package.dependencies] -kitchen = "*" -python-dateutil = "*" -pytz = "*" -six = "*" +kitchen = ">=1.2.6,<2.0.0" +python-dateutil = ">=2.8.2,<3.0.0" +pytz = ">=2023.3.post1,<2024.0" [[package]] name = "toml" @@ -2096,13 +2081,13 @@ files = [ [[package]] name = "types-python-dateutil" -version = "2.8.19.14" +version = "2.8.19.20240106" description = "Typing stubs for python-dateutil" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.8.19.14.tar.gz", hash = "sha256:1f4f10ac98bb8b16ade9dbee3518d9ace017821d94b057a425b069f834737f4b"}, - {file = "types_python_dateutil-2.8.19.14-py3-none-any.whl", hash = "sha256:f977b8de27787639986b4e28963263fd0e5158942b3ecef91b9335c130cb1ce9"}, + {file = "types-python-dateutil-2.8.19.20240106.tar.gz", hash = "sha256:1f8db221c3b98e6ca02ea83a58371b22c374f42ae5bbdf186db9c9a76581459f"}, + {file = "types_python_dateutil-2.8.19.20240106-py3-none-any.whl", hash = "sha256:efbbdc54590d0f16152fa103c9879c7d4a00e82078f6e2cf01769042165acaa2"}, ] [[package]] @@ -2166,13 +2151,13 @@ typing-extensions = ">=3.7.4" [[package]] name = "tzdata" -version = "2023.3" +version = "2023.4" description = "Provider of IANA time zone data" optional = true python-versions = ">=2" files = [ - {file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"}, - {file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"}, + {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, + {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, ] [[package]] @@ -2478,9 +2463,9 @@ fs = ["xattr"] gkeep = ["gkeepapi"] google = ["google-api-python-client", "google-auth-httplib2", "google-auth-oauthlib"] notion = ["notion-client"] -tw = ["taskw", "xdg"] +tw = ["taskw-ng", "xdg"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e5539e75d6e76cbcd008e49e0c6fac7e895b8d7cb0f0cfbfcb41160353098cb1" +content-hash = "e63a3492bb7d6bf63cc5a6c75bd8cf63a5c8a2ef0c3940e277822e169be177f8" diff --git a/pyproject.toml b/pyproject.toml index a8b92c3..f4d17ad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,7 +64,8 @@ gkeepapi = { version = "^0.13.7", optional = true } asana = { version = "^1.0.0", optional = true } caldav = { version = "^0.11.0", optional = true } icalendar = { version = "^5.0.3", optional = true } -taskw = { version = "^1.3.1", optional = true } +taskw-ng = { version = "0.2.1", optional = true } +# taskw-ng = { path = "/home/berger/src_/taskw-ng", develop = true } xattr = { version = "^0.9.9", optional = true } xdg = { version = "^6.0.0", optional = true } @@ -87,7 +88,7 @@ gkeep = ["gkeepapi"] notion = ["notion-client"] asana = ["asana"] caldav = ["caldav", "icalendar"] -tw = ["taskw", "xdg"] +tw = ["taskw-ng", "xdg"] fs = ["xattr"] # dev dependencies ------------------------------------------------------------- @@ -112,12 +113,9 @@ coveralls = "^3.3.1" pycln = "^1.3.1" check-jsonschema = "^0.14.3" # readline = "6.2.4.1" - -# isort ------------------------------------------------------------------------ - -[tool.poetry.group.dev.dependencies] ruff = "^0.1.5" +# isort ------------------------------------------------------------------------ [tool.isort] include_trailing_comma = true line_length = 95 @@ -142,8 +140,6 @@ module = [ "pytest", "pexpect", "notion_client", - "taskw", - "taskw.warrior", "google.auth.transport.requests", "google_auth_oauthlib.flow", ] diff --git a/scripts/generate-shell-completions.sh b/scripts/generate-shell-completions.sh index 1ffdf64..7bf16a7 100755 --- a/scripts/generate-shell-completions.sh +++ b/scripts/generate-shell-completions.sh @@ -4,50 +4,51 @@ set -e # helpers ---------------------------------------------------------------------- function finish { - tabs 8 + tabs 8 } trap finish EXIT function inside_virtualenv() { - python3 -qc "import sys; sys.exit(0 if sys.prefix != sys.base_prefix else 1)" + python3 -qc "import sys; sys.exit(0 if sys.prefix != sys.base_prefix else 1)" } function inside_git_repo() { - git rev-parse + git rev-parse } function completion_dir_for() { - dir_=$completions_dir/$1 - mkdir -p $dir_ - echo "$dir_/$2" + dir_=$completions_dir/$1 + mkdir -p $dir_ + echo "$dir_/$2" } function get_ext_for() { - if [[ "$1" == "bash" || "$1" == "zsh" ]]; then - echo ".sh" - elif [[ "$1" == "fish" ]]; then - echo ".fish" - else - echo "" - fi + if [[ $1 == "bash" || $1 == "zsh" ]]; then + echo ".sh" + elif [[ $1 == "fish" ]]; then + echo ".fish" + else + echo "" + fi } # sanity checks ---------------------------------------------------------------- if ! inside_virtualenv; then - echo "You are not inside a virtualenv, can't proceed. Run "poetry shell" first". - exit 1 + echo "You are not inside a virtualenv, can't proceed. Run "poetry shell" first". + exit 1 fi if ! inside_git_repo; then - echo "You are not inside a git repo..." - exit 1 + echo "You are not inside a git repo..." + exit 1 fi git_root_dir=$(git rev-parse --show-toplevel) +this_file="$git_root_dir/scripts/generate-shell-completions.sh" -if [[ $(basename $git_root_dir) != "syncall" ]]; then - echo "This isn't the syncall repo -> $git_root_dir" - exit 1 +if ! test -f "$this_file"; then + echo "This doesn't seem to be the syncall repo -> $git_root_dir" + exit 1 fi completions_dir="${git_root_dir}/completions" @@ -55,18 +56,18 @@ mkdir -p $completions_dir # main loop -------------------------------------------------------------------- for exec in tw_gkeep_sync tw_notion_sync tw_gcal_sync tw_asana_sync tw_caldav_sync fs_gkeep_sync tw_gtasks_sync; do - tabs 4 - # Run the following, grab the output and dumpt it to the completions/ files... - # _TW_GKEEP_SYNC_COMPLETE=fish_source tw_gkeep_sync - envvar="_$(echo $exec | tr "[:lower:]" "[:upper:]")_COMPLETE" - for shell in "bash" "zsh" "fish"; do - echo -e "Generating completions for [$exec\t| $shell\t]" + tabs 4 + # Run the following, grab the output and dumpt it to the completions/ files... + # _TW_GKEEP_SYNC_COMPLETE=fish_source tw_gkeep_sync + envvar="_$(echo $exec | tr "[:lower:]" "[:upper:]")_COMPLETE" + for shell in "bash" "zsh" "fish"; do + echo -e "Generating completions for [$exec\t| $shell\t]" - # Execute command, grab its output, redirect it to file. - export $envvar=${shell}_source - contents=$($exec) + # Execute command, grab its output, redirect it to file. + export $envvar=${shell}_source + contents=$($exec) - path="$(completion_dir_for $shell $exec)$(get_ext_for $shell)" - echo "$contents" > "$path" - done + path="$(completion_dir_for $shell $exec)$(get_ext_for $shell)" + echo "$contents" > "$path" + done done diff --git a/syncall/__init__.py b/syncall/__init__.py index 2b4c8a9..ff3b037 100644 --- a/syncall/__init__.py +++ b/syncall/__init__.py @@ -1,206 +1,7 @@ """__init__""" # global imports ------------------------------------------------------------------------------ -from syncall.__version__ import __version__, __version_tuple__ -from syncall.aggregator import Aggregator -from syncall.app_utils import ( - app_name, - cache_or_reuse_cached_combination, - fetch_app_configuration, - fetch_from_pass_manager, - get_config_name_for_args, - get_resolution_strategy, - inform_about_app_extras, - inform_about_combination_name_usage, - list_named_combinations, - name_to_resolution_strategy_type, - report_toplevel_exception, -) -from syncall.cli import ( - opt_asana_task_gid, - opt_asana_token_pass_path, - opt_asana_workspace_gid, - opt_asana_workspace_name, - opt_combination, - opt_custom_combination_savename, - opt_filesystem_root, - opt_gcal_calendar, - opt_gkeep_labels, - opt_gkeep_note, - opt_gkeep_passwd_pass_path, - opt_gkeep_user_pass_path, - opt_google_oauth_port, - opt_google_secret_override, - opt_gtasks_list, - opt_list_asana_workspaces, - opt_list_combinations, - opt_notion_page_id, - opt_notion_token_pass_path, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, -) -from syncall.sync_side import ItemType, SyncSide +from syncall.__version__ import __version__ as version +from syncall.__version__ import __version_tuple__ as version_tuple -__all__ = [ - "__version__", - "__version_tuple__", - "Aggregator", - "ItemType", - "SyncSide", - "app_name", - "cache_or_reuse_cached_combination", - "fetch_app_configuration", - "fetch_from_pass_manager", - "get_config_name_for_args", - "inform_about_app_extras", - "inform_about_combination_name_usage", - "list_named_combinations", - "get_resolution_strategy", - "name_to_resolution_strategy_type", - "opt_asana_task_gid", - "opt_asana_token_pass_path", - "opt_asana_workspace_gid", - "opt_asana_workspace_name", - "opt_combination", - "opt_custom_combination_savename", - "opt_gcal_calendar", - "opt_gtasks_list", - "opt_gkeep_note", - "opt_gkeep_passwd_pass_path", - "opt_gkeep_user_pass_path", - "opt_google_oauth_port", - "opt_google_secret_override", - "opt_list_asana_workspaces", - "opt_list_combinations", - "opt_notion_page_id", - "opt_notion_token_pass_path", - "opt_resolution_strategy", - "opt_tw_project", - "opt_filesystem_root", - "opt_tw_tags", - "opt_gkeep_labels", - "report_toplevel_exception", -] - -# asana ---------------------------------------------------------------------------------------- -try: - from syncall.asana.asana_side import AsanaSide - from syncall.asana.utils import list_asana_workspaces - from syncall.tw_asana_utils import convert_asana_to_tw, convert_tw_to_asana - - __all__.extend( - ["AsanaSide", "convert_asana_to_tw", "convert_tw_to_asana", "list_asana_workspaces"] - ) -except ImportError: - pass - -# tw ------------------------------------------------------------------------------------------ -try: - from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide - - __all__.extend( - [ - "TaskWarriorSide", - ] - ) -except ImportError: - pass - -# notion -------------------------------------------------------------------------------------- -try: - from syncall.notion.notion_side import NotionSide - - __all__.extend(["NotionSide"]) -except ImportError: - pass - -# notion <> tw -------------------------------------------------------------------------------- -try: - from syncall.tw_notion_utils import convert_notion_to_tw, convert_tw_to_notion - - __all__.extend(["convert_notion_to_tw", "convert_tw_to_notion"]) -except ImportError: - pass - -# gcal ---------------------------------------------------------------------------------------- -try: - from syncall.google.gcal_side import GCalSide - from syncall.tw_gcal_utils import convert_gcal_to_tw, convert_tw_to_gcal -except ImportError: - __all__.extend( - [ - "GCalSide", - "convert_gcal_to_tw", - "convert_tw_to_gcal", - ] - ) - -# gtasks -------------------------------------------------------------------------------------- -try: - from syncall.google.gtasks_side import GTasksSide - from syncall.tw_gtasks_utils import convert_gtask_to_tw, convert_tw_to_gtask -except ImportError: - __all__.extend( - [ - "GTasksSide", - "convert_gtask_to_tw", - "convert_tw_to_gtask", - ] - ) - -# gkeep --------------------------------------------------------------------------------------- -try: - from syncall.google.gkeep_note import GKeepNote - from syncall.google.gkeep_note_side import GKeepNoteSide - from syncall.google.gkeep_todo_item import GKeepTodoItem - from syncall.google.gkeep_todo_side import GKeepTodoSide - - __all__.extend( - [ - "GKeepNote", - "GKeepNoteSide", - "GKeepTodoItem", - "GKeepTodoSide", - "convert_gkeep_todo_to_tw", - "convert_tw_to_gkeep_todo", - ] - ) -except ImportError: - pass - -# gkeep <> tw -try: - from syncall.tw_gkeep_utils import convert_gkeep_todo_to_tw, convert_tw_to_gkeep_todo - - __all__.extend( - [ - "convert_gkeep_todo_to_tw", - "convert_tw_to_gkeep_todo", - ] - ) -except ImportError: - pass - -# filesytem ----------------------------------------------------------------------------------- -try: - from syncall.filesystem.filesystem_file import FilesystemFile - from syncall.filesystem.filesystem_side import FilesystemSide - - __all__.extend(["FilesystemFile", "FilesystemSide"]) -except ImportError: - pass - - -# filesytem <> gkeep -------------------------------------------------------------------------- -try: - from syncall.filesystem_gkeep_utils import ( - convert_filesystem_file_to_gkeep_note, - convert_gkeep_note_to_filesystem_file, - ) - - __all__.extend( - ["convert_filesystem_file_to_gkeep_note", "convert_gkeep_note_to_filesystem_file"] - ) -except ImportError: - pass +__all__ = ["version", "version_tuple"] diff --git a/syncall/app_utils.py b/syncall/app_utils.py index 2f72d1b..832b838 100644 --- a/syncall/app_utils.py +++ b/syncall/app_utils.py @@ -4,6 +4,7 @@ `sys.exit()` to avoid dumping stack traces to the user. """ +import atexit import inspect import logging import os @@ -16,6 +17,7 @@ from urllib.parse import quote from bubop import ( + ExitHooks, PrefsManager, format_list, log_to_syslog, @@ -128,20 +130,9 @@ def get_named_combinations(config_fname: str) -> Sequence[str]: return list(prefs_manager.keys()) -def list_named_combinations(config_fname: str) -> None: - """List the named configurations currently available for the given configuration name. - - Mainly used by the top-level synchronization apps. - """ - logger.success( - format_list( - header="\n\nNamed configurations currently available", - items=get_named_combinations(config_fname=config_fname), - ) - ) - - -def fetch_app_configuration(config_fname: str, combination: str) -> Mapping[str, Any]: +def fetch_app_configuration( + side_A_name: str, side_B_name: str, combination: str +) -> Mapping[str, Any]: """ Fetch the configuration of a top-level synchronization app. @@ -153,6 +144,7 @@ def fetch_app_configuration(config_fname: str, combination: str) -> Mapping[str, It will check whether the configuration file at hand exist and will also give meaningful errors to the user if the configuration file does not contain the said combination. """ + config_fname = determine_app_config_fname(side_A_name, side_B_name) with PrefsManager(app_name=app_name(), config_fname=config_fname) as prefs_manager: if combination not in prefs_manager: # config not found ---------------------------------------------------------------- @@ -184,6 +176,9 @@ def cache_or_reuse_cached_combination( else: config_name = custom_combination_savename + if not config_name.endswith((".yaml", ".yml")): + config_name = f"{config_name}.yaml" + # see if this combination corresponds to an already existing configuration ---------------- with PrefsManager(app_name=app_name(), config_fname=config_fname) as prefs_manager: config_exists = config_name in prefs_manager @@ -347,3 +342,51 @@ def app_log_to_syslog(): calling_file = Path(caller_frame[1]) fname = calling_file.stem log_to_syslog(name=fname) + + +def register_teardown_handler( + pdb_on_error: bool, inform_about_config: bool, combination_name: str, verbose: int +) -> ExitHooks: + """Shortcut for registering the teardown logic in a top-level sync application. + + We're explicilty not catching/reporting exceptions if the pdb_on_error argument is set + since the developer is meaning to get a prompt and debug any exception that arises. + """ + hooks: ExitHooks = ExitHooks() + + def teardown(): + if hooks.exception is not None: + if hooks.exception.__class__ is KeyboardInterrupt: + logger.error("C-c pressed, exiting...") + else: + report_toplevel_exception(is_verbose=verbose >= 1) + return 1 + + if inform_about_config: + inform_about_combination_name_usage(combination_name) + + if pdb_on_error: + logger.warning( + "pdb_on_error is enabled. Disabling exit hooks / not taking actions at the end " + "of the run." + ) + else: + hooks.register() + atexit.register(teardown) + + return hooks + + +def determine_app_config_fname(side_A_name: str, side_B_name: str): + """ + Get the configuration name for the app at hand given the names of the sides involved. + + >>> assert determine_app_config_fname("TW", "Google Tasks") == 'tw__google_tasks__configs.yaml' + >>> assert determine_app_config_fname("TW", "Google Calendar") == 'tw__google_calendar__configs.yaml' + """ + config_fname = ( + f'{side_A_name.replace(" ", "_").lower()}' + "__" + f'{side_B_name.replace(" ", "_").lower()}__configs.yaml' + ) + return config_fname diff --git a/syncall/cli.py b/syncall/cli.py index d317506..5c65d20 100644 --- a/syncall/cli.py +++ b/syncall/cli.py @@ -4,11 +4,20 @@ extra dependency. """ +import os import sys import click - -from syncall.app_utils import name_to_resolution_strategy_type +from bubop import format_list, logger + +from syncall import __version__ +from syncall.app_utils import ( + determine_app_config_fname, + error_and_exit, + fetch_from_pass_manager, + get_named_combinations, + name_to_resolution_strategy_type, +) from syncall.constants import COMBINATION_FLAGS from syncall.pdb_cli_utils import run_pdb_on_error as _run_pdb_on_error @@ -21,19 +30,41 @@ def _set_own_excepthook(ctx, param, value): return value -def opt_pdb_on_error(): +def _opt_pdb_on_error(): return click.option( "--pdb-on-error", "pdb_on_error", is_flag=True, - help="Invoke PDB if there's an uncaught exception during the program execution", + help="Invoke PDB if there's an uncaught exception during the program execution.", callback=_set_own_excepthook, expose_value=True, is_eager=True, ) -def opt_asana_task_gid(**kwargs): +# asana related options ----------------------------------------------------------------------- +def opts_asana(hidden_gid: bool): + def decorator(f): + for d in reversed( + [ + _opt_asana_token_pass_path, + _opt_asana_workspace_gid, + _opt_asana_workspace_name, + _opt_list_asana_workspaces, + ] + ): + f = d()(f) + + # --asana-task-gid is used to ease development and debugging. It is not currently + # suitable for regular use. + f = _opt_asana_task_gid(hidden=hidden_gid)(f) + + return f + + return decorator + + +def _opt_asana_task_gid(**kwargs): return click.option( "-a", "--asana-task-gid", @@ -44,16 +75,39 @@ def opt_asana_task_gid(**kwargs): ) -def opt_asana_token_pass_path(): +def _opt_asana_token_pass_path(): + def callback(ctx, param, value): + api_token_pass_path = value + + # fetch API token to connect to asana ------------------------------------------------- + asana_token = os.environ.get("ASANA_PERSONAL_ACCESS_TOKEN") + + if asana_token is None and api_token_pass_path is None: + error_and_exit( + "You must provide an Asana Personal Access asana_token, using the" + f" {'/'.join(param.opts)} option" + ) + if asana_token is not None: + logger.debug( + "Reading the Asana Personal Access asana_token (PAT) from environment" + " variable..." + ) + else: + asana_token = fetch_from_pass_manager(api_token_pass_path) + + return asana_token + return click.option( "--token", "--token-pass-path", - "token_pass_path", + "asana_token", help="Path in the UNIX password manager to fetch", + expose_value=True, + callback=callback, ) -def opt_asana_workspace_gid(): +def _opt_asana_workspace_gid(): return click.option( "-w", "--asana-workspace-gid", @@ -63,7 +117,7 @@ def opt_asana_workspace_gid(): ) -def opt_asana_workspace_name(): +def _opt_asana_workspace_name(): return click.option( "-W", "--asana-workspace-name", @@ -73,170 +127,121 @@ def opt_asana_workspace_name(): ) -def opt_list_asana_workspaces(): +def _opt_list_asana_workspaces(): return click.option( "--list-asana-workspaces", "do_list_asana_workspaces", is_flag=True, - help=f"List the available Asana workspaces", + help="List the available Asana workspaces", ) -def opt_default_duration_event_mins(): - return click.option( - "--default-event-duration-mins", - "default_event_duration_mins", - default=30, - type=int, - help=( - f"The default duration of an event that is to be created on Google Calendar [in" - f" minutes]" - ), - ) +# taskwarrior options ------------------------------------------------------------------------- +def opts_tw_filtering(): + def decorator(f): + for d in reversed( + [ + _opt_tw_filter, + _opt_tw_all_tasks, + _opt_tw_tags, + _opt_tw_project, + _opt_tw_only_tasks_modified_X_days, + _opt_prefer_scheduled_date, + ] + ): + f = d()(f) + return f + + return decorator -def opt_prefer_scheduled_date(): +def _opt_tw_filter(): return click.option( - "--prefer-scheduled-date", - "prefer_scheduled_date", - is_flag=True, + "-f", + "--tw-filter", + "tw_filter", + type=str, help=( - f'Prefer using the "scheduled" date field instead of the "due" date if the former' - f" is available" + "Taskwarrior filter for specifying the tasks to synchronize. These filters will be" + " concatenated using OR with potential tags and projects potentially specified." ), ) -def opt_list_combinations(name_A: str, name_B: str): - return click.option( - "--list-combinations", - "do_list_combinations", - is_flag=True, - help=f"List the available named {name_A}<->{name_B} combinations", - ) - - -def opt_tw_all_tasks(): +def _opt_tw_all_tasks(): return click.option( "--all", "--taskwarrior-all-tasks", "tw_sync_all_tasks", is_flag=True, - help="Sync all taskwarrior tasks [potentially very slow]", + help="Sync all taskwarrior tasks (potentially very slow).", ) -def opt_tw_tags(): +def _opt_tw_tags(): return click.option( "-t", "--taskwarrior-tags", "tw_tags", type=str, - help="Taskwarrior tags to synchronize", + help="Taskwarrior tags to synchronize.", + expose_value=True, multiple=True, ) -def opt_tw_project(): +def _opt_tw_project(): return click.option( "-p", "--tw-project", "tw_project", type=str, - help="Taskwarrior project to synchronize", + help="Taskwarrior project to synchronize.", + expose_value=True, + is_eager=True, ) -def opt_tw_only_tasks_modified_30_days(): - return click.option( - "--30-days", - "--only-modified-last-30-days", - "tw_only_modified_last_30_days", - is_flag=True, - help="Only synchronize Taskwarrior tasks that have been modified in the last 30 days", - ) +def _opt_tw_only_tasks_modified_X_days(): + def callback(ctx, param, value): + if value is None or ctx.resilient_parsing: + return + return f"modified.after:-{value}d" -def opt_filesystem_root(): return click.option( - "--fs", - "--fs-root", - "filesystem_root", - required=False, + "--days", + "--only-modified-last-X-days", + "tw_only_modified_last_X_days", type=str, - help="Directory to consider as root for synchronization operations", - ) - - -def opt_resolution_strategy(): - return click.option( - "-r", - "--resolution-strategy", - default="AlwaysSecondRS", - type=click.Choice(list(name_to_resolution_strategy_type.keys())), - help="Resolution strategy to use during conflicts", + help=( + "Only synchronize Taskwarrior tasks that have been modified in the last X days" + " (specify X, e.g., 1, 30, 0.5, etc.)." + ), + callback=callback, ) -def _list_resolution_strategies(ctx, param, value): - if value is not True: - return - - strs = name_to_resolution_strategy_type.keys() - click.echo("\n".join([f"{a}. {b}" for a, b in zip(range(1, len(strs) + 1), strs)])) - sys.exit(0) - - -def opt_list_resolution_strategies(): +def _opt_prefer_scheduled_date(): return click.option( - "--list-resolution-strategies", - callback=_list_resolution_strategies, + "--prefer-scheduled-date", + "prefer_scheduled_date", is_flag=True, - help="List all the available resolution strategies and exit", - ) - - -def opt_combination(name_A: str, name_B: str): - return click.option( - COMBINATION_FLAGS[0], - COMBINATION_FLAGS[1], - "combination_name", - type=str, - help=f"Name of an already saved {name_A}<->{name_B} combination", - ) - - -def opt_custom_combination_savename(name_A: str, name_B: str): - return click.option( - "-s", - "--save-as", - "custom_combination_savename", - type=str, help=( - f"Save the given {name_A}<->{name_B} filters combination using a specified custom" - " name." + 'Prefer using the "scheduled" date field instead of the "due" date if the former' + " is available." ), ) -def opt_filename_extension(): - return click.option( - "--ext", - "--filename-extension", - "filename_extension", - type=str, - help=f"Use this extension for locally created files", - default=".md", - ) - - +# notion -------------------------------------------------------------------------------------- def opt_notion_page_id(): return click.option( "-n", "--notion-page", "notion_page_id", type=str, - help="Page ID of the Notion page to synchronize", + help="Page ID of the Notion page to synchronize.", ) @@ -245,96 +250,119 @@ def opt_notion_token_pass_path(): "--token", "--token-pass-path", "token_pass_path", - help="Path in the UNIX password manager to fetch", + help="Path in the UNIX password manager to fetch.", ) -def opt_gkeep_user_pass_path(): +# gkeep --------------------------------------------------------------------------------------- +def opts_gkeep(): + def decorator(f): + for d in reversed( + [ + _opt_gkeep_user_pass_path, + _opt_gkeep_passwd_pass_path, + _opt_gkeep_token_pass_path, + _opt_gkeep_labels, + _opt_gkeep_ignore_labels, + _opt_gkeep_note, + ] + ): + f = d()(f) + + return f + + return decorator + + +def _opt_gkeep_user_pass_path(): return click.option( "--user", "--user-pass-path", "gkeep_user_pass_path", - help="Path in the UNIX password manager to fetch the Google username from", + help="Path in the UNIX password manager to fetch the Google username from.", default="gkeepapi/user", ) -def opt_gkeep_passwd_pass_path(): +def _opt_gkeep_passwd_pass_path(): return click.option( "--passwd", "--passwd-pass-path", "gkeep_passwd_pass_path", - help="Path in the UNIX password manager to fetch the Google password from", + help="Path in the UNIX password manager to fetch the Google password from.", default="gkeepapi/passwd", ) -def opt_gkeep_token_pass_path(): +def _opt_gkeep_token_pass_path(): return click.option( "--token", "--token-pass-path", "gkeep_token_pass_path", - help="Path in the UNIX password manager to fetch the google keep token from", + help="Path in the UNIX password manager to fetch the google keep token from.", default="gkeepapi/token", ) -def opt_gcal_calendar(): - return click.option( - "-c", - "--gcal-calendar", - type=str, - help="Name of the Google Calendar to synchronize (will be created if not there)", - ) - - -def opt_gtasks_list(): - return click.option( - "-l", - "--gtasks-list", - type=str, - help="Name of the Google Tasks list to synchronize (will be created if not there)", - ) - - -def opt_gkeep_labels(): +def _opt_gkeep_labels(): return click.option( "-k", "--gkeep-labels", type=str, multiple=True, - help="Google Keep labels whose notes to synchronize", + help="Google Keep labels whose notes to synchronize.", ) -def opt_gkeep_ignore_labels(): +def _opt_gkeep_ignore_labels(): return click.option( "-i", "--gkeep-ignore-labels", type=str, multiple=True, - help="Google Keep labels whose notes will be explicitly ignored", + help="Google Keep labels whose notes will be explicitly ignored.", ) -def opt_gkeep_note(): +def _opt_gkeep_note(): return click.option( "-k", "--gkeep-note", type=str, help=( "Full title of the Google Keep Note to synchronize - Make sure you enable the" - " checkboxes" + " checkboxes." ), ) +# google calendar ----------------------------------------------------------------------------- +def opt_gcal_calendar(): + return click.option( + "-c", + "--gcal-calendar", + type=str, + help="Name of the Google Calendar to synchronize (will be created if not there).", + ) + + +# google tasks -------------------------------------------------------------------------------- +def opt_gtasks_list(): + return click.option( + "-l", + "--gtasks-list", + type=str, + help="Name of the Google Tasks list to synchronize (will be created if not there).", + ) + + +# google-related options ---------------------------------------------------------------------- def opt_google_secret_override(): return click.option( "--google-secret", default=None, type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True), - help="Override the client secret used for the communication with the Google APIs", + help="Override the client secret used for the communication with the Google APIs.", ) @@ -343,28 +371,47 @@ def opt_google_oauth_port(): "--oauth-port", default=8081, type=int, - help="Port to use for OAuth Authentication with Google Applications", + help="Port to use for OAuth Authentication with Google Applications.", ) -def opt_caldav_calendar(): +# caldav options ------------------------------------------------------------------------------ +def opts_caldav(): + def decorator(f): + for d in reversed( + [ + _opt_caldav_calendar, + _opt_caldav_url, + _opt_caldav_user, + _opt_caldav_passwd_pass_path, + _opt_caldav_passwd_cmd, + ] + ): + f = d()(f) + + return f + + return decorator + + +def _opt_caldav_calendar(): return click.option( "--caldav-calendar", type=str, default="Personal", - help="Name of the caldav Calendar to sync (will be created if not there)", + help="Name of the caldav Calendar to sync (will be created if not there).", ) -def opt_caldav_url(): +def _opt_caldav_url(): return click.option( "--caldav-url", type=str, - help="URL where the caldav calendar is hosted at (including /dav if applicable)", + help="URL where the caldav calendar is hosted at (including /dav if applicable).", ) -def opt_caldav_user(): +def _opt_caldav_user(): return click.option( "--caldav-user", "caldav_user", @@ -372,18 +419,159 @@ def opt_caldav_user(): ) -def opt_caldav_passwd_pass_path(): +def _opt_caldav_passwd_pass_path(): return click.option( "--caldav-passwd", "--caldav-passwd-pass-path", "caldav_passwd_pass_path", - help="Path in the UNIX password manager to fetch the caldav password from", + help="Path in the UNIX password manager to fetch the caldav password from.", ) -def opt_caldav_passwd_cmd(): +def _opt_caldav_passwd_cmd(): return click.option( "--caldav-passwd-cmd", "caldav_passwd_cmd", - help="Command that outputs the caldav password on stdout", + help="Command that outputs the caldav password on stdout.", + ) + + +# filesystem related options ------------------------------------------------------------------ +def opt_filesystem_root(): + return click.option( + "--fs", + "--fs-root", + "filesystem_root", + required=False, + type=str, + help="Directory to consider as root for synchronization operations.", + ) + + +def opt_filename_extension(): + return click.option( + "--ext", + "--filename-extension", + "filename_extension", + type=str, + help="Use this extension for locally created files.", + default=".md", + ) + + +# general options ----------------------------------------------------------------------------- +def opts_miscellaneous(side_A_name: str, side_B_name: str): + def decorator(f): + for d in reversed( + [ + (_opt_list_resolution_strategies,), + (_opt_resolution_strategy,), + ( + click.version_option, + __version__, + ), + (_opt_pdb_on_error,), + (_opt_list_combinations, side_A_name, side_B_name), + (_opt_combination, side_A_name, side_B_name), + (_opt_custom_combination_savename, side_A_name, side_B_name), + ] + ): + fn = d[0] + fn_args = d[1:] + f = fn(*fn_args)(f) # type: ignore + + f = click.option("-v", "--verbose", count=True)(f) + return f + + return decorator + + +def opt_default_duration_event_mins(): + return click.option( + "--default-event-duration-mins", + "default_event_duration_mins", + default=30, + type=int, + help="The default duration of an event that is to be created [in minutes].", + ) + + +def _list_named_combinations(config_fname: str) -> None: + """List the named configurations currently available for the given configuration name.""" + logger.success( + format_list( + header="\n\nNamed configurations currently available", + items=get_named_combinations(config_fname=config_fname), + ) + ) + + +def _opt_list_combinations(side_A_name: str, side_B_name: str): + def callback(ctx, param, value): + if value is True: + _list_named_combinations( + config_fname=determine_app_config_fname( + side_A_name=side_A_name, side_B_name=side_B_name + ) + ) + sys.exit(0) + + return click.option( + "--list-combinations", + "do_list_combinations", + is_flag=True, + expose_value=False, + help=f"List the available named {side_A_name}<->{side_B_name} combinations.", + callback=callback, + ) + + +def _opt_resolution_strategy(): + return click.option( + "-r", + "--resolution-strategy", + default="AlwaysSecondRS", + type=click.Choice(list(name_to_resolution_strategy_type.keys())), + help="Resolution strategy to use during conflicts.", + ) + + +def _opt_list_resolution_strategies(): + def _list_resolution_strategies(ctx, param, value): + if value is not True: + return + + strs = name_to_resolution_strategy_type.keys() + click.echo("\n".join([f"{a}. {b}" for a, b in zip(range(1, len(strs) + 1), strs)])) + sys.exit(0) + + return click.option( + "--list-resolution-strategies", + callback=_list_resolution_strategies, + is_flag=True, + expose_value=False, + help="List all the available resolution strategies and exit.", + ) + + +def _opt_combination(side_A_name: str, side_B_name: str): + return click.option( + COMBINATION_FLAGS[0], + COMBINATION_FLAGS[1], + "combination_name", + type=str, + help=f"Name of an already saved {side_A_name}<->{side_B_name} combination.", + ) + + +def _opt_custom_combination_savename(side_A_name: str, side_B_name: str): + return click.option( + "-s", + "--save-as", + "custom_combination_savename", + type=str, + help=( + f"Save the given {side_A_name}<->{side_B_name} filters combination using a" + " specified custom name." + ), ) diff --git a/syncall/scripts/fs_gkeep_sync.py b/syncall/scripts/fs_gkeep_sync.py index e85f538..599dbf0 100644 --- a/syncall/scripts/fs_gkeep_sync.py +++ b/syncall/scripts/fs_gkeep_sync.py @@ -5,64 +5,48 @@ import click from bubop import check_optional_mutually_exclusive, format_dict, logger, loguru_tqdm_sink -from syncall import inform_about_app_extras -from syncall.cli import opt_filename_extension, opt_gkeep_ignore_labels +from syncall.app_utils import inform_about_app_extras try: - from syncall import GKeepNoteSide + from syncall.filesystem.filesystem_side import FilesystemSide + from syncall.google.gkeep_note_side import GKeepNoteSide except ImportError: inform_about_app_extras(["gkeep", "fs"]) -from syncall import ( - Aggregator, - FilesystemSide, - __version__, + +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_filesystem_file_to_gkeep_note, - convert_gkeep_note_to_filesystem_file, fetch_app_configuration, get_resolution_strategy, - inform_about_combination_name_usage, - list_named_combinations, - report_toplevel_exception, -) -from syncall.app_utils import ( - app_log_to_syslog, gkeep_read_username_password_token, + register_teardown_handler, write_to_pass_manager, ) from syncall.cli import ( - opt_combination, - opt_custom_combination_savename, + opt_filename_extension, opt_filesystem_root, - opt_gkeep_labels, - opt_gkeep_passwd_pass_path, - opt_gkeep_token_pass_path, - opt_gkeep_user_pass_path, - opt_list_combinations, - opt_resolution_strategy, + opts_gkeep, + opts_miscellaneous, +) +from syncall.filesystem_gkeep_utils import ( + convert_filesystem_file_to_gkeep_note, + convert_gkeep_note_to_filesystem_file, ) +from syncall.google.gkeep_note_side import GKeepNoteSide @click.command() -# google keep options ------------------------------------------------------------------------- -@opt_gkeep_labels() -@opt_gkeep_ignore_labels() -@opt_gkeep_user_pass_path() -@opt_gkeep_passwd_pass_path() -@opt_gkeep_token_pass_path() +@opts_gkeep() # filesystem options -------------------------------------------------------------------------- @opt_filename_extension() @opt_filesystem_root() # misc options -------------------------------------------------------------------------------- -@opt_list_combinations("Filesystem", "Google Keep") -@opt_resolution_strategy() -@opt_combination("Filesystem", "Google Keep") -@opt_custom_combination_savename("Filesystem", "Google Keep") -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_miscellaneous("Filesystem", "Google Keep") def main( filesystem_root: Optional[str], + filename_extension: str, gkeep_labels: Sequence[str], gkeep_ignore_labels: Sequence[str], gkeep_user_pass_path: str, @@ -72,8 +56,7 @@ def main( verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, - filename_extension: str, + pdb_on_error: bool, ): """ Synchronize Notes from your Google Keep with text files in a directory on your filesystem. @@ -94,10 +77,6 @@ def main( logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="fs_gkeep_configs") - return 0 - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(gkeep_labels, gkeep_ignore_labels) check_optional_mutually_exclusive(combination_name, custom_combination_savename) @@ -130,12 +109,13 @@ def main( # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="fs_gkeep_configs", combination=combination_name + side_A_name="Filesystem", side_B_name="Google Keep", combination=combination_name ) filesystem_root_path = Path(app_config["filesystem_root"]) gkeep_labels = app_config["gkeep_labels"] gkeep_ignore_labels = app_config["gkeep_ignore_labels"] filename_extension = app_config["filename_extension"] + # combination manually specified ---------------------------------------------------------- else: inform_about_config = True @@ -188,7 +168,6 @@ def main( gkeep_token_pass_path, ) - # initialize google keep ----------------------------------------------------------------- gkeep_side = GKeepNoteSide( gkeep_labels=gkeep_labels, gkeep_ignore_labels=gkeep_ignore_labels, @@ -197,12 +176,19 @@ def main( gkeep_token=gkeep_token, ) - # initialize Filesystem Side -------------------------------------------------------------- filesystem_side = FilesystemSide( filesystem_root=filesystem_root_path, filename_extension=filename_extension ) - # sync ------------------------------------------------------------------------------------ + # 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 converter_A_to_B(gkeep_note): return convert_gkeep_note_to_filesystem_file( gkeep_note=gkeep_note, @@ -212,40 +198,31 @@ def converter_A_to_B(gkeep_note): converter_A_to_B.__doc__ = convert_gkeep_note_to_filesystem_file.__doc__ - try: - with Aggregator( - side_A=gkeep_side, - side_B=filesystem_side, - converter_B_to_A=convert_filesystem_file_to_gkeep_note, - converter_A_to_B=converter_A_to_B, - resolution_strategy=get_resolution_strategy( - resolution_strategy, - side_A_type=type(gkeep_side), - side_B_type=type(filesystem_side), - ), - config_fname=combination_name, - ignore_keys=( - (), - (), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - # cache the token + # sync ------------------------------------------------------------------------------------ + with Aggregator( + side_A=gkeep_side, + side_B=filesystem_side, + converter_B_to_A=convert_filesystem_file_to_gkeep_note, + converter_A_to_B=converter_A_to_B, + resolution_strategy=get_resolution_strategy( + resolution_strategy, + side_A_type=type(gkeep_side), + side_B_type=type(filesystem_side), + ), + config_fname=combination_name, + ignore_keys=( + (), + (), + ), + ) as aggregator: + aggregator.sync() + + # cache the token ------------------------------------------------------------------------- token = gkeep_side.get_master_token() if token is not None: logger.debug(f"Caching the gkeep token in pass -> {gkeep_token_pass_path}...") write_to_pass_manager(password_path=gkeep_token_pass_path, passwd=token) - if inform_about_config: - inform_about_combination_name_usage(combination_name) - return 0 diff --git a/syncall/scripts/tw_asana_sync.py b/syncall/scripts/tw_asana_sync.py index 0ba4fc7..0b3848a 100644 --- a/syncall/scripts/tw_asana_sync.py +++ b/syncall/scripts/tw_asana_sync.py @@ -1,149 +1,145 @@ -"""Console script for asana_taskwarrior.""" -import os import sys from typing import List import asana import click -from bubop import check_optional_mutually_exclusive, format_dict, logger, loguru_tqdm_sink +from bubop import ( + check_optional_mutually_exclusive, + check_required_mutually_exclusive, + format_dict, + logger, + loguru_tqdm_sink, +) -from syncall import inform_about_app_extras -from syncall.app_utils import app_log_to_syslog, error_and_exit +from syncall.app_utils import inform_about_app_extras try: - from syncall import AsanaSide, TaskWarriorSide + from syncall.asana.asana_side import AsanaSide + from syncall.asana.utils import list_asana_workspaces + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide except ImportError: inform_about_app_extras(["asana", "tw"]) -from syncall import ( - Aggregator, - __version__, +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_asana_to_tw, - convert_tw_to_asana, + error_and_exit, fetch_app_configuration, - fetch_from_pass_manager, get_resolution_strategy, - inform_about_combination_name_usage, - list_asana_workspaces, - list_named_combinations, - opt_asana_task_gid, - opt_asana_token_pass_path, - opt_asana_workspace_gid, - opt_asana_workspace_name, - opt_combination, - opt_custom_combination_savename, - opt_list_asana_workspaces, - opt_list_combinations, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, - report_toplevel_exception, + register_teardown_handler, ) +from syncall.cli import opts_asana, opts_miscellaneous, opts_tw_filtering +from syncall.tw_asana_utils import convert_asana_to_tw, convert_tw_to_asana # CLI parsing --------------------------------------------------------------------------------- @click.command() -# Asana options ------------------------------------------------------------------------------ -# --asana-task-gid is used to ease development and debugging. It is not currently -# suitable for regular use. -@opt_asana_task_gid(hidden=True) -@opt_asana_token_pass_path() -@opt_asana_workspace_gid() -@opt_asana_workspace_name() -@opt_list_asana_workspaces() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_tags() -@opt_tw_project() -# misc options -------------------------------------------------------------------------------- -@opt_resolution_strategy() -@opt_combination("TW", "Asana") -@opt_list_combinations("TW", "Asana") -@opt_custom_combination_savename("TW", "Asana") -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_asana(hidden_gid=False) +@opts_tw_filtering() +@opts_miscellaneous("TW", "Asana") def main( asana_task_gid: str, + asana_token: str, asana_workspace_gid: str, asana_workspace_name: str, do_list_asana_workspaces: bool, + tw_filter: str, tw_tags: List[str], tw_project: str, - token_pass_path: str, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, + prefer_scheduled_date: bool, resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, + pdb_on_error: bool, ): + """Synchronize your tasks in Asana with filters from Taskwarrior.""" loguru_tqdm_sink(verbosity=verbose) - app_log_to_syslog() logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="tw_asana_configs") - return 0 - - # find token to connect to asana --------------------------------------------------------- - token = os.environ.get("ASANA_PERSONAL_ACCESS_TOKEN") - if token is None and token_pass_path is None: - # TODO Re-write this. - # Hacky way of accessing the parameter naem for the token pass path. - name = opt_asana_token_pass_path()(lambda: None).__click_params__[0].opts[-1] - error_and_exit( - f"You must provide an Asana Personal Access Token, using the {name} flag" - ) - if token is not None: - logger.debug( - "Reading the Asana Personal Access Token (PAT) from environment variable..." - ) - else: - token = fetch_from_pass_manager(token_pass_path) - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_asana_workspace = any( - [tw_project, tw_tags, asana_workspace_gid, asana_workspace_name] + + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] + + combination_of_tw_filters_and_asana_workspace = any( + [ + tw_filter_li, + tw_tags, + tw_project, + tw_sync_all_tasks, + asana_workspace_gid, + asana_workspace_name, + ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_asana_workspace + combination_name, combination_of_tw_filters_and_asana_workspace ) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_asana_configs", combination=combination_name + side_A_name="Taskwarrior", side_B_name="Asana", combination=combination_name ) tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] asana_workspace_gid = app_config["asana_workspace_gid"] - if "asana_task_gid" in app_config: - asana_task_gid = app_config["asana_task_gid"] + asana_task_gid = app_config["asana_task_gid"] + # combination manually specified ---------------------------------------------------------- + else: + inform_about_config = True + combination_name = cache_or_reuse_cached_combination( + config_args={ + "asana_workspace_gid": asana_workspace_gid, + "tw_project": tw_project, + "tw_tags": tw_tags, + "asana_task_gid": asana_task_gid, + }, + config_fname="tw_asana_configs", + custom_combination_savename=custom_combination_savename, + ) # initialize asana ----------------------------------------------------------------------- - client = asana.Client.access_token(token) - asana_disable = client.headers.get("Asana-Disable", "") - client.headers["Asana-Disable"] = ",".join( - [client.headers.get("Asana-Disable", ""), "new_user_task_lists"] + asana_client = asana.Client.access_token(asana_token) + asana_disable = asana_client.headers.get("Asana-Disable", "") + asana_client.headers["Asana-Disable"] = ",".join( + [asana_client.headers.get("Asana-Disable", ""), "new_user_task_lists"] ) - client.options["client_name"] = "syncall" + asana_client.options["client_name"] = "syncall" + + # list workspaces and exit + if do_list_asana_workspaces: + list_asana_workspaces(asana_client) + return 0 # asana workspaces------------------------------------------------------------------------- # Validate Asana workspace selection. Skip this only if we are going to # --list-asana-workspaces or if --asana-task-gid was not specified. - if asana_task_gid is None and not do_list_asana_workspaces: - if asana_workspace_gid is None and asana_workspace_name is None: - error_and_exit("Provide either an Asana workspace name or GID to sync.") - - if asana_workspace_gid is not None and asana_workspace_name is not None: - error_and_exit("Provide either Asana workspace GID or name, but not both.") + if asana_task_gid is None: + if asana_workspace_gid is None: + if asana_workspace_name is None: + error_and_exit("Provide either an Asana workspace name or GID to sync.") + else: + if asana_workspace_name is not None: + error_and_exit("Provide either Asana workspace GID or name, but not both.") found_workspace = False - for workspace in client.workspaces.find_all(): + for workspace in asana_client.workspaces.find_all(): if workspace["gid"] == asana_workspace_gid: asana_workspace_name = workspace["name"] found_workspace = True @@ -151,105 +147,86 @@ def main( if workspace["name"] == asana_workspace_name: if found_workspace: error_and_exit( - "Found multiple workspaces with the provided name. Please specify" - " workspace GID instead." + f"Found multiple workspaces with name {asana_workspace_name}. Please" + " specify workspace GID instead." ) else: asana_workspace_gid = workspace["gid"] found_workspace = True - - if not found_workspace: + else: if asana_workspace_gid: error_and_exit( - "No Asana workspace was found with a GID matching the one provided." + f"No Asana workspace was found with GID {asana_workspace_gid} ." ) if asana_workspace_name: error_and_exit( - "No Asana workspace was found with a name matching the one provided." + f"No Asana workspace was found with name {asana_workspace_name} ." ) - # combination manually specified ---------------------------------------------------------- - if combination_name is None: - config_args = { - "asana_workspace_gid": asana_workspace_gid, - "tw_project": tw_project, - "tw_tags": tw_tags, - } - if asana_task_gid is not None: - config_args["asana_task_gid"] = asana_task_gid - inform_about_config = True - combination_name = cache_or_reuse_cached_combination( - config_args, - config_fname="tw_asana_configs", - custom_combination_savename=custom_combination_savename, - ) - - # at least one of tw_tags, tw_project should be set --------------------------------------- - if not do_list_asana_workspaces and not tw_tags and not tw_project: - error_and_exit( - "You have to provide at least one valid tag or a valid project ID to use for" - " the synchronization" - ) + # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) # announce configuration ------------------------------------------------------------------ - announce_items = { - "TW Tags": tw_tags, - "TW Project": tw_project, - "Asana Workspace GID": asana_workspace_gid, - "Asana Workspace Name": asana_workspace_name, - } - if asana_task_gid is not None: - announce_items["Asana Task GID"] = asana_task_gid logger.info( format_dict( header="Configuration", - items=announce_items, + items={ + "TW Filter": " ".join(tw_filter_li), + "TW Tags": tw_tags, + "TW Project": tw_project, + "TW Sync All Tasks": tw_sync_all_tasks, + "Asana Workspace GID": asana_workspace_gid, + "Asana Workspace Name": asana_workspace_name, + "Asana Task GID": asana_task_gid, + }, prefix="\n\n", suffix="\n", ) ) - # initialize taskwarrior ------------------------------------------------------------------ - tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project) - - if do_list_asana_workspaces: - list_asana_workspaces(client) - return 0 + # initialize sides ------------------------------------------------------------------------ + tw_side = TaskWarriorSide( + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project + ) asana_side = AsanaSide( - client=client, task_gid=asana_task_gid, workspace_gid=asana_workspace_gid + client=asana_client, task_gid=asana_task_gid, workspace_gid=asana_workspace_gid + ) + + # 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, ) # sync ------------------------------------------------------------------------------------ - try: - with Aggregator( - side_A=asana_side, - side_B=tw_side, - converter_A_to_B=convert_asana_to_tw, - converter_B_to_A=convert_tw_to_asana, - resolution_strategy=get_resolution_strategy( - resolution_strategy, side_A_type=type(asana_side), side_B_type=type(tw_side) + with Aggregator( + side_A=asana_side, + side_B=tw_side, + converter_A_to_B=convert_asana_to_tw, + converter_B_to_A=convert_tw_to_asana, + resolution_strategy=get_resolution_strategy( + resolution_strategy, side_A_type=type(asana_side), side_B_type=type(tw_side) + ), + config_fname=combination_name, + ignore_keys=( + ( + "completed_at", + "created_at", + "modified_at", ), - config_fname=combination_name, - ignore_keys=( - ( - "completed_at", - "created_at", - "modified_at", - ), - ("end", "entry", "modified", "urgency"), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - if inform_about_config: - inform_about_combination_name_usage(combination_name) + ("end", "entry", "modified", "urgency"), + ), + ) as aggregator: + aggregator.sync() return 0 diff --git a/syncall/scripts/tw_caldav_sync.py b/syncall/scripts/tw_caldav_sync.py index 5594d38..0778a98 100644 --- a/syncall/scripts/tw_caldav_sync.py +++ b/syncall/scripts/tw_caldav_sync.py @@ -1,5 +1,3 @@ -import atexit -import datetime import os import subprocess from typing import List, Optional @@ -7,7 +5,6 @@ import caldav import click from bubop import ( - ExitHooks, check_optional_mutually_exclusive, check_required_mutually_exclusive, format_dict, @@ -15,19 +12,7 @@ loguru_tqdm_sink, ) -from syncall import inform_about_app_extras -from syncall.app_utils import app_log_to_syslog, error_and_exit -from syncall.cli import ( - opt_caldav_calendar, - opt_caldav_passwd_cmd, - opt_caldav_passwd_pass_path, - opt_caldav_url, - opt_caldav_user, - opt_pdb_on_error, - opt_tw_all_tasks, - opt_tw_only_tasks_modified_30_days, -) -from syncall.tw_caldav_utils import convert_caldav_to_tw, convert_tw_to_caldav +from syncall.app_utils import inform_about_app_extras try: from syncall.caldav.caldav_side import CaldavSide @@ -35,60 +20,40 @@ except ImportError: inform_about_app_extras(["caldav", "tw"]) -from syncall import ( - Aggregator, - __version__, +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, fetch_from_pass_manager, get_resolution_strategy, - inform_about_combination_name_usage, - list_named_combinations, - opt_combination, - opt_custom_combination_savename, - opt_list_combinations, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, - report_toplevel_exception, + register_teardown_handler, ) +from syncall.cli import opts_caldav, opts_miscellaneous, opts_tw_filtering +from syncall.tw_caldav_utils import convert_caldav_to_tw, convert_tw_to_caldav @click.command() -# caldav options --------------------------------------------------------------------- -@opt_caldav_calendar() -@opt_caldav_url() -@opt_caldav_user() -@opt_caldav_passwd_pass_path() -@opt_caldav_passwd_cmd() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_all_tasks() -@opt_tw_tags() -@opt_tw_project() -@opt_tw_only_tasks_modified_30_days() -# misc options -------------------------------------------------------------------------------- -@opt_list_combinations("TW", "Caldav") -@opt_resolution_strategy() -@opt_combination("TW", "Caldav") -@opt_custom_combination_savename("TW", "Caldav") -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) -@opt_pdb_on_error() +@opts_caldav() +@opts_tw_filtering() +@opts_miscellaneous("TW", "Caldav") def main( caldav_calendar: str, caldav_url: str, caldav_user: Optional[str], caldav_passwd_pass_path: str, caldav_passwd_cmd: str, - tw_sync_all_tasks: bool, + tw_filter: str, tw_tags: List[str], tw_project: str, - tw_only_modified_last_30_days: bool, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, + prefer_scheduled_date: bool, + resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - resolution_strategy: str, - do_list_combinations: bool, pdb_on_error: bool, ): """Synchronize lists of tasks from your caldav Calendar with filters from Taskwarrior. @@ -99,44 +64,46 @@ def main( The calendar in Caldav 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 - if do_list_combinations: - list_named_combinations(config_fname="tw_caldav_configs") - return 0 + # cli validation -------------------------------------------------------------------------- + check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_tags_and_tw_project = any([tw_tags, tw_project]) - check_required_mutually_exclusive( - tw_sync_all_tasks, - combination_of_tw_tags_and_tw_project, - "All TW Tasks", - "TW Tags + Projects", - ) + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] - check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_caldav_calendar = any( + combination_of_tw_filters_and_caldav_calendar = any( [ - tw_project, + tw_filter_li, tw_tags, + tw_project, tw_sync_all_tasks, caldav_calendar, ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_caldav_calendar + combination_name, combination_of_tw_filters_and_caldav_calendar ) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_caldav_configs", combination=combination_name + side_A_name="Taskwarrior", side_B_name="Caldav", combination=combination_name ) + tw_filter_li = app_config["tw_filter_li"] tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] caldav_calendar = app_config["caldav_calendar"] # combination manually specified ---------------------------------------------------------- @@ -145,24 +112,34 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "caldav_calendar": caldav_calendar, + "tw_filter_li": tw_filter_li, "tw_project": tw_project, "tw_tags": tw_tags, - "tw_sync_all_tasks": tw_sync_all_tasks, - "tw_only_modified_last_30_days": tw_only_modified_last_30_days, }, config_fname="tw_caldav_configs", custom_combination_savename=custom_combination_savename, ) + # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) + # announce configuration ------------------------------------------------------------------ logger.info( format_dict( header="Configuration", items={ + "TW Filter": " ".join(tw_filter_li), "TW Tags": tw_tags, "TW Project": tw_project, "TW Sync All Tasks": tw_sync_all_tasks, "Caldav Calendar": caldav_calendar, + "Prefer scheduled dates": prefer_scheduled_date, }, prefix="\n\n", suffix="\n", @@ -170,14 +147,9 @@ def main( ) # initialize sides ------------------------------------------------------------------------ - # TW - # TODO abstract this - only_modified_since = None - if tw_only_modified_last_30_days: - only_modified_since = datetime.datetime.now() - datetime.timedelta(days=30) - + # tw tw_side = TaskWarriorSide( - tags=tw_tags, project=tw_project, only_modified_since=only_modified_since + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project ) # caldav @@ -213,27 +185,12 @@ def main( caldav_side = CaldavSide(client=client, calendar_name=caldav_calendar) # teardown function and exception handling ------------------------------------------------ - hooks: ExitHooks = ExitHooks() - - def teardown(): - if hooks.exception is not None: - if hooks.exception.__class__ is KeyboardInterrupt: - logger.error("C-c pressed, exiting...") - else: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - if inform_about_config: - inform_about_combination_name_usage(combination_name) - - if pdb_on_error: - logger.warning( - "pdb_on_error is enabled. Disabling exit hooks / not taking actions at the end " - "of the run." - ) - else: - hooks.register() - atexit.register(teardown) + register_teardown_handler( + pdb_on_error=pdb_on_error, + inform_about_config=inform_about_config, + combination_name=combination_name, + verbose=verbose, + ) # sync ------------------------------------------------------------------------------------ with Aggregator( diff --git a/syncall/scripts/tw_gcal_sync.py b/syncall/scripts/tw_gcal_sync.py index 76edf5c..d7b6d6f 100644 --- a/syncall/scripts/tw_gcal_sync.py +++ b/syncall/scripts/tw_gcal_sync.py @@ -3,76 +3,64 @@ from typing import List import click -from bubop import check_optional_mutually_exclusive, format_dict, logger, loguru_tqdm_sink +from bubop import ( + check_optional_mutually_exclusive, + check_required_mutually_exclusive, + format_dict, + logger, + loguru_tqdm_sink, +) -from syncall import inform_about_app_extras -from syncall.app_utils import app_log_to_syslog +from syncall.app_utils import inform_about_app_extras try: - from syncall import GCalSide, TaskWarriorSide + from syncall.google.gcal_side import GCalSide + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide except ImportError: inform_about_app_extras(["google", "tw"]) -from syncall import ( - Aggregator, - __version__, +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_gcal_to_tw, - convert_tw_to_gcal, fetch_app_configuration, get_resolution_strategy, - inform_about_combination_name_usage, - list_named_combinations, - report_toplevel_exception, + register_teardown_handler, ) from syncall.cli import ( - opt_combination, - opt_custom_combination_savename, opt_default_duration_event_mins, opt_gcal_calendar, opt_google_oauth_port, opt_google_secret_override, - opt_list_combinations, - opt_list_resolution_strategies, - opt_prefer_scheduled_date, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, + opts_miscellaneous, + opts_tw_filtering, ) +from syncall.tw_gcal_utils import convert_gcal_to_tw, convert_tw_to_gcal @click.command() -# google calendar options --------------------------------------------------------------------- @opt_gcal_calendar() @opt_google_secret_override() @opt_google_oauth_port() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_tags() -@opt_tw_project() -# misc options -------------------------------------------------------------------------------- -@opt_list_combinations("TW", "Google Calendar") -@opt_list_resolution_strategies() -@opt_resolution_strategy() -@opt_combination("TW", "Google Calendar") -@opt_custom_combination_savename("TW", "Google Calendar") -@opt_prefer_scheduled_date() +@opts_tw_filtering() @opt_default_duration_event_mins() -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_miscellaneous(side_A_name="TW", side_B_name="Google Tasks") def main( gcal_calendar: str, google_secret: str, oauth_port: int, + tw_filter: str, tw_tags: List[str], tw_project: str, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, + prefer_scheduled_date: bool, resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, - list_resolution_strategies: bool, # type: ignore - prefer_scheduled_date: bool, default_event_duration_mins: int, + pdb_on_error: bool, ): """Synchronize calendars from your Google Calendar with filters from Taskwarrior. @@ -85,30 +73,43 @@ def main( logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="tw_gcal_configs") - return 0 - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_gcal_calendar = any( + + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] + + combination_of_tw_filters_and_gcal_calendar = any( [ - tw_project, + tw_filter_li, tw_tags, + tw_project, + tw_sync_all_tasks, gcal_calendar, ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_gcal_calendar + combination_name, combination_of_tw_filters_and_gcal_calendar ) + check_optional_mutually_exclusive(combination_name, custom_combination_savename) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_gcal_configs", combination=combination_name + side_A_name="Taskwarrior", + side_B_name="Google Calendar", + combination=combination_name, ) + tw_filter_li = app_config["tw_filter_li"] tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] gcal_calendar = app_config["gcal_calendar"] # combination manually specified ---------------------------------------------------------- @@ -117,6 +118,7 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "gcal_calendar": gcal_calendar, + "tw_filter_li": tw_filter_li, "tw_project": tw_project, "tw_tags": tw_tags, }, @@ -124,16 +126,15 @@ def main( custom_combination_savename=custom_combination_savename, ) - # at least one of tw_tags, tw_project should be set --------------------------------------- - if not tw_tags and not tw_project: - logger.error( - "You have to provide at least one valid tag or a valid project ID to use for the" - " synchronization. You can do so either via CLI arguments or by specifying an" - " existing saved combination" - ) - sys.exit(1) - # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) + if gcal_calendar is None: logger.error( "You have to provide the name of a Google Calendar calendar to synchronize events" @@ -147,8 +148,10 @@ def main( format_dict( header="Configuration", items={ + "TW Filter": " ".join(tw_filter_li), "TW Tags": tw_tags, "TW Project": tw_project, + "TW Sync All Tasks": tw_sync_all_tasks, "Google Calendar": gcal_calendar, "Prefer scheduled dates": prefer_scheduled_date, }, @@ -158,12 +161,22 @@ def main( ) # initialize sides ------------------------------------------------------------------------ - tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project) + tw_side = TaskWarriorSide( + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project + ) gcal_side = GCalSide( calendar_summary=gcal_calendar, oauth_port=oauth_port, client_secret=google_secret ) + # teardown function and exception handling ------------------------------------------------ + register_teardown_handler( + pdb_on_error=pdb_on_error, + inform_about_config=inform_about_config, + combination_name=combination_name, + verbose=verbose, + ) + # take extra arguments into account ------------------------------------------------------- def convert_B_to_A(*args, **kargs): return convert_tw_to_gcal( @@ -185,31 +198,21 @@ def convert_A_to_B(*args, **kargs): convert_A_to_B.__doc__ = convert_gcal_to_tw.__doc__ # sync ------------------------------------------------------------------------------------ - try: - with Aggregator( - side_A=gcal_side, - side_B=tw_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(gcal_side), side_B_type=type(tw_side) - ), - config_fname=combination_name, - ignore_keys=( - (), - (), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - if inform_about_config: - inform_about_combination_name_usage(combination_name) + with Aggregator( + side_A=gcal_side, + side_B=tw_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(gcal_side), side_B_type=type(tw_side) + ), + config_fname=combination_name, + ignore_keys=( + (), + (), + ), + ) as aggregator: + aggregator.sync() return 0 diff --git a/syncall/scripts/tw_gkeep_sync.py b/syncall/scripts/tw_gkeep_sync.py index add5a1e..180a8b9 100644 --- a/syncall/scripts/tw_gkeep_sync.py +++ b/syncall/scripts/tw_gkeep_sync.py @@ -2,74 +2,56 @@ from typing import Sequence import click -from bubop import check_optional_mutually_exclusive, format_dict, logger, loguru_tqdm_sink - -from syncall import inform_about_app_extras -from syncall.app_utils import ( - app_log_to_syslog, - gkeep_read_username_password_token, - write_to_pass_manager, +from bubop import ( + check_optional_mutually_exclusive, + check_required_mutually_exclusive, + format_dict, + logger, + loguru_tqdm_sink, ) +from syncall.app_utils import inform_about_app_extras + try: - from syncall import GKeepTodoSide, TaskWarriorSide + from syncall.google.gkeep_todo_side import GKeepTodoSide + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide except ImportError: inform_about_app_extras(["gkeep", "tw"]) -from syncall import ( - Aggregator, - __version__, +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_gkeep_todo_to_tw, - convert_tw_to_gkeep_todo, fetch_app_configuration, get_resolution_strategy, + gkeep_read_username_password_token, inform_about_combination_name_usage, - list_named_combinations, - report_toplevel_exception, -) -from syncall.cli import ( - opt_combination, - opt_custom_combination_savename, - opt_gkeep_note, - opt_gkeep_passwd_pass_path, - opt_gkeep_token_pass_path, - opt_gkeep_user_pass_path, - opt_list_combinations, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, + register_teardown_handler, + write_to_pass_manager, ) +from syncall.cli import opts_gkeep, opts_miscellaneous, opts_tw_filtering +from syncall.tw_gkeep_utils import convert_gkeep_todo_to_tw, convert_tw_to_gkeep_todo @click.command() -# google keep options --------------------------------------------------------------------- -@opt_gkeep_note() -@opt_gkeep_user_pass_path() -@opt_gkeep_passwd_pass_path() -@opt_gkeep_token_pass_path() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_tags() -@opt_tw_project() -# misc options -------------------------------------------------------------------------------- -@opt_list_combinations("TW", "Google Keep") -@opt_resolution_strategy() -@opt_combination("TW", "Google Keep") -@opt_custom_combination_savename("TW", "Google Keep") -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_gkeep() +@opts_tw_filtering() +@opts_miscellaneous(side_A_name="TW", side_B_name="Google Keep") def main( gkeep_note: str, gkeep_user_pass_path: str, gkeep_passwd_pass_path: str, gkeep_token_pass_path: str, + tw_filter: str, tw_tags: Sequence[str], tw_project: str, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, + pdb_on_error: bool, ): """Synchronize Notes from your Google Keep with filters from Taskwarrior. @@ -88,30 +70,40 @@ def main( logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="tw_gkeep_configs") - return 0 - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_gkeep_note = any( + + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] + + combination_of_tw_filters_and_gkeep_note = any( [ - tw_project, + tw_filter_li, tw_tags, + tw_project, + tw_sync_all_tasks, gkeep_note, ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_gkeep_note + combination_name, combination_of_tw_filters_and_gkeep_note ) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_gkeep_configs", combination=combination_name + side_A_name="Taskwarrior", side_B_name="Google Keep", combination=combination_name ) + tw_filter_li = app_config["tw_filter_li"] tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] gkeep_note = app_config["gkeep_note"] # combination manually specified ---------------------------------------------------------- @@ -120,6 +112,7 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "gkeep_note": gkeep_note, + "tw_filter_li": tw_filter_li, "tw_project": tw_project, "tw_tags": tw_tags, }, @@ -127,12 +120,20 @@ def main( custom_combination_savename=custom_combination_savename, ) - # at least one of tw_tags, tw_project should be set --------------------------------------- - if not tw_tags and not tw_project: + # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) + + if gkeep_note is None: logger.error( - "You have to provide at least one valid tag or a valid project ID to use for the" - " synchronization. You can do so either via CLI arguments or by specifying an" - " existing saved combination" + "You have to provide the name of a Google Keep note to synchronize items" + " to/from. You can do so either via CLI arguments or by specifying an existing" + " saved combination" ) sys.exit(1) @@ -141,8 +142,10 @@ def main( format_dict( header="Configuration", items={ + "TW Filter": " ".join(tw_filter_li), "TW Tags": tw_tags, "TW Project": tw_project, + "TW Sync All Tasks": tw_sync_all_tasks, "Google Keep Note": gkeep_note, }, prefix="\n\n", @@ -167,33 +170,36 @@ def main( ) # initialize taskwarrior ------------------------------------------------------------------ - tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project) + tw_side = TaskWarriorSide( + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project + ) + + # 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, + ) # sync ------------------------------------------------------------------------------------ - try: - with Aggregator( - side_A=gkeep_side, - side_B=tw_side, - converter_B_to_A=convert_tw_to_gkeep_todo, - converter_A_to_B=convert_gkeep_todo_to_tw, - resolution_strategy=get_resolution_strategy( - resolution_strategy, side_A_type=type(gkeep_side), side_B_type=type(tw_side) - ), - config_fname=combination_name, - ignore_keys=( - (), - ("due", "end", "entry", "modified", "urgency"), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - # cache the token + with Aggregator( + side_A=gkeep_side, + side_B=tw_side, + converter_B_to_A=convert_tw_to_gkeep_todo, + converter_A_to_B=convert_gkeep_todo_to_tw, + resolution_strategy=get_resolution_strategy( + resolution_strategy, side_A_type=type(gkeep_side), side_B_type=type(tw_side) + ), + config_fname=combination_name, + ignore_keys=( + (), + ("due", "end", "entry", "modified", "urgency"), + ), + ) as aggregator: + aggregator.sync() + + # cache the token ------------------------------------------------------------------------- token = gkeep_side.get_master_token() if token is not None: logger.debug(f"Caching the gkeep token in pass -> {gkeep_token_pass_path}...") diff --git a/syncall/scripts/tw_gtasks_sync.py b/syncall/scripts/tw_gtasks_sync.py index c5af9e6..caea26a 100644 --- a/syncall/scripts/tw_gtasks_sync.py +++ b/syncall/scripts/tw_gtasks_sync.py @@ -1,115 +1,109 @@ -import sys from typing import List import click from bubop import ( check_optional_mutually_exclusive, + check_required_mutually_exclusive, format_dict, - log_to_syslog, logger, loguru_tqdm_sink, ) -from syncall import inform_about_app_extras +from syncall.app_utils import inform_about_app_extras try: - from syncall import GTasksSide, TaskWarriorSide + from syncall.google.gtasks_side import GTasksSide + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide except ImportError: inform_about_app_extras(["google", "tw"]) -from syncall import ( - Aggregator, - __version__, +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_gtask_to_tw, - convert_tw_to_gtask, + error_and_exit, fetch_app_configuration, get_resolution_strategy, - inform_about_combination_name_usage, - list_named_combinations, - report_toplevel_exception, + register_teardown_handler, ) from syncall.cli import ( - opt_combination, - opt_custom_combination_savename, opt_google_oauth_port, opt_google_secret_override, opt_gtasks_list, - opt_list_combinations, - opt_list_resolution_strategies, - opt_prefer_scheduled_date, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, + opts_miscellaneous, + opts_tw_filtering, ) +from syncall.tw_gtasks_utils import convert_gtask_to_tw, convert_tw_to_gtask @click.command() -# google tasks options --------------------------------------------------------------------- @opt_gtasks_list() @opt_google_secret_override() @opt_google_oauth_port() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_tags() -@opt_tw_project() -# misc options -------------------------------------------------------------------------------- -@opt_list_combinations("TW", "Google Tasks") -@opt_list_resolution_strategies() -@opt_resolution_strategy() -@opt_combination("TW", "Google Tasks") -@opt_custom_combination_savename("TW", "Google Tasks") -@opt_prefer_scheduled_date() -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_tw_filtering() +@opts_miscellaneous(side_A_name="TW", side_B_name="Google Tasks") def main( gtasks_list: str, google_secret: str, oauth_port: int, + tw_filter: str, tw_tags: List[str], tw_project: str, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, + prefer_scheduled_date: bool, resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, - list_resolution_strategies: bool, # type: ignore - prefer_scheduled_date: bool, + pdb_on_error: bool, ): """Synchronize lists from your Google Tasks with filters from Taskwarrior. - The list of TW tasks is determined by a combination of TW tags and a TW project while the - list in GTasks should be provided by their name. if it doesn't exist it will be crated + The list of TW tasks can be based on a TW project, tag, on the modification date or on an + arbitrary filter 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) - log_to_syslog(name="tw_gtasks_sync") + app_log_to_syslog() logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="tw_gtasks_configs") - return 0 - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_gtasks_list = any( + + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] + + combination_of_tw_filters_and_gtasks_list = any( [ - tw_project, + tw_filter_li, tw_tags, + tw_project, + tw_sync_all_tasks, gtasks_list, ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_gtasks_list + combination_name, combination_of_tw_filters_and_gtasks_list ) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_gtasks_configs", combination=combination_name + side_A_name="TW", side_B_name="Google Tasks", combination=combination_name ) + tw_filter_li = app_config["tw_filter_li"] tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] gtasks_list = app_config["gtasks_list"] # combination manually specified ---------------------------------------------------------- @@ -118,6 +112,7 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "gtasks_list": gtasks_list, + "tw_filter_li": tw_filter_li, "tw_project": tw_project, "tw_tags": tw_tags, }, @@ -125,31 +120,31 @@ def main( custom_combination_savename=custom_combination_savename, ) - # at least one of tw_tags, tw_project should be set --------------------------------------- - if not tw_tags and not tw_project: - logger.error( - "You have to provide at least one valid tag or a valid project ID to use for the" - " synchronization. You can do so either via CLI arguments or by specifying an" - " existing saved combination" - ) - sys.exit(1) - # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) + if gtasks_list is None: - logger.error( + 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" ) - sys.exit(1) # announce configuration ------------------------------------------------------------------ logger.info( format_dict( header="Configuration", items={ + "TW Filter": " ".join(tw_filter_li), "TW Tags": tw_tags, "TW Project": tw_project, + "TW Sync All Tasks": tw_sync_all_tasks, "Google Tasks": gtasks_list, "Prefer scheduled dates": prefer_scheduled_date, }, @@ -159,12 +154,25 @@ def main( ) # initialize sides ------------------------------------------------------------------------ - tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project) + # NOTE: We don't explicitly pass the tw_sync_all_tasks boolean. However we implicitly do by + # verifying beforehand that if this flag is specified the user cannot specify any of the + # other `tw_filter_li`, `tw_tags`, `tw_project` options. + tw_side = TaskWarriorSide( + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project + ) - gtask_side = GTasksSide( + gtasks_side = GTasksSide( task_list_title=gtasks_list, oauth_port=oauth_port, client_secret=google_secret ) + # teardown function and exception handling ------------------------------------------------ + register_teardown_handler( + pdb_on_error=pdb_on_error, + inform_about_config=inform_about_config, + combination_name=combination_name, + verbose=verbose, + ) + # take extra arguments into account ------------------------------------------------------- def convert_B_to_A(*args, **kargs): return convert_tw_to_gtask( @@ -184,31 +192,21 @@ def convert_A_to_B(*args, **kargs): convert_A_to_B.__doc__ = convert_gtask_to_tw.__doc__ # sync ------------------------------------------------------------------------------------ - try: - with Aggregator( - side_A=gtask_side, - side_B=tw_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(gtask_side), side_B_type=type(tw_side) - ), - config_fname=combination_name, - ignore_keys=( - (), - (), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - if inform_about_config: - inform_about_combination_name_usage(combination_name) + with Aggregator( + side_A=gtasks_side, + side_B=tw_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(tw_side) + ), + config_fname=combination_name, + ignore_keys=( + (), + (), + ), + ) as aggregator: + aggregator.sync() return 0 diff --git a/syncall/scripts/tw_notion_sync.py b/syncall/scripts/tw_notion_sync.py index 5c98ad0..69b9dad 100644 --- a/syncall/scripts/tw_notion_sync.py +++ b/syncall/scripts/tw_notion_sync.py @@ -1,4 +1,3 @@ -"""Console script for notion_taskwarrior.""" import os import sys from typing import List @@ -6,78 +5,66 @@ import click from bubop import ( check_optional_mutually_exclusive, + check_required_mutually_exclusive, format_dict, logger, loguru_tqdm_sink, verbosity_int_to_std_logging_lvl, ) -from syncall import inform_about_app_extras -from syncall.app_utils import app_log_to_syslog +from syncall.app_utils import fetch_from_pass_manager, inform_about_app_extras try: - from syncall import NotionSide, TaskWarriorSide + from syncall.notion.notion_side import NotionSide + from syncall.taskwarrior.taskwarrior_side import TaskWarriorSide except ImportError: inform_about_app_extras(["notion", "tw"]) - from notion_client import Client # type: ignore -from syncall import ( - Aggregator, - __version__, +from syncall.aggregator import Aggregator +from syncall.app_utils import ( + app_log_to_syslog, cache_or_reuse_cached_combination, - convert_notion_to_tw, - convert_tw_to_notion, + error_and_exit, fetch_app_configuration, - fetch_from_pass_manager, get_resolution_strategy, - inform_about_combination_name_usage, - list_named_combinations, - report_toplevel_exception, + register_teardown_handler, ) from syncall.cli import ( - opt_combination, - opt_custom_combination_savename, - opt_list_combinations, opt_notion_page_id, opt_notion_token_pass_path, - opt_resolution_strategy, - opt_tw_project, - opt_tw_tags, + opts_miscellaneous, + opts_tw_filtering, ) +from syncall.tw_notion_utils import convert_notion_to_tw, convert_tw_to_notion # CLI parsing --------------------------------------------------------------------------------- @click.command() -# Notion options ------------------------------------------------------------------------------ @opt_notion_page_id() @opt_notion_token_pass_path() -# taskwarrior options ------------------------------------------------------------------------- -@opt_tw_tags() -@opt_tw_project() -# misc options -------------------------------------------------------------------------------- -@opt_resolution_strategy() -@opt_combination("TW", "Notion") -@opt_list_combinations("TW", "Notion") -@opt_custom_combination_savename("TW", "Notion") -@click.option("-v", "--verbose", count=True) -@click.version_option(__version__) +@opts_tw_filtering() +@opts_miscellaneous("TW", "Notion") def main( notion_page_id: str, + token_pass_path: str, + tw_filter: str, tw_tags: List[str], tw_project: str, - token_pass_path: str, + tw_only_modified_last_X_days: str, + tw_sync_all_tasks: bool, + prefer_scheduled_date: bool, resolution_strategy: str, verbose: int, combination_name: str, custom_combination_savename: str, - do_list_combinations: bool, + pdb_on_error: bool, ): - """Synchronise filters of TW tasks with the to_do items of Notion pages + """Synchronise filters of TW tasks with the to_do items of Notion pages. - The list of TW tasks is determined by a combination of TW tags and TW project while the - notion pages should be provided by their URLs. + The list of TW tasks can be based on a TW project, tag, on the modification date or on an + arbitrary filter while the notion pages should be provided by their URLs. """ # setup logger ---------------------------------------------------------------------------- loguru_tqdm_sink(verbosity=verbose) @@ -85,30 +72,40 @@ def main( logger.debug("Initialising...") inform_about_config = False - if do_list_combinations: - list_named_combinations(config_fname="tw_notion_configs") - return 0 - # cli validation -------------------------------------------------------------------------- check_optional_mutually_exclusive(combination_name, custom_combination_savename) - combination_of_tw_project_tags_and_notion_page = any( + + tw_filter_li = [ + t + for t in [ + tw_filter, + tw_only_modified_last_X_days, + ] + if t + ] + + combination_of_tw_filters_and_notion_page = any( [ - tw_project, + tw_filter_li, tw_tags, + tw_project, + tw_sync_all_tasks, notion_page_id, ] ) check_optional_mutually_exclusive( - combination_name, combination_of_tw_project_tags_and_notion_page + combination_name, combination_of_tw_filters_and_notion_page ) # existing combination name is provided --------------------------------------------------- if combination_name is not None: app_config = fetch_app_configuration( - config_fname="tw_notion_configs", combination=combination_name + side_A_name="Taskwarrior", side_B_name="Notion", combination=combination_name ) + tw_filter_li = app_config["tw_filter_li"] tw_tags = app_config["tw_tags"] tw_project = app_config["tw_project"] + tw_sync_all_tasks = app_config["tw_sync_all_tasks"] notion_page_id = app_config["notion_page_id"] # combination manually specified ---------------------------------------------------------- @@ -117,6 +114,7 @@ def main( combination_name = cache_or_reuse_cached_combination( config_args={ "notion_page_id": notion_page_id, + "tw_filter_li": tw_filter_li, "tw_project": tw_project, "tw_tags": tw_tags, }, @@ -124,29 +122,32 @@ def main( custom_combination_savename=custom_combination_savename, ) - # at least one of tw_tags, tw_project should be set --------------------------------------- - if not tw_tags and not tw_project: - raise RuntimeError( - "You have to provide at least one valid tag or a valid project ID to use for" - " the synchronization" - ) - # more checks ----------------------------------------------------------------------------- + combination_of_tw_related_options = any([tw_filter_li, tw_tags, tw_project]) + check_required_mutually_exclusive( + tw_sync_all_tasks, + combination_of_tw_related_options, + "sync_all_tw_tasks", + "combination of specific TW-related options", + ) + if notion_page_id is None: - logger.error( + error_and_exit( "You have to provide the page ID of the Notion page for synchronization. You can" " do so either via CLI arguments or by specifying an existing saved combination" ) - sys.exit(1) # announce configuration ------------------------------------------------------------------ logger.info( format_dict( header="Configuration", items={ + "TW Filter": " ".join(tw_filter_li), "TW Tags": tw_tags, "TW Project": tw_project, + "TW Sync All Tasks": tw_sync_all_tasks, "Notion Page ID": notion_page_id, + "Prefer scheduled dates": prefer_scheduled_date, }, prefix="\n\n", suffix="\n", @@ -170,10 +171,21 @@ def main( assert token_v2 - # initialize taskwarrior ------------------------------------------------------------------ - tw_side = TaskWarriorSide(tags=tw_tags, project=tw_project) + # 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, + ) + + # initialize sides ------------------------------------------------------------------------ + # tw + tw_side = TaskWarriorSide( + tw_filter=" ".join(tw_filter_li), tags=tw_tags, project=tw_project + ) - # initialize notion ----------------------------------------------------------------------- + # notion # client is a bit too verbose by default. client_verbosity = max(verbose - 1, 0) client = Client( @@ -182,31 +194,21 @@ def main( notion_side = NotionSide(client=client, page_id=notion_page_id) # sync ------------------------------------------------------------------------------------ - try: - with Aggregator( - side_A=notion_side, - side_B=tw_side, - converter_B_to_A=convert_tw_to_notion, - converter_A_to_B=convert_notion_to_tw, - resolution_strategy=get_resolution_strategy( - resolution_strategy, side_A_type=type(notion_side), side_B_type=type(tw_side) - ), - config_fname=combination_name, - ignore_keys=( - ("last_modified_date",), - ("due", "end", "entry", "modified", "urgency"), - ), - ) as aggregator: - aggregator.sync() - except KeyboardInterrupt: - logger.error("Exiting...") - return 1 - except: - report_toplevel_exception(is_verbose=verbose >= 1) - return 1 - - if inform_about_config: - inform_about_combination_name_usage(combination_name) + with Aggregator( + side_A=notion_side, + side_B=tw_side, + converter_B_to_A=convert_tw_to_notion, + converter_A_to_B=convert_notion_to_tw, + resolution_strategy=get_resolution_strategy( + resolution_strategy, side_A_type=type(notion_side), side_B_type=type(tw_side) + ), + config_fname=combination_name, + ignore_keys=( + ("last_modified_date",), + ("due", "end", "entry", "modified", "urgency"), + ), + ) as aggregator: + aggregator.sync() return 0 diff --git a/syncall/taskwarrior/taskwarrior_side.py b/syncall/taskwarrior/taskwarrior_side.py index dd4ccba..88b9464 100644 --- a/syncall/taskwarrior/taskwarrior_side.py +++ b/syncall/taskwarrior/taskwarrior_side.py @@ -1,23 +1,11 @@ import datetime from pathlib import Path -from typing import ( - Any, - Callable, - Dict, - List, - Literal, - Mapping, - Optional, - Sequence, - Set, - Union, - cast, -) +from typing import Any, Dict, List, Literal, Mapping, Optional, Sequence, Set, Union, cast from uuid import UUID -from bubop import assume_local_tz_if_none, logger, parse_datetime -from taskw import TaskWarrior -from taskw.warrior import TASKRC +from bubop import logger, parse_datetime +from taskw_ng import TaskWarrior +from taskw_ng.warrior import TASKRC from xdg import xdg_config_home from syncall.sync_side import ItemType, SyncSide @@ -59,7 +47,7 @@ def __init__( self, tags: Sequence[str] = tuple(), project: Optional[str] = None, - only_modified_since: Optional[datetime.datetime] = None, + tw_filter: str = "", config_file_override: Optional[Path] = None, config_overrides: Mapping[str, Any] = {}, **kargs, @@ -67,9 +55,12 @@ def __init__( """ Constructor. - :param tags: Only include tasks that have are tagged using *all* the specified tags - :param project: Only include tasks that include in this project - :param only_modified_since: Only include tasks that are modified since the specified date + :param tags: Only include tasks that have are tagged using *all* the specified tags. + Also assign these tags to newly added items + :param project: Only include tasks that include in this project. Also assign newly + added items to this project. + :param tw_filter: Arbitrary taskwarrior filter to use for determining the list of tasks + to sync :param config_file: Path to the taskwarrior RC file :param config_overrides: Dictionary of taskrc key, values to override. See also tw_config_default_overrides @@ -77,6 +68,7 @@ def __init__( super().__init__(name="Tw", fullname="Taskwarrior", **kargs) self._tags: Set[str] = set(tags) self._project: str = project or "" + self._tw_filter: str = tw_filter config_overrides_ = tw_config_default_overrides.copy() config_overrides_.update(config_overrides) @@ -114,8 +106,6 @@ def __init__( # Whether to refresh the cached list of items self._reload_items = True - self._only_modified_since = only_modified_since - def start(self): logger.info(f"Initializing {self.fullname}...") @@ -127,8 +117,15 @@ def _load_all_items(self): """ if not self._reload_items: return + filter_ = [*[f"+{tag}" for tag in self._tags]] + if self._tw_filter: + filter_.append(self._tw_filter) + if self._project: + filter_.append(f"pro:{self._project}") + filter_ = f'( {" or ".join(filter_)} )' + logger.debug(f"Using the following filter to fetch TW tasks: {filter_}") + tasks = self._tw.load_tasks_and_filter(command="all", filter_=filter_) - tasks = self._tw.load_tasks() items = [*tasks["completed"], *tasks["pending"]] self._items_cache: Dict[str, TaskwarriorRawItem] = { # type: ignore str(item["uuid"]): item for item in items @@ -157,46 +154,6 @@ def get_all_items( if skip_completed: tasks = [t for t in tasks if t["status"] != "completed"] # type: ignore - # filter the tasks based on their tags, project and modification date ----------------- - def create_tasks_filter() -> Callable[[TaskwarriorRawItem], bool]: - always_true = lambda task: True - fn = always_true - - tags_fn = lambda task: self._tags.issubset(task.get("tags", [])) - project_fn = lambda task: task.get("project", "").startswith(self._project) - - if self._only_modified_since: - mod_since_date = assume_local_tz_if_none(self._only_modified_since) - - def only_modified_since_fn(task): - mod_date = task.get("modified") - if mod_date is None: - logger.warning( - f'Task does not have a modification date {task["uuid"]}, this' - " sounds like a bug but including it anyway..." - ) - return True - - mod_date: datetime.datetime - mod_date = assume_local_tz_if_none(mod_date) - - if mod_since_date <= mod_date: - return True - - return False - - if self._tags: - fn = lambda task, fn=fn, tags_fn=tags_fn: fn(task) and tags_fn(task) - if self._project: - fn = lambda task, fn=fn, project_fn=project_fn: fn(task) and project_fn(task) - if self._only_modified_since: - fn = lambda task, fn=fn: fn(task) and only_modified_since_fn(task) - - return fn - - tasks_filter = create_tasks_filter() - tasks = [t for t in tasks if tasks_filter(t)] - for task in tasks: task["uuid"] = str(task["uuid"]) # type: ignore diff --git a/syncall/types.py b/syncall/types.py index 4475d44..ff44302 100644 --- a/syncall/types.py +++ b/syncall/types.py @@ -293,7 +293,7 @@ class AsanaRawTask(TypedDict): # Extras -------------------------------------------------------------------------------------- -# Task as returned from taskw.get_task(id=...) +# Task as returned from get_task(id=...) # TODO Are these types needed? They seem to be duplicates of TaskwarriorRawItem ... TwRawItem = Tuple[Optional[int], Dict[str, Any]] TwItem = Dict[str, Any] diff --git a/tests/test_app_utils.py b/tests/test_app_utils.py index a6f6f6c..16a1135 100644 --- a/tests/test_app_utils.py +++ b/tests/test_app_utils.py @@ -3,13 +3,13 @@ import pytest -from syncall import ( +from syncall.app_utils import ( cache_or_reuse_cached_combination, fetch_app_configuration, inform_about_combination_name_usage, - list_named_combinations, report_toplevel_exception, ) +from syncall.cli import _list_named_combinations from syncall.constants import COMBINATION_FLAGS, ISSUES_URL @@ -18,7 +18,7 @@ def test_list_named_combinations(fs, caplog, mock_prefs_manager): "syncall.app_utils.PrefsManager", return_value=mock_prefs_manager, ): - list_named_combinations("doesnt matter") + _list_named_combinations("doesnt matter") captured = caplog.text assert all( expected_config in captured @@ -33,7 +33,9 @@ def test_list_named_combinations(fs, caplog, mock_prefs_manager): def test_fetch_app_configuration(fs, caplog, mock_prefs_manager): with patch("syncall.app_utils.PrefsManager", return_value=mock_prefs_manager): # invalid combination - config = fetch_app_configuration(config_fname="doesntmatter", combination="kalimera") + config = fetch_app_configuration( + side_A_name="side A", side_B_name="side B", combination="kalimera" + ) assert list(config.keys()) == ["a", "b", "c"] assert list(config.values()) == [1, 2, [1, 2, 3]] captured = caplog.text @@ -42,7 +44,9 @@ def test_fetch_app_configuration(fs, caplog, mock_prefs_manager): # invalid combination caplog.clear() with pytest.raises(RuntimeError): - fetch_app_configuration(config_fname="doesntmatter", combination="doesntexist") + fetch_app_configuration( + side_A_name="side A", side_B_name="side B", combination="doesntexist" + ) captured = caplog.text assert "No such configuration" in captured diff --git a/tests/test_tw_asana_conversions.py b/tests/test_tw_asana_conversions.py index ea8b529..712e896 100644 --- a/tests/test_tw_asana_conversions.py +++ b/tests/test_tw_asana_conversions.py @@ -2,8 +2,8 @@ import yaml -from syncall import convert_asana_to_tw, convert_tw_to_asana from syncall.asana.asana_task import AsanaTask +from syncall.tw_asana_utils import convert_asana_to_tw, convert_tw_to_asana from .generic_test_case import GenericTestCase diff --git a/tests/test_tw_gkeep.py b/tests/test_tw_gkeep.py index 7168670..ab1fc5b 100644 --- a/tests/test_tw_gkeep.py +++ b/tests/test_tw_gkeep.py @@ -1,7 +1,7 @@ import pytest from bubop.time import format_datetime_tz -from syncall import GKeepTodoItem +from syncall.google.gkeep_todo_item import GKeepTodoItem from syncall.tw_gkeep_utils import convert_gkeep_todo_to_tw, convert_tw_to_gkeep_todo from syncall.types import TwItem