Под капотом autofocus.su

от автора

Привет. Сегодня хочу рассказать про то, как за кулисами устроена работа моего мини-проекта по ведению задач autofocus.su. В предыдущей заметке я рассказал про принципы, лежащие в основе метода Автофокуса. А тут будет скорее набор ключевых слов с короткими описаниями того, что и как связано между собой. Конкретная реализация будет отличаться в вашем конкретном случае, но направления для поисков будут понятны.

Лично мне часто не хватает какого-то скелета работоспособного приложения, чтобы было с чего начать. Надеюсь, что буду полезен.

Начнем с бэкенда.

Django

В основе проекта — Django. Конструктор для космических звездолетов. Очень много вдохновения по настройке и нюансам можно найти в статьях @kesn Спасибо ему за это!

В Джанго у меня сейчас три приложения — для задач, пользователей и создания коротких ссылок. Пользователя я переопределил своим классом User в настройках:

AUTH_USER_MODEL = 'accounts.User'

Помимо прочего, я сделал основным ключом адрес электропочты и убрал обязательные поля:

email = models.EmailField(primary_key=True) password = None REQUIRED_FIELDS = [] USERNAME_FIELD = 'email'

Этот класс я использую вместе с авторизацией через Django-sesame.

django-sesame

Авторизация в Django с помощью волшебных ссылок. Просто добавляете в проект, делаете пяток настроек и можете присылать посетителям ссылки, перейдя по которым они смогут авторизоваться:

https://example.com/sesame/login/?sesame=zxST9d0XT9xgfYLvoa9e2myN

Вид ссылки, срок действия и другие нюансы можно настроить. После авторизации помечаю пользователей, как подтвердивших свой адрес почты.

dotenv

Библиотека для python, которая читает файл .env и использует найденные значения в качестве переменных среды. А вы просто добавляете этот файл в .gitignore и в продакшене используете настоящие переменные среды. Решает кучу головной боли при переходе от разработки к развертыванию. В settings.py пишем:

import dotenv  dotenv_file = BASE_DIR / ".env" if os.path.isfile(dotenv_file):     dotenv.load_dotenv(dotenv_file)

А дальше используем переменные среды как обычно:

DEBUG = os.environ.get('DJANGO_DEBUG', default=False) in [     'True', 'true', '1', True]

Если добавить в .env строку

DJANGO_DEBUG=1

То DEBUG в настройка будет равен True

graphene-django

Библиотека для создания АПИ на основе языка запросов GraphQL. Его придумали в Facebook (организации, запрещенной на территории России). Он  позволяет делать запросы к АПИ на одну точку доступа и произвольно указывать, какие данные нужны, учитывая вложенные объекты. Например, можно сделать такой запрос:

query UserItems     {         user {             pages             page             email             isValidated         }         items {             text             id             repeats             state         }     }

И в ответ придет объект User и массив Items с задачками. А обновляются данные с помощью мутаций:

mutation addItem($text: String!){     createItem(text: $text){         user {            email         }     }   }

Конечно, это не работает прям с двух строк. Вам нужно будет указать, какие объекты может отдавать ваша точка graphql и как корректно применять мутации. Сперва меня graphql немного пугал, а потом распробовал.

Переходим к фронту.

Vue.js

В качестве фреймворка для клиентской части выбрал Vue.js потому что почему бы и нет. Сделал версию из коробки с Typescript. Находится в папке frontend рядом с приложениями Django:

Из важных настроек указал разные папки для production и development:

outputDir: process.env.NODE_ENV === "production" ? "dist" : "static"

И то, куда, собственно складывать собранные файлы:

configureWebpack: {     output: {       filename:         process.env.NODE_ENV === "production"           ? "../../static/js/[name].js"           : "static/js/[name].js",       chunkFilename:         process.env.NODE_ENV === "production"           ? "../../static/js/[name].js"           : "static/js/[name].js",     },     plugins: [       new WriteFilePlugin(),       process.env.NODE_ENV === "production"         ? new BundleTracker({             filename: "webpack-stats-prod.json",             publicPath: "/",           })         : new BundleTracker({             filename: "webpack-stats.json",             publicPath: "http://localhost:8080/",           }),     ],   },

А для того, чтобы подружить все это с Django понадобился:

webpack_loader

Плагин от Jazzband, который позволяет использовать бандлы Webpack в шаблонах:

{% extends "base.html" %}  {% load render_bundle from webpack_loader %}  {% block title %}   Autofocus {% endblock title %}  {% block head %}   {{ block.super }} {% endblock head %}      {% block body %}   <div id="app"></div> {% endblock body %}  {% block script %}   {% render_bundle 'app' %} {% endblock script %}

Vue Apollo

