10
\$\begingroup\$

I'm working on a simple online dictionary tool, using Python Flask-RESTful as the RESTful API backend. The dictionary tool is modular, it can handle multiple dictionaries, implemented as independent plugins.

I have API endpoints like this:

/api/v1/dictionaries/:dict_id/find/exact/:keyword
/api/v1/dictionaries/:dict_id/find/prefix/:keyword
/api/v1/dictionaries/:dict_id/find/suffix/:keyword
/api/v1/dictionaries/:dict_id/find/partial/:keyword

To make this work in Flask, I have to associate these endpoints with classes that extend the Resource base class, and implement a .get method.

I have multiple dictionaries, discovered dynamically from plugins. As they implement the same interface, it would make sense to have a class that can be constructed with any dictionary that implements the common interface, and have the class delegate queries to its dictionary. For example I have a class named FindExact, and I planned to create one instance of it per dictionary to handle the endpoints:

/api/v1/dictionaries/dict1/find/exact/:keyword
/api/v1/dictionaries/dict2/find/exact/:keyword
/api/v1/dictionaries/dict3/find/exact/:keyword

However, Flask's api.add_resource method takes a class as parameter, not an object. To work around this, I'm creating classes dynamically:

#!/usr/bin/env python

from flask import Flask, render_template, jsonify
from flask.ext.restful import Resource, Api, reqparse

from dictionary.base import lazy_property
from util import discover_dictionaries

app = Flask(__name__)
api = Api(app)

parser = reqparse.RequestParser()
parser.add_argument('similar', type=bool, help='Try to find similar matches when there are no exact')
parser.add_argument('list', type=bool, help='Show list of matches instead of content')

dictionaries = [_ for _ in discover_dictionaries()]


class DictionaryResource(Resource):
    @lazy_property
    def args(self):
        return parser.parse_args()

    @property
    def dict_id(self):
        """Dynamically set when creating subclass using type()"""
        return None

    @property
    def dictionary(self):
        """Dynamically set when creating subclass using type()"""
        return None

    @staticmethod
    def get_serializable_entries(entries):
        return [x.content for x in entries]

    def get_json_entries(self, serializable_entries):
        return jsonify({
            'matches': [{
                'dict': self.dict_id,
                'format': 'dl-md',
                'entries': serializable_entries,
            }]
        })

    def get_entries(self, entries):
        return self.get_json_entries(self.get_serializable_entries(entries))

    def get_entries_without_content(self, entries):
        return self.get_json_entries([{'id': x.entry_id, 'name': x.name} for x in entries])

    def get_response(self, entries, list_only=False):
        if list_only:
            return self.get_entries_without_content(entries)
        return self.get_entries(entries)


class FindExact(DictionaryResource):
    def get(self, keyword):
        entries = self.dictionary.find(keyword, find_similar=self.args['similar'])
        return self.get_response(entries, list_only=self.args['list'])


def dictionary_app_gen(dict_id, dictionary):
    def dictionary_app():
        return render_template('dictionary.html', dict_id=dict_id, dictionary=dictionary)
    return dictionary_app

api_baseurl = '/api/v1/dictionaries'


def add_resource(cname, url_template, dict_id, dictionary):
    extra_props = {'dict_id': dict_id, 'dictionary': dictionary}
    subclass = type(dict_id + cname.__name__, (cname,), extra_props)
    api.add_resource(subclass, url_template.format(api_baseurl, dict_id))


def register_dictionary_endpoints():
    for dict_id, dictionary in dictionaries:
        add_resource(FindExact, '{0}/{1}/find/exact/<string:keyword>', dict_id, dictionary)

if __name__ == '__main__':
    register_dictionary_endpoints()
    app.run()

As you notice, in my custom add_resource helper, I dynamically create a new class for a dictionary using type. This works, but it's ugly. I'd like to do this better. Maybe my approach is wrong and I should have one Flask app for each dictionary? I'm not sure how to go about that without making deployment complicated.

I'd like to hear about other mistakes as well.

The full source code is on GitHub.

You can play with the API like this, for example:

curl webdict.janosgyerik.com/api/v1/dictionaries/wud/find/exact/chair
curl webdict.janosgyerik.com/api/v1/dictionaries/wud/find/exact/chair --get -d list=1
\$\endgroup\$

1 Answer 1

5
+50
\$\begingroup\$

You don't need dynamic type creation

I don't quite understand why you're creating new types dynamically. I think you just need to define a constructor for your FindExact or DictionaryResource classes, and then pass the keyword argument resource_class_args to add_resource:

resource_class_args (tuple) – args to be forwarded to the constructor of the resource.

Then you don't need a unique type for each dictionary, and you can get rid of the dynamic type creation. You could also use resource_class_kwargs if you would prefer that.

resource_class_kwargs (dict) – kwargs to be forwarded to the constructor of the resource.

Some other minor considerations:

I'm not sure why you're doing

dictionaries = [_ for _ in discover_dictionaries()]

instead of just

dictionaries = list(discover_dictionaries())

I'm even more confused by the temporary list because you only use it in the register_dictionary_endpoints function - can't you just change that to this?

for dict_id, dictionary in discover_dictionaries():
    add_resource(FindExact, '{0}/{1}/find/exact/<string:keyword>', dict_id, dictionary)

Otherwise, by making the intermediate list, it seems that you're defeating the point of discover_dictionaries being a generator.

Also, instead of putting all of this in a single module I think I'd like it more if you used init_app and put all of your API code into another module.

from flask import Flask
from my_api import api

app = Flask(__name__)
api.init_app(app)


if __name__ == '__main__':
    app.run()

If you really didn't want to call register_dictionary_endpoints except in the case of __main__, then I think you could alter the function signature of that and add_resource to take an Api object as their first parameter, and then call it as

if __name__ == '__main__':
    register_dictionary_endpoints(api)
    app.run()
\$\endgroup\$
1
  • \$\begingroup\$ This is excellent, thanks a lot! I started a bounty as an extra reward. At the time I wrote this, the resource_class_args parameter did not exist in Flask REST. I upgraded now and it's great. \$\endgroup\$
    – janos
    Jan 27, 2016 at 22:11

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.