Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add translator between a predefined grammar and django's filter lookups #95

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ install:
- pip3 install "Django==${DJANGO_VERSION}"
- pip3 install Sphinx
- pip3 install flake8
- pip install coverage

- pip3 install coverage
- pip3 install lark-parser

script:
# makemigrations of each app is needed
- python manage.py makemigrations drip
- python manage.py makemigrations credits
- python manage.py makemigrations auth
- python manage.py makemigrations grammar
- python manage.py migrate
- flake8 .
- coverage run manage.py test
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ sphinx-rtd-theme = "*"
[packages]
Django = "==3.0.7"
apscheduler = "*"
lark-parser = "*"

[requires]
python_version = "3.8"
115 changes: 75 additions & 40 deletions Pipfile.lock

Large diffs are not rendered by default.

Empty file added grammar/__init__.py
Empty file.
26 changes: 26 additions & 0 deletions grammar/grammar.lark
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
start : query
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change " by ' in this file?

| expression

query : "("* expression ")"*
| query CONNECTOR query

expression : computed OPERATOR atom

computed : FIELD
| computed "." computed

atom : "True" -> true
| "False" -> false
| NUMBER
| WORD
| ESCAPED_STRING -> string

CONNECTOR : "and" | "or"
FIELD : (WORD | "_")+
OPERATOR : "<" | "<=" | "==" | ">=" | ">" | "null" | "is"


%import common.WORD
%import common.NUMBER
%import common.ESCAPED_STRING
%ignore " "
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing blank line

55 changes: 55 additions & 0 deletions grammar/parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from lark import Lark, Transformer


class DjangoQueriesTransformer(Transformer):
"""
Applies multiple transformations over the parsed tree
"""
def start(self, tree):
return tree[0]

def OPERATOR(self, tree):
operator_map = {
">": "__gt",
"<": "__lt",
"==": "__eq",
"is": "",
"null": "__isnull",
}
Comment on lines +12 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here: Can you change " by ' in this file?

return operator_map[tree.value]

def atom(self, a):
(a,) = a
return a.value

def query(self, tree):
if len(tree) == 3:
return {**tree[0], **tree[2]}
else:
return tree[0]

def expression(self, tree):
return {"".join(tree[:2]): tree[2]}

def computed(self, tree):
return "__".join(tree)

def string(self, s):
(s,) = s
return s.strip('"')

def true(self, _):
return True

def false(self, _):
return False


def translate_query(expression, grammar):
lexer = Lark(grammar)

parse_tree = lexer.parse(expression, start="start")
query_dict = DjangoQueriesTransformer(
visit_tokens=True
).transform(parse_tree)
return query_dict
38 changes: 38 additions & 0 deletions grammar/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from pathlib import Path

from django.test import TestCase
from drip.models import Drip

from .parser import translate_query


class GrammarTest(TestCase):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same change here

def test_grammar(self):
grammar = Path(Path(__file__).parent, "grammar.lark").open()
grammar = "".join(grammar.readlines())

Drip(name="Nacho's Drip").save()
Drip(name="Nice Drip").save()
Drip(name="Not so nice").save()

input_expression = "(enabled is False)" \
"and (sent_drips null True)" \
"and (name is \"Nacho's Drip\")"
filter = translate_query(input_expression, grammar)
assert Drip.objects.filter(**filter).count() == 1

input_expression = "enabled is False"
filter = translate_query(input_expression, grammar)
assert Drip.objects.filter(**filter).count() == 3

input_expression = "(enabled is False) and (name is \"Not so nice\")"
filter = translate_query(input_expression, grammar)
assert Drip.objects.filter(**filter).count() == 1

input_expression = "(enabled is False) and (sent_drips null True)"
filter = translate_query(input_expression, grammar)
assert Drip.objects.filter(**filter).count() == 3

input_expression = "(enabled is False) and (sent_drips null False)"
filter = translate_query(input_expression, grammar)
assert Drip.objects.filter(**filter).count() == 0
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
author = 'Kalil de Lima'
author_email = '[email protected]'
license = 'MIT'
install_requires = ['Django>=2.2', 'apscheduler']
install_requires = ['Django>=2.2', 'apscheduler', 'lark-parser']
keywords = 'django drip email user query'


Expand Down
1 change: 1 addition & 0 deletions testsettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
'django.contrib.messages',

'drip',
'grammar',

# testing only
'credits',
Expand Down