From 01fa38cd9fff1da396ba467a652fe2e80ec09231 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 17:55:53 +0300 Subject: [PATCH 01/12] config to connect to REDIS_HOST from env --- movie_catalog/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movie_catalog/config.py b/movie_catalog/config.py index 30fcb51..7ccad84 100644 --- a/movie_catalog/config.py +++ b/movie_catalog/config.py @@ -7,7 +7,7 @@ LOG_FORMAT = "[-] %(asctime)s [%(levelname)s] %(module)s-%(lineno)d - %(message)s" LOG_LEVEL = logging.DEBUG -REDIS_HOST = "localhost" +REDIS_HOST = getenv("REDIS_HOST", "localhost") REDIS_PORT = int(getenv("REDIS_PORT", 0)) or 6379 REDIS_DB = 0 REDIS_DB_TOKENS = 1 From a8f7a6dd6bed111123f7c7f161d9831b46910661 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 18:02:57 +0300 Subject: [PATCH 02/12] run redis service with project tests --- .github/workflows/python-check.yml | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index 4886b30..7344059 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -33,3 +33,37 @@ jobs: - name: Run mypy run: uv mypy movie-catalog + + run-tests: + runs-on: ubuntu-latest + needs: + - run-checks + services: + redis: + image: redis + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version-file: ".python-version" + + - name: Setup UV + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv sync --locked --all-extras --dev + + - name: Run python tests + run: uv run pytest + env: + TESTING: 1 + REDIS_PORT: 6379 From 0b54246d55362e41fa39a8eb9258d7c4a04c7442 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:14:39 +0300 Subject: [PATCH 03/12] fix ruff checks --- .../auth/services/redis_tokens_helper.py | 2 +- .../auth/services/redis_users_helper.py | 3 +- .../api/api_v1/movie_catalog/crud.py | 5 ++-- .../api/api_v1/movie_catalog/dependencies.py | 5 ++-- .../movie_catalog/views/details_view.py | 2 +- .../api_v1/movie_catalog/views/list_view.py | 2 +- movie_catalog/commands/tokens.py | 5 ++-- movie_catalog/main.py | 5 ++-- movie_catalog/stuff.py | 3 +- movie_catalog/tests/conftest.py | 5 ++-- movie_catalog/tests/test_api/conftest.py | 5 +--- .../test_movie_catalog/test_crud.py | 10 +++++-- .../test_views/test_details_veiew.py | 29 ++++++++++++------- .../test_views/test_list_view.py | 21 +++++++++----- movie_catalog/tests/test_api/test_views.py | 3 +- .../tests/test_schemas/test_movie_catalog.py | 3 +- pyproject.toml | 3 +- 17 files changed, 62 insertions(+), 49 deletions(-) diff --git a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py index 9e6ed1e..58ddee4 100644 --- a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py +++ b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py @@ -1,9 +1,9 @@ __all__ = ["redis_tokens"] +from config import REDIS_DB_TOKENS, REDIS_HOST, REDIS_PORT, REDIS_TOKENS_SET_NAME from redis import Redis from api.api_v1.auth.services.tokens_helper import AbstractTokensHelper -from config import REDIS_DB_TOKENS, REDIS_HOST, REDIS_PORT, REDIS_TOKENS_SET_NAME class RedisTokensHelper(AbstractTokensHelper): diff --git a/movie_catalog/api/api_v1/auth/services/redis_users_helper.py b/movie_catalog/api/api_v1/auth/services/redis_users_helper.py index a6beae7..7ef7130 100644 --- a/movie_catalog/api/api_v1/auth/services/redis_users_helper.py +++ b/movie_catalog/api/api_v1/auth/services/redis_users_helper.py @@ -1,8 +1,7 @@ __all__ = ["redis_users"] -from redis import Redis - from config import REDIS_DB_USERS, REDIS_HOST, REDIS_PORT +from redis import Redis from .users_helper import AbstractUsersHelper diff --git a/movie_catalog/api/api_v1/movie_catalog/crud.py b/movie_catalog/api/api_v1/movie_catalog/crud.py index 97f2898..cc3d3f5 100644 --- a/movie_catalog/api/api_v1/movie_catalog/crud.py +++ b/movie_catalog/api/api_v1/movie_catalog/crud.py @@ -1,15 +1,14 @@ import logging from typing import cast -from pydantic import BaseModel -from redis import Redis - from config import ( REDIS_DB_MOVIE_CATALOG, REDIS_HOST, REDIS_MOVIE_CATALOG_HASH_NAME, REDIS_PORT, ) +from pydantic import BaseModel +from redis import Redis from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate logger = logging.getLogger(__name__) diff --git a/movie_catalog/api/api_v1/movie_catalog/dependencies.py b/movie_catalog/api/api_v1/movie_catalog/dependencies.py index e512154..744e019 100644 --- a/movie_catalog/api/api_v1/movie_catalog/dependencies.py +++ b/movie_catalog/api/api_v1/movie_catalog/dependencies.py @@ -8,7 +8,6 @@ HTTPBasicCredentials, HTTPBearer, ) - from schemas.movie_catalog import Movie from ..auth.services import redis_tokens, redis_users @@ -87,7 +86,9 @@ def validate_basic_auth(credentials: HTTPBasicCredentials | None) -> None: def user_basic_auth_required( - credentials: Annotated[HTTPBasicCredentials | None, Depends(user_basic_auth)] = None + credentials: Annotated[ + HTTPBasicCredentials | None, Depends(user_basic_auth) + ] = None, ) -> None: validate_basic_auth(credentials=credentials) diff --git a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py index 6e5e481..d8e1810 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py @@ -1,13 +1,13 @@ from typing import Annotated from fastapi import APIRouter, Depends, status +from schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate from api.api_v1.movie_catalog.crud import storage from api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, prefetch_film, ) -from schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate router = APIRouter( prefix="/{slug}", diff --git a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py index d6c969d..3097f71 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py @@ -1,11 +1,11 @@ __all__ = ("router",) from fastapi import APIRouter, Depends, HTTPException, status +from schemas.movie_catalog import Movie, MovieCreate, MovieRead from api.api_v1.movie_catalog.crud import MovieCatalogAlreadyExists, storage from api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, ) -from schemas.movie_catalog import Movie, MovieCreate, MovieRead router: APIRouter = APIRouter( prefix="/movies", diff --git a/movie_catalog/commands/tokens.py b/movie_catalog/commands/tokens.py index 47f931a..d801556 100644 --- a/movie_catalog/commands/tokens.py +++ b/movie_catalog/commands/tokens.py @@ -2,11 +2,10 @@ from typing import Annotated import typer +from api.api_v1.auth.services import redis_tokens from rich import print from rich.markdown import Markdown -from api.api_v1.auth.services import redis_tokens - app = typer.Typer( name="token", help="Tokens management", @@ -52,7 +51,7 @@ def create_token() -> None: @app.command(name="delete") def delete_token( - token: Annotated[str, typer.Argument(help="The token to delete")] + token: Annotated[str, typer.Argument(help="The token to delete")], ) -> None: """ Delete a token. diff --git a/movie_catalog/main.py b/movie_catalog/main.py index 702cb14..8c454d8 100644 --- a/movie_catalog/main.py +++ b/movie_catalog/main.py @@ -1,11 +1,10 @@ import logging -from fastapi import FastAPI - import config -from api.main_view import router as main_router from api import router as api_router +from api.main_view import router as main_router from app_lifespan import lifespan +from fastapi import FastAPI logging.basicConfig( format=config.LOG_FORMAT, diff --git a/movie_catalog/stuff.py b/movie_catalog/stuff.py index 6e68d32..76c249c 100644 --- a/movie_catalog/stuff.py +++ b/movie_catalog/stuff.py @@ -1,6 +1,5 @@ -from redis import Redis - from config import REDIS_DB, REDIS_HOST, REDIS_PORT +from redis import Redis redis = Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, decode_responses=True) diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index d430ed3..a2d5312 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -5,7 +5,6 @@ from typing import Generator import pytest - from api.api_v1.movie_catalog.crud import storage from schemas.movie_catalog import Movie, MovieCreate @@ -19,7 +18,7 @@ def check_testing_env() -> None: @pytest.fixture(autouse=True) -def disable_logging(): +def disable_logging() -> None: logging.getLogger().setLevel(logging.CRITICAL) @@ -43,7 +42,7 @@ def build_movie_create_random_slug( ) -> MovieCreate: return MovieCreate( slug="".join( - random.choices( + random.choices( # noqa: S311 Standard pseudo-random generators are not suitable for cryptographic purposes string.ascii_letters, k=10, ), diff --git a/movie_catalog/tests/test_api/conftest.py b/movie_catalog/tests/test_api/conftest.py index 2281e77..8e998ac 100644 --- a/movie_catalog/tests/test_api/conftest.py +++ b/movie_catalog/tests/test_api/conftest.py @@ -1,9 +1,8 @@ from collections.abc import Generator import pytest -from fastapi.testclient import TestClient - from api.api_v1.auth.services import redis_tokens +from fastapi.testclient import TestClient from movie_catalog.main import app @@ -25,5 +24,3 @@ def auth_client(auth_token: str) -> Generator[TestClient]: headers = {"Authorization": f"Bearer {auth_token}"} with TestClient(app, headers=headers) as client: yield client - - diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py index 238d40e..6552690 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py @@ -3,14 +3,18 @@ from unittest import TestCase from api.api_v1.movie_catalog.crud import storage -from schemas.movie_catalog import Movie, MovieCreate, MovieUpdate, MoviePartialUpdate +from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate def create_movie() -> Movie: import random movie_in = MovieCreate( - slug="".join(random.choices(string.ascii_letters + string.digits, k=8)), + slug="".join( + random.choices( # noqa: S311 Standard pseudo-random generators are not suitable for cryptographic purposes + string.ascii_letters + string.digits, k=8 + ) + ), title="Some title", description="Some description for unit-test", year_released=1901, @@ -77,6 +81,6 @@ def test_get_by_slug(self) -> None: self.assertEqual(movie, db_movie) @classmethod - def tearDownClass(cls): + def tearDownClass(cls) -> None: for movie in cls.movies: storage.delete(movie) diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py index 3530fe0..e61319b 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py @@ -1,16 +1,18 @@ from typing import Generator import pytest -from fastapi import status - +from _pytest.fixtures import SubRequest from api.api_v1.movie_catalog.crud import storage +from fastapi import status +from fastapi.testclient import TestClient from main import app from schemas.movie_catalog import ( - Movie, - DESCRIPTION_MIN_LENGTH, DESCRIPTION_MAX_LENGTH, + DESCRIPTION_MIN_LENGTH, + Movie, MovieUpdate, ) + from tests.conftest import create_movie, create_movie_random_slug pytestmark = pytest.mark.apitest @@ -27,12 +29,12 @@ class TestDelete: ), ] ) - def movie(self, request) -> Generator[Movie, None, None]: + def movie(self, request: SubRequest) -> Generator[Movie, None, None]: movie = create_movie(slug=request.param) yield movie storage.delete(movie) - def test_delete_movie(self, movie, auth_client) -> None: + def test_delete_movie(self, movie: Movie, auth_client: TestClient) -> None: url = app.url_path_for("delete_movie", slug=movie.slug) response = auth_client.delete(url) assert response.status_code == status.HTTP_204_NO_CONTENT, response.text @@ -41,7 +43,7 @@ def test_delete_movie(self, movie, auth_client) -> None: class TestPartialUpdate: @pytest.fixture() - def movie(self, request) -> Generator[Movie, None, None]: + def movie(self, request: SubRequest) -> Generator[Movie, None, None]: movie = create_movie_random_slug(description=request.param) yield movie storage.delete(movie) @@ -64,7 +66,10 @@ def movie(self, request) -> Generator[Movie, None, None]: indirect=["movie"], ) def test_update_movie_details_partial( - self, movie, auth_client, new_description + self, + movie: Movie, + auth_client: TestClient, + new_description: str, ) -> None: url = app.url_path_for("partial_update_movie", slug=movie.slug) movie_before_update = storage.get_by_slug(movie.slug) @@ -77,7 +82,7 @@ def test_update_movie_details_partial( class TestUpdate: @pytest.fixture() - def movie(self, request) -> Generator[Movie, None, None]: + def movie(self, request: SubRequest) -> Generator[Movie, None, None]: description, title = request.param movie = create_movie_random_slug(description=description, title=title) yield movie @@ -111,7 +116,11 @@ def movie(self, request) -> Generator[Movie, None, None]: indirect=["movie"], ) def test_update_movie_details( - self, movie, auth_client, new_description, new_title + self, + movie: Movie, + auth_client: TestClient, + new_description: str, + new_title: str, ) -> None: url = app.url_path_for("update_movie", slug=movie.slug) movie_before_update = storage.get_by_slug(movie.slug) diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py index 0fc4168..6e3d484 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py @@ -4,25 +4,28 @@ from typing import Any import pytest +from _pytest.fixtures import SubRequest from fastapi import status from fastapi.testclient import TestClient - from movie_catalog.main import app -from schemas.movie_catalog import MovieCreate, Movie +from schemas.movie_catalog import Movie, MovieCreate + from tests.conftest import build_movie_create_random_slug pytestmark = pytest.mark.apitest class TestCreate: - def test_create_movie(self, caplog, auth_client: TestClient): + def test_create_movie( + self, caplog: pytest.LogCaptureFixture, auth_client: TestClient + ) -> None: caplog.set_level(logging.DEBUG) url = app.url_path_for("add_movie") movie_create = MovieCreate( slug="".join( - random.choices( + random.choices( # noqa: S311 Standard pseudo-random generators are not suitable for cryptographic purposes string.ascii_letters, k=10, ), @@ -39,7 +42,9 @@ def test_create_movie(self, caplog, auth_client: TestClient): assert received_data == movie_create, received_data assert f"Add movie <{received_data.slug}> to catalog" in caplog.text - def test_create_movie_already_exists(self, auth_client: TestClient, movie: Movie): + def test_create_movie_already_exists( + self, auth_client: TestClient, movie: Movie + ) -> None: url = app.url_path_for("add_movie") movie_create = MovieCreate(**movie.model_dump()) data = movie_create.model_dump(mode="json") @@ -63,14 +68,16 @@ class TestCreateInvalid: ), ] ) - def movie_create_values(self, request) -> tuple[dict[str, Any], str]: + def movie_create_values(self, request: SubRequest) -> tuple[dict[str, Any], str]: build = build_movie_create_random_slug() data = build.model_dump(mode="json") slug, err_type = request.param data["slug"] = slug return data, err_type - def test_invalid_slug(self, auth_client, movie_create_values): + def test_invalid_slug( + self, auth_client: TestClient, movie_create_values: tuple[dict[str, Any], str] + ) -> None: url = app.url_path_for("add_movie") created_data, expected_error_type = movie_create_values response = auth_client.post(url, json=created_data) diff --git a/movie_catalog/tests/test_api/test_views.py b/movie_catalog/tests/test_api/test_views.py index 9c5a094..c794201 100644 --- a/movie_catalog/tests/test_api/test_views.py +++ b/movie_catalog/tests/test_api/test_views.py @@ -1,8 +1,9 @@ import pytest +from fastapi.testclient import TestClient @pytest.mark.apitest -def test_root_view(client): +def test_root_view(client: TestClient) -> None: response = client.get("/") assert response.status_code == 200 assert response.json() == { diff --git a/movie_catalog/tests/test_schemas/test_movie_catalog.py b/movie_catalog/tests/test_schemas/test_movie_catalog.py index c639365..3ebc4aa 100644 --- a/movie_catalog/tests/test_schemas/test_movie_catalog.py +++ b/movie_catalog/tests/test_schemas/test_movie_catalog.py @@ -1,8 +1,7 @@ from unittest import TestCase from pydantic import ValidationError - -from schemas.movie_catalog import MovieCreate, MovieUpdate, MoviePartialUpdate, Movie +from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate class MovieCreateTestCase(TestCase): diff --git a/pyproject.toml b/pyproject.toml index be4e5dc..63c3930 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,7 +88,7 @@ select = [ "F401", # Pyflakes (F) "S", # flake8-bandit (S) "LOG", # flake8-logging (LOG) - "PT", # flake8-pytest-style (PT) + # "PT", # flake8-pytest-style (PT) # todo: remove after delete unittest style tests "RET", # flake8-return (RET) "ARG", # flake8-unused-arguments (ARG) "T20", # flake8-print (T20) @@ -106,3 +106,4 @@ unfixable = [] "stuff.py" = [ "T20", ] +"movie_catalog/tests/*" = ["S101"] From de0b9608adc7010479897ff878b347c26301c72b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:33:03 +0300 Subject: [PATCH 04/12] fix mypy errors --- .../api/api_v1/auth/services/redis_tokens_helper.py | 2 +- movie_catalog/api/api_v1/movie_catalog/views/list_view.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py index 58ddee4..4f738fb 100644 --- a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py +++ b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py @@ -3,7 +3,7 @@ from config import REDIS_DB_TOKENS, REDIS_HOST, REDIS_PORT, REDIS_TOKENS_SET_NAME from redis import Redis -from api.api_v1.auth.services.tokens_helper import AbstractTokensHelper +from movie_catalog.api.api_v1.auth.services.tokens_helper import AbstractTokensHelper class RedisTokensHelper(AbstractTokensHelper): diff --git a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py index 3097f71..291ff3e 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py @@ -1,11 +1,12 @@ __all__ = ("router",) -from fastapi import APIRouter, Depends, HTTPException, status -from schemas.movie_catalog import Movie, MovieCreate, MovieRead +from typing import cast from api.api_v1.movie_catalog.crud import MovieCatalogAlreadyExists, storage from api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, ) +from fastapi import APIRouter, Depends, HTTPException, status +from schemas.movie_catalog import Movie, MovieCreate, MovieRead router: APIRouter = APIRouter( prefix="/movies", @@ -18,7 +19,7 @@ response_model=list[MovieRead], ) def get_movie_list() -> list[Movie]: - return storage.get() + return cast(list[Movie], storage.get()) @router.post( From d74b081342fb3f8300acea9d93f6fe5db5bce325 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:37:04 +0300 Subject: [PATCH 05/12] fix ruff errors --- movie_catalog/__init__.py | 0 movie_catalog/api/api_v1/movie_catalog/views/details_view.py | 5 ++--- movie_catalog/tests/test_api/conftest.py | 1 + .../test_movie_catalog/test_views/test_details_veiew.py | 1 - .../test_movie_catalog/test_views/test_list_view.py | 4 ++-- 5 files changed, 5 insertions(+), 6 deletions(-) create mode 100644 movie_catalog/__init__.py diff --git a/movie_catalog/__init__.py b/movie_catalog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py index d8e1810..276433c 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py @@ -1,13 +1,12 @@ from typing import Annotated -from fastapi import APIRouter, Depends, status -from schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate - from api.api_v1.movie_catalog.crud import storage from api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, prefetch_film, ) +from fastapi import APIRouter, Depends, status +from schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate router = APIRouter( prefix="/{slug}", diff --git a/movie_catalog/tests/test_api/conftest.py b/movie_catalog/tests/test_api/conftest.py index 8e998ac..9aaf616 100644 --- a/movie_catalog/tests/test_api/conftest.py +++ b/movie_catalog/tests/test_api/conftest.py @@ -3,6 +3,7 @@ import pytest from api.api_v1.auth.services import redis_tokens from fastapi.testclient import TestClient + from movie_catalog.main import app diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py index e61319b..f50df4c 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py @@ -12,7 +12,6 @@ Movie, MovieUpdate, ) - from tests.conftest import create_movie, create_movie_random_slug pytestmark = pytest.mark.apitest diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py index 6e3d484..597fe63 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py @@ -7,11 +7,11 @@ from _pytest.fixtures import SubRequest from fastapi import status from fastapi.testclient import TestClient -from movie_catalog.main import app from schemas.movie_catalog import Movie, MovieCreate - from tests.conftest import build_movie_create_random_slug +from movie_catalog.main import app + pytestmark = pytest.mark.apitest From 802587619b5d19c7b27180cbf25ea409f276a54b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:39:09 +0300 Subject: [PATCH 06/12] fix ci command for mypy --- .github/workflows/python-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index 7344059..d71807a 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -32,7 +32,7 @@ jobs: version-file: 'pyproject.toml' - name: Run mypy - run: uv mypy movie-catalog + run: uv run mypy movie-catalog run-tests: runs-on: ubuntu-latest From e3dab763d438b326494728c766726ada7cd7aaca Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:40:11 +0300 Subject: [PATCH 07/12] fix ci command for mypy x2 --- .github/workflows/python-check.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-check.yml b/.github/workflows/python-check.yml index d71807a..242abfa 100644 --- a/.github/workflows/python-check.yml +++ b/.github/workflows/python-check.yml @@ -32,7 +32,7 @@ jobs: version-file: 'pyproject.toml' - name: Run mypy - run: uv run mypy movie-catalog + run: uv run mypy movie_catalog run-tests: runs-on: ubuntu-latest From 2d26c4620de25fb67e168207d94ecbca31618393 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:42:51 +0300 Subject: [PATCH 08/12] fix path to storage at conftest.py --- movie_catalog/tests/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index a2d5312..f20903c 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -5,9 +5,10 @@ from typing import Generator import pytest -from api.api_v1.movie_catalog.crud import storage from schemas.movie_catalog import Movie, MovieCreate +from movie_catalog.api.api_v1.movie_catalog.crud import storage + @pytest.fixture(scope="session", autouse=True) def check_testing_env() -> None: From e0f5c8800bd80b35a4481e337fb3acfcedab19a2 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:44:48 +0300 Subject: [PATCH 09/12] fix path to storage at conftest.py x2 --- movie_catalog/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index f20903c..7f56180 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -5,9 +5,9 @@ from typing import Generator import pytest -from schemas.movie_catalog import Movie, MovieCreate from movie_catalog.api.api_v1.movie_catalog.crud import storage +from movie_catalog.schemas.movie_catalog import Movie, MovieCreate @pytest.fixture(scope="session", autouse=True) From 5a5af200dee1528dc2673ae4125c808e27a01cc0 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:46:43 +0300 Subject: [PATCH 10/12] ignore redis-py types --- movie_catalog/tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index 7f56180..bddc1ac 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -60,14 +60,14 @@ def create_movie( description: str = "Some description for unit-test", title: str = "Some title", ) -> Movie: - return storage.create(build_movie_create(slug, description, title)) + return storage.create(build_movie_create(slug, description, title)) # type: ignore def create_movie_random_slug( description: str = "Some description for unit-test", title: str = "Some title", ) -> Movie: - return storage.create(build_movie_create_random_slug(description, title)) + return storage.create(build_movie_create_random_slug(description, title)) # type: ignore @pytest.fixture() From 317eb977fbc9436729be3b0558c6081972609a9f Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 19:53:52 +0300 Subject: [PATCH 11/12] fix imports --- .../api_v1/auth/services/redis_tokens_helper.py | 7 ++++++- .../api/api_v1/auth/services/redis_users_helper.py | 3 ++- movie_catalog/api/api_v1/movie_catalog/crud.py | 14 ++++++++++---- .../api/api_v1/movie_catalog/dependencies.py | 3 ++- .../api/api_v1/movie_catalog/views/details_view.py | 14 ++++++++++---- .../api/api_v1/movie_catalog/views/list_view.py | 12 ++++++++---- movie_catalog/main.py | 9 +++++---- movie_catalog/tests/test_api/conftest.py | 2 +- .../test_services/test_redis_tokens_helper.py | 2 +- .../test_api_v1/test_movie_catalog/test_crud.py | 9 +++++++-- .../test_movie_catalog/test_dependencies.py | 2 +- .../test_views/test_details_veiew.py | 9 +++++---- .../test_views/test_list_view.py | 4 ++-- .../tests/test_schemas/test_movie_catalog.py | 8 +++++++- 14 files changed, 67 insertions(+), 31 deletions(-) diff --git a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py index 4f738fb..890bad4 100644 --- a/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py +++ b/movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py @@ -1,9 +1,14 @@ __all__ = ["redis_tokens"] -from config import REDIS_DB_TOKENS, REDIS_HOST, REDIS_PORT, REDIS_TOKENS_SET_NAME from redis import Redis from movie_catalog.api.api_v1.auth.services.tokens_helper import AbstractTokensHelper +from movie_catalog.config import ( + REDIS_DB_TOKENS, + REDIS_HOST, + REDIS_PORT, + REDIS_TOKENS_SET_NAME, +) class RedisTokensHelper(AbstractTokensHelper): diff --git a/movie_catalog/api/api_v1/auth/services/redis_users_helper.py b/movie_catalog/api/api_v1/auth/services/redis_users_helper.py index 7ef7130..79667c0 100644 --- a/movie_catalog/api/api_v1/auth/services/redis_users_helper.py +++ b/movie_catalog/api/api_v1/auth/services/redis_users_helper.py @@ -1,8 +1,9 @@ __all__ = ["redis_users"] -from config import REDIS_DB_USERS, REDIS_HOST, REDIS_PORT from redis import Redis +from movie_catalog.config import REDIS_DB_USERS, REDIS_HOST, REDIS_PORT + from .users_helper import AbstractUsersHelper diff --git a/movie_catalog/api/api_v1/movie_catalog/crud.py b/movie_catalog/api/api_v1/movie_catalog/crud.py index cc3d3f5..5e49f8f 100644 --- a/movie_catalog/api/api_v1/movie_catalog/crud.py +++ b/movie_catalog/api/api_v1/movie_catalog/crud.py @@ -1,15 +1,21 @@ import logging from typing import cast -from config import ( +from pydantic import BaseModel +from redis import Redis + +from movie_catalog.config import ( REDIS_DB_MOVIE_CATALOG, REDIS_HOST, REDIS_MOVIE_CATALOG_HASH_NAME, REDIS_PORT, ) -from pydantic import BaseModel -from redis import Redis -from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate +from movie_catalog.schemas.movie_catalog import ( + Movie, + MovieCreate, + MoviePartialUpdate, + MovieUpdate, +) logger = logging.getLogger(__name__) redis = Redis( diff --git a/movie_catalog/api/api_v1/movie_catalog/dependencies.py b/movie_catalog/api/api_v1/movie_catalog/dependencies.py index 744e019..9005412 100644 --- a/movie_catalog/api/api_v1/movie_catalog/dependencies.py +++ b/movie_catalog/api/api_v1/movie_catalog/dependencies.py @@ -8,7 +8,8 @@ HTTPBasicCredentials, HTTPBearer, ) -from schemas.movie_catalog import Movie + +from movie_catalog.schemas.movie_catalog import Movie from ..auth.services import redis_tokens, redis_users from .crud import storage diff --git a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py index 276433c..acfe879 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/details_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/details_view.py @@ -1,12 +1,18 @@ from typing import Annotated -from api.api_v1.movie_catalog.crud import storage -from api.api_v1.movie_catalog.dependencies import ( +from fastapi import APIRouter, Depends, status + +from movie_catalog.api.api_v1.movie_catalog.crud import storage +from movie_catalog.api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, prefetch_film, ) -from fastapi import APIRouter, Depends, status -from schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate +from movie_catalog.schemas.movie_catalog import ( + Movie, + MoviePartialUpdate, + MovieRead, + MovieUpdate, +) router = APIRouter( prefix="/{slug}", diff --git a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py index 291ff3e..b87adaa 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py @@ -1,12 +1,16 @@ __all__ = ("router",) from typing import cast -from api.api_v1.movie_catalog.crud import MovieCatalogAlreadyExists, storage -from api.api_v1.movie_catalog.dependencies import ( +from fastapi import APIRouter, Depends, HTTPException, status + +from movie_catalog.api.api_v1.movie_catalog.crud import ( + MovieCatalogAlreadyExists, + storage, +) +from movie_catalog.api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, ) -from fastapi import APIRouter, Depends, HTTPException, status -from schemas.movie_catalog import Movie, MovieCreate, MovieRead +from movie_catalog.schemas.movie_catalog import Movie, MovieCreate, MovieRead router: APIRouter = APIRouter( prefix="/movies", diff --git a/movie_catalog/main.py b/movie_catalog/main.py index 8c454d8..3261cc5 100644 --- a/movie_catalog/main.py +++ b/movie_catalog/main.py @@ -1,11 +1,12 @@ import logging -import config -from api import router as api_router -from api.main_view import router as main_router -from app_lifespan import lifespan from fastapi import FastAPI +from movie_catalog import config +from movie_catalog.api import router as api_router +from movie_catalog.api.main_view import router as main_router +from movie_catalog.app_lifespan import lifespan + logging.basicConfig( format=config.LOG_FORMAT, level=config.LOG_LEVEL, diff --git a/movie_catalog/tests/test_api/conftest.py b/movie_catalog/tests/test_api/conftest.py index 9aaf616..42cff1a 100644 --- a/movie_catalog/tests/test_api/conftest.py +++ b/movie_catalog/tests/test_api/conftest.py @@ -1,9 +1,9 @@ from collections.abc import Generator import pytest -from api.api_v1.auth.services import redis_tokens from fastapi.testclient import TestClient +from movie_catalog.api.api_v1.auth.services import redis_tokens from movie_catalog.main import app diff --git a/movie_catalog/tests/test_api/test_api_v1/test_auth/test_services/test_redis_tokens_helper.py b/movie_catalog/tests/test_api/test_api_v1/test_auth/test_services/test_redis_tokens_helper.py index 5fad99a..01c5baf 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_auth/test_services/test_redis_tokens_helper.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_auth/test_services/test_redis_tokens_helper.py @@ -1,6 +1,6 @@ from unittest import TestCase -from api.api_v1.auth.services import redis_tokens +from movie_catalog.api.api_v1.auth.services import redis_tokens class RedisTokensHelperTestCase(TestCase): diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py index 6552690..16c5e71 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_crud.py @@ -2,8 +2,13 @@ from typing import ClassVar from unittest import TestCase -from api.api_v1.movie_catalog.crud import storage -from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate +from movie_catalog.api.api_v1.movie_catalog.crud import storage +from movie_catalog.schemas.movie_catalog import ( + Movie, + MovieCreate, + MoviePartialUpdate, + MovieUpdate, +) def create_movie() -> Movie: diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_dependencies.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_dependencies.py index b4d0fb7..6bbfb72 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_dependencies.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_dependencies.py @@ -1,4 +1,4 @@ -from api.api_v1.movie_catalog.dependencies import UNSAFE_METHOD +from movie_catalog.api.api_v1.movie_catalog.dependencies import UNSAFE_METHOD class TestUnsafeMethods: diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py index f50df4c..206ef48 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py @@ -2,17 +2,18 @@ import pytest from _pytest.fixtures import SubRequest -from api.api_v1.movie_catalog.crud import storage from fastapi import status from fastapi.testclient import TestClient -from main import app -from schemas.movie_catalog import ( + +from movie_catalog.api.api_v1.movie_catalog.crud import storage +from movie_catalog.main import app +from movie_catalog.schemas.movie_catalog import ( DESCRIPTION_MAX_LENGTH, DESCRIPTION_MIN_LENGTH, Movie, MovieUpdate, ) -from tests.conftest import create_movie, create_movie_random_slug +from movie_catalog.tests.conftest import create_movie, create_movie_random_slug pytestmark = pytest.mark.apitest diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py index 597fe63..bb4decd 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_list_view.py @@ -7,10 +7,10 @@ from _pytest.fixtures import SubRequest from fastapi import status from fastapi.testclient import TestClient -from schemas.movie_catalog import Movie, MovieCreate -from tests.conftest import build_movie_create_random_slug from movie_catalog.main import app +from movie_catalog.schemas.movie_catalog import Movie, MovieCreate +from movie_catalog.tests.conftest import build_movie_create_random_slug pytestmark = pytest.mark.apitest diff --git a/movie_catalog/tests/test_schemas/test_movie_catalog.py b/movie_catalog/tests/test_schemas/test_movie_catalog.py index 3ebc4aa..b54ed76 100644 --- a/movie_catalog/tests/test_schemas/test_movie_catalog.py +++ b/movie_catalog/tests/test_schemas/test_movie_catalog.py @@ -1,7 +1,13 @@ from unittest import TestCase from pydantic import ValidationError -from schemas.movie_catalog import Movie, MovieCreate, MoviePartialUpdate, MovieUpdate + +from movie_catalog.schemas.movie_catalog import ( + Movie, + MovieCreate, + MoviePartialUpdate, + MovieUpdate, +) class MovieCreateTestCase(TestCase): From 9f071f05c31ae249506159f3874d2b9424d6ae1b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Thu, 24 Jul 2025 20:00:24 +0300 Subject: [PATCH 12/12] fix mypy errors --- movie_catalog/api/api_v1/movie_catalog/views/list_view.py | 3 +-- movie_catalog/tests/conftest.py | 4 ++-- .../test_movie_catalog/test_views/test_details_veiew.py | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py index b87adaa..47b58cf 100644 --- a/movie_catalog/api/api_v1/movie_catalog/views/list_view.py +++ b/movie_catalog/api/api_v1/movie_catalog/views/list_view.py @@ -1,5 +1,4 @@ __all__ = ("router",) -from typing import cast from fastapi import APIRouter, Depends, HTTPException, status @@ -23,7 +22,7 @@ response_model=list[MovieRead], ) def get_movie_list() -> list[Movie]: - return cast(list[Movie], storage.get()) + return storage.get() @router.post( diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index bddc1ac..7f56180 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -60,14 +60,14 @@ def create_movie( description: str = "Some description for unit-test", title: str = "Some title", ) -> Movie: - return storage.create(build_movie_create(slug, description, title)) # type: ignore + return storage.create(build_movie_create(slug, description, title)) def create_movie_random_slug( description: str = "Some description for unit-test", title: str = "Some title", ) -> Movie: - return storage.create(build_movie_create_random_slug(description, title)) # type: ignore + return storage.create(build_movie_create_random_slug(description, title)) @pytest.fixture() diff --git a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py index 206ef48..6391f31 100644 --- a/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py +++ b/movie_catalog/tests/test_api/test_api_v1/test_movie_catalog/test_views/test_details_veiew.py @@ -77,7 +77,7 @@ def test_update_movie_details_partial( assert response.status_code == status.HTTP_200_OK, response.text movie_from_db = storage.get_by_slug(movie.slug) assert movie_from_db != movie_before_update - assert movie_from_db.description == new_description + assert movie_from_db.description == new_description # type: ignore class TestUpdate: @@ -135,5 +135,5 @@ def test_update_movie_details( assert response.status_code == status.HTTP_200_OK, response.text movie_from_db = storage.get_by_slug(movie.slug) assert movie_from_db != movie_before_update - assert movie_from_db.description == new_description - assert movie_from_db.title == new_title + assert movie_from_db.description == new_description # type: ignore + assert movie_from_db.title == new_title # type: ignore