Вы в хорошей компании
1
40 50+ 80+ 1500
млн человек
суммарная аудитория группы
количество изданий,

сервисов и проектов
разработчиков человек в хорошей
компании
2
3
Языки и технологии
4
Контакты
В группе компаний Rambler&Co всегда
есть открытые вакансии для тех, кто хочет
профессионально расти и развиваться,
занимаясь тем, что по-настоящему
нравится
hr@rambler-co.ru
www.rambler-co.ru/jobs
5
Реализация
Domain-Specific Languages
в Django приложении
6
Зачем нам нужен DSL???
7
Чтобы как JIRA
8
Некоторые запросы тяжело (или невозможно)
построить через GUI
• Для чего GUI подходит:



pictures with width > height and not marked

rotate 90



if height > 600px

resize to height=600px

• GUI запросы с многими критериями часто ограничены только операциями AND
(иногда только OR)



... но условие "(cond1 OR NOT cond2) AND cond3" 

в общем случае не реализуемо

• GUIs для действий обычно ограничен линейным (последовательным) потоком
действий 9
Выбор инструмента
•PyBison - LALR(1) (Look-Ahead Left-to-Right parser)
•PLY
•до версии 1.5 - SLR(Simple Left to right, reversed Rightmost derivation)
•после 2.0 только - LALR(1)
•funcparselib - LL(*) (Left to right, performing Leftmost derivation )
•codetalker
10
PLY - Как устроен?
• В первом приближении: лексер, парсер и какой-то бэкенд
• Давайте посмотрим, как скомпилировать выражение вида







в **django.db.models.Q** объект

g
A
=
N
D
X‘
‘
g
=
‘x’
AND
AND
g ‘x’
=
Bunch of chars Bunch of words Structured information
«vocabulary» «grammar»
BACKEND
«DO SOMETHING»
1 person__name="XXX" AND NOT person__name="YYY"
2 (modified > 1/4/2011 OR NOT state__name="OK") AND groups__name=="XXX"
11
PLY - Лексер1 import ply.lex as lex
2
3 tokens = (
4 'COMPA', # comparison operator
5 'STRING',
6 'NUMBER',
7 #[...]
8 )
9
10 t_COMPA = r'=|[<>]=?|~~?'
11
12 literals = '()' # shortcut for 1-char tokens
13
14 def t_STRING(t):
15 r'"[^"]*"'
16 t.value = t.value[1:-1]
17 return t
18
19 def t_NUMBER(t):
20 r'd+'
21 t.value = int(t.value)
22 return t
23
24 # [...]
25
26 def t_error(t):
27 raise CompileException(u"Cannot make sense of char: %s" % t.value[0]) 12
PLY - Грамматика
1 expression : expression B_OP expression
2 expression : U_OP expression
3 expression : '(' expression ')'
4 expression : FIELD COMPA value
5 value : STRING
6 | NUMBER
7 | DATE

NOT
U_OP
person_name
FIELD
=
COMPA
XXX
STRING
value
expression
expression
a
lexerparser
13
PLY - Парсер
• Правила грамматики описываем в docstrings
• Специальный аргумент p описывает части правил

1 def p_expression_u_op(p):
2 '''expression : U_OP expression'''
3 if p[1] == 'NOT':
4 p[0] = ~ p[2]


означает:
Если U_OP стоит перед выражением, то это новое выражение, определяемое как:
если значение U_OP равно 'NOT', тогда значение финального выражения -
это отрицание исходного выражения
14
PLY - Парсер - 2
1 import ply.yacc as yacc
2
3 def p_value(p):
4 '''value : STRING
5 | NUMBER
6 | DATE'''
7 p[0] = p[1]
8
9 def p_expression_paren(p):
10 "expression : '(' expression ')' "
11 p[0] = p[2]
12
13 def p_expression_b_op(p):
14 '''expression : expression B_OP expression'''
15 if p[2] == 'AND':
16 p[0] = p[1] & p[3]
17 elif p[2] == 'OR':
18 p[0] = p[1] | p[3]
15
PLY - Парсер - 3
1 from django.db.models import Q
2
3 comparison_to_lookup = {
4 '=': 'exact',
5 '~': 'contains',
6 '~~': 'regex',
7 '>': 'gt',
8 '>=': 'gte',
9 '<': 'lt',
10 '<=': 'lte',
11 }
12
13 def p_expression_ID(p):
14 'expression : FIELD COMPA value'
15
16 # Let's map 'person__name = "XXX"' to
17 # Q(person__name__exact=“XXX”)
18
19 lookup = comparison_to_lookup[p[2]]
20
21 field = '%s__%s' % (p[1], lookup)
22
23 d = {field: p[3]}
24
25 p[0] = Q(**d)
16
Собираем все вместе
1 def compile(expr):
2 # create separate lexer and parser for each compilation
3 # (our app is multi-threaded)
4 lexer = lex.lex()
5 parser = yacc.yacc()
6 # now, parse!
7 return parser.parse(expr,lexer=lexer)

