fast init

This commit is contained in:
ivankirpichnikov
2025-10-16 23:03:50 +03:00
parent b84e0370d6
commit 652da07d12
50 changed files with 1012 additions and 0 deletions
@@ -0,0 +1,53 @@
from dataclasses import dataclass
from datetime import timedelta
from pathlib import Path
from tomllib import loads
from typing import dataclass_transform
from adaptix import P, Retort, loader
from template_project.application.common.containers import SecretString
@dataclass_transform(frozen_default=True)
def to_configuration[ClsT](cls: type[ClsT]) -> type[ClsT]:
return dataclass(frozen=True, slots=True, repr=False)(cls)
@to_configuration
class DatabaseConfiguration:
url: SecretString
@to_configuration
class AccessTokenConfiguration:
crypto_key: str
expires_in: timedelta
@to_configuration
class ServerConfiguration:
host: str
port: int
access_log: bool
@to_configuration
class Configuration:
server: ServerConfiguration
database: DatabaseConfiguration
access_token: AccessTokenConfiguration
retort = Retort(
recipe=[
loader(SecretString, SecretString),
loader(P[AccessTokenConfiguration].expires_in, lambda value: timedelta(seconds=value)),
],
)
def load_configuration(path: Path) -> Configuration:
with path.open("r", encoding="utf-8") as config:
data = loads(config.read())
return retort.load(data, Configuration)
@@ -0,0 +1,99 @@
import argparse
import asyncio
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
import sys
from typing import Final
from dishka import AsyncContainer
from dishka.integrations.fastapi import setup_dishka
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import uvicorn
from template_project.web_api.configuration import load_configuration
from template_project.web_api.ioc.make import make_ioc
LOG_CONFIG: Final = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
}
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
yield
await app.state.dishka_container.close()
def make_asgi_application(
ioc: AsyncContainer,
) -> FastAPI:
app = FastAPI(
lifespan=lifespan,
docs_url="/docs",
title="Template project",
description="Template project API",
version="1.0.0",
openapi_url="/openapi.json",
)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
setup_dishka(container=ioc, app=app)
return app
def _main(
configuration_path: Path,
) -> None:
configuration = load_configuration(configuration_path)
ioc = make_ioc(configuration)
asgi_application = make_asgi_application(ioc)
uvicorn.run(
asgi_application,
port=configuration.server.port,
host=configuration.server.host,
log_config=LOG_CONFIG,
access_log=configuration.server.access_log,
)
def main() -> None:
if sys.platform == "win32":
from asyncio import WindowsSelectorEventLoopPolicy
asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
arg_parser = argparse.ArgumentParser()
subparsers = arg_parser.add_subparsers()
web_api_parser = subparsers.add_parser("web_api")
web_api_parser.add_argument("configuration", dest="configuration", type=Path)
args = arg_parser.parse_args()
_main(args.configuration)
if __name__ == "__main__":
main()
@@ -0,0 +1,73 @@
from abc import abstractmethod
from fastapi import Request
from template_project.application.access_token.cryptographer import AccessTokenCryptographer
from template_project.application.access_token.data_gateway import AccessTokenDataGateway
from template_project.application.access_token.entity import AccessTokenId
from template_project.application.access_token.errors import AccessTokenExpiredError
from template_project.application.common.identity_provider import IdentityProvider
from template_project.application.user.data_gateway import UserDataGateway
from template_project.application.user.entity import User
from template_project.application.user.errors import UserUnauthorizedError
TOKEN_TYPE = "Bearer"
BEARER_SECTIONS = 2
AUTH_HEADER = "Authorization"
class WebApiIdentityProvider(IdentityProvider):
def __init__(
self,
request: Request,
user_data_gateway: UserDataGateway,
access_token_data_gateway: AccessTokenDataGateway,
access_token_cryptographer: AccessTokenCryptographer,
) -> None:
self._request = request
self._user_data_gateway = user_data_gateway
self._access_token_data_gateway = access_token_data_gateway
self._access_token_cryptographer = access_token_cryptographer
@abstractmethod
async def get_current_user(self) -> User:
auth_tokn = self._request.headers[AUTH_HEADER]
access_token_id = self._parse_token(auth_tokn)
if access_token_id is None:
raise UserUnauthorizedError
access_token = await self._access_token_data_gateway.load_with_id(access_token_id)
if access_token is None:
raise UserUnauthorizedError
try:
access_token.ensure_expired()
except AccessTokenExpiredError as error:
raise UserUnauthorizedError from error
user = await self._user_data_gateway.load_with_id(access_token.user_id)
if user is None:
raise UserUnauthorizedError
return user
def _parse_token(self, token: str) -> AccessTokenId | None:
authorization_header = self._request.headers.get(AUTH_HEADER)
if authorization_header is None:
return None
sections = authorization_header.split(" ")
if len(sections) != BEARER_SECTIONS:
return None
token_type, token = sections
if token_type != TOKEN_TYPE:
return None
return self._access_token_cryptographer.decrypto(token)
@@ -0,0 +1,22 @@
from typing import AsyncIterable
from dishka import Provider, Scope, provide
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, create_async_engine
from template_project.web_api.configuration import DatabaseConfiguration
class ConnectionProvider(Provider):
@provide(scope=Scope.APP)
async def make_engine(self, configuration: DatabaseConfiguration) -> AsyncIterable[AsyncEngine]:
engine = create_async_engine(configuration.url.get_value())
yield engine
await engine.dispose()
@provide()
async def make_connection(self, engine: AsyncEngine) -> AsyncIterable[AsyncSession]:
session = AsyncSession(
bind=engine,
expire_on_commit=True,
)
async with session:
yield session
@@ -0,0 +1,25 @@
import argon2
from cryptography.fernet import Fernet
from dishka import Provider, Scope, WithParents, provide, provide_all
from template_project.adapters.access_token.cryptographer import FernetAccessTokenCryptographer
from template_project.adapters.password_utils import ArgonPasswordHasher, ArgonPasswordVerifying
from template_project.web_api.configuration import AccessTokenConfiguration
class CryptographerProvider(Provider):
scope = Scope.APP
@provide
def argon_password_hasher(self) -> argon2.PasswordHasher:
return argon2.PasswordHasher()
@provide
def fernet(self, configuration: AccessTokenConfiguration) -> Fernet:
return Fernet(configuration.crypto_key)
access_token_cryptographer = provide(WithParents[FernetAccessTokenCryptographer])
password_utils = provide_all(
WithParents[ArgonPasswordHasher],
WithParents[ArgonPasswordVerifying],
)
@@ -0,0 +1,15 @@
from dishka import Provider, Scope, WithParents, provide, provide_all
from template_project.adapters.data_gateways.access_token import DefaultAccessTokenDataGateway
from template_project.adapters.data_gateways.user import DefaultUserDataGateway
from template_project.adapters.unit_of_work import DefaultUnitOfWork
class DataGatewayProvider(Provider):
scope = Scope.REQUEST
unit_of_work = provide(WithParents[DefaultUnitOfWork])
data_gateways = provide_all(
WithParents[DefaultUserDataGateway],
WithParents[DefaultAccessTokenDataGateway],
)
@@ -0,0 +1,11 @@
from dishka import Provider, Scope, provide_all
from template_project.adapters.access_token.factory import DefaultAccessTokenFactory
class FactoryProvider(Provider):
scope = Scope.APP
factories = provide_all(
DefaultAccessTokenFactory,
)
@@ -0,0 +1,11 @@
from dishka import Provider, Scope, provide_all
from template_project.application.user.interactors.sign_up import UserSignUpInteractor
class InteractorProvider(Provider):
scope = Scope.REQUEST
interactors = provide_all(
UserSignUpInteractor,
)
+23
View File
@@ -0,0 +1,23 @@
from dishka import AsyncContainer, make_async_container
from dishka.integrations.fastapi import FastapiProvider
from template_project.web_api.configuration import AccessTokenConfiguration, Configuration, DatabaseConfiguration, ServerConfiguration
from template_project.web_api.ioc.data_gateway import DataGatewayProvider
from template_project.web_api.ioc.factory import FactoryProvider
from template_project.web_api.ioc.interactor import InteractorProvider
from template_project.web_api.ioc.cryptographer import CryptographerProvider
def make_ioc(configuration: Configuration) -> AsyncContainer:
return make_async_container(
FactoryProvider(),
FastapiProvider(),
InteractorProvider(),
DataGatewayProvider(),
CryptographerProvider(),
context={
ServerConfiguration: configuration.server,
DatabaseConfiguration: configuration.database,
AccessTokenConfiguration: configuration.access_token,
},
)
@@ -0,0 +1,33 @@
from dishka import FromDishka
from dishka.integrations.fastapi import DishkaRoute
from fastapi import APIRouter
from pydantic import BaseModel, SecretStr
from template_project.application.common.containers import SecretString
from template_project.application.user.interactors.sign_up import UserSignUpInteractor
router = APIRouter(route_class=DishkaRoute)
class UserSignUpRequest(BaseModel):
email: str
password: SecretStr
class UserSignUpResponse(BaseModel):
access_token: str
@router.post("/user/sign_up")
async def sign_up(
request: UserSignUpRequest,
interactor: FromDishka[UserSignUpInteractor],
) -> UserSignUpResponse:
response_interactor = await interactor.execute(
email=request.email,
password=SecretString(request.password.get_secret_value())
)
return UserSignUpResponse(
access_token=response_interactor.access_token,
)