Лабораторная работа 3

Упаковка FastAPI приложения в Docker, работа с источниками данных и очереди (Celery + Redis).

Готово

Цель работы

Научиться упаковывать FastAPI приложение в Docker, интегрировать парсер данных с базой данных и вызывать парсер через API и очередь (Celery + Redis). Освоить многосервисную архитектуру с оркестрацией через Docker Compose.

Выполненные задачи

Подзадача 1 — Упаковка в Docker
Создан Dockerfile для FastAPI приложения (Personal Finance API + парсер). Написан docker-compose.yml с сервисами: FastAPI приложение, PostgreSQL 16, Redis 7, Celery worker.
Подзадача 2 — Вызов парсера из FastAPI
Добавлен эндпоинт POST /parse/direct — принимает URL, асинхронно парсит страницу (aiohttp + BeautifulSoup), извлекает <title> и сохраняет в БД (таблица parsed_page). Реализованы GET /parse/history и POST /parse/batch.
Подзадача 3 — Celery + Redis очередь
Настроен Celery с Redis в качестве брокера. Добавлен эндпоинт POST /parse/async — ставит URL в очередь, возвращает task_id. Реализован GET /parse/async/status/{task_id} для отслеживания статуса задачи.

Архитектура сервисов

СервисКонтейнерПортНазначение
appfastapi_app_lr38000FastAPI — Personal Finance API + парсер
dbfinance_db_lr35432PostgreSQL 16 — основная БД
redisredis_lr36379Redis 7 — брокер Celery
celery-workercelery_worker_lr3Celery worker — фоновая обработка парсинга

Стек технологий

КомпонентТехнологияНазначение
Веб-фреймворкFastAPIREST API приложения
ORMSQLAlchemy 2.0 asyncАсинхронный доступ к PostgreSQL
База данныхPostgreSQL 16Хранение данных (финансы + parsed_page)
Асинхронная очередьCeleryФоновая обработка задач парсинга
Брокер сообщенийRedis 7Хранение задач Celery и результатов
КонтейнеризацияDocker + ComposeУпаковка и оркестрация сервисов
HTTP (async)aiohttpАсинхронные HTTP-запросы для парсинга
HTTP (sync)requestsСинхронные запросы в Celery-задачах
Парсинг HTMLbeautifulsoup4Извлечение <title> из HTML
АутентификацияPyJWTJWT-токены

Структура проекта

Lr3/ ├── Dockerfile # образ FastAPI + Celery ├── docker-compose.yml # оркестрация 4 сервисов ├── requirements.txt # зависимости (FastAPI, Celery, Redis, aiohttp…) ├── .env-example # переменные окружения ├── celery_worker.py # точка входа Celery worker ├── app/ │ ├── main.py # FastAPI приложение (финансы + парсер) │ ├── core/ │ │ ├── config.py # конфигурация (DATABASE_URL, REDIS_URL, JWT) │ │ ├── database.py # async engine, async_session_maker │ │ └── security.py # хеширование паролей, JWT │ ├── models/ │ │ └── models.py # SQLAlchemy модели + ParsedPage │ ├── schemas/ │ │ └── *.py # Pydantic схемы (User, Transaction…) │ ├── crud/ │ │ └── *.py # CRUD-операции │ ├── api/ │ │ └── routers/ │ │ ├── auth.py # аутентификация │ │ ├── transactions.py # транзакции │ │ ├── *.py # категории, теги, бюджеты, цели │ │ └── parser.py # парсер: /parse/direct, /parse/async, /parse/batch │ └── tasks/ │ └── celery_tasks.py # Celery-задача parse_url_task

API эндпоинты (парсер)

POST /parse/direct — прямой парсинг (Подзадача 2)

# Запрос
curl -X POST "http://localhost:8000/parse/direct?url=https://www.python.org"

# Ответ
{
  "id": 1,
  "url": "https://www.python.org",
  "title": "Welcome to Python.org"
}

POST /parse/async — асинхронный парсинг через Celery (Подзадача 3)

# Запрос — ставит URL в очередь Redis
curl -X POST "http://localhost:8000/parse/async?url=https://fastapi.tiangolo.com"

# Ответ
{
  "message": "Task queued",
  "task_id": "a1b2c3d4-...",
  "status": "PENDING"
}

