Part 5: RESTful Resources and Advanced Request Parsing
Tutorial Sections
- Project Overview
- Part 1: Project Setup and Environment Configuration
- Part 2: Database Models, Migrations and JWT Setup
- Part 3: API Configuration and User Registration
- Part 4: JWT Authentication, Decorators and Blacklisting Tokens
- Part 5: RESTful Resources and Advanced Request Parsing
- Part 6: Pagination, HATEOAS and Parameterized Testing
Table of Contents
Github Links for Part 5
Project Structure
The chart below shows the folder structure for this section of the tutorial. In this post, we will work on all files marked as NEW CODE
. Files that contain code from previous sections but will not be modified in this post are marked as NO CHANGES
.
. (project root folder) |- src | |- flask_api_tutorial | |- api | | |- auth | | | |- __init__.py | | | |- business.py | | | |- decorators.py | | | |- dto.py | | | |- endpoints.py | | | | | |- widgets | | | |- __init__.py | | | |- business.py | | | |- dto.py | | | |- endpoints.py | | | | | |- __init__.py | | | |- models | | |- __init__.py | | |- token_blacklist.py | | |- user.py | | |- widget.py | | | |- util | | |- __init__.py | | |- datetime_util.py | | |- result.py | | | |- __init__.py | |- config.py | |- tests | |- __init__.py | |- conftest.py | |- test_auth_login.py | |- test_auth_logout.py | |- test_auth_register.py | |- test_auth_user.py | |- test_config.py | |- test_user.py | |- util.py | |- .env |- .gitignore |- .pre-commit-config.yaml |- pyproject.toml |- pytest.ini |- README.md |- run.py |- setup.py |- tox.iniKEY:FOLDERNEW CODENO CHANGESEMPTY FILE
Introduction
In the previous section, we created four API endpoints to perform basic user registration and authentication functions. However, these API endpoints are not designed as RESTful resources (I explained my reasoning for this choice in Part 3).
In this section of the tutorial, we will create a resource that is REST-like (REST-faux? REST-adjacent?). I am deliberately not describing it as RESTful, because designing a truly RESTful system is HARD. Check out this blog post from Roy Fielding and the discussion in the comments to get an idea of what I mean.
The only features of this resource that I am willing to state are 100% bona fide REST-compliant are:
- The naming convention of the resource and associated endpoints.
- The HTTP methods supported by each endpoint that enables clients to perform CRUD operations.
- Through the use of pagination and navigational links included in JSON sent by the server, clients can interact with the API purely through hypertext (i.e., clients NEVER need to manually construct URLs to interact with the API).
The resource we will create is a collection of widgets. I decided to model something generic rather than the cliche “to-do list” project that you encounter in every introductory programming tutorial. I feel safe assuming that you are not reading this because you have a burning desire to create the next, great API-driven to-do list.
The main purpose of this section is to learn more advanced techniques for request parsing and response marshalling. The Widget
model will contain attributes that require creating custom input types for parsing request data. The Widget
model also contains hybrid properties that will require rendering various data types in JSON. Whatever project you have in mind, the techniques demonstrated with the Widget
model and associated RequestParser
and API model instances can easily be adapted to any object in your domain.
widget_ns
Endpoints
The proper way to name resources is one of the (many) hotly debated topics regarding RESTful web services. I recommend taking the time to read the articles below to understand the current, accepted best practices:
- Resource Naming (RestApiTutorial.com)
- REST API Design - Resource Modeling (ThoughtWorks)
- RESTful API Design. Best Practices in a Nutshell (Philipp Hauer's Blog)
The accepted best practice for naming resources is to use plural nouns when constructing a URI for a resource (e.g. /widgets
instead of /widget
). Another widely accepted standard is to create two endpoints per resource — one for operations that apply to the entire collection (e.g., /api/v1/widgets
) and one for operations that apply only to a single instance (e.g., /api/v1/widgets/<name>
).
Another common architectural pattern is to expose methods which allow the client to perform CRUD operations on the resource. CRUD (Create, Retrieve, Update, Delete) is a term that usually refers to relational database systems, but it is also valid for any dataset that can be manipulated by a client, including RESTful resources.
Now, you might be wondering how this can be accomplished if we only expose two endpoints per resource, and CRUD defines (at leat) four different operations. Table 1 shows how we will structure the API for our Widget
resource:
Table 1 | ||||
Widget API endpoint specifications | ||||
Endpoint Name | URI | HTTP Method | CRUD Operation | Required Token |
---|---|---|---|---|
api.widget_list | /api/v1/widgets | POST | Create a new widget | Admin user |
api.widget_list | /api/v1/widgets | GET | Retrieve a list of widgets | Regular user |
api.widget | /api/v1/widgets/<name> | GET | Retrieve a single widget | Regular user |
api.widget | /api/v1/widgets/<name> | PUT | Update an existing widget | Admin user |
api.widget | /api/v1/widgets/<name> | DELETE | Delete a single widget | Admin user |
Each endpoint can be configured to respond to a unique set of HTTP method types. Per Table 1, the api.widget_list
endpoint will support GET
and POST
requests, and the api.widget
endpoint will suport GET
, PUT
, and DELETE
requests. Each combination of endpoint and method type are mapped to a CRUD operation. The remainder of this section (and the entire next section) are devoted to implementing the Widget
API.
flask add-user
Command
Way back in Part 1, we discussed the Flask CLI and created the method that executes when the flask shell
command is invoked. The Flask CLI is based on a project called Click which can be used to create powerful Python CLI applications, and is easy to get started with thanks to excellent documentation.
Currently, the api/v1/auth/register
endpoint can only create regular (non-admin) users. We want to leave it this way since this endpoint is publically-accessible. However, we also need a way to create admin users since regular users cannot create, update or delete widget objects.
There are a few different methods we could use to create admin users. Using the flask shell
command, we can execute arbitrary Python code to create admin users. Or, we could create a function and store it in a file in our project, then run the function through the command-line. However, both of these methods are cumbersome and would require documentation if anyone else needed to create an admin user.
My preferred solution is to expose a command in the Flask CLI that can create both regular and admin users. To do so, open run.py
in the project root folder. First, update the import statements to include click
(Line 4):
|
|
Then, add the content below and save the file:
|
|
You might be wondering how is this different than the idea I dismissed, “create a function and store it in a file in our project, then run the function through the command-line”? The main difference is discoverability — Flask automatically includes a CLI with all installations and extending the CLI with custom commands is one of the intended use cases.
Also, help documentation is automatically generated for CLI commands (via click). You can view the documentation for the add-user
command by running flask add-user --help
:
(flask-api-tutorial) flask-api-tutorial $ flask add-user --help
Usage: flask add-user [OPTIONS] EMAIL
Add a new user to the database with email address = EMAIL.
Options:
--admin New user has administrator privileges
--password TEXT Do not set password on the command line!
--help Show this message and exit.
Users can view all Flask CLI commands, including custom commands by running flask
:
(flask-api-tutorial) flask-api-tutorial $ flask
Usage: flask [OPTIONS] COMMAND [ARGS]...
A general utility script for Flask applications.
Provides commands from Flask, extensions, and the application. Loads the
application defined in the FLASK_APP environment variable, or from a
wsgi.py file. Setting the FLASK_ENV environment variable to 'development'
will enable debug mode.
$ export FLASK_APP=hello.py
$ export FLASK_ENV=development
$ flask run
Options:
--version Show the flask version
--help Show this message and exit.
Commands:
add-user add a new user
db Perform database migrations.
routes Show the routes for the app.
run Run a development server.
shell Run a shell in the app context.
This is my preferred solution because the process for creating admin users is now documented, and the command is accessible only to those with access to the server where the flask instance is running.
Finally, let’s demonstrate how to use the flask add-user
command:
Create regular user
(flask-api-tutorial) flask-api-tutorial $ flask add-user user@test.com Password: Repeat for confirmation: Successfully added new user: <User email=user@test.com, public_id=be5e164e-7d33-4919-8d37-a5d02ca27d47, admin=False>
Create admin user
(flask-api-tutorial) flask-api-tutorial $ flask add-user admin@test.com --admin Password: Repeat for confirmation: Successfully added new admin user: <User email=admin@test.com, public_id=1e248b12-e08b-467f-86bf-80f547f20ce6, admin=True>
Error: Email already exists
(flask-api-tutorial) flask-api-tutorial $ flask add-user user@test.com Password: Repeat for confirmation: Error: user@test.com is already registered
As you can see, after running the command, the user is immediately prompted to create a password and to confirm the password for the new user. Before proceeding, please create an admin user with the flask add-user
command since creating, modifying and deleting widgets cannot be performed otherwise.
Widget
DB Model
Before we can begin implementing the API endpoints in Table 1, we need to create a database table to store Widget
instances. To do so, we extend db.Model
(just like we did for the User
and BlacklistedToken
classes). Create a new file widget.py
in src/flask_api_tutorial/models
and add the content below:
|
|
To demonstrate the process of serializing a complex object to/from JSON, the Widget
class includes attributes with as many different data types as possible. Additionally, the project requirements include various rules restricting which values are considered valid for the name
and deadline
attributes:
- The widget model contains attributes with URL, datetime, timedelta and bool data types, along with normal text fields.
- The widget model contains a "name" field which must be a string value containing only lowercase-letters, numbers and the "-" (hyphen character) or "_" (underscore character).
- The widget model contains a "deadline" attribute which must be a datetime value where the date component is equal to or greater than the current date. The comparison does not consider the value of the time component when this comparison is performed.
Let’s take a look at how these attributes are defined and how they fulfill the various project requirements:
Line 22: Table 1 indicates that the value of the
name
attribute will be embedded in the URI for eachWidget
. Because of this, the value must be unique which is ensured by settingunique=True
. Additionally, it would be ideal to prevent the user from creating a newwidget
if thename
contains characters that are not URL-safe (/, +, & etc.). To accomplish this, we will design a customRequestParser
data type that considers a value to be valid if it contains ONLY lowercase-letters, numbers, underscore and/or hyphen characters.Line 23: The purpose of the
info_url
attribute is to demonstrate how to implement input validation for URL values. Any values that are not recognized as a valid URL must be rejected without adding the widget to the database. The validation logic will be implemented using a built-inRequestParser
data type for URL values.Line 25: The purpose of the
deadline
attribute is to demonstrate how to implement input validation fordatetime
values. Additionally, onlydatetime
values that are either the same as or greater than the current date are considered valid. Values not recognized as validdatetime
values AND validdatetime
values in the past must be rejected without adding the widget to the database.Lines 27-28: We will also use the
Widget
class to demonstrate how relationships between database tables are defined and managed. We have defined a foreign key relationship between this table and thesite_user
table. Theowner
of each widget will be theUser
that created it (TheUser.id
attribute will be stored when eachWidget
is created).Lines 33-41: Both of these hybrid properties convert the datetime value stored in the database to the timezone of the machine executing the code and formats the datetime as a string value.
Lines 43-45:
deadline_passed
is a bool value, this has been included as part of theWidget
model solely to increase the number of data types that are serialized whenWidget
objects are included in an HTTP response. This attribute should returnTrue
if the curent date is greater than the date stored indeadline
, and should returnFalse
if the current date is less than or the same as the date stored indeadline
.Lines 47-50:
time_remaining
is atimedelta
value that represents the time remaining until thedeadline
is passed. If the curent date is greater than the date stored indeadline
, then this attribute should returntimedelta(0)
.Lines 52-55:
time_remaining_str
converts thetimedelta
object returned bytime_remaining
to a formatted string if thedeadline
has not been passed. If thedeadline
has passed, "No time remaining" is returned.Lines 57-59:
find_by_name
is a class method just like thefind_by_email
andfind_by_public_id
methods we previously created in theUser
class. Since thename
attribute must be unique, we can use it to retrieve a specificWidget
.
Next, we need to update run.py
in order for the Flask-Migrate extension to recognize the Widget
class and create a migration script that adds the new table to the database (this is the same process we previously performed for the User class in Part 2 and for the BlacklistedToken class in Part 4).
Open run.py
in the project root folder and update the import statements to include the Widget
class (Line 9). Then add the Widget
class to the dict
object that is returned by the make_shell_context
function (Line 16):
|
|
Next, run flask db migrate
and add a message explaining the changes that will be made by executing this migration script:
(flask-api-tutorial) flask-api-tutorial $ flask db migrate --message "add widget model"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'widget'
Generating /Users/aaronluna/Projects/flask-api-tutorial/migrations/versions/7c66df0e878f_add_widget_model.py ... done
Next, run flask db upgrade
to run the migration script on the database in your development environment:
(flask-api-tutorial) flask-api-tutorial $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade 079d26d45cc9 -> 7c66df0e878f, add widget model
After the widget
table has been added to the database, we can begin implementing the API endpoints specified in Table 1.
Create Widget
So where should we begin? In my opinion, the endpoint that should be implemented first is the endpoint responsible for the create operation, since without Widget
objects there’s nothing to be retrieved, updated or deleted. In Part 3 we followed the process below for each endpoint in the auth_ns
namespace. We will follow the same process to implement the widget_ns
endpoints in Table 1:
- Create request parsers/API models to validate request data and serialize response data.
- Define the business logic necessary to process the request if validation succeeds.
- Create a class that inherits from
Resource
and bind it to the API endpoint/URL route. - Define the set of HTTP methods that the API endpoint will support and expose methods on the concrete
Resource
class for each. Methods namedget
,post
,put
,delete
,patch
,options
orhead
will be called when the API endpoint receives a request of the same HTTP method type. - Document the
Resource
class and methods which handle HTTP requests as explained in the Flask-RESTx docs. Most of the content on the Swagger UI page is generated by decorating the concreteResource
classes created in Step 3 and the handler methods in Step 4. - Import the business logic functions created in Step 2 and call them within the HTTP method that corresponds to the operation performed by the business logic.
- Create unit tests to verify that the input validation provided by the request parsers/API models is working correctly, and verify the endpoint behaves as expected.
Step 1 says create request parsers/API models to validate request data and serialize response data. So let’s dive into it!
create_widget_reqparser
Request Parser
When a client sends a request to create a new Widget
, what data is required? Take a look at the attributes of the Widget
class:
|
|
The client must provide values for the three attributes highlighted above (name
, info_url
and deadline
). What about the other attributes?
id: This is the database table's primary key. The value is automatically set when a
Widget
is committed to the database (starting at one, then incrementing by one each time aWidget
is added).created_at: When a
Widget
object is created, the expression specified in thedefault
parameter is evaluated and stored as the value forcreated_at
.utc_now
returns the current date and time as adatetime
value that is timezone-aware and localized to UTC.owner_id: This value is a foreign-key, which is indicated by
db.ForeignKey("site_user.id")
. site_user is the name of the database table whereUser
objects are stored, and site_user.id is the primary-key that theowner_id
column is referencing.owner: It is important to note that
owner
is NOT an instance ofdb.Column
. This means that unlike the otherWidget
class attributes,owner
is not a column that exists in thewidget
database table. Instead, this is a relationship object that demonstates one of the main features of the SQLAlchemy ORM (Click here for more information).When processing a request to create a new
Widget
, the business logic will set the value of theowner_id
attribute to theid
of theUser
who sent the request. After theWidget
is created and committed to the database, theowner
attribute will contain aUser
object that represents theUser
who created it.Another interesting feature is achieved by
backref=db.backref("widgets")
. This creates a new attribute on allUser
objects named widgets (without modifying theUser
class at all), which is a list of allWidget
objects in the database whereUser.id
is equal toowner_id
.
Flask-RESTx includes helpful pre-defined types (e.g., email, URL, etc.) in the inputs
module for validating request data. When we created the auth_reqparser
in Part 3, we imported the email
class from flask_restx.inputs
to verify if a value provided by the client is a valid email address. You can also define custom input types if none of the pre-defined types are sufficient, and we will do so for both the name
and deadline
attributes.
Create a new file named dto.py
in src/flask_api_tutorial/api/widgets
and enter the content below:
|
|
Let’s break this down by looking at how each of the attributes are validated by the code above:
name
Argument
None of the pre-defined types in the flask_restx.inputs
module perform input validation that satisfies the requirements of the name
attribute. Thankfully, Flask-RESTx provides a way to create custom types that can be used in the same way. The example of a custom type shown below is taken from the documentation:
def my_type(value):
'''Parse my type'''
if not condition:
raise ValueError('This is not my type')
return parse(value)
The term “type” is (IMO) misleading since we only need to create a function (not a class). The function must accept at least one parameter — the value provided by the client. If the value is successfully validated, the function must convert the value to the expected data type before returning it (in the case of the name
attribute, the expected type is a string so no conversion is necessary). If the value provided by the client is invalid, the function must raise a ValueError
.
The widget_name
function is adapted directly from the example shown above to satisfy the project requirements:
|
|
Line 14: The simplist way to implement our custom type is with a regular expression. The regex
^[\w-]+$
will match any string that consists of ONLY alphanumeric characters (which includes the underscore character) and the hyphen character.The syntax of regular expressions is extremely dense. To make any regex easier to understand, we could compile it with the
re.VERBOSE
flag. This causes whitespace that is not within a character class to be ignored, allowing us to place comments within the regex to document the design and the effect of each component of the expression. For example, we could document our regex as shown below (I am only showing this for demonstration purposes, and have not modified the code insrc/flask_api_tutorial/api/widgets/dto.py
):NAME_REGEX = re.compile(r""" ^ # Matches the beginning of the string [\w-] # Character class: \w matches all alphanumeric characters (including underscore), - matches the hyphen character + # Match one or more instances of the preceding character class $ # Matches the end of the string """, re.VERBOSE)
Line 15: If the value does not match the regex, a
ValueError
is raised with a message explaining why the value is not a validwidget_name
.Line 19: If the value passed to this function matches the regex, the value is a valid
widget_name
and is returned.
We can see how this function works by testing it in the flask shell
:
(flask-api-tutorial) flask-api-tutorial $ flask shell
Python 3.7.6 (default, Jan 19 2020, 06:08:58)
[Clang 11.0.0 (clang-1100.0.33.8)] on darwin
App: flask-api-tutorial [development]
Instance: /Users/aaronluna/Projects/flask_api_tutorial/instance
>>> from flask_api_tutorial.api.widgets.dto import widget_name
# 1. 'test' is a valid widget_name
>>> widget_name("test")
'test'
# 2. 'test_1-AZ' is a valid widget_name
>>> widget_name("test_1-AZ")
'test_1-AZ'
# 3. 'some widget' is NOT a valid widget_name
>>> widget_name("some widget")
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/aaronluna/Projects/flask_api_tutorial/src/flask_api_tutorial/api/widgets/dto.py", line 18, in widget_name
f"'{name}' contains one or more invalid characters. Widget name must contain "
ValueError: 'some widget' contains one or more invalid characters. Widget name must contain only letters, numbers, hyphen and/or underscore characters.
# 4. 't**&*&' is NOT a valid widget_name
>>> widget_name("t**&*&")
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/Users/aaronluna/Projects/flask_api_tutorial/src/flask_api_tutorial/api/widgets/dto.py", line 18, in widget_name
f"'{name}' contains one or more invalid characters. Widget name must contain "
ValueError: 't**&*&' contains one or more invalid characters. Widget name must contain only letters, numbers, hyphen and/or underscore characters.
The first test passes since test consists of only letters. The second test passes because it contains one of each of the allowed character types (letter, number, hyphen, underscore) and no other characters. The third and fourth tests fail because they both contain one or more forbidden characters (space, asterisk, ampersand).
Wait, let’s back up. Didn’t the requirement for the name
attribute say that only lowercase letters were allowed? Yep, you got me. I kinda sort-of lied about test_1-AZ being a valid widget_name
. But there is a reason why I did this, which will be revealed by the configuration of the argument object for the name
attribute:
|
|
Line 49: All we need to do to use our custom type is set the value of the
type
parameter to thewidget_name
function.Lines 50-52: The
location
,required
andnullable
parameters should be familiar since we explained their purpose in Part 3.Line 53: This is the first time we are using the
case_sensitive
parameter. According to the documentation, by default,case_sensitive=True
. If this value isFalse
"this will convert all values to lowercase".
Configuring the name
argument with type=widget_name
and case_sensitive=False
allows the client to provide a value for the widget name in any combination of upper and lowercase letters. When an HTTP request is received to create a widget, if the request contains a name
attribute, the value of that attribute will be passed to the widget_name
function.
If the widget name contains only valid characters, it will be converted to lowercase before being passed to the business logic. The business logic will query the database for widgets with the same name. If a widget is found with the same name, the request will be aborted and the widget will not be created. If no widget is found with the same name, a new widget object will be created and added to the database.
info_url
Argument
|
|
The info_url
attribute can be parsed using the pre-defined URL
type from the inputs
module, which can be configured in numerous ways. For example, it is possible to restrict the allowed values to a whitelist of domains and/or exclude a blacklist of domains. You can choose to perform a DNS lookup on the domain specified by the client and reject the value if the domain fails to resolve. Check out the documentation for the URL
type for even more ways you can control the allowed URL range/format.
The only restriction on info_url
that we will employ is that the URL scheme must be either http
or https
. This can be more useful than you may think. Imagine if the Widget
class had git_url
and ssh_url
attributes in addition to info_url
. By simply changing the value of the schemes
parameter from schemes=['http', 'https']
to schemes=['git']
and schemes=['ssh']
, we could easily and effectively prevent clients from inserting values into the database that would cause errors if an application blindly tried to use those URLs to access a git repository or login to a server.
There really isn’t anything else to say about how the info_url
attribute is parsed since we already covered using a pre-defined input
type when we used the email
type as part of the auth_reqparser
in Part 3. So let’s move on to something a bit more interesting.
deadline
Argument
deadline
is the last piece of data that we must receive from the client in order to create a new widget. Utlimately, this value must be converted to a datetime
since the Widget
class has several hybrid_properties
that perform comparisons or calculations that assume this is the case. The inputs
module provides several pre-defined types that can convert request data to either a date
or a datetime
value. I’ve summarized the requirements for these types in Table 2 below:
Table 2 | |||
Pre-defined Input Types for date and datetime Values | |||
Pre-defined Type 1 | Required Format 2 | Example | Reference |
---|---|---|---|
date | YYYY-MM-DD | 2019-10-02 | N/A |
date_from_iso8601 | YYYY-MM-DD | 2019-10-02 | ISO 8601 |
datetime_from_iso8601 | YYYY-MM-DDThh:mm:ss(+/-)zh:zm | 2019-10-02T15:05:06-07:00 | ISO 8601 |
datetime_from_rfc822 | DN, DD MN YYYY hh:mm:ss (+/-)zhzm | Wed, 02 Oct 2019 15:05:06 -0700 | RFC 5322 3 |
1 Pre-defined types are located in the flask_restx.inputs module. | |||
2
| |||
3 RFC 5322 is a revision of RFC 2822, which itself obsoleted RFC 822 |
It would absolutely be possible to satisfy the project requirements using the pre-defined types in Table 2, but (IMO) they all have one big limitation — there is only a single valid format for the value provided by the client. Dates are expressed in so many different ways via text, so my preference is to design a parser that can accommodate a variety of date formats.
There is also a problem that arises from the project requirements for the deadline
attribute: the value must not be a date in the past. Since the pre-defined types only validate that the value provided by the client is in the proper format to be converted to a datetime
value, we would have to perform the task of checking if the date provided by the client is in the past in the business logic.
This second issue is another matter of opinion, since you could easily object by pointing out that the name
attribute has a requirement to be unique and the task of querying the database for widgets with the same name is not performed in the wiget_name
function we just created.
I’ll explain the distinction between these two requirements with a hypothetical situation: A request is received to create a new widget with name="test"
. This is a valid widget name, so the request is passed to the business logic and since no widget already exists in the database with name="test"
, a new widget is created. Then, an identical request is received to create a widget with name="test"
. IMO, from the point-of-view of the create_widget_reqparser
, test
is a valid widget name, so the request is again passed to the business logic. However, since a widget already exists with name="test"
, the request is aborted with a message indicating that a widget already exists in the database with name="test"
.
OTOH, consider this scenario: A request is received to create a new widget with deadline="1923-03-28"
. This string is in a valid format but since the date is obviously in the past, the create_widget_reqparser
raises a ValueError
and the request is aborted. A deadline
in the past is always invalid and a request to create a widget containing an invalid value for a required parameter must always be rejected. Hopefully my reasoning makes sense to you.
dateutil.parser
Since the pre-defined types are adequate but inflexible, what can we use to parse a date
value from a string? If we wanted to restrict ourselves to the Python standard library, we would need to create a complicated function that uses the strptime
method of either the date
or datetime
class. This would involve testing as many format strings as possible and would quickly become a nightmare.
Luckily, I see no reason to impose such a restriction for this project. IMO, the most robust and usable way to parse datetime
values from a string is the parse
function in the dateutil.parser
module (this was listed as a requirement in the setup.py
file, so it is already installed). Unlike strptime
, this method does not require you to provide a format string.
dateutil.parser.parse
recognizes a wide variety of date
and datetime
formats. The locale setting of the machine where the function is executed is taken into consideration, which is extremely helpful in situations where the order of the month and day values are transposed (e.g., US date format vs. European). You can find more information in the official documentation for the dateutil.parser
module.
future_date_from_string
Since deadline
must be either the current date or a date in the future, I decided to name the custom type function for this attribute future_date_from_string
. This function is more complex than widget_name
since two separate validations must be peformed — parsing the input string to a date
value and checking that the parsed date is not in the past:
|
|
There are a few things about the future_date_from_string
function that are worth pointing out:
Line 25: The value provided by the user is passed to the
dateutil.parser.parse
method. If theparser
is able to convert the string value to adatetime
value it is stored in theparsed_date
variable.Lines 26-32: If the
dateutil.parser.parse
method is unable to parse the value provided by the user, it raises aValueError
. Since the error message that is generated isn't very descriptive, the original error is suppressed and another value error is raised with an error message that clearly explains why the value provided by the user was rejected and provides examples of string values that would be considered valid.Line 34: Per the project requirements, the date value that was provided by the user is compared to the current date, without considering the time component of either date when making the comparison. If the date provided by the user is less than (i.e., before) the current date, the value is rejected since this means the deadline has passed.
Lines 35-40: If the deadline has passed, raise a ValueError explaining why the value is invalid.
Line 41: Per the project requirements, the deadline value flips from "not passed" to "passed" at the stroke of midnight. To achieve this, we set the deadline by taking the date component of the value provided by the user and combining it with
time.max()
, which is a shortcut for a time value equal to11:59:59 PM
.Lines 42-43: Since we need to ensure that any
datetime
value is timezone-aware before we add it to the database, we localize the value to UTC before it is returned and passed to the business logic.
We already know how to use a custom type, so the last thing we need to do is add an argument to our create_widget_reqparser
with type=future_date_from_string
;
|
|
Remember, we are following the process defined earlier to create each endpoint. We can now move on to step #2: Define the business logic necessary to process the request if validation succeeds.
create_widget
Method
Next, we need to define a function that performs the following actions:
- Add a new widget to the database
- Associate the widget with the user who created it
- Construct an HTTP response including a “Location” field in the header equal to the URI of the widget
RFC7231 defines the semantics of HTTP/1.1 messages. Specifically, it specifies when the “Location” field should be included in the header of an HTTP response:
7.1.2. LocationThe "Location" header field is used in some responses to refer to a specific resource in relation to the response. The type of relationship is defined by the combination of request method and status code semantics.
Location = URI-reference
...
For 201 (Created) responses, the Location value refers to the primary resource created by the request.
Create a new file named business.py
in src/flask_api_tutorial/api/widgets
and enter the content below:
|
|
Let’s take a look at how the create_widget
function performs the tasks listed above:
Line 13: Per the specification in Table 1, the ability to create a widget object is limited to users with the administrator role. To enforce this, we decorate the
create_widget
method with@admin_token_required
. If you would like to review how this decorator is implemented, click here.Line 14: After the request data has been parsed and validated, it is passed to the
create_widget
function as adict
object namedwidget_dict
.Lines 15-18: The function starts by checking if a
widget
already exists with the same name provided in the request. If this is true, the request is aborted. The first argument to theabort
function is the HTTP status code to include in the response. In this case, the appropriate response code is 409HTTPStatus.CONFLICT
.Line 19:
**
is the dictionary unpacking operator, you can find more info on it and the related list unpacking operator (*
) in PEP 448. It is a concise way to pass thename
,info_url
, anddeadline
values to theWidget
constructor.Lines 20-21: Thanks to the
@admin_token_required
decorator, thepublic_id
of the user who sent the request is stored increate_widget.public_id
(If you don't remember why this is is the case, review the last section where we broke down how the decorators are designed).We retrieve the
User
object that corresponds to thepublic_id
of the user that sent the request and assign it toowner
. Then,owner.id
is set as the value ofwidget.owner_id
.Lines 22-23: The new
widget
is added to the database and the changes are committed..Line 24: As we have seen previously, if we need to include a custom field in the header of a response, we must create the response object manually. One way to do this is with the
flask.jsonify
function.Line 25: Since the request to create a new widget was successful, the correct HTTP status code for the response is 201
HTTPStatus.CREATED
.Line 27: After ensuring that the response is configured with all required header fields, it is sent to the client.
Next, we need to create the API endpoint for the create operation and incorporate it with the create_widget_reqparser
and the create_widget
function.
api.widget_list
Endpoint (POST Request)
According to Table 1, the operation to create a widget
is accessed by sending a POST
request to the api.widget_list
resource, located at /api/v1/widgets
. To create this endpoint, create a new file endpoints.py
in src/flask_api_tutorial/api/widgets
and enter the content below:
|
|
There’s nothing in the code above that we haven’t already encountered and explained while implementing the auth_ns
API endpoints. Click here if you need a refresher on Namespace
objects, Resource
objects, @doc
, @response
, or @expect
decorators, etc.
The important part is Lines 26-27 where the parse_args
method of create_widget_reqparser
is used to validate the request data, which is then passed to the create_widget
function which we just defined.
Add widget_ns
Namespace to api
In order to register the widget_ns
namespace with the api
object, open src/flask_api_tutorial/api/__init__.py
and add the highlighted lines (Line 6 and Line 27):
|
|
We can verify that the endpoint was created by running flask routes
:
flask-api-tutorial $ flask routes
Endpoint Methods Rule
------------------- ------- --------------------------
api.auth_login POST /api/v1/auth/login
api.auth_logout POST /api/v1/auth/logout
api.auth_register POST /api/v1/auth/register
api.auth_user GET /api/v1/auth/user
api.doc GET /api/v1/ui
api.root GET /api/v1/
api.specs GET /api/v1/swagger.json
api.widget_list POST /api/v1/widgets
restplus_doc.static GET /swaggerui/<path:filename>
static GET /static/<path:filename>
As expected, a new endpoint has been created named api.widget_list
that responds to requests sent to /api/v1/widgets
. Currently, this endpoint only supports requests where the method type is POST
.
Normally, we would create unit tests to verify the endpoint does create widget
objects correctly. However, it was noted while explaining the design of the create_widget
function that currently an unhandled exception occurs when running this function since the business logic for creating a new Widget
depends on the api.widget
endpoint existing. We will create unit tests for both endpoints in the widget_ns
namespace when both have been fully implemented.
Checkpoint
Even though we only implemented one of the five CRUD operations specified in Table 1, we actually satisfied several of the remaining requirements. However, since we have not created any test coverage and confirmed that the process of creating a widget
object is working correctly, these will be marked as only half-complete at this point:
User Management/JWT Authentication
New users can register by providing an email address and password
Existing users can obtain a JWT by providing their email address and password
JWT contains the following claims: time the token was issued, time the token expires, a value that identifies the user, and a flag that indicates if the user has administrator access
JWT is sent in access_token field of HTTP response after successful authentication with email/password
JWTs must expire after 1 hour (in production)
JWT is sent by client in Authorization field of request header
Requests must be rejected if JWT has been modified
Requests must be rejected if JWT is expired
If user logs out, their JWT is immediately invalid/expired
If JWT is expired, user must re-authenticate with email/password to obtain a new JWT
API Resource: Widget List
All users can retrieve a list of all widgets
All users can retrieve individual widgets by name
Users with administrator access can add new widgets to the database
Users with administrator access can edit existing widgets
Users with administrator access can delete widgets from the database
The widget model contains attributes with URL, datetime, timedelta and bool data types, along with normal text fields.
URL and datetime values must be validated before a new widget is added to the database (and when an existing widget is updated).
The widget model contains a "name" attribute which must be a string value containing only lowercase-letters, numbers and the "-" (hyphen character) or "_" (underscore character).
The widget model contains a "deadline" attribute which must be a datetime value where the date component is equal to or greater than the current date. The comparison does not consider the value of the time component when this comparison is performed.
Widget name must be validated before a new widget is added to the database (and when an existing widget is updated).
If input validation fails either when adding a new widget or editing an existing widget, the API response must include error messages indicating the name(s) of the fields that failed validation.
In the next section, we will finish implementing the Widget
API. If you have any questions/feedback, please leave a comment!