Для того, чтобы подружить Vue.JS с GraphQL есть библиотека Vue Apollo. Подключаем при инициализации приложения и не забываем в настройках подключения прокинуть x-csrftoken. Его предварительно вытаскиваем из кук при помощи пакет js-cookie:

import { createApp, provide, h } from "vue"; import App from "./App.vue"; import "./registerServiceWorker"; import { DefaultApolloClient } from "@vue/apollo-composable"; import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client/core"; import Cookies from "js-cookie";  const cache = new InMemoryCache();  const link = createHttpLink({     uri: "/graphql",     headers: {         "x-csrftoken": Cookies.get("csrftoken")     } });  const apolloClient = new ApolloClient({     cache,     link: link, });  const app = createApp({     setup() {         provide(DefaultApolloClient, apolloClient);     },      render: () => h(App), });  app.mount("#app");

Tailwind CSS

Для css использую Tailwind css. Его прелесть в том, что, во-первых, там есть хорошо продуманная и связанная система классов. Во-вторых, вы можете подключить их генератор и тогда нужно будет загружать не всю библиотеку, а только те классы, которые используются. Например вы можете указать такой код:

.page__current {     @apply bg-slate-800 dark:bg-slate-400;     @apply text-white dark:text-slate-700;     @apply rounded-full;     @apply cursor-default;   }

А на выходе получится:

.page__current {   --tw-bg-opacity: 1;   background-color: rgb(30 41 59 / var(--tw-bg-opacity)); }  @media (prefers-color-scheme: dark) {   .page__current {     --tw-bg-opacity: 1;     background-color: rgb(148 163 184 / var(--tw-bg-opacity));   } }  .page__current {   --tw-text-opacity: 1;   color: rgb(255 255 255 / var(--tw-text-opacity)); }  @media (prefers-color-scheme: dark) {   .page__current {     --tw-text-opacity: 1;     color: rgb(51 65 85 / var(--tw-text-opacity));   } }  .page__current {   border-radius: 9999px;   cursor: default; }

При этом, если класс нигде не используется, то он не попадет в результирующий файл. Указать, где и какие файлы проверять на наличие классов можно в tailwind.config.js:

/** @type {import('tailwindcss').Config} */  module.exports = {   content: [     "./focus/**/*.{html,js}",     "./accounts/**/*.{html,js}",     "./frontend/src/**/*.{html,vue,js}"   ],   theme: {     extend: {},     fontFamily: {       sans: ["PT Sans", "sans-serif"],     },   },   plugins: [require("@tailwindcss/typography"), require("@tailwindcss/forms")], };

Вообще можно использовать Tailwind прямо во Vue.js, но пока не разбирался, как это делать. Хотя это было бы удобнее: можно было бы группировать стили вместе с компонентами.

Тестирование

Для юнит-тестов использую TestCase самой Django. Еще есть папка в проекте с названием functional_tests, в которой хранятся функциональные тесты, на основе Selenium+Chromedriver. В функциональных тестах покрыть весь функционал приложения. Плюс страницы ошибок. 

Пока не подступался к проверке авторизации после выкатки на сервер. Вроде есть Mailtrap, но руки пока не дошли. При тестировании локально — подменяю почтовый клиент на console.EmailBackend, который печатает почту прямо в консоль:

if DEBUG:     EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" else:     EMAIL_USE_TLS = True     EMAIL_HOST = ‘***’     EMAIL_PORT = 587     EMAIL_HOST_USER = ‘***’     EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', '')     DEFAULT_FROM_EMAIL = ‘***’

И перехватываю ее с помощью django.core.mail. Там можно посмотреть отправленные письма и прочитать их содержимое:

# А в почтовом ящике появляется письмо self.assertEqual(len(mail.outbox), 1)  # В письме есть ссылка на вход email_text = mail.outbox[0].body link = re.search(r'(http.*)$', email_text, re.MULTILINE).group(1) self.assertIsNotNone(link)

Vue.JS пока никак не тестирую.

Dock

Для развертывания на сервере я использую Dokku. Это опенсорсный аналог Heroku — облачной системы контейнеризации. Звучит сложно, а на деле вы просто пушите ветку по адресу вашего приложения и оно само там все развертывается. В первый раз я с этим подходом столкнулся в статье @ohldМасштабируемый Продакшн-реди Телеграм бот на Django.

Для того, чтобы развернуть проект Django в Dokku нужно добавить файлы в корень проекта:
.buildpacks— указывает, какой сборщик проекта использовать:

https://github.com/heroku/heroku-buildpack-python.git#v222

App.json — можно указать различные настройки. Например, cron:

{   "formation": {     "web": {       "quantity": 1     }   },   "cron": [     {       "command": "python manage.py clean_users",       "schedule": "@daily"     },     {       "command": "cd af_dbt && dbt deps && dbt run",       "schedule": "@daily"     }   ] }

