I would like my API to offer basic query features, like not only get item by ID but also using operators on item attributes:
GET /resource/?name=chuck&surname__contains=orris&age__lt=69
Proof of concept
Here is what I've done so far. I did not modify webargs code. I'm only subclassing in my own application.
I've been searching around and didn't find a universally accepted norm specifying such a query language. In my implementation, I'm using the double underscore syntax and a subset of operators from MongoEngine.
Basically, I have two families of operators, some operating on numbers, others on string.
NUMBER_OPERATORS = ('ne', 'gt', 'gte', 'lt', 'lte')
STRING_OPERATORS = (
'contains', 'icontains', 'startswith', 'istartswith',
'endswith', 'iendswith', 'iexact'
)
QUERY_OPERATORS = {
'number': NUMBER_OPERATORS,
'string': STRING_OPERATORS,
}
Those lists could probably be extended. Operators meanings should be easy to grap. Examples:
age__lt=69
means I expect the API to return records with attribute age lower than 69
surname__contains=orris
means attribute age contains string "orris"
To let webargs parse such parameters, I need to modify the Schema
so that for chosen fields, the Marshmallow field is duplicated (deepcopied) into needed variants. For instance, the field age
is duplicated into age__lt
, age__gt
,...
This is an opt-in feature. For each field I want to expose this way, I need to specify in Meta
which operators category it should use (currently only two categories: number
and string
). Auto-detection is complicated as I don't know how I would handle custom fields, and there may be other categories some day.
In the Schema, I add:
class Meta:
fields_filters = {
'name': ('string',),
'surname': ('string',),
'age': ('number',),
}
For each field, I'm passing a list of categories as one could imagine several categories applying to a field, but currently, I have no example of field that would use both number and string operators.
And the "magic" takes place here:
class SchemaOpts(ma.SchemaOpts):
def __init__(self, meta):
super(SchemaOpts, self).__init__(meta)
# Add a new meta field to pass the list of filters
self.fields_filters = getattr(meta, 'fields_filters', None)
class SchemaMeta(ma.schema.SchemaMeta):
"""Metaclass for `ModelSchema`."""
@classmethod
def get_declared_fields(mcs, klass, *args, **kwargs):
# Create empty dict using provided dict_class
declared_fields = kwargs.get('dict_class', dict)()
# Add base fields
base_fields = super(SchemaMeta, mcs).get_declared_fields(
klass, *args, **kwargs
)
declared_fields.update(base_fields)
# Get allowed filters from Meta and create filters
opts = klass.opts
fields_filters = getattr(opts, 'fields_filters', None)
if fields_filters:
filter_fields = {}
for field_name, field_filters in fields_filters.items():
field = base_fields.get(field_name, None)
if field:
for filter_category in field_filters:
for operator in QUERY_OPERATORS.get(
filter_category, ()):
filter_fields[
'{}__{}'.format(field_name, operator)
] = deepcopy(field)
declared_fields.update(filter_fields)
return declared_fields
class QueryArgsSchema(ma.compat.with_metaclass(SchemaMeta, ma.Schema)):
OPTIONS_CLASS = SchemaOpts
And finally, I use the Schema to parse the query arguments:
@use_args(ObjectSchema)
def get(self, args):
...
Questions
This raises a few points.
- Is there some sort of convention I missed when searching for a query language?
- Is this out-of-scope for webargs or could it be a useful enhancement?
- Is there no need for that? I've been investigating both flask-retful and marshmallow/webargs/... ecosystems, along with @frol's invaluable flask-restplus-server-example and saw nothing close to this, so I'm thinking maybe people just don't do that. Or maybe they only expose a few filters, and they do it in specific routes.
- Should this be in @touilleMan's marshmallow-mongoengine? I do use this library, but although the query language is inspired^Wshamelessly copied from MongoEngine (which allows me to pass the query arguments straight into the QuerySet filters...), the whole thing has no dependency on MongoEngine and this should be a generic feature.
- My
QueryArgsSchema
also has sort and pagination fields, but I didn't expose them here as this needs nothing fancy on Marshmallow's side. Maybe a real "query parameters" feature would integrate these as well.
Feedback greatly appreciated. It seems to work right now, but on the long run, I might discover it was poorly designed from the start.
Thanks.