from src.application.authentication.dependency_injection import *  # noqa: F401, F403

src/application/authentication/__init__.py
Craxti/fastapi_layered
from datetime import datetime

from fastapi import Depends
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from pydantic import ValidationError

from src.config import settings
from src.domain.authentication import TokenPayload
from src.domain.users import User, UsersRepository
from src.infrastructure.errors import AuthenticationError

__all__ = ("get_current_user",)

oauth2_oauth = OAuth2PasswordBearer(
    tokenUrl="/auth/openapi",
    scheme_name=settings.authentication.scheme,
)


async def get_current_user(token: str = Depends(oauth2_oauth)) -> User:
    try:
        payload = jwt.decode(
            token,
            settings.authentication.access_token.secret_key,
            algorithms=[settings.authentication.algorithm],
        )
        token_payload = TokenPayload(**payload)

        if datetime.fromtimestamp(token_payload.exp) < datetime.now():
            raise AuthenticationError
    except (JWTError, ValidationError):
        raise AuthenticationError

    user = await UsersRepository().get(id_=token_payload.sub)

    # TODO: Check if the token is in the blacklist

    return user

src/application/authentication/dependency_injection.py
Craxti/fastapi_layered
from src.domain.orders import Order, OrdersRepository, OrderUncommited
from src.domain.users import User
from src.infrastructure.database.transaction import transaction


@transaction
async def create(payload: dict, user: User) -> Order:
    payload.update(user_id=user.id)

    order = await OrdersRepository().create(OrderUncommited(**payload))

    # Do som other stuff...

    return order

src/application/orders.py
Craxti/fastapi_layered
from pathlib import Path

from pydantic import BaseConfig, BaseModel, BaseSettings


# API Settings
class APIUrlsSettings(BaseModel):
    """Configure public urls."""

    docs: str = "/docs"
    redoc: str = "/redoc"


class PublicApiSettings(BaseModel):
    """Configure public API settings."""

    name: str = "Backend"
    urls: APIUrlsSettings = APIUrlsSettings()


# Database Settings
class DatabaseSettings(BaseModel):
    name: str = "db.sqlite3"

    @property
    def url(self) -> str:
        return f"sqlite+aiosqlite:///./{self.name}"


class KafkaSettings(BaseModel):
    bootstrap_servers: str = "localhost:9092"


# Logging Settings
class LoggingSettings(BaseModel):
    """Configure the logging engine."""

    # The time field can be formatted using more human-friendly tokens.
    # These constitute a subset of the one used by the Pendulum library
    # https://pendulum.eustace.io/docs/#tokens
    format: str = "{time:YYYY-MM-DD HH:mm:ss} | {level: <5} | {message}"

    # The .log filename
    file: str = "backend"

    # The .log file Rotation
    rotation: str = "1MB"

    # The type of compression
    compression: str = "zip"


class AccessTokenSettings(BaseModel):
    secret_key: str = "invaliad"
    ttl: int = 100  # seconds


class RefreshTokenSettings(BaseModel):
    secret_key: str = "invaliad"
    ttl: int = 100  # seconds


class AuthenticationSettings(BaseModel):
    access_token: AccessTokenSettings = AccessTokenSettings()
    refresh_token: RefreshTokenSettings = RefreshTokenSettings()
    algorithm: str = "HS256"
    scheme: str = "Bearer"


# Settings are powered by pydantic
# https://pydantic-docs.helpmanual.io/usage/settings/
class Settings(BaseSettings):
    debug: bool = True

    # Project file system
    root_dir: Path
    src_dir: Path

    # Infrastructure settings
    database: DatabaseSettings = DatabaseSettings()

    # Application configuration
    public_api: PublicApiSettings = PublicApiSettings()
    logging: LoggingSettings = LoggingSettings()
    authentication: AuthenticationSettings = AuthenticationSettings()

    class Config(BaseConfig):
        env_nested_delimiter: str = "__"
        env_file: str = ".env"


# Define the root path
# --------------------------------------
ROOT_PATH = Path(__file__).parent.parent

