Free Online Courses for Software Developers - MrBool
× Please, log in to give us a feedback. Click here to login
×

You must be logged to download. Click here to login

×

MrBool is totally free and you can help us to help the Developers Community around the world

Yes, I'd like to help the MrBool and the Developers Community before download

No, I'd like to download without make the donation

×

MrBool is totally free and you can help us to help the Developers Community around the world

Yes, I'd like to help the MrBool and the Developers Community before download

No, I'd like to download without make the donation

Using Composible Architecture with a Rest API

In this article, we are going to see how to create composible architectures focused in Web Services world.

Flash-Restful is a significant beautiful library full of resources for creating frameworks Rest with Flask. It has everything you need to manage the routing of class-based features, a library to mobilize objects to JSON format, and reqparse, a library inspired by argparse to write input parsers.

An introduction to Reqparse

Reqparse has a very simple UI, like we can see in Listing 1.

Listing 1. Reqparse UI code.

from flask.ext.restful import reqparse
 
parser = reqparse.RequestParser()
 
parser.add_argument(
  "name",
  type=str,
  required=True
)
 
# Within a flask request context
args = parser.parse_args()
args.get('name')  # => The "name" parameter from the request

It represents the parser and then adds arguments to it. Each argument is actually an instance of a class Argument, which receives the arguments passed to add_argument. In general, Listing 2 shows a pseudocode explaining how reqparse works.

Listing 2. How reqparse works.

class Argument(object):
    def __init__(self, name, **kwargs):
        self.name = name
        self.type = kwargs.pop('type')
 
    def parse(self, data):
        val = data.get(self.name)
        try:
            return self.type(val)
        except Exception as e:
            return ValueError(e.message)
 
class RequestParser(object):
    def __init__(self):
        self.args = []
 
    def add_argument(self, name, **kwargs):
        self.args.append(Argument(name, **kwargs))
 
    def parse_args(self):
        results = {}
        for arg in self.args:
            result = arg.parse(flask.request.json)
            if isinstance(result, ValueError):
                abort(400, result.message)
            results[arg.name] = results
        return results

This is all very beautiful, but if you have ever written any kind of validation code before, you must have noticed all kinds of cases that hinder what was shown above. We will focus on the keyword argument that is required.

A tale of two philosophies

The above code is a pseudo-code, but the crux of the matter here is: what is the best way to implement what has been shown above?

To tell the story out of order, the reqparse approach (as we learned when we tried to update it) involves adding more kwargs: the Argument and RequestParser, and update their implementations with special cases for this argument. As a result, this is how the parse method Argument looks like now: Listing 3.

Listing 3. parse Argument.

def parse(self, request, bundle_errors=False):
    """Parses argument value(s) from the request, converting according to
    the argument's type.
    :param request: The flask request object to parse arguments from
    :param do not abort when first error occurs, return a
        dict with the name of the argument and the error message to be
        bundled
    """
    source = self.source(request)
 
    results = []
 
    # Sentinels
    _not_found = False
    _found = True
 
    for operator in self.operators:
        name = self.name + operator.replace("=", "", 1)
        if name in source:
            # Account for MultiDict and regular dict
            if hasattr(source, "getlist"):
                values = source.getlist(name)
            else:
                values = [source.get(name)]
 
            for value in values:
                if hasattr(value, "strip") and self.trim:
                    value = value.strip()
                if hasattr(value, "lower") and not self.case_sensitive:
                    value = value.lower()
 
                    if hasattr(self.choices, "__iter__"):
                        self.choices = [choice.lower()
                                        for choice in self.choices]
                try:
                    value = self.convert(value, operator)
                except Exception as error:
                    if self.ignore:
                        continue
                    return self.handle_validation_error(error, bundle_errors)
                if self.choices and value not in self.choices:
                    if current_app.config.get("BUNDLE_ERRORS", False) or bundle_errors:
                        return self.handle_validation_error(
                            ValueError(u"{0} is not a valid choice".format(
                                value)), bundle_errors)
                    self.handle_validation_error(
                            ValueError(u"{0} is not a valid choice".format(
                                value)), bundle_errors)
 
                if name in request.unparsed_arguments:
                    request.unparsed_arguments.pop(name)
                results.append(value)
 
    if not results and self.required:
        if isinstance(self.location, six.string_types):
            error_msg = u"Missing required parameter in {0}".format(
                _friendly_location.get(self.location, self.location)
            )
        else:
            friendly_locations = [_friendly_location.get(loc, loc)
                                  for loc in self.location]
            error_msg = u"Missing required parameter in {0}".format(
                ' or '.join(friendly_locations)
            )
        if current_app.config.get("BUNDLE_ERRORS", False) or bundle_errors:
            return self.handle_validation_error(ValueError(error_msg), bundle_errors)
        self.handle_validation_error(ValueError(error_msg), bundle_errors)
 
    if not results:
        if callable(self.default):
            return self.default(), _not_found
        else:
            return self.default, _not_found
 
    if self.action == 'append':
        return results, _found
 
    if self.action == 'store' or len(results) == 1:
        return results[0], _found
    return results, _found

