перенос миграций с alembic

This commit is contained in:
Artem Tsyrulnikov
2025-12-11 16:40:30 +03:00
parent 02729ce81b
commit 38f1b3feba
12 changed files with 293 additions and 592 deletions

View File

@@ -1,119 +0,0 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts
# Use forward slashes (/) also on windows to provide an os agnostic path
script_location = migration
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory.
prepend_sys_path = . .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to src/migrations/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "version_path_separator" below.
# version_locations = %(here)s/bar:%(here)s/bat:src/migrations/versions
# version path separator; As mentioned above, this is the character used to split
# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep.
# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas.
# Valid values for version_path_separator are:
#
# version_path_separator = :
# version_path_separator = ;
# version_path_separator = space
# version_path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
version_path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the exec runner, execute a binary
# hooks = ruff
# ruff.type = exec
# ruff.executable = %(here)s/.venv/bin/ruff
# ruff.options = --fix REVISION_SCRIPT_FILENAME
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

View File

@@ -1,49 +0,0 @@
from logging.config import fileConfig
from alembic import context
from sqlalchemy import engine_from_config, pool
from src.config import settings
from src.domain import * # noqa
try:
from src.domain import Base as SqlBase # type: ignore[attr-defined]
except ImportError:
SqlBase = None
config = context.config
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 = SqlBase.metadata if SqlBase else None
def run_migrations_offline() -> None:
context.configure(
url=config.get_main_option('sqlalchemy.url'),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={'paramstyle': 'named'},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

View File

@@ -1,26 +0,0 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}

View File

