From 20f7ac5a47093eb9524b145f9f97a24be43b7856 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:00:47 +0300 Subject: [PATCH 01/48] add jinja template var --- movie_catalog/templating/__init__.py | 0 movie_catalog/templating/jinja_templates.py | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 movie_catalog/templating/__init__.py create mode 100644 movie_catalog/templating/jinja_templates.py diff --git a/movie_catalog/templating/__init__.py b/movie_catalog/templating/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_catalog/templating/jinja_templates.py b/movie_catalog/templating/jinja_templates.py new file mode 100644 index 0000000..d3e4fd3 --- /dev/null +++ b/movie_catalog/templating/jinja_templates.py @@ -0,0 +1,6 @@ +from config import BASE_DIR +from fastapi.templating import Jinja2Templates + +templates = Jinja2Templates( + directory=BASE_DIR / "templates", +) From 8c02fef34d140965852f5d6624347af2ac2af5a5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:06:48 +0300 Subject: [PATCH 02/48] add base template --- movie_catalog/templates/base.html | 31 +++++++++++++++++++ .../templates/components/develop_message.html | 3 ++ 2 files changed, 34 insertions(+) create mode 100644 movie_catalog/templates/base.html create mode 100644 movie_catalog/templates/components/develop_message.html diff --git a/movie_catalog/templates/base.html b/movie_catalog/templates/base.html new file mode 100644 index 0000000..e22c876 --- /dev/null +++ b/movie_catalog/templates/base.html @@ -0,0 +1,31 @@ + + + + + + {% block title %}Movie{% endblock %} + + + +{% include 'components/develop_message.html' %} +
+ {% block main %} + {% endblock %} +
+
+ {% block footer %} + © Nikita Yakovlev {{ today.year }} + {% endblock %} +
+ + + + + + + diff --git a/movie_catalog/templates/components/develop_message.html b/movie_catalog/templates/components/develop_message.html new file mode 100644 index 0000000..268e849 --- /dev/null +++ b/movie_catalog/templates/components/develop_message.html @@ -0,0 +1,3 @@ +
+ The service is still under development. +
From 1723cdd69ac7243a8a1e2121cac5abadd7264faa Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:25:59 +0300 Subject: [PATCH 03/48] add context inject --- movie_catalog/templating/jinja_templates.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/movie_catalog/templating/jinja_templates.py b/movie_catalog/templating/jinja_templates.py index d3e4fd3..a981f9c 100644 --- a/movie_catalog/templating/jinja_templates.py +++ b/movie_catalog/templating/jinja_templates.py @@ -1,6 +1,19 @@ +from datetime import date + from config import BASE_DIR +from fastapi import Request from fastapi.templating import Jinja2Templates + +def inject_current_date( + request: Request, # noqa: ARG001 Unused function argument: `request` +) -> dict[str, date]: + return {"today": date.today()} + + templates = Jinja2Templates( directory=BASE_DIR / "templates", + context_processors=[ + inject_current_date, + ], ) From 3c5880582f6bc96638e8946d5c7b116b5e3b4c61 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:30:28 +0300 Subject: [PATCH 04/48] add home page --- movie_catalog/api/main_view.py | 14 +++++----- movie_catalog/templates/home.html | 43 +++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 movie_catalog/templates/home.html diff --git a/movie_catalog/api/main_view.py b/movie_catalog/api/main_view.py index e4f7b14..888b8b9 100644 --- a/movie_catalog/api/main_view.py +++ b/movie_catalog/api/main_view.py @@ -1,14 +1,12 @@ from fastapi import APIRouter, Request +from fastapi.responses import HTMLResponse +from templating.jinja_templates import templates router = APIRouter() -@router.get("/") -def root( +@router.get("/", include_in_schema=False, name="home") +def home_page( request: Request, -) -> dict[str, str]: - docs_url = request.url.replace(path="/docs") - return { - "message": "Hello World", - "docs_url": str(docs_url), - } +) -> HTMLResponse: + return templates.TemplateResponse(request=request, name="home.html") diff --git a/movie_catalog/templates/home.html b/movie_catalog/templates/home.html new file mode 100644 index 0000000..949b016 --- /dev/null +++ b/movie_catalog/templates/home.html @@ -0,0 +1,43 @@ +{% extends "base.html" %} + +{% block title %} + Home - {{ super() }} +{% endblock %} + +{% block main %} +
+
+

🎬 Great Movies Catalog

+

+ Сервис для хранения и оценки фильмов. Построен на основе FastAPI с поддержкой OpenAPI. +

+ +
+ +
+
+
+

🚀 Быстрый API

+

Высокая скорость работы и автоматическая генерация документации OpenAPI.

+
+
+
+
+

⭐ Оценки фильмов

+

Возможность ставить оценки и формировать каталог лучших фильмов.

+
+
+
+
+

🛠️ Простая интеграция

+

Чистый и понятный код, который легко расширять и дорабатывать.

+
+
+
+
+{% endblock %} From 62458aba717a59684ddee7b3dc633da70de469ea Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:45:52 +0300 Subject: [PATCH 05/48] add about project page --- movie_catalog/api/main_view.py | 7 +++ movie_catalog/templates/about.html | 70 ++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 movie_catalog/templates/about.html diff --git a/movie_catalog/api/main_view.py b/movie_catalog/api/main_view.py index 888b8b9..f3c9aea 100644 --- a/movie_catalog/api/main_view.py +++ b/movie_catalog/api/main_view.py @@ -10,3 +10,10 @@ def home_page( request: Request, ) -> HTMLResponse: return templates.TemplateResponse(request=request, name="home.html") + + +@router.get("/about", include_in_schema=False, name="about") +def about_page( + request: Request, +) -> HTMLResponse: + return templates.TemplateResponse(request=request, name="about.html") diff --git a/movie_catalog/templates/about.html b/movie_catalog/templates/about.html new file mode 100644 index 0000000..1e3652d --- /dev/null +++ b/movie_catalog/templates/about.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} + +{% block title %} + About - {{ super() }} +{% endblock %} + +{% block main %} +
+
+
+

