HOP_TYPE_PELLET = u"pellet" IMPERIAL_TYPES = {
    u"volume": u"gallon",
    u"weight_large": u"lbs",
    u"weight_small": u"oz",
    u"temperature": u"degF",
} IMPERIAL_UNITS = u"imperial" MG_PER_OZ = 28349.5 OZ_PER_MG = 1.0 / MG_PER_OZ 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 HopException(BrewdayException):
    pass class HopsUtilizationGlennTinseth(HopsUtilization):
    """
    Glenn Tinseth Hops Utilization Method

    Best for all grain brewing.
    """

    def __str__(self):
        return u"Glenn Tinseth"

    @classmethod
    def get_bigness_factor(cls, sg):
        """
        Get Bigness Factor

        :param float sg: Specific Gravity
        :return: Bigness Factor
        :rtype: float

        The Bigness factor accounts for reduced utilization due to higher wort
        gravities. Use an average gravity value for the entire boil to account
        for changes in the wort volume.

        :math:`\\text{Bigness factor} = 1.65 \\times 0.000125^{\\text{wort gravity} - 1}`
        """  # noqa
        return 1.65 * 0.000125 ** (sg - 1)

    @classmethod
    def get_boil_time_factor(cls, boil_time):
        """
        Get Boil Time Factor

        :param float boil_time: Boil Time in minutes
        :return: Boil Time Factor
        :rtype: float

        The Boil Time factor accounts for the change in utilization due to
        boil time:

        :math:`\\text{Boil Time factor} = \\frac{1 - e^{-0.04 \\times \\text{time in mins}}}{4.15}`
        """  # noqa
        return (1 - math.exp(-0.04 * boil_time)) / 4.15

    @classmethod
    def get_percent_utilization(cls, sg, boil_time):
        """
        Get the percent utilization

        :param float sg: Specific Gravity
        :param float boil_time: The Boil Time in minutes
        :return: The percent utilization
        :rtype: float

        Source:

        * http://www.realbeer.com/hops/research.html
        * http://www.rooftopbrew.net/ibu.php
        """
        bigness_factor = cls.get_bigness_factor(sg)
        boil_time_factor = cls.get_boil_time_factor(boil_time)
        return bigness_factor * boil_time_factor def validate_hop_type(hop_type):
    """
    Validate a hop type

    :param str hop_type: Type of Grain
    :return: hop type
    :rtype: str
    :raises ValidatorException: If hop type is unknown
    """
    if hop_type in HOP_TYPE_LIST:
        return hop_type
    raise ValidatorException(
        u"Unkown hop type '{}', must use {}".format(hop_type, u", ".join(HOP_TYPE_LIST))
    ) 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 HOP_TYPE_PELLET
from .constants import IMPERIAL_TYPES
from .constants import IMPERIAL_UNITS
from .constants import MG_PER_OZ
from .constants import OZ_PER_MG
from .constants import SI_TYPES
from .constants import SI_UNITS
from .constants import WEIGHT_TOLERANCE
from .exceptions import HopException
from .utilities.hops import HopsUtilizationGlennTinseth
from .validators import validate_hop_type
from .validators import validate_optional_fields
from .validators import validate_percentage
from .validators import validate_required_fields
from .validators import validate_units
# -*- coding: utf-8 -*-


__all__ = [u"Hop", u"HopAddition"]


class Hop(object):
    """
    A representation of a type of Hop.
    """

    def __init__(self, name, percent_alpha_acids=None):
        """
        :param str name: The name of the hop
        :param float percent_alpha_acids: The percent alpha acids in the hop
        :raises HopException: If percent_alpha_acids is not provided
        """
        self.name = name
        if percent_alpha_acids is None:
            raise HopException(
                u"{}: Must provide percent alpha acids".format(self.name)
            )
        self.percent_alpha_acids = validate_percentage(percent_alpha_acids)

    def __str__(self):
        if sys.version_info[0] >= 3:
            return self.__unicode__()
        else:
            return self.__unicode__().encode(u"utf8")

    def __unicode__(self):
        return u"{0}, alpha {1:0.1%}".format(
            self.name.capitalize(), self.percent_alpha_acids
        )

    def __repr__(self):
        out = u"{0}('{1}'".format(type(self).__name__, self.name)
        if self.percent_alpha_acids:
            out = u"{0}, percent_alpha_acids={1}".format(out, self.percent_alpha_acids)
        out = u"{0})".format(out)
        return out

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        if (self.name == other.name) and (
            self.percent_alpha_acids == other.percent_alpha_acids
        ):
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def to_dict(self):
        return {
            u"name": self.name,
            u"percent_alpha_acids": round(self.percent_alpha_acids, 3),
        }

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

    def format(self):
        msg = textwrap.dedent(
            u"""\
                {name} Hops
                -----------------------------------
                Alpha Acids:  {percent_alpha_acids:0.1%}""".format(
                **self.to_dict()
            )
        )
        return msg


class HopAddition(object):
    """
    A representation of the Hop as added to a Recipe.
    """

    def __init__(
        self,
        hop,
        weight=None,
        boil_time=None,
        hop_type=HOP_TYPE_PELLET,

        utilization_cls=HopsUtilizationGlennTinseth,