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

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@app.route('/todos/{uid}', methods=['GET'])
2def get_todo(uid):
3    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@app.route('/todos/{uid}', methods=['DELETE'])
2def delete_todo(uid):
3    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@app.route('/todos/{uid}', methods=['PUT'])
2def update_todo(uid):
3    body = app.current_request.json_body
4    get_app_db().update_item(
5        uid,
6        description=body.get('description'),
7        state=body.get('state'),
8        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:

 1from uuid import uuid4
 2
 3from chalice import Chalice
 4
 5
 6app = Chalice(app_name='mytodo')
 7app.debug = True
 8_DB = None
 9DEFAULT_USERNAME = 'default'
10
11
12class InMemoryTodoDB(object):
13    def __init__(self, state=None):
14        if state is None:
15            state = {}
16        self._state = state
17
18    def list_all_items(self):
19        all_items = []
20        for username in self._state:
21            all_items.extend(self.list_items(username))
22        return all_items
23
24    def list_items(self, username=DEFAULT_USERNAME):
25        return self._state.get(username, {}).values()
26
27    def add_item(self, description, metadata=None, username=DEFAULT_USERNAME):
28        if username not in self._state:
29            self._state[username] = {}
30        uid = str(uuid4())
31        self._state[username][uid] = {
32            'uid': uid,
33            'description': description,
34            'state': 'unstarted',
35            'metadata': metadata if metadata is not None else {},
36            'username': username
37        }
38        return uid
39
40    def get_item(self, uid, username=DEFAULT_USERNAME):
41        return self._state[username][uid]
42
43    def delete_item(self, uid, username=DEFAULT_USERNAME):
44        del self._state[username][uid]
45
46    def update_item(self, uid, description=None, state=None,
47                    metadata=None, username=DEFAULT_USERNAME):
48        item = self._state[username][uid]
49        if description is not None:
50            item['description'] = description
51        if state is not None:
52            item['state'] = state
53        if metadata is not None:
54            item['metadata'] = metadata
55
56
57def get_app_db():
58    global _DB
59    if _DB is None:
60        _DB = InMemoryTodoDB()
61    return _DB
62
63
64@app.route('/todos', methods=['GET'])
65def get_todos():
66    return get_app_db().list_items()
67
68
69@app.route('/todos', methods=['POST'])
70def add_new_todo():
71    body = app.current_request.json_body
72    return get_app_db().add_item(
73        description=body['description'],
74        metadata=body.get('metadata'),
75    )
76
77
78@app.route('/todos/{uid}', methods=['GET'])
79def get_todo(uid):
80    return get_app_db().get_item(uid)
81
82
83@app.route('/todos/{uid}', methods=['DELETE'])
84def delete_todo(uid):
85    return get_app_db().delete_item(uid)
86
87
88@app.route('/todos/{uid}', methods=['PUT'])
89def update_todo(uid):
90    body = app.current_request.json_body
91    get_app_db().update_item(
92        uid,
93        description=body.get('description'),
94        state=body.get('state'),
95        metadata=body.get('metadata'))