DEV Community

Cover image for How to Make an Interactive Todo List CLI using Python with an Easy Login Mechanism
Putri Karunia for Cotter

Posted on

How to Make an Interactive Todo List CLI using Python with an Easy Login Mechanism

Build your own Todo List CLI with Python using packages like Click, PyInquirer, and Cotter in minutes!

There a ton of awesome packages that helps you build a beautiful CLI, some are covered in this article Building Beautiful Command Line Interfaces with Python.

We're going to use some of them to make a nice and interactive todo list CLI.
Python Todo List CLI<br>

The API

We're going to use a ready-made REST API that helps us store and update the todo list. The API is accessible on https://cottertodolist.herokuapp.com/ and you can find the source code and documentation at the Github Repo.

The CLI

To make the CLI, we'll use several packages:

Get the full code for this tutorial in Cotter's Github.

Let's start with a super simple CLI

In your project directory, let's start by making a virtualenv:

 python -m venv venv/ 
Enter fullscreen mode Exit fullscreen mode

Then activate the virtual environment with the source command:

 source venv/bin/activate 
Enter fullscreen mode Exit fullscreen mode

Now let's install click to make our simple cli:

 pip install click 
Enter fullscreen mode Exit fullscreen mode

Then make a file for our CLI – let's call it cli.py and we'll make a simple greet command.

 # cli.py import click @click.group() def main(): """ Simple CLI that will greet you""" pass @main.command() @click.argument('name') def greet(name): """This will greet you back with your name""" click.echo("Hello, " + name) if __name__ == "__main__": main() 
Enter fullscreen mode Exit fullscreen mode

This is how it looks like when run:

 ❯ python cli.py greet Putri Hello, Putri 
Enter fullscreen mode Exit fullscreen mode

Now let's build our Todo List CLI.

Let's check our Todo List API documentation. An example request to show all lists looks like this:

 ### Show all lists GET http://localhost:1234/list Authorization: Bearer <access_token> 
Enter fullscreen mode Exit fullscreen mode

As you can see our API requires an access_token to only allow owners to view and update the todo list.

Start with Registering/Logging-in our users to the API

The first thing we need to do is to authenticate our users and register it to the Todo list API. The API looks like this:

 ### Login or Register a user using Cotter's response ### From the Python SDK POST http://localhost:1234/login_register Content-Type: application/json { "oauth_token": { "access_token": "eyJhbGciOiJF...", "id_token": "eyJhbGciOiJFUzI...", "refresh_token": "40011:ra78TcwB...", "expires_in": 3600, "token_type": "Bearer", "auth_method": "OTP" } } 
Enter fullscreen mode Exit fullscreen mode

To do this, we'll use Cotter's Python SDK to generate the oauth_tokens then send it to the todo list API.

Logging-in with Cotter

First, install Cotter using:

 pip install cotter 
Enter fullscreen mode Exit fullscreen mode

Then, let's update our cli.py to add a login function:

 # cli.py import click import os import cotter # 1️⃣ Add your Cotter API KEY ID here api_key = "<COTTER_API_KEY_ID>" @click.group() def main(): """ A Todo List CLI """ pass @main.command() def login(): """Login to use the API""" # 2️⃣ Add a file called `cotter_login_success.html`  # 3️⃣ Call Cotter's api to login  port = 8080 # Select a port  response = cotter.login_with_email_link(api_key, port) click.echo(response) if __name__ == "__main__": main() 
Enter fullscreen mode Exit fullscreen mode

You'll need an API_KEY_ID from Cotter, which you can get from the Dashboard.

Following the SDK instructions, you'll also need an HTML file called cotter_login_success.html in your project directory. Copy cotter_login_success.html from the SDK's example folder in Github. Your project folder should now contain 2 files:

 project-folder |- cli.py |- cotter_login_success.html 
Enter fullscreen mode Exit fullscreen mode

Now let's run the CLI and try to log in. You should see something like this:

 ❯ python cli.py login Open this link to login at your browser: https://js.cotter.app/app?api_key=abcdefgh-c318-4fc1-81ad-5cc8b69051e8&redirect_url=http%3A%2F%2Flocalhost%3A8080&state=eabgzskfrs&code_challenge=zBa9xK4sI7zpqvDZL8iAX9ytSo0JZ0O4gWWuVIKTXU0&type=EMAIL&auth_method=MAGIC_LINK 
