This commit is contained in:
2025-09-14 22:09:39 +05:00
parent 8ad2bfbe59
commit 3c418a97b4
30 changed files with 3404 additions and 0 deletions

27
api/__init__.py Normal file
View File

@@ -0,0 +1,27 @@
"""
API пакет для Telegram Bot
"""
from .main import app
from .routers import users_router, messages_router, settings_router
from .schemas import (
UserCreate, UserUpdate, UserResponse,
MessageCreate, MessageResponse,
BotSettingCreate, BotSettingUpdate, BotSettingResponse
)
__version__ = "1.0.0"
__all__ = [
"app",
"users_router",
"messages_router",
"settings_router",
"UserCreate",
"UserUpdate",
"UserResponse",
"MessageCreate",
"MessageResponse",
"BotSettingCreate",
"BotSettingUpdate",
"BotSettingResponse"
]

37
api/config.py Normal file
View File

@@ -0,0 +1,37 @@
"""
Конфигурация для API приложения
"""
import os
from dotenv import load_dotenv
# Загрузка переменных окружения из .env файла
load_dotenv()
class APIConfig:
"""Конфигурация API приложения"""
# Основные настройки
TITLE = "Telegram Bot API"
DESCRIPTION = "API для взаимодействия с Telegram ботом и базой данных"
VERSION = "1.0.0"
# Настройки сервера
HOST = os.getenv("API_HOST", "0.0.0.0")
PORT = int(os.getenv("API_PORT", "8000"))
RELOAD = os.getenv("API_RELOAD", "true").lower() == "true"
# Настройки CORS
CORS_ORIGINS = ["*"] # В продакшене стоит ограничить
CORS_CREDENTIALS = True
CORS_METHODS = ["*"]
CORS_HEADERS = ["*"]
# Настройки логирования
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
# API префикс
API_V1_PREFIX = "/api/v1"
# Глобальный экземпляр конфигурации
config = APIConfig()

95
api/main.py Normal file
View File

