class Fermentation(ListTableBase): """Tabular definition for fermentation steps outlining the fermentation process.""" Columns = [ Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True), Column('startTemperature', 'Start Temp', editable=True), Column('endTemperature', 'End Temp', editable=True), Column('time', editable=True), ] # ====================================================================================================================== # Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Not supported for fermentable types - they don't get defined in the Excel database.""" raise NotImplementedError( 'Fermentation does not support loading library items from Excel worksheets.' ) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """Steps are sorted manually. Deliberately left blank - will be called but nothing will happen.""" # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert this fermentation into BeerJSON.""" return { 'name': 'Why is the name required at this level?', 'fermentation_steps': [step.to_dict() for step in self.items] } # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Convert a BeerJSON dict into values for this instance.""" self.items = [] for child in data['fermentation_steps']: step = FermentationStep(recipe) step.from_dict(child) self.append(step)
class Waters(ListTableBase): """Provides for a list of Water objects, specifically created to aid in parsing Excel database files and display within a QtTableView.""" Columns = [ Column('name', size=Stretch, align=QtCore.Qt.AlignLeft), Column('calcium'), Column('magnesium'), Column('sodium'), Column('chloride'), Column('sulfate'), Column('bicarbonate'), Column('ph', 'pH'), ] # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def calcium(self): """Calculate and return the total calcium in the water based upon the percentage of each water component.""" return sum([water.calcium * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def magnesium(self): """Calculate and return the total magnesium in the water based upon the percentage of each water component.""" return sum([water.magnesium * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def sodium(self): """Calculate and return the total sodium in the water based upon the percentage of each water component.""" return sum([water.sodium * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def chloride(self): """Calculate and return the total chloride in the water based upon the percentage of each water component.""" return sum([water.chloride * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def sulfate(self): """Calculate and return the total sulfate in the water based upon the percentage of each water component.""" return sum([water.sulfate * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def bicarbonate(self): """Calculate and return the total bicarbonate in the water based upon the percentage of each water component.""" return sum([water.bicarbonate * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def carbonate(self): """Calculate and return the total carbonate in the water based upon the percentage of each water component.""" return sum([water.carbonate * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def alkalinity(self): """Calculate and return the total alkalinity in the water based upon the percentage of each water component.""" return sum([water.alkalinity * water.percentage for water in self], ConcentrationType(0, 'ppm')) # ---------------------------------------------------------------------------------------------------------------------- @property def hardness(self): """Calculate and return the total harness in the water based upon the percentage of each water component.""" return sum([ water.hardness * water.percentage.percent / 100 for water in self ]) # ---------------------------------------------------------------------------------------------------------------------- @property def ph(self): """Calculate and return the total bicarbonate in the water based upon the percentage of each water component.""" return sum( [water.ph * water.percentage.percent / 100 for water in self]) # ====================================================================================================================== # Other Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Parses out a list of hop objects from the provided Excel worksheet and appends them to this instance.""" self.items = [] for idx, row in enumerate(worksheet): # Skip the header row. if idx == 0: continue self.append( Water(name=str(row[0].value), calcium=ConcentrationType(row[1].value, 'ppm'), magnesium=ConcentrationType(row[2].value, 'ppm'), sodium=ConcentrationType(row[3].value, 'ppm'), chloride=ConcentrationType(row[4].value, 'ppm'), sulfate=ConcentrationType(row[5].value, 'ppm'), bicarbonate=ConcentrationType(row[6].value, 'ppm'), ph=float(row[7].value), notes=str(row[8].value))) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """A void sort function that consistently sorts the water in decreasing order of amount in the recipe.""" def sorter(water: Water): """Give distilled water a higher priority in sorting than any other name.""" if 'distilled' in water.name.lower(): return 'A' return 'B' + water.name self.items.sort(key=sorter, reverse=True) # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert self into a dictionary for BeerJSON storage.""" return [water.to_dict() for water in self] # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Parse list of waters from the provided BeerJSON dict.""" for item in data: water = Water(recipe) water.from_dict(item) self.append(water)
class Hops(ListTableBase): """Provides for a list of Hop objects, specifically created to aid in parsing Excel database files and display within a QtTableView.""" Columns = [ Column('amount', editable=True, hideLimited=True), Column('timing.use', 'Use In', align=QtCore.Qt.AlignLeft, editable=True, hideLimited=True), Column('timing.duration', align=QtCore.Qt.AlignHCenter, editable=True, hideLimited=True), Column('_ibus', 'IBUs', template='%.1f IBUs', hideLimited=True), Column('name', size=Stretch, align=QtCore.Qt.AlignLeft), Column('htype', 'Type', align=QtCore.Qt.AlignLeft), Column('form'), Column('origin', align=QtCore.Qt.AlignHCenter), Column('alpha') ] # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def trubLoss(self): loss = 0 for hop in self.items: if 'Boil' not in hop.timing.use: continue if 'Leaf' in hop.form: loss += hop.amount.oz * 0.0625 elif 'Pellet' in hop.form: loss += hop.amount.oz * 0.025 return loss # ====================================================================================================================== # Other Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Parses out a list of hop objects from the provided Excel worksheet and appends them to this instance.""" self.items = [] for idx, row in enumerate(worksheet): # Skip the header row. if idx == 0: continue self.append( Hop(name=str(row[0].value), htype=str(row[1].value), form=str(row[2].value), alpha=PercentType(row[3].value, '%'), beta=PercentType(row[4].value, '%'), hsi=PercentType(row[5].value, '%'), origin=str(row[6].value), substitutes=str(row[7].value), humulene=PercentType(row[8].value, '%'), caryophyllene=PercentType(row[9].value, '%'), cohumulone=PercentType(row[10].value, '%'), myrcene=PercentType(row[11].value, '%'), notes=str(row[12].value))) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """A void sort function that consistently sorts the hop in decreasing order of amount in the recipe.""" uses = ['Mash', 'Boil', 'Fermentation'] self.items.sort(key=lambda hop: (uses.index(hop.timing.use), -hop. timing.duration.sec, hop.name)) # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert self into a dictionary for BeerJSON storage.""" return [hop.to_dict() for hop in self] # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Parse list of hops from the provided BeerJSON dict.""" for item in data: hop = Hop(recipe) hop.from_dict(item) self.append(hop)
class Miscellanea(ListTableBase): """Provides for a list of misc objects, specifically created to aid in parsing Excel database files and display within a QtTableView.""" Columns = [ Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True), Column('mtype', 'Type', align=QtCore.Qt.AlignCenter, editable=True), Column('useFor', editable=True), Column('amount', editable=True), Column('timing.use', editable=True), Column('timing.duration', editable=True), ] # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def salts(self): """Run through the items in this list a return a list of items that are marked as "water agents" without "acid" in there names.""" return [ item for item in self.items if item.mtype.lower() == 'water agent' and 'acid' not in item.name.lower() ] # ---------------------------------------------------------------------------------------------------------------------- @property def mashSalts(self): """Filter the salts down to just those in the mash.""" return [ salt for salt in self.salts if salt.timing.use.lower() == 'mash' ] # ---------------------------------------------------------------------------------------------------------------------- @property def kettleSalts(self): """Filter the salts down to just those in the kettle/boil.""" return [ salt for salt in self.salts if salt.timing.use.lower() == 'boil' ] # ---------------------------------------------------------------------------------------------------------------------- @property def acids(self): """Run through the items in this list a return a list of items that are marked as "water agents" with "acid" in there names.""" return [ item for item in self.items if item.mtype.lower() == 'water agent' and 'acid' in item.name.lower() ] # ====================================================================================================================== # Other Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Misc item are not defined in Excel libraries.""" raise NotImplementedError( 'Miscellaneous items are not defined in Excel libraries.') # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """A void sort function that consistently sorts the misc in decreasing order of amount in the recipe.""" self.items.sort(key=lambda misc: (-misc.amount.root, misc.name)) # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert self into a dictionary for BeerJSON storage.""" return [item.to_dict() for item in self] # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Parse list of miscellaneous items from the provided BeerJSON dict.""" for item in data: misc = Miscellaneous(recipe) misc.from_dict(item) self.append(misc)
class Cultures(ListTableBase): """Provides for a list of Culture objects, specifically created to aid in parsing Excel database files and display within a QtTableView.""" Columns = [ Column('amount', editable=True, hideLimited=True), Column('name', size=Stretch, align=QtCore.Qt.AlignLeft), Column('ctype', 'Type'), Column('form'), Column('producer'), Column('productId', 'Product'), ] # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def averageAttenuation(self): """Returns the maximum attenuation of all of the cultures in the collection.""" return max([culture.averageAttenuation for culture in self.items]) # ====================================================================================================================== # Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Dump any items currently associated with this instance and reload from the provided Excel worksheet.""" self.items = [] for idx, row in enumerate(worksheet): # Skip the header row. if idx == 0: continue self.append( Culture(name=str(row[0].value), ctype=str(row[1].value), form=str(row[2].value), producer=str(row[3].value), productId=str(row[4].value), attenuationRange=PercentRangeType( minimum=PercentType(row[5].value * 100, '%'), maximum=PercentType(row[6].value * 100, '%')), notes=str(row[11].value))) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """A void sort function that consistently sorts the culture in decreasing order of amount in the recipe.""" self.items.sort(key=lambda culture: culture.name) # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert self into a dictionary for BeerJSON storage.""" return [culture.to_dict() for culture in self] # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Parse list of cultures from the provided BeerJSON dict.""" for item in data: culture = Culture(recipe) culture.from_dict(item) self.append(culture)
class Fermentables(ListTableBase): """Provides for a list of Fermentable objects, specifically created to aid in parsing Excel database files and display within a QtTableView.""" Columns = [ Column('amount', editable=True, hideLimited=True), Column('proportion', hideLimited=True), Column('name', 'Grain/Fermentable', size=Stretch, align=QtCore.Qt.AlignLeft), Column('color'), Column('ftype', 'Type'), Column('group'), Column('producer'), Column('origin'), ] # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def mashedSugar(self): """Return the sucrose equivalent of all of the mashed ingredients.""" return sum([ item.sucrose for item in self.items if item.isMashed and not item.addAfterBoil ]) # ---------------------------------------------------------------------------------------------------------------------- @property def nonMashedSugar(self): """Returns the total sucrose equivalent of all the non-mashed ingredients.""" return sum([item.sucrose for item in self.items if not item.isMashed]) # ---------------------------------------------------------------------------------------------------------------------- @property def fermentableSugar(self): """Returns to equivalent amount of sucrose, in pounds, that is fermentable. Excludes ingredients like lactose and Splenda.""" return sum([item.sucrose for item in self.items if item.isFermentable]) # ---------------------------------------------------------------------------------------------------------------------- @property def nonFermentableSugar(self): """Returns to equivalent amount of sucrose, in pounds, that is NOT fermentable. This includes ingredients like lactose or Splenda.""" return sum( [item.sucrose for item in self.items if not item.isFermentable]) # ---------------------------------------------------------------------------------------------------------------------- @property def steepedSugar(self): """Returns the equivalent amount of sucrose that is steeped post boil, but where it is still "mashed" so Brewhouse efficiency still plays a factor.""" return sum([ item.sucrose for item in self.items if item.isMashed and item.addAfterBoil ]) # ---------------------------------------------------------------------------------------------------------------------- @property def lateAdditionSugar(self): """Returns the equivalent amount of sucrose that is added post boil, but where the efficiency isn't a factor.""" return sum([ item.sucrose for item in self.items if not item.isMashed and item.addAfterBoil ]) # ---------------------------------------------------------------------------------------------------------------------- @property def mashWeight(self) -> MassType: """Returns the combined weight of all of the mashed ingredients.""" return sum([item.amount for item in self.items if item.isMashed], MassType(0, 'lb')) # ---------------------------------------------------------------------------------------------------------------------- @property def mashBiFi(self): """A step in calculating the mash pH but also required for calculating the overall water chemistry.""" total = 0 for fermentable in self.items: total += fermentable.bi * fermentable.proportion.percent / 100 return total # ---------------------------------------------------------------------------------------------------------------------- @property def mashPh(self): """Calculates the distilled water mash pH for each mashed fermentable and returns the result.""" phiBiFi = 0 for fermentable in self.items: phiBiFi += fermentable.phi * fermentable.bi * fermentable.proportion.percent / 100 return phiBiFi / self.mashBiFi # ====================================================================================================================== # Other Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Parses out a list og Fermentable object and appends them to this instance.""" def float_or(value, fallback=None): """Return the provided value as a float or return fallback if the float conversion fails.""" try: return float(value) except TypeError: return fallback self.items = [] for idx, row in enumerate(worksheet): # Skip the header row. if idx == 0: continue self.append( Fermentable(name=str(row[0].value), ftype=str(row[1].value), group=str(row[2].value), producer=str(row[3].value), origin=str(row[4].value), fyield=PercentType(row[5].value, '%'), color=ColorType(row[6].value, 'SRM'), moisture=PercentType(row[7].value, '%'), diastaticPower=DiastaticPowerType( row[8].value, 'Lintner'), addAfterBoil=bool(row[9].value), mashed=bool(row[10].value), phi=float_or(row[11].value), bi=float_or(row[12].value), notes=str(row[13].value))) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """A void sort function that consistently sorts the fermentable in decreasing order of amount in the recipe.""" self.items.sort( key=lambda fermentable: (-fermentable.amount.lb, fermentable.name)) # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert self into a dictionary for BeerJSON storage.""" return [fermentable.to_dict() for fermentable in self] # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, recipe, data): """Parse list of fermentables from the provided BeerJSON dict.""" for item in data: fermentable = Fermentable(recipe) fermentable.from_dict(item) self.append(fermentable)
class Mash(ListTableBase): """Tabular definition for fermentation steps outlining the fermentation process.""" Columns = [ Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True), Column('mtype', 'Type', align=QtCore.Qt.AlignLeft, editable=True), Column('temperature', editable=True), Column('time', editable=True), Column('infusionTemperature'), Column('infusionVolume'), ] # ---------------------------------------------------------------------------------------------------------------------- def __init__(self, recipe): super().__init__() self.recipe = recipe self.changed.connect(self.recalculate) self._ambient = TemperatureType(70, 'F') self._ratio = SpecificVolumeType(1.25, 'qt/lb') # ====================================================================================================================== # Properties # ---------------------------------------------------------------------------------------------------------------------- @property def ambient(self): return self._ambient @ambient.setter def ambient(self, value): if self._ambient != value: self._ambient = value self.changed.emit() # ---------------------------------------------------------------------------------------------------------------------- @property def ratio(self): return self._ratio @ratio.setter def ratio(self, value): if self._ratio != value: self._ratio = value self.changed.emit() # ---------------------------------------------------------------------------------------------------------------------- @property def totalWater(self): """Calculates the total water required for all of the mashing steps.""" self.recalculate() return sum([ step.infusionVolume for step in self if step.infusionVolume is not None ], VolumeType(0, 'gal')) # ====================================================================================================================== # Methods # ---------------------------------------------------------------------------------------------------------------------- def from_excel(self, worksheet): """Not supported for fermentable types - they don't get defined in the Excel database.""" raise NotImplementedError( 'Mash does not support loading library items from Excel worksheets.' ) # ---------------------------------------------------------------------------------------------------------------------- def sort(self): """Steps are sorted manually. Deliberately left blank - will be called but nothing will happen.""" # ---------------------------------------------------------------------------------------------------------------------- def to_dict(self): """Convert this fermentation into BeerJSON.""" return { 'name': 'Why is the name required at this level?', 'grain_temperature': self.recipe.mash.ambient.to_dict(), 'mash_steps': [step.to_dict() for step in self.items] } # ---------------------------------------------------------------------------------------------------------------------- def from_dict(self, data): """Convert a BeerJSON dict into values for this instance.""" self.ambient = TemperatureType(json=data['grain_temperature']) if 'mash_steps' in data and data[ 'mash_steps'] and 'water_grain_ratio' in data['mash_steps'][0]: self.ratio = SpecificVolumeType( json=data['mash_steps'][0]['water_grain_ratio']) self.items = [] for child in data['mash_steps']: step = MashStep(self.recipe) step.from_dict(child) self.append(step) # ---------------------------------------------------------------------------------------------------------------------- def recalculate(self): """Recalculate the temperatures and volumes for the mash steps.""" # Can't do anything if there aren't any steps yet. if not self.items: return # Calculate the initial step without any previous step. previous = self.items[0].calculate(None) # Run through the middle steps (stop one short of the end). for step in self.items[1:-1]: # NOTE: This loop will not run if there are two or less steps in the mash. previous = step.calculate(previous) if len(self) > 1: # Run the final calculation as a special case because we need to get up to our target volume. self.items[-1].calculate(previous, final=True)