@@ -1,43 +0,0 @@
"""add_placement_parser_support
Revision ID: c8f9a2d3e4b5
Revises: b1d510ad3751
Create Date: 2025-12-07 12:00:00.000000
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'c8f9a2d3e4b5'
down_revision: str | None = 'b1d510ad3751'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# 1. Добавить поле external_channel_message_id
op.add_column('placement', sa.Column('external_channel_message_id', sa.BigInteger(), nullable=True))
# 2. Добавить новые значения в enum placementstatus
# В PostgreSQL нельзя добавлять значения в enum внутри транзакции,
# поэтому используем autocommit_block
with op.get_context().autocommit_block():
op.execute("ALTER TYPE placementstatus ADD VALUE IF NOT EXISTS 'PENDING'")
op.execute("ALTER TYPE placementstatus ADD VALUE IF NOT EXISTS 'DELETED'")
def downgrade() -> None:
# 1. Откатить данные с новыми статусами на ACTIVE
op.execute("UPDATE placement SET status = 'ACTIVE' WHERE status IN ('PENDING', 'DELETED')")
# 2. Удалить поле external_channel_message_id
op.drop_column('placement', 'external_channel_message_id')
# ВАЖНО: В PostgreSQL нельзя удалить значения из enum.
# Новые значения 'PENDING' и 'DELETED' останутся в enum даже после downgrade.
# Это ограничение PostgreSQL. Если нужно полностью удалить значения,
# потребуется пересоздать enum, что сложнее и требует временного столбца.

View File

@@ -1,65 +0,0 @@
"""empty message
Revision ID: b1d510ad3751
Revises: f05f85d10837
Create Date: 2025-12-01 13:05:17.215374
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
subscription_status_enum = sa.Enum('ACTIVE', 'UNSUBSCRIBED', name='subscriptionstatus')
# revision identifiers, used by Alembic.
revision: str = 'b1d510ad3751'
down_revision: str | None = 'f05f85d10837'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'telegram_state',
sa.Column('telegram_id', sa.BigInteger(), nullable=False),
sa.Column(
'state',
sa.Enum(
'CREATIVE_WAITING_CHANNEL', 'CREATIVE_WAITING_NAME', 'CREATIVE_WAITING_TEXT', name='telegramstateenum'
),
nullable=False,
),
sa.Column('context', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_telegram_state_telegram_id'), 'telegram_state', ['telegram_id'], unique=True)
op.add_column('subscription', sa.Column('target_channel_id', sa.Uuid(), nullable=False))
subscription_status_enum.create(op.get_bind(), checkfirst=True)
op.add_column('subscription', sa.Column('status', subscription_status_enum, nullable=False))
op.add_column('subscription', sa.Column('unsubscribed_at', sa.DateTime(timezone=True), nullable=True))
op.create_index(op.f('ix_subscription_target_channel_id'), 'subscription', ['target_channel_id'], unique=False)
fk_name = op.f('fk_subscription_target_channel_id')
op.create_foreign_key(fk_name, 'subscription', 'target_channel', ['target_channel_id'], ['id'])
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
fk_name = op.f('fk_subscription_target_channel_id')
op.drop_constraint(fk_name, 'subscription', type_='foreignkey')
op.drop_index(op.f('ix_subscription_target_channel_id'), table_name='subscription')
op.drop_column('subscription', 'unsubscribed_at')
op.drop_column('subscription', 'status')
subscription_status_enum.drop(op.get_bind(), checkfirst=True)
op.drop_column('subscription', 'target_channel_id')
op.drop_index(op.f('ix_telegram_state_telegram_id'), table_name='telegram_state')
op.drop_table('telegram_state')
# ### end Alembic commands ###

View File

@@ -1,262 +0,0 @@
"""init
Revision ID: f05f85d10837
Revises:
Create Date: 2025-11-13 01:06:51.623272
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'f05f85d10837'
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
'subscriber',
sa.Column('telegram_id', sa.BigInteger(), nullable=False),
sa.Column('username', sa.String(), nullable=True),
sa.Column('first_name', sa.String(), nullable=True),
sa.Column('last_name', sa.String(), nullable=True),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_subscriber_telegram_id'), 'subscriber', ['telegram_id'], unique=True)
op.create_table(
'user',
sa.Column('telegram_id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(), nullable=True),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_user_telegram_id'), 'user', ['telegram_id'], unique=True)
op.create_table(
'external_channel',
sa.Column('telegram_id', sa.BigInteger(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=True),
sa.Column('description', sa.String(), nullable=True),
sa.Column('subscribers_count', sa.Integer(), nullable=True),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['user_id'],
['user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_external_channel_telegram_id'), 'external_channel', ['telegram_id'], unique=True)
op.create_index(op.f('ix_external_channel_user_id'), 'external_channel', ['user_id'], unique=False)
op.create_index(op.f('ix_external_channel_username'), 'external_channel', ['username'], unique=False)
op.create_table(
'login_token',
sa.Column('token', sa.String(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('used_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['user_id'],
['user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_login_token_token'), 'login_token', ['token'], unique=True)
op.create_table(
'target_channel',
sa.Column('telegram_id', sa.BigInteger(), nullable=False),
sa.Column('title', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=True),
sa.Column('status', sa.Enum('ACTIVE', 'INACTIVE', 'ARCHIVED', name='targetchannelstatus'), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['user_id'],
['user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_target_channel_telegram_id'), 'target_channel', ['telegram_id'], unique=True)
op.create_index(op.f('ix_target_channel_user_id'), 'target_channel', ['user_id'], unique=False)
op.create_index(op.f('ix_target_channel_username'), 'target_channel', ['username'], unique=False)
op.create_table(
'creative',
sa.Column('name', sa.String(), nullable=False),
sa.Column('text', sa.String(), nullable=False),
sa.Column('status', sa.Enum('ACTIVE', 'ARCHIVED', name='creativestatus'), nullable=False),
sa.Column('placements_count', sa.Integer(), server_default='0', nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('target_channel_id', sa.Uuid(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['target_channel_id'],
['target_channel.id'],
),
sa.ForeignKeyConstraint(
['user_id'],
['user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_creative_target_channel_id'), 'creative', ['target_channel_id'], unique=False)
op.create_index(op.f('ix_creative_user_id'), 'creative', ['user_id'], unique=False)
op.create_table(
'target_external_channel',
sa.Column('target_channel_id', sa.Uuid(), nullable=False),
sa.Column('external_channel_id', sa.Uuid(), nullable=False),
sa.ForeignKeyConstraint(['external_channel_id'], ['external_channel.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['target_channel_id'], ['target_channel.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('target_channel_id', 'external_channel_id'),
)
op.create_table(
'placement',
sa.Column('target_channel_id', sa.Uuid(), nullable=False),
sa.Column('external_channel_id', sa.Uuid(), nullable=False),
sa.Column('creative_id', sa.Uuid(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=False),
sa.Column('placement_date', sa.DateTime(timezone=True), nullable=False),
sa.Column('cost', sa.Float(), nullable=True),
sa.Column('comment', sa.String(), nullable=True),
sa.Column('ad_post_url', sa.String(), nullable=True),
sa.Column('invite_link_type', sa.Enum('PUBLIC', 'APPROVAL', name='invitelinktype'), nullable=False),
sa.Column('invite_link', sa.String(), nullable=False),
sa.Column('status', sa.Enum('ACTIVE', 'ARCHIVED', name='placementstatus'), nullable=False),
sa.Column('subscriptions_count', sa.Integer(), server_default='0', nullable=False),
sa.Column('views_count', sa.Integer(), nullable=True),
sa.Column(
'views_availability',
sa.Enum('UNKNOWN', 'AVAILABLE', 'UNAVAILABLE', 'MANUAL', name='postviewsavailability'),
nullable=False,
),
sa.Column('last_views_fetch_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['creative_id'],
['creative.id'],
),
sa.ForeignKeyConstraint(
['external_channel_id'],
['external_channel.id'],
),
sa.ForeignKeyConstraint(
['target_channel_id'],
['target_channel.id'],
),
sa.ForeignKeyConstraint(
['user_id'],
['user.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_placement_creative_id'), 'placement', ['creative_id'], unique=False)
op.create_index(op.f('ix_placement_external_channel_id'), 'placement', ['external_channel_id'], unique=False)
op.create_index(op.f('ix_placement_target_channel_id'), 'placement', ['target_channel_id'], unique=False)
op.create_index(op.f('ix_placement_user_id'), 'placement', ['user_id'], unique=False)
op.create_table(
'placement_views_history',
sa.Column('placement_id', sa.Uuid(), nullable=False),
sa.Column('views_count', sa.Integer(), nullable=False),
sa.Column('fetched_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['placement_id'],
['placement.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_placement_views_history_fetched_at'), 'placement_views_history', ['fetched_at'], unique=False
)
op.create_index(
op.f('ix_placement_views_history_placement_id'), 'placement_views_history', ['placement_id'], unique=False
)
op.create_table(
'subscription',
sa.Column('placement_id', sa.Uuid(), nullable=False),
sa.Column('subscriber_id', sa.Uuid(), nullable=False),
sa.Column('invite_link', sa.String(), nullable=False),
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(
['placement_id'],
['placement.id'],
),
sa.ForeignKeyConstraint(
['subscriber_id'],
['subscriber.id'],
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(op.f('ix_subscription_invite_link'), 'subscription', ['invite_link'], unique=False)
op.create_index(op.f('ix_subscription_placement_id'), 'subscription', ['placement_id'], unique=False)
op.create_index(op.f('ix_subscription_subscriber_id'), 'subscription', ['subscriber_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_subscription_subscriber_id'), table_name='subscription')
op.drop_index(op.f('ix_subscription_placement_id'), table_name='subscription')
op.drop_index(op.f('ix_subscription_invite_link'), table_name='subscription')
op.drop_table('subscription')
op.drop_index(op.f('ix_placement_views_history_placement_id'), table_name='placement_views_history')
op.drop_index(op.f('ix_placement_views_history_fetched_at'), table_name='placement_views_history')
op.drop_table('placement_views_history')
op.drop_index(op.f('ix_placement_user_id'), table_name='placement')
op.drop_index(op.f('ix_placement_target_channel_id'), table_name='placement')
op.drop_index(op.f('ix_placement_external_channel_id'), table_name='placement')
op.drop_index(op.f('ix_placement_creative_id'), table_name='placement')
op.drop_table('placement')
op.drop_table('target_external_channel')
op.drop_index(op.f('ix_creative_user_id'), table_name='creative')
op.drop_index(op.f('ix_creative_target_channel_id'), table_name='creative')
op.drop_table('creative')
op.drop_index(op.f('ix_target_channel_username'), table_name='target_channel')
op.drop_index(op.f('ix_target_channel_user_id'), table_name='target_channel')
op.drop_index(op.f('ix_target_channel_telegram_id'), table_name='target_channel')
op.drop_table('target_channel')
op.drop_index(op.f('ix_login_token_token'), table_name='login_token')
op.drop_table('login_token')
op.drop_index(op.f('ix_external_channel_username'), table_name='external_channel')
op.drop_index(op.f('ix_external_channel_user_id'), table_name='external_channel')
op.drop_index(op.f('ix_external_channel_telegram_id'), table_name='external_channel')
op.drop_table('external_channel')
op.drop_index(op.f('ix_user_telegram_id'), table_name='user')
op.drop_table('user')
op.drop_index(op.f('ix_subscriber_telegram_id'), table_name='subscriber')
op.drop_table('subscriber')
# ### end Alembic commands ###

View File

@@ -0,0 +1,221 @@
from tortoise import BaseDBAsyncClient
RUN_IN_TRANSACTION = True
async def upgrade(db: BaseDBAsyncClient) -> str:
return """
CREATE TABLE IF NOT EXISTS "subscriber" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL UNIQUE,
"username" VARCHAR(255),
"first_name" VARCHAR(255),
"last_name" VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS "idx_subscriber_telegra_5eef32" ON "subscriber" ("telegram_id");
CREATE TABLE IF NOT EXISTS "telegram_state" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL UNIQUE,
"state" VARCHAR(24) NOT NULL,
"context" JSONB NOT NULL
);
CREATE INDEX IF NOT EXISTS "idx_telegram_st_telegra_750503" ON "telegram_state" ("telegram_id");
COMMENT ON COLUMN "telegram_state"."state" IS 'CREATIVE_WAITING_CHANNEL: creative_waiting_channel\nCREATIVE_WAITING_NAME: creative_waiting_name\nCREATIVE_WAITING_TEXT: creative_waiting_text';
CREATE TABLE IF NOT EXISTS "user" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL UNIQUE,
"username" VARCHAR(255)
);
CREATE INDEX IF NOT EXISTS "idx_user_telegra_66ffbd" ON "user" ("telegram_id");
CREATE TABLE IF NOT EXISTS "external_channel" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL UNIQUE,
"title" VARCHAR(255) NOT NULL,
"username" VARCHAR(255),
"description" TEXT,
"subscribers_count" INT,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_external_ch_telegra_13cca5" ON "external_channel" ("telegram_id");
CREATE INDEX IF NOT EXISTS "idx_external_ch_usernam_9b9fb4" ON "external_channel" ("username");
CREATE INDEX IF NOT EXISTS "idx_external_ch_user_id_dd2b1c" ON "external_channel" ("user_id");
CREATE TABLE IF NOT EXISTS "login_token" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"token" VARCHAR(255) NOT NULL UNIQUE,
"expires_at" TIMESTAMPTZ NOT NULL,
"used_at" TIMESTAMPTZ,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_login_token_token_2582d2" ON "login_token" ("token");
CREATE TABLE IF NOT EXISTS "target_channel" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"telegram_id" BIGINT NOT NULL UNIQUE,
"title" VARCHAR(255) NOT NULL,
"username" VARCHAR(255),
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_target_chan_telegra_ab5a54" ON "target_channel" ("telegram_id");
CREATE INDEX IF NOT EXISTS "idx_target_chan_usernam_a6cf8b" ON "target_channel" ("username");
CREATE INDEX IF NOT EXISTS "idx_target_chan_user_id_c3bdc7" ON "target_channel" ("user_id");
COMMENT ON COLUMN "target_channel"."status" IS 'ACTIVE: active\nINACTIVE: inactive\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "creative" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"text" TEXT NOT NULL,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"placements_count" INT NOT NULL DEFAULT 0,
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_creative_target__8e3b64" ON "creative" ("target_channel_id");
CREATE INDEX IF NOT EXISTS "idx_creative_user_id_a27cfc" ON "creative" ("user_id");
COMMENT ON COLUMN "creative"."status" IS 'ACTIVE: active\nARCHIVED: archived';
CREATE TABLE IF NOT EXISTS "placement" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"placement_date" TIMESTAMPTZ NOT NULL,
"cost" DOUBLE PRECISION,
"comment" TEXT,
"ad_post_url" VARCHAR(512),
"invite_link_type" VARCHAR(8) NOT NULL,
"invite_link" VARCHAR(512) NOT NULL,
"external_channel_message_id" INT,
"status" VARCHAR(8) NOT NULL DEFAULT 'active',
"subscriptions_count" INT NOT NULL DEFAULT 0,
"views_count" INT,
"views_availability" VARCHAR(11) NOT NULL DEFAULT 'unknown',
"last_views_fetch_at" TIMESTAMPTZ,
"creative_id" UUID NOT NULL REFERENCES "creative" ("id") ON DELETE CASCADE,
"external_channel_id" UUID NOT NULL REFERENCES "external_channel" ("id") ON DELETE CASCADE,
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
"user_id" UUID NOT NULL REFERENCES "user" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_placement_creativ_32111d" ON "placement" ("creative_id");
CREATE INDEX IF NOT EXISTS "idx_placement_externa_5b96c7" ON "placement" ("external_channel_id");
CREATE INDEX IF NOT EXISTS "idx_placement_target__ed9070" ON "placement" ("target_channel_id");
CREATE INDEX IF NOT EXISTS "idx_placement_user_id_7abaf9" ON "placement" ("user_id");
COMMENT ON COLUMN "placement"."invite_link_type" IS 'PUBLIC: public\nAPPROVAL: approval';
COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending\nACTIVE: active\nDELETED: deleted\nARCHIVED: archived';
COMMENT ON COLUMN "placement"."views_availability" IS 'UNKNOWN: unknown\nAVAILABLE: available\nUNAVAILABLE: unavailable\nMANUAL: manual';
CREATE TABLE IF NOT EXISTS "placement_views_history" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"views_count" INT NOT NULL,
"fetched_at" TIMESTAMPTZ NOT NULL,
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_placement_v_fetched_4c8f51" ON "placement_views_history" ("fetched_at");
CREATE INDEX IF NOT EXISTS "idx_placement_v_placeme_89e0f4" ON "placement_views_history" ("placement_id");
CREATE TABLE IF NOT EXISTS "subscription" (
"created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" TIMESTAMPTZ,
"id" UUID NOT NULL PRIMARY KEY,
"invite_link" VARCHAR(512) NOT NULL,
"status" VARCHAR(12) NOT NULL DEFAULT 'active',
"unsubscribed_at" TIMESTAMPTZ,
"placement_id" UUID NOT NULL REFERENCES "placement" ("id") ON DELETE CASCADE,
"subscriber_id" UUID NOT NULL REFERENCES "subscriber" ("id") ON DELETE CASCADE,
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "idx_subscriptio_invite__f2058d" ON "subscription" ("invite_link");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_placeme_5decb2" ON "subscription" ("placement_id");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_subscri_e75729" ON "subscription" ("subscriber_id");
CREATE INDEX IF NOT EXISTS "idx_subscriptio_target__79117e" ON "subscription" ("target_channel_id");
COMMENT ON COLUMN "subscription"."status" IS 'ACTIVE: active\nUNSUBSCRIBED: unsubscribed';
CREATE TABLE IF NOT EXISTS "aerich" (
"id" SERIAL NOT NULL PRIMARY KEY,
"version" VARCHAR(255) NOT NULL,
"app" VARCHAR(100) NOT NULL,
"content" JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS "target_external_channel" (
"target_channel_id" UUID NOT NULL REFERENCES "target_channel" ("id") ON DELETE CASCADE,
"external_channel_id" UUID NOT NULL REFERENCES "external_channel" ("id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "uidx_target_exte_target__ba70ec" ON "target_external_channel" ("target_channel_id", "external_channel_id");"""
async def downgrade(db: BaseDBAsyncClient) -> str:
return """
"""
MODELS_STATE = (
"eJztXWtv4rga/ison2al7qil7bSnOjoSpcwMOzRUBTqr3VlFbnAhanDYXNqi0fz3Y+d+cV"
"ISEkjg/TIX26+TPK9t/Dx+bf8UFtoUq8bHro6Rqbxg4ar1UyBowf6RyDtqCWi5DHJYgoke"
"VbuwHC71aJg6kk2a/oRUA9OkKTZkXVmaikZoKrFUlSVqMi2okFmQZBHlXwtLpjbD5hzrNO"
"Pvf2iyQqb4DRvef5fP0pOC1WnkdZUpe7adLpmrpZ02mfRvPtsl2eMeJVlTrQUJSi9X5lwj"
"fnHLUqYfmQ3Lm2GCdWTiaegz2Fu6X+wlOW9ME0zdwv6rToOEKX5ClsrAEP77ZBGZYdCyn8"
"T+OPufkAMeWSMMWoWYDIufv5yvCr7ZThXYo7pfO/cfTj/9Zn+lZpgz3c60ERF+2YbIRI6p"
"jWsApO1KPJWQmQT0huaYygLzQY1axsCduqYfvX8UAdlLCFAOWpgHswdfMUwF+g3TIVFXrg"
"czMB73b3ujcef2jn3JwjD+VW2IOuMey2nbqatY6gfHJRrtH07H8Stpfe+Pv7bYf1t/DcVe"
"3HF+ufFfAnsnZJmaRLRXCU1Djc1L9YChJQPHWstpQcdGLcGxO3Ws+/KBX+lgjIv5NWpZgl"
"/dt92iWxviRu+zMzuo/XfChd050vnu88rHHEfhqWkXXKA3ScVkZs7pf9vn5xnOe+jc279h"
"tFTMI6Kb1XbyfkUgNPEbpxeMaSofQq98UyDMau69P8eRlu4B9eG28+dvkdY+GIpfvOIhYL"
"uD4XUMT8NEpmXwG2WPWAsb1T59RURknEA3sN4evgKdeboT0SjGQqc77j/0rlpOgR+EoUMT"
"bmiKLs9pktM8cjbjyzUa8WVqE76MN+ClimS8wPRRFEeLcBpzn6S0ZZ5pDHfFSa0C9+MNxo"
"UZe8jv7ZOzi7PL009nl7SI/SJ+ykUGyH1xHB8EkE75gyTPESFYlfIxA65xmUQhCWaCKVQ2"
"RrxLC0KzRQPrOaELmRwIYIyNPj1zaRQDIwneZ03Hyox8w6vEyBmDzKXfE7eauoIWpAaP0N"
"GrT9DDjYJ+nTPjtJHtjLqdm56Q0XNLgG9sV9gN6mssjtxxiY8oa5WPSH5+RfpUSmmewY9F"
"EuVr1/bzt3usIvuDUgG+8+qp9Xwpga6NkdbWQthEUEtmLdqLeAoiaGa/NXs2e5KLSe/NxD"
"pBqtfsOKpavMhRlriG3cLhfgEiG4hsSXBBiwGRDRwLIhuIbKkd1KTumOloweU218oslV7H"
"DMth1u//oG3YIR1q/Z92+/T0on18+uny/Ozi4vzy2OfYyawssn3d/8L4dsR7HAKumGouJd"
"M3aIoOtwUpkxGnvIpw2KYQlO+PLWU30OqBDL9ZAst0aThmVhGcjReIrUf2no9Yzy9Ycm0L"
"javbx7lszRLkNgHktm3IbSAOVS4OpemZHDRvEVmNNfbntgTNqmecGQ3V/gQppoQlPkhnzY"
"ryJa9cXP1yUNR02xXPeJXE2W3/vr/cQvGK3GLmXNes2TxUDU9v43Ymmi6F8fcbT6o0ONBm"
"Chlrz5gIHFUwlHuUJQiqrJxk+gVBCwQtECQj0ALBsaAFghaYQwv0fkHX1qk8g3J0qsrVv+"
"rFFfy2VKhHCvSFqGUzx7h96gyUORb6qTJgPKuRC2srIm3Rb3ukIm1L8jgqT0aqMrImEJQ4"
"7DmiNqWT52WkGFBnoM5NnX3sDcMC6rynjgXqvK9TTf9XVGKQ53Vm0rqZHXWfPCrT+jhzX1"
"VDKcv3nkHMdU/Momn972Y4uR70Wnf3vW5/1B+KUffYmSyJJijORPi+1xnEpBBZW3jTynVj"
"TEImEF/CjS9BU4k1dMnSOdtB0oW6mFlDwI3qdecn7TX0OloqVa+z86J4KuSFtmBJVcizAw"
"cX1Pe3dvLq2XHwnnBHu2m/e9VaWo+qIv8gnbu7++FDZ3DVomxQ116Q3RZ2vMEzBFyeBh0z"
"a2acZCUtOrGsv8CGgWaYK4SlxqK9U8uBRqXt0/bvu5540xe/0OEBkykFkY4PsQ3hN71Bb8"
"z2g7tco6ZbxN2wSfvDCgddxq0PcqP4i4Jf8yMYszrQocFBAb0ghX6TQqfFq6LDBL+mLQ4Z"
"FnmmpIhwxoyJ+E0cfhevWm4ROiI8dPqDDqUDdEhw3lilI8dEDKVbJJRz2xEnbAayQMQqNv"
"84OVlj+Dg5SR0/WFbUdSqiM2MH9SdsyvMCakxKFSDL7JrEu+fw5VwFjJkdSDh55iwyH4Ap"
"5gcIJBy+AoevbAewjHV8OEekjHNEssbHEjDlnIHRWFRTxv/3cQ0fnLshnuGTehsLZGwm8j"
"6AtQvZafLGL2dGP1cMU9MVXNburwdW61e70lU9ZcrUnWCpustm0IxCVTUMka2Ed0VaTFao"
"V7xprRH2JYUbuWMEQWAQBNbUEIS9iRWCILA9dSwEge2r2ri79ZNd9MeSF1Bs3bxQL4habm"
"d0257q07heEMws803p4nYg20W2ZmxI6Es+e2RHrD7eRuq0GWfkH98kcChaKPcoi5cZ0XJA"
"xYCKwYwdqBg4FqgYUDE41jaTi235WNuanshabcx2JaeGPCm6YUp5sYxaAZqReL68YEaMDh"
"vLBCNdZ6kUVgOrXg2MwJLOMH3Y3uWYQUlgmcAygYwAywTHAssElrk+y6z35tVKT0+sZO/q"
"Pu2vjO+mnIijyfWoe9+/ZvsnLeIL/YX2UK6Ffgb4SezDb1TkFydpDsMTrMQ2aSWWf49LTu"
"wShgcIHux5gliAjeErKRaA36lLADG6oN9YFBPjFdxKvZ3dZLsJUolCzVERE75IlxGT7QCE"
"RBASy5n4g94EQiI4FoTEA2HqEK4CtzDXNh6gATE/zbsoaJ+l7r7opSjES6vl0YFwroywkS"
"gFh02UeNiEd9THhtFT5R57srXIKe66xYHetwynbFR2AzX/9uQIpLnvoC7h4Ka63ULN+aT4"
"PdS8u7yjt1CnHAIVv4eaq5mWeAu1w7ayVVGXRY3otAoLPFU0UuAoUxX1GJnhlwVVFFRREM"
"9AFQXHgioKqiioonVSRf1ZSlEZqg4KqdC973WY4iR97/THffGLRH/vRbE3uGr5B2i+IsWk"
"8HvTxx8kYSN2bnscA4YUpzS7r4lT2qTz1CLKVvtsHY3xLF1iPIuLW/Rx3stEffvHaCimTF"
"gCk5hHJ4Qi/fdUkc2jlqoY5j+VqYzBpPDRUlSKqfGRPbaieSHDIjIcJi7pit/HFRvnWAXX"
"+TbNVRnfYUtpHALjSWzpvMXT8oCtAFuBSe2uJ7XAVvbUscBWgK0AWynKVmoaftCQQI5CBz"
"vAsmSOtaM8iFRy38dOgFG1mUI/T3vGmy5RDlhNY1ZRg+GA5eu0vSIbAlLBPpGmrGBnkv4O"
"1hV5LnBov5tzlEX8UVCmNtQ/da607hTJ/c2uwQxpwwOy05n+C9YNt7usOxUKmUBMqw8k6x"
"o5QHSLNxPAk+PjdQ4OOD5OPzmA5XGUZt7O1HeUZu6B91tTmitjrvuhKf/6P7Pizcs="
)

View File

@@ -5,6 +5,7 @@ description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"aerich>=0.9.2",
"aiogram>=3.16.0", "aiogram>=3.16.0",
"aiolimiter>=1.2.1", "aiolimiter>=1.2.1",
"asyncpg>=0.30.0", "asyncpg>=0.30.0",
@@ -66,3 +67,8 @@ exclude = [
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
pythonpath = ["src", "."] pythonpath = ["src", "."]
[tool.aerich]
tortoise_orm = "shared.datebase_base.TORTOISE_ORM"
location = "./migrations"
src_folder = "./."

View File

@@ -1,3 +1,5 @@
import os
import pydantic import pydantic
from tortoise import Tortoise from tortoise import Tortoise
@@ -10,6 +12,19 @@ class DatabaseConfig(pydantic.BaseModel):
MAX_OVERFLOW: int = 10 MAX_OVERFLOW: int = 10
TORTOISE_ORM = {
'connections': {'default': os.getenv('DB__URL')},
'apps': {
'models': {
'models': ['src.domain', 'aerich.models'],
'default_connection': 'default',
},
},
'use_tz': True,
'timezone': 'UTC',
}
class DatabaseBase: class DatabaseBase:
def __init__(self, config: DatabaseConfig) -> None: def __init__(self, config: DatabaseConfig) -> None:
self.config = config self.config = config
@@ -17,7 +32,7 @@ class DatabaseBase:
async def connect(self) -> None: async def connect(self) -> None:
await Tortoise.init( await Tortoise.init(
db_url=str(self.config.URL), db_url=str(self.config.URL),
modules={'models': ['src.domain']}, modules={'models': ['src.domain', 'aerich.models']},
use_tz=True, use_tz=True,
timezone='UTC', timezone='UTC',
) )
@@ -47,35 +62,19 @@ class DatabaseBase:
conn = connections.get('default') conn = connections.get('default')
# Проверяем существование таблицы alembic_version # Проверяем существование таблицы aerich
result = await conn.execute_query( result = await conn.execute_query(
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'alembic_version')" "SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'aerich')"
) )
if not result[1][0]['exists']: if not result[1][0]['exists']:
logging.error('Migrations not applied. Run: alembic upgrade head') logging.warning('Migrations not applied. Run: aerich upgrade')
return return
# Получаем текущую версию # Получаем последнюю примененную версию
result = await conn.execute_query('SELECT version_num FROM alembic_version')
if not result[1]:
logging.error('No migration version found in database. Run: alembic upgrade head')
return
current_version = result[1][0]['version_num']
# Сравниваем с head версией
try: try:
from alembic.config import Config result = await conn.execute_query('SELECT version, app FROM aerich ORDER BY id DESC LIMIT 1')
from alembic.script import ScriptDirectory if result[1]:
current_version = result[1][0]['version']
alembic_cfg = Config('alembic.ini') logging.info(f'Current migration version: {current_version}')
script = ScriptDirectory.from_config(alembic_cfg)
head_revision = script.get_current_head()
if head_revision and current_version != head_revision:
logging.warning(
f'Database version {current_version} is outdated (latest: {head_revision}). '
f'Run: alembic upgrade head'
)
except Exception as e: except Exception as e:
logging.debug(f'Could not check migration head: {e}') logging.debug(f'Could not check migration version: {e}')

View File

@@ -22,9 +22,8 @@ class ExternalChannel(TimestampedModel):
'models.User', related_name='external_channels', on_delete=fields.CASCADE, index=True 'models.User', related_name='external_channels', on_delete=fields.CASCADE, index=True
) )
target_channels: fields.ManyToManyRelation['TargetChannel']
if TYPE_CHECKING: if TYPE_CHECKING:
target_channels: fields.ManyToManyRelation['TargetChannel']
user_id: UUID user_id: UUID
class Meta: class Meta:

View File

@@ -32,6 +32,8 @@ class TargetChannel(TimestampedModel):
'models.ExternalChannel', 'models.ExternalChannel',
related_name='target_channels', related_name='target_channels',
through='target_external_channel', through='target_external_channel',
forward_key='external_channel_id',
backward_key='target_channel_id',
) )
if TYPE_CHECKING: if TYPE_CHECKING:

