Initial Commit

This commit is contained in:
riley 2021-09-25 15:47:26 -04:00
commit e917bea322
9 changed files with 386 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*/__pycache__/

39
README.md Normal file
View File

@ -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
```

35
todo.md Normal file
View File

@ -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

28
todo/Category.py Normal file
View File

@ -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__()

18
todo/Note.py Normal file
View File

@ -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__()

42
todo/Task.py Normal file
View File

@ -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__()

178
todo/Todo.py Normal file
View File

@ -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()

38
todo/TodoObject.py Normal file
View File

@ -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

7
todo/__init__.py Normal file
View File

@ -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']