From e917bea322e14cf556fd85c3afc4b08559c50555 Mon Sep 17 00:00:00 2001 From: riley Date: Sat, 25 Sep 2021 15:47:26 -0400 Subject: [PATCH] Initial Commit --- .gitignore | 1 + README.md | 39 ++++++++++ todo.md | 35 +++++++++ todo/Category.py | 28 +++++++ todo/Note.py | 18 +++++ todo/Task.py | 42 +++++++++++ todo/Todo.py | 178 +++++++++++++++++++++++++++++++++++++++++++++ todo/TodoObject.py | 38 ++++++++++ todo/__init__.py | 7 ++ 9 files changed, 386 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 todo.md create mode 100644 todo/Category.py create mode 100644 todo/Note.py create mode 100644 todo/Task.py create mode 100644 todo/Todo.py create mode 100644 todo/TodoObject.py create mode 100644 todo/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c01bd8c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*/__pycache__/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..ba6819f --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# todo.py + +todo.py is my attempt to organize myself and get things straight. I disliked the specification declared by the folks at [todo-md](https://www.github.com/todo-md/todo-md), so naturally, I'm making my own. + +## Specification + +```md +# todo.md + +## Section + +- [ ] some incomplete task with a due date |Sep 24 2021 +- [x] some complete task without a due date + +### Sub-Section +- [ ] here's another task with a due date |Oct 13 2021 +``` + +## TODO +kinda funny how a todo tool has a todo list, anyways here's what I've got to do to finish this thing: + +```md +# todo.md + +## Features +- [ ] add recurring tasks + - thinking about having the following section for spec +- [ ] add subtasks +- [ ] add notes + +## RECURRING +- some recurring task |how often it should repeat |the last time completed + - possible values for repeat time: + - daily + - weekly - mon + - biweekly - wed + - monthly - 15 + - yearly - jan - 17 +``` diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..520907a --- /dev/null +++ b/todo.md @@ -0,0 +1,35 @@ +# todo.md + +this is the to-do list for the `todo.md` project. *woah*. + +## spec +- one line represents an object in `todo.md` +- an object can either be a category header, a task, or a note. +- category headers can go as many levels as necessary. +- tasks can have due-dates, and they can either be complete, or incomplete. + - [ ] this is an incomplete task. + - [x] this is a complete task. + - [ ] tasks can be nested on other tasks or notes. + +- [ ] or they can be on the root of the category. + - all tasks must belong under a category though. + - so no line 1 tasks. + +## features +- [ ] add recurring tasks + - thinking about having the following section for spec +- [x] add tasks +- [x] add notes + - notes can be listed or unlisted. + - listed notes are like these here. + - unlisted notes is what's on line 3. + - and clearly, they can be nested. + +## recurring +- some recurring task |how often it should repeat |the last time completed + - possible values for repeat time: + - daily + - weekly - mon + - biweekly - wed + - monthly - 15 + - yearly - jan - 17 \ No newline at end of file diff --git a/todo/Category.py b/todo/Category.py new file mode 100644 index 0000000..4d9f294 --- /dev/null +++ b/todo/Category.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from .Task import Task +from .TodoObject import TodoObject + +class Category(TodoObject): + def __init__(self, name : str, level: int, parent = None): + super().__init__(level, name, parent) + + def get_subcategories(self) -> list[Category]: + categories = [] + for child in self.children: + if isinstance(child, Category): + categories.append(child) + return categories + + def get_tasks(self) -> list[Task]: + tasks = [] + for child in self.children: + if isinstance(child, Task): + tasks.append(child) + return tasks + + def __str__(self): + return "#"*(self.level+1) + f" {self.text}" + + def __add__(self, other): + return other + self.__str__() diff --git a/todo/Note.py b/todo/Note.py new file mode 100644 index 0000000..d7e0b1c --- /dev/null +++ b/todo/Note.py @@ -0,0 +1,18 @@ +from typing import Optional + +from .TodoObject import TodoObject + +class Note(TodoObject): + def __init__(self, level: int, text: str, parent: TodoObject = None, listed: bool = True): + self.listed = listed + super().__init__(level, text, parent) + + def __str__(self): + if self.listed: + return f"- {self.text}" + else: + return self.text + + def __add__(self, other: str): + return other + self.__str__() + diff --git a/todo/Task.py b/todo/Task.py new file mode 100644 index 0000000..7f15c14 --- /dev/null +++ b/todo/Task.py @@ -0,0 +1,42 @@ +from __future__ import annotations + +from typing import Union +from datetime import datetime + +from .Note import Note +from .TodoObject import TodoObject + +class Task(TodoObject): + def __init__(self, name : str, date = Union[datetime, None], complete: bool = False, level: int = 0, parent = None): + self.date = date + self.complete = complete + super().__init__(level, name, parent) + + def get_subtasks(self) -> list[Task]: + tasks = [] + for child in self.children: + if isinstance(child, Task): + tasks.append(child) + return tasks + + def get_notes(self): + notes = [] + for child in self.children: + if isinstance(child, Note): + notes.append(child) + + def __str__(self): + output = "" + if self.complete: + output += "- [x]" + else: + output += "- [ ]" + if self.date is not None: + self.date: datetime + output += f" {self.text} |{self.date.strftime('%b %d %Y')}" + else: + output += f" {self.text}" + return output + + def __add__(self, other): + return other + self.__str__() diff --git a/todo/Todo.py b/todo/Todo.py new file mode 100644 index 0000000..6f6af30 --- /dev/null +++ b/todo/Todo.py @@ -0,0 +1,178 @@ +#!/bin/python3 +import re +import logging + +from pathlib import Path +from datetime import datetime +from typing import Optional + +from .TodoObject import TodoObject +from .Category import Category +from .Note import Note +from .Task import Task + +logger = logging.getLogger("Todo") +logger.addHandler(logging.StreamHandler()) + +class Todo(TodoObject): + def __init__(self, data_file : Path = Path.home().joinpath("todo.md")): + """Todo class, reads and parses a todo.md from a file. Default file is 'todo.md' in the home directory. + + Args: + data_file (Path, optional): Location of the markdown file to load from. Defaults to ~/todo.md. + """ + self.data_file = data_file + super().__init__(-1, "File") + self._load_data() + + def __str__(self): + return self.get_md() + + def _get_level(self, line: str) -> int: + """Calculates the level at which this element lies. + + Args: + line (str): The string to parse. + + Returns: + int: The level, starting at 0. + """ + count = 0 + if line[0] == "-" or line[0] == " ": + for char in line: + if char == " ": + count += 1 + else: + break + return count // 2 + elif line[0] == "#": + for char in line: + if char == "#": + count += 1 + else: + break + return count - 1 + else: # catch-all for unlisted notes + return 0 + + def _get_parent(self, level, object: TodoObject): + """Gets the parent of an object at level X. + + Args: + level (int): The level to find the parent for. + object (TodoObject): Object to grab parents from. + + Returns: + TodoObject: The object found. + """ + if level > 0: + for child in object.children: + if child.level < level: + return child + return object + else: + return object + + def _parse_note(self, line: str, parent: TodoObject): + """Parses a markdown line representing a note. + + Args: + line (str): The line to parse. + category (Category): The category this note belongs to. + """ + level = self._get_level(line) + parent = self._get_parent(level, parent) + if re.match(" * -", line): + Note(level, line[level*2+2:], parent) + else: + Note(level, line[level*2:], parent, False) + + def _parse_task(self, line: str, parent: Category): + """Parses the markdown line representing a task. + + Args: + line (str): The line to parse. + category (Category): The category this task belongs to. + """ + level = self._get_level(line) + parent = self._get_parent(level, parent) + if "[ ]" in line: + complete = False + elif "[x]" in line: + complete = True + line = line[level*2+6:] + if "|" in line: + line = line.replace(" |", "|") + name = line.split("|")[0] + date = datetime.strptime(line.split("|")[1], '%b %d %Y') + else: + name = line + date = None + Task(name, date, complete, level, parent) + + def _parse_category(self, line: str): + """Parses the markdown line representing a category of todos. + + Args: + line (str): The line to parse. + """ + level = self._get_level(line) + parent = self._get_parent(level, self) + line = line[level+2:] + Category(line, level, parent) + + def _load_data(self): + """Load categories and tasks from self.data_file.""" + with self.data_file.open("r") as f: + for line in f: + line = line.rstrip() + if len(line.strip()) == 0: # skip empty lines + continue + elif line[0] == "#": + self._parse_category(line) + elif re.match(" *- \\[[x ]\\]", line): + self._parse_task(line, self.children[-1]) + else: + self._parse_note(line, self.children[-1]) + + def get_md(self, category_spacing: int = 1, task_spacing: int = 0, note_spacing: int = 0) -> str: + """Gets the markdown text of the current data loaded into the object. + + Args: + category_spacing (int, optional): Amount of newlines between categories. Defaults to 1. + task_spacing (int, optional): Amount of newlines between tasks. Defaults to 0. + note_spacing (int, optional): Amount of newlines between category notes. Defaults to 0. + + Returns: + str: the markdown text generated + """ + output = "" + for i in self.get_children(immediate=True): + if isinstance(i, Category): + output += f"{i}\n" + elif isinstance(i, Task): + output += f"{i.level*2*' '}{i}\n" + elif isinstance(i, Note): + output += f"{i.level*2*' '}{i}\n" + return output + + def get_category(self, name: str) -> Optional[Category]: + """Gets the requested category by name. + + Args: + name (str): The name of the category to return. + + Returns: + Optional[Category]: Returns the category if found. + """ + for i in self.children: + if isinstance(i, Category) and i.text == name: + return i + +def main(): + """Generates a markdown of my todos""" + todo = Todo() + print(todo) + +if __name__ == "__main__": + main() diff --git a/todo/TodoObject.py b/todo/TodoObject.py new file mode 100644 index 0000000..4dbab1d --- /dev/null +++ b/todo/TodoObject.py @@ -0,0 +1,38 @@ +import logging + +from typing import Optional + +logger = logging.getLogger("TodoObject") +logger.setLevel(logging.INFO) +logger.addHandler(logging.StreamHandler()) + +class TodoObject: + def __init__(self, level, text, parent = None): + self.text: str = text + self.level: int = level + self.parents: list[TodoObject] = [] + self.children: list[TodoObject] = [] + self.parent: Optional[TodoObject] = parent + if parent is not None: + self.set_parents(parent) + + def get_children(self, immediate = False): + output = [] + for child in self.children: + if immediate and child.parent is self: + output.append(child) + elif not immediate: + output.append(child) + + def set_parents(self, parent): + parent: TodoObject + for p in parent.parents: + logger.debug(f"adding parent '{p.text}' to '{self.text}'") + self.parents.append(p) + logger.debug(f"adding child '{self.text}' to '{p.text}'") + p.children.append(self) + logger.debug(f"adding parent '{parent.text}' to '{self.text}'") + self.parents.append(parent) + logger.debug(f"adding child '{self.text}' to '{parent.text}'") + parent.children.append(self) + self.parent = parent diff --git a/todo/__init__.py b/todo/__init__.py new file mode 100644 index 0000000..0dbf1b1 --- /dev/null +++ b/todo/__init__.py @@ -0,0 +1,7 @@ +from .TodoObject import * +from .Category import * +from .Note import * +from .Task import * +from .Todo import * + +__all__ = ['Todo', 'Category', 'Note', 'Task', 'TodoObject'] \ No newline at end of file