You've already forked RekomenciBackend
fast init
This commit is contained in:
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user