Updated project structure
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -160,3 +160,6 @@ cython_debug/
|
||||
#.idea/
|
||||
|
||||
.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