Enter fullscreen mode Exit fullscreen mode

This should also open your browser where you can enter your email and login.

Opened Link to Login

Once you're done logging-in, you should see the following response in your terminal:

 { "oauth_token": { "access_token": "eyJhbGciOiJFU...", "id_token": "eyJhbGciOiJF...", "refresh_token": "40291:czHCOxamERp1yA...Y", "expires_in": 3600, "token_type": "Bearer", "auth_method": "OTP" }, "identifier": {...}, "token": {...}, "user": {...} } 
Enter fullscreen mode Exit fullscreen mode

This is perfect for our /login_register API endpoint, so let's send the response from login to that endpoint inside our login function.

Register or Login the user to the todo list API

First, we need to install requests to call HTTP requests:

 pip install requests 
Enter fullscreen mode Exit fullscreen mode

Update our login function inside cli.py to the following:

 # cli.py import requests @main.command() def login(): """Login to use the API""" # Add a file called `cotter_login_success.html`  # Call Cotter's api to login  port = 8080 # Select a port  response = cotter.login_with_email_link(api_key, port) # 1️⃣ Add your Cotter API KEY ID here  url = 'https://cottertodolist.herokuapp.com/login_register' headers = {'Content-Type': 'application/json'} data = response resp = requests.post(url, json=data, headers=headers) if resp.status_code != 200: resp.raise_for_status() click.echo(resp.json()) 
Enter fullscreen mode Exit fullscreen mode

You should now see a response {'user_id': '274825255751516680'} which means we have successfully registered or logged-in our user.

Storing the oauth_tokens for later use
We don't want to ask the user to login every time we need the access_token. Fortunately, Cotter already provides a function to easily store and get (and automatically refresh) the access_token from a file.

Update your login function by storing the token just before echoing the response:

 # cli.py from cotter import tokenhandler token_file_name = "cotter_token.json" @main.command() def login(): ... tokenhandler.store_token_to_file(response["oauth_token"], token_file_name) click.echo(resp.json()) 
Enter fullscreen mode Exit fullscreen mode

Create a Todo List

Our create todo list API looks like this:

 ### Create a new Todo list POST http://localhost:1234/list/create Authorization: Bearer <access_token> Content-Type: application/json { "name": "Web Repo" } 
Enter fullscreen mode Exit fullscreen mode

Let's add a function called create in our cli.py below our login function

 # cli.py @main.command() @click.option('--name', prompt='List name', help='Name for your new todo list') def create(name): """Create a todo list""" # Get access token  access_token = tokenhandler.get_token_from_file(token_file_name, api_key)["access_token"] # Construct request  url = "https://cottertodolist.herokuapp.com/list/create" headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + access_token} data = { "name": name } response = requests.post(url, json=data, headers=headers) if response.status_code != 200: response.raise_for_status() click.echo("List " + name + " created!") 
Enter fullscreen mode Exit fullscreen mode

