Spense.app "под капотом". Код.

Пока Spense v0.2 в разработке, хочу рассказать о внутренней организации приложения с технической точки зрения. Статья эта в основном для (веб-)разработчиков, и язык у нее соответствующий, поэтому если вы читаете и ничего не понимаете, все в порядке, можно просто не читать.

В двух словах

Бэкенд на Django (Python), фронтенд на Django-шаблонах и Bootstrap, с щепоткой JavaScript и местами htmx (уже нет).

Почему так скучно?

Звучит совсем не хайпово, да. Но стоит помнить, что Spense в текущем его состоянии - это не полноценный продукт. Это скорее прототип, в котором мне часто нужно что-то менять и проверять идеи. Если идеи сработают, я выкину этот код и напишу другой, а если не сработают, просто оставлю на память.

Так что Django я выбрал не потому что я ее очень люблю (на самом деле я ее ненавижу), а потому что за последний год я успел к ней привыкнуть, и прототипировать приложение мне с ней получается легко и быстро.

По части архитектуры бэкенда хвалиться пока нечем, всё ровно так, как обычно делается в Django-проектах. Код разбит на три Django-аппы:

  • history - для моделей БД и кусочка логики про кошельки и транзакции;
  • tags - для моделей БД и кусочка логики вокруг тегов;
  • app - основные вьюхи с кучей логики внутри, HTML-шаблоны и статика.

Поначалу для меня это разделение имело смысл, но сейчас я скорее от этого страдаю, потому что до сих пор не могу запомнить, где какие модели лежат. Перемещать модели между Django-аппами - это отдельный геморрой, поэтому продолжаю страдать.

С фронтом все еще скучнее. Последний раз я писал более-менее сложный интерфейс на React в 2017 году, и после этого крайне редко касался современных JS фреймворков. Простого HTML мне хватало в большинстве случаев, и jQuery, как верный старый пёс, всегда приходил на помощь. Здесь я, правда, решил jQuery пока не вкатывать, а использовать вместо него "vanilla JS", но на практике выглядит всё похожим образом.

Например, вот кусочек про подгрузку "smart tags" - категорий расходов, которые Spense пытается угадать по введенной сумме:

{% block script %}
    <script>
        const $amount = document.getElementById('amount');
        const $wallet = document.getElementById('wallet');
        const $add = document.getElementById('add');
        const $listSmartTags = document.getElementsByClassName('smart-tags-list')[0];
        /* ... */

        $amount.addEventListener('keyup', function (e) {
            if (e.target.value) {
                $add.removeAttribute("disabled")
                updateSmartTags()
            } else {
                $add.setAttribute("disabled", "")
                $listSmartTags.innerHTML = '';
            }
        })

        /* ... */

        function updateSmartTags() {
            const walletId = $wallet.value;
            const amount = $amount.value;

            fetch(`{% url 'smart_tags' %}?wallet_id=${walletId}&amount=${amount}`)
                .then(res => res.json())
                .then(/* ... */)
            /* ... */
        }
    </script>
{% endblock %}
Тег {% url %} внутри JS-строки вперемешку с вклеенными JS-переменными - 🤌

Как видите, все довольно прямолинейно: скрипты я оставляю прямо в теге <script> на странице, и внутри там почти все как в jQuery, даже префикс $ имеется, чтобы отличать DOM-элементы от других идентификаторов.

Ну а по части Bootstrap - пока это для меня просто выбор по умолчанию. Там вроде Tailwind набирает обороты, но я пока не согласен к нему привыкать. Хоть в моем HTML тоже проскакивает class="row border-top border-black border-opacity-10 pt-1" или просто инлайновые стили в атрибуте style, я стараюсь выносить их в отдельный CSS-класс, если их становится много. Tailwind для таких случаев официально рекомендует multi-cursor editing, и я даже не знаю, как это комментировать.

Работа с кодом

На базовом уровне все тривиально: один git-репозиторий, которых хостится на GitHub внутри моей "организации".

В репозитории настроено несколько pre-commit-хуков, включая ruff --fix и ruff format (я почти везде переехал с flake8+black на ruff из-за того, что это очень шустрый швейцарский нож).

В самом начале, кстати, не было ни хуков, ни форматирования кода, писал и коммитил просто как есть. Типа, зачем мне какой-то кодстайл, если я это разрабатываю в одно лицо, и мне ни с кем не надо спорить об отступах и скобочках?

Но в прошлом году я понял, что это порождает некоторые трудности, и нужно хотя бы иметь базовую проверку кода и чистить неиспользуемые импорты. Ну а заодно и "причесывать" сам код, потому что против Black code style я вообще ничего не имею, и он мне даже нравится.

Так что в какой-то момент я всё отформатировал, починил, добавил хуки и воспользовался .git-blame-ignore-revs, чтобы git blame эти коммиты игнорировал.

