flask-dino-utils - A toolbox to make API development easier.
So, you are already a trained python developer and you love building projects using Flask, but there are some processes that you already do like a robot, like building the logic to paginate your api. How cool would it be if those things could be already wrapped up as possible? Well, search no more. Flask-Dino-Utils is here to help you build robust flask apis in no time.
Assumptions
In order to use this library we make some assumptions regarding your project.
- You're using the following frameworks:
flask-classy
,flask-sqlalchemy
andmarshmallow
. - Each of your objects has an autoincrement id. (With any name).
- Your class attributes are exactly the same as your table columns.
Our Big Baby: The FlaskImprovedView
So, you love Flask-Classy and basically you use FlaskView
as your superclass in any view you create. Well, say no more. You're not going to use that class anymore. Now, you will start using FlaskImprovedView which adds the following functionality:
- In
GET /
: Adds pagination, sorting, and filtering your objects. For pagination you can useper_page
andpage
parameters. For sorting you can usesort_field
andsort_dir
(asc and desc) parameters. And for filtering we have a custom and easy-to-use syntax to add any filter you need. See Flask-Dino Filter syntax for more information. - In
GET /<id>
: Does the logic needed to return the object. - In
POST /
: Creates an object validating the json input using thebody_validation
attribute. It creates and returns the object. - In
PUT /<id>
: Modifies an existing object giving the id sent by parameter. - In
DELETE /<id>
: Delete (or alter an active field) an object.
FlaskImprovedView attributes.
Here's an very quick example of an implementation of a child view:
class UserPositionView(FlaskImprovedView):
route_base = "/user_positions" # Endpoint
id_name = "id_user_position" # The id field name
view_schema = UserPositionSchema() # The marshmallow schema object
view_model = UserPosition # The SQLAlchemy Model class
db_engine = db # The database engine to make the operations
active_field = "active" # The active field if you do not want to delete any database registry but to alter a boolean value.
body_validation = { # A set of definitions in the params validation process.
"user_position_description": {
"required": True,
"validation_tuple": [(TYPE_VALIDATOR, unicode)]
},
"company": {
"derivated": True,
"object_type": Company,
"id_name": "id_company",
"create_behavior": ASSOCIATE_EXISTING_OBJECT,
"many": False,
"fields": {
"id_company": {
"required": True,
"validation_tuple": [(TYPE_VALIDATION, int)]
}
}
}
}
-
route_base
: TheFlaskView
route attribute. Nothing new. -
id_name
: The name of the id attribute in order to perform some logic in POST and PUT commands. -
view_schema
: The marshmallow serializer object instantiated. -
view_model
: The SQLAlchemy class to perform queries over orm model. -
body_validation
: The parameters validation in payload body in an object creation/modification. (See Payload validation for more info). -
db_engine
: The database engine to operate. Usually it's a SqlAlchemy initiated engine. -
active_field
: The name of the active attribute if you do not want to delete any table registry but to alter a boolean value.
The derivated attributes
As any object-oriented programming world, we will have relationships between objects that will end up in json (or list) inside our body.
This object, obviously, requires a validation also. You may have a single-object or a list of objects that needs to be created or associated among with the main object.
The parameters for the body_validation
attribute for derivated objects are:
-
derivated
:True
specify that the attribute is a derivated one. Otherwise will not be taken by the validation algorithms. -
object_type
: Same as the view, we need to know the object type of your derivated param. -
id_name
: We need to know which of the fields is the id attribute. -
create_behavior
: This is an important one. You may define:-
ASSOCIATE_EXISTING_OBJECT
: In this case, whether is aPOST
or aPUT
method, the method will look for the id of the object sent and will be associating it to our main object. It will not create a new one. - `CREATE_NEW_OBJECT: In this case, a new object (or objects) will be created among with our object creation.
-
-
many
: If it isTrue
the view will expect a list of derivated objects. If it isFalse
will expect only one. -
fields
: In this case, we will be defining the validation fields inside this key.
Individual decorators for custom endpoints.
Okay, so you're saying: "Awesome! But my api is much more complex than 5 CRUD methods. I need more power man.". No problem! We added decorators for each individual operation so you can add it to any endpoint and customize your logic as well. Remember, we only want you to take care of your code and not repetitive and boring stuff.
Pagination
In order to paginate your response, you shall do two things:
- (Optional) Add the
@paginable()
decorator to your response. This will validate the pagination parameters. - Use the
paginated_response(args, data, schema_class)
to paginate your SqlAlchemy query and get the result. Theargs
param should be yourrequest.args
object, thedata
is the in progress sqlalchemy query with the data and theschema_class
is your marshmallow schema definition to include in the items response.
Important: This second step will return the marshmallow serialized object ready for the response. You can not add more filters, sorting, etc. after this. You shall call paginated_response
at the end of your logic.
Sorting
` To sort your response, you shall do two things:
- (Optional) Add the
@sortable(ObjectType)
decorator to your response method. This will validate the sorting parameters. These aresort_dir
(asc or desc) and thesort_field
which is the attribute/column to perform the sorting. - Use the
sort(args, data)
method to sort your response. Theargs
param should be yourrequest.args
object, thedata
is the in progress sqlalchemy query with the data.
Payload validation
Now things become interesting. You can validate any param in any restful method with different conditions to forget about doing individual validation on each one. This becomes very useful if you loose lot of time validating each attribute and data when you develop your webservices. In our library we use the concept of validation tuples which consist in the validation logic and the validation data. The available validation logic and data consists in:
Validator logic | Description | Data |
---|---|---|
TYPE_VALIDATOR | Validates the type of the input param | The object type to expect. (E.g. str, int, float) |
MIN_VALIDATOR | If numeric param, validates the minimum value possible | The minimun number to expect (included). |
MAX_VALIDATOR | If numeric param, validates the maximum value possible | The maximum number to expect (included). |
REGEX_VALIDATOR | Validates that the param against a regular expression | The regular expression to check. |
VALID_VALUES_VALIDATOR | Validates against choices to use in your parameters | A list of possible values |
NUMERIC_STRING_VALIDATOR | Sometimes, you cannot send a numeric value (e.g. in a query arg). In this case you should check if the parameter is numeric-convertible. | No data is needed here. |
Using this logic you can validate your parameters using the body_validation
argument for POST and PUT payloads or using the custom decorator in an api method.
The body_validation argument
This argument defines the validations in the payload of POST and PUT methods. The syntax is the following:
-
key
: the data to validate. -
"required"
: if the parameter is required or not. Default False. -
"validation_tuple"
: The list of validation tuples to check your parameters.
An example of this:
body_validation = {
"name": {
"required": True,
"validation_tuple": [(TYPE_VALIDATOR, unicode)]
},
"age": {
"required": False,
"validation_tuple": [(TYPE_VALIDATOR, int),(MIN_VALIDATOR, 0),(MAX_VALIDATOR, 150)]
}
}
This can be translated in individual decorators using them in any method. For example:
@validate_param(REQUEST_BODY_PARAMS, "name", [(TYPE_VALIDATOR, unicode)], True)
@validate_param(REQUEST_BODY_PARAMS, "age", [(TYPE_VALIDATOR, int),(MIN_VALIDATOR, 0),(MAX_VALIDATOR, 150)], False)
def post(self):
...
In this case the REQUEST_BODY_PARAMS
defines whether to look for the param. You can use REQUEST_QUERY_PARAMS
to check against query parameters.
Filtering
Wow! If you read the docs and get here you're surely very impressed on how amazing restful services development can be funny without concerning on boring stuff. Well, you're right; but we have more secret weapons. In this case we're introducing the filtering option to your GET /
requests without making you code anything (Yeh! Anything! Grab a beer and come back to Candy Crush). Not, really. The filter option is embbeded in FlaskImprovedView
and includes every SqlAlchemy operator that you may imagine. We have a custom syntax for this, so please pay attention. Imagine that you want to filter people with name "Jack". So you developed an endpoint like this:
curl http://localhost:5000/api/v1/people
Well, if this request is using a FlaskImprovedView
we can add the filter as the following:
curl http://localhost:5000/api/v1/people?filter=name;eq;Jack
But imagine you need people named Jack which are men, and they are between 20s and 40s. Okap, this can be done using the following request.
curl http://localhost:5000/api/v1/people?filter=name;eq;Jack$age;gt;20$age;lt;40
You can deduce in the filter syntax that the filter separator is the special character $
and the filter logic is composed by: <filter_key>;<filter_operator>;<filter_value>
. Filter operators can be found here. Note: avoid using python underscores.
The Authenticator
Under our security implementation, we have a really simple way of authenticating and authorizing users. The class Authenticator
under authenticators
script has all the information needed to provide a basic authentcation method.
We will assume the following object structure:
User 1----1 Roles 1----n Permission
In this case we're assuming that each user has one role associated and each role has a set of permissions associated to it. Each permission has a field regarding an unique id.
The Authenticator
class has some predefined attributes we need to setup in orde to use it:
from flask_dino_utils.authenticators import Authenticator
from models import User
authenticator = Authenticator()
authenticator.USER_OBJECT = User
authenticator.USERNAME_ATTR = "user_username"
authenticator.PASSWORD_ATTR = "user_password"
authenticator.SUPER_ROLE_ATTR = "user_role.is_super_role"
authenticator.PERMISSIONS_ATTR = "user_role.permissions"
authenticator.KEY_PERMISSION_ATTR = "user_permission_arn"
You can see different attributes, let me explain them:
-
USER_OBJECT
:User # The object user defined on our model -
USERNAME_ATTR
: (Required) The username attribute name to check -
PASSWORD_ATTR
: (Required) The password attribute to check. Note: using sample comparation. The password must be encrypted from the beginning. -
SUPER_ROLE_ATTR
: (Optional) You can have a boolean value defining that the role is super_role without concerning about permissions -
PERMISSIONS_ATTR
: (Optional) if set, each authentication will compare against permissions -
KEY_PERMISSION_ATTR
: (Required if defined self.PEERMISSIONS_ATTR), permissions field to compare.
After this you can add the following line to your authenticated methods:
def index(self):
authenticator.basic_auth(authorization=request.authorization, permission="GetAllUsers")
return super(UserView, self).index()
The method basic_auth
receives the authorization data (required) and the permission uuid. If permission
is not set, will authenticate any user and move forward.
In this case you can see that, before doing anything, first it authenticates and authorizates the user.