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
6 changes: 3 additions & 3 deletions .github/workflows/python-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ jobs:

run-tests:
runs-on: ubuntu-latest
container: node:20-bookworm-slim
needs:
- run-checks
services:
Expand All @@ -46,8 +47,6 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 6379:6379

steps:
- uses: actions/checkout@v4
Expand All @@ -66,7 +65,8 @@ jobs:
run: uv run pytest movie_catalog --cov=movie_catalog --cov-report=xml:coverage.xml
env:
TESTING: 1
REDIS_PORT: 6379
MOVIE_CATALOG__REDIS__CONNECTION__HOST: redis
MOVIE_CATALOG__REDIS__CONNECTION__PORT: 6379

- name: Upload artefacts
uses: actions/upload-artifact@v4
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -172,4 +172,5 @@ cython_debug/

# PyPI configuration file
.pypirc
/movie_catalog/movie-catalog.json
/movie_catalog/movie-catalog.json
config.local.yaml
1 change: 1 addition & 0 deletions movie_catalog/.env.template
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
MOVIE_CATALOG__LOGGING__LOG_LEVEL_NAME=INFO
15 changes: 5 additions & 10 deletions movie_catalog/api/api_v1/auth/services/redis_tokens_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,7 @@
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,
)
from movie_catalog.config import settings


class RedisTokensHelper(AbstractTokensHelper):
Expand All @@ -31,8 +26,8 @@ def delete_token(self, token: str) -> None:


redis_tokens = RedisTokensHelper(
host=REDIS_HOST,
port=REDIS_PORT,
db=REDIS_DB_TOKENS,
tokens_set_name=REDIS_TOKENS_SET_NAME,
host=settings.redis.connection.host,
port=settings.redis.connection.port,
db=settings.redis.db.tokens,
tokens_set_name=settings.redis.collections.tokens_set,
)
8 changes: 6 additions & 2 deletions movie_catalog/api/api_v1/auth/services/redis_users_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from redis import Redis

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

from .users_helper import AbstractUsersHelper

Expand All @@ -15,4 +15,8 @@ def get_user_password(self, username: str) -> str | None:
return self.redis.get(username)


redis_users = RedisUsersHelper(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB_USERS)
redis_users = RedisUsersHelper(
host=settings.redis.connection.host,
port=settings.redis.connection.port,
db=settings.redis.db.users,
)
27 changes: 13 additions & 14 deletions movie_catalog/api/api_v1/movie_catalog/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,7 @@
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 movie_catalog.config import settings
from movie_catalog.schemas.movie_catalog import (
Movie,
MovieCreate,
Expand All @@ -18,8 +13,12 @@
)

logger = logging.getLogger(__name__)

redis = Redis(
host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB_MOVIE_CATALOG, decode_responses=True
host=settings.redis.connection.host,
port=settings.redis.connection.port,
db=settings.redis.db.movie_catalog,
decode_responses=True,
)


Expand All @@ -32,11 +31,11 @@ class MovieCatalogAlreadyExists(MovieCatalogBaseError):


class MovieCatalogStorage(BaseModel):
movie_catalog: dict[str, Movie] = {}
hast_name: str

def save_data(self, movie: Movie) -> None:
redis.hset(
name=REDIS_MOVIE_CATALOG_HASH_NAME,
name=self.hast_name,
key=movie.slug,
value=movie.model_dump_json(),
)
Expand All @@ -45,16 +44,16 @@ def save_data(self, movie: Movie) -> None:
def get(self) -> list[Movie]:
return [
Movie.model_validate_json(value)
for value in cast(set[str], redis.hvals(name=REDIS_MOVIE_CATALOG_HASH_NAME))
for value in cast(set[str], redis.hvals(name=self.hast_name))
]

def get_by_slug(self, slug: str) -> Movie | None:
if data := redis.hget(name=REDIS_MOVIE_CATALOG_HASH_NAME, key=slug):
if data := redis.hget(name=self.hast_name, key=slug):
return Movie.model_validate_json(data)
return None

def exists(self, slug: str) -> bool:
return bool(redis.hexists(name=REDIS_MOVIE_CATALOG_HASH_NAME, key=slug))
return bool(redis.hexists(name=self.hast_name, key=slug))