We will pause here to point out that this code works perfectly well, and Flask-Restful has been a great library to use. But look for some branch of logic required for special cases. For example, this branch leaves the lowercase value and then have to verify the choice argument (used to specify a set of valid values that the entry could take).

Listing 4. Verifying the choice argument.

if hasattr(value, "lower") and not self.case_sensitive:
        value = value.lower()
 
        if hasattr(self.choices, "__iter__"):
            self.choices = [choice.lower()
                            for choice in self.choices]

This smells like a bug born of an extremely obscure case and, in fact, we can find the commit where this branch was added.

Going back in time before all this: as our application grew more and more about the requirements of its data, a reusable library "validators" was born. It simply provides functions that can be passed to the type parameter of the add_argument reqparse. It started simply enough, with validators like email (Listing 5).

Listing 5. Simple validators.

import re
EMAIL_RE = re.compile(r".+@.+\..+")
 
def email(s):
    if not EMAIL_RE.match(s):
        raise ValidationError("Invalid email format")
    return s

Eventually, however, it gains some additional tracks. Here is the validator choice that was added at some point, probably when we did not have patience to find a choice of built-in reqparse option.

Listing 6. Additional tracks.

def choice (choices, msg = 'Invalid Choice'):
     def choose (choice):
         if not choice in choices:
             raise ValidationError (msg = msg)
         if isinstance (choices, dict):
             return choices.get (choice)
         else:
             return choice
     return choose
 
# Usage
parser.add_argument (
   'Category'
   type = validators.choice ([ 'News', 'Entertainment', 'Other'])
)

We see how the responsibility for this validation is now transferred to the type function, ensuring that no special treatment of choice code is kept away from the commonly used analysis functions? Unintentionally, the validators library added a lot of functionality built the reqparse in parallel. In parallel to the keywords reqparse, validators earned optional, required, nullable and validated_list.

Recently, I had to update Flask-Restful for some reason, and it was terrifying when some validators stopped working. This was not really anyone's fault, just a case where my validation functions expected some edge-case behavior, that had changed. But I figured it out, and now do not need all the features that reqparse offers. I changed the RequestParser and Argument classes for some custom that look like much from the example of pseudo-code above that with current implementations that exist within the flash-restful. There is a process in which everything is done at once, but over time, and all the keywords-based validators are moving based on functions, and Argument and RequestParser implementations continue getting more and more simple.

Incidentally, this is the kind of system that WTForms, another great imput-wrangling library, always used. Each Field class accepts a list of validators, which can be fully written in a personalized way, as my above (you can even make them closures returning functions). The WTForms provides that, instead of extra arguments is a number of validators covering common cases, as DataRequired, EqualTo, ... :

class MyForm(Form):
  email = StringField("email", validators=[DataRequired(), Email()])

But not to be outdone in the field of keyword austerity, validators has long provided a comp method for this type of situation:

Listing 7. Function add_argument

parser.add_argument(
  'email',
  type=validators.comp(validators.required, validators.email)
)

So what's the lesson? There is something along the lines: whenever possible, choose the function of the composition to those cases where it really makes things simpler.



Web developer and passioned for web design, SEO and front end technologies.

What did you think of this post?
Services
[Close]
To have full access to this post (or download the associated files) you must have MrBool Credits.

  See the prices for this post in Mr.Bool Credits System below:

Individually – in this case the price for this post is US$ 0,00 (Buy it now)
in this case you will buy only this video by paying the full price with no discount.

Package of 10 credits - in this case the price for this post is US$ 0,00
This subscription is ideal if you want to download few videos. In this plan you will receive a discount of 50% in each video. Subscribe for this package!

Package of 50 credits – in this case the price for this post is US$ 0,00
This subscription is ideal if you want to download several videos. In this plan you will receive a discount of 83% in each video. Subscribe for this package!


> More info about MrBool Credits
[Close]
You must be logged to download.

Click here to login