From 0cd0914c999478f28970da364befc58eb84ffdc0 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 16 Feb 2024 13:07:59 +0400 Subject: [PATCH 01/39] Refactor pyproject.toml, add .ruff.toml, format with ruff --- .ruff.toml | 26 ++++++ pyproject.toml | 56 +++---------- .../command_handlers/create_movie.py | 12 ++- .../command_handlers/delete_movie.py | 8 +- .../command_handlers/rate_movie.py | 8 +- .../command_handlers/register_user.py | 8 +- .../command_handlers/review_movie.py | 8 +- .../command_handlers/unrate_movie.py | 8 +- .../application/query_handlers/get_movie.py | 8 +- .../query_handlers/get_movie_ratings.py | 17 +++- .../query_handlers/get_movie_reviews.py | 17 +++- .../application/query_handlers/get_movies.py | 12 ++- .../query_handlers/get_my_ratings.py | 17 +++- .../query_handlers/get_my_reviews.py | 17 +++- .../application/query_handlers/get_rating.py | 8 +- .../application/query_handlers/get_review.py | 8 +- src/amdb/application/query_handlers/login.py | 4 +- src/amdb/domain/constants/exceptions.py | 4 +- src/amdb/domain/services/access_concern.py | 8 +- src/amdb/domain/services/rate_movie.py | 4 +- src/amdb/domain/services/unrate_movie.py | 6 +- .../auth/session/identity_provider.py | 8 +- .../password_manager/password_manager.py | 4 +- .../persistence/alembic/config.py | 4 +- .../migrations/versions/65f8840f4494_.py | 12 ++- .../migrations/versions/85a348467b90_.py | 4 +- .../migrations/versions/9e92de201574_.py | 4 +- .../persistence/sqlalchemy/gateway_factory.py | 4 +- .../persistence/sqlalchemy/gateways/movie.py | 8 +- .../persistence/sqlalchemy/gateways/rating.py | 18 ++++- .../persistence/sqlalchemy/gateways/review.py | 18 ++++- .../persistence/sqlalchemy/gateways/user.py | 4 +- .../sqlalchemy/gateways/user_password_hash.py | 8 +- .../persistence/sqlalchemy/mappers/movie.py | 4 +- .../persistence/sqlalchemy/mappers/rating.py | 4 +- .../persistence/sqlalchemy/mappers/review.py | 10 ++- .../persistence/sqlalchemy/mappers/user.py | 4 +- src/amdb/main/cli/di.py | 12 ++- src/amdb/main/ioc.py | 80 ++++++++++++++----- src/amdb/main/web_api/app.py | 4 +- src/amdb/main/web_api/config.py | 4 +- src/amdb/main/web_api/di.py | 20 +++-- src/amdb/presentation/cli/movie.py | 4 +- src/amdb/presentation/handler_factory.py | 12 ++- .../web_api/dependencies/identity_provider.py | 20 +++-- .../web_api/exception_handlers.py | 8 +- .../web_api/routers/auth/login.py | 12 ++- .../web_api/routers/auth/register.py | 12 ++- .../web_api/routers/movies/get_movie.py | 12 ++- .../web_api/routers/movies/get_movies.py | 12 ++- .../routers/ratings/get_movie_ratings.py | 21 +++-- .../web_api/routers/ratings/get_my_ratings.py | 21 +++-- .../web_api/routers/ratings/get_rating.py | 12 ++- .../web_api/routers/ratings/rate_movie.py | 12 ++- .../web_api/routers/ratings/unrate_movie.py | 12 ++- .../routers/reviews/get_movie_reviews.py | 21 +++-- .../web_api/routers/reviews/get_my_reviews.py | 21 +++-- .../web_api/routers/reviews/get_review.py | 12 ++- .../web_api/routers/reviews/review_movie.py | 12 ++- .../command_handlers/test_create_movie.py | 12 ++- .../command_handlers/test_delete_movie.py | 8 +- .../command_handlers/test_rate_movie.py | 8 +- .../command_handlers/test_register_user.py | 8 +- .../command_handlers/test_review_movie.py | 8 +- .../command_handlers/test_unrate_movie.py | 8 +- tests/unit/application/conftest.py | 36 ++++++--- .../query_handlers/test_get_movie.py | 8 +- .../query_handlers/test_get_movie_ratings.py | 21 +++-- .../query_handlers/test_get_movie_reviews.py | 21 +++-- .../query_handlers/test_get_movies.py | 12 ++- .../query_handlers/test_get_my_ratings.py | 21 +++-- .../query_handlers/test_get_my_reviews.py | 21 +++-- .../query_handlers/test_get_rating.py | 8 +- .../query_handlers/test_get_review.py | 8 +- .../application/query_handlers/test_login.py | 4 +- .../infrastructure/alembic/test_stairway.py | 12 ++- 76 files changed, 700 insertions(+), 262 deletions(-) create mode 100644 .ruff.toml diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..e3dc0bb --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,26 @@ +line-length = 79 +src = ["src"] +include = ["src/**.py", "tests/**.py"] + +extend-select = [ + "N", # https://docs.astral.sh/ruff/settings/#pep8-naming + "EM", # https://docs.astral.sh/ruff/settings/#flake8-errmsg + "ISC", # https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat + "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "Q", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt +] +select = [ + "F401", # unused-import + "F406", # undefined-local-with-nested-import-star-usage + "COM812", # missing-trailing-comma + "DTZ003", # call-datetime-utcnow + "EM102", # f-string-in-exception + "INP001", # implicit-namespace-package + "PIE794", # duplicate-class-field-definition + "PIE796", # non-unique-enums + "T201", # print + "SLF001", # private-member-access +] + +[format] +quote-style = "double" diff --git a/pyproject.toml b/pyproject.toml index fe9e7be..1c0694a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,13 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" +[tool.setuptools.packages.find] +where = ["src"] +include = ["amdb*"] + +[tool.setuptools.package-data] +amdb = ["infrastructure/persistence/alembic/alembic.ini"] + [project] name = "amdb" description = "Awesome Movie Database Backend" @@ -32,10 +39,10 @@ web_api = [ cli = [ "typer[all]==0.9.*", ] -style = [ - "mypy==1.8.0", - "ruff==0.1.9", - "types-redis", +dev = [ + "mypy==1.8.*", + "ruff==0.1.*", + "pre-commit==3.5.*", ] test = [ "pytest", @@ -43,48 +50,7 @@ test = [ coverage = [ "pytest-cov", ] -dev = [ - "amdb[web_api,cli,style,test,coverage]", - "pre-commit==3.5.0", -] [project.scripts] amdb-cli = "amdb.main.cli.__main__:main" amdb-web_api = "amdb.main.web_api.__main__:main" - -[tool.setuptools.packages.find] -where = ["src"] -include = ["amdb*"] - -[tool.setuptools.package-data] -amdb = ["infrastructure/persistence/alembic/alembic.ini"] - -[tool.pytest.ini_options] -pythonpath = "src/" - -[tool.ruff.lint] -extend-select = [ - "N", # https://docs.astral.sh/ruff/settings/#pep8-naming - "EM", # https://docs.astral.sh/ruff/settings/#flake8-errmsg - "ISC", # https://docs.astral.sh/ruff/settings/#flake8-implicit-str-concat - "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g - "Q", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt -] -select = [ - "F401", # unused-import - "F406", # undefined-local-with-nested-import-star-usage - "COM812", # missing-trailing-comma - "DTZ003", # call-datetime-utcnow - "EM102", # f-string-in-exception - "INP001", # implicit-namespace-package - "PIE794", # duplicate-class-field-definition - "PIE796", # non-unique-enums - "T201", # print - "SLF001", # private-member-access -] - -[tool.ruff] -line-length = 99 - -[tool.ruff.format] -quote-style = "double" diff --git a/src/amdb/application/command_handlers/create_movie.py b/src/amdb/application/command_handlers/create_movie.py index 0b81bb5..3c481e0 100644 --- a/src/amdb/application/command_handlers/create_movie.py +++ b/src/amdb/application/command_handlers/create_movie.py @@ -3,11 +3,17 @@ from amdb.domain.entities.movie import MovieId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import CREATE_MOVIE_ACCESS_DENIED +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.common.constants.exceptions import ( + CREATE_MOVIE_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError from amdb.application.commands.create_movie import CreateMovieCommand diff --git a/src/amdb/application/command_handlers/delete_movie.py b/src/amdb/application/command_handlers/delete_movie.py index 7cca51f..c4b5963 100644 --- a/src/amdb/application/command_handlers/delete_movie.py +++ b/src/amdb/application/command_handlers/delete_movie.py @@ -1,10 +1,14 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( DELETE_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/rate_movie.py b/src/amdb/application/command_handlers/rate_movie.py index bb022e7..ee50707 100644 --- a/src/amdb/application/command_handlers/rate_movie.py +++ b/src/amdb/application/command_handlers/rate_movie.py @@ -7,12 +7,16 @@ from amdb.domain.entities.rating import RatingId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.rate_movie import RateMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( RATE_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/register_user.py b/src/amdb/application/command_handlers/register_user.py index 6498ff0..c3f3431 100644 --- a/src/amdb/application/command_handlers/register_user.py +++ b/src/amdb/application/command_handlers/register_user.py @@ -3,10 +3,14 @@ from amdb.domain.entities.user import UserId from amdb.domain.services.create_user import CreateUser from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork from amdb.application.common.interfaces.password_manager import PasswordManager -from amdb.application.common.constants.exceptions import USER_NAME_ALREADY_EXISTS +from amdb.application.common.constants.exceptions import ( + USER_NAME_ALREADY_EXISTS, +) from amdb.application.common.exception import ApplicationError from amdb.application.commands.register_user import RegisterUserCommand diff --git a/src/amdb/application/command_handlers/review_movie.py b/src/amdb/application/command_handlers/review_movie.py index 3a6b80d..036305e 100644 --- a/src/amdb/application/command_handlers/review_movie.py +++ b/src/amdb/application/command_handlers/review_movie.py @@ -7,12 +7,16 @@ from amdb.domain.entities.review import ReviewId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( REVIEW_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/unrate_movie.py b/src/amdb/application/command_handlers/unrate_movie.py index f9fb90a..67e19e2 100644 --- a/src/amdb/application/command_handlers/unrate_movie.py +++ b/src/amdb/application/command_handlers/unrate_movie.py @@ -3,11 +3,15 @@ from amdb.domain.entities.movie import Movie from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( UNRATE_MOVIE_ACCESS_DENIED, USER_IS_NOT_OWNER, diff --git a/src/amdb/application/query_handlers/get_movie.py b/src/amdb/application/query_handlers/get_movie.py index 52fe8e6..0632db5 100644 --- a/src/amdb/application/query_handlers/get_movie.py +++ b/src/amdb/application/query_handlers/get_movie.py @@ -1,7 +1,11 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_movie import GetMovieQuery, GetMovieResult from amdb.application.common.constants.exceptions import ( GET_MOVIE_ACCESS_DENIED, diff --git a/src/amdb/application/query_handlers/get_movie_ratings.py b/src/amdb/application/query_handlers/get_movie_ratings.py index 880e706..6566f04 100644 --- a/src/amdb/application/query_handlers/get_movie_ratings.py +++ b/src/amdb/application/query_handlers/get_movie_ratings.py @@ -1,9 +1,16 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_movie_ratings import GetMovieRatingsQuery, GetMovieRatingsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_movie_ratings import ( + GetMovieRatingsQuery, + GetMovieRatingsResult, +) from amdb.application.common.constants.exceptions import ( GET_MOVIE_RATINGS_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, @@ -29,7 +36,9 @@ def __init__( def execute(self, query: GetMovieRatingsQuery) -> GetMovieRatingsResult: current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movie_ratings() + required_permissions = ( + self._permissions_gateway.for_get_movie_ratings() + ) access = self._access_concern.authorize( current_permissions=current_permissions, required_permissions=required_permissions, diff --git a/src/amdb/application/query_handlers/get_movie_reviews.py b/src/amdb/application/query_handlers/get_movie_reviews.py index b4d0474..7d8534f 100644 --- a/src/amdb/application/query_handlers/get_movie_reviews.py +++ b/src/amdb/application/query_handlers/get_movie_reviews.py @@ -1,14 +1,21 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( GET_MOVIE_REVIEWS_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, ) from amdb.application.common.exception import ApplicationError -from amdb.application.queries.get_movie_reviews import GetMovieReviewsQuery, GetMovieReviewsResult +from amdb.application.queries.get_movie_reviews import ( + GetMovieReviewsQuery, + GetMovieReviewsResult, +) class GetMovieReviewsHandler: @@ -29,7 +36,9 @@ def __init__( def execute(self, query: GetMovieReviewsQuery) -> GetMovieReviewsResult: current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movie_reviews() + required_permissions = ( + self._permissions_gateway.for_get_movie_reviews() + ) access = self._access_concern.authorize( current_permissions=current_permissions, required_permissions=required_permissions, diff --git a/src/amdb/application/query_handlers/get_movies.py b/src/amdb/application/query_handlers/get_movies.py index 2f685f9..d06909d 100644 --- a/src/amdb/application/query_handlers/get_movies.py +++ b/src/amdb/application/query_handlers/get_movies.py @@ -1,9 +1,15 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult -from amdb.application.common.constants.exceptions import GET_MOVIES_ACCESS_DENIED +from amdb.application.common.constants.exceptions import ( + GET_MOVIES_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError diff --git a/src/amdb/application/query_handlers/get_my_ratings.py b/src/amdb/application/query_handlers/get_my_ratings.py index e09ad50..5fcef7a 100644 --- a/src/amdb/application/query_handlers/get_my_ratings.py +++ b/src/amdb/application/query_handlers/get_my_ratings.py @@ -1,9 +1,18 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_my_ratings import GetMyRatingsQuery, GetMyRatingsResult -from amdb.application.common.constants.exceptions import GET_MY_RATINGS_ACCESS_DENIED +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_my_ratings import ( + GetMyRatingsQuery, + GetMyRatingsResult, +) +from amdb.application.common.constants.exceptions import ( + GET_MY_RATINGS_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError diff --git a/src/amdb/application/query_handlers/get_my_reviews.py b/src/amdb/application/query_handlers/get_my_reviews.py index 7db59dc..9004cbd 100644 --- a/src/amdb/application/query_handlers/get_my_reviews.py +++ b/src/amdb/application/query_handlers/get_my_reviews.py @@ -1,10 +1,19 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import GET_MY_REVIEWS_ACCESS_DENIED +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.common.constants.exceptions import ( + GET_MY_REVIEWS_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError -from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult +from amdb.application.queries.get_my_reviews import ( + GetMyReviewsQuery, + GetMyReviewsResult, +) class GetMyReviewsHandler: diff --git a/src/amdb/application/query_handlers/get_rating.py b/src/amdb/application/query_handlers/get_rating.py index 9615d0b..2c902ea 100644 --- a/src/amdb/application/query_handlers/get_rating.py +++ b/src/amdb/application/query_handlers/get_rating.py @@ -1,7 +1,11 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult from amdb.application.common.constants.exceptions import ( GET_RATING_ACCESS_DENIED, diff --git a/src/amdb/application/query_handlers/get_review.py b/src/amdb/application/query_handlers/get_review.py index bd8bc32..43cf7de 100644 --- a/src/amdb/application/query_handlers/get_review.py +++ b/src/amdb/application/query_handlers/get_review.py @@ -1,7 +1,11 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.common.constants.exceptions import ( GET_REVIEW_ACCESS_DENIED, REVIEW_DOES_NOT_EXIST, diff --git a/src/amdb/application/query_handlers/login.py b/src/amdb/application/query_handlers/login.py index 62b9766..47a01e1 100644 --- a/src/amdb/application/query_handlers/login.py +++ b/src/amdb/application/query_handlers/login.py @@ -3,7 +3,9 @@ from amdb.domain.entities.user import UserId from amdb.domain.services.access_concern import AccessConcern from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.password_manager import PasswordManager from amdb.application.common.constants.exceptions import ( LOGIN_ACCESS_DENIED, diff --git a/src/amdb/domain/constants/exceptions.py b/src/amdb/domain/constants/exceptions.py index a85439d..dff4fd1 100644 --- a/src/amdb/domain/constants/exceptions.py +++ b/src/amdb/domain/constants/exceptions.py @@ -1 +1,3 @@ -INVALID_RATING_VALUE = "Rating value must be from 0 to 10 and be a multiple of 0.5" +INVALID_RATING_VALUE = ( + "Rating value must be from 0 to 10 and be a multiple of 0.5" +) diff --git a/src/amdb/domain/services/access_concern.py b/src/amdb/domain/services/access_concern.py index d9c515c..29ad163 100644 --- a/src/amdb/domain/services/access_concern.py +++ b/src/amdb/domain/services/access_concern.py @@ -1,3 +1,7 @@ class AccessConcern: - def authorize(self, current_permissions: int, required_permissions: int) -> bool: - return current_permissions & required_permissions == required_permissions + def authorize( + self, current_permissions: int, required_permissions: int + ) -> bool: + return ( + current_permissions & required_permissions == required_permissions + ) diff --git a/src/amdb/domain/services/rate_movie.py b/src/amdb/domain/services/rate_movie.py index 7dd9d8d..0e8a5c3 100644 --- a/src/amdb/domain/services/rate_movie.py +++ b/src/amdb/domain/services/rate_movie.py @@ -20,7 +20,9 @@ def __call__( if rating <= 0 or rating > 10 or rating % 0.5 != 0: raise DomainError(INVALID_RATING_VALUE) - movie.rating = (movie.rating * movie.rating_count + rating) / (movie.rating_count + 1) + movie.rating = (movie.rating * movie.rating_count + rating) / ( + movie.rating_count + 1 + ) movie.rating_count += 1 return Rating( diff --git a/src/amdb/domain/services/unrate_movie.py b/src/amdb/domain/services/unrate_movie.py index 4ecb0af..44edcd1 100644 --- a/src/amdb/domain/services/unrate_movie.py +++ b/src/amdb/domain/services/unrate_movie.py @@ -13,7 +13,7 @@ def __call__( movie.rating = 0 movie.rating_count = 0 else: - movie.rating = (movie.rating * movie.rating_count - rating.value) / ( - movie.rating_count - 1 - ) + movie.rating = ( + movie.rating * movie.rating_count - rating.value + ) / (movie.rating_count - 1) movie.rating_count -= 1 diff --git a/src/amdb/infrastructure/auth/session/identity_provider.py b/src/amdb/infrastructure/auth/session/identity_provider.py index 554192a..5c3eea8 100644 --- a/src/amdb/infrastructure/auth/session/identity_provider.py +++ b/src/amdb/infrastructure/auth/session/identity_provider.py @@ -1,8 +1,12 @@ from typing import Optional, cast from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.redis.gateways.session import RedisSessionGateway -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway +from amdb.infrastructure.persistence.redis.gateways.session import ( + RedisSessionGateway, +) +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) from amdb.infrastructure.exception import InfrastructureError from .constants.exceptions import NO_SESSION_ID, SESSION_DOES_NOT_EXIST from .model import SessionId diff --git a/src/amdb/infrastructure/password_manager/password_manager.py b/src/amdb/infrastructure/password_manager/password_manager.py index 46416bd..254471f 100644 --- a/src/amdb/infrastructure/password_manager/password_manager.py +++ b/src/amdb/infrastructure/password_manager/password_manager.py @@ -25,4 +25,6 @@ def set(self, user_id: UserId, password: str) -> None: def verify(self, user_id: UserId, password: str) -> bool: user_password_hash = self._user_password_hash_gateway.get(user_id) user_password_hash = cast(UserPasswordHash, user_password_hash) - return self._hasher.verify(password.encode(), user_password_hash.password_hash) + return self._hasher.verify( + password.encode(), user_password_hash.password_hash + ) diff --git a/src/amdb/infrastructure/persistence/alembic/config.py b/src/amdb/infrastructure/persistence/alembic/config.py index 0a75e2f..5d7f07d 100644 --- a/src/amdb/infrastructure/persistence/alembic/config.py +++ b/src/amdb/infrastructure/persistence/alembic/config.py @@ -4,5 +4,7 @@ ALEMBIC_CONFIG = str( - importlib.resources.files(amdb.infrastructure.persistence.alembic).joinpath("alembic.ini"), + importlib.resources.files( + amdb.infrastructure.persistence.alembic + ).joinpath("alembic.ini"), ) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py index 44504f6..fa79316 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py @@ -101,11 +101,17 @@ def upgrade() -> None: """, ) with op.batch_alter_table("ratings") as batch_op: - batch_op.add_column(sa.Column("id", sa.Uuid(), nullable=False, default="uuid7()")) + batch_op.add_column( + sa.Column("id", sa.Uuid(), nullable=False, default="uuid7()") + ) batch_op.drop_constraint("pk_ratings", type_="primary") batch_op.create_primary_key("pk_ratings", ["id"]) - batch_op.create_unique_constraint("uq_ratings", ("user_id", "movie_id")) - op.create_unique_constraint("uq_reviews", "reviews", ("user_id", "movie_id")) + batch_op.create_unique_constraint( + "uq_ratings", ("user_id", "movie_id") + ) + op.create_unique_constraint( + "uq_reviews", "reviews", ("user_id", "movie_id") + ) def downgrade() -> None: diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py index a5b2eb5..853078e 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py @@ -35,7 +35,9 @@ def upgrade() -> None: sa.Column("type", sa.SmallInteger(), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), - sa.ForeignKeyConstraint(["movie_id"], ["movies.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["movie_id"], ["movies.id"], ondelete="CASCADE" + ), sa.PrimaryKeyConstraint("id"), ) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py index f53e96d..22b25a5 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py @@ -49,7 +49,9 @@ def upgrade() -> None: sa.Column("user_id", sa.Uuid(), nullable=False), sa.Column("value", sa.Float(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=True), - sa.ForeignKeyConstraint(["movie_id"], ["movies.id"], ondelete="CASCADE"), + sa.ForeignKeyConstraint( + ["movie_id"], ["movies.id"], ondelete="CASCADE" + ), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("movie_id", "user_id"), ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py index e10eba9..b30327c 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py @@ -38,7 +38,9 @@ def rating(self) -> SQLAlchemyRatingGateway: return SQLAlchemyRatingGateway(self._session, RatingMapper()) def user_password_hash(self) -> SQLAlchemyUserPasswordHashGateway: - return SQLAlchemyUserPasswordHashGateway(self._session, UserPasswordHashMapper()) + return SQLAlchemyUserPasswordHashGateway( + self._session, UserPasswordHashMapper() + ) def review(self) -> SQLAlchemyReviewGateway: return SQLAlchemyReviewGateway(self._session, ReviewMapper()) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py index 7a34bb4..b7e38a6 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py @@ -4,8 +4,12 @@ from sqlalchemy.orm.session import Session from amdb.domain.entities.movie import MovieId, Movie as MovieEntity -from amdb.infrastructure.persistence.sqlalchemy.models.movie import Movie as MovieModel -from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import MovieMapper +from amdb.infrastructure.persistence.sqlalchemy.models.movie import ( + Movie as MovieModel, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import ( + MovieMapper, +) class SQLAlchemyMovieGateway: diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py index ec3c2c0..c20f165 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py @@ -6,8 +6,12 @@ from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId, Rating as RatingEntity -from amdb.infrastructure.persistence.sqlalchemy.models.rating import Rating as RatingModel -from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import RatingMapper +from amdb.infrastructure.persistence.sqlalchemy.models.rating import ( + Rating as RatingModel, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import ( + RatingMapper, +) class SQLAlchemyRatingGateway: @@ -48,7 +52,10 @@ def list_with_movie_id( offset: int, ) -> list[RatingEntity]: statement = ( - select(RatingModel).where(RatingModel.movie_id == movie_id).limit(limit).offset(offset) + select(RatingModel) + .where(RatingModel.movie_id == movie_id) + .limit(limit) + .offset(offset) ) rating_models = self._session.scalars(statement) @@ -66,7 +73,10 @@ def list_with_user_id( offset: int, ) -> list[RatingEntity]: statement = ( - select(RatingModel).where(RatingModel.user_id == user_id).limit(limit).offset(offset) + select(RatingModel) + .where(RatingModel.user_id == user_id) + .limit(limit) + .offset(offset) ) rating_models = self._session.scalars(statement) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py index 1f5582f..5c76ea7 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py @@ -6,8 +6,12 @@ from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.review import ReviewId, Review as ReviewEntity -from amdb.infrastructure.persistence.sqlalchemy.models.review import Review as ReviewModel -from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ReviewMapper +from amdb.infrastructure.persistence.sqlalchemy.models.review import ( + Review as ReviewModel, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ( + ReviewMapper, +) class SQLAlchemyReviewGateway: @@ -48,7 +52,10 @@ def list_with_movie_id( offset: int, ) -> list[ReviewEntity]: statement = ( - select(ReviewModel).where(ReviewModel.movie_id == movie_id).limit(limit).offset(offset) + select(ReviewModel) + .where(ReviewModel.movie_id == movie_id) + .limit(limit) + .offset(offset) ) review_models = self._session.scalars(statement) @@ -66,7 +73,10 @@ def list_with_user_id( offset: int, ) -> list[ReviewEntity]: statement = ( - select(ReviewModel).where(ReviewModel.user_id == user_id).limit(limit).offset(offset) + select(ReviewModel) + .where(ReviewModel.user_id == user_id) + .limit(limit) + .offset(offset) ) review_models = self._session.scalars(statement) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py index ff3251f..2d22d1b 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py @@ -4,7 +4,9 @@ from sqlalchemy.orm.session import Session from amdb.domain.entities.user import UserId, User as UserEntity -from amdb.infrastructure.persistence.sqlalchemy.models.user import User as UserModel +from amdb.infrastructure.persistence.sqlalchemy.models.user import ( + User as UserModel, +) from amdb.infrastructure.persistence.sqlalchemy.mappers.user import UserMapper diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py index 0345880..31b3980 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py @@ -22,9 +22,13 @@ def __init__( self._mapper = mapper def get(self, user_id: UserId) -> Optional[UserPasswordHash]: - user_password_hash_model = self._session.get(UserPasswordHashModel, user_id) + user_password_hash_model = self._session.get( + UserPasswordHashModel, user_id + ) if user_password_hash_model: - return self._mapper.to_password_manager_model(user_password_hash_model) + return self._mapper.to_password_manager_model( + user_password_hash_model + ) return None def save(self, user_password_hash: UserPasswordHash) -> None: diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py index 478d27a..520ab2f 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py @@ -1,5 +1,7 @@ from amdb.domain.entities.movie import MovieId, Movie as MovieEntity -from amdb.infrastructure.persistence.sqlalchemy.models.movie import Movie as MovieModel +from amdb.infrastructure.persistence.sqlalchemy.models.movie import ( + Movie as MovieModel, +) class MovieMapper: diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py index b93e5ea..ed34246 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py @@ -1,7 +1,9 @@ from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId, Rating as RatingEntity -from amdb.infrastructure.persistence.sqlalchemy.models.rating import Rating as RatingModel +from amdb.infrastructure.persistence.sqlalchemy.models.rating import ( + Rating as RatingModel, +) class RatingMapper: diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py index 968ef6c..8a28c94 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py @@ -1,7 +1,13 @@ from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, ReviewType, Review as ReviewEntity -from amdb.infrastructure.persistence.sqlalchemy.models.review import Review as ReviewModel +from amdb.domain.entities.review import ( + ReviewId, + ReviewType, + Review as ReviewEntity, +) +from amdb.infrastructure.persistence.sqlalchemy.models.review import ( + Review as ReviewModel, +) class ReviewMapper: diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py index 54dec2a..86f28fc 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py @@ -1,5 +1,7 @@ from amdb.domain.entities.user import UserId, User as UserEntity -from amdb.infrastructure.persistence.sqlalchemy.models.user import User as UserModel +from amdb.infrastructure.persistence.sqlalchemy.models.user import ( + User as UserModel, +) class UserMapper: diff --git a/src/amdb/main/cli/di.py b/src/amdb/main/cli/di.py index 87e71d1..8f1f9d8 100644 --- a/src/amdb/main/cli/di.py +++ b/src/amdb/main/cli/di.py @@ -6,14 +6,18 @@ from redis.client import Redis from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) from amdb.infrastructure.auth.raw.identity_provider import RawIdentityProvider from amdb.infrastructure.security.hasher import Hasher from amdb.main.config import GenericConfig from amdb.main.ioc import IoC -IDENTITY_PROVIDER_USER_ID = UserId(UUID("00000000-0000-0000-0000-000000000000")) +IDENTITY_PROVIDER_USER_ID = UserId( + UUID("00000000-0000-0000-0000-000000000000") +) IDENTITY_PROVIDER_PERMISSIONS = 12 @@ -22,7 +26,9 @@ class DependenciesDict(TypedDict): identity_provider: RawIdentityProvider -def create_dependencies_dict(generic_config: GenericConfig) -> DependenciesDict: +def create_dependencies_dict( + generic_config: GenericConfig, +) -> DependenciesDict: redis = Redis( host=generic_config.redis.host, port=generic_config.redis.port, diff --git a/src/amdb/main/ioc.py b/src/amdb/main/ioc.py index 51cb261..32a5969 100644 --- a/src/amdb/main/ioc.py +++ b/src/amdb/main/ioc.py @@ -9,7 +9,9 @@ from amdb.domain.services.rate_movie import RateMovie from amdb.domain.services.unrate_movie import UnrateMovie from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.command_handlers.delete_movie import DeleteMovieHandler @@ -19,18 +21,26 @@ from amdb.application.query_handlers.login import LoginHandler from amdb.application.query_handlers.get_movies import GetMoviesHandler from amdb.application.query_handlers.get_movie import GetMovieHandler -from amdb.application.query_handlers.get_movie_ratings import GetMovieRatingsHandler +from amdb.application.query_handlers.get_movie_ratings import ( + GetMovieRatingsHandler, +) from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler from amdb.application.query_handlers.get_rating import GetRatingHandler -from amdb.application.query_handlers.get_movie_reviews import GetMovieReviewsHandler +from amdb.application.query_handlers.get_movie_reviews import ( + GetMovieReviewsHandler, +) from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler from amdb.application.query_handlers.get_review import GetReviewHandler from amdb.infrastructure.persistence.sqlalchemy.gateway_factory import ( build_sqlalchemy_gateway_factory, ) -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) from amdb.infrastructure.security.hasher import Hasher -from amdb.infrastructure.password_manager.password_manager import HashingPasswordManager +from amdb.infrastructure.password_manager.password_manager import ( + HashingPasswordManager, +) from amdb.presentation.handler_factory import HandlerFactory @@ -47,7 +57,9 @@ def __init__( @contextmanager def register_user(self) -> Iterator[RegisterUserHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: hashing_password_manager = HashingPasswordManager( hasher=self._hasher, user_password_hash_gateway=gateway_factory.user_password_hash(), @@ -62,7 +74,9 @@ def register_user(self) -> Iterator[RegisterUserHandler]: @contextmanager def login(self) -> Iterator[LoginHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: hashing_password_manager = HashingPasswordManager( hasher=self._hasher, user_password_hash_gateway=gateway_factory.user_password_hash(), @@ -79,7 +93,9 @@ def get_movies( self, identity_provider: IdentityProvider, ) -> Iterator[GetMoviesHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMoviesHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -92,7 +108,9 @@ def get_movie( self, identity_provider: IdentityProvider, ) -> Iterator[GetMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMovieHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -105,7 +123,9 @@ def create_movie( self, identity_provider: IdentityProvider, ) -> Iterator[CreateMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield CreateMovieHandler( access_concern=AccessConcern(), create_movie=CreateMovie(), @@ -120,7 +140,9 @@ def delete_movie( self, identity_provider: IdentityProvider, ) -> Iterator[DeleteMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield DeleteMovieHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -136,7 +158,9 @@ def get_movie_ratings( self, identity_provider: IdentityProvider, ) -> Iterator[GetMovieRatingsHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMovieRatingsHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -150,7 +174,9 @@ def get_my_ratings( self, identity_provider: IdentityProvider, ) -> Iterator[GetMyRatingsHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMyRatingsHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -163,7 +189,9 @@ def get_rating( self, identity_provider: IdentityProvider, ) -> Iterator[GetRatingHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetRatingHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -176,7 +204,9 @@ def rate_movie( self, identity_provider: IdentityProvider, ) -> Iterator[RateMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), @@ -193,7 +223,9 @@ def unrate_movie( self, identity_provider: IdentityProvider, ) -> Iterator[UnrateMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), @@ -209,7 +241,9 @@ def get_movie_reviews( self, identity_provider: IdentityProvider, ) -> Iterator[GetMovieReviewsHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMovieReviewsHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -223,7 +257,9 @@ def get_my_reviews( self, identity_provider: IdentityProvider, ) -> Iterator[GetMyReviewsHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetMyReviewsHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -236,7 +272,9 @@ def get_review( self, identity_provider: IdentityProvider, ) -> Iterator[GetReviewHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield GetReviewHandler( access_concern=AccessConcern(), permissions_gateway=self._permissions_gateway, @@ -249,7 +287,9 @@ def review_movie( self, identity_provider: IdentityProvider, ) -> Iterator[ReviewMovieHandler]: - with build_sqlalchemy_gateway_factory(self._sessionmaker) as gateway_factory: + with build_sqlalchemy_gateway_factory( + self._sessionmaker + ) as gateway_factory: yield ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index 1f2a9da..d19e906 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -1,7 +1,9 @@ from fastapi import FastAPI from amdb.infrastructure.auth.session.config import SessionConfig -from amdb.presentation.web_api.exception_handlers import setup_exception_handlers +from amdb.presentation.web_api.exception_handlers import ( + setup_exception_handlers, +) from amdb.presentation.web_api.routers.setup import setup_routers from amdb.main.config import GenericConfig from .config import FastAPIConfig diff --git a/src/amdb/main/web_api/config.py b/src/amdb/main/web_api/config.py index 858dabb..a490322 100644 --- a/src/amdb/main/web_api/config.py +++ b/src/amdb/main/web_api/config.py @@ -22,7 +22,9 @@ def build_web_api_config() -> "WebAPIConfig": port=int(_get_env(UVICORN_PORT_ENV)), ) session_config = SessionConfig( - session_lifetime=timedelta(minutes=int(_get_env(SESSION_LIFETIME_ENV))), + session_lifetime=timedelta( + minutes=int(_get_env(SESSION_LIFETIME_ENV)) + ), ) return WebAPIConfig( fastapi=fastapi_config, diff --git a/src/amdb/main/web_api/di.py b/src/amdb/main/web_api/di.py index 2d70b4a..948002c 100644 --- a/src/amdb/main/web_api/di.py +++ b/src/amdb/main/web_api/di.py @@ -5,8 +5,12 @@ from amdb.infrastructure.security.hasher import Hasher from amdb.infrastructure.auth.session.config import SessionConfig -from amdb.infrastructure.persistence.redis.gateways.session import RedisSessionGateway -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway +from amdb.infrastructure.persistence.redis.gateways.session import ( + RedisSessionGateway, +) +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) from amdb.infrastructure.auth.session.session_processor import SessionProcessor from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub @@ -30,10 +34,14 @@ def setup_dependecies( redis=redis, session_lifetime=session_config.session_lifetime, ) - app.dependency_overrides[Stub(RedisSessionGateway)] = lambda: redis_session_gateway # type: ignore + app.dependency_overrides[Stub(RedisSessionGateway)] = ( + lambda: redis_session_gateway + ) # type: ignore redis_permissions_gateway = RedisPermissionsGateway(redis) - app.dependency_overrides[Stub(RedisPermissionsGateway)] = lambda: redis_permissions_gateway # type: ignore + app.dependency_overrides[Stub(RedisPermissionsGateway)] = ( + lambda: redis_permissions_gateway + ) # type: ignore engine = create_engine(generic_config.postgres.dsn) ioc = IoC( @@ -44,4 +52,6 @@ def setup_dependecies( app.dependency_overrides[HandlerFactory] = lambda: ioc # type: ignore session_processor = SessionProcessor() - app.dependency_overrides[Stub(SessionProcessor)] = lambda: session_processor # type: ignore + app.dependency_overrides[Stub(SessionProcessor)] = ( + lambda: session_processor + ) # type: ignore diff --git a/src/amdb/presentation/cli/movie.py b/src/amdb/presentation/cli/movie.py index 095de7a..7cbad71 100644 --- a/src/amdb/presentation/cli/movie.py +++ b/src/amdb/presentation/cli/movie.py @@ -8,7 +8,9 @@ import rich.table from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.create_movie import CreateMovieCommand from amdb.application.commands.delete_movie import DeleteMovieCommand from amdb.application.queries.get_movies import GetMoviesQuery diff --git a/src/amdb/presentation/handler_factory.py b/src/amdb/presentation/handler_factory.py index d9b2f66..930141b 100644 --- a/src/amdb/presentation/handler_factory.py +++ b/src/amdb/presentation/handler_factory.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod from typing import ContextManager -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.command_handlers.delete_movie import DeleteMovieHandler @@ -11,10 +13,14 @@ from amdb.application.query_handlers.login import LoginHandler from amdb.application.query_handlers.get_movies import GetMoviesHandler from amdb.application.query_handlers.get_movie import GetMovieHandler -from amdb.application.query_handlers.get_movie_ratings import GetMovieRatingsHandler +from amdb.application.query_handlers.get_movie_ratings import ( + GetMovieRatingsHandler, +) from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler from amdb.application.query_handlers.get_rating import GetRatingHandler -from amdb.application.query_handlers.get_movie_reviews import GetMovieReviewsHandler +from amdb.application.query_handlers.get_movie_reviews import ( + GetMovieReviewsHandler, +) from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler from amdb.application.query_handlers.get_review import GetReviewHandler diff --git a/src/amdb/presentation/web_api/dependencies/identity_provider.py b/src/amdb/presentation/web_api/dependencies/identity_provider.py index d58e912..77f8169 100644 --- a/src/amdb/presentation/web_api/dependencies/identity_provider.py +++ b/src/amdb/presentation/web_api/dependencies/identity_provider.py @@ -2,21 +2,31 @@ from fastapi import Cookie, Depends -from amdb.infrastructure.persistence.redis.gateways.session import RedisSessionGateway -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway -from amdb.infrastructure.auth.session.identity_provider import SessionIdentityProvider +from amdb.infrastructure.persistence.redis.gateways.session import ( + RedisSessionGateway, +) +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) from amdb.infrastructure.auth.session.model import SessionId from amdb.presentation.web_api.constants import SESSION_ID_COOKIE from .depends_stub import Stub def get_identity_provider( - session_gateway: Annotated[RedisSessionGateway, Depends(Stub(RedisSessionGateway))], + session_gateway: Annotated[ + RedisSessionGateway, Depends(Stub(RedisSessionGateway)) + ], permissions_gateway: Annotated[ RedisPermissionsGateway, Depends(Stub(RedisPermissionsGateway)), ], - session_id: Annotated[Optional[str], Cookie(alias=SESSION_ID_COOKIE)] = None, + session_id: Annotated[ + Optional[str], Cookie(alias=SESSION_ID_COOKIE) + ] = None, ) -> SessionIdentityProvider: return SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, diff --git a/src/amdb/presentation/web_api/exception_handlers.py b/src/amdb/presentation/web_api/exception_handlers.py index e537331..4408072 100644 --- a/src/amdb/presentation/web_api/exception_handlers.py +++ b/src/amdb/presentation/web_api/exception_handlers.py @@ -9,7 +9,9 @@ def setup_exception_handlers(app: FastAPI) -> None: app.add_exception_handler(DomainError, _domain_error_handler) app.add_exception_handler(ApplicationError, _application_error_handler) - app.add_exception_handler(InfrastructureError, _infrastructure_error_handler) + app.add_exception_handler( + InfrastructureError, _infrastructure_error_handler + ) def _domain_error_handler(_, error: ApplicationError) -> JSONResponse: @@ -20,5 +22,7 @@ def _application_error_handler(_, error: ApplicationError) -> JSONResponse: return JSONResponse(content={"message": error.message}, status_code=400) -def _infrastructure_error_handler(_, error: InfrastructureError) -> JSONResponse: +def _infrastructure_error_handler( + _, error: InfrastructureError +) -> JSONResponse: return JSONResponse(content=None, status_code=500) diff --git a/src/amdb/presentation/web_api/routers/auth/login.py b/src/amdb/presentation/web_api/routers/auth/login.py index 5d48b80..8423b92 100644 --- a/src/amdb/presentation/web_api/routers/auth/login.py +++ b/src/amdb/presentation/web_api/routers/auth/login.py @@ -5,7 +5,9 @@ from amdb.domain.entities.user import UserId from amdb.application.queries.login import LoginQuery from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.gateways.session import RedisSessionGateway +from amdb.infrastructure.persistence.redis.gateways.session import ( + RedisSessionGateway, +) from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub from amdb.presentation.web_api.constants import SESSION_ID_COOKIE @@ -13,8 +15,12 @@ async def login( ioc: Annotated[HandlerFactory, Depends()], - session_processor: Annotated[SessionProcessor, Depends(Stub(SessionProcessor))], - session_gateway: Annotated[RedisSessionGateway, Depends(Stub(RedisSessionGateway))], + session_processor: Annotated[ + SessionProcessor, Depends(Stub(SessionProcessor)) + ], + session_gateway: Annotated[ + RedisSessionGateway, Depends(Stub(RedisSessionGateway)) + ], login_query: LoginQuery, response: Response, ) -> UserId: diff --git a/src/amdb/presentation/web_api/routers/auth/register.py b/src/amdb/presentation/web_api/routers/auth/register.py index c98bec9..e25e7c9 100644 --- a/src/amdb/presentation/web_api/routers/auth/register.py +++ b/src/amdb/presentation/web_api/routers/auth/register.py @@ -5,7 +5,9 @@ from amdb.domain.entities.user import UserId from amdb.application.commands.register_user import RegisterUserCommand from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.gateways.session import RedisSessionGateway +from amdb.infrastructure.persistence.redis.gateways.session import ( + RedisSessionGateway, +) from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub from amdb.presentation.web_api.constants import SESSION_ID_COOKIE @@ -13,8 +15,12 @@ async def register( ioc: Annotated[HandlerFactory, Depends()], - session_processor: Annotated[SessionProcessor, Depends(Stub(SessionProcessor))], - session_gateway: Annotated[RedisSessionGateway, Depends(Stub(RedisSessionGateway))], + session_processor: Annotated[ + SessionProcessor, Depends(Stub(SessionProcessor)) + ], + session_gateway: Annotated[ + RedisSessionGateway, Depends(Stub(RedisSessionGateway)) + ], register_user_command: RegisterUserCommand, response: Response, ) -> UserId: diff --git a/src/amdb/presentation/web_api/routers/movies/get_movie.py b/src/amdb/presentation/web_api/routers/movies/get_movie.py index afbec2a..8768121 100644 --- a/src/amdb/presentation/web_api/routers/movies/get_movie.py +++ b/src/amdb/presentation/web_api/routers/movies/get_movie.py @@ -3,15 +3,21 @@ from fastapi import Depends from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_movie import GetMovieResult, GetMovieQuery from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) def get_movie( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], movie_id: MovieId, ) -> GetMovieResult: """ diff --git a/src/amdb/presentation/web_api/routers/movies/get_movies.py b/src/amdb/presentation/web_api/routers/movies/get_movies.py index 4cb9624..3794fdc 100644 --- a/src/amdb/presentation/web_api/routers/movies/get_movies.py +++ b/src/amdb/presentation/web_api/routers/movies/get_movies.py @@ -2,15 +2,21 @@ from fastapi import Depends -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult async def get_movies( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], limit: int = 100, offset: int = 0, ) -> GetMoviesResult: diff --git a/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py b/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py index 64b777b..660eec9 100644 --- a/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py +++ b/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py @@ -3,15 +3,24 @@ from fastapi import Depends from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_movie_ratings import GetMovieRatingsQuery, GetMovieRatingsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_movie_ratings import ( + GetMovieRatingsQuery, + GetMovieRatingsResult, +) from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_movie_ratings( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], movie_id: MovieId, limit: int = 100, offset: int = 0, @@ -27,6 +36,8 @@ async def get_movie_ratings( limit=limit, offset=offset, ) - get_movie_ratings_result = get_movie_ratings_handler.execute(get_movie_ratings_query) + get_movie_ratings_result = get_movie_ratings_handler.execute( + get_movie_ratings_query + ) return get_movie_ratings_result diff --git a/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py b/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py index 5ad4fa5..f0aa378 100644 --- a/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py +++ b/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py @@ -2,15 +2,24 @@ from fastapi import Depends -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_my_ratings import GetMyRatingsQuery, GetMyRatingsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_my_ratings import ( + GetMyRatingsQuery, + GetMyRatingsResult, +) from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_my_ratings( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], limit: int = 100, offset: int = 0, ) -> GetMyRatingsResult: @@ -23,6 +32,8 @@ async def get_my_ratings( limit=limit, offset=offset, ) - get_my_ratings_result = get_my_ratings_handler.execute(get_my_ratings_query) + get_my_ratings_result = get_my_ratings_handler.execute( + get_my_ratings_query + ) return get_my_ratings_result diff --git a/src/amdb/presentation/web_api/routers/ratings/get_rating.py b/src/amdb/presentation/web_api/routers/ratings/get_rating.py index 7e3581f..2cb994e 100644 --- a/src/amdb/presentation/web_api/routers/ratings/get_rating.py +++ b/src/amdb/presentation/web_api/routers/ratings/get_rating.py @@ -3,15 +3,21 @@ from fastapi import Depends from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_rating( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], rating_id: RatingId, ) -> GetRatingResult: """ diff --git a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py b/src/amdb/presentation/web_api/routers/ratings/rate_movie.py index 4fc9a80..6ba1727 100644 --- a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py +++ b/src/amdb/presentation/web_api/routers/ratings/rate_movie.py @@ -3,15 +3,21 @@ from fastapi import Depends from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.rate_movie import RateMovieCommand from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def rate_movie( ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], rate_movie_command: RateMovieCommand, ) -> RatingId: """ diff --git a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py b/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py index 6b03068..08c30c6 100644 --- a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py +++ b/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py @@ -3,15 +3,21 @@ from fastapi import Depends from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.unrate_movie import UnrateMovieCommand from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def unrate_movie( ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], rating_id: RatingId, ) -> None: """ diff --git a/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py index 1f8d3c7..e4c6bdc 100644 --- a/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py +++ b/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py @@ -3,15 +3,24 @@ from fastapi import Depends from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_movie_reviews import GetMovieReviewsQuery, GetMovieReviewsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_movie_reviews import ( + GetMovieReviewsQuery, + GetMovieReviewsResult, +) from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_movie_reviews( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], movie_id: MovieId, limit: int = 100, offset: int = 0, @@ -27,6 +36,8 @@ async def get_movie_reviews( limit=limit, offset=offset, ) - get_movie_reviews_result = get_movie_reviews_handler.execute(get_movie_reviews_query) + get_movie_reviews_result = get_movie_reviews_handler.execute( + get_movie_reviews_query + ) return get_movie_reviews_result diff --git a/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py index e0e4fc5..60942e5 100644 --- a/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py +++ b/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py @@ -2,15 +2,24 @@ from fastapi import Depends -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_my_reviews import ( + GetMyReviewsQuery, + GetMyReviewsResult, +) from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_my_reviews( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], limit: int = 100, offset: int = 0, ) -> GetMyReviewsResult: @@ -19,6 +28,8 @@ async def get_my_reviews( limit=limit, offset=offset, ) - get_my_reviews_result = get_my_reviews_handler.execute(get_my_reviews_query) + get_my_reviews_result = get_my_reviews_handler.execute( + get_my_reviews_query + ) return get_my_reviews_result diff --git a/src/amdb/presentation/web_api/routers/reviews/get_review.py b/src/amdb/presentation/web_api/routers/reviews/get_review.py index 59d364e..5c1d7a2 100644 --- a/src/amdb/presentation/web_api/routers/reviews/get_review.py +++ b/src/amdb/presentation/web_api/routers/reviews/get_review.py @@ -3,15 +3,21 @@ from fastapi import Depends from amdb.domain.entities.review import ReviewId -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_review import GetReviewQuery, GetReviewResult from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) async def get_review( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], review_id: ReviewId, ) -> GetReviewResult: """ diff --git a/src/amdb/presentation/web_api/routers/reviews/review_movie.py b/src/amdb/presentation/web_api/routers/reviews/review_movie.py index e726f8c..2e47a0a 100644 --- a/src/amdb/presentation/web_api/routers/reviews/review_movie.py +++ b/src/amdb/presentation/web_api/routers/reviews/review_movie.py @@ -5,10 +5,14 @@ from amdb.domain.entities.movie import MovieId from amdb.domain.entities.review import ReviewId, ReviewType -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.review_movie import ReviewMovieCommand from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import get_identity_provider +from amdb.presentation.web_api.dependencies.identity_provider import ( + get_identity_provider, +) class ReviewMovie(BaseModel): @@ -19,7 +23,9 @@ class ReviewMovie(BaseModel): async def review_movie( ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[IdentityProvider, Depends(get_identity_provider)], + identity_provider: Annotated[ + IdentityProvider, Depends(get_identity_provider) + ], movie_id: MovieId, data: ReviewMovie, ) -> ReviewId: diff --git a/tests/unit/application/command_handlers/test_create_movie.py b/tests/unit/application/command_handlers/test_create_movie.py index 19de162..5d017b9 100644 --- a/tests/unit/application/command_handlers/test_create_movie.py +++ b/tests/unit/application/command_handlers/test_create_movie.py @@ -5,13 +5,19 @@ from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.create_movie import CreateMovieCommand from amdb.application.command_handlers.create_movie import CreateMovieHandler -from amdb.application.common.constants.exceptions import CREATE_MOVIE_ACCESS_DENIED +from amdb.application.common.constants.exceptions import ( + CREATE_MOVIE_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError diff --git a/tests/unit/application/command_handlers/test_delete_movie.py b/tests/unit/application/command_handlers/test_delete_movie.py index 5523bec..6821f40 100644 --- a/tests/unit/application/command_handlers/test_delete_movie.py +++ b/tests/unit/application/command_handlers/test_delete_movie.py @@ -6,12 +6,16 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.delete_movie import DeleteMovieCommand from amdb.application.command_handlers.delete_movie import DeleteMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_rate_movie.py b/tests/unit/application/command_handlers/test_rate_movie.py index 86240a9..376a737 100644 --- a/tests/unit/application/command_handlers/test_rate_movie.py +++ b/tests/unit/application/command_handlers/test_rate_movie.py @@ -11,12 +11,16 @@ from amdb.domain.services.rate_movie import RateMovie from amdb.domain.constants.exceptions import INVALID_RATING_VALUE from amdb.domain.exception import DomainError -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.rate_movie import RateMovieCommand from amdb.application.command_handlers.rate_movie import RateMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_register_user.py b/tests/unit/application/command_handlers/test_register_user.py index e67ff38..e7aa115 100644 --- a/tests/unit/application/command_handlers/test_register_user.py +++ b/tests/unit/application/command_handlers/test_register_user.py @@ -4,12 +4,16 @@ from amdb.domain.entities.user import UserId, User from amdb.domain.services.create_user import CreateUser from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork from amdb.application.common.interfaces.password_manager import PasswordManager from amdb.application.commands.register_user import RegisterUserCommand from amdb.application.command_handlers.register_user import RegisterUserHandler -from amdb.application.common.constants.exceptions import USER_NAME_ALREADY_EXISTS +from amdb.application.common.constants.exceptions import ( + USER_NAME_ALREADY_EXISTS, +) from amdb.application.common.exception import ApplicationError diff --git a/tests/unit/application/command_handlers/test_review_movie.py b/tests/unit/application/command_handlers/test_review_movie.py index e2b7d33..20d494d 100644 --- a/tests/unit/application/command_handlers/test_review_movie.py +++ b/tests/unit/application/command_handlers/test_review_movie.py @@ -9,12 +9,16 @@ from amdb.domain.entities.review import ReviewId, ReviewType, Review from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.review_movie import ReviewMovieCommand from amdb.application.command_handlers.review_movie import ReviewMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_unrate_movie.py b/tests/unit/application/command_handlers/test_unrate_movie.py index d2683bc..45b5a9f 100644 --- a/tests/unit/application/command_handlers/test_unrate_movie.py +++ b/tests/unit/application/command_handlers/test_unrate_movie.py @@ -9,12 +9,16 @@ from amdb.domain.entities.rating import RatingId, Rating from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.commands.unrate_movie import UnrateMovieCommand from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index a67644a..76a8d77 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -7,23 +7,41 @@ from redis.client import Redis from amdb.infrastructure.persistence.sqlalchemy.models.base import Model -from amdb.infrastructure.persistence.sqlalchemy.gateways.user import SQLAlchemyUserGateway -from amdb.infrastructure.persistence.sqlalchemy.gateways.movie import SQLAlchemyMovieGateway -from amdb.infrastructure.persistence.sqlalchemy.gateways.rating import SQLAlchemyRatingGateway +from amdb.infrastructure.persistence.sqlalchemy.gateways.user import ( + SQLAlchemyUserGateway, +) +from amdb.infrastructure.persistence.sqlalchemy.gateways.movie import ( + SQLAlchemyMovieGateway, +) +from amdb.infrastructure.persistence.sqlalchemy.gateways.rating import ( + SQLAlchemyRatingGateway, +) from amdb.infrastructure.persistence.sqlalchemy.gateways.user_password_hash import ( SQLAlchemyUserPasswordHashGateway, ) -from amdb.infrastructure.persistence.sqlalchemy.gateways.review import SQLAlchemyReviewGateway -from amdb.infrastructure.persistence.redis.gateways.permissions import RedisPermissionsGateway +from amdb.infrastructure.persistence.sqlalchemy.gateways.review import ( + SQLAlchemyReviewGateway, +) +from amdb.infrastructure.persistence.redis.gateways.permissions import ( + RedisPermissionsGateway, +) from amdb.infrastructure.persistence.sqlalchemy.mappers.user import UserMapper -from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import MovieMapper -from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import RatingMapper +from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import ( + MovieMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import ( + RatingMapper, +) from amdb.infrastructure.persistence.sqlalchemy.mappers.user_password_hash import ( UserPasswordHashMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ReviewMapper +from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ( + ReviewMapper, +) from amdb.infrastructure.security.hasher import Hasher -from amdb.infrastructure.password_manager.password_manager import HashingPasswordManager +from amdb.infrastructure.password_manager.password_manager import ( + HashingPasswordManager, +) @pytest.fixture(scope="package") diff --git a/tests/unit/application/query_handlers/test_get_movie.py b/tests/unit/application/query_handlers/test_get_movie.py index 1ce4466..c87297c 100644 --- a/tests/unit/application/query_handlers/test_get_movie.py +++ b/tests/unit/application/query_handlers/test_get_movie.py @@ -6,10 +6,14 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_movie import GetMovieQuery, GetMovieResult from amdb.application.query_handlers.get_movie import GetMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/query_handlers/test_get_movie_ratings.py b/tests/unit/application/query_handlers/test_get_movie_ratings.py index fe6c102..b824a16 100644 --- a/tests/unit/application/query_handlers/test_get_movie_ratings.py +++ b/tests/unit/application/query_handlers/test_get_movie_ratings.py @@ -11,11 +11,20 @@ from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_movie_ratings import GetMovieRatingsQuery, GetMovieRatingsResult -from amdb.application.query_handlers.get_movie_ratings import GetMovieRatingsHandler +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_movie_ratings import ( + GetMovieRatingsQuery, + GetMovieRatingsResult, +) +from amdb.application.query_handlers.get_movie_ratings import ( + GetMovieRatingsHandler, +) from amdb.application.common.constants.exceptions import ( GET_MOVIE_RATINGS_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, @@ -86,7 +95,9 @@ def test_get_movie_ratings( identity_provider=identity_provider_with_correct_permissions, ) - get_movie_ratings_result = get_movie_ratings_handler.execute(get_movie_ratings_query) + get_movie_ratings_result = get_movie_ratings_handler.execute( + get_movie_ratings_query + ) expected_get_movie_ratings_result = GetMovieRatingsResult( ratings=[rating], rating_count=1, diff --git a/tests/unit/application/query_handlers/test_get_movie_reviews.py b/tests/unit/application/query_handlers/test_get_movie_reviews.py index b596920..3aae0d8 100644 --- a/tests/unit/application/query_handlers/test_get_movie_reviews.py +++ b/tests/unit/application/query_handlers/test_get_movie_reviews.py @@ -8,14 +8,23 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.review import ReviewId, ReviewType, Review from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_movie_reviews import GetMovieReviewsQuery, GetMovieReviewsResult -from amdb.application.query_handlers.get_movie_reviews import GetMovieReviewsHandler +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_movie_reviews import ( + GetMovieReviewsQuery, + GetMovieReviewsResult, +) +from amdb.application.query_handlers.get_movie_reviews import ( + GetMovieReviewsHandler, +) from amdb.application.common.constants.exceptions import ( GET_MOVIE_REVIEWS_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, @@ -84,7 +93,9 @@ def test_get_movie_reviews( identity_provider=identity_provider_with_correct_permissions, ) - get_movie_reviews_result = get_movie_reviews_handler.execute(get_movie_reviews_query) + get_movie_reviews_result = get_movie_reviews_handler.execute( + get_movie_reviews_query + ) expected_get_movie_reviews_result = GetMovieReviewsResult( reviews=[review], review_count=1, diff --git a/tests/unit/application/query_handlers/test_get_movies.py b/tests/unit/application/query_handlers/test_get_movies.py index d6aa732..a648328 100644 --- a/tests/unit/application/query_handlers/test_get_movies.py +++ b/tests/unit/application/query_handlers/test_get_movies.py @@ -6,13 +6,19 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult from amdb.application.query_handlers.get_movies import GetMoviesHandler -from amdb.application.common.constants.exceptions import GET_MOVIES_ACCESS_DENIED +from amdb.application.common.constants.exceptions import ( + GET_MOVIES_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError diff --git a/tests/unit/application/query_handlers/test_get_my_ratings.py b/tests/unit/application/query_handlers/test_get_my_ratings.py index f547a29..eaff9eb 100644 --- a/tests/unit/application/query_handlers/test_get_my_ratings.py +++ b/tests/unit/application/query_handlers/test_get_my_ratings.py @@ -11,12 +11,21 @@ from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_my_ratings import GetMyRatingsQuery, GetMyRatingsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_my_ratings import ( + GetMyRatingsQuery, + GetMyRatingsResult, +) from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler -from amdb.application.common.constants.exceptions import GET_MY_RATINGS_ACCESS_DENIED +from amdb.application.common.constants.exceptions import ( + GET_MY_RATINGS_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError @@ -81,7 +90,9 @@ def test_get_my_ratings( identity_provider=identity_provider_with_correct_permissions, ) - get_my_ratings_result = get_my_ratings_handler.execute(get_my_ratings_query) + get_my_ratings_result = get_my_ratings_handler.execute( + get_my_ratings_query + ) expected_get_my_ratings_result = GetMyRatingsResult( ratings=[rating], rating_count=1, diff --git a/tests/unit/application/query_handlers/test_get_my_reviews.py b/tests/unit/application/query_handlers/test_get_my_reviews.py index 1670532..ac4bafd 100644 --- a/tests/unit/application/query_handlers/test_get_my_reviews.py +++ b/tests/unit/application/query_handlers/test_get_my_reviews.py @@ -8,15 +8,24 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.review import ReviewId, ReviewType, Review from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider -from amdb.application.queries.get_my_reviews import GetMyReviewsQuery, GetMyReviewsResult +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) +from amdb.application.queries.get_my_reviews import ( + GetMyReviewsQuery, + GetMyReviewsResult, +) from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler -from amdb.application.common.constants.exceptions import GET_MY_REVIEWS_ACCESS_DENIED +from amdb.application.common.constants.exceptions import ( + GET_MY_REVIEWS_ACCESS_DENIED, +) from amdb.application.common.exception import ApplicationError @@ -83,7 +92,9 @@ def test_get_my_reviews( identity_provider=identity_provider_with_correct_permissions, ) - get_my_reviews_result = get_my_reviews_handler.execute(get_my_reviews_query) + get_my_reviews_result = get_my_reviews_handler.execute( + get_my_reviews_query + ) expected_get_my_reviews_result = GetMyReviewsResult( reviews=[review], review_count=1, diff --git a/tests/unit/application/query_handlers/test_get_rating.py b/tests/unit/application/query_handlers/test_get_rating.py index a881321..4f8be6f 100644 --- a/tests/unit/application/query_handlers/test_get_rating.py +++ b/tests/unit/application/query_handlers/test_get_rating.py @@ -11,9 +11,13 @@ from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult from amdb.application.query_handlers.get_rating import GetRatingHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/query_handlers/test_get_review.py b/tests/unit/application/query_handlers/test_get_review.py index b4651a1..858996b 100644 --- a/tests/unit/application/query_handlers/test_get_review.py +++ b/tests/unit/application/query_handlers/test_get_review.py @@ -8,12 +8,16 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.review import ReviewId, ReviewType, Review from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.user_gateway import UserGateway from amdb.application.common.interfaces.movie_gateway import MovieGateway from amdb.application.common.interfaces.review_gateway import ReviewGateway from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import IdentityProvider +from amdb.application.common.interfaces.identity_provider import ( + IdentityProvider, +) from amdb.application.queries.get_review import GetReviewQuery, GetReviewResult from amdb.application.query_handlers.get_review import GetReviewHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/query_handlers/test_login.py b/tests/unit/application/query_handlers/test_login.py index e7736f7..d996d61 100644 --- a/tests/unit/application/query_handlers/test_login.py +++ b/tests/unit/application/query_handlers/test_login.py @@ -4,7 +4,9 @@ from amdb.domain.entities.user import UserId, User from amdb.domain.services.access_concern import AccessConcern from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import PermissionsGateway +from amdb.application.common.interfaces.permissions_gateway import ( + PermissionsGateway, +) from amdb.application.common.interfaces.unit_of_work import UnitOfWork from amdb.application.common.interfaces.password_manager import PasswordManager from amdb.application.queries.login import LoginQuery diff --git a/tests/unit/infrastructure/alembic/test_stairway.py b/tests/unit/infrastructure/alembic/test_stairway.py index 149eedf..b50338f 100644 --- a/tests/unit/infrastructure/alembic/test_stairway.py +++ b/tests/unit/infrastructure/alembic/test_stairway.py @@ -12,12 +12,16 @@ import alembic.script -def get_revisions(alembic_config: alembic.config.Config) -> list[alembic.script.Script]: +def get_revisions( + alembic_config: alembic.config.Config, +) -> list[alembic.script.Script]: # Get directory object with Alembic migrations revisions_dir = alembic.script.ScriptDirectory.from_config(alembic_config) # Get & sort migrations, from first to last - revisions: list[alembic.script.Script] = list(revisions_dir.walk_revisions("base", "heads")) + revisions: list[alembic.script.Script] = list( + revisions_dir.walk_revisions("base", "heads") + ) revisions.reverse() return revisions @@ -28,5 +32,7 @@ def test_migrations_stairway(alembic_config: alembic.config.Config): alembic.command.upgrade(alembic_config, revision.revision) # We need -1 for downgrading first migration (its down_revision is None) - alembic.command.downgrade(alembic_config, revision.down_revision or "-1") # type: ignore + alembic.command.downgrade( + alembic_config, revision.down_revision or "-1" + ) # type: ignore alembic.command.upgrade(alembic_config, revision.revision) From bd87b926352ab797dfefc06816799749accc68d1 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Mon, 19 Feb 2024 21:24:19 +0400 Subject: [PATCH 02/39] Remove old handlers, add new handlers --- .../command_handlers/create_movie.py | 12 +- .../command_handlers/delete_movie.py | 14 +- .../command_handlers/rate_movie.py | 16 +- .../command_handlers/register_user.py | 8 +- .../command_handlers/review_movie.py | 16 +- .../command_handlers/unrate_movie.py | 12 +- .../common/constants/exceptions.py | 5 +- .../{interfaces => gateways}/__init__.py | 0 .../movie_gateway.py => gateways/movie.py} | 0 .../permissions.py} | 17 +- .../rating_gateway.py => gateways/rating.py} | 0 .../review_gateway.py => gateways/review.py} | 0 .../user_gateway.py => gateways/user.py} | 0 .../{interfaces => }/identity_provider.py | 0 .../{interfaces => }/password_manager.py | 0 .../application/common/readers/__init__.py | 0 src/amdb/application/common/readers/movie.py | 27 +++ src/amdb/application/common/readers/review.py | 14 ++ .../common/{interfaces => }/unit_of_work.py | 0 .../common/view_models/__init__.py | 0 .../common/view_models/detailed_movie.py | 32 ++++ .../common/view_models/non_detailed_movie.py | 20 ++ .../application/common/view_models/review.py | 32 ++++ .../application/queries/detailed_movie.py | 8 + src/amdb/application/queries/get_movie.py | 17 -- .../application/queries/get_movie_ratings.py | 26 --- .../application/queries/get_movie_reviews.py | 30 --- src/amdb/application/queries/get_movies.py | 25 --- .../application/queries/get_my_ratings.py | 27 --- .../application/queries/get_my_reviews.py | 29 --- src/amdb/application/queries/get_rating.py | 19 -- src/amdb/application/queries/get_review.py | 21 --- .../queries/non_detailed_movies.py | 7 + src/amdb/application/queries/reviews.py | 10 + .../query_handlers/detailed_movie.py | 47 +++++ .../query_handlers/get_movie_ratings.py | 63 ------- .../query_handlers/get_movie_reviews.py | 63 ------- .../application/query_handlers/get_movies.py | 49 ----- .../query_handlers/get_my_ratings.py | 55 ------ .../query_handlers/get_my_reviews.py | 55 ------ .../application/query_handlers/get_rating.py | 52 ------ .../application/query_handlers/get_review.py | 54 ------ src/amdb/application/query_handlers/login.py | 8 +- .../query_handlers/non_detailed_movies.py | 43 +++++ .../{get_movie.py => reviews.py} | 37 ++-- .../command_handlers/test_create_movie.py | 12 +- .../command_handlers/test_delete_movie.py | 16 +- .../command_handlers/test_rate_movie.py | 16 +- .../command_handlers/test_register_user.py | 10 +- .../command_handlers/test_review_movie.py | 16 +- .../command_handlers/test_unrate_movie.py | 16 +- .../query_handlers/test_detailed_movie.py | 174 ++++++++++++++++++ .../query_handlers/test_get_movie.py | 115 ------------ .../query_handlers/test_get_movie_ratings.py | 156 ---------------- .../query_handlers/test_get_movie_reviews.py | 154 ---------------- .../query_handlers/test_get_movies.py | 111 ----------- .../query_handlers/test_get_my_reviews.py | 129 ------------- .../query_handlers/test_get_rating.py | 140 -------------- .../query_handlers/test_get_review.py | 140 -------------- .../application/query_handlers/test_login.py | 10 +- ...ratings.py => test_non_detailed_movies.py} | 83 +++++---- 61 files changed, 547 insertions(+), 1721 deletions(-) rename src/amdb/application/common/{interfaces => gateways}/__init__.py (100%) rename src/amdb/application/common/{interfaces/movie_gateway.py => gateways/movie.py} (100%) rename src/amdb/application/common/{interfaces/permissions_gateway.py => gateways/permissions.py} (70%) rename src/amdb/application/common/{interfaces/rating_gateway.py => gateways/rating.py} (100%) rename src/amdb/application/common/{interfaces/review_gateway.py => gateways/review.py} (100%) rename src/amdb/application/common/{interfaces/user_gateway.py => gateways/user.py} (100%) rename src/amdb/application/common/{interfaces => }/identity_provider.py (100%) rename src/amdb/application/common/{interfaces => }/password_manager.py (100%) create mode 100644 src/amdb/application/common/readers/__init__.py create mode 100644 src/amdb/application/common/readers/movie.py create mode 100644 src/amdb/application/common/readers/review.py rename src/amdb/application/common/{interfaces => }/unit_of_work.py (100%) create mode 100644 src/amdb/application/common/view_models/__init__.py create mode 100644 src/amdb/application/common/view_models/detailed_movie.py create mode 100644 src/amdb/application/common/view_models/non_detailed_movie.py create mode 100644 src/amdb/application/common/view_models/review.py create mode 100644 src/amdb/application/queries/detailed_movie.py delete mode 100644 src/amdb/application/queries/get_movie.py delete mode 100644 src/amdb/application/queries/get_movie_ratings.py delete mode 100644 src/amdb/application/queries/get_movie_reviews.py delete mode 100644 src/amdb/application/queries/get_movies.py delete mode 100644 src/amdb/application/queries/get_my_ratings.py delete mode 100644 src/amdb/application/queries/get_my_reviews.py delete mode 100644 src/amdb/application/queries/get_rating.py delete mode 100644 src/amdb/application/queries/get_review.py create mode 100644 src/amdb/application/queries/non_detailed_movies.py create mode 100644 src/amdb/application/queries/reviews.py create mode 100644 src/amdb/application/query_handlers/detailed_movie.py delete mode 100644 src/amdb/application/query_handlers/get_movie_ratings.py delete mode 100644 src/amdb/application/query_handlers/get_movie_reviews.py delete mode 100644 src/amdb/application/query_handlers/get_movies.py delete mode 100644 src/amdb/application/query_handlers/get_my_ratings.py delete mode 100644 src/amdb/application/query_handlers/get_my_reviews.py delete mode 100644 src/amdb/application/query_handlers/get_rating.py delete mode 100644 src/amdb/application/query_handlers/get_review.py create mode 100644 src/amdb/application/query_handlers/non_detailed_movies.py rename src/amdb/application/query_handlers/{get_movie.py => reviews.py} (54%) create mode 100644 tests/unit/application/query_handlers/test_detailed_movie.py delete mode 100644 tests/unit/application/query_handlers/test_get_movie.py delete mode 100644 tests/unit/application/query_handlers/test_get_movie_ratings.py delete mode 100644 tests/unit/application/query_handlers/test_get_movie_reviews.py delete mode 100644 tests/unit/application/query_handlers/test_get_movies.py delete mode 100644 tests/unit/application/query_handlers/test_get_my_reviews.py delete mode 100644 tests/unit/application/query_handlers/test_get_rating.py delete mode 100644 tests/unit/application/query_handlers/test_get_review.py rename tests/unit/application/query_handlers/{test_get_my_ratings.py => test_non_detailed_movies.py} (50%) diff --git a/src/amdb/application/command_handlers/create_movie.py b/src/amdb/application/command_handlers/create_movie.py index 3c481e0..ff40f65 100644 --- a/src/amdb/application/command_handlers/create_movie.py +++ b/src/amdb/application/command_handlers/create_movie.py @@ -3,14 +3,10 @@ from amdb.domain.entities.movie import MovieId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import ( CREATE_MOVIE_ACCESS_DENIED, ) diff --git a/src/amdb/application/command_handlers/delete_movie.py b/src/amdb/application/command_handlers/delete_movie.py index c4b5963..8b7ab42 100644 --- a/src/amdb/application/command_handlers/delete_movie.py +++ b/src/amdb/application/command_handlers/delete_movie.py @@ -1,14 +1,12 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( +from amdb.application.common.gateways.permissions import ( PermissionsGateway, ) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import ( DELETE_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/rate_movie.py b/src/amdb/application/command_handlers/rate_movie.py index ee50707..f576ad2 100644 --- a/src/amdb/application/command_handlers/rate_movie.py +++ b/src/amdb/application/command_handlers/rate_movie.py @@ -7,16 +7,12 @@ from amdb.domain.entities.rating import RatingId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.rate_movie import RateMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import ( RATE_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/register_user.py b/src/amdb/application/command_handlers/register_user.py index c3f3431..bfc0f6c 100644 --- a/src/amdb/application/command_handlers/register_user.py +++ b/src/amdb/application/command_handlers/register_user.py @@ -2,12 +2,12 @@ from amdb.domain.entities.user import UserId from amdb.domain.services.create_user import CreateUser -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import ( +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.permissions import ( PermissionsGateway, ) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.password_manager import PasswordManager +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.password_manager import PasswordManager from amdb.application.common.constants.exceptions import ( USER_NAME_ALREADY_EXISTS, ) diff --git a/src/amdb/application/command_handlers/review_movie.py b/src/amdb/application/command_handlers/review_movie.py index 036305e..be4609c 100644 --- a/src/amdb/application/command_handlers/review_movie.py +++ b/src/amdb/application/command_handlers/review_movie.py @@ -7,16 +7,12 @@ from amdb.domain.entities.review import ReviewId from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import ( REVIEW_MOVIE_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, diff --git a/src/amdb/application/command_handlers/unrate_movie.py b/src/amdb/application/command_handlers/unrate_movie.py index 67e19e2..c766e50 100644 --- a/src/amdb/application/command_handlers/unrate_movie.py +++ b/src/amdb/application/command_handlers/unrate_movie.py @@ -3,15 +3,13 @@ from amdb.domain.entities.movie import Movie from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.application.common.interfaces.permissions_gateway import ( +from amdb.application.common.gateways.permissions import ( PermissionsGateway, ) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import ( UNRATE_MOVIE_ACCESS_DENIED, USER_IS_NOT_OWNER, diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index dfbfecf..19a3df2 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -3,13 +3,10 @@ GET_MOVIE_ACCESS_DENIED = "Access to getting movie is denied" CREATE_MOVIE_ACCESS_DENIED = "Access to movie creation is denied" DELETE_MOVIE_ACCESS_DENIED = "Access to movie deletion is denied" -GET_MOVIE_RATINGS_ACCESS_DENIED = "Access to getting movie ratings is denied" -GET_MY_RATINGS_ACCESS_DENIED = "Access to getting your ratings is denied" GET_RATING_ACCESS_DENIED = "Access to getting rating is denied" RATE_MOVIE_ACCESS_DENIED = "Access to movie rating is denied" UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied" -GET_MOVIE_REVIEWS_ACCESS_DENIED = "Access to getting movie reviews is denied" -GET_MY_REVIEWS_ACCESS_DENIED = "Access to getting your reviews is denied" +GET_REVIEWS_ACCESS_DENIED = "Access to getting movie reviews is denied" GET_REVIEW_ACCESS_DENIED = "Access to getting review is denied" REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied" diff --git a/src/amdb/application/common/interfaces/__init__.py b/src/amdb/application/common/gateways/__init__.py similarity index 100% rename from src/amdb/application/common/interfaces/__init__.py rename to src/amdb/application/common/gateways/__init__.py diff --git a/src/amdb/application/common/interfaces/movie_gateway.py b/src/amdb/application/common/gateways/movie.py similarity index 100% rename from src/amdb/application/common/interfaces/movie_gateway.py rename to src/amdb/application/common/gateways/movie.py diff --git a/src/amdb/application/common/interfaces/permissions_gateway.py b/src/amdb/application/common/gateways/permissions.py similarity index 70% rename from src/amdb/application/common/interfaces/permissions_gateway.py rename to src/amdb/application/common/gateways/permissions.py index 86ac24b..94b29b2 100644 --- a/src/amdb/application/common/interfaces/permissions_gateway.py +++ b/src/amdb/application/common/gateways/permissions.py @@ -28,28 +28,13 @@ def for_create_movie(self) -> int: def for_delete_movie(self) -> int: raise NotImplementedError - def for_get_movie_ratings(self) -> int: - raise NotImplementedError - - def for_get_my_ratings(self) -> int: - raise NotImplementedError - - def for_get_rating(self) -> int: - raise NotImplementedError - def for_rate_movie(self) -> int: raise NotImplementedError def for_unrate_movie(self) -> int: raise NotImplementedError - def for_get_movie_reviews(self) -> int: - raise NotImplementedError - - def for_get_my_reviews(self) -> int: - raise NotImplementedError - - def for_get_review(self) -> int: + def for_get_reviews(self) -> int: raise NotImplementedError def for_review_movie(self) -> int: diff --git a/src/amdb/application/common/interfaces/rating_gateway.py b/src/amdb/application/common/gateways/rating.py similarity index 100% rename from src/amdb/application/common/interfaces/rating_gateway.py rename to src/amdb/application/common/gateways/rating.py diff --git a/src/amdb/application/common/interfaces/review_gateway.py b/src/amdb/application/common/gateways/review.py similarity index 100% rename from src/amdb/application/common/interfaces/review_gateway.py rename to src/amdb/application/common/gateways/review.py diff --git a/src/amdb/application/common/interfaces/user_gateway.py b/src/amdb/application/common/gateways/user.py similarity index 100% rename from src/amdb/application/common/interfaces/user_gateway.py rename to src/amdb/application/common/gateways/user.py diff --git a/src/amdb/application/common/interfaces/identity_provider.py b/src/amdb/application/common/identity_provider.py similarity index 100% rename from src/amdb/application/common/interfaces/identity_provider.py rename to src/amdb/application/common/identity_provider.py diff --git a/src/amdb/application/common/interfaces/password_manager.py b/src/amdb/application/common/password_manager.py similarity index 100% rename from src/amdb/application/common/interfaces/password_manager.py rename to src/amdb/application/common/password_manager.py diff --git a/src/amdb/application/common/readers/__init__.py b/src/amdb/application/common/readers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/readers/movie.py b/src/amdb/application/common/readers/movie.py new file mode 100644 index 0000000..8a3667b --- /dev/null +++ b/src/amdb/application/common/readers/movie.py @@ -0,0 +1,27 @@ +from typing import Protocol, Optional + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.application.common.view_models.detailed_movie import ( + DetailedMovieViewModel, +) +from amdb.application.common.view_models.non_detailed_movie import ( + NonDetailedMovieViewModel, +) + + +class MovieViewModelReader(Protocol): + def list_non_detailed( + self, + current_user_id: Optional[UserId], + limit: int, + offset: int, + ) -> list[NonDetailedMovieViewModel]: + raise NotImplementedError + + def detailed( + self, + movie_id: MovieId, + current_user_id: Optional[UserId], + ) -> Optional[DetailedMovieViewModel]: + raise NotImplementedError diff --git a/src/amdb/application/common/readers/review.py b/src/amdb/application/common/readers/review.py new file mode 100644 index 0000000..f04f6f1 --- /dev/null +++ b/src/amdb/application/common/readers/review.py @@ -0,0 +1,14 @@ +from typing import Protocol + +from amdb.domain.entities.movie import MovieId +from amdb.application.common.view_models.review import ReviewViewModel + + +class ReviewViewModelReader(Protocol): + def list( + self, + movie_id: MovieId, + limit: int, + offset: int, + ) -> list[ReviewViewModel]: + raise NotImplementedError diff --git a/src/amdb/application/common/interfaces/unit_of_work.py b/src/amdb/application/common/unit_of_work.py similarity index 100% rename from src/amdb/application/common/interfaces/unit_of_work.py rename to src/amdb/application/common/unit_of_work.py diff --git a/src/amdb/application/common/view_models/__init__.py b/src/amdb/application/common/view_models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/view_models/detailed_movie.py b/src/amdb/application/common/view_models/detailed_movie.py new file mode 100644 index 0000000..61b86e5 --- /dev/null +++ b/src/amdb/application/common/view_models/detailed_movie.py @@ -0,0 +1,32 @@ +from datetime import date, datetime +from typing import Optional + +from typing_extensions import TypedDict + +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.domain.entities.review import ReviewId, ReviewType + + +class UserRating(TypedDict): + id: RatingId + value: float + created_at: datetime + + +class UserReview(TypedDict): + id: ReviewId + title: str + content: str + type: ReviewType + created_at: datetime + + +class DetailedMovieViewModel(TypedDict): + id: MovieId + title: str + release_date: date + rating: float + rating_count: int + user_rating: Optional[UserRating] + user_review: Optional[UserReview] diff --git a/src/amdb/application/common/view_models/non_detailed_movie.py b/src/amdb/application/common/view_models/non_detailed_movie.py new file mode 100644 index 0000000..03b3c09 --- /dev/null +++ b/src/amdb/application/common/view_models/non_detailed_movie.py @@ -0,0 +1,20 @@ +from datetime import date +from typing import Optional + +from typing_extensions import TypedDict + +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId + + +class UserRating(TypedDict): + id: RatingId + value: float + + +class NonDetailedMovieViewModel(TypedDict): + id: MovieId + title: str + release_date: date + rating: float + user_rating: Optional[UserRating] diff --git a/src/amdb/application/common/view_models/review.py b/src/amdb/application/common/view_models/review.py new file mode 100644 index 0000000..be4e8e3 --- /dev/null +++ b/src/amdb/application/common/view_models/review.py @@ -0,0 +1,32 @@ +__all__ = ("ReviewViewModel",) + +from datetime import datetime +from typing import Optional + +from typing_extensions import TypedDict + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.domain.entities.review import ReviewId, ReviewType + + +class Rating(TypedDict): + id: RatingId + value: float + created_at: datetime + + +class Review(TypedDict): + id: ReviewId + title: str + content: str + type: ReviewType + created_at: datetime + + +class ReviewViewModel(TypedDict): + user_id: UserId + movie_id: MovieId + review: Review + rating: Optional[Rating] diff --git a/src/amdb/application/queries/detailed_movie.py b/src/amdb/application/queries/detailed_movie.py new file mode 100644 index 0000000..5da846a --- /dev/null +++ b/src/amdb/application/queries/detailed_movie.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from amdb.domain.entities.movie import MovieId + + +@dataclass(frozen=True, slots=True) +class GetDetailedMovieQuery: + movie_id: MovieId diff --git a/src/amdb/application/queries/get_movie.py b/src/amdb/application/queries/get_movie.py deleted file mode 100644 index fb197e7..0000000 --- a/src/amdb/application/queries/get_movie.py +++ /dev/null @@ -1,17 +0,0 @@ -from dataclasses import dataclass -from datetime import date - -from amdb.domain.entities.movie import MovieId - - -@dataclass(frozen=True, slots=True) -class GetMovieQuery: - movie_id: MovieId - - -@dataclass(frozen=True, slots=True) -class GetMovieResult: - title: str - release_date: date - rating: float - rating_count: int diff --git a/src/amdb/application/queries/get_movie_ratings.py b/src/amdb/application/queries/get_movie_ratings.py deleted file mode 100644 index 51e1a6a..0000000 --- a/src/amdb/application/queries/get_movie_ratings.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId - - -@dataclass(frozen=True, slots=True) -class GetMovieRatingsQuery: - movie_id: MovieId - limit: int - offset: int - - -@dataclass(frozen=True, slots=True) -class Rating: - user_id: UserId - movie_id: MovieId - value: float - created_at: datetime - - -@dataclass(frozen=True, slots=True) -class GetMovieRatingsResult: - ratings: list[Rating] - rating_count: int diff --git a/src/amdb/application/queries/get_movie_reviews.py b/src/amdb/application/queries/get_movie_reviews.py deleted file mode 100644 index 395fd60..0000000 --- a/src/amdb/application/queries/get_movie_reviews.py +++ /dev/null @@ -1,30 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, ReviewType - - -@dataclass(frozen=True, slots=True) -class GetMovieReviewsQuery: - movie_id: MovieId - limit: int - offset: int - - -@dataclass(frozen=True, slots=True) -class Review: - id: ReviewId - user_id: UserId - movie_id: MovieId - title: str - content: str - type: ReviewType - created_at: datetime - - -@dataclass(frozen=True, slots=True) -class GetMovieReviewsResult: - reviews: list[Review] - review_count: int diff --git a/src/amdb/application/queries/get_movies.py b/src/amdb/application/queries/get_movies.py deleted file mode 100644 index 985338d..0000000 --- a/src/amdb/application/queries/get_movies.py +++ /dev/null @@ -1,25 +0,0 @@ -from dataclasses import dataclass -from datetime import date - -from amdb.domain.entities.movie import MovieId - - -@dataclass(frozen=True, slots=True) -class GetMoviesQuery: - limit: int - offset: int - - -@dataclass(frozen=True, slots=True) -class Movie: - id: MovieId - title: str - release_date: date - rating: float - rating_count: int - - -@dataclass(frozen=True, slots=True) -class GetMoviesResult: - movies: list[Movie] - movie_count: int diff --git a/src/amdb/application/queries/get_my_ratings.py b/src/amdb/application/queries/get_my_ratings.py deleted file mode 100644 index ee2a926..0000000 --- a/src/amdb/application/queries/get_my_ratings.py +++ /dev/null @@ -1,27 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.rating import RatingId - - -@dataclass(frozen=True, slots=True) -class GetMyRatingsQuery: - limit: int - offset: int - - -@dataclass(frozen=True, slots=True) -class Rating: - id: RatingId - user_id: UserId - movie_id: MovieId - value: float - created_at: datetime - - -@dataclass(frozen=True, slots=True) -class GetMyRatingsResult: - ratings: list[Rating] - rating_count: int diff --git a/src/amdb/application/queries/get_my_reviews.py b/src/amdb/application/queries/get_my_reviews.py deleted file mode 100644 index 85ebd96..0000000 --- a/src/amdb/application/queries/get_my_reviews.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, ReviewType - - -@dataclass(frozen=True, slots=True) -class GetMyReviewsQuery: - limit: int - offset: int - - -@dataclass(frozen=True, slots=True) -class Review: - id: ReviewId - user_id: UserId - movie_id: MovieId - title: str - content: str - type: ReviewType - created_at: datetime - - -@dataclass(frozen=True, slots=True) -class GetMyReviewsResult: - reviews: list[Review] - review_count: int diff --git a/src/amdb/application/queries/get_rating.py b/src/amdb/application/queries/get_rating.py deleted file mode 100644 index 16745bb..0000000 --- a/src/amdb/application/queries/get_rating.py +++ /dev/null @@ -1,19 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.rating import RatingId - - -@dataclass(frozen=True, slots=True) -class GetRatingQuery: - rating_id: RatingId - - -@dataclass(frozen=True, slots=True) -class GetRatingResult: - user_id: UserId - movie_id: MovieId - value: float - created_at: datetime diff --git a/src/amdb/application/queries/get_review.py b/src/amdb/application/queries/get_review.py deleted file mode 100644 index ff8189e..0000000 --- a/src/amdb/application/queries/get_review.py +++ /dev/null @@ -1,21 +0,0 @@ -from dataclasses import dataclass -from datetime import datetime - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, ReviewType - - -@dataclass(frozen=True, slots=True) -class GetReviewQuery: - review_id: ReviewId - - -@dataclass(frozen=True, slots=True) -class GetReviewResult: - user_id: UserId - movie_id: MovieId - title: str - content: str - type: ReviewType - created_at: datetime diff --git a/src/amdb/application/queries/non_detailed_movies.py b/src/amdb/application/queries/non_detailed_movies.py new file mode 100644 index 0000000..7b5ff87 --- /dev/null +++ b/src/amdb/application/queries/non_detailed_movies.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class GetNonDetailedMoviesQuery: + limit: int + offset: int diff --git a/src/amdb/application/queries/reviews.py b/src/amdb/application/queries/reviews.py new file mode 100644 index 0000000..a3577b0 --- /dev/null +++ b/src/amdb/application/queries/reviews.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from amdb.domain.entities.movie import MovieId + + +@dataclass(frozen=True, slots=True) +class GetReviewsQuery: + movie_id: MovieId + limit: int + offset: int diff --git a/src/amdb/application/query_handlers/detailed_movie.py b/src/amdb/application/query_handlers/detailed_movie.py new file mode 100644 index 0000000..b001775 --- /dev/null +++ b/src/amdb/application/query_handlers/detailed_movie.py @@ -0,0 +1,47 @@ +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.constants.exceptions import ( + GET_MOVIE_ACCESS_DENIED, + MOVIE_DOES_NOT_EXIST, +) +from amdb.application.common.exception import ApplicationError +from amdb.application.common.view_models.detailed_movie import DetailedMovieViewModel +from amdb.application.queries.detailed_movie import GetDetailedMovieQuery + + +class GetDetailedMovieHandler: + def __init__( + self, + *, + access_concern: AccessConcern, + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, + identity_provider: IdentityProvider, + ) -> None: + self._access_concern = access_concern + self._permissions_gateway = permissions_gateway + self._movie_view_model_reader = movie_view_model_reader + self._identity_provider = identity_provider + + def execute(self, query: GetDetailedMovieQuery) -> DetailedMovieViewModel: + current_permissions = self._identity_provider.get_permissions() + required_permissions = self._permissions_gateway.for_get_movie() + access = self._access_concern.authorize( + current_permissions=current_permissions, + required_permissions=required_permissions, + ) + if not access: + raise ApplicationError(GET_MOVIE_ACCESS_DENIED) + + current_user_id = self._identity_provider.get_user_id() + + detailed_movie_view_model = self._movie_view_model_reader.detailed( + movie_id=query.movie_id, + current_user_id=current_user_id, + ) + if not detailed_movie_view_model: + raise ApplicationError(MOVIE_DOES_NOT_EXIST) + + return detailed_movie_view_model diff --git a/src/amdb/application/query_handlers/get_movie_ratings.py b/src/amdb/application/query_handlers/get_movie_ratings.py deleted file mode 100644 index 6566f04..0000000 --- a/src/amdb/application/query_handlers/get_movie_ratings.py +++ /dev/null @@ -1,63 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie_ratings import ( - GetMovieRatingsQuery, - GetMovieRatingsResult, -) -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_RATINGS_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -class GetMovieRatingsHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._movie_gateway = movie_gateway - self._rating_gateway = rating_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetMovieRatingsQuery) -> GetMovieRatingsResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = ( - self._permissions_gateway.for_get_movie_ratings() - ) - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MOVIE_RATINGS_ACCESS_DENIED) - - movie = self._movie_gateway.with_id(query.movie_id) - if not movie: - raise ApplicationError(MOVIE_DOES_NOT_EXIST) - - ratings = self._rating_gateway.list_with_movie_id( - movie_id=query.movie_id, - limit=query.limit, - offset=query.offset, - ) - get_movie_ratings_result = GetMovieRatingsResult( - ratings=ratings, # type: ignore - rating_count=len(ratings), - ) - - return get_movie_ratings_result diff --git a/src/amdb/application/query_handlers/get_movie_reviews.py b/src/amdb/application/query_handlers/get_movie_reviews.py deleted file mode 100644 index 7d8534f..0000000 --- a/src/amdb/application/query_handlers/get_movie_reviews.py +++ /dev/null @@ -1,63 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_REVIEWS_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError -from amdb.application.queries.get_movie_reviews import ( - GetMovieReviewsQuery, - GetMovieReviewsResult, -) - - -class GetMovieReviewsHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._movie_gateway = movie_gateway - self._review_gateway = review_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetMovieReviewsQuery) -> GetMovieReviewsResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = ( - self._permissions_gateway.for_get_movie_reviews() - ) - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MOVIE_REVIEWS_ACCESS_DENIED) - - movie = self._movie_gateway.with_id(query.movie_id) - if not movie: - raise ApplicationError(MOVIE_DOES_NOT_EXIST) - - reviews = self._review_gateway.list_with_movie_id( - movie_id=query.movie_id, - limit=query.limit, - offset=query.offset, - ) - get_movie_reviews_result = GetMovieReviewsResult( - reviews=reviews, # type: ignore - review_count=len(reviews), - ) - - return get_movie_reviews_result diff --git a/src/amdb/application/query_handlers/get_movies.py b/src/amdb/application/query_handlers/get_movies.py deleted file mode 100644 index d06909d..0000000 --- a/src/amdb/application/query_handlers/get_movies.py +++ /dev/null @@ -1,49 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult -from amdb.application.common.constants.exceptions import ( - GET_MOVIES_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError - - -class GetMoviesHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._movie_gateway = movie_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetMoviesQuery) -> GetMoviesResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movies() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MOVIES_ACCESS_DENIED) - - movies = self._movie_gateway.list( - limit=query.limit, - offset=query.offset, - ) - get_movies_result = GetMoviesResult( - movies=movies, # type: ignore - movie_count=len(movies), - ) - - return get_movies_result diff --git a/src/amdb/application/query_handlers/get_my_ratings.py b/src/amdb/application/query_handlers/get_my_ratings.py deleted file mode 100644 index 5fcef7a..0000000 --- a/src/amdb/application/query_handlers/get_my_ratings.py +++ /dev/null @@ -1,55 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_my_ratings import ( - GetMyRatingsQuery, - GetMyRatingsResult, -) -from amdb.application.common.constants.exceptions import ( - GET_MY_RATINGS_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError - - -class GetMyRatingsHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - rating_gateway: RatingGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._rating_gateway = rating_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetMyRatingsQuery) -> GetMyRatingsResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_rating() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MY_RATINGS_ACCESS_DENIED) - - current_user_id = self._identity_provider.get_user_id() - - ratings = self._rating_gateway.list_with_user_id( - user_id=current_user_id, - limit=query.limit, - offset=query.offset, - ) - get_my_ratings_result = GetMyRatingsResult( - ratings=ratings, # type: ignore - rating_count=len(ratings), - ) - - return get_my_ratings_result diff --git a/src/amdb/application/query_handlers/get_my_reviews.py b/src/amdb/application/query_handlers/get_my_reviews.py deleted file mode 100644 index 9004cbd..0000000 --- a/src/amdb/application/query_handlers/get_my_reviews.py +++ /dev/null @@ -1,55 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.common.constants.exceptions import ( - GET_MY_REVIEWS_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError -from amdb.application.queries.get_my_reviews import ( - GetMyReviewsQuery, - GetMyReviewsResult, -) - - -class GetMyReviewsHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - review_gateway: ReviewGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._review_gateway = review_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetMyReviewsQuery) -> GetMyReviewsResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_my_reviews() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MY_REVIEWS_ACCESS_DENIED) - - current_user_id = self._identity_provider.get_user_id() - - reviews = self._review_gateway.list_with_user_id( - user_id=current_user_id, - limit=query.limit, - offset=query.offset, - ) - get_my_reviews_result = GetMyReviewsResult( - reviews=reviews, # type: ignore - review_count=len(reviews), - ) - - return get_my_reviews_result diff --git a/src/amdb/application/query_handlers/get_rating.py b/src/amdb/application/query_handlers/get_rating.py deleted file mode 100644 index 2c902ea..0000000 --- a/src/amdb/application/query_handlers/get_rating.py +++ /dev/null @@ -1,52 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult -from amdb.application.common.constants.exceptions import ( - GET_RATING_ACCESS_DENIED, - RATING_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -class GetRatingHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - rating_gateway: RatingGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._rating_gateway = rating_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetRatingQuery) -> GetRatingResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_rating() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_RATING_ACCESS_DENIED) - - rating = self._rating_gateway.with_id(query.rating_id) - if not rating: - raise ApplicationError(RATING_DOES_NOT_EXIST) - - get_rating_result = GetRatingResult( - user_id=rating.user_id, - movie_id=rating.movie_id, - value=rating.value, - created_at=rating.created_at, - ) - - return get_rating_result diff --git a/src/amdb/application/query_handlers/get_review.py b/src/amdb/application/query_handlers/get_review.py deleted file mode 100644 index 43cf7de..0000000 --- a/src/amdb/application/query_handlers/get_review.py +++ /dev/null @@ -1,54 +0,0 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.common.constants.exceptions import ( - GET_REVIEW_ACCESS_DENIED, - REVIEW_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError -from amdb.application.queries.get_review import GetReviewQuery, GetReviewResult - - -class GetReviewHandler: - def __init__( - self, - *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - review_gateway: ReviewGateway, - identity_provider: IdentityProvider, - ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._review_gateway = review_gateway - self._identity_provider = identity_provider - - def execute(self, query: GetReviewQuery) -> GetReviewResult: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_review() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_REVIEW_ACCESS_DENIED) - - review = self._review_gateway.with_id(query.review_id) - if not review: - raise ApplicationError(REVIEW_DOES_NOT_EXIST) - - get_review_result = GetReviewResult( - user_id=review.user_id, - movie_id=review.movie_id, - title=review.title, - content=review.content, - type=review.type, - created_at=review.created_at, - ) - - return get_review_result diff --git a/src/amdb/application/query_handlers/login.py b/src/amdb/application/query_handlers/login.py index 47a01e1..be4aa6f 100644 --- a/src/amdb/application/query_handlers/login.py +++ b/src/amdb/application/query_handlers/login.py @@ -2,11 +2,9 @@ from amdb.domain.entities.user import UserId from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.password_manager import PasswordManager +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.password_manager import PasswordManager from amdb.application.common.constants.exceptions import ( LOGIN_ACCESS_DENIED, USER_DOES_NOT_EXIST, diff --git a/src/amdb/application/query_handlers/non_detailed_movies.py b/src/amdb/application/query_handlers/non_detailed_movies.py new file mode 100644 index 0000000..8d7c58d --- /dev/null +++ b/src/amdb/application/query_handlers/non_detailed_movies.py @@ -0,0 +1,43 @@ +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.constants.exceptions import GET_MOVIES_ACCESS_DENIED +from amdb.application.common.exception import ApplicationError +from amdb.application.common.view_models.non_detailed_movie import NonDetailedMovieViewModel +from amdb.application.queries.non_detailed_movies import GetNonDetailedMoviesQuery + + +class GetNonDetailedMoviesHandler: + def __init__( + self, + *, + access_concern: AccessConcern, + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, + identity_provider: IdentityProvider, + ) -> None: + self._access_concern = access_concern + self._permissions_gateway = permissions_gateway + self._movie_view_model_reader = movie_view_model_reader + self._identity_provider = identity_provider + + def execute(self, query: GetNonDetailedMoviesQuery) -> list[NonDetailedMovieViewModel]: + current_permissions = self._identity_provider.get_permissions() + required_permissions = self._permissions_gateway.for_get_movies() + access = self._access_concern.authorize( + current_permissions=current_permissions, + required_permissions=required_permissions, + ) + if not access: + raise ApplicationError(GET_MOVIES_ACCESS_DENIED) + + current_user_id = self._identity_provider.get_user_id() + + non_detailed_movie_models = self._movie_view_model_reader.list_non_detailed( + current_user_id=current_user_id, + limit=query.limit, + offset=query.offset, + ) + + return non_detailed_movie_models diff --git a/src/amdb/application/query_handlers/get_movie.py b/src/amdb/application/query_handlers/reviews.py similarity index 54% rename from src/amdb/application/query_handlers/get_movie.py rename to src/amdb/application/query_handlers/reviews.py index 0632db5..6c92913 100644 --- a/src/amdb/application/query_handlers/get_movie.py +++ b/src/amdb/application/query_handlers/reviews.py @@ -1,52 +1,51 @@ from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie import GetMovieQuery, GetMovieResult +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.readers.review import ReviewViewModelReader +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.view_models.review import ReviewViewModel from amdb.application.common.constants.exceptions import ( - GET_MOVIE_ACCESS_DENIED, + GET_REVIEWS_ACCESS_DENIED, MOVIE_DOES_NOT_EXIST, ) from amdb.application.common.exception import ApplicationError +from amdb.application.queries.reviews import GetReviewsQuery -class GetMovieHandler: +class GetReviewsHandler: def __init__( self, *, access_concern: AccessConcern, permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, + review_view_model_reader: ReviewViewModelReader, identity_provider: IdentityProvider, ) -> None: self._access_concern = access_concern self._permissions_gateway = permissions_gateway self._movie_gateway = movie_gateway + self._review_view_model_reader = review_view_model_reader self._identity_provider = identity_provider - def execute(self, query: GetMovieQuery) -> GetMovieResult: + def execute(self, query: GetReviewsQuery) -> list[ReviewViewModel]: current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movie() + required_permissions = self._permissions_gateway.for_get_reviews() access = self._access_concern.authorize( current_permissions=current_permissions, required_permissions=required_permissions, ) if not access: - raise ApplicationError(GET_MOVIE_ACCESS_DENIED) + raise ApplicationError(GET_REVIEWS_ACCESS_DENIED) movie = self._movie_gateway.with_id(query.movie_id) if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - get_movie_result = GetMovieResult( - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - rating_count=movie.rating_count, + review_view_models = self._review_view_model_reader.list( + movie_id=query.movie_id, + limit=query.limit, + offset=query.offset, ) - return get_movie_result + return review_view_models diff --git a/tests/unit/application/command_handlers/test_create_movie.py b/tests/unit/application/command_handlers/test_create_movie.py index 5d017b9..3486bd8 100644 --- a/tests/unit/application/command_handlers/test_create_movie.py +++ b/tests/unit/application/command_handlers/test_create_movie.py @@ -5,14 +5,10 @@ from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.create_movie import CreateMovieCommand from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_delete_movie.py b/tests/unit/application/command_handlers/test_delete_movie.py index 6821f40..be66daa 100644 --- a/tests/unit/application/command_handlers/test_delete_movie.py +++ b/tests/unit/application/command_handlers/test_delete_movie.py @@ -6,16 +6,12 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.delete_movie import DeleteMovieCommand from amdb.application.command_handlers.delete_movie import DeleteMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_rate_movie.py b/tests/unit/application/command_handlers/test_rate_movie.py index 376a737..559f0ef 100644 --- a/tests/unit/application/command_handlers/test_rate_movie.py +++ b/tests/unit/application/command_handlers/test_rate_movie.py @@ -11,16 +11,12 @@ from amdb.domain.services.rate_movie import RateMovie from amdb.domain.constants.exceptions import INVALID_RATING_VALUE from amdb.domain.exception import DomainError -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.rate_movie import RateMovieCommand from amdb.application.command_handlers.rate_movie import RateMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_register_user.py b/tests/unit/application/command_handlers/test_register_user.py index e7aa115..7c3dda9 100644 --- a/tests/unit/application/command_handlers/test_register_user.py +++ b/tests/unit/application/command_handlers/test_register_user.py @@ -3,12 +3,10 @@ from amdb.domain.entities.user import UserId, User from amdb.domain.services.create_user import CreateUser -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.password_manager import PasswordManager +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.password_manager import PasswordManager from amdb.application.commands.register_user import RegisterUserCommand from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_review_movie.py b/tests/unit/application/command_handlers/test_review_movie.py index 20d494d..fd5982d 100644 --- a/tests/unit/application/command_handlers/test_review_movie.py +++ b/tests/unit/application/command_handlers/test_review_movie.py @@ -9,16 +9,12 @@ from amdb.domain.entities.review import ReviewId, ReviewType, Review from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.review_movie import ReviewMovieCommand from amdb.application.command_handlers.review_movie import ReviewMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/command_handlers/test_unrate_movie.py b/tests/unit/application/command_handlers/test_unrate_movie.py index 45b5a9f..c3ba201 100644 --- a/tests/unit/application/command_handlers/test_unrate_movie.py +++ b/tests/unit/application/command_handlers/test_unrate_movie.py @@ -9,16 +9,12 @@ from amdb.domain.entities.rating import RatingId, Rating from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.unrate_movie import UnrateMovieCommand from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/query_handlers/test_detailed_movie.py b/tests/unit/application/query_handlers/test_detailed_movie.py new file mode 100644 index 0000000..8331c92 --- /dev/null +++ b/tests/unit/application/query_handlers/test_detailed_movie.py @@ -0,0 +1,174 @@ +from unittest.mock import Mock +from datetime import date, datetime, timezone + +import pytest +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import UserId, User +from amdb.domain.entities.movie import MovieId, Movie +from amdb.domain.entities.rating import RatingId, Rating +from amdb.domain.entities.review import ReviewId, ReviewType, Review +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.view_models.detailed_movie import ( + UserRating, + UserReview, + DetailedMovieViewModel, +) +from amdb.application.queries.detailed_movie import GetDetailedMovieQuery +from amdb.application.query_handlers.detailed_movie import GetDetailedMovieHandler +from amdb.application.common.constants.exceptions import ( + GET_MOVIE_ACCESS_DENIED, + MOVIE_DOES_NOT_EXIST, +) +from amdb.application.common.exception import ApplicationError + + +@pytest.fixture +def identity_provider_with_correct_permissions( + permissions_gateway: PermissionsGateway, +) -> IdentityProvider: + identity_provider = Mock() + + correct_permissions = permissions_gateway.for_get_movie() + identity_provider.get_permissions = Mock(return_value=correct_permissions) + + return identity_provider + + + +def test_get_detailed_movie( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, + identity_provider_with_correct_permissions: IdentityProvider, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + release_date=date(1999, 3, 31), + rating=8, + rating_count=1, + ) + movie_gateway.save(movie) + + rating = Rating( + id=RatingId(uuid7()), + movie_id=movie.id, + user_id=user.id, + value=8, + created_at=datetime.now(timezone.utc), + ) + rating_gateway.save(rating) + + review = Review( + id=ReviewId(uuid7()), + user_id=user.id, + movie_id=movie.id, + title="Not bad", + content="Great soundtrack", + type=ReviewType.POSITIVE, + created_at=datetime.now(timezone.utc), + ) + review_gateway.save(review) + + unit_of_work.commit() + + identity_provider_with_correct_permissions.get_user_id = Mock( + return_value=user.id, + ) + + get_detailed_movie_query = GetDetailedMovieQuery( + movie_id=movie.id, + ) + get_detailed_movie_handler = GetDetailedMovieHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_view_model_reader=movie_view_model_reader, + identity_provider=identity_provider_with_correct_permissions, + ) + + expected_result = DetailedMovieViewModel( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating_count=movie.rating_count, + user_rating=UserRating( + id=rating.id, + value=rating.value, + created_at=rating.created_at, + ), + user_review=UserReview( + id=review.id, + title=review.title, + content=review.content, + type=review.type, + created_at=review.type, + ) + ) + result = get_detailed_movie_handler.execute(get_detailed_movie_query) + + assert expected_result == result + + +def test_get_detailed_movie_should_raise_error_when_access_is_denied( + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, + identity_provider_with_incorrect_permissions: IdentityProvider, +): + get_detailed_movie_query = GetDetailedMovieQuery( + movie_id=MovieId(uuid7()), + ) + get_detailed_movie_handler = GetDetailedMovieHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_view_model_reader=movie_view_model_reader, + identity_provider=identity_provider_with_incorrect_permissions, + ) + + with pytest.raises(ApplicationError) as error: + get_detailed_movie_handler.execute(get_detailed_movie_query) + + assert error.value.message == GET_MOVIE_ACCESS_DENIED + + +def test_get_detailed_movie_should_raise_error_when_movie_does_not_exist( + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, + identity_provider_with_correct_permissions: IdentityProvider, +): + identity_provider_with_correct_permissions.get_user_id = Mock( + return_value=UserId(uuid7()), + ) + + get_detailed_movie_query = GetDetailedMovieQuery( + movie_id=MovieId(uuid7()), + ) + get_detailed_movie_handler = GetDetailedMovieHandler( + access_concern=AccessConcern(), + permissions_gateway=permissions_gateway, + movie_view_model_reader=movie_view_model_reader, + identity_provider=identity_provider_with_correct_permissions, + ) + + with pytest.raises(ApplicationError) as error: + get_detailed_movie_handler.execute(get_detailed_movie_query) + + assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_movie.py b/tests/unit/application/query_handlers/test_get_movie.py deleted file mode 100644 index c87297c..0000000 --- a/tests/unit/application/query_handlers/test_get_movie.py +++ /dev/null @@ -1,115 +0,0 @@ -from unittest.mock import Mock -from datetime import date - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie import GetMovieQuery, GetMovieResult -from amdb.application.query_handlers.get_movie import GetMovieHandler -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_movie( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - movie = Movie( - id=MovieId(uuid7()), - title="Matrix", - release_date=date(1999, 3, 31), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie) - - unit_of_work.commit() - - get_movie_query = GetMovieQuery( - movie_id=movie.id, - ) - get_movie_handler = GetMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_movie_result = get_movie_handler.execute(get_movie_query) - expected_get_movie_result = GetMovieResult( - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - rating_count=movie.rating_count, - ) - - assert get_movie_result == expected_get_movie_result - - -def test_get_movie_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_movie_query = GetMovieQuery( - movie_id=MovieId(uuid7()), - ) - get_movie_handler = GetMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_handler.execute(get_movie_query) - - assert error.value.message == GET_MOVIE_ACCESS_DENIED - - -def test_get_movie_should_raise_error_when_movie_does_not_exist( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - identity_provider_with_correct_permissions: IdentityProvider, -): - get_movie_query = GetMovieQuery( - movie_id=MovieId(uuid7()), - ) - get_movie_handler = GetMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_handler.execute(get_movie_query) - - assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_movie_ratings.py b/tests/unit/application/query_handlers/test_get_movie_ratings.py deleted file mode 100644 index b824a16..0000000 --- a/tests/unit/application/query_handlers/test_get_movie_ratings.py +++ /dev/null @@ -1,156 +0,0 @@ -from unittest.mock import Mock -from datetime import date, datetime, timezone - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.user import UserId, User -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.entities.rating import RatingId, Rating -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie_ratings import ( - GetMovieRatingsQuery, - GetMovieRatingsResult, -) -from amdb.application.query_handlers.get_movie_ratings import ( - GetMovieRatingsHandler, -) -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_RATINGS_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movie_ratings() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_movie_ratings( - user_gateway: UserGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - user = User( - id=UserId(uuid7()), - name="John Doe", - ) - user_gateway.save(user) - - movie = Movie( - id=MovieId(uuid7()), - title="Matrix", - release_date=date(1999, 3, 31), - rating=10, - rating_count=1, - ) - movie_gateway.save(movie) - - rating = Rating( - id=RatingId(uuid7()), - movie_id=movie.id, - user_id=user.id, - value=10, - created_at=datetime.now(timezone.utc), - ) - rating_gateway.save(rating) - - unit_of_work.commit() - - identity_provider_with_correct_permissions.get_user_id = Mock( - return_value=user.id, - ) - - get_movie_ratings_query = GetMovieRatingsQuery( - movie_id=movie.id, - limit=10, - offset=0, - ) - get_movie_ratings_handler = GetMovieRatingsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_movie_ratings_result = get_movie_ratings_handler.execute( - get_movie_ratings_query - ) - expected_get_movie_ratings_result = GetMovieRatingsResult( - ratings=[rating], - rating_count=1, - ) - - assert get_movie_ratings_result == expected_get_movie_ratings_result - - -def test_get_movie_ratings_should_raise_error_when_access_is_denied( - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_movie_ratings_query = GetMovieRatingsQuery( - movie_id=MovieId(uuid7()), - limit=10, - offset=0, - ) - get_movie_ratings_handler = GetMovieRatingsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_ratings_handler.execute(get_movie_ratings_query) - - assert error.value.message == GET_MOVIE_RATINGS_ACCESS_DENIED - - -def test_get_movie_ratings_should_raise_error_when_movie_does_not_exist( - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - identity_provider_with_correct_permissions: IdentityProvider, -): - get_movie_ratings_query = GetMovieRatingsQuery( - movie_id=MovieId(uuid7()), - limit=10, - offset=0, - ) - get_movie_ratings_handler = GetMovieRatingsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_ratings_handler.execute(get_movie_ratings_query) - - assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_movie_reviews.py b/tests/unit/application/query_handlers/test_get_movie_reviews.py deleted file mode 100644 index 3aae0d8..0000000 --- a/tests/unit/application/query_handlers/test_get_movie_reviews.py +++ /dev/null @@ -1,154 +0,0 @@ -from unittest.mock import Mock -from datetime import date, datetime, timezone - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.user import UserId, User -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.entities.review import ReviewId, ReviewType, Review -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie_reviews import ( - GetMovieReviewsQuery, - GetMovieReviewsResult, -) -from amdb.application.query_handlers.get_movie_reviews import ( - GetMovieReviewsHandler, -) -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_REVIEWS_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movie_reviews() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_movie_reviews( - permissions_gateway: PermissionsGateway, - user_gateway: UserGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - user = User( - id=UserId(uuid7()), - name="John Doe", - ) - user_gateway.save(user) - - movie = Movie( - id=MovieId(uuid7()), - title="Gone girl", - release_date=date(2014, 10, 3), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie) - - review = Review( - id=ReviewId(uuid7()), - user_id=user.id, - movie_id=movie.id, - title="Masterpice", - content="Extremely underrated", - type=ReviewType.POSITIVE, - created_at=datetime.now(timezone.utc), - ) - review_gateway.save(review) - - unit_of_work.commit() - - get_movie_reviews_query = GetMovieReviewsQuery( - movie_id=movie.id, - limit=10, - offset=0, - ) - get_movie_reviews_handler = GetMovieReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_movie_reviews_result = get_movie_reviews_handler.execute( - get_movie_reviews_query - ) - expected_get_movie_reviews_result = GetMovieReviewsResult( - reviews=[review], - review_count=1, - ) - - assert get_movie_reviews_result == expected_get_movie_reviews_result - - -def test_get_movie_reviews_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_movie_reviews_query = GetMovieReviewsQuery( - movie_id=MovieId(uuid7()), - limit=10, - offset=0, - ) - get_movie_reviews_handler = GetMovieReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_reviews_handler.execute(get_movie_reviews_query) - - assert error.value.message == GET_MOVIE_REVIEWS_ACCESS_DENIED - - -def test_get_movie_reviews_should_raise_error_when_movie_does_not_exist( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - identity_provider_with_correct_permissions: IdentityProvider, -): - get_movie_reviews_query = GetMovieReviewsQuery( - movie_id=MovieId(uuid7()), - limit=10, - offset=0, - ) - get_movie_reviews_handler = GetMovieReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movie_reviews_handler.execute(get_movie_reviews_query) - - assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_movies.py b/tests/unit/application/query_handlers/test_get_movies.py deleted file mode 100644 index a648328..0000000 --- a/tests/unit/application/query_handlers/test_get_movies.py +++ /dev/null @@ -1,111 +0,0 @@ -from unittest.mock import Mock -from datetime import date - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult -from amdb.application.query_handlers.get_movies import GetMoviesHandler -from amdb.application.common.constants.exceptions import ( - GET_MOVIES_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movies() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_movies( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - movie1 = Movie( - id=MovieId(uuid7()), - title="Matrix", - release_date=date(1999, 3, 31), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie1) - - movie2 = Movie( - id=MovieId(uuid7()), - title="There Will Be Blood", - release_date=date(2007, 9, 26), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie2) - - movie3 = Movie( - id=MovieId(uuid7()), - title="Mulholland Drive", - release_date=date(2001, 5, 16), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie3) - - unit_of_work.commit() - - get_movies_query = GetMoviesQuery( - limit=10, - offset=1, - ) - get_movies_handler = GetMoviesHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_movies_result = get_movies_handler.execute(get_movies_query) - expected_get_movies_result = GetMoviesResult( - movies=[movie2, movie3], - movie_count=2, - ) - - assert get_movies_result == expected_get_movies_result - - -def test_get_movies_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_movies_query = GetMoviesQuery( - limit=10, - offset=1, - ) - get_movies_handler = GetMoviesHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_movies_handler.execute(get_movies_query) - - assert error.value.message == GET_MOVIES_ACCESS_DENIED diff --git a/tests/unit/application/query_handlers/test_get_my_reviews.py b/tests/unit/application/query_handlers/test_get_my_reviews.py deleted file mode 100644 index ac4bafd..0000000 --- a/tests/unit/application/query_handlers/test_get_my_reviews.py +++ /dev/null @@ -1,129 +0,0 @@ -from unittest.mock import Mock -from datetime import date, datetime, timezone - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.user import UserId, User -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.entities.review import ReviewId, ReviewType, Review -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_my_reviews import ( - GetMyReviewsQuery, - GetMyReviewsResult, -) -from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler -from amdb.application.common.constants.exceptions import ( - GET_MY_REVIEWS_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_my_reviews() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_my_reviews( - permissions_gateway: PermissionsGateway, - user_gateway: UserGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - user = User( - id=UserId(uuid7()), - name="John Doe", - ) - user_gateway.save(user) - - movie = Movie( - id=MovieId(uuid7()), - title="Gone girl", - release_date=date(2014, 10, 3), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie) - - review = Review( - id=ReviewId(uuid7()), - user_id=user.id, - movie_id=movie.id, - title="Masterpice", - content="Extremely underrated", - type=ReviewType.POSITIVE, - created_at=datetime.now(timezone.utc), - ) - review_gateway.save(review) - - unit_of_work.commit() - - identity_provider_with_correct_permissions.get_user_id = Mock( - return_value=user.id, - ) - - get_my_reviews_query = GetMyReviewsQuery( - limit=10, - offset=0, - ) - get_my_reviews_handler = GetMyReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_my_reviews_result = get_my_reviews_handler.execute( - get_my_reviews_query - ) - expected_get_my_reviews_result = GetMyReviewsResult( - reviews=[review], - review_count=1, - ) - - assert get_my_reviews_result == expected_get_my_reviews_result - - -def test_get_my_reviews_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - review_gateway: ReviewGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_my_reviews_query = GetMyReviewsQuery( - limit=10, - offset=0, - ) - get_my_reviews_handler = GetMyReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - identity_provider_with_incorrect_permissions.get_user_id = Mock( - return_value=UserId(uuid7()), - ) - - with pytest.raises(ApplicationError) as error: - get_my_reviews_handler.execute(get_my_reviews_query) - - assert error.value.message == GET_MY_REVIEWS_ACCESS_DENIED diff --git a/tests/unit/application/query_handlers/test_get_rating.py b/tests/unit/application/query_handlers/test_get_rating.py deleted file mode 100644 index 4f8be6f..0000000 --- a/tests/unit/application/query_handlers/test_get_rating.py +++ /dev/null @@ -1,140 +0,0 @@ -from unittest.mock import Mock -from datetime import date, datetime, timezone - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.user import UserId, User -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.entities.rating import RatingId, Rating -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult -from amdb.application.query_handlers.get_rating import GetRatingHandler -from amdb.application.common.constants.exceptions import ( - GET_RATING_ACCESS_DENIED, - RATING_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_rating() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_rating( - user_gateway: UserGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - user = User( - id=UserId(uuid7()), - name="John Doe", - ) - user_gateway.save(user) - - movie = Movie( - id=MovieId(uuid7()), - title="Matrix", - release_date=date(1999, 3, 31), - rating=10, - rating_count=1, - ) - movie_gateway.save(movie) - - rating = Rating( - id=RatingId(uuid7()), - movie_id=movie.id, - user_id=user.id, - value=10, - created_at=datetime.now(timezone.utc), - ) - rating_gateway.save(rating) - - unit_of_work.commit() - - identity_provider_with_correct_permissions.get_user_id = Mock( - return_value=user.id, - ) - - get_rating_query = GetRatingQuery( - rating_id=rating.id, - ) - get_rating_handler = GetRatingHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_rating_result = get_rating_handler.execute(get_rating_query) - expected_get_rating_result = GetRatingResult( - user_id=rating.user_id, - movie_id=rating.movie_id, - value=rating.value, - created_at=rating.created_at, - ) - - assert get_rating_result == expected_get_rating_result - - -def test_get_rating_should_raise_error_when_access_is_denied( - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_rating_query = GetRatingQuery( - rating_id=RatingId(uuid7()), - ) - get_rating_handler = GetRatingHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_rating_handler.execute(get_rating_query) - - assert error.value.message == GET_RATING_ACCESS_DENIED - - -def test_get_rating_should_raise_error_when_rating_does_not_exist( - rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, - identity_provider_with_correct_permissions: IdentityProvider, -): - get_rating_query = GetRatingQuery( - rating_id=RatingId(uuid7()), - ) - get_rating_handler = GetRatingHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - rating_gateway=rating_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_rating_handler.execute(get_rating_query) - - assert error.value.message == RATING_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_review.py b/tests/unit/application/query_handlers/test_get_review.py deleted file mode 100644 index 858996b..0000000 --- a/tests/unit/application/query_handlers/test_get_review.py +++ /dev/null @@ -1,140 +0,0 @@ -from unittest.mock import Mock -from datetime import date, datetime, timezone - -import pytest -from uuid_extensions import uuid7 - -from amdb.domain.entities.user import UserId, User -from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.entities.review import ReviewId, ReviewType, Review -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.review_gateway import ReviewGateway -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_review import GetReviewQuery, GetReviewResult -from amdb.application.query_handlers.get_review import GetReviewHandler -from amdb.application.common.constants.exceptions import ( - GET_REVIEW_ACCESS_DENIED, - REVIEW_DOES_NOT_EXIST, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_review() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - -def test_get_review( - permissions_gateway: PermissionsGateway, - user_gateway: UserGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, -): - user = User( - id=UserId(uuid7()), - name="John Doe", - ) - user_gateway.save(user) - - movie = Movie( - id=MovieId(uuid7()), - title="Gone girl", - release_date=date(2014, 10, 3), - rating=0, - rating_count=0, - ) - movie_gateway.save(movie) - - review = Review( - id=ReviewId(uuid7()), - user_id=user.id, - movie_id=movie.id, - title="Masterpice", - content="Extremely underrated", - type=ReviewType.POSITIVE, - created_at=datetime.now(timezone.utc), - ) - review_gateway.save(review) - - unit_of_work.commit() - - get_review_query = GetReviewQuery( - review_id=review.id, - ) - get_review_handler = GetReviewHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - get_review_result = get_review_handler.execute(get_review_query) - expected_get_review_result = GetReviewResult( - user_id=review.user_id, - movie_id=review.movie_id, - title=review.title, - content=review.content, - type=review.type, - created_at=review.created_at, - ) - - assert get_review_result == expected_get_review_result - - -def test_get_review_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - review_gateway: ReviewGateway, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_review_query = GetReviewQuery( - review_id=ReviewId(uuid7()), - ) - get_review_handler = GetReviewHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_review_handler.execute(get_review_query) - - assert error.value.message == GET_REVIEW_ACCESS_DENIED - - -def test_get_review_should_raise_error_when_review_does_not_exist( - permissions_gateway: PermissionsGateway, - review_gateway: ReviewGateway, - identity_provider_with_correct_permissions: IdentityProvider, -): - get_review_query = GetReviewQuery( - review_id=ReviewId(uuid7()), - ) - get_review_handler = GetReviewHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - review_gateway=review_gateway, - identity_provider=identity_provider_with_correct_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_review_handler.execute(get_review_query) - - assert error.value.message == REVIEW_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_login.py b/tests/unit/application/query_handlers/test_login.py index d996d61..91cc151 100644 --- a/tests/unit/application/query_handlers/test_login.py +++ b/tests/unit/application/query_handlers/test_login.py @@ -3,12 +3,10 @@ from amdb.domain.entities.user import UserId, User from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.password_manager import PasswordManager +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.password_manager import PasswordManager from amdb.application.queries.login import LoginQuery from amdb.application.query_handlers.login import LoginHandler from amdb.application.common.constants.exceptions import ( diff --git a/tests/unit/application/query_handlers/test_get_my_ratings.py b/tests/unit/application/query_handlers/test_non_detailed_movies.py similarity index 50% rename from tests/unit/application/query_handlers/test_get_my_ratings.py rename to tests/unit/application/query_handlers/test_non_detailed_movies.py index eaff9eb..6b32a6a 100644 --- a/tests/unit/application/query_handlers/test_get_my_ratings.py +++ b/tests/unit/application/query_handlers/test_non_detailed_movies.py @@ -8,24 +8,20 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.rating import RatingId, Rating from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.interfaces.user_gateway import UserGateway -from amdb.application.common.interfaces.movie_gateway import MovieGateway -from amdb.application.common.interfaces.rating_gateway import RatingGateway -from amdb.application.common.interfaces.permissions_gateway import ( - PermissionsGateway, -) -from amdb.application.common.interfaces.unit_of_work import UnitOfWork -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_my_ratings import ( - GetMyRatingsQuery, - GetMyRatingsResult, -) -from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler -from amdb.application.common.constants.exceptions import ( - GET_MY_RATINGS_ACCESS_DENIED, +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.view_models.non_detailed_movie import ( + UserRating, + NonDetailedMovieViewModel, ) +from amdb.application.queries.non_detailed_movies import GetNonDetailedMoviesQuery +from amdb.application.query_handlers.non_detailed_movies import GetNonDetailedMoviesHandler +from amdb.application.common.constants.exceptions import GET_MOVIE_ACCESS_DENIED from amdb.application.common.exception import ApplicationError @@ -35,18 +31,19 @@ def identity_provider_with_correct_permissions( ) -> IdentityProvider: identity_provider = Mock() - correct_permissions = permissions_gateway.for_get_my_ratings() + correct_permissions = permissions_gateway.for_get_movie() identity_provider.get_permissions = Mock(return_value=correct_permissions) return identity_provider -def test_get_my_ratings( +def test_get_non_detailed_movies( user_gateway: UserGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, - permissions_gateway: PermissionsGateway, unit_of_work: UnitOfWork, + permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, identity_provider_with_correct_permissions: IdentityProvider, ): user = User( @@ -59,7 +56,7 @@ def test_get_my_ratings( id=MovieId(uuid7()), title="Matrix", release_date=date(1999, 3, 31), - rating=10, + rating=8, rating_count=1, ) movie_gateway.save(movie) @@ -68,7 +65,7 @@ def test_get_my_ratings( id=RatingId(uuid7()), movie_id=movie.id, user_id=user.id, - value=10, + value=8, created_at=datetime.now(timezone.utc), ) rating_gateway.save(rating) @@ -79,45 +76,51 @@ def test_get_my_ratings( return_value=user.id, ) - get_my_ratings_query = GetMyRatingsQuery( + get_non_detailed_movies_query = GetNonDetailedMoviesQuery( limit=10, offset=0, ) - get_my_ratings_handler = GetMyRatingsHandler( + get_non_detailed_movies_handler = GetNonDetailedMoviesHandler( access_concern=AccessConcern(), permissions_gateway=permissions_gateway, - rating_gateway=rating_gateway, + movie_view_model_reader=movie_view_model_reader, identity_provider=identity_provider_with_correct_permissions, ) - get_my_ratings_result = get_my_ratings_handler.execute( - get_my_ratings_query - ) - expected_get_my_ratings_result = GetMyRatingsResult( - ratings=[rating], - rating_count=1, - ) + expected_result = [ + NonDetailedMovieViewModel( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + user_rating=UserRating( + id=rating.id, + value=rating.value, + ), + ), + ] + result = get_non_detailed_movies_handler.execute(get_non_detailed_movies_query) - assert get_my_ratings_result == expected_get_my_ratings_result + assert expected_result == result -def test_get_my_ratings_should_raise_error_when_access_is_denied( - rating_gateway: RatingGateway, +def test_get_non_detailed_movies_should_raise_error_when_access_is_denied( permissions_gateway: PermissionsGateway, + movie_view_model_reader: MovieViewModelReader, identity_provider_with_incorrect_permissions: IdentityProvider, ): - get_my_ratings_query = GetMyRatingsQuery( + get_non_detailed_movies_query = GetNonDetailedMoviesQuery( limit=10, offset=0, ) - get_my_ratings_handler = GetMyRatingsHandler( + get_non_detailed_movies_handler = GetNonDetailedMoviesHandler( access_concern=AccessConcern(), permissions_gateway=permissions_gateway, - rating_gateway=rating_gateway, + movie_view_model_reader=movie_view_model_reader, identity_provider=identity_provider_with_incorrect_permissions, ) with pytest.raises(ApplicationError) as error: - get_my_ratings_handler.execute(get_my_ratings_query) + get_non_detailed_movies_handler.execute(get_non_detailed_movies_query) - assert error.value.message == GET_MY_RATINGS_ACCESS_DENIED + assert error.value.message == GET_MOVIE_ACCESS_DENIED From 17c20e65dfa5b7d6f61864ee336684d5973dc257 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Thu, 22 Feb 2024 20:57:55 +0400 Subject: [PATCH 03/39] Refactor gateway imlementations, add new tests, fix routes --- .env.template | 28 -- .pre-commit-config.yaml | 4 +- config/prod_config.template.toml | 13 + config/test_config.template.toml | 5 + docker-compose.yaml | 18 +- pyproject.toml | 9 +- .../command_handlers/create_movie.py | 2 +- .../command_handlers/delete_movie.py | 2 +- .../command_handlers/rate_movie.py | 4 +- .../command_handlers/review_movie.py | 4 +- .../command_handlers/unrate_movie.py | 4 +- .../common/constants/exceptions.py | 5 - src/amdb/application/common/gateways/movie.py | 3 - .../common/gateways/permissions.py | 9 - .../application/common/gateways/rating.py | 16 - .../application/common/gateways/review.py | 16 - .../application/common/identity_provider.py | 9 +- .../readers/{movie.py => detailed_movie.py} | 15 +- .../common/readers/non_detailed_movie.py | 16 + .../application/common/view_models/review.py | 12 +- .../query_handlers/detailed_movie.py | 36 +-- .../query_handlers/non_detailed_movies.py | 42 +-- .../application/query_handlers/reviews.py | 23 +- src/amdb/domain/services/access_concern.py | 4 +- .../auth/raw/identity_provider.py | 9 +- .../infrastructure/auth/session/config.py | 14 +- .../auth/session/constants/exceptions.py | 2 - .../auth/session/identity_provider.py | 43 ++- .../auth/session/{model.py => session.py} | 0 .../auth/session/session_gateway.py | 8 + .../auth/session/session_processor.py | 9 +- .../password_manager/hash_computer.py | 28 ++ .../{model.py => password_hash.py} | 6 +- .../password_manager/password_hash_gateway.py | 12 + .../password_manager/password_manager.py | 51 ++- .../persistence/alembic/config.py | 2 +- .../migrations/versions/65f8840f4494_.py | 9 +- .../migrations/versions/85a348467b90_.py | 4 +- .../migrations/versions/9e92de201574_.py | 4 +- .../persistence/redis/config.py | 15 +- .../redis/mappers}/__init__.py | 0 .../{gateways => mappers}/permissions.py | 8 +- .../redis/{gateways => mappers}/session.py | 8 +- .../persistence/sqlalchemy/config.py | 24 +- .../persistence/sqlalchemy/gateway_factory.py | 49 --- .../persistence/sqlalchemy/gateways/movie.py | 52 ---- .../persistence/sqlalchemy/gateways/rating.py | 101 ------ .../persistence/sqlalchemy/gateways/review.py | 96 ------ .../persistence/sqlalchemy/gateways/user.py | 37 --- .../sqlalchemy/gateways/user_password_hash.py | 37 --- .../mappers/entities}/__init__.py | 0 .../sqlalchemy/mappers/entities/movie.py | 53 ++++ .../sqlalchemy/mappers/entities/rating.py | 68 ++++ .../sqlalchemy/mappers/entities/review.py | 68 ++++ .../sqlalchemy/mappers/entities/user.py | 41 +++ .../persistence/sqlalchemy/mappers/movie.py | 24 -- .../sqlalchemy/mappers/password_hash.py | 41 +++ .../persistence/sqlalchemy/mappers/rating.py | 26 -- .../persistence/sqlalchemy/mappers/review.py | 34 -- .../persistence/sqlalchemy/mappers/user.py | 18 -- .../sqlalchemy/mappers/user_password_hash.py | 31 -- .../view_models}/__init__.py | 0 .../mappers/view_models/detailed_movie.py | 119 +++++++ .../mappers/view_models/non_detailed_movie.py | 91 ++++++ .../sqlalchemy/mappers/view_models/review.py | 109 +++++++ .../persistence/sqlalchemy/models/movie.py | 2 +- ...user_password_hash.py => password_hash.py} | 2 +- .../persistence/sqlalchemy/models/rating.py | 10 +- .../persistence/sqlalchemy/models/review.py | 10 +- .../persistence/sqlalchemy/models/user.py | 2 +- src/amdb/infrastructure/security/__init__.py | 0 src/amdb/infrastructure/security/hasher.py | 30 -- src/amdb/main/cli/__main__.py | 18 +- src/amdb/main/cli/app.py | 47 ++- src/amdb/main/cli/di.py | 49 --- src/amdb/main/config.py | 51 --- src/amdb/main/ioc.py | 294 +++++++----------- src/amdb/main/web_api/__main__.py | 29 +- src/amdb/main/web_api/app.py | 15 +- src/amdb/main/web_api/config.py | 63 +--- src/amdb/main/web_api/di.py | 53 ++-- src/amdb/presentation/cli/movie.py | 103 +----- src/amdb/presentation/handler_factory.py | 66 +--- .../web_api/dependencies/identity_provider.py | 27 +- .../web_api/exception_handlers.py | 6 +- .../web_api/routers/auth/login.py | 13 +- .../web_api/routers/auth/register.py | 13 +- .../web_api/routers/movies/get_movie.py | 34 -- .../web_api/routers/movies/get_movies.py | 55 +++- .../web_api/routers/movies/router.py | 13 +- .../routers/ratings/get_movie_ratings.py | 43 --- .../web_api/routers/ratings/get_my_ratings.py | 39 --- .../web_api/routers/ratings/get_rating.py | 35 --- .../web_api/routers/ratings/rate_movie.py | 7 +- .../web_api/routers/ratings/router.py | 26 -- .../web_api/routers/ratings/unrate_movie.py | 7 +- .../routers/reviews/get_movie_reviews.py | 43 --- .../web_api/routers/reviews/get_my_reviews.py | 35 --- .../web_api/routers/reviews/get_review.py | 34 -- .../web_api/routers/reviews/get_reviews.py | 28 ++ .../web_api/routers/reviews/review_movie.py | 7 +- .../web_api/routers/reviews/router.py | 24 +- .../command_handlers/test_create_movie.py | 2 +- .../command_handlers/test_delete_movie.py | 2 +- .../command_handlers/test_rate_movie.py | 8 +- .../command_handlers/test_review_movie.py | 6 +- .../command_handlers/test_unrate_movie.py | 6 +- tests/unit/application/conftest.py | 110 ++++--- .../query_handlers/test_detailed_movie.py | 80 ++--- .../query_handlers/test_get_reviews.py | 121 +++++++ .../test_non_detailed_movies.py | 70 ++--- tests/unit/conftest.py | 41 +-- .../infrastructure/alembic/test_stairway.py | 7 +- 113 files changed, 1477 insertions(+), 1959 deletions(-) delete mode 100644 .env.template create mode 100644 config/prod_config.template.toml create mode 100644 config/test_config.template.toml rename src/amdb/application/common/readers/{movie.py => detailed_movie.py} (51%) create mode 100644 src/amdb/application/common/readers/non_detailed_movie.py delete mode 100644 src/amdb/infrastructure/auth/session/constants/exceptions.py rename src/amdb/infrastructure/auth/session/{model.py => session.py} (100%) create mode 100644 src/amdb/infrastructure/auth/session/session_gateway.py create mode 100644 src/amdb/infrastructure/password_manager/hash_computer.py rename src/amdb/infrastructure/password_manager/{model.py => password_hash.py} (55%) create mode 100644 src/amdb/infrastructure/password_manager/password_hash_gateway.py rename src/amdb/infrastructure/{auth/session/constants => persistence/redis/mappers}/__init__.py (100%) rename src/amdb/infrastructure/persistence/redis/{gateways => mappers}/permissions.py (90%) rename src/amdb/infrastructure/persistence/redis/{gateways => mappers}/session.py (83%) delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py rename src/amdb/infrastructure/persistence/{redis/gateways => sqlalchemy/mappers/entities}/__init__.py (100%) create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/password_hash.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py delete mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/user_password_hash.py rename src/amdb/infrastructure/persistence/sqlalchemy/{gateways => mappers/view_models}/__init__.py (100%) create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py rename src/amdb/infrastructure/persistence/sqlalchemy/models/{user_password_hash.py => password_hash.py} (91%) delete mode 100644 src/amdb/infrastructure/security/__init__.py delete mode 100644 src/amdb/infrastructure/security/hasher.py delete mode 100644 src/amdb/main/cli/di.py delete mode 100644 src/amdb/main/config.py delete mode 100644 src/amdb/presentation/web_api/routers/movies/get_movie.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/get_rating.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/get_review.py create mode 100644 src/amdb/presentation/web_api/routers/reviews/get_reviews.py create mode 100644 tests/unit/application/query_handlers/test_get_reviews.py diff --git a/.env.template b/.env.template deleted file mode 100644 index 719e826..0000000 --- a/.env.template +++ /dev/null @@ -1,28 +0,0 @@ -TEST_POSTGRES_USER= -TEST_POSTGRES_PASSWORD= -TEST_POSTGRES_DB= -TEST_POSTGRES_HOST= -TEST_POSTGRES_PORT= - -TEST_REDIS_HOST= -TEST_REDIS_PORT= -TEST_REDIS_DB= -TEST_REDIS_PASSWORD= - -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_DB= -POSTGRES_HOST= -POSTGRES_PORT= - -REDIS_HOST= -REDIS_PORT= -REDIS_DB= -REDIS_PASSWORD= - -FASTAPI_VERSION= - -UVICORN_HOST= -UVICORN_PORT= - -SESSION_LIFETIME= # Minutes diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7832c5..e457d2a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,4 +17,6 @@ repos: hooks: - id: mypy files: ^src/ - additional_dependencies: [types-redis] + additional_dependencies: + - types-redis + - types-toml diff --git a/config/prod_config.template.toml b/config/prod_config.template.toml new file mode 100644 index 0000000..bc037b7 --- /dev/null +++ b/config/prod_config.template.toml @@ -0,0 +1,13 @@ +[postgres] +url = "postgresql://postgres:1234@127.0.0.1:5432/amdb" + +[redis] +url = "redis://:1234@127.0.0.1:6379/0" + +[auth-session] +lifetime = 3600 # Minutes + +[web-api] +version = "0.5.0" +host = "127.0.0.1" +port = 8000 diff --git a/config/test_config.template.toml b/config/test_config.template.toml new file mode 100644 index 0000000..e662e37 --- /dev/null +++ b/config/test_config.template.toml @@ -0,0 +1,5 @@ +[postgres] +url = "postgresql://postgres:1234@127.0.0.1:5433/amdb_test" + +[redis] +url = "redis://:1234@127.0.0.1:6378/1" diff --git a/docker-compose.yaml b/docker-compose.yaml index a9ff65e..a12f994 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -13,23 +13,7 @@ services: dockerfile: ./Dockerfile target: web_api environment: - - POSTGRES_USER - - POSTGRES_PASSWORD - - POSTGRES_DB - - POSTGRES_HOST=postgres - - POSTGRES_PORT - - - FASTAPI_VERSION - - - UVICORN_HOST=0.0.0.0 - - UVICORN_PORT - - - REDIS_HOST=redis - - REDIS_PORT - - REDIS_DB - - REDIS_PASSWORD - - - SESSION_LIFETIME + - CONFIG_PATH postgres: profiles: [web_api] diff --git a/pyproject.toml b/pyproject.toml index 1c0694a..8167880 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,10 +25,11 @@ maintainers = [ ] dependencies = [ "uuid7", - "sqlalchemy==2.0.23", - "psycopg2-binary==2.9.9", - "alembic==1.13.0", - "redis>=5.0.0", + "toml==0.10.*", + "sqlalchemy==2.0.*", + "psycopg2-binary==2.9.*", + "alembic==1.13.*", + "redis==5.0.*", ] [project.optional-dependencies] diff --git a/src/amdb/application/command_handlers/create_movie.py b/src/amdb/application/command_handlers/create_movie.py index ff40f65..76d503f 100644 --- a/src/amdb/application/command_handlers/create_movie.py +++ b/src/amdb/application/command_handlers/create_movie.py @@ -33,7 +33,7 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: CreateMovieCommand) -> MovieId: - current_permissions = self._identity_provider.get_permissions() + current_permissions = self._identity_provider.permissions() required_permissions = self._permissions_gateway.for_create_movie() access = self._access_concern.authorize( current_permissions=current_permissions, diff --git a/src/amdb/application/command_handlers/delete_movie.py b/src/amdb/application/command_handlers/delete_movie.py index 8b7ab42..e61e465 100644 --- a/src/amdb/application/command_handlers/delete_movie.py +++ b/src/amdb/application/command_handlers/delete_movie.py @@ -36,7 +36,7 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: DeleteMovieCommand) -> None: - current_permissions = self._identity_provider.get_permissions() + current_permissions = self._identity_provider.permissions() required_permissions = self._permissions_gateway.for_delete_movie() access = self._access_concern.authorize( current_permissions=current_permissions, diff --git a/src/amdb/application/command_handlers/rate_movie.py b/src/amdb/application/command_handlers/rate_movie.py index f576ad2..a01142c 100644 --- a/src/amdb/application/command_handlers/rate_movie.py +++ b/src/amdb/application/command_handlers/rate_movie.py @@ -45,7 +45,7 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: RateMovieCommand) -> RatingId: - current_permissions = self._identity_provider.get_permissions() + current_permissions = self._identity_provider.permissions() required_permissions = self._permissions_gateway.for_rate_movie() access = self._access_concern.authorize( current_permissions=current_permissions, @@ -58,7 +58,7 @@ def execute(self, command: RateMovieCommand) -> RatingId: if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - current_user_id = self._identity_provider.get_user_id() + current_user_id = self._identity_provider.user_id() rating = self._rating_gateway.with_user_id_and_movie_id( user_id=current_user_id, diff --git a/src/amdb/application/command_handlers/review_movie.py b/src/amdb/application/command_handlers/review_movie.py index be4609c..7f0ae3d 100644 --- a/src/amdb/application/command_handlers/review_movie.py +++ b/src/amdb/application/command_handlers/review_movie.py @@ -45,7 +45,7 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: ReviewMovieCommand) -> ReviewId: - current_permissions = self._identity_provider.get_permissions() + current_permissions = self._identity_provider.permissions() required_permissions = self._permissions_gateway.for_review_movie() access = self._access_concern.authorize( current_permissions=current_permissions, @@ -58,7 +58,7 @@ def execute(self, command: ReviewMovieCommand) -> ReviewId: if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - current_user_id = self._identity_provider.get_user_id() + current_user_id = self._identity_provider.user_id() review = self._review_gateway.with_movie_id_and_user_id( user_id=current_user_id, diff --git a/src/amdb/application/command_handlers/unrate_movie.py b/src/amdb/application/command_handlers/unrate_movie.py index c766e50..e32c577 100644 --- a/src/amdb/application/command_handlers/unrate_movie.py +++ b/src/amdb/application/command_handlers/unrate_movie.py @@ -40,7 +40,7 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: UnrateMovieCommand) -> None: - current_permissions = self._identity_provider.get_permissions() + current_permissions = self._identity_provider.permissions() required_permissions = self._permissions_gateway.for_unrate_movie() access = self._access_concern.authorize( current_permissions=current_permissions, @@ -53,7 +53,7 @@ def execute(self, command: UnrateMovieCommand) -> None: if not rating: raise ApplicationError(RATING_DOES_NOT_EXIST) - current_user_id = self._identity_provider.get_user_id() + current_user_id = self._identity_provider.user_id() if current_user_id != rating.user_id: raise ApplicationError(USER_IS_NOT_OWNER) diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index 19a3df2..68aacdd 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -1,13 +1,8 @@ LOGIN_ACCESS_DENIED = "Access to login in denied" -GET_MOVIES_ACCESS_DENIED = "Access to getting movies is denied" -GET_MOVIE_ACCESS_DENIED = "Access to getting movie is denied" CREATE_MOVIE_ACCESS_DENIED = "Access to movie creation is denied" DELETE_MOVIE_ACCESS_DENIED = "Access to movie deletion is denied" -GET_RATING_ACCESS_DENIED = "Access to getting rating is denied" RATE_MOVIE_ACCESS_DENIED = "Access to movie rating is denied" UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied" -GET_REVIEWS_ACCESS_DENIED = "Access to getting movie reviews is denied" -GET_REVIEW_ACCESS_DENIED = "Access to getting review is denied" REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied" USER_IS_NOT_OWNER = "User is not owner" diff --git a/src/amdb/application/common/gateways/movie.py b/src/amdb/application/common/gateways/movie.py index 0c74df9..3bad7bd 100644 --- a/src/amdb/application/common/gateways/movie.py +++ b/src/amdb/application/common/gateways/movie.py @@ -15,6 +15,3 @@ def update(self, movie: Movie) -> None: def delete(self, movie: Movie) -> None: raise NotImplementedError - - def list(self, limit: int, offset: int) -> list[Movie]: - raise NotImplementedError diff --git a/src/amdb/application/common/gateways/permissions.py b/src/amdb/application/common/gateways/permissions.py index 94b29b2..501c912 100644 --- a/src/amdb/application/common/gateways/permissions.py +++ b/src/amdb/application/common/gateways/permissions.py @@ -16,12 +16,6 @@ def for_new_user(self) -> int: def for_login(self) -> int: raise NotImplementedError - def for_get_movies(self) -> int: - raise NotImplementedError - - def for_get_movie(self) -> int: - raise NotImplementedError - def for_create_movie(self) -> int: raise NotImplementedError @@ -34,8 +28,5 @@ def for_rate_movie(self) -> int: def for_unrate_movie(self) -> int: raise NotImplementedError - def for_get_reviews(self) -> int: - raise NotImplementedError - def for_review_movie(self) -> int: raise NotImplementedError diff --git a/src/amdb/application/common/gateways/rating.py b/src/amdb/application/common/gateways/rating.py index 9c5c3b1..af29b70 100644 --- a/src/amdb/application/common/gateways/rating.py +++ b/src/amdb/application/common/gateways/rating.py @@ -16,22 +16,6 @@ def with_user_id_and_movie_id( ) -> Optional[Rating]: raise NotImplementedError - def list_with_movie_id( - self, - movie_id: MovieId, - limit: int, - offset: int, - ) -> list[Rating]: - raise NotImplementedError - - def list_with_user_id( - self, - user_id: UserId, - limit: int, - offset: int, - ) -> list[Rating]: - raise NotImplementedError - def save(self, rating: Rating) -> None: raise NotImplementedError diff --git a/src/amdb/application/common/gateways/review.py b/src/amdb/application/common/gateways/review.py index 37f842f..ab2b5ca 100644 --- a/src/amdb/application/common/gateways/review.py +++ b/src/amdb/application/common/gateways/review.py @@ -16,22 +16,6 @@ def with_movie_id_and_user_id( ) -> Optional[Review]: raise NotImplementedError - def list_with_movie_id( - self, - movie_id: MovieId, - limit: int, - offset: int, - ) -> list[Review]: - raise NotImplementedError - - def list_with_user_id( - self, - user_id: UserId, - limit: int, - offset: int, - ) -> list[Review]: - raise NotImplementedError - def save(self, review: Review) -> None: raise NotImplementedError diff --git a/src/amdb/application/common/identity_provider.py b/src/amdb/application/common/identity_provider.py index 915e505..769cd06 100644 --- a/src/amdb/application/common/identity_provider.py +++ b/src/amdb/application/common/identity_provider.py @@ -1,11 +1,14 @@ -from typing import Protocol +from typing import Protocol, Optional from amdb.domain.entities.user import UserId class IdentityProvider(Protocol): - def get_user_id(self) -> UserId: + def user_id(self) -> UserId: raise NotImplementedError - def get_permissions(self) -> int: + def user_id_or_none(self) -> Optional[UserId]: + raise NotImplementedError + + def permissions(self) -> int: raise NotImplementedError diff --git a/src/amdb/application/common/readers/movie.py b/src/amdb/application/common/readers/detailed_movie.py similarity index 51% rename from src/amdb/application/common/readers/movie.py rename to src/amdb/application/common/readers/detailed_movie.py index 8a3667b..9cff8e4 100644 --- a/src/amdb/application/common/readers/movie.py +++ b/src/amdb/application/common/readers/detailed_movie.py @@ -5,21 +5,10 @@ from amdb.application.common.view_models.detailed_movie import ( DetailedMovieViewModel, ) -from amdb.application.common.view_models.non_detailed_movie import ( - NonDetailedMovieViewModel, -) - -class MovieViewModelReader(Protocol): - def list_non_detailed( - self, - current_user_id: Optional[UserId], - limit: int, - offset: int, - ) -> list[NonDetailedMovieViewModel]: - raise NotImplementedError - def detailed( +class DetailedMovieViewModelReader(Protocol): + def one( self, movie_id: MovieId, current_user_id: Optional[UserId], diff --git a/src/amdb/application/common/readers/non_detailed_movie.py b/src/amdb/application/common/readers/non_detailed_movie.py new file mode 100644 index 0000000..f799f3a --- /dev/null +++ b/src/amdb/application/common/readers/non_detailed_movie.py @@ -0,0 +1,16 @@ +from typing import Protocol, Optional + +from amdb.domain.entities.user import UserId +from amdb.application.common.view_models.non_detailed_movie import ( + NonDetailedMovieViewModel, +) + + +class NonDetailedMovieViewModelReader(Protocol): + def list( + self, + current_user_id: Optional[UserId], + limit: int, + offset: int, + ) -> list[NonDetailedMovieViewModel]: + raise NotImplementedError diff --git a/src/amdb/application/common/view_models/review.py b/src/amdb/application/common/view_models/review.py index be4e8e3..8091a8b 100644 --- a/src/amdb/application/common/view_models/review.py +++ b/src/amdb/application/common/view_models/review.py @@ -1,23 +1,20 @@ -__all__ = ("ReviewViewModel",) - from datetime import datetime from typing import Optional from typing_extensions import TypedDict from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId from amdb.domain.entities.review import ReviewId, ReviewType -class Rating(TypedDict): +class UserRating(TypedDict): id: RatingId value: float created_at: datetime -class Review(TypedDict): +class UserReview(TypedDict): id: ReviewId title: str content: str @@ -27,6 +24,5 @@ class Review(TypedDict): class ReviewViewModel(TypedDict): user_id: UserId - movie_id: MovieId - review: Review - rating: Optional[Rating] + user_review: UserReview + user_rating: Optional[UserRating] diff --git a/src/amdb/application/query_handlers/detailed_movie.py b/src/amdb/application/query_handlers/detailed_movie.py index b001775..fc139ca 100644 --- a/src/amdb/application/query_handlers/detailed_movie.py +++ b/src/amdb/application/query_handlers/detailed_movie.py @@ -1,13 +1,12 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway -from amdb.application.common.readers.movie import MovieViewModelReader -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, +from amdb.application.common.readers.detailed_movie import ( + DetailedMovieViewModelReader, ) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError -from amdb.application.common.view_models.detailed_movie import DetailedMovieViewModel +from amdb.application.common.view_models.detailed_movie import ( + DetailedMovieViewModel, +) from amdb.application.queries.detailed_movie import GetDetailedMovieQuery @@ -15,29 +14,16 @@ class GetDetailedMovieHandler: def __init__( self, *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, + detailed_movie_reader: DetailedMovieViewModelReader, identity_provider: IdentityProvider, ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._movie_view_model_reader = movie_view_model_reader + self._detailed_movie_reader = detailed_movie_reader self._identity_provider = identity_provider def execute(self, query: GetDetailedMovieQuery) -> DetailedMovieViewModel: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movie() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MOVIE_ACCESS_DENIED) - - current_user_id = self._identity_provider.get_user_id() + current_user_id = self._identity_provider.user_id_or_none() - detailed_movie_view_model = self._movie_view_model_reader.detailed( + detailed_movie_view_model = self._detailed_movie_reader.one( movie_id=query.movie_id, current_user_id=current_user_id, ) diff --git a/src/amdb/application/query_handlers/non_detailed_movies.py b/src/amdb/application/query_handlers/non_detailed_movies.py index 8d7c58d..d525d1e 100644 --- a/src/amdb/application/query_handlers/non_detailed_movies.py +++ b/src/amdb/application/query_handlers/non_detailed_movies.py @@ -1,40 +1,32 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway -from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.readers.non_detailed_movie import ( + NonDetailedMovieViewModelReader, +) from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import GET_MOVIES_ACCESS_DENIED -from amdb.application.common.exception import ApplicationError -from amdb.application.common.view_models.non_detailed_movie import NonDetailedMovieViewModel -from amdb.application.queries.non_detailed_movies import GetNonDetailedMoviesQuery +from amdb.application.common.view_models.non_detailed_movie import ( + NonDetailedMovieViewModel, +) +from amdb.application.queries.non_detailed_movies import ( + GetNonDetailedMoviesQuery, +) class GetNonDetailedMoviesHandler: def __init__( self, *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, + non_detailed_movie_reader: NonDetailedMovieViewModelReader, identity_provider: IdentityProvider, ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway - self._movie_view_model_reader = movie_view_model_reader + self._non_detailed_movie_reader = non_detailed_movie_reader self._identity_provider = identity_provider - def execute(self, query: GetNonDetailedMoviesQuery) -> list[NonDetailedMovieViewModel]: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_movies() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_MOVIES_ACCESS_DENIED) - - current_user_id = self._identity_provider.get_user_id() + def execute( + self, + query: GetNonDetailedMoviesQuery, + ) -> list[NonDetailedMovieViewModel]: + current_user_id = self._identity_provider.user_id_or_none() - non_detailed_movie_models = self._movie_view_model_reader.list_non_detailed( + non_detailed_movie_models = self._non_detailed_movie_reader.list( current_user_id=current_user_id, limit=query.limit, offset=query.offset, diff --git a/src/amdb/application/query_handlers/reviews.py b/src/amdb/application/query_handlers/reviews.py index 6c92913..736add7 100644 --- a/src/amdb/application/query_handlers/reviews.py +++ b/src/amdb/application/query_handlers/reviews.py @@ -1,13 +1,7 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.readers.review import ReviewViewModelReader -from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.review import ReviewViewModel -from amdb.application.common.constants.exceptions import ( - GET_REVIEWS_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError from amdb.application.queries.reviews import GetReviewsQuery @@ -16,28 +10,13 @@ class GetReviewsHandler: def __init__( self, *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, review_view_model_reader: ReviewViewModelReader, - identity_provider: IdentityProvider, ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway self._movie_gateway = movie_gateway self._review_view_model_reader = review_view_model_reader - self._identity_provider = identity_provider def execute(self, query: GetReviewsQuery) -> list[ReviewViewModel]: - current_permissions = self._identity_provider.get_permissions() - required_permissions = self._permissions_gateway.for_get_reviews() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(GET_REVIEWS_ACCESS_DENIED) - movie = self._movie_gateway.with_id(query.movie_id) if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) diff --git a/src/amdb/domain/services/access_concern.py b/src/amdb/domain/services/access_concern.py index 29ad163..3405fce 100644 --- a/src/amdb/domain/services/access_concern.py +++ b/src/amdb/domain/services/access_concern.py @@ -1,6 +1,8 @@ class AccessConcern: def authorize( - self, current_permissions: int, required_permissions: int + self, + current_permissions: int, + required_permissions: int, ) -> bool: return ( current_permissions & required_permissions == required_permissions diff --git a/src/amdb/infrastructure/auth/raw/identity_provider.py b/src/amdb/infrastructure/auth/raw/identity_provider.py index 1032843..ec0556d 100644 --- a/src/amdb/infrastructure/auth/raw/identity_provider.py +++ b/src/amdb/infrastructure/auth/raw/identity_provider.py @@ -1,3 +1,5 @@ +from typing import Optional + from amdb.domain.entities.user import UserId @@ -6,8 +8,11 @@ def __init__(self, user_id: UserId, permissions: int) -> None: self._user_id = user_id self._permissions = permissions - def get_user_id(self) -> UserId: + def user_id(self) -> UserId: + return self._user_id + + def user_id_or_none(self) -> Optional[UserId]: return self._user_id - def get_permissions(self) -> int: + def permissions(self) -> int: return self._permissions diff --git a/src/amdb/infrastructure/auth/session/config.py b/src/amdb/infrastructure/auth/session/config.py index 016398e..8100d97 100644 --- a/src/amdb/infrastructure/auth/session/config.py +++ b/src/amdb/infrastructure/auth/session/config.py @@ -1,7 +1,19 @@ from dataclasses import dataclass from datetime import timedelta +from typing import Union +from os import PathLike + +import toml @dataclass(frozen=True, slots=True) class SessionConfig: - session_lifetime: timedelta + lifetime: timedelta + + @classmethod + def from_toml(cls, path: Union[PathLike, str]) -> "SessionConfig": + toml_as_dict = toml.load(path) + session_section_as_dict = toml_as_dict["auth-session"] + return SessionConfig( + lifetime=session_section_as_dict["lifetime"], + ) diff --git a/src/amdb/infrastructure/auth/session/constants/exceptions.py b/src/amdb/infrastructure/auth/session/constants/exceptions.py deleted file mode 100644 index b2d0e7a..0000000 --- a/src/amdb/infrastructure/auth/session/constants/exceptions.py +++ /dev/null @@ -1,2 +0,0 @@ -NO_SESSION_ID = "Session id is not passed" -SESSION_DOES_NOT_EXIST = "Session doesn't exist" diff --git a/src/amdb/infrastructure/auth/session/identity_provider.py b/src/amdb/infrastructure/auth/session/identity_provider.py index 5c3eea8..0ab7ea3 100644 --- a/src/amdb/infrastructure/auth/session/identity_provider.py +++ b/src/amdb/infrastructure/auth/session/identity_provider.py @@ -1,15 +1,14 @@ from typing import Optional, cast from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.redis.gateways.session import ( - RedisSessionGateway, -) -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, -) +from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.infrastructure.exception import InfrastructureError -from .constants.exceptions import NO_SESSION_ID, SESSION_DOES_NOT_EXIST -from .model import SessionId +from .session import SessionId, Session +from .session_gateway import SessionGateway + + +NO_SESSION_ID = "Session id is not passed" +SESSION_DOES_NOT_EXIST = "Session doesn't exist" class SessionIdentityProvider: @@ -17,24 +16,29 @@ def __init__( self, *, session_id: Optional[SessionId], - session_gateway: RedisSessionGateway, - permissions_gateway: RedisPermissionsGateway, + session_gateway: SessionGateway, + permissions_gateway: PermissionsGateway, ) -> None: self._session_id = session_id self._session_gateway = session_gateway self._permissions_gateway = permissions_gateway - def get_user_id(self) -> UserId: - return self._get_user_id() + def user_id(self) -> UserId: + return self._session().user_id + + def user_id_or_none(self) -> Optional[UserId]: + session = self._session_or_none() + return session.user_id if session else None - def get_permissions(self) -> int: - user_id = self._get_user_id() - permissions = self._permissions_gateway.with_user_id(user_id) + def permissions(self) -> int: + session = self._session() + + permissions = self._permissions_gateway.with_user_id(session.user_id) permissions = cast(int, permissions) return permissions - def _get_user_id(self) -> UserId: + def _session(self) -> Session: if not self._session_id: raise InfrastructureError(NO_SESSION_ID) @@ -42,4 +46,9 @@ def _get_user_id(self) -> UserId: if not session: raise InfrastructureError(SESSION_DOES_NOT_EXIST) - return session.user_id + return session + + def _session_or_none(self) -> Optional[Session]: + if self._session_id: + return self._session_gateway.with_id(self._session_id) + return None diff --git a/src/amdb/infrastructure/auth/session/model.py b/src/amdb/infrastructure/auth/session/session.py similarity index 100% rename from src/amdb/infrastructure/auth/session/model.py rename to src/amdb/infrastructure/auth/session/session.py diff --git a/src/amdb/infrastructure/auth/session/session_gateway.py b/src/amdb/infrastructure/auth/session/session_gateway.py new file mode 100644 index 0000000..979ba31 --- /dev/null +++ b/src/amdb/infrastructure/auth/session/session_gateway.py @@ -0,0 +1,8 @@ +from typing import Optional, Protocol + +from .session import SessionId, Session + + +class SessionGateway(Protocol): + def with_id(self, session_id: SessionId) -> Optional[Session]: + raise NotImplementedError diff --git a/src/amdb/infrastructure/auth/session/session_processor.py b/src/amdb/infrastructure/auth/session/session_processor.py index 25da7c0..3890c10 100644 --- a/src/amdb/infrastructure/auth/session/session_processor.py +++ b/src/amdb/infrastructure/auth/session/session_processor.py @@ -1,13 +1,16 @@ from uuid import uuid4 from amdb.domain.entities.user import UserId -from .model import SessionId, Session +from .session import SessionId, Session class SessionProcessor: def create(self, user_id: UserId) -> Session: - session_id = uuid4().hex + uuid4().hex return Session( - id=SessionId(session_id), + id=self._gen_session_id(), user_id=user_id, ) + + def _gen_session_id(self) -> SessionId: + random_value = uuid4().hex + uuid4().hex + uuid4().hex + return SessionId(random_value) diff --git a/src/amdb/infrastructure/password_manager/hash_computer.py b/src/amdb/infrastructure/password_manager/hash_computer.py new file mode 100644 index 0000000..a0b6881 --- /dev/null +++ b/src/amdb/infrastructure/password_manager/hash_computer.py @@ -0,0 +1,28 @@ +import hashlib + + +class HashComputer: + _ALGORITHM = "sha256" + _ITERATIONS = 10000 + + def hash(self, value: bytes, salt: bytes) -> bytes: + return hashlib.pbkdf2_hmac( + hash_name=self._ALGORITHM, + password=value, + salt=salt, + iterations=self._ITERATIONS, + ) + + def verify( + self, + value: bytes, + hashed_value: bytes, + salt: bytes, + ) -> bool: + computed_hash = hashlib.pbkdf2_hmac( + hash_name=self._ALGORITHM, + password=value, + salt=salt, + iterations=self._ITERATIONS, + ) + return computed_hash == hashed_value diff --git a/src/amdb/infrastructure/password_manager/model.py b/src/amdb/infrastructure/password_manager/password_hash.py similarity index 55% rename from src/amdb/infrastructure/password_manager/model.py rename to src/amdb/infrastructure/password_manager/password_hash.py index 3e982ef..3cfc0ac 100644 --- a/src/amdb/infrastructure/password_manager/model.py +++ b/src/amdb/infrastructure/password_manager/password_hash.py @@ -1,10 +1,10 @@ from dataclasses import dataclass from amdb.domain.entities.user import UserId -from amdb.infrastructure.security.hasher import HashData @dataclass(frozen=True, slots=True) -class UserPasswordHash: +class PasswordHash: user_id: UserId - password_hash: HashData + hash: bytes + salt: bytes diff --git a/src/amdb/infrastructure/password_manager/password_hash_gateway.py b/src/amdb/infrastructure/password_manager/password_hash_gateway.py new file mode 100644 index 0000000..7cbe02c --- /dev/null +++ b/src/amdb/infrastructure/password_manager/password_hash_gateway.py @@ -0,0 +1,12 @@ +from typing import Protocol, Optional + +from amdb.domain.entities.user import UserId +from amdb.infrastructure.password_manager.password_hash import PasswordHash + + +class PasswordHashGateway(Protocol): + def with_user_id(self, user_id: UserId) -> Optional[PasswordHash]: + raise NotImplementedError + + def save(self, password_hash: PasswordHash) -> None: + raise NotImplementedError diff --git a/src/amdb/infrastructure/password_manager/password_manager.py b/src/amdb/infrastructure/password_manager/password_manager.py index 254471f..e324f67 100644 --- a/src/amdb/infrastructure/password_manager/password_manager.py +++ b/src/amdb/infrastructure/password_manager/password_manager.py @@ -1,30 +1,47 @@ -from typing import cast +import os from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.sqlalchemy.gateways.user_password_hash import ( - SQLAlchemyUserPasswordHashGateway, -) -from amdb.infrastructure.security.hasher import Hasher -from .model import UserPasswordHash +from amdb.infrastructure.exception import InfrastructureError +from .password_hash import PasswordHash +from .password_hash_gateway import PasswordHashGateway +from .hash_computer import HashComputer + + +PASSWORD_HASH_DOES_NOT_EXIST = "Password hash doesn't exist" class HashingPasswordManager: def __init__( self, - hasher: Hasher, - user_password_hash_gateway: SQLAlchemyUserPasswordHashGateway, + hash_computer: HashComputer, + password_hash_gateway: PasswordHashGateway, ) -> None: - self._hasher = hasher - self._user_password_hash_gateway = user_password_hash_gateway + self._hash_computer = hash_computer + self._password_hash_gateway = password_hash_gateway def set(self, user_id: UserId, password: str) -> None: - password_hash = self._hasher.hash(password.encode()) - user_password_hash = UserPasswordHash(user_id, password_hash) - self._user_password_hash_gateway.save(user_password_hash) + salt = self._gen_random_bytes() + hash = self._hash_computer.hash( + value=password.encode(), + salt=salt, + ) + password_hash = PasswordHash( + user_id=user_id, + hash=hash, + salt=salt, + ) + self._password_hash_gateway.save(password_hash) def verify(self, user_id: UserId, password: str) -> bool: - user_password_hash = self._user_password_hash_gateway.get(user_id) - user_password_hash = cast(UserPasswordHash, user_password_hash) - return self._hasher.verify( - password.encode(), user_password_hash.password_hash + password_hash = self._password_hash_gateway.with_user_id(user_id) + if not password_hash: + raise InfrastructureError(PASSWORD_HASH_DOES_NOT_EXIST) + + return self._hash_computer.verify( + value=password.encode(), + hashed_value=password_hash.hash, + salt=password_hash.salt, ) + + def _gen_random_bytes(self) -> bytes: + return os.urandom(32) diff --git a/src/amdb/infrastructure/persistence/alembic/config.py b/src/amdb/infrastructure/persistence/alembic/config.py index 5d7f07d..ea4e137 100644 --- a/src/amdb/infrastructure/persistence/alembic/config.py +++ b/src/amdb/infrastructure/persistence/alembic/config.py @@ -5,6 +5,6 @@ ALEMBIC_CONFIG = str( importlib.resources.files( - amdb.infrastructure.persistence.alembic + amdb.infrastructure.persistence.alembic, ).joinpath("alembic.ini"), ) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py index fa79316..11fb0b3 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py @@ -102,15 +102,18 @@ def upgrade() -> None: ) with op.batch_alter_table("ratings") as batch_op: batch_op.add_column( - sa.Column("id", sa.Uuid(), nullable=False, default="uuid7()") + sa.Column("id", sa.Uuid(), nullable=False, default="uuid7()"), ) batch_op.drop_constraint("pk_ratings", type_="primary") batch_op.create_primary_key("pk_ratings", ["id"]) batch_op.create_unique_constraint( - "uq_ratings", ("user_id", "movie_id") + "uq_ratings", + ("user_id", "movie_id"), ) op.create_unique_constraint( - "uq_reviews", "reviews", ("user_id", "movie_id") + "uq_reviews", + "reviews", + ("user_id", "movie_id"), ) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py index 853078e..77d5d02 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py @@ -36,7 +36,9 @@ def upgrade() -> None: sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.ForeignKeyConstraint( - ["movie_id"], ["movies.id"], ondelete="CASCADE" + ["movie_id"], + ["movies.id"], + ondelete="CASCADE", ), sa.PrimaryKeyConstraint("id"), ) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py index 22b25a5..be2a42b 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/9e92de201574_.py @@ -50,7 +50,9 @@ def upgrade() -> None: sa.Column("value", sa.Float(), nullable=False), sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=True), sa.ForeignKeyConstraint( - ["movie_id"], ["movies.id"], ondelete="CASCADE" + ["movie_id"], + ["movies.id"], + ondelete="CASCADE", ), sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("movie_id", "user_id"), diff --git a/src/amdb/infrastructure/persistence/redis/config.py b/src/amdb/infrastructure/persistence/redis/config.py index c623ebe..3a4abc8 100644 --- a/src/amdb/infrastructure/persistence/redis/config.py +++ b/src/amdb/infrastructure/persistence/redis/config.py @@ -1,9 +1,16 @@ from dataclasses import dataclass +from typing import Union +from os import PathLike + +import toml @dataclass(frozen=True, slots=True) class RedisConfig: - host: str - port: int - db: int - password: str + url: str + + @classmethod + def from_toml(cls, path: Union[PathLike, str]) -> "RedisConfig": + toml_as_dict = toml.load(path) + redis_section_as_dict = toml_as_dict["redis"] + return RedisConfig(url=redis_section_as_dict["url"]) diff --git a/src/amdb/infrastructure/auth/session/constants/__init__.py b/src/amdb/infrastructure/persistence/redis/mappers/__init__.py similarity index 100% rename from src/amdb/infrastructure/auth/session/constants/__init__.py rename to src/amdb/infrastructure/persistence/redis/mappers/__init__.py diff --git a/src/amdb/infrastructure/persistence/redis/gateways/permissions.py b/src/amdb/infrastructure/persistence/redis/mappers/permissions.py similarity index 90% rename from src/amdb/infrastructure/persistence/redis/gateways/permissions.py rename to src/amdb/infrastructure/persistence/redis/mappers/permissions.py index 35fcaf8..e5554bd 100644 --- a/src/amdb/infrastructure/persistence/redis/gateways/permissions.py +++ b/src/amdb/infrastructure/persistence/redis/mappers/permissions.py @@ -1,18 +1,18 @@ -from typing import Optional +from typing import Optional, cast from redis.client import Redis from amdb.domain.entities.user import UserId -class RedisPermissionsGateway: +class PermissionsMapper: def __init__(self, redis: Redis) -> None: self._redis = redis def with_user_id(self, user_id: UserId) -> Optional[int]: permissions = self._redis.get(f"permissions:user_id:{user_id.hex}") if permissions: - return int(permissions) + return int(cast(str, permissions)) return None def set(self, user_id: UserId, permissions: int) -> None: @@ -54,7 +54,7 @@ def for_rate_movie(self) -> int: def for_unrate_movie(self) -> int: return 4 - def for_get_movie_reviews(self) -> int: + def for_get_reviews(self) -> int: return 4 def for_get_my_reviews(self) -> int: diff --git a/src/amdb/infrastructure/persistence/redis/gateways/session.py b/src/amdb/infrastructure/persistence/redis/mappers/session.py similarity index 83% rename from src/amdb/infrastructure/persistence/redis/gateways/session.py rename to src/amdb/infrastructure/persistence/redis/mappers/session.py index 40dbd4f..72b0714 100644 --- a/src/amdb/infrastructure/persistence/redis/gateways/session.py +++ b/src/amdb/infrastructure/persistence/redis/mappers/session.py @@ -1,14 +1,14 @@ from datetime import timedelta -from typing import Optional +from typing import Optional, cast from uuid import UUID from redis import Redis from amdb.domain.entities.user import UserId -from amdb.infrastructure.auth.session.model import SessionId, Session +from amdb.infrastructure.auth.session.session import SessionId, Session -class RedisSessionGateway: +class SessionMapper: def __init__( self, *, @@ -34,6 +34,6 @@ def with_id(self, session_id: SessionId) -> Optional[Session]: if user_id: return Session( id=session_id, - user_id=UserId(UUID(user_id)), + user_id=UserId(UUID(cast(str, user_id))), ) return None diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/config.py b/src/amdb/infrastructure/persistence/sqlalchemy/config.py index 7445a03..5a44813 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/config.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/config.py @@ -1,20 +1,16 @@ from dataclasses import dataclass +from typing import Union +from os import PathLike + +import toml @dataclass(frozen=True, slots=True) class PostgresConfig: - host: str - port: str - name: str - user: str - password: str + url: str - @property - def dsn(self) -> str: - return "postgresql://{}:{}@{}:{}/{}".format( - self.user, - self.password, - self.host, - self.port, - self.name, - ) + @classmethod + def from_toml(cls, path: Union[PathLike, str]) -> "PostgresConfig": + toml_as_dict = toml.load(path) + postgres_section_as_dict = toml_as_dict["postgres"] + return PostgresConfig(url=postgres_section_as_dict["url"]) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py deleted file mode 100644 index b30327c..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateway_factory.py +++ /dev/null @@ -1,49 +0,0 @@ -from contextlib import contextmanager -from typing import Iterator - -from sqlalchemy.orm import Session, sessionmaker - -from .gateways.user import SQLAlchemyUserGateway -from .gateways.movie import SQLAlchemyMovieGateway -from .gateways.rating import SQLAlchemyRatingGateway -from .gateways.user_password_hash import SQLAlchemyUserPasswordHashGateway -from .gateways.review import SQLAlchemyReviewGateway -from .mappers.user import UserMapper -from .mappers.movie import MovieMapper -from .mappers.rating import RatingMapper -from .mappers.user_password_hash import UserPasswordHashMapper -from .mappers.review import ReviewMapper - - -@contextmanager -def build_sqlalchemy_gateway_factory( - sessionmaker: sessionmaker[Session], -) -> Iterator["SQLAlchemyGatewayFactory"]: - session = sessionmaker() - yield SQLAlchemyGatewayFactory(session) - session.close() - - -class SQLAlchemyGatewayFactory: - def __init__(self, session: Session) -> None: - self._session = session - - def user(self) -> SQLAlchemyUserGateway: - return SQLAlchemyUserGateway(self._session, UserMapper()) - - def movie(self) -> SQLAlchemyMovieGateway: - return SQLAlchemyMovieGateway(self._session, MovieMapper()) - - def rating(self) -> SQLAlchemyRatingGateway: - return SQLAlchemyRatingGateway(self._session, RatingMapper()) - - def user_password_hash(self) -> SQLAlchemyUserPasswordHashGateway: - return SQLAlchemyUserPasswordHashGateway( - self._session, UserPasswordHashMapper() - ) - - def review(self) -> SQLAlchemyReviewGateway: - return SQLAlchemyReviewGateway(self._session, ReviewMapper()) - - def unit_of_work(self) -> Session: - return self._session diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py deleted file mode 100644 index b7e38a6..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/movie.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Optional - -from sqlalchemy import select -from sqlalchemy.orm.session import Session - -from amdb.domain.entities.movie import MovieId, Movie as MovieEntity -from amdb.infrastructure.persistence.sqlalchemy.models.movie import ( - Movie as MovieModel, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import ( - MovieMapper, -) - - -class SQLAlchemyMovieGateway: - def __init__( - self, - session: Session, - mapper: MovieMapper, - ) -> None: - self._session = session - self._mapper = mapper - - def with_id(self, movie_id: MovieId) -> Optional[MovieEntity]: - movie_model = self._session.get(MovieModel, movie_id) - if movie_model: - return self._mapper.to_entity(movie_model) - return None - - def save(self, movie: MovieEntity) -> None: - movie_model = self._mapper.to_model(movie) - self._session.add(movie_model) - - def update(self, movie: MovieEntity) -> None: - movie_model = self._mapper.to_model(movie) - self._session.merge(movie_model) - - def delete(self, movie: MovieEntity) -> None: - movie_model = self._session.get(MovieModel, movie.id) - if movie_model: - self._session.delete(movie_model) - - def list(self, limit: int, offset: int) -> list[MovieEntity]: - statement = select(MovieModel).limit(limit).offset(offset) - movie_models = self._session.scalars(statement) - - movie_entities = [] - for movie_model in movie_models: - movie_entity = self._mapper.to_entity(movie_model) - movie_entities.append(movie_entity) - - return movie_entities diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py deleted file mode 100644 index c20f165..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/rating.py +++ /dev/null @@ -1,101 +0,0 @@ -from typing import Optional - -from sqlalchemy import select, delete, and_ -from sqlalchemy.orm.session import Session - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.rating import RatingId, Rating as RatingEntity -from amdb.infrastructure.persistence.sqlalchemy.models.rating import ( - Rating as RatingModel, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import ( - RatingMapper, -) - - -class SQLAlchemyRatingGateway: - def __init__( - self, - session: Session, - mapper: RatingMapper, - ) -> None: - self._session = session - self._mapper = mapper - - def with_id(self, id: RatingId) -> Optional[RatingEntity]: - rating_model = self._session.get(RatingModel, id) - if rating_model: - return self._mapper.to_entity(rating_model) - return None - - def with_user_id_and_movie_id( - self, - user_id: UserId, - movie_id: MovieId, - ) -> Optional[RatingEntity]: - statement = select(RatingModel).where( - and_( - RatingModel.user_id == user_id, - RatingModel.movie_id == movie_id, - ), - ) - rating_model = self._session.scalar(statement) - if rating_model: - return self._mapper.to_entity(rating_model) - return None - - def list_with_movie_id( - self, - movie_id: MovieId, - limit: int, - offset: int, - ) -> list[RatingEntity]: - statement = ( - select(RatingModel) - .where(RatingModel.movie_id == movie_id) - .limit(limit) - .offset(offset) - ) - rating_models = self._session.scalars(statement) - - rating_entities = [] - for rating_model in rating_models: - rating_entity = self._mapper.to_entity(rating_model) - rating_entities.append(rating_entity) - - return rating_entities - - def list_with_user_id( - self, - user_id: UserId, - limit: int, - offset: int, - ) -> list[RatingEntity]: - statement = ( - select(RatingModel) - .where(RatingModel.user_id == user_id) - .limit(limit) - .offset(offset) - ) - rating_models = self._session.scalars(statement) - - rating_entities = [] - for rating_model in rating_models: - rating_entity = self._mapper.to_entity(rating_model) - rating_entities.append(rating_entity) - - return rating_entities - - def save(self, rating: RatingEntity) -> None: - rating_model = self._mapper.to_model(rating) - self._session.add(rating_model) - - def delete(self, rating: RatingEntity) -> None: - rating_model = self._session.get(RatingModel, rating.id) - if rating_model: - self._session.delete(rating_model) - - def delete_with_movie_id(self, movie_id: MovieId) -> None: - statement = delete(RatingModel).where(RatingModel.movie_id == movie_id) - self._session.execute(statement) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py deleted file mode 100644 index 5c76ea7..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/review.py +++ /dev/null @@ -1,96 +0,0 @@ -from typing import Optional - -from sqlalchemy import select, delete, and_ -from sqlalchemy.orm.session import Session - -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, Review as ReviewEntity -from amdb.infrastructure.persistence.sqlalchemy.models.review import ( - Review as ReviewModel, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ( - ReviewMapper, -) - - -class SQLAlchemyReviewGateway: - def __init__( - self, - session: Session, - mapper: ReviewMapper, - ) -> None: - self._session = session - self._mapper = mapper - - def with_id(self, review_id: ReviewId) -> Optional[ReviewEntity]: - review_model = self._session.get(ReviewModel, review_id) - if review_model: - return self._mapper.to_entity(review_model) - return None - - def with_movie_id_and_user_id( - self, - user_id: UserId, - movie_id: MovieId, - ) -> Optional[ReviewEntity]: - statement = select(ReviewModel).where( - and_( - ReviewModel.user_id == user_id, - ReviewModel.movie_id == movie_id, - ), - ) - review_model = self._session.scalar(statement) - if review_model: - return self._mapper.to_entity(review_model) - return None - - def list_with_movie_id( - self, - movie_id: MovieId, - limit: int, - offset: int, - ) -> list[ReviewEntity]: - statement = ( - select(ReviewModel) - .where(ReviewModel.movie_id == movie_id) - .limit(limit) - .offset(offset) - ) - review_models = self._session.scalars(statement) - - review_entities = [] - for review_model in review_models: - review_entity = self._mapper.to_entity(review_model) - review_entities.append(review_entity) - - return review_entities - - def list_with_user_id( - self, - user_id: UserId, - limit: int, - offset: int, - ) -> list[ReviewEntity]: - statement = ( - select(ReviewModel) - .where(ReviewModel.user_id == user_id) - .limit(limit) - .offset(offset) - ) - review_models = self._session.scalars(statement) - - review_entities = [] - for review_model in review_models: - review_entity = self._mapper.to_entity(review_model) - review_entities.append(review_entity) - - return review_entities - - def save(self, review: ReviewEntity) -> None: - review_model = self._mapper.to_model(review) - self._session.add(review_model) - - def delete_with_movie_id(self, movie_id: MovieId) -> None: - statement = delete(ReviewModel).where(ReviewModel.movie_id == movie_id) - self._session.execute(statement) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py deleted file mode 100644 index 2d22d1b..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Optional - -from sqlalchemy import select -from sqlalchemy.orm.session import Session - -from amdb.domain.entities.user import UserId, User as UserEntity -from amdb.infrastructure.persistence.sqlalchemy.models.user import ( - User as UserModel, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.user import UserMapper - - -class SQLAlchemyUserGateway: - def __init__( - self, - session: Session, - mapper: UserMapper, - ) -> None: - self._session = session - self._mapper = mapper - - def with_id(self, user_id: UserId) -> Optional[UserEntity]: - user_model = self._session.get(UserModel, user_id) - if user_model: - return self._mapper.to_entity(user_model) - return None - - def with_name(self, user_name: str) -> Optional[UserEntity]: - statement = select(UserModel).where(UserModel.name == user_name) - user_model = self._session.scalar(statement) - if user_model: - return self._mapper.to_entity(user_model) - return None - - def save(self, user: UserEntity) -> None: - user_model = self._mapper.to_model(user) - self._session.add(user_model) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py b/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py deleted file mode 100644 index 31b3980..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/user_password_hash.py +++ /dev/null @@ -1,37 +0,0 @@ -from typing import Optional - -from sqlalchemy.orm.session import Session - -from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.sqlalchemy.models.user_password_hash import ( - UserPasswordHash as UserPasswordHashModel, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.user_password_hash import ( - UserPasswordHashMapper, -) -from amdb.infrastructure.password_manager.model import UserPasswordHash - - -class SQLAlchemyUserPasswordHashGateway: - def __init__( - self, - session: Session, - mapper: UserPasswordHashMapper, - ) -> None: - self._session = session - self._mapper = mapper - - def get(self, user_id: UserId) -> Optional[UserPasswordHash]: - user_password_hash_model = self._session.get( - UserPasswordHashModel, user_id - ) - if user_password_hash_model: - return self._mapper.to_password_manager_model( - user_password_hash_model - ) - return None - - def save(self, user_password_hash: UserPasswordHash) -> None: - user_password_hash_model = self._mapper.to_model(user_password_hash) - self._session.add(user_password_hash_model) - self._session.commit() diff --git a/src/amdb/infrastructure/persistence/redis/gateways/__init__.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/__init__.py similarity index 100% rename from src/amdb/infrastructure/persistence/redis/gateways/__init__.py rename to src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/__init__.py diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py new file mode 100644 index 0000000..67ca571 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py @@ -0,0 +1,53 @@ +from typing import Annotated, Optional + +from sqlalchemy import Connection, Row, select, insert, update, delete + +from amdb.domain.entities.movie import MovieId, Movie +from amdb.infrastructure.persistence.sqlalchemy.models.movie import MovieModel + + +class MovieMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_id(self, movie_id: MovieId) -> Optional[Movie]: + statement = select(MovieModel).where(MovieModel.id == movie_id) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def save(self, movie: Movie) -> None: + statement = insert(MovieModel).values( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + rating_count=movie.rating_count, + ) + self._connection.execute(statement) + + def update(self, movie: Movie) -> None: + statement = update(MovieModel).values( + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + rating_count=movie.rating_count, + ) + self._connection.execute(statement) + + def delete(self, movie: Movie) -> None: + statement = delete(MovieModel).where(MovieModel.id == movie.id) + self._connection.execute(statement) + + def _to_entity( + self, + row: Annotated[MovieModel, Row[tuple[MovieModel]]], + ) -> Movie: + return Movie( + id=MovieId(row.id), + title=row.title, + release_date=row.release_date, + rating=row.rating, + rating_count=row.rating_count, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py new file mode 100644 index 0000000..846e8f5 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py @@ -0,0 +1,68 @@ +from typing import Annotated, Optional + +from sqlalchemy import Connection, Row, select, insert, delete, and_ + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId, Rating +from amdb.infrastructure.persistence.sqlalchemy.models.rating import ( + RatingModel, +) + + +class RatingMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_id(self, rating_id: RatingId) -> Optional[Rating]: + statement = select(RatingModel).where(RatingModel.id == rating_id) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def with_user_id_and_movie_id( + self, + user_id: UserId, + movie_id: MovieId, + ) -> Optional[Rating]: + statement = select(RatingModel).where( + and_( + RatingModel.user_id == user_id, + RatingModel.movie_id == movie_id, + ), + ) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def save(self, rating: Rating) -> None: + statement = insert(RatingModel).values( + id=rating.id, + movie_id=rating.movie_id, + user_id=rating.user_id, + value=rating.value, + created_at=rating.created_at, + ) + self._connection.execute(statement) + + def delete(self, rating: Rating) -> None: + statement = delete(RatingModel).where(RatingModel.id == rating.id) + self._connection.execute(statement) + + def delete_with_movie_id(self, movie_id: MovieId) -> None: + statement = delete(RatingModel).where(RatingModel.movie_id == movie_id) + self._connection.execute(statement) + + def _to_entity( + self, + row: Annotated[RatingModel, Row[tuple[RatingModel]]], + ) -> Rating: + return Rating( + id=RatingId(row.id), + movie_id=MovieId(row.movie_id), + user_id=UserId(row.user_id), + value=row.value, + created_at=row.created_at, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py new file mode 100644 index 0000000..d08255d --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py @@ -0,0 +1,68 @@ +from typing import Annotated, Optional + +from sqlalchemy import Connection, Row, select, insert, delete, and_ + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.review import ReviewId, ReviewType, Review +from amdb.infrastructure.persistence.sqlalchemy.models.review import ( + ReviewModel, +) + + +class ReviewMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_id(self, review_id: ReviewId) -> Optional[Review]: + statement = select(ReviewModel).where(ReviewModel.id == review_id) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def with_movie_id_and_user_id( + self, + user_id: UserId, + movie_id: MovieId, + ) -> Optional[Review]: + statement = select(ReviewModel).where( + and_( + ReviewModel.user_id == user_id, + ReviewModel.movie_id == movie_id, + ), + ) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def save(self, review: Review) -> None: + statement = insert(ReviewModel).values( + id=review.id, + user_id=review.user_id, + movie_id=review.movie_id, + title=review.title, + content=review.content, + type=review.type.value, + created_at=review.created_at, + ) + self._connection.execute(statement) + + def delete_with_movie_id(self, movie_id: MovieId) -> None: + statement = delete(ReviewModel).where(ReviewModel.movie_id == movie_id) + self._connection.execute(statement) + + def _to_entity( + self, + row: Annotated[ReviewModel, Row[tuple[ReviewModel]]], + ) -> Review: + return Review( + id=ReviewId(row.id), + user_id=UserId(row.user_id), + movie_id=MovieId(row.movie_id), + title=row.title, + content=row.content, + type=ReviewType(row.type), + created_at=row.created_at, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py new file mode 100644 index 0000000..db427ac --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py @@ -0,0 +1,41 @@ +from typing import Annotated, Optional + +from sqlalchemy import Connection, Row, select, insert + +from amdb.domain.entities.user import UserId, User +from amdb.infrastructure.persistence.sqlalchemy.models.user import UserModel + + +class UserMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_id(self, user_id: UserId) -> Optional[User]: + statement = select(UserModel).where(UserModel.id == user_id) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def with_name(self, user_name: str) -> Optional[User]: + statement = select(UserModel).where(UserModel.name == user_name) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + + def save(self, user: User) -> None: + statement = insert(UserModel).values( + id=UserId(user.id), + name=user.name, + ) + self._connection.execute(statement) + + def _to_entity( + self, + row: Annotated[UserModel, Row[tuple[UserModel]]], + ) -> User: + return User( + id=UserId(row.id), + name=row.name, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py deleted file mode 100644 index 520ab2f..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/movie.py +++ /dev/null @@ -1,24 +0,0 @@ -from amdb.domain.entities.movie import MovieId, Movie as MovieEntity -from amdb.infrastructure.persistence.sqlalchemy.models.movie import ( - Movie as MovieModel, -) - - -class MovieMapper: - def to_model(self, movie: MovieEntity) -> MovieModel: - return MovieModel( - id=movie.id, - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - rating_count=movie.rating_count, - ) - - def to_entity(self, movie: MovieModel) -> MovieEntity: - return MovieEntity( - id=MovieId(movie.id), - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - rating_count=movie.rating_count, - ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/password_hash.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/password_hash.py new file mode 100644 index 0000000..9688223 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/password_hash.py @@ -0,0 +1,41 @@ +from typing import Annotated, Optional + +from sqlalchemy import Connection, Row, select, insert + +from amdb.domain.entities.user import UserId +from amdb.infrastructure.password_manager.password_hash import PasswordHash +from amdb.infrastructure.persistence.sqlalchemy.models.password_hash import ( + PasswordHashModel, +) + + +class PasswordHashMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_user_id(self, user_id: UserId) -> Optional[PasswordHash]: + statement = select(PasswordHashModel).where( + PasswordHashModel.user_id == user_id, + ) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_data_structure(row) # type: ignore + return None + + def save(self, password_hash: PasswordHash) -> None: + statement = insert(PasswordHashModel).values( + user_id=password_hash.user_id, + hash=password_hash.hash, + salt=password_hash.salt, + ) + self._connection.execute(statement) + + def _to_data_structure( + self, + row: Annotated[PasswordHashModel, Row], + ) -> PasswordHash: + return PasswordHash( + user_id=UserId(row.user_id), + hash=row.hash, + salt=row.salt, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py deleted file mode 100644 index ed34246..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/rating.py +++ /dev/null @@ -1,26 +0,0 @@ -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.rating import RatingId, Rating as RatingEntity -from amdb.infrastructure.persistence.sqlalchemy.models.rating import ( - Rating as RatingModel, -) - - -class RatingMapper: - def to_model(self, rating: RatingEntity) -> RatingModel: - return RatingModel( - id=rating.id, - movie_id=rating.movie_id, - user_id=rating.user_id, - value=rating.value, - created_at=rating.created_at, - ) - - def to_entity(self, rating: RatingModel) -> RatingEntity: - return RatingEntity( - id=RatingId(rating.id), - movie_id=MovieId(rating.movie_id), - user_id=UserId(rating.user_id), - value=rating.value, - created_at=rating.created_at, - ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py deleted file mode 100644 index 8a28c94..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/review.py +++ /dev/null @@ -1,34 +0,0 @@ -from amdb.domain.entities.user import UserId -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ( - ReviewId, - ReviewType, - Review as ReviewEntity, -) -from amdb.infrastructure.persistence.sqlalchemy.models.review import ( - Review as ReviewModel, -) - - -class ReviewMapper: - def to_model(self, review: ReviewEntity) -> ReviewModel: - return ReviewModel( - id=review.id, - user_id=review.user_id, - movie_id=review.movie_id, - title=review.title, - content=review.content, - type=review.type.value, - created_at=review.created_at, - ) - - def to_entity(self, review: ReviewModel) -> ReviewEntity: - return ReviewEntity( - id=ReviewId(review.id), - user_id=UserId(review.user_id), - movie_id=MovieId(review.movie_id), - title=review.title, - content=review.content, - type=ReviewType(review.type), - created_at=review.created_at, - ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py deleted file mode 100644 index 86f28fc..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user.py +++ /dev/null @@ -1,18 +0,0 @@ -from amdb.domain.entities.user import UserId, User as UserEntity -from amdb.infrastructure.persistence.sqlalchemy.models.user import ( - User as UserModel, -) - - -class UserMapper: - def to_model(self, user: UserEntity) -> UserModel: - return UserModel( - id=user.id, - name=user.name, - ) - - def to_entity(self, user: UserModel) -> UserEntity: - return UserEntity( - id=UserId(user.id), - name=user.name, - ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user_password_hash.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user_password_hash.py deleted file mode 100644 index 7f0e15e..0000000 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/user_password_hash.py +++ /dev/null @@ -1,31 +0,0 @@ -from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.sqlalchemy.models.user_password_hash import ( - UserPasswordHash as UserPasswordHashModel, -) -from amdb.infrastructure.security.hasher import HashData -from amdb.infrastructure.password_manager.model import UserPasswordHash - - -class UserPasswordHashMapper: - def to_model( - self, - user_password_hash: UserPasswordHash, - ) -> UserPasswordHashModel: - return UserPasswordHashModel( - user_id=user_password_hash.user_id, - hash=user_password_hash.password_hash.hash, - salt=user_password_hash.password_hash.salt, - ) - - def to_password_manager_model( - self, - user_password_hash: UserPasswordHashModel, - ) -> UserPasswordHash: - password_hash = HashData( - hash=user_password_hash.hash, - salt=user_password_hash.salt, - ) - return UserPasswordHash( - user_id=UserId(user_password_hash.user_id), - password_hash=password_hash, - ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/gateways/__init__.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/__init__.py similarity index 100% rename from src/amdb/infrastructure/persistence/sqlalchemy/gateways/__init__.py rename to src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/__init__.py diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py new file mode 100644 index 0000000..de14469 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py @@ -0,0 +1,119 @@ +__all__ = ("DetailedMovieViewModelMapper",) + +from typing import Optional, TypedDict +from datetime import date, datetime +from uuid import UUID + +from sqlalchemy import Connection, Row, text + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.domain.entities.review import ReviewId, ReviewType +from amdb.application.common.view_models.detailed_movie import ( + UserRating, + UserReview, + DetailedMovieViewModel, +) + + +class RowAsDict(TypedDict): + movie_id: UUID + movie_title: str + movie_release_date: date + movie_rating: float + movie_rating_count: int + user_rating_id: Optional[UUID] + user_rating_value: Optional[float] + user_rating_created_at: Optional[datetime] + user_review_id: Optional[UUID] + user_review_title: Optional[str] + user_review_content: Optional[str] + user_review_type: Optional[int] + user_review_created_at: Optional[datetime] + + @classmethod # type: ignore + def from_row(cls, row: Row) -> "RowAsDict": + return RowAsDict(**row._mapping) # noqa: SLF001 + + +class DetailedMovieViewModelMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def one( + self, + movie_id: MovieId, + current_user_id: Optional[UserId], + ) -> Optional[DetailedMovieViewModel]: + statement = text( + """ + SELECT + m.id movie_id, + m.title movie_title, + m.release_date movie_release_date, + m.rating movie_rating, + m.rating_count movie_rating_count, + urt.id user_rating_id, + urt.value user_rating_value, + urt.created_at user_rating_created_at, + urv.id user_review_id, + urv.title user_review_title, + urv.content user_review_content, + urv.type user_review_type, + urv.created_at user_review_created_at + FROM + movies m + LEFT JOIN ratings urt + ON urt.user_id = :current_user_id + LEFT JOIN reviews urv + ON urv.user_id = :current_user_id + WHERE + m.id = :movie_id + LIMIT 1 + """, + ) + parameters = { + "movie_id": movie_id, + "current_user_id": current_user_id, + } + row = self._connection.execute(statement, parameters).fetchone() + if row: + row_as_dict = RowAsDict.from_row(row) # type: ignore + return self._to_view_model(row_as_dict) + return None + + def _to_view_model( + self, + row_as_dict: RowAsDict, + ) -> DetailedMovieViewModel: + if row_as_dict["user_rating_id"]: + user_rating = UserRating( + id=RatingId(row_as_dict["user_rating_id"]), # type: ignore + value=row_as_dict["user_rating_value"], # type: ignore + created_at=row_as_dict["user_rating_created_at"], # type: ignore + ) + else: + user_rating = None + + if row_as_dict["user_review_id"]: + user_review = UserReview( + id=ReviewId(row_as_dict["user_review_id"]), # type: ignore + title=row_as_dict["user_review_title"], # type: ignore + content=row_as_dict["user_review_content"], # type: ignore + type=ReviewType(row_as_dict["user_review_type"]), # type: ignore + created_at=row_as_dict["user_review_created_at"], # type: ignore + ) + else: + user_review = None + + detailed_movie_view_model = DetailedMovieViewModel( + id=MovieId(row_as_dict["movie_id"]), + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + rating_count=row_as_dict["movie_rating_count"], + user_rating=user_rating, + user_review=user_review, + ) + return detailed_movie_view_model diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py new file mode 100644 index 0000000..b9b65ce --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py @@ -0,0 +1,91 @@ +__all__ = ("NonDetailedMovieViewModelMapper",) + +from typing import Optional, TypedDict +from datetime import date +from uuid import UUID + +from sqlalchemy import Connection, Row, text + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.application.common.view_models.non_detailed_movie import ( + UserRating, + NonDetailedMovieViewModel, +) + + +class RowAsDict(TypedDict): + movie_id: UUID + movie_title: str + movie_release_date: date + movie_rating: float + user_rating_id: Optional[UUID] + user_rating_value: Optional[float] + + @classmethod # type: ignore + def from_row(cls, row: Row) -> "RowAsDict": + return RowAsDict(**row._mapping) # noqa: SLF001 + + +class NonDetailedMovieViewModelMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def list( + self, + current_user_id: Optional[UserId], + limit: int, + offset: int, + ) -> list[NonDetailedMovieViewModel]: + statement = text( + """ + SELECT + m.id movie_id, + m.title movie_title, + m.release_date movie_release_date, + m.rating movie_rating, + urt.id user_rating_id, + urt.value user_rating_value + FROM + movies m + LEFT JOIN ratings urt + ON urt.user_id = :current_user_id + LIMIT :limit OFFSET :offset + """, + ) + parameters = { + "current_user_id": current_user_id, + "limit": limit, + "offset": offset, + } + rows = self._connection.execute(statement, parameters).fetchall() + + non_detailed_view_models = [] + for row in rows: + row_as_dict = RowAsDict.from_row(row) # type: ignore + non_detailed_view_model = self._to_view_model(row_as_dict) + non_detailed_view_models.append(non_detailed_view_model) + + return non_detailed_view_models + + def _to_view_model( + self, + row_as_dict: RowAsDict, + ) -> NonDetailedMovieViewModel: + if row_as_dict["user_rating_id"]: + user_rating = UserRating( + id=RatingId(row_as_dict["user_rating_id"]), # type: ignore + value=row_as_dict["user_rating_value"], # type: ignore + ) + else: + user_rating = None + + non_detailed_movie_view_model = NonDetailedMovieViewModel( + id=MovieId(row_as_dict["movie_id"]), + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + user_rating=user_rating, + ) + return non_detailed_movie_view_model diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py new file mode 100644 index 0000000..a3443a1 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py @@ -0,0 +1,109 @@ +__all__ = ("ReviewViewModelMapper",) + +from typing import Optional, TypedDict +from datetime import datetime +from uuid import UUID + +from sqlalchemy import Connection, Row, text + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.domain.entities.review import ReviewId, ReviewType +from amdb.application.common.view_models.review import ( + UserRating, + UserReview, + ReviewViewModel, +) + + +class RowAsDict(TypedDict): + user_id: UUID + user_review_id: UUID + user_review_title: str + user_review_content: str + user_review_type: int + user_review_created_at: datetime + user_rating_id: Optional[UUID] + user_rating_value: Optional[float] + user_rating_created_at: Optional[datetime] + + @classmethod # type: ignore + def from_row(cls, row: Row) -> "RowAsDict": + return RowAsDict(row._mapping) # noqa: SLF001 + + +class ReviewViewModelMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def list( + self, + movie_id: MovieId, + limit: int, + offset: int, + ) -> list[ReviewViewModel]: + statement = text( + """ + SELECT + urv.user_id user_id, + urv.id user_review_id, + urv.title user_review_title, + urv.content user_review_content, + urv.type user_review_type, + urv.created_at user_review_created_at, + urt.id user_rating_id, + urt.value user_rating_value, + urt.created_at user_rating_created_at + FROM + reviews urv + LEFT JOIN ratings urt + ON urt.movie_id = urv.movie_id + AND urt.user_id = urv.user_id + WHERE + urv.movie_id = :movie_id + LIMIT :limit OFFSET :offset + """, + ) + parameters = { + "movie_id": movie_id, + "limit": limit, + "offset": offset, + } + rows = self._connection.execute(statement, parameters).fetchall() + + review_view_models = [] + for row in rows: + row_as_dict = RowAsDict.from_row(row) # type: ignore + review_view_model = self._to_view_model(row_as_dict) + review_view_models.append(review_view_model) + + return review_view_models + + def _to_view_model( + self, + row_as_dict: RowAsDict, + ) -> ReviewViewModel: + user_review = UserReview( + id=ReviewId(row_as_dict["user_review_id"]), + title=row_as_dict["user_review_title"], + content=row_as_dict["user_review_content"], + type=ReviewType(row_as_dict["user_review_type"]), + created_at=row_as_dict["user_review_created_at"], + ) + + if row_as_dict["user_rating_id"]: + user_rating = UserRating( + id=RatingId(row_as_dict["user_rating_id"]), # type: ignore + value=row_as_dict["user_rating_value"], # type: ignore + created_at=row_as_dict["user_rating_created_at"], # type: ignore + ) + else: + user_rating = None + + review_view_model = ReviewViewModel( + user_id=UserId(row_as_dict["user_id"]), + user_review=user_review, + user_rating=user_rating, + ) + return review_view_model diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/movie.py index 3794544..d6fa74a 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/movie.py @@ -6,7 +6,7 @@ from .base import Model -class Movie(Model): +class MovieModel(Model): __tablename__ = "movies" id: Mapped[UUID] = mapped_column( diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/user_password_hash.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/password_hash.py similarity index 91% rename from src/amdb/infrastructure/persistence/sqlalchemy/models/user_password_hash.py rename to src/amdb/infrastructure/persistence/sqlalchemy/models/password_hash.py index f042808..48d8a60 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/user_password_hash.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/password_hash.py @@ -6,7 +6,7 @@ from .base import Model -class UserPasswordHash(Model): +class PasswordHashModel(Model): __tablename__ = "user_password_hashes" user_id: Mapped[UUID] = mapped_column( diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py index 3f1942b..baaab42 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py @@ -5,11 +5,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Model -from .user import User -from .movie import Movie +from .user import UserModel +from .movie import MovieModel -class Rating(Model): +class RatingModel(Model): __tablename__ = "ratings" id: Mapped[UUID] = mapped_column( @@ -24,7 +24,7 @@ class Rating(Model): value: Mapped[float] created_at: Mapped[datetime] - movie: Mapped[Movie] = relationship() - user: Mapped[User] = relationship() + movie: Mapped[MovieModel] = relationship() + user: Mapped[UserModel] = relationship() __table_args__ = (UniqueConstraint("user_id", "movie_id"),) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py index 46c2646..90c4f6f 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py @@ -5,11 +5,11 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from .base import Model -from .user import User -from .movie import Movie +from .user import UserModel +from .movie import MovieModel -class Review(Model): +class ReviewModel(Model): __tablename__ = "reviews" id: Mapped[UUID] = mapped_column( @@ -26,5 +26,5 @@ class Review(Model): type: Mapped[int] created_at: Mapped[datetime] - user: Mapped[User] = relationship() - movie: Mapped[Movie] = relationship() + user: Mapped[UserModel] = relationship() + movie: Mapped[MovieModel] = relationship() diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py index 52d58f1..52514d5 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py @@ -5,7 +5,7 @@ from .base import Model -class User(Model): +class UserModel(Model): __tablename__ = "users" id: Mapped[UUID] = mapped_column( diff --git a/src/amdb/infrastructure/security/__init__.py b/src/amdb/infrastructure/security/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/amdb/infrastructure/security/hasher.py b/src/amdb/infrastructure/security/hasher.py deleted file mode 100644 index ba1af36..0000000 --- a/src/amdb/infrastructure/security/hasher.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import hashlib -from dataclasses import dataclass - - -@dataclass(frozen=True, slots=True) -class HashData: - hash: bytes - salt: bytes - - -class Hasher: - def hash(self, value: bytes) -> HashData: - salt = os.urandom(32) - hash = hashlib.pbkdf2_hmac( - hash_name="sha256", - password=value, - salt=salt, - iterations=10000, - ) - return HashData(hash=hash, salt=salt) - - def verify(self, value: bytes, hash_data: HashData) -> bool: - hash = hashlib.pbkdf2_hmac( - hash_name="sha256", - password=value, - salt=hash_data.salt, - iterations=10000, - ) - return hash == hash_data.hash diff --git a/src/amdb/main/cli/__main__.py b/src/amdb/main/cli/__main__.py index 35c4a92..e9d6f6f 100644 --- a/src/amdb/main/cli/__main__.py +++ b/src/amdb/main/cli/__main__.py @@ -1,11 +1,23 @@ -from amdb.main.config import build_generic_config +import os + +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig from .app import create_app def main() -> None: - generic_config = build_generic_config() + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + postgres_config = PostgresConfig.from_toml(path_to_config) + redis_config = RedisConfig.from_toml(path_to_config) - app = create_app(generic_config) + app = create_app( + postgres_config=postgres_config, + redis_config=redis_config, + ) app() diff --git a/src/amdb/main/cli/app.py b/src/amdb/main/cli/app.py index bbc2648..abdc0b8 100644 --- a/src/amdb/main/cli/app.py +++ b/src/amdb/main/cli/app.py @@ -1,14 +1,53 @@ +from typing import cast +from uuid import UUID + import typer +from sqlalchemy import create_engine +from redis import Redis +from amdb.domain.entities.user import UserId +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig +from amdb.infrastructure.persistence.redis.mappers.permissions import ( + PermissionsMapper, +) +from amdb.infrastructure.password_manager.hash_computer import HashComputer +from amdb.infrastructure.auth.raw.identity_provider import RawIdentityProvider from amdb.presentation.cli.setup import setup_typer_command_handlers -from amdb.main.config import GenericConfig -from .di import create_dependencies_dict +from amdb.main.ioc import IoC + + +IDENTITY_PROVIDER_USER_ID = UserId( + UUID("00000000-0000-0000-0000-000000000000"), +) +IDENTITY_PROVIDER_PERMISSIONS = 12 + +def create_app( + postgres_config: PostgresConfig, + redis_config: RedisConfig, +) -> typer.Typer: + sqlalchemy_engine = create_engine(postgres_config.url) + redis = Redis.from_url(redis_config.url, decode_responses=True) + permissions_mapper = PermissionsMapper(cast(Redis, redis)) + + ioc = IoC( + sqlalchemy_engine=sqlalchemy_engine, + permissions_mapper=permissions_mapper, + hash_computer=HashComputer(), + ) + raw_identity_provider = RawIdentityProvider( + user_id=IDENTITY_PROVIDER_USER_ID, + permissions=IDENTITY_PROVIDER_PERMISSIONS, + ) + dependencies = { + "ioc": ioc, + "identity_provider": raw_identity_provider, + } -def create_app(generic_config: GenericConfig) -> typer.Typer: app = typer.Typer( rich_markup_mode="rich", - context_settings={"obj": create_dependencies_dict(generic_config)}, + context_settings={"obj": dependencies}, ) setup_typer_command_handlers(app) diff --git a/src/amdb/main/cli/di.py b/src/amdb/main/cli/di.py deleted file mode 100644 index 8f1f9d8..0000000 --- a/src/amdb/main/cli/di.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import TypedDict -from uuid import UUID - -from sqlalchemy import create_engine -from sqlalchemy.orm.session import sessionmaker -from redis.client import Redis - -from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, -) -from amdb.infrastructure.auth.raw.identity_provider import RawIdentityProvider -from amdb.infrastructure.security.hasher import Hasher -from amdb.main.config import GenericConfig -from amdb.main.ioc import IoC - - -IDENTITY_PROVIDER_USER_ID = UserId( - UUID("00000000-0000-0000-0000-000000000000") -) -IDENTITY_PROVIDER_PERMISSIONS = 12 - - -class DependenciesDict(TypedDict): - ioc: IoC - identity_provider: RawIdentityProvider - - -def create_dependencies_dict( - generic_config: GenericConfig, -) -> DependenciesDict: - redis = Redis( - host=generic_config.redis.host, - port=generic_config.redis.port, - db=generic_config.redis.db, - password=generic_config.redis.password, - ) - engine = create_engine(generic_config.postgres.dsn) - ioc = IoC( - sessionmaker=sessionmaker(engine), - permissions_gateway=RedisPermissionsGateway(redis), - hasher=Hasher(), - ) - identity_provider = RawIdentityProvider( - user_id=IDENTITY_PROVIDER_USER_ID, - permissions=IDENTITY_PROVIDER_PERMISSIONS, - ) - - return DependenciesDict(ioc=ioc, identity_provider=identity_provider) diff --git a/src/amdb/main/config.py b/src/amdb/main/config.py deleted file mode 100644 index 9bdb79b..0000000 --- a/src/amdb/main/config.py +++ /dev/null @@ -1,51 +0,0 @@ -import os -from dataclasses import dataclass - -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig - - -POSTGRES_HOST_ENV = "POSTGRES_HOST" -POSTGRES_PORT_ENV = "POSTGRES_PORT" -POSTGRES_NAME_ENV = "POSTGRES_DB" -POSTGRES_USER_ENV = "POSTGRES_USER" -POSTGRES_PASSWORD_ENV = "POSTGRES_PASSWORD" - -REDIS_HOST_ENV = "REDIS_HOST" -REDIS_PORT_ENV = "REDIS_PORT" -REDIS_DB_ENV = "REDIS_DB" -REDIS_PASSWORD_ENV = "REDIS_PASSWORD" - - -def build_generic_config() -> "GenericConfig": - postgres_config = PostgresConfig( - host=_get_env(POSTGRES_HOST_ENV), - port=_get_env(POSTGRES_PORT_ENV), - name=_get_env(POSTGRES_NAME_ENV), - user=_get_env(POSTGRES_USER_ENV), - password=_get_env(POSTGRES_PASSWORD_ENV), - ) - redis_config = RedisConfig( - host=_get_env(REDIS_HOST_ENV), - port=int(_get_env(REDIS_PORT_ENV)), - db=int(_get_env(REDIS_DB_ENV)), - password=_get_env(REDIS_PASSWORD_ENV), - ) - return GenericConfig( - postgres=postgres_config, - redis=redis_config, - ) - - -def _get_env(key: str) -> str: - value = os.getenv(key) - if value is None: - message = f"Env variable {key} is not set" - raise ValueError(message) - return value - - -@dataclass(frozen=True, slots=True) -class GenericConfig: - postgres: PostgresConfig - redis: RedisConfig diff --git a/src/amdb/main/ioc.py b/src/amdb/main/ioc.py index 32a5969..53b769e 100644 --- a/src/amdb/main/ioc.py +++ b/src/amdb/main/ioc.py @@ -1,7 +1,7 @@ from contextlib import contextmanager from typing import Iterator -from sqlalchemy.orm import Session, sessionmaker +from sqlalchemy import Engine from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_user import CreateUser @@ -9,9 +9,7 @@ from amdb.domain.services.rate_movie import RateMovie from amdb.domain.services.unrate_movie import UnrateMovie from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.command_handlers.delete_movie import DeleteMovieHandler @@ -19,25 +17,41 @@ from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.command_handlers.review_movie import ReviewMovieHandler from amdb.application.query_handlers.login import LoginHandler -from amdb.application.query_handlers.get_movies import GetMoviesHandler -from amdb.application.query_handlers.get_movie import GetMovieHandler -from amdb.application.query_handlers.get_movie_ratings import ( - GetMovieRatingsHandler, +from amdb.application.query_handlers.detailed_movie import ( + GetDetailedMovieHandler, +) +from amdb.application.query_handlers.non_detailed_movies import ( + GetNonDetailedMoviesHandler, +) +from amdb.application.query_handlers.reviews import GetReviewsHandler +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( + UserMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( + MovieMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( + RatingMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( + ReviewMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( + NonDetailedMovieViewModelMapper, ) -from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler -from amdb.application.query_handlers.get_rating import GetRatingHandler -from amdb.application.query_handlers.get_movie_reviews import ( - GetMovieReviewsHandler, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( + DetailedMovieViewModelMapper, ) -from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler -from amdb.application.query_handlers.get_review import GetReviewHandler -from amdb.infrastructure.persistence.sqlalchemy.gateway_factory import ( - build_sqlalchemy_gateway_factory, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.review import ( + ReviewViewModelMapper, ) -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( + PasswordHashMapper, ) -from amdb.infrastructure.security.hasher import Hasher +from amdb.infrastructure.persistence.redis.mappers.permissions import ( + PermissionsMapper, +) +from amdb.infrastructure.password_manager.hash_computer import HashComputer from amdb.infrastructure.password_manager.password_manager import ( HashingPasswordManager, ) @@ -47,74 +61,70 @@ class IoC(HandlerFactory): def __init__( self, - sessionmaker: sessionmaker[Session], - permissions_gateway: RedisPermissionsGateway, - hasher: Hasher, + sqlalchemy_engine: Engine, + permissions_mapper: PermissionsMapper, + hash_computer: HashComputer, ) -> None: - self._sessionmaker = sessionmaker - self._permissions_gateway = permissions_gateway - self._hasher = hasher + self._sqlalchemy_engine = sqlalchemy_engine + self._permissions_mapper = permissions_mapper + self._hash_computer = hash_computer @contextmanager def register_user(self) -> Iterator[RegisterUserHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - hashing_password_manager = HashingPasswordManager( - hasher=self._hasher, - user_password_hash_gateway=gateway_factory.user_password_hash(), + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: + password_manager = HashingPasswordManager( + hash_computer=self._hash_computer, + password_hash_gateway=PasswordHashMapper( + sqlalchemy_connection, + ), ) yield RegisterUserHandler( create_user=CreateUser(), - user_gateway=gateway_factory.user(), - permissions_gateway=self._permissions_gateway, - unit_of_work=gateway_factory.unit_of_work(), - password_manager=hashing_password_manager, + user_gateway=UserMapper(sqlalchemy_connection), + permissions_gateway=self._permissions_mapper, + unit_of_work=sqlalchemy_connection, + password_manager=password_manager, ) @contextmanager def login(self) -> Iterator[LoginHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - hashing_password_manager = HashingPasswordManager( - hasher=self._hasher, - user_password_hash_gateway=gateway_factory.user_password_hash(), + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: + password_manager = HashingPasswordManager( + hash_computer=self._hash_computer, + password_hash_gateway=PasswordHashMapper( + sqlalchemy_connection, + ), ) yield LoginHandler( access_concern=AccessConcern(), - user_gateway=gateway_factory.user(), - permissions_gateway=self._permissions_gateway, - password_manager=hashing_password_manager, + user_gateway=UserMapper(sqlalchemy_connection), + permissions_gateway=self._permissions_mapper, + password_manager=password_manager, ) @contextmanager - def get_movies( + def get_non_detailed_movies( self, identity_provider: IdentityProvider, - ) -> Iterator[GetMoviesHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMoviesHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), + ) -> Iterator[GetNonDetailedMoviesHandler]: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: + yield GetNonDetailedMoviesHandler( + non_detailed_movie_reader=NonDetailedMovieViewModelMapper( + sqlalchemy_connection, + ), identity_provider=identity_provider, ) @contextmanager - def get_movie( + def get_detailed_movie( self, identity_provider: IdentityProvider, - ) -> Iterator[GetMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), + ) -> Iterator[GetDetailedMovieHandler]: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: + yield GetDetailedMovieHandler( + detailed_movie_reader=DetailedMovieViewModelMapper( + sqlalchemy_connection, + ), identity_provider=identity_provider, ) @@ -123,15 +133,13 @@ def create_movie( self, identity_provider: IdentityProvider, ) -> Iterator[CreateMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: yield CreateMovieHandler( access_concern=AccessConcern(), create_movie=CreateMovie(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), - unit_of_work=gateway_factory.unit_of_work(), + permissions_gateway=self._permissions_mapper, + movie_gateway=MovieMapper(sqlalchemy_connection), + unit_of_work=sqlalchemy_connection, identity_provider=identity_provider, ) @@ -140,62 +148,14 @@ def delete_movie( self, identity_provider: IdentityProvider, ) -> Iterator[DeleteMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: yield DeleteMovieHandler( access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), - rating_gateway=gateway_factory.rating(), - review_gateway=gateway_factory.review(), - unit_of_work=gateway_factory.unit_of_work(), - identity_provider=identity_provider, - ) - - @contextmanager - def get_movie_ratings( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetMovieRatingsHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMovieRatingsHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), - rating_gateway=gateway_factory.rating(), - identity_provider=identity_provider, - ) - - @contextmanager - def get_my_ratings( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetMyRatingsHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMyRatingsHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - rating_gateway=gateway_factory.rating(), - identity_provider=identity_provider, - ) - - @contextmanager - def get_rating( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetRatingHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetRatingHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - rating_gateway=gateway_factory.rating(), + permissions_gateway=self._permissions_mapper, + movie_gateway=MovieMapper(sqlalchemy_connection), + rating_gateway=RatingMapper(sqlalchemy_connection), + review_gateway=ReviewMapper(sqlalchemy_connection), + unit_of_work=sqlalchemy_connection, identity_provider=identity_provider, ) @@ -204,17 +164,15 @@ def rate_movie( self, identity_provider: IdentityProvider, ) -> Iterator[RateMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: yield RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), - permissions_gateway=self._permissions_gateway, - user_gateway=gateway_factory.user(), - movie_gateway=gateway_factory.movie(), - rating_gateway=gateway_factory.rating(), - unit_of_work=gateway_factory.unit_of_work(), + permissions_gateway=self._permissions_mapper, + user_gateway=UserMapper(sqlalchemy_connection), + movie_gateway=MovieMapper(sqlalchemy_connection), + rating_gateway=RatingMapper(sqlalchemy_connection), + unit_of_work=sqlalchemy_connection, identity_provider=identity_provider, ) @@ -223,63 +181,25 @@ def unrate_movie( self, identity_provider: IdentityProvider, ) -> Iterator[UnrateMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: yield UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), - rating_gateway=gateway_factory.rating(), - unit_of_work=gateway_factory.unit_of_work(), - identity_provider=identity_provider, - ) - - @contextmanager - def get_movie_reviews( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetMovieReviewsHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMovieReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - movie_gateway=gateway_factory.movie(), - review_gateway=gateway_factory.review(), - identity_provider=identity_provider, - ) - - @contextmanager - def get_my_reviews( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetMyReviewsHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetMyReviewsHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - review_gateway=gateway_factory.review(), + permissions_gateway=self._permissions_mapper, + movie_gateway=MovieMapper(sqlalchemy_connection), + rating_gateway=RatingMapper(sqlalchemy_connection), + unit_of_work=sqlalchemy_connection, identity_provider=identity_provider, ) @contextmanager - def get_review( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetReviewHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: - yield GetReviewHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_gateway, - review_gateway=gateway_factory.review(), - identity_provider=identity_provider, + def get_reviews(self) -> Iterator[GetReviewsHandler]: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: + yield GetReviewsHandler( + movie_gateway=MovieMapper(sqlalchemy_connection), + review_view_model_reader=ReviewViewModelMapper( + sqlalchemy_connection, + ), ) @contextmanager @@ -287,16 +207,14 @@ def review_movie( self, identity_provider: IdentityProvider, ) -> Iterator[ReviewMovieHandler]: - with build_sqlalchemy_gateway_factory( - self._sessionmaker - ) as gateway_factory: + with self._sqlalchemy_engine.connect() as sqlalchemy_connection: yield ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), - permissions_gateway=self._permissions_gateway, - user_gateway=gateway_factory.user(), - movie_gateway=gateway_factory.movie(), - review_gateway=gateway_factory.review(), - unit_of_work=gateway_factory.unit_of_work(), + permissions_gateway=self._permissions_mapper, + user_gateway=UserMapper(sqlalchemy_connection), + movie_gateway=MovieMapper(sqlalchemy_connection), + review_gateway=ReviewMapper(sqlalchemy_connection), + unit_of_work=sqlalchemy_connection, identity_provider=identity_provider, ) diff --git a/src/amdb/main/web_api/__main__.py b/src/amdb/main/web_api/__main__.py index 60d67f1..f321b67 100644 --- a/src/amdb/main/web_api/__main__.py +++ b/src/amdb/main/web_api/__main__.py @@ -1,26 +1,37 @@ import asyncio +import os from uvicorn import Server, Config -from amdb.main.config import build_generic_config -from .config import build_web_api_config +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig +from amdb.infrastructure.auth.session.config import SessionConfig +from .config import WebAPIConfig from .app import create_app async def main() -> None: - web_api_config = build_web_api_config() - generic_config = build_generic_config() + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + web_api_config = WebAPIConfig.from_toml(path_to_config) + postgres_config = PostgresConfig.from_toml(path_to_config) + redis_config = RedisConfig.from_toml(path_to_config) + session_config = SessionConfig.from_toml(path_to_config) app = create_app( - fastapi_config=web_api_config.fastapi, - session_config=web_api_config.session, - generic_config=generic_config, + web_api_config=web_api_config, + postgres_config=postgres_config, + redis_config=redis_config, + session_config=session_config, ) server = Server( Config( app=app, - host=web_api_config.uvicorn.host, - port=web_api_config.uvicorn.port, + host=web_api_config.host, + port=web_api_config.port, ), ) diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index d19e906..dfa1555 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -1,25 +1,28 @@ from fastapi import FastAPI +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.auth.session.config import SessionConfig from amdb.presentation.web_api.exception_handlers import ( setup_exception_handlers, ) from amdb.presentation.web_api.routers.setup import setup_routers -from amdb.main.config import GenericConfig -from .config import FastAPIConfig from .di import setup_dependecies +from .config import WebAPIConfig def create_app( - fastapi_config: FastAPIConfig, + web_api_config: WebAPIConfig, + postgres_config: PostgresConfig, + redis_config: RedisConfig, session_config: SessionConfig, - generic_config: GenericConfig, ) -> FastAPI: - app = FastAPI(version=fastapi_config.version) + app = FastAPI(version=web_api_config.version) setup_dependecies( app=app, session_config=session_config, - generic_config=generic_config, + postgres_config=postgres_config, + redis_config=redis_config, ) setup_exception_handlers(app) setup_routers(app) diff --git a/src/amdb/main/web_api/config.py b/src/amdb/main/web_api/config.py index a490322..60a0108 100644 --- a/src/amdb/main/web_api/config.py +++ b/src/amdb/main/web_api/config.py @@ -1,59 +1,22 @@ -import os from dataclasses import dataclass -from datetime import timedelta +from typing import Union +from os import PathLike -from amdb.infrastructure.auth.session.config import SessionConfig - - -FASTAPI_VERSION_ENV = "FASTAPI_VERSION" - -UVICORN_HOST_ENV = "UVICORN_HOST" -UVICORN_PORT_ENV = "UVICORN_PORT" - -SESSION_LIFETIME_ENV = "SESSION_LIFETIME" - - -def build_web_api_config() -> "WebAPIConfig": - fastapi_config = FastAPIConfig( - version=_get_env(FASTAPI_VERSION_ENV), - ) - uvicorn_config = UvicornConfig( - host=_get_env(UVICORN_HOST_ENV), - port=int(_get_env(UVICORN_PORT_ENV)), - ) - session_config = SessionConfig( - session_lifetime=timedelta( - minutes=int(_get_env(SESSION_LIFETIME_ENV)) - ), - ) - return WebAPIConfig( - fastapi=fastapi_config, - uvicorn=uvicorn_config, - session=session_config, - ) - - -def _get_env(key: str) -> str: - value = os.getenv(key) - if value is None: - message = f"Env variable {key} is not set" - raise ValueError(message) - return value +import toml @dataclass(frozen=True, slots=True) -class FastAPIConfig: +class WebAPIConfig: version: str - - -@dataclass(frozen=True, slots=True) -class UvicornConfig: host: str port: int - -@dataclass(frozen=True, slots=True) -class WebAPIConfig: - fastapi: FastAPIConfig - uvicorn: UvicornConfig - session: SessionConfig + @classmethod + def from_toml(cls, path: Union[PathLike, str]) -> "WebAPIConfig": + toml_as_dict = toml.load(path) + web_api_section_as_dict = toml_as_dict["web-api"] + return WebAPIConfig( + version=web_api_section_as_dict["version"], + host=web_api_section_as_dict["host"], + port=web_api_section_as_dict["port"], + ) diff --git a/src/amdb/main/web_api/di.py b/src/amdb/main/web_api/di.py index 948002c..2a03b52 100644 --- a/src/amdb/main/web_api/di.py +++ b/src/amdb/main/web_api/di.py @@ -1,53 +1,46 @@ +from typing import cast + from fastapi import FastAPI from sqlalchemy import create_engine -from sqlalchemy.orm.session import sessionmaker from redis.client import Redis -from amdb.infrastructure.security.hasher import Hasher +from amdb.infrastructure.auth.session.session_processor import SessionProcessor +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.auth.session.config import SessionConfig -from amdb.infrastructure.persistence.redis.gateways.session import ( - RedisSessionGateway, -) -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper +from amdb.infrastructure.persistence.redis.mappers.permissions import ( + PermissionsMapper, ) -from amdb.infrastructure.auth.session.session_processor import SessionProcessor +from amdb.infrastructure.password_manager.hash_computer import HashComputer from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub -from amdb.main.config import GenericConfig from amdb.main.ioc import IoC def setup_dependecies( app: FastAPI, session_config: SessionConfig, - generic_config: GenericConfig, + postgres_config: PostgresConfig, + redis_config: RedisConfig, ) -> None: - redis = Redis( - host=generic_config.redis.host, - port=generic_config.redis.port, - db=generic_config.redis.db, - password=generic_config.redis.password, - decode_responses=True, - ) - redis_session_gateway = RedisSessionGateway( - redis=redis, - session_lifetime=session_config.session_lifetime, + redis = Redis.from_url(redis_config.url, decode_responses=True) + session_mapper = SessionMapper( + redis=cast(Redis, redis), + session_lifetime=session_config.lifetime, ) - app.dependency_overrides[Stub(RedisSessionGateway)] = ( - lambda: redis_session_gateway - ) # type: ignore + app.dependency_overrides[Stub(SessionMapper)] = lambda: session_mapper # type: ignore - redis_permissions_gateway = RedisPermissionsGateway(redis) - app.dependency_overrides[Stub(RedisPermissionsGateway)] = ( - lambda: redis_permissions_gateway + permissions_mapper = PermissionsMapper(cast(Redis, redis)) + app.dependency_overrides[Stub(PermissionsMapper)] = ( + lambda: permissions_mapper ) # type: ignore - engine = create_engine(generic_config.postgres.dsn) + sqlalchemy_engine = create_engine(postgres_config.url) ioc = IoC( - sessionmaker=sessionmaker(engine), - permissions_gateway=redis_permissions_gateway, - hasher=Hasher(), + sqlalchemy_engine=sqlalchemy_engine, + permissions_mapper=permissions_mapper, + hash_computer=HashComputer(), ) app.dependency_overrides[HandlerFactory] = lambda: ioc # type: ignore diff --git a/src/amdb/presentation/cli/movie.py b/src/amdb/presentation/cli/movie.py index 7cbad71..a430a1d 100644 --- a/src/amdb/presentation/cli/movie.py +++ b/src/amdb/presentation/cli/movie.py @@ -8,116 +8,15 @@ import rich.table from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.create_movie import CreateMovieCommand from amdb.application.commands.delete_movie import DeleteMovieCommand -from amdb.application.queries.get_movies import GetMoviesQuery -from amdb.application.queries.get_movie import GetMovieQuery from amdb.presentation.handler_factory import HandlerFactory movie_commands = typer.Typer(name="movie") -@movie_commands.command() -def list( - ctx: typer.Context, - limit: Annotated[ - int, - typer.Option( - "--limit", - "-l", - help="Number of movies that should be [blue]listed[/blue].", - max=200, - min=1, - ), - ] = 100, - offset: Annotated[ - int, - typer.Option( - "--offset", - "-o", - help="Number of movies that should be offsetted.", - min=0, - ), - ] = 0, -) -> None: - """ - [blue]List[/blue] movies. - """ - ioc: HandlerFactory = ctx.obj["ioc"] - identity_provider: IdentityProvider = ctx.obj["identity_provider"] - - with ioc.get_movies(identity_provider) as get_movies_handler: - get_movies_query = GetMoviesQuery( - limit=limit, - offset=offset, - ) - get_movies_result = get_movies_handler.execute(get_movies_query) - - movies_table = rich.table.Table( - "id", - "title", - "release_date", - "rating", - "rating_count", - box=rich.box.ROUNDED, - ) - for movie in get_movies_result.movies: - movies_table.add_row( - str(movie.id), - movie.title, - str(movie.release_date), - str(movie.rating), - str(movie.rating_count), - ) - - rich.print(movies_table) - rich.print( - f"Listed movie count: {get_movies_result.movie_count}", - f"with limit: {limit}", - f"and offset: {offset}", - ) - - -@movie_commands.command() -def get( - ctx: typer.Context, - movie_id: Annotated[UUID, typer.Argument(help="Movie id.")], -) -> None: - """ - [blue]Get[/blue] movie. - """ - ioc: HandlerFactory = ctx.obj["ioc"] - identity_provider: IdentityProvider = ctx.obj["identity_provider"] - - with ioc.get_movie(identity_provider) as get_movie_handler: - get_movie_query = GetMovieQuery( - movie_id=MovieId(movie_id), - ) - get_movie_result = get_movie_handler.execute(get_movie_query) - - movies_table = rich.table.Table( - "id", - "title", - "release_date", - "rating", - "rating_count", - box=rich.box.ROUNDED, - ) - movies_table.add_row( - str(movie_id), - get_movie_result.title, - str(get_movie_result.release_date), - str(get_movie_result.rating), - str(get_movie_result.rating_count), - ) - - rich.print(movies_table) - - @movie_commands.command() def create( ctx: typer.Context, diff --git a/src/amdb/presentation/handler_factory.py b/src/amdb/presentation/handler_factory.py index 930141b..5ad8d87 100644 --- a/src/amdb/presentation/handler_factory.py +++ b/src/amdb/presentation/handler_factory.py @@ -1,9 +1,7 @@ from abc import ABC, abstractmethod from typing import ContextManager -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.command_handlers.delete_movie import DeleteMovieHandler @@ -11,18 +9,13 @@ from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler from amdb.application.command_handlers.review_movie import ReviewMovieHandler from amdb.application.query_handlers.login import LoginHandler -from amdb.application.query_handlers.get_movies import GetMoviesHandler -from amdb.application.query_handlers.get_movie import GetMovieHandler -from amdb.application.query_handlers.get_movie_ratings import ( - GetMovieRatingsHandler, +from amdb.application.query_handlers.detailed_movie import ( + GetDetailedMovieHandler, ) -from amdb.application.query_handlers.get_my_ratings import GetMyRatingsHandler -from amdb.application.query_handlers.get_rating import GetRatingHandler -from amdb.application.query_handlers.get_movie_reviews import ( - GetMovieReviewsHandler, +from amdb.application.query_handlers.non_detailed_movies import ( + GetNonDetailedMoviesHandler, ) -from amdb.application.query_handlers.get_my_reviews import GetMyReviewsHandler -from amdb.application.query_handlers.get_review import GetReviewHandler +from amdb.application.query_handlers.reviews import GetReviewsHandler class HandlerFactory(ABC): @@ -35,17 +28,17 @@ def login(self) -> ContextManager[LoginHandler]: raise NotImplementedError @abstractmethod - def get_movies( + def get_non_detailed_movies( self, identity_provider: IdentityProvider, - ) -> ContextManager[GetMoviesHandler]: + ) -> ContextManager[GetNonDetailedMoviesHandler]: raise NotImplementedError @abstractmethod - def get_movie( + def get_detailed_movie( self, identity_provider: IdentityProvider, - ) -> ContextManager[GetMovieHandler]: + ) -> ContextManager[GetDetailedMovieHandler]: raise NotImplementedError @abstractmethod @@ -62,27 +55,6 @@ def delete_movie( ) -> ContextManager[DeleteMovieHandler]: raise NotImplementedError - @abstractmethod - def get_movie_ratings( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetMovieRatingsHandler]: - raise NotImplementedError - - @abstractmethod - def get_my_ratings( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetMyRatingsHandler]: - raise NotImplementedError - - @abstractmethod - def get_rating( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetRatingHandler]: - raise NotImplementedError - @abstractmethod def rate_movie( self, @@ -98,23 +70,7 @@ def unrate_movie( raise NotImplementedError @abstractmethod - def get_movie_reviews( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetMovieReviewsHandler]: - raise NotImplementedError - - def get_my_reviews( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetMyReviewsHandler]: - raise NotImplementedError - - @abstractmethod - def get_review( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetReviewHandler]: + def get_reviews(self) -> ContextManager[GetReviewsHandler]: raise NotImplementedError @abstractmethod diff --git a/src/amdb/presentation/web_api/dependencies/identity_provider.py b/src/amdb/presentation/web_api/dependencies/identity_provider.py index 77f8169..3aee77c 100644 --- a/src/amdb/presentation/web_api/dependencies/identity_provider.py +++ b/src/amdb/presentation/web_api/dependencies/identity_provider.py @@ -2,34 +2,31 @@ from fastapi import Cookie, Depends -from amdb.infrastructure.persistence.redis.gateways.session import ( - RedisSessionGateway, -) -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper +from amdb.infrastructure.persistence.redis.mappers.permissions import ( + PermissionsMapper, ) from amdb.infrastructure.auth.session.identity_provider import ( SessionIdentityProvider, ) -from amdb.infrastructure.auth.session.model import SessionId +from amdb.infrastructure.auth.session.session import SessionId from amdb.presentation.web_api.constants import SESSION_ID_COOKIE from .depends_stub import Stub def get_identity_provider( - session_gateway: Annotated[ - RedisSessionGateway, Depends(Stub(RedisSessionGateway)) - ], - permissions_gateway: Annotated[ - RedisPermissionsGateway, - Depends(Stub(RedisPermissionsGateway)), + session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], + permissions_mapper: Annotated[ + PermissionsMapper, + Depends(Stub(PermissionsMapper)), ], session_id: Annotated[ - Optional[str], Cookie(alias=SESSION_ID_COOKIE) + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), ] = None, ) -> SessionIdentityProvider: return SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, - session_gateway=session_gateway, - permissions_gateway=permissions_gateway, + session_gateway=session_mapper, + permissions_gateway=permissions_mapper, ) diff --git a/src/amdb/presentation/web_api/exception_handlers.py b/src/amdb/presentation/web_api/exception_handlers.py index 4408072..b2dcede 100644 --- a/src/amdb/presentation/web_api/exception_handlers.py +++ b/src/amdb/presentation/web_api/exception_handlers.py @@ -10,7 +10,8 @@ def setup_exception_handlers(app: FastAPI) -> None: app.add_exception_handler(DomainError, _domain_error_handler) app.add_exception_handler(ApplicationError, _application_error_handler) app.add_exception_handler( - InfrastructureError, _infrastructure_error_handler + InfrastructureError, + _infrastructure_error_handler, ) @@ -23,6 +24,7 @@ def _application_error_handler(_, error: ApplicationError) -> JSONResponse: def _infrastructure_error_handler( - _, error: InfrastructureError + _, + error: InfrastructureError, ) -> JSONResponse: return JSONResponse(content=None, status_code=500) diff --git a/src/amdb/presentation/web_api/routers/auth/login.py b/src/amdb/presentation/web_api/routers/auth/login.py index 8423b92..7b594ea 100644 --- a/src/amdb/presentation/web_api/routers/auth/login.py +++ b/src/amdb/presentation/web_api/routers/auth/login.py @@ -5,9 +5,7 @@ from amdb.domain.entities.user import UserId from amdb.application.queries.login import LoginQuery from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.gateways.session import ( - RedisSessionGateway, -) +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub from amdb.presentation.web_api.constants import SESSION_ID_COOKIE @@ -16,11 +14,10 @@ async def login( ioc: Annotated[HandlerFactory, Depends()], session_processor: Annotated[ - SessionProcessor, Depends(Stub(SessionProcessor)) - ], - session_gateway: Annotated[ - RedisSessionGateway, Depends(Stub(RedisSessionGateway)) + SessionProcessor, + Depends(Stub(SessionProcessor)), ], + session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], login_query: LoginQuery, response: Response, ) -> UserId: @@ -38,7 +35,7 @@ async def login( user_id = login_handler.execute(login_query) session = session_processor.create(user_id=user_id) - session_gateway.save(session) + session_mapper.save(session) response.set_cookie( key=SESSION_ID_COOKIE, diff --git a/src/amdb/presentation/web_api/routers/auth/register.py b/src/amdb/presentation/web_api/routers/auth/register.py index e25e7c9..c0c38fa 100644 --- a/src/amdb/presentation/web_api/routers/auth/register.py +++ b/src/amdb/presentation/web_api/routers/auth/register.py @@ -5,9 +5,7 @@ from amdb.domain.entities.user import UserId from amdb.application.commands.register_user import RegisterUserCommand from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.gateways.session import ( - RedisSessionGateway, -) +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.depends_stub import Stub from amdb.presentation.web_api.constants import SESSION_ID_COOKIE @@ -16,11 +14,10 @@ async def register( ioc: Annotated[HandlerFactory, Depends()], session_processor: Annotated[ - SessionProcessor, Depends(Stub(SessionProcessor)) - ], - session_gateway: Annotated[ - RedisSessionGateway, Depends(Stub(RedisSessionGateway)) + SessionProcessor, + Depends(Stub(SessionProcessor)), ], + session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], register_user_command: RegisterUserCommand, response: Response, ) -> UserId: @@ -36,7 +33,7 @@ async def register( user_id = register_user_handler.execute(register_user_command) session = session_processor.create(user_id=user_id) - session_gateway.save(session) + session_mapper.save(session) response.set_cookie( key=SESSION_ID_COOKIE, diff --git a/src/amdb/presentation/web_api/routers/movies/get_movie.py b/src/amdb/presentation/web_api/routers/movies/get_movie.py deleted file mode 100644 index 8768121..0000000 --- a/src/amdb/presentation/web_api/routers/movies/get_movie.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie import GetMovieResult, GetMovieQuery -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -def get_movie( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - movie_id: MovieId, -) -> GetMovieResult: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - """ - with ioc.get_movie(identity_provider) as get_movie_handler: - get_movie_query = GetMovieQuery( - movie_id=movie_id, - ) - get_movie_result = get_movie_handler.execute(get_movie_query) - - return get_movie_result diff --git a/src/amdb/presentation/web_api/routers/movies/get_movies.py b/src/amdb/presentation/web_api/routers/movies/get_movies.py index 3794fdc..aa06916 100644 --- a/src/amdb/presentation/web_api/routers/movies/get_movies.py +++ b/src/amdb/presentation/web_api/routers/movies/get_movies.py @@ -2,33 +2,68 @@ from fastapi import Depends -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, +from amdb.domain.entities.movie import MovieId +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.queries.detailed_movie import GetDetailedMovieQuery +from amdb.application.queries.non_detailed_movies import ( + GetNonDetailedMoviesQuery, +) +from amdb.application.common.view_models.detailed_movie import ( + DetailedMovieViewModel, +) +from amdb.application.common.view_models.non_detailed_movie import ( + NonDetailedMovieViewModel, ) from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.identity_provider import ( get_identity_provider, ) -from amdb.application.queries.get_movies import GetMoviesQuery, GetMoviesResult -async def get_movies( +async def get_non_detailed_movies( ioc: Annotated[HandlerFactory, Depends()], identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) + IdentityProvider, + Depends(get_identity_provider), ], limit: int = 100, offset: int = 0, -) -> GetMoviesResult: +) -> list[NonDetailedMovieViewModel]: """ ## Errors: \n - When access is denied \n """ - with ioc.get_movies(identity_provider) as get_movies_handler: - get_movies_query = GetMoviesQuery( + with ioc.get_non_detailed_movies( + identity_provider, + ) as get_non_detailed_movies_handler: + get_non_detailed_movies_query = GetNonDetailedMoviesQuery( limit=limit, offset=offset, ) - get_movies_result = get_movies_handler.execute(get_movies_query) + result = get_non_detailed_movies_handler.execute( + get_non_detailed_movies_query, + ) + return result - return get_movies_result + +async def get_detailed_movie( + ioc: Annotated[HandlerFactory, Depends()], + identity_provider: Annotated[ + IdentityProvider, + Depends(get_identity_provider), + ], + movie_id: MovieId, +) -> DetailedMovieViewModel: + """ + ## Errors: \n + - When access is denied \n + - When movie doesn't exist \n + """ + with ioc.get_detailed_movie( + identity_provider, + ) as get_detailed_movie_handler: + get_detailed_movie_query = GetDetailedMovieQuery( + movie_id=movie_id, + ) + result = get_detailed_movie_handler.execute(get_detailed_movie_query) + return result diff --git a/src/amdb/presentation/web_api/routers/movies/router.py b/src/amdb/presentation/web_api/routers/movies/router.py index 1ccfc4d..890b974 100644 --- a/src/amdb/presentation/web_api/routers/movies/router.py +++ b/src/amdb/presentation/web_api/routers/movies/router.py @@ -1,23 +1,22 @@ from fastapi import APIRouter -from .get_movies import get_movies -from .get_movie import get_movie +from .get_movies import get_non_detailed_movies, get_detailed_movie def create_movies_router() -> APIRouter: router = APIRouter( - prefix="/movies", + prefix="", tags=["movies"], ) router.add_api_route( - path="", - endpoint=get_movies, + path="/non-detailed-movies", + endpoint=get_non_detailed_movies, methods=["GET"], ) router.add_api_route( - path="/{movie_id}", - endpoint=get_movie, + path="/detailed-movies/{movie_id}", + endpoint=get_detailed_movie, methods=["GET"], ) diff --git a/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py b/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py deleted file mode 100644 index 660eec9..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/get_movie_ratings.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie_ratings import ( - GetMovieRatingsQuery, - GetMovieRatingsResult, -) -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_movie_ratings( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - movie_id: MovieId, - limit: int = 100, - offset: int = 0, -) -> GetMovieRatingsResult: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - """ - with ioc.get_movie_ratings(identity_provider) as get_movie_ratings_handler: - get_movie_ratings_query = GetMovieRatingsQuery( - movie_id=movie_id, - limit=limit, - offset=offset, - ) - get_movie_ratings_result = get_movie_ratings_handler.execute( - get_movie_ratings_query - ) - - return get_movie_ratings_result diff --git a/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py b/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py deleted file mode 100644 index f0aa378..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/get_my_ratings.py +++ /dev/null @@ -1,39 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_my_ratings import ( - GetMyRatingsQuery, - GetMyRatingsResult, -) -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_my_ratings( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - limit: int = 100, - offset: int = 0, -) -> GetMyRatingsResult: - """ - Errors: \n - - When access is denied \n - """ - with ioc.get_my_ratings(identity_provider) as get_my_ratings_handler: - get_my_ratings_query = GetMyRatingsQuery( - limit=limit, - offset=offset, - ) - get_my_ratings_result = get_my_ratings_handler.execute( - get_my_ratings_query - ) - - return get_my_ratings_result diff --git a/src/amdb/presentation/web_api/routers/ratings/get_rating.py b/src/amdb/presentation/web_api/routers/ratings/get_rating.py deleted file mode 100644 index 2cb994e..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/get_rating.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_rating import GetRatingQuery, GetRatingResult -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_rating( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - rating_id: RatingId, -) -> GetRatingResult: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - - When rating doesn't exist \n - """ - with ioc.get_rating(identity_provider) as get_rating_handler: - get_rating_query = GetRatingQuery( - rating_id=rating_id, - ) - get_rating_result = get_rating_handler.execute(get_rating_query) - - return get_rating_result diff --git a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py b/src/amdb/presentation/web_api/routers/ratings/rate_movie.py index 6ba1727..585a648 100644 --- a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py +++ b/src/amdb/presentation/web_api/routers/ratings/rate_movie.py @@ -3,9 +3,7 @@ from fastapi import Depends from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.rate_movie import RateMovieCommand from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.identity_provider import ( @@ -16,7 +14,8 @@ async def rate_movie( ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) + IdentityProvider, + Depends(get_identity_provider), ], rate_movie_command: RateMovieCommand, ) -> RatingId: diff --git a/src/amdb/presentation/web_api/routers/ratings/router.py b/src/amdb/presentation/web_api/routers/ratings/router.py index 224b3ba..26fbd93 100644 --- a/src/amdb/presentation/web_api/routers/ratings/router.py +++ b/src/amdb/presentation/web_api/routers/ratings/router.py @@ -1,8 +1,5 @@ from fastapi import APIRouter -from .get_movie_ratings import get_movie_ratings -from .get_my_ratings import get_my_ratings -from .get_rating import get_rating from .rate_movie import rate_movie from .unrate_movie import unrate_movie @@ -13,35 +10,12 @@ def create_ratings_router() -> APIRouter: tags=["ratings"], ) - router.add_api_route( - path="/movies/{movie_id}/ratings", - endpoint=get_movie_ratings, - methods=["GET"], - tags=["movies"], - ) - router.add_api_route( - path="/ratings/{rating_id}", - endpoint=get_rating, - methods=["GET"], - ) router.add_api_route( path="/me/ratings", endpoint=rate_movie, methods=["POST"], tags=["me"], ) - router.add_api_route( - path="/me/ratings", - endpoint=get_my_ratings, - methods=["GET"], - tags=["me"], - ) - router.add_api_route( - path="/me/ratings/{rating_id}", - endpoint=get_rating, - methods=["GET"], - tags=["me"], - ) router.add_api_route( path="/me/ratings/{rating_id}", endpoint=unrate_movie, diff --git a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py b/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py index 08c30c6..e0b9f4f 100644 --- a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py +++ b/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py @@ -3,9 +3,7 @@ from fastapi import Depends from amdb.domain.entities.rating import RatingId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.unrate_movie import UnrateMovieCommand from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.identity_provider import ( @@ -16,7 +14,8 @@ async def unrate_movie( ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) + IdentityProvider, + Depends(get_identity_provider), ], rating_id: RatingId, ) -> None: diff --git a/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py deleted file mode 100644 index e4c6bdc..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/get_movie_reviews.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_movie_reviews import ( - GetMovieReviewsQuery, - GetMovieReviewsResult, -) -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_movie_reviews( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - movie_id: MovieId, - limit: int = 100, - offset: int = 0, -) -> GetMovieReviewsResult: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - """ - with ioc.get_movie_reviews(identity_provider) as get_movie_reviews_handler: - get_movie_reviews_query = GetMovieReviewsQuery( - movie_id=movie_id, - limit=limit, - offset=offset, - ) - get_movie_reviews_result = get_movie_reviews_handler.execute( - get_movie_reviews_query - ) - - return get_movie_reviews_result diff --git a/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py deleted file mode 100644 index 60942e5..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/get_my_reviews.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_my_reviews import ( - GetMyReviewsQuery, - GetMyReviewsResult, -) -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_my_reviews( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - limit: int = 100, - offset: int = 0, -) -> GetMyReviewsResult: - with ioc.get_my_reviews(identity_provider) as get_my_reviews_handler: - get_my_reviews_query = GetMyReviewsQuery( - limit=limit, - offset=offset, - ) - get_my_reviews_result = get_my_reviews_handler.execute( - get_my_reviews_query - ) - - return get_my_reviews_result diff --git a/src/amdb/presentation/web_api/routers/reviews/get_review.py b/src/amdb/presentation/web_api/routers/reviews/get_review.py deleted file mode 100644 index 5c1d7a2..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/get_review.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.review import ReviewId -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) -from amdb.application.queries.get_review import GetReviewQuery, GetReviewResult -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_review( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) - ], - review_id: ReviewId, -) -> GetReviewResult: - """ - ## Errors: \n - - When access is denied \n - - When review doesn't exist \n - """ - with ioc.get_review(identity_provider) as get_review_handler: - get_review_query = GetReviewQuery( - review_id=review_id, - ) - get_review_result = get_review_handler.execute(get_review_query) - - return get_review_result diff --git a/src/amdb/presentation/web_api/routers/reviews/get_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_reviews.py new file mode 100644 index 0000000..70c5962 --- /dev/null +++ b/src/amdb/presentation/web_api/routers/reviews/get_reviews.py @@ -0,0 +1,28 @@ +from typing import Annotated + +from fastapi import Depends + +from amdb.domain.entities.movie import MovieId +from amdb.application.common.view_models.review import ReviewViewModel +from amdb.application.queries.reviews import GetReviewsQuery +from amdb.presentation.handler_factory import HandlerFactory + + +async def get_reviews( + ioc: Annotated[HandlerFactory, Depends()], + movie_id: MovieId, + limit: int = 100, + offset: int = 0, +) -> list[ReviewViewModel]: + """ + ## Errors: \n + - When movie doesn't exist \n + """ + with ioc.get_reviews() as get_reviews_handler: + get_reviews_query = GetReviewsQuery( + movie_id=movie_id, + limit=limit, + offset=offset, + ) + result = get_reviews_handler.execute(get_reviews_query) + return result diff --git a/src/amdb/presentation/web_api/routers/reviews/review_movie.py b/src/amdb/presentation/web_api/routers/reviews/review_movie.py index 2e47a0a..0498eb9 100644 --- a/src/amdb/presentation/web_api/routers/reviews/review_movie.py +++ b/src/amdb/presentation/web_api/routers/reviews/review_movie.py @@ -5,9 +5,7 @@ from amdb.domain.entities.movie import MovieId from amdb.domain.entities.review import ReviewId, ReviewType -from amdb.application.common.interfaces.identity_provider import ( - IdentityProvider, -) +from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.review_movie import ReviewMovieCommand from amdb.presentation.handler_factory import HandlerFactory from amdb.presentation.web_api.dependencies.identity_provider import ( @@ -24,7 +22,8 @@ class ReviewMovie(BaseModel): async def review_movie( ioc: Annotated[HandlerFactory, Depends()], identity_provider: Annotated[ - IdentityProvider, Depends(get_identity_provider) + IdentityProvider, + Depends(get_identity_provider), ], movie_id: MovieId, data: ReviewMovie, diff --git a/src/amdb/presentation/web_api/routers/reviews/router.py b/src/amdb/presentation/web_api/routers/reviews/router.py index 4e15602..4fb4ce9 100644 --- a/src/amdb/presentation/web_api/routers/reviews/router.py +++ b/src/amdb/presentation/web_api/routers/reviews/router.py @@ -1,9 +1,7 @@ from fastapi import APIRouter -from .get_movie_reviews import get_movie_reviews -from .get_my_reviews import get_my_reviews +from .get_reviews import get_reviews from .review_movie import review_movie -from .get_review import get_review def create_reviews_router() -> APIRouter: @@ -14,13 +12,7 @@ def create_reviews_router() -> APIRouter: router.add_api_route( path="/movies/{movie_id}/reviews", - endpoint=get_movie_reviews, - methods=["GET"], - tags=["movies"], - ) - router.add_api_route( - path="/reviews/{review_id}", - endpoint=get_review, + endpoint=get_reviews, methods=["GET"], ) router.add_api_route( @@ -29,17 +21,5 @@ def create_reviews_router() -> APIRouter: methods=["POST"], tags=["me"], ) - router.add_api_route( - path="/me/reviews", - endpoint=get_my_reviews, - methods=["GET"], - tags=["me"], - ) - router.add_api_route( - path="/me/reviews/{review_id}", - endpoint=get_review, - methods=["GET"], - tags=["me"], - ) return router diff --git a/tests/unit/application/command_handlers/test_create_movie.py b/tests/unit/application/command_handlers/test_create_movie.py index 3486bd8..5cbbb86 100644 --- a/tests/unit/application/command_handlers/test_create_movie.py +++ b/tests/unit/application/command_handlers/test_create_movie.py @@ -24,7 +24,7 @@ def identity_provider_with_correct_permissions( identity_provider = Mock() correct_permissions = permissions_gateway.for_create_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) + identity_provider.permissions = Mock(return_value=correct_permissions) return identity_provider diff --git a/tests/unit/application/command_handlers/test_delete_movie.py b/tests/unit/application/command_handlers/test_delete_movie.py index be66daa..acad807 100644 --- a/tests/unit/application/command_handlers/test_delete_movie.py +++ b/tests/unit/application/command_handlers/test_delete_movie.py @@ -28,7 +28,7 @@ def identity_provider_with_correct_permissions( identity_provider = Mock() correct_permissions = permissions_gateway.for_delete_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) + identity_provider.permissions = Mock(return_value=correct_permissions) return identity_provider diff --git a/tests/unit/application/command_handlers/test_rate_movie.py b/tests/unit/application/command_handlers/test_rate_movie.py index 559f0ef..e6b35f2 100644 --- a/tests/unit/application/command_handlers/test_rate_movie.py +++ b/tests/unit/application/command_handlers/test_rate_movie.py @@ -34,7 +34,7 @@ def identity_provider_with_correct_permissions( identity_provider = Mock() correct_permissions = permissions_gateway.for_rate_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) + identity_provider.permissions = Mock(return_value=correct_permissions) return identity_provider @@ -64,7 +64,7 @@ def test_rate_movie( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) @@ -178,7 +178,7 @@ def test_rate_movie_should_raise_error_when_movie_already_rated( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) @@ -242,7 +242,7 @@ def test_rate_movie_should_raise_error_when_rating_is_invalid( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) diff --git a/tests/unit/application/command_handlers/test_review_movie.py b/tests/unit/application/command_handlers/test_review_movie.py index fd5982d..ecccbc0 100644 --- a/tests/unit/application/command_handlers/test_review_movie.py +++ b/tests/unit/application/command_handlers/test_review_movie.py @@ -32,7 +32,7 @@ def identity_provider_with_correct_permissions( identity_provider = Mock() correct_permissions = permissions_gateway.for_review_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) + identity_provider.permissions = Mock(return_value=correct_permissions) return identity_provider @@ -62,7 +62,7 @@ def test_review_movie( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) @@ -184,7 +184,7 @@ def test_review_movie_should_raise_error_when_movie_already_reviewed( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) diff --git a/tests/unit/application/command_handlers/test_unrate_movie.py b/tests/unit/application/command_handlers/test_unrate_movie.py index c3ba201..bc5d2b1 100644 --- a/tests/unit/application/command_handlers/test_unrate_movie.py +++ b/tests/unit/application/command_handlers/test_unrate_movie.py @@ -32,7 +32,7 @@ def identity_provider_with_correct_permissions( identity_provider = Mock() correct_permissions = permissions_gateway.for_unrate_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) + identity_provider.permissions = Mock(return_value=correct_permissions) return identity_provider @@ -71,7 +71,7 @@ def test_unrate_movie( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=user.id, ) @@ -177,7 +177,7 @@ def test_unrate_movie_should_raise_error_when_user_is_not_rating_owner( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider_with_correct_permissions.user_id = Mock( return_value=UserId(uuid7()), ) diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index 76a8d77..0af152e 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -2,49 +2,44 @@ from unittest.mock import Mock import pytest -from sqlalchemy import Engine, create_engine -from sqlalchemy.orm import Session +from sqlalchemy import Connection, Engine, create_engine from redis.client import Redis from amdb.infrastructure.persistence.sqlalchemy.models.base import Model -from amdb.infrastructure.persistence.sqlalchemy.gateways.user import ( - SQLAlchemyUserGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( + UserMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.gateways.movie import ( - SQLAlchemyMovieGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( + MovieMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.gateways.rating import ( - SQLAlchemyRatingGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( + RatingMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.gateways.user_password_hash import ( - SQLAlchemyUserPasswordHashGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( + ReviewMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.gateways.review import ( - SQLAlchemyReviewGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( + PasswordHashMapper, ) -from amdb.infrastructure.persistence.redis.gateways.permissions import ( - RedisPermissionsGateway, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( + NonDetailedMovieViewModelMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.user import UserMapper -from amdb.infrastructure.persistence.sqlalchemy.mappers.movie import ( - MovieMapper, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( + DetailedMovieViewModelMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.rating import ( - RatingMapper, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.review import ( + ReviewViewModelMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.user_password_hash import ( - UserPasswordHashMapper, +from amdb.infrastructure.persistence.redis.mappers.permissions import ( + PermissionsMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.review import ( - ReviewMapper, -) -from amdb.infrastructure.security.hasher import Hasher +from amdb.infrastructure.password_manager.hash_computer import HashComputer from amdb.infrastructure.password_manager.password_manager import ( HashingPasswordManager, ) -@pytest.fixture(scope="package") +@pytest.fixture(scope="session") def sqlalchemy_engine(postgres_url: str) -> Engine: return create_engine(url=postgres_url) @@ -57,61 +52,74 @@ def clear_database(sqlalchemy_engine: Engine) -> Iterator[None]: @pytest.fixture -def sqlalchemy_session(sqlalchemy_engine: Engine) -> Iterator[Session]: +def sqlalchemy_connection(sqlalchemy_engine: Engine) -> Iterator[Connection]: connection = sqlalchemy_engine.connect() - session = Session(connection, expire_on_commit=False) + yield connection + connection.close() - yield session - session.close() - connection.close() +@pytest.fixture +def permissions_gateway(redis: Redis) -> PermissionsMapper: + return PermissionsMapper(redis) @pytest.fixture -def permissions_gateway(redis: Redis) -> RedisPermissionsGateway: - return RedisPermissionsGateway(redis) +def user_gateway(sqlalchemy_connection: Connection) -> UserMapper: + return UserMapper(sqlalchemy_connection) @pytest.fixture -def user_gateway(sqlalchemy_session: Session) -> SQLAlchemyUserGateway: - return SQLAlchemyUserGateway(sqlalchemy_session, UserMapper()) +def movie_gateway(sqlalchemy_connection: Connection) -> MovieMapper: + return MovieMapper(sqlalchemy_connection) @pytest.fixture -def movie_gateway(sqlalchemy_session: Session) -> SQLAlchemyMovieGateway: - return SQLAlchemyMovieGateway(sqlalchemy_session, MovieMapper()) +def rating_gateway(sqlalchemy_connection: Connection) -> RatingMapper: + return RatingMapper(sqlalchemy_connection) @pytest.fixture -def rating_gateway(sqlalchemy_session: Session) -> SQLAlchemyRatingGateway: - return SQLAlchemyRatingGateway(sqlalchemy_session, RatingMapper()) +def review_gateway(sqlalchemy_connection: Connection) -> ReviewMapper: + return ReviewMapper(sqlalchemy_connection) @pytest.fixture -def review_gateway(sqlalchemy_session: Session) -> SQLAlchemyReviewGateway: - return SQLAlchemyReviewGateway(sqlalchemy_session, ReviewMapper()) +def detailed_movie_reader( + sqlalchemy_connection: Connection, +) -> DetailedMovieViewModelMapper: + return DetailedMovieViewModelMapper(sqlalchemy_connection) @pytest.fixture -def password_manager(sqlalchemy_session: Session) -> HashingPasswordManager: - user_password_hash_gateway = SQLAlchemyUserPasswordHashGateway( - session=sqlalchemy_session, - mapper=UserPasswordHashMapper(), - ) +def non_detailed_movie_reader( + sqlalchemy_connection: Connection, +) -> NonDetailedMovieViewModelMapper: + return NonDetailedMovieViewModelMapper(sqlalchemy_connection) + + +@pytest.fixture +def review_reader(sqlalchemy_connection: Connection) -> ReviewViewModelMapper: + return ReviewViewModelMapper(sqlalchemy_connection) + + +@pytest.fixture +def password_manager( + sqlalchemy_connection: Connection, +) -> HashingPasswordManager: return HashingPasswordManager( - hasher=Hasher(), - user_password_hash_gateway=user_password_hash_gateway, + hash_computer=HashComputer(), + password_hash_gateway=PasswordHashMapper(sqlalchemy_connection), ) @pytest.fixture -def unit_of_work(sqlalchemy_session: Session) -> Session: - return sqlalchemy_session +def unit_of_work(sqlalchemy_connection: Connection) -> Connection: + return sqlalchemy_connection @pytest.fixture(scope="session") def identity_provider_with_incorrect_permissions() -> Mock: identity_provider = Mock() - identity_provider.get_permissions = Mock(return_value=0) + identity_provider.permissions = Mock(return_value=0) return identity_provider diff --git a/tests/unit/application/query_handlers/test_detailed_movie.py b/tests/unit/application/query_handlers/test_detailed_movie.py index 8331c92..c86e5fd 100644 --- a/tests/unit/application/query_handlers/test_detailed_movie.py +++ b/tests/unit/application/query_handlers/test_detailed_movie.py @@ -8,14 +8,14 @@ from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.rating import RatingId, Rating from amdb.domain.entities.review import ReviewId, ReviewType, Review -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.readers.detailed_movie import ( + DetailedMovieViewModelReader, +) from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.detailed_movie import ( UserRating, @@ -23,36 +23,20 @@ DetailedMovieViewModel, ) from amdb.application.queries.detailed_movie import GetDetailedMovieQuery -from amdb.application.query_handlers.detailed_movie import GetDetailedMovieHandler -from amdb.application.common.constants.exceptions import ( - GET_MOVIE_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, +from amdb.application.query_handlers.detailed_movie import ( + GetDetailedMovieHandler, ) +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider - - - def test_get_detailed_movie( user_gateway: UserGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, - identity_provider_with_correct_permissions: IdentityProvider, + detailed_movie_reader: DetailedMovieViewModelReader, ): user = User( id=UserId(uuid7()), @@ -91,7 +75,8 @@ def test_get_detailed_movie( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider: IdentityProvider = Mock() + identity_provider.user_id_or_none = Mock( return_value=user.id, ) @@ -99,16 +84,15 @@ def test_get_detailed_movie( movie_id=movie.id, ) get_detailed_movie_handler = GetDetailedMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_view_model_reader=movie_view_model_reader, - identity_provider=identity_provider_with_correct_permissions, + detailed_movie_reader=detailed_movie_reader, + identity_provider=identity_provider, ) expected_result = DetailedMovieViewModel( id=movie.id, title=movie.title, release_date=movie.release_date, + rating=movie.rating, rating_count=movie.rating_count, user_rating=UserRating( id=rating.id, @@ -120,52 +104,28 @@ def test_get_detailed_movie( title=review.title, content=review.content, type=review.type, - created_at=review.type, - ) + created_at=review.created_at, + ), ) result = get_detailed_movie_handler.execute(get_detailed_movie_query) assert expected_result == result -def test_get_detailed_movie_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_detailed_movie_query = GetDetailedMovieQuery( - movie_id=MovieId(uuid7()), - ) - get_detailed_movie_handler = GetDetailedMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_view_model_reader=movie_view_model_reader, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - get_detailed_movie_handler.execute(get_detailed_movie_query) - - assert error.value.message == GET_MOVIE_ACCESS_DENIED - - def test_get_detailed_movie_should_raise_error_when_movie_does_not_exist( - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, - identity_provider_with_correct_permissions: IdentityProvider, + detailed_movie_reader: DetailedMovieViewModelReader, ): - identity_provider_with_correct_permissions.get_user_id = Mock( - return_value=UserId(uuid7()), + identity_provider: IdentityProvider = Mock() + identity_provider.user_id_or_none = Mock( + return_value=None, ) get_detailed_movie_query = GetDetailedMovieQuery( movie_id=MovieId(uuid7()), ) get_detailed_movie_handler = GetDetailedMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_view_model_reader=movie_view_model_reader, - identity_provider=identity_provider_with_correct_permissions, + detailed_movie_reader=detailed_movie_reader, + identity_provider=identity_provider, ) with pytest.raises(ApplicationError) as error: diff --git a/tests/unit/application/query_handlers/test_get_reviews.py b/tests/unit/application/query_handlers/test_get_reviews.py new file mode 100644 index 0000000..86b4d8b --- /dev/null +++ b/tests/unit/application/query_handlers/test_get_reviews.py @@ -0,0 +1,121 @@ +from datetime import date, datetime, timezone + +import pytest +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import UserId, User +from amdb.domain.entities.movie import MovieId, Movie +from amdb.domain.entities.rating import RatingId, Rating +from amdb.domain.entities.review import ReviewId, ReviewType, Review +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.readers.review import ReviewViewModelReader +from amdb.application.common.view_models.review import ( + UserRating, + UserReview, + ReviewViewModel, +) +from amdb.application.queries.reviews import GetReviewsQuery +from amdb.application.query_handlers.reviews import GetReviewsHandler +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST +from amdb.application.common.exception import ApplicationError + + +def test_get_reviews( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + review_reader: ReviewViewModelReader, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + release_date=date(1999, 3, 31), + rating=8, + rating_count=1, + ) + movie_gateway.save(movie) + + rating = Rating( + id=RatingId(uuid7()), + movie_id=movie.id, + user_id=user.id, + value=8, + created_at=datetime.now(timezone.utc), + ) + rating_gateway.save(rating) + + review = Review( + id=ReviewId(uuid7()), + user_id=user.id, + movie_id=movie.id, + title="Not bad", + content="Great soundtrack", + type=ReviewType.POSITIVE, + created_at=datetime.now(timezone.utc), + ) + review_gateway.save(review) + + unit_of_work.commit() + + get_reviews_query = GetReviewsQuery( + movie_id=movie.id, + limit=10, + offset=0, + ) + get_reviews_handler = GetReviewsHandler( + movie_gateway=movie_gateway, + review_view_model_reader=review_reader, + ) + + expected_result = [ + ReviewViewModel( + user_id=user.id, + user_review=UserReview( + id=review.id, + title=review.title, + content=review.content, + type=review.type, + created_at=review.created_at, + ), + user_rating=UserRating( + id=rating.id, + value=rating.value, + created_at=rating.created_at, + ), + ), + ] + result = get_reviews_handler.execute(get_reviews_query) + + assert expected_result == result + + +def test_get_reviews_should_raise_error_when_movie_does_not_exist( + movie_gateway: MovieGateway, + review_reader: ReviewViewModelReader, +): + get_reviews_query = GetReviewsQuery( + movie_id=MovieId(uuid7()), + limit=10, + offset=0, + ) + get_reviews_handler = GetReviewsHandler( + movie_gateway=movie_gateway, + review_view_model_reader=review_reader, + ) + + with pytest.raises(ApplicationError) as error: + get_reviews_handler.execute(get_reviews_query) + + assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_non_detailed_movies.py b/tests/unit/application/query_handlers/test_non_detailed_movies.py index 6b32a6a..633e9d7 100644 --- a/tests/unit/application/query_handlers/test_non_detailed_movies.py +++ b/tests/unit/application/query_handlers/test_non_detailed_movies.py @@ -1,40 +1,29 @@ from unittest.mock import Mock from datetime import date, datetime, timezone -import pytest from uuid_extensions import uuid7 from amdb.domain.entities.user import UserId, User from amdb.domain.entities.movie import MovieId, Movie from amdb.domain.entities.rating import RatingId, Rating -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.readers.movie import MovieViewModelReader +from amdb.application.common.readers.non_detailed_movie import ( + NonDetailedMovieViewModelReader, +) from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.non_detailed_movie import ( UserRating, NonDetailedMovieViewModel, ) -from amdb.application.queries.non_detailed_movies import GetNonDetailedMoviesQuery -from amdb.application.query_handlers.non_detailed_movies import GetNonDetailedMoviesHandler -from amdb.application.common.constants.exceptions import GET_MOVIE_ACCESS_DENIED -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_get_movie() - identity_provider.get_permissions = Mock(return_value=correct_permissions) - - return identity_provider +from amdb.application.queries.non_detailed_movies import ( + GetNonDetailedMoviesQuery, +) +from amdb.application.query_handlers.non_detailed_movies import ( + GetNonDetailedMoviesHandler, +) def test_get_non_detailed_movies( @@ -42,9 +31,7 @@ def test_get_non_detailed_movies( movie_gateway: MovieGateway, rating_gateway: RatingGateway, unit_of_work: UnitOfWork, - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, - identity_provider_with_correct_permissions: IdentityProvider, + non_detailed_movie_reader: NonDetailedMovieViewModelReader, ): user = User( id=UserId(uuid7()), @@ -72,7 +59,8 @@ def test_get_non_detailed_movies( unit_of_work.commit() - identity_provider_with_correct_permissions.get_user_id = Mock( + identity_provider: IdentityProvider = Mock() + identity_provider.user_id_or_none = Mock( return_value=user.id, ) @@ -81,14 +69,12 @@ def test_get_non_detailed_movies( offset=0, ) get_non_detailed_movies_handler = GetNonDetailedMoviesHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_view_model_reader=movie_view_model_reader, - identity_provider=identity_provider_with_correct_permissions, + non_detailed_movie_reader=non_detailed_movie_reader, + identity_provider=identity_provider, ) expected_result = [ - NonDetailedMovieViewModel( + NonDetailedMovieViewModel( id=movie.id, title=movie.title, release_date=movie.release_date, @@ -99,28 +85,8 @@ def test_get_non_detailed_movies( ), ), ] - result = get_non_detailed_movies_handler.execute(get_non_detailed_movies_query) - - assert expected_result == result - - -def test_get_non_detailed_movies_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_view_model_reader: MovieViewModelReader, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - get_non_detailed_movies_query = GetNonDetailedMoviesQuery( - limit=10, - offset=0, - ) - get_non_detailed_movies_handler = GetNonDetailedMoviesHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_view_model_reader=movie_view_model_reader, - identity_provider=identity_provider_with_incorrect_permissions, + result = get_non_detailed_movies_handler.execute( + get_non_detailed_movies_query, ) - with pytest.raises(ApplicationError) as error: - get_non_detailed_movies_handler.execute(get_non_detailed_movies_query) - - assert error.value.message == GET_MOVIE_ACCESS_DENIED + assert expected_result == result diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index aa4ad28..f2cefb3 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,49 +1,24 @@ import os +from typing import cast import pytest from redis.client import Redis from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig -TEST_POSTGRES_HOST_ENV = "TEST_POSTGRES_HOST" -TEST_POSTGRES_PORT_ENV = "TEST_POSTGRES_PORT" -TEST_POSTGRES_NAME_ENV = "TEST_POSTGRES_DB" -TEST_POSTGRES_USER_ENV = "TEST_POSTGRES_USER" -TEST_POSTGRES_PASSWORD_ENV = "TEST_POSTGRES_PASSWORD" - -TEST_REDIS_HOST_ENV = "TEST_REDIS_HOST" -TEST_REDIS_PORT_ENV = "TEST_REDIS_PORT" -TEST_REDIS_DB_ENV = "TEST_REDIS_DB" -TEST_REDIS_PASSWORD_ENV = "TEST_REDIS_PASSWORD" - - -def _get_env(key: str) -> str: - value = os.getenv(key) - if value is None: - message = f"Env variable {key} is not set" - raise ValueError(message) - return value +CONFIG_PATH = os.getenv("TEST_CONFIG_PATH") @pytest.fixture(scope="session") def postgres_url() -> str: - postgres_config = PostgresConfig( - host=_get_env(TEST_POSTGRES_HOST_ENV), - port=_get_env(TEST_POSTGRES_PORT_ENV), - name=_get_env(TEST_POSTGRES_NAME_ENV), - user=_get_env(TEST_POSTGRES_USER_ENV), - password=_get_env(TEST_POSTGRES_PASSWORD_ENV), - ) - return postgres_config.dsn + postgres_config = PostgresConfig.from_toml(CONFIG_PATH) + return postgres_config.url @pytest.fixture(scope="session") def redis() -> Redis: - redis = Redis( - host=_get_env(TEST_REDIS_HOST_ENV), - port=int(_get_env(TEST_REDIS_PORT_ENV)), - db=int(_get_env(TEST_REDIS_DB_ENV)), - password=_get_env(TEST_REDIS_PASSWORD_ENV), - ) - return redis + redis_config = RedisConfig.from_toml(CONFIG_PATH) + redis = Redis.from_url(redis_config.url, decode_responses=True) + return cast(Redis, redis) diff --git a/tests/unit/infrastructure/alembic/test_stairway.py b/tests/unit/infrastructure/alembic/test_stairway.py index b50338f..31a3ad7 100644 --- a/tests/unit/infrastructure/alembic/test_stairway.py +++ b/tests/unit/infrastructure/alembic/test_stairway.py @@ -7,6 +7,8 @@ https://github.com/alvassin/alembic-quickstart """ +from typing import cast + import alembic.config import alembic.command import alembic.script @@ -20,7 +22,7 @@ def get_revisions( # Get & sort migrations, from first to last revisions: list[alembic.script.Script] = list( - revisions_dir.walk_revisions("base", "heads") + revisions_dir.walk_revisions("base", "heads"), ) revisions.reverse() @@ -33,6 +35,7 @@ def test_migrations_stairway(alembic_config: alembic.config.Config): # We need -1 for downgrading first migration (its down_revision is None) alembic.command.downgrade( - alembic_config, revision.down_revision or "-1" + alembic_config, + cast(str, revision.down_revision) or "-1", ) # type: ignore alembic.command.upgrade(alembic_config, revision.revision) From 28d3f4e37be36e5c937620a7ce13b632e879913e Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 23 Feb 2024 13:49:44 +0400 Subject: [PATCH 04/39] `UnitOfWork`: Remove rollback method --- src/amdb/application/common/unit_of_work.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/amdb/application/common/unit_of_work.py b/src/amdb/application/common/unit_of_work.py index afc756d..c4c6cba 100644 --- a/src/amdb/application/common/unit_of_work.py +++ b/src/amdb/application/common/unit_of_work.py @@ -4,6 +4,3 @@ class UnitOfWork(Protocol): def commit(self) -> None: raise NotImplementedError - - def rollback(self) -> None: - raise NotImplementedError From 3d0ad49668176a9aa8bf316e4335d5a4edf049c9 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 23 Feb 2024 18:30:42 +0400 Subject: [PATCH 05/39] `CreateMovie` and `DeleteMovie` now don't require permissions --- .../command_handlers/create_movie.py | 19 ------ .../command_handlers/delete_movie.py | 22 +------ .../common/constants/exceptions.py | 2 - .../common/gateways/permissions.py | 6 -- .../command_handlers/test_create_movie.py | 52 ----------------- .../command_handlers/test_delete_movie.py | 58 +------------------ 6 files changed, 2 insertions(+), 157 deletions(-) diff --git a/src/amdb/application/command_handlers/create_movie.py b/src/amdb/application/command_handlers/create_movie.py index 76d503f..402f187 100644 --- a/src/amdb/application/command_handlers/create_movie.py +++ b/src/amdb/application/command_handlers/create_movie.py @@ -1,16 +1,10 @@ from uuid_extensions import uuid7 from amdb.domain.entities.movie import MovieId -from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.unit_of_work import UnitOfWork from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import ( - CREATE_MOVIE_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError from amdb.application.commands.create_movie import CreateMovieCommand @@ -18,30 +12,17 @@ class CreateMovieHandler: def __init__( self, *, - access_concern: AccessConcern, create_movie: CreateMovie, - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, unit_of_work: UnitOfWork, identity_provider: IdentityProvider, ) -> None: - self._access_concern = access_concern self._create_movie = create_movie - self._permissions_gateway = permissions_gateway self._movie_gateway = movie_gateway self._unit_of_work = unit_of_work self._identity_provider = identity_provider def execute(self, command: CreateMovieCommand) -> MovieId: - current_permissions = self._identity_provider.permissions() - required_permissions = self._permissions_gateway.for_create_movie() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(CREATE_MOVIE_ACCESS_DENIED) - movie = self._create_movie( id=MovieId(uuid7()), title=command.title, diff --git a/src/amdb/application/command_handlers/delete_movie.py b/src/amdb/application/command_handlers/delete_movie.py index e61e465..6d9502a 100644 --- a/src/amdb/application/command_handlers/delete_movie.py +++ b/src/amdb/application/command_handlers/delete_movie.py @@ -1,16 +1,9 @@ -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import ( - PermissionsGateway, -) from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.common.constants.exceptions import ( - DELETE_MOVIE_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError from amdb.application.commands.delete_movie import DeleteMovieCommand @@ -19,16 +12,12 @@ class DeleteMovieHandler: def __init__( self, *, - access_concern: AccessConcern, - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, identity_provider: IdentityProvider, ) -> None: - self._access_concern = access_concern - self._permissions_gateway = permissions_gateway self._movie_gateway = movie_gateway self._rating_gateway = rating_gateway self._review_gateway = review_gateway @@ -36,15 +25,6 @@ def __init__( self._identity_provider = identity_provider def execute(self, command: DeleteMovieCommand) -> None: - current_permissions = self._identity_provider.permissions() - required_permissions = self._permissions_gateway.for_delete_movie() - access = self._access_concern.authorize( - current_permissions=current_permissions, - required_permissions=required_permissions, - ) - if not access: - raise ApplicationError(DELETE_MOVIE_ACCESS_DENIED) - movie = self._movie_gateway.with_id(command.movie_id) if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index 68aacdd..db05d96 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -1,6 +1,4 @@ LOGIN_ACCESS_DENIED = "Access to login in denied" -CREATE_MOVIE_ACCESS_DENIED = "Access to movie creation is denied" -DELETE_MOVIE_ACCESS_DENIED = "Access to movie deletion is denied" RATE_MOVIE_ACCESS_DENIED = "Access to movie rating is denied" UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied" REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied" diff --git a/src/amdb/application/common/gateways/permissions.py b/src/amdb/application/common/gateways/permissions.py index 501c912..8b34ac7 100644 --- a/src/amdb/application/common/gateways/permissions.py +++ b/src/amdb/application/common/gateways/permissions.py @@ -16,12 +16,6 @@ def for_new_user(self) -> int: def for_login(self) -> int: raise NotImplementedError - def for_create_movie(self) -> int: - raise NotImplementedError - - def for_delete_movie(self) -> int: - raise NotImplementedError - def for_rate_movie(self) -> int: raise NotImplementedError diff --git a/tests/unit/application/command_handlers/test_create_movie.py b/tests/unit/application/command_handlers/test_create_movie.py index 5cbbb86..719cccd 100644 --- a/tests/unit/application/command_handlers/test_create_movie.py +++ b/tests/unit/application/command_handlers/test_create_movie.py @@ -1,76 +1,24 @@ -from unittest.mock import Mock from datetime import date -import pytest - -from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_movie import CreateMovie -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.create_movie import CreateMovieCommand from amdb.application.command_handlers.create_movie import CreateMovieHandler -from amdb.application.common.constants.exceptions import ( - CREATE_MOVIE_ACCESS_DENIED, -) -from amdb.application.common.exception import ApplicationError - - -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_create_movie() - identity_provider.permissions = Mock(return_value=correct_permissions) - - return identity_provider def test_create_movie( - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, ): create_movie_command = CreateMovieCommand( title="Matrix", release_date=date(1999, 3, 31), ) create_movie_handler = CreateMovieHandler( - access_concern=AccessConcern(), create_movie=CreateMovie(), - permissions_gateway=permissions_gateway, movie_gateway=movie_gateway, unit_of_work=unit_of_work, - identity_provider=identity_provider_with_correct_permissions, ) create_movie_handler.execute(create_movie_command) - - -def test_create_movie_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - unit_of_work: UnitOfWork, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - create_movie_command = CreateMovieCommand( - title="Matrix", - release_date=date(1999, 3, 31), - ) - create_movie_handler = CreateMovieHandler( - access_concern=AccessConcern(), - create_movie=CreateMovie(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - create_movie_handler.execute(create_movie_command) - - assert error.value.message == CREATE_MOVIE_ACCESS_DENIED diff --git a/tests/unit/application/command_handlers/test_delete_movie.py b/tests/unit/application/command_handlers/test_delete_movie.py index acad807..5fd6fdb 100644 --- a/tests/unit/application/command_handlers/test_delete_movie.py +++ b/tests/unit/application/command_handlers/test_delete_movie.py @@ -1,45 +1,24 @@ -from unittest.mock import Mock from datetime import date import pytest from uuid_extensions import uuid7 from amdb.domain.entities.movie import MovieId, Movie -from amdb.domain.services.access_concern import AccessConcern -from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.delete_movie import DeleteMovieCommand from amdb.application.command_handlers.delete_movie import DeleteMovieHandler -from amdb.application.common.constants.exceptions import ( - DELETE_MOVIE_ACCESS_DENIED, - MOVIE_DOES_NOT_EXIST, -) +from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError -@pytest.fixture -def identity_provider_with_correct_permissions( - permissions_gateway: PermissionsGateway, -) -> IdentityProvider: - identity_provider = Mock() - - correct_permissions = permissions_gateway.for_delete_movie() - identity_provider.permissions = Mock(return_value=correct_permissions) - - return identity_provider - - def test_delete_movie( - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, ): movie = Movie( id=MovieId(uuid7()), @@ -56,64 +35,29 @@ def test_delete_movie( movie_id=movie.id, ) delete_movie_handler = DeleteMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, movie_gateway=movie_gateway, rating_gateway=rating_gateway, review_gateway=review_gateway, unit_of_work=unit_of_work, - identity_provider=identity_provider_with_correct_permissions, ) delete_movie_handler.execute(delete_movie_command) -def test_delete_movie_should_raise_error_when_access_is_denied( - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - identity_provider_with_incorrect_permissions: IdentityProvider, -): - delete_movie_command = DeleteMovieCommand( - movie_id=MovieId(uuid7()), - ) - delete_movie_handler = DeleteMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - review_gateway=review_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider_with_incorrect_permissions, - ) - - with pytest.raises(ApplicationError) as error: - delete_movie_handler.execute(delete_movie_command) - - assert error.value.message == DELETE_MOVIE_ACCESS_DENIED - - def test_delete_movie_should_raise_error_when_movie_does_not_exist( - permissions_gateway: PermissionsGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - identity_provider_with_correct_permissions: IdentityProvider, ): delete_movie_command = DeleteMovieCommand( movie_id=MovieId(uuid7()), ) delete_movie_handler = DeleteMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=permissions_gateway, movie_gateway=movie_gateway, rating_gateway=rating_gateway, review_gateway=review_gateway, unit_of_work=unit_of_work, - identity_provider=identity_provider_with_correct_permissions, ) with pytest.raises(ApplicationError) as error: From d2fbd05287a21a4ddcbbf3c27518bc21e83a40d8 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 23 Feb 2024 19:54:04 +0400 Subject: [PATCH 06/39] Grammer, remove unused params --- src/amdb/application/command_handlers/create_movie.py | 3 --- src/amdb/application/command_handlers/delete_movie.py | 3 --- src/amdb/application/common/constants/exceptions.py | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/src/amdb/application/command_handlers/create_movie.py b/src/amdb/application/command_handlers/create_movie.py index 402f187..af21d18 100644 --- a/src/amdb/application/command_handlers/create_movie.py +++ b/src/amdb/application/command_handlers/create_movie.py @@ -4,7 +4,6 @@ from amdb.domain.services.create_movie import CreateMovie from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.identity_provider import IdentityProvider from amdb.application.commands.create_movie import CreateMovieCommand @@ -15,12 +14,10 @@ def __init__( create_movie: CreateMovie, movie_gateway: MovieGateway, unit_of_work: UnitOfWork, - identity_provider: IdentityProvider, ) -> None: self._create_movie = create_movie self._movie_gateway = movie_gateway self._unit_of_work = unit_of_work - self._identity_provider = identity_provider def execute(self, command: CreateMovieCommand) -> MovieId: movie = self._create_movie( diff --git a/src/amdb/application/command_handlers/delete_movie.py b/src/amdb/application/command_handlers/delete_movie.py index 6d9502a..cbc9092 100644 --- a/src/amdb/application/command_handlers/delete_movie.py +++ b/src/amdb/application/command_handlers/delete_movie.py @@ -2,7 +2,6 @@ from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError from amdb.application.commands.delete_movie import DeleteMovieCommand @@ -16,13 +15,11 @@ def __init__( rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - identity_provider: IdentityProvider, ) -> None: self._movie_gateway = movie_gateway self._rating_gateway = rating_gateway self._review_gateway = review_gateway self._unit_of_work = unit_of_work - self._identity_provider = identity_provider def execute(self, command: DeleteMovieCommand) -> None: movie = self._movie_gateway.with_id(command.movie_id) diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index db05d96..dbd3147 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -3,7 +3,7 @@ UNRATE_MOVIE_ACCESS_DENIED = "Access to movie unrating is denied" REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied" -USER_IS_NOT_OWNER = "User is not owner" +USER_IS_NOT_OWNER = "User is not an owner" USER_NAME_ALREADY_EXISTS = "User name already exists" USER_DOES_NOT_EXIST = "User doesn't exist" From 3aafde9512e6f84ac9a8126a4280a88b0a5a181f Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 24 Feb 2024 00:56:28 +0400 Subject: [PATCH 07/39] Refactor presentation and main, remove cli, add dishka di --- pyproject.toml | 7 +- .../readers/{review.py => detailed_review.py} | 8 +- .../{review.py => detailed_review.py} | 2 +- .../{reviews.py => detailed_reviews.py} | 2 +- .../{reviews.py => detailed_reviews.py} | 23 +- .../auth/raw/identity_provider.py | 18 - .../infrastructure/auth/session/config.py | 6 +- .../raw => persistence/caching}/__init__.py | 0 .../persistence/caching/permissions_mapper.py | 65 ++++ .../persistence/redis/cache}/__init__.py | 0 .../redis/cache/permissions_mapper.py | 26 ++ .../persistence/redis/config.py | 4 +- .../persistence/redis/mappers/permissions.py | 67 ---- .../persistence/sqlalchemy/config.py | 4 +- .../sqlalchemy/mappers/permissions.py | 31 ++ .../{review.py => detailed_review.py} | 16 +- .../sqlalchemy/models/permissions.py | 16 + src/amdb/main/cli/__main__.py | 25 -- src/amdb/main/cli/app.py | 54 --- src/amdb/main/ioc.py | 220 ----------- src/amdb/main/providers.py | 358 ++++++++++++++++++ src/amdb/main/web_api/__main__.py | 42 +- src/amdb/main/web_api/app.py | 64 +++- src/amdb/main/web_api/config.py | 4 +- src/amdb/main/web_api/di.py | 50 --- src/amdb/main/web_api/providers.py | 30 ++ src/amdb/presentation/cli/alembic.py | 17 - src/amdb/presentation/cli/movie.py | 89 ----- src/amdb/presentation/cli/setup.py | 9 - src/amdb/presentation/create_handler.py | 16 + src/amdb/presentation/handler_factory.py | 81 ---- .../{cli => web_api/auth}/__init__.py | 0 src/amdb/presentation/web_api/auth/login.py | 48 +++ .../presentation/web_api/auth/register.py | 46 +++ src/amdb/presentation/web_api/auth/router.py | 22 ++ .../web_api/dependencies/depends_stub.py | 17 - .../web_api/dependencies/identity_provider.py | 32 -- .../web_api/exception_handlers.py | 5 +- .../{dependencies => movies}/__init__.py | 0 .../web_api/movies/get_detailed.py | 55 +++ .../web_api/movies/get_non_detailed.py | 57 +++ .../presentation/web_api/movies/router.py | 17 + .../web_api/{routers => ratings}/__init__.py | 0 .../web_api/ratings/rate_movie.py | 49 +++ .../presentation/web_api/ratings/router.py | 20 + .../web_api/ratings/unrate_movie.py | 51 +++ .../{routers/auth => reviews}/__init__.py | 0 .../web_api/reviews/get_detailed.py | 35 ++ .../web_api/reviews/review_movie.py | 49 +++ .../presentation/web_api/reviews/router.py | 17 + src/amdb/presentation/web_api/router.py | 13 + .../web_api/routers/auth/login.py | 46 --- .../web_api/routers/auth/register.py | 44 --- .../web_api/routers/auth/router.py | 24 -- .../web_api/routers/movies/__init__.py | 0 .../web_api/routers/movies/get_movies.py | 69 ---- .../web_api/routers/movies/router.py | 23 -- .../web_api/routers/ratings/__init__.py | 0 .../web_api/routers/ratings/rate_movie.py | 33 -- .../web_api/routers/ratings/router.py | 26 -- .../web_api/routers/ratings/unrate_movie.py | 32 -- .../web_api/routers/reviews/__init__.py | 0 .../web_api/routers/reviews/get_reviews.py | 28 -- .../web_api/routers/reviews/review_movie.py | 46 --- .../web_api/routers/reviews/router.py | 25 -- .../presentation/web_api/routers/setup.py | 13 - tests/unit/application/conftest.py | 30 +- ...eviews.py => test_get_detailed_reviews.py} | 40 +- 68 files changed, 1154 insertions(+), 1212 deletions(-) rename src/amdb/application/common/readers/{review.py => detailed_review.py} (53%) rename src/amdb/application/common/view_models/{review.py => detailed_review.py} (93%) rename src/amdb/application/queries/{reviews.py => detailed_reviews.py} (84%) rename src/amdb/application/query_handlers/{reviews.py => detailed_reviews.py} (51%) delete mode 100644 src/amdb/infrastructure/auth/raw/identity_provider.py rename src/amdb/infrastructure/{auth/raw => persistence/caching}/__init__.py (100%) create mode 100644 src/amdb/infrastructure/persistence/caching/permissions_mapper.py rename src/amdb/{main/cli => infrastructure/persistence/redis/cache}/__init__.py (100%) create mode 100644 src/amdb/infrastructure/persistence/redis/cache/permissions_mapper.py delete mode 100644 src/amdb/infrastructure/persistence/redis/mappers/permissions.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/permissions.py rename src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/{review.py => detailed_review.py} (89%) create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/models/permissions.py delete mode 100644 src/amdb/main/cli/__main__.py delete mode 100644 src/amdb/main/cli/app.py delete mode 100644 src/amdb/main/ioc.py create mode 100644 src/amdb/main/providers.py delete mode 100644 src/amdb/main/web_api/di.py create mode 100644 src/amdb/main/web_api/providers.py delete mode 100644 src/amdb/presentation/cli/alembic.py delete mode 100644 src/amdb/presentation/cli/movie.py delete mode 100644 src/amdb/presentation/cli/setup.py create mode 100644 src/amdb/presentation/create_handler.py delete mode 100644 src/amdb/presentation/handler_factory.py rename src/amdb/presentation/{cli => web_api/auth}/__init__.py (100%) create mode 100644 src/amdb/presentation/web_api/auth/login.py create mode 100644 src/amdb/presentation/web_api/auth/register.py create mode 100644 src/amdb/presentation/web_api/auth/router.py delete mode 100644 src/amdb/presentation/web_api/dependencies/depends_stub.py delete mode 100644 src/amdb/presentation/web_api/dependencies/identity_provider.py rename src/amdb/presentation/web_api/{dependencies => movies}/__init__.py (100%) create mode 100644 src/amdb/presentation/web_api/movies/get_detailed.py create mode 100644 src/amdb/presentation/web_api/movies/get_non_detailed.py create mode 100644 src/amdb/presentation/web_api/movies/router.py rename src/amdb/presentation/web_api/{routers => ratings}/__init__.py (100%) create mode 100644 src/amdb/presentation/web_api/ratings/rate_movie.py create mode 100644 src/amdb/presentation/web_api/ratings/router.py create mode 100644 src/amdb/presentation/web_api/ratings/unrate_movie.py rename src/amdb/presentation/web_api/{routers/auth => reviews}/__init__.py (100%) create mode 100644 src/amdb/presentation/web_api/reviews/get_detailed.py create mode 100644 src/amdb/presentation/web_api/reviews/review_movie.py create mode 100644 src/amdb/presentation/web_api/reviews/router.py create mode 100644 src/amdb/presentation/web_api/router.py delete mode 100644 src/amdb/presentation/web_api/routers/auth/login.py delete mode 100644 src/amdb/presentation/web_api/routers/auth/register.py delete mode 100644 src/amdb/presentation/web_api/routers/auth/router.py delete mode 100644 src/amdb/presentation/web_api/routers/movies/__init__.py delete mode 100644 src/amdb/presentation/web_api/routers/movies/get_movies.py delete mode 100644 src/amdb/presentation/web_api/routers/movies/router.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/__init__.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/rate_movie.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/router.py delete mode 100644 src/amdb/presentation/web_api/routers/ratings/unrate_movie.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/__init__.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/get_reviews.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/review_movie.py delete mode 100644 src/amdb/presentation/web_api/routers/reviews/router.py delete mode 100644 src/amdb/presentation/web_api/routers/setup.py rename tests/unit/application/query_handlers/{test_get_reviews.py => test_get_detailed_reviews.py} (70%) diff --git a/pyproject.toml b/pyproject.toml index 8167880..1adc221 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,8 +24,9 @@ maintainers = [ { name = "madnoberson", email = "baseddepartmentzx77@gmail.com" }, ] dependencies = [ - "uuid7", + "uuid7==0.1.*", "toml==0.10.*", + "dishka==0.4.*", "sqlalchemy==2.0.*", "psycopg2-binary==2.9.*", "alembic==1.13.*", @@ -37,9 +38,6 @@ web_api = [ "fastapi==0.103.*", "uvicorn==0.22.*", ] -cli = [ - "typer[all]==0.9.*", -] dev = [ "mypy==1.8.*", "ruff==0.1.*", @@ -53,5 +51,4 @@ coverage = [ ] [project.scripts] -amdb-cli = "amdb.main.cli.__main__:main" amdb-web_api = "amdb.main.web_api.__main__:main" diff --git a/src/amdb/application/common/readers/review.py b/src/amdb/application/common/readers/detailed_review.py similarity index 53% rename from src/amdb/application/common/readers/review.py rename to src/amdb/application/common/readers/detailed_review.py index f04f6f1..2cdf4a4 100644 --- a/src/amdb/application/common/readers/review.py +++ b/src/amdb/application/common/readers/detailed_review.py @@ -1,14 +1,16 @@ from typing import Protocol from amdb.domain.entities.movie import MovieId -from amdb.application.common.view_models.review import ReviewViewModel +from amdb.application.common.view_models.detailed_review import ( + DetailedReviewViewModel, +) -class ReviewViewModelReader(Protocol): +class DetailedReviewViewModelReader(Protocol): def list( self, movie_id: MovieId, limit: int, offset: int, - ) -> list[ReviewViewModel]: + ) -> list[DetailedReviewViewModel]: raise NotImplementedError diff --git a/src/amdb/application/common/view_models/review.py b/src/amdb/application/common/view_models/detailed_review.py similarity index 93% rename from src/amdb/application/common/view_models/review.py rename to src/amdb/application/common/view_models/detailed_review.py index 8091a8b..da2f376 100644 --- a/src/amdb/application/common/view_models/review.py +++ b/src/amdb/application/common/view_models/detailed_review.py @@ -22,7 +22,7 @@ class UserReview(TypedDict): created_at: datetime -class ReviewViewModel(TypedDict): +class DetailedReviewViewModel(TypedDict): user_id: UserId user_review: UserReview user_rating: Optional[UserRating] diff --git a/src/amdb/application/queries/reviews.py b/src/amdb/application/queries/detailed_reviews.py similarity index 84% rename from src/amdb/application/queries/reviews.py rename to src/amdb/application/queries/detailed_reviews.py index a3577b0..c8c7525 100644 --- a/src/amdb/application/queries/reviews.py +++ b/src/amdb/application/queries/detailed_reviews.py @@ -4,7 +4,7 @@ @dataclass(frozen=True, slots=True) -class GetReviewsQuery: +class GetDetailedReviewsQuery: movie_id: MovieId limit: int offset: int diff --git a/src/amdb/application/query_handlers/reviews.py b/src/amdb/application/query_handlers/detailed_reviews.py similarity index 51% rename from src/amdb/application/query_handlers/reviews.py rename to src/amdb/application/query_handlers/detailed_reviews.py index 736add7..c60fa6a 100644 --- a/src/amdb/application/query_handlers/reviews.py +++ b/src/amdb/application/query_handlers/detailed_reviews.py @@ -1,27 +1,34 @@ from amdb.application.common.gateways.movie import MovieGateway -from amdb.application.common.readers.review import ReviewViewModelReader -from amdb.application.common.view_models.review import ReviewViewModel +from amdb.application.common.readers.detailed_review import ( + DetailedReviewViewModelReader, +) +from amdb.application.common.view_models.detailed_review import ( + DetailedReviewViewModel, +) from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError -from amdb.application.queries.reviews import GetReviewsQuery +from amdb.application.queries.detailed_reviews import GetDetailedReviewsQuery -class GetReviewsHandler: +class GetDetailedReviewsHandler: def __init__( self, *, movie_gateway: MovieGateway, - review_view_model_reader: ReviewViewModelReader, + detailed_review_reader: DetailedReviewViewModelReader, ) -> None: self._movie_gateway = movie_gateway - self._review_view_model_reader = review_view_model_reader + self._detailed_review_reader = detailed_review_reader - def execute(self, query: GetReviewsQuery) -> list[ReviewViewModel]: + def execute( + self, + query: GetDetailedReviewsQuery, + ) -> list[DetailedReviewViewModel]: movie = self._movie_gateway.with_id(query.movie_id) if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - review_view_models = self._review_view_model_reader.list( + review_view_models = self._detailed_review_reader.list( movie_id=query.movie_id, limit=query.limit, offset=query.offset, diff --git a/src/amdb/infrastructure/auth/raw/identity_provider.py b/src/amdb/infrastructure/auth/raw/identity_provider.py deleted file mode 100644 index ec0556d..0000000 --- a/src/amdb/infrastructure/auth/raw/identity_provider.py +++ /dev/null @@ -1,18 +0,0 @@ -from typing import Optional - -from amdb.domain.entities.user import UserId - - -class RawIdentityProvider: - def __init__(self, user_id: UserId, permissions: int) -> None: - self._user_id = user_id - self._permissions = permissions - - def user_id(self) -> UserId: - return self._user_id - - def user_id_or_none(self) -> Optional[UserId]: - return self._user_id - - def permissions(self) -> int: - return self._permissions diff --git a/src/amdb/infrastructure/auth/session/config.py b/src/amdb/infrastructure/auth/session/config.py index 8100d97..9093717 100644 --- a/src/amdb/infrastructure/auth/session/config.py +++ b/src/amdb/infrastructure/auth/session/config.py @@ -1,7 +1,5 @@ from dataclasses import dataclass from datetime import timedelta -from typing import Union -from os import PathLike import toml @@ -11,9 +9,9 @@ class SessionConfig: lifetime: timedelta @classmethod - def from_toml(cls, path: Union[PathLike, str]) -> "SessionConfig": + def from_toml(cls, path: str) -> "SessionConfig": toml_as_dict = toml.load(path) session_section_as_dict = toml_as_dict["auth-session"] return SessionConfig( - lifetime=session_section_as_dict["lifetime"], + lifetime=timedelta(minutes=session_section_as_dict["lifetime"]), ) diff --git a/src/amdb/infrastructure/auth/raw/__init__.py b/src/amdb/infrastructure/persistence/caching/__init__.py similarity index 100% rename from src/amdb/infrastructure/auth/raw/__init__.py rename to src/amdb/infrastructure/persistence/caching/__init__.py diff --git a/src/amdb/infrastructure/persistence/caching/permissions_mapper.py b/src/amdb/infrastructure/persistence/caching/permissions_mapper.py new file mode 100644 index 0000000..90f587f --- /dev/null +++ b/src/amdb/infrastructure/persistence/caching/permissions_mapper.py @@ -0,0 +1,65 @@ +from typing import Optional + +from amdb.domain.entities.user import UserId +from amdb.infrastructure.persistence.sqlalchemy.mappers.permissions import ( + PermissionsMapper, +) +from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( + PermissionsMapperCacheProvider, +) + + +class CachingPermissionsMapper: + def __init__( + self, + permissions_mapper: PermissionsMapper, + cache_provider: PermissionsMapperCacheProvider, + ) -> None: + self._permissions_mapper = permissions_mapper + self._cache_provider = cache_provider + + def with_user_id(self, user_id: UserId) -> Optional[int]: + permissions_from_cache = self._cache_provider.with_user_id(user_id) + if permissions_from_cache: + return permissions_from_cache + + permissions_from_database = self._permissions_mapper.with_user_id( + user_id, + ) + if permissions_from_database: + self._cache_provider.set( + user_id=user_id, + permissions=permissions_from_database, + ) + + return permissions_from_database + + def set(self, user_id: UserId, permissions: int) -> None: + self._permissions_mapper.set( + user_id=user_id, + permissions=permissions, + ) + self._cache_provider.set( + user_id=user_id, + permissions=permissions, + ) + + def for_new_user(self) -> int: + return ( + self.for_login() + + self.for_rate_movie() + + self.for_unrate_movie() + + self.for_review_movie() + ) + + def for_login(self) -> int: + return 2 + + def for_rate_movie(self) -> int: + return 4 + + def for_unrate_movie(self) -> int: + return 8 + + def for_review_movie(self) -> int: + return 16 diff --git a/src/amdb/main/cli/__init__.py b/src/amdb/infrastructure/persistence/redis/cache/__init__.py similarity index 100% rename from src/amdb/main/cli/__init__.py rename to src/amdb/infrastructure/persistence/redis/cache/__init__.py diff --git a/src/amdb/infrastructure/persistence/redis/cache/permissions_mapper.py b/src/amdb/infrastructure/persistence/redis/cache/permissions_mapper.py new file mode 100644 index 0000000..fe3760b --- /dev/null +++ b/src/amdb/infrastructure/persistence/redis/cache/permissions_mapper.py @@ -0,0 +1,26 @@ +from datetime import timedelta +from typing import Optional, cast + +from redis import Redis + +from amdb.domain.entities.user import UserId + + +class PermissionsMapperCacheProvider: + _CACHE_TIME = timedelta(hours=24) + + def __init__(self, redis: Redis) -> None: + self._redis = redis + + def with_user_id(self, user_id: UserId) -> Optional[int]: + permissions = self._redis.get(f"permissions:user_id:{user_id.hex}") + if permissions: + return int(cast(str, permissions)) + return None + + def set(self, user_id: UserId, permissions: int) -> None: + self._redis.set( + name=f"permissions:user_id:{user_id.hex}", + value=permissions, + ex=self._CACHE_TIME, + ) diff --git a/src/amdb/infrastructure/persistence/redis/config.py b/src/amdb/infrastructure/persistence/redis/config.py index 3a4abc8..06cdffd 100644 --- a/src/amdb/infrastructure/persistence/redis/config.py +++ b/src/amdb/infrastructure/persistence/redis/config.py @@ -1,6 +1,4 @@ from dataclasses import dataclass -from typing import Union -from os import PathLike import toml @@ -10,7 +8,7 @@ class RedisConfig: url: str @classmethod - def from_toml(cls, path: Union[PathLike, str]) -> "RedisConfig": + def from_toml(cls, path: str) -> "RedisConfig": toml_as_dict = toml.load(path) redis_section_as_dict = toml_as_dict["redis"] return RedisConfig(url=redis_section_as_dict["url"]) diff --git a/src/amdb/infrastructure/persistence/redis/mappers/permissions.py b/src/amdb/infrastructure/persistence/redis/mappers/permissions.py deleted file mode 100644 index e5554bd..0000000 --- a/src/amdb/infrastructure/persistence/redis/mappers/permissions.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import Optional, cast - -from redis.client import Redis - -from amdb.domain.entities.user import UserId - - -class PermissionsMapper: - def __init__(self, redis: Redis) -> None: - self._redis = redis - - def with_user_id(self, user_id: UserId) -> Optional[int]: - permissions = self._redis.get(f"permissions:user_id:{user_id.hex}") - if permissions: - return int(cast(str, permissions)) - return None - - def set(self, user_id: UserId, permissions: int) -> None: - self._redis.set( - name=f"permissions:user_id:{user_id.hex}", - value=permissions, - ) - - def for_login(self) -> int: - return 2 - - def for_new_user(self) -> int: - return 6 - - def for_get_movies(self) -> int: - return 4 - - def for_get_movie(self) -> int: - return 4 - - def for_create_movie(self) -> int: - return 8 - - def for_delete_movie(self) -> int: - return 8 - - def for_get_movie_ratings(self) -> int: - return 4 - - def for_get_my_ratings(self) -> int: - return 4 - - def for_get_rating(self) -> int: - return 4 - - def for_rate_movie(self) -> int: - return 4 - - def for_unrate_movie(self) -> int: - return 4 - - def for_get_reviews(self) -> int: - return 4 - - def for_get_my_reviews(self) -> int: - return 4 - - def for_get_review(self) -> int: - return 4 - - def for_review_movie(self) -> int: - return 4 diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/config.py b/src/amdb/infrastructure/persistence/sqlalchemy/config.py index 5a44813..96951e9 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/config.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/config.py @@ -1,6 +1,4 @@ from dataclasses import dataclass -from typing import Union -from os import PathLike import toml @@ -10,7 +8,7 @@ class PostgresConfig: url: str @classmethod - def from_toml(cls, path: Union[PathLike, str]) -> "PostgresConfig": + def from_toml(cls, path: str) -> "PostgresConfig": toml_as_dict = toml.load(path) postgres_section_as_dict = toml_as_dict["postgres"] return PostgresConfig(url=postgres_section_as_dict["url"]) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/permissions.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/permissions.py new file mode 100644 index 0000000..b3392d0 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/permissions.py @@ -0,0 +1,31 @@ +from typing import Optional + +from sqlalchemy import Connection, Row, select, insert + +from amdb.domain.entities.user import UserId +from amdb.infrastructure.persistence.sqlalchemy.models.permissions import ( + PermissionsModel, +) + + +class PermissionsMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def with_user_id(self, user_id: UserId) -> Optional[int]: + statement = select(PermissionsModel).where( + PermissionsModel.user_id == user_id, + ) + row: Optional[Row[tuple[PermissionsModel]]] = self._connection.execute( + statement, + ).one_or_none() + if row is not None: + return row.value + return None + + def set(self, user_id: UserId, permissions: int) -> None: + statement = insert(PermissionsModel).values( + user_id=user_id, + value=permissions, + ) + self._connection.execute(statement) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py similarity index 89% rename from src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py rename to src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py index a3443a1..3145c32 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py @@ -1,4 +1,4 @@ -__all__ = ("ReviewViewModelMapper",) +__all__ = ("DetailedReviewViewModelMapper",) from typing import Optional, TypedDict from datetime import datetime @@ -10,10 +10,10 @@ from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId from amdb.domain.entities.review import ReviewId, ReviewType -from amdb.application.common.view_models.review import ( +from amdb.application.common.view_models.detailed_review import ( UserRating, UserReview, - ReviewViewModel, + DetailedReviewViewModel, ) @@ -33,7 +33,7 @@ def from_row(cls, row: Row) -> "RowAsDict": return RowAsDict(row._mapping) # noqa: SLF001 -class ReviewViewModelMapper: +class DetailedReviewViewModelMapper: def __init__(self, connection: Connection) -> None: self._connection = connection @@ -42,7 +42,7 @@ def list( movie_id: MovieId, limit: int, offset: int, - ) -> list[ReviewViewModel]: + ) -> list[DetailedReviewViewModel]: statement = text( """ SELECT @@ -83,7 +83,7 @@ def list( def _to_view_model( self, row_as_dict: RowAsDict, - ) -> ReviewViewModel: + ) -> DetailedReviewViewModel: user_review = UserReview( id=ReviewId(row_as_dict["user_review_id"]), title=row_as_dict["user_review_title"], @@ -101,9 +101,9 @@ def _to_view_model( else: user_rating = None - review_view_model = ReviewViewModel( + detailed_review_view_model = DetailedReviewViewModel( user_id=UserId(row_as_dict["user_id"]), user_review=user_review, user_rating=user_rating, ) - return review_view_model + return detailed_review_view_model diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/permissions.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/permissions.py new file mode 100644 index 0000000..f0d6b4a --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/permissions.py @@ -0,0 +1,16 @@ +from uuid import UUID + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column + +from .base import Model + + +class PermissionsModel(Model): + __tablename__ = "permissions" + + user_id: Mapped[UUID] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + primary_key=True, + ) + value: Mapped[int] diff --git a/src/amdb/main/cli/__main__.py b/src/amdb/main/cli/__main__.py deleted file mode 100644 index e9d6f6f..0000000 --- a/src/amdb/main/cli/__main__.py +++ /dev/null @@ -1,25 +0,0 @@ -import os - -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig -from .app import create_app - - -def main() -> None: - path_to_config = os.getenv("CONFIG_PATH") - if not path_to_config: - message = "Path to config env var is not set" - raise ValueError(message) - - postgres_config = PostgresConfig.from_toml(path_to_config) - redis_config = RedisConfig.from_toml(path_to_config) - - app = create_app( - postgres_config=postgres_config, - redis_config=redis_config, - ) - - app() - - -main() diff --git a/src/amdb/main/cli/app.py b/src/amdb/main/cli/app.py deleted file mode 100644 index abdc0b8..0000000 --- a/src/amdb/main/cli/app.py +++ /dev/null @@ -1,54 +0,0 @@ -from typing import cast -from uuid import UUID - -import typer -from sqlalchemy import create_engine -from redis import Redis - -from amdb.domain.entities.user import UserId -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig -from amdb.infrastructure.persistence.redis.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.password_manager.hash_computer import HashComputer -from amdb.infrastructure.auth.raw.identity_provider import RawIdentityProvider -from amdb.presentation.cli.setup import setup_typer_command_handlers -from amdb.main.ioc import IoC - - -IDENTITY_PROVIDER_USER_ID = UserId( - UUID("00000000-0000-0000-0000-000000000000"), -) -IDENTITY_PROVIDER_PERMISSIONS = 12 - - -def create_app( - postgres_config: PostgresConfig, - redis_config: RedisConfig, -) -> typer.Typer: - sqlalchemy_engine = create_engine(postgres_config.url) - redis = Redis.from_url(redis_config.url, decode_responses=True) - permissions_mapper = PermissionsMapper(cast(Redis, redis)) - - ioc = IoC( - sqlalchemy_engine=sqlalchemy_engine, - permissions_mapper=permissions_mapper, - hash_computer=HashComputer(), - ) - raw_identity_provider = RawIdentityProvider( - user_id=IDENTITY_PROVIDER_USER_ID, - permissions=IDENTITY_PROVIDER_PERMISSIONS, - ) - dependencies = { - "ioc": ioc, - "identity_provider": raw_identity_provider, - } - - app = typer.Typer( - rich_markup_mode="rich", - context_settings={"obj": dependencies}, - ) - setup_typer_command_handlers(app) - - return app diff --git a/src/amdb/main/ioc.py b/src/amdb/main/ioc.py deleted file mode 100644 index 53b769e..0000000 --- a/src/amdb/main/ioc.py +++ /dev/null @@ -1,220 +0,0 @@ -from contextlib import contextmanager -from typing import Iterator - -from sqlalchemy import Engine - -from amdb.domain.services.access_concern import AccessConcern -from amdb.domain.services.create_user import CreateUser -from amdb.domain.services.create_movie import CreateMovie -from amdb.domain.services.rate_movie import RateMovie -from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.domain.services.review_movie import ReviewMovie -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.command_handlers.register_user import RegisterUserHandler -from amdb.application.command_handlers.create_movie import CreateMovieHandler -from amdb.application.command_handlers.delete_movie import DeleteMovieHandler -from amdb.application.command_handlers.rate_movie import RateMovieHandler -from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler -from amdb.application.command_handlers.review_movie import ReviewMovieHandler -from amdb.application.query_handlers.login import LoginHandler -from amdb.application.query_handlers.detailed_movie import ( - GetDetailedMovieHandler, -) -from amdb.application.query_handlers.non_detailed_movies import ( - GetNonDetailedMoviesHandler, -) -from amdb.application.query_handlers.reviews import GetReviewsHandler -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( - UserMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( - MovieMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( - RatingMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( - ReviewMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( - NonDetailedMovieViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( - DetailedMovieViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.review import ( - ReviewViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( - PasswordHashMapper, -) -from amdb.infrastructure.persistence.redis.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.password_manager.hash_computer import HashComputer -from amdb.infrastructure.password_manager.password_manager import ( - HashingPasswordManager, -) -from amdb.presentation.handler_factory import HandlerFactory - - -class IoC(HandlerFactory): - def __init__( - self, - sqlalchemy_engine: Engine, - permissions_mapper: PermissionsMapper, - hash_computer: HashComputer, - ) -> None: - self._sqlalchemy_engine = sqlalchemy_engine - self._permissions_mapper = permissions_mapper - self._hash_computer = hash_computer - - @contextmanager - def register_user(self) -> Iterator[RegisterUserHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - password_manager = HashingPasswordManager( - hash_computer=self._hash_computer, - password_hash_gateway=PasswordHashMapper( - sqlalchemy_connection, - ), - ) - yield RegisterUserHandler( - create_user=CreateUser(), - user_gateway=UserMapper(sqlalchemy_connection), - permissions_gateway=self._permissions_mapper, - unit_of_work=sqlalchemy_connection, - password_manager=password_manager, - ) - - @contextmanager - def login(self) -> Iterator[LoginHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - password_manager = HashingPasswordManager( - hash_computer=self._hash_computer, - password_hash_gateway=PasswordHashMapper( - sqlalchemy_connection, - ), - ) - yield LoginHandler( - access_concern=AccessConcern(), - user_gateway=UserMapper(sqlalchemy_connection), - permissions_gateway=self._permissions_mapper, - password_manager=password_manager, - ) - - @contextmanager - def get_non_detailed_movies( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetNonDetailedMoviesHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield GetNonDetailedMoviesHandler( - non_detailed_movie_reader=NonDetailedMovieViewModelMapper( - sqlalchemy_connection, - ), - identity_provider=identity_provider, - ) - - @contextmanager - def get_detailed_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[GetDetailedMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield GetDetailedMovieHandler( - detailed_movie_reader=DetailedMovieViewModelMapper( - sqlalchemy_connection, - ), - identity_provider=identity_provider, - ) - - @contextmanager - def create_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[CreateMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield CreateMovieHandler( - access_concern=AccessConcern(), - create_movie=CreateMovie(), - permissions_gateway=self._permissions_mapper, - movie_gateway=MovieMapper(sqlalchemy_connection), - unit_of_work=sqlalchemy_connection, - identity_provider=identity_provider, - ) - - @contextmanager - def delete_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[DeleteMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield DeleteMovieHandler( - access_concern=AccessConcern(), - permissions_gateway=self._permissions_mapper, - movie_gateway=MovieMapper(sqlalchemy_connection), - rating_gateway=RatingMapper(sqlalchemy_connection), - review_gateway=ReviewMapper(sqlalchemy_connection), - unit_of_work=sqlalchemy_connection, - identity_provider=identity_provider, - ) - - @contextmanager - def rate_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[RateMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield RateMovieHandler( - access_concern=AccessConcern(), - rate_movie=RateMovie(), - permissions_gateway=self._permissions_mapper, - user_gateway=UserMapper(sqlalchemy_connection), - movie_gateway=MovieMapper(sqlalchemy_connection), - rating_gateway=RatingMapper(sqlalchemy_connection), - unit_of_work=sqlalchemy_connection, - identity_provider=identity_provider, - ) - - @contextmanager - def unrate_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[UnrateMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield UnrateMovieHandler( - access_concern=AccessConcern(), - unrate_movie=UnrateMovie(), - permissions_gateway=self._permissions_mapper, - movie_gateway=MovieMapper(sqlalchemy_connection), - rating_gateway=RatingMapper(sqlalchemy_connection), - unit_of_work=sqlalchemy_connection, - identity_provider=identity_provider, - ) - - @contextmanager - def get_reviews(self) -> Iterator[GetReviewsHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield GetReviewsHandler( - movie_gateway=MovieMapper(sqlalchemy_connection), - review_view_model_reader=ReviewViewModelMapper( - sqlalchemy_connection, - ), - ) - - @contextmanager - def review_movie( - self, - identity_provider: IdentityProvider, - ) -> Iterator[ReviewMovieHandler]: - with self._sqlalchemy_engine.connect() as sqlalchemy_connection: - yield ReviewMovieHandler( - access_concern=AccessConcern(), - review_movie=ReviewMovie(), - permissions_gateway=self._permissions_mapper, - user_gateway=UserMapper(sqlalchemy_connection), - movie_gateway=MovieMapper(sqlalchemy_connection), - review_gateway=ReviewMapper(sqlalchemy_connection), - unit_of_work=sqlalchemy_connection, - identity_provider=identity_provider, - ) diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py new file mode 100644 index 0000000..1f082c2 --- /dev/null +++ b/src/amdb/main/providers.py @@ -0,0 +1,358 @@ +from typing import Iterable, cast + +from dishka import Provider, Scope, provide, alias +from sqlalchemy import Connection, Engine, create_engine +from redis import Redis + +from amdb.domain.services.access_concern import AccessConcern +from amdb.domain.services.create_user import CreateUser +from amdb.domain.services.create_movie import CreateMovie +from amdb.domain.services.rate_movie import RateMovie +from amdb.domain.services.unrate_movie import UnrateMovie +from amdb.domain.services.review_movie import ReviewMovie +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.gateways.review import ReviewGateway +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.readers.detailed_movie import ( + DetailedMovieViewModelReader, +) +from amdb.application.common.readers.non_detailed_movie import ( + NonDetailedMovieViewModelReader, +) +from amdb.application.common.readers.detailed_review import ( + DetailedReviewViewModelReader, +) +from amdb.application.common.password_manager import PasswordManager +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.command_handlers.register_user import RegisterUserHandler +from amdb.application.command_handlers.create_movie import CreateMovieHandler +from amdb.application.command_handlers.delete_movie import DeleteMovieHandler +from amdb.application.command_handlers.rate_movie import RateMovieHandler +from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler +from amdb.application.command_handlers.review_movie import ReviewMovieHandler +from amdb.application.query_handlers.login import LoginHandler +from amdb.application.query_handlers.detailed_movie import ( + GetDetailedMovieHandler, +) +from amdb.application.query_handlers.non_detailed_movies import ( + GetNonDetailedMoviesHandler, +) +from amdb.application.query_handlers.detailed_reviews import ( + GetDetailedReviewsHandler, +) +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( + UserMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( + MovieMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( + RatingMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( + ReviewMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.permissions import ( + PermissionsMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( + PasswordHashMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( + DetailedMovieViewModelMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( + NonDetailedMovieViewModelMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( + DetailedReviewViewModelMapper, +) +from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( + PermissionsMapperCacheProvider, +) +from amdb.infrastructure.persistence.caching.permissions_mapper import ( + CachingPermissionsMapper, +) +from amdb.infrastructure.password_manager.hash_computer import HashComputer +from amdb.infrastructure.password_manager.password_manager import ( + HashingPasswordManager, +) +from amdb.presentation.create_handler import CreateHandler + + +class ConnectionsProvider(Provider): + scope = Scope.APP + + def __init__( + self, + *, + postgres_config: PostgresConfig, + redis_config: RedisConfig, + ) -> None: + super().__init__() + self._postgsres_config = postgres_config + self._redis_config = redis_config + + @provide + def sqlaclhemy_engine(self) -> Engine: + return create_engine(self._postgsres_config.url) + + @provide + def redis(self) -> Redis: + redis = Redis.from_url( + url=self._redis_config.url, + decode_responses=True, + ) + return cast(Redis, redis) + + @provide(scope=Scope.REQUEST) + def sqlalchemy_connection( + self, + sqlalchemy_engine: Engine, + ) -> Iterable[Connection]: + with sqlalchemy_engine.connect() as sqlalchemy_connection: + yield sqlalchemy_connection + + +class AdaptersProvider(Provider): + scope = Scope.REQUEST + + user_gateway = provide(source=UserMapper, provides=UserGateway) + movie_gateway = provide(source=MovieMapper, provides=MovieGateway) + rating_gateway = provide(source=RatingMapper, provides=RatingGateway) + review_gateway = provide(source=ReviewMapper, provides=ReviewGateway) + detailed_movie_reader = provide( + source=DetailedMovieViewModelMapper, + provides=DetailedMovieViewModelReader, + ) + non_detailed_movie_reader = provide( + source=NonDetailedMovieViewModelMapper, + provides=NonDetailedMovieViewModelReader, + ) + detailed_review_reader = provide( + source=DetailedReviewViewModelMapper, + provides=DetailedReviewViewModelReader, + ) + + unit_of_work = alias(source=Connection, provides=UnitOfWork) + + @provide + def permissions_mapper( + self, + sqlalchemy_connection: Connection, + redis: Redis, + ) -> PermissionsMapper: + permissions_mapper = PermissionsMapper(sqlalchemy_connection) + cache_provider = PermissionsMapperCacheProvider(redis) + return CachingPermissionsMapper( # type: ignore + permissions_mapper=permissions_mapper, + cache_provider=cache_provider, + ) + + @provide + def permissions_gateway( + self, + sqlalchemy_connection: Connection, + redis: Redis, + ) -> PermissionsGateway: + permissions_mapper = PermissionsMapper(sqlalchemy_connection) + cache_provider = PermissionsMapperCacheProvider(redis) + return CachingPermissionsMapper( + permissions_mapper=permissions_mapper, + cache_provider=cache_provider, + ) + + @provide + def password_manager( + self, + sqlalchemy_connection: Connection, + ) -> PasswordManager: + password_hash_mapper = PasswordHashMapper(sqlalchemy_connection) + return HashingPasswordManager( + hash_computer=HashComputer(), + password_hash_gateway=password_hash_mapper, + ) + + +class HandlersProvider(Provider): + scope = Scope.REQUEST + + @provide + def register_user_handler( + self, + user_gateway: UserGateway, + permissions_gateway: PermissionsGateway, + unit_of_work: UnitOfWork, + password_manager: PasswordManager, + ) -> RegisterUserHandler: + return RegisterUserHandler( + create_user=CreateUser(), + user_gateway=user_gateway, + permissions_gateway=permissions_gateway, + unit_of_work=unit_of_work, + password_manager=password_manager, + ) + + @provide + def login_handler( + self, + user_gateway: UserGateway, + permissions_gateway: PermissionsGateway, + password_manager: PasswordManager, + ) -> LoginHandler: + return LoginHandler( + access_concern=AccessConcern(), + user_gateway=user_gateway, + permissions_gateway=permissions_gateway, + password_manager=password_manager, + ) + + @provide + def create_movie_handler( + self, + movie_gateway: MovieGateway, + unit_of_work: UnitOfWork, + ) -> CreateMovieHandler: + return CreateMovieHandler( + create_movie=CreateMovie(), + movie_gateway=movie_gateway, + unit_of_work=unit_of_work, + ) + + @provide + def delete_movie_handler( + self, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + ) -> DeleteMovieHandler: + return DeleteMovieHandler( + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + review_gateway=review_gateway, + unit_of_work=unit_of_work, + ) + + @provide + def get_detailed_reviews_handler( + self, + movie_gateway: MovieGateway, + detailed_review_reader: DetailedReviewViewModelReader, + ) -> GetDetailedReviewsHandler: + return GetDetailedReviewsHandler( + movie_gateway=movie_gateway, + detailed_review_reader=detailed_review_reader, + ) + + +class HandlerCreatorsProvider(Provider): + scope = Scope.REQUEST + + @provide + def get_detailed_movie_handler( + self, + detailed_movie_reader: DetailedMovieViewModelReader, + ) -> CreateHandler[GetDetailedMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetDetailedMovieHandler: + return GetDetailedMovieHandler( + detailed_movie_reader=detailed_movie_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def get_non_detailed_movies_handler( + self, + non_detailed_movie_reader: NonDetailedMovieViewModelReader, + ) -> CreateHandler[GetNonDetailedMoviesHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetNonDetailedMoviesHandler: + return GetNonDetailedMoviesHandler( + non_detailed_movie_reader=non_detailed_movie_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def rate_movie_handler( + self, + permissions_gateway: PermissionsGateway, + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[RateMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> RateMovieHandler: + return RateMovieHandler( + access_concern=AccessConcern(), + rate_movie=RateMovie(), + permissions_gateway=permissions_gateway, + user_gateway=user_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def unrate_movie_handler( + self, + permissions_gateway: PermissionsGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[UnrateMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> UnrateMovieHandler: + return UnrateMovieHandler( + access_concern=AccessConcern(), + unrate_movie=UnrateMovie(), + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def review_movie_handler( + self, + permissions_gateway: PermissionsGateway, + user_gateway: UserGateway, + movie_gateway: MovieGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[ReviewMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> ReviewMovieHandler: + return ReviewMovieHandler( + access_concern=AccessConcern(), + review_movie=ReviewMovie(), + permissions_gateway=permissions_gateway, + user_gateway=user_gateway, + movie_gateway=movie_gateway, + review_gateway=review_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler diff --git a/src/amdb/main/web_api/__main__.py b/src/amdb/main/web_api/__main__.py index f321b67..44388f6 100644 --- a/src/amdb/main/web_api/__main__.py +++ b/src/amdb/main/web_api/__main__.py @@ -1,41 +1,5 @@ -import asyncio -import os +from .app import run_web_api -from uvicorn import Server, Config -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig -from amdb.infrastructure.auth.session.config import SessionConfig -from .config import WebAPIConfig -from .app import create_app - - -async def main() -> None: - path_to_config = os.getenv("CONFIG_PATH") - if not path_to_config: - message = "Path to config env var is not set" - raise ValueError(message) - - web_api_config = WebAPIConfig.from_toml(path_to_config) - postgres_config = PostgresConfig.from_toml(path_to_config) - redis_config = RedisConfig.from_toml(path_to_config) - session_config = SessionConfig.from_toml(path_to_config) - - app = create_app( - web_api_config=web_api_config, - postgres_config=postgres_config, - redis_config=redis_config, - session_config=session_config, - ) - server = Server( - Config( - app=app, - host=web_api_config.host, - port=web_api_config.port, - ), - ) - - await server.serve() - - -asyncio.run(main()) +def main() -> None: + run_web_api() diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index dfa1555..1902b52 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -1,30 +1,62 @@ +import os + +import uvicorn from fastapi import FastAPI +from dishka import make_async_container +from dishka.integrations.fastapi import setup_dishka from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.auth.session.config import SessionConfig +from amdb.presentation.web_api.router import router from amdb.presentation.web_api.exception_handlers import ( setup_exception_handlers, ) -from amdb.presentation.web_api.routers.setup import setup_routers -from .di import setup_dependecies +from amdb.main.providers import ( + ConnectionsProvider, + AdaptersProvider, + HandlersProvider, + HandlerCreatorsProvider, +) +from .providers import SessionAdaptersProvider from .config import WebAPIConfig -def create_app( - web_api_config: WebAPIConfig, - postgres_config: PostgresConfig, - redis_config: RedisConfig, - session_config: SessionConfig, -) -> FastAPI: - app = FastAPI(version=web_api_config.version) - setup_dependecies( - app=app, - session_config=session_config, - postgres_config=postgres_config, - redis_config=redis_config, +def run_web_api() -> None: + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + web_api_config = WebAPIConfig.from_toml(path_to_config) + postgres_config = PostgresConfig.from_toml(path_to_config) + redis_config = RedisConfig.from_toml(path_to_config) + session_config = SessionConfig.from_toml(path_to_config) + + app = FastAPI( + title="Awesome Movie Database", + version=web_api_config.version, + swagger_ui_parameters={"defaultModelsExpandDepth": -1}, ) + app.include_router(router) setup_exception_handlers(app) - setup_routers(app) - return app + container = make_async_container( + ConnectionsProvider( + postgres_config=postgres_config, + redis_config=redis_config, + ), + AdaptersProvider(), + SessionAdaptersProvider( + session_config=session_config, + ), + HandlersProvider(), + HandlerCreatorsProvider(), + ) + setup_dishka(container, app) + + uvicorn.run( + app=app, + host=web_api_config.host, + port=web_api_config.port, + ) diff --git a/src/amdb/main/web_api/config.py b/src/amdb/main/web_api/config.py index 60a0108..050409c 100644 --- a/src/amdb/main/web_api/config.py +++ b/src/amdb/main/web_api/config.py @@ -1,6 +1,4 @@ from dataclasses import dataclass -from typing import Union -from os import PathLike import toml @@ -12,7 +10,7 @@ class WebAPIConfig: port: int @classmethod - def from_toml(cls, path: Union[PathLike, str]) -> "WebAPIConfig": + def from_toml(cls, path: str) -> "WebAPIConfig": toml_as_dict = toml.load(path) web_api_section_as_dict = toml_as_dict["web-api"] return WebAPIConfig( diff --git a/src/amdb/main/web_api/di.py b/src/amdb/main/web_api/di.py deleted file mode 100644 index 2a03b52..0000000 --- a/src/amdb/main/web_api/di.py +++ /dev/null @@ -1,50 +0,0 @@ -from typing import cast - -from fastapi import FastAPI -from sqlalchemy import create_engine -from redis.client import Redis - -from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig -from amdb.infrastructure.auth.session.config import SessionConfig -from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper -from amdb.infrastructure.persistence.redis.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.password_manager.hash_computer import HashComputer -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.depends_stub import Stub -from amdb.main.ioc import IoC - - -def setup_dependecies( - app: FastAPI, - session_config: SessionConfig, - postgres_config: PostgresConfig, - redis_config: RedisConfig, -) -> None: - redis = Redis.from_url(redis_config.url, decode_responses=True) - session_mapper = SessionMapper( - redis=cast(Redis, redis), - session_lifetime=session_config.lifetime, - ) - app.dependency_overrides[Stub(SessionMapper)] = lambda: session_mapper # type: ignore - - permissions_mapper = PermissionsMapper(cast(Redis, redis)) - app.dependency_overrides[Stub(PermissionsMapper)] = ( - lambda: permissions_mapper - ) # type: ignore - - sqlalchemy_engine = create_engine(postgres_config.url) - ioc = IoC( - sqlalchemy_engine=sqlalchemy_engine, - permissions_mapper=permissions_mapper, - hash_computer=HashComputer(), - ) - app.dependency_overrides[HandlerFactory] = lambda: ioc # type: ignore - - session_processor = SessionProcessor() - app.dependency_overrides[Stub(SessionProcessor)] = ( - lambda: session_processor - ) # type: ignore diff --git a/src/amdb/main/web_api/providers.py b/src/amdb/main/web_api/providers.py new file mode 100644 index 0000000..a1af227 --- /dev/null +++ b/src/amdb/main/web_api/providers.py @@ -0,0 +1,30 @@ +from dishka import Provider, Scope, provide +from redis import Redis + +from amdb.infrastructure.auth.session.config import SessionConfig +from amdb.infrastructure.auth.session.session_processor import SessionProcessor +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper + + +class SessionAdaptersProvider(Provider): + scope = Scope.APP + + def __init__(self, session_config: SessionConfig) -> None: + super().__init__() + self._session_config = session_config + + @provide + def session_config(self) -> SessionConfig: + return self._session_config + + @provide + def session_processor(self) -> SessionProcessor: + return SessionProcessor() + + @provide + def session_gateway(self, redis: Redis) -> SessionGateway: + return SessionMapper( + redis=redis, + session_lifetime=self._session_config.lifetime, + ) diff --git a/src/amdb/presentation/cli/alembic.py b/src/amdb/presentation/cli/alembic.py deleted file mode 100644 index 37372c6..0000000 --- a/src/amdb/presentation/cli/alembic.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated - -import typer -from alembic import config - -from amdb.infrastructure.persistence.alembic.config import ALEMBIC_CONFIG - - -migration_commands = typer.Typer(name="migration") - - -@migration_commands.command() -def alembic(commands: Annotated[list[str], typer.Argument()]) -> None: - """ - [green]Run[/green] alembic. - """ - config.main(["-c", ALEMBIC_CONFIG, *commands]) diff --git a/src/amdb/presentation/cli/movie.py b/src/amdb/presentation/cli/movie.py deleted file mode 100644 index a430a1d..0000000 --- a/src/amdb/presentation/cli/movie.py +++ /dev/null @@ -1,89 +0,0 @@ -from datetime import datetime -from typing import Annotated -from uuid import UUID - -import typer -import rich -import rich.box -import rich.table - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.commands.create_movie import CreateMovieCommand -from amdb.application.commands.delete_movie import DeleteMovieCommand -from amdb.presentation.handler_factory import HandlerFactory - - -movie_commands = typer.Typer(name="movie") - - -@movie_commands.command() -def create( - ctx: typer.Context, - title: Annotated[str, typer.Option("--title", "-t", help="Movie title.")], - release_date: Annotated[ - datetime, - typer.Option("--release_date", "-rd", help="Movie release date."), - ], - silently: Annotated[ - bool, - typer.Option("--silently", "-s", help="Do not print movie id."), - ] = False, -) -> None: - """ - [green]Create[/green] movie. - - If --silently is not used, will print movie id. - """ - ioc: HandlerFactory = ctx.obj["ioc"] - identity_provider: IdentityProvider = ctx.obj["identity_provider"] - - with ioc.create_movie(identity_provider) as create_movie_handler: - create_movie_command = CreateMovieCommand( - title=title, - release_date=release_date.date(), - ) - movie_id = create_movie_handler.execute(create_movie_command) - - if not silently: - rich.print(movie_id) - - -@movie_commands.command() -def delete( - ctx: typer.Context, - movie_id: Annotated[UUID, typer.Argument(help="Movie id.")], - force: Annotated[ - bool, - typer.Option("--force", "-f", help="Do not ask for confirmation."), - ] = False, - silently: Annotated[ - bool, - typer.Option("--silently", "-s", help="Do not print movie id"), - ] = False, -) -> None: - """ - [red]Delete[/red] movie. Also [red]deletes[/red] ratings and - reviews related to movie. - - If --force is not used, will ask for confirmation. - If --silently is not used, will print movie id. - """ - if not force: - typer.confirm( - text="Are you sure you want to delete movie?", - default=True, - abort=True, - ) - - ioc: HandlerFactory = ctx.obj["ioc"] - identity_provider: IdentityProvider = ctx.obj["identity_provider"] - - with ioc.delete_movie(identity_provider) as delete_movie_handler: - delete_movie_command = DeleteMovieCommand( - movie_id=MovieId(movie_id), - ) - delete_movie_handler.execute(delete_movie_command) - - if not silently: - rich.print(movie_id) diff --git a/src/amdb/presentation/cli/setup.py b/src/amdb/presentation/cli/setup.py deleted file mode 100644 index 7b948f5..0000000 --- a/src/amdb/presentation/cli/setup.py +++ /dev/null @@ -1,9 +0,0 @@ -import typer - -from .movie import movie_commands -from .alembic import migration_commands - - -def setup_typer_command_handlers(app: typer.Typer) -> None: - app.add_typer(movie_commands) - app.add_typer(migration_commands) diff --git a/src/amdb/presentation/create_handler.py b/src/amdb/presentation/create_handler.py new file mode 100644 index 0000000..a408fdc --- /dev/null +++ b/src/amdb/presentation/create_handler.py @@ -0,0 +1,16 @@ +__all__ = ("CreateHandler",) + +from typing import TypeVar, Protocol + +from amdb.application.common.identity_provider import IdentityProvider + + +H = TypeVar("H", covariant=True) + + +class CreateHandler(Protocol[H]): + def __call__( + self, + identity_provider: IdentityProvider, + ) -> H: + raise NotImplementedError diff --git a/src/amdb/presentation/handler_factory.py b/src/amdb/presentation/handler_factory.py deleted file mode 100644 index 5ad8d87..0000000 --- a/src/amdb/presentation/handler_factory.py +++ /dev/null @@ -1,81 +0,0 @@ -from abc import ABC, abstractmethod -from typing import ContextManager - -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.command_handlers.register_user import RegisterUserHandler -from amdb.application.command_handlers.create_movie import CreateMovieHandler -from amdb.application.command_handlers.delete_movie import DeleteMovieHandler -from amdb.application.command_handlers.rate_movie import RateMovieHandler -from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler -from amdb.application.command_handlers.review_movie import ReviewMovieHandler -from amdb.application.query_handlers.login import LoginHandler -from amdb.application.query_handlers.detailed_movie import ( - GetDetailedMovieHandler, -) -from amdb.application.query_handlers.non_detailed_movies import ( - GetNonDetailedMoviesHandler, -) -from amdb.application.query_handlers.reviews import GetReviewsHandler - - -class HandlerFactory(ABC): - @abstractmethod - def register_user(self) -> ContextManager[RegisterUserHandler]: - raise NotImplementedError - - @abstractmethod - def login(self) -> ContextManager[LoginHandler]: - raise NotImplementedError - - @abstractmethod - def get_non_detailed_movies( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetNonDetailedMoviesHandler]: - raise NotImplementedError - - @abstractmethod - def get_detailed_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[GetDetailedMovieHandler]: - raise NotImplementedError - - @abstractmethod - def create_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[CreateMovieHandler]: - raise NotImplementedError - - @abstractmethod - def delete_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[DeleteMovieHandler]: - raise NotImplementedError - - @abstractmethod - def rate_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[RateMovieHandler]: - raise NotImplementedError - - @abstractmethod - def unrate_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[UnrateMovieHandler]: - raise NotImplementedError - - @abstractmethod - def get_reviews(self) -> ContextManager[GetReviewsHandler]: - raise NotImplementedError - - @abstractmethod - def review_movie( - self, - identity_provider: IdentityProvider, - ) -> ContextManager[ReviewMovieHandler]: - raise NotImplementedError diff --git a/src/amdb/presentation/cli/__init__.py b/src/amdb/presentation/web_api/auth/__init__.py similarity index 100% rename from src/amdb/presentation/cli/__init__.py rename to src/amdb/presentation/web_api/auth/__init__.py diff --git a/src/amdb/presentation/web_api/auth/login.py b/src/amdb/presentation/web_api/auth/login.py new file mode 100644 index 0000000..e3f8fa8 --- /dev/null +++ b/src/amdb/presentation/web_api/auth/login.py @@ -0,0 +1,48 @@ +from typing import Annotated +from datetime import datetime, timezone + +from fastapi import Response +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.user import UserId +from amdb.application.queries.login import LoginQuery +from amdb.application.query_handlers.login import LoginHandler +from amdb.infrastructure.auth.session.config import SessionConfig +from amdb.infrastructure.auth.session.session_processor import SessionProcessor +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +@inject +async def login( + *, + handler: Annotated[LoginHandler, Depends()], + session_processor: Annotated[SessionProcessor, Depends()], + session_mapper: Annotated[SessionMapper, Depends()], + session_config: Annotated[SessionConfig, Depends()], + query: LoginQuery, + response: Response, +) -> UserId: + """ + Logins, returns user id, creates new authentication session + and sets cookie with its id. \n\n + + #### Returns 400: \n + * When user doesn't exist \n + * When password is incorrect \n + * When access is denied \n + """ + user_id = handler.execute(query) + + session = session_processor.create(user_id) + session_id = session_mapper.save(session) + session_expires_at = datetime.now(timezone.utc) + session_config.lifetime + + response.set_cookie( + key=SESSION_ID_COOKIE, + value=session_id, + expires=session_expires_at, + httponly=True, + ) + + return user_id diff --git a/src/amdb/presentation/web_api/auth/register.py b/src/amdb/presentation/web_api/auth/register.py new file mode 100644 index 0000000..5f78e0c --- /dev/null +++ b/src/amdb/presentation/web_api/auth/register.py @@ -0,0 +1,46 @@ +from typing import Annotated +from datetime import datetime, timezone + +from fastapi import Response +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.user import UserId +from amdb.application.commands.register_user import RegisterUserCommand +from amdb.application.command_handlers.register_user import RegisterUserHandler +from amdb.infrastructure.auth.session.config import SessionConfig +from amdb.infrastructure.auth.session.session_processor import SessionProcessor +from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +@inject +async def register( + *, + handler: Annotated[RegisterUserHandler, Depends()], + session_processor: Annotated[SessionProcessor, Depends()], + session_mapper: Annotated[SessionMapper, Depends()], + session_config: Annotated[SessionConfig, Depends()], + command: RegisterUserCommand, + response: Response, +) -> UserId: + """ + Registers user, returns his id, creates new + authentication session and sets cookie with its id. \n\n + + #### Returns 400: \n + * When name is already taken + """ + user_id = handler.execute(command) + + session = session_processor.create(user_id) + session_id = session_mapper.save(session) + session_expires_at = datetime.now(timezone.utc) + session_config.lifetime + + response.set_cookie( + key=SESSION_ID_COOKIE, + value=session_id, + expires=session_expires_at, + httponly=True, + ) + + return user_id diff --git a/src/amdb/presentation/web_api/auth/router.py b/src/amdb/presentation/web_api/auth/router.py new file mode 100644 index 0000000..f3b4f6d --- /dev/null +++ b/src/amdb/presentation/web_api/auth/router.py @@ -0,0 +1,22 @@ +__all__ = ("auth_router",) + +from fastapi import APIRouter + +from .register import register +from .login import login + + +auth_router = APIRouter( + prefix="/auth", + tags=["auth"], +) +auth_router.add_api_route( + path="/register", + endpoint=register, + methods=["POST"], +) +auth_router.add_api_route( + path="/login", + endpoint=login, + methods=["POST"], +) diff --git a/src/amdb/presentation/web_api/dependencies/depends_stub.py b/src/amdb/presentation/web_api/dependencies/depends_stub.py deleted file mode 100644 index 7a5f2e1..0000000 --- a/src/amdb/presentation/web_api/dependencies/depends_stub.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Callable - - -class Stub: - def __init__(self, dependency: Callable) -> None: - self.dependency = dependency - - def __call__(self) -> None: - raise NotImplementedError - - def __eq__(self, value: object) -> bool: - if isinstance(value, Stub): - return self.dependency == value.dependency - return False - - def __hash__(self) -> int: - return hash(self.dependency) diff --git a/src/amdb/presentation/web_api/dependencies/identity_provider.py b/src/amdb/presentation/web_api/dependencies/identity_provider.py deleted file mode 100644 index 3aee77c..0000000 --- a/src/amdb/presentation/web_api/dependencies/identity_provider.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Annotated, Optional - -from fastapi import Cookie, Depends - -from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper -from amdb.infrastructure.persistence.redis.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.auth.session.identity_provider import ( - SessionIdentityProvider, -) -from amdb.infrastructure.auth.session.session import SessionId -from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -from .depends_stub import Stub - - -def get_identity_provider( - session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], - permissions_mapper: Annotated[ - PermissionsMapper, - Depends(Stub(PermissionsMapper)), - ], - session_id: Annotated[ - Optional[str], - Cookie(alias=SESSION_ID_COOKIE), - ] = None, -) -> SessionIdentityProvider: - return SessionIdentityProvider( - session_id=SessionId(session_id) if session_id else None, - session_gateway=session_mapper, - permissions_gateway=permissions_mapper, - ) diff --git a/src/amdb/presentation/web_api/exception_handlers.py b/src/amdb/presentation/web_api/exception_handlers.py index b2dcede..1ded5b6 100644 --- a/src/amdb/presentation/web_api/exception_handlers.py +++ b/src/amdb/presentation/web_api/exception_handlers.py @@ -23,8 +23,5 @@ def _application_error_handler(_, error: ApplicationError) -> JSONResponse: return JSONResponse(content={"message": error.message}, status_code=400) -def _infrastructure_error_handler( - _, - error: InfrastructureError, -) -> JSONResponse: +def _infrastructure_error_handler(_, _2: InfrastructureError) -> JSONResponse: return JSONResponse(content=None, status_code=500) diff --git a/src/amdb/presentation/web_api/dependencies/__init__.py b/src/amdb/presentation/web_api/movies/__init__.py similarity index 100% rename from src/amdb/presentation/web_api/dependencies/__init__.py rename to src/amdb/presentation/web_api/movies/__init__.py diff --git a/src/amdb/presentation/web_api/movies/get_detailed.py b/src/amdb/presentation/web_api/movies/get_detailed.py new file mode 100644 index 0000000..ec2153a --- /dev/null +++ b/src/amdb/presentation/web_api/movies/get_detailed.py @@ -0,0 +1,55 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.movie import MovieId +from amdb.application.common.view_models.detailed_movie import ( + DetailedMovieViewModel, +) +from amdb.application.queries.detailed_movie import GetDetailedMovieQuery +from amdb.application.query_handlers.detailed_movie import ( + GetDetailedMovieHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[GetDetailedMovieHandler] + + +@inject +async def get_detailed_movie( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + movie_id: MovieId, +) -> DetailedMovieViewModel: + """ + Returns detailed movie information, detailed current user rating + and review on it. \n\n + + #### Returns 400: \n + * When movie doesn't exist + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + + handler = create_handler(identity_provider) + query = GetDetailedMovieQuery(movie_id=movie_id) + + return handler.execute(query) diff --git a/src/amdb/presentation/web_api/movies/get_non_detailed.py b/src/amdb/presentation/web_api/movies/get_non_detailed.py new file mode 100644 index 0000000..65596f3 --- /dev/null +++ b/src/amdb/presentation/web_api/movies/get_non_detailed.py @@ -0,0 +1,57 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.application.common.view_models.non_detailed_movie import ( + NonDetailedMovieViewModel, +) +from amdb.application.queries.non_detailed_movies import ( + GetNonDetailedMoviesQuery, +) +from amdb.application.query_handlers.non_detailed_movies import ( + GetNonDetailedMoviesHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[GetNonDetailedMoviesHandler] + + +@inject +async def get_non_detailed_movies( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + limit: int = 100, + offset: int = 0, +) -> list[NonDetailedMovieViewModel]: + """ + Returns list of non detailed movies and non detailed current + user rating. \n\n + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + + handler = create_handler(identity_provider) + query = GetNonDetailedMoviesQuery( + limit=limit, + offset=offset, + ) + + return handler.execute(query) diff --git a/src/amdb/presentation/web_api/movies/router.py b/src/amdb/presentation/web_api/movies/router.py new file mode 100644 index 0000000..e476763 --- /dev/null +++ b/src/amdb/presentation/web_api/movies/router.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter + +from .get_non_detailed import get_non_detailed_movies +from .get_detailed import get_detailed_movie + + +movies_router = APIRouter(tags=["movies"]) +movies_router.add_api_route( + path="/non-detailed-movies", + endpoint=get_non_detailed_movies, + methods=["GET"], +) +movies_router.add_api_route( + path="/detailed-movies/{movie_id}", + endpoint=get_detailed_movie, + methods=["GET"], +) diff --git a/src/amdb/presentation/web_api/routers/__init__.py b/src/amdb/presentation/web_api/ratings/__init__.py similarity index 100% rename from src/amdb/presentation/web_api/routers/__init__.py rename to src/amdb/presentation/web_api/ratings/__init__.py diff --git a/src/amdb/presentation/web_api/ratings/rate_movie.py b/src/amdb/presentation/web_api/ratings/rate_movie.py new file mode 100644 index 0000000..f824a98 --- /dev/null +++ b/src/amdb/presentation/web_api/ratings/rate_movie.py @@ -0,0 +1,49 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.rating import RatingId +from amdb.application.commands.rate_movie import RateMovieCommand +from amdb.application.command_handlers.rate_movie import RateMovieHandler +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[RateMovieHandler] + + +@inject +async def rate_movie( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + command: RateMovieCommand, +) -> RatingId: + """ + Create movie rating and returns its id. \n\n + + #### Returns 400: + * When access is denied + * When movie doesn't exist + * When rating already exists + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + handler = create_handler(identity_provider) + + return handler.execute(command) diff --git a/src/amdb/presentation/web_api/ratings/router.py b/src/amdb/presentation/web_api/ratings/router.py new file mode 100644 index 0000000..410a184 --- /dev/null +++ b/src/amdb/presentation/web_api/ratings/router.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +from .rate_movie import rate_movie +from .unrate_movie import unrate_movie + + +ratings_router = APIRouter( + prefix="/ratings", + tags=["ratings"], +) +ratings_router.add_api_route( + path="", + endpoint=rate_movie, + methods=["POST"], +) +ratings_router.add_api_route( + path="/{rating_id}", + endpoint=unrate_movie, + methods=["DELETE"], +) diff --git a/src/amdb/presentation/web_api/ratings/unrate_movie.py b/src/amdb/presentation/web_api/ratings/unrate_movie.py new file mode 100644 index 0000000..44fbb37 --- /dev/null +++ b/src/amdb/presentation/web_api/ratings/unrate_movie.py @@ -0,0 +1,51 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.rating import RatingId +from amdb.application.commands.unrate_movie import UnrateMovieCommand +from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[UnrateMovieHandler] + + +@inject +async def unrate_movie( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + rating_id: RatingId, +) -> None: + """ + Removes movie rating. \n\n + + #### Returns 400: + * When access is denied + * When rating doesn't exist + * When user is not a rating owner + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + + handler = create_handler(identity_provider) + command = UnrateMovieCommand(rating_id=rating_id) + + handler.execute(command) diff --git a/src/amdb/presentation/web_api/routers/auth/__init__.py b/src/amdb/presentation/web_api/reviews/__init__.py similarity index 100% rename from src/amdb/presentation/web_api/routers/auth/__init__.py rename to src/amdb/presentation/web_api/reviews/__init__.py diff --git a/src/amdb/presentation/web_api/reviews/get_detailed.py b/src/amdb/presentation/web_api/reviews/get_detailed.py new file mode 100644 index 0000000..454ba64 --- /dev/null +++ b/src/amdb/presentation/web_api/reviews/get_detailed.py @@ -0,0 +1,35 @@ +from typing import Annotated + +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.movie import MovieId +from amdb.application.common.view_models.detailed_review import ( + DetailedReviewViewModel, +) +from amdb.application.queries.detailed_reviews import GetDetailedReviewsQuery +from amdb.application.query_handlers.detailed_reviews import ( + GetDetailedReviewsHandler, +) + + +@inject +async def get_detailed_reviews( + *, + handler: Annotated[GetDetailedReviewsHandler, Depends()], + movie_id: MovieId, + limit: int = 100, + offset: int = 0, +) -> list[DetailedReviewViewModel]: + """ + Returns detailed movie reviews with ratings.\n\n + + #### Returns 400: + * When movie doesn't exist + """ + query = GetDetailedReviewsQuery( + movie_id=movie_id, + limit=limit, + offset=offset, + ) + + return handler.execute(query) diff --git a/src/amdb/presentation/web_api/reviews/review_movie.py b/src/amdb/presentation/web_api/reviews/review_movie.py new file mode 100644 index 0000000..e28f471 --- /dev/null +++ b/src/amdb/presentation/web_api/reviews/review_movie.py @@ -0,0 +1,49 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.domain.entities.review import ReviewId +from amdb.application.commands.review_movie import ReviewMovieCommand +from amdb.application.command_handlers.review_movie import ReviewMovieHandler +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[ReviewMovieHandler] + + +@inject +async def review_movie( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + command: ReviewMovieCommand, +) -> ReviewId: + """ + Create movie review and returns its id.\n\n + + #### Returns 400: + * When access is denied + * When movie doesn't exist + * When review already exists + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + handler = create_handler(identity_provider) + + return handler.execute(command) diff --git a/src/amdb/presentation/web_api/reviews/router.py b/src/amdb/presentation/web_api/reviews/router.py new file mode 100644 index 0000000..32aebc1 --- /dev/null +++ b/src/amdb/presentation/web_api/reviews/router.py @@ -0,0 +1,17 @@ +from fastapi import APIRouter + +from .get_detailed import get_detailed_reviews +from .review_movie import review_movie + + +reviews_router = APIRouter(tags=["reviews"]) +reviews_router.add_api_route( + path="/movies/{movie_id}/detailed-reviews", + endpoint=get_detailed_reviews, + methods=["GET"], +) +reviews_router.add_api_route( + path="/reviews", + endpoint=review_movie, + methods=["POST"], +) diff --git a/src/amdb/presentation/web_api/router.py b/src/amdb/presentation/web_api/router.py new file mode 100644 index 0000000..a24f55a --- /dev/null +++ b/src/amdb/presentation/web_api/router.py @@ -0,0 +1,13 @@ +from fastapi import APIRouter + +from .auth.router import auth_router +from .movies.router import movies_router +from .ratings.router import ratings_router +from .reviews.router import reviews_router + + +router = APIRouter(prefix="/v1") +router.include_router(auth_router) +router.include_router(movies_router) +router.include_router(ratings_router) +router.include_router(reviews_router) diff --git a/src/amdb/presentation/web_api/routers/auth/login.py b/src/amdb/presentation/web_api/routers/auth/login.py deleted file mode 100644 index 7b594ea..0000000 --- a/src/amdb/presentation/web_api/routers/auth/login.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Annotated - -from fastapi import Response, Depends - -from amdb.domain.entities.user import UserId -from amdb.application.queries.login import LoginQuery -from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.depends_stub import Stub -from amdb.presentation.web_api.constants import SESSION_ID_COOKIE - - -async def login( - ioc: Annotated[HandlerFactory, Depends()], - session_processor: Annotated[ - SessionProcessor, - Depends(Stub(SessionProcessor)), - ], - session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], - login_query: LoginQuery, - response: Response, -) -> UserId: - """ - ## Returns: \n - - user id \n - - session id in cookies \n - - ## Errors: \n - - When access is denied \n - - When user name doesn't exist \n - - When password is incorrect \n - """ - with ioc.login() as login_handler: - user_id = login_handler.execute(login_query) - - session = session_processor.create(user_id=user_id) - session_mapper.save(session) - - response.set_cookie( - key=SESSION_ID_COOKIE, - value=session.id, - httponly=True, - ) - - return user_id diff --git a/src/amdb/presentation/web_api/routers/auth/register.py b/src/amdb/presentation/web_api/routers/auth/register.py deleted file mode 100644 index c0c38fa..0000000 --- a/src/amdb/presentation/web_api/routers/auth/register.py +++ /dev/null @@ -1,44 +0,0 @@ -from typing import Annotated - -from fastapi import Response, Depends - -from amdb.domain.entities.user import UserId -from amdb.application.commands.register_user import RegisterUserCommand -from amdb.infrastructure.auth.session.session_processor import SessionProcessor -from amdb.infrastructure.persistence.redis.mappers.session import SessionMapper -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.depends_stub import Stub -from amdb.presentation.web_api.constants import SESSION_ID_COOKIE - - -async def register( - ioc: Annotated[HandlerFactory, Depends()], - session_processor: Annotated[ - SessionProcessor, - Depends(Stub(SessionProcessor)), - ], - session_mapper: Annotated[SessionMapper, Depends(Stub(SessionMapper))], - register_user_command: RegisterUserCommand, - response: Response, -) -> UserId: - """ - ## Returns: \n - - user id \n - - session id in cookies \n - - ## Errors: \n - - When user name already exists \n - """ - with ioc.register_user() as register_user_handler: - user_id = register_user_handler.execute(register_user_command) - - session = session_processor.create(user_id=user_id) - session_mapper.save(session) - - response.set_cookie( - key=SESSION_ID_COOKIE, - value=session.id, - httponly=True, - ) - - return user_id diff --git a/src/amdb/presentation/web_api/routers/auth/router.py b/src/amdb/presentation/web_api/routers/auth/router.py deleted file mode 100644 index ef97d8b..0000000 --- a/src/amdb/presentation/web_api/routers/auth/router.py +++ /dev/null @@ -1,24 +0,0 @@ -from fastapi import APIRouter - -from .register import register -from .login import login - - -def create_auth_router() -> APIRouter: - router = APIRouter( - prefix="/auth", - tags=["auth"], - ) - - router.add_api_route( - path="/register", - endpoint=register, - methods=["POST"], - ) - router.add_api_route( - path="/login", - endpoint=login, - methods=["POST"], - ) - - return router diff --git a/src/amdb/presentation/web_api/routers/movies/__init__.py b/src/amdb/presentation/web_api/routers/movies/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/amdb/presentation/web_api/routers/movies/get_movies.py b/src/amdb/presentation/web_api/routers/movies/get_movies.py deleted file mode 100644 index aa06916..0000000 --- a/src/amdb/presentation/web_api/routers/movies/get_movies.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.queries.detailed_movie import GetDetailedMovieQuery -from amdb.application.queries.non_detailed_movies import ( - GetNonDetailedMoviesQuery, -) -from amdb.application.common.view_models.detailed_movie import ( - DetailedMovieViewModel, -) -from amdb.application.common.view_models.non_detailed_movie import ( - NonDetailedMovieViewModel, -) -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def get_non_detailed_movies( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, - Depends(get_identity_provider), - ], - limit: int = 100, - offset: int = 0, -) -> list[NonDetailedMovieViewModel]: - """ - ## Errors: \n - - When access is denied \n - """ - with ioc.get_non_detailed_movies( - identity_provider, - ) as get_non_detailed_movies_handler: - get_non_detailed_movies_query = GetNonDetailedMoviesQuery( - limit=limit, - offset=offset, - ) - result = get_non_detailed_movies_handler.execute( - get_non_detailed_movies_query, - ) - return result - - -async def get_detailed_movie( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, - Depends(get_identity_provider), - ], - movie_id: MovieId, -) -> DetailedMovieViewModel: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - """ - with ioc.get_detailed_movie( - identity_provider, - ) as get_detailed_movie_handler: - get_detailed_movie_query = GetDetailedMovieQuery( - movie_id=movie_id, - ) - result = get_detailed_movie_handler.execute(get_detailed_movie_query) - return result diff --git a/src/amdb/presentation/web_api/routers/movies/router.py b/src/amdb/presentation/web_api/routers/movies/router.py deleted file mode 100644 index 890b974..0000000 --- a/src/amdb/presentation/web_api/routers/movies/router.py +++ /dev/null @@ -1,23 +0,0 @@ -from fastapi import APIRouter - -from .get_movies import get_non_detailed_movies, get_detailed_movie - - -def create_movies_router() -> APIRouter: - router = APIRouter( - prefix="", - tags=["movies"], - ) - - router.add_api_route( - path="/non-detailed-movies", - endpoint=get_non_detailed_movies, - methods=["GET"], - ) - router.add_api_route( - path="/detailed-movies/{movie_id}", - endpoint=get_detailed_movie, - methods=["GET"], - ) - - return router diff --git a/src/amdb/presentation/web_api/routers/ratings/__init__.py b/src/amdb/presentation/web_api/routers/ratings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py b/src/amdb/presentation/web_api/routers/ratings/rate_movie.py deleted file mode 100644 index 585a648..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/rate_movie.py +++ /dev/null @@ -1,33 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.rating import RatingId -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.commands.rate_movie import RateMovieCommand -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def rate_movie( - ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], - identity_provider: Annotated[ - IdentityProvider, - Depends(get_identity_provider), - ], - rate_movie_command: RateMovieCommand, -) -> RatingId: - """ - ## Returns: \n - - Rating id - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - - When movie is already rated \n - """ - with ioc.rate_movie(identity_provider) as rate_movie_handler: - rating_id = rate_movie_handler.execute(rate_movie_command) - - return rating_id diff --git a/src/amdb/presentation/web_api/routers/ratings/router.py b/src/amdb/presentation/web_api/routers/ratings/router.py deleted file mode 100644 index 26fbd93..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/router.py +++ /dev/null @@ -1,26 +0,0 @@ -from fastapi import APIRouter - -from .rate_movie import rate_movie -from .unrate_movie import unrate_movie - - -def create_ratings_router() -> APIRouter: - router = APIRouter( - prefix="", - tags=["ratings"], - ) - - router.add_api_route( - path="/me/ratings", - endpoint=rate_movie, - methods=["POST"], - tags=["me"], - ) - router.add_api_route( - path="/me/ratings/{rating_id}", - endpoint=unrate_movie, - methods=["DELETE"], - tags=["me"], - ) - - return router diff --git a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py b/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py deleted file mode 100644 index e0b9f4f..0000000 --- a/src/amdb/presentation/web_api/routers/ratings/unrate_movie.py +++ /dev/null @@ -1,32 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.rating import RatingId -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.commands.unrate_movie import UnrateMovieCommand -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -async def unrate_movie( - ioc: Annotated[HandlerFactory, Depends(HandlerFactory)], - identity_provider: Annotated[ - IdentityProvider, - Depends(get_identity_provider), - ], - rating_id: RatingId, -) -> None: - """ - ## Errors: \n - - When access is denied \n - - When user is not an owner of rating \n - - When movie is already rated \n - """ - with ioc.unrate_movie(identity_provider) as unrate_movie_handler: - unrate_movie_command = UnrateMovieCommand( - rating_id=rating_id, - ) - unrate_movie_handler.execute(unrate_movie_command) diff --git a/src/amdb/presentation/web_api/routers/reviews/__init__.py b/src/amdb/presentation/web_api/routers/reviews/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/amdb/presentation/web_api/routers/reviews/get_reviews.py b/src/amdb/presentation/web_api/routers/reviews/get_reviews.py deleted file mode 100644 index 70c5962..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/get_reviews.py +++ /dev/null @@ -1,28 +0,0 @@ -from typing import Annotated - -from fastapi import Depends - -from amdb.domain.entities.movie import MovieId -from amdb.application.common.view_models.review import ReviewViewModel -from amdb.application.queries.reviews import GetReviewsQuery -from amdb.presentation.handler_factory import HandlerFactory - - -async def get_reviews( - ioc: Annotated[HandlerFactory, Depends()], - movie_id: MovieId, - limit: int = 100, - offset: int = 0, -) -> list[ReviewViewModel]: - """ - ## Errors: \n - - When movie doesn't exist \n - """ - with ioc.get_reviews() as get_reviews_handler: - get_reviews_query = GetReviewsQuery( - movie_id=movie_id, - limit=limit, - offset=offset, - ) - result = get_reviews_handler.execute(get_reviews_query) - return result diff --git a/src/amdb/presentation/web_api/routers/reviews/review_movie.py b/src/amdb/presentation/web_api/routers/reviews/review_movie.py deleted file mode 100644 index 0498eb9..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/review_movie.py +++ /dev/null @@ -1,46 +0,0 @@ -from typing import Annotated - -from fastapi import Depends -from pydantic import BaseModel - -from amdb.domain.entities.movie import MovieId -from amdb.domain.entities.review import ReviewId, ReviewType -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.commands.review_movie import ReviewMovieCommand -from amdb.presentation.handler_factory import HandlerFactory -from amdb.presentation.web_api.dependencies.identity_provider import ( - get_identity_provider, -) - - -class ReviewMovie(BaseModel): - title: str - content: str - type: ReviewType - - -async def review_movie( - ioc: Annotated[HandlerFactory, Depends()], - identity_provider: Annotated[ - IdentityProvider, - Depends(get_identity_provider), - ], - movie_id: MovieId, - data: ReviewMovie, -) -> ReviewId: - """ - ## Errors: \n - - When access is denied \n - - When movie doesn't exist \n - - When movie is already reviewd \n - """ - with ioc.review_movie(identity_provider) as review_movie_handler: - review_movie_command = ReviewMovieCommand( - movie_id=movie_id, - title=data.title, - content=data.content, - type=data.type, - ) - review_id = review_movie_handler.execute(review_movie_command) - - return review_id diff --git a/src/amdb/presentation/web_api/routers/reviews/router.py b/src/amdb/presentation/web_api/routers/reviews/router.py deleted file mode 100644 index 4fb4ce9..0000000 --- a/src/amdb/presentation/web_api/routers/reviews/router.py +++ /dev/null @@ -1,25 +0,0 @@ -from fastapi import APIRouter - -from .get_reviews import get_reviews -from .review_movie import review_movie - - -def create_reviews_router() -> APIRouter: - router = APIRouter( - prefix="", - tags=["reviews"], - ) - - router.add_api_route( - path="/movies/{movie_id}/reviews", - endpoint=get_reviews, - methods=["GET"], - ) - router.add_api_route( - path="/me/reviews", - endpoint=review_movie, - methods=["POST"], - tags=["me"], - ) - - return router diff --git a/src/amdb/presentation/web_api/routers/setup.py b/src/amdb/presentation/web_api/routers/setup.py deleted file mode 100644 index 616b55b..0000000 --- a/src/amdb/presentation/web_api/routers/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from fastapi import FastAPI - -from .auth.router import create_auth_router -from .movies.router import create_movies_router -from .ratings.router import create_ratings_router -from .reviews.router import create_reviews_router - - -def setup_routers(app: FastAPI) -> None: - app.include_router(create_auth_router()) - app.include_router(create_movies_router()) - app.include_router(create_ratings_router()) - app.include_router(create_reviews_router()) diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index 0af152e..5baac92 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -21,17 +21,23 @@ from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( PasswordHashMapper, ) +from amdb.infrastructure.persistence.sqlalchemy.mappers.permissions import ( + PermissionsMapper, +) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( NonDetailedMovieViewModelMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( DetailedMovieViewModelMapper, ) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.review import ( - ReviewViewModelMapper, +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( + DetailedReviewViewModelMapper, ) -from amdb.infrastructure.persistence.redis.mappers.permissions import ( - PermissionsMapper, +from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( + PermissionsMapperCacheProvider, +) +from amdb.infrastructure.persistence.caching.permissions_mapper import ( + CachingPermissionsMapper, ) from amdb.infrastructure.password_manager.hash_computer import HashComputer from amdb.infrastructure.password_manager.password_manager import ( @@ -59,8 +65,14 @@ def sqlalchemy_connection(sqlalchemy_engine: Engine) -> Iterator[Connection]: @pytest.fixture -def permissions_gateway(redis: Redis) -> PermissionsMapper: - return PermissionsMapper(redis) +def permissions_gateway( + redis: Redis, + sqlalchemy_connection: Connection, +) -> CachingPermissionsMapper: + return CachingPermissionsMapper( + permissions_mapper=PermissionsMapper(sqlalchemy_connection), + cache_provider=PermissionsMapperCacheProvider(redis), + ) @pytest.fixture @@ -98,8 +110,10 @@ def non_detailed_movie_reader( @pytest.fixture -def review_reader(sqlalchemy_connection: Connection) -> ReviewViewModelMapper: - return ReviewViewModelMapper(sqlalchemy_connection) +def detailed_review_reader( + sqlalchemy_connection: Connection, +) -> DetailedMovieViewModelMapper: + return DetailedReviewViewModelMapper(sqlalchemy_connection) @pytest.fixture diff --git a/tests/unit/application/query_handlers/test_get_reviews.py b/tests/unit/application/query_handlers/test_get_detailed_reviews.py similarity index 70% rename from tests/unit/application/query_handlers/test_get_reviews.py rename to tests/unit/application/query_handlers/test_get_detailed_reviews.py index 86b4d8b..b879fc8 100644 --- a/tests/unit/application/query_handlers/test_get_reviews.py +++ b/tests/unit/application/query_handlers/test_get_detailed_reviews.py @@ -12,25 +12,29 @@ from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.readers.review import ReviewViewModelReader -from amdb.application.common.view_models.review import ( +from amdb.application.common.readers.detailed_review import ( + DetailedReviewViewModelReader, +) +from amdb.application.common.view_models.detailed_review import ( UserRating, UserReview, - ReviewViewModel, + DetailedReviewViewModel, +) +from amdb.application.queries.detailed_reviews import GetDetailedReviewsQuery +from amdb.application.query_handlers.detailed_reviews import ( + GetDetailedReviewsHandler, ) -from amdb.application.queries.reviews import GetReviewsQuery -from amdb.application.query_handlers.reviews import GetReviewsHandler from amdb.application.common.constants.exceptions import MOVIE_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError -def test_get_reviews( +def test_get_detailed_reviews( user_gateway: UserGateway, movie_gateway: MovieGateway, rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - review_reader: ReviewViewModelReader, + detailed_review_reader: DetailedReviewViewModelReader, ): user = User( id=UserId(uuid7()), @@ -69,18 +73,18 @@ def test_get_reviews( unit_of_work.commit() - get_reviews_query = GetReviewsQuery( + get_detailed_reviews_query = GetDetailedReviewsQuery( movie_id=movie.id, limit=10, offset=0, ) - get_reviews_handler = GetReviewsHandler( + get_detailed_reviews_handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, - review_view_model_reader=review_reader, + detailed_review_reader=detailed_review_reader, ) expected_result = [ - ReviewViewModel( + DetailedReviewViewModel( user_id=user.id, user_review=UserReview( id=review.id, @@ -96,26 +100,26 @@ def test_get_reviews( ), ), ] - result = get_reviews_handler.execute(get_reviews_query) + result = get_detailed_reviews_handler.execute(get_detailed_reviews_query) assert expected_result == result -def test_get_reviews_should_raise_error_when_movie_does_not_exist( +def test_get_detailed_reviews_should_raise_error_when_movie_does_not_exist( movie_gateway: MovieGateway, - review_reader: ReviewViewModelReader, + detailed_review_reader: DetailedReviewViewModelReader, ): - get_reviews_query = GetReviewsQuery( + get_detailed_reviews_query = GetDetailedReviewsQuery( movie_id=MovieId(uuid7()), limit=10, offset=0, ) - get_reviews_handler = GetReviewsHandler( + get_detailed_reviews_handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, - review_view_model_reader=review_reader, + detailed_review_reader=detailed_review_reader, ) with pytest.raises(ApplicationError) as error: - get_reviews_handler.execute(get_reviews_query) + get_detailed_reviews_handler.execute(get_detailed_reviews_query) assert error.value.message == MOVIE_DOES_NOT_EXIST From 0c32872631da46e9eb38d93e2b458d3a4daa4ea7 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 24 Feb 2024 01:05:27 +0400 Subject: [PATCH 08/39] Add `create reviews table` migration --- .../migrations/versions/a2f7c2383ba8_.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py new file mode 100644 index 0000000..2a7b54c --- /dev/null +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py @@ -0,0 +1,37 @@ +""" +Add permissions table + +Revision ID: a2f7c2383ba8 +Revises: 65f8840f4494 +Create Date: 2024-02-24 00:59:16.630215 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = "a2f7c2383ba8" +down_revision: Union[str, None] = "65f8840f4494" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "permissions", + sa.Column("user_id", sa.Uuid(), nullable=False), + sa.Column("value", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("user_id"), + sa.ForeignKeyConstraint( + ["user_id"], + ["users.id"], + ondelete="CASCADE", + ), + ) + + +def downgrade() -> None: + op.drop_table("permissions") From ac44d71f95e6f692d7abb61440fedbb8420c3cc4 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 24 Feb 2024 12:22:28 +0400 Subject: [PATCH 09/39] Make review type string --- src/amdb/domain/entities/review.py | 10 +-- .../persistence/alembic/migrations/env.py | 28 +++------ .../migrations/versions/a2f7c2383ba8_.py | 61 ++++++++++++++++++- .../persistence/sqlalchemy/models/review.py | 2 +- src/amdb/main/web_api/providers.py | 7 +++ 5 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/amdb/domain/entities/review.py b/src/amdb/domain/entities/review.py index 31f8e39..1b1a22c 100644 --- a/src/amdb/domain/entities/review.py +++ b/src/amdb/domain/entities/review.py @@ -1,7 +1,7 @@ from dataclasses import dataclass from datetime import datetime from typing import NewType -from enum import IntEnum +from enum import Enum from uuid import UUID from .user import UserId @@ -11,10 +11,10 @@ ReviewId = NewType("ReviewId", UUID) -class ReviewType(IntEnum): - NEUTRAL = 0 - POSITIVE = 1 - NEGATIVE = 2 +class ReviewType(Enum): + NEUTRAL = "neutral" + POSITIVE = "positive" + NEGATIVE = "negative" @dataclass(frozen=True, slots=True) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/env.py b/src/amdb/infrastructure/persistence/alembic/migrations/env.py index 00a7fa0..8186e3d 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/env.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/env.py @@ -6,6 +6,7 @@ from alembic import context from amdb.infrastructure.persistence.sqlalchemy.models.base import Model +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -27,33 +28,20 @@ # my_important_option = config.get_main_option("my_important_option") # ... etc. -POSTGRES_HOST_ENV = "POSTGRES_HOST" -POSTGRES_PORT_ENV = "POSTGRES_PORT" -POSTGRES_NAME_ENV = "POSTGRES_DB" -POSTGRES_USER_ENV = "POSTGRES_USER" -POSTGRES_PASSWORD_ENV = "POSTGRES_PASSWORD" - - -def get_env(key: str) -> str: - value = os.getenv(key) - if value is None: - message = f"Env variable {key} is not set" - raise ValueError(message) - return value - def get_sqlalchemy_url() -> str: sqlalchemy_url = config.get_main_option("sqlalchemy.url") if sqlalchemy_url is not None: return sqlalchemy_url else: - host = get_env(POSTGRES_HOST_ENV) - port = get_env(POSTGRES_PORT_ENV) - name = get_env(POSTGRES_NAME_ENV) - user = get_env(POSTGRES_USER_ENV) - password = get_env(POSTGRES_PASSWORD_ENV) + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + postgres_config = PostgresConfig.from_toml(path_to_config) - return f"postgresql://{user}:{password}@{host}:{port}/{name}" + return postgres_config.url def run_migrations_offline() -> None: diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py index 2a7b54c..061ebe4 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py @@ -1,5 +1,6 @@ """ -Add permissions table +Add permissions table, +Make type column string in review table Revision ID: a2f7c2383ba8 Revises: 65f8840f4494 @@ -31,7 +32,65 @@ def upgrade() -> None: ondelete="CASCADE", ), ) + with op.batch_alter_table("reviews") as batch_op: + batch_op.alter_column( + "type", + new_column_name="old_type", + ) + batch_op.add_column( + sa.Column("type", sa.String(), nullable=True), + ) + op.execute( + """ + UPDATE reviews + SET type = + ( + SELECT + CASE + WHEN r.old_type = 0 THEN 'neutral' + WHEN r.old_type = 1 THEN 'positive' + WHEN r.old_type = 2 THEN 'negative' + END + FROM reviews r + ) + """, + ) + with op.batch_alter_table("reviews") as batch_op: + batch_op.drop_column("old_type") + batch_op.alter_column( + "type", + nullable=False, + ) def downgrade() -> None: op.drop_table("permissions") + with op.batch_alter_table("reviews") as batch_op: + batch_op.alter_column( + "type", + new_column_name="old_type", + ) + batch_op.add_column( + sa.Column("type", sa.Integer(), nullable=True), + ) + op.execute( + """ + UPDATE reviews + SET type = + ( + SELECT + CASE + WHEN r.old_type = 'neutral' THEN 0 + WHEN r.old_type = 'positive' THEN 1 + WHEN r.old_type = 'negative' THEN 2 + END + FROM reviews r + ) + """, + ) + with op.batch_alter_table("reviews") as batch_op: + batch_op.drop_column("old_type") + batch_op.alter_column( + "type", + nullable=False, + ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py index 90c4f6f..37d3221 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py @@ -23,7 +23,7 @@ class ReviewModel(Model): ) title: Mapped[str] content: Mapped[str] - type: Mapped[int] + type: Mapped[str] created_at: Mapped[datetime] user: Mapped[UserModel] = relationship() diff --git a/src/amdb/main/web_api/providers.py b/src/amdb/main/web_api/providers.py index a1af227..8c37894 100644 --- a/src/amdb/main/web_api/providers.py +++ b/src/amdb/main/web_api/providers.py @@ -22,6 +22,13 @@ def session_config(self) -> SessionConfig: def session_processor(self) -> SessionProcessor: return SessionProcessor() + @provide + def session_mapper(self, redis: Redis) -> SessionMapper: + return SessionMapper( + redis=redis, + session_lifetime=self._session_config.lifetime, + ) + @provide def session_gateway(self, redis: Redis) -> SessionGateway: return SessionMapper( From 1db6f89ee335facd42822680e9120c71820c3447 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 24 Feb 2024 17:54:42 +0400 Subject: [PATCH 10/39] Add doc strings to `IdentityProvider` protocol --- src/amdb/application/common/identity_provider.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/amdb/application/common/identity_provider.py b/src/amdb/application/common/identity_provider.py index 769cd06..b992b4a 100644 --- a/src/amdb/application/common/identity_provider.py +++ b/src/amdb/application/common/identity_provider.py @@ -5,10 +5,22 @@ class IdentityProvider(Protocol): def user_id(self) -> UserId: + """ + Returns current user id if authenticated, + otherwise raises error + """ raise NotImplementedError def user_id_or_none(self) -> Optional[UserId]: + """ + Returns current user id if authenticated, + otherwise returns None + """ raise NotImplementedError def permissions(self) -> int: + """ + Returns current user permissions if authenticated, + otherwise raises error + """ raise NotImplementedError From 8b43cdfd8b14f39f8359eb40f89ecd469020adbd Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 24 Feb 2024 19:50:20 +0400 Subject: [PATCH 11/39] Refactor tests --- .../command_handlers/test_create_movie.py | 6 ++-- .../command_handlers/test_delete_movie.py | 6 ++-- .../command_handlers/test_rate_movie.py | 30 +++++++++---------- .../command_handlers/test_register_user.py | 12 ++++---- .../command_handlers/test_review_movie.py | 24 +++++++-------- .../command_handlers/test_unrate_movie.py | 24 +++++++-------- .../query_handlers/test_detailed_movie.py | 12 ++++---- .../test_get_detailed_reviews.py | 12 ++++---- .../application/query_handlers/test_login.py | 12 ++++---- .../test_non_detailed_movies.py | 8 ++--- 10 files changed, 72 insertions(+), 74 deletions(-) diff --git a/tests/unit/application/command_handlers/test_create_movie.py b/tests/unit/application/command_handlers/test_create_movie.py index 719cccd..1a73f7f 100644 --- a/tests/unit/application/command_handlers/test_create_movie.py +++ b/tests/unit/application/command_handlers/test_create_movie.py @@ -11,14 +11,14 @@ def test_create_movie( movie_gateway: MovieGateway, unit_of_work: UnitOfWork, ): - create_movie_command = CreateMovieCommand( + command = CreateMovieCommand( title="Matrix", release_date=date(1999, 3, 31), ) - create_movie_handler = CreateMovieHandler( + handler = CreateMovieHandler( create_movie=CreateMovie(), movie_gateway=movie_gateway, unit_of_work=unit_of_work, ) - create_movie_handler.execute(create_movie_command) + handler.execute(command) diff --git a/tests/unit/application/command_handlers/test_delete_movie.py b/tests/unit/application/command_handlers/test_delete_movie.py index 5fd6fdb..9e01fa0 100644 --- a/tests/unit/application/command_handlers/test_delete_movie.py +++ b/tests/unit/application/command_handlers/test_delete_movie.py @@ -50,10 +50,10 @@ def test_delete_movie_should_raise_error_when_movie_does_not_exist( review_gateway: ReviewGateway, unit_of_work: UnitOfWork, ): - delete_movie_command = DeleteMovieCommand( + command = DeleteMovieCommand( movie_id=MovieId(uuid7()), ) - delete_movie_handler = DeleteMovieHandler( + handler = DeleteMovieHandler( movie_gateway=movie_gateway, rating_gateway=rating_gateway, review_gateway=review_gateway, @@ -61,6 +61,6 @@ def test_delete_movie_should_raise_error_when_movie_does_not_exist( ) with pytest.raises(ApplicationError) as error: - delete_movie_handler.execute(delete_movie_command) + handler.execute(command) assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/command_handlers/test_rate_movie.py b/tests/unit/application/command_handlers/test_rate_movie.py index e6b35f2..c7cb76f 100644 --- a/tests/unit/application/command_handlers/test_rate_movie.py +++ b/tests/unit/application/command_handlers/test_rate_movie.py @@ -68,11 +68,11 @@ def test_rate_movie( return_value=user.id, ) - rate_movie_command = RateMovieCommand( + command = RateMovieCommand( movie_id=movie.id, rating=9, ) - rate_movie_handler = RateMovieHandler( + handler = RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), permissions_gateway=permissions_gateway, @@ -83,7 +83,7 @@ def test_rate_movie( identity_provider=identity_provider_with_correct_permissions, ) - rate_movie_handler.execute(rate_movie_command) + handler.execute(command) def test_rate_movie_should_raise_error_when_access_is_denied( @@ -94,11 +94,11 @@ def test_rate_movie_should_raise_error_when_access_is_denied( unit_of_work: UnitOfWork, identity_provider_with_incorrect_permissions: IdentityProvider, ): - rate_movie_command = RateMovieCommand( + command = RateMovieCommand( movie_id=MovieId(uuid7()), rating=9, ) - rate_movie_handler = RateMovieHandler( + handler = RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), permissions_gateway=permissions_gateway, @@ -110,7 +110,7 @@ def test_rate_movie_should_raise_error_when_access_is_denied( ) with pytest.raises(ApplicationError) as error: - rate_movie_handler.execute(rate_movie_command) + handler.execute(command) assert error.value.message == RATE_MOVIE_ACCESS_DENIED @@ -123,11 +123,11 @@ def test_rate_movie_should_raise_error_when_movie_does_not_exist( unit_of_work: UnitOfWork, identity_provider_with_correct_permissions: IdentityProvider, ): - rate_movie_command = RateMovieCommand( + command = RateMovieCommand( movie_id=MovieId(uuid7()), rating=9, ) - rate_movie_handler = RateMovieHandler( + handler = RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), permissions_gateway=permissions_gateway, @@ -139,7 +139,7 @@ def test_rate_movie_should_raise_error_when_movie_does_not_exist( ) with pytest.raises(ApplicationError) as error: - rate_movie_handler.execute(rate_movie_command) + handler.execute(command) assert error.value.message == MOVIE_DOES_NOT_EXIST @@ -182,11 +182,11 @@ def test_rate_movie_should_raise_error_when_movie_already_rated( return_value=user.id, ) - rate_movie_command = RateMovieCommand( + command = RateMovieCommand( movie_id=movie.id, rating=9, ) - rate_movie_handler = RateMovieHandler( + handler = RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), permissions_gateway=permissions_gateway, @@ -198,7 +198,7 @@ def test_rate_movie_should_raise_error_when_movie_already_rated( ) with pytest.raises(ApplicationError) as error: - rate_movie_handler.execute(rate_movie_command) + handler.execute(command) assert error.value.message == MOVIE_ALREADY_RATED @@ -246,11 +246,11 @@ def test_rate_movie_should_raise_error_when_rating_is_invalid( return_value=user.id, ) - rate_movie_command = RateMovieCommand( + command = RateMovieCommand( movie_id=movie.id, rating=rating_value, ) - rate_movie_handler = RateMovieHandler( + handler = RateMovieHandler( access_concern=AccessConcern(), rate_movie=RateMovie(), permissions_gateway=permissions_gateway, @@ -262,6 +262,6 @@ def test_rate_movie_should_raise_error_when_rating_is_invalid( ) with pytest.raises(DomainError) as error: - rate_movie_handler.execute(rate_movie_command) + handler.execute(command) assert error.value.message == INVALID_RATING_VALUE diff --git a/tests/unit/application/command_handlers/test_register_user.py b/tests/unit/application/command_handlers/test_register_user.py index 7c3dda9..281c5b4 100644 --- a/tests/unit/application/command_handlers/test_register_user.py +++ b/tests/unit/application/command_handlers/test_register_user.py @@ -21,11 +21,11 @@ def test_register_user( unit_of_work: UnitOfWork, password_manager: PasswordManager, ): - register_user_command = RegisterUserCommand( + command = RegisterUserCommand( name="John Doe", password="Secret", ) - register_user_handler = RegisterUserHandler( + handler = RegisterUserHandler( create_user=CreateUser(), user_gateway=user_gateway, permissions_gateway=permissions_gateway, @@ -33,7 +33,7 @@ def test_register_user( password_manager=password_manager, ) - register_user_handler.execute(register_user_command) + handler.execute(command) def test_create_user_should_raise_error_when_user_name_already_exists( @@ -51,11 +51,11 @@ def test_create_user_should_raise_error_when_user_name_already_exists( user_gateway.save(user) unit_of_work.commit() - register_user_command = RegisterUserCommand( + command = RegisterUserCommand( name=user_name, password="Secret", ) - register_user_handler = RegisterUserHandler( + handler = RegisterUserHandler( create_user=CreateUser(), user_gateway=user_gateway, permissions_gateway=permissions_gateway, @@ -64,6 +64,6 @@ def test_create_user_should_raise_error_when_user_name_already_exists( ) with pytest.raises(ApplicationError) as error: - register_user_handler.execute(register_user_command) + handler.execute(command) assert error.value.message == USER_NAME_ALREADY_EXISTS diff --git a/tests/unit/application/command_handlers/test_review_movie.py b/tests/unit/application/command_handlers/test_review_movie.py index ecccbc0..54b47d7 100644 --- a/tests/unit/application/command_handlers/test_review_movie.py +++ b/tests/unit/application/command_handlers/test_review_movie.py @@ -66,13 +66,13 @@ def test_review_movie( return_value=user.id, ) - review_movie_command = ReviewMovieCommand( + command = ReviewMovieCommand( movie_id=movie.id, title="Not bad", content="Great soundtrack", type=ReviewType.POSITIVE, ) - review_movie_handler = ReviewMovieHandler( + handler = ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), permissions_gateway=permissions_gateway, @@ -83,7 +83,7 @@ def test_review_movie( identity_provider=identity_provider_with_correct_permissions, ) - review_movie_handler.execute(review_movie_command) + handler.execute(command) def test_review_movie_should_raise_error_when_access_is_denied( @@ -94,13 +94,13 @@ def test_review_movie_should_raise_error_when_access_is_denied( unit_of_work: UnitOfWork, identity_provider_with_incorrect_permissions: IdentityProvider, ): - review_movie_command = ReviewMovieCommand( + command = ReviewMovieCommand( movie_id=MovieId(uuid7()), title="Mid", content="So-so", type=ReviewType.NEUTRAL, ) - review_movie_handler = ReviewMovieHandler( + handler = ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), permissions_gateway=permissions_gateway, @@ -112,7 +112,7 @@ def test_review_movie_should_raise_error_when_access_is_denied( ) with pytest.raises(ApplicationError) as error: - review_movie_handler.execute(review_movie_command) + handler.execute(command) assert error.value.message == REVIEW_MOVIE_ACCESS_DENIED @@ -125,13 +125,13 @@ def test_review_movie_should_raise_error_when_movie_does_not_exist( unit_of_work: UnitOfWork, identity_provider_with_correct_permissions: IdentityProvider, ): - review_movie_command = ReviewMovieCommand( + command = ReviewMovieCommand( movie_id=MovieId(uuid7()), title="Fantastic", content="Awesome!!", type=ReviewType.POSITIVE, ) - review_movie_handler = ReviewMovieHandler( + handler = ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), permissions_gateway=permissions_gateway, @@ -143,7 +143,7 @@ def test_review_movie_should_raise_error_when_movie_does_not_exist( ) with pytest.raises(ApplicationError) as error: - review_movie_handler.execute(review_movie_command) + handler.execute(command) assert error.value.message == MOVIE_DOES_NOT_EXIST @@ -188,13 +188,13 @@ def test_review_movie_should_raise_error_when_movie_already_reviewed( return_value=user.id, ) - review_movie_command = ReviewMovieCommand( + command = ReviewMovieCommand( movie_id=movie.id, title="Masterpice", content="Extremely underrated", type=ReviewType.POSITIVE, ) - review_movie_handler = ReviewMovieHandler( + handler = ReviewMovieHandler( access_concern=AccessConcern(), review_movie=ReviewMovie(), permissions_gateway=permissions_gateway, @@ -206,6 +206,6 @@ def test_review_movie_should_raise_error_when_movie_already_reviewed( ) with pytest.raises(ApplicationError) as error: - review_movie_handler.execute(review_movie_command) + handler.execute(command) assert error.value.message == MOVIE_ALREADY_REVIEWED diff --git a/tests/unit/application/command_handlers/test_unrate_movie.py b/tests/unit/application/command_handlers/test_unrate_movie.py index bc5d2b1..34385f9 100644 --- a/tests/unit/application/command_handlers/test_unrate_movie.py +++ b/tests/unit/application/command_handlers/test_unrate_movie.py @@ -75,10 +75,10 @@ def test_unrate_movie( return_value=user.id, ) - unrate_movie_command = UnrateMovieCommand( + command = UnrateMovieCommand( rating_id=rating.id, ) - unrate_movie_handler = UnrateMovieHandler( + handler = UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), permissions_gateway=permissions_gateway, @@ -88,7 +88,7 @@ def test_unrate_movie( identity_provider=identity_provider_with_correct_permissions, ) - unrate_movie_handler.execute(unrate_movie_command) + handler.execute(command) def test_unrate_movie_should_raise_error_when_access_is_denied( @@ -98,10 +98,10 @@ def test_unrate_movie_should_raise_error_when_access_is_denied( unit_of_work: UnitOfWork, identity_provider_with_incorrect_permissions: IdentityProvider, ): - unrate_movie_command = UnrateMovieCommand( + command = UnrateMovieCommand( rating_id=RatingId(uuid7()), ) - unrate_movie_handler = UnrateMovieHandler( + handler = UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), permissions_gateway=permissions_gateway, @@ -112,7 +112,7 @@ def test_unrate_movie_should_raise_error_when_access_is_denied( ) with pytest.raises(ApplicationError) as error: - unrate_movie_handler.execute(unrate_movie_command) + handler.execute(command) assert error.value.message == UNRATE_MOVIE_ACCESS_DENIED @@ -124,10 +124,10 @@ def test_unrate_movie_should_raise_error_when_rating_does_not_exist( unit_of_work: UnitOfWork, identity_provider_with_correct_permissions: IdentityProvider, ): - unrate_movie_command = UnrateMovieCommand( + command = UnrateMovieCommand( rating_id=RatingId(uuid7()), ) - unrate_movie_handler = UnrateMovieHandler( + handler = UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), permissions_gateway=permissions_gateway, @@ -138,7 +138,7 @@ def test_unrate_movie_should_raise_error_when_rating_does_not_exist( ) with pytest.raises(ApplicationError) as error: - unrate_movie_handler.execute(unrate_movie_command) + handler.execute(command) assert error.value.message == RATING_DOES_NOT_EXIST @@ -181,10 +181,10 @@ def test_unrate_movie_should_raise_error_when_user_is_not_rating_owner( return_value=UserId(uuid7()), ) - unrate_movie_command = UnrateMovieCommand( + command = UnrateMovieCommand( rating_id=rating.id, ) - unrate_movie_handler = UnrateMovieHandler( + handler = UnrateMovieHandler( access_concern=AccessConcern(), unrate_movie=UnrateMovie(), permissions_gateway=permissions_gateway, @@ -195,6 +195,6 @@ def test_unrate_movie_should_raise_error_when_user_is_not_rating_owner( ) with pytest.raises(ApplicationError) as error: - unrate_movie_handler.execute(unrate_movie_command) + handler.execute(command) assert error.value.message == USER_IS_NOT_OWNER diff --git a/tests/unit/application/query_handlers/test_detailed_movie.py b/tests/unit/application/query_handlers/test_detailed_movie.py index c86e5fd..43af222 100644 --- a/tests/unit/application/query_handlers/test_detailed_movie.py +++ b/tests/unit/application/query_handlers/test_detailed_movie.py @@ -80,10 +80,10 @@ def test_get_detailed_movie( return_value=user.id, ) - get_detailed_movie_query = GetDetailedMovieQuery( + query = GetDetailedMovieQuery( movie_id=movie.id, ) - get_detailed_movie_handler = GetDetailedMovieHandler( + handler = GetDetailedMovieHandler( detailed_movie_reader=detailed_movie_reader, identity_provider=identity_provider, ) @@ -107,7 +107,7 @@ def test_get_detailed_movie( created_at=review.created_at, ), ) - result = get_detailed_movie_handler.execute(get_detailed_movie_query) + result = handler.execute(query) assert expected_result == result @@ -120,15 +120,15 @@ def test_get_detailed_movie_should_raise_error_when_movie_does_not_exist( return_value=None, ) - get_detailed_movie_query = GetDetailedMovieQuery( + query = GetDetailedMovieQuery( movie_id=MovieId(uuid7()), ) - get_detailed_movie_handler = GetDetailedMovieHandler( + handler = GetDetailedMovieHandler( detailed_movie_reader=detailed_movie_reader, identity_provider=identity_provider, ) with pytest.raises(ApplicationError) as error: - get_detailed_movie_handler.execute(get_detailed_movie_query) + handler.execute(query) assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_get_detailed_reviews.py b/tests/unit/application/query_handlers/test_get_detailed_reviews.py index b879fc8..f4e4511 100644 --- a/tests/unit/application/query_handlers/test_get_detailed_reviews.py +++ b/tests/unit/application/query_handlers/test_get_detailed_reviews.py @@ -73,12 +73,12 @@ def test_get_detailed_reviews( unit_of_work.commit() - get_detailed_reviews_query = GetDetailedReviewsQuery( + query = GetDetailedReviewsQuery( movie_id=movie.id, limit=10, offset=0, ) - get_detailed_reviews_handler = GetDetailedReviewsHandler( + handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, detailed_review_reader=detailed_review_reader, ) @@ -100,7 +100,7 @@ def test_get_detailed_reviews( ), ), ] - result = get_detailed_reviews_handler.execute(get_detailed_reviews_query) + result = handler.execute(query) assert expected_result == result @@ -109,17 +109,17 @@ def test_get_detailed_reviews_should_raise_error_when_movie_does_not_exist( movie_gateway: MovieGateway, detailed_review_reader: DetailedReviewViewModelReader, ): - get_detailed_reviews_query = GetDetailedReviewsQuery( + query = GetDetailedReviewsQuery( movie_id=MovieId(uuid7()), limit=10, offset=0, ) - get_detailed_reviews_handler = GetDetailedReviewsHandler( + handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, detailed_review_reader=detailed_review_reader, ) with pytest.raises(ApplicationError) as error: - get_detailed_reviews_handler.execute(get_detailed_reviews_query) + handler.execute(query) assert error.value.message == MOVIE_DOES_NOT_EXIST diff --git a/tests/unit/application/query_handlers/test_login.py b/tests/unit/application/query_handlers/test_login.py index 91cc151..9e6a943 100644 --- a/tests/unit/application/query_handlers/test_login.py +++ b/tests/unit/application/query_handlers/test_login.py @@ -101,11 +101,11 @@ def test_login_should_raise_error_when_password_is_incorrect( unit_of_work.commit() - login_query = LoginQuery( + query = LoginQuery( name=user.name, password="invalid_password", ) - login_handler = LoginHandler( + handler = LoginHandler( access_concern=AccessConcern(), user_gateway=user_gateway, permissions_gateway=permissions_gateway, @@ -113,7 +113,7 @@ def test_login_should_raise_error_when_password_is_incorrect( ) with pytest.raises(ApplicationError) as error: - login_handler.execute(login_query) + handler.execute(query) assert error.value.message == INCORRECT_PASSWORD @@ -145,11 +145,11 @@ def test_login_should_raise_error_when_access_is_denied( unit_of_work.commit() - login_query = LoginQuery( + query = LoginQuery( name=user.name, password=user_password, ) - login_handler = LoginHandler( + handler = LoginHandler( access_concern=AccessConcern(), user_gateway=user_gateway, permissions_gateway=permissions_gateway, @@ -157,6 +157,6 @@ def test_login_should_raise_error_when_access_is_denied( ) with pytest.raises(ApplicationError) as error: - login_handler.execute(login_query) + handler.execute(query) assert error.value.message == LOGIN_ACCESS_DENIED diff --git a/tests/unit/application/query_handlers/test_non_detailed_movies.py b/tests/unit/application/query_handlers/test_non_detailed_movies.py index 633e9d7..d92fea9 100644 --- a/tests/unit/application/query_handlers/test_non_detailed_movies.py +++ b/tests/unit/application/query_handlers/test_non_detailed_movies.py @@ -64,11 +64,11 @@ def test_get_non_detailed_movies( return_value=user.id, ) - get_non_detailed_movies_query = GetNonDetailedMoviesQuery( + query = GetNonDetailedMoviesQuery( limit=10, offset=0, ) - get_non_detailed_movies_handler = GetNonDetailedMoviesHandler( + handler = GetNonDetailedMoviesHandler( non_detailed_movie_reader=non_detailed_movie_reader, identity_provider=identity_provider, ) @@ -85,8 +85,6 @@ def test_get_non_detailed_movies( ), ), ] - result = get_non_detailed_movies_handler.execute( - get_non_detailed_movies_query, - ) + result = handler.execute(query) assert expected_result == result From ad0f7c1bbf456515f988984d3d6d05a22a68e53a Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Mon, 26 Feb 2024 18:53:26 +0400 Subject: [PATCH 12/39] Add `GetMyDetailedRatingsQuery`, refactor view model mappers --- .../common/readers/detailed_movie.py | 2 +- .../common/readers/detailed_review.py | 4 +- .../common/readers/my_detailed_ratings.py | 16 +++ .../common/readers/non_detailed_movie.py | 4 +- .../common/readers/rating_for_export.py | 14 +++ .../common/view_models/detailed_movie.py | 14 ++- .../common/view_models/detailed_review.py | 8 +- .../common/view_models/my_detailed_ratings.py | 30 +++++ .../common/view_models/non_detailed_movie.py | 10 +- .../common/view_models/rating_for_export.py | 23 ++++ .../queries/my_detailed_ratings.py | 7 ++ .../query_handlers/detailed_movie.py | 6 +- .../query_handlers/detailed_reviews.py | 10 +- .../query_handlers/my_detailed_ratings.py | 35 ++++++ .../query_handlers/non_detailed_movies.py | 10 +- .../mappers/view_models/detailed_movie.py | 104 +++++++---------- .../mappers/view_models/detailed_review.py | 85 +++++--------- .../view_models/my_detailed_ratings.py | 106 ++++++++++++++++++ .../mappers/view_models/non_detailed_movie.py | 73 +++++------- .../mappers/view_models/rating_for_export.py | 61 ++++++++++ src/amdb/main/providers.py | 52 +++++++-- .../web_api/ratings/get_my_detailed.py | 57 ++++++++++ .../presentation/web_api/ratings/router.py | 6 + tests/unit/application/conftest.py | 24 ++-- .../query_handlers/test_detailed_movie.py | 21 ++-- ...ed_reviews.py => test_detailed_reviews.py} | 18 +-- .../test_my_detailed_ratings.py | 99 ++++++++++++++++ .../test_non_detailed_movies.py | 21 ++-- 28 files changed, 680 insertions(+), 240 deletions(-) create mode 100644 src/amdb/application/common/readers/my_detailed_ratings.py create mode 100644 src/amdb/application/common/readers/rating_for_export.py create mode 100644 src/amdb/application/common/view_models/my_detailed_ratings.py create mode 100644 src/amdb/application/common/view_models/rating_for_export.py create mode 100644 src/amdb/application/queries/my_detailed_ratings.py create mode 100644 src/amdb/application/query_handlers/my_detailed_ratings.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/my_detailed_ratings.py create mode 100644 src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/rating_for_export.py create mode 100644 src/amdb/presentation/web_api/ratings/get_my_detailed.py rename tests/unit/application/query_handlers/{test_get_detailed_reviews.py => test_detailed_reviews.py} (89%) create mode 100644 tests/unit/application/query_handlers/test_my_detailed_ratings.py diff --git a/src/amdb/application/common/readers/detailed_movie.py b/src/amdb/application/common/readers/detailed_movie.py index 9cff8e4..10b3684 100644 --- a/src/amdb/application/common/readers/detailed_movie.py +++ b/src/amdb/application/common/readers/detailed_movie.py @@ -8,7 +8,7 @@ class DetailedMovieViewModelReader(Protocol): - def one( + def get( self, movie_id: MovieId, current_user_id: Optional[UserId], diff --git a/src/amdb/application/common/readers/detailed_review.py b/src/amdb/application/common/readers/detailed_review.py index 2cdf4a4..df752a7 100644 --- a/src/amdb/application/common/readers/detailed_review.py +++ b/src/amdb/application/common/readers/detailed_review.py @@ -6,8 +6,8 @@ ) -class DetailedReviewViewModelReader(Protocol): - def list( +class DetailedReviewViewModelsReader(Protocol): + def get( self, movie_id: MovieId, limit: int, diff --git a/src/amdb/application/common/readers/my_detailed_ratings.py b/src/amdb/application/common/readers/my_detailed_ratings.py new file mode 100644 index 0000000..3291691 --- /dev/null +++ b/src/amdb/application/common/readers/my_detailed_ratings.py @@ -0,0 +1,16 @@ +from typing import Protocol + +from amdb.domain.entities.user import UserId +from amdb.application.common.view_models.my_detailed_ratings import ( + MyDetailedRatingsViewModel, +) + + +class MyDetailedRatingsViewModelReader(Protocol): + def get( + self, + current_user_id: UserId, + limit: int, + offset: int, + ) -> MyDetailedRatingsViewModel: + raise NotImplementedError diff --git a/src/amdb/application/common/readers/non_detailed_movie.py b/src/amdb/application/common/readers/non_detailed_movie.py index f799f3a..f1e7202 100644 --- a/src/amdb/application/common/readers/non_detailed_movie.py +++ b/src/amdb/application/common/readers/non_detailed_movie.py @@ -6,8 +6,8 @@ ) -class NonDetailedMovieViewModelReader(Protocol): - def list( +class NonDetailedMovieViewModelsReader(Protocol): + def get( self, current_user_id: Optional[UserId], limit: int, diff --git a/src/amdb/application/common/readers/rating_for_export.py b/src/amdb/application/common/readers/rating_for_export.py new file mode 100644 index 0000000..6fa6130 --- /dev/null +++ b/src/amdb/application/common/readers/rating_for_export.py @@ -0,0 +1,14 @@ +from typing import Protocol + +from amdb.domain.entities.user import UserId +from amdb.application.common.view_models.rating_for_export import ( + RatingForExportViewModel, +) + + +class RatingForExportViewModelsReader(Protocol): + def get( + self, + current_user_id: UserId, + ) -> list[RatingForExportViewModel]: + raise NotImplementedError diff --git a/src/amdb/application/common/view_models/detailed_movie.py b/src/amdb/application/common/view_models/detailed_movie.py index 61b86e5..ef58bbd 100644 --- a/src/amdb/application/common/view_models/detailed_movie.py +++ b/src/amdb/application/common/view_models/detailed_movie.py @@ -8,13 +8,13 @@ from amdb.domain.entities.review import ReviewId, ReviewType -class UserRating(TypedDict): +class UserRatingViewModel(TypedDict): id: RatingId value: float created_at: datetime -class UserReview(TypedDict): +class UserReviewViewModel(TypedDict): id: ReviewId title: str content: str @@ -22,11 +22,15 @@ class UserReview(TypedDict): created_at: datetime -class DetailedMovieViewModel(TypedDict): +class MovieViewModel(TypedDict): id: MovieId title: str release_date: date rating: float rating_count: int - user_rating: Optional[UserRating] - user_review: Optional[UserReview] + + +class DetailedMovieViewModel(TypedDict): + movie: MovieViewModel + user_rating: Optional[UserRatingViewModel] + user_review: Optional[UserReviewViewModel] diff --git a/src/amdb/application/common/view_models/detailed_review.py b/src/amdb/application/common/view_models/detailed_review.py index da2f376..f97d2e5 100644 --- a/src/amdb/application/common/view_models/detailed_review.py +++ b/src/amdb/application/common/view_models/detailed_review.py @@ -8,13 +8,13 @@ from amdb.domain.entities.review import ReviewId, ReviewType -class UserRating(TypedDict): +class RatingViewModel(TypedDict): id: RatingId value: float created_at: datetime -class UserReview(TypedDict): +class ReviewViewModel(TypedDict): id: ReviewId title: str content: str @@ -24,5 +24,5 @@ class UserReview(TypedDict): class DetailedReviewViewModel(TypedDict): user_id: UserId - user_review: UserReview - user_rating: Optional[UserRating] + review: ReviewViewModel + rating: Optional[RatingViewModel] diff --git a/src/amdb/application/common/view_models/my_detailed_ratings.py b/src/amdb/application/common/view_models/my_detailed_ratings.py new file mode 100644 index 0000000..f2c6aea --- /dev/null +++ b/src/amdb/application/common/view_models/my_detailed_ratings.py @@ -0,0 +1,30 @@ +from datetime import date, datetime + +from typing_extensions import TypedDict + +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId + + +class MovieViewModel(TypedDict): + id: MovieId + title: str + release_date: date + rating: float + rating_count: int + + +class RatingViewModel(TypedDict): + id: RatingId + value: float + created_at: datetime + + +class DetailedRatingViewModel(TypedDict): + movie: MovieViewModel + rating: RatingViewModel + + +class MyDetailedRatingsViewModel(TypedDict): + detailed_ratings: list[DetailedRatingViewModel] + rating_count: int diff --git a/src/amdb/application/common/view_models/non_detailed_movie.py b/src/amdb/application/common/view_models/non_detailed_movie.py index 03b3c09..29db03c 100644 --- a/src/amdb/application/common/view_models/non_detailed_movie.py +++ b/src/amdb/application/common/view_models/non_detailed_movie.py @@ -7,14 +7,18 @@ from amdb.domain.entities.rating import RatingId -class UserRating(TypedDict): +class UserRatingViewModel(TypedDict): id: RatingId value: float -class NonDetailedMovieViewModel(TypedDict): +class MovieViewModel(TypedDict): id: MovieId title: str release_date: date rating: float - user_rating: Optional[UserRating] + + +class NonDetailedMovieViewModel(TypedDict): + movie: MovieViewModel + user_rating: Optional[UserRatingViewModel] diff --git a/src/amdb/application/common/view_models/rating_for_export.py b/src/amdb/application/common/view_models/rating_for_export.py new file mode 100644 index 0000000..a138d35 --- /dev/null +++ b/src/amdb/application/common/view_models/rating_for_export.py @@ -0,0 +1,23 @@ +from datetime import date, datetime + +from typing_extensions import TypedDict + +from amdb.domain.entities.movie import MovieId + + +class MovieViewModel(TypedDict): + id: MovieId + title: str + release_date: date + rating: float + rating_count: int + + +class RatingViewModel(TypedDict): + value: float + created_at: datetime + + +class RatingForExportViewModel(TypedDict): + movie: MovieViewModel + rating: RatingViewModel diff --git a/src/amdb/application/queries/my_detailed_ratings.py b/src/amdb/application/queries/my_detailed_ratings.py new file mode 100644 index 0000000..bf34d83 --- /dev/null +++ b/src/amdb/application/queries/my_detailed_ratings.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True, slots=True) +class GetMyDetailedRatingsQuery: + limit: int + offset: int diff --git a/src/amdb/application/query_handlers/detailed_movie.py b/src/amdb/application/query_handlers/detailed_movie.py index fc139ca..3ee5b05 100644 --- a/src/amdb/application/query_handlers/detailed_movie.py +++ b/src/amdb/application/query_handlers/detailed_movie.py @@ -23,11 +23,11 @@ def __init__( def execute(self, query: GetDetailedMovieQuery) -> DetailedMovieViewModel: current_user_id = self._identity_provider.user_id_or_none() - detailed_movie_view_model = self._detailed_movie_reader.one( + view_model = self._detailed_movie_reader.get( movie_id=query.movie_id, current_user_id=current_user_id, ) - if not detailed_movie_view_model: + if not view_model: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - return detailed_movie_view_model + return view_model diff --git a/src/amdb/application/query_handlers/detailed_reviews.py b/src/amdb/application/query_handlers/detailed_reviews.py index c60fa6a..8b86fc0 100644 --- a/src/amdb/application/query_handlers/detailed_reviews.py +++ b/src/amdb/application/query_handlers/detailed_reviews.py @@ -1,6 +1,6 @@ from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.readers.detailed_review import ( - DetailedReviewViewModelReader, + DetailedReviewViewModelsReader, ) from amdb.application.common.view_models.detailed_review import ( DetailedReviewViewModel, @@ -15,10 +15,10 @@ def __init__( self, *, movie_gateway: MovieGateway, - detailed_review_reader: DetailedReviewViewModelReader, + detailed_reviews_reader: DetailedReviewViewModelsReader, ) -> None: self._movie_gateway = movie_gateway - self._detailed_review_reader = detailed_review_reader + self._detailed_reviews_reader = detailed_reviews_reader def execute( self, @@ -28,10 +28,10 @@ def execute( if not movie: raise ApplicationError(MOVIE_DOES_NOT_EXIST) - review_view_models = self._detailed_review_reader.list( + view_models = self._detailed_reviews_reader.get( movie_id=query.movie_id, limit=query.limit, offset=query.offset, ) - return review_view_models + return view_models diff --git a/src/amdb/application/query_handlers/my_detailed_ratings.py b/src/amdb/application/query_handlers/my_detailed_ratings.py new file mode 100644 index 0000000..5e933e8 --- /dev/null +++ b/src/amdb/application/query_handlers/my_detailed_ratings.py @@ -0,0 +1,35 @@ +from amdb.application.common.view_models.my_detailed_ratings import ( + MyDetailedRatingsViewModel, +) +from amdb.application.common.readers.my_detailed_ratings import ( + MyDetailedRatingsViewModelReader, +) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.queries.my_detailed_ratings import ( + GetMyDetailedRatingsQuery, +) + + +class GetMyDetailedRatingsQueryHandler: + def __init__( + self, + *, + my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, + identity_provider: IdentityProvider, + ) -> None: + self._my_detailed_ratings_reader = my_detailed_ratings_reader + self._identity_provider = identity_provider + + def execute( + self, + query: GetMyDetailedRatingsQuery, + ) -> MyDetailedRatingsViewModel: + current_user_id = self._identity_provider.user_id() + + view_model = self._my_detailed_ratings_reader.get( + current_user_id=current_user_id, + limit=query.limit, + offset=query.offset, + ) + + return view_model diff --git a/src/amdb/application/query_handlers/non_detailed_movies.py b/src/amdb/application/query_handlers/non_detailed_movies.py index d525d1e..68e236d 100644 --- a/src/amdb/application/query_handlers/non_detailed_movies.py +++ b/src/amdb/application/query_handlers/non_detailed_movies.py @@ -1,5 +1,5 @@ from amdb.application.common.readers.non_detailed_movie import ( - NonDetailedMovieViewModelReader, + NonDetailedMovieViewModelsReader, ) from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.non_detailed_movie import ( @@ -14,10 +14,10 @@ class GetNonDetailedMoviesHandler: def __init__( self, *, - non_detailed_movie_reader: NonDetailedMovieViewModelReader, + non_detailed_movies_reader: NonDetailedMovieViewModelsReader, identity_provider: IdentityProvider, ) -> None: - self._non_detailed_movie_reader = non_detailed_movie_reader + self._non_detailed_movies_reader = non_detailed_movies_reader self._identity_provider = identity_provider def execute( @@ -26,10 +26,10 @@ def execute( ) -> list[NonDetailedMovieViewModel]: current_user_id = self._identity_provider.user_id_or_none() - non_detailed_movie_models = self._non_detailed_movie_reader.list( + view_models = self._non_detailed_movies_reader.get( current_user_id=current_user_id, limit=query.limit, offset=query.offset, ) - return non_detailed_movie_models + return view_models diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py index de14469..467837a 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_movie.py @@ -1,47 +1,24 @@ -__all__ = ("DetailedMovieViewModelMapper",) +from typing import Optional -from typing import Optional, TypedDict -from datetime import date, datetime -from uuid import UUID - -from sqlalchemy import Connection, Row, text +from sqlalchemy import Connection, text from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId from amdb.domain.entities.review import ReviewId, ReviewType from amdb.application.common.view_models.detailed_movie import ( - UserRating, - UserReview, + MovieViewModel, + UserRatingViewModel, + UserReviewViewModel, DetailedMovieViewModel, ) -class RowAsDict(TypedDict): - movie_id: UUID - movie_title: str - movie_release_date: date - movie_rating: float - movie_rating_count: int - user_rating_id: Optional[UUID] - user_rating_value: Optional[float] - user_rating_created_at: Optional[datetime] - user_review_id: Optional[UUID] - user_review_title: Optional[str] - user_review_content: Optional[str] - user_review_type: Optional[int] - user_review_created_at: Optional[datetime] - - @classmethod # type: ignore - def from_row(cls, row: Row) -> "RowAsDict": - return RowAsDict(**row._mapping) # noqa: SLF001 - - class DetailedMovieViewModelMapper: def __init__(self, connection: Connection) -> None: self._connection = connection - def one( + def get( self, movie_id: MovieId, current_user_id: Optional[UserId], @@ -79,41 +56,42 @@ def one( } row = self._connection.execute(statement, parameters).fetchone() if row: - row_as_dict = RowAsDict.from_row(row) # type: ignore - return self._to_view_model(row_as_dict) - return None + row_as_dict = row._mapping # noqa: SLF001 - def _to_view_model( - self, - row_as_dict: RowAsDict, - ) -> DetailedMovieViewModel: - if row_as_dict["user_rating_id"]: - user_rating = UserRating( - id=RatingId(row_as_dict["user_rating_id"]), # type: ignore - value=row_as_dict["user_rating_value"], # type: ignore - created_at=row_as_dict["user_rating_created_at"], # type: ignore + movie = MovieViewModel( + id=MovieId(row_as_dict["movie_id"]), + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + rating_count=row_as_dict["movie_rating_count"], ) - else: - user_rating = None - if row_as_dict["user_review_id"]: - user_review = UserReview( - id=ReviewId(row_as_dict["user_review_id"]), # type: ignore - title=row_as_dict["user_review_title"], # type: ignore - content=row_as_dict["user_review_content"], # type: ignore - type=ReviewType(row_as_dict["user_review_type"]), # type: ignore - created_at=row_as_dict["user_review_created_at"], # type: ignore - ) - else: - user_review = None + rating_exists = row_as_dict["user_rating_id"] is not None + if rating_exists: + user_rating = UserRatingViewModel( + id=RatingId(row_as_dict["user_rating_id"]), + value=row_as_dict["user_rating_value"], + created_at=row_as_dict["user_rating_created_at"], + ) + else: + user_rating = None - detailed_movie_view_model = DetailedMovieViewModel( - id=MovieId(row_as_dict["movie_id"]), - title=row_as_dict["movie_title"], - release_date=row_as_dict["movie_release_date"], - rating=row_as_dict["movie_rating"], - rating_count=row_as_dict["movie_rating_count"], - user_rating=user_rating, - user_review=user_review, - ) - return detailed_movie_view_model + review_exists = row_as_dict["user_review_id"] is not None + if review_exists: + user_review = UserReviewViewModel( + id=ReviewId(row_as_dict["user_review_id"]), + title=row_as_dict["user_review_title"], + content=row_as_dict["user_review_content"], + type=ReviewType(row_as_dict["user_review_type"]), + created_at=row_as_dict["user_review_created_at"], + ) + else: + user_review = None + + view_model = DetailedMovieViewModel( + movie=movie, + user_rating=user_rating, + user_review=user_review, + ) + return view_model + return None diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py index 3145c32..9e5f239 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/detailed_review.py @@ -1,43 +1,21 @@ -__all__ = ("DetailedReviewViewModelMapper",) - -from typing import Optional, TypedDict -from datetime import datetime -from uuid import UUID - -from sqlalchemy import Connection, Row, text +from sqlalchemy import Connection, text from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId from amdb.domain.entities.review import ReviewId, ReviewType from amdb.application.common.view_models.detailed_review import ( - UserRating, - UserReview, + RatingViewModel, + ReviewViewModel, DetailedReviewViewModel, ) -class RowAsDict(TypedDict): - user_id: UUID - user_review_id: UUID - user_review_title: str - user_review_content: str - user_review_type: int - user_review_created_at: datetime - user_rating_id: Optional[UUID] - user_rating_value: Optional[float] - user_rating_created_at: Optional[datetime] - - @classmethod # type: ignore - def from_row(cls, row: Row) -> "RowAsDict": - return RowAsDict(row._mapping) # noqa: SLF001 - - -class DetailedReviewViewModelMapper: +class DetailedReviewViewModelsMapper: def __init__(self, connection: Connection) -> None: self._connection = connection - def list( + def get( self, movie_id: MovieId, limit: int, @@ -72,38 +50,33 @@ def list( } rows = self._connection.execute(statement, parameters).fetchall() - review_view_models = [] + view_models = [] for row in rows: - row_as_dict = RowAsDict.from_row(row) # type: ignore - review_view_model = self._to_view_model(row_as_dict) - review_view_models.append(review_view_model) + row_as_dict = row._mapping # noqa: SLF001 - return review_view_models + review = ReviewViewModel( + id=ReviewId(row_as_dict["user_review_id"]), + title=row_as_dict["user_review_title"], + content=row_as_dict["user_review_content"], + type=ReviewType(row_as_dict["user_review_type"]), + created_at=row_as_dict["user_review_created_at"], + ) - def _to_view_model( - self, - row_as_dict: RowAsDict, - ) -> DetailedReviewViewModel: - user_review = UserReview( - id=ReviewId(row_as_dict["user_review_id"]), - title=row_as_dict["user_review_title"], - content=row_as_dict["user_review_content"], - type=ReviewType(row_as_dict["user_review_type"]), - created_at=row_as_dict["user_review_created_at"], - ) + rating_exists = row_as_dict["user_rating_id"] is not None + if rating_exists: + rating = RatingViewModel( + id=RatingId(row_as_dict["user_rating_id"]), + value=row_as_dict["user_rating_value"], + created_at=row_as_dict["user_rating_created_at"], + ) + else: + rating = None - if row_as_dict["user_rating_id"]: - user_rating = UserRating( - id=RatingId(row_as_dict["user_rating_id"]), # type: ignore - value=row_as_dict["user_rating_value"], # type: ignore - created_at=row_as_dict["user_rating_created_at"], # type: ignore + view_model = DetailedReviewViewModel( + user_id=UserId(row_as_dict["user_id"]), + review=review, + rating=rating, ) - else: - user_rating = None + view_models.append(view_model) - detailed_review_view_model = DetailedReviewViewModel( - user_id=UserId(row_as_dict["user_id"]), - user_review=user_review, - user_rating=user_rating, - ) - return detailed_review_view_model + return view_models diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/my_detailed_ratings.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/my_detailed_ratings.py new file mode 100644 index 0000000..7ecb489 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/my_detailed_ratings.py @@ -0,0 +1,106 @@ +from sqlalchemy import Connection, text + +from amdb.domain.entities.user import UserId +from amdb.domain.entities.movie import MovieId +from amdb.domain.entities.rating import RatingId +from amdb.application.common.view_models.my_detailed_ratings import ( + MovieViewModel, + RatingViewModel, + DetailedRatingViewModel, + MyDetailedRatingsViewModel, +) + + +class MyDetailedRatingsViewModelMapper: + def __init__(self, connecion: Connection) -> None: + self._connection = connecion + + def get( + self, + current_user_id: UserId, + limit: int, + offset: int, + ) -> MyDetailedRatingsViewModel: + detailed_ratings = self._detailed_ratings( + current_user_id=current_user_id, + limit=limit, + offset=offset, + ) + rating_count = self._rating_count( + current_user_id=current_user_id, + ) + view_model = MyDetailedRatingsViewModel( + detailed_ratings=detailed_ratings, + rating_count=rating_count, + ) + return view_model + + def _detailed_ratings( + self, + current_user_id: UserId, + limit: int, + offset: int, + ) -> list[DetailedRatingViewModel]: + statement = text( + """ + SELECT + m.id movie_id, + m.title movie_title, + m.release_date movie_release_date, + m.rating movie_rating, + m.rating_count movie_rating_count, + urt.id user_rating_id, + urt.value user_rating_value, + urt.created_at user_rating_created_at + FROM + ratings urt + LEFT JOIN movies m + ON m.id = urt.movie_id + WHERE + urt.user_id = :current_user_id + LIMIT :limit OFFSET :offset + """, + ) + parameters = { + "current_user_id": current_user_id, + "limit": limit, + "offset": offset, + } + rows = self._connection.execute(statement, parameters).fetchall() + + detailed_ratings = [] + for row in rows: + row_as_dict = row._mapping # noqa: SLF001 + detailed_rating = DetailedRatingViewModel( + movie=MovieViewModel( + id=MovieId(row_as_dict["movie_id"]), + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + rating_count=row_as_dict["movie_rating_count"], + ), + rating=RatingViewModel( + id=RatingId(row_as_dict["user_rating_id"]), + value=row_as_dict["user_rating_value"], + created_at=row_as_dict["user_rating_created_at"], + ), + ) + detailed_ratings.append(detailed_rating) + + return detailed_ratings + + def _rating_count(self, current_user_id: UserId) -> int: + statement = text( + """ + SELECT COUNT(urt.id) FROM ratings urt + WHERE urt.user_id = :current_user_id + """, + ) + parameters = { + "current_user_id": current_user_id, + } + rating_count = self._connection.execute( + statement, + parameters, + ).scalar_one() + return rating_count diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py index b9b65ce..147fa94 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/non_detailed_movie.py @@ -1,38 +1,22 @@ -__all__ = ("NonDetailedMovieViewModelMapper",) +from typing import Optional -from typing import Optional, TypedDict -from datetime import date -from uuid import UUID - -from sqlalchemy import Connection, Row, text +from sqlalchemy import Connection, text from amdb.domain.entities.user import UserId from amdb.domain.entities.movie import MovieId from amdb.domain.entities.rating import RatingId from amdb.application.common.view_models.non_detailed_movie import ( - UserRating, + MovieViewModel, + UserRatingViewModel, NonDetailedMovieViewModel, ) -class RowAsDict(TypedDict): - movie_id: UUID - movie_title: str - movie_release_date: date - movie_rating: float - user_rating_id: Optional[UUID] - user_rating_value: Optional[float] - - @classmethod # type: ignore - def from_row(cls, row: Row) -> "RowAsDict": - return RowAsDict(**row._mapping) # noqa: SLF001 - - -class NonDetailedMovieViewModelMapper: +class NonDetailedMovieViewModelsMapper: def __init__(self, connection: Connection) -> None: self._connection = connection - def list( + def get( self, current_user_id: Optional[UserId], limit: int, @@ -61,31 +45,30 @@ def list( } rows = self._connection.execute(statement, parameters).fetchall() - non_detailed_view_models = [] + view_models = [] for row in rows: - row_as_dict = RowAsDict.from_row(row) # type: ignore - non_detailed_view_model = self._to_view_model(row_as_dict) - non_detailed_view_models.append(non_detailed_view_model) + row_as_dict = row._mapping # noqa: SLF001 - return non_detailed_view_models + movie = MovieViewModel( + id=MovieId(row_as_dict["movie_id"]), + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + ) - def _to_view_model( - self, - row_as_dict: RowAsDict, - ) -> NonDetailedMovieViewModel: - if row_as_dict["user_rating_id"]: - user_rating = UserRating( - id=RatingId(row_as_dict["user_rating_id"]), # type: ignore - value=row_as_dict["user_rating_value"], # type: ignore + rating_exists = row_as_dict["user_rating_id"] is not None + if rating_exists: + user_rating = UserRatingViewModel( + id=RatingId(row_as_dict["user_rating_id"]), + value=row_as_dict["user_rating_value"], + ) + else: + user_rating = None + + view_model = NonDetailedMovieViewModel( + movie=movie, + user_rating=user_rating, ) - else: - user_rating = None + view_models.append(view_model) - non_detailed_movie_view_model = NonDetailedMovieViewModel( - id=MovieId(row_as_dict["movie_id"]), - title=row_as_dict["movie_title"], - release_date=row_as_dict["movie_release_date"], - rating=row_as_dict["movie_rating"], - user_rating=user_rating, - ) - return non_detailed_movie_view_model + return view_models diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/rating_for_export.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/rating_for_export.py new file mode 100644 index 0000000..a9e2f24 --- /dev/null +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/view_models/rating_for_export.py @@ -0,0 +1,61 @@ +from sqlalchemy import Connection, text + +from amdb.domain.entities.user import UserId +from amdb.application.common.view_models.rating_for_export import ( + MovieViewModel, + RatingViewModel, + RatingForExportViewModel, +) + + +class RatingForExportViewModelMapper: + def __init__(self, connection: Connection) -> None: + self._connection = connection + + def get( + self, + current_user_id: UserId, + ) -> list[RatingForExportViewModel]: + statement = text( + """ + SELECT + m.id movie_id, + m.title movie_title, + m.release_date movie_release_date, + m.rating movie_rating, + m.rating_count movie_rating_count, + urt.value user_rating_value, + urt.created_at user_rating_created_at + FROM + ratings urt + LEFT JOIN movies m + ON m.id = urt.movie_id + WHERE + urt.user_id = :current_user_id + """, + ) + parameters = {"current_user_id": current_user_id} + rows = self._connection.execute(statement, parameters).fetchall() + + view_models = [] + for row in rows: + row_as_dict = row._mapping # noqa: SLF001 + + movie = MovieViewModel( + id=row_as_dict["movie_id"], + title=row_as_dict["movie_title"], + release_date=row_as_dict["movie_release_date"], + rating=row_as_dict["movie_rating"], + rating_count=row_as_dict["movie_rating_count"], + ) + rating = RatingViewModel( + value=row_as_dict["user_rating_value"], + created_at=row_as_dict["user_rating_created_at"], + ) + view_model = RatingForExportViewModel( + movie=movie, + rating=rating, + ) + view_models.append(view_model) + + return view_models diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index 1f082c2..eaf83e4 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -20,10 +20,13 @@ DetailedMovieViewModelReader, ) from amdb.application.common.readers.non_detailed_movie import ( - NonDetailedMovieViewModelReader, + NonDetailedMovieViewModelsReader, ) from amdb.application.common.readers.detailed_review import ( - DetailedReviewViewModelReader, + DetailedReviewViewModelsReader, +) +from amdb.application.common.readers.my_detailed_ratings import ( + MyDetailedRatingsViewModelReader, ) from amdb.application.common.password_manager import PasswordManager from amdb.application.common.identity_provider import IdentityProvider @@ -43,6 +46,9 @@ from amdb.application.query_handlers.detailed_reviews import ( GetDetailedReviewsHandler, ) +from amdb.application.query_handlers.my_detailed_ratings import ( + GetMyDetailedRatingsQueryHandler, +) from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( @@ -67,10 +73,13 @@ DetailedMovieViewModelMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( - NonDetailedMovieViewModelMapper, + NonDetailedMovieViewModelsMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( - DetailedReviewViewModelMapper, + DetailedReviewViewModelsMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( + MyDetailedRatingsViewModelMapper, ) from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( PermissionsMapperCacheProvider, @@ -131,12 +140,16 @@ class AdaptersProvider(Provider): provides=DetailedMovieViewModelReader, ) non_detailed_movie_reader = provide( - source=NonDetailedMovieViewModelMapper, - provides=NonDetailedMovieViewModelReader, + source=NonDetailedMovieViewModelsMapper, + provides=NonDetailedMovieViewModelsReader, ) detailed_review_reader = provide( - source=DetailedReviewViewModelMapper, - provides=DetailedReviewViewModelReader, + source=DetailedReviewViewModelsMapper, + provides=DetailedReviewViewModelsReader, + ) + my_detailed_ratings_reader = provide( + source=MyDetailedRatingsViewModelMapper, + provides=MyDetailedRatingsViewModelReader, ) unit_of_work = alias(source=Connection, provides=UnitOfWork) @@ -243,11 +256,11 @@ def delete_movie_handler( def get_detailed_reviews_handler( self, movie_gateway: MovieGateway, - detailed_review_reader: DetailedReviewViewModelReader, + detailed_reviews_reader: DetailedReviewViewModelsReader, ) -> GetDetailedReviewsHandler: return GetDetailedReviewsHandler( movie_gateway=movie_gateway, - detailed_review_reader=detailed_review_reader, + detailed_reviews_reader=detailed_reviews_reader, ) @@ -272,13 +285,28 @@ def create_handler( @provide def get_non_detailed_movies_handler( self, - non_detailed_movie_reader: NonDetailedMovieViewModelReader, + non_detailed_movies_reader: NonDetailedMovieViewModelsReader, ) -> CreateHandler[GetNonDetailedMoviesHandler]: def create_handler( identity_provider: IdentityProvider, ) -> GetNonDetailedMoviesHandler: return GetNonDetailedMoviesHandler( - non_detailed_movie_reader=non_detailed_movie_reader, + non_detailed_movies_reader=non_detailed_movies_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def get_my_detailed_ratings_handler( + self, + my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, + ) -> CreateHandler[GetMyDetailedRatingsQueryHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetMyDetailedRatingsQueryHandler: + return GetMyDetailedRatingsQueryHandler( + my_detailed_ratings_reader=my_detailed_ratings_reader, identity_provider=identity_provider, ) diff --git a/src/amdb/presentation/web_api/ratings/get_my_detailed.py b/src/amdb/presentation/web_api/ratings/get_my_detailed.py new file mode 100644 index 0000000..0b8e4a0 --- /dev/null +++ b/src/amdb/presentation/web_api/ratings/get_my_detailed.py @@ -0,0 +1,57 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.application.common.view_models.my_detailed_ratings import ( + MyDetailedRatingsViewModel, +) +from amdb.application.queries.my_detailed_ratings import ( + GetMyDetailedRatingsQuery, +) +from amdb.application.query_handlers.my_detailed_ratings import ( + GetMyDetailedRatingsQueryHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[GetMyDetailedRatingsQueryHandler] + + +@inject +async def get_my_detailed_ratings( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + limit: int = 100, + offset: int = 0, +) -> MyDetailedRatingsViewModel: + """ + Returns current user ratings with movies information and + rating count. + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + + handler = create_handler(identity_provider) + query = GetMyDetailedRatingsQuery( + limit=limit, + offset=offset, + ) + + return handler.execute(query) diff --git a/src/amdb/presentation/web_api/ratings/router.py b/src/amdb/presentation/web_api/ratings/router.py index 410a184..4bc30b3 100644 --- a/src/amdb/presentation/web_api/ratings/router.py +++ b/src/amdb/presentation/web_api/ratings/router.py @@ -1,5 +1,6 @@ from fastapi import APIRouter +from .get_my_detailed import get_my_detailed_ratings from .rate_movie import rate_movie from .unrate_movie import unrate_movie @@ -8,6 +9,11 @@ prefix="/ratings", tags=["ratings"], ) +ratings_router.add_api_route( + path="/me/detailed-ratings", + endpoint=get_my_detailed_ratings, + methods=["GET"], +) ratings_router.add_api_route( path="", endpoint=rate_movie, diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index 5baac92..7601d36 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -25,13 +25,16 @@ PermissionsMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( - NonDetailedMovieViewModelMapper, + NonDetailedMovieViewModelsMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( DetailedMovieViewModelMapper, ) from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( - DetailedReviewViewModelMapper, + DetailedReviewViewModelsMapper, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( + MyDetailedRatingsViewModelMapper, ) from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( PermissionsMapperCacheProvider, @@ -103,17 +106,24 @@ def detailed_movie_reader( @pytest.fixture -def non_detailed_movie_reader( +def non_detailed_movies_reader( sqlalchemy_connection: Connection, -) -> NonDetailedMovieViewModelMapper: - return NonDetailedMovieViewModelMapper(sqlalchemy_connection) +) -> NonDetailedMovieViewModelsMapper: + return NonDetailedMovieViewModelsMapper(sqlalchemy_connection) @pytest.fixture -def detailed_review_reader( +def detailed_reviews_reader( sqlalchemy_connection: Connection, ) -> DetailedMovieViewModelMapper: - return DetailedReviewViewModelMapper(sqlalchemy_connection) + return DetailedReviewViewModelsMapper(sqlalchemy_connection) + + +@pytest.fixture +def my_detailed_ratings_reader( + sqlalchemy_connection: Connection, +) -> MyDetailedRatingsViewModelMapper: + return MyDetailedRatingsViewModelMapper(sqlalchemy_connection) @pytest.fixture diff --git a/tests/unit/application/query_handlers/test_detailed_movie.py b/tests/unit/application/query_handlers/test_detailed_movie.py index 43af222..b2543b6 100644 --- a/tests/unit/application/query_handlers/test_detailed_movie.py +++ b/tests/unit/application/query_handlers/test_detailed_movie.py @@ -18,8 +18,9 @@ ) from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.detailed_movie import ( - UserRating, - UserReview, + MovieViewModel, + UserRatingViewModel, + UserReviewViewModel, DetailedMovieViewModel, ) from amdb.application.queries.detailed_movie import GetDetailedMovieQuery @@ -89,17 +90,19 @@ def test_get_detailed_movie( ) expected_result = DetailedMovieViewModel( - id=movie.id, - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - rating_count=movie.rating_count, - user_rating=UserRating( + movie=MovieViewModel( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + rating_count=movie.rating_count, + ), + user_rating=UserRatingViewModel( id=rating.id, value=rating.value, created_at=rating.created_at, ), - user_review=UserReview( + user_review=UserReviewViewModel( id=review.id, title=review.title, content=review.content, diff --git a/tests/unit/application/query_handlers/test_get_detailed_reviews.py b/tests/unit/application/query_handlers/test_detailed_reviews.py similarity index 89% rename from tests/unit/application/query_handlers/test_get_detailed_reviews.py rename to tests/unit/application/query_handlers/test_detailed_reviews.py index f4e4511..d7521c9 100644 --- a/tests/unit/application/query_handlers/test_get_detailed_reviews.py +++ b/tests/unit/application/query_handlers/test_detailed_reviews.py @@ -13,11 +13,11 @@ from amdb.application.common.gateways.review import ReviewGateway from amdb.application.common.unit_of_work import UnitOfWork from amdb.application.common.readers.detailed_review import ( - DetailedReviewViewModelReader, + DetailedReviewViewModelsReader, ) from amdb.application.common.view_models.detailed_review import ( - UserRating, - UserReview, + RatingViewModel, + ReviewViewModel, DetailedReviewViewModel, ) from amdb.application.queries.detailed_reviews import GetDetailedReviewsQuery @@ -34,7 +34,7 @@ def test_get_detailed_reviews( rating_gateway: RatingGateway, review_gateway: ReviewGateway, unit_of_work: UnitOfWork, - detailed_review_reader: DetailedReviewViewModelReader, + detailed_reviews_reader: DetailedReviewViewModelsReader, ): user = User( id=UserId(uuid7()), @@ -80,20 +80,20 @@ def test_get_detailed_reviews( ) handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, - detailed_review_reader=detailed_review_reader, + detailed_reviews_reader=detailed_reviews_reader, ) expected_result = [ DetailedReviewViewModel( user_id=user.id, - user_review=UserReview( + review=ReviewViewModel( id=review.id, title=review.title, content=review.content, type=review.type, created_at=review.created_at, ), - user_rating=UserRating( + rating=RatingViewModel( id=rating.id, value=rating.value, created_at=rating.created_at, @@ -107,7 +107,7 @@ def test_get_detailed_reviews( def test_get_detailed_reviews_should_raise_error_when_movie_does_not_exist( movie_gateway: MovieGateway, - detailed_review_reader: DetailedReviewViewModelReader, + detailed_reviews_reader: DetailedReviewViewModelsReader, ): query = GetDetailedReviewsQuery( movie_id=MovieId(uuid7()), @@ -116,7 +116,7 @@ def test_get_detailed_reviews_should_raise_error_when_movie_does_not_exist( ) handler = GetDetailedReviewsHandler( movie_gateway=movie_gateway, - detailed_review_reader=detailed_review_reader, + detailed_reviews_reader=detailed_reviews_reader, ) with pytest.raises(ApplicationError) as error: diff --git a/tests/unit/application/query_handlers/test_my_detailed_ratings.py b/tests/unit/application/query_handlers/test_my_detailed_ratings.py new file mode 100644 index 0000000..7c5e903 --- /dev/null +++ b/tests/unit/application/query_handlers/test_my_detailed_ratings.py @@ -0,0 +1,99 @@ +from datetime import date, datetime, timezone +from unittest.mock import Mock + +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import UserId, User +from amdb.domain.entities.movie import MovieId, Movie +from amdb.domain.entities.rating import RatingId, Rating +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.readers.my_detailed_ratings import ( + MyDetailedRatingsViewModelReader, +) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.view_models.my_detailed_ratings import ( + MovieViewModel, + RatingViewModel, + DetailedRatingViewModel, + MyDetailedRatingsViewModel, +) +from amdb.application.queries.my_detailed_ratings import ( + GetMyDetailedRatingsQuery, +) +from amdb.application.query_handlers.my_detailed_ratings import ( + GetMyDetailedRatingsQueryHandler, +) + + +def test_get_my_detailed_ratings( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + release_date=date(1999, 3, 31), + rating=8, + rating_count=1, + ) + movie_gateway.save(movie) + + rating = Rating( + id=RatingId(uuid7()), + movie_id=movie.id, + user_id=user.id, + value=8, + created_at=datetime.now(timezone.utc), + ) + rating_gateway.save(rating) + + unit_of_work.commit() + + identity_provider: IdentityProvider = Mock() + identity_provider.user_id = Mock( + return_value=user.id, + ) + + query = GetMyDetailedRatingsQuery( + limit=10, + offset=0, + ) + handler = GetMyDetailedRatingsQueryHandler( + my_detailed_ratings_reader=my_detailed_ratings_reader, + identity_provider=identity_provider, + ) + + expected_result = MyDetailedRatingsViewModel( + detailed_ratings=[ + DetailedRatingViewModel( + movie=MovieViewModel( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + rating_count=movie.rating_count, + ), + rating=RatingViewModel( + id=rating.id, + value=rating.value, + created_at=rating.created_at, + ), + ), + ], + rating_count=1, + ) + result = handler.execute(query) + + assert expected_result == result diff --git a/tests/unit/application/query_handlers/test_non_detailed_movies.py b/tests/unit/application/query_handlers/test_non_detailed_movies.py index d92fea9..2fe5d0a 100644 --- a/tests/unit/application/query_handlers/test_non_detailed_movies.py +++ b/tests/unit/application/query_handlers/test_non_detailed_movies.py @@ -11,11 +11,12 @@ from amdb.application.common.gateways.rating import RatingGateway from amdb.application.common.unit_of_work import UnitOfWork from amdb.application.common.readers.non_detailed_movie import ( - NonDetailedMovieViewModelReader, + NonDetailedMovieViewModelsReader, ) from amdb.application.common.identity_provider import IdentityProvider from amdb.application.common.view_models.non_detailed_movie import ( - UserRating, + MovieViewModel, + UserRatingViewModel, NonDetailedMovieViewModel, ) from amdb.application.queries.non_detailed_movies import ( @@ -31,7 +32,7 @@ def test_get_non_detailed_movies( movie_gateway: MovieGateway, rating_gateway: RatingGateway, unit_of_work: UnitOfWork, - non_detailed_movie_reader: NonDetailedMovieViewModelReader, + non_detailed_movies_reader: NonDetailedMovieViewModelsReader, ): user = User( id=UserId(uuid7()), @@ -69,17 +70,19 @@ def test_get_non_detailed_movies( offset=0, ) handler = GetNonDetailedMoviesHandler( - non_detailed_movie_reader=non_detailed_movie_reader, + non_detailed_movies_reader=non_detailed_movies_reader, identity_provider=identity_provider, ) expected_result = [ NonDetailedMovieViewModel( - id=movie.id, - title=movie.title, - release_date=movie.release_date, - rating=movie.rating, - user_rating=UserRating( + movie=MovieViewModel( + id=movie.id, + title=movie.title, + release_date=movie.release_date, + rating=movie.rating, + ), + user_rating=UserRatingViewModel( id=rating.id, value=rating.value, ), From c7edbf7449559a1677a18aa667b3c43c7dacc645 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Tue, 27 Feb 2024 19:27:57 +0400 Subject: [PATCH 13/39] Add email attribute to user entity --- .../command_handlers/register_user.py | 10 ++++++++++ src/amdb/application/commands/register_user.py | 2 ++ .../application/common/constants/exceptions.py | 1 + src/amdb/application/common/gateways/user.py | 3 +++ .../query_handlers/my_detailed_ratings.py | 2 +- src/amdb/domain/constants/exceptions.py | 1 + src/amdb/domain/entities/user.py | 3 ++- src/amdb/domain/services/create_user.py | 16 ++++++++++++++++ src/amdb/domain/validators/__init__.py | 0 src/amdb/domain/validators/email.py | 14 ++++++++++++++ .../alembic/migrations/versions/65f8840f4494_.py | 6 +++--- .../alembic/migrations/versions/a2f7c2383ba8_.py | 8 +++++++- .../sqlalchemy/mappers/entities/user.py | 9 +++++++++ .../persistence/sqlalchemy/models/user.py | 4 ++++ src/amdb/main/providers.py | 11 ++++++----- 15 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 src/amdb/domain/validators/__init__.py create mode 100644 src/amdb/domain/validators/email.py diff --git a/src/amdb/application/command_handlers/register_user.py b/src/amdb/application/command_handlers/register_user.py index bfc0f6c..29363f4 100644 --- a/src/amdb/application/command_handlers/register_user.py +++ b/src/amdb/application/command_handlers/register_user.py @@ -10,6 +10,7 @@ from amdb.application.common.password_manager import PasswordManager from amdb.application.common.constants.exceptions import ( USER_NAME_ALREADY_EXISTS, + USER_EMAIL_ALREADY_EXISTS, ) from amdb.application.common.exception import ApplicationError from amdb.application.commands.register_user import RegisterUserCommand @@ -36,9 +37,13 @@ def execute(self, command: RegisterUserCommand) -> UserId: if user: raise ApplicationError(USER_NAME_ALREADY_EXISTS) + if command.email: + self._ensure_email_is_not_taken(command.email) + new_user = self._create_user( id=UserId(uuid7()), name=command.name, + email=command.email, ) self._user_gateway.save(new_user) @@ -54,3 +59,8 @@ def execute(self, command: RegisterUserCommand) -> UserId: self._unit_of_work.commit() return new_user.id + + def _ensure_email_is_not_taken(self, email: str) -> None: + user = self._user_gateway.with_email(email) + if user: + raise ApplicationError(USER_EMAIL_ALREADY_EXISTS) diff --git a/src/amdb/application/commands/register_user.py b/src/amdb/application/commands/register_user.py index 54f0fba..bb15bef 100644 --- a/src/amdb/application/commands/register_user.py +++ b/src/amdb/application/commands/register_user.py @@ -1,7 +1,9 @@ from dataclasses import dataclass +from typing import Optional @dataclass(frozen=True, slots=True) class RegisterUserCommand: name: str + email: Optional[str] password: str diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index dbd3147..c788bd8 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -6,6 +6,7 @@ USER_IS_NOT_OWNER = "User is not an owner" USER_NAME_ALREADY_EXISTS = "User name already exists" +USER_EMAIL_ALREADY_EXISTS = "User email already exists" USER_DOES_NOT_EXIST = "User doesn't exist" INCORRECT_PASSWORD = "Password is not correct" diff --git a/src/amdb/application/common/gateways/user.py b/src/amdb/application/common/gateways/user.py index 63aa44c..a0eefc6 100644 --- a/src/amdb/application/common/gateways/user.py +++ b/src/amdb/application/common/gateways/user.py @@ -10,5 +10,8 @@ def with_id(self, user_id: UserId) -> Optional[User]: def with_name(self, user_name: str) -> Optional[User]: raise NotImplementedError + def with_email(self, user_email: str) -> Optional[User]: + raise NotImplementedError + def save(self, user: User) -> None: raise NotImplementedError diff --git a/src/amdb/application/query_handlers/my_detailed_ratings.py b/src/amdb/application/query_handlers/my_detailed_ratings.py index 5e933e8..9fd8e04 100644 --- a/src/amdb/application/query_handlers/my_detailed_ratings.py +++ b/src/amdb/application/query_handlers/my_detailed_ratings.py @@ -10,7 +10,7 @@ ) -class GetMyDetailedRatingsQueryHandler: +class GetMyDetailedRatingsHandler: def __init__( self, *, diff --git a/src/amdb/domain/constants/exceptions.py b/src/amdb/domain/constants/exceptions.py index dff4fd1..64f1a31 100644 --- a/src/amdb/domain/constants/exceptions.py +++ b/src/amdb/domain/constants/exceptions.py @@ -1,3 +1,4 @@ INVALID_RATING_VALUE = ( "Rating value must be from 0 to 10 and be a multiple of 0.5" ) +INVALID_EMAIL = "Email is invalid" diff --git a/src/amdb/domain/entities/user.py b/src/amdb/domain/entities/user.py index 77e3a2b..46eb3b6 100644 --- a/src/amdb/domain/entities/user.py +++ b/src/amdb/domain/entities/user.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import NewType +from typing import NewType, Optional from uuid import UUID @@ -10,3 +10,4 @@ class User: id: UserId name: str + email: Optional[str] diff --git a/src/amdb/domain/services/create_user.py b/src/amdb/domain/services/create_user.py index 6830cbd..86d942d 100644 --- a/src/amdb/domain/services/create_user.py +++ b/src/amdb/domain/services/create_user.py @@ -1,14 +1,30 @@ +from typing import Optional + from amdb.domain.entities.user import UserId, User +from amdb.domain.validators.email import ValidateEmail class CreateUser: + def __init__( + self, + validate_email: ValidateEmail, + ) -> None: + self._validate_email = validate_email + def __call__( self, *, id: UserId, name: str, + email: Optional[str], ) -> User: + if email: + email = self._validate_email(email) + else: + email = None + return User( id=id, name=name, + email=email, ) diff --git a/src/amdb/domain/validators/__init__.py b/src/amdb/domain/validators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/domain/validators/email.py b/src/amdb/domain/validators/email.py new file mode 100644 index 0000000..8380eda --- /dev/null +++ b/src/amdb/domain/validators/email.py @@ -0,0 +1,14 @@ +import re + +from amdb.domain.constants.exceptions import INVALID_EMAIL +from amdb.domain.exception import DomainError + + +class ValidateEmail: + _REGEX = r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,7}\b" + + def __call__(self, email: str) -> str: + match = re.fullmatch(self._REGEX, email) + if not match: + raise DomainError(INVALID_EMAIL) + return email diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py index 11fb0b3..11c9645 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py @@ -1,9 +1,9 @@ """ Add uuid7 function, Add id column to ratings table, -Add primary key constraint on id in ratings table, -Add unique constraint on pair of user_id and movie_id in ratings table, -Add unique constraint on pair of user_id and movie_id in reviews table +Add primary key constraint on id of ratings table, +Add unique constraint on pair of user_id and movie_id of ratings table, +Add unique constraint on pair of user_id and movie_id of reviews table Revision ID: 65f8840f4494 Revises: 85a348467b90 diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py index 061ebe4..90301ee 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py @@ -1,6 +1,7 @@ """ Add permissions table, -Make type column string in review table +Make type column string to reviews table +Add email column to users table Revision ID: a2f7c2383ba8 Revises: 65f8840f4494 @@ -61,6 +62,10 @@ def upgrade() -> None: "type", nullable=False, ) + op.add_column( + "users", + sa.Column("email", sa.String(), nullable=True, unique=True), + ) def downgrade() -> None: @@ -94,3 +99,4 @@ def downgrade() -> None: "type", nullable=False, ) + op.drop_column("users", "email") diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py index db427ac..87fbf7f 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py @@ -24,10 +24,18 @@ def with_name(self, user_name: str) -> Optional[User]: return self._to_entity(row) # type: ignore return None + def with_email(self, user_email: str) -> Optional[User]: + statement = select(UserModel).where(UserModel.email == user_email) + row = self._connection.execute(statement).one_or_none() + if row: + return self._to_entity(row) # type: ignore + return None + def save(self, user: User) -> None: statement = insert(UserModel).values( id=UserId(user.id), name=user.name, + email=user.email, ) self._connection.execute(statement) @@ -38,4 +46,5 @@ def _to_entity( return User( id=UserId(row.id), name=row.name, + email=row.email, ) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py index 52514d5..2b16918 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/user.py @@ -1,3 +1,4 @@ +from typing import Optional from uuid import UUID from sqlalchemy.orm import Mapped, mapped_column @@ -14,3 +15,6 @@ class UserModel(Model): name: Mapped[str] = mapped_column( unique=True, ) + email: Mapped[Optional[str]] = mapped_column( + unique=True, + ) diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index eaf83e4..871e485 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -10,6 +10,7 @@ from amdb.domain.services.rate_movie import RateMovie from amdb.domain.services.unrate_movie import UnrateMovie from amdb.domain.services.review_movie import ReviewMovie +from amdb.domain.validators.email import ValidateEmail from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway @@ -47,7 +48,7 @@ GetDetailedReviewsHandler, ) from amdb.application.query_handlers.my_detailed_ratings import ( - GetMyDetailedRatingsQueryHandler, + GetMyDetailedRatingsHandler, ) from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig from amdb.infrastructure.persistence.redis.config import RedisConfig @@ -204,7 +205,7 @@ def register_user_handler( password_manager: PasswordManager, ) -> RegisterUserHandler: return RegisterUserHandler( - create_user=CreateUser(), + create_user=CreateUser(validate_email=ValidateEmail()), user_gateway=user_gateway, permissions_gateway=permissions_gateway, unit_of_work=unit_of_work, @@ -301,11 +302,11 @@ def create_handler( def get_my_detailed_ratings_handler( self, my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, - ) -> CreateHandler[GetMyDetailedRatingsQueryHandler]: + ) -> CreateHandler[GetMyDetailedRatingsHandler]: def create_handler( identity_provider: IdentityProvider, - ) -> GetMyDetailedRatingsQueryHandler: - return GetMyDetailedRatingsQueryHandler( + ) -> GetMyDetailedRatingsHandler: + return GetMyDetailedRatingsHandler( my_detailed_ratings_reader=my_detailed_ratings_reader, identity_provider=identity_provider, ) From ff46bc07edb4825ec2ef6f8fe0255fe0070b5ffc Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Tue, 27 Feb 2024 20:04:27 +0400 Subject: [PATCH 14/39] Fix tests --- .../unit/application/command_handlers/test_rate_movie.py | 3 +++ .../application/command_handlers/test_register_user.py | 8 ++++++-- .../application/command_handlers/test_review_movie.py | 2 ++ .../application/command_handlers/test_unrate_movie.py | 2 ++ .../application/query_handlers/test_detailed_movie.py | 1 + .../application/query_handlers/test_detailed_reviews.py | 1 + tests/unit/application/query_handlers/test_login.py | 3 +++ .../query_handlers/test_my_detailed_ratings.py | 5 +++-- .../query_handlers/test_non_detailed_movies.py | 1 + 9 files changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/unit/application/command_handlers/test_rate_movie.py b/tests/unit/application/command_handlers/test_rate_movie.py index c7cb76f..1f3593c 100644 --- a/tests/unit/application/command_handlers/test_rate_movie.py +++ b/tests/unit/application/command_handlers/test_rate_movie.py @@ -50,6 +50,7 @@ def test_rate_movie( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -155,6 +156,7 @@ def test_rate_movie_should_raise_error_when_movie_already_rated( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -228,6 +230,7 @@ def test_rate_movie_should_raise_error_when_rating_is_invalid( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/command_handlers/test_register_user.py b/tests/unit/application/command_handlers/test_register_user.py index 281c5b4..b9e8839 100644 --- a/tests/unit/application/command_handlers/test_register_user.py +++ b/tests/unit/application/command_handlers/test_register_user.py @@ -3,6 +3,7 @@ from amdb.domain.entities.user import UserId, User from amdb.domain.services.create_user import CreateUser +from amdb.domain.validators.email import ValidateEmail from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.application.common.unit_of_work import UnitOfWork @@ -23,10 +24,11 @@ def test_register_user( ): command = RegisterUserCommand( name="John Doe", + email="John@doe.com", password="Secret", ) handler = RegisterUserHandler( - create_user=CreateUser(), + create_user=CreateUser(validate_email=ValidateEmail()), user_gateway=user_gateway, permissions_gateway=permissions_gateway, unit_of_work=unit_of_work, @@ -47,16 +49,18 @@ def test_create_user_should_raise_error_when_user_name_already_exists( user = User( id=UserId(uuid7()), name=user_name, + email="John@doe.com", ) user_gateway.save(user) unit_of_work.commit() command = RegisterUserCommand( name=user_name, + email=None, password="Secret", ) handler = RegisterUserHandler( - create_user=CreateUser(), + create_user=CreateUser(validate_email=ValidateEmail()), user_gateway=user_gateway, permissions_gateway=permissions_gateway, unit_of_work=unit_of_work, diff --git a/tests/unit/application/command_handlers/test_review_movie.py b/tests/unit/application/command_handlers/test_review_movie.py index 54b47d7..45681b1 100644 --- a/tests/unit/application/command_handlers/test_review_movie.py +++ b/tests/unit/application/command_handlers/test_review_movie.py @@ -48,6 +48,7 @@ def test_review_movie( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -159,6 +160,7 @@ def test_review_movie_should_raise_error_when_movie_already_reviewed( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/command_handlers/test_unrate_movie.py b/tests/unit/application/command_handlers/test_unrate_movie.py index 34385f9..2bd6491 100644 --- a/tests/unit/application/command_handlers/test_unrate_movie.py +++ b/tests/unit/application/command_handlers/test_unrate_movie.py @@ -48,6 +48,7 @@ def test_unrate_movie( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -154,6 +155,7 @@ def test_unrate_movie_should_raise_error_when_user_is_not_rating_owner( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/query_handlers/test_detailed_movie.py b/tests/unit/application/query_handlers/test_detailed_movie.py index b2543b6..d265eb3 100644 --- a/tests/unit/application/query_handlers/test_detailed_movie.py +++ b/tests/unit/application/query_handlers/test_detailed_movie.py @@ -42,6 +42,7 @@ def test_get_detailed_movie( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/query_handlers/test_detailed_reviews.py b/tests/unit/application/query_handlers/test_detailed_reviews.py index d7521c9..cdcd8ad 100644 --- a/tests/unit/application/query_handlers/test_detailed_reviews.py +++ b/tests/unit/application/query_handlers/test_detailed_reviews.py @@ -39,6 +39,7 @@ def test_get_detailed_reviews( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/query_handlers/test_login.py b/tests/unit/application/query_handlers/test_login.py index 9e6a943..cd2c203 100644 --- a/tests/unit/application/query_handlers/test_login.py +++ b/tests/unit/application/query_handlers/test_login.py @@ -28,6 +28,7 @@ def test_login( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -91,6 +92,7 @@ def test_login_should_raise_error_when_password_is_incorrect( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -129,6 +131,7 @@ def test_login_should_raise_error_when_access_is_denied( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) diff --git a/tests/unit/application/query_handlers/test_my_detailed_ratings.py b/tests/unit/application/query_handlers/test_my_detailed_ratings.py index 7c5e903..dd7836d 100644 --- a/tests/unit/application/query_handlers/test_my_detailed_ratings.py +++ b/tests/unit/application/query_handlers/test_my_detailed_ratings.py @@ -24,7 +24,7 @@ GetMyDetailedRatingsQuery, ) from amdb.application.query_handlers.my_detailed_ratings import ( - GetMyDetailedRatingsQueryHandler, + GetMyDetailedRatingsHandler, ) @@ -38,6 +38,7 @@ def test_get_my_detailed_ratings( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) @@ -70,7 +71,7 @@ def test_get_my_detailed_ratings( limit=10, offset=0, ) - handler = GetMyDetailedRatingsQueryHandler( + handler = GetMyDetailedRatingsHandler( my_detailed_ratings_reader=my_detailed_ratings_reader, identity_provider=identity_provider, ) diff --git a/tests/unit/application/query_handlers/test_non_detailed_movies.py b/tests/unit/application/query_handlers/test_non_detailed_movies.py index 2fe5d0a..150632f 100644 --- a/tests/unit/application/query_handlers/test_non_detailed_movies.py +++ b/tests/unit/application/query_handlers/test_non_detailed_movies.py @@ -37,6 +37,7 @@ def test_get_non_detailed_movies( user = User( id=UserId(uuid7()), name="John Doe", + email="John@doe.com", ) user_gateway.save(user) From 0484238137563cd3afcd1028b1b1cdbe93a807e2 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Tue, 27 Feb 2024 20:08:28 +0400 Subject: [PATCH 15/39] Add new test for `RegisterUserCommand` --- .../command_handlers/test_register_user.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/unit/application/command_handlers/test_register_user.py b/tests/unit/application/command_handlers/test_register_user.py index b9e8839..113656b 100644 --- a/tests/unit/application/command_handlers/test_register_user.py +++ b/tests/unit/application/command_handlers/test_register_user.py @@ -12,6 +12,7 @@ from amdb.application.command_handlers.register_user import RegisterUserHandler from amdb.application.common.constants.exceptions import ( USER_NAME_ALREADY_EXISTS, + USER_EMAIL_ALREADY_EXISTS, ) from amdb.application.common.exception import ApplicationError @@ -52,6 +53,7 @@ def test_create_user_should_raise_error_when_user_name_already_exists( email="John@doe.com", ) user_gateway.save(user) + unit_of_work.commit() command = RegisterUserCommand( @@ -71,3 +73,39 @@ def test_create_user_should_raise_error_when_user_name_already_exists( handler.execute(command) assert error.value.message == USER_NAME_ALREADY_EXISTS + + +def test_create_user_should_raise_error_when_user_email_already_exists( + user_gateway: UserGateway, + permissions_gateway: PermissionsGateway, + unit_of_work: UnitOfWork, + password_manager: PasswordManager, +): + user_email = "John@doe.com" + + user = User( + id=UserId(uuid7()), + name="John Doe", + email=user_email, + ) + user_gateway.save(user) + + unit_of_work.commit() + + command = RegisterUserCommand( + name="Johny Doe", + email=user_email, + password="Secret", + ) + handler = RegisterUserHandler( + create_user=CreateUser(validate_email=ValidateEmail()), + user_gateway=user_gateway, + permissions_gateway=permissions_gateway, + unit_of_work=unit_of_work, + password_manager=password_manager, + ) + + with pytest.raises(ApplicationError) as error: + handler.execute(command) + + assert error.value.message == USER_EMAIL_ALREADY_EXISTS From a02cb8c851c59efb10b670b5bbe9dee503610284 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Tue, 27 Feb 2024 21:03:55 +0400 Subject: [PATCH 16/39] Add `ExportMyRatingsQuery` --- .../application/common/constants/export.py | 5 ++ .../application/common/converters/__init__.py | 0 .../common/converters/rating_for_export.py | 13 ++++ .../application/queries/export_my_ratings.py | 8 ++ .../query_handlers/export_my_ratings.py | 44 +++++++++++ .../infrastructure/converters/__init__.py | 0 .../converters/ratings_for_export.py | 40 ++++++++++ src/amdb/main/providers.py | 27 +++++++ .../presentation/web_api/exports/__init__.py | 0 .../web_api/exports/my_ratings.py | 59 +++++++++++++++ .../presentation/web_api/exports/router.py | 16 ++++ .../web_api/ratings/get_my_detailed.py | 4 +- src/amdb/presentation/web_api/router.py | 3 + tests/unit/application/conftest.py | 10 +++ .../query_handlers/test_export_my_ratings.py | 73 +++++++++++++++++++ 15 files changed, 300 insertions(+), 2 deletions(-) create mode 100644 src/amdb/application/common/constants/export.py create mode 100644 src/amdb/application/common/converters/__init__.py create mode 100644 src/amdb/application/common/converters/rating_for_export.py create mode 100644 src/amdb/application/queries/export_my_ratings.py create mode 100644 src/amdb/application/query_handlers/export_my_ratings.py create mode 100644 src/amdb/infrastructure/converters/__init__.py create mode 100644 src/amdb/infrastructure/converters/ratings_for_export.py create mode 100644 src/amdb/presentation/web_api/exports/__init__.py create mode 100644 src/amdb/presentation/web_api/exports/my_ratings.py create mode 100644 src/amdb/presentation/web_api/exports/router.py create mode 100644 tests/unit/application/query_handlers/test_export_my_ratings.py diff --git a/src/amdb/application/common/constants/export.py b/src/amdb/application/common/constants/export.py new file mode 100644 index 0000000..5f1ae6c --- /dev/null +++ b/src/amdb/application/common/constants/export.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class ExportFormat(Enum): + CSV = "csv" diff --git a/src/amdb/application/common/converters/__init__.py b/src/amdb/application/common/converters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/converters/rating_for_export.py b/src/amdb/application/common/converters/rating_for_export.py new file mode 100644 index 0000000..cdd2083 --- /dev/null +++ b/src/amdb/application/common/converters/rating_for_export.py @@ -0,0 +1,13 @@ +from typing import Protocol + +from amdb.application.common.view_models.rating_for_export import ( + RatingForExportViewModel, +) + + +class RatingsForExportConverter(Protocol): + def to_csv( + self, + view_models: list[RatingForExportViewModel], + ) -> str: + raise NotImplementedError diff --git a/src/amdb/application/queries/export_my_ratings.py b/src/amdb/application/queries/export_my_ratings.py new file mode 100644 index 0000000..9b785aa --- /dev/null +++ b/src/amdb/application/queries/export_my_ratings.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass + +from amdb.application.common.constants.export import ExportFormat + + +@dataclass(frozen=True, slots=True) +class ExportMyRatingsQuery: + format: ExportFormat diff --git a/src/amdb/application/query_handlers/export_my_ratings.py b/src/amdb/application/query_handlers/export_my_ratings.py new file mode 100644 index 0000000..3f002e3 --- /dev/null +++ b/src/amdb/application/query_handlers/export_my_ratings.py @@ -0,0 +1,44 @@ +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.readers.rating_for_export import ( + RatingForExportViewModelsReader, +) +from amdb.application.common.converters.rating_for_export import ( + RatingsForExportConverter, +) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.view_models.rating_for_export import ( + RatingForExportViewModel, +) +from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery + + +class ExportMyRatingsHandler: + def __init__( + self, + *, + ratings_for_export_reader: RatingForExportViewModelsReader, + ratings_for_export_converter: RatingsForExportConverter, + identity_provider: IdentityProvider, + ) -> None: + self._ratings_for_export_reader = ratings_for_export_reader + self._ratings_for_export_converter = ratings_for_export_converter + self._identity_provider = identity_provider + + def execute(self, query: ExportMyRatingsQuery) -> str: + current_user_id = self._identity_provider.user_id() + + view_models = self._ratings_for_export_reader.get( + current_user_id=current_user_id, + ) + return self._convert_view_models_to_format( + view_models=view_models, + format=query.format, + ) + + def _convert_view_models_to_format( + self, + view_models: list[RatingForExportViewModel], + format: ExportFormat, + ) -> str: + if format is ExportFormat.CSV: + return self._ratings_for_export_converter.to_csv(view_models) diff --git a/src/amdb/infrastructure/converters/__init__.py b/src/amdb/infrastructure/converters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/infrastructure/converters/ratings_for_export.py b/src/amdb/infrastructure/converters/ratings_for_export.py new file mode 100644 index 0000000..1d4e6d2 --- /dev/null +++ b/src/amdb/infrastructure/converters/ratings_for_export.py @@ -0,0 +1,40 @@ +import csv +from io import StringIO + +from amdb.application.common.view_models.rating_for_export import ( + RatingForExportViewModel, +) + + +class RealRatingsForExportConverter: + def to_csv( + self, + view_models: list[RatingForExportViewModel], + ) -> str: + with StringIO() as file: + csv_writer = csv.writer(file) + csv_writer.writerow( + [ + "id", + "title", + "release_date", + "rating", + "rating_count", + "your_rating", + "your_rating_created_at", + ], + ) + for view_model in view_models: + csv_writer.writerow( + [ + view_model["movie"]["id"], + view_model["movie"]["title"], + view_model["movie"]["release_date"], + view_model["movie"]["rating"], + view_model["movie"]["rating_count"], + view_model["rating"]["value"], + view_model["rating"]["created_at"], + ], + ) + csv_file = file.getvalue() + return csv_file diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index 871e485..6539466 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -50,6 +50,9 @@ from amdb.application.query_handlers.my_detailed_ratings import ( GetMyDetailedRatingsHandler, ) +from amdb.application.query_handlers.export_my_ratings import ( + ExportMyRatingsHandler, +) from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( @@ -82,6 +85,9 @@ from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( MyDetailedRatingsViewModelMapper, ) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.rating_for_export import ( + RatingForExportViewModelMapper, +) from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( PermissionsMapperCacheProvider, ) @@ -92,6 +98,9 @@ from amdb.infrastructure.password_manager.password_manager import ( HashingPasswordManager, ) +from amdb.infrastructure.converters.ratings_for_export import ( + RealRatingsForExportConverter, +) from amdb.presentation.create_handler import CreateHandler @@ -385,3 +394,21 @@ def create_handler( ) return create_handler + + @provide + def export_my_ratings( + self, + sqlaclhemy_connection: Connection, + ) -> CreateHandler[ExportMyRatingsHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> ExportMyRatingsHandler: + return ExportMyRatingsHandler( + ratings_for_export_reader=RatingForExportViewModelMapper( + connection=sqlaclhemy_connection, + ), + ratings_for_export_converter=RealRatingsForExportConverter(), + identity_provider=identity_provider, + ) + + return create_handler diff --git a/src/amdb/presentation/web_api/exports/__init__.py b/src/amdb/presentation/web_api/exports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/presentation/web_api/exports/my_ratings.py b/src/amdb/presentation/web_api/exports/my_ratings.py new file mode 100644 index 0000000..581bce3 --- /dev/null +++ b/src/amdb/presentation/web_api/exports/my_ratings.py @@ -0,0 +1,59 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from fastapi.responses import StreamingResponse +from dishka.integrations.fastapi import Depends, inject + +from amdb.application.common.constants.export import ExportFormat +from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery +from amdb.application.query_handlers.export_my_ratings import ( + ExportMyRatingsHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[ExportMyRatingsHandler] + + +@inject +async def export_my_ratings( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + format: ExportFormat = ExportFormat.CSV, +) -> bytes: + """ + Creates file of specified format with current user ratings and + returns it.\n\n + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + + handler = create_handler(identity_provider) + query = ExportMyRatingsQuery(format=format) + file = handler.execute(query) + + if query.format is ExportFormat.CSV: + media_type = "text/csv" + + response = StreamingResponse( + content=iter(file), + media_type=media_type, + headers={"Content-Disposition": "attachment; filename=export.csv"}, + ) + return response diff --git a/src/amdb/presentation/web_api/exports/router.py b/src/amdb/presentation/web_api/exports/router.py new file mode 100644 index 0000000..110ac0b --- /dev/null +++ b/src/amdb/presentation/web_api/exports/router.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter +from fastapi.responses import StreamingResponse + +from .my_ratings import export_my_ratings + + +exports_router = APIRouter( + prefix="/exports", + tags=["exports"], +) +exports_router.add_api_route( + path="/my-ratings", + endpoint=export_my_ratings, + methods=["GET"], + response_class=StreamingResponse, +) diff --git a/src/amdb/presentation/web_api/ratings/get_my_detailed.py b/src/amdb/presentation/web_api/ratings/get_my_detailed.py index 0b8e4a0..50bad45 100644 --- a/src/amdb/presentation/web_api/ratings/get_my_detailed.py +++ b/src/amdb/presentation/web_api/ratings/get_my_detailed.py @@ -10,7 +10,7 @@ GetMyDetailedRatingsQuery, ) from amdb.application.query_handlers.my_detailed_ratings import ( - GetMyDetailedRatingsQueryHandler, + GetMyDetailedRatingsHandler, ) from amdb.application.common.gateways.permissions import PermissionsGateway from amdb.infrastructure.auth.session.session import SessionId @@ -22,7 +22,7 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[GetMyDetailedRatingsQueryHandler] +HandlerCreator = CreateHandler[GetMyDetailedRatingsHandler] @inject diff --git a/src/amdb/presentation/web_api/router.py b/src/amdb/presentation/web_api/router.py index a24f55a..661120e 100644 --- a/src/amdb/presentation/web_api/router.py +++ b/src/amdb/presentation/web_api/router.py @@ -4,10 +4,13 @@ from .movies.router import movies_router from .ratings.router import ratings_router from .reviews.router import reviews_router +from .exports.router import exports_router router = APIRouter(prefix="/v1") + router.include_router(auth_router) router.include_router(movies_router) router.include_router(ratings_router) router.include_router(reviews_router) +router.include_router(exports_router) diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index 7601d36..f4d68dc 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -36,6 +36,9 @@ from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( MyDetailedRatingsViewModelMapper, ) +from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.rating_for_export import ( + RatingForExportViewModelMapper, +) from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( PermissionsMapperCacheProvider, ) @@ -126,6 +129,13 @@ def my_detailed_ratings_reader( return MyDetailedRatingsViewModelMapper(sqlalchemy_connection) +@pytest.fixture +def ratings_for_export_reader( + sqlalchemy_connection: Connection, +) -> RatingForExportViewModelMapper: + return RatingForExportViewModelMapper(sqlalchemy_connection) + + @pytest.fixture def password_manager( sqlalchemy_connection: Connection, diff --git a/tests/unit/application/query_handlers/test_export_my_ratings.py b/tests/unit/application/query_handlers/test_export_my_ratings.py new file mode 100644 index 0000000..9ea0310 --- /dev/null +++ b/tests/unit/application/query_handlers/test_export_my_ratings.py @@ -0,0 +1,73 @@ +from datetime import date, datetime, timezone +from unittest.mock import Mock + +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import User, UserId +from amdb.domain.entities.movie import Movie, MovieId +from amdb.domain.entities.rating import Rating, RatingId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.gateways.movie import MovieGateway +from amdb.application.common.gateways.rating import RatingGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.readers.rating_for_export import ( + RatingForExportViewModelsReader, +) +from amdb.infrastructure.converters.ratings_for_export import ( + RealRatingsForExportConverter, +) +from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery +from amdb.application.query_handlers.export_my_ratings import ( + ExportMyRatingsHandler, +) + + +def test_export_my_ratings_in_csv( + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + ratings_for_export_reader: RatingForExportViewModelsReader, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + email="John@doe.com", + ) + user_gateway.save(user) + + movie = Movie( + id=MovieId(uuid7()), + title="Matrix", + release_date=date(1999, 3, 31), + rating=8, + rating_count=1, + ) + movie_gateway.save(movie) + + rating = Rating( + id=RatingId(uuid7()), + movie_id=movie.id, + user_id=user.id, + value=8, + created_at=datetime.now(timezone.utc), + ) + rating_gateway.save(rating) + + unit_of_work.commit() + + identity_provider: IdentityProvider = Mock() + identity_provider.user_id = Mock( + return_value=user.id, + ) + + query = ExportMyRatingsQuery(format=ExportFormat.CSV) + handler = ExportMyRatingsHandler( + ratings_for_export_reader=ratings_for_export_reader, + ratings_for_export_converter=RealRatingsForExportConverter(), + identity_provider=identity_provider, + ) + + handler.execute(query) From 656aa7741122bf3d37c014782d7020c5dd773a3b Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 28 Feb 2024 12:37:36 +0400 Subject: [PATCH 17/39] Add `UpdateMyProfileCommand` --- .../command_handlers/update_my_profile.py | 37 ++++++++++++++++ .../application/commands/update_my_profile.py | 7 +++ src/amdb/application/common/gateways/user.py | 3 ++ src/amdb/domain/services/update_profile.py | 13 ++++++ .../sqlalchemy/mappers/entities/user.py | 6 ++- src/amdb/main/providers.py | 24 +++++++++- .../presentation/web_api/profiles/__init__.py | 0 .../presentation/web_api/profiles/router.py | 11 +++++ .../web_api/profiles/update_my.py | 44 +++++++++++++++++++ src/amdb/presentation/web_api/router.py | 2 + .../test_update_my_profile.py | 44 +++++++++++++++++++ 11 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 src/amdb/application/command_handlers/update_my_profile.py create mode 100644 src/amdb/application/commands/update_my_profile.py create mode 100644 src/amdb/domain/services/update_profile.py create mode 100644 src/amdb/presentation/web_api/profiles/__init__.py create mode 100644 src/amdb/presentation/web_api/profiles/router.py create mode 100644 src/amdb/presentation/web_api/profiles/update_my.py create mode 100644 tests/unit/application/command_handlers/test_update_my_profile.py diff --git a/src/amdb/application/command_handlers/update_my_profile.py b/src/amdb/application/command_handlers/update_my_profile.py new file mode 100644 index 0000000..bb3066f --- /dev/null +++ b/src/amdb/application/command_handlers/update_my_profile.py @@ -0,0 +1,37 @@ +from typing import cast + +from amdb.domain.entities.user import User +from amdb.domain.services.update_profile import UpdateProfile +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.commands.update_my_profile import UpdateMyProfileCommand + + +class UpdateMyProfileHandler: + def __init__( + self, + *, + update_profile: UpdateProfile, + user_gateway: UserGateway, + unit_of_work: UnitOfWork, + identity_provider: IdentityProvider, + ) -> None: + self._update_profile = update_profile + self._user_gateway = user_gateway + self._unit_of_work = unit_of_work + self._identity_provider = identity_provider + + def execute(self, command: UpdateMyProfileCommand) -> None: + current_user_id = self._identity_provider.user_id() + + user = self._user_gateway.with_id(current_user_id) + user = cast(User, user) + + self._update_profile( + user=user, + email=command.email, + ) + self._user_gateway.update(user) + + self._unit_of_work.commit() diff --git a/src/amdb/application/commands/update_my_profile.py b/src/amdb/application/commands/update_my_profile.py new file mode 100644 index 0000000..ac62ee8 --- /dev/null +++ b/src/amdb/application/commands/update_my_profile.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass(frozen=True, slots=True) +class UpdateMyProfileCommand: + email: Optional[str] diff --git a/src/amdb/application/common/gateways/user.py b/src/amdb/application/common/gateways/user.py index a0eefc6..b7a98e8 100644 --- a/src/amdb/application/common/gateways/user.py +++ b/src/amdb/application/common/gateways/user.py @@ -15,3 +15,6 @@ def with_email(self, user_email: str) -> Optional[User]: def save(self, user: User) -> None: raise NotImplementedError + + def update(self, user: User) -> None: + raise NotImplementedError diff --git a/src/amdb/domain/services/update_profile.py b/src/amdb/domain/services/update_profile.py new file mode 100644 index 0000000..e3eae83 --- /dev/null +++ b/src/amdb/domain/services/update_profile.py @@ -0,0 +1,13 @@ +from typing import Optional + +from amdb.domain.entities.user import User + + +class UpdateProfile: + def __call__( + self, + *, + user: User, + email: Optional[str], + ) -> None: + user.email = email diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py index 87fbf7f..8b58f30 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py @@ -1,6 +1,6 @@ from typing import Annotated, Optional -from sqlalchemy import Connection, Row, select, insert +from sqlalchemy import Connection, Row, select, insert, update from amdb.domain.entities.user import UserId, User from amdb.infrastructure.persistence.sqlalchemy.models.user import UserModel @@ -39,6 +39,10 @@ def save(self, user: User) -> None: ) self._connection.execute(statement) + def update(self, user: User) -> None: + statement = update(UserModel).values(email=user.email) + self._connection.execute(statement) + def _to_entity( self, row: Annotated[UserModel, Row[tuple[UserModel]]], diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index 6539466..01c5ac4 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -6,6 +6,7 @@ from amdb.domain.services.access_concern import AccessConcern from amdb.domain.services.create_user import CreateUser +from amdb.domain.services.update_profile import UpdateProfile from amdb.domain.services.create_movie import CreateMovie from amdb.domain.services.rate_movie import RateMovie from amdb.domain.services.unrate_movie import UnrateMovie @@ -32,6 +33,9 @@ from amdb.application.common.password_manager import PasswordManager from amdb.application.common.identity_provider import IdentityProvider from amdb.application.command_handlers.register_user import RegisterUserHandler +from amdb.application.command_handlers.update_my_profile import ( + UpdateMyProfileHandler, +) from amdb.application.command_handlers.create_movie import CreateMovieHandler from amdb.application.command_handlers.delete_movie import DeleteMovieHandler from amdb.application.command_handlers.rate_movie import RateMovieHandler @@ -277,6 +281,24 @@ def get_detailed_reviews_handler( class HandlerCreatorsProvider(Provider): scope = Scope.REQUEST + @provide + def update_my_profile_handler( + self, + user_gateway: UserGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[UpdateMyProfileHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> UpdateMyProfileHandler: + return UpdateMyProfileHandler( + update_profile=UpdateProfile(), + user_gateway=user_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + @provide def get_detailed_movie_handler( self, @@ -396,7 +418,7 @@ def create_handler( return create_handler @provide - def export_my_ratings( + def export_my_ratings_handler( self, sqlaclhemy_connection: Connection, ) -> CreateHandler[ExportMyRatingsHandler]: diff --git a/src/amdb/presentation/web_api/profiles/__init__.py b/src/amdb/presentation/web_api/profiles/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/presentation/web_api/profiles/router.py b/src/amdb/presentation/web_api/profiles/router.py new file mode 100644 index 0000000..a818e4e --- /dev/null +++ b/src/amdb/presentation/web_api/profiles/router.py @@ -0,0 +1,11 @@ +from fastapi import APIRouter + +from .update_my import update_my_profile + + +profiles_router = APIRouter(tags=["profiles"]) +profiles_router.add_api_route( + path="/me/profile", + endpoint=update_my_profile, + methods=["PATCH"], +) diff --git a/src/amdb/presentation/web_api/profiles/update_my.py b/src/amdb/presentation/web_api/profiles/update_my.py new file mode 100644 index 0000000..3dec5bd --- /dev/null +++ b/src/amdb/presentation/web_api/profiles/update_my.py @@ -0,0 +1,44 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject +from amdb.application.commands.update_my_profile import UpdateMyProfileCommand +from amdb.application.command_handlers.update_my_profile import ( + UpdateMyProfileHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[UpdateMyProfileHandler] + + +@inject +async def update_my_profile( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + command: UpdateMyProfileCommand, +) -> None: + """ + Updates current user profile + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + handler = create_handler(identity_provider) + + handler.execute(command) diff --git a/src/amdb/presentation/web_api/router.py b/src/amdb/presentation/web_api/router.py index 661120e..9264645 100644 --- a/src/amdb/presentation/web_api/router.py +++ b/src/amdb/presentation/web_api/router.py @@ -1,6 +1,7 @@ from fastapi import APIRouter from .auth.router import auth_router +from .profiles.router import profiles_router from .movies.router import movies_router from .ratings.router import ratings_router from .reviews.router import reviews_router @@ -10,6 +11,7 @@ router = APIRouter(prefix="/v1") router.include_router(auth_router) +router.include_router(profiles_router) router.include_router(movies_router) router.include_router(ratings_router) router.include_router(reviews_router) diff --git a/tests/unit/application/command_handlers/test_update_my_profile.py b/tests/unit/application/command_handlers/test_update_my_profile.py new file mode 100644 index 0000000..daad48e --- /dev/null +++ b/tests/unit/application/command_handlers/test_update_my_profile.py @@ -0,0 +1,44 @@ +from unittest.mock import Mock + +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import User, UserId +from amdb.domain.services.update_profile import UpdateProfile +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.commands.update_my_profile import UpdateMyProfileCommand +from amdb.application.command_handlers.update_my_profile import ( + UpdateMyProfileHandler, +) + + +def test_update_my_profile( + user_gateway: UserGateway, + unit_of_work: UnitOfWork, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + email="John@doe.com", + ) + user_gateway.save(user) + + unit_of_work.commit() + + identity_provider: IdentityProvider = Mock() + identity_provider.user_id = Mock( + return_value=user.id, + ) + + command = UpdateMyProfileCommand( + email="Johny@doe.com", + ) + handler = UpdateMyProfileHandler( + update_profile=UpdateProfile(), + user_gateway=user_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + handler.execute(command) From 35a831e9f61bde351fc0a5adb5bd8768b1b513ac Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 28 Feb 2024 12:45:47 +0400 Subject: [PATCH 18/39] Refactor web api routes docstrings, update path to get ratings route --- src/amdb/presentation/web_api/auth/login.py | 8 ++++---- src/amdb/presentation/web_api/auth/register.py | 2 +- src/amdb/presentation/web_api/exports/my_ratings.py | 4 ++-- src/amdb/presentation/web_api/movies/get_detailed.py | 2 +- src/amdb/presentation/web_api/movies/get_non_detailed.py | 2 +- src/amdb/presentation/web_api/profiles/update_my.py | 2 +- src/amdb/presentation/web_api/ratings/router.py | 6 +++--- 7 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/amdb/presentation/web_api/auth/login.py b/src/amdb/presentation/web_api/auth/login.py index e3f8fa8..7477686 100644 --- a/src/amdb/presentation/web_api/auth/login.py +++ b/src/amdb/presentation/web_api/auth/login.py @@ -27,10 +27,10 @@ async def login( Logins, returns user id, creates new authentication session and sets cookie with its id. \n\n - #### Returns 400: \n - * When user doesn't exist \n - * When password is incorrect \n - * When access is denied \n + #### Returns 400: + * When user doesn't exist + * When password is incorrect + * When access is denied """ user_id = handler.execute(query) diff --git a/src/amdb/presentation/web_api/auth/register.py b/src/amdb/presentation/web_api/auth/register.py index 5f78e0c..e73517f 100644 --- a/src/amdb/presentation/web_api/auth/register.py +++ b/src/amdb/presentation/web_api/auth/register.py @@ -27,7 +27,7 @@ async def register( Registers user, returns his id, creates new authentication session and sets cookie with its id. \n\n - #### Returns 400: \n + #### Returns 400: * When name is already taken """ user_id = handler.execute(command) diff --git a/src/amdb/presentation/web_api/exports/my_ratings.py b/src/amdb/presentation/web_api/exports/my_ratings.py index 581bce3..54d3f90 100644 --- a/src/amdb/presentation/web_api/exports/my_ratings.py +++ b/src/amdb/presentation/web_api/exports/my_ratings.py @@ -33,10 +33,10 @@ async def export_my_ratings( Cookie(alias=SESSION_ID_COOKIE), ] = None, format: ExportFormat = ExportFormat.CSV, -) -> bytes: +): """ Creates file of specified format with current user ratings and - returns it.\n\n + returns it. """ identity_provider = SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, diff --git a/src/amdb/presentation/web_api/movies/get_detailed.py b/src/amdb/presentation/web_api/movies/get_detailed.py index ec2153a..a6eefc4 100644 --- a/src/amdb/presentation/web_api/movies/get_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_detailed.py @@ -40,7 +40,7 @@ async def get_detailed_movie( Returns detailed movie information, detailed current user rating and review on it. \n\n - #### Returns 400: \n + #### Returns 400: * When movie doesn't exist """ identity_provider = SessionIdentityProvider( diff --git a/src/amdb/presentation/web_api/movies/get_non_detailed.py b/src/amdb/presentation/web_api/movies/get_non_detailed.py index 65596f3..bf4752b 100644 --- a/src/amdb/presentation/web_api/movies/get_non_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_non_detailed.py @@ -40,7 +40,7 @@ async def get_non_detailed_movies( ) -> list[NonDetailedMovieViewModel]: """ Returns list of non detailed movies and non detailed current - user rating. \n\n + user rating. """ identity_provider = SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, diff --git a/src/amdb/presentation/web_api/profiles/update_my.py b/src/amdb/presentation/web_api/profiles/update_my.py index 3dec5bd..f1ec2a8 100644 --- a/src/amdb/presentation/web_api/profiles/update_my.py +++ b/src/amdb/presentation/web_api/profiles/update_my.py @@ -32,7 +32,7 @@ async def update_my_profile( command: UpdateMyProfileCommand, ) -> None: """ - Updates current user profile + Updates current user profile. """ identity_provider = SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, diff --git a/src/amdb/presentation/web_api/ratings/router.py b/src/amdb/presentation/web_api/ratings/router.py index 4bc30b3..32daaa1 100644 --- a/src/amdb/presentation/web_api/ratings/router.py +++ b/src/amdb/presentation/web_api/ratings/router.py @@ -6,7 +6,7 @@ ratings_router = APIRouter( - prefix="/ratings", + prefix="", tags=["ratings"], ) ratings_router.add_api_route( @@ -15,12 +15,12 @@ methods=["GET"], ) ratings_router.add_api_route( - path="", + path="/ratings", endpoint=rate_movie, methods=["POST"], ) ratings_router.add_api_route( - path="/{rating_id}", + path="/ratings/{rating_id}", endpoint=unrate_movie, methods=["DELETE"], ) From bdce374af746718d1daa53b53b0e3f6120fdd9af Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 28 Feb 2024 13:00:03 +0400 Subject: [PATCH 19/39] Update `UpdateProfile` domain service --- src/amdb/domain/services/update_profile.py | 10 ++++++++++ src/amdb/main/providers.py | 2 +- .../command_handlers/test_update_my_profile.py | 3 ++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/amdb/domain/services/update_profile.py b/src/amdb/domain/services/update_profile.py index e3eae83..a862bdb 100644 --- a/src/amdb/domain/services/update_profile.py +++ b/src/amdb/domain/services/update_profile.py @@ -1,13 +1,23 @@ from typing import Optional from amdb.domain.entities.user import User +from amdb.domain.validators.email import ValidateEmail class UpdateProfile: + def __init__( + self, + *, + validate_email: ValidateEmail, + ) -> None: + self._validate_email = validate_email + def __call__( self, *, user: User, email: Optional[str], ) -> None: + if email: + self._validate_email(email) user.email = email diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index 01c5ac4..17f0dc5 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -291,7 +291,7 @@ def create_handler( identity_provider: IdentityProvider, ) -> UpdateMyProfileHandler: return UpdateMyProfileHandler( - update_profile=UpdateProfile(), + update_profile=UpdateProfile(validate_email=ValidateEmail()), user_gateway=user_gateway, unit_of_work=unit_of_work, identity_provider=identity_provider, diff --git a/tests/unit/application/command_handlers/test_update_my_profile.py b/tests/unit/application/command_handlers/test_update_my_profile.py index daad48e..2d57022 100644 --- a/tests/unit/application/command_handlers/test_update_my_profile.py +++ b/tests/unit/application/command_handlers/test_update_my_profile.py @@ -4,6 +4,7 @@ from amdb.domain.entities.user import User, UserId from amdb.domain.services.update_profile import UpdateProfile +from amdb.domain.validators.email import ValidateEmail from amdb.application.common.gateways.user import UserGateway from amdb.application.common.unit_of_work import UnitOfWork from amdb.application.common.identity_provider import IdentityProvider @@ -35,7 +36,7 @@ def test_update_my_profile( email="Johny@doe.com", ) handler = UpdateMyProfileHandler( - update_profile=UpdateProfile(), + update_profile=UpdateProfile(validate_email=ValidateEmail()), user_gateway=user_gateway, unit_of_work=unit_of_work, identity_provider=identity_provider, From 370b56d24fa8b64bd957e5b7061a9886df0f11b1 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Thu, 29 Feb 2024 00:43:18 +0400 Subject: [PATCH 20/39] Update `update_my_profile` route docstring --- src/amdb/presentation/web_api/profiles/update_my.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/amdb/presentation/web_api/profiles/update_my.py b/src/amdb/presentation/web_api/profiles/update_my.py index f1ec2a8..6cddafd 100644 --- a/src/amdb/presentation/web_api/profiles/update_my.py +++ b/src/amdb/presentation/web_api/profiles/update_my.py @@ -32,7 +32,10 @@ async def update_my_profile( command: UpdateMyProfileCommand, ) -> None: """ - Updates current user profile. + Updates current user profile.\n\n + + ####Returns 400: + * When email is invalid """ identity_provider = SessionIdentityProvider( session_id=SessionId(session_id) if session_id else None, From ef48ecb6c1e885492b0b5a9203a93ed03ccf312c Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Thu, 29 Feb 2024 12:41:43 +0400 Subject: [PATCH 21/39] Add `RequestMyRatingsExportQuery` --- .../common/constants/exceptions.py | 2 +- .../application/common/constants/sending.py | 5 + .../application/common/services/__init__.py | 0 .../services/ensure_can_use_sending_method.py | 15 +++ .../application/common/task_queue/__init__.py | 0 .../task_queue/export_and_send_my_ratings.py | 16 +++ .../queries/request_my_ratings_export.py | 10 ++ .../request_my_ratings_export.py | 45 +++++++ src/amdb/main/providers.py | 23 ++++ .../web_api/exports/request_my_ratings.py | 51 ++++++++ .../presentation/web_api/exports/router.py | 10 +- .../test_request_my_ratings_export.py | 111 ++++++++++++++++++ 12 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 src/amdb/application/common/constants/sending.py create mode 100644 src/amdb/application/common/services/__init__.py create mode 100644 src/amdb/application/common/services/ensure_can_use_sending_method.py create mode 100644 src/amdb/application/common/task_queue/__init__.py create mode 100644 src/amdb/application/common/task_queue/export_and_send_my_ratings.py create mode 100644 src/amdb/application/queries/request_my_ratings_export.py create mode 100644 src/amdb/application/query_handlers/request_my_ratings_export.py create mode 100644 src/amdb/presentation/web_api/exports/request_my_ratings.py create mode 100644 tests/unit/application/query_handlers/test_request_my_ratings_export.py diff --git a/src/amdb/application/common/constants/exceptions.py b/src/amdb/application/common/constants/exceptions.py index c788bd8..995215f 100644 --- a/src/amdb/application/common/constants/exceptions.py +++ b/src/amdb/application/common/constants/exceptions.py @@ -4,7 +4,7 @@ REVIEW_MOVIE_ACCESS_DENIED = "Access to movie reviewing is denied" USER_IS_NOT_OWNER = "User is not an owner" - +USER_HAS_NO_EMAIL = "User has no email" USER_NAME_ALREADY_EXISTS = "User name already exists" USER_EMAIL_ALREADY_EXISTS = "User email already exists" USER_DOES_NOT_EXIST = "User doesn't exist" diff --git a/src/amdb/application/common/constants/sending.py b/src/amdb/application/common/constants/sending.py new file mode 100644 index 0000000..c0a7a90 --- /dev/null +++ b/src/amdb/application/common/constants/sending.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class SendingMethod(Enum): + EMAIL = "email" diff --git a/src/amdb/application/common/services/__init__.py b/src/amdb/application/common/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/services/ensure_can_use_sending_method.py b/src/amdb/application/common/services/ensure_can_use_sending_method.py new file mode 100644 index 0000000..f03667a --- /dev/null +++ b/src/amdb/application/common/services/ensure_can_use_sending_method.py @@ -0,0 +1,15 @@ +from amdb.domain.entities.user import User +from amdb.application.common.constants.sending import SendingMethod +from amdb.application.common.constants.exceptions import USER_HAS_NO_EMAIL +from amdb.application.common.exception import ApplicationError + + +class EnsureCanUseSendingMethod: + def __call__( + self, + *, + user: User, + sending_method: SendingMethod, + ) -> None: + if sending_method is SendingMethod.EMAIL and not user.email: + raise ApplicationError(USER_HAS_NO_EMAIL) diff --git a/src/amdb/application/common/task_queue/__init__.py b/src/amdb/application/common/task_queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/task_queue/export_and_send_my_ratings.py b/src/amdb/application/common/task_queue/export_and_send_my_ratings.py new file mode 100644 index 0000000..a5ba5ee --- /dev/null +++ b/src/amdb/application/common/task_queue/export_and_send_my_ratings.py @@ -0,0 +1,16 @@ +from typing import Protocol + +from amdb.domain.entities.user import UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod + + +class EnqueueExportAndSendingMyRatings(Protocol): + def __call__( + self, + *, + user_id: UserId, + export_format: ExportFormat, + sending_method: SendingMethod, + ) -> None: + raise NotImplementedError diff --git a/src/amdb/application/queries/request_my_ratings_export.py b/src/amdb/application/queries/request_my_ratings_export.py new file mode 100644 index 0000000..dfadb40 --- /dev/null +++ b/src/amdb/application/queries/request_my_ratings_export.py @@ -0,0 +1,10 @@ +from dataclasses import dataclass + +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod + + +@dataclass(frozen=True, slots=True) +class RequestMyRatingsExportQuery: + format: ExportFormat + sending_method: SendingMethod diff --git a/src/amdb/application/query_handlers/request_my_ratings_export.py b/src/amdb/application/query_handlers/request_my_ratings_export.py new file mode 100644 index 0000000..2e3eb63 --- /dev/null +++ b/src/amdb/application/query_handlers/request_my_ratings_export.py @@ -0,0 +1,45 @@ +from typing import cast + +from amdb.domain.entities.user import User +from amdb.application.common.services.ensure_can_use_sending_method import ( + EnsureCanUseSendingMethod, +) +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.task_queue.export_and_send_my_ratings import ( + EnqueueExportAndSendingMyRatings, +) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.queries.request_my_ratings_export import ( + RequestMyRatingsExportQuery, +) + + +class RequestMyRatingsExportHandler: + def __init__( + self, + *, + ensure_can_use_sending_method: EnsureCanUseSendingMethod, + user_gateway: UserGateway, + enqueue_export_and_sending: EnqueueExportAndSendingMyRatings, + identity_provider: IdentityProvider, + ) -> None: + self._ensure_can_use_sending_method = ensure_can_use_sending_method + self._user_gateway = user_gateway + self._enqueue_export_and_sending = enqueue_export_and_sending + self._identity_provider = identity_provider + + def execute(self, query: RequestMyRatingsExportQuery) -> None: + current_user_id = self._identity_provider.user_id() + + user = self._user_gateway.with_id(current_user_id) + user = cast(User, user) + + self._ensure_can_use_sending_method( + user=user, + sending_method=query.sending_method, + ) + self._enqueue_export_and_sending( + user_id=current_user_id, + export_format=query.format, + sending_method=query.sending_method, + ) diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py index 17f0dc5..4f0e14a 100644 --- a/src/amdb/main/providers.py +++ b/src/amdb/main/providers.py @@ -12,6 +12,9 @@ from amdb.domain.services.unrate_movie import UnrateMovie from amdb.domain.services.review_movie import ReviewMovie from amdb.domain.validators.email import ValidateEmail +from amdb.application.common.services.ensure_can_use_sending_method import ( + EnsureCanUseSendingMethod, +) from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.movie import MovieGateway from amdb.application.common.gateways.rating import RatingGateway @@ -57,6 +60,9 @@ from amdb.application.query_handlers.export_my_ratings import ( ExportMyRatingsHandler, ) +from amdb.application.query_handlers.request_my_ratings_export import ( + RequestMyRatingsExportHandler, +) from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig from amdb.infrastructure.persistence.redis.config import RedisConfig from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( @@ -434,3 +440,20 @@ def create_handler( ) return create_handler + + @provide + def request_my_ratings_export_handler( + self, + user_gateway: UserGateway, + ) -> CreateHandler[RequestMyRatingsExportHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> RequestMyRatingsExportHandler: + return RequestMyRatingsExportHandler( + ensure_can_use_sending_method=EnsureCanUseSendingMethod(), + user_gateway=user_gateway, + enqueue_export_and_sending=lambda **kwargs: ..., # type: ignore + identity_provider=identity_provider, + ) + + return create_handler diff --git a/src/amdb/presentation/web_api/exports/request_my_ratings.py b/src/amdb/presentation/web_api/exports/request_my_ratings.py new file mode 100644 index 0000000..23014f3 --- /dev/null +++ b/src/amdb/presentation/web_api/exports/request_my_ratings.py @@ -0,0 +1,51 @@ +from typing import Annotated, Optional + +from fastapi import Cookie +from dishka.integrations.fastapi import Depends, inject + +from amdb.application.queries.request_my_ratings_export import ( + RequestMyRatingsExportQuery, +) +from amdb.application.query_handlers.request_my_ratings_export import ( + RequestMyRatingsExportHandler, +) +from amdb.application.common.gateways.permissions import PermissionsGateway +from amdb.infrastructure.auth.session.session import SessionId +from amdb.infrastructure.auth.session.session_gateway import SessionGateway +from amdb.infrastructure.auth.session.identity_provider import ( + SessionIdentityProvider, +) +from amdb.presentation.create_handler import CreateHandler +from amdb.presentation.web_api.constants import SESSION_ID_COOKIE + + +HandlerCreator = CreateHandler[RequestMyRatingsExportHandler] + + +@inject +async def request_my_ratings_export( + *, + create_handler: Annotated[HandlerCreator, Depends()], + session_gateway: Annotated[SessionGateway, Depends()], + permissions_gateway: Annotated[PermissionsGateway, Depends()], + session_id: Annotated[ + Optional[str], + Cookie(alias=SESSION_ID_COOKIE), + ] = None, + query: RequestMyRatingsExportQuery, +) -> None: + """ + Sends file of specified format with current user ratings using + specified sending method.\n\n + + ####Returns 400: + * When email sending method was passed and user has no email + """ + identity_provider = SessionIdentityProvider( + session_id=SessionId(session_id) if session_id else None, + session_gateway=session_gateway, + permissions_gateway=permissions_gateway, + ) + handler = create_handler(identity_provider) + + handler.execute(query) diff --git a/src/amdb/presentation/web_api/exports/router.py b/src/amdb/presentation/web_api/exports/router.py index 110ac0b..e6c3649 100644 --- a/src/amdb/presentation/web_api/exports/router.py +++ b/src/amdb/presentation/web_api/exports/router.py @@ -2,15 +2,21 @@ from fastapi.responses import StreamingResponse from .my_ratings import export_my_ratings +from .request_my_ratings import request_my_ratings_export exports_router = APIRouter( - prefix="/exports", + prefix="", tags=["exports"], ) exports_router.add_api_route( - path="/my-ratings", + path="/exports/my-ratings", endpoint=export_my_ratings, methods=["GET"], response_class=StreamingResponse, ) +exports_router.add_api_route( + path="/export-requests/my-ratings", + endpoint=request_my_ratings_export, + methods=["POST"], +) diff --git a/tests/unit/application/query_handlers/test_request_my_ratings_export.py b/tests/unit/application/query_handlers/test_request_my_ratings_export.py new file mode 100644 index 0000000..87822a2 --- /dev/null +++ b/tests/unit/application/query_handlers/test_request_my_ratings_export.py @@ -0,0 +1,111 @@ +from unittest.mock import Mock +from typing import Optional + +import pytest +from uuid_extensions import uuid7 + +from amdb.domain.entities.user import User, UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod +from amdb.application.common.services.ensure_can_use_sending_method import ( + EnsureCanUseSendingMethod, +) +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.task_queue.export_and_send_my_ratings import ( + EnqueueExportAndSendingMyRatings, +) +from amdb.application.common.identity_provider import IdentityProvider +from amdb.application.common.constants.exceptions import ( + USER_HAS_NO_EMAIL, +) +from amdb.application.common.exception import ApplicationError +from amdb.application.queries.request_my_ratings_export import ( + RequestMyRatingsExportQuery, +) +from amdb.application.query_handlers.request_my_ratings_export import ( + RequestMyRatingsExportHandler, +) + + +def test_request_my_ratings_export( + user_gateway: UserGateway, + unit_of_work: UnitOfWork, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + email="John@doe.com", + ) + user_gateway.save(user) + + unit_of_work.commit() + + identity_provider: IdentityProvider = Mock() + identity_provider.user_id = Mock( + return_value=user.id, + ) + enqueue_export_and_sending: EnqueueExportAndSendingMyRatings = Mock() + + query = RequestMyRatingsExportQuery( + format=ExportFormat.CSV, + sending_method=SendingMethod.EMAIL, + ) + handler = RequestMyRatingsExportHandler( + ensure_can_use_sending_method=EnsureCanUseSendingMethod(), + user_gateway=user_gateway, + enqueue_export_and_sending=enqueue_export_and_sending, + identity_provider=identity_provider, + ) + + handler.execute(query) + + +@pytest.mark.parametrize( + ( + "sending_method", + "email", + ), + ( + ( + SendingMethod.EMAIL, + None, + ), + ), +) +def test_request_my_ratings_export_should_raise_error_when_user_cannot_use_sending_method( + sending_method: SendingMethod, + email: Optional[str], + user_gateway: UserGateway, + unit_of_work: UnitOfWork, +): + user = User( + id=UserId(uuid7()), + name="John Doe", + email=email, + ) + user_gateway.save(user) + + unit_of_work.commit() + + identity_provider: IdentityProvider = Mock() + identity_provider.user_id = Mock( + return_value=user.id, + ) + enqueue_export_and_sending: EnqueueExportAndSendingMyRatings = Mock() + + query = RequestMyRatingsExportQuery( + format=ExportFormat.CSV, + sending_method=sending_method, + ) + handler = RequestMyRatingsExportHandler( + ensure_can_use_sending_method=EnsureCanUseSendingMethod(), + user_gateway=user_gateway, + enqueue_export_and_sending=enqueue_export_and_sending, + identity_provider=identity_provider, + ) + + with pytest.raises(ApplicationError) as error: + handler.execute(query) + + assert error.value.message == USER_HAS_NO_EMAIL From 8cb0b7eb97b5f2ca2a01da76357540c429084bc6 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 15:17:18 +0400 Subject: [PATCH 22/39] Add `ExportAndSendMyRatingsQuery` --- .../common/converters/rating_for_export.py | 3 +- .../application/common/entities/__init__.py | 0 src/amdb/application/common/entities/file.py | 4 ++ .../application/common/senders/__init__.py | 0 src/amdb/application/common/senders/email.py | 14 ++++ .../common/services/convert_to_file.py | 22 ++++++ .../queries/export_and_send_my_ratings.py | 12 ++++ .../export_and_send_my_ratings.py | 72 +++++++++++++++++++ .../query_handlers/export_my_ratings.py | 27 +++---- .../converters/ratings_for_export.py | 5 +- src/amdb/infrastructure/senders/__init__.py | 0 src/amdb/infrastructure/senders/email.py | 14 ++++ .../infrastructure/task_queue/__init__.py | 0 .../task_queue/export_and_send_my_ratings.py | 14 ++++ 14 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 src/amdb/application/common/entities/__init__.py create mode 100644 src/amdb/application/common/entities/file.py create mode 100644 src/amdb/application/common/senders/__init__.py create mode 100644 src/amdb/application/common/senders/email.py create mode 100644 src/amdb/application/common/services/convert_to_file.py create mode 100644 src/amdb/application/queries/export_and_send_my_ratings.py create mode 100644 src/amdb/application/query_handlers/export_and_send_my_ratings.py create mode 100644 src/amdb/infrastructure/senders/__init__.py create mode 100644 src/amdb/infrastructure/senders/email.py create mode 100644 src/amdb/infrastructure/task_queue/__init__.py create mode 100644 src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py diff --git a/src/amdb/application/common/converters/rating_for_export.py b/src/amdb/application/common/converters/rating_for_export.py index cdd2083..fa88497 100644 --- a/src/amdb/application/common/converters/rating_for_export.py +++ b/src/amdb/application/common/converters/rating_for_export.py @@ -1,5 +1,6 @@ from typing import Protocol +from amdb.application.common.entities.file import File from amdb.application.common.view_models.rating_for_export import ( RatingForExportViewModel, ) @@ -9,5 +10,5 @@ class RatingsForExportConverter(Protocol): def to_csv( self, view_models: list[RatingForExportViewModel], - ) -> str: + ) -> File: raise NotImplementedError diff --git a/src/amdb/application/common/entities/__init__.py b/src/amdb/application/common/entities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/entities/file.py b/src/amdb/application/common/entities/file.py new file mode 100644 index 0000000..2f849f5 --- /dev/null +++ b/src/amdb/application/common/entities/file.py @@ -0,0 +1,4 @@ +from typing import NewType + + +File = NewType("File", str) diff --git a/src/amdb/application/common/senders/__init__.py b/src/amdb/application/common/senders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/application/common/senders/email.py b/src/amdb/application/common/senders/email.py new file mode 100644 index 0000000..778aa04 --- /dev/null +++ b/src/amdb/application/common/senders/email.py @@ -0,0 +1,14 @@ +from typing import Protocol + +from amdb.application.common.entities.file import File + + +class SendEmail(Protocol): + def __call__( + self, + *, + email: str, + subject: str, + files: list[File], + ) -> None: + raise NotImplementedError diff --git a/src/amdb/application/common/services/convert_to_file.py b/src/amdb/application/common/services/convert_to_file.py new file mode 100644 index 0000000..b47a68b --- /dev/null +++ b/src/amdb/application/common/services/convert_to_file.py @@ -0,0 +1,22 @@ +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.entities.file import File +from amdb.application.common.converters.rating_for_export import ( + RatingsForExportConverter, +) +from amdb.application.common.view_models.rating_for_export import ( + RatingForExportViewModel, +) + + +class ConvertMyRatingsToFile: + def __init__(self, converter: RatingsForExportConverter) -> None: + self._converter = converter + + def __call__( + self, + *, + view_models: list[RatingForExportViewModel], + format: ExportFormat, + ) -> File: + if format is ExportFormat.CSV: + return self._converter.to_csv(view_models) diff --git a/src/amdb/application/queries/export_and_send_my_ratings.py b/src/amdb/application/queries/export_and_send_my_ratings.py new file mode 100644 index 0000000..884c337 --- /dev/null +++ b/src/amdb/application/queries/export_and_send_my_ratings.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from amdb.domain.entities.user import UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod + + +@dataclass(frozen=True, slots=True) +class ExportAndSendMyRatingsQuery: + user_id: UserId + format: ExportFormat + sending_method: SendingMethod diff --git a/src/amdb/application/query_handlers/export_and_send_my_ratings.py b/src/amdb/application/query_handlers/export_and_send_my_ratings.py new file mode 100644 index 0000000..2966b55 --- /dev/null +++ b/src/amdb/application/query_handlers/export_and_send_my_ratings.py @@ -0,0 +1,72 @@ +from amdb.domain.entities.user import User +from amdb.application.common.constants.sending import SendingMethod +from amdb.application.common.entities.file import File +from amdb.application.common.services.ensure_can_use_sending_method import ( + EnsureCanUseSendingMethod, +) +from amdb.application.common.services.convert_to_file import ( + ConvertMyRatingsToFile, +) +from amdb.application.common.gateways.user import UserGateway +from amdb.application.common.readers.rating_for_export import ( + RatingForExportViewModelsReader, +) +from amdb.application.common.senders.email import SendEmail +from amdb.application.common.constants.exceptions import USER_DOES_NOT_EXIST +from amdb.application.common.exception import ApplicationError +from amdb.application.queries.export_and_send_my_ratings import ( + ExportAndSendMyRatingsQuery, +) + + +class ExportAndSendMyRatingsHandler: + def __init__( + self, + *, + ensure_can_use_sending_method: EnsureCanUseSendingMethod, + convert_my_ratings_to_file: ConvertMyRatingsToFile, + user_gateway: UserGateway, + ratings_for_export_reader: RatingForExportViewModelsReader, + send_email: SendEmail, + ) -> None: + self._ensure_can_use_sending_method = ensure_can_use_sending_method + self._convert_my_ratings_to_file = convert_my_ratings_to_file + self._user_gateway = user_gateway + self._ratings_for_export_reader = ratings_for_export_reader + self._send_email = send_email + + def execute(self, query: ExportAndSendMyRatingsQuery) -> None: + user = self._user_gateway.with_id(query.user_id) + if not user: + raise ApplicationError(USER_DOES_NOT_EXIST) + + self._ensure_can_use_sending_method( + user=user, + sending_method=query.sending_method, + ) + view_models = self._ratings_for_export_reader.get( + current_user_id=query.user_id, + ) + file = self._convert_my_ratings_to_file( + view_models=view_models, + format=query.format, + ) + self._send_file( + user=user, + file=file, + sending_method=query.sending_method, + ) + + def _send_file( + self, + *, + user: User, + file: File, + sending_method: SendingMethod, + ) -> None: + if sending_method is SendingMethod.EMAIL and user.email: + self._send_email( + email=user.email, + subject="Your exported ratings", + files=[file], + ) diff --git a/src/amdb/application/query_handlers/export_my_ratings.py b/src/amdb/application/query_handlers/export_my_ratings.py index 3f002e3..d989808 100644 --- a/src/amdb/application/query_handlers/export_my_ratings.py +++ b/src/amdb/application/query_handlers/export_my_ratings.py @@ -1,14 +1,11 @@ -from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.entities.file import File +from amdb.application.common.services.convert_to_file import ( + ConvertMyRatingsToFile, +) from amdb.application.common.readers.rating_for_export import ( RatingForExportViewModelsReader, ) -from amdb.application.common.converters.rating_for_export import ( - RatingsForExportConverter, -) from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.common.view_models.rating_for_export import ( - RatingForExportViewModel, -) from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery @@ -16,29 +13,23 @@ class ExportMyRatingsHandler: def __init__( self, *, + convert_my_ratings_to_file: ConvertMyRatingsToFile, ratings_for_export_reader: RatingForExportViewModelsReader, - ratings_for_export_converter: RatingsForExportConverter, identity_provider: IdentityProvider, ) -> None: + self._convert_my_ratings_to_file = convert_my_ratings_to_file self._ratings_for_export_reader = ratings_for_export_reader - self._ratings_for_export_converter = ratings_for_export_converter self._identity_provider = identity_provider - def execute(self, query: ExportMyRatingsQuery) -> str: + def execute(self, query: ExportMyRatingsQuery) -> File: current_user_id = self._identity_provider.user_id() view_models = self._ratings_for_export_reader.get( current_user_id=current_user_id, ) - return self._convert_view_models_to_format( + file = self._convert_my_ratings_to_file( view_models=view_models, format=query.format, ) - def _convert_view_models_to_format( - self, - view_models: list[RatingForExportViewModel], - format: ExportFormat, - ) -> str: - if format is ExportFormat.CSV: - return self._ratings_for_export_converter.to_csv(view_models) + return file diff --git a/src/amdb/infrastructure/converters/ratings_for_export.py b/src/amdb/infrastructure/converters/ratings_for_export.py index 1d4e6d2..10d5ae3 100644 --- a/src/amdb/infrastructure/converters/ratings_for_export.py +++ b/src/amdb/infrastructure/converters/ratings_for_export.py @@ -1,6 +1,7 @@ import csv from io import StringIO +from amdb.application.common.entities.file import File from amdb.application.common.view_models.rating_for_export import ( RatingForExportViewModel, ) @@ -10,7 +11,7 @@ class RealRatingsForExportConverter: def to_csv( self, view_models: list[RatingForExportViewModel], - ) -> str: + ) -> File: with StringIO() as file: csv_writer = csv.writer(file) csv_writer.writerow( @@ -37,4 +38,4 @@ def to_csv( ], ) csv_file = file.getvalue() - return csv_file + return File(csv_file) diff --git a/src/amdb/infrastructure/senders/__init__.py b/src/amdb/infrastructure/senders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/infrastructure/senders/email.py b/src/amdb/infrastructure/senders/email.py new file mode 100644 index 0000000..db4d294 --- /dev/null +++ b/src/amdb/infrastructure/senders/email.py @@ -0,0 +1,14 @@ +from typing import Protocol + +from amdb.application.common.entities.file import File + + +class SendFakeEmail(Protocol): + def __call__( + self, + *, + email: str, + subject: str, + files: list[File], + ) -> None: + ... diff --git a/src/amdb/infrastructure/task_queue/__init__.py b/src/amdb/infrastructure/task_queue/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py b/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py new file mode 100644 index 0000000..b6a91ab --- /dev/null +++ b/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py @@ -0,0 +1,14 @@ +from amdb.domain.entities.user import UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod + + +class EnqueueFakeExportAndSendingMyRatings: + def __call__( + self, + *, + user_id: UserId, + export_format: ExportFormat, + sending_method: SendingMethod, + ) -> None: + ... From 1c9625abe2cf8bd7f03e9d4410a7fd1992754efd Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 15:33:37 +0400 Subject: [PATCH 23/39] Update dishka version to 0.6.0, refactor --- pyproject.toml | 2 +- src/amdb/presentation/web_api/auth/login.py | 10 +++++----- src/amdb/presentation/web_api/auth/register.py | 10 +++++----- src/amdb/presentation/web_api/exports/my_ratings.py | 8 ++++---- .../presentation/web_api/exports/request_my_ratings.py | 8 ++++---- src/amdb/presentation/web_api/movies/get_detailed.py | 8 ++++---- .../presentation/web_api/movies/get_non_detailed.py | 8 ++++---- src/amdb/presentation/web_api/profiles/update_my.py | 10 +++++----- .../presentation/web_api/ratings/get_my_detailed.py | 8 ++++---- src/amdb/presentation/web_api/ratings/rate_movie.py | 8 ++++---- src/amdb/presentation/web_api/ratings/unrate_movie.py | 8 ++++---- src/amdb/presentation/web_api/reviews/get_detailed.py | 4 ++-- src/amdb/presentation/web_api/reviews/review_movie.py | 8 ++++---- 13 files changed, 50 insertions(+), 50 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1adc221..0e653d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ maintainers = [ dependencies = [ "uuid7==0.1.*", "toml==0.10.*", - "dishka==0.4.*", + "dishka==0.6.*", "sqlalchemy==2.0.*", "psycopg2-binary==2.9.*", "alembic==1.13.*", diff --git a/src/amdb/presentation/web_api/auth/login.py b/src/amdb/presentation/web_api/auth/login.py index 7477686..c9fd441 100644 --- a/src/amdb/presentation/web_api/auth/login.py +++ b/src/amdb/presentation/web_api/auth/login.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from fastapi import Response -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.user import UserId from amdb.application.queries.login import LoginQuery @@ -16,10 +16,10 @@ @inject async def login( *, - handler: Annotated[LoginHandler, Depends()], - session_processor: Annotated[SessionProcessor, Depends()], - session_mapper: Annotated[SessionMapper, Depends()], - session_config: Annotated[SessionConfig, Depends()], + handler: Annotated[LoginHandler, FromDishka()], + session_processor: Annotated[SessionProcessor, FromDishka()], + session_mapper: Annotated[SessionMapper, FromDishka()], + session_config: Annotated[SessionConfig, FromDishka()], query: LoginQuery, response: Response, ) -> UserId: diff --git a/src/amdb/presentation/web_api/auth/register.py b/src/amdb/presentation/web_api/auth/register.py index e73517f..4595177 100644 --- a/src/amdb/presentation/web_api/auth/register.py +++ b/src/amdb/presentation/web_api/auth/register.py @@ -2,7 +2,7 @@ from datetime import datetime, timezone from fastapi import Response -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.user import UserId from amdb.application.commands.register_user import RegisterUserCommand @@ -16,10 +16,10 @@ @inject async def register( *, - handler: Annotated[RegisterUserHandler, Depends()], - session_processor: Annotated[SessionProcessor, Depends()], - session_mapper: Annotated[SessionMapper, Depends()], - session_config: Annotated[SessionConfig, Depends()], + handler: Annotated[RegisterUserHandler, FromDishka()], + session_processor: Annotated[SessionProcessor, FromDishka()], + session_mapper: Annotated[SessionMapper, FromDishka()], + session_config: Annotated[SessionConfig, FromDishka()], command: RegisterUserCommand, response: Response, ) -> UserId: diff --git a/src/amdb/presentation/web_api/exports/my_ratings.py b/src/amdb/presentation/web_api/exports/my_ratings.py index 54d3f90..22796a9 100644 --- a/src/amdb/presentation/web_api/exports/my_ratings.py +++ b/src/amdb/presentation/web_api/exports/my_ratings.py @@ -2,7 +2,7 @@ from fastapi import Cookie from fastapi.responses import StreamingResponse -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.application.common.constants.export import ExportFormat from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery @@ -25,9 +25,9 @@ @inject async def export_my_ratings( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/exports/request_my_ratings.py b/src/amdb/presentation/web_api/exports/request_my_ratings.py index 23014f3..13b27c6 100644 --- a/src/amdb/presentation/web_api/exports/request_my_ratings.py +++ b/src/amdb/presentation/web_api/exports/request_my_ratings.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.application.queries.request_my_ratings_export import ( RequestMyRatingsExportQuery, @@ -25,9 +25,9 @@ @inject async def request_my_ratings_export( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/movies/get_detailed.py b/src/amdb/presentation/web_api/movies/get_detailed.py index a6eefc4..3b9a53f 100644 --- a/src/amdb/presentation/web_api/movies/get_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_detailed.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.movie import MovieId from amdb.application.common.view_models.detailed_movie import ( @@ -27,9 +27,9 @@ @inject async def get_detailed_movie( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/movies/get_non_detailed.py b/src/amdb/presentation/web_api/movies/get_non_detailed.py index bf4752b..c2ce3d7 100644 --- a/src/amdb/presentation/web_api/movies/get_non_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_non_detailed.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.application.common.view_models.non_detailed_movie import ( NonDetailedMovieViewModel, @@ -28,9 +28,9 @@ @inject async def get_non_detailed_movies( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/profiles/update_my.py b/src/amdb/presentation/web_api/profiles/update_my.py index 6cddafd..9f87b3e 100644 --- a/src/amdb/presentation/web_api/profiles/update_my.py +++ b/src/amdb/presentation/web_api/profiles/update_my.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.application.commands.update_my_profile import UpdateMyProfileCommand from amdb.application.command_handlers.update_my_profile import ( UpdateMyProfileHandler, @@ -22,9 +22,9 @@ @inject async def update_my_profile( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), @@ -34,7 +34,7 @@ async def update_my_profile( """ Updates current user profile.\n\n - ####Returns 400: + #### Returns 400: * When email is invalid """ identity_provider = SessionIdentityProvider( diff --git a/src/amdb/presentation/web_api/ratings/get_my_detailed.py b/src/amdb/presentation/web_api/ratings/get_my_detailed.py index 50bad45..285b22c 100644 --- a/src/amdb/presentation/web_api/ratings/get_my_detailed.py +++ b/src/amdb/presentation/web_api/ratings/get_my_detailed.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.application.common.view_models.my_detailed_ratings import ( MyDetailedRatingsViewModel, @@ -28,9 +28,9 @@ @inject async def get_my_detailed_ratings( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/ratings/rate_movie.py b/src/amdb/presentation/web_api/ratings/rate_movie.py index f824a98..11a0e76 100644 --- a/src/amdb/presentation/web_api/ratings/rate_movie.py +++ b/src/amdb/presentation/web_api/ratings/rate_movie.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.rating import RatingId from amdb.application.commands.rate_movie import RateMovieCommand @@ -22,9 +22,9 @@ @inject async def rate_movie( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/ratings/unrate_movie.py b/src/amdb/presentation/web_api/ratings/unrate_movie.py index 44fbb37..59e2c56 100644 --- a/src/amdb/presentation/web_api/ratings/unrate_movie.py +++ b/src/amdb/presentation/web_api/ratings/unrate_movie.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.rating import RatingId from amdb.application.commands.unrate_movie import UnrateMovieCommand @@ -22,9 +22,9 @@ @inject async def unrate_movie( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), diff --git a/src/amdb/presentation/web_api/reviews/get_detailed.py b/src/amdb/presentation/web_api/reviews/get_detailed.py index 454ba64..c5b64c7 100644 --- a/src/amdb/presentation/web_api/reviews/get_detailed.py +++ b/src/amdb/presentation/web_api/reviews/get_detailed.py @@ -1,6 +1,6 @@ from typing import Annotated -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.movie import MovieId from amdb.application.common.view_models.detailed_review import ( @@ -15,7 +15,7 @@ @inject async def get_detailed_reviews( *, - handler: Annotated[GetDetailedReviewsHandler, Depends()], + handler: Annotated[GetDetailedReviewsHandler, FromDishka()], movie_id: MovieId, limit: int = 100, offset: int = 0, diff --git a/src/amdb/presentation/web_api/reviews/review_movie.py b/src/amdb/presentation/web_api/reviews/review_movie.py index e28f471..5498a21 100644 --- a/src/amdb/presentation/web_api/reviews/review_movie.py +++ b/src/amdb/presentation/web_api/reviews/review_movie.py @@ -1,7 +1,7 @@ from typing import Annotated, Optional from fastapi import Cookie -from dishka.integrations.fastapi import Depends, inject +from dishka.integrations.fastapi import FromDishka, inject from amdb.domain.entities.review import ReviewId from amdb.application.commands.review_movie import ReviewMovieCommand @@ -22,9 +22,9 @@ @inject async def review_movie( *, - create_handler: Annotated[HandlerCreator, Depends()], - session_gateway: Annotated[SessionGateway, Depends()], - permissions_gateway: Annotated[PermissionsGateway, Depends()], + create_handler: Annotated[HandlerCreator, FromDishka()], + session_gateway: Annotated[SessionGateway, FromDishka()], + permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ Optional[str], Cookie(alias=SESSION_ID_COOKIE), From e730fec7983eeb07ffc1e514b44df4d96e427a3e Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 15:47:37 +0400 Subject: [PATCH 24/39] Rename export endpoints --- src/amdb/presentation/web_api/exports/request_my_ratings.py | 2 +- src/amdb/presentation/web_api/exports/router.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/amdb/presentation/web_api/exports/request_my_ratings.py b/src/amdb/presentation/web_api/exports/request_my_ratings.py index 13b27c6..ece5de0 100644 --- a/src/amdb/presentation/web_api/exports/request_my_ratings.py +++ b/src/amdb/presentation/web_api/exports/request_my_ratings.py @@ -38,7 +38,7 @@ async def request_my_ratings_export( Sends file of specified format with current user ratings using specified sending method.\n\n - ####Returns 400: + #### Returns 400: * When email sending method was passed and user has no email """ identity_provider = SessionIdentityProvider( diff --git a/src/amdb/presentation/web_api/exports/router.py b/src/amdb/presentation/web_api/exports/router.py index e6c3649..c7adec5 100644 --- a/src/amdb/presentation/web_api/exports/router.py +++ b/src/amdb/presentation/web_api/exports/router.py @@ -10,13 +10,13 @@ tags=["exports"], ) exports_router.add_api_route( - path="/exports/my-ratings", + path="/my-ratings-export", endpoint=export_my_ratings, methods=["GET"], response_class=StreamingResponse, ) exports_router.add_api_route( - path="/export-requests/my-ratings", + path="/my-ratings-export-requests", endpoint=request_my_ratings_export, methods=["POST"], ) From 8bd0bad7fd0480b4ac53c3448bb2af8f069cb316 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 15:50:31 +0400 Subject: [PATCH 25/39] Refactor --- src/amdb/presentation/create_handler.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/amdb/presentation/create_handler.py b/src/amdb/presentation/create_handler.py index a408fdc..c737f67 100644 --- a/src/amdb/presentation/create_handler.py +++ b/src/amdb/presentation/create_handler.py @@ -1,16 +1,14 @@ -__all__ = ("CreateHandler",) - from typing import TypeVar, Protocol from amdb.application.common.identity_provider import IdentityProvider -H = TypeVar("H", covariant=True) +_H = TypeVar("_H", covariant=True) -class CreateHandler(Protocol[H]): +class CreateHandler(Protocol[_H]): def __call__( self, identity_provider: IdentityProvider, - ) -> H: + ) -> _H: raise NotImplementedError From c6d65fbddd82b8717dc43e614bb7ec9b9e8d815b Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 16:37:25 +0400 Subject: [PATCH 26/39] Fix export my ratings test --- .../application/query_handlers/test_export_my_ratings.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit/application/query_handlers/test_export_my_ratings.py b/tests/unit/application/query_handlers/test_export_my_ratings.py index 9ea0310..9cad525 100644 --- a/tests/unit/application/query_handlers/test_export_my_ratings.py +++ b/tests/unit/application/query_handlers/test_export_my_ratings.py @@ -6,6 +6,9 @@ from amdb.domain.entities.user import User, UserId from amdb.domain.entities.movie import Movie, MovieId from amdb.domain.entities.rating import Rating, RatingId +from amdb.application.common.services.convert_to_file import ( + ConvertMyRatingsToFile, +) from amdb.application.common.constants.export import ExportFormat from amdb.application.common.gateways.user import UserGateway from amdb.application.common.gateways.movie import MovieGateway @@ -65,8 +68,10 @@ def test_export_my_ratings_in_csv( query = ExportMyRatingsQuery(format=ExportFormat.CSV) handler = ExportMyRatingsHandler( + convert_my_ratings_to_file=ConvertMyRatingsToFile( + converter=RealRatingsForExportConverter(), + ), ratings_for_export_reader=ratings_for_export_reader, - ratings_for_export_converter=RealRatingsForExportConverter(), identity_provider=identity_provider, ) From 565b36061e1be668455b137a5e923eb4f0d55013 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Wed, 6 Mar 2024 18:23:08 +0400 Subject: [PATCH 27/39] Refactor sqlalchemy models and mappers --- .../persistence/sqlalchemy/mappers/entities/movie.py | 5 +---- .../persistence/sqlalchemy/mappers/entities/rating.py | 2 +- .../persistence/sqlalchemy/mappers/entities/review.py | 2 +- .../persistence/sqlalchemy/mappers/entities/user.py | 2 +- .../infrastructure/persistence/sqlalchemy/models/rating.py | 7 +------ .../infrastructure/persistence/sqlalchemy/models/review.py | 7 +------ 6 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py index 67ca571..5f43986 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py @@ -40,10 +40,7 @@ def delete(self, movie: Movie) -> None: statement = delete(MovieModel).where(MovieModel.id == movie.id) self._connection.execute(statement) - def _to_entity( - self, - row: Annotated[MovieModel, Row[tuple[MovieModel]]], - ) -> Movie: + def _to_entity(self, row: Annotated[MovieModel, Row]) -> Movie: return Movie( id=MovieId(row.id), title=row.title, diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py index 846e8f5..c6523fa 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/rating.py @@ -57,7 +57,7 @@ def delete_with_movie_id(self, movie_id: MovieId) -> None: def _to_entity( self, - row: Annotated[RatingModel, Row[tuple[RatingModel]]], + row: Annotated[RatingModel, Row], ) -> Rating: return Rating( id=RatingId(row.id), diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py index d08255d..7c9612a 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/review.py @@ -55,7 +55,7 @@ def delete_with_movie_id(self, movie_id: MovieId) -> None: def _to_entity( self, - row: Annotated[ReviewModel, Row[tuple[ReviewModel]]], + row: Annotated[ReviewModel, Row], ) -> Review: return Review( id=ReviewId(row.id), diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py index 8b58f30..b1f44a9 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/user.py @@ -45,7 +45,7 @@ def update(self, user: User) -> None: def _to_entity( self, - row: Annotated[UserModel, Row[tuple[UserModel]]], + row: Annotated[UserModel, Row], ) -> User: return User( id=UserId(row.id), diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py index baaab42..deadce6 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/rating.py @@ -2,11 +2,9 @@ from uuid import UUID from sqlalchemy import ForeignKey, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from .base import Model -from .user import UserModel -from .movie import MovieModel class RatingModel(Model): @@ -24,7 +22,4 @@ class RatingModel(Model): value: Mapped[float] created_at: Mapped[datetime] - movie: Mapped[MovieModel] = relationship() - user: Mapped[UserModel] = relationship() - __table_args__ = (UniqueConstraint("user_id", "movie_id"),) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py index 37d3221..79c78fe 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/models/review.py @@ -2,11 +2,9 @@ from uuid import UUID from sqlalchemy import ForeignKey -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column from .base import Model -from .user import UserModel -from .movie import MovieModel class ReviewModel(Model): @@ -25,6 +23,3 @@ class ReviewModel(Model): content: Mapped[str] type: Mapped[str] created_at: Mapped[datetime] - - user: Mapped[UserModel] = relationship() - movie: Mapped[MovieModel] = relationship() From 152303631ef25ec7e6500a52c3be472b6af2a1a2 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 11:37:43 +0400 Subject: [PATCH 28/39] Refactor dishka providers, refactor imports --- .../application/command_handlers/__init__.py | 17 + .../{converters => converting}/__init__.py | 0 .../rating_for_export.py | 0 .../application/common/gateways/__init__.py | 13 + .../application/common/readers/__init__.py | 13 + .../common/{senders => sending}/__init__.py | 0 .../common/{senders => sending}/email.py | 0 .../application/common/services/__init__.py | 7 + .../common/services/convert_to_file.py | 2 +- ...se_sending_method.py => ensure_can_use.py} | 0 .../application/query_handlers/__init__.py | 19 + .../export_and_send_my_ratings.py | 4 +- .../request_my_ratings_export.py | 2 +- src/amdb/domain/services/__init__.py | 17 + src/amdb/domain/services/update_profile.py | 1 - .../{converters => converting}/__init__.py | 0 .../ratings_for_export.py | 0 .../sqlalchemy/mappers/__init__.py | 25 + .../{senders => sending}/__init__.py | 0 .../{senders => sending}/email.py | 0 src/amdb/main/providers.py | 459 ------------------ src/amdb/main/providers/__init__.py | 41 ++ .../main/providers/application_services.py | 19 + src/amdb/main/providers/command_handlers.py | 184 +++++++ src/amdb/main/providers/configs.py | 24 + src/amdb/main/providers/connections.py | 33 ++ src/amdb/main/providers/converting.py | 17 + src/amdb/main/providers/data_mappers.py | 97 ++++ src/amdb/main/providers/domain_services.py | 23 + src/amdb/main/providers/domain_validators.py | 9 + src/amdb/main/providers/password_manager.py | 22 + src/amdb/main/providers/query_handlers.py | 170 +++++++ src/amdb/main/providers/sending.py | 10 + src/amdb/main/providers/task_queue.py | 17 + src/amdb/main/web_api/app.py | 38 +- tests/unit/application/conftest.py | 30 +- .../query_handlers/test_export_my_ratings.py | 2 +- .../test_request_my_ratings_export.py | 2 +- 38 files changed, 819 insertions(+), 498 deletions(-) rename src/amdb/application/common/{converters => converting}/__init__.py (100%) rename src/amdb/application/common/{converters => converting}/rating_for_export.py (100%) rename src/amdb/application/common/{senders => sending}/__init__.py (100%) rename src/amdb/application/common/{senders => sending}/email.py (100%) rename src/amdb/application/common/services/{ensure_can_use_sending_method.py => ensure_can_use.py} (100%) rename src/amdb/infrastructure/{converters => converting}/__init__.py (100%) rename src/amdb/infrastructure/{converters => converting}/ratings_for_export.py (100%) rename src/amdb/infrastructure/{senders => sending}/__init__.py (100%) rename src/amdb/infrastructure/{senders => sending}/email.py (100%) delete mode 100644 src/amdb/main/providers.py create mode 100644 src/amdb/main/providers/__init__.py create mode 100644 src/amdb/main/providers/application_services.py create mode 100644 src/amdb/main/providers/command_handlers.py create mode 100644 src/amdb/main/providers/configs.py create mode 100644 src/amdb/main/providers/connections.py create mode 100644 src/amdb/main/providers/converting.py create mode 100644 src/amdb/main/providers/data_mappers.py create mode 100644 src/amdb/main/providers/domain_services.py create mode 100644 src/amdb/main/providers/domain_validators.py create mode 100644 src/amdb/main/providers/password_manager.py create mode 100644 src/amdb/main/providers/query_handlers.py create mode 100644 src/amdb/main/providers/sending.py create mode 100644 src/amdb/main/providers/task_queue.py diff --git a/src/amdb/application/command_handlers/__init__.py b/src/amdb/application/command_handlers/__init__.py index e69de29..9315147 100644 --- a/src/amdb/application/command_handlers/__init__.py +++ b/src/amdb/application/command_handlers/__init__.py @@ -0,0 +1,17 @@ +__all__ = ( + "RegisterUserHandler", + "UpdateMyProfileHandler", + "CreateMovieHandler", + "DeleteMovieHandler", + "RateMovieHandler", + "UnrateMovieHandler", + "ReviewMovieHandler", +) + +from .register_user import RegisterUserHandler +from .update_my_profile import UpdateMyProfileHandler +from .create_movie import CreateMovieHandler +from .delete_movie import DeleteMovieHandler +from .rate_movie import RateMovieHandler +from .unrate_movie import UnrateMovieHandler +from .review_movie import ReviewMovieHandler diff --git a/src/amdb/application/common/converters/__init__.py b/src/amdb/application/common/converting/__init__.py similarity index 100% rename from src/amdb/application/common/converters/__init__.py rename to src/amdb/application/common/converting/__init__.py diff --git a/src/amdb/application/common/converters/rating_for_export.py b/src/amdb/application/common/converting/rating_for_export.py similarity index 100% rename from src/amdb/application/common/converters/rating_for_export.py rename to src/amdb/application/common/converting/rating_for_export.py diff --git a/src/amdb/application/common/gateways/__init__.py b/src/amdb/application/common/gateways/__init__.py index e69de29..79c5c57 100644 --- a/src/amdb/application/common/gateways/__init__.py +++ b/src/amdb/application/common/gateways/__init__.py @@ -0,0 +1,13 @@ +__all__ = ( + "UserGateway", + "MovieGateway", + "RatingGateway", + "ReviewGateway", + "PermissionsGateway", +) + +from .user import UserGateway +from .movie import MovieGateway +from .rating import RatingGateway +from .review import ReviewGateway +from .permissions import PermissionsGateway diff --git a/src/amdb/application/common/readers/__init__.py b/src/amdb/application/common/readers/__init__.py index e69de29..771f12d 100644 --- a/src/amdb/application/common/readers/__init__.py +++ b/src/amdb/application/common/readers/__init__.py @@ -0,0 +1,13 @@ +__all__ = ( + "DetailedMovieViewModelReader", + "DetailedReviewViewModelsReader", + "RatingForExportViewModelsReader", + "NonDetailedMovieViewModelsReader", + "MyDetailedRatingsViewModelReader", +) + +from .detailed_movie import DetailedMovieViewModelReader +from .detailed_review import DetailedReviewViewModelsReader +from .rating_for_export import RatingForExportViewModelsReader +from .non_detailed_movie import NonDetailedMovieViewModelsReader +from .my_detailed_ratings import MyDetailedRatingsViewModelReader diff --git a/src/amdb/application/common/senders/__init__.py b/src/amdb/application/common/sending/__init__.py similarity index 100% rename from src/amdb/application/common/senders/__init__.py rename to src/amdb/application/common/sending/__init__.py diff --git a/src/amdb/application/common/senders/email.py b/src/amdb/application/common/sending/email.py similarity index 100% rename from src/amdb/application/common/senders/email.py rename to src/amdb/application/common/sending/email.py diff --git a/src/amdb/application/common/services/__init__.py b/src/amdb/application/common/services/__init__.py index e69de29..e2809d7 100644 --- a/src/amdb/application/common/services/__init__.py +++ b/src/amdb/application/common/services/__init__.py @@ -0,0 +1,7 @@ +__all__ = ( + "ConvertMyRatingsToFile", + "EnsureCanUseSendingMethod", +) + +from .convert_to_file import ConvertMyRatingsToFile +from .ensure_can_use import EnsureCanUseSendingMethod diff --git a/src/amdb/application/common/services/convert_to_file.py b/src/amdb/application/common/services/convert_to_file.py index b47a68b..77f13ba 100644 --- a/src/amdb/application/common/services/convert_to_file.py +++ b/src/amdb/application/common/services/convert_to_file.py @@ -1,6 +1,6 @@ from amdb.application.common.constants.export import ExportFormat from amdb.application.common.entities.file import File -from amdb.application.common.converters.rating_for_export import ( +from amdb.application.common.converting.rating_for_export import ( RatingsForExportConverter, ) from amdb.application.common.view_models.rating_for_export import ( diff --git a/src/amdb/application/common/services/ensure_can_use_sending_method.py b/src/amdb/application/common/services/ensure_can_use.py similarity index 100% rename from src/amdb/application/common/services/ensure_can_use_sending_method.py rename to src/amdb/application/common/services/ensure_can_use.py diff --git a/src/amdb/application/query_handlers/__init__.py b/src/amdb/application/query_handlers/__init__.py index e69de29..5734721 100644 --- a/src/amdb/application/query_handlers/__init__.py +++ b/src/amdb/application/query_handlers/__init__.py @@ -0,0 +1,19 @@ +__all__ = ( + "LoginHandler", + "GetDetailedMovieHandler", + "GetDetailedReviewsHandler", + "ExportMyRatingsHandler", + "RequestMyRatingsExportHandler", + "ExportAndSendMyRatingsHandler", + "GetMyDetailedRatingsHandler", + "GetNonDetailedMoviesHandler", +) + +from .login import LoginHandler +from .detailed_movie import GetDetailedMovieHandler +from .detailed_reviews import GetDetailedReviewsHandler +from .export_my_ratings import ExportMyRatingsHandler +from .request_my_ratings_export import RequestMyRatingsExportHandler +from .export_and_send_my_ratings import ExportAndSendMyRatingsHandler +from .my_detailed_ratings import GetMyDetailedRatingsHandler +from .non_detailed_movies import GetNonDetailedMoviesHandler diff --git a/src/amdb/application/query_handlers/export_and_send_my_ratings.py b/src/amdb/application/query_handlers/export_and_send_my_ratings.py index 2966b55..cf53ebd 100644 --- a/src/amdb/application/query_handlers/export_and_send_my_ratings.py +++ b/src/amdb/application/query_handlers/export_and_send_my_ratings.py @@ -1,7 +1,7 @@ from amdb.domain.entities.user import User from amdb.application.common.constants.sending import SendingMethod from amdb.application.common.entities.file import File -from amdb.application.common.services.ensure_can_use_sending_method import ( +from amdb.application.common.services.ensure_can_use import ( EnsureCanUseSendingMethod, ) from amdb.application.common.services.convert_to_file import ( @@ -11,7 +11,7 @@ from amdb.application.common.readers.rating_for_export import ( RatingForExportViewModelsReader, ) -from amdb.application.common.senders.email import SendEmail +from amdb.application.common.sending.email import SendEmail from amdb.application.common.constants.exceptions import USER_DOES_NOT_EXIST from amdb.application.common.exception import ApplicationError from amdb.application.queries.export_and_send_my_ratings import ( diff --git a/src/amdb/application/query_handlers/request_my_ratings_export.py b/src/amdb/application/query_handlers/request_my_ratings_export.py index 2e3eb63..2e3e6a7 100644 --- a/src/amdb/application/query_handlers/request_my_ratings_export.py +++ b/src/amdb/application/query_handlers/request_my_ratings_export.py @@ -1,7 +1,7 @@ from typing import cast from amdb.domain.entities.user import User -from amdb.application.common.services.ensure_can_use_sending_method import ( +from amdb.application.common.services.ensure_can_use import ( EnsureCanUseSendingMethod, ) from amdb.application.common.gateways.user import UserGateway diff --git a/src/amdb/domain/services/__init__.py b/src/amdb/domain/services/__init__.py index e69de29..d9a39f6 100644 --- a/src/amdb/domain/services/__init__.py +++ b/src/amdb/domain/services/__init__.py @@ -0,0 +1,17 @@ +__all__ = ( + "AccessConcern", + "CreateUser", + "UpdateProfile", + "CreateMovie", + "RateMovie", + "UnrateMovie", + "ReviewMovie", +) + +from .access_concern import AccessConcern +from .create_user import CreateUser +from .update_profile import UpdateProfile +from .create_movie import CreateMovie +from .rate_movie import RateMovie +from .unrate_movie import UnrateMovie +from .review_movie import ReviewMovie diff --git a/src/amdb/domain/services/update_profile.py b/src/amdb/domain/services/update_profile.py index a862bdb..fba3921 100644 --- a/src/amdb/domain/services/update_profile.py +++ b/src/amdb/domain/services/update_profile.py @@ -7,7 +7,6 @@ class UpdateProfile: def __init__( self, - *, validate_email: ValidateEmail, ) -> None: self._validate_email = validate_email diff --git a/src/amdb/infrastructure/converters/__init__.py b/src/amdb/infrastructure/converting/__init__.py similarity index 100% rename from src/amdb/infrastructure/converters/__init__.py rename to src/amdb/infrastructure/converting/__init__.py diff --git a/src/amdb/infrastructure/converters/ratings_for_export.py b/src/amdb/infrastructure/converting/ratings_for_export.py similarity index 100% rename from src/amdb/infrastructure/converters/ratings_for_export.py rename to src/amdb/infrastructure/converting/ratings_for_export.py diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/__init__.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/__init__.py index e69de29..d9977a7 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/__init__.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/__init__.py @@ -0,0 +1,25 @@ +__all__ = ( + "UserMapper", + "MovieMapper", + "RatingMapper", + "ReviewMapper", + "DetailedMovieViewModelMapper", + "DetailedReviewViewModelsMapper", + "RatingForExportViewModelMapper", + "NonDetailedMovieViewModelsMapper", + "MyDetailedRatingsViewModelMapper", + "PermissionsMapper", + "PasswordHashMapper", +) + +from .entities.user import UserMapper +from .entities.movie import MovieMapper +from .entities.rating import RatingMapper +from .entities.review import ReviewMapper +from .view_models.detailed_movie import DetailedMovieViewModelMapper +from .view_models.detailed_review import DetailedReviewViewModelsMapper +from .view_models.rating_for_export import RatingForExportViewModelMapper +from .view_models.non_detailed_movie import NonDetailedMovieViewModelsMapper +from .view_models.my_detailed_ratings import MyDetailedRatingsViewModelMapper +from .permissions import PermissionsMapper +from .password_hash import PasswordHashMapper diff --git a/src/amdb/infrastructure/senders/__init__.py b/src/amdb/infrastructure/sending/__init__.py similarity index 100% rename from src/amdb/infrastructure/senders/__init__.py rename to src/amdb/infrastructure/sending/__init__.py diff --git a/src/amdb/infrastructure/senders/email.py b/src/amdb/infrastructure/sending/email.py similarity index 100% rename from src/amdb/infrastructure/senders/email.py rename to src/amdb/infrastructure/sending/email.py diff --git a/src/amdb/main/providers.py b/src/amdb/main/providers.py deleted file mode 100644 index 4f0e14a..0000000 --- a/src/amdb/main/providers.py +++ /dev/null @@ -1,459 +0,0 @@ -from typing import Iterable, cast - -from dishka import Provider, Scope, provide, alias -from sqlalchemy import Connection, Engine, create_engine -from redis import Redis - -from amdb.domain.services.access_concern import AccessConcern -from amdb.domain.services.create_user import CreateUser -from amdb.domain.services.update_profile import UpdateProfile -from amdb.domain.services.create_movie import CreateMovie -from amdb.domain.services.rate_movie import RateMovie -from amdb.domain.services.unrate_movie import UnrateMovie -from amdb.domain.services.review_movie import ReviewMovie -from amdb.domain.validators.email import ValidateEmail -from amdb.application.common.services.ensure_can_use_sending_method import ( - EnsureCanUseSendingMethod, -) -from amdb.application.common.gateways.user import UserGateway -from amdb.application.common.gateways.movie import MovieGateway -from amdb.application.common.gateways.rating import RatingGateway -from amdb.application.common.gateways.review import ReviewGateway -from amdb.application.common.gateways.permissions import PermissionsGateway -from amdb.application.common.unit_of_work import UnitOfWork -from amdb.application.common.readers.detailed_movie import ( - DetailedMovieViewModelReader, -) -from amdb.application.common.readers.non_detailed_movie import ( - NonDetailedMovieViewModelsReader, -) -from amdb.application.common.readers.detailed_review import ( - DetailedReviewViewModelsReader, -) -from amdb.application.common.readers.my_detailed_ratings import ( - MyDetailedRatingsViewModelReader, -) -from amdb.application.common.password_manager import PasswordManager -from amdb.application.common.identity_provider import IdentityProvider -from amdb.application.command_handlers.register_user import RegisterUserHandler -from amdb.application.command_handlers.update_my_profile import ( - UpdateMyProfileHandler, -) -from amdb.application.command_handlers.create_movie import CreateMovieHandler -from amdb.application.command_handlers.delete_movie import DeleteMovieHandler -from amdb.application.command_handlers.rate_movie import RateMovieHandler -from amdb.application.command_handlers.unrate_movie import UnrateMovieHandler -from amdb.application.command_handlers.review_movie import ReviewMovieHandler -from amdb.application.query_handlers.login import LoginHandler -from amdb.application.query_handlers.detailed_movie import ( - GetDetailedMovieHandler, -) -from amdb.application.query_handlers.non_detailed_movies import ( - GetNonDetailedMoviesHandler, -) -from amdb.application.query_handlers.detailed_reviews import ( - GetDetailedReviewsHandler, -) -from amdb.application.query_handlers.my_detailed_ratings import ( - GetMyDetailedRatingsHandler, -) -from amdb.application.query_handlers.export_my_ratings import ( - ExportMyRatingsHandler, -) -from amdb.application.query_handlers.request_my_ratings_export import ( - RequestMyRatingsExportHandler, -) -from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig -from amdb.infrastructure.persistence.redis.config import RedisConfig -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( - UserMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( - MovieMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( - RatingMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( - ReviewMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( - PasswordHashMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( - DetailedMovieViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( - NonDetailedMovieViewModelsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( - DetailedReviewViewModelsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( - MyDetailedRatingsViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.rating_for_export import ( - RatingForExportViewModelMapper, -) -from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( - PermissionsMapperCacheProvider, -) -from amdb.infrastructure.persistence.caching.permissions_mapper import ( - CachingPermissionsMapper, -) -from amdb.infrastructure.password_manager.hash_computer import HashComputer -from amdb.infrastructure.password_manager.password_manager import ( - HashingPasswordManager, -) -from amdb.infrastructure.converters.ratings_for_export import ( - RealRatingsForExportConverter, -) -from amdb.presentation.create_handler import CreateHandler - - -class ConnectionsProvider(Provider): - scope = Scope.APP - - def __init__( - self, - *, - postgres_config: PostgresConfig, - redis_config: RedisConfig, - ) -> None: - super().__init__() - self._postgsres_config = postgres_config - self._redis_config = redis_config - - @provide - def sqlaclhemy_engine(self) -> Engine: - return create_engine(self._postgsres_config.url) - - @provide - def redis(self) -> Redis: - redis = Redis.from_url( - url=self._redis_config.url, - decode_responses=True, - ) - return cast(Redis, redis) - - @provide(scope=Scope.REQUEST) - def sqlalchemy_connection( - self, - sqlalchemy_engine: Engine, - ) -> Iterable[Connection]: - with sqlalchemy_engine.connect() as sqlalchemy_connection: - yield sqlalchemy_connection - - -class AdaptersProvider(Provider): - scope = Scope.REQUEST - - user_gateway = provide(source=UserMapper, provides=UserGateway) - movie_gateway = provide(source=MovieMapper, provides=MovieGateway) - rating_gateway = provide(source=RatingMapper, provides=RatingGateway) - review_gateway = provide(source=ReviewMapper, provides=ReviewGateway) - detailed_movie_reader = provide( - source=DetailedMovieViewModelMapper, - provides=DetailedMovieViewModelReader, - ) - non_detailed_movie_reader = provide( - source=NonDetailedMovieViewModelsMapper, - provides=NonDetailedMovieViewModelsReader, - ) - detailed_review_reader = provide( - source=DetailedReviewViewModelsMapper, - provides=DetailedReviewViewModelsReader, - ) - my_detailed_ratings_reader = provide( - source=MyDetailedRatingsViewModelMapper, - provides=MyDetailedRatingsViewModelReader, - ) - - unit_of_work = alias(source=Connection, provides=UnitOfWork) - - @provide - def permissions_mapper( - self, - sqlalchemy_connection: Connection, - redis: Redis, - ) -> PermissionsMapper: - permissions_mapper = PermissionsMapper(sqlalchemy_connection) - cache_provider = PermissionsMapperCacheProvider(redis) - return CachingPermissionsMapper( # type: ignore - permissions_mapper=permissions_mapper, - cache_provider=cache_provider, - ) - - @provide - def permissions_gateway( - self, - sqlalchemy_connection: Connection, - redis: Redis, - ) -> PermissionsGateway: - permissions_mapper = PermissionsMapper(sqlalchemy_connection) - cache_provider = PermissionsMapperCacheProvider(redis) - return CachingPermissionsMapper( - permissions_mapper=permissions_mapper, - cache_provider=cache_provider, - ) - - @provide - def password_manager( - self, - sqlalchemy_connection: Connection, - ) -> PasswordManager: - password_hash_mapper = PasswordHashMapper(sqlalchemy_connection) - return HashingPasswordManager( - hash_computer=HashComputer(), - password_hash_gateway=password_hash_mapper, - ) - - -class HandlersProvider(Provider): - scope = Scope.REQUEST - - @provide - def register_user_handler( - self, - user_gateway: UserGateway, - permissions_gateway: PermissionsGateway, - unit_of_work: UnitOfWork, - password_manager: PasswordManager, - ) -> RegisterUserHandler: - return RegisterUserHandler( - create_user=CreateUser(validate_email=ValidateEmail()), - user_gateway=user_gateway, - permissions_gateway=permissions_gateway, - unit_of_work=unit_of_work, - password_manager=password_manager, - ) - - @provide - def login_handler( - self, - user_gateway: UserGateway, - permissions_gateway: PermissionsGateway, - password_manager: PasswordManager, - ) -> LoginHandler: - return LoginHandler( - access_concern=AccessConcern(), - user_gateway=user_gateway, - permissions_gateway=permissions_gateway, - password_manager=password_manager, - ) - - @provide - def create_movie_handler( - self, - movie_gateway: MovieGateway, - unit_of_work: UnitOfWork, - ) -> CreateMovieHandler: - return CreateMovieHandler( - create_movie=CreateMovie(), - movie_gateway=movie_gateway, - unit_of_work=unit_of_work, - ) - - @provide - def delete_movie_handler( - self, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - ) -> DeleteMovieHandler: - return DeleteMovieHandler( - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - review_gateway=review_gateway, - unit_of_work=unit_of_work, - ) - - @provide - def get_detailed_reviews_handler( - self, - movie_gateway: MovieGateway, - detailed_reviews_reader: DetailedReviewViewModelsReader, - ) -> GetDetailedReviewsHandler: - return GetDetailedReviewsHandler( - movie_gateway=movie_gateway, - detailed_reviews_reader=detailed_reviews_reader, - ) - - -class HandlerCreatorsProvider(Provider): - scope = Scope.REQUEST - - @provide - def update_my_profile_handler( - self, - user_gateway: UserGateway, - unit_of_work: UnitOfWork, - ) -> CreateHandler[UpdateMyProfileHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> UpdateMyProfileHandler: - return UpdateMyProfileHandler( - update_profile=UpdateProfile(validate_email=ValidateEmail()), - user_gateway=user_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def get_detailed_movie_handler( - self, - detailed_movie_reader: DetailedMovieViewModelReader, - ) -> CreateHandler[GetDetailedMovieHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> GetDetailedMovieHandler: - return GetDetailedMovieHandler( - detailed_movie_reader=detailed_movie_reader, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def get_non_detailed_movies_handler( - self, - non_detailed_movies_reader: NonDetailedMovieViewModelsReader, - ) -> CreateHandler[GetNonDetailedMoviesHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> GetNonDetailedMoviesHandler: - return GetNonDetailedMoviesHandler( - non_detailed_movies_reader=non_detailed_movies_reader, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def get_my_detailed_ratings_handler( - self, - my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, - ) -> CreateHandler[GetMyDetailedRatingsHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> GetMyDetailedRatingsHandler: - return GetMyDetailedRatingsHandler( - my_detailed_ratings_reader=my_detailed_ratings_reader, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def rate_movie_handler( - self, - permissions_gateway: PermissionsGateway, - user_gateway: UserGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - unit_of_work: UnitOfWork, - ) -> CreateHandler[RateMovieHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> RateMovieHandler: - return RateMovieHandler( - access_concern=AccessConcern(), - rate_movie=RateMovie(), - permissions_gateway=permissions_gateway, - user_gateway=user_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def unrate_movie_handler( - self, - permissions_gateway: PermissionsGateway, - movie_gateway: MovieGateway, - rating_gateway: RatingGateway, - unit_of_work: UnitOfWork, - ) -> CreateHandler[UnrateMovieHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> UnrateMovieHandler: - return UnrateMovieHandler( - access_concern=AccessConcern(), - unrate_movie=UnrateMovie(), - permissions_gateway=permissions_gateway, - movie_gateway=movie_gateway, - rating_gateway=rating_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def review_movie_handler( - self, - permissions_gateway: PermissionsGateway, - user_gateway: UserGateway, - movie_gateway: MovieGateway, - review_gateway: ReviewGateway, - unit_of_work: UnitOfWork, - ) -> CreateHandler[ReviewMovieHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> ReviewMovieHandler: - return ReviewMovieHandler( - access_concern=AccessConcern(), - review_movie=ReviewMovie(), - permissions_gateway=permissions_gateway, - user_gateway=user_gateway, - movie_gateway=movie_gateway, - review_gateway=review_gateway, - unit_of_work=unit_of_work, - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def export_my_ratings_handler( - self, - sqlaclhemy_connection: Connection, - ) -> CreateHandler[ExportMyRatingsHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> ExportMyRatingsHandler: - return ExportMyRatingsHandler( - ratings_for_export_reader=RatingForExportViewModelMapper( - connection=sqlaclhemy_connection, - ), - ratings_for_export_converter=RealRatingsForExportConverter(), - identity_provider=identity_provider, - ) - - return create_handler - - @provide - def request_my_ratings_export_handler( - self, - user_gateway: UserGateway, - ) -> CreateHandler[RequestMyRatingsExportHandler]: - def create_handler( - identity_provider: IdentityProvider, - ) -> RequestMyRatingsExportHandler: - return RequestMyRatingsExportHandler( - ensure_can_use_sending_method=EnsureCanUseSendingMethod(), - user_gateway=user_gateway, - enqueue_export_and_sending=lambda **kwargs: ..., # type: ignore - identity_provider=identity_provider, - ) - - return create_handler diff --git a/src/amdb/main/providers/__init__.py b/src/amdb/main/providers/__init__.py new file mode 100644 index 0000000..6677090 --- /dev/null +++ b/src/amdb/main/providers/__init__.py @@ -0,0 +1,41 @@ +__all__ = ( + "ConfigsProvider", + "DomainValidatorsProvider", + "DomainServicesProvider", + "ConnectionsProvider", + "EntityMappersProvider", + "ViewModelMappersProvider", + "ApplicationModelMappersProvider", + "SendingAdaptersProvider", + "TaskQueueAdaptersProvider", + "ConvertingAdaptersProvider", + "PasswordManagerProvider", + "ApllicationServicesProvider", + "CommandHandlersProvider", + "CommandHandlerMakersProvider", + "QueryHandlersProvider", + "QueryHandlerMakersProvider", +) + +from .configs import ConfigsProvider +from .domain_validators import DomainValidatorsProvider +from .domain_services import DomainServicesProvider +from .connections import ConnectionsProvider +from .data_mappers import ( + EntityMappersProvider, + ViewModelMappersProvider, + ApplicationModelMappersProvider, +) +from .sending import SendingAdaptersProvider +from .task_queue import TaskQueueAdaptersProvider +from .converting import ConvertingAdaptersProvider +from .password_manager import PasswordManagerProvider +from .application_services import ApllicationServicesProvider +from .command_handlers import ( + CommandHandlersProvider, + CommandHandlerMakersProvider, +) +from .query_handlers import ( + QueryHandlersProvider, + QueryHandlerMakersProvider, +) diff --git a/src/amdb/main/providers/application_services.py b/src/amdb/main/providers/application_services.py new file mode 100644 index 0000000..70b923e --- /dev/null +++ b/src/amdb/main/providers/application_services.py @@ -0,0 +1,19 @@ +from dishka import Provider, Scope, provide + +from amdb.application.common.services import ( + ConvertMyRatingsToFile, + EnsureCanUseSendingMethod, +) + + +class ApllicationServicesProvider(Provider): + scope = Scope.APP + + convert_my_ratings_to_file = provide( + ConvertMyRatingsToFile, + provides=ConvertMyRatingsToFile, + ) + ensure_can_use_sending_method = provide( + EnsureCanUseSendingMethod, + provides=EnsureCanUseSendingMethod, + ) diff --git a/src/amdb/main/providers/command_handlers.py b/src/amdb/main/providers/command_handlers.py new file mode 100644 index 0000000..c08d467 --- /dev/null +++ b/src/amdb/main/providers/command_handlers.py @@ -0,0 +1,184 @@ +from dishka import Provider, Scope, provide + +from amdb.domain.services import ( + AccessConcern, + CreateUser, + UpdateProfile, + CreateMovie, + RateMovie, + UnrateMovie, + ReviewMovie, +) +from amdb.application.common.gateways import ( + UserGateway, + MovieGateway, + RatingGateway, + ReviewGateway, + PermissionsGateway, +) +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.application.common.password_manager import PasswordManager +from amdb.application.common.identity_provider import ( + IdentityProvider, +) +from amdb.application.command_handlers import ( + RegisterUserHandler, + UpdateMyProfileHandler, + CreateMovieHandler, + DeleteMovieHandler, + RateMovieHandler, + UnrateMovieHandler, + ReviewMovieHandler, +) +from amdb.presentation.create_handler import CreateHandler + + +class CommandHandlersProvider(Provider): + scope = Scope.REQUEST + + @provide + def register_user( + self, + create_user: CreateUser, + user_gateway: UserGateway, + permissions_gateway: PermissionsGateway, + unit_of_work: UnitOfWork, + password_manager: PasswordManager, + ) -> RegisterUserHandler: + return RegisterUserHandler( + create_user=create_user, + user_gateway=user_gateway, + permissions_gateway=permissions_gateway, + unit_of_work=unit_of_work, + password_manager=password_manager, + ) + + @provide + def create_movie( + self, + create_movie: CreateMovie, + movie_gateway: MovieGateway, + unit_of_work: UnitOfWork, + ) -> CreateMovieHandler: + return CreateMovieHandler( + create_movie=create_movie, + movie_gateway=movie_gateway, + unit_of_work=unit_of_work, + ) + + @provide + def delete_movie( + self, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + ) -> DeleteMovieHandler: + return DeleteMovieHandler( + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + review_gateway=review_gateway, + unit_of_work=unit_of_work, + ) + + +class CommandHandlerMakersProvider(Provider): + scope = Scope.REQUEST + + @provide + def update_my_profile( + self, + update_profile: UpdateProfile, + user_gateway: UserGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[UpdateMyProfileHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> UpdateMyProfileHandler: + return UpdateMyProfileHandler( + update_profile=update_profile, + user_gateway=user_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def rate_movie_handler( + self, + access_concern: AccessConcern, + rate_movie: RateMovie, + permissions_gateway: PermissionsGateway, + user_gateway: UserGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[RateMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> RateMovieHandler: + return RateMovieHandler( + access_concern=access_concern, + rate_movie=rate_movie, + permissions_gateway=permissions_gateway, + user_gateway=user_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def unrate_movie_handler( + self, + access_concern: AccessConcern, + unrate_movie: UnrateMovie, + permissions_gateway: PermissionsGateway, + movie_gateway: MovieGateway, + rating_gateway: RatingGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[UnrateMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> UnrateMovieHandler: + return UnrateMovieHandler( + access_concern=access_concern, + unrate_movie=unrate_movie, + permissions_gateway=permissions_gateway, + movie_gateway=movie_gateway, + rating_gateway=rating_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def review_movie_handler( + self, + access_concern: AccessConcern, + review_movie: ReviewMovie, + permissions_gateway: PermissionsGateway, + user_gateway: UserGateway, + movie_gateway: MovieGateway, + review_gateway: ReviewGateway, + unit_of_work: UnitOfWork, + ) -> CreateHandler[ReviewMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> ReviewMovieHandler: + return ReviewMovieHandler( + access_concern=access_concern, + review_movie=review_movie, + permissions_gateway=permissions_gateway, + user_gateway=user_gateway, + movie_gateway=movie_gateway, + review_gateway=review_gateway, + unit_of_work=unit_of_work, + identity_provider=identity_provider, + ) + + return create_handler diff --git a/src/amdb/main/providers/configs.py b/src/amdb/main/providers/configs.py new file mode 100644 index 0000000..e64ff38 --- /dev/null +++ b/src/amdb/main/providers/configs.py @@ -0,0 +1,24 @@ +from dishka import Provider, Scope, provide + +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig + + +class ConfigsProvider(Provider): + def __init__( + self, + *, + postgres_config: PostgresConfig, + redis_config: RedisConfig, + ) -> None: + super().__init__() + self._postgres_config = postgres_config + self._redis_config = redis_config + + @provide(scope=Scope.APP) + def postgres_config(self) -> PostgresConfig: + return self._postgres_config + + @provide(scope=Scope.APP) + def redis_config(self) -> RedisConfig: + return self._redis_config diff --git a/src/amdb/main/providers/connections.py b/src/amdb/main/providers/connections.py new file mode 100644 index 0000000..da814a0 --- /dev/null +++ b/src/amdb/main/providers/connections.py @@ -0,0 +1,33 @@ +from typing import Iterable, cast + +from dishka import Provider, Scope, provide +from sqlalchemy import Connection, Engine, create_engine +from redis import Redis + +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig + + +class ConnectionsProvider(Provider): + @provide(scope=Scope.APP) + def sqlaclhemy_engine( + self, + postgres_config: PostgresConfig, + ) -> Engine: + return create_engine(postgres_config.url) + + @provide(scope=Scope.REQUEST) + def sqlalchemy_connection( + self, + sqlalchemy_engine: Engine, + ) -> Iterable[Connection]: + with sqlalchemy_engine.connect() as conn: + yield conn + + @provide(scope=Scope.APP) + def redis(self, redis_config: RedisConfig) -> Redis: + redis = Redis.from_url( + url=redis_config.url, + decode_responses=True, + ) + return cast(Redis, redis) diff --git a/src/amdb/main/providers/converting.py b/src/amdb/main/providers/converting.py new file mode 100644 index 0000000..9ca69d4 --- /dev/null +++ b/src/amdb/main/providers/converting.py @@ -0,0 +1,17 @@ +from dishka import Provider, Scope, provide + +from amdb.application.common.converting.rating_for_export import ( + RatingsForExportConverter, +) +from amdb.infrastructure.converting.ratings_for_export import ( + RealRatingsForExportConverter, +) + + +class ConvertingAdaptersProvider(Provider): + scope = Scope.APP + + ratings_for_export = provide( + RealRatingsForExportConverter, + provides=RatingsForExportConverter, + ) diff --git a/src/amdb/main/providers/data_mappers.py b/src/amdb/main/providers/data_mappers.py new file mode 100644 index 0000000..3e33174 --- /dev/null +++ b/src/amdb/main/providers/data_mappers.py @@ -0,0 +1,97 @@ +from dishka import Provider, Scope, alias, provide +from sqlalchemy import Connection +from redis import Redis + +from amdb.application.common.gateways import ( + UserGateway, + MovieGateway, + RatingGateway, + ReviewGateway, + PermissionsGateway, +) +from amdb.application.common.readers import ( + DetailedMovieViewModelReader, + DetailedReviewViewModelsReader, + RatingForExportViewModelsReader, + MyDetailedRatingsViewModelReader, + NonDetailedMovieViewModelsReader, +) +from amdb.application.common.unit_of_work import UnitOfWork +from amdb.infrastructure.password_manager.password_hash_gateway import ( + PasswordHashGateway, +) +from amdb.infrastructure.persistence.sqlalchemy.mappers import ( + UserMapper, + MovieMapper, + RatingMapper, + ReviewMapper, + PermissionsMapper, + PasswordHashMapper, + DetailedMovieViewModelMapper, + DetailedReviewViewModelsMapper, + RatingForExportViewModelMapper, + MyDetailedRatingsViewModelMapper, + NonDetailedMovieViewModelsMapper, +) +from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( + PermissionsMapperCacheProvider, +) +from amdb.infrastructure.persistence.caching.permissions_mapper import ( + CachingPermissionsMapper, +) + + +class EntityMappersProvider(Provider): + scope = Scope.REQUEST + + user = provide(UserMapper, provides=UserGateway) + movie = provide(MovieMapper, provides=MovieGateway) + rating = provide(RatingMapper, provides=RatingGateway) + review = provide(ReviewMapper, provides=ReviewGateway) + + unit_of_work = alias(source=Connection, provides=UnitOfWork) + + +class ApplicationModelMappersProvider(Provider): + scope = Scope.REQUEST + + password_hash = provide( + PasswordHashMapper, + provides=PasswordHashGateway, + ) + + @provide + def permissions( + self, + redis: Redis, + sqlalchemy_connection: Connection, + ) -> PermissionsGateway: + return CachingPermissionsMapper( + permissions_mapper=PermissionsMapper(sqlalchemy_connection), + cache_provider=PermissionsMapperCacheProvider(redis), + ) + + +class ViewModelMappersProvider(Provider): + scope = Scope.REQUEST + + detailed_movie = provide( + DetailedMovieViewModelMapper, + provides=DetailedMovieViewModelReader, + ) + detailed_review = provide( + DetailedReviewViewModelsMapper, + provides=DetailedReviewViewModelsReader, + ) + rating_for_export = provide( + RatingForExportViewModelMapper, + provides=RatingForExportViewModelsReader, + ) + my_detailed_ratings = provide( + MyDetailedRatingsViewModelMapper, + provides=MyDetailedRatingsViewModelReader, + ) + non_detailed_movie = provide( + NonDetailedMovieViewModelsMapper, + provides=NonDetailedMovieViewModelsReader, + ) diff --git a/src/amdb/main/providers/domain_services.py b/src/amdb/main/providers/domain_services.py new file mode 100644 index 0000000..35ccbfc --- /dev/null +++ b/src/amdb/main/providers/domain_services.py @@ -0,0 +1,23 @@ +from dishka import Provider, Scope, provide + +from amdb.domain.services import ( + AccessConcern, + CreateUser, + UpdateProfile, + CreateMovie, + RateMovie, + UnrateMovie, + ReviewMovie, +) + + +class DomainServicesProvider(Provider): + scope = Scope.APP + + access_concern = provide(AccessConcern, provides=AccessConcern) + create_user = provide(CreateUser, provides=CreateUser) + create_movie = provide(CreateMovie, provides=CreateMovie) + update_profile = provide(UpdateProfile, provides=UpdateProfile) + rate_movie = provide(RateMovie, provides=RateMovie) + unrate_movie = provide(UnrateMovie, provides=UnrateMovie) + review_movie = provide(ReviewMovie, provides=ReviewMovie) diff --git a/src/amdb/main/providers/domain_validators.py b/src/amdb/main/providers/domain_validators.py new file mode 100644 index 0000000..373f5f6 --- /dev/null +++ b/src/amdb/main/providers/domain_validators.py @@ -0,0 +1,9 @@ +from dishka import Provider, Scope, provide + +from amdb.domain.validators.email import ValidateEmail + + +class DomainValidatorsProvider(Provider): + scope = Scope.APP + + email = provide(ValidateEmail, provides=ValidateEmail) diff --git a/src/amdb/main/providers/password_manager.py b/src/amdb/main/providers/password_manager.py new file mode 100644 index 0000000..d848a2c --- /dev/null +++ b/src/amdb/main/providers/password_manager.py @@ -0,0 +1,22 @@ +from dishka import Provider, Scope, provide + +from amdb.application.common.password_manager import PasswordManager +from amdb.infrastructure.password_manager.hash_computer import HashComputer +from amdb.infrastructure.password_manager.password_hash_gateway import ( + PasswordHashGateway, +) +from amdb.infrastructure.password_manager.password_manager import ( + HashingPasswordManager, +) + + +class PasswordManagerProvider(Provider): + @provide(scope=Scope.REQUEST) + def password_manager( + self, + password_hash_gateway: PasswordHashGateway, + ) -> PasswordManager: + return HashingPasswordManager( + hash_computer=HashComputer(), + password_hash_gateway=password_hash_gateway, + ) diff --git a/src/amdb/main/providers/query_handlers.py b/src/amdb/main/providers/query_handlers.py new file mode 100644 index 0000000..ed9b4cd --- /dev/null +++ b/src/amdb/main/providers/query_handlers.py @@ -0,0 +1,170 @@ +from dishka import Provider, Scope, provide + +from amdb.domain.services.access_concern import AccessConcern +from amdb.application.common.services import ( + ConvertMyRatingsToFile, + EnsureCanUseSendingMethod, +) +from amdb.application.common.gateways import ( + UserGateway, + MovieGateway, + PermissionsGateway, +) +from amdb.application.common.readers import ( + DetailedMovieViewModelReader, + DetailedReviewViewModelsReader, + RatingForExportViewModelsReader, + NonDetailedMovieViewModelsReader, + MyDetailedRatingsViewModelReader, +) +from amdb.application.common.password_manager import PasswordManager +from amdb.application.common.sending.email import SendEmail +from amdb.application.common.task_queue.export_and_send_my_ratings import ( + EnqueueExportAndSendingMyRatings, +) +from amdb.application.common.identity_provider import ( + IdentityProvider, +) +from amdb.application.query_handlers import ( + LoginHandler, + GetDetailedMovieHandler, + GetDetailedReviewsHandler, + ExportMyRatingsHandler, + RequestMyRatingsExportHandler, + ExportAndSendMyRatingsHandler, + GetMyDetailedRatingsHandler, + GetNonDetailedMoviesHandler, +) +from amdb.presentation.create_handler import CreateHandler + + +class QueryHandlersProvider(Provider): + scope = Scope.REQUEST + + @provide + def login( + self, + access_concern: AccessConcern, + user_gateway: UserGateway, + permissions_gateway: PermissionsGateway, + password_manager: PasswordManager, + ) -> LoginHandler: + return LoginHandler( + access_concern=access_concern, + user_gateway=user_gateway, + permissions_gateway=permissions_gateway, + password_manager=password_manager, + ) + + @provide + def get_detailed_reviews( + self, + movie_gateway: MovieGateway, + detailed_reviews_reader: DetailedReviewViewModelsReader, + ) -> GetDetailedReviewsHandler: + return GetDetailedReviewsHandler( + movie_gateway=movie_gateway, + detailed_reviews_reader=detailed_reviews_reader, + ) + + @provide + def export_and_send_my_ratings( + self, + ensure_can_use_sending_method: EnsureCanUseSendingMethod, + convert_my_ratings_to_file: ConvertMyRatingsToFile, + user_gateway: UserGateway, + ratings_for_export_reader: RatingForExportViewModelsReader, + send_email: SendEmail, + ) -> ExportAndSendMyRatingsHandler: + return ExportAndSendMyRatingsHandler( + ensure_can_use_sending_method=ensure_can_use_sending_method, + convert_my_ratings_to_file=convert_my_ratings_to_file, + user_gateway=user_gateway, + ratings_for_export_reader=ratings_for_export_reader, + send_email=send_email, + ) + + +class QueryHandlerMakersProvider(Provider): + scope = Scope.REQUEST + + @provide + def get_detailed_movie( + self, + detailed_movie_reader: DetailedMovieViewModelReader, + ) -> CreateHandler[GetDetailedMovieHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetDetailedMovieHandler: + return GetDetailedMovieHandler( + detailed_movie_reader=detailed_movie_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def get_non_detailed_movies( + self, + non_detailed_movies_reader: NonDetailedMovieViewModelsReader, + ) -> CreateHandler[GetNonDetailedMoviesHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetNonDetailedMoviesHandler: + return GetNonDetailedMoviesHandler( + non_detailed_movies_reader=non_detailed_movies_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def get_my_detailed_ratings( + self, + my_detailed_ratings_reader: MyDetailedRatingsViewModelReader, + ) -> CreateHandler[GetMyDetailedRatingsHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> GetMyDetailedRatingsHandler: + return GetMyDetailedRatingsHandler( + my_detailed_ratings_reader=my_detailed_ratings_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def export_my_ratings( + self, + convert_my_ratings_to_file: ConvertMyRatingsToFile, + ratings_for_export_reader: RatingForExportViewModelsReader, + ) -> CreateHandler[ExportMyRatingsHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> ExportMyRatingsHandler: + return ExportMyRatingsHandler( + convert_my_ratings_to_file=convert_my_ratings_to_file, + ratings_for_export_reader=ratings_for_export_reader, + identity_provider=identity_provider, + ) + + return create_handler + + @provide + def request_my_ratings_export( + self, + ensure_can_use_sending_method: EnsureCanUseSendingMethod, + user_gateway: UserGateway, + enuqueue_export_and_sending: EnqueueExportAndSendingMyRatings, + ) -> CreateHandler[RequestMyRatingsExportHandler]: + def create_handler( + identity_provider: IdentityProvider, + ) -> RequestMyRatingsExportHandler: + return RequestMyRatingsExportHandler( + ensure_can_use_sending_method=ensure_can_use_sending_method, + user_gateway=user_gateway, + enqueue_export_and_sending=enuqueue_export_and_sending, + identity_provider=identity_provider, + ) + + return create_handler diff --git a/src/amdb/main/providers/sending.py b/src/amdb/main/providers/sending.py new file mode 100644 index 0000000..51e5467 --- /dev/null +++ b/src/amdb/main/providers/sending.py @@ -0,0 +1,10 @@ +from dishka import Provider, Scope, provide + +from amdb.application.common.sending.email import SendEmail +from amdb.infrastructure.sending.email import SendFakeEmail + + +class SendingAdaptersProvider(Provider): + scope = Scope.APP + + email = provide(SendFakeEmail, provides=SendEmail) diff --git a/src/amdb/main/providers/task_queue.py b/src/amdb/main/providers/task_queue.py new file mode 100644 index 0000000..1afa736 --- /dev/null +++ b/src/amdb/main/providers/task_queue.py @@ -0,0 +1,17 @@ +from dishka import Provider, Scope, provide + +from amdb.application.common.task_queue.export_and_send_my_ratings import ( + EnqueueExportAndSendingMyRatings, +) +from amdb.infrastructure.task_queue.export_and_send_my_ratings import ( + EnqueueFakeExportAndSendingMyRatings, +) + + +class TaskQueueAdaptersProvider(Provider): + scope = Scope.APP + + export_and_send_ratings = provide( + EnqueueFakeExportAndSendingMyRatings, + provides=EnqueueExportAndSendingMyRatings, + ) diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index 1902b52..b3932f4 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -13,10 +13,22 @@ setup_exception_handlers, ) from amdb.main.providers import ( + ConfigsProvider, + DomainValidatorsProvider, + DomainServicesProvider, ConnectionsProvider, - AdaptersProvider, - HandlersProvider, - HandlerCreatorsProvider, + EntityMappersProvider, + ViewModelMappersProvider, + ApplicationModelMappersProvider, + SendingAdaptersProvider, + TaskQueueAdaptersProvider, + ConvertingAdaptersProvider, + PasswordManagerProvider, + ApllicationServicesProvider, + CommandHandlersProvider, + CommandHandlerMakersProvider, + QueryHandlersProvider, + QueryHandlerMakersProvider, ) from .providers import SessionAdaptersProvider from .config import WebAPIConfig @@ -42,16 +54,28 @@ def run_web_api() -> None: setup_exception_handlers(app) container = make_async_container( - ConnectionsProvider( + ConfigsProvider( postgres_config=postgres_config, redis_config=redis_config, ), - AdaptersProvider(), + DomainValidatorsProvider(), + ConnectionsProvider(), + DomainServicesProvider(), + EntityMappersProvider(), + ViewModelMappersProvider(), + ApplicationModelMappersProvider(), + SendingAdaptersProvider(), + TaskQueueAdaptersProvider(), + PasswordManagerProvider(), + ConvertingAdaptersProvider(), + ApllicationServicesProvider(), + CommandHandlersProvider(), + CommandHandlerMakersProvider(), + QueryHandlersProvider(), + QueryHandlerMakersProvider(), SessionAdaptersProvider( session_config=session_config, ), - HandlersProvider(), - HandlerCreatorsProvider(), ) setup_dishka(container, app) diff --git a/tests/unit/application/conftest.py b/tests/unit/application/conftest.py index f4d68dc..eec4682 100644 --- a/tests/unit/application/conftest.py +++ b/tests/unit/application/conftest.py @@ -6,38 +6,18 @@ from redis.client import Redis from amdb.infrastructure.persistence.sqlalchemy.models.base import Model -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.user import ( +from amdb.infrastructure.persistence.sqlalchemy.mappers import ( UserMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.movie import ( MovieMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.rating import ( RatingMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.entities.review import ( ReviewMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.password_hash import ( - PasswordHashMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.permissions import ( - PermissionsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.non_detailed_movie import ( - NonDetailedMovieViewModelsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_movie import ( DetailedMovieViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.detailed_review import ( DetailedReviewViewModelsMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.my_detailed_ratings import ( - MyDetailedRatingsViewModelMapper, -) -from amdb.infrastructure.persistence.sqlalchemy.mappers.view_models.rating_for_export import ( RatingForExportViewModelMapper, + MyDetailedRatingsViewModelMapper, + NonDetailedMovieViewModelsMapper, + PasswordHashMapper, + PermissionsMapper, ) from amdb.infrastructure.persistence.redis.cache.permissions_mapper import ( PermissionsMapperCacheProvider, diff --git a/tests/unit/application/query_handlers/test_export_my_ratings.py b/tests/unit/application/query_handlers/test_export_my_ratings.py index 9cad525..d1d9231 100644 --- a/tests/unit/application/query_handlers/test_export_my_ratings.py +++ b/tests/unit/application/query_handlers/test_export_my_ratings.py @@ -18,7 +18,7 @@ from amdb.application.common.readers.rating_for_export import ( RatingForExportViewModelsReader, ) -from amdb.infrastructure.converters.ratings_for_export import ( +from amdb.infrastructure.converting.ratings_for_export import ( RealRatingsForExportConverter, ) from amdb.application.queries.export_my_ratings import ExportMyRatingsQuery diff --git a/tests/unit/application/query_handlers/test_request_my_ratings_export.py b/tests/unit/application/query_handlers/test_request_my_ratings_export.py index 87822a2..6cfec55 100644 --- a/tests/unit/application/query_handlers/test_request_my_ratings_export.py +++ b/tests/unit/application/query_handlers/test_request_my_ratings_export.py @@ -7,7 +7,7 @@ from amdb.domain.entities.user import User, UserId from amdb.application.common.constants.export import ExportFormat from amdb.application.common.constants.sending import SendingMethod -from amdb.application.common.services.ensure_can_use_sending_method import ( +from amdb.application.common.services.ensure_can_use import ( EnsureCanUseSendingMethod, ) from amdb.application.common.gateways.user import UserGateway From e2e3ae2c4e39985e7a725e66bb0982100d7aa6c5 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 11:43:50 +0400 Subject: [PATCH 29/39] Refactor http handlers --- src/amdb/presentation/web_api/exports/my_ratings.py | 4 ++-- src/amdb/presentation/web_api/exports/request_my_ratings.py | 4 ++-- src/amdb/presentation/web_api/movies/get_detailed.py | 4 ++-- src/amdb/presentation/web_api/movies/get_non_detailed.py | 4 ++-- src/amdb/presentation/web_api/profiles/update_my.py | 4 ++-- src/amdb/presentation/web_api/ratings/get_my_detailed.py | 4 ++-- src/amdb/presentation/web_api/ratings/rate_movie.py | 4 ++-- src/amdb/presentation/web_api/ratings/unrate_movie.py | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/amdb/presentation/web_api/exports/my_ratings.py b/src/amdb/presentation/web_api/exports/my_ratings.py index 22796a9..f1c4148 100644 --- a/src/amdb/presentation/web_api/exports/my_ratings.py +++ b/src/amdb/presentation/web_api/exports/my_ratings.py @@ -19,13 +19,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[ExportMyRatingsHandler] +HandlerMaker = CreateHandler[ExportMyRatingsHandler] @inject async def export_my_ratings( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/exports/request_my_ratings.py b/src/amdb/presentation/web_api/exports/request_my_ratings.py index ece5de0..cba6c85 100644 --- a/src/amdb/presentation/web_api/exports/request_my_ratings.py +++ b/src/amdb/presentation/web_api/exports/request_my_ratings.py @@ -19,13 +19,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[RequestMyRatingsExportHandler] +HandlerMaker = CreateHandler[RequestMyRatingsExportHandler] @inject async def request_my_ratings_export( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/movies/get_detailed.py b/src/amdb/presentation/web_api/movies/get_detailed.py index 3b9a53f..e57c377 100644 --- a/src/amdb/presentation/web_api/movies/get_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_detailed.py @@ -21,13 +21,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[GetDetailedMovieHandler] +HandlerMaker = CreateHandler[GetDetailedMovieHandler] @inject async def get_detailed_movie( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/movies/get_non_detailed.py b/src/amdb/presentation/web_api/movies/get_non_detailed.py index c2ce3d7..521a6f3 100644 --- a/src/amdb/presentation/web_api/movies/get_non_detailed.py +++ b/src/amdb/presentation/web_api/movies/get_non_detailed.py @@ -22,13 +22,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[GetNonDetailedMoviesHandler] +HandlerMaker = CreateHandler[GetNonDetailedMoviesHandler] @inject async def get_non_detailed_movies( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/profiles/update_my.py b/src/amdb/presentation/web_api/profiles/update_my.py index 9f87b3e..76be628 100644 --- a/src/amdb/presentation/web_api/profiles/update_my.py +++ b/src/amdb/presentation/web_api/profiles/update_my.py @@ -16,13 +16,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[UpdateMyProfileHandler] +HandlerMaker = CreateHandler[UpdateMyProfileHandler] @inject async def update_my_profile( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/ratings/get_my_detailed.py b/src/amdb/presentation/web_api/ratings/get_my_detailed.py index 285b22c..ee6d940 100644 --- a/src/amdb/presentation/web_api/ratings/get_my_detailed.py +++ b/src/amdb/presentation/web_api/ratings/get_my_detailed.py @@ -22,13 +22,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[GetMyDetailedRatingsHandler] +HandlerMaker = CreateHandler[GetMyDetailedRatingsHandler] @inject async def get_my_detailed_ratings( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/ratings/rate_movie.py b/src/amdb/presentation/web_api/ratings/rate_movie.py index 11a0e76..934570a 100644 --- a/src/amdb/presentation/web_api/ratings/rate_movie.py +++ b/src/amdb/presentation/web_api/ratings/rate_movie.py @@ -16,13 +16,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[RateMovieHandler] +HandlerMaker = CreateHandler[RateMovieHandler] @inject async def rate_movie( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ diff --git a/src/amdb/presentation/web_api/ratings/unrate_movie.py b/src/amdb/presentation/web_api/ratings/unrate_movie.py index 59e2c56..8bda81b 100644 --- a/src/amdb/presentation/web_api/ratings/unrate_movie.py +++ b/src/amdb/presentation/web_api/ratings/unrate_movie.py @@ -16,13 +16,13 @@ from amdb.presentation.web_api.constants import SESSION_ID_COOKIE -HandlerCreator = CreateHandler[UnrateMovieHandler] +HandlerMaker = CreateHandler[UnrateMovieHandler] @inject async def unrate_movie( *, - create_handler: Annotated[HandlerCreator, FromDishka()], + create_handler: Annotated[HandlerMaker, FromDishka()], session_gateway: Annotated[SessionGateway, FromDishka()], permissions_gateway: Annotated[PermissionsGateway, FromDishka()], session_id: Annotated[ From 05cd1c41c00764e2b40be7604ee3d476f245432d Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 14:08:55 +0400 Subject: [PATCH 30/39] Fix commands optional fields have no default value --- src/amdb/application/commands/register_user.py | 2 +- src/amdb/application/commands/update_my_profile.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/amdb/application/commands/register_user.py b/src/amdb/application/commands/register_user.py index bb15bef..e50bdde 100644 --- a/src/amdb/application/commands/register_user.py +++ b/src/amdb/application/commands/register_user.py @@ -5,5 +5,5 @@ @dataclass(frozen=True, slots=True) class RegisterUserCommand: name: str - email: Optional[str] password: str + email: Optional[str] = None diff --git a/src/amdb/application/commands/update_my_profile.py b/src/amdb/application/commands/update_my_profile.py index ac62ee8..0fb1bb9 100644 --- a/src/amdb/application/commands/update_my_profile.py +++ b/src/amdb/application/commands/update_my_profile.py @@ -4,4 +4,4 @@ @dataclass(frozen=True, slots=True) class UpdateMyProfileCommand: - email: Optional[str] + email: Optional[str] = None From a7de54e3cc07a518c2f9bd2adf0633a9cf44e87f Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 16:00:08 +0400 Subject: [PATCH 31/39] Update latest migration: fill permissions table with default data --- .../alembic/migrations/versions/a2f7c2383ba8_.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py index 90301ee..276fd5d 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py @@ -1,6 +1,7 @@ """ Add permissions table, -Make type column string to reviews table +Fill permission table with default data, +Make type column string to reviews table, Add email column to users table Revision ID: a2f7c2383ba8 @@ -25,7 +26,7 @@ def upgrade() -> None: op.create_table( "permissions", sa.Column("user_id", sa.Uuid(), nullable=False), - sa.Column("value", sa.Integer(), nullable=False), + sa.Column("value", sa.Integer(), nullable=False, default=30), sa.PrimaryKeyConstraint("user_id"), sa.ForeignKeyConstraint( ["user_id"], @@ -33,6 +34,12 @@ def upgrade() -> None: ondelete="CASCADE", ), ) + op.execute( + """ + INSERT INTO permissions (user_id) + (SELECT u.id FROM users u) + """, + ) with op.batch_alter_table("reviews") as batch_op: batch_op.alter_column( "type", From 8e7f795a6e33ab91c27fe9c49f2c21d9a0c0ae11 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 16:05:01 +0400 Subject: [PATCH 32/39] Fix grammer --- .../alembic/migrations/versions/65f8840f4494_.py | 8 ++++---- .../alembic/migrations/versions/85a348467b90_.py | 2 +- .../alembic/migrations/versions/a2f7c2383ba8_.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py index 11c9645..aef0513 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/65f8840f4494_.py @@ -1,9 +1,9 @@ """ Add uuid7 function, -Add id column to ratings table, -Add primary key constraint on id of ratings table, -Add unique constraint on pair of user_id and movie_id of ratings table, -Add unique constraint on pair of user_id and movie_id of reviews table +Add id column in ratings table, +Add primary key constraint on id in ratings table, +Add unique constraint on pair of user_id and movie_id in ratings table, +Add unique constraint on pair of user_id and movie_id in reviews table Revision ID: 65f8840f4494 Revises: 85a348467b90 diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py index 77d5d02..48ca05d 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/85a348467b90_.py @@ -1,5 +1,5 @@ """ -Add release_date column to movies table, +Add release_date column in movies table, Add review table Revision ID: 85a348467b90 diff --git a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py index 276fd5d..506ec52 100644 --- a/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py +++ b/src/amdb/infrastructure/persistence/alembic/migrations/versions/a2f7c2383ba8_.py @@ -1,8 +1,8 @@ """ Add permissions table, Fill permission table with default data, -Make type column string to reviews table, -Add email column to users table +Make type column string in reviews table, +Add email column in users table Revision ID: a2f7c2383ba8 Revises: 65f8840f4494 From 30a92e32d9ffbd8ae7bdc5a05364f9f829fe7090 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Fri, 8 Mar 2024 17:25:16 +0400 Subject: [PATCH 33/39] Fix movie table race condition --- .../persistence/sqlalchemy/mappers/entities/movie.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py index 5f43986..5a5a077 100644 --- a/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py +++ b/src/amdb/infrastructure/persistence/sqlalchemy/mappers/entities/movie.py @@ -11,7 +11,11 @@ def __init__(self, connection: Connection) -> None: self._connection = connection def with_id(self, movie_id: MovieId) -> Optional[Movie]: - statement = select(MovieModel).where(MovieModel.id == movie_id) + statement = ( + select(MovieModel) + .where(MovieModel.id == movie_id) + .with_for_update() + ) row = self._connection.execute(statement).one_or_none() if row: return self._to_entity(row) # type: ignore From 0fc1c440f44e908b1847c20c40df4ee91bd2598d Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 9 Mar 2024 14:17:49 +0400 Subject: [PATCH 34/39] Add worker --- pyproject.toml | 4 +- .../redis}/task_queue/__init__.py | 0 .../task_queue/export_and_send_my_ratings.py | 26 +++++++ src/amdb/infrastructure/sending/email.py | 11 +-- .../task_queue/export_and_send_my_ratings.py | 14 ---- src/amdb/main/providers/task_queue.py | 6 +- src/amdb/main/web_api/__main__.py | 5 -- src/amdb/main/web_api/app.py | 2 +- ...viders.py => session_adapters_provider.py} | 0 src/amdb/main/worker/__init__.py | 0 src/amdb/main/worker/app.py | 68 +++++++++++++++++++ src/amdb/presentation/worker/__init__.py | 0 .../worker/export_and_send_my_ratings.py | 28 ++++++++ src/amdb/presentation/worker/router.py | 10 +++ 14 files changed, 146 insertions(+), 28 deletions(-) rename src/amdb/infrastructure/{ => persistence/redis}/task_queue/__init__.py (100%) create mode 100644 src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py delete mode 100644 src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py delete mode 100644 src/amdb/main/web_api/__main__.py rename src/amdb/main/web_api/{providers.py => session_adapters_provider.py} (100%) create mode 100644 src/amdb/main/worker/__init__.py create mode 100644 src/amdb/main/worker/app.py create mode 100644 src/amdb/presentation/worker/__init__.py create mode 100644 src/amdb/presentation/worker/export_and_send_my_ratings.py create mode 100644 src/amdb/presentation/worker/router.py diff --git a/pyproject.toml b/pyproject.toml index 0e653d6..f91b680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ dependencies = [ "psycopg2-binary==2.9.*", "alembic==1.13.*", "redis==5.0.*", + "faststream[redis]==0.4.*", ] [project.optional-dependencies] @@ -51,4 +52,5 @@ coverage = [ ] [project.scripts] -amdb-web_api = "amdb.main.web_api.__main__:main" +amdb-web_api = "amdb.main.web_api.app:run_web_api" +amdb-worker = "amdb.main.worker.app:run_worker" diff --git a/src/amdb/infrastructure/task_queue/__init__.py b/src/amdb/infrastructure/persistence/redis/task_queue/__init__.py similarity index 100% rename from src/amdb/infrastructure/task_queue/__init__.py rename to src/amdb/infrastructure/persistence/redis/task_queue/__init__.py diff --git a/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py b/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py new file mode 100644 index 0000000..9165d15 --- /dev/null +++ b/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py @@ -0,0 +1,26 @@ +import json + +from redis import Redis + +from amdb.domain.entities.user import UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod + + +class EnqueueExportAndSendingMyRatingsInRedis: + def __init__(self, redis: Redis) -> None: + self._redis = redis + + def __call__( + self, + *, + user_id: UserId, + export_format: ExportFormat, + sending_method: SendingMethod, + ) -> None: + data = { + "user_id": user_id.hex, + "export_format": export_format.value, + "sending_method": sending_method.value, + } + self._redis.lpush("tasks", json.dumps(data)) diff --git a/src/amdb/infrastructure/sending/email.py b/src/amdb/infrastructure/sending/email.py index db4d294..faa8484 100644 --- a/src/amdb/infrastructure/sending/email.py +++ b/src/amdb/infrastructure/sending/email.py @@ -1,9 +1,7 @@ -from typing import Protocol - from amdb.application.common.entities.file import File -class SendFakeEmail(Protocol): +class SendFakeEmail: def __call__( self, *, @@ -11,4 +9,9 @@ def __call__( subject: str, files: list[File], ) -> None: - ... + print( # noqa + "Email has been sent. \n" + f"Address: {email} \n" + f"Subject: {subject} \n" + f"Number of file: {len(files)}", + ) diff --git a/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py b/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py deleted file mode 100644 index b6a91ab..0000000 --- a/src/amdb/infrastructure/task_queue/export_and_send_my_ratings.py +++ /dev/null @@ -1,14 +0,0 @@ -from amdb.domain.entities.user import UserId -from amdb.application.common.constants.export import ExportFormat -from amdb.application.common.constants.sending import SendingMethod - - -class EnqueueFakeExportAndSendingMyRatings: - def __call__( - self, - *, - user_id: UserId, - export_format: ExportFormat, - sending_method: SendingMethod, - ) -> None: - ... diff --git a/src/amdb/main/providers/task_queue.py b/src/amdb/main/providers/task_queue.py index 1afa736..f4187e8 100644 --- a/src/amdb/main/providers/task_queue.py +++ b/src/amdb/main/providers/task_queue.py @@ -3,8 +3,8 @@ from amdb.application.common.task_queue.export_and_send_my_ratings import ( EnqueueExportAndSendingMyRatings, ) -from amdb.infrastructure.task_queue.export_and_send_my_ratings import ( - EnqueueFakeExportAndSendingMyRatings, +from amdb.infrastructure.persistence.redis.task_queue.export_and_send_my_ratings import ( + EnqueueExportAndSendingMyRatingsInRedis, ) @@ -12,6 +12,6 @@ class TaskQueueAdaptersProvider(Provider): scope = Scope.APP export_and_send_ratings = provide( - EnqueueFakeExportAndSendingMyRatings, + EnqueueExportAndSendingMyRatingsInRedis, provides=EnqueueExportAndSendingMyRatings, ) diff --git a/src/amdb/main/web_api/__main__.py b/src/amdb/main/web_api/__main__.py deleted file mode 100644 index 44388f6..0000000 --- a/src/amdb/main/web_api/__main__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .app import run_web_api - - -def main() -> None: - run_web_api() diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index b3932f4..a1af0e2 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -30,7 +30,7 @@ QueryHandlersProvider, QueryHandlerMakersProvider, ) -from .providers import SessionAdaptersProvider +from .session_adapters_provider import SessionAdaptersProvider from .config import WebAPIConfig diff --git a/src/amdb/main/web_api/providers.py b/src/amdb/main/web_api/session_adapters_provider.py similarity index 100% rename from src/amdb/main/web_api/providers.py rename to src/amdb/main/web_api/session_adapters_provider.py diff --git a/src/amdb/main/worker/__init__.py b/src/amdb/main/worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/main/worker/app.py b/src/amdb/main/worker/app.py new file mode 100644 index 0000000..e67db4e --- /dev/null +++ b/src/amdb/main/worker/app.py @@ -0,0 +1,68 @@ +import asyncio +import os + +from faststream import FastStream +from faststream.redis import RedisBroker +from dishka import make_async_container +from dishka.integrations.faststream import setup_dishka + +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig +from amdb.presentation.worker.router import router +from amdb.main.providers import ( + ConfigsProvider, + DomainValidatorsProvider, + DomainServicesProvider, + ConnectionsProvider, + EntityMappersProvider, + ViewModelMappersProvider, + ApplicationModelMappersProvider, + SendingAdaptersProvider, + TaskQueueAdaptersProvider, + ConvertingAdaptersProvider, + PasswordManagerProvider, + ApllicationServicesProvider, + CommandHandlersProvider, + CommandHandlerMakersProvider, + QueryHandlersProvider, + QueryHandlerMakersProvider, +) + + +def run_worker() -> None: + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + postgres_config = PostgresConfig.from_toml(path_to_config) + redis_config = RedisConfig.from_toml(path_to_config) + + broker = RedisBroker(url=redis_config.url) + broker.include_router(router) + + app = FastStream(broker) + container = make_async_container( + ConfigsProvider( + postgres_config=postgres_config, + redis_config=redis_config, + ), + DomainValidatorsProvider(), + ConnectionsProvider(), + DomainServicesProvider(), + EntityMappersProvider(), + ViewModelMappersProvider(), + ApplicationModelMappersProvider(), + SendingAdaptersProvider(), + TaskQueueAdaptersProvider(), + PasswordManagerProvider(), + ConvertingAdaptersProvider(), + ApllicationServicesProvider(), + CommandHandlersProvider(), + CommandHandlerMakersProvider(), + QueryHandlersProvider(), + QueryHandlerMakersProvider(), + ) + setup_dishka(container, app) + + asyncio.run(app.run()) diff --git a/src/amdb/presentation/worker/__init__.py b/src/amdb/presentation/worker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/presentation/worker/export_and_send_my_ratings.py b/src/amdb/presentation/worker/export_and_send_my_ratings.py new file mode 100644 index 0000000..f673820 --- /dev/null +++ b/src/amdb/presentation/worker/export_and_send_my_ratings.py @@ -0,0 +1,28 @@ +from typing import Annotated + +from dishka.integrations.faststream import FromDishka, inject + +from amdb.domain.entities.user import UserId +from amdb.application.common.constants.export import ExportFormat +from amdb.application.common.constants.sending import SendingMethod +from amdb.application.queries.export_and_send_my_ratings import ( + ExportAndSendMyRatingsQuery, +) +from amdb.application.query_handlers.export_and_send_my_ratings import ( + ExportAndSendMyRatingsHandler, +) + + +@inject +async def export_and_send_my_ratings( + handler: Annotated[ExportAndSendMyRatingsHandler, FromDishka()], + user_id: UserId, + export_format: ExportFormat, + sending_method: SendingMethod, +) -> None: + query = ExportAndSendMyRatingsQuery( + user_id=user_id, + format=export_format, + sending_method=sending_method, + ) + handler.execute(query) diff --git a/src/amdb/presentation/worker/router.py b/src/amdb/presentation/worker/router.py new file mode 100644 index 0000000..f77340a --- /dev/null +++ b/src/amdb/presentation/worker/router.py @@ -0,0 +1,10 @@ +from faststream.redis import RedisRoute, RedisRouter + +from .export_and_send_my_ratings import export_and_send_my_ratings + + +router = RedisRouter( + handlers=[ + RedisRoute(export_and_send_my_ratings, list="tasks"), + ], +) From 49cbb4f7e4786f177e924d46501430b5f2c09bee Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 9 Mar 2024 14:19:36 +0400 Subject: [PATCH 35/39] Rename export and send my ratings task queue name --- .../redis/task_queue/export_and_send_my_ratings.py | 5 ++++- src/amdb/presentation/worker/router.py | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py b/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py index 9165d15..1adbef1 100644 --- a/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py +++ b/src/amdb/infrastructure/persistence/redis/task_queue/export_and_send_my_ratings.py @@ -23,4 +23,7 @@ def __call__( "export_format": export_format.value, "sending_method": sending_method.value, } - self._redis.lpush("tasks", json.dumps(data)) + self._redis.lpush( + "export_and_send_my_ratings_tasks", + json.dumps(data), + ) diff --git a/src/amdb/presentation/worker/router.py b/src/amdb/presentation/worker/router.py index f77340a..d3456b6 100644 --- a/src/amdb/presentation/worker/router.py +++ b/src/amdb/presentation/worker/router.py @@ -5,6 +5,9 @@ router = RedisRouter( handlers=[ - RedisRoute(export_and_send_my_ratings, list="tasks"), + RedisRoute( + export_and_send_my_ratings, + list="export_and_send_my_ratings_tasks", + ), ], ) From 59a0e248203f3aaf2c65400706635716824e7321 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sat, 9 Mar 2024 14:32:15 +0400 Subject: [PATCH 36/39] Update README --- README.md | 54 +++++++++++------------------------------------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index dde7f6d..c531511 100644 --- a/README.md +++ b/README.md @@ -19,46 +19,14 @@

-## How to run: - -### Using docker-compose: - -1. Provide `.env` file with variables from `.env.template` - -2. Run docker-compose - -```sh -docker-compose --env-file ./.env up web_api -``` - -### Manually: - -1. Install - -```sh -pip install -e ".[web_api,cli]" -``` - -2. Provide env variables from `.env.template` - -3. Run server - -```sh -amdb-web_api -``` - -4. Run cli - -```sh -amdb-cli -``` - -## How to run migrations: - -1. Provide env variables for postgres from `.env.template` - -2. Run migrations: - -``` -amdb-cli migration alembic upgrade head -``` +## Used technologies: + +* [Python 3](https://www.python.org/downloads/) + * [FastAPI](https://github.com/tiangolo/fastapi) - Web framework for building APIs + * [FastStream](https://github.com/airtai/faststream) - framework for building message queues + * [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) - Toolkit for building high level db integrations + * [alembic](https://github.com/sqlalchemy/alembic) - Tool for writing db migrations + * [redis-py](https://github.com/redis/redis-py) - Redis python client + * [Dishka](https://github.com/reagento/dishka) - DI framework +* [PostgreSQL](https://www.postgresql.org/) +* [Redis](https://redis.io/) From 1e7d68f288c65c00d50ab918ff8f5bb4da05084d Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sun, 10 Mar 2024 19:06:05 +0400 Subject: [PATCH 37/39] Add CLI --- README.md | 5 +- pyproject.toml | 4 +- src/amdb/main/cli/__init__.py | 0 src/amdb/main/cli/app.py | 92 ++++++++++++++++++++++++++ src/amdb/main/web_api/app.py | 8 +-- src/amdb/main/web_api/config.py | 20 ------ src/amdb/presentation/cli/__init__.py | 0 src/amdb/presentation/cli/movie.py | 95 +++++++++++++++++++++++++++ 8 files changed, 195 insertions(+), 29 deletions(-) create mode 100644 src/amdb/main/cli/__init__.py create mode 100644 src/amdb/main/cli/app.py delete mode 100644 src/amdb/main/web_api/config.py create mode 100644 src/amdb/presentation/cli/__init__.py create mode 100644 src/amdb/presentation/cli/movie.py diff --git a/README.md b/README.md index c531511..dfc65c1 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ ## Used technologies: * [Python 3](https://www.python.org/downloads/) - * [FastAPI](https://github.com/tiangolo/fastapi) - Web framework for building APIs - * [FastStream](https://github.com/airtai/faststream) - framework for building message queues + * [FastAPI](https://github.com/tiangolo/fastapi) - Framework for building WEB APIs + * [FastStream](https://github.com/airtai/faststream) - Framework for building message queues + * [Typer](https://github.com/tiangolo/typer) - Framework for buildig CLIs * [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy) - Toolkit for building high level db integrations * [alembic](https://github.com/sqlalchemy/alembic) - Tool for writing db migrations * [redis-py](https://github.com/redis/redis-py) - Redis python client diff --git a/pyproject.toml b/pyproject.toml index f91b680..c1c7052 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "alembic==1.13.*", "redis==5.0.*", "faststream[redis]==0.4.*", + "typer[all]==0.9.*", ] [project.optional-dependencies] @@ -52,5 +53,4 @@ coverage = [ ] [project.scripts] -amdb-web_api = "amdb.main.web_api.app:run_web_api" -amdb-worker = "amdb.main.worker.app:run_worker" +amdb = "amdb.main.cli.app:run_cli" diff --git a/src/amdb/main/cli/__init__.py b/src/amdb/main/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/main/cli/app.py b/src/amdb/main/cli/app.py new file mode 100644 index 0000000..02ba000 --- /dev/null +++ b/src/amdb/main/cli/app.py @@ -0,0 +1,92 @@ +import os +from typing import Annotated + +import typer +from dishka import make_container +from alembic import config + +from amdb.infrastructure.persistence.sqlalchemy.config import PostgresConfig +from amdb.infrastructure.persistence.redis.config import RedisConfig +from amdb.infrastructure.persistence.alembic.config import ALEMBIC_CONFIG +from amdb.presentation.cli.movie import movie_commands +from amdb.main.providers import ( + ConfigsProvider, + DomainValidatorsProvider, + DomainServicesProvider, + ConnectionsProvider, + EntityMappersProvider, + ViewModelMappersProvider, + ApplicationModelMappersProvider, + SendingAdaptersProvider, + TaskQueueAdaptersProvider, + ConvertingAdaptersProvider, + PasswordManagerProvider, + ApllicationServicesProvider, + CommandHandlersProvider, + CommandHandlerMakersProvider, + QueryHandlersProvider, + QueryHandlerMakersProvider, +) +from amdb.main.web_api.app import run_web_api +from amdb.main.worker.app import run_worker + + +def run_cli() -> None: + path_to_config = os.getenv("CONFIG_PATH") + if not path_to_config: + message = "Path to config env var is not set" + raise ValueError(message) + + postgres_config = PostgresConfig.from_toml(path_to_config) + redis_config = RedisConfig.from_toml(path_to_config) + + container = make_container( + ConfigsProvider( + postgres_config=postgres_config, + redis_config=redis_config, + ), + DomainValidatorsProvider(), + ConnectionsProvider(), + DomainServicesProvider(), + EntityMappersProvider(), + ViewModelMappersProvider(), + ApplicationModelMappersProvider(), + SendingAdaptersProvider(), + TaskQueueAdaptersProvider(), + PasswordManagerProvider(), + ConvertingAdaptersProvider(), + ApllicationServicesProvider(), + CommandHandlersProvider(), + CommandHandlerMakersProvider(), + QueryHandlersProvider(), + QueryHandlerMakersProvider(), + ) + + app = typer.Typer( + rich_markup_mode="rich", + context_settings={"obj": {"container": container}}, + ) + app.add_typer(movie_commands) + + @app.command() + def alembic(commands: Annotated[list[str], typer.Argument()]) -> None: + """ + [green]Run[/green] alembic. + """ + config.main(["-c", ALEMBIC_CONFIG, *commands]) + + @app.command() + def web_api() -> None: + """ + [green]Run[/green] web api. + """ + run_web_api() + + @app.command() + def worker() -> None: + """ + [green]Run[/green] worker. + """ + run_worker() + + app() diff --git a/src/amdb/main/web_api/app.py b/src/amdb/main/web_api/app.py index a1af0e2..8069cb7 100644 --- a/src/amdb/main/web_api/app.py +++ b/src/amdb/main/web_api/app.py @@ -31,7 +31,6 @@ QueryHandlerMakersProvider, ) from .session_adapters_provider import SessionAdaptersProvider -from .config import WebAPIConfig def run_web_api() -> None: @@ -40,14 +39,13 @@ def run_web_api() -> None: message = "Path to config env var is not set" raise ValueError(message) - web_api_config = WebAPIConfig.from_toml(path_to_config) postgres_config = PostgresConfig.from_toml(path_to_config) redis_config = RedisConfig.from_toml(path_to_config) session_config = SessionConfig.from_toml(path_to_config) app = FastAPI( title="Awesome Movie Database", - version=web_api_config.version, + version="0.5.0", swagger_ui_parameters={"defaultModelsExpandDepth": -1}, ) app.include_router(router) @@ -81,6 +79,6 @@ def run_web_api() -> None: uvicorn.run( app=app, - host=web_api_config.host, - port=web_api_config.port, + host="0.0.0.0", + port=8000, ) diff --git a/src/amdb/main/web_api/config.py b/src/amdb/main/web_api/config.py deleted file mode 100644 index 050409c..0000000 --- a/src/amdb/main/web_api/config.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass - -import toml - - -@dataclass(frozen=True, slots=True) -class WebAPIConfig: - version: str - host: str - port: int - - @classmethod - def from_toml(cls, path: str) -> "WebAPIConfig": - toml_as_dict = toml.load(path) - web_api_section_as_dict = toml_as_dict["web-api"] - return WebAPIConfig( - version=web_api_section_as_dict["version"], - host=web_api_section_as_dict["host"], - port=web_api_section_as_dict["port"], - ) diff --git a/src/amdb/presentation/cli/__init__.py b/src/amdb/presentation/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/amdb/presentation/cli/movie.py b/src/amdb/presentation/cli/movie.py new file mode 100644 index 0000000..d5d6600 --- /dev/null +++ b/src/amdb/presentation/cli/movie.py @@ -0,0 +1,95 @@ +from datetime import datetime +from typing import Annotated +from uuid import UUID + +import typer +import rich +from dishka import Container + +from amdb.domain.entities.movie import MovieId +from amdb.application.commands.create_movie import CreateMovieCommand +from amdb.application.commands.delete_movie import DeleteMovieCommand +from amdb.application.command_handlers.create_movie import CreateMovieHandler +from amdb.application.command_handlers.delete_movie import DeleteMovieHandler + + +movie_commands = typer.Typer( + name="movie", + help="[yellow]Manage[/yellow] movies", +) + + +@movie_commands.command() +def create( + ctx: typer.Context, + title: Annotated[ + str, + typer.Option("--title", "-t", help="Movie title."), + ], + release_date: Annotated[ + datetime, + typer.Option("--release_date", "-rd", help="Movie release date."), + ], + silently: Annotated[ + bool, + typer.Option("--silently", "-s", help="Do not print movie id."), + ] = False, +) -> None: + """ + [green]Create[/green] movie. + + If --silently is not used, will print movie id. + """ + container: Container = ctx.obj["container"] + + with container() as request_container: + handler = request_container.get(CreateMovieHandler) + command = CreateMovieCommand( + title=title, + release_date=release_date, + ) + movie_id = handler.execute(command) + + if not silently: + rich.print(movie_id) + + +@movie_commands.command() +def delete( + ctx: typer.Context, + movie_id: Annotated[ + UUID, + typer.Argument(help="Movie id."), + ], + force: Annotated[ + bool, + typer.Option("--force", "-f", help="Do not ask for confirmation."), + ] = False, + silently: Annotated[ + bool, + typer.Option("--silently", "-s", help="Do not print movie id"), + ] = False, +) -> None: + """ + [red]Delete[/red] movie. Also [red]deletes[/red] ratings and + reviews related to movie. + + If --force is not used, will ask for confirmation. + If --silently is not used, will print movie id. + """ + if not force: + typer.confirm( + text="Are you sure you want to delete movie?", + default=True, + abort=True, + ) + + container: Container = ctx.obj["container"] + + with container() as request_container: + handler = request_container.get(DeleteMovieHandler) + command = DeleteMovieCommand(movie_id=MovieId(movie_id)) + handler.execute(command) + + if not silently: + rich.print(movie_id) From 46642932aab273b19087ce886884f28b05d7915a Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sun, 10 Mar 2024 20:21:58 +0400 Subject: [PATCH 38/39] Refactor Dockerfile, docker-compose.yaml, config and README --- Dockerfile | 12 ++++++-- README.md | 51 ++++++++++++++++++++++++++++++++ config/prod_config.template.toml | 5 ---- docker-compose.yaml | 33 ++++++++++++++------- 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7939ab0..d198a54 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,14 @@ FROM base AS web_api COPY --from=builder ./app/dist ./ -RUN $(printf "pip install %s[web_api,cli]" amdb*.whl) +RUN $(printf "pip install %s[web_api]" amdb*.whl) -CMD ["amdb-web_api"] +CMD ["amdb", "web-api"] + +FROM base AS worker + +COPY --from=builder ./app/dist ./ + +RUN pip install amdb*.whl + +CMD ["amdb", "worker"] diff --git a/README.md b/README.md index dfc65c1..21565d4 100644 --- a/README.md +++ b/README.md @@ -31,3 +31,54 @@ * [Dishka](https://github.com/reagento/dishka) - DI framework * [PostgreSQL](https://www.postgresql.org/) * [Redis](https://redis.io/) + + +## How to run: + +### Manually: + +1. Install + +```sh +pip install -e ".[web_api]" +``` + +2. Create [config](./config/prod_config.template.toml) file + +3. Provide `CONFIG_PATH` env variable + +4. Run migrations + +```sh +amdb alembic upgrade head +``` + +5. Run worker + +```sh +amdb worker +``` + +6. Run server + +```sh +amdb web_api +``` + +### Using docker-compose: + +1. Create [config](./config/prod_config.template.toml) file + +2. Provide `CONFIG_PATH`, `REDIS_PASSWORD`, `REDIS_PORT_NUMBER`, `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB`, `SERVER_HOST`, `SERVER_PORT` env variables + +3. Run worker and server + +```sh +docker-compose up web_api +``` + +4. Run migrations + +```sh +docker exec amdb_backend.web_api amdb alembic upgrade head +``` diff --git a/config/prod_config.template.toml b/config/prod_config.template.toml index bc037b7..26b0e4c 100644 --- a/config/prod_config.template.toml +++ b/config/prod_config.template.toml @@ -6,8 +6,3 @@ url = "redis://:1234@127.0.0.1:6379/0" [auth-session] lifetime = 3600 # Minutes - -[web-api] -version = "0.5.0" -host = "127.0.0.1" -port = 8000 diff --git a/docker-compose.yaml b/docker-compose.yaml index a12f994..dc2ead2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,10 +2,10 @@ version: "3" services: web_api: - profiles: [web_api] + profiles: [web_api, worker] container_name: amdb_backend.web_api - depends_on: [postgres, redis] - ports: ["${UVICORN_HOST}:${UVICORN_PORT}:${UVICORN_PORT}"] + depends_on: [postgres, redis, worker] + ports: ["${SERVER_HOST:-0.0.0.0}:${SERVER_PORT:-8000}:8000"] restart: unless-stopped build: @@ -15,23 +15,36 @@ services: environment: - CONFIG_PATH + worker: + profiles: [web_api, worker] + container_name: amdb_backend.worker + depends_on: [postgres, redis] + restart: unless-stopped + + build: + context: ./ + dockerfile: ./Dockerfile + target: worker + environment: + - CONFIG_PATH + postgres: - profiles: [web_api] + profiles: [web_api, worker] container_name: amdb_backend.postgres image: postgres:15-alpine restart: unless-stopped environment: - - POSTGRES_USER - - POSTGRES_PASSWORD - - POSTGRES_DB + - POSTGRES_USER=${POSTGRES_USER:-postgres} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-1234} + - POSTGRES_DB=${POSTGRES_DB:-amdb} redis: - profiles: [web_api] + profiles: [web_api, worker] container_name: amdb_backend.redis image: bitnami/redis:7.2 restart: unless-stopped environment: - - REDIS_PASSWORD - - REDIS_PORT_NUMBER=${REDIS_PORT} + - REDIS_PASSWORD=${REDIS_PASSWORD:-1234} + - REDIS_PORT_NUMBER=${REDIS_PORT:-6379} From 7c0cf89a7590991d4feaa1f552f4be5acd93a773 Mon Sep 17 00:00:00 2001 From: Madnoberson Date: Sun, 10 Mar 2024 20:45:35 +0400 Subject: [PATCH 39/39] Update CHANGELOG --- docs/CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 64b5825..f4dc7ad 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,35 @@ # Changelog +## [v1.0.0](https://github.com/Awesome-Movie-Database/amdb-backend/releases/tag/v1.0.0) (2024-03-10) + +### Added + +- `User` now can list `detailed reviews` +- `User` now can get `detailed movie` +- `User` now can list his `detailed ratings` +- `User` now can get `non detailed movie` +- `User` now can export his `ratings` in CSV format +- `User` now can request export his `ratings` in CSV format + +### Changed + +- Now `Create movie` and `Delete movie` don't require permissions +- [*Breaking change*] Now `Review` type is a string +- [*Breaking change*] Removed ability to list `Movies` +- [*Breaking change*] Removed ability to list `Ratings` +- [*Breaking change*] Removed ability to get `Movie` +- [*Breaking change*] Removed ability to get `Review` + +### Fixed + +- Race condition during rating `Movie` by many `Users` at the same time + +### Echancements + +- Added permissions table +- Now you can run server and worker using CLI + + ## [v0.5.0](https://github.com/Awesome-Movie-Database/amdb-backend/releases/tag/v0.5.0) (2024-01-29) ### Added