Updated project structure
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -160,3 +160,6 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
.reclaim.toml
|
.reclaim.toml
|
||||||
|
things2reclaim/config/.toggl.toml
|
||||||
|
things2reclaim/config/.things2reclaim.toml
|
||||||
|
things2reclaim/data/tasks.db
|
||||||
|
|||||||
366
poetry.lock
generated
Normal file
366
poetry.lock
generated
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.4.0"
|
||||||
|
description = "High level compatibility layer for multiple asynchronous event loop implementations"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"},
|
||||||
|
{file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
idna = ">=2.8"
|
||||||
|
sniffio = ">=1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"]
|
||||||
|
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
|
||||||
|
trio = ["trio (>=0.23)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2024.6.2"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
|
||||||
|
{file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
|
||||||
|
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.14.0"
|
||||||
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
|
||||||
|
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "4.1.0"
|
||||||
|
description = "HTTP/2 State-Machine based protocol implementation"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
files = [
|
||||||
|
{file = "h2-4.1.0-py3-none-any.whl", hash = "sha256:03a46bcf682256c95b5fd9e9a99c1323584c3eec6440d379b9903d709476bc6d"},
|
||||||
|
{file = "h2-4.1.0.tar.gz", hash = "sha256:a83aca08fbe7aacb79fec788c9c0bac936343560ed9ec18b82a13a12c28d2abb"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
hpack = ">=4.0,<5"
|
||||||
|
hyperframe = ">=6.0,<7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hpack"
|
||||||
|
version = "4.0.0"
|
||||||
|
description = "Pure-Python HPACK header compression"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
files = [
|
||||||
|
{file = "hpack-4.0.0-py3-none-any.whl", hash = "sha256:84a076fad3dc9a9f8063ccb8041ef100867b1878b25ef0ee63847a5d53818a6c"},
|
||||||
|
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.5"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"},
|
||||||
|
{file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.13,<0.15"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
trio = ["trio (>=0.22.0,<0.26.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.27.0"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
||||||
|
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
certifi = "*"
|
||||||
|
h2 = {version = ">=3,<5", optional = true, markers = "extra == \"http2\""}
|
||||||
|
httpcore = "==1.*"
|
||||||
|
idna = "*"
|
||||||
|
sniffio = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli", "brotlicffi"]
|
||||||
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyperframe"
|
||||||
|
version = "6.0.1"
|
||||||
|
description = "HTTP/2 framing layer for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
files = [
|
||||||
|
{file = "hyperframe-6.0.1-py3-none-any.whl", hash = "sha256:0ec6bafd80d8ad2195c4f03aacba3a8265e57bc4cff261e802bf39970ed02a15"},
|
||||||
|
{file = "hyperframe-6.0.1.tar.gz", hash = "sha256:ae510046231dc8e9ecb1a6586f63d2347bf4c8905914aa84ba585ae85f28a914"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.7"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
files = [
|
||||||
|
{file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"},
|
||||||
|
{file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "3.0.0"
|
||||||
|
description = "Python port of markdown-it. Markdown parsing, done right!"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
|
||||||
|
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
mdurl = ">=0.1,<1.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
|
||||||
|
code-style = ["pre-commit (>=3.0,<4.0)"]
|
||||||
|
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
|
||||||
|
linkify = ["linkify-it-py (>=1,<3)"]
|
||||||
|
plugins = ["mdit-py-plugins"]
|
||||||
|
profiling = ["gprof2dot"]
|
||||||
|
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
|
||||||
|
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
description = "Markdown URL utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
|
||||||
|
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.18.0"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||||
|
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
description = "Extensions to the standard Python datetime module"
|
||||||
|
optional = false
|
||||||
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
|
files = [
|
||||||
|
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
|
||||||
|
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
six = ">=1.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2024.1"
|
||||||
|
description = "World timezone definitions, modern and historical"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
|
||||||
|
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reclaim-sdk"
|
||||||
|
version = "0.4.2"
|
||||||
|
description = "Unofficial Reclaim.ai Python API"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = []
|
||||||
|
develop = false
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
httpx = {version = "*", extras = ["http2"]}
|
||||||
|
python-dateutil = "*"
|
||||||
|
toml = "*"
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/cato447/reclaim-sdk"
|
||||||
|
reference = "HEAD"
|
||||||
|
resolved_reference = "95f2042e6c6da77b6d92577fa3274961afe412dc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "13.7.1"
|
||||||
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
files = [
|
||||||
|
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
||||||
|
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
markdown-it-py = ">=2.2.0"
|
||||||
|
pygments = ">=2.13.0,<3.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shellingham"
|
||||||
|
version = "1.5.4"
|
||||||
|
description = "Tool to Detect Surrounding Shell"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
|
||||||
|
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
files = [
|
||||||
|
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||||
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sniffio"
|
||||||
|
version = "1.3.1"
|
||||||
|
description = "Sniff out which async library your code is running under"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||||
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "things.py"
|
||||||
|
version = "0.0.15"
|
||||||
|
description = "A simple Python 3 library to read your Things app data."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = []
|
||||||
|
develop = false
|
||||||
|
|
||||||
|
[package.source]
|
||||||
|
type = "git"
|
||||||
|
url = "https://github.com/thingsapi/things.py"
|
||||||
|
reference = "HEAD"
|
||||||
|
resolved_reference = "85975337a4119fa80bf811e326eb863fce5f4905"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.10.2"
|
||||||
|
description = "Python Library for Tom's Obvious, Minimal Language"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
files = [
|
||||||
|
{file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
|
||||||
|
{file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typer"
|
||||||
|
version = "0.12.3"
|
||||||
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"},
|
||||||
|
{file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
rich = ">=10.11.0"
|
||||||
|
shellingham = ">=1.3.0"
|
||||||
|
typing-extensions = ">=3.7.4.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.12.2"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||||
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "2.0"
|
||||||
|
python-versions = "^3.12"
|
||||||
|
content-hash = "47c36617ea3af29526db5695b4d54427a61e041c2e8c0ce4340f9afceff32684"
|
||||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
[tool.poetry]
|
||||||
|
name = "task-automation"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Simon Bußmann <simon.bussmann@tum.de>"]
|
||||||
|
readme = "README.md"
|
||||||
|
packages = [{include = "things2reclaim"}]
|
||||||
|
include = [{path = "things2reclaim/data", format=["sdist", "wheel"]},{path = "things2reclaim/configs", format=["sdist", "wheel"]}]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.12"
|
||||||
|
pytz = "^2024.1"
|
||||||
|
things-py = {git = "https://github.com/thingsapi/things.py"}
|
||||||
|
reclaim-sdk = {git = "https://github.com/cato447/reclaim-sdk"}
|
||||||
|
rich = "^13.7.1"
|
||||||
|
typer = {extras = ["all"], version = "^0.12.3"}
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
task-automation = 'things2reclaim.things2reclaim:main'
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
#!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3
|
|
||||||
|
|
||||||
import re
|
|
||||||
from datetime import date, datetime, timedelta
|
|
||||||
from time import sleep
|
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
import things
|
|
||||||
import typer
|
|
||||||
from pytz import timezone
|
|
||||||
from reclaim_sdk.models.task import ReclaimTask
|
|
||||||
from rich import print as rprint
|
|
||||||
from rich.console import Console
|
|
||||||
from rich.table import Table
|
|
||||||
from rich.text import Text
|
|
||||||
from typing_extensions import Annotated
|
|
||||||
|
|
||||||
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
|
||||||
console = Console()
|
|
||||||
|
|
||||||
regex = (
|
|
||||||
r"((\d+\.?\d*) (hours|hrs|hour|hr|h))? ?((\d+\.?\d*) (mins|min|minutes|minute|m))?"
|
|
||||||
)
|
|
||||||
pattern = re.compile(regex)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_uni_projects():
|
|
||||||
uni_area = next(area for area in things.areas() if area["title"] == "Uni")
|
|
||||||
return things.projects(area=uni_area["uuid"])
|
|
||||||
|
|
||||||
|
|
||||||
def get_tasks_for_project(project) -> Dict | List[Dict]:
|
|
||||||
return things.tasks(project=project["uuid"], type="to-do")
|
|
||||||
|
|
||||||
|
|
||||||
def get_task_tags(things_task: Dict) -> Dict[str, str]:
|
|
||||||
return {k: v for (k, v) in [tag.split(": ") for tag in things_task["tags"]]}
|
|
||||||
|
|
||||||
|
|
||||||
def set_default_reclaim_values(things_task, reclaim_task):
|
|
||||||
tags_dict = get_task_tags(things_task)
|
|
||||||
estimated_time = tags_dict.get("EstimatedTime")
|
|
||||||
if estimated_time is None:
|
|
||||||
raise ValueError("EstimatedTime tag is required")
|
|
||||||
estimated_time = calculate_time_on_unit(estimated_time)
|
|
||||||
reclaim_task.min_work_duration = estimated_time
|
|
||||||
reclaim_task.max_work_duration = estimated_time
|
|
||||||
reclaim_task.duration = estimated_time
|
|
||||||
if things_task.get("start_date") is not None:
|
|
||||||
reclaim_task.start_date = datetime.strptime(
|
|
||||||
f"{things_task['start_date']} 08:00", "%Y-%m-%d %H:%M"
|
|
||||||
)
|
|
||||||
if things_task.get("deadline") is not None:
|
|
||||||
reclaim_task.due_date = datetime.strptime(
|
|
||||||
f"{things_task['deadline']} 22:00", "%Y-%m-%d %H:%M"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_time_on_unit(tag_value) -> float | None:
|
|
||||||
# This is a regex to match time in the format of 1h 30m
|
|
||||||
# Minutes are optional if hours are present
|
|
||||||
# Hours are optional if minutes are present
|
|
||||||
# The regex will match two words when the correct format is found (format and emtpy word)
|
|
||||||
values = pattern.findall(tag_value)
|
|
||||||
time = 0
|
|
||||||
if len(values) != 2:
|
|
||||||
raise ValueError("Invalid time format")
|
|
||||||
_, hours, _, _, mins, _ = values[0]
|
|
||||||
if "" == hours and "" == mins:
|
|
||||||
raise ValueError("Regex matched empty string")
|
|
||||||
if "" != hours:
|
|
||||||
time += float(hours)
|
|
||||||
if "" != mins:
|
|
||||||
time += float(mins) / 60
|
|
||||||
|
|
||||||
return time
|
|
||||||
|
|
||||||
|
|
||||||
def map_tag_values(things_task, reclaim_task):
|
|
||||||
tags_dict = get_task_tags(things_task)
|
|
||||||
for tag in tags_dict:
|
|
||||||
match tag:
|
|
||||||
case "MinTime":
|
|
||||||
reclaim_task.min_work_duration = calculate_time_on_unit(tags_dict[tag])
|
|
||||||
case "MaxTime":
|
|
||||||
reclaim_task.max_work_duration = calculate_time_on_unit(tags_dict[tag])
|
|
||||||
case "DeadlineTime":
|
|
||||||
if things_task.get("deadline") is not None:
|
|
||||||
reclaim_task.due_date = datetime.strptime(
|
|
||||||
f"{things_task['deadline']} {tags_dict[tag]}", "%Y-%m-%d %H:%M"
|
|
||||||
)
|
|
||||||
case "StartTime":
|
|
||||||
if things_task.get("start_date") is not None:
|
|
||||||
reclaim_task.start_date = datetime.strptime(
|
|
||||||
f"{things_task['start_date']} {tags_dict[tag]}",
|
|
||||||
"%Y-%m-%d %H:%M",
|
|
||||||
)
|
|
||||||
case _:
|
|
||||||
print(f"Tag {tag} not recognized")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_things_id_tag(things_task) -> str:
|
|
||||||
return f"things_task:{things_task["uuid"]}"
|
|
||||||
|
|
||||||
|
|
||||||
def things_to_reclaim(things_task, project_title):
|
|
||||||
with ReclaimTask() as reclaim_task:
|
|
||||||
reclaim_task.name = "{} {}".format(project_title, things_task["title"])
|
|
||||||
reclaim_task.description = generate_things_id_tag(things_task=things_task)
|
|
||||||
set_default_reclaim_values(things_task=things_task, reclaim_task=reclaim_task)
|
|
||||||
map_tag_values(things_task=things_task, reclaim_task=reclaim_task)
|
|
||||||
reclaim_task_pretty_print(reclaim_task)
|
|
||||||
|
|
||||||
|
|
||||||
def things_task_pretty_print(task, project_title):
|
|
||||||
print(f"\tTitle: {project_title} {task['title']}")
|
|
||||||
print(f"\tStart date: {task['start_date']}")
|
|
||||||
print(f"\tDeadline: {task['deadline']}")
|
|
||||||
print(f"\tTags: {task['tags']}")
|
|
||||||
|
|
||||||
|
|
||||||
def reclaim_task_pretty_print(task):
|
|
||||||
print(f"\tTitle: {task.name}")
|
|
||||||
print(f"\tStart date: {task.start_date}")
|
|
||||||
print(f"\tDeadline: {task.due_date}")
|
|
||||||
print(f"\tMin work duration: {task.min_work_duration}")
|
|
||||||
print(f"\tMax work duration: {task.max_work_duration}")
|
|
||||||
print(f"\tDuration: {task.duration}")
|
|
||||||
|
|
||||||
|
|
||||||
def full_name(things_task) -> str:
|
|
||||||
return f"{things_task['project_title']} {things_task['title']}"
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_things_tasks() -> List:
|
|
||||||
tasks = []
|
|
||||||
projects = extract_uni_projects()
|
|
||||||
for project in projects:
|
|
||||||
tasks += get_tasks_for_project(project)
|
|
||||||
return tasks
|
|
||||||
|
|
||||||
|
|
||||||
def things_reclaim_is_equal(things_task, reclaim_task) -> bool:
|
|
||||||
return full_name(things_task=things_task) == reclaim_task.name
|
|
||||||
|
|
||||||
|
|
||||||
def get_course_names():
|
|
||||||
projects = extract_uni_projects()
|
|
||||||
return [course["title"] for course in projects]
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("upload")
|
|
||||||
def upload_things_to_reclaim(verbose: bool = False):
|
|
||||||
"""
|
|
||||||
Upload things tasks to reclaim
|
|
||||||
"""
|
|
||||||
projects = extract_uni_projects()
|
|
||||||
reclaim_task_names = [task.name for task in ReclaimTask().search()]
|
|
||||||
tasks_uploaded = 0
|
|
||||||
for project in projects:
|
|
||||||
things_tasks = get_tasks_for_project(project)
|
|
||||||
for things_task in things_tasks:
|
|
||||||
full_task_name = full_name(things_task=things_task)
|
|
||||||
if full_task_name not in reclaim_task_names:
|
|
||||||
tasks_uploaded += 1
|
|
||||||
print(f"Creating task {full_task_name} in Reclaim")
|
|
||||||
things_to_reclaim(things_task, project["title"])
|
|
||||||
else:
|
|
||||||
if verbose:
|
|
||||||
print(f"Task {things_task['title']} already exists in Reclaim")
|
|
||||||
if tasks_uploaded == 0:
|
|
||||||
rprint("No new tasks were found")
|
|
||||||
elif tasks_uploaded == 1:
|
|
||||||
rprint(f"Uploaded {tasks_uploaded} task{'s' if tasks_uploaded > 1 else ''}")
|
|
||||||
|
|
||||||
|
|
||||||
def get_subject_reclaim_tasks(subject, tasks):
|
|
||||||
return [task for task in tasks if task.name.startswith(subject)]
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("list")
|
|
||||||
def list_reclaim_tasks(subject: Annotated[Optional[str], typer.Argument()] = None):
|
|
||||||
"""
|
|
||||||
List all current tasks
|
|
||||||
"""
|
|
||||||
reclaim_tasks = ReclaimTask().search()
|
|
||||||
if subject is not None:
|
|
||||||
reclaim_tasks = get_subject_reclaim_tasks(subject, reclaim_tasks)
|
|
||||||
current_date = datetime.now().replace(tzinfo=timezone("UTC"))
|
|
||||||
table = Table("Index", "Task", "Days left", title="Task list")
|
|
||||||
for id, task in enumerate(reclaim_tasks):
|
|
||||||
if current_date > task.due_date:
|
|
||||||
days_behind = (current_date - task.due_date).days
|
|
||||||
table.add_row(
|
|
||||||
f"({id + 1})",
|
|
||||||
task.name,
|
|
||||||
Text(f"{days_behind} days overdue", style="bold red"),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
days_left = (task.due_date - current_date).days
|
|
||||||
table.add_row(
|
|
||||||
f"({id + 1})",
|
|
||||||
task.name,
|
|
||||||
Text(f"{days_left} days left", style="bold white"),
|
|
||||||
)
|
|
||||||
console.print(table)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("stats")
|
|
||||||
def show_task_stats():
|
|
||||||
"""
|
|
||||||
Show task stats
|
|
||||||
"""
|
|
||||||
current_date = datetime.now().replace(tzinfo=timezone("UTC"))
|
|
||||||
reclaim_tasks = ReclaimTask().search()
|
|
||||||
tasks_fine = [task for task in reclaim_tasks if task.due_date >= current_date]
|
|
||||||
tasks_overdue = [task for task in reclaim_tasks if task.due_date < current_date]
|
|
||||||
|
|
||||||
fine_per_course = ["Fine"]
|
|
||||||
overdue_per_course = ["Overdue"]
|
|
||||||
course_names = get_course_names()
|
|
||||||
for course_name in course_names:
|
|
||||||
fine_per_course.append(
|
|
||||||
str(len(get_subject_reclaim_tasks(course_name, tasks_fine)))
|
|
||||||
)
|
|
||||||
overdue_per_course.append(
|
|
||||||
str(len(get_subject_reclaim_tasks(course_name, tasks_overdue)))
|
|
||||||
)
|
|
||||||
|
|
||||||
table = Table(*(["Status"] + course_names))
|
|
||||||
table.add_row(*fine_per_course)
|
|
||||||
table.add_row(*overdue_per_course)
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("time")
|
|
||||||
def print_time_needed():
|
|
||||||
"""
|
|
||||||
Print sum of time needed for all reclaim tasks
|
|
||||||
"""
|
|
||||||
tasks = ReclaimTask.search()
|
|
||||||
tasks.sort(key=lambda x: x.scheduled_start_date)
|
|
||||||
|
|
||||||
time_needed = 0
|
|
||||||
|
|
||||||
for task in tasks:
|
|
||||||
time_needed += task.duration
|
|
||||||
|
|
||||||
print(f"Time needed to complete {len(tasks)} Tasks: {time_needed} hrs")
|
|
||||||
print(f"Average time needed to complete a Task: {time_needed/len(tasks):.2f} hrs")
|
|
||||||
|
|
||||||
last_task_date = tasks[-1].scheduled_start_date
|
|
||||||
today = datetime.now().replace(tzinfo=timezone("UTC"))
|
|
||||||
|
|
||||||
print(
|
|
||||||
f"Last task is scheduled for {last_task_date.strftime('%d.%m.%Y')} ({last_task_date - today} till completion)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("finished")
|
|
||||||
def remove_finished_tasks_from_things():
|
|
||||||
"""
|
|
||||||
Complete finished reclaim tasks in things
|
|
||||||
"""
|
|
||||||
reclaim_things_uuids = [
|
|
||||||
task.description.split(":")[1].strip() for task in ReclaimTask.search()
|
|
||||||
]
|
|
||||||
finished_someting = False
|
|
||||||
for task in get_all_things_tasks():
|
|
||||||
if task["uuid"] not in reclaim_things_uuids:
|
|
||||||
finished_someting = True
|
|
||||||
print(f"Found completed task: {full_name(things_task=task)}")
|
|
||||||
things.complete(task["uuid"])
|
|
||||||
|
|
||||||
if not finished_someting:
|
|
||||||
print("Reclaim and Things are synced")
|
|
||||||
|
|
||||||
|
|
||||||
@app.command("sync")
|
|
||||||
def sync_things_and_reclaim(verbose: bool = False):
|
|
||||||
"""
|
|
||||||
Sync tasks between things and reclaim
|
|
||||||
First updated all finished tasks in reclaim to completed in things
|
|
||||||
Then upload all new tasks from things to reclaim
|
|
||||||
"""
|
|
||||||
rprint("[bold white]Pulling from Reclaim[/bold white]")
|
|
||||||
remove_finished_tasks_from_things()
|
|
||||||
rprint("---------------------------------------------")
|
|
||||||
sleep(2)
|
|
||||||
rprint("[bold white]Pushing to Reclaim[/bold white]")
|
|
||||||
upload_things_to_reclaim(verbose=verbose)
|
|
||||||
rprint("---------------------------------------------")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app()
|
|
||||||
45
things2reclaim/database_handler.py
Normal file
45
things2reclaim/database_handler.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
class UploadedTasksDB(object):
|
||||||
|
def __init__(self, filename):
|
||||||
|
self.conn: sqlite3.Connection = sqlite3.connect(filename)
|
||||||
|
self.__create_tables()
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
self.conn.close()
|
||||||
|
|
||||||
|
def __create_tables(self):
|
||||||
|
sql_statements = [
|
||||||
|
"""CREATE TABLE IF NOT EXISTS uploaded_tasks (
|
||||||
|
id integer primary key,
|
||||||
|
things_task_id varchar(36) NOT NULL UNIQUE
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
]
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
for statement in sql_statements:
|
||||||
|
cursor.execute(statement)
|
||||||
|
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def add_uploaded_task(self, task_id: str):
|
||||||
|
insert_statement = "INSERT INTO uploaded_tasks(things_task_id) VALUES(?)"
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute(insert_statement, [task_id])
|
||||||
|
self.conn.commit()
|
||||||
|
|
||||||
|
def get_all_uploaded_tasks(self):
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute("SELECT * FROM uploaded_tasks")
|
||||||
|
rows = cursor.fetchall()
|
||||||
|
return [things_id for (_, things_id) in rows]
|
||||||
|
|
||||||
|
def remove_uploaded_task(self, task_id: str):
|
||||||
|
delete_statement = "DELETE FROM uploaded_tasks WHERE things_task_id = ?"
|
||||||
|
cursor = self.conn.cursor()
|
||||||
|
cursor.execute(delete_statement, (task_id,))
|
||||||
|
self.conn.commit()
|
||||||
34
things2reclaim/reclaim_handler.py
Normal file
34
things2reclaim/reclaim_handler.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
from typing import List
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from reclaim_sdk.models.task import ReclaimTask
|
||||||
|
from reclaim_sdk.client import ReclaimClient
|
||||||
|
|
||||||
|
CONFIG_PATH = Path("config/.reclaim.toml")
|
||||||
|
|
||||||
|
_config = {}
|
||||||
|
|
||||||
|
with open(CONFIG_PATH, "rb") as f:
|
||||||
|
_config = tomllib.load(f)
|
||||||
|
|
||||||
|
RECLAIM_TOKEN = _config["reclaim_ai"]["token"]
|
||||||
|
|
||||||
|
ReclaimClient(token=RECLAIM_TOKEN)
|
||||||
|
|
||||||
|
|
||||||
|
def get_reclaim_tasks():
|
||||||
|
return ReclaimTask.search()
|
||||||
|
|
||||||
|
|
||||||
|
def filter_for_subject(subject, tasks):
|
||||||
|
return [task for task in tasks if task.name.startswith(subject)]
|
||||||
|
|
||||||
|
|
||||||
|
def create_reaclaim_task(**params):
|
||||||
|
new_task = ReclaimTask(**params)
|
||||||
|
new_task.save()
|
||||||
|
|
||||||
|
|
||||||
|
def get_reclaim_things_ids() -> List[str]:
|
||||||
|
return [task.description.split(":")[1].strip() for task in ReclaimTask.search()]
|
||||||
255
things2reclaim/things2reclaim.py
Executable file
255
things2reclaim/things2reclaim.py
Executable file
@@ -0,0 +1,255 @@
|
|||||||
|
#!/opt/homebrew/Caskroom/miniconda/base/envs/things-automation/bin/python3
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
import typer
|
||||||
|
from pytz import timezone
|
||||||
|
from rich import print as rprint
|
||||||
|
from rich.console import Console
|
||||||
|
from rich.table import Table
|
||||||
|
from rich.text import Text
|
||||||
|
from typing_extensions import Annotated
|
||||||
|
|
||||||
|
import utils
|
||||||
|
import things_handler
|
||||||
|
import reclaim_handler
|
||||||
|
from database_handler import UploadedTasksDB
|
||||||
|
import toggl_handler
|
||||||
|
|
||||||
|
CONFIG_PATH = Path("config/.things2reclaim.toml")
|
||||||
|
|
||||||
|
_config = {}
|
||||||
|
with open(CONFIG_PATH, "rb") as f:
|
||||||
|
_config = tomllib.load(f)
|
||||||
|
|
||||||
|
DATABASE_PATH = _config["database"]["path"]
|
||||||
|
|
||||||
|
app = typer.Typer(add_completion=False, no_args_is_help=True)
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
|
def complete_task_name(incomplete: str):
|
||||||
|
for name in [task.name for task in reclaim_handler.get_reclaim_tasks()]:
|
||||||
|
if name.startswith(incomplete):
|
||||||
|
yield name
|
||||||
|
|
||||||
|
|
||||||
|
def things_to_reclaim(things_task):
|
||||||
|
tags = things_handler.get_task_tags(things_task)
|
||||||
|
estimated_time = tags.get("EstimatedTime")
|
||||||
|
if estimated_time is None:
|
||||||
|
raise ValueError("EstimatedTime tag is required")
|
||||||
|
estimated_time = utils.calculate_time_on_unit(estimated_time)
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"name": things_handler.full_name(things_task),
|
||||||
|
"description": utils.generate_things_id_tag(things_task),
|
||||||
|
"tags": things_handler.get_task_tags(things_task),
|
||||||
|
"min_work_duration": estimated_time,
|
||||||
|
"max_work_duration": estimated_time,
|
||||||
|
"duration": estimated_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
if things_task.get("start_date"):
|
||||||
|
params["start_date"] = datetime.strptime(
|
||||||
|
f"{things_task['start_date']} 08:00", "%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
if things_task.get("deadline"):
|
||||||
|
params["due_date"] = (
|
||||||
|
datetime.strptime(f"{things_task['deadline']} 22:00", "%Y-%m-%d %H:%M"),
|
||||||
|
)
|
||||||
|
|
||||||
|
utils.map_tag_values(things_task, tags, params)
|
||||||
|
|
||||||
|
reclaim_handler.create_reaclaim_task(**params)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("init")
|
||||||
|
def initialize_uploaded_database(verbose: bool = False):
|
||||||
|
"""
|
||||||
|
Initializes the uploaded tasks database
|
||||||
|
"""
|
||||||
|
reclaim_things_uuids = reclaim_handler.get_reclaim_things_ids()
|
||||||
|
added_tasks = 0
|
||||||
|
with UploadedTasksDB(DATABASE_PATH) as db:
|
||||||
|
for task_id in reclaim_things_uuids:
|
||||||
|
try:
|
||||||
|
db.add_uploaded_task(task_id)
|
||||||
|
added_tasks += 1
|
||||||
|
except sqlite3.IntegrityError as e:
|
||||||
|
if verbose:
|
||||||
|
print(
|
||||||
|
f"Task with ID {task_id} already in database | Exception: {e}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if added_tasks == 0:
|
||||||
|
print("uploaded_tasks table is already initialized")
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
f"Added {added_tasks} task{'s' if added_tasks > 1 else ''} to uploaded_tasks table"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("upload")
|
||||||
|
def upload_things_to_reclaim(verbose: bool = False):
|
||||||
|
"""
|
||||||
|
Upload things tasks to reclaim
|
||||||
|
"""
|
||||||
|
projects = things_handler.extract_uni_projects()
|
||||||
|
reclaim_task_names = [task.name for task in reclaim_handler.get_reclaim_tasks()]
|
||||||
|
tasks_uploaded = 0
|
||||||
|
with UploadedTasksDB(DATABASE_PATH) as db:
|
||||||
|
for project in projects:
|
||||||
|
things_tasks = things_handler.get_tasks_for_project(project)
|
||||||
|
for things_task in things_tasks:
|
||||||
|
full_task_name = things_handler.full_name(things_task=things_task)
|
||||||
|
if full_task_name not in reclaim_task_names:
|
||||||
|
tasks_uploaded += 1
|
||||||
|
print(f"Creating task {full_task_name} in Reclaim")
|
||||||
|
things_to_reclaim(things_task)
|
||||||
|
db.add_uploaded_task(things_task["uuid"])
|
||||||
|
else:
|
||||||
|
if verbose:
|
||||||
|
print(f"Task {things_task['title']} already exists in Reclaim")
|
||||||
|
if tasks_uploaded == 0:
|
||||||
|
rprint("No new tasks were found")
|
||||||
|
elif tasks_uploaded == 1:
|
||||||
|
rprint(f"Uploaded {tasks_uploaded} task{'s' if tasks_uploaded > 1 else ''}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("list")
|
||||||
|
def list_reclaim_tasks(subject: Annotated[Optional[str], typer.Argument()] = None):
|
||||||
|
"""
|
||||||
|
List all current tasks
|
||||||
|
"""
|
||||||
|
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
|
if subject is not None:
|
||||||
|
reclaim_tasks = reclaim_handler.filter_for_subject(subject, reclaim_tasks)
|
||||||
|
current_date = datetime.now().replace(tzinfo=timezone("UTC"))
|
||||||
|
table = Table("Index", "Task", "Days left", title="Task list")
|
||||||
|
for id, task in enumerate(reclaim_tasks):
|
||||||
|
if current_date > task.due_date:
|
||||||
|
days_behind = (current_date - task.due_date).days
|
||||||
|
table.add_row(
|
||||||
|
f"({id + 1})",
|
||||||
|
task.name,
|
||||||
|
Text(f"{days_behind} days overdue", style="bold red"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
days_left = (task.due_date - current_date).days
|
||||||
|
table.add_row(
|
||||||
|
f"({id + 1})",
|
||||||
|
task.name,
|
||||||
|
Text(f"{days_left} days left", style="bold white"),
|
||||||
|
)
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("start")
|
||||||
|
def start_task(
|
||||||
|
task_name: Annotated[
|
||||||
|
str, typer.Option(help="Task to start", autocompletion=complete_task_name)
|
||||||
|
],
|
||||||
|
):
|
||||||
|
print(f"Starting task: {task_name}")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("stats")
|
||||||
|
def show_task_stats():
|
||||||
|
"""
|
||||||
|
Show task stats
|
||||||
|
"""
|
||||||
|
current_date = datetime.now().replace(tzinfo=timezone("UTC"))
|
||||||
|
reclaim_tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
|
tasks_fine = [task for task in reclaim_tasks if task.due_date >= current_date]
|
||||||
|
tasks_overdue = [task for task in reclaim_tasks if task.due_date < current_date]
|
||||||
|
|
||||||
|
fine_per_course = ["Fine"]
|
||||||
|
overdue_per_course = ["Overdue"]
|
||||||
|
course_names = things_handler.get_course_names()
|
||||||
|
for course_name in course_names:
|
||||||
|
fine_per_course.append(
|
||||||
|
str(len(reclaim_handler.filter_for_subject(course_name, tasks_fine)))
|
||||||
|
)
|
||||||
|
overdue_per_course.append(
|
||||||
|
str(len(reclaim_handler.filter_for_subject(course_name, tasks_overdue)))
|
||||||
|
)
|
||||||
|
|
||||||
|
table = Table(*(["Status"] + course_names))
|
||||||
|
table.add_row(*fine_per_course)
|
||||||
|
table.add_row(*overdue_per_course)
|
||||||
|
|
||||||
|
console.print(table)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("time")
|
||||||
|
def print_time_needed():
|
||||||
|
"""
|
||||||
|
Print sum of time needed for all reclaim tasks
|
||||||
|
"""
|
||||||
|
tasks = reclaim_handler.get_reclaim_tasks()
|
||||||
|
time_needed = 0
|
||||||
|
|
||||||
|
for task in tasks:
|
||||||
|
time_needed += task.duration
|
||||||
|
|
||||||
|
print(f"Time needed to complete {len(tasks)} Tasks: {time_needed} hrs")
|
||||||
|
print(f"Average time needed to complete a Task: {time_needed/len(tasks):.2f} hrs")
|
||||||
|
|
||||||
|
try:
|
||||||
|
tasks.sort(key=lambda x: x.scheduled_start_date)
|
||||||
|
except TypeError:
|
||||||
|
print("To many to-dos on list. Not all are scheduled")
|
||||||
|
return
|
||||||
|
last_task_date = tasks[-1].scheduled_start_date
|
||||||
|
today = datetime.now().replace(tzinfo=timezone("UTC"))
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Last task is scheduled for {last_task_date.strftime('%d.%m.%Y')} ({last_task_date - today} till completion)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("finished")
|
||||||
|
def remove_finished_tasks_from_things():
|
||||||
|
"""
|
||||||
|
Complete finished reclaim tasks in things
|
||||||
|
"""
|
||||||
|
reclaim_things_uuids = reclaim_handler.get_reclaim_things_ids()
|
||||||
|
finished_someting = False
|
||||||
|
with UploadedTasksDB(DATABASE_PATH) as db:
|
||||||
|
for task in things_handler.get_all_uploaded_things_tasks():
|
||||||
|
if task["uuid"] not in reclaim_things_uuids:
|
||||||
|
finished_someting = True
|
||||||
|
print(
|
||||||
|
f"Found completed task: {things_handler.full_name(things_task=task)}"
|
||||||
|
)
|
||||||
|
things_handler.complete(task["uuid"])
|
||||||
|
db.remove_uploaded_task(task["uuid"])
|
||||||
|
|
||||||
|
if not finished_someting:
|
||||||
|
print("Reclaim and Things are synced")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command("sync")
|
||||||
|
def sync_things_and_reclaim(verbose: bool = False):
|
||||||
|
"""
|
||||||
|
Sync tasks between things and reclaim
|
||||||
|
First updated all finished tasks in reclaim to completed in things
|
||||||
|
Then upload all new tasks from things to reclaim
|
||||||
|
"""
|
||||||
|
rprint("[bold white]Pulling from Reclaim[/bold white]")
|
||||||
|
remove_finished_tasks_from_things()
|
||||||
|
rprint("---------------------------------------------")
|
||||||
|
rprint("[bold white]Pushing to Reclaim[/bold white]")
|
||||||
|
upload_things_to_reclaim(verbose=verbose)
|
||||||
|
rprint("---------------------------------------------")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app()
|
||||||
61
things2reclaim/things_handler.py
Normal file
61
things2reclaim/things_handler.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from typing import Dict, List
|
||||||
|
from pathlib import Path
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
import things
|
||||||
|
|
||||||
|
from database_handler import UploadedTasksDB
|
||||||
|
|
||||||
|
_config = {}
|
||||||
|
CONFIG_PATH = Path("config/.things2reclaim.toml")
|
||||||
|
with open(CONFIG_PATH, "rb") as f:
|
||||||
|
_config = tomllib.load(f)
|
||||||
|
|
||||||
|
|
||||||
|
DATABASE_PATH = _config["database"]["path"]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_uni_projects():
|
||||||
|
uni_area = next(area for area in things.areas() if area["title"] == "Uni")
|
||||||
|
return things.projects(area=uni_area["uuid"])
|
||||||
|
|
||||||
|
|
||||||
|
def get_task(task_id: int):
|
||||||
|
return things.get(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def complete(task_id: int):
|
||||||
|
things.complete(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_tasks_for_project(project) -> Dict | List[Dict]:
|
||||||
|
return things.tasks(project=project["uuid"], type="to-do")
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_things_tasks() -> List:
|
||||||
|
tasks = []
|
||||||
|
projects = extract_uni_projects()
|
||||||
|
for project in projects:
|
||||||
|
tasks += get_tasks_for_project(project)
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_uploaded_things_tasks() -> List:
|
||||||
|
tasks = []
|
||||||
|
with UploadedTasksDB(DATABASE_PATH) as db:
|
||||||
|
for task_id in db.get_all_uploaded_tasks():
|
||||||
|
tasks.append(get_task(task_id))
|
||||||
|
return tasks
|
||||||
|
|
||||||
|
|
||||||
|
def get_task_tags(things_task: Dict) -> Dict[str, str]:
|
||||||
|
return {k: v for (k, v) in [tag.split(": ") for tag in things_task["tags"]]}
|
||||||
|
|
||||||
|
|
||||||
|
def full_name(things_task) -> str:
|
||||||
|
return f"{things_task['project_title']} {things_task['title']}"
|
||||||
|
|
||||||
|
|
||||||
|
def get_course_names():
|
||||||
|
projects = extract_uni_projects()
|
||||||
|
return [course["title"] for course in projects]
|
||||||
74
things2reclaim/toggl_handler.py
Normal file
74
things2reclaim/toggl_handler.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from toggl_python.entities import TimeEntry
|
||||||
|
import toggl_python
|
||||||
|
import tomllib
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
_config = {}
|
||||||
|
|
||||||
|
CONFIG_PATH = Path("config/.toggl.toml")
|
||||||
|
with open(CONFIG_PATH, "rb") as f:
|
||||||
|
_config = tomllib.load(f)
|
||||||
|
|
||||||
|
TOKEN = _config["toggl_track"]["token"]
|
||||||
|
|
||||||
|
auth = toggl_python.TokenAuth(TOKEN)
|
||||||
|
workspace = toggl_python.Workspaces(auth=auth).list()[0]
|
||||||
|
project_dict = {
|
||||||
|
project.name: project
|
||||||
|
for project in toggl_python.Workspaces(auth=auth).projects(_id=workspace.id)
|
||||||
|
if project.active
|
||||||
|
}
|
||||||
|
time_entry_editor = toggl_python.WorkspaceTimeEntries(
|
||||||
|
auth=auth, workspace_id=workspace.id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_time_entries(since_days: int = 30):
|
||||||
|
if since_days > 90:
|
||||||
|
raise ValueError("since_days can't be more than 90 days")
|
||||||
|
time_stamp = int((datetime.now() - timedelta(days=since_days)).timestamp())
|
||||||
|
return toggl_python.TimeEntries(auth=auth).list(since=time_stamp)
|
||||||
|
|
||||||
|
|
||||||
|
def get_current_time_entry():
|
||||||
|
time_entries = toggl_python.TimeEntries(auth=auth)
|
||||||
|
time_entries.ADDITIONAL_METHODS = {
|
||||||
|
"current": {
|
||||||
|
"url": "me/time_entries/current",
|
||||||
|
"entity": toggl_python.TimeEntry,
|
||||||
|
"single_item": True,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time_entries.current()
|
||||||
|
|
||||||
|
|
||||||
|
def create_task_time_entry(description: str, project: str):
|
||||||
|
if project not in project_dict.keys():
|
||||||
|
raise ValueError(f"{project} is not an active toggl project")
|
||||||
|
time_entry = TimeEntry(
|
||||||
|
created_with="things-automation",
|
||||||
|
wid=workspace.id,
|
||||||
|
pid=project_dict[project].id,
|
||||||
|
description=description,
|
||||||
|
duration=-1,
|
||||||
|
)
|
||||||
|
return time_entry
|
||||||
|
|
||||||
|
|
||||||
|
def start_task(description: str, project: str):
|
||||||
|
time_entry_editor.create(create_task_time_entry(description, project))
|
||||||
|
|
||||||
|
|
||||||
|
def stop_current_task():
|
||||||
|
cur_task = get_current_time_entry()
|
||||||
|
if cur_task is None:
|
||||||
|
raise RuntimeError("No time entry is currently running")
|
||||||
|
if time_entry_editor.DETAIL_URL is None:
|
||||||
|
raise ValueError("DetailURL not set")
|
||||||
|
url = time_entry_editor.BASE_URL.join(
|
||||||
|
time_entry_editor.DETAIL_URL.format(id=cur_task.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
return time_entry_editor.patch(url)
|
||||||
79
things2reclaim/utils.py
Normal file
79
things2reclaim/utils.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import re
|
||||||
|
from typing import Union, Dict
|
||||||
|
import things_handler
|
||||||
|
|
||||||
|
regex = (
|
||||||
|
r"((\d+\.?\d*) (hours|hrs|hour|hr|h))? ?((\d+\.?\d*) (mins|min|minutes|minute|m))?"
|
||||||
|
)
|
||||||
|
pattern = re.compile(regex)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_time_on_unit(tag_value: str) -> float:
|
||||||
|
# This is a regex to match time in the format of 1h 30m
|
||||||
|
# Minutes are optional if hours are present
|
||||||
|
# Hours are optional if minutes are present
|
||||||
|
# The regex will match two words when the correct format is found (format and emtpy word)
|
||||||
|
values = pattern.findall(tag_value)
|
||||||
|
time = 0
|
||||||
|
if len(values) != 2:
|
||||||
|
raise ValueError("Invalid time format")
|
||||||
|
_, hours, _, _, mins, _ = values[0]
|
||||||
|
if "" == hours and "" == mins:
|
||||||
|
raise ValueError("Regex matched empty string")
|
||||||
|
if "" != hours:
|
||||||
|
time += float(hours)
|
||||||
|
if "" != mins:
|
||||||
|
time += float(mins) / 60
|
||||||
|
|
||||||
|
return time
|
||||||
|
|
||||||
|
|
||||||
|
def map_tag_values(
|
||||||
|
things_task,
|
||||||
|
tags_dict: Dict[str, str],
|
||||||
|
params: Dict[str, Union[str, datetime, float]],
|
||||||
|
):
|
||||||
|
for tag, value in tags_dict.items():
|
||||||
|
match tag:
|
||||||
|
case "MinTime":
|
||||||
|
params["min_work_duration"] = calculate_time_on_unit(value)
|
||||||
|
case "MaxTime":
|
||||||
|
params["max_work_duration "] = calculate_time_on_unit(value)
|
||||||
|
case "DeadlineTime":
|
||||||
|
if things_task.get("deadline") is not None:
|
||||||
|
params["due_date"] = datetime.strptime(
|
||||||
|
f"{things_task['deadline']} {value}", "%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
|
case "StartTime":
|
||||||
|
if things_task.get("start_date") is not None:
|
||||||
|
params["start_date"] = datetime.strptime(
|
||||||
|
f"{things_task['start_date']} {value}",
|
||||||
|
"%Y-%m-%d %H:%M",
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
print(f"Tag {tag} not recognized")
|
||||||
|
|
||||||
|
|
||||||
|
def generate_things_id_tag(things_task) -> str:
|
||||||
|
return f"things_task:{things_task["uuid"]}"
|
||||||
|
|
||||||
|
|
||||||
|
def things_task_pretty_print(task, project_title):
|
||||||
|
print(f"\tTitle: {project_title} {task['title']}")
|
||||||
|
print(f"\tStart date: {task['start_date']}")
|
||||||
|
print(f"\tDeadline: {task['deadline']}")
|
||||||
|
print(f"\tTags: {task['tags']}")
|
||||||
|
|
||||||
|
|
||||||
|
def reclaim_task_pretty_print(task):
|
||||||
|
print(f"\tTitle: {task.name}")
|
||||||
|
print(f"\tStart date: {task.start_date}")
|
||||||
|
print(f"\tDeadline: {task.due_date}")
|
||||||
|
print(f"\tMin work duration: {task.min_work_duration}")
|
||||||
|
print(f"\tMax work duration: {task.max_work_duration}")
|
||||||
|
print(f"\tDuration: {task.duration}")
|
||||||
|
|
||||||
|
|
||||||
|
def things_reclaim_is_equal(things_task, reclaim_task) -> bool:
|
||||||
|
return things_handler.full_name(things_task=things_task) == reclaim_task.name
|
||||||
Reference in New Issue
Block a user