# ======================================
# Load settings
# ======================================
settings = Settings(
    # NOTE: We would like to hard-code the root and applications directories
    #       to avoid overriding via environment variables
    root_dir=ROOT_PATH,
    src_dir=ROOT_PATH / "src",
)

src/config/__init__.py
Craxti/fastapi_layered
from src.domain.authentication.models import *  # noqa: F401, F403

src/domain/authentication/__init__.py
Craxti/fastapi_layered
from pydantic import Field

from src.infrastructure.models import InternalModel, PublicModel

__all__ = (
    "TokenClaimRequestBody",
    "TokenClaimPublic",
    "TokenPayload",
    "AccessToken",
    "RefreshToken",
)


# Public models
# ------------------------------------------------------
class TokenClaimRequestBody(PublicModel):
    login: str = Field("OpenAPI documentation")
    password: str = Field("OpenAPI documentation")


class TokenClaimPublic(PublicModel):
    access: str = Field("OpenAPI documentation")
    refresh: str = Field("OpenAPI documentation")


# Internal models
# ------------------------------------------------------
class TokenPayload(InternalModel):
    sub: int
    exp: int


class AccessToken(InternalModel):
    payload: TokenPayload
    raw_token: str


class RefreshToken(InternalModel):
    payload: TokenPayload
    raw_token: str

src/domain/authentication/models.py
Craxti/fastapi_layered
from src.domain.orders.models import *  # noqa: F401, F403
from src.domain.orders.repository import *  # noqa: F401, F403

src/domain/orders/__init__.py
Craxti/fastapi_layered
from pydantic import Field

from src.infrastructure.models import InternalModel, PublicModel

__all__ = ("OrderCreateRequestBody", "OrderPublic", "OrderUncommited", "Order")


# Public models
# ------------------------------------------------------
class _OrderPublic(PublicModel):
    amount: int = Field(description="OpenAPI description")
    product_id: int = Field(description="OpenAPI description")


class OrderCreateRequestBody(_OrderPublic):
    """Order create request body."""

    pass


class OrderPublic(_OrderPublic):
    """The internal application representation."""

    id: int


# Internal models
# ------------------------------------------------------
class _OrderInternal(InternalModel):
    amount: int
    product_id: int
    user_id: int


class OrderUncommited(_OrderInternal):
    """This schema is used for creating instance in the database."""

    pass


class Order(_OrderInternal):
    """Existed order representation."""

    id: int

src/domain/orders/models.py
Craxti/fastapi_layered
from typing import AsyncGenerator

from src.infrastructure.database import BaseRepository, OrdersTable

from .models import Order, OrderUncommited

all = ("OrdersRepository",)


class OrdersRepository(BaseRepository[OrdersTable]):
    schema_class = OrdersTable

    async def all(self) -> AsyncGenerator[Order, None]:
        async for instance in self._all():
            yield Order.from_orm(instance)

    async def get(self, id_: int) -> Order:
        instance = await self._get(key="id", value=id_)
        return Order.from_orm(instance)

    async def create(self, schema: OrderUncommited) -> Order:
        instance: OrdersTable = await self._save(schema.dict())
        return Order.from_orm(instance)

src/domain/orders/repository.py
Craxti/fastapi_layered
from src.domain.products.models import *  # noqa: F401, F403
from src.domain.products.repository import *  # noqa: F401, F403

src/domain/products/__init__.py
Craxti/fastapi_layered
from pydantic import Field

from src.infrastructure.models import InternalModel, PublicModel

__all__ = (
    "ProductCreateRequestBody",
    "ProductPublic",
    "ProductUncommited",
    "Product",
)


# Public models
# ------------------------------------------------------
class _ProductPublic(PublicModel):
    name: str = Field(description="OpenAPI description")
    price: int = Field(description="OpenAPI description")


class ProductCreateRequestBody(_ProductPublic):
    """Product create request body."""

    pass


class ProductPublic(_ProductPublic):
    """The internal application representation."""

    id: int


# Internal models
# ------------------------------------------------------
class _ProductInternal(InternalModel):
    name: str
    price: int


class ProductUncommited(_ProductInternal):
    """This schema is used for creating instance in the database."""

    pass