About the Project

+

+ FastAPI Movie Catalog is a learning project built to demonstrate the + capabilities of the FastAPI framework. It allows you to manage a movie collection: + add new titles, store ratings, and organize your own catalog. +

+

+ The project includes both a backend API powered by FastAPI and a web interface based on + Jinja2 templates and Bootstrap 5. This makes the service convenient for developers, who can interact + with the API directly, as well as for end users who prefer a simple web UI. +

+

+ The main focus of the project is to provide a clear and extendable architecture: + developers can use it as a foundation to build more complex applications, + experiment with REST APIs, or integrate additional features such as authentication, + search, and recommendations. +

+ + +

Key Features

+
    +
  • 🎬 CRUD operations for movies (create, read, update, delete)
  • +
  • ⭐ User-friendly movie rating system
  • +
  • 📑 Automatic API documentation via Swagger and ReDoc
  • +
  • 🖥️ Responsive Bootstrap 5 interface
  • +
  • ⚡ High performance thanks to FastAPI’s async architecture
  • +
  • 🛠️ Easy to extend with new modules (e.g., authentication, search, + recommendations) +
  • +
+ +

+ With its clean codebase and modern stack, FastAPI Movie Catalog is + both a practical tool and a great entry point for those who want to learn how to + design, document, and deploy APIs in Python. +

+ +

Social

