init
This commit is contained in:
13
src/adapter/postgres/postgres.py
Normal file
13
src/adapter/postgres/postgres.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import uuid
|
||||
|
||||
from shared.postgres.postgres_helper import DatabaseConfig, DatabaseHelper
|
||||
from src import domain
|
||||
|
||||
|
||||
class Postgres(DatabaseHelper):
|
||||
def __init__(self, config: DatabaseConfig, migrations_path: str = 'migration/versions') -> None:
|
||||
super().__init__(config, migrations_path)
|
||||
|
||||
async def get_user(self, user_id: uuid.UUID) -> domain.User | None:
|
||||
async with self.session_getter() as session:
|
||||
return await session.get(domain.User, user_id)
|
||||
6
src/adapter/telegram/telegram.py
Normal file
6
src/adapter/telegram/telegram.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from shared.telegram import TelegramHelper
|
||||
|
||||
|
||||
class Telegram(TelegramHelper):
|
||||
async def send_message(self, chat_id: int, text: str) -> None:
|
||||
await self.bot.send_message(chat_id=chat_id, text=text)
|
||||
19
src/config.py
Normal file
19
src/config.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
from shared.config_helper import load_settings
|
||||
from shared.logger import LoggerConfig
|
||||
from shared.postgres import DatabaseConfig
|
||||
from shared.telegram.telegram_bot import TelegramConfig
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_file='.env', case_sensitive=False, env_nested_delimiter='__'
|
||||
)
|
||||
|
||||
db: DatabaseConfig
|
||||
logger: LoggerConfig
|
||||
telegram: TelegramConfig
|
||||
|
||||
|
||||
settings = load_settings(Settings)
|
||||
17
src/controller/http/user.py
Normal file
17
src/controller/http/user.py
Normal file
@@ -0,0 +1,17 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from src import dto
|
||||
from src.dependencies import get_usecase
|
||||
from src.usecase.usecase import Usecase
|
||||
|
||||
user_router = APIRouter(prefix='/users', dependencies=[Depends(get_usecase)])
|
||||
|
||||
|
||||
@user_router.get('/{user_id}')
|
||||
async def get_user(
|
||||
user_id: uuid.UUID,
|
||||
usecase: Usecase = Depends(get_usecase),
|
||||
) -> dto.GetUserOutput:
|
||||
return await usecase.get_user(user_id)
|
||||
83
src/controller/telegram/bot.py
Normal file
83
src/controller/telegram/bot.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import uuid
|
||||
|
||||
from aiogram import Router
|
||||
from aiogram.filters import Command
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.dependencies import get_usecase
|
||||
|
||||
telegram_router = Router()
|
||||
|
||||
|
||||
@telegram_router.message(Command('start'))
|
||||
async def cmd_start(message: Message) -> None:
|
||||
await message.answer(
|
||||
'Привет! Я бот для работы с пользователями.\n\n'
|
||||
'Доступные команды:\n'
|
||||
'/getuser <user_id> - получить информацию о пользователе\n'
|
||||
'/notify <user_id> <message> - отправить уведомление пользователю'
|
||||
)
|
||||
|
||||
|
||||
@telegram_router.message(Command('getuser'))
|
||||
async def cmd_get_user(message: Message) -> None:
|
||||
usecase = get_usecase()
|
||||
|
||||
if not message.text:
|
||||
await message.answer('Ошибка: не удалось прочитать текст команды')
|
||||
return
|
||||
|
||||
args = message.text.split()
|
||||
if len(args) < 2:
|
||||
await message.answer(
|
||||
'Использование: /getuser <user_id>\n' 'Пример: /getuser 123e4567-e89b-12d3-a456-426614174000'
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(args[1])
|
||||
except ValueError:
|
||||
await message.answer('Ошибка: неверный формат UUID')
|
||||
return
|
||||
|
||||
try:
|
||||
user = await usecase.get_user(user_id)
|
||||
await message.answer(
|
||||
f'Пользователь найден:\n'
|
||||
f'ID: {user.id}\n'
|
||||
f'Имя: {user.name}\n'
|
||||
f'Создан: {user.created_at}\n'
|
||||
f'Обновлён: {user.updated_at}'
|
||||
)
|
||||
except Exception as e:
|
||||
await message.answer(f'Ошибка при получении пользователя: {str(e)}')
|
||||
|
||||
|
||||
@telegram_router.message(Command('notify'))
|
||||
async def cmd_notify_user(message: Message) -> None:
|
||||
usecase = get_usecase()
|
||||
|
||||
if not message.text or not message.from_user:
|
||||
await message.answer('Ошибка: не удалось прочитать команду')
|
||||
return
|
||||
|
||||
args = message.text.split(maxsplit=2)
|
||||
if len(args) < 3:
|
||||
await message.answer(
|
||||
'Использование: /notify <user_id> <message>\n'
|
||||
'Пример: /notify 123e4567-e89b-12d3-a456-426614174000 Привет!'
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(args[1])
|
||||
notification_text = args[2]
|
||||
except ValueError:
|
||||
await message.answer('Ошибка: неверный формат UUID')
|
||||
return
|
||||
|
||||
try:
|
||||
await usecase.notify_user(user_id, message.from_user.id, notification_text)
|
||||
await message.answer('Уведомление отправлено!')
|
||||
except Exception as e:
|
||||
await message.answer(f'Ошибка при отправке уведомления: {str(e)}')
|
||||
36
src/dependencies.py
Normal file
36
src/dependencies.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from collections.abc import AsyncGenerator, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.usecase.usecase import Usecase
|
||||
|
||||
_usecase_instance: Usecase | None = None
|
||||
_session_context_manager: Callable[[], AbstractAsyncContextManager[AsyncSession]] | None = None
|
||||
|
||||
|
||||
def set_usecase(usecase: Usecase) -> None:
|
||||
global _usecase_instance
|
||||
_usecase_instance = usecase
|
||||
|
||||
|
||||
def get_usecase() -> Usecase:
|
||||
if _usecase_instance is None:
|
||||
raise RuntimeError('Usecase not initialized. Call set_usecase() first')
|
||||
|
||||
return _usecase_instance
|
||||
|
||||
|
||||
def set_get_session(
|
||||
session_context_manager: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
global _session_context_manager
|
||||
_session_context_manager = session_context_manager
|
||||
|
||||
|
||||
async def get_session() -> AsyncGenerator[AsyncSession]:
|
||||
if _session_context_manager is None:
|
||||
raise RuntimeError('get_session not initialized. Call set_get_session() first')
|
||||
|
||||
async with _session_context_manager() as session:
|
||||
yield session
|
||||
5
src/domain/__init__.py
Normal file
5
src/domain/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
__all__ = ('Base', 'User', 'UserNotFound')
|
||||
|
||||
from .base import Base
|
||||
from .error import UserNotFound
|
||||
from .user import User
|
||||
21
src/domain/base.py
Normal file
21
src/domain/base.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
__abstract__ = True
|
||||
|
||||
# Авто имя таблиц по названию класса
|
||||
@declared_attr.directive
|
||||
def __tablename__(cls) -> str:
|
||||
return f'{cls.__name__.lower()}s'
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(primary_key=True, default=uuid.uuid4)
|
||||
created_at: Mapped[datetime.datetime] = mapped_column(server_default=func.now())
|
||||
updated_at: Mapped[datetime.datetime] = mapped_column(
|
||||
server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
deleted_at: Mapped[datetime.datetime | None]
|
||||
7
src/domain/error.py
Normal file
7
src/domain/error.py
Normal file
@@ -0,0 +1,7 @@
|
||||
import uuid
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
|
||||
|
||||
def UserNotFound(user_id: uuid.UUID) -> HTTPException:
|
||||
return HTTPException(status.HTTP_404_NOT_FOUND, f'User {user_id} not found')
|
||||
7
src/domain/user.py
Normal file
7
src/domain/user.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from sqlalchemy.orm import Mapped
|
||||
|
||||
from src import domain
|
||||
|
||||
|
||||
class User(domain.Base):
|
||||
name: Mapped[str]
|
||||
3
src/dto/__init__.py
Normal file
3
src/dto/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
__all__ = ('GetUserInput', 'GetUserOutput')
|
||||
|
||||
from .get_user import GetUserInput, GetUserOutput
|
||||
12
src/dto/get_user.py
Normal file
12
src/dto/get_user.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import uuid
|
||||
|
||||
import pydantic
|
||||
|
||||
|
||||
class GetUserInput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
|
||||
|
||||
class GetUserOutput(pydantic.BaseModel):
|
||||
user_id: uuid.UUID
|
||||
name: str
|
||||
51
src/main.py
Normal file
51
src/main.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRouter
|
||||
|
||||
from shared import logger
|
||||
from src import dependencies
|
||||
from src.adapter.postgres.postgres import Postgres
|
||||
from src.adapter.telegram.telegram import Telegram
|
||||
from src.config import settings
|
||||
from src.controller.http.user import user_router
|
||||
from src.controller.telegram.bot import telegram_router
|
||||
from src.usecase.usecase import Usecase
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[None]:
|
||||
await postgres.connect()
|
||||
await telegram.start_polling()
|
||||
|
||||
yield
|
||||
|
||||
await telegram.stop_polling()
|
||||
await postgres.close()
|
||||
|
||||
|
||||
logger.init(settings.logger)
|
||||
|
||||
# Dependencies
|
||||
postgres = Postgres(settings.db)
|
||||
dependencies.set_get_session(postgres.session_getter)
|
||||
|
||||
telegram = Telegram(settings.telegram, telegram_router)
|
||||
|
||||
usecase = Usecase(
|
||||
database=postgres,
|
||||
telegram_writer=telegram,
|
||||
)
|
||||
dependencies.set_usecase(usecase)
|
||||
|
||||
app: FastAPI = FastAPI(lifespan=lifespan, version=settings.logger.APP_VERSION)
|
||||
|
||||
# Routing
|
||||
api_router = APIRouter(prefix='/api')
|
||||
|
||||
v1_router = APIRouter(prefix='/v1')
|
||||
v1_router.include_router(user_router)
|
||||
api_router.include_router(v1_router)
|
||||
|
||||
app.include_router(api_router)
|
||||
18
src/usecase/get_user.py
Normal file
18
src/usecase/get_user.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain, dto
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.usecase.usecase import Usecase
|
||||
|
||||
|
||||
async def get_user(self: 'Usecase', user_id: uuid.UUID) -> dto.GetUserOutput:
|
||||
user: domain.User | None = await self.database.get_user(user_id=user_id)
|
||||
if user is None:
|
||||
raise domain.UserNotFound(user_id)
|
||||
|
||||
return dto.GetUserOutput(
|
||||
user_id=user.id,
|
||||
name=user.name,
|
||||
)
|
||||
22
src/usecase/notify_user.py
Normal file
22
src/usecase/notify_user.py
Normal file
@@ -0,0 +1,22 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from src.usecase.usecase import Usecase
|
||||
|
||||
|
||||
async def notify_user(self: 'Usecase', user_id: uuid.UUID, chat_id: int, message: str) -> None:
|
||||
"""Send notification to user via Telegram"""
|
||||
if self.telegram_writer is None:
|
||||
raise RuntimeError('TelegramWriter not configured')
|
||||
|
||||
# Get user from database
|
||||
user = await self.database.get_user(user_id)
|
||||
if user is None:
|
||||
raise domain.UserNotFound(user_id)
|
||||
|
||||
# Send personalized message
|
||||
text = f'Привет, {user.name}!\n\n{message}'
|
||||
await self.telegram_writer.send_message(chat_id=chat_id, text=text)
|
||||
25
src/usecase/usecase.py
Normal file
25
src/usecase/usecase.py
Normal file
@@ -0,0 +1,25 @@
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
from src import domain
|
||||
|
||||
from .get_user import get_user
|
||||
from .notify_user import notify_user
|
||||
|
||||
|
||||
class Database(Protocol):
|
||||
async def get_user(self, user_id: uuid.UUID) -> domain.User | None: ...
|
||||
|
||||
|
||||
class TelegramWriter(Protocol):
|
||||
async def send_message(self, chat_id: int, text: str) -> None: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class Usecase:
|
||||
database: Database
|
||||
telegram_writer: TelegramWriter
|
||||
|
||||
get_user = get_user
|
||||
notify_user = notify_user
|
||||
Reference in New Issue
Block a user