Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
run:
python main.py

migrations:
alembic revision --autogenerate

migrate:
alembic upgrade head

ruff:
ruff check .

Expand Down
51 changes: 51 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[alembic]
script_location = .\app\core\database\migrations
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s

prepend_sys_path = .
version_path_separator = os

[post_write_hooks]
hooks = ruff_format, ruff_fix

ruff_format.type = exec
ruff_format.executable = ruff
ruff_format.options = format REVISION_SCRIPT_FILENAME

ruff_fix.type = exec
ruff_fix.executable = ruff
ruff_fix.options = check --fix REVISION_SCRIPT_FILENAME

[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
8 changes: 2 additions & 6 deletions app/app/app.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
from pathlib import Path

from aiohttp.web import Application as AiohttpApplication

from app.core.config import Config, setup_config
from app.core.database.database import Database, setup_database
from app.core.store import Store, setup_store

__all__ = ("Application", "setup_app")


class Application(AiohttpApplication):
database: Database | None = None
Expand All @@ -18,8 +14,8 @@ class Application(AiohttpApplication):
app = Application()


def setup_app(config_path: Path) -> Application:
setup_config(app, config_path)
def setup_app() -> Application:
setup_config(app)
setup_database(app)
setup_store(app)
return app
38 changes: 27 additions & 11 deletions app/core/config.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,46 @@
import typing
from pathlib import Path
from functools import cached_property

from pydantic import BaseModel, Field
from pydantic import BaseModel
from pydantic_settings import BaseSettings, SettingsConfigDict
from sqlalchemy.engine.url import URL

if typing.TYPE_CHECKING:
from app.app.app import Application


class BotConfig(BaseModel):
token: str = Field("...", validation_alias="BOT__TOKEN")
token: str = "..."


class DatabaseConfig(BaseModel):
host: str = Field("localhost", validation_alias="DATABASE__HOST")
port: int = Field(5432, validation_alias="DATABASE__PORT")
user: str = Field("postgres", validation_alias="DATABASE__USER")
password: str = Field("postgres", validation_alias="DATABASE__PASSWORD")
database: str = Field("project", validation_alias="DATABASE__DATABASE")
host: str = "localhost"
port: int = 5432
user: str = "postgres"
password: str = "postgres"
database: str = "project"

@cached_property
def url(self) -> URL:
return URL.create(
drivername="postgresql+asyncpg",
username=self.user,
password=self.password,
host=self.host,
port=self.port,
database=self.database,
)


class Config(BaseSettings):
bot: BotConfig | None = None
database: DatabaseConfig | None = None
model_config = SettingsConfigDict(env_file=".env", env_nested_delimiter='__')

model_config = SettingsConfigDict(
env_file=(".env", "../../../../.env"), # Если main / если миграции
env_nested_delimiter="__",
)

def setup_config(app: "Application", config_path: Path) -> None:
app.config = Config(_env_file=config_path)

def setup_config(app: "Application") -> None:
app.config = Config()
Empty file.
75 changes: 75 additions & 0 deletions app/core/database/migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import asyncio
import importlib
import pkgutil
from logging.config import fileConfig

from alembic import context
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config

import app
from app.app.app import app as application, setup_app
from app.core.database.sqlalchemy_base import BaseModel

setup_app()
config = context.config

for module_info in pkgutil.walk_packages(app.__path__, prefix=app.__name__ + "."):
importlib.import_module(module_info.name)

if config.config_file_name is not None:
fileConfig(config.config_file_name)

if application.config is None or application.config.database is None:
raise ValueError("No configuration file provided")

config.set_main_option(
"sqlalchemy.url",
application.config.database.url.render_as_string(hide_password=False),
)

target_metadata = BaseModel.metadata


def run_migrations_offline() -> None:
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def do_run_migrations(connection: Connection) -> None:
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()


async def run_async_migrations() -> None:
connectable = async_engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)

await connectable.dispose()


def run_migrations_online() -> None:
asyncio.run(run_async_migrations())


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
28 changes: 28 additions & 0 deletions app/core/database/migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""${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:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}


def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""empty message

Revision ID: 32d5607c5cc0
Revises:
Create Date: 2025-04-14 15:24:41.833280

"""

from collections.abc import Sequence

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "32d5607c5cc0"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"telegram_user",
sa.Column("username", sa.String(length=64), nullable=False),
sa.Column("score", sa.Integer(), nullable=False),
sa.Column("win_count", sa.Integer(), nullable=False),
sa.Column("loss_count", sa.Integer(), nullable=False),
sa.Column("id", sa.BigInteger(), nullable=False),
sa.CheckConstraint("loss_count >= 0", name="loss_count_non_negative"),
sa.CheckConstraint("win_count >= 0", name="win_count_non_negative"),
sa.PrimaryKeyConstraint("id"),
)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("telegram_user")
# ### end Alembic commands ###
6 changes: 6 additions & 0 deletions app/core/database/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from sqlalchemy import BigInteger
from sqlalchemy.orm import Mapped, mapped_column


class IDMixin:
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
Comment thread
Gray-Advantage marked this conversation as resolved.
Empty file added app/users/__init__.py
Empty file.
19 changes: 19 additions & 0 deletions app/users/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlalchemy import CheckConstraint, String
from sqlalchemy.orm import Mapped, mapped_column

from app.core.database.mixins import IDMixin
from app.core.database.sqlalchemy_base import BaseModel


class TelegramUserModel(IDMixin, BaseModel):
Comment thread
Gray-Advantage marked this conversation as resolved.
__tablename__ = "telegram_user"

__table_args__ = (
CheckConstraint("win_count >= 0", name="win_count_non_negative"),
CheckConstraint("loss_count >= 0", name="loss_count_non_negative"),
)

username: Mapped[str] = mapped_column(String(64))
score: Mapped[int] = mapped_column(default=0)
win_count: Mapped[int] = mapped_column(default=0)
loss_count: Mapped[int] = mapped_column(default=0)
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
services:
postgres:
image: postgres:17.4-alpine
ports:
- ${DATABASE__PORT}:${DATABASE__PORT}
Comment thread
Gray-Advantage marked this conversation as resolved.
env_file:
- .env
environment:
- POSTGRES_USER=${DATABASE__USER}
- POSTGRES_PASSWORD=${DATABASE__PASSWORD}
- POSTGRES_DB=${DATABASE__DATABASE}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 2s
timeout: 5s
retries: 10
command: -p ${DATABASE__PORT}
4 changes: 1 addition & 3 deletions main.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from pathlib import Path

from aiohttp.web import run_app

from app.app.app import setup_app

if __name__ == "__main__":
run_app(setup_app(Path(__file__).resolve().parent / ".env"))
run_app(setup_app())
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ extend-select = [
# По мере "бесячих" ошибок буду дополнять, а так, всё строго
extend-ignore = [
"D1",
"CPY001",
# По рекомендации https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules
"W191",
"E111",
Expand All @@ -117,6 +118,7 @@ extend-ignore = [
"urls.py" = ["PLC0415"]
"store.py" = ["PLC0415"]
"tests/*.py" = ["SIM300", "F403", "F405", "INP001"]
"*/versions/*.py" = ["D415", "INP001"]


[tool.ruff.lint.pydocstyle]
Expand Down