class Product(_ProductInternal):
    """Existed product representation."""

    id: int

src/domain/products/models.py
Craxti/fastapi_layered
from typing import AsyncGenerator

from src.domain.products.models import Product, ProductUncommited
from src.infrastructure.database import BaseRepository, ProductsTable

all = ("ProductRepository",)


class ProductRepository(BaseRepository[ProductsTable]):
    schema_class = ProductsTable

    async def all(self) -> AsyncGenerator[Product, None]:
        async for instance in self._all():
            yield Product.from_orm(instance)

    async def get(self, id_: int) -> Product:
        instance = await self._get(key="id", value=id_)
        return Product.from_orm(instance)

    async def create(self, schema: ProductUncommited) -> Product:
        instance: ProductsTable = await self._save(schema.dict())
        return Product.from_orm(instance)

src/domain/products/repository.py
Craxti/fastapi_layered
from src.domain.users.models import *  # noqa: F401, F403
from src.domain.users.repository import *  # noqa: F401, F403

src/domain/users/__init__.py
Craxti/fastapi_layered
from src.infrastructure.models import InternalModel

__all__ = ("UserUncommited", "User")


# Internal models
# ------------------------------------------------------
class UserUncommited(InternalModel):
    """This schema is used for creating instance in the database."""

    usernae: str
    password: str


class User(UserUncommited):
    """Existed product representation."""

    id: int

src/domain/users/models.py
Craxti/fastapi_layered
from typing import AsyncGenerator

from src.infrastructure.database import BaseRepository, UsersTable

from .models import User, UserUncommited

all = ("UsersRepository",)


class UsersRepository(BaseRepository[UsersTable]):
    schema_class = UsersTable

    async def all(self) -> AsyncGenerator[User, None]:
        async for instance in self._all():
            yield User.from_orm(instance)

    async def get(self, id_: int) -> User:
        instance = await self._get(key="id", value=id_)
        return User.from_orm(instance)

    async def create(self, schema: UserUncommited) -> User:
        instance: UsersTable = await self._save(schema.dict())
        return User.from_orm(instance)

src/domain/users/repository.py
Craxti/fastapi_layered
from src.infrastructure.application.factory import *  # noqa: F401, F403

src/infrastructure/application/__init__.py
Craxti/fastapi_layered
import asyncio
from functools import partial
from typing import Callable, Coroutine, Iterable

from fastapi import APIRouter, FastAPI
from fastapi.exceptions import RequestValidationError
from pydantic import ValidationError

from src.infrastructure.errors import (
    BaseError,
    custom_base_errors_handler,
    pydantic_validation_errors_handler,
    python_base_error_handler,
)

__all__ = ("create",)


def create(
    *_,
    rest_routers: Iterable[APIRouter],
    startup_tasks: Iterable[Callable[[], Coroutine]] | None = None,
    shutdown_tasks: Iterable[Callable[[], Coroutine]] | None = None,
    **kwargs,
) -> FastAPI:
    """The application factory using FastAPI framework.
    🎉 Only passing routes is mandatory to start.
    """

    # Initialize the base FastAPI application
    app = FastAPI(**kwargs)

    # Include REST API routers
    for router in rest_routers:
        app.include_router(router)

    # Extend FastAPI default error handlers
    app.exception_handler(RequestValidationError)(
        pydantic_validation_errors_handler
    )
    app.exception_handler(BaseError)(custom_base_errors_handler)
    app.exception_handler(ValidationError)(pydantic_validation_errors_handler)
    app.exception_handler(Exception)(python_base_error_handler)

    # Define startup tasks that are running asynchronous using FastAPI hook
    if startup_tasks:
        for task in startup_tasks:
            coro = partial(asyncio.create_task, task())
            app.on_event("startup")(coro)

    # Define shutdown tasks using FastAPI hook
    if shutdown_tasks:
        for task in shutdown_tasks:
            app.on_event("shutdown")(task)

    return app

src/infrastructure/application/factory.py
Craxti/fastapi_layered
"""
This module includes all shared utils and tools for the database interaction.
"""

