Created
January 26, 2018 16:04
-
-
Save AmatanHead/afc67c9da0ef2ae724e12a51986d67b5 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- coding: utf-8 -*- | |
from __future__ import print_function, absolute_import, division | |
import re | |
from statbox_abt_metrics.meta import Meta | |
from yandex_inflector_python import Inflector | |
from ....utils import typehints as _t | |
__all__ = [ | |
'Context', | |
'Dim', | |
'Slice', | |
'Custom', | |
'If', | |
'IfAll', | |
'IfAny', | |
] | |
class Context(object): | |
#: Инфлектор для текущего языка. | |
inflector = None # type: Inflector | |
#: Название локали, для которой происходит генерация. Может быть | |
#: ``'ru_name'``, ``'en_name'``, ``'description'``. | |
locale = 'ru_name' # type: str | |
#: Список названий всех измерений куба меты. Названия перечислены в порядке | |
#: применения декартова произведения. | |
dimensions = () # type: _t.Tuple[str, ...] | |
#: Данные по каждому из измерений. Ключами являются названия измерений, | |
#: значениями -- объекты :py:class:`~.Meta`. | |
data = {} # type: _t.Dict[str, _t.Meta] | |
def get(self, dim): | |
""" | |
Получить значение измерения для текущей локали. | |
:param dim: название измерения. | |
:return: значение измерения или пустая строка. | |
:rtype: ``str`` | |
""" | |
if dim in self.data: | |
return getattr(self.data[dim], self.locale, '') | |
else: | |
return u'' | |
def has(self, dim): | |
""" | |
Проверить, есть ли значение измерения для текущей локали | |
и не пусто ли оно. | |
:param dim: название измерения. | |
:return: ``True``, если значение не пусто. | |
:rtype: ``bool`` | |
""" | |
return bool(self.get(dim)) | |
# Типы для улучшения статического анализа | |
TPredicate = _t.Callable[[Context], bool] | |
TMutator = _t.Callable[[Context, unicode, bool, bool], _t.Tuple[unicode, bool, bool]] | |
class TemplateNode(object): | |
def eval(self, context): | |
""" | |
Сгенерировать текст для данного контекста. | |
:param context: контекст, в котором выполняется этот узел. | |
:type context: :py:class:`~.Context` | |
:return: тьюпл ``(text, any, all)``, где | |
* ``text`` -- текстовый результат выполнения узла. | |
* ``all`` -- ``bool``, ``True`` если все подузлы дерева вернули | |
непустое значение. | |
* ``any`` -- ``bool``, ``True`` если хотя бы один из подузлов | |
``Dim``, ``Slice``, ``Custom`` дерева вернул непустое значение. | |
""" | |
raise NotImplementedError() | |
def inflect(self, gram): | |
# type: (str) -> Inflect | |
""" | |
Изменить падеж и число словосочетания в шаблоне. | |
:param gram: обозначение граммемы нужного падежа. | |
""" | |
return Inflect(self, gram) | |
def inflect_if(self, predicate, gram): | |
# type: (TPredicate, str) -> InflectIf | |
""" | |
Если выполняется условие, изменить падеж и число | |
словосочетания в шаблоне. | |
:param predicate: callable, принимающий на вход :py:class:`~.Context` | |
и возвращающий ``bool`` -- результат проверки. | |
:param gram: обозначение граммемы нужного падежа. | |
""" | |
return InflectIf(self, predicate, gram) | |
def mutate(self, mutator): | |
# type: (TMutator) -> Mutate | |
""" | |
Применить функцию к тексту, полученному в результате выполнения | |
поддерева. | |
:param mutator: callable, функция, изменяющая текст. | |
Принимает на вход контекст исполнения, изменяемый текст, | |
переменные ``all_ok``, ``any_ok`` (см. | |
:py:meth:`.TemplateNode.eval`) и возвращает тьюпл с измененным | |
текстом и новыми ``all_ok``, ``any_ok``. | |
""" | |
return Mutate(self, mutator) | |
def mutate_if(self, predicate, mutator): | |
# type: (TPredicate, TMutator) -> MutateIf | |
""" | |
Если выполняется условие, применить к тексту мутатор. | |
:param predicate: callable, принимающий на вход :py:class:`~.Context` | |
и возвращающий ``bool`` -- результат проверки. | |
:param mutator: см. :py:meth:`.TemplateNode.mutate`. | |
""" | |
return MutateIf(self, predicate, mutator) | |
def mutate_simple(self, mutator): | |
# type: (typing.Callable[[unicode], unicode]) -> Mutate | |
""" | |
Применить к тексту упрощенный мутатор, не изменяющий | |
``all_ok`` и ``any_ok``. | |
:param mutator: callable, принимающий на вход строку и возвращающий | |
другую строку. | |
""" | |
return self.mutate(lambda ctx, text, a, b: (mutator(text), a, b)) | |
def lower(self): | |
""" | |
Перевести текст в поддереве в нижний регистр. | |
""" | |
return self.mutate_simple(unicode.lower) | |
def upper(self): | |
""" | |
Перевести текст в поддереве в верхний регистр. | |
""" | |
return self.mutate_simple(unicode.upper) | |
def title(self): | |
""" | |
Перевести первую букву каждого слова в поддереве в верхний регистр, | |
остальные буквы -- в нижний. | |
""" | |
return self.mutate_simple(unicode.title) | |
def clause(self): | |
""" | |
Перевести первую букву текста в поддереве в верхний регистр, | |
остальные не трогать. | |
""" | |
return self.mutate_simple(lambda s: s[0].upper() + s[1:] if s else s) | |
def __add__(self, rhs): | |
return _add(self, rhs) | |
def __iadd__(self, rhs): | |
return _add(self, rhs) | |
def __radd__(self, lhs): | |
return _add(lhs, self) | |
class Dim(TemplateNode): | |
""" | |
Извлекает из контекста значение измерения для текущей локали. | |
""" | |
def __init__(self, dim): | |
# type: (str) -> Dim | |
self.dim = dim | |
def eval(self, context): | |
data = context.get(self.dim) | |
return data, bool(data), bool(data) | |
class Slice(TemplateNode): | |
""" | |
Извлекает из контекста значения измерений, название которых начинается на | |
``base`` и конкатенирует их с использованием ``joiner``. | |
""" | |
def __init__(self, base, joiner=u', '): | |
# type: (str, unicode) -> Slice | |
self.base = base | |
self.joiner = joiner | |
def eval(self, context): | |
keys = [k for k in context.dimensions if k.startswith(self.base)] | |
slices = [context.get(k) for k in keys] | |
slices = filter(None, slices) | |
return self.joiner.join(slices), bool(slices), bool(slices) | |
class Custom(TemplateNode): | |
""" | |
Применяет к контексту функцию ``functor``. | |
:param functor: callable, принимающий на вход :py:class:`~.Context` | |
и возвращающий тьюпл ``(text, any, all)``. | |
См. :py:meth:`.TemplateNode.eval`. | |
""" | |
def __init__(self, functor): | |
# type: (typing.Callable[[Context], typing.Tuple[unicode, bool, bool]]) -> Custom | |
self.functor = functor | |
def eval(self, context): | |
return self.functor(context) | |
class Const(TemplateNode): | |
""" | |
Служебный узел с константным значением. | |
""" | |
def __init__(self, data): | |
# type: (typing.Any) -> Const | |
self.data = unicode(data) | |
def eval(self, context): | |
return self.data, True, False | |
def __repr__(self): | |
return self.data | |
class Sum(TemplateNode): | |
""" | |
Служебный узел, получающийся в результате сложения нескольких других узлов. | |
""" | |
def __init__(self, components): | |
# type: (typing.List[TemplateNode]) -> Sum | |
self.components = components | |
def eval(self, context): | |
text, all_ok, any_ok = '', True, False | |
for component in self.components: | |
result = component.eval(context) | |
text += result[0] | |
all_ok &= result[1] | |
any_ok |= result[2] | |
return text, all_ok, any_ok | |
class If(TemplateNode): | |
""" | |
Ветвление шаблона. Если предикат верен для данного контекста, | |
в шаблон подставляется ``on_then``, иначе -- ``on_else``. | |
:param predicate: callable, принимающий на вход :py:class:`~.Context` | |
и возвращающий ``bool`` -- результат проверки. | |
:param on_then: строка или другой узел шаблона, подставляется в случае | |
прохождения проверки. | |
:param on_else: строка или другой узел шаблона, подставляется в случае | |
провала проверки. | |
""" | |
def __init__(self, predicate, on_then, on_else=''): | |
# type: (TPredicate, typing.Any, typing.Any) -> If | |
self.predicate = predicate | |
if not isinstance(on_then, TemplateNode): | |
on_then = Const(on_then) | |
self.on_then = on_then | |
if not isinstance(on_else, TemplateNode): | |
on_else = Const(on_else) | |
self.on_else = on_else | |
def eval(self, context): | |
if self.predicate(context): | |
return self.on_then.eval(context) | |
else: | |
return self.on_else.eval(context) | |
class IfAll(TemplateNode): | |
""" | |
Специальный иф для скрытия пустых размерностей. | |
Выполняет поддерево ``on_then``. Если при выполнении ``on_then`` | |
все узлы типов :py:class:`.Dim`, :py:class:`.Slice`, :py:class:`.Custom` | |
развернулись в непустое значение, возвращает результат выполнения | |
``on_then``, иначе запускает и возвращает поддерево ``on_else``. | |
""" | |
_check = staticmethod(lambda all_ok, any_ok: all_ok) | |
def __init__(self, on_then, on_else=''): | |
# type: (typing.Any, typing.Any) -> IfAll | |
if not isinstance(on_then, TemplateNode): | |
on_then = Const(on_then) | |
self.on_then = on_then | |
if not isinstance(on_else, TemplateNode): | |
on_else = Const(on_else) | |
self.on_else = on_else | |
def eval(self, context): | |
text, all_ok, any_ok = self.on_then.eval(context) | |
if self._check(all_ok, any_ok): | |
return text, all_ok, any_ok | |
else: | |
return self.on_else.eval(context) | |
class IfAny(IfAll): | |
""" | |
Выполняет поддерево ``on_then``. Если хотябы один узел :py:class:`.Dim`, | |
:py:class:`.Slice`, :py:class:`.Custom` этого поддерева развернулся | |
в непустое значение, возвращает ``on_then``, иначе возвращает ``on_else``. | |
См. :py:class:`~.IfAll`. | |
""" | |
_check = staticmethod(lambda all_ok, any_ok: any_ok) | |
class Inflect(TemplateNode): | |
""" | |
Служебный узел, получающийся в результате вызова | |
:py:meth:`.TemplateNode.inflect`. | |
""" | |
def __init__(self, child, gram): | |
# type: (typing.Any, str) -> Inflect | |
if not isinstance(child, TemplateNode): | |
child = Const(child) | |
self.child = child | |
self.gram = gram | |
def eval(self, context): | |
text, all_ok, any_ok = self.child.eval(context) | |
text = context.inflector.Inflect(text, self.gram) | |
return text, all_ok, any_ok | |
class InflectIf(Inflect): | |
""" | |
Служебный узел, получающийся в результате вызова | |
:py:meth:`.TemplateNode.inflect_if`. | |
""" | |
def __init__(self, child, predicate, gram): | |
# type: (typing.Any, TPredicate, str) -> InflectIf | |
super(InflectIf, self).__init__(child, gram) | |
self.predicate = predicate | |
def eval(self, context): | |
if self.predicate(context): | |
return super(InflectIf, self).eval(context) | |
else: | |
return self.child.eval(context) | |
class Mutate(TemplateNode): | |
""" | |
Служебный узел, получающийся в результате вызова | |
:py:meth:`.TemplateNode.mutate`. | |
""" | |
def __init__(self, child, mutator): | |
# type: (typing.Any, TMutator) -> Mutate | |
if not isinstance(child, TemplateNode): | |
child = Const(child) | |
self.child = child | |
self.mutator = mutator | |
def eval(self, context): | |
text, all_ok, any_ok = self.child.eval(context) | |
return self.mutator(context, text, all_ok, any_ok) | |
class MutateIf(Mutate): | |
""" | |
Служебный узел, получающийся в результате вызова | |
:py:meth:`.TemplateNode.mutate_if`. | |
""" | |
def __init__(self, child, predicate, mutator): | |
# type: (typing.Any, TPredicate, TMutator) -> MutateIf | |
super(MutateIf, self).__init__(child, mutator) | |
self.predicate = predicate | |
def eval(self, context): | |
if self.predicate(context): | |
return super(MutateIf, self).eval(context) | |
else: | |
return self.child.eval(context) | |
def _add(lhs, rhs): | |
# type: (typing.Any, typing.Any) -> TemplateNode | |
if not isinstance(lhs, TemplateNode): | |
lhs = Const(lhs) | |
if not isinstance(rhs, TemplateNode): | |
rhs = Const(rhs) | |
lhs_is_const = isinstance(lhs, Const) | |
lhs_is_sum = isinstance(lhs, Sum) | |
rhs_is_const = isinstance(rhs, Const) | |
rhs_is_sum = isinstance(rhs, Sum) | |
if lhs_is_sum and not lhs.components: | |
return rhs | |
if rhs_is_sum and not rhs.components: | |
return lhs | |
if lhs_is_const and rhs_is_const: | |
return Const(lhs.data + rhs.data) | |
elif lhs_is_sum: | |
if rhs_is_const and isinstance(lhs.components[-1], Const): | |
last = Const(lhs.components[-1].data + rhs.data) | |
return Sum(lhs.components[:-1] + [last]) | |
else: | |
return Sum(lhs.components + [rhs]) | |
elif rhs_is_sum: | |
if lhs_is_const and isinstance(rhs.components[0], Const): | |
first = Const(lhs.data + rhs.components[0].data) | |
return Sum([first] + rhs.components[1:]) | |
else: | |
return Sum([lhs] + rhs.components) | |
elif lhs_is_sum and rhs_is_sum: | |
return Sum(lhs.components + rhs.components) | |
else: | |
return Sum([lhs, rhs]) | |
def main(): | |
from itertools import product | |
test_measures = [u'Среднее', u'Сумма'] | |
uses = [u'Good Use', u'Deep Use', u'Good или Deep Use', u'Всего'] | |
event_types = [u'Сессии{g=pl}', u'Хиты{g=pl}', u'Пользователи{g=pl}', u'Хитов{g=pl} на сессию', u'Всего'] | |
counters = [u'Поисковый запрос'] | |
oss = [u'Windows', u'iOS', u'Android'] | |
test_measure_mutator = lambda _, t, a, b: (u'Доля', a, b) if t == u'Среднее' else (t, a, b) | |
untotal = lambda _, t, a, b: (u'', False, False) if t == u'Всего' else (t, a, b) | |
context = Context() | |
context.inflector = Inflector('ru') | |
context.dimensions = ('test_measure', 'event_type', 'counter', 'counter_os', 'use_type') | |
# ======== TEMPLATE DEFINITION ======== | |
combiner = IfAll( | |
Dim('test_measure').mutate(test_measure_mutator).lower() + | |
u' ' + | |
Dim('event_type').mutate(untotal).inflect('gen').lower() + | |
u' для ' + | |
Dim('counter').inflect('gen').lower(), | |
Dim('counter').lower() | |
) | |
combiner += IfAll(u' с ' + Dim('use_type').mutate(untotal)) | |
combiner += IfAll(u', ' + Slice('counter_')) | |
combiner = combiner.clause() | |
# ====== END TEMPLATE DEFINITION ====== | |
for test_measure, use, event_type, counter, os in product(test_measures, uses, event_types, counters, oss): | |
context.data = dict( | |
test_measure=Meta('', test_measure), | |
use_type=Meta('', use), | |
event_type=Meta('', event_type), | |
counter=Meta('', counter), | |
counter_os=Meta('', os), | |
) | |
print(combiner.eval(context)[0]) | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment