У меня есть идея проекта:
Мы собираемся сделать 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
Подробное описание модулей и классов
app/

Главный пакет приложения, содержащий основной код сервиса.
init.py
Инициализирует пакет app.
main.py
Точка входа в приложение FastAPI.

from fastapi import FastAPI
from .routers import users, subscriptions, tickets

app = FastAPI()

# Подключение роутеров
app.include_router(users.router)
app.include_router(subscriptions.router)
app.include_router(tickets.router)
config.py
Хранит конфигурации и настройки приложения.

import os

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')
database.py
Настраивает подключение к базе данных с помощью SQLAlchemy.

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)
models.py
Описывает модели данных с использованием SQLAlchemy ORM.

from sqlalchemy import Column, Integer, String, Numeric, Date, ForeignKey, Interval
from sqlalchemy.orm import relationship
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class User(Base):
    __tablename__ = 'users'

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String(100))
    email = Column(String(100))
    telegram_id = Column(String(100), unique=True, index=True)
    subscriptions = relationship("Subscription", back_populates="user")

class Subscription(Base):
    __tablename__ = 'subscriptions'

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    from_city = Column(String(50))
    to_city = Column(String(50))
    max_price = Column(Numeric)
    notify_frequency = Column(Interval)
    user = relationship("User", back_populates="subscriptions")

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(String(200))

class Notification(Base):
    __tablename__ = 'notifications'

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey('users.id'))
    ticket_id = Column(Integer, ForeignKey('tickets.id'))
    status = Column(String(20))
schemas.py
Содержит Pydantic-модели для валидации данных и сериализации.

from pydantic import BaseModel
from datetime import date, timedelta

class UserBase(BaseModel):
    name: str
    email: str
    telegram_id: str

class UserCreate(UserBase):
    pass

class User(UserBase):
    id: int
    class Config:
        orm_mode = True

class SubscriptionBase(BaseModel):
    from_city: str
    to_city: str
    max_price: float
    notify_frequency: timedelta

class SubscriptionCreate(SubscriptionBase):
    user_id: int

class Subscription(SubscriptionBase):
    id: int
    user_id: int
    class Config:
        orm_mode = True

class TicketBase(BaseModel):
    from_city: str
    to_city: str
    price: float
    flight_date: date
    link: str

class TicketCreate(TicketBase):
    pass

class Ticket(TicketBase):
    id: int
    class Config:
        orm_mode = True

class NotificationBase(BaseModel):
    user_id: int
    ticket_id: int
    status: str

class NotificationCreate(NotificationBase):
    pass

class Notification(NotificationBase):
    id: int
    class Config:
        orm_mode = True
crud.py
Содержит функции для работы с базой данных: создание, чтение, обновление, удаление.

from sqlalchemy.orm import Session

from . import models, schemas

# Функции для пользователей
def get_user(db: Session, user_id: int):
    return db.query(models.User).filter(models.User.id == user_id).first()

def get_user_by_telegram_id(db: Session, telegram_id: str):
    return db.query(models.User).filter(models.User.telegram_id == telegram_id).first()

def create_user(db: Session, user: schemas.UserCreate):
    db_user = models.User(
        name=user.name,
        email=user.email,
        telegram_id=user.telegram_id
    )
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

# Функции для подписок, билетов и уведомлений аналогично
dependencies.py
Определяет зависимости для маршрутов, например, подключение к базе данных.

from .database import SessionLocal

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
routers/
Пакет, содержащий маршруты (эндпоинты) для API приложения.
init.py
Инициализирует пакет routers.
users.py
Маршруты для управления пользователями.

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from .. import crud, schemas
from ..dependencies import get_db

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

@router.post("/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_telegram_id(db, telegram_id=user.telegram_id)
    if db_user:
        raise HTTPException(status_code=400, detail="Пользователь с таким Telegram ID уже существует")
    return crud.create_user(db=db, user=user)

@router.get("/{user_id}", response_model=schemas.User)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.get_user(db=db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="Пользователь не найден")
    return db_user
subscriptions.py
Маршруты для управления подписками.

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session

from .. import crud, schemas
from ..dependencies import get_db

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

@router.post("/", response_model=schemas.Subscription)
def create_subscription(subscription: schemas.SubscriptionCreate, db: Session = Depends(get_db)):
    return crud.create_subscription(db=db, subscription=subscription)

@router.get("/user/{user_id}", response_model=List[schemas.Subscription])
def read_subscriptions(user_id: int, db: Session = Depends(get_db)):
    subscriptions = crud.get_subscriptions_by_user(db=db, user_id=user_id)
    return subscriptions
tickets.py
Маршруты для управления билетами.

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session

from .. import crud, schemas
from ..dependencies import get_db

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

@router.get("/", response_model=List[schemas.Ticket])
def read_tickets(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)):
    tickets = crud.get_tickets(db=db, skip=skip, limit=limit)
    return tickets
services/
Пакет, содержащий интеграции с внешними сервисами.
init.py
Инициализирует пакет services.
aviasales_api.py
Класс для работы с API Aviasales.

import requests
from typing import Dict, Any

from ..config import AVIASALES_API_KEY

class AviasalesAPI:
    def __init__(self):
        self.api_key = AVIASALES_API_KEY
        self.base_url = "https://api.aviasales.ru/"

    def fetch_tickets(self, from_city: str, to_city: str) -> Dict[str, Any]:
        params = {
            "origin": from_city,
            "destination": to_city,
            "token": self.api_key
        }
        response = requests.get(f"{self.base_url}/some_endpoint", params=params)
        if response.status_code == 200:
            return response.json()
        else:
            raise Exception(f"Ошибка при получении данных: {response.status_code}")
telegram_bot.py
Класс для отправки уведомлений через Telegram.

