feat: типизация для tortoise ORM
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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())
|
||||
@@ -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
|
||||
|
||||
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)
|
||||
14
uv.lock
generated
14
uv.lock
generated
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user