Section 1: Create initial Todo application

For the rest of this workshop, we will be building a serverless Todo application. The application will allow for creating Todo’s, getting Todo’s, updating Todo’s, and deleting Todo’s. In terms of the REST API, it will consist of the following:

HTTP Method

URI Path

Description

GET

/todos/

Gets a list of all Todo’s

POST

/todos/

Creates a new Todo

GET

/todos/{id}

Gets a specific Todo

DELETE

/todos/{id}

Deletes a specific Todo

PUT

/todos/{id}

Updates the state of a Todo

Furthermore, a Todo will have the following schema:

{
  "description": {"type": "str"},
  "uid": {"type: "str"},
  "state": {"type: "str", "enum": ["unstarted", "started", "completed"]},
  "metadata": {
    "type": "object"
  },
  "username": {"type": "str"}
}

This step will focus on how to build a simple in-memory version of the Todo application. For this section we will be doing the following to create this version of the application:

Install Chalice

This step will ensure that chalice is installed in your virtualenv.

Instructions

  1. Install chalice inside of your virtualenv:

    $ pip install chalice
    

Verification

To make sure chalice was installed correctly, run:

$ chalice --version

Create a new Chalice project

Create the new Chalice project for the Todo application.

Instructions

  1. Create a new Chalice project called mytodo with the new-project command:

    $ chalice new-project mytodo
    

Verification

To ensure that the project was created, list the contents of the newly created mytodo directory:

$ ls mytodo
app.py           requirements.txt

It should contain an app.py file and a requirements.txt file.

Add the starting app.py

Copy a boilerplate app.py file to begin working on the Todo application

Instructions

  1. If you have not already done so, clone the repository for this workshop:

    $ git clone https://github.com/aws-samples/chalice-workshop.git
    
  2. Copy the over the app.py file to the mytodo Chalice application:

    $ cp ../chalice-workshop/code/todo-app/part1/01-new-project/app.py mytodo/app.py
    

Verification

To verify that the boilerplate application is working correctly, move into the mytodo application directory and run chalice local to spin up a version of the application running locally:

$ cd mytodo
$ chalice local
Serving on localhost:8000

In a separate terminal window now install httpie:

$ pip install httpie

And make an HTTP request to application running the localhost:

$ http localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:31:12 GMT
Server: BaseHTTP/0.3 Python/2.7.10

[]

This should return an empty list back as there are no Todo’s currently in the application.

Add a route for creating a Todo

Add a route for creating a Todo.

Instructions

  1. Open the app.py in an editor of your choice

  2. At the bottom of the app.py file add a function called add_new_todo()

  3. Decorate the add_new_todo() function with a route that only accepts POST to the URI /todos.

  4. In the add_new_todo() function use the app.current_request.json_body to add the Todo (which includes its description and metadata) to the database.

  5. In the add_new_todo() function return the ID of the Todo that was added in the database.

1
2
3
4
5
6
7
@app.route('/todos', methods=['POST'])
def add_new_todo():
    body = app.current_request.json_body
    return get_app_db().add_item(
        description=body['description'],
        metadata=body.get('metadata'),
    )

Verification

To verify that the new route works, run chalice local and in a separate terminal window run the following using httpie:

$ echo '{"description": "My first Todo", "metadata": {}}' | http POST localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:44:24 GMT
Server: BaseHTTP/0.3 Python/2.7.10

8cc673f0-7dd3-4e9d-a20b-245fcd34859d

This will return the ID of the Todo. For this example, it is 8cc673f0-7dd3-4e9d-a20b-245fcd34859d. Now check that it is now listed when you retrieve all Todos:

$ http localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 142
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:46:53 GMT
Server: BaseHTTP/0.3 Python/2.7.10

[
    {
        "description": "My first Todo",
        "metadata": {},
        "state": "unstarted",
        "uid": "8cc673f0-7dd3-4e9d-a20b-245fcd34859d",
        "username": "default"
    }
]

Add a route for getting a specific Todo

Add a route for getting a specific Todo.

Instructions

  1. In the app.py, add a function called get_todo() that accepts a uid as a parameter.

  2. Decorate the get_todo() function with a route that only accepts GET to the URI /todos/{uid}.

  3. In the get_todo() function return the specific Todo item from the database using the uid function parameter.

1
2
3
@app.route('/todos/{uid}', methods=['GET'])
def get_todo(uid):
    return get_app_db().get_item(uid)

Verification

To verify that the new route works, run chalice local and in a separate terminal window run the following using httpie:

$ echo '{"description": "My first Todo", "metadata": {}}' | http POST localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:44:24 GMT
Server: BaseHTTP/0.3 Python/2.7.10

8cc673f0-7dd3-4e9d-a20b-245fcd34859d

Now use the returned ID 8cc673f0-7dd3-4e9d-a20b-245fcd34859d to request the specific Todo:

$ http localhost:8000/todos/8cc673f0-7dd3-4e9d-a20b-245fcd34859d
HTTP/1.1 200 OK
Content-Length: 140
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:52:35 GMT
Server: BaseHTTP/0.3 Python/2.7.10

{
    "description": "My first Todo",
    "metadata": {},
    "state": "unstarted",
    "uid": "8cc673f0-7dd3-4e9d-a20b-245fcd34859d",
    "username": "default"
}

Add a route for deleting a specific Todo

Add a route for deleting a specific Todo.

Instructions

  1. In the app.py, add a function called delete_todo() that accepts a uid as a parameter.

  2. Decorate the delete_todo() function with a route that only accepts DELETE to the URI /todos/{uid}.

  3. In the delete_todo() function delete the Todo from the database using the uid function parameter.

1
2
3
@app.route('/todos/{uid}', methods=['DELETE'])
def delete_todo(uid):
    return get_app_db().delete_item(uid)

Verification

To verify that the new route works, run chalice local and in a separate terminal window run the following using httpie:

$ echo '{"description": "My first Todo", "metadata": {}}' | http POST localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:44:24 GMT
Server: BaseHTTP/0.3 Python/2.7.10

8cc673f0-7dd3-4e9d-a20b-245fcd34859d

Now check that it is now listed when you retrieve all Todos:

$ http localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 142
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:46:53 GMT
Server: BaseHTTP/0.3 Python/2.7.10

[
    {
        "description": "My first Todo",
        "metadata": {},
        "state": "unstarted",
        "uid": "8cc673f0-7dd3-4e9d-a20b-245fcd34859d",
        "username": "default"
    }
]

Now use the returned ID 8cc673f0-7dd3-4e9d-a20b-245fcd34859d to delete the specific Todo:

$ http DELETE localhost:8000/todos/8cc673f0-7dd3-4e9d-a20b-245fcd34859d
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:57:32 GMT
Server: BaseHTTP/0.3 Python/2.7.10

null

Now if all of the Todo’s are listed, it will no longer be present:

$ http localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 2
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:31:12 GMT
Server: BaseHTTP/0.3 Python/2.7.10

[]

Add a route for updating the state of a specific Todo

Add a route for updating the state of a specific Todo.

Instructions

  1. In the app.py, add a function called update_todo() that accepts a uid as a parameter.

  2. Decorate the update_todo() function with a route that only accepts PUT to the URI /todos/{uid}.

  3. In the update_todo() function use the app.current_request to update the Todo (which includes its description, metadata, and state) in the database for the uid provided.

1
2
3
4
5
6
7
8
@app.route('/todos/{uid}', methods=['PUT'])
def update_todo(uid):
    body = app.current_request.json_body
    get_app_db().update_item(
        uid,
        description=body.get('description'),
        state=body.get('state'),
        metadata=body.get('metadata'))

Verification

To verify that the new route works, run chalice local and in a separate terminal window run the following using httpie:

$ echo '{"description": "My first Todo", "metadata": {}}' | http POST localhost:8000/todos
HTTP/1.1 200 OK
Content-Length: 36
Content-Type: application/json
Date: Thu, 19 Oct 2017 23:44:24 GMT
Server: BaseHTTP/0.3 Python/2.7.10

de9a4981-f7fd-4639-97fb-2af247f20d79

Now determine the state of this newly added Todo:

$ http localhost:8000/todos/de9a4981-f7fd-4639-97fb-2af247f20d79
HTTP/1.1 200 OK
Content-Length: 140
Content-Type: application/json
Date: Fri, 20 Oct 2017 00:03:26 GMT
Server: BaseHTTP/0.3 Python/2.7.10

{
    "description": "My first Todo",
    "metadata": {},
    "state": "unstarted",
    "uid": "de9a4981-f7fd-4639-97fb-2af247f20d79",
    "username": "default"
}

Update the state of this Todo to started:

$ echo '{"state": "started"}' | http PUT localhost:8000/todos/de9a4981-f7fd-4639-97fb-2af247f20d79
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: application/json
Date: Fri, 20 Oct 2017 00:05:07 GMT
Server: BaseHTTP/0.3 Python/2.7.10

null

Ensure that the Todo has the started state when described:

$ http localhost:8000/todos/de9a4981-f7fd-4639-97fb-2af247f20d79
HTTP/1.1 200 OK
Content-Length: 138
Content-Type: application/json
Date: Fri, 20 Oct 2017 00:05:54 GMT
Server: BaseHTTP/0.3 Python/2.7.10

{
    "description": "My first Todo",
    "metadata": {},
    "state": "started",
    "uid": "de9a4981-f7fd-4639-97fb-2af247f20d79",
    "username": "default"
}

Final Code

When you are done your final code should look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from uuid import uuid4

from chalice import Chalice


app = Chalice(app_name='mytodo')
app.debug = True
_DB = None
DEFAULT_USERNAME = 'default'


class InMemoryTodoDB(object):
    def __init__(self, state=None):
        if state is None:
            state = {}
        self._state = state

    def list_all_items(self):
        all_items = []
        for username in self._state:
            all_items.extend(self.list_items(username))
        return all_items

    def list_items(self, username=DEFAULT_USERNAME):
        return self._state.get(username, {}).values()

    def add_item(self, description, metadata=None, username=DEFAULT_USERNAME):
        if username not in self._state:
            self._state[username] = {}
        uid = str(uuid4())
        self._state[username][uid] = {
            'uid': uid,
            'description': description,
            'state': 'unstarted',
            'metadata': metadata if metadata is not None else {},
            'username': username
        }
        return uid

    def get_item(self, uid, username=DEFAULT_USERNAME):
        return self._state[username][uid]

    def delete_item(self, uid, username=DEFAULT_USERNAME):
        del self._state[username][uid]

    def update_item(self, uid, description=None, state=None,
                    metadata=None, username=DEFAULT_USERNAME):
        item = self._state[username][uid]
        if description is not None:
            item['description'] = description
        if state is not None:
            item['state'] = state
        if metadata is not None:
            item['metadata'] = metadata


def get_app_db():
    global _DB
    if _DB is None:
        _DB = InMemoryTodoDB()
    return _DB


@app.route('/todos', methods=['GET'])
def get_todos():
    return get_app_db().list_items()


@app.route('/todos', methods=['POST'])
def add_new_todo():
    body = app.current_request.json_body
    return get_app_db().add_item(
        description=body['description'],
        metadata=body.get('metadata'),
    )


@app.route('/todos/{uid}', methods=['GET'])
def get_todo(uid):
    return get_app_db().get_item(uid)


@app.route('/todos/{uid}', methods=['DELETE'])
def delete_todo(uid):
    return get_app_db().delete_item(uid)


@app.route('/todos/{uid}', methods=['PUT'])
def update_todo(uid):
    body = app.current_request.json_body
    get_app_db().update_item(
        uid,
        description=body.get('description'),
        state=body.get('state'),
        metadata=body.get('metadata'))