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
PRO

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
  2. Fasttrack Bot as a service fstrk.io [email protected] 2

  3. История [email protected] 3

  4. История [email protected] 4

  5. История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается

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

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

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

    хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API • ... и мы пилим свой конструктор [email protected] 4
  9. Как устроены чатботы в туториалах [email protected] 5

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

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

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

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

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

  15. Как организовать выполнение команд? Договориться о формате команд • Что

    мы зашиваем в кнопки пользователю? Придумать, как их маршрутизировать • Когда эти команды прилетают к нам: как их распознавать? [email protected] 11
  16. Как организовать выполнение команд? # вьюха 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
  17. Как организовать выполнение команд? # Может быть, так? # высылаем

    пейлоады типа "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
  18. Что если... def do_logic(bot_uuid, chat_id, payload): ... payload логика /get_node/15/

    get_node(15) /answer_question/12/ answer_question(12) [email protected] 14
  19. Что если... urlpatterns = [ url(r'^get_node/(?P<node_pk>.+)/', utils.get_node), url(r'^answer_question/(?P<question_pk>.+)/', utils.answer_question), ]

    [email protected] 15
  20. Что если... 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
  21. Вложенный URLconf [email protected] 17

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

  23. 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
  24. Инсайт: скорость 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
  25. Инсайт: скорость urlconf То же самое с reverse и с

    template-тегом {% url %}. {% for obj in objects %} <a href="{% url 'some_view' %}"></a> {% endfor %} Если объектов objects много - это замедлит вам рендер шаблона [email protected] 21
  26. Что мы говорим клиентам [email protected] 22

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

    22
  28. Что мы говорим клиентам • "Чатбот за 5 минут!" •

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

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

    "Без навыков программирования!" • "Визуальный конструктор!" • ...это не совсем так. [email protected] 23
  31. Как отправить сообщение в ФБ { "payload": { "text": "Выберите

    свою пиццу", "buttons": [ { "type": "postback", "data": "pizza1", "title": "Маргарита" }, { "type": "postback", "data": "pizza2", "title": "Четыре сыра" } ] } } [email protected] 24
  32. Шаблонизация! { "payload": { "text": "Выберите свою пиццу", "buttons": [

    {% for item in api_response %}, { "type": "postback", "data": "{{ item.id }}", "title": "{{ item.name }}" }, {% endfor %} ] } } [email protected] 25
  33. Теперь у нас еще и свой шаблонизатор [email protected] 26

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

  35. Безопасность • Что если юзер вызовет тег {% debug %}?

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

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

    • Что если юзер сделает {% url %}? • Запретить! [email protected] 27
  38. Безопасность # Шаблон нормального человека template = Template(string) rendered =

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

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

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

  42. Как настраивать бизнес-логику? • Нужен способ писать логику на простом

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

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

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

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

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

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

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email • ... • Язык должен быть легко тестируемым и расширяемым [email protected] 31
  50. Что если... {% for item in objects %} {% if

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

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

    objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} [email protected] 34
  53. Что если... Если мы засунем действия бота в render(), то

    мы дадим возможность юзерам писать логику на DTL! [email protected] 35
  54. Бизнес-логика на DTL {% if request.content == 'Супер пароль' %}

    {% send_message "Пароль верен!" %} {% send_email user.email subject="Сабж" body="Важный контент" %} {% else %} {% send_message "Пароль неверен!" %} {% switch_to "Ввод пароля" %} {% endif %} [email protected] 36
  55. Получилась low- code система • Юзеры настраивают граф переходов со

    статикой • Сложная логика пишется на DTL [email protected] 37
  56. +1 Template Engine [email protected] 38

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

  58. Каждый цикл "вопрос- ответ" требует препроцессинга • Укорачиваем ссылки в

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

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

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

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

    сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы • ... [email protected] 39
  63. Что если... NESTED_MIDDLEWARE = [ 'routing.LinkShortenMiddleware', 'routing.PngToJpgMiddleware', 'routing.LimitResponseTimeMiddleware', 'routing.LogMessagesMiddleware', ]

    [email protected] 40
  64. Фунциональный 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
  65. Фунциональный 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
  66. Примеры мидлваров 1. Логируем запросы и ответы def logging_middleware(next_handler): def

    handler(request): log(request) response = next_handler(request) log(response) return response return handler [email protected] 43
  67. Примеры мидлваров 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
  68. Теперь у нас есть вложенный Middleware [email protected] 45

  69. [email protected] 46

  70. Ещё Django Достаем глубоко вложенный элемент словаря some_dict = {

    "a": { "b": [ {"c": 15}, {"d": 20}, ] } } path = 'a.b.1.d' [email protected] 47
  71. Ещё 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
  72. Ещё 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
  73. Ещё 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
  74. Теперь поиздеваемся над 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
  75. Теперь поиздеваемся над 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
  76. Резюме • Django полон интересных паттернов • Они не только

    для веб-разработки • Иногда полезно заглядывать под капот [email protected] 53
  77. Спасибо! Михаил Новиков Founder & Dev Lead, Fasttrack [email protected] [email protected]

    54