From 02729ce81b80f966dfdc6891e0b68cc8beb26ea4 Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Thu, 11 Dec 2025 12:01:20 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=82=D0=B8=D0=BF=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20tortoise=20ORM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- migration/env.py | 8 +- pyproject.toml | 1 + shared/scripts/check_env.py | 112 -------------------------- src/adapter/postgres.py | 22 ++--- src/domain/base.py | 11 +++ src/domain/creative.py | 26 +++--- src/domain/external_channel.py | 27 ++++--- src/domain/login_token.py | 22 +++-- src/domain/placement.py | 51 ++++++------ src/domain/placement_views_history.py | 24 +++--- src/domain/subscriber.py | 12 +-- src/domain/subscription.py | 40 +++++---- src/domain/target_channel.py | 26 +++--- src/domain/telegram_state.py | 13 +-- src/domain/user.py | 16 +--- src/domain/user_conversation_state.py | 19 ----- uv.lock | 14 ++++ 17 files changed, 181 insertions(+), 263 deletions(-) delete mode 100755 shared/scripts/check_env.py create mode 100644 src/domain/base.py delete mode 100644 src/domain/user_conversation_state.py diff --git a/migration/env.py b/migration/env.py index 3c1a153..e1206ff 100644 --- a/migration/env.py +++ b/migration/env.py @@ -5,7 +5,11 @@ from sqlalchemy import engine_from_config, pool from src.config import settings from src.domain import * # noqa -from src.domain import Base + +try: + from src.domain import Base as SqlBase # type: ignore[attr-defined] +except ImportError: + SqlBase = None config = context.config @@ -13,7 +17,7 @@ if config.config_file_name: fileConfig(config.config_file_name) config.set_main_option('sqlalchemy.url', f'{settings.db.URL}?async_fallback=True') -target_metadata = Base.metadata +target_metadata = SqlBase.metadata if SqlBase else None def run_migrations_offline() -> None: diff --git a/pyproject.toml b/pyproject.toml index 8792395..6f17220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ dependencies = [ "python-multipart>=0.0.20", "sqlalchemy[asyncio]>=2.0.38", "tortoise-orm>=0.25.1", + "tortoise-orm-stubs>=1.0.2", "uvicorn>=0.38.0", ] diff --git a/shared/scripts/check_env.py b/shared/scripts/check_env.py deleted file mode 100755 index edd30fa..0000000 --- a/shared/scripts/check_env.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -import sys -from pathlib import Path - - -def parse_env_file(file_path: Path) -> set[str]: - if not file_path.exists(): - return set() - - variables = set() - with open(file_path) as f: - for line in f: - line = line.strip() - if line and not line.startswith('#') and '=' in line: - variables.add(line.split('=', 1)[0].strip()) - return variables - - -def get_pydantic_settings_fields() -> tuple[set[str], set[str]]: - sys.path.insert(0, str(Path(__file__).parent.parent.parent)) - from pydantic import BaseModel - - from src.config import Settings - - all_fields, required_fields = set(), set() - - def extract_fields(model: type[BaseModel], prefix: str = '') -> None: - for field_name, field_info in model.model_fields.items(): - env_var_name = f'{prefix}{field_name}'.upper() - field_type = field_info.annotation - if hasattr(field_type, '__origin__'): - field_type = field_type.__args__[0] if field_type.__args__ else field_type - - try: - if isinstance(field_type, type) and issubclass(field_type, BaseModel): - extract_fields(field_type, prefix=f'{env_var_name}__') - else: - all_fields.add(env_var_name) - if field_info.is_required(): - required_fields.add(env_var_name) - except TypeError: - all_fields.add(env_var_name) - if field_info.is_required(): - required_fields.add(env_var_name) - - extract_fields(Settings) - return all_fields, required_fields - - -def main() -> int: - project_root = Path(__file__).parent.parent.parent - env_file = project_root / '.env' - env_example_file = project_root / '.env.example' - - if not env_example_file.exists(): - print('❌ Файл .env.example не найден') - return 1 - - env_vars = parse_env_file(env_file) - env_example_vars = parse_env_file(env_example_file) - all_pydantic_fields, required_pydantic_fields = get_pydantic_settings_fields() - - print('🔍 Проверка соответствия .env и .env.example\n') - has_errors = False - - missing_in_env = env_example_vars - env_vars - if missing_in_env: - has_errors = True - print('❌ Переменные из .env.example отсутствуют в .env:') - for var in sorted(missing_in_env): - print(f' - {var}') - print() - - extra_in_env = env_vars - env_example_vars - if extra_in_env: - has_errors = True - print('⚠️ Переменные в .env, которых нет в .env.example:') - for var in sorted(extra_in_env): - print(f' - {var}') - print() - - missing_in_pydantic = env_example_vars - all_pydantic_fields - if missing_in_pydantic: - has_errors = True - print('❌ Переменные из .env.example не определены в Pydantic Settings (src/config.py):') - for var in sorted(missing_in_pydantic): - print(f' - {var}') - print() - - missing_in_example = required_pydantic_fields - env_example_vars - if missing_in_example: - has_errors = True - print('❌ Обязательные поля Pydantic Settings отсутствуют в .env.example:') - for var in sorted(missing_in_example): - print(f' - {var}') - print() - - if not has_errors: - print('✅ Все проверки пройдены успешно!') - print(f' - Переменных в .env.example: {len(env_example_vars)}') - print(f' - Переменных в .env: {len(env_vars)}') - print( - f' - Полей в Pydantic Settings (всего/обязательных): {len(all_pydantic_fields)}/{len(required_pydantic_fields)}' - ) - return 0 - - print('❌ Обнаружены несоответствия') - return 1 - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/src/adapter/postgres.py b/src/adapter/postgres.py index 160cae5..b25ecc0 100644 --- a/src/adapter/postgres.py +++ b/src/adapter/postgres.py @@ -44,13 +44,13 @@ class Postgres(DatabaseBase): if not channel_id and not telegram_id: raise ValueError('Either channel_id or telegram_id must be provided') - filters = {'user_id': user_id} + query = domain.TargetChannel.filter(user_id=user_id) if channel_id: - filters['id'] = channel_id + query = query.filter(id=channel_id) if telegram_id: - filters['telegram_id'] = telegram_id + query = query.filter(telegram_id=telegram_id) - return await domain.TargetChannel.get_or_none(**filters) + return await query.first() async def upsert_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel: existing = await self.get_target_channel(user_id=channel.user_id, telegram_id=channel.telegram_id) @@ -78,20 +78,22 @@ class Postgres(DatabaseBase): async def get_external_channel( self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None ) -> domain.ExternalChannel | None: - filters = {'user_id': user_id} + query = domain.ExternalChannel.filter(user_id=user_id) if channel_id: - filters['id'] = channel_id + query = query.filter(id=channel_id) if telegram_id: - filters['telegram_id'] = telegram_id + query = query.filter(telegram_id=telegram_id) - return await domain.ExternalChannel.get_or_none(**filters) + return await query.first() async def create_external_channel(self, channel: domain.ExternalChannel) -> domain.ExternalChannel: await channel.save() return channel async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]: - target = await domain.TargetChannel.get_or_none(id=target_channel_id).prefetch_related('external_channels') + target = await ( + domain.TargetChannel.filter(id=target_channel_id).prefetch_related('external_channels').first() + ) if not target: return [] return await target.external_channels.all() @@ -110,7 +112,7 @@ class Postgres(DatabaseBase): for target_id in target_channel_ids: target_channel = await domain.TargetChannel.get_or_none(id=target_id) if target_channel: - await external_channel.external_channels.add(target_channel) + await external_channel.target_channels.add(target_channel) async def remove_external_channel_from_target( self, external_channel_id: uuid.UUID, target_channel_id: uuid.UUID diff --git a/src/domain/base.py b/src/domain/base.py new file mode 100644 index 0000000..e629692 --- /dev/null +++ b/src/domain/base.py @@ -0,0 +1,11 @@ +from tortoise import fields +from tortoise.models import Model + + +class TimestampedModel(Model): + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + deleted_at = fields.DatetimeField(null=True) + + class Meta: + abstract = True diff --git a/src/domain/creative.py b/src/domain/creative.py index 6fc1a4e..a2062d4 100644 --- a/src/domain/creative.py +++ b/src/domain/creative.py @@ -1,7 +1,14 @@ import enum +from typing import TYPE_CHECKING +from uuid import UUID from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .target_channel import TargetChannel + from .user import User class CreativeStatus(str, enum.Enum): @@ -9,24 +16,23 @@ class CreativeStatus(str, enum.Enum): ARCHIVED = 'archived' -class Creative(Model): +class Creative(TimestampedModel): id = fields.UUIDField(pk=True) name = fields.CharField(max_length=255) text = fields.TextField() status = fields.CharEnumField(CreativeStatus, default=CreativeStatus.ACTIVE) placements_count = fields.IntField(default=0) - user = fields.ForeignKeyField('models.User', related_name='creatives', on_delete=fields.CASCADE, index=True) - target_channel = fields.ForeignKeyField( + user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField( + 'models.User', related_name='creatives', on_delete=fields.CASCADE, index=True + ) + target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField( 'models.TargetChannel', related_name='creatives', on_delete=fields.CASCADE, index=True ) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - - # Reverse relations: - # placements: fields.ReverseRelation['Placement'] + if TYPE_CHECKING: + user_id: UUID + target_channel_id: UUID class Meta: table = 'creative' diff --git a/src/domain/external_channel.py b/src/domain/external_channel.py index 94ed503..0448eb2 100644 --- a/src/domain/external_channel.py +++ b/src/domain/external_channel.py @@ -1,8 +1,16 @@ +from typing import TYPE_CHECKING +from uuid import UUID + from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .target_channel import TargetChannel + from .user import User -class ExternalChannel(Model): +class ExternalChannel(TimestampedModel): id = fields.UUIDField(pk=True) telegram_id = fields.BigIntField(unique=True, index=True) title = fields.CharField(max_length=255) @@ -10,17 +18,14 @@ class ExternalChannel(Model): description = fields.TextField(null=True) subscribers_count = fields.IntField(null=True) - user = fields.ForeignKeyField('models.User', related_name='external_channels', on_delete=fields.CASCADE, index=True) + user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField( + 'models.User', related_name='external_channels', on_delete=fields.CASCADE, index=True + ) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) + target_channels: fields.ManyToManyRelation['TargetChannel'] - # Many-to-many с TargetChannel (определено в TargetChannel) - # target_channels: fields.ManyToManyRelation['TargetChannel'] - - # Reverse relations: - # placements: fields.ReverseRelation['Placement'] + if TYPE_CHECKING: + user_id: UUID class Meta: table = 'external_channel' diff --git a/src/domain/login_token.py b/src/domain/login_token.py index cb8c4b0..24e4170 100644 --- a/src/domain/login_token.py +++ b/src/domain/login_token.py @@ -1,17 +1,25 @@ +from typing import TYPE_CHECKING +from uuid import UUID + from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .user import User -class LoginToken(Model): +class LoginToken(TimestampedModel): id = fields.UUIDField(pk=True) token = fields.CharField(max_length=255, unique=True, index=True) - user = fields.ForeignKeyField('models.User', related_name='login_tokens', on_delete=fields.CASCADE) + user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField( + 'models.User', related_name='login_tokens', on_delete=fields.CASCADE + ) expires_at = fields.DatetimeField() used_at = fields.DatetimeField(null=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - class Meta: table = 'login_token' + + if TYPE_CHECKING: + user_id: UUID diff --git a/src/domain/placement.py b/src/domain/placement.py index 90f5078..6b59cb7 100644 --- a/src/domain/placement.py +++ b/src/domain/placement.py @@ -1,7 +1,16 @@ import enum +from typing import TYPE_CHECKING +from uuid import UUID from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .creative import Creative + from .external_channel import ExternalChannel + from .target_channel import TargetChannel + from .user import User class PlacementStatus(str, enum.Enum): @@ -23,20 +32,8 @@ class PostViewsAvailability(str, enum.Enum): MANUAL = 'manual' # Просмотры вводятся вручную -class Placement(Model): +class Placement(TimestampedModel): id = fields.UUIDField(pk=True) - - # Foreign keys - target_channel = fields.ForeignKeyField( - 'models.TargetChannel', related_name='placements', on_delete=fields.CASCADE, index=True - ) - external_channel = fields.ForeignKeyField( - 'models.ExternalChannel', related_name='placements', on_delete=fields.CASCADE, index=True - ) - creative = fields.ForeignKeyField('models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True) - user = fields.ForeignKeyField('models.User', related_name='placements', on_delete=fields.CASCADE, index=True) - - # Данные закупа placement_date = fields.DatetimeField() cost = fields.FloatField(null=True) comment = fields.TextField(null=True) @@ -45,24 +42,32 @@ class Placement(Model): invite_link = fields.CharField(max_length=512) external_channel_message_id = fields.IntField(null=True) - # Статус status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE) - # Метрики subscriptions_count = fields.IntField(default=0) views_count = fields.IntField(null=True) - # Статус доступности просмотров views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN) last_views_fetch_at = fields.DatetimeField(null=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) + target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField( + 'models.TargetChannel', related_name='placements', on_delete=fields.CASCADE, index=True + ) + external_channel: fields.ForeignKeyRelation['ExternalChannel'] = fields.ForeignKeyField( + 'models.ExternalChannel', related_name='placements', on_delete=fields.CASCADE, index=True + ) + creative: fields.ForeignKeyRelation['Creative'] = fields.ForeignKeyField( + 'models.Creative', related_name='placements', on_delete=fields.CASCADE, index=True + ) + user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField( + 'models.User', related_name='placements', on_delete=fields.CASCADE, index=True + ) - # Reverse relations: - # subscriptions: fields.ReverseRelation['Subscription'] - # views_histories: fields.ReverseRelation['PlacementViewsHistory'] + if TYPE_CHECKING: + target_channel_id: UUID + external_channel_id: UUID + creative_id: UUID + user_id: UUID class Meta: table = 'placement' diff --git a/src/domain/placement_views_history.py b/src/domain/placement_views_history.py index 99eea54..e1c945f 100644 --- a/src/domain/placement_views_history.py +++ b/src/domain/placement_views_history.py @@ -1,20 +1,26 @@ +from typing import TYPE_CHECKING +from uuid import UUID + from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .placement import Placement -class PlacementViewsHistory(Model): +class PlacementViewsHistory(TimestampedModel): id = fields.UUIDField(pk=True) - placement = fields.ForeignKeyField( - 'models.Placement', related_name='views_histories', on_delete=fields.CASCADE, index=True - ) - views_count = fields.IntField() # Количество просмотров на момент снимка fetched_at = fields.DatetimeField(index=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) + placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField( + 'models.Placement', related_name='views_histories', on_delete=fields.CASCADE, index=True + ) + + if TYPE_CHECKING: + placement_id: UUID class Meta: table = 'placement_views_history' diff --git a/src/domain/subscriber.py b/src/domain/subscriber.py index aaee840..960177f 100644 --- a/src/domain/subscriber.py +++ b/src/domain/subscriber.py @@ -1,20 +1,14 @@ from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel -class Subscriber(Model): +class Subscriber(TimestampedModel): id = fields.UUIDField(pk=True) telegram_id = fields.BigIntField(unique=True, index=True) username = fields.CharField(max_length=255, null=True) first_name = fields.CharField(max_length=255, null=True) last_name = fields.CharField(max_length=255, null=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - - # Reverse relations: - # subscriptions: fields.ReverseRelation['Subscription'] - class Meta: table = 'subscriber' diff --git a/src/domain/subscription.py b/src/domain/subscription.py index cd572b3..a612938 100644 --- a/src/domain/subscription.py +++ b/src/domain/subscription.py @@ -1,7 +1,15 @@ import enum +from typing import TYPE_CHECKING +from uuid import UUID from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .placement import Placement + from .subscriber import Subscriber + from .target_channel import TargetChannel class SubscriptionStatus(str, enum.Enum): @@ -9,26 +17,26 @@ class SubscriptionStatus(str, enum.Enum): UNSUBSCRIBED = 'unsubscribed' -class Subscription(Model): +class Subscription(TimestampedModel): id = fields.UUIDField(pk=True) - - placement = fields.ForeignKeyField( - 'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True - ) - subscriber = fields.ForeignKeyField( - 'models.Subscriber', related_name='subscriptions', on_delete=fields.CASCADE, index=True - ) - target_channel = fields.ForeignKeyField( - 'models.TargetChannel', related_name='subscriptions', on_delete=fields.CASCADE, index=True - ) - invite_link = fields.CharField(max_length=512, index=True) status = fields.CharEnumField(SubscriptionStatus, default=SubscriptionStatus.ACTIVE) unsubscribed_at = fields.DatetimeField(null=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) + placement: fields.ForeignKeyRelation['Placement'] = fields.ForeignKeyField( + 'models.Placement', related_name='subscriptions', on_delete=fields.CASCADE, index=True + ) + subscriber: fields.ForeignKeyRelation['Subscriber'] = fields.ForeignKeyField( + 'models.Subscriber', related_name='subscriptions', on_delete=fields.CASCADE, index=True + ) + target_channel: fields.ForeignKeyRelation['TargetChannel'] = fields.ForeignKeyField( + 'models.TargetChannel', related_name='subscriptions', on_delete=fields.CASCADE, index=True + ) + + if TYPE_CHECKING: + placement_id: UUID + subscriber_id: UUID + target_channel_id: UUID class Meta: table = 'subscription' diff --git a/src/domain/target_channel.py b/src/domain/target_channel.py index 4aeaf34..db0ca80 100644 --- a/src/domain/target_channel.py +++ b/src/domain/target_channel.py @@ -1,7 +1,14 @@ import enum +from typing import TYPE_CHECKING +from uuid import UUID from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel + +if TYPE_CHECKING: + from .external_channel import ExternalChannel + from .user import User class TargetChannelStatus(str, enum.Enum): @@ -10,30 +17,25 @@ class TargetChannelStatus(str, enum.Enum): ARCHIVED = 'archived' -class TargetChannel(Model): +class TargetChannel(TimestampedModel): id = fields.UUIDField(pk=True) telegram_id = fields.BigIntField(unique=True, index=True) title = fields.CharField(max_length=255) username = fields.CharField(max_length=255, null=True, index=True) status = fields.CharEnumField(TargetChannelStatus, default=TargetChannelStatus.ACTIVE) - user = fields.ForeignKeyField('models.User', related_name='target_channels', on_delete=fields.CASCADE, index=True) + user: fields.ForeignKeyRelation['User'] = fields.ForeignKeyField( + 'models.User', related_name='target_channels', on_delete=fields.CASCADE, index=True + ) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - - # Many-to-many с ExternalChannel через таблицу target_external_channel external_channels: fields.ManyToManyRelation['ExternalChannel'] = fields.ManyToManyField( 'models.ExternalChannel', related_name='target_channels', through='target_external_channel', ) - # Reverse relations: - # creatives: fields.ReverseRelation['Creative'] - # placements: fields.ReverseRelation['Placement'] - # subscriptions: fields.ReverseRelation['Subscription'] + if TYPE_CHECKING: + user_id: UUID class Meta: table = 'target_channel' diff --git a/src/domain/telegram_state.py b/src/domain/telegram_state.py index c118c26..77f9df0 100644 --- a/src/domain/telegram_state.py +++ b/src/domain/telegram_state.py @@ -1,28 +1,21 @@ import enum from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel class TelegramStateEnum(str, enum.Enum): - """Telegram bot conversation states.""" - CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel' CREATIVE_WAITING_NAME = 'creative_waiting_name' CREATIVE_WAITING_TEXT = 'creative_waiting_text' -class TelegramState(Model): - """Хранит текущее состояние разговора пользователя с ботом для multi-step flows.""" - +class TelegramState(TimestampedModel): id = fields.UUIDField(pk=True) telegram_id = fields.BigIntField(unique=True, index=True) state = fields.CharEnumField(TelegramStateEnum) context = fields.JSONField(default=dict) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - class Meta: table = 'telegram_state' diff --git a/src/domain/user.py b/src/domain/user.py index 6ba52b5..dbf6d1b 100644 --- a/src/domain/user.py +++ b/src/domain/user.py @@ -1,22 +1,12 @@ from tortoise import fields -from tortoise.models import Model + +from .base import TimestampedModel -class User(Model): +class User(TimestampedModel): id = fields.UUIDField(pk=True) telegram_id = fields.BigIntField(unique=True, index=True) username = fields.CharField(max_length=255, null=True) - created_at = fields.DatetimeField(auto_now_add=True) - updated_at = fields.DatetimeField(auto_now=True) - deleted_at = fields.DatetimeField(null=True) - - # Reverse relations (автоматически создаются): - # target_channels: fields.ReverseRelation['TargetChannel'] - # login_tokens: fields.ReverseRelation['LoginToken'] - # creatives: fields.ReverseRelation['Creative'] - # placements: fields.ReverseRelation['Placement'] - # external_channels: fields.ReverseRelation['ExternalChannel'] - class Meta: table = 'user' diff --git a/src/domain/user_conversation_state.py b/src/domain/user_conversation_state.py deleted file mode 100644 index 692d1b0..0000000 --- a/src/domain/user_conversation_state.py +++ /dev/null @@ -1,19 +0,0 @@ -import enum -from typing import Any - -from sqlalchemy.dialects.postgresql import JSONB -from sqlalchemy.orm import Mapped, mapped_column - -from src import domain - - -class TelegramStateEnum(enum.StrEnum): - CREATIVE_WAITING_CHANNEL = 'creative_waiting_channel' - CREATIVE_WAITING_NAME = 'creative_waiting_name' - CREATIVE_WAITING_TEXT = 'creative_waiting_text' - - -class TelegramState(domain.Base): - telegram_id: Mapped[int] = mapped_column(unique=True, index=True) - state: Mapped[TelegramStateEnum] - context: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict) diff --git a/uv.lock b/uv.lock index dda38f9..e4d3e95 100644 --- a/uv.lock +++ b/uv.lock @@ -982,6 +982,7 @@ dependencies = [ { name = "python-multipart" }, { name = "sqlalchemy", extra = ["asyncio"] }, { name = "tortoise-orm" }, + { name = "tortoise-orm-stubs" }, { name = "uvicorn" }, ] @@ -1010,6 +1011,7 @@ requires-dist = [ { name = "python-multipart", specifier = ">=0.0.20" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.38" }, { name = "tortoise-orm", specifier = ">=0.25.1" }, + { name = "tortoise-orm-stubs", specifier = ">=1.0.2" }, { name = "uvicorn", specifier = ">=0.38.0" }, ] @@ -1039,6 +1041,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/55/2bda7f4445f4c07b734385b46d1647a388d05160cf5b8714a713e8709378/tortoise_orm-0.25.1-py3-none-any.whl", hash = "sha256:df0ef7e06eb0650a7e5074399a51ee6e532043308c612db2cac3882486a3fd9f", size = 167723, upload-time = "2025-06-05T10:43:29.309Z" }, ] +[[package]] +name = "tortoise-orm-stubs" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tortoise-orm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ba/49/45b06cda907e55226b8ed4ddc71d13ff61505bfe366d72276462eeee9d2b/tortoise_orm_stubs-1.0.2.tar.gz", hash = "sha256:f4d6a810f295bebd83aa71b05ebd2decd883517f3c9530bd2376b9209b0777c6", size = 4559, upload-time = "2023-11-20T14:48:26.806Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/b1/f0b111dcf9381987f8acb143dd95b77934a3e9120a6c63b2cf4255c2934c/tortoise_orm_stubs-1.0.2-py3-none-any.whl", hash = "sha256:5ae3c2b0eb0286669563634b98202bbdf46349966b1c85659f3160de4fb655d6", size = 4681, upload-time = "2023-11-20T14:48:22.536Z" }, +] + [[package]] name = "ty" version = "0.0.1a25"