@@ -0,0 +1,95 @@
"""
FastAPI приложение для взаимодействия с Telegram ботом
"""
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from contextlib import asynccontextmanager
import logging
import sys
import os
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database.database import db, get_db_session, init_database, test_db_connection
from database.models import User, Message, BotSettings
from api.routers import users_router, messages_router, settings_router
from api.schemas import HealthCheckResponse, RootResponse
from api.config import config
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Управление жизненным циклом приложения"""
# Инициализация при запуске
logger.info("Запуск FastAPI приложения...")
# Проверка подключения к БД
if not test_db_connection():
logger.error("Не удалось подключиться к базе данных!")
raise Exception("Database connection failed")
# Создание таблиц если они не существуют
try:
init_database()
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Ошибка инициализации базы данных: {e}")
raise
yield
# Очистка при завершении
logger.info("Завершение работы FastAPI приложения...")
db.close()
# Создание FastAPI приложения
app = FastAPI(
title=config.TITLE,
description=config.DESCRIPTION,
version=config.VERSION,
lifespan=lifespan
)
# Настройка CORS
app.add_middleware(
CORSMiddleware,
allow_origins=config.CORS_ORIGINS,
allow_credentials=config.CORS_CREDENTIALS,
allow_methods=config.CORS_METHODS,
allow_headers=config.CORS_HEADERS,
)
# Подключение роутеров
app.include_router(users_router, prefix=config.API_V1_PREFIX)
app.include_router(messages_router, prefix=config.API_V1_PREFIX)
app.include_router(settings_router, prefix=config.API_V1_PREFIX)
@app.get("/", response_model=RootResponse)
async def root():
"""Корневой эндпоинт"""
return RootResponse(
message=config.TITLE,
version=config.VERSION,
status="running"
)
@app.get("/health", response_model=HealthCheckResponse)
async def health_check():
"""Проверка состояния приложения"""
db_status = test_db_connection()
return HealthCheckResponse(
status="healthy" if db_status else "unhealthy",
database="connected" if db_status else "disconnected"
)
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host=config.HOST, port=config.PORT, reload=config.RELOAD)

259
api/routers.py Normal file
View File

@@ -0,0 +1,259 @@
"""
API маршруты для работы с пользователями, сообщениями и настройками
"""
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.orm import Session
from typing import List, Optional
import sys
import os
# Добавляем путь к корневой директории проекта
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from database.database import db
from database.models import User, Message, BotSettings
from api.schemas import (
UserCreate, UserUpdate, UserResponse,
MessageCreate, MessageResponse,
BotSettingCreate, BotSettingUpdate, BotSettingResponse,
MessageResponseModel, ErrorResponse
)
# Зависимость для получения сессии БД
def get_db():
"""Получение сессии базы данных"""
session = db.get_session_sync()
try:
yield session
finally:
session.close()
# Роутер для работы с пользователями
users_router = APIRouter(prefix="/users", tags=["Users"])
@users_router.get("/{telegram_id}", response_model=UserResponse)
async def get_user_by_telegram_id(telegram_id: int, db_session: Session = Depends(get_db)):
"""Получение пользователя по Telegram ID"""
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
return user
@users_router.get("/", response_model=List[UserResponse])
async def get_all_users(
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
is_active: Optional[bool] = Query(default=None),
db_session: Session = Depends(get_db)
):
"""Получение списка всех пользователей с фильтрацией"""
query = db_session.query(User)
if is_active is not None:
query = query.filter(User.is_active == is_active)
users = query.offset(offset).limit(limit).all()
return users
@users_router.post("/", response_model=UserResponse, status_code=status.HTTP_201_CREATED)
async def create_user(user_data: UserCreate, db_session: Session = Depends(get_db)):
"""Создание нового пользователя"""
# Проверяем, существует ли уже пользователь
existing_user = db_session.query(User).filter(User.telegram_id == user_data.telegram_id).first()
if existing_user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Пользователь уже существует"
)
# Создаем нового пользователя
new_user = User(**user_data.model_dump())
db_session.add(new_user)
db_session.commit()
db_session.refresh(new_user)
return new_user
@users_router.put("/{telegram_id}", response_model=UserResponse)
async def update_user(
telegram_id: int,
user_data: UserUpdate,
db_session: Session = Depends(get_db)
):
"""Обновление данных пользователя"""
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
# Обновляем только переданные поля
update_data = user_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(user, field, value)
db_session.commit()
db_session.refresh(user)
return user
@users_router.delete("/{telegram_id}", response_model=MessageResponseModel)
async def delete_user(telegram_id: int, db_session: Session = Depends(get_db)):
"""Деактивация пользователя (мягкое удаление)"""
user = db_session.query(User).filter(User.telegram_id == telegram_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
user.is_active = False
db_session.commit()
return MessageResponseModel(message="Пользователь деактивирован")
# Роутер для работы с сообщениями
messages_router = APIRouter(prefix="/messages", tags=["Messages"])
@messages_router.get("/", response_model=List[MessageResponse])
async def get_messages(
user_id: Optional[int] = Query(default=None),
message_type: Optional[str] = Query(default=None),
limit: int = Query(default=100, ge=1, le=1000),
offset: int = Query(default=0, ge=0),
db_session: Session = Depends(get_db)
):
"""Получение сообщений с фильтрацией"""
query = db_session.query(Message)
if user_id:
query = query.filter(Message.user_id == user_id)
if message_type:
query = query.filter(Message.message_type == message_type)
messages = query.order_by(Message.created_at.desc()).offset(offset).limit(limit).all()
return messages
@messages_router.get("/{message_id}", response_model=MessageResponse)
async def get_message(message_id: int, db_session: Session = Depends(get_db)):
"""Получение конкретного сообщения по ID"""
message = db_session.query(Message).filter(Message.id == message_id).first()
if not message:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Сообщение не найдено"
)
return message
@messages_router.post("/", response_model=MessageResponse, status_code=status.HTTP_201_CREATED)
async def create_message(message_data: MessageCreate, db_session: Session = Depends(get_db)):
"""Создание нового сообщения"""
# Проверяем, существует ли пользователь
user = db_session.query(User).filter(User.id == message_data.user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден"
)
new_message = Message(**message_data.model_dump())
db_session.add(new_message)
db_session.commit()
db_session.refresh(new_message)
return new_message
# Роутер для работы с настройками бота
settings_router = APIRouter(prefix="/settings", tags=["Bot Settings"])
@settings_router.get("/", response_model=dict)
async def get_all_settings(db_session: Session = Depends(get_db)):
"""Получение всех настроек бота в виде словаря"""
settings = db_session.query(BotSettings).all()
return {setting.key: setting.value for setting in settings}
@settings_router.get("/list", response_model=List[BotSettingResponse])
async def get_all_settings_list(db_session: Session = Depends(get_db)):
"""Получение всех настроек бота в виде списка объектов"""
settings = db_session.query(BotSettings).all()
return settings
@settings_router.get("/{key}", response_model=BotSettingResponse)
async def get_setting(key: str, db_session: Session = Depends(get_db)):
"""Получение конкретной настройки по ключу"""
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
if not setting:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Настройка не найдена"
)
return setting
@settings_router.post("/", response_model=BotSettingResponse, status_code=status.HTTP_201_CREATED)
async def create_or_update_setting(
setting_data: BotSettingCreate,
db_session: Session = Depends(get_db)
):
"""Создание или обновление настройки"""
existing_setting = db_session.query(BotSettings).filter(BotSettings.key == setting_data.key).first()
if existing_setting:
# Обновляем существующую настройку
update_data = setting_data.model_dump(exclude={"key"})
for field, value in update_data.items():
if value is not None:
setattr(existing_setting, field, value)
db_session.commit()
db_session.refresh(existing_setting)
return existing_setting
else:
# Создаем новую настройку
new_setting = BotSettings(**setting_data.model_dump())
db_session.add(new_setting)
db_session.commit()
db_session.refresh(new_setting)
return new_setting
@settings_router.put("/{key}", response_model=BotSettingResponse)
async def update_setting(
key: str,
setting_data: BotSettingUpdate,
db_session: Session = Depends(get_db)
):
"""Обновление существующей настройки"""
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
if not setting:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Настройка не найдена"
)
update_data = setting_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(setting, field, value)
db_session.commit()
db_session.refresh(setting)
return setting
@settings_router.delete("/{key}", response_model=MessageResponseModel)
async def delete_setting(key: str, db_session: Session = Depends(get_db)):
"""Удаление настройки"""
setting = db_session.query(BotSettings).filter(BotSettings.key == key).first()
if not setting:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Настройка не найдена"
)
db_session.delete(setting)
db_session.commit()
return MessageResponseModel(message=f"Настройка '{key}' удалена")

18
api/run_api.py Normal file
View File

@@ -0,0 +1,18 @@
"""
Скрипт для запуска API сервера
"""
import uvicorn
from api.config import config
if __name__ == "__main__":
print(f"Запуск {config.TITLE} v{config.VERSION}")
print(f"Документация будет доступна по адресу: http://{config.HOST}:{config.PORT}/docs")
uvicorn.run(
"api.main:app",
host=config.HOST,
port=config.PORT,
reload=config.RELOAD,
log_level=config.LOG_LEVEL.lower()
)

123
api/schemas.py Normal file
View File

@@ -0,0 +1,123 @@
"""
Pydantic схемы для валидации данных API
"""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional
from datetime import datetime
# Базовые схемы для пользователей
class UserBase(BaseModel):
"""Базовая схема пользователя"""
telegram_id: int = Field(..., description="Telegram ID пользователя")
username: Optional[str] = Field(None, max_length=255, description="Username в Telegram")
first_name: Optional[str] = Field(None, max_length=255, description="Имя пользователя")
last_name: Optional[str] = Field(None, max_length=255, description="Фамилия пользователя")
language_code: str = Field(default="ru", max_length=10, description="Код языка")
class UserCreate(UserBase):
"""Схема для создания пользователя"""
pass
class UserUpdate(BaseModel):
"""Схема для обновления пользователя"""
username: Optional[str] = Field(None, max_length=255)
first_name: Optional[str] = Field(None, max_length=255)
last_name: Optional[str] = Field(None, max_length=255)
language_code: Optional[str] = Field(None, max_length=10)
is_active: Optional[bool] = None
class UserResponse(UserBase):
"""Схема ответа с данными пользователя"""
model_config = ConfigDict(from_attributes=True)
id: int
is_active: bool
is_admin: bool
created_at: datetime
updated_at: Optional[datetime] = None
# Базовые схемы для сообщений
class MessageBase(BaseModel):
"""Базовая схема сообщения"""
telegram_message_id: int = Field(..., description="ID сообщения в Telegram")
user_id: int = Field(..., description="ID пользователя в нашей БД")
text: Optional[str] = Field(None, description="Текст сообщения")
message_type: str = Field(default="text", max_length=50, description="Тип сообщения")
class MessageCreate(MessageBase):
"""Схема для создания сообщения"""
pass
class MessageResponse(MessageBase):
"""Схема ответа с данными сообщения"""
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
# Базовые схемы для настроек бота
class BotSettingBase(BaseModel):
"""Базовая схема настройки бота"""
key: str = Field(..., max_length=255, description="Ключ настройки")
value: Optional[str] = Field(None, description="Значение настройки")
description: Optional[str] = Field(None, description="Описание настройки")
class BotSettingCreate(BotSettingBase):
"""Схема для создания настройки"""
pass
class BotSettingUpdate(BaseModel):
"""Схема для обновления настройки"""
value: Optional[str] = None
description: Optional[str] = None
class BotSettingResponse(BotSettingBase):
"""Схема ответа с данными настройки"""
model_config = ConfigDict(from_attributes=True)
id: int
created_at: datetime
updated_at: Optional[datetime] = None
# Схемы для общих ответов API
class MessageResponseModel(BaseModel):
"""Общая схема ответа с сообщением"""
message: str
status: str = "success"
class ErrorResponse(BaseModel):
"""Схема ответа с ошибкой"""
detail: str
status: str = "error"
class HealthCheckResponse(BaseModel):
"""Схема ответа проверки здоровья"""
status: str
database: str
class RootResponse(BaseModel):
"""Схема корневого ответа"""
message: str
version: str
status: str
# Схемы для пагинации
class PaginationParams(BaseModel):
"""Параметры пагинации"""
limit: int = Field(default=100, ge=1, le=1000, description="Количество элементов на странице")
offset: int = Field(default=0, ge=0, description="Смещение")
class PaginatedResponse(BaseModel):
"""Схема ответа с пагинацией"""
items: list
total: int
limit: int
offset: int
has_next: bool
has_prev: bool