+++ /dev/null
-#!/usr/bin/python
-# -*- encoding: utf-8 -*-
-#
-# Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
-#
-# This file is part of Cookbook.
-#
-# Cookbook is free software: you can redistribute it and/or modify it
-# under the terms of the GNU General Public License as published by the
-# Free Software Foundation, either version 3 of the License, or (at your
-# option) any later version.
-#
-# Cookbook is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Cookbook. If not, see <http://www.gnu.org/licenses/>.
-
-"""Represent a cookbook and recipes with Python classes.
-"""
-
-from __future__ import with_statement
-
-import os
-import os.path
-import textwrap
-import types
-from urllib import quote_plus
-
-import yaml
-
-
-def string_for_yaml(unicode_):
- """
- >>> string_for_yaml(None)
- >>> string_for_yaml('all ascii')
- 'all ascii'
- >>> string_for_yaml(u'all ascii')
- 'all ascii'
- >>> string_for_yaml(u'½ ascii')
- u'\\xc2\\xbd ascii'
- """
- if unicode_ == None:
- return unicode_
- try:
- string = unicode_.encode('ascii')
- return string
- except UnicodeEncodeError:
- return unicode_
-
-def to_yaml_object(obj):
- """
- >>> to_yaml_object(None)
- >>> to_yaml_object('all ascii')
- 'all ascii'
- >>> to_yaml_object(u'all ascii')
- 'all ascii'
- >>> to_yaml_object('all ascii')
- 'all ascii'
- >>> to_yaml_object(u'½ ascii')
- u'\\xc2\\xbd ascii'
- >>> class x (object):
- ... def to_yaml(self):
- ... return 'to_yaml return value'
- >>> to_yaml_object(x())
- 'to_yaml return value'
- >>> to_yaml_object([u'all ascii', u'½ ascii'])
- ['all ascii', u'\\xc2\\xbd ascii']
- """
- if obj == None:
- return obj
- if hasattr(obj, 'to_yaml'):
- return obj.to_yaml()
- if type(obj) in types.StringTypes:
- return string_for_yaml(obj)
- if hasattr(obj, '__len__'):
- ret = []
- for item in obj:
- ret.append(to_yaml_object(item))
- return ret
- raise NotImplementedError(
- 'cannot convert %s to YAMLable dict:\n%s' % (type(obj), unicode(obj)))
-
-
-class Amount (object):
- """
- >>> import pprint
- >>> a = Amount(value='1', units='T.')
- >>> str(a)
- '1 T.'
- >>> pprint.pprint(a.to_yaml())
- {'alternatives': [], 'range_': None, 'units': 'T.', 'value': '1'}
- >>> x = Amount()
- >>> x.from_yaml(a.to_yaml())
- >>> str(x)
- '1 T.'
- >>> b = Amount(value='1')
- >>> str(b)
- '1'
- >>> pprint.pprint(b.to_yaml())
- {'alternatives': [], 'range_': None, 'units': None, 'value': '1'}
- >>> x.from_yaml(b.to_yaml())
- >>> str(x)
- '1'
- >>> c = Amount(value='15', units='mL', alternatives=[a])
- >>> str(c)
- '15 mL (1 T.)'
- >>> pprint.pprint(c.to_yaml())
- {'alternatives': [{'alternatives': [],
- 'range_': None,
- 'units': 'T.',
- 'value': '1'}],
- 'range_': None,
- 'units': 'mL',
- 'value': '15'}
- >>> x.from_yaml(c.to_yaml())
- >>> str(x)
- '15 mL (1 T.)'
- >>> d = Amount(units='T.', range_=['1','2'])
- >>> str(d)
- '1-2 T.'
- >>> pprint.pprint(d.to_yaml())
- {'alternatives': [], 'range_': ['1', '2'], 'units': 'T.', 'value': None}
- >>> x.from_yaml(d.to_yaml())
- >>> str(x)
- '1-2 T.'
- """
- def __init__(self, value=None, units=None, range_=None, alternatives=None):
- self.value = value
- self.units = units
- self.range_ = range_
- if alternatives == None:
- alternatives = []
- self.alternatives = alternatives
-
- def __str__(self):
- return str(self.__unicode__())
-
- def __unicode__(self):
- if self.range_ == None:
- value = self.value
- else:
- value = '-'.join(self.range_)
- ret = [value]
- if self.units != None:
- ret.append(self.units)
- if len(self.alternatives) > 0:
- ret.append('(%s)' % ', '.join(
- [unicode(a) for a in self.alternatives]))
- return ' '.join([x for x in ret if x != None])
-
- def to_yaml(self):
- d = {}
- for key in ['value', 'range_', 'units', 'alternatives']:
- d[key] = to_yaml_object(getattr(self, key))
- return d
-
- def from_yaml(self, dict):
- for key in ['value', 'range_', 'units']:
- setattr(self, key, dict.get(key, None))
- self.alternatives = []
- for a in dict.get('alternatives', []):
- amount = Amount()
- amount.from_yaml(a)
- self.alternatives.append(amount)
-
-
-class Ingredient (object):
- """
- >>> import pprint
- >>> i = Ingredient('eye of newt', Amount('1'))
- >>> str(i)
- '1 eye of newt'
- >>> pprint.pprint(i.to_yaml())
- {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '1'},
- 'name': 'eye of newt',
- 'note': None}
- >>> x = Ingredient()
- >>> x.from_yaml(i.to_yaml())
- >>> str(x)
- '1 eye of newt'
- >>> j = Ingredient('salamanders', Amount('2'), 'diced fine')
- >>> str(j)
- '2 salamanders, diced fine'
- >>> pprint.pprint(j.to_yaml())
- {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '2'},
- 'name': 'salamanders',
- 'note': 'diced fine'}
- >>> x.from_yaml(j.to_yaml())
- >>> str(x)
- '2 salamanders, diced fine'
- """
- def __init__(self, name=None, amount=None, note=None):
- self.name = name
- self.amount = amount
- self.note = note
-
- def __str__(self):
- return str(self.__unicode__())
-
- def __unicode__(self):
- if self.note == None:
- return '%s %s' % (unicode(self.amount or ''), self.name)
- return '%s %s, %s' % (unicode(self.amount or ''), self.name, self.note)
-
- def to_yaml(self):
- d = {}
- for key in ['name', 'amount', 'note']:
- d[key] = to_yaml_object(getattr(self, key))
- return d
-
- def from_yaml(self, dict):
- for key in ['name', 'note']:
- setattr(self, key, dict.get(key, None))
- self.amount = Amount()
- self.amount.from_yaml(dict.get('amount', {}))
-
-
-class IngredientBlock (list):
- """
- >>> import pprint
- >>> ib = IngredientBlock(None, [
- ... Ingredient('eye of newt', Amount('1')),
- ... Ingredient('salamanders', Amount('2'), 'diced fine')])
- >>> print str(ib)
- Ingredients:
- 1 eye of newt
- 2 salamanders, diced fine
- >>> pprint.pprint(ib.to_yaml())
- {'ingredients': [{'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '1'},
- 'name': 'eye of newt',
- 'note': None},
- {'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '2'},
- 'name': 'salamanders',
- 'note': 'diced fine'}],
- 'name': None}
- >>> x = IngredientBlock()
- >>> x.from_yaml(ib.to_yaml())
- >>> print str(x)
- Ingredients:
- 1 eye of newt
- 2 salamanders, diced fine
- >>> ib.name = 'Dressing'
- >>> print str(ib)
- Dressing:
- 1 eye of newt
- 2 salamanders, diced fine
- >>> pprint.pprint(ib.to_yaml())
- {'ingredients': [{'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '1'},
- 'name': 'eye of newt',
- 'note': None},
- {'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '2'},
- 'name': 'salamanders',
- 'note': 'diced fine'}],
- 'name': 'Dressing'}
- >>> x = IngredientBlock()
- >>> x.from_yaml(ib.to_yaml())
- >>> print str(x)
- Dressing:
- 1 eye of newt
- 2 salamanders, diced fine
- """
- def __init__(self, name=None, *args, **kwargs):
- self.name = name
- super(IngredientBlock, self).__init__(*args, **kwargs)
-
- def heading(self):
- if self.name == None:
- return 'Ingredients'
- return self.name
-
- def __str__(self):
- return str(self.__unicode__())
-
- def __unicode__(self):
- ret = [unicode(i) for i in self]
- ret.insert(0, '%s:' % self.heading())
- return '\n '.join(ret)
-
- def to_yaml(self):
- d = {}
- for key in ['name']:
- d[key] = to_yaml_object(getattr(self, key))
- d['ingredients'] = to_yaml_object(list(self))
- return d
-
- def from_yaml(self, dict):
- self.name = dict.get('name', None)
- while len(self) > 0:
- self.pop()
- for i in dict.get('ingredients', []):
- ingredient = Ingredient()
- ingredient.from_yaml(i)
- self.append(ingredient)
-
-
-class Directions (list):
- """
- >>> import pprint
- >>> d = Directions(['paragraph 1', 'paragraph 2', 'paragraph 3'])
- >>> print str(d)
- paragraph 1
- <BLANKLINE>
- paragraph 2
- <BLANKLINE>
- paragraph 3
- >>> pprint.pprint(d.to_yaml())
- 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
- >>> x = Directions()
- >>> x.from_yaml(d.to_yaml())
- >>> print str(x)
- paragraph 1
- <BLANKLINE>
- paragraph 2
- <BLANKLINE>
- paragraph 3
- >>> pprint.pprint(x.to_yaml())
- 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
- """
- def __str__(self):
- return str(self.__unicode__())
-
- def __unicode__(self):
- return '\n\n'.join(self)
-
- def wrapped_paragraphs(self, *args, **kwargs):
- return ['\n'.join(
- textwrap.wrap(
- paragraph,
- *args,
- **kwargs))
- for paragraph in self]
-
- def wrap(self, *args, **kwargs):
- return '\n\n'.join(self.wrapped_paragraphs(*args, **kwargs))
-
- def to_yaml(self):
- return string_for_yaml('\n\n'.join([paragraph.rstrip('\n')
- for paragraph in self]))
-
- def from_yaml(self, string):
- if string == None:
- return
- while len(self) > 0:
- self.pop()
- for paragraph in string.split('\n\n'):
- self.append(paragraph)
-
-
-class Recipe (object):
- """
- >>> import pprint
- >>> r = Recipe('Hot Newt Dressing',
- ... [IngredientBlock(None, [
- ... Ingredient('eye of newt', Amount('1')),
- ... Ingredient('salamanders', Amount('2'), 'diced fine')])],
- ... Directions(['Mix ingredients until salamander starts to smoke',
- ... 'Serve warm over rice']),
- ... yield_='enough for one apprentice',
- ... author='Merlin',
- ... source='CENSORED',
- ... url='http://merlin.uk/recipes/1234/',
- ... tags=['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'])
- >>> print str(r)
- Recipe: Hot Newt Dressing
- Yield: enough for one apprentice
- From: Merlin
- Source: CENSORED
- URL: http://merlin.uk/recipes/1234/
- Ingredients:
- 1 eye of newt
- 2 salamanders, diced fine
- Mix ingredients until salamander starts to smoke
- <BLANKLINE>
- Serve warm over rice
- >>> r.matches_tags(None)
- True
- >>> r.matches_tags([])
- True
- >>> r.matches_tags(['dinner', 'apprentice'])
- True
- >>> r.matches_tags(['dinner', 'dragon'])
- False
- >>> pprint.pprint(r.to_yaml())
- {'author': 'Merlin',
- 'directions': 'Mix ingredients until salamander starts to smoke\\n\\nServe warm over rice',
- 'ingredient_blocks': [{'ingredients': [{'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '1'},
- 'name': 'eye of newt',
- 'note': None},
- {'amount': {'alternatives': [],
- 'range_': None,
- 'units': None,
- 'value': '2'},
- 'name': 'salamanders',
- 'note': 'diced fine'}],
- 'name': None}],
- 'name': 'Hot Newt Dressing',
- 'source': 'CENSORED',
- 'tags': ['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'],
- 'url': 'http://merlin.uk/recipes/1234/',
- 'yield_': 'enough for one apprentice'}
- >>> x = Recipe()
- >>> x.from_yaml(r.to_yaml())
- >>> print str(x)
- Recipe: Hot Newt Dressing
- Yield: enough for one apprentice
- From: Merlin
- Source: CENSORED
- URL: http://merlin.uk/recipes/1234/
- Ingredients:
- 1 eye of newt
- 2 salamanders, diced fine
- Mix ingredients until salamander starts to smoke
- <BLANKLINE>
- Serve warm over rice
- """
- def __init__(self, name=None, ingredient_blocks=None, directions=None,
- yield_=None, author=None, source=None, url=None, tags=None):
- self.name = name
- self.ingredient_blocks = ingredient_blocks
- self.directions = directions
- self.yield_ = yield_
- self.author = author
- self.source = source
- self.url = url
- self.tags = tags
- self.path = None
-
- def clean_name(self, ascii=False):
- name = self.name
- for from_,to in [(' ','_'), ('/', '_'), (',', ''), ('&', 'and')]:
- name = name.replace(from_, to)
- if ascii == True:
- return quote_plus(name.encode('utf-8'))
- return name
-
- def matches_tags(self, tags):
- """Return True if this recipe is tagges with each of the tags in tags.
- """
- if tags in [None, []]:
- return True
- if self.tags == None:
- return False
- for t in tags:
- if t not in self.tags:
- return False
- return True
-
- def __str__(self):
- return str(self.__unicode__())
-
- def __unicode__(self):
- return '\n'.join([
- 'Recipe: %s' % self.name,
- 'Yield: %s' % self.yield_,
- 'From: %s' % self.author,
- 'Source: %s' % self.source,
- 'URL: %s' % self.url,
- '\n'.join([unicode(ib) for ib in self.ingredient_blocks]),
- unicode(self.directions),
- ])
-
- def to_yaml(self):
- d = {}
- for key in ['name', 'ingredient_blocks', 'directions', 'yield_',
- 'author', 'source', 'url', 'tags']:
- d[key] = to_yaml_object(getattr(self, key))
- return d
-
- def from_yaml(self, dict):
- for key in ['name', 'yield_', 'author', 'source', 'url', 'tags']:
- setattr(self, key, dict.get(key, None))
- self.ingredient_blocks = []
- for ib in dict.get('ingredient_blocks', []):
- ingredient_block = IngredientBlock()
- ingredient_block.from_yaml(ib)
- self.ingredient_blocks.append(ingredient_block)
- self.directions = Directions()
- self.directions.from_yaml(dict.get('directions', None))
-
- def save(self, stream, path=None):
- yaml.dump(self.to_yaml(), stream,
- default_flow_style=False, allow_unicode=True, width=78)
- if path != None:
- self.path = path
-
- def load(self, stream, path=None):
- dict = yaml.load(stream)
- self.from_yaml(dict)
- if path != None:
- self.path = path
-
-
-class Cookbook (list):
- """
- TODO: doctests
- """
- def __init__(self, name="Mom's cookbook", *args, **kwargs):
- self.name = name
- self.save_dir = u'recipe'
- super(Cookbook, self).__init__(*args, **kwargs)
-
- def save(self):
- if not os.path.isdir(self.save_dir):
- os.mkdir(self.save_dir)
- paths = []
- for recipe in self:
- p = self._free_path(recipe, paths)
- paths.append(p)
- with open(p, 'w') as f:
- recipe.save(f, p)
-
- def load(self):
- for path in sorted(os.listdir(self.save_dir)):
- r = Recipe()
- p = os.path.join(self.save_dir, path)
- with open(p, 'r') as f:
- r.load(f, p)
- self.append(r)
-
- def make_index(self):
- self.index = {}
- paths = [recipe.path for recipe in self]
- for recipe in self:
- self.index[recipe.name] = recipe
- self.index[recipe.clean_name()] = recipe
- self.index[recipe.clean_name(ascii=True)] = recipe
- if recipe.path == None:
- p = self._free_path(recipe, paths)
- paths.append(p)
- recipe.path = p
-
- def _free_path(self, recipe, paths):
- base_path = os.path.join(self.save_dir, recipe.clean_name())
- path = base_path
- i = 2
- while path in paths:
- path = '%s_%d' % (base_path, i)
- i += 1
- return path
-
- def tags(self):
- """List all tags used in this cookbook.
- """
- tags = set()
- for recipe in self:
- if recipe.tags != None:
- tags = tags.union(set(recipe.tags))
- return sorted(tags)
-
- def tagged(self, tags=None):
- """Iterate through all recipes matching the given list of tags.
- """
- for recipe in self:
- if recipe.matches_tags(tags):
- yield recipe
-
-
-def test():
- import doctest
- doctest.testmod()