GRAIN_TYPE_CEREAL = u"cereal" GRAIN_TYPE_DME = u"dme" GRAIN_TYPE_LME = u"lme" IMPERIAL_TYPES = {
    u"volume": u"gallon",
    u"weight_large": u"lbs",
    u"weight_small": u"oz",
    u"temperature": u"degF",
} IMPERIAL_UNITS = u"imperial" KG_PER_POUND = 0.453592 POUND_PER_KG = 1.0 / KG_PER_POUND PPG_CEREAL = 30.0 PPG_DME = 44.0 PPG_LME = 36.0 SI_TYPES = {
    u"volume": u"liter",
    u"weight_large": u"kg",
    u"weight_small": u"mg",
    u"temperature": u"degC",
} SI_UNITS = u"metric" WEIGHT_TOLERANCE = 0.005 class GrainException(BrewdayException):
    pass def hwe_to_basis(hwe):
    """
    Hot Water Extract to Basis Percentage

    :param float hwe: Hot Water Extract as Ldeg/kg, dry basis
    :return: Basis as percentage
    :rtype: float
    """
    return hwe / LITERS_OF_WORT_AT_SG def hwe_to_ppg(hwe):
    """
    Hot Water Extract to Points Per Gallon

    :param float hwe: Hot Water Extract
    :return: Points Per Gallon
    :rtype: float
    """
    return hwe / PPG_TO_HWE_CONVERSION def ppg_to_hwe(ppg):
    """
    Points Per Gallon to Hot Water Extract

    :param float ppg: Points Per Gallon
    :return: Hot Water Extract
    :rtype: float
    """
    return ppg * PPG_TO_HWE_CONVERSION def validate_optional_fields(data, optional_fields, data_field=u"data"):
    """
    Validate fields which are optional as part of the data.

    :param dict data: A python dictionary to check for required fields
    :param list(tuple) optional_fields: Values and types to check for in data
    :param str data_field: The key in the data dictionary containing the optional fields
    :raises ValidatorException: Optional field is of the wrong type

    The format is a list of tuples where the first element is a string with
    a value that should be a key found in the data dict and
    where the second element is a python type or list/tuple of
    python types to check the field against.
    """  # noqa
    # If no optional data field present then return
    if data_field not in data:
        return
    for field, field_type in optional_fields:
        if field in data[data_field]:
            if field_type == str:
                try:
                    field_type = unicode
                except NameError:
                    field_type = str
            # With optional fields only check the type as they are overrides
            # and not all overrides need to be present
            if not isinstance(data[data_field][field], field_type):
                raise ValidatorException(
                    u"Optional field '{}' in '{}' is not of type '{}'".format(  # noqa
                        field, data_field, field_type
                    )
                ) def validate_percentage(percent):
    """
    Validate decimal percentage

    :param float percent: Percentage between 0.0 and 1.0
    :return: percentage
    :rtype: float
    :raises ValidatorException: If decimal percentage not between 0.0 and 1.0
    """
    if 0.0 <= percent <= 1.0:
        return percent
    raise ValidatorException(u"Percentage values should be in decimal format") def validate_required_fields(data, required_fields):
    """
    Validate fields which are required as part of the data.

    :param dict data: A python dictionary to check for required fields
    :param list(tuple) required_fields: Values and types to check for in data
    :raises ValidatorException: Required field is missing from data
    :raises ValidatorException: Required field is of the wrong type

    The format is a list of tuples where the first element is a string with
    a value that should be a key found in the data dict and
    where the second element is a python type or list/tuple of
    python types to check the field against.
    """
    for field, field_type in required_fields:
        if field not in data:
            raise ValidatorException(
                u"Required field '{}' missing from data".format(field)  # noqa
            )
        if field_type == str:
            try:
                field_type = unicode
            except NameError:
                field_type = str
        if not isinstance(data[field], field_type):
            raise ValidatorException(
                u"Required field '{}' is not of type '{}'".format(  # noqa
                    field, field_type
                )
            ) def validate_units(units):
    """
    Validate units

    :param str units: Unit type
    :return: units
    :rtype: str
    :raises ValidatorException: If units is unknown
    """
    if units in [IMPERIAL_UNITS, SI_UNITS]:
        return units
    raise ValidatorException(
        u"Unkown units '{}', must use {} or {}".format(units, IMPERIAL_UNITS, SI_UNITS)
    )
