Reimplement manip configuration settings

Add support for future color changes
Once and for all settle how crop/scaling will work
Add proper exceptions
This commit is contained in:
Ethan Paul 2021-10-29 01:32:04 -04:00
parent 67abbd5374
commit dff79571ba
No known key found for this signature in database
GPG Key ID: D0E2CBF1245E92BF
3 changed files with 119 additions and 69 deletions

View File

@ -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}")

View File

@ -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}

View File

@ -37,3 +37,7 @@ class ServerError(KodakException):
class ImageFileRemovedError(ServerError):
"""Image file removed from server"""
class ConfigurationError(ServerError):
"""Failed to load the application configuration"""