Streamline Your TODOs with TODO Notifier for Python projects

Ashutosh Kumar
8 min readApr 18, 2023

Are you tired of tracking your TODOs in Python projects? Do you wish there was a tool that could scrape and summarize all your TODOs for you? Checkout the TODO Notifier, an open source project available on GitHub and PyPi.

At my last company, I had the pleasure of using a tool that summarized all the TODO items in a Python project. It would send periodic reminders to individual developers to complete them. The tool was helpful and could identify expired and upcoming TODOs with an expected completion date.

When I moved to a new company, I realized how much I missed that tool. So, I decided to take matters into my own hands and develop a similar tool myself. It was a challenging but rewarding experience. And I’m excited to share my journey of building a Python TODO management tool.

With TODO Notifier, you can set up automated summaries of TODO items in your code by module. It also lists all expired and upcoming TODO items based on expiry date. Moreover, you can customize the tool to generate and send these summaries over email. And send reminders to individual developers (upcoming feature) to finish their TODO items.

Let’s first see the TODO Notifier in action. I will be using TODO Notifier on itself to generate the default summaries. Please note that I am not using any notifier for this demo.

TODO Notifier Demo
Sample Summary generated using TODO Notifier

Setup

Setting up TODO Notifier is simple. Follow these steps and you will have TODO Notifier ready in no time. If you want more information, please check out the GitHub or PyPi page.

  1. Install it with pip: pip install todonotifier

2. Create a driver file. Let’s name it driver.py

Let’s look at a sample driver.py file. The recommended format for TODO items is TODO {2022–05–22} @user_name msg where 2022–05–22 is the optional expected completion date for this TODO item. user_name is the optional unique user name for the respective developer (recommended to use Email ids). And msg is the usual optional message.

import os

from todonotifier.config import BaseConfig, DefaultConfig
from todonotifier.notifier import BaseNotifier, EmailNotifier
from todonotifier.connect import CONNECT_METHOD, Connect
from todonotifier.driver import run as driver_run


# PROJECTS is a list of tuple.
# First item of each tuple is the fully qualified Git link of the project
# Second item of each tuple is project name used by TODO Notifier to refer the project
PROJECTS = [("https://github.com/ashu-tosh-kumar/todo_notifier", "todo_notifier")]

# Sender and Recipient are needed only if we want to setup Email based notifications
SENDER = "sender@email.com"
RECIPIENTS = ["recipient1@gmail.com", "recipient@gmail.com"]
EMAIL_HOST = "smtp.gmail.com" # Change accordingly
EMAIL_PORT = 465 # Change accordingly


def run():
"""Main starting point of the TODO application to be modified by end users per their need

This script can be modified to add a `for` loop for multiple projects
"""
# Notifier is optional. Notifier is used to notify the end users
_password = os.environ["email_password"] # Email ID password for SENDER
notifier: BaseNotifier = EmailNotifier(SENDER, _password, EMAIL_HOST, EMAIL_PORT, RECIPIENTS)
config: BaseConfig = DefaultConfig(save_html_reports=True, ignore_todo_case=True, notifier=notifier) # Change per need

for git_url, project_dir_name in PROJECTS:
# branch name is optional and is needed for git URLs (else will take default git branch)
connect = Connect(connect_method=CONNECT_METHOD.GIT_CLONE, project_dir_name=project_dir_name, url=git_url, branch_name="main")

# It will run the TODO Notifier
# It will generate default (or custom ones if added) summaries
# It will then use `notifier` passed in `config` to notify. In this case, email the recipients
driver_run(connect, config)


if __name__ == "__main__":
run()

Let’s have a look at the above code in detail to understand what we need to setup the TODO Notifier.

PROJECTS : List of projects. Each item of list is a tuple of two strings. First string is the git URL of the project. Please note that if your repository is private, you would need to pass a fully qualified url, example:https://<username>:<token>@gitlab.com/group/project.git . Second string is the name used by TODO Notifier to refer to the respective project. But, the project URL can also be fully qualified address of a local project on your system. In that case you would also need to change the connect_method in config to DRY_RUN_DIR if it’s a directory and DRY_RUN_FILE if it’s a file.

