Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: refactor filmin to a more clean code flavour #16

Merged
merged 1 commit into from
Aug 13, 2023
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
2 changes: 2 additions & 0 deletions env-api-dev-example
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
APP_ENV=dev
DEBUG=true
# SqlAchemy Verbose
SA_ECHO=true
160 changes: 90 additions & 70 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ pytest-asyncio = "^0.21.0"
poetry-types = "^0.4.0"
beartype = "^0.14.1"
types-redis = "^4.6.0.2"
faker = "^19.3.0"


[build-system]
Expand Down
7 changes: 2 additions & 5 deletions src/app/app_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from core.di.mixins import CoreContainerMixin
from filmin.di.mixins import FilminContainerMixin
from infra.cache.ports import AbstractCacheRepository
from infra.database.sqlalchemy.session import AbstractDatabase
from infra.cache.redis_cache import RedisCache
from infra.database.sqlalchemy.session import AbstractDatabase, Database
from utils.di import DIContainer, di_singleton


Expand All @@ -12,14 +13,10 @@ class AppContainerMixin:
cache_repository: AbstractCacheRepository

def _get_db(self) -> AbstractDatabase:
from infra.database.sqlalchemy.session import Database

return Database()

@di_singleton
def _get_cache_repository(self) -> AbstractCacheRepository:
from infra.cache.redis_cache import RedisCache

