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
30 from urllib import quote_plus
35 def string_for_yaml(unicode_):
37 >>> string_for_yaml(None)
38 >>> string_for_yaml('all ascii')
40 >>> string_for_yaml(u'all ascii')
42 >>> string_for_yaml(u'½ ascii')
48 string = unicode_.encode('ascii')
50 except UnicodeEncodeError:
53 def to_yaml_object(obj):
55 >>> to_yaml_object(None)
56 >>> to_yaml_object('all ascii')
58 >>> to_yaml_object(u'all ascii')
60 >>> to_yaml_object('all ascii')
62 >>> to_yaml_object(u'½ ascii')
65 ... def to_yaml(self):
66 ... return 'to_yaml return value'
67 >>> to_yaml_object(x())
68 'to_yaml return value'
69 >>> to_yaml_object([u'all ascii', u'½ ascii'])
70 ['all ascii', u'\\xc2\\xbd ascii']
74 if hasattr(obj, 'to_yaml'):
76 if type(obj) in types.StringTypes:
77 return string_for_yaml(obj)
78 if hasattr(obj, '__len__'):
81 ret.append(to_yaml_object(item))
83 raise NotImplementedError(
84 'cannot convert %s to YAMLable dict:\n%s' % (type(obj), unicode(obj)))
87 class Amount (object):
90 >>> a = Amount(value='1', units='T.')
93 >>> pprint.pprint(a.to_yaml())
94 {'alternatives': [], 'range_': None, 'units': 'T.', 'value': '1'}
96 >>> x.from_yaml(a.to_yaml())
99 >>> b = Amount(value='1')
102 >>> pprint.pprint(b.to_yaml())
103 {'alternatives': [], 'range_': None, 'units': None, 'value': '1'}
104 >>> x.from_yaml(b.to_yaml())
107 >>> c = Amount(value='15', units='mL', alternatives=[a])
110 >>> pprint.pprint(c.to_yaml())
111 {'alternatives': [{'alternatives': [],
118 >>> x.from_yaml(c.to_yaml())
121 >>> d = Amount(units='T.', range_=['1','2'])
124 >>> pprint.pprint(d.to_yaml())
125 {'alternatives': [], 'range_': ['1', '2'], 'units': 'T.', 'value': None}
126 >>> x.from_yaml(d.to_yaml())
130 def __init__(self, value=None, units=None, range_=None, alternatives=None):
134 if alternatives == None:
136 self.alternatives = alternatives
139 return str(self.__unicode__())
141 def __unicode__(self):
142 if self.range_ == None:
145 value = '-'.join(self.range_)
147 if self.units != None:
148 ret.append(self.units)
149 if len(self.alternatives) > 0:
150 ret.append('(%s)' % ', '.join(
151 [unicode(a) for a in self.alternatives]))
152 return ' '.join([x for x in ret if x != None])
156 for key in ['value', 'range_', 'units', 'alternatives']:
157 d[key] = to_yaml_object(getattr(self, key))
160 def from_yaml(self, dict):
161 for key in ['value', 'range_', 'units']:
162 setattr(self, key, dict.get(key, None))
163 self.alternatives = []
164 for a in dict.get('alternatives', []):
167 self.alternatives.append(amount)
170 class Ingredient (object):
173 >>> i = Ingredient('eye of newt', Amount('1'))
176 >>> pprint.pprint(i.to_yaml())
177 {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '1'},
178 'name': 'eye of newt',
181 >>> x.from_yaml(i.to_yaml())
184 >>> j = Ingredient('salamanders', Amount('2'), 'diced fine')
186 '2 salamanders, diced fine'
187 >>> pprint.pprint(j.to_yaml())
188 {'amount': {'alternatives': [], 'range_': None, 'units': None, 'value': '2'},
189 'name': 'salamanders',
190 'note': 'diced fine'}
191 >>> x.from_yaml(j.to_yaml())
193 '2 salamanders, diced fine'
195 def __init__(self, name=None, amount=None, note=None):
201 return str(self.__unicode__())
203 def __unicode__(self):
204 if self.note == None:
205 return '%s %s' % (unicode(self.amount or ''), self.name)
206 return '%s %s, %s' % (unicode(self.amount or ''), self.name, self.note)
210 for key in ['name', 'amount', 'note']:
211 d[key] = to_yaml_object(getattr(self, key))
214 def from_yaml(self, dict):
215 for key in ['name', 'note']:
216 setattr(self, key, dict.get(key, None))
217 self.amount = Amount()
218 self.amount.from_yaml(dict.get('amount', {}))
221 class IngredientBlock (list):
224 >>> ib = IngredientBlock(None, [
225 ... Ingredient('eye of newt', Amount('1')),
226 ... Ingredient('salamanders', Amount('2'), 'diced fine')])
230 2 salamanders, diced fine
231 >>> pprint.pprint(ib.to_yaml())
232 {'ingredients': [{'amount': {'alternatives': [],
236 'name': 'eye of newt',
238 {'amount': {'alternatives': [],
242 'name': 'salamanders',
243 'note': 'diced fine'}],
245 >>> x = IngredientBlock()
246 >>> x.from_yaml(ib.to_yaml())
250 2 salamanders, diced fine
251 >>> ib.name = 'Dressing'
255 2 salamanders, diced fine
256 >>> pprint.pprint(ib.to_yaml())
257 {'ingredients': [{'amount': {'alternatives': [],
261 'name': 'eye of newt',
263 {'amount': {'alternatives': [],
267 'name': 'salamanders',
268 'note': 'diced fine'}],
270 >>> x = IngredientBlock()
271 >>> x.from_yaml(ib.to_yaml())
275 2 salamanders, diced fine
277 def __init__(self, name=None, *args, **kwargs):
279 super(IngredientBlock, self).__init__(*args, **kwargs)
282 if self.name == None:
287 return str(self.__unicode__())
289 def __unicode__(self):
290 ret = [unicode(i) for i in self]
291 ret.insert(0, '%s:' % self.heading())
292 return '\n '.join(ret)
297 d[key] = to_yaml_object(getattr(self, key))
298 d['ingredients'] = to_yaml_object(list(self))
301 def from_yaml(self, dict):
302 self.name = dict.get('name', None)
305 for i in dict.get('ingredients', []):
306 ingredient = Ingredient()
307 ingredient.from_yaml(i)
308 self.append(ingredient)
311 class Directions (list):
314 >>> d = Directions(['paragraph 1', 'paragraph 2', 'paragraph 3'])
321 >>> pprint.pprint(d.to_yaml())
322 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
324 >>> x.from_yaml(d.to_yaml())
331 >>> pprint.pprint(x.to_yaml())
332 'paragraph 1\\n\\nparagraph 2\\n\\nparagraph 3'
335 return str(self.__unicode__())
337 def __unicode__(self):
338 return '\n\n'.join(self)
340 def wrapped_paragraphs(self, *args, **kwargs):
346 for paragraph in self]
348 def wrap(self, *args, **kwargs):
349 return '\n\n'.join(self.wrapped_paragraphs(*args, **kwargs))
352 return string_for_yaml('\n\n'.join([paragraph.rstrip('\n')
353 for paragraph in self]))
355 def from_yaml(self, string):
360 for paragraph in string.split('\n\n'):
361 self.append(paragraph)
364 class Recipe (object):
367 >>> r = Recipe('Hot Newt Dressing',
368 ... [IngredientBlock(None, [
369 ... Ingredient('eye of newt', Amount('1')),
370 ... Ingredient('salamanders', Amount('2'), 'diced fine')])],
371 ... Directions(['Mix ingredients until salamander starts to smoke',
372 ... 'Serve warm over rice']),
373 ... yield_='enough for one apprentice',
375 ... source='CENSORED',
376 ... url='http://merlin.uk/recipes/1234/',
377 ... tags=['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'])
379 Recipe: Hot Newt Dressing
380 Yield: enough for one apprentice
383 URL: http://merlin.uk/recipes/1234/
386 2 salamanders, diced fine
387 Mix ingredients until salamander starts to smoke
390 >>> r.matches_tags(None)
392 >>> r.matches_tags([])
394 >>> r.matches_tags(['dinner', 'apprentice'])
396 >>> r.matches_tags(['dinner', 'dragon'])
398 >>> pprint.pprint(r.to_yaml())
400 'directions': 'Mix ingredients until salamander starts to smoke\\n\\nServe warm over rice',
401 'ingredient_blocks': [{'ingredients': [{'amount': {'alternatives': [],
405 'name': 'eye of newt',
407 {'amount': {'alternatives': [],
411 'name': 'salamanders',
412 'note': 'diced fine'}],
414 'name': 'Hot Newt Dressing',
415 'source': 'CENSORED',
416 'tags': ['dinner', 'easy', 'apprentice', 'eye', 'newt', 'salamander'],
417 'url': 'http://merlin.uk/recipes/1234/',
418 'yield_': 'enough for one apprentice'}
420 >>> x.from_yaml(r.to_yaml())
422 Recipe: Hot Newt Dressing
423 Yield: enough for one apprentice
426 URL: http://merlin.uk/recipes/1234/
429 2 salamanders, diced fine
430 Mix ingredients until salamander starts to smoke
434 def __init__(self, name=None, ingredient_blocks=None, directions=None,
435 yield_=None, author=None, source=None, url=None, tags=None):
437 self.ingredient_blocks = ingredient_blocks
438 self.directions = directions
446 def clean_name(self, ascii=False):
448 for from_,to in [(' ','_'), ('/', '_'), (',', ''), ('&', 'and')]:
449 name = name.replace(from_, to)
451 return quote_plus(name.encode('utf-8'))
454 def matches_tags(self, tags):
455 """Return True if this recipe is tagges with each of the tags in tags.
457 if tags in [None, []]:
459 if self.tags == None:
462 if t not in self.tags:
467 return str(self.__unicode__())
469 def __unicode__(self):
471 'Recipe: %s' % self.name,
472 'Yield: %s' % self.yield_,
473 'From: %s' % self.author,
474 'Source: %s' % self.source,
475 'URL: %s' % self.url,
476 '\n'.join([unicode(ib) for ib in self.ingredient_blocks]),
477 unicode(self.directions),
482 for key in ['name', 'ingredient_blocks', 'directions', 'yield_',
483 'author', 'source', 'url', 'tags']:
484 d[key] = to_yaml_object(getattr(self, key))
487 def from_yaml(self, dict):
488 for key in ['name', 'yield_', 'author', 'source', 'url', 'tags']:
489 setattr(self, key, dict.get(key, None))
490 self.ingredient_blocks = []
491 for ib in dict.get('ingredient_blocks', []):
492 ingredient_block = IngredientBlock()
493 ingredient_block.from_yaml(ib)
494 self.ingredient_blocks.append(ingredient_block)
495 self.directions = Directions()
496 self.directions.from_yaml(dict.get('directions', None))
498 def save(self, stream, path=None):
499 yaml.dump(self.to_yaml(), stream,
500 default_flow_style=False, allow_unicode=True, width=78)
504 def load(self, stream, path=None):
505 dict = yaml.load(stream)
511 class Cookbook (list):
515 def __init__(self, name="Mom's cookbook", *args, **kwargs):
517 self.save_dir = u'recipe'
518 super(Cookbook, self).__init__(*args, **kwargs)
521 if not os.path.isdir(self.save_dir):
522 os.mkdir(self.save_dir)
525 p = self._free_path(recipe, paths)
527 with open(p, 'w') as f:
531 for path in sorted(os.listdir(self.save_dir)):
533 p = os.path.join(self.save_dir, path)
534 with open(p, 'r') as f:
538 def make_index(self):
540 paths = [recipe.path for recipe in self]
542 self.index[recipe.name] = recipe
543 self.index[recipe.clean_name()] = recipe
544 self.index[recipe.clean_name(ascii=True)] = recipe
545 if recipe.path == None:
546 p = self._free_path(recipe, paths)
550 def _free_path(self, recipe, paths):
551 base_path = os.path.join(self.save_dir, recipe.clean_name())
555 path = '%s_%d' % (base_path, i)
560 """List all tags used in this cookbook.
564 if recipe.tags != None:
565 tags = tags.union(set(recipe.tags))
568 def tagged(self, tags=None):
569 """Iterate through all recipes matching the given list of tags.
572 if recipe.matches_tags(tags):