+ + +
+
+
+{% endblock %} From a8db47920a068dba5c91ac62b86952612aa1222c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:46:33 +0300 Subject: [PATCH 06/48] add to footer link for feedback --- movie_catalog/templates/base.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/movie_catalog/templates/base.html b/movie_catalog/templates/base.html index e22c876..d13d7b7 100644 --- a/movie_catalog/templates/base.html +++ b/movie_catalog/templates/base.html @@ -6,6 +6,7 @@ {% block title %}Movie{% endblock %} + {% include 'components/develop_message.html' %} @@ -16,6 +17,11 @@
{% block footer %} © Nikita Yakovlev {{ today.year }} + {% endblock %}
From 59800f5c1ad8e5b079c7026a031c3084e87384f9 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 18:54:37 +0300 Subject: [PATCH 07/48] add features as context --- movie_catalog/api/main_view.py | 16 +++++++++++++++- movie_catalog/templates/home.html | 27 +++++++++------------------ 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/movie_catalog/api/main_view.py b/movie_catalog/api/main_view.py index f3c9aea..6a1e1f1 100644 --- a/movie_catalog/api/main_view.py +++ b/movie_catalog/api/main_view.py @@ -1,3 +1,5 @@ +from typing import Any + from fastapi import APIRouter, Request from fastapi.responses import HTMLResponse from templating.jinja_templates import templates @@ -9,7 +11,19 @@ def home_page( request: Request, ) -> HTMLResponse: - return templates.TemplateResponse(request=request, name="home.html") + + context: dict[str, Any] = {} + features = { + "🚀 Fast API": "High performance with automatic OpenAPI documentation generation.", + "⭐ Movie Ratings": "Rate movies and build your own catalog of the best ones.", + "️🛠️ Easy Integration": "Clean and simple codebase that is easy to extend and improve.", + } + context.update( + features=features, + ) + return templates.TemplateResponse( + request=request, name="home.html", context=context + ) @router.get("/about", include_in_schema=False, name="about") diff --git a/movie_catalog/templates/home.html b/movie_catalog/templates/home.html index 949b016..0c6059a 100644 --- a/movie_catalog/templates/home.html +++ b/movie_catalog/templates/home.html @@ -9,7 +9,7 @@

🎬 Great Movies Catalog

- Сервис для хранения и оценки фильмов. Построен на основе FastAPI с поддержкой OpenAPI. + A service for storing and rating movies. Built with FastAPI and powered by OpenAPI.

Swagger Docs @@ -20,24 +20,15 @@

🎬 Great Movies Catalog

-
-
-

🚀 Быстрый API

-

Высокая скорость работы и автоматическая генерация документации OpenAPI.

+ {% for feature_name, feature in features.items() %} +
+
+

{{ feature_name }}

+

{{ feature }}

+
-
-
-
-

⭐ Оценки фильмов

-

Возможность ставить оценки и формировать каталог лучших фильмов.

-
-
-
-
-

🛠️ Простая интеграция

-

Чистый и понятный код, который легко расширять и дорабатывать.