return RedisCache(
url=settings.REDIS_URL,
user=settings.REDIS_USER,
Expand Down
2 changes: 1 addition & 1 deletion src/app/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from auth.adapters.api.http.router import router as auth_router
from config import settings
from core.adapters.api.http.router import router as core_router
from filmin.api.router import router as filmin_router
from filmin.adapters.api.http.router import router as filmin_router
from shared.exceptions import APPExceptionError


Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
79 changes: 45 additions & 34 deletions src/filmin/api/genre.py → src/filmin/adapters/api/http/genre.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,30 @@
from typing import Dict, List, Union

from fastapi import Depends
from fastapi.responses import JSONResponse
from fastapi.routing import APIRouter

from app.app_container import AppContainer
from app.exceptions import EmptyPayloadExceptionError
from app.schemas import Session
from app.session_deps import check_access_token, is_admin_session
from filmin.api.schemas.genre import (
from filmin.adapters.api.http.presenters.genre import GenrePagedListPresenter, GenrePresenter
from filmin.adapters.api.http.schemas.genre import (
CreateGenreRequestDTO,
CreateGenreResponseDTO,
GenreListPagedResponse,
GenreResponse,
UpdateGenreRequestDTO,
UpdatePartialGenreRequestDTO,
)
from filmin.data.repositories.ports.genre import AbstractGenreRepository
from filmin.domain.schemas.genre import Genre
from filmin.domain.entities.genre import Genre
from filmin.domain.services.genre import GenreService
from filmin.domain.use_cases.genre import CreateGenreUseCase, GetGenresUseCase, GetGenreUseCase, UpdateGenreUseCase
from filmin.schemas.genre import CreateGenreInDTO, UpdatePartialGenreInDTO
from shared.api.schemas.page import PagedResponseSchema, PageParams
from shared.api.schemas.page import PageParams


router = APIRouter(prefix='/genre')


def get_genre_repository() -> AbstractGenreRepository:
from app.app_container import AppContainer

return AppContainer().genre_repository


@router.get(
'/{code}',
responses={
Expand All @@ -38,32 +35,33 @@ def get_genre_repository() -> AbstractGenreRepository:
)
async def get_genre(
code: str,
genre_repository: AbstractGenreRepository = Depends(get_genre_repository),
container: AppContainer = Depends(AppContainer),
_: Session = Depends(check_access_token),
) -> Genre:
return await genre_repository.get_by_id(code)
) -> GenreResponse:
service = GenreService(container.genre_repository)
presenter = GenrePresenter()
usecase = GetGenreUseCase(presenter, service)
await usecase.execute(code)
return presenter.result


@router.get(
'',
response_model=PagedResponseSchema[Genre],
responses={
200: {'description': 'Successful Response'},
422: {'description': 'Unprocessable Entity'},
},
)
async def list_genre(
async def list_genres(
page_params: PageParams = Depends(),
genre_repository: AbstractGenreRepository = Depends(get_genre_repository),
container: AppContainer = Depends(AppContainer),
_: Session = Depends(check_access_token),
) -> Dict[str, Union[int, List[Genre]]]:
count, items = await genre_repository.get_xpage(**page_params.model_dump())
return {
'total': count,
'results': items,
'page': page_params.page,
'size': page_params.size,
}
) -> GenreListPagedResponse:
service = GenreService(container.genre_repository)
presenter = GenrePagedListPresenter(page_params)
usecase = GetGenresUseCase(presenter, service)
await usecase.execute(page_params)
return presenter.result


@router.post(
Expand All @@ -78,11 +76,15 @@ async def list_genre(
)
async def create_genre(
request_data: CreateGenreRequestDTO,
genre_repository: AbstractGenreRepository = Depends(get_genre_repository),
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> Genre:
in_dto = CreateGenreInDTO.model_validate(request_data.model_dump())
return await genre_repository.create(in_dto)
service = GenreService(container.genre_repository)
presenter = GenrePresenter()
usecase = CreateGenreUseCase(presenter, service)
await usecase.execute(in_dto)
return presenter.result


@router.put(
Expand All @@ -96,12 +98,16 @@ async def create_genre(
async def update_genre(
code: str,
request_data: UpdateGenreRequestDTO,
genre_repository: AbstractGenreRepository = Depends(get_genre_repository),
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> Genre:
) -> GenreResponse:
in_data = request_data.model_dump()
in_dto = UpdatePartialGenreInDTO.model_validate(in_data)
return await genre_repository.update(code, in_dto)
service = GenreService(container.genre_repository)
presenter = GenrePresenter()
usecase = UpdateGenreUseCase(presenter, service)
await usecase.execute(code, in_dto)
return presenter.result


@router.patch(
Expand All @@ -115,11 +121,16 @@ async def update_genre(
async def update_genre_partially(
code: str,
request_data: UpdatePartialGenreRequestDTO,
genre_repository: AbstractGenreRepository = Depends(get_genre_repository),
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> Genre:
) -> GenreResponse:
in_data = request_data.model_dump(exclude_unset=True)
if not in_data.keys():
raise EmptyPayloadExceptionError()
in_dto = UpdatePartialGenreInDTO.model_validate(in_data)
return await genre_repository.update(code, in_dto)

service = GenreService(container.genre_repository)
presenter = GenrePresenter()
usecase = UpdateGenreUseCase(presenter, service)
await usecase.execute(code, in_dto)
return presenter.result
145 changes: 145 additions & 0 deletions src/filmin/adapters/api/http/movie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from typing import Annotated
import uuid

from fastapi import Depends
from fastapi.responses import JSONResponse
from fastapi.routing import APIRouter

from app.app_container import AppContainer
from app.exceptions import EmptyPayloadExceptionError
from app.schemas import Session
from app.session_deps import is_admin_session
from filmin.adapters.api.http.presenters.movie import (
MoviePagedListPresenter,
MoviePresenter,
)
from filmin.adapters.api.http.schemas.movie import (
CreateMovieRequestDTO,
CreateMovieResponseDTO,
MovieListPagedResponse,
MovieResponse,
UpdateMovieRequestDTO,
UpdatePartialMovieRequestDTO,
)
from filmin.domain.entities.movie import Movie
from filmin.domain.services.movie import MovieService
from filmin.domain.use_cases.movie import (
CreateMovieUseCase,
GetMoviesUseCase,
GetMovieUseCase,
UpdateMovieUseCase,
)
from filmin.schemas.movie import CreateMovieInDTO, UpdatePartialMovieInDTO
from shared.api.schemas.page import PageParams


router = APIRouter(prefix='/movie')


@router.get(
'/{uuid}',
responses={
200: {'description': 'Successful Response'},
404: {'description': 'Movie not found'},
422: {'description': 'Unprocessable Entity'},
},
)
async def get_movie(
uuid: Annotated[str, uuid.UUID],
container: AppContainer = Depends(AppContainer),
) -> MovieResponse:
service = MovieService(container.movie_repository)
presenter = MoviePresenter()
usecase = GetMovieUseCase(presenter, service)
await usecase.execute(uuid)
return presenter.result


@router.get(
'',
responses={
200: {'description': 'Successful Response'},
422: {'description': 'Unprocessable Entity'},
},
)
async def list_movies(
page_params: PageParams = Depends(),
container: AppContainer = Depends(AppContainer),
) -> MovieListPagedResponse:
service = MovieService(container.movie_repository)
presenter = MoviePagedListPresenter(page_params)
usecase = GetMoviesUseCase(presenter, service)
await usecase.execute(page_params)
return presenter.result


@router.post(
'',
response_class=JSONResponse,
response_model=CreateMovieResponseDTO,
status_code=201,
responses={
201: {'description': 'Item created'},
422: {'description': 'Unprocessable Entity'},
},
)
async def create_movie(
request_data: CreateMovieRequestDTO,
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> Movie:
in_dto = CreateMovieInDTO.model_validate(request_data.model_dump())
service = MovieService(container.movie_repository)
presenter = MoviePresenter()
usecase = CreateMovieUseCase(presenter, service)
await usecase.execute(in_dto)
return presenter.result


@router.put(
'/{uuid}',
responses={
200: {'description': 'Item updated'},
404: {'description': 'Item not found'},
422: {'description': 'Unprocessable Entity'},
},
)
async def update_movie(
uuid: Annotated[str, uuid.UUID],
request_data: UpdateMovieRequestDTO,
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> MovieResponse:
in_data = request_data.model_dump()
in_dto = UpdatePartialMovieInDTO.model_validate(in_data)
service = MovieService(container.movie_repository)
presenter = MoviePresenter()
usecase = UpdateMovieUseCase(presenter, service)
await usecase.execute(uuid, in_dto)
return presenter.result


@router.patch(
'/{uuid}',
responses={
200: {'description': 'Item updated'},
404: {'description': 'Item not found'},
422: {'description': 'Unprocessable Entity'},
},
)
async def update_movie_partially(
uuid: Annotated[str, uuid.UUID],
request_data: UpdatePartialMovieRequestDTO,
container: AppContainer = Depends(AppContainer),
_: Session = Depends(is_admin_session),
) -> MovieResponse:
in_data = request_data.model_dump(exclude_unset=True)
if not in_data.keys():
raise EmptyPayloadExceptionError()
in_dto = UpdatePartialMovieInDTO.model_validate(in_data)

service = MovieService(container.movie_repository)
presenter = MoviePresenter()
usecase = UpdateMovieUseCase(presenter, service)
await usecase.execute(uuid, in_dto)
return presenter.result
29 changes: 29 additions & 0 deletions src/filmin/adapters/api/http/presenters/genre.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from typing import List

from filmin.adapters.api.http.schemas.genre import GenreListPagedResponse, GenreResponse
from filmin.domain.entities.genre import Genre
from shared.api.schemas.page import PageParams
from shared.presenter import AbstractPresenter


class GenrePresenter(AbstractPresenter[Genre, GenreResponse]):
result: GenreResponse

async def present(self, data: Genre) -> None:
self.result = GenreResponse.model_validate(data, from_attributes=True)


class GenrePagedListPresenter(AbstractPresenter[List[Genre], List[GenreResponse]]):
result: GenreListPagedResponse

def __init__(self, page_params: PageParams) -> None:
self.page_params = page_params

async def present(self, data: List[Genre]) -> None:
list_items = [GenreResponse.model_validate(item, from_attributes=True) for item in data]
self.result = GenreListPagedResponse(
results=list_items,
total=len(list_items),
page=self.page_params.page,
size=self.page_params.size,
)
Loading