from src.infrastructure.database.repository import *  # noqa: F401, F403
from src.infrastructure.database.tables import *  # noqa: F401, F403

src/infrastructure/database/__init__.py
Craxti/fastapi_layered
import asyncio
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

from src.config import settings
from src.infrastructure.database.tables import Base, OrdersTable, ProductsTable

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
    fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.

config.set_main_option("sqlalchemy.url", settings.database.url)


def run_migrations_offline() -> None:
    """Run migrations in 'offline' mode.

    This configures the context with just a URL
    and not an Engine, though an Engine is acceptable
    here as well.  By skipping the Engine creation
    we don't even need a DBAPI to be available.

    Calls to context.execute() here emit the given string to the
    script output.

    """
    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:
    """In this scenario we need to create an Engine
    and associate a connection with the context.

    """

    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:
    """Run migrations in 'online' mode."""

    asyncio.run(run_async_migrations())


if context.is_offline_mode():
    run_migrations_offline()
else:
    run_migrations_online()

src/infrastructure/database/migrations/env.py
Craxti/fastapi_layered
"""empty message

Revision ID: 69888b706120
Revises: 
Create Date: 2023-07-23 20:56:26.430715

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '69888b706120'
down_revision = None
branch_labels = None
depends_on = None


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('products',
    sa.Column('name', sa.String(), nullable=False),
    sa.Column('price', sa.Integer(), nullable=False),
    sa.Column('id', sa.Integer(), nullable=False),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_products'))
    )
    op.create_table('users',
    sa.Column('username', sa.String(), nullable=False),
    sa.Column('password', sa.String(), nullable=False),
    sa.Column('id', sa.Integer(), nullable=False),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_users'))
    )
    op.create_table('orders',
    sa.Column('amount', sa.Integer(), nullable=False),
    sa.Column('product_id', sa.Integer(), nullable=False),
    sa.Column('user_id', sa.Integer(), nullable=False),
    sa.Column('id', sa.Integer(), nullable=False),
    sa.ForeignKeyConstraint(['product_id'], ['products.id'], name=op.f('fk_orders_product_id_products')),
    sa.ForeignKeyConstraint(['user_id'], ['users.id'], name=op.f('fk_orders_user_id_users')),
    sa.PrimaryKeyConstraint('id', name=op.f('pk_orders'))
    )
    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('orders')
    op.drop_table('users')
    op.drop_table('products')
    # ### end Alembic commands ###

src/infrastructure/database/migrations/versions/69888b706120_.py
Craxti/fastapi_layered
from typing import Any, AsyncGenerator, Generic, Type

from sqlalchemy import Result, asc, delete, desc, func, select, update

from src.infrastructure.database.session import Session
from src.infrastructure.database.tables import ConcreteTable
from src.infrastructure.errors import (
    DatabaseError,
    NotFoundError,
    UnprocessableError,
)

__all__ = ("BaseRepository",)



class BaseRepository(Session, Generic[ConcreteTable]):  # type: ignore
    """This class implements the base interface for working with database
    # and makes it easier to work with type annotations.
    """

    schema_class: Type[ConcreteTable]

    def __init__(self) -> None:
        super().__init__()

        if not self.schema_class:
            raise UnprocessableError(
                message=(
                    "Can not initiate the class without schema_class attribute"
                )
            )

    async def _update(
        self, key: str, value: Any, payload: dict[str, Any]
    ) -> ConcreteTable:
        """Updates an existed instance of the model in the related table.
        If some data is not exist in the payload then the null value will
        be passed to the schema class."""

        query = (
            update(self.schema_class)
            .where(getattr(self.schema_class, key) == value)
            .values(payload)
            .returning(self.schema_class)
        )
        result: Result = await self.execute(query)
        await self._session.flush()

        if not (schema := result.scalar_one_or_none()):
            raise DatabaseError

        return schema

    async def _get(self, key: str, value: Any) -> ConcreteTable:
        """Return only one result by filters"""

        query = select(self.schema_class).where(
            getattr(self.schema_class, key) == value
        )
        result: Result = await self.execute(query)

        if not (_result := result.scalars().one_or_none()):
            raise NotFoundError

        return _result

    async def count(self) -> int:
        result: Result = await self.execute(func.count(self.schema_class.id))
        value = result.scalar()

        if not isinstance(value, int):
            raise UnprocessableError(
                message=(
                    "For some reason count function returned not an integer."
                    f"Value: {value}"
                ),
            )

        return value

    async def _first(self, by: str = "id") -> ConcreteTable:
        result: Result = await self.execute(
            select(self.schema_class).order_by(asc(by)).limit(1)
        )

        if not (_result := result.scalar_one_or_none()):
            raise NotFoundError

        return _result

    async def _last(self, by: str = "id") -> ConcreteTable:
        result: Result = await self.execute(
            select(self.schema_class).order_by(desc(by)).limit(1)
        )

        if not (_result := result.scalar_one_or_none()):
            raise NotFoundError

        return _result

    async def _save(self, payload: dict[str, Any]) -> ConcreteTable:
        try:
            schema = self.schema_class(**payload)
            self._session.add(schema)
            await self._session.flush()
            await self._session.refresh(schema)
            return schema
        except self._ERRORS:
            raise DatabaseError

    async def _all(self) -> AsyncGenerator[ConcreteTable, None]:
        result: Result = await self.execute(select(self.schema_class))
        schemas = result.scalars().all()

        for schema in schemas:
            yield schema

    async def delete(self, id_: int) -> None:
        await self.execute(
            delete(self.schema_class).where(self.schema_class.id == id_)
        )
        await self._session.flush()

src/infrastructure/database/repository.py
Craxti/fastapi_layered
from contextvars import ContextVar

from sqlalchemy import Result
from sqlalchemy.exc import IntegrityError, PendingRollbackError
from sqlalchemy.ext.asyncio import (
    AsyncEngine,
    AsyncSession,
    async_sessionmaker,
    create_async_engine,
)

from src.config import settings
from src.infrastructure.errors import DatabaseError

__all__ = ("get_session", "engine", "CTX_SESSION")


engine: AsyncEngine = create_async_engine(
    settings.database.url, future=True, pool_pre_ping=True, echo=False
)


def get_session(engine: AsyncEngine | None = engine) -> AsyncSession:
    Session: async_sessionmaker = async_sessionmaker(
        engine, expire_on_commit=False, autoflush=False
    )

    return Session()


CTX_SESSION: ContextVar[AsyncSession] = ContextVar(
    "session", default=get_session()
)


class Session:
    # All sqlalchemy errors that can be raised
    _ERRORS = (IntegrityError, PendingRollbackError)

    def __init__(self) -> None:
        self._session: AsyncSession = CTX_SESSION.get()

    async def execute(self, query) -> Result:
        try:
            result = await self._session.execute(query)
            return result
        except self._ERRORS:
            raise DatabaseError

src/infrastructure/database/session.py
Craxti/fastapi_layered
from typing import TypeVar

from sqlalchemy import Column, ForeignKey, Integer, MetaData, String
from sqlalchemy.orm import declarative_base

__all__ = ("UsersTable", "ProductsTable", "OrdersTable")

meta = MetaData(
    naming_convention={
        "ix": "ix_%(column_0_label)s",
        "uq": "uq_%(table_name)s_%(column_0_name)s",
        "ck": "ck_%(table_name)s_`%(constraint_name)s`",
        "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
        "pk": "pk_%(table_name)s",
    }
)


class _Base:
    """Base class for all database models."""

    id = Column(Integer, primary_key=True)


Base = declarative_base(cls=_Base, metadata=meta)

ConcreteTable = TypeVar("ConcreteTable", bound=Base)  # type: ignore


class UsersTable(Base):
    __tablename__ = "users"

    username: str = Column(String, nullable=False)  # type: ignore
    password: str = Column(String, nullable=False)  # type: ignore


class ProductsTable(Base):
    __tablename__ = "products"

    name: str = Column(String, nullable=False)  # type: ignore
    price: int = Column(Integer, nullable=False)  # type: ignore


class OrdersTable(Base):
    __tablename__ = "orders"

    amount: int = Column(Integer, nullable=False, default=1)  # type: ignore

    product_id: int = Column(
        ForeignKey(ProductsTable.id),
        nullable=False,
    )  # type: ignore[var-annotated]
    user_id: int = Column(
        ForeignKey(UsersTable.id),
        nullable=False,
    )  # type: ignore[var-annotated]

src/infrastructure/database/tables.py
Craxti/fastapi_layered
from functools import wraps

from loguru import logger
from sqlalchemy.exc import IntegrityError, PendingRollbackError
from sqlalchemy.ext.asyncio import AsyncSession

from src.infrastructure.database.session import CTX_SESSION, get_session
from src.infrastructure.errors import DatabaseError


def transaction(coro):
    """This decorator should be used with all coroutines
    that want's access the database for saving a new data.
    """

    @wraps(coro)
    async def inner(*args, **kwargs):
        session: AsyncSession = get_session()
        CTX_SESSION.set(session)

        try:
            result = await coro(*args, **kwargs)
            await session.commit()
            return result
        except DatabaseError as error:
            # NOTE: If any sort of issues are occurred in the code
            #       they are handled on the BaseCRUD level and raised
            #       as a DatabseError.
            #       If the DatabseError is handled within domain/application
            #       levels it is possible that `await session.commit()`
            #       would raise an error.
            logger.error(f"Rolling back changes.\n{error}")
            await session.rollback()
            raise DatabaseError
        except (IntegrityError, PendingRollbackError) as error:
            # NOTE: Since there is a session commit on this level it should
            #       be handled because it can raise some errors also
            logger.error(f"Rolling back changes.\n{error}")
            await session.rollback()
        finally:
            await session.close()

    return inner

src/infrastructure/database/transaction.py
Craxti/fastapi_layered
from src.infrastructure.errors.base import *  # noqa: F401, F403
from src.infrastructure.errors.handlers import *  # noqa: F401, F403

src/infrastructure/errors/__init__.py
Craxti/fastapi_layered
"""
This module is responsible for describing shared errors
that are handled on the last step of processing the request.
"""


from typing import Any

from starlette import status

__all__ = (
    "BaseError",
    "BadRequestError",
    "UnprocessableError",
    "NotFoundError",
    "DatabaseError",
    "AuthenticationError",
    "AuthorizationError",
)


class BaseError(Exception):
    def __init__(
        self,
        *_: tuple[Any],
        message: str = "",
        status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
    ) -> None:
        self.message: str = message
        self.status_code: int = status_code

        super().__init__(message)


class BadRequestError(BaseError):
    def __init__(self, *_: tuple[Any], message: str = "Bad request") -> None:
        super().__init__(
            message=message,
            status_code=status.HTTP_400_BAD_REQUEST,
        )


class UnprocessableError(BaseError):
    def __init__(
        self, *_: tuple[Any], message: str = "Validation error"
    ) -> None:
        super().__init__(
            message=message,
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        )


class NotFoundError(BaseError):
    def __init__(self, *_: tuple[Any], message: str = "Not found") -> None:
        super().__init__(
            message=message, status_code=status.HTTP_404_NOT_FOUND
        )


class DatabaseError(BaseError):
    def __init__(
        self, *_: tuple[Any], message: str = "Database error"
    ) -> None:
        super().__init__(
            message=message, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
        )


class AuthenticationError(BaseError):
    def __init__(
        self, *_: tuple[Any], message: str = "Authentication error"
    ) -> None:
        super().__init__(
            message=message,
            status_code=status.HTTP_401_UNAUTHORIZED,
        )


class AuthorizationError(BaseError):
    def __init__(
        self, *_: tuple[Any], message: str = "Authorization error"
    ) -> None:
        super().__init__(
            message=message, status_code=status.HTTP_403_FORBIDDEN
        )

src/infrastructure/errors/base.py
Craxti/fastapi_layered
"""
This module is used for representing FastAPI error handlers
that are dispatched automatically by fastapi engine.
"""

from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from starlette import status
from starlette.requests import Request

from src.infrastructure.errors.base import BaseError
from src.infrastructure.models import ErrorResponse, ErrorResponseMulti

__all__ = (
    "custom_base_errors_handler",
    "python_base_error_handler",
    "pydantic_validation_errors_handler",
)


def custom_base_errors_handler(_: Request, error: BaseError) -> JSONResponse:
    """This function is called if the BaseError was raised."""

    response = ErrorResponseMulti(
        results=[ErrorResponse(message=error.message.capitalize())]
    )

    return JSONResponse(
        response.dict(by_alias=True),
        status_code=error.status_code,
    )


def python_base_error_handler(_: Request, error: Exception) -> JSONResponse:
    """This function is called if the Exception was raised."""

    response = ErrorResponseMulti(
        results=[ErrorResponse(message=f"Unhandled error: {error}")]
    )

    return JSONResponse(
        content=jsonable_encoder(response.dict(by_alias=True)),
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    )


def pydantic_validation_errors_handler(
    _: Request, error: RequestValidationError
) -> JSONResponse:
    """This function is called if the Pydantic validation error was raised."""

    response = ErrorResponseMulti(
        results=[
            ErrorResponse(
                message=err["msg"],
                path=list(err["loc"]),
            )
            for err in error.errors()
        ]
    )

    return JSONResponse(
        content=jsonable_encoder(response.dict(by_alias=True)),
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
    )

src/infrastructure/errors/handlers.py
Craxti/fastapi_layered
from src.infrastructure.models.base import *  # noqa: F401, F403
from src.infrastructure.models.response import *  # noqa: F401, F403

src/infrastructure/models/__init__.py
Craxti/fastapi_layered
"""
This model includes basic data models that are used in the whole application.
"""

import json
from typing import TypeVar

from pydantic import BaseModel, Extra

__all__ = (
    "InternalModel",
    "_InternalModel",
    "PublicModel",
    "_PublicModel",
    "FrozenModel",
)


def to_camelcase(string: str) -> str:
    """The alias generator for PublicModel."""

    resp = "".join(
        word.capitalize() if index else word
        for index, word in enumerate(string.split("_"))
    )
    return resp


_json_encoders = {
    # np.float32: lambda v: float(v) if v else None,
}


class FrozenModel(BaseModel):
    class Config:
        json_encoders = _json_encoders
        orm_mode = True
        use_enum_values = True
        allow_population_by_field_name = True
        arbitrary_types_allowed = True
        allow_mutation = False


class InternalModel(BaseModel):
    class Config:
        json_encoders = _json_encoders
        extra = Extra.forbid
        orm_mode = True
        use_enum_values = True
        allow_population_by_field_name = True
        validate_assignment = True
        arbitrary_types_allowed = True


_InternalModel = TypeVar("_InternalModel", bound=InternalModel)


class PublicModel(BaseModel):
    class Config:
        json_encoders = _json_encoders
        extra = Extra.ignore
        orm_mode = True
        use_enum_values = True
        validate_assignment = True
        alias_generator = to_camelcase
        allow_population_by_field_name = True
        arbitrary_types_allowed = True

    def encoded_dict(self, by_alias=True):
        """This method might be useful is the data should be passed
        only with primitives that are allowed by JSON format.
        The regular .dict() does not return the ISO datetime format
        but the .json() - does. This method is a combination of them.
        """
        return json.loads(self.json(by_alias=by_alias))


_PublicModel = TypeVar("_PublicModel", bound=PublicModel)

src/infrastructure/models/base.py
Craxti/fastapi_layered
from collections.abc import Mapping
from typing import Any, Generic

from pydantic import Field, conlist
from pydantic.generics import GenericModel

from src.infrastructure.models.base import PublicModel, _PublicModel

__all__ = (
    "ResponseMulti",
    "Response",
    "_Response",
    "ErrorResponse",
    "ErrorResponseMulti",
)


class ResponseMulti(PublicModel, GenericModel, Generic[_PublicModel]):
    """Generic response model that consist multiple results."""

    result: list[_PublicModel]


class Response(PublicModel, GenericModel, Generic[_PublicModel]):
    """Generic response model that consist only one result."""

    result: _PublicModel


_Response = Mapping[int | str, dict[str, Any]]


class ErrorResponse(PublicModel):
    """Error response model."""

    message: str = Field(description="This field represent the message")
    path: list = Field(
        description="The path to the field that raised the error",
        default_factory=list,
    )


class ErrorResponseMulti(PublicModel):
    """The public error respnse model that includes multiple objects."""

    results: conlist(ErrorResponse, min_items=1)  # type: ignore

src/infrastructure/models/response.py
Craxti/fastapi_layered
from fastapi import FastAPI
from loguru import logger

from src.config import settings
from src.infrastructure import application
from src.presentation import rest

# Adjust the logging
# -------------------------------
logger.add(
    "".join(
        [
            str(settings.root_dir),
            "/logs/",
            settings.logging.file.lower(),
            ".log",
        ]
    ),
    format=settings.logging.format,
    rotation=settings.logging.rotation,
    compression=settings.logging.compression,
    level="INFO",
)


# Adjust the application
# -------------------------------
app: FastAPI = application.create(
    debug=settings.debug,
    rest_routers=(rest.products.router, rest.orders.router),
    startup_tasks=[],
    shutdown_tasks=[],
)

src/main.py
Craxti/fastapi_layered
from src.presentation.rest import orders, products

src/presentation/rest/__init__.py
Craxti/fastapi_layered
from fastapi import APIRouter, Depends, Request, status

from src.application import orders
from src.application.authentication import get_current_user
from src.domain.orders import (
    Order,
    OrderCreateRequestBody,
    OrderPublic,
    OrdersRepository,
)
from src.domain.users import User
from src.infrastructure.database.transaction import transaction
from src.infrastructure.models import Response, ResponseMulti

router = APIRouter(prefix="/orders", tags=["Orders"])


@router.get("", status_code=status.HTTP_200_OK)
@transaction
async def orders_list(
    request: Request, user: User = Depends(get_current_user)
) -> ResponseMulti[OrderPublic]:
    """Get all orders."""

    # Get all products from the database
    orders_public = [
        OrderPublic.from_orm(order) async for order in OrdersRepository().all()
    ]

    return ResponseMulti[OrderPublic](result=orders_public)


@router.post("", status_code=status.HTTP_201_CREATED)
async def order_create(
    request: Request,
    schema: OrderCreateRequestBody,
    user: User = Depends(get_current_user),
) -> Response[OrderPublic]:
    """Create a new order."""

    # Save product to the database
    order: Order = await orders.create(payload=schema.dict(), user=user)
    order_public = OrderPublic.from_orm(order)

    return Response[OrderPublic](result=order_public)

src/presentation/rest/orders.py
Craxti/fastapi_layered
from fastapi import APIRouter, Depends, Request, status

from src.application.authentication import get_current_user
from src.domain.products import (
    Product,
    ProductCreateRequestBody,
    ProductPublic,
    ProductRepository,
    ProductUncommited,
)
from src.domain.users import User
from src.infrastructure.database.transaction import transaction
from src.infrastructure.models import Response, ResponseMulti

router = APIRouter(prefix="/products", tags=["Products"])


@router.get("", status_code=status.HTTP_200_OK)
@transaction
async def products_list(request: Request) -> ResponseMulti[ProductPublic]:
    """Get all products."""

    # Get all products from the database
    products_public = [
        ProductPublic.from_orm(product)
        async for product in ProductRepository().all()
    ]

    return ResponseMulti[ProductPublic](result=products_public)


@router.post("", status_code=status.HTTP_201_CREATED)
@transaction
async def product_create(
    _: Request,
    schema: ProductCreateRequestBody,
    user: User = Depends(get_current_user),
) -> Response[ProductPublic]:
    """Create a new product."""

    # Save product to the database
    product: Product = await ProductRepository().create(
        ProductUncommited(**schema.dict())
    )
    product_public = ProductPublic.from_orm(product)

    return Response[ProductPublic](result=product_public)

src/presentation/rest/products.py
Craxti/fastapi_layered