Функция compile вернет Q объект соответсвующим запросом (или выкинет
CompileException). Можно просто использовать во вьюхе или форме:
1 # try with things like
2 # expr = 'person__name="XXX" AND NOT person__name="YYY"'
3 # or expr = 'modified > 1/4/2011 OR NOT state__name="OK"'
4
5 try:
6 q = compile(expr)
7 except CompileException, e:
8 # error handling
9
10 qs = Contact.objects.filter(q)
17
Ограничения
• Мы ограничены функциональностью Django's ORM
• Только язык запросов (в отличие скриптовых языков), 

т.е. декларативный, а не императивный язык

18
Дальнейшее развитие языка
• Вместо (или в дополнение) построения Q объектов, строим функции
1 def p_statement(p):
2 'statement : ACTION expression'
3
4 if p[1] == 'PUBLISH':
5 p[0] = lambda: Video.objects.filter(p[2]).update(published=True)
6 elif ...
19
Дальнейшее развитие языка
• поддержка операторов

1 def p_expression_op(p):
2 '''expression : expression ADD_OP expression'''
3 p[0] = OpNode(op=p[2], children=[p[1], p[3]])
• пишем интерпритатор:

1 class OpNode:
2
3 def execute(self):
4 args = [c.execute() for c in self.children]
5 if self.op == '+':
6 return operators.add(*args)
7 else:
8 return operators.sub(*args)
20
PLY - Скорость?
1 @profile
2 def test_compile(times=1000):
3 input = """content_types IN ("Video", "Person") AND 
4 (modified > 1/4/2015 OR NOT state="Published") AND 
5 title~"context.title"
6 """
7 for n in range(times):
8 ex = compile(input)
• результаты замеров:


1 *** PROFILER RESULTS ***
2 function called 1 times
3
4 961098 function calls (954964 primitive calls) in 0.873 seconds
21
Django
• надо хранить запрос
• бывают разные модели и у них разные поля
• одним запросом надо получить несколько моделей
• запрос один - а контексты разные
• имена полей одинаковые - классы полей разные
22
Django - QSerializer
• операции serialize / deserialize
• любимый json dumps / loads
• обход имен и значений полей
• применение функций к именам и значениям полей
• обработка специального имени поля “content_types"
• обработка выражений вида parent_id = "context.get_id()"
23
Django - QSerializer - Скорость
1 from video.cms.q_serializer import QSerializer
2
3 q_ser = QSerializer()
4
5 input = """content_types IN ("Video", "Person") AND 
6 (modified > 1/4/2015 OR NOT state="Published") AND 
7 title~"context.title"
8 """
9 ex = compile(input)
10
11 @profile
12 def test_serialization(times=1000):
13 for n in range(times):
14 q_ser.deserialize(q_ser.serialize(ex))
• результаты замеров:
1 *** PROFILER RESULTS ***
2 function called 1 times
3
4 137463 function calls (129481 primitive calls) in 0.135 seconds
24
Django - QSerializer - Получаем список моделей
• надо  обежать  все  дерево  запроса  и  получить  имена  полей  и  значения

