У меня есть идея проекта:

Мы собираемся сделать WEB - сервис aviasalesforbitches - авиасейлс на минималках. Наш сервис позволит пользователям "подписываться" на интересующие их авиабилеты и получать регулярные обновления их цены. Если цена будет ниже средней за определенный промежуток времени, либо ниже цены, заданной пользователем, то ему в TG придет уведомление об этом.

Вот его структура:
aviasalesforbitches/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── config.py
│   ├── database.py
│   ├── models.py
│   ├── schemas.py
│   ├── crud.py
│   ├── dependencies.py
│   ├── routers/
│   │   ├── __init__.py
│   │   ├── users.py
│   │   ├── subscriptions.py
│   │   ├── tickets.py
│   ├── services/
│   │   ├── __init__.py
│   │   ├── aviasales_api.py
│   │   ├── telegram_bot.py
│   └── utils/
│       ├── __init__.py
│       └── notifications.py
├── tests/
│   ├── __init__.py
│   ├── test_users.py
│   ├── test_subscriptions.py
│   ├── test_tickets.py
│   └── test_notifications.py
├── requirements.txt
└── README.md

Вот содержание файлов:
tickets.py:
# app/routers/tickets.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from typing import List, Optional
from .. import crud, schemas
from ..dependencies import get_db

router = APIRouter(
    prefix="/tickets",
    tags=["tickets"]
)

@router.get("/", response_model=List[schemas.Ticket])
def get_tickets(
    origin: str = Query(None, description="Код города отправления (например, 'MOW')"),
    destination: str = Query(None, description="Код города назначения (например, 'LED')"),
    departure_at: Optional[str] = Query(None, description="Дата вылета в формате 'YYYY-MM-DD'"),
    min_price: Optional[float] = Query(None, description="Минимальная цена"),
    max_price: Optional[float] = Query(None, description="Максимальная цена"),
    limit: int = Query(100, description="Количество записей для возврата"),
    db: Session = Depends(get_db)
):
    """
    Получить список билетов из базы данных по заданным критериям.
    """
    try:
        tickets = crud.get_tickets_filtered(
            db=db,
            origin=origin,
            destination=destination,
            departure_at=departure_at,
            min_price=min_price,
            max_price=max_price,
            limit=limit
        )
        return tickets
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))
aviasales_api.py:
# app/services/aviasales_api.py
import requests
from typing import Dict, Any, Optional, List
from ..config import AVIASALES_API_KEY
from sqlalchemy.orm import Session
from .. import crud, schemas
from dateutil.parser import parse
import logging

logger = logging.getLogger(__name__)

class AviasalesAPI:
    def __init__(self, db: Session):
        self.api_key = AVIASALES_API_KEY
        self.base_url = "https://api.travelpayouts.com/aviasales/v3/"
        self.db = db

    def get_prices_for_dates(
        self,
        origin: str,
        destination: str,
        departure_at: Optional[str] = None,
        return_at: Optional[str] = None,
        currency: str = 'rub',
        one_way: bool = True,
        direct: bool = False,
        limit: int = 1000,
        page: int = 1,
        sorting: str = 'price',
        unique: bool = False
    ) -> List[schemas.Ticket]:
        url = f"{self.base_url}prices_for_dates"
        params = {
            "origin": origin,
            "destination": destination,
            "departure_at": departure_at,
            "return_at": return_at,
            "one_way": str(one_way).lower(),
            "direct": str(direct).lower(),
            "currency": currency,
            "limit": limit,
            "page": page,
            "sorting": sorting,
            "unique": str(unique).lower(),
            "token": self.api_key
        }
        # Удаляем параметры со значением None
        params = {k: v for k, v in params.items() if v is not None}
        try:
            response = requests.get(url, params=params)
            response.raise_for_status()  # Выбрасывает исключение для статусов 4XX/5XX
            tickets_data = response.json()

            # Теперь сохраняем данные в базу данных
            added_tickets = []

            for ticket_info in tickets_data.get('data', []):
                logger.info(f"Обработка билета: {ticket_info}")

                link = ticket_info.get('deep_link') or ticket_info.get('link')
                if not link:
                    continue  # Пропускаем билеты без ссылки

                existing_ticket = crud.get_ticket_by_link(self.db, link=link)
                if existing_ticket:
                    logger.info(f"Билет уже существует: {link}")
                    continue  # Пропускаем существующие билеты

                # Парсим дату
                try:
                    flight_date = parse(ticket_info.get('departure_at')).date()
                except (ValueError, TypeError):
                    logger.warning(f"Некорректный формат даты: {ticket_info.get('departure_at')}")
                    continue  # Пропускаем билеты с некорректной датой

                # Создаём Pydantic-модель для валидации данных
                ticket_create = schemas.TicketCreate(
                    from_city=ticket_info.get('origin'),
                    to_city=ticket_info.get('destination'),
                    price=float(ticket_info.get('price')),
                    flight_date=flight_date,
                    link=link,
                    airline=ticket_info.get('airline', None),
                    stops=int(ticket_info.get('transfers', 0))
                )

                # Создаём и сохраняем билет
                ticket = crud.create_ticket(db=self.db, ticket=ticket_create)
                added_tickets.append(ticket)
                logger.info(f"Добавлен новый билет: {ticket.from_city} → {ticket.to_city} за {ticket.price} руб.")

            return added_tickets

        except requests.exceptions.RequestException as e:
            raise Exception(f"Ошибка при запросе к Aviasales API: {e}")
        except Exception as e:
            logger.error(f"Ошибка при обработке данных билетов: {e}")
            raise