Notifier : Notifier is a child class object of BaseNotifier class. The same object handles notifying the end users. By default, TODO Notifier provides EmailNotifier that can notify/share summaries via email. You can write your own Notifier if you wish. For example, if you want to notify users using AWS SES or AWS SNS etc.

Config : Config is a child class of BaseConfig class. By default, the TODO Notifier provides a DefaultConfig that is flexible enough to control all aspects of the TODO Notifier. But if you wish, you can write your own configuration class by inheriting from BaseConfig or even DefaultConfig .

DefaultConfig : As discussed above, DefaultConfig is the default configuration class provided by TODO Notifier. You can use following arguments provided by DefaultConfig to control the TODO Notifier.

1. exclude_dirs: Dict[str, List[str]] = Directories that needs to be ignored. For format, check todonotifier.constants.DEFAULT_EXCLUDE_DIRS . It’s very powerful. You can use absolute address, or relative address or even regex to exclude directories.

2. flag_default_exclude_dirs: bool = Boolean with default value ofTrue . If False, the TODO Notifier will not exclude default list of directories: todonotifier.constants.DEFAULT_EXCLUDE_DIRS

3. exclude_files: Dict[str, List[str]] = Files that needs to be ignored. For format, check todonotifier.constants.DEFAULT_EXCLUDE_FILES . It’s very powerful. You can use absolute address, or relative address or even regex to exclude files.

4. flag_default_exclude_files: bool = Boolean with default value ofTrue . If False, the TODO Notifier will not exclude default list of directories: todonotifier.constants.DEFAULT_EXCLUDE_FILES

5. summary_generators: List[BaseSummaryGenerator] = List of summary generators. You can write your own summary generator by inheriting from todonotifier.summary_generators.BaseSummaryGenerator

6. flag_default_summary_generators: bool = Boolean with default value ofTrue . If True , the TODO Notifier will generate default summaries. TODO Notifier provides three default summary generators: ByModuleSummaryGenerator, ExpiredTodosByUserSummaryGenerator, andUpcomingWeekTodosByUserSummaryGenerator . These names are self explanatory else please check their docstrings for more information. These summary generators are available in module todonotifier.summary_generators.py

7. generate_html: bool = Boolean with default value ofTrue . If True, the TODO Notifier will generate HTML reports of the summaries.

8. save_html_reports: bool = Boolean with default value of False . If True, the TODO Notifier will save generatedHTML reports in current directory under folder /.report

9. ignore_todo_case: bool = Boolean with default value of False . If True, the TODO Notifier will consider both lowercase todo items and uppercase TODO items.

10. notifier: Union[BaseNotifier, None] = Notifier object (instance of any child class of BaseNotifier ).

NOTE: If you don’t want to use the native flow of generating HTML summaries and sharing them via a notifier. And want to integrate TODO Notifier with a downstream application, you can do that too . Set the respective flags to generate HTML summaries and notifier to None. Then you can get a handle of the summary objects as show below. You can then use these objects to add any custom logic or for downstream integration.

# Access all the summary generators.
# Each summary generator holds the generated HTML summary in `container` instance variable
list_of_all_summary_generators = config.config.summary_generators

# Access summary from (say) ByModuleSummaryGenerator
by_module_summary_generator = config.summary_generators[0]
print(by_module_summary_generator.name) # Check name of the respective summary generator

# Generated `list` of `TODO` objects
# Low level handle of all scraped TODO items
by_module_summary_generator.container

# Generated html summary of `TODO` objects
# Only if HTML summary generation flag is turned on
by_module_summary_generator.html

Conclusion

TODO Notifier is flexible and customizable. It’s open source and free to use. You can adjust the date format to match your project’s needs. You can also send the generated summaries over Email or any other notification service. You can exclude certain modules or files from the summaries.

TODO Notifier is a powerful tool for Python developers who want to streamline their TODO management. With its easy setup and customizable options, it’s a must-have for any project with many developers and lots of TODOs to track.

If you’re interested in trying out TODO Notifier, you can find the code on GitHub and install it with pip. Give it a try and let me know what you think!

Resources

