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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ cloudflared.deb
.ruff_cache
.gigaide
coverage.xml
.coverage
.coverage
alembic.ini
148 changes: 0 additions & 148 deletions alembic.ini

This file was deleted.

Binary file removed alembic/__pycache__/env.cpython-313.pyc
Binary file not shown.
4 changes: 2 additions & 2 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from logging.config import fileConfig
from src.infrastructure.db.database import Base
from src.domain.models import Appointment, Ban, MagicToken, Service, User # noqa: F401
from src.config import DATABASE_URL
from src.config import get_database_url

from sqlalchemy.ext.asyncio import async_engine_from_config
from sqlalchemy import pool
Expand All @@ -13,7 +13,7 @@
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
config.set_main_option("sqlalchemy.url", f"postgresql+asyncpg://{DATABASE_URL}") # Меняем ссылку на БД, беря ее не из .ini файла
config.set_main_option("sqlalchemy.url", f"postgresql+asyncpg://{get_database_url( )}") # Меняем ссылку на БД, беря ее не из .ini файла

# Interpret the config file for Python logging.
# This line sets up loggers basically.
Expand Down
102 changes: 102 additions & 0 deletions alembic/versions/4972fe91500d_v0_0_1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""v0.0.1

Revision ID: 4972fe91500d
Revises: 001
Create Date: 2026-05-31 17:00:41.411887

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '4972fe91500d'
down_revision: Union[str, Sequence[str], None] = '001'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('bans',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('reason', sa.String(length=150), nullable=False, comment='Причина бана'),
sa.Column('user_id', sa.Integer(), nullable=False, comment='ID пользователя, который получил бан'),
sa.Column('banned_by', sa.Integer(), nullable=False, comment='Тот, кто выдал бан'),
sa.Column('revoked_by', sa.Integer(), nullable=True, comment='Тот, кто снял бан'),
sa.Column('banned_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=True, comment='Время окончания бана'),
sa.Column('revoked_at', sa.DateTime(timezone=True), nullable=True, comment='Время отмены бана'),
sa.Column('revoked_reason', sa.String(length=150), nullable=True, comment='Причина отмены бана'),
sa.CheckConstraint('expires_at IS NULL OR expires_at > banned_at', name='ck_ban_expires_after_banned'),
sa.CheckConstraint('revoked_at IS NULL OR (revoked_at >= banned_at AND (expires_at IS NULL OR revoked_at <= expires_at))', name='ck_ban_revoke_logic'),
sa.ForeignKeyConstraint(['banned_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['revoked_by'], ['users.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_bans_banned_by'), 'bans', ['banned_by'], unique=False)
op.create_index(op.f('ix_bans_revoked_by'), 'bans', ['revoked_by'], unique=False)
op.create_index(op.f('ix_bans_user_id'), 'bans', ['user_id'], unique=False)
op.create_index('uq_active_ban', 'bans', ['user_id'], unique=True, postgresql_where=sa.text('revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())'))
op.alter_column('appointments', 'comment',
existing_type=sa.VARCHAR(length=1200),
nullable=False)
op.add_column('magic_tokens', sa.Column('user_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'magic_tokens', 'users', ['user_id'], ['id'])
op.alter_column('services', 'description',
existing_type=sa.VARCHAR(length=2000),
nullable=False)
op.alter_column('services', 'address',
existing_type=sa.VARCHAR(length=100),
nullable=False)
op.add_column('users', sa.Column('user_role', sa.Enum('user', 'admin', 'moderator'), nullable=False))
Comment thread
Fl1riX marked this conversation as resolved.
op.alter_column('users', 'email',
existing_type=sa.VARCHAR(length=50),
nullable=False)
op.drop_constraint(op.f('users_phone_key'), 'users', type_='unique')
op.drop_constraint(op.f('users_telegram_id_key'), 'users', type_='unique')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.drop_index(op.f('ix_users_phone'), table_name='users')
op.create_index(op.f('ix_users_phone'), 'users', ['phone'], unique=True)
op.drop_index(op.f('ix_users_telegram_id'), table_name='users')
op.create_index(op.f('ix_users_telegram_id'), 'users', ['telegram_id'], unique=True)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_users_telegram_id'), table_name='users')
op.create_index(op.f('ix_users_telegram_id'), 'users', ['telegram_id'], unique=False)
op.drop_index(op.f('ix_users_phone'), table_name='users')
op.create_index(op.f('ix_users_phone'), 'users', ['phone'], unique=False)
op.drop_index(op.f('ix_users_email'), table_name='users')
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=False)
op.create_unique_constraint(op.f('users_telegram_id_key'), 'users', ['telegram_id'], postgresql_nulls_not_distinct=False)
op.create_unique_constraint(op.f('users_phone_key'), 'users', ['phone'], postgresql_nulls_not_distinct=False)
op.alter_column('users', 'email',
existing_type=sa.VARCHAR(length=50),
nullable=True)
op.drop_column('users', 'user_role')
op.alter_column('services', 'address',
existing_type=sa.VARCHAR(length=100),
nullable=True)
op.alter_column('services', 'description',
existing_type=sa.VARCHAR(length=2000),
nullable=True)
op.drop_constraint(None, 'magic_tokens', type_='foreignkey')
Comment thread
Fl1riX marked this conversation as resolved.
op.drop_column('magic_tokens', 'user_id')
op.alter_column('appointments', 'comment',
existing_type=sa.VARCHAR(length=1200),
nullable=True)
op.drop_index('uq_active_ban', table_name='bans', postgresql_where=sa.text('revoked_at IS NULL AND (expires_at IS NULL OR expires_at > now())'))
op.drop_index(op.f('ix_bans_user_id'), table_name='bans')
op.drop_index(op.f('ix_bans_revoked_by'), table_name='bans')
op.drop_index(op.f('ix_bans_banned_by'), table_name='bans')
op.drop_table('bans')
# ### end Alembic commands ###
31 changes: 30 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from fastapi.middleware.cors import CORSMiddleware
from slowapi.middleware import SlowAPIMiddleware
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from contextlib import asynccontextmanager
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession

from src.presentation.api.v1.endpoints import router as endpoints_router
from src.presentation.api.v1.auth.auth import router as auth_router
Expand All @@ -12,15 +14,40 @@
from src.limiter import limiter
from src.presentation.middlewares import MetricsMiddleware
from src.infrastructure.tasks.cleanup_magic_tokens import cleanup_telegram_tokens
from src.config import get_database_url

@asynccontextmanager
async def lifespan(app: FastAPI):
DB_URL = f"postgresql+asyncpg://{get_database_url()}"

engine = create_async_engine(
DB_URL
)

SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)

app.state.SessionLocal = SessionLocal
# При старте выполняется ко до yiled
yield
# выполняется код после yiled и остановка

await engine.dispose()


app = FastAPI(
title="SteelTime",
version="0.0.1",
description="Система для бронирования услуг",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json"
openapi_url="/openapi.json",
lifespan=lifespan
)

app.state.limiter = limiter
app.add_middleware(MetricsMiddleware)
app.add_middleware(SlowAPIMiddleware)
Expand Down Expand Up @@ -52,6 +79,8 @@ async def startup():
)
scheduler.start()



@app.get("/")
@limiter.limit("5/minute")
def welcome(request: Request):
Expand Down
13 changes: 11 additions & 2 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
BOT_SECRET = os.getenv("BOT_SECRET")

DATABASE_URL = os.getenv("DATABASE_URL")
def get_database_url() -> str:
db_url = os.getenv("DATABASE_URL")
if not db_url:
raise RuntimeError("!!! В .env не задана DB_URL !!!")
return db_url

SECRET_KEY = os.getenv("SECRET_KEY", "development-secret-key-i-love_coding")
def get_secret_key() -> str:
secret = os.getenv("SECRET_KEY")
if not secret:
raise RuntimeError("!!! Не задан секретный ключ !!!")
return secret

ALGORITHM = os.getenv("ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", 30))
21 changes: 4 additions & 17 deletions src/infrastructure/db/database.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,11 @@
#from pathlib import Path
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
from sqlalchemy.orm import declarative_base
from src.config import DATABASE_URL

DB_URL = f"postgresql+asyncpg://{DATABASE_URL}"

engine = create_async_engine(
DB_URL
)

SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)
from fastapi import Request

Base = declarative_base()

async def get_db():
async with SessionLocal() as session:
async def get_db(request: Request):

async with request.app.state.SessionLocal() as session:
try:
yield session
finally:
Expand Down
Loading
Loading