рекурсивный  генератор:
1 class QSerializer(object):
2 [...]
3 @staticmethod
4 def keys_values_generator(d):
5 for child in d.get('children'):
6 if isinstance(child, dict):
7 for key, val in QSerializer.keys_values_generator(child):
8 yield key, val
9 else:
10 key, val = QSerializer.prepare_value(child)
11 yield key, val
25
Django - QSerializer - Подстановка контекста
• волшебный .each()
1 class QSerializer(object):
2 [...]
3 @staticmethod
4 def each(d, v_func=lambda x: x,
5 k_func=lambda x: x, post_func=lambda x, y: (x, y)):
6
7 process_child = partial(QSerializer.apply_func, v_func=v_func,
8 k_func=k_func, post_func=post_func)
9
10 d['children'] = list(filter(lambda x: x is not None,
11 map(process_child, d['children'])))
12 return d
13
14 @staticmethod
15 def apply_func(child, v_func=lambda x: x,
16 k_func=lambda x: x, post_func=lambda x, y: (x, y)):
17
18 if isinstance(child, dict):
19 return QSerializer.each(child, v_func, k_func, post_func)
20 else:
21 key, val = QSerializer.prepare_value(child)
22 return post_func(k_func(key), v_func(val))
26
Django - QSerializer - Подстановка контекста
1 class QSerializer(object):
2 [...]
3
4 @staticmethod
5 def execute_in_context(d, context):
6 d = copy.deepcopy(d)
7
8 def context_processor(val):
9 if not isinstance(val, str):
10 return val
11 if not val.startswith("context."):
12 return val
13 call_path = val.split(".")
14 if call_path[-1] == '':
15 call_path = call_path[:-1]
16 if len(call_path) == 1:
17 raise Exception("need method name")
18
19 call_path = call_path[1:]
20
21 return reduce(QSerializer.call_or_get, call_path, context)
22
23 QSerializer.each(d, context_processor)
24 return d
27
Django - QSerializer - Подстановка контекста
1 class QSerializer(object):
2 [...]
3
4 @staticmethod
5 def call_or_get(obj, name):
6 print(obj, name)
7 if name.endswith('()') and len(name) > 2:
8 name = name[:-2]
9 obj_attr = getattr(obj, name)
10 if callable(obj_attr):
11 caller = methodcaller(name)
12 return caller(obj)
13 else:
14 return obj_attr
28
Django - Но ведь делали как в JIRA!
• кастомные виджеты
• нужна поддержка NOT
• нужна поддержка livesearch
• текущая конфигурация виджетов
• доступные виджеты для фильтра
29
Вопросы?
30

