Update ToDo API
In this chapter we will create endpoint /api/todos/{:id}
to update our Task
model
Write the Test
Write test at tests/features/task/update_task_api_test.py
from http import HTTPStatus
from tests.base import BaseAPITestCase
from app.tasks.models.task import Task
from app.factory import db
class UpdateTaskAPITest(BaseAPITestCase):
def test_update_task(self):
endpoint = "/api/todos"
task = Task(name="Buy groceries")
db.session.add(task)
db.session.commit()
db.session.refresh(task)
json_data = {
"name": "Buy milk",
"completed": True,
}
resp = self.client.post(
f"{endpoint}/{task.id}",
json=json_data
)
self.assertEqual(resp.status_code, HTTPStatus.OK)
resp_json = resp.json or dict()
data = resp_json["data"]
self.assertEqual(data["name"], "Buy milk")
self.assertEqual(data["completed"], True)
Task.query.delete()
db.session.commit()
Run our test
$ bin/test tests/features/task/update_task_api_test.py
It will error
F
======================================================================
FAIL: test_update_task (tests.features.task.update_task_api_test.UpdateTaskAPITest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/src/app/tests/features/task/update_task_api_test.py", line 24, in test_update_task
self.assertEqual(resp.status_code, HTTPStatus.OK)
AssertionError: 405 != <HTTPStatus.OK: 200>
----------------------------------------------------------------------
Ran 1 test in 0.032s
FAILED (failures=1)
Let's implement our view at app/tasks/views/update_task_api.py
from http import HTTPStatus
from flask import jsonify, make_response, request
from flask.typing import ResponseReturnValue
from flask.views import View
from app.factory import db
from app.tasks.models.task import Task
from app.tasks.schemas.task_schema import TaskSchema
class UpdateTaskAPI(View):
def dispatch_request(self, task_id) -> ResponseReturnValue:
task_schema = TaskSchema()
json_data = request.get_json() or dict()
task = Task.query.filter(
Task.id == task_id
).first()
task.query.update(json_data)
db.session.commit()
resp = {
"data": task_schema.dump(task)
}
return jsonify(resp), HTTPStatus.OK
Update our routes to
from app.http.route import Route
from app.tasks.views.detail_task_api import DetailTaskAPI
from app.tasks.views.list_task_api import ListTaskAPI
from app.tasks.views.create_task_api import CreateTaskAPI
from app.tasks.views.update_task_api import UpdateTaskAPI
from app.views.main_api import MainView
api_routes = [
Route.get("/", MainView),
Route.group("/api", routes=[
Route.post("/todos", view=CreateTaskAPI),
Route.get("/todos", view=ListTaskAPI),
Route.get("/todos/<task_id>", view=DetailTaskAPI),
Route.put("/todos/<task_id>", view=UpdateTaskAPI),
]),
]
Re-run the test
$ bin/test tests/features/task/update_task_api_test.py
It should pass
.
----------------------------------------------------------------------
Ran 1 test in 0.041s
OK
Let's add test when updated Task
does not exists
...
def test_update_task_when_id_not_exists(self):
endpoint = "/api/todos"
json_data = {
"name": "Buy milk tea",
}
resp = self.client.put(f"{endpoint}/123", json=json_data)
self.assertEqual(resp.status_code, HTTPStatus.NOT_FOUND)
Re-run the test
$ bin/test tests/features/task/update_task_api_test.py
It should fail
.E
======================================================================
ERROR: test_update_task_when_id_not_exists (tests.features.task.update_task_api_test.UpdateTaskAPITest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/src/app/tests/features/task/update_task_api_test.py", line 39, in test_update_task_when_id_not_exists
resp = self.client.put(f"{endpoint}/123", json=json_data)
File "/usr/local/lib/python3.8/site-packages/werkzeug/test.py", line 1150, in put
return self.open(*args, **kw)
File "/usr/local/lib/python3.8/site-packages/flask/testing.py", line 223, in open
response = super().open(
File "/usr/local/lib/python3.8/site-packages/werkzeug/test.py", line 1094, in open
response = self.run_wsgi_app(request.environ, buffered=buffered)
File "/usr/local/lib/python3.8/site-packages/werkzeug/test.py", line 961, in run_wsgi_app
rv = run_wsgi_app(self.application, environ, buffered=buffered)
File "/usr/local/lib/python3.8/site-packages/werkzeug/test.py", line 1242, in run_wsgi_app
app_rv = app(environ, start_response)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2548, in __call__
return self.wsgi_app(environ, start_response)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2528, in wsgi_app
response = self.handle_exception(e)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 2525, in wsgi_app
response = self.full_dispatch_request()
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1822, in full_dispatch_request
rv = self.handle_user_exception(e)
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1820, in full_dispatch_request
rv = self.dispatch_request()
File "/usr/local/lib/python3.8/site-packages/flask/app.py", line 1796, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
File "/usr/local/lib/python3.8/site-packages/flask/views.py", line 107, in view
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
File "/usr/src/app/app/tasks/views/update_task_api.py", line 28, in dispatch_request
task.query.update(json_data)
AttributeError: 'NoneType' object has no attribute 'query'
----------------------------------------------------------------------
Ran 2 tests in 0.057s
FAILED (errors=1)
Fix our implementation to
from http import HTTPStatus
from flask import jsonify, make_response, request
from flask.typing import ResponseReturnValue
from flask.views import View
from app.factory import db
from app.tasks.models.task import Task
from app.tasks.schemas.task_schema import TaskSchema
class UpdateTaskAPI(View):
def dispatch_request(self, task_id) -> ResponseReturnValue:
task_schema = TaskSchema()
json_data = request.get_json() or dict()
task = Task.query.filter(
Task.id == task_id
).first_or_404()
task.query.update(json_data)
db.session.commit()
resp = {
"data": task_schema.dump(task)
}
return jsonify(resp), HTTPStatus.OK
Re-run the test
$ bin/test tests/features/task/update_task_api_test.py
It should pass
..
----------------------------------------------------------------------
Ran 2 tests in 0.056s
OK
Add more test to validate json schema
...
def test_update_task_when_required_payload_empty(self):
endpoint = "/api/todos"
task = Task(name="Buy groceries")
db.session.add(task)
db.session.commit()
db.session.refresh(task)
json_data = {}
resp = self.client.put(
f"{endpoint}/{task.id}",
json=json_data
)
self.assertEqual(resp.status_code, HTTPStatus.BAD_REQUEST)
Task.query.delete()
db.session.commit()
Re-run the test
$ bin/test tests/features/task/update_task_api_test.py
It should fail
..F
======================================================================
FAIL: test_update_task_when_required_payload_empty (tests.features.task.update_task_api_test.UpdateTaskAPITest)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/usr/src/app/tests/features/task/update_task_api_test.py", line 55, in test_update_task_when_required_payload_empty
self.assertEqual(resp.status_code, HTTPStatus.BAD_REQUEST)
AssertionError: 200 != <HTTPStatus.BAD_REQUEST: 400>
----------------------------------------------------------------------
Ran 3 tests in 0.074s
FAILED (failures=1)
Update our implementation to
from http import HTTPStatus
from flask import jsonify, make_response, request
from flask.typing import ResponseReturnValue
from flask.views import View
from app.factory import db
from app.tasks.models.task import Task
from app.tasks.schemas.task_schema import TaskSchema
class UpdateTaskAPI(View):
def dispatch_request(self, task_id) -> ResponseReturnValue:
task_schema = TaskSchema()
json_data = request.get_json() or dict()
errors = task_schema.validate(json_data)
if errors:
return make_response(
jsonify(errors=str(errors)),
HTTPStatus.BAD_REQUEST
)
task = Task.query.filter(
Task.id == task_id
).first_or_404()
task.query.update(json_data)
db.session.commit()
resp = {
"data": task_schema.dump(task)
}
return jsonify(resp), HTTPStatus.OK
Re-run the test
$ bin/test tests/features/task/update_task_api_test.py
It should pass
...
----------------------------------------------------------------------
Ran 3 tests in 0.072s
OK
Let's re-run all of our tests
$ bin/test discover -s tests -p '*_test.py'
It should pass
..........
----------------------------------------------------------------------
Ran 10 tests in 0.186s
OK
Your final project structure should be look like this
flask-todo
├── app
│ ├── config.py
│ ├── factory.py
│ ├── http
│ │ ├── __init__.py
│ │ └── route.py
│ ├── __init__.py
│ ├── routes
│ │ ├── api.py
│ │ └── __init__.py
│ ├── tasks
│ │ ├── __init__.py
│ │ ├── models
│ │ │ ├── __init__.py
│ │ │ └── task.py
│ │ ├── schemas
│ │ │ ├── __init__.py
│ │ │ └── task_schema.py
│ │ └── views
│ │ ├── create_task_api.py
│ │ ├── detail_task_api.py
│ │ ├── __init__.py
│ │ ├── list_task_api.py
│ │ └── update_task_api.py
│ └── views
│ ├── __init__.py
│ └── main_api.py
├── bin
│ └── test
├── db
│ ├── create.sql
│ └── Dockerfile
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── .env.dev
├── .gitignore
├── migrations
│ ├── alembic.ini
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions
│ └── 213adaf34061_create_tasks_tables.py
├── requirements.txt
├── serve.py
└── tests
├── base.py
├── features
│ ├── __init__.py
│ └── task
│ ├── create_task_api_test.py
│ ├── detail_task_api_test.py
│ ├── __init__.py
│ ├── list_task_api_test.py
│ └── update_task_api_test.py
├── __init__.py
└── unit
├── __init__.py
└── task
├── __init__.py
└── task_model_test.py
17 directories, 45 files
We can commit our works before continuing to next chapter.
(venv)$ git add .
(venv)$ git commit -m "Create UpdateTaskAPI endpoint"