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