Making of external DSL for Django ORM - Павел Петлинский, Rambler&Co

  • 1.
    Вы в хорошейкомпании 1
  • 2.
    40 50+ 80+1500 млн человек суммарная аудитория группы количество изданий,
 сервисов и проектов разработчиков человек в хорошей компании 2
  • 3.
  • 4.
  • 5.
    Контакты В группе компанийRambler&Co всегда есть открытые вакансии для тех, кто хочет профессионально расти и развиваться, занимаясь тем, что по-настоящему нравится hr@rambler-co.ru www.rambler-co.ru/jobs 5
  • 6.
  • 7.
  • 8.
  • 9.
    Некоторые запросы тяжело(или невозможно) построить через GUI • Для чего GUI подходит:
 
 pictures with width > height and not marked
 rotate 90
 
 if height > 600px
 resize to height=600px
 • GUI запросы с многими критериями часто ограничены только операциями AND (иногда только OR)
 
 ... но условие "(cond1 OR NOT cond2) AND cond3" 
 в общем случае не реализуемо
 • GUIs для действий обычно ограничен линейным (последовательным) потоком действий 9
  • 10.
    Выбор инструмента •PyBison -LALR(1) (Look-Ahead Left-to-Right parser) •PLY •до версии 1.5 - SLR(Simple Left to right, reversed Rightmost derivation) •после 2.0 только - LALR(1) •funcparselib - LL(*) (Left to right, performing Leftmost derivation ) •codetalker 10
  • 11.
    PLY - Какустроен? • В первом приближении: лексер, парсер и какой-то бэкенд • Давайте посмотрим, как скомпилировать выражение вида
 
 
 
 в **django.db.models.Q** объект
 g A = N D X‘ ‘ g = ‘x’ AND AND g ‘x’ = Bunch of chars Bunch of words Structured information «vocabulary» «grammar» BACKEND «DO SOMETHING» 1 person__name="XXX" AND NOT person__name="YYY" 2 (modified > 1/4/2011 OR NOT state__name="OK") AND groups__name=="XXX" 11
  • 12.
    PLY - Лексер1import ply.lex as lex 2 3 tokens = ( 4 'COMPA', # comparison operator 5 'STRING', 6 'NUMBER', 7 #[...] 8 ) 9 10 t_COMPA = r'=|[<>]=?|~~?' 11 12 literals = '()' # shortcut for 1-char tokens 13 14 def t_STRING(t): 15 r'"[^"]*"' 16 t.value = t.value[1:-1] 17 return t 18 19 def t_NUMBER(t): 20 r'd+' 21 t.value = int(t.value) 22 return t 23 24 # [...] 25 26 def t_error(t): 27 raise CompileException(u"Cannot make sense of char: %s" % t.value[0]) 12
  • 13.
    PLY - Грамматика 1expression : expression B_OP expression 2 expression : U_OP expression 3 expression : '(' expression ')' 4 expression : FIELD COMPA value 5 value : STRING 6 | NUMBER 7 | DATE
 NOT U_OP person_name FIELD = COMPA XXX STRING value expression expression a lexerparser 13
  • 14.
    PLY - Парсер •Правила грамматики описываем в docstrings • Специальный аргумент p описывает части правил
 1 def p_expression_u_op(p): 2 '''expression : U_OP expression''' 3 if p[1] == 'NOT': 4 p[0] = ~ p[2] 
 означает: Если U_OP стоит перед выражением, то это новое выражение, определяемое как: если значение U_OP равно 'NOT', тогда значение финального выражения - это отрицание исходного выражения 14
  • 15.
    PLY - Парсер- 2 1 import ply.yacc as yacc 2 3 def p_value(p): 4 '''value : STRING 5 | NUMBER 6 | DATE''' 7 p[0] = p[1] 8 9 def p_expression_paren(p): 10 "expression : '(' expression ')' " 11 p[0] = p[2] 12 13 def p_expression_b_op(p): 14 '''expression : expression B_OP expression''' 15 if p[2] == 'AND': 16 p[0] = p[1] & p[3] 17 elif p[2] == 'OR': 18 p[0] = p[1] | p[3] 15
  • 16.
    PLY - Парсер- 3 1 from django.db.models import Q 2 3 comparison_to_lookup = { 4 '=': 'exact', 5 '~': 'contains', 6 '~~': 'regex', 7 '>': 'gt', 8 '>=': 'gte', 9 '<': 'lt', 10 '<=': 'lte', 11 } 12 13 def p_expression_ID(p): 14 'expression : FIELD COMPA value' 15 16 # Let's map 'person__name = "XXX"' to 17 # Q(person__name__exact=“XXX”) 18 19 lookup = comparison_to_lookup[p[2]] 20 21 field = '%s__%s' % (p[1], lookup) 22 23 d = {field: p[3]} 24 25 p[0] = Q(**d) 16
  • 17.
    Собираем все вместе 1def compile(expr): 2 # create separate lexer and parser for each compilation 3 # (our app is multi-threaded) 4 lexer = lex.lex() 5 parser = yacc.yacc() 6 # now, parse! 7 return parser.parse(expr,lexer=lexer)
 Функция compile вернет Q объект соответсвующим запросом (или выкинет CompileException). Можно просто использовать во вьюхе или форме: 1 # try with things like 2 # expr = 'person__name="XXX" AND NOT person__name="YYY"' 3 # or expr = 'modified > 1/4/2011 OR NOT state__name="OK"' 4 5 try: 6 q = compile(expr) 7 except CompileException, e: 8 # error handling 9 10 qs = Contact.objects.filter(q) 17
  • 18.
    Ограничения • Мы ограниченыфункциональностью Django's ORM • Только язык запросов (в отличие скриптовых языков), 
 т.е. декларативный, а не императивный язык
 18
  • 19.
    Дальнейшее развитие языка •Вместо (или в дополнение) построения Q объектов, строим функции 1 def p_statement(p): 2 'statement : ACTION expression' 3 4 if p[1] == 'PUBLISH': 5 p[0] = lambda: Video.objects.filter(p[2]).update(published=True) 6 elif ... 19
  • 20.
    Дальнейшее развитие языка •поддержка операторов 
1 def p_expression_op(p): 2 '''expression : expression ADD_OP expression''' 3 p[0] = OpNode(op=p[2], children=[p[1], p[3]]) • пишем интерпритатор: 
1 class OpNode: 2 3 def execute(self): 4 args = [c.execute() for c in self.children] 5 if self.op == '+': 6 return operators.add(*args) 7 else: 8 return operators.sub(*args) 20
  • 21.
    PLY - Скорость? 1@profile 2 def test_compile(times=1000): 3 input = """content_types IN ("Video", "Person") AND 4 (modified > 1/4/2015 OR NOT state="Published") AND 5 title~"context.title" 6 """ 7 for n in range(times): 8 ex = compile(input) • результаты замеров: 
 1 *** PROFILER RESULTS *** 2 function called 1 times 3 4 961098 function calls (954964 primitive calls) in 0.873 seconds 21
  • 22.
    Django • надо хранитьзапрос • бывают разные модели и у них разные поля • одним запросом надо получить несколько моделей • запрос один - а контексты разные • имена полей одинаковые - классы полей разные 22
  • 23.
    Django - QSerializer •операции serialize / deserialize • любимый json dumps / loads • обход имен и значений полей • применение функций к именам и значениям полей • обработка специального имени поля “content_types" • обработка выражений вида parent_id = "context.get_id()" 23
  • 24.
    Django - QSerializer- Скорость 1 from video.cms.q_serializer import QSerializer 2 3 q_ser = QSerializer() 4 5 input = """content_types IN ("Video", "Person") AND 6 (modified > 1/4/2015 OR NOT state="Published") AND 7 title~"context.title" 8 """ 9 ex = compile(input) 10 11 @profile 12 def test_serialization(times=1000): 13 for n in range(times): 14 q_ser.deserialize(q_ser.serialize(ex)) • результаты замеров: 1 *** PROFILER RESULTS *** 2 function called 1 times 3 4 137463 function calls (129481 primitive calls) in 0.135 seconds 24
  • 25.
    Django - QSerializer- Получаем список моделей • надо  обежать  все  дерево  запроса  и  получить  имена  полей  и  значения
 рекурсивный  генератор: 1 class QSerializer(object): 2 [...] 3 @staticmethod 4 def keys_values_generator(d): 5 for child in d.get('children'): 6 if isinstance(child, dict): 7 for key, val in QSerializer.keys_values_generator(child): 8 yield key, val 9 else: 10 key, val = QSerializer.prepare_value(child) 11 yield key, val 25
  • 26.
    Django - QSerializer- Подстановка контекста • волшебный .each() 1 class QSerializer(object): 2 [...] 3 @staticmethod 4 def each(d, v_func=lambda x: x, 5 k_func=lambda x: x, post_func=lambda x, y: (x, y)): 6 7 process_child = partial(QSerializer.apply_func, v_func=v_func, 8 k_func=k_func, post_func=post_func) 9 10 d['children'] = list(filter(lambda x: x is not None, 11 map(process_child, d['children']))) 12 return d 13 14 @staticmethod 15 def apply_func(child, v_func=lambda x: x, 16 k_func=lambda x: x, post_func=lambda x, y: (x, y)): 17 18 if isinstance(child, dict): 19 return QSerializer.each(child, v_func, k_func, post_func) 20 else: 21 key, val = QSerializer.prepare_value(child) 22 return post_func(k_func(key), v_func(val)) 26
  • 27.
    Django - QSerializer- Подстановка контекста 1 class QSerializer(object): 2 [...] 3 4 @staticmethod 5 def execute_in_context(d, context): 6 d = copy.deepcopy(d) 7 8 def context_processor(val): 9 if not isinstance(val, str): 10 return val 11 if not val.startswith("context."): 12 return val 13 call_path = val.split(".") 14 if call_path[-1] == '': 15 call_path = call_path[:-1] 16 if len(call_path) == 1: 17 raise Exception("need method name") 18 19 call_path = call_path[1:] 20 21 return reduce(QSerializer.call_or_get, call_path, context) 22 23 QSerializer.each(d, context_processor) 24 return d 27
  • 28.
    Django - QSerializer- Подстановка контекста 1 class QSerializer(object): 2 [...] 3 4 @staticmethod 5 def call_or_get(obj, name): 6 print(obj, name) 7 if name.endswith('()') and len(name) > 2: 8 name = name[:-2] 9 obj_attr = getattr(obj, name) 10 if callable(obj_attr): 11 caller = methodcaller(name) 12 return caller(obj) 13 else: 14 return obj_attr 28
  • 29.
    Django - Новедь делали как в JIRA! • кастомные виджеты • нужна поддержка NOT • нужна поддержка livesearch • текущая конфигурация виджетов • доступные виджеты для фильтра 29
  • 30.