import json
import sys
import textwrap
from .constants import GRAIN_TYPE_CEREAL
from .constants import GRAIN_TYPE_DME
from .constants import GRAIN_TYPE_LME
from .constants import IMPERIAL_TYPES
from .constants import IMPERIAL_UNITS
from .constants import KG_PER_POUND
from .constants import POUND_PER_KG
from .constants import PPG_CEREAL
from .constants import PPG_DME
from .constants import PPG_LME
from .constants import SI_TYPES
from .constants import SI_UNITS
from .constants import WEIGHT_TOLERANCE
from .exceptions import GrainException
from .utilities.malt import hwe_to_basis
from .utilities.malt import hwe_to_ppg
from .utilities.malt import ppg_to_hwe
from .validators import validate_optional_fields
from .validators import validate_percentage
from .validators import validate_required_fields
from .validators import validate_units
        :rtype: dict
        """
        return {
            u"grain_weight": round(self.get_cereal_weight(), 2),
            u"lme_weight": round(self.get_lme_weight(), 2),
            u"dry_weight": round(self.get_dme_weight(), 2),
        }

    def convert_to_cereal(self, ppg=None, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_CEREAL

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_CEREAL
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_CEREAL:
            return self
        if not ppg:
            raise GrainException("Must provide PPG to convert to cereal")

        validate_percentage(brew_house_yield)
        new_grain = self.grain.convert_to_cereal(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg

        # When converting away from cereal BHY works in reverse
        weight = self.weight * ppg_factor / brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_CEREAL, units=self.units
        )

    def convert_to_lme(self, ppg=PPG_LME, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_LME

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_LME
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_LME:
            return self

        # BHY applies to cereal grains
        validate_percentage(brew_house_yield)
        if self.grain_type == GRAIN_TYPE_DME:
            brew_house_yield = 1.0
        new_grain = self.grain.convert_to_lme(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg
        weight = self.weight * ppg_factor * brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_LME, units=self.units
        )

    def convert_to_dme(self, ppg=PPG_DME, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_DME

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_DME
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_DME:
            return self

        # BHY applies to cereal grains
        validate_percentage(brew_house_yield)
        if self.grain_type == GRAIN_TYPE_LME:
            brew_house_yield = 1.0
        new_grain = self.grain.convert_to_dme(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg
        weight = self.weight * ppg_factor * brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_DME, units=self.units
        )

    @property
    def gu(self):
        return self.get_gravity_units()

    def get_gravity_units(self):
        """
        Get the gravity units for the Grain Addition
        :return: Gravity Units as PPG or HWE depending on units
        :rtype: float
        """
        # Pick the attribute based on units
        if self.units == IMPERIAL_UNITS:
            attr = u"ppg"
        if self.units == SI_UNITS:
            attr = u"hwe"

        return getattr(self.grain, attr) * self.weight

    def to_dict(self):
        grain_data = self.grain.to_dict()
        return {
            u"name": grain_data.pop("name"),
            u"data": grain_data,
            u"weight": round(self.weight, 2),
            u"grain_type": self.grain_type,
            u"units": self.units,
        }

    def to_json(self):
        return json.dumps(self.to_dict(), sort_keys=True)

    @classmethod
    def validate(cls, grain_data):
        required_fields = [(u"name", str), (u"weight", float)]
        optional_fields = [
            (u"color", (int, float)),
            (u"ppg", (int, float)),
            (u"hwe", (int, float)),
            (u"grain_type", str),
            (u"units", str),
        ]
        validate_required_fields(grain_data, required_fields)

        validate_optional_fields(grain_data, optional_fields)