Antida Python School. Итоговое задание по курсу. Учебный проект "Сервис для учета личных финансов".
Проект представляет собой приложение, написанное на языке Python с применением web-фреймворка Flask. Реализован REST API, обеспечивающий требуемые методы (см. раздел "Формулировка задания")
Предметная область: «Сервис для учета личных финансов»
Словесное описание требований
- Сайт представляет собой сервис для учета личных финансов. Пользователи сервиса фиксируют свои доходы и расходы, чтобы видеть отчеты по своим личным финансам.
- Для работы на сайте необходима регистрация, обязательные поля профиля — имя, фамилия, email, пароль. Для авторизации используется пара email и пароль.
- Зарегистрированный пользователь может:
- Добавлять операции доходов и расходов, а также редактировать и удалять добавленные операции
- Добавлять категории для операций, а также редактировать и удалять существующие категории
- Строить отчеты с разрезами по временному периоду и категории
- При добавлении операции пользователь передает следующие поля:
- Тип операции (доход/расход)
- Сумма
- Описание операции (не обязательно)
- Категория (не обязательно, у операции может быть 1 или 0 категорий)
- Дата и время (не обязательно, по умолчанию используется текущее время)
- При добавлении категории пользователь передает следующие поля:
- Название
- Родительская категория (не обязательно, если не указана, то создается категория верхнего уровня)
- Категории могут вкладываться друг в друга, образуя древовидную структуру. Например: категория «Еда» может содержать подкатегории «Продукты», «Рестораны», «Обеды».
- При построении отчета об операциях, пользователь может указать категорию и временной период, по которым строится отчет:
- Если указана категория, то учитываются операции из категории и всех ее подкатегорий, подкатегорий ее подкатегорий и так далее. Если категория не указано, то в отчет включаются все операции за период независимо от категории
- Варианты временных периодов: текущая неделя (с понедельника по воскресенье), предыдущая неделя, текущий месяц, предыдущий месяц, текущий квартал, предыдущий квартал, текущий год, предыдущий год, произвольный диапазон (указываются даты начала и конца диапазона), всё время
- Отчет об операциях содержит:
- Список операций, соответствующий указанным категории и временному периоду. Для каждой операции отдается дата, сумма, описание и категория (вместе со всеми родительскими категориями). Список отсортирован по дате и отдается с пагинацией
- Общую сумма для всех операций из списка
Сформированное на основе формального описания задачи техническое задание: ТЗ Rev.1.1.pdf
Изображения моделей, получившихся в ходе проектирования базы данных:
Авторизация пользователя. Вход и выход
POST /auth/login
Request:
{
"email": str,
"password": str
}
POST /auth/logout
Регистрация пользователя
POST /register
Request:
{
"email": str,
"password": str,
"first_name": str,
"last_name": str
}
Response:
{
"id": int,
"email": str,
"first_name": str,
"last_name": str
}
Создание категории
Метод доступен только авторизованным пользователям.POST /category
Request:
{
"name": str,
"parent_id": int?
}
Response:
{
"id": int,
"name": str,
"parent_id": int?
}
Получение категории
Метод доступен только авторизованным пользователям. Поиск производится по уникальному (в рамках дерева данного пользователя) имени категории.GET /category
Request:
{
"name": str,
}
Response:
{
"id": int,
"name": str,
"parent_id": int?
}
Редактирование категории
Метод доступен только авторизованным пользователям. Пользователь может редактировать только созданные им категории.PATCH /category/<id>
Request:
{
"name": str?,
"parent_id": int?
}
Response:
{
"id": int,
"name": str,
"parent_id": int?
}
Удаление категории
Метод доступен только авторизованным пользователям. Пользователь может удалять только созданные им категории.DELETE /category/<id>
Создание операции
Доступно только авторизованным пользователям. Поле type указывает на тип операции - true для операции прихода, false для операци расхода.POST /transactions
Request:
{
"type": bool,
"amount": str,
"description": str?,
"category_id": int?,
"date": int?
}
Responce:
{
"id": int,
"type": bool,
"amount": str,
"description": str?,
"category_id": int?,
"date": int
}
Редактирование операции
Доступно только авторизованным пользователям. Метод доступен только для операций, которые созданы пользователем, выполняющим запрос. Редактирование происходит по id операции.PATCH /transactions/<id>
Request:
{
"type": bool?,
"amount": str?,
"description": str?,
"category_id": int?,
"date": int?
}
Responce:
{
"id": int,
"type": bool,
"amount": str,
"description": str?,
"category_id": int?,
"date": int
}
Удаление операции
Удаляются лишь те операции, которые созданы авторизованным пользователем. Удаление происходит по id операции.DELETE /transactions/<id>
Получение списка операций
Пользователь может получить только собственные операции. Список можно фильтровать с помощью query string параметров, все параметры необязательные.Список выводится с использованием пагинации. Параметры page_size и page отвечают за регулировку пагинации: page - отображает текущую страницу, page_size - регулирует количество операций на странице.
Параметры from, to, period отвечают за фильтрацию по времени: from - дата в виде timestamp, отфильтровывает те записи, дата которых превышает заданную, to - дата в виде timestamp, отфильтровывает те записи, дата которых не превышает заданную, period - фильтрация по одному из предустановленных периодов. Если передан параметр period, from и to игнорируются.
Список предустановленных периодов:
-
week – текущая неделя с понедельника по воскресенье
-
last_week – предыдущая неделя с понедельника по воскресенье
-
month – текущий месяц
-
last_month – предыдущий месяц
-
quarter – текущий квартал
-
last_quarter – предыдущий квартал
-
year – текущий год
-
last_year – предыдущий год
GET /transactions
Query string: category_id: int? from: int? to: int? period: str? page_size: int? page: int? Response: { "operations": [ { "id": int, "date": int, "type": bool, "description": str?, "amount": str, "categories": [ { "id": int, "name": str } ] } ], "total": str, "total_items": int, "total_pages": int, "page_size": int, "page": int, "next_page": str?, "prev_page": str? }
- Версия: v1.0.0
Для запуска приложения, в системе должен быть установлен интерпретатор Python версии 3.8 или новее. Совместимость с более ранними версиями не гарантируется.
Разработку рекомендуется вести в изолированном режиме из под виртуального окружения. Для его установки потребуется установить пакет virtualenv:
$ pip install virtualenv
Чтобы клонировать проект локально - вызовите интерфейс командной строки (например, Git Bash) в желаемой директории, выполните команду:
$ git clone https://github.com/jasper7466/Study-APS-Task3.git
и перейдите в директорию проекта:
$ cd ./Study-APS-Task3
$ python -m virtualenv venv
$ . venv/Scripts/activate
Зависимости зафиксированы в файле requirements.txt
. Для их автоматической установки достаточно выполнить команду:
$ pip install -r requirements.txt
Примечание: если работа ведётся не из под venv - пакеты установятся в систему
Добавление в PYTHONPATH пути до корня приложения (где лежит модуль app.py):
$ export PYTHONPATH=./src
(или set PYTHONPATH=./src
)
Установка пути до приложения относительно PYTHONPATH в переменную окружения FLASK_APP:
$ export FLASK_APP=app:create_app
(или set FLASK_APP=app:create_app
)
Для запуска в режиме отладки следует устанавить переменную окружения FLASK_ENV в значение "development":
$ export FLASK_ENV=development
(или set FLASK_ENV=development
)
При такой настройке:
- сервер будет запущен в режиме автоматической "горячей" перезагрузки приложения при детектировании изменений в его исходных кодах
- при возникновении ошибок в браузере будет выводиться stack trace
Установку переменных окружения и некоторых других переменных параметров при каждом запуске приложения можно избежать, добавив в корневую директорию проекта файлы .env
и .flaskenv
следующего содержания:
.env:
DB_CONNECTION = example.db
SECRET_KEY = secret_key
.flaskenv:
PYTHONPATH = ./src
FLASK_APP = app:create_app
FLASK_ENV = development
Указанные в этих файлах параметры будут автоматически подтягиваться и применяться с помощью пакета python-dotenv
Для запуска приложения в настроенном на предыдущем шаге режиме выполните команду:
$ flask run
После завершения доработки проекта, в случае добавления новых пакетов - следует зафиксировать зависимости, обновив файл requirements.txt
. Это можно сделать автоматически, выполнив команду:
$ pip freeze > requirements.txt
Команду имеет смысл выполнять из под виртуального окружения, если оно используется. В противном случае будут подтянуты все локальные пакеты из системы.
- Файл БД не исключён из индекса и присутствует в репозитории для удобства продолжения тестирования/доработки. При отладке желательно скопировать его вне директории проекта, изменив соответствующим образом переменную
DB_CONNECTION
в файле.env
, либо добавить в.gitignore
. - В качестве инструмента для тестирования API задействован Postman. Коллекция тестовых запросов приведена в виде экспортированного в формате "Collection v2.1" JSON-файла
API test requests.postman_collection.json
в корне проекта.
- Python
- Flask
- HTTP
- REST API
- Sqlite
- Валидация структуры запросов выполнена частично
- Валидация передаваемых данных выполнена частично
- Код не покрыт автоматическими тестами
- Присутствуют методы, лежащие не совсем в своём сервисе -> дублирование одинаковых исключений от разных сервисов
- Реализована защита от замыкания категории "сама на себя", но не реализована защита от закольцовывания дерева "на себя". Реализовать не сложно (на базе метода get_categories сервиса transactions), но весьма проблематично в текущем исполнении по причине, указанной в предыдущем пункте
- Фиксирование зависимостей лучше выполнить более прогрессивным способом, с помощью специализированных библиотек (pipenv, poetry)
- В Docstrings желательно прописывать ожидаемые типы переменных, это упростит тестирование
- Возвращаемый ответ должен всегда иметь фиксированную для каждого конкретного эндпоинта структуру, это упростит работу фронтенд-разработчику. В текущей реализации необязательные поля не возвращаются в случае, если они пусты (null, undefined)
- В случае пустого тела ответа (когда по запросу ничего не найдено) - следует возвращать пустой объект, а не код ошибки 404
- Желательно унифицировать способ отображения элементов и обращения к ним (или везде по id или везде по имени)
- Для запроса записей с фильтром по временному периоду можно попробовать воспользоваться встроенными инструментами SQLite
- Для случая, когда в теле запроса отсутствуют обязательные для его обработки поля - лучше возвращать код ошибки 422