From dff79571ba63284dca07641f7b9cf8bd6ddad012 Mon Sep 17 00:00:00 2001 From: Ethan Paul <24588726+enpaul@users.noreply.github.com> Date: Fri, 29 Oct 2021 01:32:04 -0400 Subject: [PATCH] Reimplement manip configuration settings Add support for future color changes Once and for all settle how crop/scaling will work Add proper exceptions --- kodak/configuration.py | 174 ++++++++++++++++++++++++++--------------- kodak/constants.py | 10 +-- kodak/exceptions.py | 4 + 3 files changed, 119 insertions(+), 69 deletions(-) diff --git a/kodak/configuration.py b/kodak/configuration.py index 52100f6..6307a2b 100644 --- a/kodak/configuration.py +++ b/kodak/configuration.py @@ -1,3 +1,4 @@ +import enum import json import os from dataclasses import dataclass @@ -5,18 +6,40 @@ from dataclasses import field from pathlib import Path from typing import Any from typing import Dict -from typing import NamedTuple from typing import Optional -from typing import Sequence +from typing import Set from typing import Union from kodak import constants +from kodak import exceptions -class DimensionConfig(NamedTuple): - strategy: constants.DimensionStrategy - anchor: constants.Anchor - value: int +def _get_int(var: str, default: Optional[int]) -> Optional[int]: + return int(os.environ[var]) if var in os.environ else default + + +def _get_float(var: str, default: Optional[float]) -> Optional[float]: + return float(os.environ[var]) if var in os.environ else default + + +def _get_enum_by_name( + var: str, enumeration: enum.Enum, default: enum.Enum +) -> enum.Enum: + return enumeration[os.environ[var].upper()] if var in os.environ else default + + +def _get_enum_by_value( + var: str, enumeration: enum.Enum, default: enum.Enum +) -> enum.Enum: + return enumeration(os.environ[var].lower()) if var in os.environ else default + + +def _get_path(var: str, default: Union[str, Path]) -> Path: + return Path(os.environ.get(var, default)).expanduser().resolve() + + +def _get_bool(var: str, default: bool) -> bool: + return os.getenv(var, str(default)).lower() == "true" @dataclass @@ -29,9 +52,9 @@ class DatabaseSqliteConfig: @classmethod def from_env(cls): return cls( - path=Path(os.environ.get("KODAK_DB_SQLITE_PATH", cls.path)), - pragmas=json.loads(os.environ["KODAK_DB_SQLITE_PRAGMAS"]) - if "KODAK_DB_SQLITE_PRAGMAS" in os.environ + path=_get_path("KODAK_DATABASE_SQLITE_PATH", cls.path), + pragmas=json.loads(os.environ["KODAK_DATABASE_SQLITE_PRAGMAS"]) + if "KODAK_DATABASE_SQLITE_PRAGMAS" in os.environ else constants.DEFAULT_SQLITE_PRAGMAS, ) @@ -43,16 +66,16 @@ class DatabaseMariaConfig: username: str = "root" password: Optional[str] = None port: int = 3306 - schema: str = "fresnel" + schema: str = "kodak" @classmethod def from_env(cls): return cls( - hostname=os.getenv("KODAK_DB_MARIA_HOSTNAME", cls.hostname), - username=os.getenv("KODAK_DB_MARIA_USERNAME", cls.username), - password=os.environ.get("KODAK_DB_MARIA_PASSWORD", cls.password), - port=int(os.environ.get("KODAK_DB_MARIA_PORT", cls.port)), - schema=os.getenv("KODAK_DB_MARIA_SCHEMA", cls.schema), + hostname=os.getenv("KODAK_DATABASE_MARIADB_HOSTNAME", cls.hostname), + username=os.getenv("KODAK_DATABASE_MARIADB_USERNAME", cls.username), + password=os.environ.get("KODAK_DATABASE_MARIADB_PASSWORD", cls.password), + port=_get_int("KODAK_DATABASE_MARIADB_PORT", cls.port), + schema=os.getenv("KODAK_DATABASE_MARIADB_SCHEMA", cls.schema), ) @@ -66,56 +89,86 @@ class DatabaseConfig: @classmethod def from_env(cls): return cls( - backend=constants.DatabaseBackend[os.environ["KODAK_DB_BACKEND"].upper()] - if "KODAK_DB_BACKEND" in os.environ - else cls.backend + backend=_get_enum_by_name( + "KODAK_DATABASE_BACKEND", constants.DatabaseBackend, cls.backend + ) + ) + + +@dataclass +class ManipCropConfig: + horizontal: Optional[int] = None + vertical: Optional[int] = None + anchor: constants.CropAnchor = constants.CropAnchor.C + + @classmethod + def from_env(cls, key: str): + return cls( + anchor=_get_enum_by_value( + f"KODAK_MANIP_{key}_CROP_ANCHOR", constants.CropAnchor, cls.anchor + ), + horizontal=_get_int(f"KODAK_MANIP_{key}_CROP_HORIZONTAL", cls.horizontal), + vertical=_get_int(f"KODAK_MANIP_{key}_CROP_VERTICAL", cls.vertical), + ) + + +@dataclass +class ManipScaleConfig: + horizontal: Optional[Union[int, float]] = None + vertical: Optional[Union[int, float]] = None + strategy: constants.ScaleStrategy = constants.ScaleStrategy.ABSOLUTE + + @classmethod + def from_env(cls, key: str): + strategy = _get_enum_by_name( + f"KODAK_MANIP_{key}_SCALE_STRATEGY", constants.ScaleStrategy, cls.strategy + ) + + if strategy == constants.ScaleStrategy.ABSOLUTE: + parser = _get_int + elif strategy == constants.ScaleStrategy.RELATIVE: + parser = _get_float + else: + raise RuntimeError("This path should not be possible") + + return cls( + strategy=strategy, + vertical=parser(f"KODAK_MANIP_{key}_SCALE_VERTICAL", cls.vertical), + horizontal=parser(f"KODAK_MANIP_{key}_SCALE_HORIZONTAL", cls.horizontal), ) @dataclass class ManipConfig: name: str - strategy: constants.DimensionStrategy = constants.DimensionStrategy.SCALE - anchor: constants.Anchor = constants.Anchor.C - formats: Sequence[constants.ImageFormat] = ( - constants.ImageFormat.JPEG, - constants.ImageFormat.PNG, + crop: ManipCropConfig = field(default_factory=ManipCropConfig.from_env) + scale: ManipScaleConfig = field(default_factory=ManipScaleConfig.from_env) + formats: Set[constants.ImageFormat] = field( + default_factory=lambda: constants.DEFAULT_SUPPORTED_FORMATS ) - horizontal: Optional[Union[int, float]] = None - vertical: Optional[Union[int, float]] = None + black_and_white: bool = False + # TODO: Implement support for these settings + # brightness: int = 0 + # contrast: int = 0 + # sepia: bool = False @classmethod def from_env(cls, key: str): - strategy = ( - constants.DimensionStrategy[ - os.environ[f"KODAK_MANIP_{key}_STRATEGY"].upper() - ] - if f"KODAK_MANIP_{key}_STRATEGY" in os.environ - else cls.strategy - ) - - dimension_conversion = ( - float if strategy == constants.DimensionStrategy.RELATIVE else int - ) - return cls( name=os.getenv(f"KODAK_MANIP_{key}_NAME", key.lower()), - strategy=strategy, - anchor=constants.Anchor(os.environ[f"KODAK_MANIP_{key}_ANCHOR"].lower()) - if f"KODAK_MANIP_{key}_ANCHOR" in os.environ - else cls.anchor, - formats=[ - constants.ImageFormat[item.upper()] - for item in os.environ[f"KODAK_MANIP_{key}_FORMATS"].split(",") - ] + crop=ManipCropConfig.from_env(key), + scale=ManipScaleConfig.from_env(key), + formats=set( + [ + constants.ImageFormat[item.strip().upper()] + for item in os.environ[f"KODAK_MANIP_{key}_FORMATS"].split(",") + ] + ) if f"KODAK_MANIP_{key}_FORMATS" in os.environ - else cls.formats, - horizontal=dimension_conversion(os.environ[f"KODAK_MANIP_{key}_HORIZONTAL"]) - if f"KODAK_MANIP_{key}_HORIZONTAL" in os.environ - else cls.horizontal, - vertical=dimension_conversion(os.environ[f"KODAK_MANIP_{key}_VERTICAL"]) - if f"KODAK_MANIP_{key}_VERTICAL" in os.environ - else cls.vertical, + else constants.DEFAULT_SUPPORTED_FORMATS, + black_and_white=_get_bool( + f"KODAK_MANIP_{key}_BLACK_AND_WHITE", cls.black_and_white + ), ) @@ -138,17 +191,10 @@ class KodakConfig: ] ) return cls( - sourcedir=Path(os.environ.get("KODAK_SOURCEDIR", cls.sourcedir)) - .expanduser() - .resolve(), - manipdir=Path(os.environ.get("KODAK_MANIPDIR", cls.manipdir)) - .expanduser() - .resolve(), - expose_source=os.getenv( - "KODAK_EXPOSE_SOURCE", str(cls.expose_source) - ).lower() - == "true", - private=os.getenv("KODAK_PRIVATE", str(cls.private)).lower() == "true", + sourcedir=_get_path("KODAK_SOURCEDIR", cls.sourcedir), + manipdir=_get_path("KODAK_MANIPDIR", cls.manipdir), + expose_source=_get_bool("KODAK_EXPOSE_SOURCE", cls.expose_source), + private=_get_bool("KODAK_PRIVATE", cls.private), manips={name.lower(): ManipConfig.from_env(name) for name in manip_names}, ) @@ -157,4 +203,4 @@ def load() -> KodakConfig: try: return KodakConfig.from_env() except (ValueError, TypeError, IndexError, KeyError) as err: - raise RuntimeError(err) + raise exceptions.ConfigurationError(f"Failed to load configuration: {err}") diff --git a/kodak/constants.py b/kodak/constants.py index 978e321..2b7652a 100644 --- a/kodak/constants.py +++ b/kodak/constants.py @@ -8,19 +8,17 @@ class DatabaseBackend(enum.Enum): SQLITE = peewee.SqliteDatabase -class DimensionStrategy(enum.Enum): - CROP = enum.auto() - SCALE = enum.auto() +class ScaleStrategy(enum.Enum): RELATIVE = enum.auto() + ABSOLUTE = enum.auto() class ImageFormat(enum.Enum): JPEG = enum.auto() PNG = enum.auto() - GIF = enum.auto() -class Anchor(enum.Enum): +class CropAnchor(enum.Enum): TL = "top-left" TC = "top-center" TR = "top-center" @@ -39,3 +37,5 @@ DEFAULT_SQLITE_PRAGMAS = { "ignore_check_constraints": 0, "synchronous": 0, } + +DEFAULT_SUPPORTED_FORMATS = {ImageFormat.JPEG, ImageFormat.PNG} diff --git a/kodak/exceptions.py b/kodak/exceptions.py index 416fc4f..19fa416 100644 --- a/kodak/exceptions.py +++ b/kodak/exceptions.py @@ -37,3 +37,7 @@ class ServerError(KodakException): class ImageFileRemovedError(ServerError): """Image file removed from server""" + + +class ConfigurationError(ServerError): + """Failed to load the application configuration"""