refractor structure project and add user endpoint

This commit is contained in:
2026-01-08 15:05:23 +03:00
parent 423ce8bc62
commit 8e0131451d
25 changed files with 329 additions and 218 deletions
+11
View File
@@ -0,0 +1,11 @@
from celery import Celery
from app.core.config import settings
celery_app = Celery(
"app.core.celery_app",
broker=settings.RABBITMQ_URL,
backend="rpc://"
)
celery_app.autodiscover_tasks(["app.tasks"])
+63
View File
@@ -0,0 +1,63 @@
from typing import Literal, List
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
COMPANY_NAME: str
MODE: Literal["DEV", "TEST", "PROD"]
LOG_LEVEL: Literal["ERROR", "WARNING", "INFO", "DEBUG"]
HOST: str
PORT: int
WORKERS: int
URL: str
CORS_ORIGINS: List[str] = ["http://localhost:5500", "http://127.0.0.1:5500", "http://localhost:8080", "http://127.0.0.1:8080", "null"]
CORS_HEADERS: List[str] = ["*"]
CORS_METHODS: List[str] = ["*"]
SECRET_KEY: str
ALGORITHM: str
ACCESS_TOKEN_EXPIRE_MINUTES: int = 15
EMAIL_TOKEN_EXPIRE_MINUTES: int = 60
REFRESH_TOKEN_EXPIRE_DAYS: int = 30
SMTP_SERVER: str
SMTP_PORT: int
SMTP_EMAIL: str
SMTP_PASS: str
DB_HOST: str
DB_PORT: int
DB_PASS: str
DB_USER: str
DB_NAME: str
@property
def DATABASE_URL(self):
return f"postgresql+asyncpg://{self.DB_USER}:{self.DB_PASS}@{self.DB_HOST}:{self.DB_PORT}/{self.DB_NAME}"
REDIS_HOST: str = "localhost"
REDIS_PORT: int = 6397
REDIS_PASS: str = ""
REDIS_DB: int = 0
@property
def REDIS_URL(self):
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
RMQ_HOST: str
RMQ_USER: str
RMQ_PASS: str
RMQ_PORT: int
@property
def RABBITMQ_URL(self) -> str:
return f"amqp://{self.RMQ_USER}:{self.RMQ_PASS}@{self.RMQ_HOST}:{self.RMQ_PORT}//"
model_config = SettingsConfigDict(env_file=".env", extra="allow")
settings: Settings = Settings()
+9
View File
@@ -0,0 +1,9 @@
from enum import Enum
DB_NAMING_CONVENTION = {
"ix": "%(column_0_label)s_idx",
"uq": "%(table_name)s_%(column_0_name)s_key",
"ck": "%(table_name)s_%(constraint_name)s_check",
"fk": "%(table_name)s_%(column_0_name)s_fkey",
"pk": "%(table_name)s_pkey",
}
+133
View File
@@ -0,0 +1,133 @@
from typing import TypeVar, Generic, Optional, List, Union, Dict, Any
import logging
from sqlalchemy import delete, insert, select, update, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError
from pydantic import BaseModel
from app.core.database import Base
log = logging.getLogger(__name__)
ModelType = TypeVar("ModelType", bound=Base)
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseModel)
class BaseDAO(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
model = None
@classmethod
async def find_one_or_none(cls, session: AsyncSession, *filter, **filter_by) -> Optional[ModelType]:
stmt = select(cls.model).filter(*filter).filter_by(**filter_by)
result = await session.execute(stmt)
return result.scalars().one_or_none()
@classmethod
async def find_all(
cls,
session: AsyncSession,
offset: Optional[int],
limit: Optional[int],
*filter,
**filter_by
) -> List[ModelType]:
stmt = select(cls.model).filter(*filter).filter_by(**filter_by)
if offset is not None:
stmt = stmt.offset(offset)
if limit is not None:
stmt = stmt.limit(limit)
result = await session.execute(stmt)
return result.scalars().all()
@classmethod
async def add(
cls,
session: AsyncSession,
obj_in: Union[CreateSchemaType, Dict[str, Any]]
) -> Optional[ModelType]:
if isinstance(obj_in, dict):
create_data = obj_in
else:
create_data = obj_in.model_dump(exclude_unset=True)
try:
stmt = insert(cls.model).values(**create_data).returning(cls.model)
result = await session.execute(stmt)
return result.scalars().first()
except (SQLAlchemyError, Exception) as ex:
if isinstance(ex, SQLAlchemyError):
msg = "Database Exc: Cannot insert data into table"
elif isinstance(ex, Exception):
msg = "Unknown Exc: Cannot insert data into table"
log.error(msg, extra={"table": cls.model.__tablename__}, exc_info=True)
@classmethod
async def delete(cls, session: AsyncSession, *filter, **filter_by) -> None:
stmt = delete(cls.model).filter(*filter).filter_by(**filter_by)
await session.execute(stmt)
@classmethod
async def update(
cls,
session: AsyncSession,
*where,
obj_in: Union[UpdateSchemaType, Dict[str, Any]]
) -> Optional[ModelType]:
if isinstance(obj_in, Dict):
update_data = obj_in
else:
update_data = obj_in.model_dump(exclude_unset=True)
stmt = update(cls.model).where(*where).values(update_data).returning(cls.model)
result = await session.execute(stmt)
return result.scalars().one()
@classmethod
async def add_bulk(cls, session: AsyncSession, data: List[Dict[str, Any]]):
try:
result = await session.execute(
insert(cls.model).returning(cls.model),
data
)
return result.scalars().all()
except (SQLAlchemyError, Exception) as e:
if isinstance(e, SQLAlchemyError):
msg = "Database Exc"
elif isinstance(e, Exception):
msg = "Unknown Exc"
msg += ": Cannot bulk insert data into table"
log.error(msg, extra={"table": cls.model.__tablename__}, exc_info=True)
return None
@classmethod
async def update_bulk(cls, session: AsyncSession, data: List[Dict[str, Any]]):
try:
stmt = update(cls.model)
await session.execute(update(cls.model), data)
except (SQLAlchemyError, Exception) as e:
if isinstance(e, SQLAlchemyError):
msg = "Database Exc"
elif isinstance(e, Exception):
msg = "Unknown Exc"
msg += ": Cannot bulk update data into table"
log.error(msg, extra={"table": cls.model.__tablename__}, exc_info=True)
return None
@classmethod
async def count(cls, session: AsyncSession, *filter, **filter_by):
stmt = select(func.count()).select_from(
cls.model).filter(*filter).filter_by(**filter_by)
result = await session.execute(stmt)
return result.scalar()
+26
View File
@@ -0,0 +1,26 @@
from datetime import datetime
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import MetaData, NullPool, func
from app.core.config import settings
from app.core.constants import DB_NAMING_CONVENTION
class Base(DeclarativeBase):
metadata = MetaData(naming_convention=DB_NAMING_CONVENTION)
created_at: Mapped[datetime] = mapped_column(server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(server_default=func.now(), onupdate=func.now())
if settings.MODE == "TEST":
DATABASE_URL = settings.TEST_DATABASE_URL
DATABASE_PARAMS = {"poolclass": NullPool}
else:
DATABASE_URL = settings.DATABASE_URL
DATABASE_PARAMS = {}
async_engine = create_async_engine(DATABASE_URL, **DATABASE_PARAMS)
async_session_maker = async_sessionmaker(async_engine, expire_on_commit=False)
+16
View File
@@ -0,0 +1,16 @@
from fastapi import HTTPException, status
class InvalidTokenException(HTTPException):
def __init__(self):
super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
class TokenExpiredException(HTTPException):
def __init__(self):
super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token has expired")
class InvalidCredentialsException(HTTPException):
def __init__(self):
super().__init__(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid username or password")
+38
View File
@@ -0,0 +1,38 @@
from logging.config import dictConfig
from app.core.config import settings
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"colored": {
"()": "colorlog.ColoredFormatter",
"format": "[%(asctime)s] %(log_color)s%(levelname)s%(reset)s:"
" (%(module)s) %(message)s",
"log_colors": {
"DEBUG": "cyan",
"INFO": "green",
"WARNING": "yellow",
"ERROR": "red",
"CRITICAL": "purple",
},
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "colored",
},
},
"root": {
"level": settings.LOG_LEVEL,
"handlers": ["console"],
},
}
def set_logging():
dictConfig(LOGGING_CONFIG)
+22
View File
@@ -0,0 +1,22 @@
from redis.asyncio import Redis, from_url
from app.core.config import settings
redis_client: Redis = None
async def init_redis() -> None:
global redis_client
redis_client = await from_url(
settings.REDIS_URL,
encoding="utf-8",
decode_responses=True
)
async def close_redis() -> None:
if redis_client:
await redis_client.close()
async def get_redis() -> Redis:
return redis_client