Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Django внутри Django: framework для чатботов

Django внутри Django: framework для чатботов

Михаил Новиков (Fasttrack, Тимлид) @ Moscow Python Meetup 65

"С одной стороны — это доклад о том, как построить low-code платформу на базе Django Template Language, сделать ее безопасной и дать пользователям описывать бизнес-процессы на языке джанго-шаблонов.

С другой — это рассказ для продвинутых (и не очень) разработчиков, как можно извратить джангу и пользоваться абсолютно всеми ее компонентами не по назначению☺ мы переписали роутер, urlconf, middleware, написали свою систему вьюх, свой template engine на базе родного джанговского и тд.

Полезное для слушателей — на примере нашего "Django внутри Django" я расскажу о кишках самой джанги, об интересных паттернах, которые в ней применяются (например, миддлвары, которые начиная с версии 2 сделаны в функциональном стиле), об инсайтах по оптимизации (например, оказывается, резолв по урезанной части urlconf’а примерно в 10 раз быстрее резолва по полному urlconf’у) и так далее".

Видео: http://www.moscowpython.ru/meetup/65/django-in-django/

Moscow Python Meetup

June 27, 2019
Tweet

More Decks by Moscow Python Meetup

Other Decks in Programming

Transcript

  1. Django внутри Django
    Фреймворк для чатботов
    Михаил Новиков
    Founder & Dev Lead, Fasttrack
    [email protected]
    [email protected] 1

    View full-size slide

  2. Fasttrack
    Bot as a service
    fstrk.io
    [email protected] 2

    View full-size slide

  3. История
    • 2016 год. В чатботов додумались
    запихнуть кнопки. Начинается
    хайп
    [email protected] 4

    View full-size slide

  4. История
    • 2016 год. В чатботов додумались
    запихнуть кнопки. Начинается
    хайп
    • FB выкатывает Messenger API
    [email protected] 4

    View full-size slide

  5. История
    • 2016 год. В чатботов додумались
    запихнуть кнопки. Начинается
    хайп
    • FB выкатывает Messenger API
    • Viber выкатывает Public Account
    API
    [email protected] 4

    View full-size slide

  6. История
    • 2016 год. В чатботов додумались
    запихнуть кнопки. Начинается
    хайп
    • FB выкатывает Messenger API
    • Viber выкатывает Public Account
    API
    • ... и мы пилим свой
    конструктор
    [email protected] 4

    View full-size slide

  7. Как устроены чатботы в туториалах
    [email protected] 5

    View full-size slide

  8. Как устроены чатботы в реальной жизни
    [email protected] 6

    View full-size slide

  9. 2016: простой конструктор
    [email protected] 7

    View full-size slide

  10. 2017: сложный конструктор
    [email protected] 8

    View full-size slide

  11. 2018: очень сложный конструктор
    [email protected] 9

    View full-size slide

  12. Как организовать выполнение команд?
    [email protected] 10

    View full-size slide

  13. Как организовать выполнение команд?
    Договориться о формате команд
    • Что мы зашиваем в кнопки
    пользователю?
    Придумать, как их
    маршрутизировать
    • Когда эти команды прилетают к
    нам: как их распознавать?
    [email protected] 11

    View full-size slide

  14. Как организовать выполнение команд?
    # вьюха DRF
    def receive_webhook(request, bot_uuid):
    chat_id = request.data['chat_id']
    payload = request.data['payload']
    # вход в логику
    def do_logic(bot_uuid, chat_id, payload):
    ...
    [email protected] 12

    View full-size slide

  15. Как организовать выполнение команд?
    # Может быть, так?
    # высылаем пейлоады типа "get_node_10",
    # "answer_question_15", "choose_operator_3"
    if payload.startswith('get_node'):
    return get_node(payload)
    elif payload.startswith('answer_question'):
    return answer_question(payload)
    elif ...
    # еще 100 условий
    [email protected] 13

    View full-size slide

  16. Что если...
    def do_logic(bot_uuid, chat_id, payload):
    ...
    payload логика
    /get_node/15/ get_node(15)
    /answer_question/12/ answer_question(12)
    [email protected] 14

    View full-size slide

  17. Что если...
    urlpatterns = [
    url(r'^get_node/(?P.+)/',
    utils.get_node),
    url(r'^answer_question/(?P.+)/',
    utils.answer_question),
    ]
    [email protected] 15

    View full-size slide

  18. Что если...
    from django.urls import resolve
    # вьюха DRF
    def receive_webhook(request, bot_uuid):
    chat_id = request.data['chat_id']
    payload = request.data['payload'] # /get_node/15/
    match = resolve(payload) # превращаем строчку в функцию с параметрами
    if match:
    view = match.view # apps.routing.views.get_node
    args = match.args # ()
    kwargs = match.kwargs # {"node_pk": 15}
    return view(*args, **kwargs)
    [email protected] 16

    View full-size slide

  19. Вложенный URLconf
    [email protected] 17

    View full-size slide

  20. Вложенный django
    [email protected] 18

    View full-size slide

  21. Regular views vs. nested views
    • Обычные вьюхи: резолвятся обычным urlconf, принимают Request и
    возвращают Response.
    • Вложенные вьюхи: резолвятся вложенным urlconf, принимают BotRequest и
    возвращают BotResponse.
    def get_node(botrequest, node_id):
    node = Node.objects.get(pk=node_id)
    response = build_response(botrequest, node)
    return BotResponse(messages=response.messages)
    [email protected] 19

    View full-size slide

  22. Инсайт: скорость urlconf
    Если у вас очень много урлов, то resolve по подмножеству
    urlconf намного дешевле, чем по всему urlconf
    resolve('/get_node/15/')
    # 1000 runs: 2.3576704149367 s
    resolve('/get_node/15/', urlconf='apps.routing.urls')
    # 1000 runs: 0.0216069859853 s
    [email protected] 20

    View full-size slide

  23. Инсайт: скорость urlconf
    То же самое с reverse и с template-тегом {% url %}.
    {% for obj in objects %}

    {% endfor %}
    Если объектов objects много - это замедлит вам
    рендер шаблона
    [email protected] 21

    View full-size slide

  24. Что мы говорим
    клиентам
    [email protected] 22

    View full-size slide

  25. Что мы говорим
    клиентам
    • "Чатбот за 5 минут!"
    [email protected] 22

    View full-size slide

  26. Что мы говорим
    клиентам
    • "Чатбот за 5 минут!"
    • "Без навыков
    программирования!"
    [email protected] 22

    View full-size slide

  27. Что мы говорим
    клиентам
    • "Чатбот за 5 минут!"
    • "Без навыков
    программирования!"
    • "Визуальный конструктор!"
    [email protected] 22

    View full-size slide

  28. Что мы говорим
    клиентам
    • "Чатбот за 5 минут!"
    • "Без навыков
    программирования!"
    • "Визуальный конструктор!"
    • ...это не совсем так.
    [email protected] 23

    View full-size slide

  29. Как отправить сообщение в ФБ
    {
    "payload": {
    "text": "Выберите свою пиццу",
    "buttons": [
    {
    "type": "postback",
    "data": "pizza1",
    "title": "Маргарита"
    },
    {
    "type": "postback",
    "data": "pizza2",
    "title": "Четыре сыра"
    }
    ]
    }
    }
    [email protected] 24

    View full-size slide

  30. Шаблонизация!
    {
    "payload": {
    "text": "Выберите свою пиццу",
    "buttons": [
    {% for item in api_response %},
    {
    "type": "postback",
    "data": "{{ item.id }}",
    "title": "{{ item.name }}"
    },
    {% endfor %}
    ]
    }
    }
    [email protected] 25

    View full-size slide

  31. Теперь у нас еще и свой шаблонизатор
    [email protected] 26

    View full-size slide

  32. Безопасность
    [email protected] 27

    View full-size slide

  33. Безопасность
    • Что если юзер вызовет тег {% debug %}?
    [email protected] 27

    View full-size slide

  34. Безопасность
    • Что если юзер вызовет тег {% debug %}?
    • Что если юзер сделает {% url %}?
    [email protected] 27

    View full-size slide

  35. Безопасность
    • Что если юзер вызовет тег {% debug %}?
    • Что если юзер сделает {% url %}?
    • Запретить!
    [email protected] 27

    View full-size slide

  36. Безопасность
    # Шаблон нормального человека
    template = Template(string)
    rendered = template.render(context)
    # Шаблон курильщика
    class SafeEngine(Engine):
    default_builtins = [
    ... # переопределяем дефолтные теги
    ]
    template = Template(string, engine=engine)
    rendered = template.render(context)
    [email protected] 28

    View full-size slide

  37. Безопасность
    # Шаблон нормального человека
    template = Template(string)
    rendered = template.render(context)
    # Шаблон курильщика
    class SafeEngine(Engine):
    default_builtins = [
    ... # переопределяем дефолтные теги
    ]
    template = Template(string, engine=engine)
    rendered = template.render(context)
    [email protected] 29

    View full-size slide

  38. Как настраивать
    бизнес-логику?
    [email protected] 30

    View full-size slide

  39. Как настраивать бизнес-логику?
    [email protected] 31

    View full-size slide

  40. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    [email protected] 31

    View full-size slide

  41. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    [email protected] 31

    View full-size slide

  42. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    [email protected] 31

    View full-size slide

  43. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    • Переключиться на др.блок
    [email protected] 31

    View full-size slide

  44. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    • Переключиться на др.блок
    • Отправить сообщение
    [email protected] 31

    View full-size slide

  45. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    • Переключиться на др.блок
    • Отправить сообщение
    • Отправить email
    [email protected] 31

    View full-size slide

  46. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    • Переключиться на др.блок
    • Отправить сообщение
    • Отправить email
    • ...
    [email protected] 31

    View full-size slide

  47. Как настраивать бизнес-логику?
    • Нужен способ писать логику на простом мета-языке
    • Язык должен быть безопасным (rm -rf)
    • Язык должен выполнять типовые команды
    • Переключиться на др.блок
    • Отправить сообщение
    • Отправить email
    • ...
    • Язык должен быть легко тестируемым и расширяемым
    [email protected] 31

    View full-size slide

  48. Что если...
    {% for item in objects %}
    {% if item.expired %}
    Срок истек
    {% else %}
    Срок: {% now %}
    {% endif %}
    {% endfor %}
    [email protected] 32

    View full-size slide

  49. Template tags
    1. Собирается дерево нодов
    {% for item in objects %}
    {% if item.expired %}
    Срок истек
    {% else %}
    Срок: {% now %}
    {% endif %}
    {% endfor %}
    [email protected] 33

    View full-size slide

  50. Template tags
    2. Выполняется рекурсивный render()
    {% for item in objects %}
    {% if item.expired %}
    Срок истек
    {% else %}
    Срок: {% now %}
    {% endif %}
    {% endfor %}
    [email protected] 34

    View full-size slide

  51. Что если...
    Если мы засунем действия бота в
    render(), то мы дадим
    возможность юзерам писать
    логику на DTL!
    [email protected] 35

    View full-size slide

  52. Бизнес-логика на DTL
    {% if request.content == 'Супер пароль' %}
    {% send_message "Пароль верен!" %}
    {% send_email user.email
    subject="Сабж"
    body="Важный контент" %}
    {% else %}
    {% send_message "Пароль неверен!" %}
    {% switch_to "Ввод пароля" %}
    {% endif %}
    [email protected] 36

    View full-size slide

  53. Получилась low-
    code система
    • Юзеры настраивают граф
    переходов со статикой
    • Сложная логика пишется на DTL
    [email protected] 37

    View full-size slide

  54. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    [email protected] 39

    View full-size slide

  55. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    • Укорачиваем ссылки в
    сообщениях
    [email protected] 39

    View full-size slide

  56. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    • Укорачиваем ссылки в
    сообщениях
    • Все PNG преобразуем в JPG
    [email protected] 39

    View full-size slide

  57. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    • Укорачиваем ссылки в
    сообщениях
    • Все PNG преобразуем в JPG
    • Ограничиваем время ответа бота
    [email protected] 39

    View full-size slide

  58. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    • Укорачиваем ссылки в
    сообщениях
    • Все PNG преобразуем в JPG
    • Ограничиваем время ответа бота
    • Логируем вопросы и ответы
    [email protected] 39

    View full-size slide

  59. Каждый цикл "вопрос-
    ответ" требует
    препроцессинга
    • Укорачиваем ссылки в
    сообщениях
    • Все PNG преобразуем в JPG
    • Ограничиваем время ответа бота
    • Логируем вопросы и ответы
    • ...
    [email protected] 39

    View full-size slide

  60. Что если...
    NESTED_MIDDLEWARE = [
    'routing.LinkShortenMiddleware',
    'routing.PngToJpgMiddleware',
    'routing.LimitResponseTimeMiddleware',
    'routing.LogMessagesMiddleware',
    ]
    [email protected] 40

    View full-size slide

  61. Фунциональный Middleware в Django
    def some_middleware(next_handler):
    # функция создает и возвращает мидлвар
    def handler(request):
    # код до след. мидлваров
    response = next_handler(request)
    # код после след.мидлваров
    return response
    return handler
    # как использовать: формируем цепочку мидлваров
    middleware_chain = middleware1(middleware2(middleware3))
    # оборачиваем конечную вьюху в мидлвар
    wrapped_get_node = middleware_chain(get_node)
    # вызываем ее
    response = wrapped_get_node(node)
    [email protected] 41

    View full-size slide

  62. Фунциональный Middleware в Django
    def some_middleware(next_handler):
    # функция создает и возвращает мидлвар
    def handler(request):
    # код до след. мидлваров
    response = next_handler(request)
    # код после след.мидлваров
    return response
    return handler
    # как использовать: формируем цепочку мидлваров
    middleware_chain = middleware1(middleware2(middleware3))
    # оборачиваем конечную вьюху в мидлвар
    wrapped_get_node = middleware_chain(get_node)
    # вызываем ее
    response = wrapped_get_node(node)
    [email protected] 42

    View full-size slide

  63. Примеры мидлваров
    1. Логируем запросы и ответы
    def logging_middleware(next_handler):
    def handler(request):
    log(request)
    response = next_handler(request)
    log(response)
    return response
    return handler
    [email protected] 43

    View full-size slide

  64. Примеры мидлваров
    2. Не пускаем неавторизованных юзеров
    def restrict_access_middleware(next_handler):
    def handler(request):
    if request.user.is_authenticated:
    # цепочка мидлваров продолжится,
    # только если юзер аутентифицирован
    response = next_handler(request)
    else:
    # иначе цепочка оборвется
    response = 'Access Denied'
    return response
    return handler
    [email protected] 44

    View full-size slide

  65. Теперь у нас есть вложенный Middleware
    [email protected] 45

    View full-size slide

  66. Ещё Django
    Достаем глубоко вложенный элемент словаря
    some_dict = {
    "a": {
    "b": [
    {"c": 15},
    {"d": 20},
    ]
    }
    }
    path = 'a.b.1.d'
    [email protected] 47

    View full-size slide

  67. Ещё Django
    Достаем глубоко вложенный элемент словаря
    from django.template import Variable
    some_dict = {
    "a": {
    "b": [
    {"c": 15},
    {"d": 20},
    ]
    }
    }
    path = 'a.b.1.d'
    result = Variable(path).resolve(some_dict)
    [email protected] 48

    View full-size slide

  68. Ещё Django
    Сервисные объекты
    class CreateBooking(Service):
    name = forms.CharField()
    email = forms.EmailField()
    def process(self):
    name = self.cleaned_data['name']
    email = self.cleaned_data['email']
    # ... логика
    CreateBooking.execute({'name': 'Mike', 'email': '[email protected]'})
    [email protected] 49

    View full-size slide

  69. Ещё Django
    Сервисные объекты
    class CreateBooking(Service):
    name = forms.CharField()
    email = forms.EmailField()
    def process(self):
    name = self.cleaned_data['name']
    email = self.cleaned_data['email']
    # ... логика
    CreateBooking.execute({'name': 'Mike', 'email': '[email protected]'})
    [email protected] 50

    View full-size slide

  70. Теперь поиздеваемся над Celery
    Ловим таски, созданные другими приложениями
    # custom_consumer.py
    from celery.worker import consumer
    class Consumer(consumer.Consumer):
    def on_unknown_message(self, body, message):
    try:
    data = json.loads(body)
    do_stuff(data)
    finally:
    message.ack()
    # ------------------------
    # app.py
    app = Celery('my_app')
    app.conf.worker_consumer = 'custom_consumer.Consumer'
    [email protected] 51

    View full-size slide

  71. Теперь поиздеваемся над Celery
    Ловим таски, созданные другими приложениями
    # custom_consumer.py
    from celery.worker import consumer
    class Consumer(consumer.Consumer):
    def on_unknown_message(self, body, message):
    try:
    data = json.loads(body)
    do_stuff(data)
    finally:
    message.ack()
    # ------------------------
    # app.py
    app = Celery('my_app')
    app.conf.worker_consumer = 'custom_consumer.Consumer'
    [email protected] 52

    View full-size slide

  72. Резюме
    • Django полон интересных паттернов
    • Они не только для веб-разработки
    • Иногда полезно заглядывать под капот
    [email protected] 53

    View full-size slide

  73. Спасибо!
    Михаил Новиков
    Founder & Dev Lead, Fasttrack
    [email protected]
    [email protected] 54

    View full-size slide