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
1 Answer 1
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()
-
\$\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– janos2016年01月27日 22:11:56 +00:00Commented Jan 27, 2016 at 22:11