from telegram import Bot
from telegram.error import TelegramError

from ..config import TELEGRAM_BOT_TOKEN

class TelegramBot:
    def __init__(self):
        self.bot = Bot(token=TELEGRAM_BOT_TOKEN)

    def send_notification(self, chat_id: str, message: str):
        try:
            self.bot.send_message(chat_id=chat_id, text=message, parse_mode="Markdown")
        except TelegramError as e:
            print(f"Ошибка при отправке сообщения: {e}")
utils/
Пакет с вспомогательными модулями.
init.py
Инициализирует пакет utils.
notifications.py
Содержит функции для обработки и отправки уведомлений пользователям.

from sqlalchemy.orm import Session

from .. import crud, services, models

def check_and_send_notifications(db: Session):
    # Получаем все подписки
    subscriptions = crud.get_all_subscriptions(db=db)
    aviasales_api = services.aviasales_api.AviasalesAPI()
    telegram_bot = services.telegram_bot.TelegramBot()
    
    for subscription in subscriptions:
        # Получаем новые билеты по подписке
        tickets_data = aviasales_api.fetch_tickets(subscription.from_city, subscription.to_city)
        
        # Обрабатываем полученные данные и фильтруем по цене
        for ticket_info in tickets_data:
            if ticket_info['price'] <= subscription.max_price:
                # Сохраняем билет в базе данных
                ticket = crud.create_ticket(db=db, ticket_data=ticket_info)
                
                # Отправляем уведомление пользователю
                message = f"""
                🎟 *Найден дешевый билет!*
                *Направление:* {ticket.from_city} → {ticket.to_city}
                *Цена:* {ticket.price} руб.
                *Дата:* {ticket.flight_date}
                [Купить билет]({ticket.link})
                """
                telegram_bot.send_notification(subscription.user.telegram_id, message)
                
                # Сохраняем уведомление в базе данных
                notification_data = {
                    "user_id": subscription.user_id,
                    "ticket_id": ticket.id,
                    "status": "sent"
                }
                crud.create_notification(db=db, notification=notification_data)
tests/

Пакет для модульных тестов приложения.
test_users.py: тесты для пользователей.
test_subscriptions.py: тесты для подписок.
test_tickets.py: тесты для билетов.
test_notifications.py: тесты для уведомлений.
requirements.txt

Содержит список зависимостей проекта.

fastapi
uvicorn
sqlalchemy
psycopg2-binary
pydantic
requests
python-telegram-bot
alembic
Пошаговое руководство по реализации
1. Настройка проекта

Установи виртуальное окружение и установи все зависимости из requirements.txt.
Создай базу данных PostgreSQL и настрой подключение в config.py.
2. Модели и база данных

Определи все модели данных в models.py.
Инициализируй базу данных, создав необходимые таблицы.
3. Схемы данных

Создай schemas.py с Pydantic-моделями для валидации входящих и исходящих данных.
4. CRUD-операции

Реализуй функции для базовых операций с данными в crud.py для каждой модели.
5. Маршруты API

Напиши эндпоинты в routers/ для пользователей, подписок и билетов.
Убедись, что они используют соответствующие CRUD-функции и возвращают корректные данные.
6. Интеграция с Aviasales API

Реализуй класс AviasalesAPI в services/aviasales_api.py.
Напиши функции для получения данных о билетах по заданным направлениям.
7. Телеграм-бот

Реализуй класс TelegramBot в services/telegram_bot.py.
Убедись, что бот может отправлять сообщения пользователям по их telegram_id.
8. Уведомления

В utils/notifications.py напиши функцию, которая будет:
Проверять новые билеты по подпискам.
Отправлять уведомления пользователям о подходящих билетах.
Сохранять информацию об отправленных уведомлениях.
Настрой периодическое выполнение этой функции (например, с помощью планировщика задач или асинхронных фоновых задач FastAPI).
9. Тестирование

Напиши тесты в tests/ для проверки основных функций и эндпоинтов.
10. Документация и запуск

Обнови README.md с инструкциями по установке и запуску проекта.
Используй встроенную документацию FastAPI по адресу /docs для просмотра и тестирования API.
Дополнительные советы
Логирование: Добавь логирование важнейших событий для упрощения отладки и мониторинга.
Обработка ошибок: Удели внимание обработке исключений и возвращай понятные сообщения об ошибках.
Безопасность: Убедись, что запросы корректно валидируются и защищены от возможных уязвимостей.
Переменные окружения: Используй переменные окружения для хранения конфиденциальной информации (API-ключи, токены).
Автоматизация миграций: Рассмотри использование Alembic для управления миграциями базы данных.
Начало работы
Установка зависимостей:

pip install -r requirements.txt
Запуск приложения:

uvicorn app.main:app --reload
Просмотр документации API:   Открой в браузере http://localhost:8000/docs.
Заключение
Следуя этой структуре, ты сможешь эффективно организовать свой код и успешно реализовать проект. Не забывай регулярно коммитить изменения в систему контроля версий и тестировать каждую часть приложения по мере ее разработки.
Если возникнут вопросы или нужна будет помощь с каким-либо модулем, обращайся! 😊
НО код в avisales.api теперь такой:
import requests
from typing import Dict, Any, Optional
from ..config import AVIASALES_API_KEY
class AviasalesAPI:
def init(self):
self.api_key = AVIASALES_API_KEY
self.base_url = "https://api.travelpayouts.com/aviasales/v3/"

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 = 30,
    page: int = 1,
    sorting: str = 'price',
    unique: bool = False
) -> Dict[str, Any]:
    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
        return response.json()
    except requests.exceptions.RequestException as e:
        raise Exception(f"Ошибка при запросе к Aviasales API: {e}")
Как мне сделать так, чтобы данные по этому запросу передавались в мою базу данных в postgresql?