config.py:
# app/config.py
import os
from dotenv import load_dotenv

# Загрузка переменных окружения из .env файла
load_dotenv()

DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://user:password@localhost/dbname')
AVIASALES_API_KEY = os.getenv('AVIASALES_API_KEY', 'your_aviasales_api_key')
TELEGRAM_BOT_TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'your_telegram_bot_token')


crud.py:
from sqlalchemy.orm import Session
from . import models, schemas
from datetime import datetime
from typing import Optional
from .models import Ticket

def get_ticket_by_link(db: Session, link: str):
    return db.query(models.Ticket).filter(models.Ticket.link == link).first()

def create_ticket(db: Session, ticket: schemas.TicketCreate) -> models.Ticket:
    db_ticket = models.Ticket(
        from_city=ticket.from_city,
        to_city=ticket.to_city,
        price=ticket.price,
        flight_date=ticket.flight_date,
        link=ticket.link,
        airline=ticket.airline,
        stops=ticket.stops
    )
    db.add(db_ticket)
    db.commit()
    db.refresh(db_ticket)
    return db_ticket


def get_tickets_filtered(
        db: Session,
        origin: Optional[str] = None,
        destination: Optional[str] = None,
        departure_at: Optional[str] = None,
        min_price: Optional[float] = None,
        max_price: Optional[float] = None,
        limit: int = 100
):
    query = db.query(models.Ticket)

    if origin:
        query = query.filter(models.Ticket.from_city == origin)
    if destination:
        query = query.filter(models.Ticket.to_city == destination)
    if departure_at:
        try:
            departure_date = datetime.strptime(departure_at, '%Y-%m-%d').date()
            query = query.filter(models.Ticket.flight_date == departure_date)
        except ValueError:
            raise ValueError("Некорректный формат даты. Ожидается YYYY-MM-DD.")
    if min_price is not None:
        query = query.filter(models.Ticket.price >= min_price)
    if max_price is not None:
        query = query.filter(models.Ticket.price <= max_price)

    tickets = query.limit(limit).all()
    return tickets

def get_tickets(db: Session, skip: int = 0, limit: int = 10):
    return db.query(models.Ticket).offset(skip).limit(limit).all()

database.py:
import os
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

from .config import DATABASE_URL

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

dependencies.py:
from .database import SessionLocal
from sqlalchemy.orm import Session

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
main.py:
# app/main.py
import time
from fastapi import FastAPI
from app.routers import tickets
from app.database import engine
from app.models import Base
from apscheduler.schedulers.background import BackgroundScheduler
import logging
from datetime import datetime, timedelta
from app.services.aviasales_api import AviasalesAPI
from app.database import SessionLocal
from contextlib import asynccontextmanager


# Удаление всех таблиц
Base.metadata.drop_all(bind=engine)

# Создание таблиц в базе данных
Base.metadata.create_all(bind=engine)

# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Настройка планировщика
scheduler = BackgroundScheduler()

