~fkfd/closure-filter-poc

59a908271e3f96eeaecd7819c4ce5a31d6da4a87 — Frederick Yin 4 years ago
Initial PoC commit
3 files changed, 143 insertions(+), 0 deletions(-)

A .gitignore
A filter.py
A objects.py
A  => .gitignore +3 -0
@@ 1,3 @@
.vscode/
__pycache__


A  => filter.py +112 -0
@@ 1,112 @@
import sys
from datetime import datetime
from objects import Webcomic

def get_attribute(obj, attr: str):
    if type(obj) == dict:
        try:
            return obj[attr]
        except KeyError:
            return None
    else:
        try:
            return getattr(obj, attr)
        except AttributeError:
            return None


def filter_closure(filter_string: str) -> list:
    # Test each object given against provided criteria.
    # Criteria are separated with ampersands(&) TODO: OR(|),
    # each of which is of format 'attribute[.conversion]<operator>value'.
    # `operator` can be one of =, <, >, <=, >=, and !=.
    # [TODO: -> (is a superset of), and <- (is a subset of).]
    criteria = filter_string.split('&')
    def as_is(v): return v
    convertions = {
        # convertions to apply to left and right, respectively
        # applied with: convertions[i][0 or 1](value of either side)
        'str': (str, str),
        'str[]': (list, str),
        'int': (int, int),
        'int[]': (list, int),
        'len': (len, int),  # get number of elems in key
        # syntactic sugar; analogous to True if s == 'true' else False
        'bool': (bool, lambda s: s == 'true' and True or False)
    }
    operators = {
        '<=': lambda a, b: a <= b,
        '>=': lambda a, b: a >= b,
        '!=': lambda a, b: a != b,
        '=': lambda a, b: a == b,
        '!<-': lambda a, b: a not in b,
        '!->': lambda a, b: b not in a,
        '<-': lambda a, b: a in b,
        '->': lambda a, b: b in a,
        '<': lambda a, b: a < b,
        '>': lambda a, b: a > b,
    }

    for criterion in criteria:
        attr, op, value = '', '', ''
        for o in operators.keys():
            # This attempts to separate the criterion string into 3 parts.
            # If o is not present, op and value are set blank by
            # criterion.partition and goes into another loop.
            attr, op, value = criterion.partition(o)
            # print(attr, op, value)
            if op:
                break

        if not op in operators.keys():
            print('No operator found in expression. Skip.', file=sys.stderr)
            continue

        # split key further for type conversion
        attr, conv = attr.split('.', 1)
        # print(attr, conv, op, value)

        if not conv in convertions.keys():
            print(
                f'Type conversion "{conv}" for attribute "{attr}" is not understood. Skip.',
                file=sys.stderr)
            continue  # skip this criterion

        yield lambda obj: operators[op](
            convertions[conv][0](
                get_attribute(obj, attr)
            ),
            convertions[conv][1](value)
        )


if __name__ == '__main__':
    xkcd = Webcomic(
        'xkcd',
        'https://xkcd.com',
        ['Randall Munroe'],
        ['humor', 'romance', 'sarcasm', 'math', 'language', 'compsci'],
        datetime(2006, 1, 1),
        'triweekly')
    smbc = Webcomic(
        'SMBC',
        'https://smbc-comics.com',
        ['Zach Weinersmith'],
        ['humor', 'philosophy', 'sociology', 'sci-fi', 'graph jokes'],
        datetime(2002, 9, 5),
        'daily')
    cnh = Webcomic(
        'C&H',
        'http://explosm.net',
        ['Kris Wilson', 'Dave McElfatrick', 'Rob DenBleyker', 'Matt Melvin'],
        ['satire', 'surreal', 'puns', 'adult content'],
        datetime(2005, 1, 25),
        'daily')

    str1 = 'authors.len<2&age.int<15'
    str2 = 'ssl.bool=true&themes.str[]->humor'
    closures = [filter_closure(s) for s in (str1, str2)]
    for closure in closures:
        for test in closure:
            for webcomic in (xkcd, smbc, cnh):
                print(webcomic.name, test(webcomic))

A  => objects.py +28 -0
@@ 1,28 @@
# Example classes to demonstrate how closure filtering works
from datetime import datetime


class Webcomic:
    def __init__(
        self,
        name: str,
        url: str,
        authors: list,
        themes: list,
        since: datetime,
        schedule: str,
    ):
        self.name = name
        self.url = url
        self.authors = authors
        self.themes = themes
        self.since = since
        self.schedule = schedule

    @property
    def age(self):
        return datetime.now().year - self.since.year
        
    @property
    def ssl(self):
        return self.url.startswith('https://')
\ No newline at end of file