from typing import List, Tuple, Optional, Iterator, Iterable
from nextcloud_tasks_api import TaskFile, TaskList as ApiTaskList
from nextcloud_tasks_api.ical import Task as ApiTask
class TaskError(Exception):
pass
TaskList = ApiTaskList
class Task(ApiTask):
def __init__(self, ctx: Optional[TaskFile]) -> None:
try:
super().__init__(ctx.content if ctx is not None else None)
self._ctx: Optional[TaskFile] = ctx
except (ValueError, KeyError) as e:
raise TaskError(f"Cannot parse task from '{type(ctx)}': {str(e)}")
def apply(self) -> None:
self.ctx.content = self.to_string()
@property
def ctx(self) -> TaskFile:
if self._ctx is None:
raise TaskError("Task not bound to an API object")
return self._ctx
class TaskTree:
class Node:
def __init__(self, task: Task) -> None:
self.task: Task = task
self.children: List[TaskTree.Node] = []
def _find(self, uid: str) -> Optional['TaskTree.Node']:
if self.task.uid == uid:
return self
return self.find(self.children, uid)
@classmethod
def find(cls, nodes: List['TaskTree.Node'], uid: str) -> Optional['TaskTree.Node']:
for child in nodes:
found: Optional[TaskTree.Node] = child._find(uid)
if found is not None:
return found
return None
def _sort(self) -> None:
self.children.sort(key=lambda n: n.task.summary.lower() if n.task.summary else '')
for child in self.children:
child._sort()
@classmethod
def sort(cls, nodes: List['TaskTree.Node']) -> None:
nodes.sort(key=lambda n: n.task.summary.lower() if n.task.summary else '')
for node in nodes:
node._sort()
@property
def completed(self) -> Optional[bool]:
is_completed: bool = self.task.completed is not None
for child in self.children:
if child.completed != is_completed:
return None # mixed
else:
return is_completed
def dump(self, exclude_completed: bool, depth: int = 0) -> Iterator[Tuple[int, Task]]:
if exclude_completed and self.completed is True:
return
else:
yield depth, self.task
for child in self.children:
yield from child.dump(exclude_completed, depth + 1)
def __init__(self, tasks: Iterable[Task], exclude_completed: bool) -> None:
self._exclude_completed: bool = exclude_completed
self._tree: List[TaskTree.Node] = self._build_tree(tasks)
self._tasks: List[Tuple[int, Task]] = list(self._dump())
def tasks(self) -> Iterator[Tuple[int, Task]]:
yield from self._tasks
def index(self, task: Task) -> Optional[int]:
try:
index: int = 0
for _, node in self.tasks():
if node.uid == task.uid:
return index
index += 1
return None
except (ValueError, KeyError) as e:
raise TaskError(f"Cannot find task uid: {str(e)}") from None
def _dump(self) -> Iterator[Tuple[int, Task]]:
try:
for node in self._tree:
yield from node.dump(self._exclude_completed)
except (ValueError, KeyError) as e:
raise TaskError(f"Cannot list tasks: {str(e)}") from None
@classmethod
def _build_tree(cls, tasks: Iterable[Task], sort: bool = True) -> List['TaskTree.Node']:
tree: List[TaskTree.Node] = [TaskTree.Node(_) for _ in tasks] # need to buffer all to find late parents
try:
for node in list(tree): # duplicate and change in-place
related_to: Optional[str] = node.task.related_to
if related_to is not None:
parent: Optional[TaskTree.Node] = TaskTree.Node.find(tree, related_to)
if parent is not None:
parent.children.append(node)
tree.remove(node)
if sort:
TaskTree.Node.sort(tree)
except (ValueError, KeyError) as e:
raise TaskError(f"Cannot build task tree: {str(e)}") from None
return tree