-
-
+ {% endfor %}
+ {% endblock %} From c53c49b0d0af170b79fa0927daa5201ae6ab983c Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sat, 27 Sep 2025 20:02:12 +0300 Subject: [PATCH 08/48] add navbar with active links --- movie_catalog/templates/base.html | 11 +++++-- .../templates/components/navbar.html | 32 +++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 movie_catalog/templates/components/navbar.html diff --git a/movie_catalog/templates/base.html b/movie_catalog/templates/base.html index d13d7b7..530d326 100644 --- a/movie_catalog/templates/base.html +++ b/movie_catalog/templates/base.html @@ -7,9 +7,14 @@ + {% include 'components/develop_message.html' %} +
+ {% include "components/navbar.html" %} +
+
{% block main %} {% endblock %} @@ -18,9 +23,9 @@ {% block footer %} © Nikita Yakovlev {{ today.year }} {% endblock %} diff --git a/movie_catalog/templates/components/navbar.html b/movie_catalog/templates/components/navbar.html new file mode 100644 index 0000000..2ab982d --- /dev/null +++ b/movie_catalog/templates/components/navbar.html @@ -0,0 +1,32 @@ +{% set navigation = [ + ("home", "Home"), + ("about", "About") +] %} + + From 6b52e46c4f6577cdc8b5b2e07d52de9a7ce92900 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:21:55 +0300 Subject: [PATCH 09/48] add new pytest marker --- pyproject.toml | 4 +++- uv.lock | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8f1a0ba..b1dc16b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ dependencies = [ "fastapi[standard]>=0.115.12", "pydantic-settings[yaml]>=2.10.1", "redis[hiredis]>=6.0.0", + "ruff>=0.11.12", "typer>=0.15.2", "types-redis>=4.6.0.20241004", ] @@ -30,7 +31,8 @@ required-version = "~=0.8.2" minversion = "8.3" addopts = ["-ra", "--strict-markers"] markers = [ - "apitest: test any api call" + "apitest: test any api call", + "templatetest: test any http HTML template page" ] [tool.mypy] diff --git a/uv.lock b/uv.lock index 7cc3d71..0c7a5fd 100644 --- a/uv.lock +++ b/uv.lock @@ -256,6 +256,7 @@ dependencies = [ { name = "fastapi", extra = ["standard"] }, { name = "pydantic-settings", extra = ["yaml"] }, { name = "redis", extra = ["hiredis"] }, + { name = "ruff" }, { name = "typer" }, { name = "types-redis" }, ] @@ -275,6 +276,7 @@ requires-dist = [ { name = "fastapi", extras = ["standard"], specifier = ">=0.115.12" }, { name = "pydantic-settings", extras = ["yaml"], specifier = ">=2.10.1" }, { name = "redis", extras = ["hiredis"], specifier = ">=6.0.0" }, + { name = "ruff", specifier = ">=0.11.12" }, { name = "typer", specifier = ">=0.15.2" }, { name = "types-redis", specifier = ">=4.6.0.20241004" }, ] From 212b2865874dc73d148d5b3675bcd323f904d46b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:29:01 +0300 Subject: [PATCH 10/48] editing the test to get an HTML response --- movie_catalog/tests/test_api/test_views.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/movie_catalog/tests/test_api/test_views.py b/movie_catalog/tests/test_api/test_views.py index c794201..8c113f3 100644 --- a/movie_catalog/tests/test_api/test_views.py +++ b/movie_catalog/tests/test_api/test_views.py @@ -1,12 +1,20 @@ import pytest +from fastapi import status from fastapi.testclient import TestClient -@pytest.mark.apitest -def test_root_view(client: TestClient) -> None: +@pytest.mark.templatetest +def test_root(client: TestClient) -> None: response = client.get("/") - assert response.status_code == 200 - assert response.json() == { - "message": "Hello World", - "docs_url": "http://testserver/docs", - } + assert response.status_code == status.HTTP_200_OK, response.text + assert response.template.name == "home.html" # type: ignore[attr-defined] + assert "features" in response.context, response.context # type: ignore[attr-defined] + assert isinstance(response.context["features"], dict) # type: ignore[attr-defined] + + +@pytest.mark.templatetest +def test_about(client: TestClient) -> None: + response = client.get("/about") + assert response.status_code == status.HTTP_200_OK + assert response.template.name == "about.html" # type: ignore[attr-defined] + assert "today" in response.context, response.context # type: ignore[attr-defined] From 6f3b7425fa42b6eb923c7087fd4a798fec402ec5 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:31:47 +0300 Subject: [PATCH 11/48] move storage to independent module --- movie_catalog/api/api_v1/movie_catalog/dependencies.py | 2 +- movie_catalog/api/api_v1/movie_catalog/views/details_view.py | 2 +- movie_catalog/api/api_v1/movie_catalog/views/list_view.py | 4 ++-- movie_catalog/storage/__init__.py | 0 movie_catalog/storage/movie_catalog/__init__.py | 0 movie_catalog/{api/api_v1 => storage}/movie_catalog/crud.py | 0 movie_catalog/tests/conftest.py | 2 +- .../test_api/test_api_v1/test_movie_catalog/test_crud.py | 4 ++-- .../test_movie_catalog/test_views/test_details_veiew.py | 2 +- 9 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 movie_catalog/storage/__init__.py create mode 100644 movie_catalog/storage/movie_catalog/__init__.py rename movie_catalog/{api/api_v1 => storage}/movie_catalog/crud.py (100%) diff --git a/movie_catalog/api/api_v1/movie_catalog/dependencies.py b/movie_catalog/api/api_v1/movie_catalog/dependencies.py index 9005412..6e754b4 100644 --- a/movie_catalog/api/api_v1/movie_catalog/dependencies.py +++ b/movie_catalog/api/api_v1/movie_catalog/dependencies.py @@ -8,11 +8,11 @@ HTTPBasicCredentials, HTTPBearer, ) +from storage.movie_catalog.crud import storage from movie_catalog.schemas.movie_catalog import Movie from ..auth.services import redis_tokens, redis_users -from .crud import storage logger = logging.getLogger(__name__) 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 acfe879..8b37e3c 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,8 +1,8 @@ from typing import Annotated from fastapi import APIRouter, Depends, status +from storage.movie_catalog.crud import storage -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, 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 47b58cf..62125d2 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 movie_catalog.api.api_v1.movie_catalog.crud import ( +from storage.movie_catalog.crud import ( MovieCatalogAlreadyExists, storage, ) + from movie_catalog.api.api_v1.movie_catalog.dependencies import ( api_token_or_user_basic_auth_required, ) diff --git a/movie_catalog/storage/__init__.py b/movie_catalog/storage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_catalog/storage/movie_catalog/__init__.py b/movie_catalog/storage/movie_catalog/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_catalog/api/api_v1/movie_catalog/crud.py b/movie_catalog/storage/movie_catalog/crud.py similarity index 100% rename from movie_catalog/api/api_v1/movie_catalog/crud.py rename to movie_catalog/storage/movie_catalog/crud.py diff --git a/movie_catalog/tests/conftest.py b/movie_catalog/tests/conftest.py index 7f56180..764d000 100644 --- a/movie_catalog/tests/conftest.py +++ b/movie_catalog/tests/conftest.py @@ -5,8 +5,8 @@ from typing import Generator import pytest +from storage.movie_catalog.crud import storage -from movie_catalog.api.api_v1.movie_catalog.crud import storage from movie_catalog.schemas.movie_catalog import Movie, MovieCreate 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 533f7c8..9c9a181 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,11 +3,11 @@ from unittest import TestCase import pytest - -from movie_catalog.api.api_v1.movie_catalog.crud import ( +from storage.movie_catalog.crud import ( MovieCatalogAlreadyExists, storage, ) + from movie_catalog.schemas.movie_catalog import ( Movie, MovieCreate, 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 6391f31..4b0a5f7 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 @@ -4,8 +4,8 @@ from _pytest.fixtures import SubRequest from fastapi import status from fastapi.testclient import TestClient +from storage.movie_catalog.crud import storage -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, From b7e25958fa4616b78b4cc2609efebf2d0d367773 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:34:34 +0300 Subject: [PATCH 12/48] move exceptions.py to a separate module --- .../api/api_v1/movie_catalog/views/list_view.py | 4 ++-- movie_catalog/exceptions.py | 10 ++++++++++ movie_catalog/storage/movie_catalog/crud.py | 11 ++--------- .../test_api_v1/test_movie_catalog/test_crud.py | 6 +++--- 4 files changed, 17 insertions(+), 14 deletions(-) create mode 100644 movie_catalog/exceptions.py 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 62125d2..921a6da 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,8 +1,8 @@ __all__ = ("router",) +from exceptions import MovieAlreadyExists from fastapi import APIRouter, Depends, HTTPException, status from storage.movie_catalog.crud import ( - MovieCatalogAlreadyExists, storage, ) @@ -70,7 +70,7 @@ def add_movie( ) -> Movie: try: return storage.create_or_rise_if_exists(movie_create) - except MovieCatalogAlreadyExists: + except MovieAlreadyExists: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Movie with slug <{movie_create.slug}> already exists.", diff --git a/movie_catalog/exceptions.py b/movie_catalog/exceptions.py new file mode 100644 index 0000000..7047442 --- /dev/null +++ b/movie_catalog/exceptions.py @@ -0,0 +1,10 @@ +class MovieCatalogBaseError(Exception): + """ + Base exception class for Movie Catalog + """ + + +class MovieAlreadyExists(MovieCatalogBaseError): + """ + Exception raised when a movie already exists + """ diff --git a/movie_catalog/storage/movie_catalog/crud.py b/movie_catalog/storage/movie_catalog/crud.py index a06e00a..179b773 100644 --- a/movie_catalog/storage/movie_catalog/crud.py +++ b/movie_catalog/storage/movie_catalog/crud.py @@ -1,6 +1,7 @@ import logging from typing import cast +from exceptions import MovieAlreadyExists from pydantic import BaseModel from redis import Redis @@ -22,14 +23,6 @@ ) -class MovieCatalogBaseError(Exception): - pass - - -class MovieCatalogAlreadyExists(MovieCatalogBaseError): - pass - - class MovieCatalogStorage(BaseModel): hast_name: str @@ -68,7 +61,7 @@ def create_or_rise_if_exists(self, create_movie: MovieCreate) -> Movie: return self.create(create_movie) logger.error("Movie with slug <%s> already exists.", create_movie.slug) - raise MovieCatalogAlreadyExists(create_movie.slug) + raise MovieAlreadyExists(create_movie.slug) def delete_by_slug(self, slug: str) -> None: redis.hdel(self.hast_name, slug) 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 9c9a181..a15b1ad 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,8 +3,8 @@ from unittest import TestCase import pytest +from exceptions import MovieAlreadyExists from storage.movie_catalog.crud import ( - MovieCatalogAlreadyExists, storage, ) @@ -99,7 +99,7 @@ def tearDownClass(cls) -> None: def test_create_or_raise_if_exists(movie: Movie) -> None: movie_create = MovieCreate(**movie.model_dump()) - with pytest.raises(MovieCatalogAlreadyExists, match=movie_create.slug) as exc_info: + with pytest.raises(MovieAlreadyExists, match=movie_create.slug) as exc_info: storage.create_or_rise_if_exists(movie_create) assert exc_info.value.args[0] == movie_create.slug @@ -108,7 +108,7 @@ def test_create_or_raise_if_exists(movie: Movie) -> None: def test_create_twice() -> None: movie_create = build_movie_create_random_slug() storage.create_or_rise_if_exists(movie_create) - with pytest.raises(MovieCatalogAlreadyExists, match=movie_create.slug) as exc_info: + with pytest.raises(MovieAlreadyExists, match=movie_create.slug) as exc_info: storage.create_or_rise_if_exists(movie_create) assert exc_info.value.args == (movie_create.slug,) From 4e4ec7c78bbd194514693ed01a4fbc5fe18c3166 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:35:47 +0300 Subject: [PATCH 13/48] optimize import --- movie_catalog/storage/movie_catalog/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/movie_catalog/storage/movie_catalog/__init__.py b/movie_catalog/storage/movie_catalog/__init__.py index e69de29..113a4fd 100644 --- a/movie_catalog/storage/movie_catalog/__init__.py +++ b/movie_catalog/storage/movie_catalog/__init__.py @@ -0,0 +1 @@ +from .crud import MovieCatalogStorage as MovieCatalogStorage From 82285ac6ef3805d29dca09db2547f169c56fb85a Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:40:59 +0300 Subject: [PATCH 14/48] add to global state var for storage --- movie_catalog/app_lifespan.py | 6 +++++- movie_catalog/dependencies/__init__.py | 0 movie_catalog/dependencies/movie_catalog.py | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 movie_catalog/dependencies/__init__.py create mode 100644 movie_catalog/dependencies/movie_catalog.py diff --git a/movie_catalog/app_lifespan.py b/movie_catalog/app_lifespan.py index 930a567..41ec888 100644 --- a/movie_catalog/app_lifespan.py +++ b/movie_catalog/app_lifespan.py @@ -1,12 +1,16 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager +from config import settings from fastapi import FastAPI +from storage.movie_catalog import MovieCatalogStorage @asynccontextmanager async def lifespan( app: FastAPI, # noqa: ARG001 ) -> AsyncIterator[None]: - + app.state.movie_catalog_storage = MovieCatalogStorage( + hast_name=settings.redis.collections.movie_catalog_hash, + ) yield diff --git a/movie_catalog/dependencies/__init__.py b/movie_catalog/dependencies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/movie_catalog/dependencies/movie_catalog.py b/movie_catalog/dependencies/movie_catalog.py new file mode 100644 index 0000000..d5193d1 --- /dev/null +++ b/movie_catalog/dependencies/movie_catalog.py @@ -0,0 +1,16 @@ +from typing import Annotated + +from fastapi import Depends, Request +from storage.movie_catalog import MovieCatalogStorage + + +def get_movie_catalog_storage( + request: Request, +) -> MovieCatalogStorage: + return request.app.state.movie_catalog_storage + + +GetMovieCatalogStorage = Annotated[ + MovieCatalogStorage, + Depends(get_movie_catalog_storage), +] From d4a86223e91d23243ab0b04d228085995e6edb79 Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 19:43:48 +0300 Subject: [PATCH 15/48] move HTML view to rest module --- movie_catalog/main.py | 4 ++-- movie_catalog/rest/__init__.py | 8 ++++++++ movie_catalog/{api => rest}/main_view.py | 0 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 movie_catalog/rest/__init__.py rename movie_catalog/{api => rest}/main_view.py (100%) diff --git a/movie_catalog/main.py b/movie_catalog/main.py index eb5f09b..c34fb15 100644 --- a/movie_catalog/main.py +++ b/movie_catalog/main.py @@ -1,9 +1,9 @@ import logging from fastapi import FastAPI +from rest import router as rest_router 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 from movie_catalog.config import settings @@ -19,5 +19,5 @@ version="1.0", lifespan=lifespan, ) -app.include_router(main_router) +app.include_router(rest_router) app.include_router(api_router) diff --git a/movie_catalog/rest/__init__.py b/movie_catalog/rest/__init__.py new file mode 100644 index 0000000..94b6e5d --- /dev/null +++ b/movie_catalog/rest/__init__.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +from .main_view import router as main_router + +router = APIRouter( + include_in_schema=False, +) +router.include_router(main_router) diff --git a/movie_catalog/api/main_view.py b/movie_catalog/rest/main_view.py similarity index 100% rename from movie_catalog/api/main_view.py rename to movie_catalog/rest/main_view.py From 7477de4235f7d9e5964beff46ad3b85b9f9cc5ef Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 20:18:53 +0300 Subject: [PATCH 16/48] add new view: list movie rating --- movie_catalog/rest/__init__.py | 2 + movie_catalog/rest/movie_catalog/__init__.py | 6 ++ movie_catalog/rest/movie_catalog/list_view.py | 20 +++++++ .../templates/movie-catalog/list.html | 57 +++++++++++++++++++ movie_catalog/templating/__init__.py | 1 + 5 files changed, 86 insertions(+) create mode 100644 movie_catalog/rest/movie_catalog/__init__.py create mode 100644 movie_catalog/rest/movie_catalog/list_view.py create mode 100644 movie_catalog/templates/movie-catalog/list.html diff --git a/movie_catalog/rest/__init__.py b/movie_catalog/rest/__init__.py index 94b6e5d..a477dda 100644 --- a/movie_catalog/rest/__init__.py +++ b/movie_catalog/rest/__init__.py @@ -1,8 +1,10 @@ from fastapi import APIRouter from .main_view import router as main_router +from .movie_catalog import router as movie_catalog_router router = APIRouter( include_in_schema=False, ) router.include_router(main_router) +router.include_router(movie_catalog_router) diff --git a/movie_catalog/rest/movie_catalog/__init__.py b/movie_catalog/rest/movie_catalog/__init__.py new file mode 100644 index 0000000..776a40d --- /dev/null +++ b/movie_catalog/rest/movie_catalog/__init__.py @@ -0,0 +1,6 @@ +from fastapi import APIRouter + +from .list_view import router as list_router + +router = APIRouter(prefix="/movie-catalog", tags=["movie-catalog"]) +router.include_router(list_router) diff --git a/movie_catalog/rest/movie_catalog/list_view.py b/movie_catalog/rest/movie_catalog/list_view.py new file mode 100644 index 0000000..12c3208 --- /dev/null +++ b/movie_catalog/rest/movie_catalog/list_view.py @@ -0,0 +1,20 @@ +from typing import Any + +from dependencies.movie_catalog import GetMovieCatalogStorage +from fastapi import APIRouter, Request +from starlette.responses import HTMLResponse +from templating import templates + +router = APIRouter() + + +@router.get("/", name="movie-catalog:list") +def list_view(request: Request, storage: GetMovieCatalogStorage) -> HTMLResponse: + context: dict[str, Any] = {} + movie_catalog = storage.get() + context.update(movie_catalog=movie_catalog) + return templates.TemplateResponse( + request=request, + name="movie-catalog/list.html", + context=context, + ) diff --git a/movie_catalog/templates/movie-catalog/list.html b/movie_catalog/templates/movie-catalog/list.html new file mode 100644 index 0000000..a06807b --- /dev/null +++ b/movie_catalog/templates/movie-catalog/list.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} +{% block title %} + Movies Rating +{% endblock %} + +{% block main %} +
+

