Files
tgex-backend/CLAUDE.md
2025-12-01 13:20:08 +03:00

339 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
This is a FastAPI + Telegram Bot backend application for managing Telegram channel integrations. The project uses:
- **FastAPI** for HTTP API endpoints
- **aiogram** for Telegram bot functionality
- **SQLAlchemy 2.0** (async) for database operations
- **PostgreSQL** with asyncpg driver
- **Alembic** for database migrations
- **uv** for Python package management
## Development Commands
### Environment Setup
```bash
# Install dependencies (using uv)
uv sync
# Start PostgreSQL (via Docker Compose)
docker-compose up -d
# Run database migrations
uv run alembic upgrade head
```
### Running the Application
```bash
# Start the FastAPI server
uv run uvicorn src.main:app --host 0.0.0.0 --port 8000 --reload
```
### Code Quality
```bash
# Run linter (Ruff)
uv run ruff check .
# Format code
uv run ruff format .
# Type checking (mypy with strict mode)
uv run mypy .
```
### Testing
```bash
# Run all tests
uv run pytest
# Run a single test file
uv run pytest tests/test_file.py
# Run a specific test
uv run pytest tests/test_file.py::test_function_name
```
### Database Migrations
```bash
# Create a new migration
uv run alembic revision --autogenerate -m "description"
# Apply migrations
uv run alembic upgrade head
# Rollback one migration
uv run alembic downgrade -1
```
## Architecture
### Layered Architecture
The codebase follows a clean architecture pattern with clear separation of concerns:
1. **Domain Layer** (`src/domain/`)
- SQLAlchemy ORM models that represent database entities
- All models inherit from `domain.Base` which provides:
- Auto-generated `id` (UUID primary key)
- Timestamps: `created_at`, `updated_at`, `deleted_at`
- Automatic table naming (pluralized lowercase class name)
- Timezone-aware datetime fields
2. **Use Case Layer** (`src/usecase/`)
- Business logic functions organized by feature
- Each use case is a standalone function with signature: `async def use_case_name(self: 'Usecase', ...) -> ReturnType`
- Depends on Protocol interfaces (Database, TelegramBotWriter, JWTEncoder)
- Use cases are assembled in the `Usecase` dataclass for dependency injection
- **IMPORTANT RULES:**
- Один use case отвечает за полноценный сценарий «от триггера до завершения»
- Use case НЕ должен вызывать другие use cases (общую логику выносить в хелперы/сервисы)
- Use case содержит ВСЮ бизнес-логику, включая управление состояниями диалога
- Use case может отправлять сообщения пользователям через `self.telegram_bot` protocol
- Всегда оборачивать операции БД в `async with self.database.transaction()`
3. **Adapter Layer** (`src/adapter/`)
- Concrete implementations of protocol interfaces:
- `Postgres`: Database operations (implements Database protocol)
- `Telegram`: Telegram bot integration (implements TelegramWriter protocol)
- `JWT`: Token encoding/decoding (implements JWTEncoder protocol)
4. **Controller Layer** (`src/controller/`)
- **HTTP controllers** (`src/controller/http/`): FastAPI route handlers
- **Telegram callbacks** (`src/controller/telegram_callback/`): Telegram event handlers
- **ВАЖНО: Controllers - это тонкий слой роутинга**
- Контроллер только извлекает данные из событий (HTTP request / Telegram update)
- Валидирует базовые параметры (user exists, message not empty)
- Вызывает соответствующий use case
- НЕ содержит бизнес-логику
- НЕ обращается к базе данных напрямую
- НЕ отправляет сообщения пользователям
- Все handlers регистрируются через декораторы на `telegram_callback_router`
- Handlers импортируются в `__init__.py` для auto-регистрации
5. **DTO Layer** (`src/dto/`)
- Pydantic models for request/response validation
- Separate from domain models to decouple API contracts from database schema
### Key Architectural Patterns
**Dependency Injection via Protocols:**
- Use cases depend on Protocol interfaces, not concrete implementations
- Allows for easy testing and swapping implementations
- See `src/usecase/__init__.py` for protocol definitions
**Transaction Management:**
- Database uses context-aware transactions via `DatabaseBase.transaction()`
- Session stored in ContextVar (`_session_ctx`) for implicit session access
- Always use `async with database.transaction()` for database operations
- The session is accessible via `database.session` within transaction context
**Shared Base Classes:**
- `shared/datebase_base.py`: Base database class with connection pooling and migration checking
- `shared/telegram_base.py`: Base Telegram bot class with polling lifecycle
- `shared/logger/`: Structured logging with JSON and console formatters
**Configuration:**
- All config in `src/config.py` using Pydantic Settings
- Environment variables loaded from `.env` file
- Nested config via double underscore: `DB__URL`, `TELEGRAM__TOKEN`
### Application Lifecycle
1. **Startup** (in `src/main.py`):
- Logger initialized
- Adapters instantiated (Postgres, Telegram, JWT)
- Usecase dataclass created with adapter dependencies
- FastAPI lifespan context:
- Connect to database (includes migration check)
- Start Telegram bot polling
2. **Request Flow**:
- HTTP: FastAPI route → Controller → Usecase → Adapter → Database
- Telegram: Event → Telegram callback → Usecase → Adapter → Database
3. **Shutdown**:
- Stop Telegram bot polling
- Close database connections
### Important Implementation Details
**Database Session Management:**
- DO NOT create sessions manually
- Use `async with database.transaction()` which handles begin/commit/rollback
- Access session via `database.session` property within transaction context
- Session lifecycle is managed by ContextVars for thread-safety
**Telegram Bot Integration:**
- Bot runs in polling mode (not webhook)
- Routers registered in `src/controller/telegram_callback/`
- Base class handles graceful shutdown of polling task
**Authentication:**
- JWT-based authentication for HTTP endpoints
- Login flow: Telegram bot → Login token → JWT access token
- Use `get_current_user` dependency for protected routes
**Code Style:**
- Single quotes for strings (configured in Ruff)
- Line length: 120 characters
- Python 3.13+ syntax
- Strict mypy typing enabled
### Telegram Bot Patterns
**State Management:**
- Состояния хранятся в БД через модель `TelegramState`
- Используется `TelegramStateEnum` для типизации состояний (не строки!)
- Database protocol методы: `get_telegram_state()`, `set_telegram_state()`, `clear_telegram_state()`
**Controller Pattern (Telegram):**
```python
from aiogram import F
from aiogram.filters import Command
from aiogram.types import Message
@telegram_callback_router.message(Command('command_name'))
async def cmd_handler(message: Message) -> None:
if not message.from_user:
log.error('Failed to get user data')
return
usecase = dependencies.get_usecase()
await usecase.some_use_case(
telegram_id=message.from_user.id,
chat_id=message.chat.id
)
```
**Use Case Pattern (Telegram):**
```python
from src import domain
async def some_use_case(self: 'Usecase', telegram_id: int, chat_id: int) -> None:
async with self.database.transaction():
# 1. Получить/проверить пользователя
user = await self.database.get_user(telegram_id=telegram_id)
if not user:
await self.telegram_bot.send_message('Не авторизован', chat_id=chat_id)
return
# 2. Получить состояние (если нужно)
state = await self.database.get_telegram_state(telegram_id)
# 3. Бизнес-логика
# ...
# 4. Обновить состояние
await self.database.set_telegram_state(
telegram_id=telegram_id,
state=domain.TelegramStateEnum.SOME_STATE,
context={'key': 'value'}
)
# 5. Отправить сообщение
await self.telegram_bot.send_message('✅ Успешно', chat_id=chat_id)
```
**Inline Keyboard Pattern:**
```python
from aiogram.types import InlineKeyboardButton
# В use case:
buttons = [
[InlineKeyboardButton(text='Кнопка', callback_data='callback_id:value')]
for item in items
]
await self.telegram_bot.send_message_with_inline_keyboard(
'Выберите опцию:', chat_id=chat_id, buttons=buttons
)
```
**Callback Query Handler:**
```python
@telegram_callback_router.callback_query(F.data.startswith('prefix:'))
async def callback_handler(callback: CallbackQuery) -> None:
if not callback.from_user or not callback.data:
return
usecase = dependencies.get_usecase()
await usecase.handle_callback(
telegram_id=callback.from_user.id,
chat_id=callback.message.chat.id,
callback_data=callback.data,
message_id=callback.message.message_id
)
await callback.answer() # Убрать "loading" на кнопке
```
**Text Message Filters:**
```python
# Обрабатывать только не-команды
@telegram_callback_router.message(F.text & ~F.text.startswith('/'))
async def text_handler(message: Message) -> None:
# Этот handler НЕ сработает для команд типа /start
...
```
**Важные правила для Telegram handlers:**
- Специфичные фильтры (Command) регистрировать РАНЬШЕ общих (F.text)
- Использовать `~F.text.startswith('/')` для исключения команд в текстовых handlers
- Всегда проверять `message.from_user` перед использованием
- Callback handlers должны вызывать `await callback.answer()` в конце
- НЕ импортировать aiogram типы в usecase layer (только через TYPE_CHECKING в Protocol)
### Adding New Protocol Methods
When adding new functionality that requires protocol methods:
**1. Define Protocol in usecase layer (`src/usecase/__init__.py`):**
```python
if typing.TYPE_CHECKING:
from aiogram.types import InlineKeyboardButton # Импорт только для типов
class TelegramBotWriter(typing.Protocol):
async def send_message_with_inline_keyboard(
self, text: str, chat_id: int, buttons: list[list['InlineKeyboardButton']]
) -> None: ...
```
**2. Implement in adapter (`src/adapter/telegram_bot.py`):**
```python
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
class TelegramBot(TelegramBase):
async def send_message_with_inline_keyboard(
self, text: str, chat_id: int, buttons: list[list[InlineKeyboardButton]]
) -> None:
keyboard = InlineKeyboardMarkup(inline_keyboard=buttons)
await self.bot.send_message(chat_id=chat_id, text=text, reply_markup=keyboard)
```
**3. Use in use case:**
```python
from aiogram.types import InlineKeyboardButton # Можно импортировать в use case
buttons = [[InlineKeyboardButton(text='Text', callback_data='data')]]
await self.telegram_bot.send_message_with_inline_keyboard('Message', chat_id, buttons)
```
**Типизация Enum в Domain:**
- Используйте `enum.StrEnum` для строковых enum (Python 3.11+)
- Enum значения в lowercase с underscores
- Передавайте enum в SQLAlchemy через `Enum(EnumClass)`
```python
import enum
from sqlalchemy import Enum
from sqlalchemy.orm import Mapped, mapped_column
class TelegramStateEnum(enum.StrEnum):
CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel'
CREATIVE_WAITING_NAME = 'creative_waiting_name'
class TelegramState(Base):
state: Mapped[TelegramStateEnum] = mapped_column(Enum(TelegramStateEnum), nullable=False)
```