2 # -*- encoding: utf-8 -*-
4 # Copyright (C) 2010 W. Trevor King <wking@drexel.edu>
6 # This file is part of Cookbook.
8 # Cookbook is free software: you can redistribute it and/or modify it
9 # under the terms of the GNU General Public License as published by the
10 # Free Software Foundation, either version 3 of the License, or (at your
11 # option) any later version.
13 # Cookbook is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with Cookbook. If not, see <http://www.gnu.org/licenses/>.
21 """Represent a cookbook and recipes with Python classes.
24 from __future__ import with_statement
34 def string_for_yaml(unicode_):
36 >>> string_for_yaml(None)
37 >>> string_for_yaml('all ascii')
39 >>> string_for_yaml(u'all ascii')
41 >>> string_for_yaml(u'½ ascii')
47 string = unicode_.encode('ascii')
49 except UnicodeEncodeError:
52 def to_yaml_object(obj):
54 >>> to_yaml_object(None)
55 >>> to_yaml_object('all ascii')
57 >>> to_yaml_object(u'all ascii')
59 >>> to_yaml_object('all ascii')
61 >>> to_yaml_object(u'½ ascii')
64 ... def to_yaml(self):
65 ... return 'to_yaml return value'
66 >>> to_yaml_object(x())
67 'to_yaml return value'
68 >>> to_yaml_object([u'all ascii', u'½ ascii'])
69 ['all ascii', u'\\xc2\\xbd ascii']
73 if hasattr(obj, 'to_yaml'):
75 if type(obj) in types.StringTypes:
76 return string_for_yaml(obj)
77 if hasattr(obj, '__len__'):
80 ret.append(to_yaml_object(item))
82 raise NotImplementedError(
83 'cannot convert %s to YAMLable dict:\n%s' % (type(obj), unicode(obj)))
86 class Amount (object):
89 >>> a = Amount(value='1', units='T.')
92 >>> pprint.pprint(a.to_yaml())
93 {'alternatives': [], 'range_': None, 'units': 'T.', 'value': '1'}
95 >>> x.from_yaml(a.to_yaml())
98 >>> b = Amount(value='1')
101 >>> pprint.pprint(b.to_yaml())
102 {'alternatives': [], 'range_': None, 'units': None, 'value': '1'}
103 >>> x.from_yaml(b.to_yaml())
106 >>> c = Amount(value='15', units='mL', alternatives=[a])
109 >>> pprint.pprint(c.to_yaml())
110 {'alternatives': [{'alternatives': [],
117 >>> x.from_yaml(c.to_yaml())
120 >>> d = Amount(units='T.', range_=['1','2'])
123 >>> pprint.pprint(d.to_yaml())
124 {'alternatives': [], 'range_': ['1', '2'], 'units': 'T.', 'value': None}
125 >>> x.from_yaml(d.to_yaml())
129 def __init__(self, value=None, units=None, range_=None, alternatives=None):
133 if alternatives == None:
135 self.alternatives = alternatives
138 return str(self.__unicode__())
140 def __unicode__(self):
141 if self.range_ == None:
144 value = '-'.join(self.range_)
146 if self.units != None:
147 ret.append(self.units)
148 if len(self.alternatives) > 0:
149 ret.append('(%s)' % ', '.join(
150 [unicode(a) for a in self.alternatives]))
155 for key in ['value', 'range_', 'units', 'alternatives']:
156 d[key] = to_yaml_object(getattr(self, key))
159 def from_yaml(self, dict):
160 for key in ['value', 'range_', 'units']:
161 setattr(self, key, dict.get(key, None))
162 self.alternatives = []
163 for a in dict.get('alternatives', []):
166 self.alternatives.append(amount)
169 class Ingredient (object):
172 >>> i = Ingredient('eye of newt', Amount('1'))
175 >>> pprint.pprint(i.to_yaml())
176 {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '1'},
177 'name': 'eye of newt',
180 >>> x.from_yaml(i.to_yaml())
183 >>> j = Ingredient('salamanders', Amount('2'), 'diced fine')
185 '2 salamanders, diced fine'
186 >>> pprint.pprint(j.to_yaml())
187 {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '2'},
188 'name': 'salamanders',
189 'note': 'diced fine'}
190 >>> x.from_yaml(j.to_yaml())
192 '2 salamanders, diced fine'
194 def __init__(self, name=None, amount=None, note=None):
200 return str(self.__unicode__())
202 def __unicode__(self):
203 if self.note == None:
204 return '%s %s' % (unicode(self.amount), self.name)
205 return '%s %s, %s' % (unicode(self.amount), self.name, self.note)
209 for key in ['name', 'amount', 'note']:
210 d[key] = to_yaml_object(getattr(self, key))
213 def from_yaml(self, dict):
214 for key in ['name', 'note']:
215 setattr(self, key, dict.get(key, None))
216 self.amount = Amount()
217 self.amount.from_yaml(dict.get('amount', {}))
220 class IngredientBlock (list):
223 >>> ib = IngredientBlock(None, [
224 ... Ingredient('eye of newt', Amount('1')),
225 ... Ingredient('salamanders', Amount('2'), 'diced fine')])
229 2 salamanders, diced fine
230 >>> pprint.pprint(ib.to_yaml())
231 {'ingredients': [{'amount': {'alternatives': [],
235 'name': 'eye of newt',
237 {'amount': {'alternatives': [],
241 'name': 'salamanders',
242 'note': 'diced fine'}],
244 >>> x = IngredientBlock()
245 >>> x.from_yaml(ib.to_yaml())
249 2 salamanders, diced fine
250 >>> ib.name = 'Dressing'
254 2 salamanders, diced fine
255 >>> pprint.pprint(ib.to_yaml())
256 {'ingredients': [{'amount': {'alternatives': [],
260 'name': 'eye of newt',
262 {'amount': {'alternatives': [],
266 'name': 'salamanders',
267 'note': 'diced fine'}],
269 >>> x = IngredientBlock()
270 >>> x.from_yaml(ib.to_yaml())
274 2 salamanders, diced fine
276 def __init__(self, name=None, *args, **kwargs):
278 super(IngredientBlock, self).__init__(*args, **kwargs)
281 if self.name == None:
286 return str(self.__unicode__())
288 def __unicode__(self):
289 ret = [unicode(i) for i in self]
290 ret.insert(0, '%s:' % self.heading())
291 return '\n '.join(ret)
296 d[key] = to_yaml_object(getattr(self, key))
297 d['ingredients'] = to_yaml_object(list(self))
300 def from_yaml(self, dict):
301 self.name = dict.get('name', None)
304 for i in dict.get('ingredients', []):
305 ingredient = Ingredient()
306 ingredient.from_yaml(i)
307 self.append(ingredient)
310 class Directions (list):
313 >>> d = Directions(['paragraph 1', 'paragraph 2', 'paragraph 3'])
320 >>> pprint.pprint(d.to_yaml())
321 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
323 >>> x.from_yaml(d.to_yaml())
330 >>> pprint.pprint(x.to_yaml())
331 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
334 return str(self.__unicode__())
336 def __unicode__(self):
337 return '\n\n'.join(self)
339 def wrapped_paragraphs(self, *args, **kwargs):
345 for paragraph in self]
347 def wrap(self, *args, **kwargs):
348 return '\n\n'.join(self.wrapped_paragraphs(*args, **kwargs))
351 return string_for_yaml('\n\n'.join([paragraph.rstrip('\n')
352 for paragraph in self]))
354 def from_yaml(self, string):
357 for paragraph in string.split('\n\n'):
358 self.append(paragraph)
361 class Recipe (object):
364 >>> r = Recipe('Hot Newt Dressing',
365 ... [IngredientBlock(None, [
366 ... Ingredient('eye of newt', Amount('1')),
367 ... Ingredient('salamanders', Amount('2'), 'diced fine')])],
368 ... Directions(['Mix ingredients until salamander starts to smoke',
369 ... 'Serve warm over rice']),
370 ... yield_='enough for one apprentice',
372 ... source='CENSORED',
373 ... url='http://merlin.uk/recipes/1234/',
374 ... tags=['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'])
376 Recipe: Hot Newt Dressing
377 Yield: enough for one apprentice
380 URL: http://merlin.uk/recipes/1234/
383 2 salamanders, diced fine
384 Mix ingredients until salamander starts to smoke
387 >>> r.matches_tags(None)
389 >>> r.matches_tags([])
391 >>> r.matches_tags(['dinner', 'apprentice'])
393 >>> r.matches_tags(['dinner', 'dragon'])
395 >>> pprint.pprint(r.to_yaml())
397 'directions': 'Mix ingredients until salamander starts to smoke\\n\\nServe warm over rice',
398 'ingredient_blocks': [{'ingredients': [{'amount': {'alternatives': [],
402 'name': 'eye of newt',
404 {'amount': {'alternatives': [],
408 'name': 'salamanders',
409 'note': 'diced fine'}],
411 'name': 'Hot Newt Dressing',
412 'source': 'CENSORED',
413 'tags': ['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'],
414 'url': 'http://merlin.uk/recipes/1234/',
415 'yield_': 'enough for one apprentice'}
417 >>> x.from_yaml(r.to_yaml())
419 Recipe: Hot Newt Dressing
420 Yield: enough for one apprentice
423 URL: http://merlin.uk/recipes/1234/
426 2 salamanders, diced fine
427 Mix ingredients until salamander starts to smoke
431 def __init__(self, name=None, ingredient_blocks=None, directions=None,
432 yield_=None, author=None, source=None, url=None, tags=None):
434 self.ingredient_blocks = ingredient_blocks
435 self.directions = directions
442 def clean_name(self):
444 for from_,to in [(' ','_'), ('/', '_'),
445 (',', ''), (u'\xe2\x80\x99', ''),
447 name = name.replace(from_, to)
450 def matches_tags(self, tags):
451 """Return True if this recipe is tagges with each of the tags in tags.
453 if tags in [None, []]:
455 if self.tags == None:
458 if t not in self.tags:
463 return str(self.__unicode__())
465 def __unicode__(self):
467 'Recipe: %s' % self.name,
468 'Yield: %s' % self.yield_,
469 'From: %s' % self.author,
470 'Source: %s' % self.source,
471 'URL: %s' % self.url,
472 '\n'.join([unicode(ib) for ib in self.ingredient_blocks]),
473 unicode(self.directions),
478 for key in ['name', 'ingredient_blocks', 'directions', 'yield_',
479 'author', 'source', 'url', 'tags']:
480 d[key] = to_yaml_object(getattr(self, key))
483 def from_yaml(self, dict):
484 for key in ['name', 'yield_', 'author', 'source', 'url', 'tags']:
485 setattr(self, key, dict.get(key, None))
486 self.ingredient_blocks = []
487 for ib in dict.get('ingredient_blocks', []):
488 ingredient_block = IngredientBlock()
489 ingredient_block.from_yaml(ib)
490 self.ingredient_blocks.append(ingredient_block)
491 self.directions = Directions()
492 self.directions.from_yaml(dict.get('directions', None))
494 def save(self, stream):
495 yaml.dump(self.to_yaml(), stream,
496 default_flow_style=False, allow_unicode=True, width=78)
498 def load(self, stream):
499 dict = yaml.load(stream)
503 class Cookbook (list):
507 def __init__(self, name="Mom's cookbook", *args, **kwargs):
509 super(Cookbook, self).__init__(*args, **kwargs)
511 def save(self, dir='recipe'):
512 if not os.path.isdir(dir):
516 base_path = recipe.clean_name()
520 path = '%s_%d' % (base_path, i)
526 with open(os.path.join(dir, path), 'w') as f:
529 def load(self, dir='recipe'):
530 for path in os.listdir(dir):
532 p = os.path.join(dir, path)
533 with open(p, 'r') as f:
538 def make_index(self):
541 self.index[recipe.name] = recipe
542 self.index[recipe.clean_name()] = recipe
545 """List all tags used in this cookbook.
549 if recipe.tags != None:
550 tags = tags.union(set(recipe.tags))
553 def tagged(self, tags=None):
554 """Iterate through all recipes matching the given list of tags.
557 if recipe.matches_tags(tags):