Тесты

Для тестов я использую pytest + pytest-django, но тестов там сейчас очень мало. В основном получается так, что все фичи так или иначе связаны с интерфейсом, и я их просто руками протыкиваю по время разработки. А так как я еще и активный пользователь, то я автоматически становлюсь и ручным тестировщиком большинства фич. Конечно, код часто покрывает только happy path, с минимальной валидацией входных данных, но большего мне пока и не надо.

Зависимости

За Python-зависимости сейчас отвечает poetry. Сначала было просто pip install -r requirements.txt, пока зависимостей было штук 5, которые я два года не обновлял. Но довольно скоро стало понятно, что poetry+pyproject.toml гораздо приятнее и удобнее.

Для поддержания актуальности зависимостей я настроил бот Renovate, который находит новые версии вообще для всего, что у меня есть в репозитории. У Renovate есть как минимум два значимых отличия от dependabot:

  • он умеет собирать минорные апдейты в один pull request,
  • и он умеет сам его мёрджить (но не сразу, а когда у него есть настроение).

То есть в большинстве случаев от меня никаких действий вообще не требуется, разве что если CI на этом pull request ломается.

CHANGELOG.md

Я уже писал, что веду проект в GitHub Projects, но вдобавок я решил еще и записывать значимые изменения в CHANGELOG.md. Например, для версии 0.1 список изменений выглядит так:

Кстати, существует множество разных инструментов для ведения чейнджлогов. Я попробовал scriv, и процесс генерации чейнджлога мне сначала показался приятным.

Допустим, для следующей версии проекта запланирован десяток фич, которые разрабатываются в разных ветках, вливаются в master/main, потом в какой-то момент проект тегается новой версией, и создается релиз. Scriv позволяет создавать в процессе разработки отдельные файлы в каталоге changelod.d, по кусочку чейнджлога на каждую фичу. При этом каждый кусочек внутри себя может содержать и "Added", и "Removed", и "Fixed" секции. А когда нам нужно оформить релиз, мы запускаем scriv collect, который красиво сливает эти кусочки в один, берет номер новой версии из pyproject.toml и добавляет всё вместе к CHANGELOG.md. Так был сгенерирован список к Spense 0.1 на скриншоте выше.

У этого подхода есть большой плюс, когда несколько разработчиков параллельно работают в разных ветках, и merge-конфликты в чейнджлоге в этом случае исключены. Но после своего релиза я увидел для себя большой минус: из строчки CHANGELOG.md нельзя "прыгнуть" на коммит или pull request, в котором были реализованы указанные изменения, а можно только посмотреть на коммит с самим чейнджлогом:

git blame бессилен

Поэтому я решил просто редактировать CHANGELOG.md по ходу разработки, так что для версии 0.2 и далее чейнджлог в сочетании с историей репозитория будет иметь двойную пользу: и расскажет о сути изменений, и укажет на сами изменения.

git blame счастлив

В общем и целом

Как видите, ничего сверхъестественного, но при этом все организовано так, чтобы у разработки был какой-то темп. Идей у меня много, а времени - пару вечеров да выходные, поэтому сейчас важно не заострять внимание на тех деталях, которые совсем не важны, но и не доводить текущий код до неподдерживаемого состояния.

Из всех проблем меня сейчас больше всего волнует отсутствие тестов, потому что я уже чувствую, что новые изменения совсем скоро начнуть ломать текущую функциональность. Поэтому надо хотя бы бэкендную часть покрыть тестами процентов на 80, чтобы я пореже ловил "Internal Server Error" после покупок в магазине.

Ну и это еще не весь рассказ о том, что сейчас "под капотом", так что напишу еще и про CI, и про инфраструктуру, и про то, как я иногда привлекаю AI для код-ревью, и про то, как недавно всё упало, и еще про что-нибудь.

Stay tuned!

Привет с Большого Бодуна

Попытались тут посмотреть новую "Дюну", и вообще не поняли, в чем соль. Не то чтобы я целиком и полностью понял первый фильм, но...

Airplane! (1980)

Посмотрел вчера фильм Airplane! (1980), это одна из тех старых комедий, где люди каламбурят на серьезных щах, создавая абсурдн...

Яндекс.Лавка

До сих пор не могу привыкнуть к тому, насколько быстрой стала доставка Лавки у нас на районе. Буквально сегодняшний случай: Юля ...

Удел барабанщика

Наткнулся на ролик, где барабанщик Арьен ван Весенбек отрабатывает свою партию. Никогда так пристально не следил за ударными, и ...

Старомодный сайт

Хочу, чтобы все сайты были такими. Классические три колонки, табличная верстка, подчеркнутые ссылки, никаких развесистых шрифтов...