# Проверка статуса
curl "http://localhost:8000/parse/async/status/a1b2c3d4-..."

# Ответ (после выполнения)
{
  "task_id": "a1b2c3d4-...",
  "status": "SUCCESS",
  "result": {
    "id": 2,
    "url": "https://fastapi.tiangolo.com",
    "title": "FastAPI"
  }
}

POST /parse/batch — пакетный парсинг

# Запрос
curl -X POST "http://localhost:8000/parse/batch" \
  -H "Content-Type: application/json" \
  -d '["https://www.python.org", "https://github.com"]'

# Ответ
{
  "parsed": 2,
  "time_seconds": 1.234,
  "results": [...]
}

GET /parse/history — история парсинга

curl "http://localhost:8000/parse/history?limit=5"

Celery + Redis: поток выполнения

  1. Клиент отправляет POST /parse/async?url=...
  2. FastAPI вызывает parse_url_task.delay(url) — задача помещается в очередь Redis
  3. FastAPI сразу возвращает клиенту task_id и статус PENDING
  4. Celery Worker (отдельный контейнер) забирает задачу из Redis
  5. Worker выполняет HTTP-запрос к URL, парсит <title>, сохраняет в PostgreSQL
  6. Результат сохраняется в Redis backend
  7. Клиент может запросить статус через GET /parse/async/status/{task_id}

Модель ParsedPage

# app/models/models.py (добавлено)
class ParsedPage(Base):
    __tablename__ = "parsed_page"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    url: Mapped[str] = mapped_column(Text, nullable=False)
    title: Mapped[str] = mapped_column(String(500), nullable=False)
    parsed_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())

Celery-задача

# app/tasks/celery_tasks.py
from celery import Celery

celery_app = Celery("parser_tasks", broker=REDIS_URL, backend=REDIS_URL)

@celery_app.task(bind=True, name="parse_url")
def parse_url_task(self, url: str) -> dict:
    """Fetch URL, extract , save to DB."""
    resp = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
    soup = BeautifulSoup(resp.text, "html.parser")
    title = soup.title.string.strip() if soup.title else "No title"

    # Save to PostgreSQL (sync)
    page = ParsedPage(url=url, title=title, parsed_at=datetime.now(timezone.utc))
    session.add(page); session.commit()
    return {"id": page.id, "url": url, "title": title}</code></pre>
  </div>

  <!-- Docker -->
  <div class="section">
    <h2>
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="2" width="20" height="20" rx="2.18"/><line x1="7" y1="2" x2="7" y2="22"/><line x1="17" y1="2" x2="17" y2="22"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="2" y1="7" x2="7" y2="7"/><line x1="2" y1="17" x2="7" y2="17"/><line x1="17" y1="7" x2="22" y2="7"/><line x1="17" y1="17" x2="22" y2="17"/></svg>
      Docker Compose
    </h2>
    <pre><code># docker-compose.yml
services:
  db:        # PostgreSQL 16 — основная БД
  redis:     # Redis 7 — брокер Celery
  app:       # FastAPI (Personal Finance API + парсер)
  celery-worker:  # Celery worker — фоновая обработка

# Запуск
$ docker compose up -d

# Сервисы:
#  - http://localhost:8000/docs      — Swagger UI
#  - http://localhost:8000/parse/...  — эндпоинты парсера
#  - PostgreSQL на localhost:5432
#  - Redis на localhost:6379</code></pre>
  </div>

  <!-- Выводы -->
  <div class="section">
    <h2>
      <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
      Выводы
    </h2>
    <p>В рамках лабораторной работы №3 были решены следующие задачи:</p>
    <ul>
      <li>FastAPI приложение из ЛР1 упаковано в Docker-контейнер вместе с базой данных и парсером</li>
      <li>Реализован прямой вызов парсера через HTTP (POST /parse/direct) — подзадача 2</li>
      <li>Настроена асинхронная очередь задач на базе Celery + Redis — подзадача 3</li>
      <li>Создан docker-compose.yml для оркестрации 4 сервисов</li>
      <li>Все три подзадачи реализованы в одном приложении без дублирования кода</li>
    </ul>
    <p>Освоены навыки контейнеризации веб-приложений, настройки асинхронных очередей задач (первый шаг к MLOps) и многосервисной архитектуры.</p>
  </div>

</div>

</body>
</html>