feat: переезд на tortose
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
__all__ = (
|
||||
'Base',
|
||||
'User',
|
||||
'TargetChannel',
|
||||
'ExternalChannel',
|
||||
@@ -31,7 +30,6 @@ __all__ = (
|
||||
'PlacementNotFound',
|
||||
)
|
||||
|
||||
from .base import Base
|
||||
from .creative import Creative, CreativeStatus
|
||||
from .error import (
|
||||
ChannelAlreadyExists,
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import datetime
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, declared_attr, mapped_column
|
||||
from sqlalchemy.types import DateTime
|
||||
|
||||
|
||||
def _camel_to_snake(name: str) -> str:
|
||||
name = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', name).lower()
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
__abstract__ = True
|
||||
|
||||
# Use timezone-aware datetime (Postgres TIMESTAMP WITH TIME ZONE)
|
||||
type_annotation_map = {
|
||||
datetime.datetime: DateTime(timezone=True),
|
||||
}
|
||||
|
||||
# Авто имя таблиц по названию класса в snake_case
|
||||
@declared_attr.directive
|
||||
def __tablename__(cls) -> str:
|
||||
return _camel_to_snake(cls.__name__)
|
||||
|
||||
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]
|
||||
@@ -1,27 +1,32 @@
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
import enum
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .target_channel import TargetChannel
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class CreativeStatus(StrEnum):
|
||||
class CreativeStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class Creative(domain.Base):
|
||||
name: Mapped[str]
|
||||
text: Mapped[str]
|
||||
status: Mapped[CreativeStatus]
|
||||
placements_count: Mapped[int] = mapped_column(default=0, server_default='0')
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'), index=True)
|
||||
class Creative(Model):
|
||||
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)
|
||||
|
||||
target_channel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('target_channel.id'), index=True)
|
||||
target_channel: Mapped['TargetChannel'] = relationship(back_populates='creatives')
|
||||
user = fields.ForeignKeyField('models.User', related_name='creatives', on_delete=fields.CASCADE, index=True)
|
||||
target_channel = 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']
|
||||
|
||||
class Meta:
|
||||
table = 'creative'
|
||||
|
||||
@@ -1,31 +1,26 @@
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TargetChannel
|
||||
|
||||
target_external_channel = Table(
|
||||
'target_external_channel',
|
||||
domain.Base.metadata,
|
||||
Column('target_channel_id', ForeignKey('target_channel.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('external_channel_id', ForeignKey('external_channel.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class ExternalChannel(domain.Base):
|
||||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
|
||||
title: Mapped[str]
|
||||
username: Mapped[str | None] = mapped_column(index=True)
|
||||
description: Mapped[str | None]
|
||||
subscribers_count: Mapped[int | None]
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'), index=True)
|
||||
class ExternalChannel(Model):
|
||||
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)
|
||||
description = fields.TextField(null=True)
|
||||
subscribers_count = fields.IntField(null=True)
|
||||
|
||||
target_channels: Mapped[list['TargetChannel']] = relationship(
|
||||
secondary=target_external_channel,
|
||||
back_populates='external_channels',
|
||||
)
|
||||
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)
|
||||
|
||||
# Many-to-many с TargetChannel (определено в TargetChannel)
|
||||
# target_channels: fields.ManyToManyRelation['TargetChannel']
|
||||
|
||||
# Reverse relations:
|
||||
# placements: fields.ReverseRelation['Placement']
|
||||
|
||||
class Meta:
|
||||
table = 'external_channel'
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import datetime
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from src import domain
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class LoginToken(domain.Base):
|
||||
token: Mapped[str] = mapped_column(unique=True, index=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'))
|
||||
expires_at: Mapped[datetime.datetime]
|
||||
used_at: Mapped[datetime.datetime | None]
|
||||
class LoginToken(Model):
|
||||
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)
|
||||
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'
|
||||
|
||||
@@ -1,70 +1,68 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
import enum
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .creative import Creative
|
||||
from .external_channel import ExternalChannel
|
||||
from .placement_views_history import PlacementViewsHistory
|
||||
from .subscription import Subscription
|
||||
from .target_channel import TargetChannel
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class PlacementStatus(StrEnum):
|
||||
class PlacementStatus(str, enum.Enum):
|
||||
PENDING = 'pending' # Ожидается публикация поста
|
||||
ACTIVE = 'active' # Пост найден и отслеживается
|
||||
DELETED = 'deleted' # Пост удален из канала
|
||||
ARCHIVED = 'archived' # Вручную архивировано
|
||||
|
||||
|
||||
class InviteLinkType(StrEnum):
|
||||
class InviteLinkType(str, enum.Enum):
|
||||
PUBLIC = 'public' # открытая ссылка
|
||||
APPROVAL = 'approval' # с одобрением ботом
|
||||
|
||||
|
||||
class PostViewsAvailability(StrEnum):
|
||||
class PostViewsAvailability(str, enum.Enum):
|
||||
UNKNOWN = 'unknown' # Ещё не проверялось
|
||||
AVAILABLE = 'available' # Просмотры доступны
|
||||
UNAVAILABLE = 'unavailable' # Нет доступа / битая ссылка / приватный канал
|
||||
MANUAL = 'manual' # Просмотры вводятся вручную
|
||||
|
||||
|
||||
class Placement(domain.Base):
|
||||
# Основные поля
|
||||
target_channel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('target_channel.id'), index=True)
|
||||
external_channel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('external_channel.id'), index=True)
|
||||
creative_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('creative.id'), index=True)
|
||||
user_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'), index=True)
|
||||
class Placement(Model):
|
||||
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: Mapped[datetime.datetime] # дата размещения (планируемая или фактическая)
|
||||
cost: Mapped[float | None] # стоимость закупа
|
||||
comment: Mapped[str | None] # комментарий
|
||||
ad_post_url: Mapped[str | None] # ссылка на рекламное сообщение во внешнем канале
|
||||
invite_link_type: Mapped[InviteLinkType] # тип пригласительной ссылки
|
||||
invite_link: Mapped[str] # уникальная пригласительная ссылка
|
||||
external_channel_message_id: Mapped[int | None] # message_id в Telegram после нахождения поста
|
||||
placement_date = fields.DatetimeField()
|
||||
cost = fields.FloatField(null=True)
|
||||
comment = fields.TextField(null=True)
|
||||
ad_post_url = fields.CharField(max_length=512, null=True)
|
||||
invite_link_type = fields.CharEnumField(InviteLinkType)
|
||||
invite_link = fields.CharField(max_length=512)
|
||||
external_channel_message_id = fields.IntField(null=True)
|
||||
|
||||
# Статус
|
||||
status: Mapped[PlacementStatus] = mapped_column(default=PlacementStatus.ACTIVE)
|
||||
status = fields.CharEnumField(PlacementStatus, default=PlacementStatus.ACTIVE)
|
||||
|
||||
# Метрики (будут обновляться из Telegram API)
|
||||
subscriptions_count: Mapped[int] = mapped_column(default=0, server_default='0')
|
||||
views_count: Mapped[int | None] # последние известные просмотры (кэш из последнего snapshot)
|
||||
# Метрики
|
||||
subscriptions_count = fields.IntField(default=0)
|
||||
views_count = fields.IntField(null=True)
|
||||
|
||||
# Статус доступности просмотров
|
||||
views_availability: Mapped[PostViewsAvailability] = mapped_column(default=PostViewsAvailability.UNKNOWN)
|
||||
last_views_fetch_at: Mapped[datetime.datetime | None] # когда последний раз получали просмотры
|
||||
views_availability = fields.CharEnumField(PostViewsAvailability, default=PostViewsAvailability.UNKNOWN)
|
||||
last_views_fetch_at = fields.DatetimeField(null=True)
|
||||
|
||||
# Relationships
|
||||
target_channel: Mapped['TargetChannel'] = relationship()
|
||||
external_channel: Mapped['ExternalChannel'] = relationship()
|
||||
creative: Mapped['Creative'] = relationship()
|
||||
subscriptions: Mapped[list['Subscription']] = relationship(back_populates='placement')
|
||||
views_histories: Mapped[list['PlacementViewsHistory']] = relationship(back_populates='placement')
|
||||
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']
|
||||
# views_histories: fields.ReverseRelation['PlacementViewsHistory']
|
||||
|
||||
class Meta:
|
||||
table = 'placement'
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .placement import Placement
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class PlacementViewsHistory(domain.Base):
|
||||
placement_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('placement.id'), index=True)
|
||||
views_count: Mapped[int] # Количество просмотров на момент снимка
|
||||
fetched_at: Mapped[datetime.datetime] = mapped_column(index=True)
|
||||
class PlacementViewsHistory(Model):
|
||||
id = fields.UUIDField(pk=True)
|
||||
|
||||
placement: Mapped['Placement'] = relationship(back_populates='views_histories')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
table = 'placement_views_history'
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import BigInteger
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .subscription import Subscription
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class Subscriber(domain.Base):
|
||||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
|
||||
username: Mapped[str | None]
|
||||
first_name: Mapped[str | None]
|
||||
last_name: Mapped[str | None]
|
||||
class Subscriber(Model):
|
||||
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)
|
||||
|
||||
subscriptions: Mapped[list['Subscription']] = relationship(back_populates='subscriber')
|
||||
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'
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import datetime
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
import enum
|
||||
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .placement import Placement
|
||||
from .subscriber import Subscriber
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class SubscriptionStatus(StrEnum):
|
||||
class SubscriptionStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
UNSUBSCRIBED = 'unsubscribed'
|
||||
|
||||
|
||||
class Subscription(domain.Base):
|
||||
placement_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('placement.id'), index=True)
|
||||
subscriber_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('subscriber.id'), index=True)
|
||||
target_channel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('target_channel.id'), index=True)
|
||||
invite_link: Mapped[str] = mapped_column(index=True) # По которой пришёл пользователь
|
||||
status: Mapped[SubscriptionStatus] = mapped_column(default=SubscriptionStatus.ACTIVE)
|
||||
unsubscribed_at: Mapped[datetime.datetime | None]
|
||||
class Subscription(Model):
|
||||
id = fields.UUIDField(pk=True)
|
||||
|
||||
# Relationships
|
||||
placement: Mapped['Placement'] = relationship(back_populates='subscriptions')
|
||||
subscriber: Mapped['Subscriber'] = relationship(back_populates='subscriptions')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
table = 'subscription'
|
||||
|
||||
@@ -1,36 +1,39 @@
|
||||
import uuid
|
||||
from enum import StrEnum
|
||||
from typing import TYPE_CHECKING
|
||||
import enum
|
||||
|
||||
from sqlalchemy import BigInteger, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ExternalChannel
|
||||
from .creative import Creative
|
||||
from .user import User
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class TargetChannelStatus(StrEnum):
|
||||
class TargetChannelStatus(str, enum.Enum):
|
||||
ACTIVE = 'active'
|
||||
INACTIVE = 'inactive'
|
||||
ARCHIVED = 'archived'
|
||||
|
||||
|
||||
class TargetChannel(domain.Base):
|
||||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True)
|
||||
title: Mapped[str]
|
||||
username: Mapped[str | None] = mapped_column(index=True)
|
||||
status: Mapped[TargetChannelStatus] = mapped_column(default=TargetChannelStatus.ACTIVE)
|
||||
class TargetChannel(Model):
|
||||
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_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('user.id'), index=True)
|
||||
user: Mapped['User'] = relationship(back_populates='target_channels')
|
||||
user = fields.ForeignKeyField('models.User', related_name='target_channels', on_delete=fields.CASCADE, index=True)
|
||||
|
||||
external_channels: Mapped[list['ExternalChannel']] = relationship(
|
||||
secondary='target_external_channel',
|
||||
back_populates='target_channels',
|
||||
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',
|
||||
)
|
||||
|
||||
creatives: Mapped[list['Creative']] = relationship(back_populates='target_channel')
|
||||
# Reverse relations:
|
||||
# creatives: fields.ReverseRelation['Creative']
|
||||
# placements: fields.ReverseRelation['Placement']
|
||||
# subscriptions: fields.ReverseRelation['Subscription']
|
||||
|
||||
class Meta:
|
||||
table = 'target_channel'
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import enum
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import BigInteger, Enum
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from . import Base
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class TelegramStateEnum(str, enum.Enum):
|
||||
@@ -16,9 +12,17 @@ class TelegramStateEnum(str, enum.Enum):
|
||||
CREATIVE_WAITING_TEXT = 'creative_waiting_text'
|
||||
|
||||
|
||||
class TelegramState(Base):
|
||||
class TelegramState(Model):
|
||||
"""Хранит текущее состояние разговора пользователя с ботом для multi-step flows."""
|
||||
|
||||
telegram_id: Mapped[int] = mapped_column(BigInteger, unique=True, nullable=False, index=True)
|
||||
state: Mapped[TelegramStateEnum] = mapped_column(Enum(TelegramStateEnum), nullable=False)
|
||||
context: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False, default=dict)
|
||||
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'
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from src import domain
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TargetChannel
|
||||
from tortoise import fields
|
||||
from tortoise.models import Model
|
||||
|
||||
|
||||
class User(domain.Base):
|
||||
telegram_id: Mapped[int] = mapped_column(unique=True, index=True)
|
||||
username: Mapped[str | None]
|
||||
class User(Model):
|
||||
id = fields.UUIDField(pk=True)
|
||||
telegram_id = fields.BigIntField(unique=True, index=True)
|
||||
username = fields.CharField(max_length=255, null=True)
|
||||
|
||||
target_channels: Mapped[list['TargetChannel']] = relationship(back_populates='user')
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user