def move_iter (mod, iter, sibling=None, parent=None, direction="before"): """move_iter will move iter relative to sibling or parent. Direction (before or after) tells us whether to insert_before or insert_after (with a sib) or to prepend or append (with a parent).""" if direction != "after": direction = "before" path = mod.get_path(iter) if sibling: dpath = mod.get_path(sibling) elif parent: dpath = mod.get_path(parent) else: dpath = () rowdata = get_row(mod, iter) children=harvest_children(mod, iter) if direction != "after": direction = "before" path = mod.get_path(iter) if sibling: dpath = mod.get_path(sibling) elif parent: dpath = mod.get_path(parent) else: dpath = () rowdata = get_row(mod, iter) children=harvest_children(mod, iter) def insert_new (parent): """A little subroutine to insert our row. We'll call this at the appropriate time depending on the order of source and destination iters""" if not parent: parent=None if len(dpath) > 1: parent=mod.get_iter(dpath[0:-1]) if parent==sibling or not sibling: """If our parent is our destination iter or if we have only a parent specified, we're moving inside, not before""" if direction=="before": return mod.append(parent, rowdata) else: return mod.prepend(parent, rowdata) elif direction=="before": return mod.insert_before(parent,sibling,rowdata) else: return mod.insert_after(parent,sibling,rowdata) # if the source is before the destination, we add then remove. otherwise, we remove then add. path_last = path_compare(path,dpath) if path_last==1: # Source after destination (remove, then add) remove_children(mod, iter) mod.remove(iter) new=insert_new(parent) insert_children(mod, new, children) elif path_last==0: debug("Moving into my self is nonsensical!",1) else: # Source before destination (add, then remove) new=insert_new(parent) insert_children(mod, new, children) remove_children(mod, iter) mod.remove(iter)
def append (self,obj): debug('Appending %s'%obj,0) list.append(self,obj) if obj.is_undo==False: # Is this necessary? Not sure... for h in self.action_hooks: h(self,obj,'perform') self.gui_update()
def compile_regexps (self): testtimer = TimeAction('mealmaster_importer.compile_regexps',10) debug("start compile_regexps",5) plaintext_importer.TextImporter.compile_regexps(self) self.start_matcher = re.compile(mm_start_pattern) self.end_matcher = re.compile("^[M-][M-][M-][M-][M-]\s*$") self.group_matcher = re.compile("^\s*([M-][M-][M-][M-][M-])-*\s*([^-]+)\s*-*",re.IGNORECASE) self.ing_cont_matcher = re.compile("^\s*[-;]") self.ing_opt_matcher = re.compile("(.+?)\s*\(?\s*optional\)?\s*$",re.IGNORECASE) self.ing_or_matcher = re.compile("^[- ]*[Oo][Rr][- ]*$",re.IGNORECASE) self.variation_matcher = re.compile("^\s*(VARIATION|HINT|NOTES?)(:.*)?",re.IGNORECASE) # a crude ingredient matcher -- we look for two numbers, # intermingled with spaces followed by a space or more, # followed by a two digit unit (or spaces) self.ing_num_matcher = re.compile( "^\s*%s+\s+[a-z ]{1,2}\s+.*\w+.*"%convert.NUMBER_REGEXP, re.IGNORECASE) self.amt_field_matcher = re.compile("^(\s*%s\s*)$"%convert.NUMBER_REGEXP) # we build a regexp to match anything that looks like # this: ^\s*ATTRIBUTE: Some entry of some kind...$ self.mmf = mmf attrmatch="^\s*(" for k in self.mmf.recattrs.keys(): attrmatch += "%s|"%re.escape(k) attrmatch="%s):\s*(.*)\s*$"%attrmatch[0:-1] self.attr_matcher = re.compile(attrmatch) testtimer.end()
def handle_ingline (self,line): if self.ing_or_matcher.match(line): self.in_or = True return amt = line.__getslice__(*self.amt_col).strip() unit = line.__getslice__(*self.unit_col).strip() itm = line[self.itm_col[0]:].strip() gm=self.ing_group_matcher.match(itm) if gm: if self.ing: self.commit_ing() self.group = gm.groups()[1] # undo grouping if it has no letters... if re.match('^[^A-Za-z]*$',self.group): self.group=None return if amt or unit: if self.in_or: self.ing['optional']=True if self.ing: self.commit_ing() self.start_ing() if self.in_or: self.ing['optional']=True self.in_or = False self.add_amt(amt) self.add_unit(unit) self.add_item(itm) return elif self.ing and self.ing.has_key('item'): # otherwise, we assume we are a continuation and # add onto the previous item self.ing['item']=self.ing['item']+' '+itm.strip() else: debug('"%s" in the midst of ingredients looks like instructions!'%itm.strip(),2) self.instr += "\n"+itm.strip()
def parse_range(number_string): """Parse a range and return a tuple with a low and high number as floats. We will also parse regular numbers, in which case the tuple will only have one item. """ if type(number_string) in [int, float]: return (float(number_string), None) nums = convert.RANGE_MATCHER.split(number_string.strip()) if len(nums) > 2: debug("WARNING: String %s does not appear to be a normal range." % number_string, 0) retval = map(convert.frac_to_float, nums) # filter any non-numbers before we take 1 and 3 retval = filter(lambda x: x, retval) if len(retval) > 2: debug("Parsing range as %s-%s" % (retval[0], retval[-1]), 0) retval = retval[0], retval[-1] else: retval = map(convert.frac_to_float, nums) if len(retval) == 2: return tuple(retval) elif len(retval) == 1: return tuple(retval + [None]) else: return (None, None)
def __init__ (self, parent_thread=None,conv=None): debug('MastercookXMLHandler starting',0) xml_importer.RecHandler.__init__(self,parent_thread=parent_thread,conv=None) self.total = 0 self.recs_done = 0 self.elements = { 'mx2':['source','date'], #'Summ':[], 'Nam':[], 'RcpE':['name'], 'RTxt':[], 'Serv':['qty'], 'PropT':['elapsed'], 'IngR':['name','unit','qty'], 'IPrp':[], #'DirS':[], 'DirT':[], 'Desc':[], 'Srce':[], 'Note':[], 'CatT':[], 'Yield':['unit','qty'], } self.current_elements = [] self.bufs = [] xml.sax.ContentHandler.__init__(self) importer.Importer.__init__(self,conv=conv)
def create_thumbnail (path, thumbpath, uri, type="large"): """Create a thumbnail at path and return path""" mtime = os.stat(path)[8] size = os.path.getsize(path) if int(size) > MAX_THUMBSIZE: debug('File too large!',0) return None try: im = Image.open(path) except: return None w,h = im.size if type=='large': geo = (256,256) else: geo = (128,128) im.thumbnail(geo) # set thumbnail attributes info={} info['Thumb::MTime']=str(mtime) info['Thumb::Image::Width']=str(w) info['Thumb::Image::Height'] =str(h) info['Software']='Gourmet Recipe Manager' info['URI']=str(uri) # now we must create our image guy import PngImagePlugin pnginfo = PngImagePlugin.PngInfo() for k,v in info.items(): pnginfo.add_text(k,v) im.save(thumbpath, pnginfo=pnginfo) # we must make all thumbnails permissions 700 os.chmod(thumbpath,0700) return thumbpath
def get_referenced_rec(self, ing): """Get recipe referenced by ingredient object.""" if hasattr(ing, "refid") and ing.refid: rec = self.get_rec(ing.refid) if rec: return rec # otherwise, our reference is no use! Something's been # foobared. Unfortunately, this does happen, so rather than # screwing our user, let's try to look based on title/item # name (the name of the ingredient *should* be the title of # the recipe, though the user could change this) if hasattr(ing, "item"): recs = self.search(self.recipe_table, "title", ing.item, exact=True, use_regexp=False) if len(recs) == 0: self.modify_ing(ing, {"idref": recs[0].id}) return recs[0] else: debug( """Warning: there is more than one recipe titled"%(title)s" and our id reference to %(idref)s failed to match any recipes. We are going to assume recipe ID %(id)s is the correct one.""" % {"title": ing.item, "idref": ing.refid, "id": recs[0].id}, 0, ) return recs[0]
def compile_regexps (self): """Compile our regular expressions for the rezkonv format. """ testtimer = TimeAction('mealmaster_importer.compile_regexps',10) debug("start compile_regexps",5) plaintext_importer.TextImporter.compile_regexps(self) self.start_matcher = re.compile(rzc_start_pattern) self.end_matcher = re.compile("^[=M-][=M-][=M-][=M-][=M-]\s*$") self.group_matcher = re.compile("^\s*([=M-][=M-][=M-][=M-][=M-]+)-*\s*([^-]+)\s*-*",re.IGNORECASE) self.ing_cont_matcher = re.compile("^\s*[-;]") self.ing_opt_matcher = re.compile("(.+?)\s*\(?\s*optional\)?\s*$",re.IGNORECASE) # or or the German, oder self.ing_or_matcher = re.compile("^[-= ]*[Oo][dD]?[eE]?[Rr][-= ]*$",re.IGNORECASE) self.variation_matcher = re.compile("^\s*(VARIATION|HINT|NOTES?|VERÄNDERUNG|VARIANTEN|TIPANMERKUNGEN)(:.*)?",re.IGNORECASE) # a crude ingredient matcher -- we look for two numbers, intermingled with spaces # followed by a space or more, followed by a two digit unit (or spaces) self.ing_num_matcher = re.compile( "^\s*%(top)s%(num)s+\s+[A-Za-z ][A-Za-z ]? .*"%{'top':convert.DIVIDEND_REGEXP, 'num':convert.NUMBER_REGEXP}, re.IGNORECASE) self.amt_field_matcher = convert.NUMBER_MATCHER # we build a regexp to match anything that looks like # this: ^\s*ATTRIBUTE: Some entry of some kind...$ attrmatch="^\s*(" self.mmf = rzc for k in self.mmf.recattrs.keys(): attrmatch += "%s|"%re.escape(k) attrmatch="%s):\s*(.*)\s*$"%attrmatch[0:-1] self.attr_matcher = re.compile(attrmatch) testtimer.end()
def set_sensitive (self,w,val): debug('set_sensitive',0) if not w: #import traceback; traceback.print_stack() #print 'No widget to sensitize',w,val return w.set_sensitive(val)
def toss_regs (self, instr): m = self.toss_regexp.search(instr) if m: outstr = instr[0:m.start()] + instr[m.end():] debug('Converted "%s" to "%s"'%(instr,outstr),1) return outstr else: return instr
def encode (self, l): for e in self.encodings: try: return l.decode(e) except: debug('Could not decode as %s'%e,2) pass raise Exception("Could not encode %s" % l)
def load_properties (self): #if os.name=='nt': return for p,f in ['window_size', self.w.resize],['position',self.w.move]: if self.dictionary.has_key(p) and self.dictionary[p]: debug('applying %s %s'%(f,self.dictionary[p]),3) #if os.name=='nt' and p=='position' and self.dictionary['position'][1]<20: # #print 'FIDDLING WITH WINDOW FOR WINDOWS' # #self.dictionary[p] = self.dictionary[p][0],20 apply(f,self.dictionary[p])
def delete_rec(self, rec): """Delete recipe object rec from our database.""" if type(rec) != int: rec = rec.id debug("deleting recipe ID %s" % rec, 0) self.delete_by_criteria(self.recipe_table, {"id": rec}) self.delete_by_criteria(self.categories_table, {"id": rec}) self.delete_by_criteria(self.ingredients_table, {"id": rec}) debug("deleted recipe ID %s" % rec, 0)
def startElement (self, name, attrs): self.in_mixed=0 if not self.elements.has_key(name): debug('Unhandled element: %s'%name,0) return else: self.current_elements = [name] + self.current_elements handler = self._get_handler(name) handler(start=True,attrs=attrs)
def field_width (tuple): testtimer = TimeAction('mealmaster_importer.field_width',10) debug("start field_width",10) if tuple[1]: testtimer.end() return tuple[1]-tuple[0] else: testtimer.end() return None
def pre_run (self): self.emit('progress',0.03, _("Tidying up XML")) cleaner = Mx2Cleaner() base,ext=os.path.splitext(self.fn) cleanfn = base + ".gourmetcleaned" + ext cleaner.cleanup(self.fn,cleanfn) debug('Cleaned up file saved to %s'%cleanfn,1) self.orig_fn = self.fn self.fn = cleanfn
def run (self): """Import our recipe to our database. This must be called after self.d is already populated by scraping our web page. """ debug('Scraping url %s'%self.url,0) try: self.d = scrape_url(self.url, progress=self.prog) except: print 'Trouble using default recipe filter to download %s'%self.url traceback.print_exc() print 'We will use a generic importer instead.' self.d = {} debug('Scraping url returned %s'%self.d,0) do_generic = not self.d if not do_generic: try: if self.prog: self.prog(-1,'Parsing webpage based on template.') self.get_url_based_on_template() except: if not self.interactive: raise do_generic = True print """Automated HTML Import failed ***Falling back to generic import*** We were attempting to scrape using the following rules: """ print self.d print """The following exception was raised:""" traceback.print_exc() print """If you think automated import should have worked for the webpage you were importing, copy the output starting at "Automated HTML Import failed" into a bug report and submit it at the GitHub site https://github.com/thinkle/gourmet/issues Sorry automated import didn't work. I hope you like the new generic web importer! """ if do_generic: if not self.interactive: raise Exception("Unable to find importer for %s" % self.url) # Interactive we go... self.prog(-1,_("Don't recognize this webpage. Using generic importer...")) gs = GenericScraper() text,images = gs.scrape_url(self.url, progress=self.prog) if not text and not images: raise Exception("Unable to obtain text or images from url %s" % self.url) import interactive_importer ii = interactive_importer.InteractiveImporter(self.rd) ii.set_text(text) ii.add_attribute('link',self.url) ii.set_images(images) ii.run() if self.prog: self.prog(1,_('Import complete.')) return
def tarball_to_filelist (fi, progress=None, name="zipfile"): tb = tarfile.TarFile.open(fi,mode='r') fi_info = tb.next() filist = [] while fi_info: fi = tb.extractfile(fi_info) if fi: filist.append(fi) fi_info = tb.next() debug('tarball_to_filelist returning %s'%filist,0) return filist
def do_run (self): debug('Running ing hooks',0) for i in self.added_ings: for h in self.rd_orig_ing_hooks: h(i) #debug('Running rec hooks',0) #for r in self._added_recs: # for h in self.rd_orig_hooks: # h(r) self.rd.add_ing_hooks = self.rd_orig_ing_hooks
def density_itm_changed (self, *args): debug('density_itm_changed',5) self.changing_item=True itm=cb_get_active_text(self.itemComboBox) if itm != _('None'): self.densitySpinButton.set_value(self.conv.density_table[itm]) else: self.densitySpinButton.set_value(0) self.changed() self.changing_item=False
def modify_ing (self, ing, ingdict): """modify ing based on dictionary of properties and new values.""" #self.delete_ing(ing) #return self.add_ing(ingdict) for k,v in ingdict.items(): if hasattr(ing,k): self.changed=True setattr(ing,k,v) else: debug("Warning: ing has no attribute %s (attempted to set value to %s" %(k,v),0) return ing
def replace_ings (self, ingdicts): """Add a new ingredients and remove old ingredient list.""" ## we assume (hope!) all ingdicts are for the same ID id=ingdicts[0]['id'] debug("Deleting ingredients for recipe with ID %s"%id,1) ings = self.get_ings(id) for i in ings: debug("Deleting ingredient: %s"%i.ingredient,5) self.delete_ing(i) for ingd in ingdicts: self.add_ing(ingd)
def toggle_widget (self, w, val): """Toggle the visibility of widget 'w'""" if val: method = 'hide' else: method = 'show' if type(w)==type(""): w = [w] for wn in w: widg = self.glade.get_widget(wn) if widg: getattr(widg,method)() else: debug('There is no widget %s'%wn,1)
def check_for_sleep(self): if self.terminated: raise Exception("Exporter Terminated!") while self.suspended: if self.terminated: debug("Thread Terminated!", 0) raise Exception("Exporter Terminated!") if use_threads: time.sleep(1) else: time.sleep(0.1)
def get_fields (string, tuples): testtimer = TimeAction('mealmaster_importer.get_fields',10) debug("start get_fields",10) lst = [] for t in tuples: if t: lst.append(string[t[0]:t[1]]) else: lst.append("") testtimer.end() return lst
def send_email (self): self.url = "mailto:" if self.emailaddress: self.url += self.emailaddress if self.subject: self.url_append('subject',self.subject) if self.body: self.url_append('body',self.body) for a in self.attachments: self.url_append('attachment',a) debug('launching URL %s'%self.url,0) gglobals.launch_url(self.url)
def handle_group (self, groupm): """Start a new ingredient group.""" testtimer = TimeAction('mealmaster_importer.handle_group',10) debug("start handle_group",10) # the only group of the match will contain # the name of the group. We'll put it into # a more sane title case (MealMaster defaults # to all caps name = groupm.groups()[1].title() self.group=name if re.match('^[^A-Za-z]*$',self.group): self.group=None testtimer.end()
def field_match (strings, tup, matcher): testtimer = TimeAction('mealmaster_importer.field_match',10) debug("start field_match",10) if type(matcher)==type(""): matcher=re.compile(matcher) for f in [s[tup[0]:tup[1]] for s in strings]: #f=s[tup[0]:tup[1]] if f and not matcher.match(f): testtimer.end() return False testtimer.end() return True
def __init__ (self, rd, recipe_table, out, conv=None): debug('rtf_exporter_multidoc starting!',5) self.doc = PyRTF.Document() exporter.ExporterMultirec.__init__(self, rd, recipe_table, out, one_file=True, ext='rtf', exporter=rtf_exporter, exporter_kwargs={'doc':self.doc, 'multidoc':True}) debug('rtf_exporter_multidoc done!',5)
def finish_ing (self): timeaction = TimeAction('importer.finish_ing 1',10) # Strip whitespace... for key in ['item','ingkey','unit']: if self.ing.has_key(key): self.ing[key]=re.sub('\s+',' ',self.ing[key]).strip() if not ( (self.ing.has_key('refid') and self.ing['refid']) or (self.ing.has_key('ingkey') and self.ing['ingkey']) ): #self.ing['ingkey']=self.km.get_key(self.ing['item'],0.9) if self.ing.has_key('item'): self.ing['ingkey']=self.km.get_key_fast(self.ing['item']) else: debug('Ingredient has no item! %s'%self.ing,-1) timeaction.end() # if we have an amount (and it's not None), let's convert it # to a number if self.ing.has_key('amount') and self.ing['amount']\ and not self.ing.has_key('rangeamount'): if convert.RANGE_MATCHER.search(str(self.ing['amount'])): self.ing['amount'],self.ing['rangeamount']=parse_range(self.ing['amount']) if self.ing.has_key('amount'): self.ing['amount']=convert.frac_to_float( self.ing['amount'] ) if self.ing.has_key('rangeamount'): self.ing['rangeamount']=convert.frac_to_float( self.ing['rangeamount'] ) timeaction = TimeAction('importer.commit_ing 2',10) if not (self.ing.has_key('position') and self.ing['position']): self.ing['position']=self.position self.position+=1 timeaction.end() timeaction = TimeAction('importer.commit_ing 3',10) if self.group: self.ing['inggroup']=self.group timeaction.end() timeaction = TimeAction('importer.commit_ing 4',10) self.added_ings.append(self.ing); self.ing = {} timeaction.end()
def commit_rec(self): """Commit our recipe to our database.""" testtimer = TimeAction('mealmaster_importer.commit_rec', 10) if self.committed: return debug("start _commit_rec", 5) self.instr = self.unwrap_lines(self.instr) self.mod = self.unwrap_lines(self.mod) self.rec['instructions'] = self.instr if self.mod: self.rec['modifications'] = self.mod self.parse_inglist() if self.source: self.rec['source'] = self.source importer.Importer.commit_rec(self) # blank rec self.committed = True self.in_variation = False testtimer.end()
def reassign_buttons (self, pausecb=None, stopcb=None): debug('reassign_buttons called with pausecb=%s, stopcb=%s'%(pausecb,stopcb),1) while self.custom_pause_handlers: h=self.custom_pause_handlers.pop() if self.pause.handler_is_connected(h): self.pause.disconnect(h) if pausecb: self.pause.connect('toggled',pausecb) self.pause.set_property('visible',True) else: self.pause.set_property('visible',False) while self.custom_stop_handlers: h=self.custom_stop_handlers.pop() if self.stop.handler_is_connected(h): self.stop.disconnect(h) if stopcb: self.stop.connect('clicked',stopcb) #self.stop.connect('clicked',self.cancelcb) self.stop.set_property('visible',True) else: self.stop.set_property('visible',False)
def add_ing(self, ingdict): """Add ingredient to ingredients_table based on ingdict and return ingredient object. Ingdict contains: id: recipe_id unit: unit item: description key: keyed descriptor alternative: not yet implemented (alternative) #optional: yes|no optional: True|False (boolean) position: INTEGER [position in list] refid: id of reference recipe. If ref is provided, everything else is irrelevant except for amount. """ self.changed = True debug('adding to ingredients_table %s' % ingdict, 3) timer = TimeAction('rdatabase.add_ing 2', 5) if ingdict.has_key('amount') and not ingdict['amount']: del ingdict['amount'] self.ingredients_table.append(ingdict) timer.end() debug('running ing hooks %s' % self.add_ing_hooks, 3) timer = TimeAction('rdatabase.add_ing 3', 5) if self.add_ing_hooks: self.run_hooks(self.add_ing_hooks, self.ingredients_table[-1]) timer.end() debug('done with ing hooks', 3) return self.ingredients_table[-1]
def __init__(self, rows, titles=None): """Handed a list of data, we create a simple treeview. The rules are simple. Each row can be a LIST in which case it is taken to be a list of columns (and each LIST is assumed to be the same length). Alternatively, each row can be an item, in which case there is only one column. All items must produce a string with str(item).""" debug('QuickTree got rows: %s' % rows, 0) Gtk.ScrolledWindow.__init__(self) self.tv = Gtk.TreeView() self.rows = rows self.titles = titles if self.rows: first = self.rows[0] if type(first) != type(()) and type(first) != type([]): debug('Mappifying!', 0) self.rows = [[x] for x in self.rows] self.setup_columns() self.setup_model() self.add(self.tv) self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) self.show_all()
def find_columns(strings, char=" "): testtimer = TimeAction('mealmaster_importer.find_columns', 10) """Return a list of character indices that match char for each string in strings.""" debug("start find_columns", 10) # we start with the columns in the first string if not strings: return None strings = strings[0:] strings.sort(lambda x, y: len(x) > len(y)) columns = [ match.start() for match in re.finditer(re.escape(char), strings[0]) ] if len(strings) == 1: return columns # we eliminate all columns that aren't blank for every string for s in strings: for c in columns[0:]: # we'll be modifying columns if c < len(s) and s[c] != char: columns.remove(c) columns.sort() testtimer.end() return columns
def do_run(self): with open(self.fn, 'rb') as fp: attrs = pl.load(fp) debug(f'Imported property list {attrs}') plist = NSKeyedArchiverPlist.to_dict(attrs) attrDict = plist['attributeDictionary'] if 'title' in attrDict and attrDict['title'].strip(): self.start_rec() self.rec['title'] = attrDict['title'].strip() if 'instructions' in attrDict: self.rec['instructions'] = attrDict['instructions'].strip() if 'sourceAsString' in attrDict: self.rec['source'] = attrDict['sourceAsString'].strip() # 2021-05-09 Not sure about mapping rating if 'rating' in attrDict: self.rec['rating'] = attrDict['rating'] # 2121-06-09 RecipeBox data model seems to allow multiple categories, like tags # Pick the first category if 'categories' in plist and len(plist['categories']) > 0: it = iter(plist['categories']) self.rec['category'] = next(it)['RBCategoryObjectName'].strip() if 'ingredientsListing' in attrDict: for l in attrDict['ingredientsListing'].strip().splitlines(): l = l.strip() if len(l) > 0: quantity, unit, ingredient = '', '', '' parsed = self.rd.parse_ingredient(l, conv=self.conv, get_key=False) if parsed and parsed.get('amount', '') and parsed.get( 'item', ''): quantity = str(parsed['amount']) unit = parsed.get('unit', '') ingredient = parsed['item'] debug( f'Parsed from ingredient string "{l}": quantity={quantity}, unit={unit}, ingredient={ingredient}' ) if quantity.strip() or unit.strip( ) or ingredient.strip(): self.start_ing() if quantity: self.add_amt(quantity) if unit: self.add_unit(unit) optm = _OPT_MATCHER.match(ingredient) if optm: item = optm.groups()[0] self.ing['optional'] = True else: item = ingredient self.add_item(item) debug("Committing ingredient: %s" % self.ing, 6) self.commit_ing() self.commit_rec() self._run_cleanup_()
def parse_range (number_string): """Parse a range and return a tuple with a low and high number as floats. We will also parse regular numbers, in which case the tuple will only have one item. """ if type(number_string) in [int,float]: return (float(number_string),None) nums=convert.RANGE_MATCHER.split(number_string.strip()) if len(nums) > 2: debug('WARNING: String %s does not appear to be a normal range.'%number_string,0) retval = map(convert.frac_to_float,nums) # filter any non-numbers before we take 1 and 3 retval = filter(lambda x: x, retval) if len(retval) > 2: debug('Parsing range as %s-%s'%(retval[0],retval[-1]),0) retval = retval[0],retval[-1] else: retval = map(convert.frac_to_float,nums) if len(retval)==2: return tuple(retval) elif len(retval)==1: return tuple(retval+[None]) else: return (None,None)
def ingredient_parser(self, s, conv=None, get_key=True): """Handed a string, we hand back a dictionary (sans recipe ID)""" debug('ingredient_parser handed: %s' % s, 0) s = unicode(s) # convert to unicode so our ING MATCHER works properly s = s.strip("\n\t #*+-") debug('ingredient_parser handed: "%s"' % s, 1) m = convert.ING_MATCHER.match(s) if m: debug('ingredient parser successfully parsed %s' % s, 1) d = {} g = m.groups() a, u, i = (g[convert.ING_MATCHER_AMT_GROUP], g[convert.ING_MATCHER_UNIT_GROUP], g[convert.ING_MATCHER_ITEM_GROUP]) if a: asplit = convert.RANGE_MATCHER.split(a) if len(asplit) == 2: d['amount'] = convert.frac_to_float(asplit[0].strip()) d['rangeamount'] = convert.frac_to_float(asplit[1].strip()) else: d['amount'] = convert.frac_to_float(a.strip()) if u: if conv and conv.unit_dict.has_key(u.strip()): d['unit'] = conv.unit_dict[u.strip()] else: d['unit'] = u.strip() if i: optmatch = re.search('\s+\(?[Oo]ptional\)?', i) if optmatch: d['optional'] = True i = i[0:optmatch.start()] + i[optmatch.end():] d['item'] = i.strip() if get_key: d['ingkey'] = self.km.get_key(i.strip()) debug('ingredient_parser returning: %s' % d, 0) return d else: debug("Unable to parse %s" % s, 0) return None
def handle_ingline(self, line): if self.ing_or_matcher.match(line): self.in_or = True return amt = line[slice(*self.amt_col)].strip() unit = line[slice(*self.unit_col)].strip() itm = line[self.itm_col[0]:].strip() gm = self.ing_group_matcher.match(itm) if gm: if self.ing: self.commit_ing() self.group = gm.groups()[1] # undo grouping if it has no letters... if re.match('^[^A-Za-z]*$', self.group): self.group = None return if amt or unit: if self.in_or: self.ing['optional'] = True if self.ing: self.commit_ing() self.start_ing() if self.in_or: self.ing['optional'] = True self.in_or = False self.add_amt(amt) self.add_unit(unit) self.add_item(itm) return elif self.ing and 'item' in self.ing: # otherwise, we assume we are a continuation and # add onto the previous item self.ing['item'] = self.ing['item'] + ' ' + itm.strip() else: debug( '"%s" in the midst of ingredients looks like instructions!' % itm.strip(), 2) self.instr += "\n" + itm.strip()
def create_thumbnail(path, thumbpath, uri, type="large"): """Create a thumbnail at path and return path""" mtime = os.stat(path)[8] size = os.path.getsize(path) if int(size) > MAX_THUMBSIZE: debug('File too large!', 0) return None try: im = Image.open(path) except: return None w, h = im.size if type == 'large': geo = (256, 256) else: geo = (128, 128) im.thumbnail(geo) # set thumbnail attributes info = {} info['Thumb::MTime'] = str(mtime) info['Thumb::Image::Width'] = str(w) info['Thumb::Image::Height'] = str(h) info['Software'] = 'Gourmet Recipe Manager' info['URI'] = str(uri) # now we must create our image guy try: from PIL import PngImagePlugin except ImportError: import PngImagePlugin pnginfo = PngImagePlugin.PngInfo() for k, v in list(info.items()): pnginfo.add_text(k, v) im.save(thumbpath, pnginfo=pnginfo) # we must make all thumbnails permissions 700 os.chmod(thumbpath, 0o700) return thumbpath
def compile_regexps(self): """Compile our regular expressions for the rezkonv format. """ testtimer = TimeAction('mealmaster_importer.compile_regexps', 10) debug("start compile_regexps", 5) plaintext_importer.TextImporter.compile_regexps(self) self.start_matcher = re.compile(rzc_start_pattern) self.end_matcher = re.compile(r"^[=M-][=M-][=M-][=M-][=M-]\s*$") self.group_matcher = re.compile( r"^\s*([=M-][=M-][=M-][=M-][=M-]+)-*\s*([^-]+)\s*-*", re.IGNORECASE) self.ing_cont_matcher = re.compile(r"^\s*[-;]") self.ing_opt_matcher = re.compile(r"(.+?)\s*\(?\s*optional\)?\s*$", re.IGNORECASE) # or or the German, oder self.ing_or_matcher = re.compile(r"^[-= ]*[Oo][dD]?[eE]?[Rr][-= ]*$", re.IGNORECASE) self.variation_matcher = re.compile( r"^\s*(VARIATION|HINT|NOTES?|VERÄNDERUNG|VARIANTEN|TIPANMERKUNGEN)(:.*)?", re.IGNORECASE) # a crude ingredient matcher -- we look for two numbers, intermingled with spaces # followed by a space or more, followed by a two digit unit (or spaces) self.ing_num_matcher = re.compile( r"^\s*%(top)s%(num)s+\s+[A-Za-z ][A-Za-z ]? .*" % { 'top': convert.DIVIDEND_REGEXP, 'num': convert.NUMBER_REGEXP }, re.IGNORECASE) self.amt_field_matcher = convert.NUMBER_MATCHER # we build a regexp to match anything that looks like # this: ^\s*ATTRIBUTE: Some entry of some kind...$ attrmatch = r"^\s*(" self.mmf = rzc for k in list(self.mmf.recattrs.keys()): attrmatch += "%s|" % re.escape(k) attrmatch = r"%s):\s*(.*)\s*$" % attrmatch[0:-1] self.attr_matcher = re.compile(attrmatch) testtimer.end()
def zipfile_to_filelist(fi, progress=None, name="zipfile"): """Take our zipfile and return a list of files. We're quite liberal in what we allow fi to be. If fi is a string, we open it as a filename. if fi is a file object, we handle it, even if it's an icky file object that needs some messy manipulation to work (i.e. a urllib.urlopen() object). """ # handle filename if type(fi) in [unicode, str]: fi = open(fi, 'rb') # handle unseekable elif not hasattr(fi, 'seek'): # slurp up the file into a StringIO so we can seek within it debug('Slurping up file into StringIO', 1) tmpfi = StringIO.StringIO( read_socket_w_progress(fi, progress, _('Loading zip archive'))) fi.close() fi = tmpfi # and now we actually do our work... debug('ZipFile(fi)', 1) zf = zipfile.ZipFile(fi) flist = [] fbase = os.path.join(tempfile.tempdir, name) while os.path.exists(fbase): fbase = add_to_fn(fbase) os.mkdir(fbase) nlist = zf.namelist() totlen = float(len(nlist)) for i, n in enumerate(nlist): debug('Unzipping item %s' % i, 1) if progress: progress(float(i) / totlen, _("Unzipping zip archive")) fn = os.path.join(fbase, n) ifi = open(fn, 'wb') ifi.write(zf.read(n)) ifi.close() flist.append(fn) zf.close() debug('zipfile returning filelist %s' % flist, 1) return flist
def set_sensitive (self,w,val): debug('set_sensitive',0) if not w: #import traceback; traceback.print_stack() #print 'No widget to sensitize',w,val return try: w.set_sensitive(val) debug('%s.set_sensitive succeeded'%w,0) except AttributeError: # 2.6 will give gtk.Action a set_sensitive property, but for now... #if type(w)==gtk.Action: for p in w.get_proxies(): debug('setting %s sensitivity to %s'%(w,val),0) #p.set_sensitive(val) p.set_property('sensitive',val)
def do_add_rec(self, rdict): """Add a recipe based on a dictionary of properties and values.""" self.changed = True if not rdict.has_key('deleted'): rdict['deleted'] = 0 if not rdict.has_key('id'): rdict['id'] = self.new_id() try: debug('Adding recipe %s' % rdict, 4) self.recipe_table.append(rdict) debug('Running add hooks %s' % self.add_hooks, 2) if self.add_hooks: self.run_hooks(self.add_hooks, self.recipe_table[-1]) return self.recipe_table[-1] except: debug("There was a problem adding recipe%s" % rdict, -1) raise
def switch_context(self, hid): # set sensitivity for current context debug('switching context...', 0) self.get_history().gui_update()
def remove(self, obj): debug('Removing %s' % obj, 0) list.remove(self, obj) self.gui_update()
def gui_update(self): debug('gui_update', 0) if len(self) >= 1: undoables = [x.is_undo for x in self] if False in undoables: self.set_sensitive(self.undo_widget, True) debug('Sensitizing undo_widget', 0) else: self.set_sensitive(self.undo_widget, False) debug('Desensizing undo_widget', 0) if True in undoables: debug('Sensitizing redo_widget', 0) self.set_sensitive(self.redo_widget, True) else: debug('Desensitizing redo widget', 0) self.set_sensitive(self.redo_widget, False) if self[-1].reapplyable: debug('Sensitizing "reapply" widgets', 0) self.set_sensitive(self.reapply_widget, True) if self[-1].reapply_name: if type(self.reapply_widget) == Gtk.MenuItem: alabel = self.reapply_widget.get_children()[0] alabel.set_text_with_mnemonic(self[-1].reapply_name) alabel.set_use_markup(True) else: debug('Nothing to undo, desensitizing widgets', 0) self.set_sensitive(self.redo_widget, False) self.set_sensitive(self.undo_widget, False) self.set_sensitive(self.reapply_widget, False)
def reapply(self, *args): debug('Reapplying', 0) action = self[-1] action.reapply() for h in self.action_hooks: h(self, action, 'reapply')
sc = Gtk.Button('show changes') vb = Gtk.VBox() bb = Gtk.HButtonBox() bb.add(ub) bb.add(rb) bb.add(sc) vb.add(bb) vb.add(e) vb.add(tv) vb.add(sb) sb.show() w.add(vb) uhl = UndoHistoryList(ub, rb, signal='clicked') UndoableTextView(tv, uhl) UndoableEntry(e, uhl) UndoableGenericWidget(sb, uhl) w.show_all() w.connect('delete-event', lambda *args: Gtk.main_quit()) def show_changes(*args): for c in uhl: if hasattr(c, 'initial_text'): print(c, ' initial: ', c.initial_text, ' current: ', c.text) else: print(c) ub.connect('clicked', lambda *args: debug('Undo clicked!', 0)) sc.connect('clicked', show_changes) rb.connect('clicked', lambda *args: debug('Redo clicked!', 0)) Gtk.main()
def split_fields(strings, char=" "): testtimer = TimeAction('mealmaster_importer.split_fields', 10) debug("start split_fields", 10) fields = find_fields(strings, char) testtimer.end()
def run(self): """Import our recipe to our database. This must be called after self.d is already populated by scraping our web page. """ debug('Scraping url %s' % self.url, 0) try: self.d = scrape_url(self.url, progress=self.prog) except: print('Trouble using default recipe filter to download %s' % self.url) traceback.print_exc() print('We will use a generic importer instead.') self.d = {} debug('Scraping url returned %s' % self.d, 0) do_generic = not self.d if not do_generic: try: if self.prog: self.prog(-1, 'Parsing webpage based on template.') self.get_url_based_on_template() except: if not self.interactive: raise do_generic = True print("""Automated HTML Import failed ***Falling back to generic import*** We were attempting to scrape using the following rules: """) print(self.d) print("""The following exception was raised:""") traceback.print_exc() print( """If you think automated import should have worked for the webpage you were importing, copy the output starting at "Automated HTML Import failed" into a bug report and submit it at the GitHub site https://github.com/thinkle/gourmet/issues Sorry automated import didn't work. I hope you like the new generic web importer! """) if do_generic: if not self.interactive: raise Exception("Unable to find importer for %s" % self.url) # Interactive we go... self.prog( -1, _("Don't recognize this webpage. Using generic importer...")) gs = GenericScraper() text, images = gs.scrape_url(self.url, progress=self.prog) if not text and not images: raise Exception("Unable to obtain text or images from url %s" % self.url) from . import interactive_importer ii = interactive_importer.InteractiveImporter(self.rd) ii.set_text(text) ii.add_attribute('link', self.url) ii.set_images(images) ii.run() if self.prog: self.prog(1, _('Import complete.')) return
def handle_line(self, line): if self.rec_start_matcher.match(line): debug('rec_start! %s' % line, 0) self.looking_for_title = True if self.rec: self.commit_rec() self.instr = "" self.mods = "" self.in_instructions = False self.in_mods = False self.in_ings = False self.in_attrs = False self.start_rec() return if self.reccol_headers: # we try to parse underlining after our standard ing headers. rcm = self.rec_col_underline_matcher.match(line) # if there is no underlining, use our headers themselves for fields if not rcm: rcm = self.reccol_headers debug('Found ing columns', 0) self.get_ing_cols(rcm) self.in_ings = True self.reccol_headers = False if self.dash_matcher.match(line): return rcm = self.rec_col_matcher.match(line) if rcm: self.reccol_headers = rcm self.looking_for_title = False self.in_attrs = False self.last_attr = "" return if self.blank_matcher.match(line): # blank line ends ingredients if self.in_ings: debug('blank line, end of ings', 0) self.in_ings = False self.in_instructions = True if self.ing: self.commit_ing() if self.in_instructions: debug('blank line added to instructions: %s' % line, 0) if self.in_mods: self.mods += "\n" else: self.instr += "\n" return if self.looking_for_title: debug('found my title! %s' % line.strip(), 0) self.rec['title'] = line.strip() self.looking_for_title = False self.in_attrs = True return if self.in_ings: debug('handling ingredient line %s' % line, 0) self.handle_ingline(line) return if self.in_attrs: debug('handing attrline %s' % line, 0) self.handle_attribute(line) return else: self.in_instructions = True if self.mods_matcher.match(line): self.in_mods = True if self.in_mods: debug('handling modifications line %s' % line, 0) self.add_to_attr('mods', line) else: debug('handling instructions line %s' % line, 0) self.add_to_attr('instr', line)
def handle_line(self, l): """Handle an individual line of a mealmaster file. We're quite loose at handling mealmaster files. We look at each line and determine what it is most likely to be: ingredients and instructions can be intermingled: instructions will simply be added to the instructions and ingredients to the ingredient list. This may result in loss of information (for instructions that specifically follow ingredients) or in mis-parsing (for instructions that look like ingredients). But we're following, more or less, the specs laid out here <http://phprecipebook.sourceforge.net/docs/MM_SPEC.DOC>""" testtimer = TimeAction('mealmaster_importer.handle_line', 10) debug("start handle_line", 10) #gt.gtk_update() if self.start_matcher.match(l): debug("recipe start %s" % l, 4) if 'Windows Gourmet' in l: self.unit_length = 15 self.new_rec() self.last_line_was = 'new_rec' self.in_variation = False return if self.end_matcher.match(l): debug("recipe end %s" % l, 4) self.commit_rec() self.last_line_was = 'end_rec' return groupm = self.group_matcher.match(l) if groupm: debug("new group %s" % l, 4) self.handle_group(groupm) self.last_line_was = 'group' return attrm = self.attr_matcher.match(l) if attrm: debug('Found attribute in %s' % l, 4) attr, val = attrm.groups() debug("Writing attribute, %s=%s" % (attr, val), 4) self.rec[self.mmf.recattrs[attr]] = val.strip() self.last_line_was = 'attr' return if not self.instr and self.blank_matcher.match(l): debug('ignoring blank line before instructions', 4) self.last_line_was = 'blank' return if self.variation_matcher.match(l): debug('in variation', 4) self.in_variation = True if self.is_ingredient(l) and not self.in_variation: debug('in ingredient', 4) contm = self.ing_cont_matcher.match(l) if contm: # only continuations after ingredients are ingredients if self.ingrs and self.last_line_was == 'ingr': debug('continuing %s' % self.ingrs[-1][0], 4) continuation = " %s" % l[contm.end():].strip() self.ingrs[-1][0] += continuation self.last_line_was = 'ingr' else: self.instr += l self.last_line_was = 'instr' else: self.last_line_was = 'ingr' self.ingrs.append([l, self.group]) else: ## otherwise, we assume a line of instructions if self.last_line_was == 'blank': add_blank = True else: add_blank = False if self.in_variation: debug('Adding to instructions: %s' % l, 4) self.last_line_was = 'mod' add_to = 'mod' else: debug('Adding to modifications: %s' % l, 4) self.last_line_was = 'instr' add_to = 'instr' if getattr(self, add_to): if add_blank: setattr(self, add_to, getattr(self, add_to) + "\n") setattr(self, add_to, getattr(self, add_to) + l.strip() + "\n") else: setattr(self, add_to, l.strip() + "\n") testtimer.end()
def undo_action(): debug('undoable_modify_ing unmodifying %s' % orig_dic, 2) self.modify_ing(ing, orig_dic) if make_visible: make_visible(ing, orig_dic)
def __init__(self): debug('recipeManager.__init__()', 3) RecData.__init__(self) self.km = keymanager.KeyManager(rm=self)
def get_url_based_on_template (self): """Get URL based on template stored in d """ self.start_rec() # Set link self.rec['link']=self.url # Add webpage as source if self.add_webpage_source: # add Domain as source domain = self.url.split('/')[2] src=self.d.get('source',None) add_str = '(%s)'%domain if isinstance(src, list): src.append(add_str) elif src: src = [src, add_str] else: src = domain # no parens if we're the only source self.d['source']=src for k,v in list(self.d.items()): debug('processing %s:%s'%(k,v),1) if self.prog: self.prog(-1,_('Importing recipe')) # parsed ingredients... if k=='ingredient_parsed': if not isinstance(v, list): v = [v] for ingdic in v: if self.prog: self.prog(-1,_('Processing ingredients')) # we take a special keyword, "text", which gets # parsed if 'text' in ingdic: d = self.rd.parse_ingredient(ingdic['text'],conv=self.conv) if d: for dk,dv in list(d.items()): if dk not in ingdic or not ingdic[dk]: ingdic[dk]=dv elif 'item' not in ingdic: ingdic['item']=ingdic['text'] del ingdic['text'] self.start_ing(**ingdic) self.commit_ing() continue # Listy stuff... elif isinstance(v, list): if k in self.JOIN_AS_PARAGRAPHS: v = "\n".join(v) else: v = " ".join(v) # Ingredients in blocks if k == 'ingredient_block': for l in v.split('\n'): if self.prog: self.prog(-1,_('Processing ingredients.')) dic=self.rd.parse_ingredient(l,conv=self.conv) if dic: self.start_ing(**dic) self.commit_ing() elif k == 'image': try: if v: img = get_image_from_tag(v,self.url) except: print('Error retrieving image') print('tried to retrieve image from %s'%v) else: if img: self.rec['image'] = img else: self.rec[k]=v #print 'COMMITTING RECIPE',self.rec self.commit_rec() if self.prog: self.prog(1,_('Import complete.'))
def do_action(): debug('undoable_modify_ing modifying %s' % dic, 2) self.modify_ing(ing, dic) if make_visible: make_visible(ing, dic)
def characters(self, ch): debug('adding to %s bufs: %s' % (len(self.bufs), ch), 0) for buf in self.bufs: setattr(self, buf, getattr(self, buf) + ch)
def write_grouphead(self, name): debug('write_grouphead called with %s' % name, 0) group = (name, []) self.ings.append(group) # add to our master self.ings = group[1] # change current list to group list