def create(self, create_movie: MovieCreate) -> Movie:
movie = Movie(
Expand All @@ -72,7 +71,7 @@ def create_or_rise_if_exists(self, create_movie: MovieCreate) -> Movie:
raise MovieCatalogAlreadyExists(create_movie.slug)

def delete_by_slug(self, slug: str) -> None:
redis.hdel(REDIS_MOVIE_CATALOG_HASH_NAME, slug)
redis.hdel(self.hast_name, slug)
logger.debug("Remove movie <%s> from catalog.", slug)

def delete(self, movie: Movie) -> None:
Expand All @@ -93,4 +92,4 @@ def partial_update(self, movie: Movie, updated_movie: MoviePartialUpdate) -> Mov
return movie


storage = MovieCatalogStorage()
storage = MovieCatalogStorage(hast_name=settings.redis.collections.movie_catalog_hash)
3 changes: 3 additions & 0 deletions movie_catalog/config.default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
movie-catalog:
logging:
log_level_name: INFO
117 changes: 104 additions & 13 deletions movie_catalog/config.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,108 @@
import logging
from os import getenv
from pathlib import Path
from typing import Literal, Self

from pydantic import BaseModel, model_validator
from pydantic_settings import (
BaseSettings,
PydanticBaseSettingsSource,
SettingsConfigDict,
YamlConfigSettingsSource,
)

BASE_DIR = Path(__file__).resolve().parent
STORAGE_PATH = BASE_DIR / "movie_catalog.json"
LOG_FORMAT = "[-] %(asctime)s [%(levelname)s] %(module)s-%(lineno)d - %(message)s"
LOG_LEVEL = logging.DEBUG

REDIS_HOST = getenv("REDIS_HOST", "localhost")
REDIS_PORT = int(getenv("REDIS_PORT", 0)) or 6379
REDIS_DB = 0
REDIS_DB_TOKENS = 1
REDIS_DB_USERS = 2
REDIS_DB_MOVIE_CATALOG = 3
REDIS_TOKENS_SET_NAME = "tokens"
REDIS_MOVIE_CATALOG_HASH_NAME = "movie_catalog"


class LoggingConfig(BaseModel):
log_format: str = (
"[-] %(asctime)s [%(levelname)s] %(module)s-%(lineno)d - %(message)s"
)
log_level_name: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "WARNING"
log_date_format: str = "%Y-%m-%d %H:%M:%S"

@property
def log_level(self) -> int:
return logging.getLevelNamesMapping()[self.log_level_name]


class RedisConnectionConfig(BaseModel):
host: str = "localhost"
port: int = 6379


class RedisDatabaseConfig(BaseModel):
default: int = 0
tokens: int = 1
users: int = 2
movie_catalog: int = 3

@model_validator(mode="after")
def validate_dbs_numbers_unique(self) -> Self:
db_value = list(self.model_dump().values())
if len(set(db_value)) != len(db_value):
raise ValueError("Database numbers must have unique values")
return self


class RedisCollectionsNamesConfig(BaseModel):
tokens_set: str = "tokens"
movie_catalog_hash: str = "movie_catalog"


class RedisConfig(BaseModel):
connection: RedisConnectionConfig = RedisConnectionConfig()
db: RedisDatabaseConfig = RedisDatabaseConfig()
collections: RedisCollectionsNamesConfig = RedisCollectionsNamesConfig()


class Settings(BaseSettings):
model_config = SettingsConfigDict(
case_sensitive=False,
env_file=(
BASE_DIR / ".env.template",
BASE_DIR / ".env",
),
env_prefix="MOVIE_CATALOG__",
env_nested_delimiter="__",
yaml_file=(
BASE_DIR / "config.default.yaml",
BASE_DIR / "config.local.yaml",
),
yaml_config_section="movie-catalog",
)

@classmethod
def settings_customise_sources(
cls,
settings_cls: type[BaseSettings],
init_settings: PydanticBaseSettingsSource,
env_settings: PydanticBaseSettingsSource,
dotenv_settings: PydanticBaseSettingsSource,
file_secret_settings: PydanticBaseSettingsSource,
) -> tuple[PydanticBaseSettingsSource, ...]:
"""
Define the sources and their order for loading the settings values.

Args:
settings_cls: The Settings class.
init_settings: The `InitSettingsSource` instance.
env_settings: The `EnvSettingsSource` instance.
dotenv_settings: The `DotEnvSettingsSource` instance.
file_secret_settings: The `SecretsSettingsSource` instance.

Returns:
A tuple containing the sources and their order for loading the settings values.
"""
return (
init_settings,
env_settings,
dotenv_settings,
file_secret_settings,
YamlConfigSettingsSource(settings_cls),
)

logging: LoggingConfig = LoggingConfig()
redis: RedisConfig = RedisConfig()


settings = Settings()
7 changes: 4 additions & 3 deletions movie_catalog/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

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
from movie_catalog.config import settings

logging.basicConfig(
format=config.LOG_FORMAT,
level=config.LOG_LEVEL,
format=settings.logging.log_format,
level=settings.logging.log_level,
datefmt=settings.logging.log_date_format,
)

app = FastAPI(
Expand Down
9 changes: 7 additions & 2 deletions movie_catalog/stuff.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from config import REDIS_DB, REDIS_HOST, REDIS_PORT
from config import settings
from redis import Redis

redis = Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, decode_responses=True)
redis = Redis(
host=settings.redis.connection.host,
port=settings.redis.connection.port,
db=settings.redis.db.default,
decode_responses=True,
)


def main() -> None:
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]>=0.115.12",
"pydantic-settings[yaml]>=2.10.1",
"redis[hiredis]>=6.0.0",
"typer>=0.15.2",
"types-redis>=4.6.0.20241004",
Expand Down
Loading