The function above takes an argument called --name (it'll ask you for the name of the list), then call the API with our access token. If you run it, it'll look like this:

 ❯ python cli.py create List name: Web Repo List Web Repo created! 
Enter fullscreen mode Exit fullscreen mode

Add a Task to the list

To add a task to a list, the API looks like this:

 ### Create a Task within a list ### name = List name, task = Task name/description POST http://localhost:1234/todo/create Authorization: Bearer <access_token> Content-Type: application/json { "name": "Web Repo", "task": "Update favicon.ico" } 
Enter fullscreen mode Exit fullscreen mode

Since the user might have more than one list, we want to ask them to choose which list they want to use. We can use PyInquirer to make a nice prompt with multiple options. First, install PyInquirer:

 pip install PyInquirer 
Enter fullscreen mode Exit fullscreen mode

Then add a function called add in our cli.py.

 # cli.py # Import PyInquirer prompt from PyInquirer import prompt @main.command() def add(): """Create a todo task for a list""" # Get access token from file  access_token = tokenhandler.get_token_from_file(token_file_name, api_key)["access_token"] # Get all todo lists for the user  url = "https://cottertodolist.herokuapp.com/list" headers = {'Authorization': 'Bearer ' + access_token} response = requests.get(url, headers=headers) lists = response.json() # Prompt to pick list  options = map(lambda x: x["name"], lists) questions = [ { 'type': 'list', 'name': 'list_name', 'message': 'Add task to which list?', 'choices': options, }, { 'type': 'input', 'name': 'task_name', 'message': 'Task description', } ] answers = prompt(questions) if not answers: return # Call API to create task fot the selected list  url = "https://cottertodolist.herokuapp.com/todo/create" headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + access_token} data = { "name": answers['list_name'], "task": answers['task_name'] } response = requests.post(url, json=data, headers=headers) if response.status_code != 200: response.raise_for_status() click.echo("Task " + answers['task_name'] + " is added in list " + answers['list_name']) 
Enter fullscreen mode Exit fullscreen mode
  • First, we needed to call the API to list all todo lists for the user.
  • The first question has type: list which asks the user to choose which list they want to add a new task to.
  • The second question has type: input which asks the user to type in the new task's description.
  • Using the answers, we call our API to add the task to the chosen list.

python cli.py add

Show our Todo Lists

Now that we've created a list and added some tasks, let's see the list! The API to see a list looks like this:

 ### Show all lists GET http://localhost:1234/list Authorization: Bearer <access_token> 
Enter fullscreen mode Exit fullscreen mode

Let's add a function called ls to list the todo lists for this user in cli.py. We'll apply this rule:

  • python cli.py ls will ask you which list you want to see, then show that list.
  • python cli.py ls -a will immediately show all your lists.
 # cli.py  @main.command() @click.option('-a', '--all', is_flag=True) # Make a boolean flag def ls(all): """Show lists""" # Get access token from file  access_token = tokenhandler.get_token_from_file(token_file_name, api_key)["access_token"] # Get all lists  url = "https://cottertodolist.herokuapp.com/list" headers = {'Authorization': 'Bearer ' + access_token} response = requests.get(url, headers=headers) if response.status_code != 200: response.raise_for_status() listsFormatted = response.json() if all == True: # Show all tasks in all lists  for chosenList in listsFormatted: click.echo('\n' + chosenList['name']) for task in chosenList['tasks']: if task['done'] == True: click.echo("[✔] " + task['task']) else: click.echo("[ ] " + task['task']) else: # Show a prompt to choose a list  questions = [ { 'type': 'list', 'name': 'list', 'message': 'Which list do you want to see?', 'choices': list(map(lambda lst: lst['name'], listsFormatted)) }, ] answers = prompt(questions) if not answers: return # Get the chosen list  chosenList = list(filter(lambda lst: lst['name'] == answers['list'], listsFormatted)) if len(chosenList) <= 0: click.echo("Invalid choice of list") return chosenList = chosenList[0] # Show tasks in the chosen list  click.echo(chosenList['name']) for task in chosenList['tasks']: if task['done'] == True: click.echo("[✔] " + task['task']) else: click.echo("[ ] " + task['task']) 
Enter fullscreen mode Exit fullscreen mode
  • First, we get all the todo lists for this user
  • If the flag -a is specified, then we iterate over each list and print the list name and tasks along with the checkmark
  • If the flag -a is not specified, then we prompt the user to select a list, then we iterate over the tasks for that selected list and print the tasks with the checkmark.

Now try it out! It should look like this:

 ❯ python cli.py ls -a Web Repo [ ] Update favicon.ico [ ] Add our logo to the footer [ ] Add a GIF that shows how our product works Morning Routine [ ] Drink coffee [ ] Grab yogurt [ ] Drink fish-oil 
Enter fullscreen mode Exit fullscreen mode

Obviously, none of our tasks are done since we haven't made the function to mark a task as done. Let' do that next.

Check and Un-check a Task

We'll use PyInquirer's powerful checklist type to allow the user to check and un-check the tasks. Our API to update a task looks like this:

 ### Update task set done = true or false by id PUT http://localhost:1234/todo/update/done/274822869038400008 Authorization: Bearer <access_token> Content-Type: application/json { "done": true } 
Enter fullscreen mode Exit fullscreen mode

Let's add a function called toggle to our cli.py.

 # cli.py @main.command() def toggle(): """Update tasks in a list""" # Get access token from file  access_token = tokenhandler.get_token_from_file(token_file_name, api_key)["access_token"] # Call API to list all tasks  url = "https://cottertodolist.herokuapp.com/list" headers = {'Authorization': 'Bearer ' + access_token} response = requests.get(url, headers=headers) if response.status_code != 200: response.raise_for_status() listsFormatted = response.json() # Show a prompt to choose a list  questions = [ { 'type': 'list', 'name': 'list', 'message': 'Which list do you want to update?', 'choices': list(map(lambda lst: lst['name'], listsFormatted)) }, ] answers = prompt(questions) if not answers: return # Get the chosen list  chosenList = list(filter(lambda lst: lst['name'] == answers['list'], listsFormatted)) if len(chosenList) <= 0: click.echo("Invalid choice of list") return chosenList = chosenList[0] # Show an interactive checklist for the tasks  questions = [ { 'type': 'checkbox', 'message': chosenList['name'], 'name': chosenList['name'], 'choices': list(map(lambda task: {'name': task['task'], 'checked': task["done"]}, chosenList['tasks'])), } ] answers = prompt(questions) if not answers: return # Call our Update API for each task in the list  # set `done` as True or False based on answers  for task in chosenList['tasks']: url = "https://cottertodolist.herokuapp.com/todo/update/done/" + task['id'] headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + access_token} data = { "done": task['task'] in answers[chosenList['name']] } response = requests.put(url, json=data, headers=headers) if response.status_code != 200: response.raise_for_status() click.echo(answers) 
Enter fullscreen mode Exit fullscreen mode
  • First, we call the list API to get all the lists for the user and prompt them to select a list.
  • Then using PyInquirer with type: checklist we can show them a checklist for the tasks in the list, and set {'checked': True} for tasks that are already done.
  • The user can use <space> to select or deselect a task, and press enter to update the task.

It looks like this:

python cli.py toggle

Let's see our complete todo list again:

 

❯ python cli.py ls -a

Web Repo
[ ] Update favicon.ico
[ ] Add our logo to the footer
[ ] Add a GIF that shows how our product works

Morning Routine
[✔] Drink coffee
[✔] Grab yogurt
[ ] Drink fish-oil

Enter fullscreen mode Exit fullscreen mode




Awesome! Our Todo-List CLI is done!

The final result should look like this:

Python Todo List CLI<br>
This post is written by the team at Cotter – we are building lightweight, fast, and passwordless login solutions for websites, mobile apps, and now CLIs too! If you're building a website, app, or CLI, we have all the tools you need to get a login flow set up in minutes.


What's Next?

It's pretty lame if your user has to always call python cli.py create, we want to change it to todo create instead. Follow this article to find out how to do that.


Questions & Feedback

Come and talk to the founders of Cotter and other developers who are using Cotter on Cotter's Slack Channel.

Ready to use Cotter?

If you enjoyed this tutorial and want to integrate Cotter into your website or app, you can create a free account and check out our documentation.

If you need help, ping us on our Slack channel or email us at team@cotter.app.

Top comments (3)

Collapse
 
skruzic1 profile image
Stanko Kružić

This doesn't work for me. I am constantly getting a 401 error from cottertodolist after successful login. It looks like problem is with this part of code:

 resp = requests.post(url, json=data, headers=headers) if resp.status_code != 200: resp.raise_for_status() 

The error I get is:

requests.exceptions.HTTPError: 401 Client Error: UNAUTHORIZED for url: https://cottertodolist.herokuapp.com/login_register

Any help is appreciated.

Collapse
 
putrikarunia profile image
Putri Karunia

Hi Stanko, thanks for catching this! I believe this issue is resolved in Github.

Collapse
 
skruzic1 profile image
Stanko Kružić

Sure it is, I created the issue :)

Thanks for the solution.