Procfile — указываются процессы, которые запускаются после билда:

release: python manage.py migrate --noinput && python manage.py collectstatic --no-input web: gunicorn --bind :$PORT --workers 4 --worker-class uvicorn.workers.UvicornWorker autofocus.asgi:application

Runtime.txt — указываем необходимую версию Python:

python-3.10.8

После этого просто пушим изменения на сервер и все. Dokku шуршит и бесшовно выкатывает новую версию поверх старой.

Postrgres

Также вам понадобится какая-то база данных. Я выбрал Postgres и плагин к Dokku dokku postgres. С помощью команд create и link создаете базу данных и подключаете ее к вашему приложению. Также, вы можете сделать базу доступной снаружи контейнера при помощью команды expose. А еще можно настроить резервное копирование базы, например, в бакет в Яндекс.Облаке. Для этого нужно вызвать команду backup-auth. В качестве первого параметра указать название сервиса БД, а в качестве второго и третьего — id и key сервисного аккаунта, который имеет доступ к макету.

dokku postgres:backup-auth <service> \     <aws-access-key-id> <aws-secret-access-key> \     ru-central1 s3v4 https://storage.yandexcloud.net

После чего настроить расписание резервного копирования с помощью команды backup-schedule. После этого в бакете начнут появляться бэкапы:

dj_database_url

Расширение для Django, которое ищет переменную окружения под названием DATABASE_URL и, если она есть, создает подключение на ее основе. В коде Django указываете:

DATABASES = {     'default': dj_database_url.config(conn_max_age=600,                                       default="sqlite:///db.sqlite3"), }

И тогда на локальном компьютере будет работать база SQLLite, а на сервере, где указана переменная вида:

DATABASE_URL=postgres://user:p#ssword!@localhost/foobar

Автоматически подключится к Postgres. А dokku-postgres ее как раз и устанавливает, при создании контейнера с базой.

dokku-letsencrypt

Плагин, который занимается созданием и обновлением сертификатов для доменов. Указываете имя приложения и домены, а все остальное плагин сделает сам.

Whitenoise

Модуль, который решает проблему отдачи статических файлов в Django. Сжимает их, проставляет правильные заголовки, поддерживает работу с CDN. Отдача статических файлов в Джанго для меня до сих пор пляска с бубном сейчас работает в следующем режиме. В Докку проброшена папка staticfiles между контейнером с приложением и папкой на сервере с помощью команды storage. В настройках Джанго так:

STATICFILES_DIRS = (     os.path.join(BASE_DIR, 'static'), )  STATIC_URL = 'static/'  if not DEBUG:     STATICFILES_STORAGE = ('whitenoise.storage.'                            'CompressedManifestStaticFilesStorage')     STATIC_ROOT = BASE_DIR / 'staticfiles'

Rollbar

Когда-то давно наткнулся на rollbar в совете бюро и с тех пор использую в своих проектах. Устанавливается двумя (ну ладно, семью) строчками кода. Позволяет в режиме реального времени выявлять ошибки на продакшене:

Django Management Command

В процессе работы для каждого нового пользователя создаются задачи для онбординга. Этих пользователей, с задачами, которые никогда не отмечали, становится много и надо время от времени избавляться от мертвых душ. Это сделано с помощью команды Django, которая запускается в Dokku по крону, описанному в app.json:

"cron": [     {       "command": "python manage.py clean_users",       "schedule": "@daily"     },     ...   ]

Развертывание

Сейчас развертывание организовано простым bash скриптом, который обновляет паки vuejs, css, запускает юнит-тесты и функциональные тесты и, если все хорошо, коммитит проект в Dokku. В докку есть два практически идентичных приложения autofocus-stg и autofocus. Сперва идет деплой в первый. Если функциональные тесты отрабатывают, то руками запускаю деплой в рабочую версию. 

Вообще dokku можно легко разворачивать с помощью GitHub Actions. Но руки пока не дошли. Это можно подсмотреть в описании телеграмм-бота.

Аналитика

Для подсчета продуктовой аналитики использую dbt с адаптером Postgres. С его помощью можно быстро сделать работающий конвеер, обрабатывающий ваши данные с помощью цепочек SQL-запросов. Dbt тоже запускается каждый день по расписанию. А данные собираются в схему dbt в базе данных:

А потом из этой таблицы я забираю данные в DataLens для визуализации:

Продолжение следует

На данном этапе это примерно все, что работает внутри. Если есть какие-то вопросы — буду рад ответить. Если есть замечания, как лучше сделать — буду рад выслушать. ? Спасибо, что дочитали.

Всем мир ♥️


ссылка на оригинал статьи https://habr.com/ru/post/704672/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *