# -*- coding: utf-8 -*-
from __future__ import print_function, unicode_literals
from eight import *
from .utils import python_2_unicode_compatible
import collections
import operator
import itertools
[docs]
operators = {
"<": operator.lt,
"<=": operator.le,
"==": operator.eq,
"is": operator.eq, # Note: not pure python `is`!
"iis": lambda x, y: x.lower() == y.lower(),
"!=": operator.ne,
"<>": operator.ne,
"not": operator.ne,
"inot": lambda x, y: x.lower() != y.lower(),
">=": operator.ge,
">": operator.gt,
"has": operator.contains,
"ihas": lambda x, y: y.lower() in x.lower(),
"nothas": lambda x, y: not operator.contains(x, y),
"in": lambda x, y: operator.contains(y, x),
"notin": lambda x, y: not operator.contains(y, x),
# No iin because in normally doesn't take string inputs
"len": lambda x, y: len(x) == y,
}
[docs]
def try_op(f, x, y):
try:
return f(x, y)
except:
return False
[docs]
class Dictionaries(object):
"""Pretends to be a single dictionary when applying a ``Query`` to multiple databases.
Usage:
first_database = Database(...).load()
second_database = Database(...).load()
my_joined_dataset = Dictionaries(first_database, second_database)
search_results = Query(filter_1, filter_2)(my_joined_dataset)
"""
def __init__(self, *args):
[docs]
def items(self):
return itertools.chain(*[x.items() for x in self.dicts])
@python_2_unicode_compatible
[docs]
class Result(object):
"""A container that wraps a filtered dataset. Returned by a calling a ``Query`` object. A result object functions like a read-only dictionary; you can call ``Result[some_key]``, or ``some_key in Result``, or ``len(Result)``.
The dataset can also be sorted, using ``sort(field)``; the underlying data is then a ``collections.OrderedDict``.
Args:
* *result* (dict): The filtered dataset.
"""
def __init__(self, result):
if not isinstance(result, dict):
raise ValueError(u"Must pass dictionary")
def __str__(self):
return "Query result with %i entries" % len(self.result)
def __repr__(self):
if not self.result:
return u"Query result:\n\tNo query results found."
data = list(self.result.items())[:20]
return (u"Query result: (total %i)\n" % len(self.result) + \
u"\n".join([u"%s: %s" % (k, v.get("name", "Unknown"))
for k, v in data])
)
[docs]
def sort(self, field, reverse=False):
"""Sort the filtered dataset. Operates in place; does not return anything.
Args:
* *field* (str): The key used for sorting.
* *reverse* (bool, optional): Reverse normal sorting order.
"""
self.result = collections.OrderedDict(sorted(self.result.items(),
key=lambda t: t[1].get(field, None), reverse=reverse))
# Generic dictionary methods
def __len__(self):
return len(self.result)
def __iter__(self):
return iter(self.result)
[docs]
def keys(self):
return self.result.keys()
[docs]
def items(self):
return self.result.items()
def items(self):
return self.result.items()
def __getitem__(self, key):
return self.result[key]
def __contains__(self, key):
return key in self.result
[docs]
class Query(object):
"""A container for a set of filters applied to a dataset.
Filters are applied by calling the ``Query`` object, and passing the dataset to filter as the argument. Calling a ``Query`` with some data returns a ``Result`` object with the filtered dataset.
Args:
* *filters* (filters): One or more ``Filter`` objects.
"""
def __init__(self, *filters):
[docs]
self.filters = list(filters)
[docs]
def add(self, filter_):
"""Add another filter.
Args:
*filter_* (``Filter``): A Filter object.
"""
self.filters.append(filter_)
def __call__(self, data):
for filter_ in self.filters:
data = filter_(data)
return Result(data)
[docs]
class Filter(object):
"""A filter on a dataset.
The following functions are supported:
* "<", "<=", "==", ">", ">=": Mathematical relations
* "is", "not": Identity relations. Work on any Python object.
* "in", "notin": List or string relations.
* "iin", "iis", "inot": Case-insensitive string relations.
* "len": Length relation.
In addition, any function which defines a relationship between an input and an output can also be used.
Examples:
* All ``name`` values are *"foo"*: ``Filter("name", "is", "foo")``
* All ``name`` values include the string *"foo"*: ``Filter("name", "has", "foo")``
* Category (a list of categories and subcategories) includes *"foo"*: ``Filter("category", "has", "foo")``
Args:
* *key* (str): The field to filter on.
* *function* (str or object): One of the pre-defined filters, or a callable object.
* *value* (object): The value to test against.
Returns:
A ``Result`` object which wraps a new data dictionary.
"""
def __init__(self, key, function, value):
[docs]
self.function = function
if not callable(function):
self.function = operators.get(function, None)
if not self.function:
raise ValueError("No valid function found")
def __call__(self, data):
return dict(((k, v) for k, v in data.items() if try_op(
self.function, v.get(self.key, None), self.value)))
[docs]
def NF(value):
"""Shortcut for a name filter"""
return Filter(u"name", u"has", value)
[docs]
def PF(value):
"""Shortcut for a reference product filter"""
return Filter(u"reference product", u"has", value)