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/

53b0434aded1fb944ec3037c382158c1?s=128

Moscow Python Meetup

June 27, 2019
Tweet

Transcript

  1. 7.

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

    хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API mn@fstrk.io 4
  2. 8.

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

    хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API • ... и мы пилим свой конструктор mn@fstrk.io 4
  3. 15.

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

    мы зашиваем в кнопки пользователю? Придумать, как их маршрутизировать • Когда эти команды прилетают к нам: как их распознавать? mn@fstrk.io 11
  4. 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): ... mn@fstrk.io 12
  5. 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 условий mn@fstrk.io 13
  6. 18.

    Что если... def do_logic(bot_uuid, chat_id, payload): ... payload логика /get_node/15/

    get_node(15) /answer_question/12/ answer_question(12) mn@fstrk.io 14
  7. 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) mn@fstrk.io 16
  8. 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) mn@fstrk.io 19
  9. 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 mn@fstrk.io 20
  10. 25.

    Инсайт: скорость urlconf То же самое с reverse и с

    template-тегом {% url %}. {% for obj in objects %} <a href="{% url 'some_view' %}"></a> {% endfor %} Если объектов objects много - это замедлит вам рендер шаблона mn@fstrk.io 21
  11. 28.

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

    "Без навыков программирования!" mn@fstrk.io 22
  12. 29.

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

    "Без навыков программирования!" • "Визуальный конструктор!" mn@fstrk.io 22
  13. 30.

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

    "Без навыков программирования!" • "Визуальный конструктор!" • ...это не совсем так. mn@fstrk.io 23
  14. 31.

    Как отправить сообщение в ФБ { "payload": { "text": "Выберите

    свою пиццу", "buttons": [ { "type": "postback", "data": "pizza1", "title": "Маргарита" }, { "type": "postback", "data": "pizza2", "title": "Четыре сыра" } ] } } mn@fstrk.io 24
  15. 32.

    Шаблонизация! { "payload": { "text": "Выберите свою пиццу", "buttons": [

    {% for item in api_response %}, { "type": "postback", "data": "{{ item.id }}", "title": "{{ item.name }}" }, {% endfor %} ] } } mn@fstrk.io 25
  16. 36.

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

    • Что если юзер сделает {% url %}? mn@fstrk.io 27
  17. 37.

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

    • Что если юзер сделает {% url %}? • Запретить! mn@fstrk.io 27
  18. 38.

    Безопасность # Шаблон нормального человека template = Template(string) rendered =

    template.render(context) # Шаблон курильщика class SafeEngine(Engine): default_builtins = [ ... # переопределяем дефолтные теги ] template = Template(string, engine=engine) rendered = template.render(context) mn@fstrk.io 28
  19. 39.

    Безопасность # Шаблон нормального человека template = Template(string) rendered =

    template.render(context) # Шаблон курильщика class SafeEngine(Engine): default_builtins = [ ... # переопределяем дефолтные теги ] template = Template(string, engine=engine) rendered = template.render(context) mn@fstrk.io 29
  20. 43.

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

    мета-языке • Язык должен быть безопасным (rm -rf) mn@fstrk.io 31
  21. 44.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды mn@fstrk.io 31
  22. 45.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок mn@fstrk.io 31
  23. 46.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение mn@fstrk.io 31
  24. 47.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email mn@fstrk.io 31
  25. 48.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email • ... mn@fstrk.io 31
  26. 49.

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

    мета-языке • Язык должен быть безопасным (rm -rf) • Язык должен выполнять типовые команды • Переключиться на др.блок • Отправить сообщение • Отправить email • ... • Язык должен быть легко тестируемым и расширяемым mn@fstrk.io 31
  27. 50.

    Что если... {% for item in objects %} {% if

    item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} mn@fstrk.io 32
  28. 51.

    Template tags 1. Собирается дерево нодов {% for item in

    objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} mn@fstrk.io 33
  29. 52.

    Template tags 2. Выполняется рекурсивный render() {% for item in

    objects %} {% if item.expired %} Срок истек {% else %} Срок: {% now %} {% endif %} {% endfor %} mn@fstrk.io 34
  30. 53.

    Что если... Если мы засунем действия бота в render(), то

    мы дадим возможность юзерам писать логику на DTL! mn@fstrk.io 35
  31. 54.

    Бизнес-логика на DTL {% if request.content == 'Супер пароль' %}

    {% send_message "Пароль верен!" %} {% send_email user.email subject="Сабж" body="Важный контент" %} {% else %} {% send_message "Пароль неверен!" %} {% switch_to "Ввод пароля" %} {% endif %} mn@fstrk.io 36
  32. 55.

    Получилась low- code система • Юзеры настраивают граф переходов со

    статикой • Сложная логика пишется на DTL mn@fstrk.io 37
  33. 60.

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

    сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота mn@fstrk.io 39
  34. 61.

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

    сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы mn@fstrk.io 39
  35. 62.

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

    сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы • ... mn@fstrk.io 39
  36. 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) mn@fstrk.io 41
  37. 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) mn@fstrk.io 42
  38. 66.

    Примеры мидлваров 1. Логируем запросы и ответы def logging_middleware(next_handler): def

    handler(request): log(request) response = next_handler(request) log(response) return response return handler mn@fstrk.io 43
  39. 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 mn@fstrk.io 44
  40. 70.

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

    "a": { "b": [ {"c": 15}, {"d": 20}, ] } } path = 'a.b.1.d' mn@fstrk.io 47
  41. 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) mn@fstrk.io 48
  42. 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': 'mn@fstrk.io'}) mn@fstrk.io 49
  43. 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': 'mn@fstrk.io'}) mn@fstrk.io 50
  44. 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' mn@fstrk.io 51
  45. 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' mn@fstrk.io 52
  46. 76.

    Резюме • Django полон интересных паттернов • Они не только

    для веб-разработки • Иногда полезно заглядывать под капот mn@fstrk.io 53