In this chapter, we will improve the RESTful API that we started in the previous chapter and we will add authentication related security to it. We will:
PATCH
methodWhen we created the Category
model, we specified the True
value for the unique argument when we created the db.Column
instance named name
. As a result, the migrations generated the necessary unique constraint to make sure that the name
field has unique values in the category
table. This way, the database won't allow us to insert duplicate values for category.name
. However, the error message generated when we try to do so is not clear.
Run the following command to create a category with a duplicate name. There is already an existing category with the name equal to 'Information'
:
http POST :5000/api/categories/ name='Information'
The following is the equivalent curl
command:
curl -iX POST -H "Content-Type: application/json" -d '{"name":"Information"}'
:5000/api/categories/
The previous command will compose and send a POST
HTTP request with the specified JSON key-value pair. The unique constraint in the category.name
field won't allow the database table to persist the new category. Thus, the request will return an HTTP 400 Bad Request
status code with an integrity error message. The following lines show a sample response:
HTTP/1.0 400 BAD REQUEST Content-Length: 282 Content-Type: application/json Date: Mon, 15 Aug 2016 03:53:27 GMT Server: Werkzeug/0.11.10 Python/3.5.1 { "error": "(psycopg2.IntegrityError) duplicate key value violates unique constraint "category_name_key" DETAIL: Key (name)=(Information) already exists. [SQL: 'INSERT INTO category (name) VALUES (%(name)s) RETURNING category.id'] [parameters: {'name': 'Information'}]" }
Obviously, the error message is extremely technical and provides too many details about the database and the query that failed. We might parse the error message to automatically generate a more user friendly error message. However, instead of doing so, we want to avoid trying to insert a row that we know will fail. We will add code to make sure that a category is unique before we try to persist it. Of course, there is still a chance to receive the previously shown error if somebody inserts a category with the same name between the time we run our code, indicating that a category name is unique, and persist the changes in the database. However, the chances are lower and we can reduce the changes of the previously shown error message to be shown.
In a production-ready REST API we should never return the error messages returned by SQLAlchemy or any other database-related data, as it might include sensitive data that we don't want the users of our API to be able to retrieve. In this case, we are returning all the errors for debugging purposes and to be able to improve our API.
Now, we will add a new class method to the Category
class to allow us to determine whether a name is unique or not. Open the api/models.py
file and add the following lines within the declaration of the Category
class. The code file for the sample is included in the restful_python_chapter_07_01
folder:
@classmethod def is_unique(cls, id, name): existing_category = cls.query.filter_by(name=name).first() if existing_category is None: return True else: if existing_category.id == id: return True else: return False
The new Category.is_unique
class method receives the id
and the name
for the category that we want to make sure that has a unique name. If the category is a new one that hasn't been saved yet, we will receive a 0 for the id
value. Otherwise, we will receive the category id in the argument.
The method calls the query.filter_by
method for the current class to retrieve a category whose name matches the other category name. In case there is a category that matches the criteria, the method will return True
only if the id is the same one than the one received in the argument. In case no category matches the criteria, the method will return True
.
We will use the previously created class method to check whether a category is unique or not before creating and persisting it in the CategoryListResource.post
method. Open the api/views.py
file and replace the existing post
method declared in the CategoryListResource
class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01
folder:
def post(self): request_dict = request.get_json() if not request_dict: resp = {'message': 'No input data provided'} return resp, status.HTTP_400_BAD_REQUEST errors = category_schema.validate(request_dict) if errors: return errors, status.HTTP_400_BAD_REQUEST category_name = request_dict['name'] if not Category.is_unique(id=0, name=category_name): response = {'error': 'A category with the same name already exists'} return response, status.HTTP_400_BAD_REQUEST try: category = Category(category_name) category.add(category) query = Category.query.get(category.id) result = category_schema.dump(query).data return result, status.HTTP_201_CREATED except SQLAlchemyError as e: db.session.rollback() resp = {"error": str(e)} return resp, status.HTTP_400_BAD_REQUEST
Now, we will perform the same validation in the CategoryResource.patch
method. Open the api/views.py
file and replace the existing patch
method declared in the CategoryResource
class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01
folder:
def patch(self, id): category = Category.query.get_or_404(id) category_dict = request.get_json() if not category_dict: resp = {'message': 'No input data provided'} return resp, status.HTTP_400_BAD_REQUEST errors = category_schema.validate(category_dict) if errors: return errors, status.HTTP_400_BAD_REQUEST try: if 'name' in category_dict: category_name = category_dict['name'] if Category.is_unique(id=id, name=category_name): category.name = category_name else: response = {'error': 'A category with the same name already exists'} return response, status.HTTP_400_BAD_REQUEST category.update() return self.get(id) except SQLAlchemyError as e: db.session.rollback() resp = {"error": str(e)} return resp, status.HTTP_400_BAD_REQUEST
Run the following command to again create a category with a duplicate name:
http POST :5000/api/categories/ name='Information'
The following is the equivalent curl
command:
curl -iX POST -H "Content-Type: application/json" -d '{"name":"Information"}'
:5000/api/categories/
The previous command will compose and send a POST
HTTP request with the specified JSON key-value pair. The changes we made will generate a response with a user friendly error message and will avoid trying to persist the changes. The request will return an HTTP 400 Bad Request
status code with the error message in the JSON body. The following lines show a sample response:
HTTP/1.0 400 BAD REQUEST Content-Length: 64 Content-Type: application/json Date: Mon, 15 Aug 2016 04:38:43 GMT Server: Werkzeug/0.11.10 Python/3.5.1 { "error": "A category with the same name already exists" }
Now, we will add a new class
method to the Message
class to allow us to determine whether a message is unique or not. Open the api/models.py
file and add the following lines within the declaration of the Message
class. The code file for the sample is included in the restful_python_chapter_07_01
folder:
@classmethod def is_unique(cls, id, message): existing_message = cls.query.filter_by(message=message).first() if existing_message is None: return True else: if existing_message.id == id: return True else: return False
The new Message.is_unique
class method receives the id
and the message
for the message that we want to make sure that has a unique value for the message field. If the message is a new one that hasn't been saved yet, we will receive a 0
for the id
value. Otherwise, we will receive the message id in the argument.
The method calls the query.filter_by
method for the current class to retrieve a message whose message field matches the other message's message. In case there is a message that matches the criteria, the method will return True
only if the id is the same one than the one received in the argument. In case no message matches the criteria, the method will return True
.
We will use the previously created class method to check whether a message is unique or not before creating and persisting it in the MessageListResource.post
method. Open the api/views.py
file and replace the existing post
method declared in the MessageListResource
class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01
folder:
def post(self): request_dict = request.get_json() if not request_dict: response = {'message': 'No input data provided'} return response, status.HTTP_400_BAD_REQUEST errors = message_schema.validate(request_dict) if errors: return errors, status.HTTP_400_BAD_REQUEST message_message = request_dict['message'] if not Message.is_unique(id=0, message=message_message): response = {'error': 'A message with the same message already exists'} return response, status.HTTP_400_BAD_REQUEST try: category_name = request_dict['category']['name'] category = Category.query.filter_by(name=category_name).first() if category is None: # Create a new Category category = Category(name=category_name) db.session.add(category) # Now that we are sure we have a category # create a new Message message = Message( message=message_message, duration=request_dict['duration'], category=category) message.add(message) query = Message.query.get(message.id) result = message_schema.dump(query).data return result, status.HTTP_201_CREATED except SQLAlchemyError as e: db.session.rollback() resp = {"error": str(e)} return resp, status.HTTP_400_BAD_REQUEST
Now, we will perform the same validation in the MessageResource.patch
method. Open the api/views.py
file and replace the existing patch
method declared in the MessageResource
class with the following lines. The lines that have been added or modified are highlighted. The code file for the sample is included in the restful_python_chapter_07_01
folder:
def patch(self, id): message = Message.query.get_or_404(id) message_dict = request.get_json(force=True) if 'message' in message_dict: message_message = message_dict['message'] if Message.is_unique(id=id, message=message_message): message.message = message_message else: response = {'error': 'A message with the same message already exists'} return response, status.HTTP_400_BAD_REQUEST if 'duration' in message_dict: message.duration = message_dict['duration'] if 'printed_times' in message_dict: message.printed_times = message_dict['printed_times'] if 'printed_once' in message_dict: message.printed_once = message_dict['printed_once'] dumped_message, dump_errors = message_schema.dump(message) if dump_errors: return dump_errors, status.HTTP_400_BAD_REQUEST validate_errors = message_schema.validate(dumped_message) if validate_errors: return validate_errors, status.HTTP_400_BAD_REQUEST try: message.update() return self.get(id) except SQLAlchemyError as e: db.session.rollback() resp = {"error": str(e)} return resp, status.HTTP_400_BAD_REQUEST
Run the following command to create a message with a duplicate value for the message field:
http POST :5000/api/messages/ message='Checking temperature sensor' duration=25 category="Information"
The following is the equivalent curl
command:
curl -iX POST -H "Content-Type: application/json" -d '{"message":"Checking temperature sensor", "duration":25, "category": "Information"}' :5000/api/messages/
The previous command will compose and send a POST
HTTP request with the specified JSON key-value pair. The changes we made will generate a response with a user friendly error message and will avoid trying to persist the changes in the message. The request will return an HTTP 400 Bad Request
status code with the error message in the JSON body. The following lines show a sample response:
HTTP/1.0 400 BAD REQUEST Content-Length: 66 Content-Type: application/json Date: Mon, 15 Aug 2016 04:55:46 GMT Server: Werkzeug/0.11.10 Python/3.5.1 { "error": "A message with the same message already exists" }
18.222.182.66