Sample code to use SNS as a notifier

import json
from datetime import datetime
from typing import List, Tuple

from botocore.exceptions import ClientError
from todonotifier.notifier import BaseNotifier


# Code taken from https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/python/example_code/sns/sns_basics.py
class SnsNotifier(BaseNotifier):
"""Encapsulates Amazon SNS topic and subscription functions."""

def __init__(self, sns_client, topic_arn: str) -> None:
"""Initializer for `SnsNotifier`

Args:
sns_client: A Boto3 client for SNS
topic_arn (str): Topic to which to publish to.
"""
self._topic_arn = topic_arn
self._sns_client = sns_client

def _publish_email_message(self, subject: str, email_message: str) -> str:
"""Publishes a multi-format message to a topic. A multi-format message takes
different forms based on the protocol of the subscriber. For example,
an SMS subscriber might receive a short, text-only version of the message
while an email subscriber could receive an HTML version of the message.

Args:
subject (str): The subject of the message.
email_message (str): The version of the message sent to email subscribers.

Returns:
str: The ID of the message.
"""
try:
message = {"default": email_message, "email": email_message}
response = self._sns_client.publish(TopicArn=self._topic_arn, Message=json.dumps(message), Subject=subject, MessageStructure="json")
message_id = response["MessageId"]
print(f"Published email-format message to topic {self._topic_arn}.")
except ClientError:
print(f"Couldn't publish message to topic {self._topic_arn}.")
raise
else:
return message_id

def notify(self, summary: List[Tuple[str, str]]) -> None:
"""Sends email to `receivers_list` with `html` content

Args:
summary (List[Tuple[str, str]]): List of tuples where each tuple consists of summary name and generated summary html
"""
html = self._aggregate_all_summaries(summary)
subject = f"TODO Summary - {datetime.today().date()}"

self._publish_email_message(subject, html)

Sample code to use SES as a notifier

from datetime import datetime
from typing import List, Tuple

from botocore.exceptions import ClientError
from todonotifier.notifier import BaseNotifier

CHARSET = "UTF-8"


class SesNotifier(BaseNotifier):
"""Encapsulates Amazon SNS topic and subscription functions.
Code taken from
https://docs.aws.amazon.com/ses/latest/dg/send-an-email-using-sdk-programmatically.html
"""

def __init__(self, boto3_client, sender: str, recipient: str) -> None:
"""Initializer for `SesNotifier`

Args:
boto3_client: Boto3 client for SES
sender (str): Email ID to which email needs to be sent
recipient (str): Email ID to be used to send email
"""
self._boto3_client = boto3_client
self._sender = sender
self._recipient = recipient

def _send_email_message(self, subject: str, text_message: str, html_message: str) -> None:
"""Sends an email with given subject and text and/or html content

Args:
subject (str): The subject of the message.
html_message (str): The version of the message sent to email subscribers.
"""
# Try to send the email.
try:
# Provide the contents of the email.
response = self._boto3_client.send_email(
Destination={
"ToAddresses": [
self._recipient,
],
},
Message={
"Body": {
"Html": {
"Charset": CHARSET,
"Data": html_message,
},
"Text": {
"Charset": CHARSET,
"Data": text_message,
},
},
"Subject": {
"Charset": CHARSET,
"Data": subject,
},
},
Source=self._sender,
)
# Display an error if something goes wrong.
except ClientError as e:
print(f"Error in sending email: {e.response['Error']['Message']}")
raise
else:
print(f"Email sent! Message ID: {response['MessageId']}")

def notify(self, summary: List[Tuple[str, str]]) -> None:
"""Overrides the base class `notify` that gets invoked by the TODO Notifier to
notify the users by any means

Args:
summary (List[Tuple[str, str]]): List of tuples where each tuple consists of
summary name and generated summary html
"""
html = self._aggregate_all_summaries(summary)
subject = f"TODO Summary - {datetime.today().date()}"

self._send_email_message(subject, "", html)

Find TODO Notifier on GitHub, PyPi, ProductHunt, SourceForge

--

--

Ashutosh Kumar

Full Stack Software Developer. Focused on taking the red pill to problem solving.