feat: типизация для tortoise ORM
This commit is contained in:
11
src/domain/base.py
Normal file
11
src/domain/base.py
Normal file
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user