38
uv.lock generated
View File

@@ -2,6 +2,21 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.13"
[[package]]
name = "aerich"
version = "0.9.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "asyncclick" },
{ name = "dictdiffer" },
{ name = "tortoise-orm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c4/60/5d3885f531fab2cecec67510e7b821efc403940ed9eefd034b2c21350f3c/aerich-0.9.2.tar.gz", hash = "sha256:02d58658714eebe396fe7bd9f9401db3a60a44dc885910ad3990920d0357317d", size = 74231, upload-time = "2025-10-10T05:53:49.632Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/87/1a/956c6b1e35881bb9835a33c8db1565edcd133f8e45321010489092a0df40/aerich-0.9.2-py3-none-any.whl", hash = "sha256:d0f007acb21f6559f1eccd4e404fb039cf48af2689e0669afa62989389c0582d", size = 46451, upload-time = "2025-10-10T05:53:48.71Z" },
]
[[package]] [[package]]
name = "aiofiles" name = "aiofiles"
version = "24.1.0" version = "24.1.0"
@@ -149,6 +164,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" }, { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
] ]
[[package]]
name = "asyncclick"
version = "8.3.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f9/ca/25e426d16bd0e91c1c9259112cecd17b2c2c239bdd8e5dba430f3bd5e3ef/asyncclick-8.3.0.7.tar.gz", hash = "sha256:8a80d8ac613098ee6a9a8f0248f60c66c273e22402cf3f115ed7f071acfc71d3", size = 277634, upload-time = "2025-10-11T08:35:44.841Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/01/d9/782ffcb4c97b889bc12d8276637d2739b99520390ee8fec77c07416c5d12/asyncclick-8.3.0.7-py3-none-any.whl", hash = "sha256:7607046de39a3f315867cad818849f973e29d350c10d92f251db3ff7600c6c7d", size = 109925, upload-time = "2025-10-11T08:35:43.378Z" },
]
[[package]] [[package]]
name = "asyncpg" name = "asyncpg"
version = "0.30.0" version = "0.30.0"
@@ -217,6 +244,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "dictdiffer"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/7b/35cbccb7effc5d7e40f4c55e2b79399e1853041997fcda15c9ff160abba0/dictdiffer-0.9.0.tar.gz", hash = "sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578", size = 31513, upload-time = "2021-07-22T13:24:29.276Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/ef/4cb333825d10317a36a1154341ba37e6e9c087bac99c1990ef07ffdb376f/dictdiffer-0.9.0-py2.py3-none-any.whl", hash = "sha256:442bfc693cfcadaf46674575d2eba1c53b42f5e404218ca2c2ff549f2df56595", size = 16754, upload-time = "2021-07-22T13:24:26.783Z" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.121.0" version = "0.121.0"
@@ -970,6 +1006,7 @@ name = "tgex-backend"
version = "0.1.0" version = "0.1.0"
source = { virtual = "." } source = { virtual = "." }
dependencies = [ dependencies = [
{ name = "aerich" },
{ name = "aiogram" }, { name = "aiogram" },
{ name = "aiolimiter" }, { name = "aiolimiter" },
{ name = "asyncpg" }, { name = "asyncpg" },
@@ -999,6 +1036,7 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aerich", specifier = ">=0.9.2" },
{ name = "aiogram", specifier = ">=3.16.0" }, { name = "aiogram", specifier = ">=3.16.0" },
{ name = "aiolimiter", specifier = ">=1.2.1" }, { name = "aiolimiter", specifier = ">=1.2.1" },
{ name = "asyncpg", specifier = ">=0.30.0" }, { name = "asyncpg", specifier = ">=0.30.0" },