🎬 Movies Rating

+ + {% if movie_catalog %} +
+
+
+ + + + + + + + + + + {% for movie in movie_catalog %} + + + + + + + {% endfor %} + +
TitleDescriptionYearRating
+ {{ movie.title }} + + {{ movie.description[:100] ~ ('...' if movie.description|length > 100) }} + + {{ movie.year_released }} + + {% if movie.rating >= 8 %} + {{ movie.rating }} + {% elif movie.rating >= 5 %} + {{ movie.rating }} + {% else %} + {{ movie.rating }} + {% endif %} +
+
+
+
+ {% else %} + + {% endif %} +
+{% endblock %} diff --git a/movie_catalog/templating/__init__.py b/movie_catalog/templating/__init__.py index e69de29..5274155 100644 --- a/movie_catalog/templating/__init__.py +++ b/movie_catalog/templating/__init__.py @@ -0,0 +1 @@ +from .jinja_templates import templates as templates From 9adc68023c842349163c981a6acf3db3bbd8694b Mon Sep 17 00:00:00 2001 From: Nikita Yakovlev Date: Sun, 5 Oct 2025 20:19:05 +0300 Subject: [PATCH 17/48] include new page to nav bar --- movie_catalog/templates/components/navbar.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/movie_catalog/templates/components/navbar.html b/movie_catalog/templates/components/navbar.html index 2ab982d..6ca45f9 100644 --- a/movie_catalog/templates/components/navbar.html +++ b/movie_catalog/templates/components/navbar.html @@ -1,6 +1,7 @@ {% set navigation = [ ("home", "Home"), - ("about", "About") + ("about", "About"), + ("movie-catalog:list", "Movie Rating"), ] %}