diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e66ab0a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + services: + postgres: + image: postgres:17 + env: + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_DB: test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + SECRET_KEY: ci-secret-key + DEBUG: "False" + ALLOWED_HOSTS: localhost,127.0.0.1 + POSTGRES_DB: test + POSTGRES_USER: test + POSTGRES_PASSWORD: test + POSTGRES_HOST: localhost + POSTGRES_PORT: "5432" + CELERY_BROKER_URL: amqp://guest:guest@localhost:5672// + CELERY_RESULT_BACKEND: rpc:// + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + - name: Migrate + run: python manage.py migrate + - name: Lint + run: ruff check . + - name: Format check + run: ruff format --check . + - name: Tests + run: pytest --cov --cov-report=term-missing --cov-fail-under=55 + - name: Coverage badge data + id: coverage + run: | + coverage json -o coverage.json + python - <<'PY' >> "$GITHUB_OUTPUT" + import json + + totals = json.load(open("coverage.json"))["totals"] + percent = totals["percent_covered"] + color = "green" if percent >= 80 else "yellow" if percent >= 55 else "red" + print(f"total={totals['percent_covered_display']}%") + print(f"color={color}") + PY + - name: Update coverage badge + uses: schneegans/dynamic-badges-action@v1.7.0 + continue-on-error: true + with: + auth: ${{ secrets.GIST_TOKEN }} + gistID: 097e2cc0afbb6ee7fdbfb4d2d67bb87d + filename: coverage.json + label: coverage + message: ${{ steps.coverage.outputs.total }} + color: ${{ steps.coverage.outputs.color }} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d126f82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Anna Putilovskaya + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index dd6d768..6d052eb 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,127 @@ -# Сервис уведомлений - -### Задача: - -Создать сервис для отправки уведомлений пользователям. -Пользователь может получать уведомления через: -- Email -- SMS -- Telegram - -Нужно обеспечить надежную доставку - если один способ не сработал, попробовать другой. - ---- -### Технический требования: - -- Язык программирования: - - Python 3.12 -- Фреймворк: - - Django Rest Framework -- ORM: - - Django ORM -- Хранение данных: - - PostgreSQL 17 -- Очереди сообщений: - - Celery + RabbitMQ - ---- -### Запуск проекта: - -- Клонируйте проект: - ```bash - git clone https://github.com/annaputilovskaya/NotificationService.git - ``` -- Создайте файл .env в корне проекта по образцу (Файл .env.example) и заполните его данными. - - Команда для Unix-подобных систем для копирования файла: - ```bash - cp .env.example .env - ``` -- Убедитесь, что на Вашем устройстве установлен Docker. - - Проверка версии Docker: - ```bash - docker --version - ``` - Если докер не установлен, [осуществите его установку по инструкции с официального сайта](https://www.docker.com/) - -- Осуществите сборку образов и запуск контейнеров: - ```bash - docker compose up -d --build - ``` - ---- -### Структура базы данных: - -image - ---- -### Функционал и бизнес-логика - -1. Пользователи: - - Регистрация с email и паролем - - Получение токена для авторизация - - ОБновление токена авторизации - -image - - - -2. Получатели - - Создание получателя - - Просмотр получателя - - Просмотр списка получателей - - Изменение получтаеля - - Удаление получателя - -image - - - -3. Уведомления - - Создание уведомления - - Просмотр уведомления - - Просмотр списка уведомлений - -image - - - При создания уведомления создается задача по отправке уведомления. - Происходит выбор стратегии отпраки уведомления в следующем порядке: - - Email - - Telegram - - SMS - - В случае, если первая стратегия не сработала, переходим ко второй, далее к третьей. - Если ни одна страегия отправки для каких-либо получателей не сработала, повторяем попытку отправки для этих получателей через определенный временной интервал. - - --- -Сервис подходит для отправки разовых уведомлений. Если нужны повторяющиеся уведомления, возможна доработка. - -В проекте настроено логирование отправки уведомлений с отправкой логов в консоль и с сохранением в файл. - -Проведено ручное тестирование проекта с помощью Postman. - -Автоматизированное тестирование не проводилось. - -Для более подробной информации об API ознакомьтесь с интерактивной документацией. - -Если проект запущен на localhost, пройдите по [ссылке](http://localhost:8000/swagger/) - - +# Notification Service + +[![CI](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml/badge.svg)](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml) +[![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/annaputilovskaya/097e2cc0afbb6ee7fdbfb4d2d67bb87d/raw/coverage.json)](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +[![Python](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/) + +An API for reliable delivery of notifications to users across multiple channels — Email, SMS, and Telegram — with automatic failover between channels. + +[Русская версия](README.ru.md) + +## Features + +- **Users** — email/password sign-up, issuing and refreshing JWT access tokens. + + Users + +- **Recipients** — create, view, update, and delete recipients. + + Recipients + +- **Notifications** — create a notification, view a single notification and a list. + + Notifications + +- **Reliable delivery (failover)** — creating a notification enqueues a Celery task. Strategies are tried in order **Email → Telegram → SMS**: when a channel fails, the next one is used. If no strategy succeeds for some recipients, delivery to those recipients is retried after a delay. +- **Pagination** — list endpoints return a paginated `{"count": ..., "results": [...]}` payload (the `?page=` parameter). +- **Logging** — delivery logs go to the console and to a file. +- The service targets one-off notifications; recurring notifications are a possible extension. + +## Stack + +- Python 3.12 +- Django REST Framework (Django ORM) +- PostgreSQL 17 +- Celery + RabbitMQ +- drf-yasg (Swagger/Redoc) + +## Architecture + +```mermaid +flowchart LR + Client["Client"] --> API["Django REST API"] + API -->|create notification| Task["Celery: send_notification"] + Task --> Email["Email strategy"] + Email -->|fail| Telegram["Telegram strategy"] + Telegram -->|fail| SMS["SMS strategy"] + Email --> SMTP[("SMTP")] + Telegram --> TG[("Telegram API")] + SMS --> SMSAPI[("sms.ru")] + Task --> DB[("PostgreSQL")] +``` + +## Data model + +Database schema + +## Getting started + +### Prerequisites + +- Docker and Docker Compose + +### Configuration + +Copy `.env.example` to `.env` and fill in the values: + +```bash +cp .env.example .env +``` + +| Variable | Description | Example | +|---|---|---| +| `SECRET_KEY` | Django secret key | `django-insecure-...` | +| `DEBUG` | Debug mode | `False` | +| `ALLOWED_HOSTS` | Allowed hosts (comma-separated) | `localhost,127.0.0.1` | +| `CORS_ALLOWED_ORIGINS` | Allowed CORS origins | `http://localhost:8000` | +| `POSTGRES_*` | Database connection settings | `POSTGRES_DB=notifications` | +| `CELERY_BROKER_URL` | Celery broker URL (RabbitMQ) | `amqp://user:pass@rabbitmq:5672//` | +| `EMAIL_*` | Mail server settings | `EMAIL_HOST=smtp.example.com` | +| `TELEGRAM_TOKEN` | Telegram bot token | `123456:ABC` | +| `SMS_API_KEY` / `SMS_SENDER` | sms.ru credentials | `SMS_SENDER=Service` | +| `SMS_TEST` | sms.ru test mode (does not send SMS) | `0` | + +### Install & run + +Clone the repository and start the services: + +```bash +git clone https://github.com/annaputilovskaya/NotificationService.git +cd NotificationService +docker compose up -d --build +``` + +Verify Docker is installed (otherwise follow the [official instructions](https://www.docker.com/)): + +```bash +docker --version +``` + +Interactive API documentation (Swagger): http://localhost:8000/swagger/ + +## Tests + +Tests run against PostgreSQL. Start the database and run: + +```bash +docker compose up -d db +python -m pytest --cov +``` + +External services (SMTP, Telegram, sms.ru) are mocked. The project was originally checked manually via Postman; automated core and smoke tests are now in place. + +## Project structure + +``` +config/ # Django settings, Celery, urls +notifications/ # notifications: model, strategies, Celery task +recipients/ # recipients +users/ # users, JWT +conftest.py # shared pytest fixtures +pyproject.toml # ruff/pytest/coverage config +``` + +## License + +Released under the MIT License — see [LICENSE](LICENSE). diff --git a/README.ru.md b/README.ru.md new file mode 100644 index 0000000..b0db662 --- /dev/null +++ b/README.ru.md @@ -0,0 +1,127 @@ +# Сервис уведомлений + +[![CI](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml/badge.svg)](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml) +[![coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/annaputilovskaya/097e2cc0afbb6ee7fdbfb4d2d67bb87d/raw/coverage.json)](https://github.com/annaputilovskaya/NotificationService/actions/workflows/ci.yml) +[![License](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE) +[![Python](https://img.shields.io/badge/python-3.12-blue.svg)](https://www.python.org/) + +API для надёжной доставки уведомлений пользователям по нескольким каналам — Email, SMS и Telegram — с автоматическим переключением между каналами при сбое. + +[English version](README.md) + +## Возможности + +- **Пользователи** — регистрация по email и паролю, получение и обновление JWT-токена авторизации. + + Пользователи + +- **Получатели** — создание, просмотр, изменение и удаление получателей. + + Получатели + +- **Уведомления** — создание уведомления, просмотр одного уведомления и списка. + + Уведомления + +- **Надёжная доставка (failover)** — при создании уведомления ставится задача Celery. Стратегии перебираются по порядку **Email → Telegram → SMS**: если канал не сработал, используется следующий. Если ни одна стратегия не сработала для части получателей, попытка повторяется для этих получателей через заданный интервал. +- **Пагинация** — эндпоинты списков отдают постраничный ответ `{"count": ..., "results": [...]}` (параметр `?page=`). +- **Логирование** — логи отправки пишутся в консоль и сохраняются в файл. +- Сервис ориентирован на разовые уведомления; поддержка повторяющихся уведомлений — возможная доработка. + +## Стек + +- Python 3.12 +- Django REST Framework (Django ORM) +- PostgreSQL 17 +- Celery + RabbitMQ +- drf-yasg (Swagger/Redoc) + +## Архитектура + +```mermaid +flowchart LR + Client["Клиент"] --> API["Django REST API"] + API -->|создание уведомления| Task["Celery: send_notification"] + Task --> Email["Email strategy"] + Email -->|fail| Telegram["Telegram strategy"] + Telegram -->|fail| SMS["SMS strategy"] + Email --> SMTP[("SMTP")] + Telegram --> TG[("Telegram API")] + SMS --> SMSAPI[("sms.ru")] + Task --> DB[("PostgreSQL")] +``` + +## Модель данных + +Схема базы данных + +## Начало работы + +### Требования + +- Docker и Docker Compose + +### Конфигурация + +Скопируйте `.env.example` в `.env` и заполните значения: + +```bash +cp .env.example .env +``` + +| Переменная | Описание | Пример | +|---|---|---| +| `SECRET_KEY` | Секретный ключ Django | `django-insecure-...` | +| `DEBUG` | Режим отладки | `False` | +| `ALLOWED_HOSTS` | Разрешённые хосты (через запятую) | `localhost,127.0.0.1` | +| `CORS_ALLOWED_ORIGINS` | Разрешённые CORS-origins | `http://localhost:8000` | +| `POSTGRES_*` | Параметры подключения к БД | `POSTGRES_DB=notifications` | +| `CELERY_BROKER_URL` | URL брокера Celery (RabbitMQ) | `amqp://user:pass@rabbitmq:5672//` | +| `EMAIL_*` | Настройки почтового сервера | `EMAIL_HOST=smtp.example.com` | +| `TELEGRAM_TOKEN` | Токен Telegram-бота | `123456:ABC` | +| `SMS_API_KEY` / `SMS_SENDER` | Доступ к sms.ru | `SMS_SENDER=Service` | +| `SMS_TEST` | Тестовый режим sms.ru (не отправляет СМС) | `0` | + +### Установка и запуск + +Склонируйте репозиторий и запустите сервисы: + +```bash +git clone https://github.com/annaputilovskaya/NotificationService.git +cd NotificationService +docker compose up -d --build +``` + +Проверка версии Docker (если не установлен — [инструкция](https://www.docker.com/)): + +```bash +docker --version +``` + +Интерактивная документация API (Swagger): http://localhost:8000/swagger/ + +## Тесты + +Тесты запускаются на PostgreSQL. Поднимите БД и выполните: + +```bash +docker compose up -d db +python -m pytest --cov +``` + +Внешние сервисы (SMTP, Telegram, sms.ru) в тестах замоканы. Ранее проект проверялся вручную через Postman; теперь добавлены автоматизированные тесты ядра и smoke. + +## Структура проекта + +``` +config/ # настройки Django, Celery, urls +notifications/ # уведомления: модель, стратегии, задача Celery +recipients/ # получатели +users/ # пользователи, JWT +conftest.py # общие фикстуры pytest +pyproject.toml # конфиг ruff/pytest/coverage +``` + +## Лицензия + +Проект распространяется под лицензией MIT — см. [LICENSE](LICENSE). diff --git a/config/settings.py b/config/settings.py index 0806c51..aca1454 100644 --- a/config/settings.py +++ b/config/settings.py @@ -178,6 +178,7 @@ # Logging LOG_FILE_NAME = "notifications.log" LOG_FILE_PATH = os.path.join(BASE_DIR, "logs", LOG_FILE_NAME) +os.makedirs(os.path.dirname(LOG_FILE_PATH), exist_ok=True) LOGGING = { "version": 1,