def scheduled_job():
    logger.info("Запуск задачи по сбору билетов")
    db = SessionLocal()
    try:
        aviasales_api = AviasalesAPI(db=db)
        origins = ["MOW", "LED", "NYC", "LON", "PAR"]
        destinations = ["MOW", "LED", "NYC", "LON", "PAR"]
        today = datetime.today()
        days_ahead = 7  # Уменьшили количество дней вперед

        for origin in origins:
            for destination in destinations:
                if origin == destination:
                    continue  # Пропускаем, если города совпадают
                for day_offset in range(days_ahead):
                    departure_date = (today + timedelta(days=day_offset)).strftime('%Y-%m-%d')
                    try:
                        tickets = aviasales_api.get_prices_for_dates(
                            origin=origin,
                            destination=destination,
                            departure_at=departure_date,
                            currency='rub',
                            one_way=True,
                            direct=False,
                            limit=1000,
                            page=1,
                            sorting='price',
                            unique=False
                        )
                        logger.info(f"Загружено {len(tickets)} билетов для направления {origin} -> {destination} на дату {departure_date}")
                        time.sleep(0.5)  # Задержка в полсекунды между запросами
                    except Exception as e:
                        logger.error(f"Ошибка при загрузке билетов для направления {origin} -> {destination} на дату {departure_date}: {e}")
            logger.info(f"Загрузка билетов для города {origin} завершена")
        logger.info("Задача по сбору билетов завершена успешно")
    except Exception as e:
        logger.error(f"Ошибка в задаче по сбору билетов: {e}")
    finally:
        db.close()

scheduled_job()

scheduler.add_job(scheduled_job, 'interval', hours=1)

@asynccontextmanager
async def lifespan(app: FastAPI):
    # Код для выполнения при запуске приложения
    scheduler.start()
    yield
    # Код для выполнения при завершении приложения
    scheduler.shutdown()

app = FastAPI(lifespan=lifespan)

# Подключение роутеров
app.include_router(tickets.router)

models.py:
# app/models.py
from sqlalchemy import Column, Integer, String, Numeric, Date, Text, ARRAY, Enum, CheckConstraint, ForeignKey
from sqlalchemy.orm import relationship, join
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class Ticket(Base):
    __tablename__ = 'tickets'

    id = Column(Integer, primary_key=True, index=True)
    from_city = Column(String(50))
    to_city = Column(String(50))
    price = Column(Numeric)
    flight_date = Column(Date)
    link = Column(Text, unique=True)
    airline = Column(String(100), nullable=True)
    stops = Column(Integer, nullable=True)


class User(Base):
    __tablename__ = 'users'

    user_id = Column(Integer, primary_key=True, autoincrement=True)
    name = Column(Text, nullable=False)
    tg_nick = Column(String(32), nullable=False)

    cards = relationship("Card", secondary="user_cards", back_populates="users")


class Card(Base):
    __tablename__ = 'cards'

    card_id = Column(Integer, primary_key=True, autoincrement=True)
    origin = Column(String(3), nullable=False)
    destination = Column(String(3), nullable=False)
    price_threshold = Column(Integer, default=-1)
    notification_freq = Column(
        Enum('halfday', 'day', 'week', 'month', name="notification_frequency_enum"),
        nullable=False,
        server_default='day'
    )

    __table_args__ = (
        CheckConstraint("notification_freq in ('halfday', 'day', 'week', 'month')",
                        name="notification_freq_check"),
    )

    users = relationship("User", secondary="user_cards", back_populates="cards")


class UserCard(Base):  # Связующая/ассоциативная таблица
    __tablename__ = 'user_cards'

    user_id = Column(Integer, ForeignKey('users.user_id'), primary_key=True)
    card_id = Column(Integer, ForeignKey('cards.card_id'), primary_key=True)
schemas.py:

from pydantic import BaseModel
from datetime import date
from typing import Optional

class TicketBase(BaseModel):
    from_city: str
    to_city: str
    price: float
    flight_date: date
    link: str
    airline: Optional[str] = None
    stops: Optional[int] = 0

class TicketCreate(TicketBase):
    pass

class Ticket(TicketBase):
    id: int

    class Config:
        from_attributes = True


Расскажи подробно, поэтапное, как работает проект и что нужно доделать