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. История • 2016 год. В чатботов додумались запихнуть кнопки. Начинается

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

    хайп • FB выкатывает Messenger API • Viber выкатывает Public Account API • ... и мы пилим свой конструктор [email protected] 4
  3. Как организовать выполнение команд? Договориться о формате команд • Что

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

    пейлоады типа "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
  6. Что если... 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
  7. 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
  8. Инсайт: скорость 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
  9. Инсайт: скорость urlconf То же самое с reverse и с

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

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

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

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

    {% for item in api_response %}, { "type": "postback", "data": "{{ item.id }}", "title": "{{ item.name }}" }, {% endfor %} ] } } [email protected] 25
  14. Безопасность • Что если юзер вызовет тег {% debug %}?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    сообщениях • Все PNG преобразуем в JPG • Ограничиваем время ответа бота • Логируем вопросы и ответы • ... [email protected] 39
  31. Фунциональный 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
  32. Фунциональный 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
  33. Примеры мидлваров 1. Логируем запросы и ответы def logging_middleware(next_handler): def

    handler(request): log(request) response = next_handler(request) log(response) return response return handler [email protected] 43
  34. Примеры мидлваров 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
  35. Ещё 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
  36. Ещё 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
  37. Ещё 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
  38. Теперь поиздеваемся над 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
  39. Теперь поиздеваемся над 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
  40. Резюме • Django полон интересных паттернов • Они не только

    для веб-разработки • Иногда полезно заглядывать под капот [email protected] 53