Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion .github/workflows/python-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,38 @@ 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
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
Empty file added movie_catalog/__init__.py
Empty file.
9 changes: 7 additions & 2 deletions movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

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
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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from redis import Redis

from config import REDIS_DB_USERS, REDIS_HOST, REDIS_PORT
from movie_catalog.config import REDIS_DB_USERS, REDIS_HOST, REDIS_PORT

from .users_helper import AbstractUsersHelper

Expand Down
9 changes: 7 additions & 2 deletions movie_catalog/api/api_v1/movie_catalog/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@
from pydantic import BaseModel
from redis import Redis

from config import (
from movie_catalog.config import (
REDIS_DB_MOVIE_CATALOG,
REDIS_HOST,
REDIS_MOVIE_CATALOG_HASH_NAME,
REDIS_PORT,
)
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(
Expand Down
6 changes: 4 additions & 2 deletions movie_catalog/api/api_v1/movie_catalog/dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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
Expand Down Expand Up @@ -87,7 +87,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)

Expand Down
11 changes: 8 additions & 3 deletions movie_catalog/api/api_v1/movie_catalog/views/details_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

from fastapi import APIRouter, Depends, status

from api.api_v1.movie_catalog.crud import storage
from api.api_v1.movie_catalog.dependencies import (
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 schemas.movie_catalog import Movie, MoviePartialUpdate, MovieRead, MovieUpdate
from movie_catalog.schemas.movie_catalog import (
Movie,
MoviePartialUpdate,
MovieRead,
MovieUpdate,
)

router = APIRouter(
prefix="/{slug}",
Expand Down
10 changes: 7 additions & 3 deletions movie_catalog/api/api_v1/movie_catalog/views/list_view.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
__all__ = ("router",)

from fastapi import APIRouter, Depends, HTTPException, status

from api.api_v1.movie_catalog.crud import MovieCatalogAlreadyExists, storage
from api.api_v1.movie_catalog.dependencies import (
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 schemas.movie_catalog import Movie, MovieCreate, MovieRead
from movie_catalog.schemas.movie_catalog import Movie, MovieCreate, MovieRead

router: APIRouter = APIRouter(
prefix="/movies",
Expand Down
5 changes: 2 additions & 3 deletions movie_catalog/commands/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion movie_catalog/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions movie_catalog/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

from fastapi import FastAPI

import config
from api.main_view import router as main_router
from api import router as api_router
from app_lifespan import lifespan
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,
Expand Down
3 changes: 1 addition & 2 deletions movie_catalog/stuff.py
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
8 changes: 4 additions & 4 deletions movie_catalog/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

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
from movie_catalog.schemas.movie_catalog import Movie, MovieCreate


@pytest.fixture(scope="session", autouse=True)
Expand All @@ -19,7 +19,7 @@ def check_testing_env() -> None:


@pytest.fixture(autouse=True)
def disable_logging():
def disable_logging() -> None:
logging.getLogger().setLevel(logging.CRITICAL)


Expand All @@ -43,7 +43,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,
),
Expand Down
4 changes: 1 addition & 3 deletions movie_catalog/tests/test_api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import pytest
from fastapi.testclient import TestClient

from api.api_v1.auth.services import redis_tokens
from movie_catalog.api.api_v1.auth.services import redis_tokens
from movie_catalog.main import app


Expand All @@ -25,5 +25,3 @@ def auth_client(auth_token: str) -> Generator[TestClient]:
headers = {"Authorization": f"Bearer {auth_token}"}
with TestClient(app, headers=headers) as client:
yield client


Original file line number Diff line number Diff line change
@@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,24 @@
from typing import ClassVar
from unittest import TestCase

from api.api_v1.movie_catalog.crud import storage
from schemas.movie_catalog import Movie, MovieCreate, MovieUpdate, MoviePartialUpdate
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:
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,
Expand Down Expand Up @@ -77,6 +86,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)
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
from typing import Generator

import pytest
from _pytest.fixtures import SubRequest
from fastapi import status
from fastapi.testclient import TestClient

from api.api_v1.movie_catalog.crud import storage
from main import app
from schemas.movie_catalog import (
Movie,
DESCRIPTION_MIN_LENGTH,
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

Expand All @@ -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
Expand All @@ -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)
Expand All @@ -64,20 +66,23 @@ 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)
response = auth_client.patch(url, json={"description": new_description})
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:
@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
Expand Down Expand Up @@ -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)
Expand All @@ -126,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
Loading