def prot_itemsdone(self, empty): if self.item_tag == None: return if self.discard: self.item_tag = None self.item_buf = [] self.item_removes = [] self.item_adds = [] return if self.item_adds: self.item_tag.add_items(self.item_adds) # Eliminate discarded items. This has to be done here, so we have # access to all of the items given in the multiple ITEM responses. for id in self.item_tag: if id not in self.item_buf: self.item_removes.append(id) if self.item_removes: self.item_tag.remove_items(self.item_removes) self.item_tag = None self.item_buf = [] self.item_removes = [] self.item_adds = [] if self.still_updating: self.still_updating -= 1 if not self.still_updating: log.debug("Calling curses_update_complete") call_hook("curses_update_complete", [])
def run(self): # We want this as early as possible signal.signal(signal.SIGUSR1, self.sigusr1) # Get config from daemon if not config.init(self, CANTO_PROTOCOL_COMPATIBLE): print("Invalid daemon version") print("Wanted: %s" % CANTO_PROTOCOL_COMPATIBLE) print("Got: %s" % config.version) sys.exit(-1) else: log.info("Version check passed: %s" % CANTO_PROTOCOL_COMPATIBLE) # Create Tags for each TagCore self.gui = CantoCursesGui(self, self.glog_handler) tag_updater.init(self) # Initial signal setup. signal.signal(signal.SIGWINCH, self.winch) signal.signal(signal.SIGCHLD, self.child) finalize_eval_settings() call_hook("curses_start", []) if self.plugin_errors: log.error("The following error occurred loading plugins:\n\n%s" % self.plugin_errors) while self.gui.alive: self.gui.tick() time.sleep(1)
def prot_deltags(self, tags): if not self.initd: for tag in tags: if tag in self.vars["strtags"]: self.vars["strtags"].remove(tag) if tag in self.config["tagorder"]: self.config["tagorder"].append(tag) return c = self.get_conf() changes = False for tag in tags: if tag in self.vars["strtags"]: if tag in c["tagorder"]: c["tagorder"] = [ x for x in self.config["tagorder"] if x != tag ] changes = True self.vars["strtags"].remove(tag) call_hook("curses_del_tag", [ tag ]) self.eval_tags() else: log.debug("Got DELTAG for non-existent tag!") if changes: self.set_conf(c)
def eval_tags(self): prevtags = self.vars["curtags"] sorted_tags = [] r = re.compile(self.config["tags"]) for tag in self.vars["strtags"]: # This can happen between the time that a tag is removed from the config # and the time that we receive a DELTAG event. if tag not in self.config["tagorder"]: continue elif r.match(tag): sorted_tags.append((self.config["tagorder"].index(tag), tag)) sorted_tags.sort() self.set_var("curtags", [ x for (i, x) in sorted_tags ]) if not self.vars["curtags"]: log.warn("NOTE: Current 'tags' setting eliminated all tags!") # If evaluated tags differ, we need to let other know. if prevtags != self.vars["curtags"]: log.debug("Evaluated Tags Changed:\n%s\n", json.dumps(self.vars["curtags"], indent=4)) call_hook("curses_eval_tags_changed", [])
def eval_tags(self): prevtags = self.vars["curtags"] sorted_tags = [] r = re.compile(self.config["tags"]) for tag in self.vars["strtags"]: # This can happen between the time that a tag is removed from the config # and the time that we receive a DELTAG event. if tag not in self.config["tagorder"]: continue elif r.match(tag): sorted_tags.append((self.config["tagorder"].index(tag), tag)) sorted_tags.sort() self.set_var("curtags", [x for (i, x) in sorted_tags]) if not self.vars["curtags"]: log.warn("NOTE: Current 'tags' setting eliminated all tags!") # If evaluated tags differ, we need to let other know. if prevtags != self.vars["curtags"]: log.debug("Evaluated Tags Changed:\n%s\n", json.dumps(self.vars["curtags"], indent=4)) call_hook("curses_eval_tags_changed", [])
def prot_deltags(self, tags): if not self.initd: for tag in tags: if tag in self.vars["strtags"]: self.vars["strtags"].remove(tag) if tag in self.config["tagorder"]: self.config["tagorder"].append(tag) return c = self.get_conf() changes = False for tag in tags: if tag in self.vars["strtags"]: if tag in c["tagorder"]: c["tagorder"] = [ x for x in self.config["tagorder"] if x != tag ] changes = True self.vars["strtags"].remove(tag) call_hook("curses_del_tag", [tag]) self.eval_tags() else: log.debug("Got DELTAG for non-existent tag!") if changes: self.set_conf(c)
def on_daemon_serving(): log.debug("Synchronizing subscriptions.") ino_subs = api.get_subs() for c_feed in config.json["feeds"]: url = c_feed["url"] for sub in ino_subs: if sub["url"] == url: break else: log.debug("Old feed: %s", url) call_hook("daemon_del_configs", [None, {"feeds": [c_feed]}]) for sub in ino_subs: url = sub["url"] name = sub["title"] for c_feed in config.json["feeds"]: if c_feed["url"] == url: break if c_feed["name"] == name: log.info("Found feed with same name, but not URL? Skipping.") break else: log.debug("New feed: %s", url) call_hook("daemon_set_configs", [None, { "feeds": [{ "name": name, "url": url }] }])
def prot_deltags(self, tags): if not self.initd: for tag in tags: if tag in self.vars["strtags"]: self.vars["strtags"].remove(tag) if tag in self.config["tagorder"]: self.config["tagorder"].append(tag) return c = self.get_conf() for tag in tags: if tag in self.vars["strtags"]: new_alltags = self.vars["alltags"] i, tagobj = [ x for x in enumerate(new_alltags) if x[1].tag == tag ][0] tagobj.die() # Remove it from our vars. del new_alltags[i] self.vars["alltags"] = new_alltags self.vars["strtags"].remove(tag) call_hook("curses_del_tag", tag) else: log.debug("Got DELTAG for non-existent tag!") if tag in c["tagorder"]: c["tagorder"] = [ x for x in self.config["tagorder"] if x != tag ] self.set_conf(c) self.eval_tags()
def sync_subscriptions(): log.info("Syncing subscriptions with Google") auth = ClientAuthMethod(USERNAME, PASSWORD) reader = GoogleReader(auth) reader.buildSubscriptionList() gurls = [ (f.title, f.feedUrl) for f in reader.getSubscriptionList() ] curls = [ f.URL for f in allfeeds.get_feeds() ] names = [ f.name for f in allfeeds.get_feeds() ] new_feeds = [] for gtitle, gurl in gurls[:]: if gurl not in curls: # Handle name collisions because we're not prepared to handle ERROR # responses from config if gtitle in names: offset = 2 while (gtitle + " (%d)" % offset) in names: offset += 1 gtitle = gtitle + " (%d)" % offset attrs = { "url" : gurl, "name" : gtitle } new_feeds.append(attrs) names.append(gtitle) call_hook("set_configs", [ None, { "feeds" : new_feeds }]) for curl in curls[:]: if curl not in gurls: reader.subscribe('feed/' + curl)
def on_daemon_serving(): log.debug("Synchronizing subscriptions.") ino_subs = api.get_subs() for c_feed in config.json["feeds"]: url = c_feed["url"] for sub in ino_subs: if sub["url"] == url: break else: log.debug("Old feed: %s", url) call_hook("daemon_del_configs", [ None, { "feeds" : [ c_feed ] } ] ) for sub in ino_subs: url = sub["url"] name = sub["title"] for c_feed in config.json["feeds"]: if c_feed["url"] == url: break if c_feed["name"] == name: log.info("Found feed with same name, but not URL? Skipping.") break else: log.debug("New feed: %s", url) call_hook("daemon_set_configs", [ None, { "feeds" : [ { "name" : name, "url" : url } ] } ])
def reset(self): self.lock.acquire_write() call_hook("curses_items_removed", [ self, self[:] ]) del self[:] self.changed() self.lock.release_write()
def set_var(self, tweak, value): # We only care if the value is different, or it's a message # value, which should always cause a fresh message display, # even if it's the same error as before. if self.vars[tweak] != value: self.vars[tweak] = value call_hook("curses_var_change", [{tweak: value}])
def set_var(self, tweak, value): # We only care if the value is different, or it's a message # value, which should always cause a fresh message display, # even if it's the same error as before. if self.vars[tweak] != value: self.vars[tweak] = value call_hook("curses_var_change", [{ tweak : value }])
def reset(self): for item in self: item.die() call_hook("curses_items_removed", [ self, self[:] ]) del self[:] # Request redraw to update item counts. self.need_redraw()
def on_del_tag(self, tag): for tagcore in alltagcores[:]: if tagcore.tag == tag: if len(tagcore): call_hook("curses_items_removed", [ tagcore, tagcore ] ) tagcore.set_items([]) call_hook("curses_del_tagcore", [ tagcore ]) alltagcores.remove(tagcore) while tagcore in self.updating: self.updating.remove(tagcore) return
def on_del_tag(self, tag): for tagcore in alltagcores[:]: if tagcore.tag == tag: if len(tagcore): call_hook("curses_items_removed", [tagcore, tagcore]) tagcore.set_items([]) call_hook("curses_del_tagcore", [tagcore]) alltagcores.remove(tagcore) while tagcore in self.updating: self.updating.remove(tagcore) return
def add_items(self, ids): self.lock.acquire_write() added = [] for id in ids: self.append(id) added.append(id) call_hook("curses_items_added", [ self, added ] ) self.changed() self.lock.release_write()
def prot_configs(self, given, write = False): log.debug("prot_configs given:\n%s\n" % json.dumps(given, indent=4, sort_keys=True)) if "tags" in given: for tag in list(given["tags"].keys()): ntc = given["tags"][tag] tc = self.get_tag_conf(tag) changes, deletions =\ self.validate_config(ntc, tc, self.tag_validators) if changes: self.tag_config[tag] = ntc call_hook("curses_tag_opt_change", [ { tag : changes } ]) if write: self.write("SETCONFIGS", { "tags" : { tag : changes }}) if deletions and write: self.write("DELCONFIGS", { "tags" : { tag : deletions }}) if "CantoCurses" in given: new_config = given["CantoCurses"] changes, deletions =\ self.validate_config(new_config, self.config,\ self.validators) if changes: self.config = new_config call_hook("curses_opt_change", [ changes ]) if write: self.write("SETCONFIGS", { "CantoCurses" : changes }) if deletions and write: self.write("DELCONFIGS", { "CantoCurses" : deletions }) if "defaults" in given: # We don't honor any default settings, so just record them # and pass them on to the daemon if write self.daemon_defaults.update(given["defaults"]) if write: self.write("SETCONFIGS", { "defaults" : self.daemon_defaults }) call_hook("curses_def_opt_change", [ given["defaults"] ]) if "feeds" in given: self.daemon_feedconf = given["feeds"] if write: self.write("SETCONFIGS", { "feeds" : self.daemon_feedconf }) call_hook("curses_feed_opt_change", [ given["feeds"] ]) self.initd = True
def start(self): try: self.init() self.run() except KeyboardInterrupt: pass except Exception as e: tb = traceback.format_exc() log.error("Exiting on exception:") log.error("\n" + "".join(tb)) call_hook("curses_exit", []) log.info("Exiting.") sys.exit(0)
def add_items(self, ids): added = [] for id in ids: s = Story(id, self.callbacks) self.append(s) added.append(s) rel = len(self) - 1 s.set_rel_offset(rel) s.set_offset(self.item_offset + rel) s.set_sel_offset(self.sel_offset + rel) # Request redraw to update item counts. self.need_redraw() call_hook("curses_items_added", [ self, added ] )
def reset(self): # Tag should be sorted on sync if we were reset, regardless of whether # a sync was done when the tag was empty, so keep track of this and # the Tag object will clear it on sync. self.was_reset = True self.lock.acquire_write() if len(self): call_hook("curses_items_removed", [ self, self[:] ]) del self[:] self.changed() self.lock.release_write()
def set_var(self, tweak, value): # We only care if the value is different, or it's a message # value, which should always cause a fresh message display, # even if it's the same error as before. config_lock.acquire_write() if self.vars[tweak] != value: # If we're selecting or unselecting a story, then # we need to make sure it doesn't disappear. self.vars[tweak] = value config_lock.release_write() call_hook("curses_var_change", [{ tweak : value }]) else: config_lock.release_write()
def prot_newtags(self, tags): if not self.initd: for tag in tags: if tag not in self.vars["strtags"]: self.vars["strtags"].append(tag) if tag not in self.config["tagorder"]: self.config["tagorder"].append(tag) return c = self.get_conf() # Likely the same as tags changes = False newtags = [] for tag in tags: if tag not in c["tagorder"]: c["tagorder"] = c["tagorder"] + [tag] changes = True if tag not in self.vars["strtags"]: # If we don't have configuration for this # tag already, substitute the default template. if tag not in self.tag_config: log.debug("Using default tag config for %s", tag) self.tag_config[tag] = self.tag_template_config.copy() self.vars["strtags"].append(tag) newtags.append(tag) changes = True # If there aren't really any tags we didn't know about, no bail. if not changes: return self.set_conf(c) for tag in newtags: log.debug("New tag %s", tag) call_hook("curses_new_tag", [tag]) self.eval_tags()
def prot_newtags(self, tags): if not self.initd: for tag in tags: if tag not in self.vars["strtags"]: self.vars["strtags"].append(tag) if tag not in self.config["tagorder"]: self.config["tagorder"].append(tag) return c = self.get_conf() # Likely the same as tags changes = False newtags = [] for tag in tags: if tag not in c["tagorder"]: c["tagorder"] = c["tagorder"] + [ tag ] changes = True if tag not in self.vars["strtags"]: # If we don't have configuration for this # tag already, substitute the default template. if tag not in self.tag_config: log.debug("Using default tag config for %s", tag) self.tag_config[tag] = self.tag_template_config.copy() self.vars["strtags"].append(tag) newtags.append(tag) changes = True # If there aren't really any tags we didn't know about, no bail. if not changes: return self.set_conf(c) for tag in newtags: log.debug("New tag %s", tag) call_hook("curses_new_tag", [ tag ]) self.eval_tags()
def prot_attributes(self, d): # Update attributes, and then notify everyone to grab new content. self.lock.acquire_write() for key in d.keys(): if key in self.attributes: # If we're updating, we want to create a whole new dict object # so that our stories dicts don't get updated without a sync cp = self.attributes[key].copy() cp.update(d[key]) self.attributes[key] = cp else: self.attributes[key] = d[key] self.lock.release_write() call_hook("curses_attributes", [self.attributes])
def prot_attributes(self, d): # Update attributes, and then notify everyone to grab new content. self.lock.acquire_write() for key in d.keys(): if key in self.attributes: # If we're updating, we want to create a whole new dict object # so that our stories dicts don't get updated without a sync cp = self.attributes[key].copy() cp.update(d[key]) self.attributes[key] = cp else: self.attributes[key] = d[key] self.lock.release_write() call_hook("curses_attributes", [ self.attributes ])
def remove_items(self, ids): self.lock.acquire_write() removed = [] # Copy self so we can remove from self # without screwing up iteration. for idx, id in enumerate(self[:]): if id in ids: log.debug("removing: %s" % (id,)) list.remove(self, id) removed.append(id) call_hook("curses_items_removed", [ self, removed ] ) self.changed() self.lock.release_write()
def eval_tags(self): prevtags = self.vars["curtags"] sorted_tags = [] r = re.compile(self.config["tags"]) for tag in self.vars["alltags"]: if r.match(tag.tag): sorted_tags.append((self.config["tagorder"].index(tag.tag), tag)) sorted_tags.sort() self.set_var("curtags", [ x for (i, x) in sorted_tags ]) if not self.vars["curtags"]: log.warn("NOTE: Current 'tags' setting eliminated all tags!") # If evaluated tags differ, we need to refresh. if prevtags != self.vars["curtags"] and self.screen: log.debug("Evaluated Tags Changed: %s" % [ t.tag for t in self.vars["curtags"]]) call_hook("curses_eval_tags_changed", [])
def prot_items(self, updates): # Daemon should now only return with one tag in an items response tag = list(updates.keys())[0] for have_tag in alltagcores: if have_tag.tag == tag: break else: return sorted_updated_ids = list(enumerate(updates[tag])) sorted_updated_ids.sort(key=lambda x: x[1]) sorted_current_ids = list(enumerate(have_tag)) sorted_updated_ids.sort(key=lambda x: x[1]) new_ids = [] cur_ids = [] old_ids = [] for c_place, c_id in sorted_current_ids: while sorted_updated_ids and c_id > sorted_updated_ids[0][1]: new_ids.append(sorted_updated_ids.pop(0)) if not sorted_updated_ids or c_id < sorted_updated_ids[0][1]: old_ids.append(c_id) else: place = sorted_updated_ids.pop(0)[0] cur_ids.append((place, c_id)) new_ids += sorted_updated_ids all_ids = new_ids + cur_ids all_ids.sort() have_tag.set_items([x[1] for x in all_ids]) if new_ids: call_hook("curses_items_added", [have_tag, [x[1] for x in new_ids]]) if old_ids: call_hook("curses_items_removed", [have_tag, old_ids]) if have_tag in self.updating: have_tag.was_reset = True call_hook("curses_tag_updated", [have_tag]) self.updating.remove(have_tag) if self.updating == []: call_hook("curses_update_complete", [])
def prot_items(self, updates): # Daemon should now only return with one tag in an items response tag = list(updates.keys())[0] for have_tag in alltagcores: if have_tag.tag == tag: break else: return sorted_updated_ids = list(enumerate(updates[tag])) sorted_updated_ids.sort(key=lambda x : x[1]) sorted_current_ids = list(enumerate(have_tag)) sorted_updated_ids.sort(key=lambda x : x[1]) new_ids = [] cur_ids = [] old_ids = [] for c_place, c_id in sorted_current_ids: while sorted_updated_ids and c_id > sorted_updated_ids[0][1]: new_ids.append(sorted_updated_ids.pop(0)) if not sorted_updated_ids or c_id < sorted_updated_ids[0][1]: old_ids.append(c_id) else: place = sorted_updated_ids.pop(0)[0] cur_ids.append((place, c_id)) new_ids += sorted_updated_ids all_ids = new_ids + cur_ids all_ids.sort() have_tag.set_items([x[1] for x in all_ids]) if new_ids: call_hook("curses_items_added", [ have_tag, [x[1] for x in new_ids] ] ) if old_ids: call_hook("curses_items_removed", [ have_tag, old_ids ] ) if have_tag in self.updating: have_tag.was_reset = True call_hook("curses_tag_updated", [ have_tag ]) self.updating.remove(have_tag) if self.updating == []: call_hook("curses_update_complete", [])
def cmd_syncto(self, socket=None, args=None): if self.fresh_content: f, fname = mkstemp() os.close(f) # Lock feeds to make sure nothing's in flight wlock_all() # Sync the shelf so it's all on disk self.backend.shelf.sync() shutil.copyfile(self.backend.feed_path, fname) # Let everything else continue wunlock_all() call_hook("daemon_syncto", ["db", fname]) # Cleanup temp file os.unlink(fname) self.fresh_content = False self.sent_content = True if self.fresh_config: f, fname = mkstemp() os.close(f) config_lock.acquire_read() shutil.copyfile(self.backend.conf_path, fname) config_lock.release_read() call_hook("daemon_syncto", ["conf", fname]) os.unlink(fname) self.fresh_config = False self.sent_config = True
def cmd_syncto(self, socket = None, args = None): if self.fresh_content: f, fname = mkstemp() os.close(f) # Lock feeds to make sure nothing's in flight wlock_all() # Sync the shelf so it's all on disk self.backend.shelf.sync() shutil.copyfile(self.backend.feed_path, fname) # Let everything else continue wunlock_all() call_hook("daemon_syncto", [ "db", fname ]) # Cleanup temp file os.unlink(fname) self.fresh_content = False self.sent_content = True if self.fresh_config: f, fname = mkstemp() os.close(f) config_lock.acquire_read() shutil.copyfile(self.backend.conf_path, fname) config_lock.release_read() call_hook("daemon_syncto", [ "conf", fname ]) os.unlink(fname) self.fresh_config = False self.sent_config = True
def set_var(self, tweak, value): # We only care if the value is different, or it's a message # value, which should always cause a fresh message display, # even if it's the same error as before. if self.vars[tweak] != value: # If we're selecting or unselecting a story, then # we need to make sure it doesn't disappear. if tweak in [ "selected", "reader_item" ]: if self.vars[tweak] and hasattr(self.vars[tweak], "id"): self.vars["protected_ids"].remove(self.vars[tweak].id) self.write("UNPROTECT",\ { "filter-immune" : [ self.vars[tweak].id ] }) # Fake a TAGCHANGE because unprotected items have the # possibility to filtered out and we only refresh items for # tags that get a TAGCHANGE on tick. for tag in self.vars["alltags"]: if self.vars[tweak].id in tag.get_ids(): self.prot_tagchange(tag.tag) if value and hasattr(value, "id"): # protected_ids just tells the prot_items to not allow # this item to have it's auto protection stripped. self.vars["protected_ids"].append(value.id) # Set an additional protection, filter-immune so hardened # filters won't eliminate it. self.write("PROTECT", { "filter-immune" : [ value.id ] }) self.vars[tweak] = value call_hook("curses_var_change", [{ tweak : value }])
def prot_configs(self, given, write = False): log.debug("prot_configs given: %s" % given) if "tags" in given: for tag in list(given["tags"].keys()): ntc = given["tags"][tag] tc = self.tag_config[tag] changes, deletions =\ self.validate_config(ntc, tc, self.tag_validators) if changes: self.tag_config[tag] = ntc call_hook("curses_tag_opt_change", [ { tag : changes } ]) if write: self.write("SETCONFIGS", { "tags" : { tag : changes }}) if deletions and write: self.write("DELCONFIGS", { "tags" : { tag : deletions }}) if "CantoCurses" in given: new_config = given["CantoCurses"] changes, deletions =\ self.validate_config(new_config, self.config,\ self.validators) if changes: self.config = new_config call_hook("curses_opt_change", [ changes ]) if write: self.write("SETCONFIGS", { "CantoCurses" : changes }) if deletions and write: self.write("DELCONFIGS", { "CantoCurses" : deletions })
def remove_items(self, ids): removed = [] # Copy self so we can remove from self # without screwing up iteration. for idx, item in enumerate(self[:]): if item.id in ids: log.debug("removing: %s" % (item.id,)) list.remove(self, item) item.die() removed.append(item) # Update indices of items. for i, story in enumerate(self): story.set_rel_offset(i) story.set_offset(self.item_offset + i) story.set_sel_offset(self.sel_offset + i) # Request redraw to update item counts. self.need_redraw() call_hook("curses_items_removed", [ self, removed ] )
def on_new_tag(self, tag): self.write("WATCHTAGS", [tag]) self.prot_tagchange(tag) call_hook("curses_new_tagcore", [TagCore(tag)])
def check(self): config_script = { 'VERSION' : { '*' : [('VERSION', CANTO_PROTOCOL_COMPATIBLE)] }, 'CONFIGS' : { '*' : [('CONFIGS', { "CantoCurses" : config.template_config })] }, } config_backend = TestBackend("config", config_script) config.init(config_backend, CANTO_PROTOCOL_COMPATIBLE) config_backend.inject("NEWTAGS", [ "maintag:Slashdot", "maintag:reddit" ]) tagcore_script = {} tag_backend = TestBackend("tagcore", tagcore_script) on_hook("curses_items_removed", self.on_items_removed) on_hook("curses_items_added", self.on_items_added) on_hook("curses_new_tagcore", self.on_new_tagcore) on_hook("curses_del_tagcore", self.on_del_tagcore) on_hook("curses_attributes", self.on_attributes) on_hook("curses_update_complete", self.on_update_complete) on_hook("curses_tag_updated", self.on_tag_updated) # 1. Previously existing tags in config should be populated on init self.reset_flags() tag_updater.init(tag_backend) for tag in config.vars["strtags"]: for tc in alltagcores: if tc.tag == tag: break else: raise Exception("Couldn't find TC for tag %s" % tag) self.compare_flags(NEW_TC) self.reset_flags() # 2. Getting empty ITEMS responses should cause no events tag_backend.inject("ITEMS", { "maintag:Slashdot" : [] }) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ITEMS", { "maintag:reddit" : [] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(0) # 3. Getting a non-empty ITEMS response should cause items_added tag_backend.inject("ITEMS", { "maintag:Slashdot" : [ "id1", "id2" ] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_ADDED) # 4. Getting attributes should cause attributes hook self.reset_flags() id1_content = { "title" : "id1", "canto-state" : [], "canto-tags" : [], "link" : "id1-link", "enclosures" : "" } id2_content = { "title" : "id2", "canto-state" : [], "canto-tags" : [], "link" : "id2-link", "enclosures" : "" } all_content = { "id1" : id1_content, "id2" : id2_content } tag_backend.inject("ATTRIBUTES", all_content) self.compare_flags(ATTRIBUTES) self.compare_var("attributes", all_content) id1_got = tag_updater.get_attributes("id1") if id1_got != id1_content: raise Exception("Bad content: wanted %s - got %s" % (id1_content, id1_got)) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 5. Removing an item should *NOT* cause its attributes to be forgotten # that happens on stories_removed, and should cause ITEMS_REMOVED self.reset_flags() tag_backend.inject("ITEMS", { "maintag:Slashdot" : [ "id1" ] }) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_REMOVED) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 6. Getting a stories_removed hook should make it forget attributes self.reset_flags() call_hook("curses_stories_removed", [ FakeTag("maintag:Slashdot"), [ FakeStory("id2") ] ]) if "id2" in tag_updater.attributes: raise Exception("Expected id2 to be removed, but it isn't!") self.compare_flags(0) # 7. Getting attributes for non-existent IDs should return empty id2_got = tag_updater.get_attributes("id2") if id2_got != {}: raise Exception("Expected non-existent id to return empty! Got %s" % id2_got) self.compare_flags(0) # 8. Getting stories_removed for item still in tag should do nothing call_hook("curses_stories_removed", [ FakeTag("maintag:Slashdot"), [ FakeStory("id1") ] ]) if "id1" not in tag_updater.attributes: raise Exception("Expected id1 to remain in attributes!") self.compare_flags(0) # 9. Config adding a tag should create a new tagcore config_backend.inject("NEWTAGS", [ "maintag:Test1" ]) self.compare_flags(NEW_TC) # 10. Config removing an empty tag should delete a tagcore self.reset_flags() config_backend.inject("DELTAGS", [ "maintag:reddit" ]) self.compare_flags(DEL_TC) self.compare_var("del_tc", "maintag:reddit") # 11. Config removing an populated tag should delete a tagcore and # cause items_removed. NOTE for now tagcores are never deleted, they # just exist empty self.reset_flags() config_backend.inject("DELTAGS", [ "maintag:Slashdot" ]) self.compare_flags(DEL_TC | ITEMS_REMOVED) self.compare_var("del_tc", "maintag:Slashdot") self.compare_var("oir_tctag", "maintag:Slashdot") self.compare_var("oir_tcids", [ "id1" ]) # 12. Update should cause all tags to generate a tag_update # hook call on ITEMS, and update_complete when all done. self.reset_flags() tag_updater.update() tag_backend.inject("ITEMS", { "maintag:Test1" : [ "id3", "id4" ] }) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ATTRIBUTES", { "id3" : { "test" : "test" }, "id4" : { "test" : "test" }}) print(tag_updater.updating) self.compare_flags(TAG_UPDATED | UPDATE_COMPLETE | ITEMS_ADDED | ATTRIBUTES) self.compare_var("otu_tag", "maintag:Test1") return True
def prot_configs(self, given, write = False): log.debug("prot_configs given:\n%s\n" % json.dumps(given, indent=4, sort_keys=True)) if "tags" in given: for tag in list(given["tags"].keys()): ntc = given["tags"][tag] tc = self.get_tag_conf(tag) changes, deletions =\ self.validate_config(ntc, tc, self.tag_validators) if changes: self.tag_config[tag] = ntc call_hook("curses_tag_opt_change", [ { tag : changes } ]) if write: self.write("SETCONFIGS", { "tags" : { tag : changes }}) if deletions and write: self.write("DELCONFIGS", { "tags" : { tag : deletions }}) if "CantoCurses" in given: new_config = given["CantoCurses"] changes, deletions =\ self.validate_config(new_config, self.config,\ self.validators) if changes: self.config = new_config call_hook("curses_opt_change", [ changes ]) if "tags" in changes: self.eval_tags() if write: self.write("SETCONFIGS", { "CantoCurses" : changes }) if deletions and write: self.write("DELCONFIGS", { "CantoCurses" : deletions }) if "defaults" in given: changes = {} for key in given["defaults"]: if key in self.daemon_defaults: if given["defaults"][key] != self.daemon_defaults[key]: changes[key] = given["defaults"][key] else: changes[key] = given["defaults"][key] self.daemon_defaults.update(changes) if write: self.write("SETCONFIGS", { "defaults" : self.daemon_defaults }) call_hook("curses_def_opt_change", [ changes ]) if "feeds" in given: self.daemon_feedconf = given["feeds"] if write: self.write("SETCONFIGS", { "feeds" : self.daemon_feedconf }) call_hook("curses_feed_opt_change", [ given["feeds"] ]) self.initd = True
def sync(self, force=False): if force or self.tagcore.changes: current_stories = [] added_stories = [] sel = self.callbacks["get_var"]("selected") self.tagcore.lock.acquire_read() self.tagcore.ack_changes() for story in self: if story.id in self.tagcore: current_stories.append( (self.tagcore.index(story.id), story)) elif story == sel: # If we preserve the selection in an "undead" state, then # we keep set tagcore changed so that the next sync operation # will re-evaluate it. self.tagcore.changed() if current_stories: place = max([x[0] for x in current_stories]) + .5 else: place = -1 current_stories.append((place, story)) for place, id in enumerate(self.tagcore): if id not in [x[1].id for x in current_stories]: s = Story(self, id, self.callbacks) current_stories.append((place, s)) added_stories.append(s) self.tagcore.lock.release_read() call_hook("curses_stories_added", [self, added_stories]) conf = config.get_conf() if conf["update"]["style"] == "maintain" or self.tagcore.was_reset: self.tagcore.was_reset = False current_stories.sort() current_stories = [x[1] for x in current_stories] deleted = [] for story in self: if not story in current_stories: deleted.append(story) story.die() # Properly dispose of the remaining stories call_hook("curses_stories_removed", [self, deleted]) del self[:] self.extend(current_stories) # Trigger a refresh so that classes above (i.e. TagList) will remap # items self.need_refresh() # Pass the sync onto story objects for s in self: s.sync() self.updates_pending = 0
def on_del_tag(self, tag): for tagcore in alltagcores: if tagcore.tag == tag: tagcore.reset() call_hook("curses_del_tagcore", [ tagcore ]) return
def prot_configs(self, given, write=False): log.debug("prot_configs given:\n%s\n", json.dumps(given, indent=4, sort_keys=True)) if "tags" in given: for tag in list(given["tags"].keys()): ntc = given["tags"][tag] tc = self.get_tag_conf(tag) changes, deletions =\ self.validate_config(ntc, tc, self.tag_validators) if write: if changes: self.wait_write("SETCONFIGS", {"tags": {tag: changes}}) if deletions: self.wait_write("DELCONFIGS", {"tags": { tag: deletions }}) if changes: self.tag_config[tag] = ntc call_hook("curses_tag_opt_change", [{tag: changes}]) if "CantoCurses" in given: new_config = given["CantoCurses"] if "config_version" in new_config: self.config_version = new_config["config_version"] changes, deletions =\ self.validate_config(new_config, self.config,\ self.validators) if "config_version" not in new_config or\ new_config["config_version"] != CURRENT_CONFIG_VERSION: log.debug("Configuration migrated from %s to %s",\ self.config_version, CURRENT_CONFIG_VERSION) self.config_version = CURRENT_CONFIG_VERSION new_config["config_version"] = CURRENT_CONFIG_VERSION changes["config_version"] = CURRENT_CONFIG_VERSION self.write("SETCONFIGS", { "CantoCurses": { "config_version": CURRENT_CONFIG_VERSION } }) if write: if changes: self.wait_write("SETCONFIGS", {"CantoCurses": changes}) if deletions: self.wait_write("DELCONFIGS", {"CantoCurses": deletions}) if changes: self.config = new_config call_hook("curses_opt_change", [changes]) if "tags" in changes: self.eval_tags() if "defaults" in given: changes = {} for key in given["defaults"]: if key in self.daemon_defaults: if given["defaults"][key] != self.daemon_defaults[key]: changes[key] = given["defaults"][key] else: changes[key] = given["defaults"][key] self.daemon_defaults.update(changes) if write: self.wait_write("SETCONFIGS", {"defaults": self.daemon_defaults}) call_hook("curses_def_opt_change", [changes]) if "feeds" in given: self.daemon_feedconf = given["feeds"] if write: self.wait_write("SETCONFIGS", {"feeds": self.daemon_feedconf}) call_hook("curses_feed_opt_change", [given["feeds"]]) self.initd = True
def check(self): hooks.on_hook("test", self.hook_a) # No key hooks.on_hook("test", self.hook_b, "first_remove") hooks.on_hook("test", self.hook_c, "first_remove") hooks.on_hook("test2", self.hook_a, "second_remove") hooks.call_hook("test", []) if self.test_set != "abc": raise Exception("Basic hook test failed: %s" % self.test_set) self.test_set = "" hooks.call_hook("test2", []) if self.test_set != "a": raise Exception("Basic hook test2 failed: %s" % self.test_set) self.test_set = "" hooks.unhook_all("first_remove") hooks.call_hook("test", []) if self.test_set != "a": raise Exception("unhook_all failed: %s" % self.test_set) self.test_set = "" hooks.remove_hook("test", self.hook_a) hooks.call_hook("test", []) if self.test_set != "": raise Exception("remove_hook failed: %s" % self.test_set) hooks.call_hook("test2", []) if self.test_set != "a": raise Exception("improper hook removed: %s" % self.test_set) hooks.unhook_all("second_remove") if hooks.hooks != {}: raise Exception("hooks.hooks should be empty! %s" % hooks.hooks) hooks.on_hook("argtest", self.hook_args) for args in [ [], ["abc"], [1, 2, 3] ]: self.test_args = [] hooks.call_hook("argtest", args) if self.test_args != tuple(args): raise Exception("hook arguments failed in %s out %s" % (args, self.test_args)) return True
def on_new_tag(self, tag): self.prot_tagchange(tag) call_hook("curses_new_tagcore", [ TagCore(tag) ])
def prot_configs(self, given, write = False): log.debug("prot_configs given:\n%s\n", json.dumps(given, indent=4, sort_keys=True)) if "tags" in given: for tag in list(given["tags"].keys()): ntc = given["tags"][tag] tc = self.get_tag_conf(tag) changes, deletions =\ self.validate_config(ntc, tc, self.tag_validators) if write: if changes: self.wait_write("SETCONFIGS", { "tags" : { tag : changes }}) if deletions: self.wait_write("DELCONFIGS", { "tags" : { tag : deletions }}) if changes: self.tag_config[tag] = ntc call_hook("curses_tag_opt_change", [ { tag : changes } ]) if "CantoCurses" in given: new_config = given["CantoCurses"] if "config_version" in new_config: self.config_version = new_config["config_version"] changes, deletions =\ self.validate_config(new_config, self.config,\ self.validators) if "config_version" not in new_config or\ new_config["config_version"] != CURRENT_CONFIG_VERSION: log.debug("Configuration migrated from %s to %s",\ self.config_version, CURRENT_CONFIG_VERSION) self.config_version = CURRENT_CONFIG_VERSION new_config["config_version"] = CURRENT_CONFIG_VERSION changes["config_version"] = CURRENT_CONFIG_VERSION self.write("SETCONFIGS", { "CantoCurses" : {"config_version" : CURRENT_CONFIG_VERSION } }) if write: if changes: self.wait_write("SETCONFIGS", { "CantoCurses" : changes }) if deletions: self.wait_write("DELCONFIGS", { "CantoCurses" : deletions }) if changes: self.config = new_config call_hook("curses_opt_change", [ changes ]) if "tags" in changes: self.eval_tags() if "defaults" in given: changes = {} for key in given["defaults"]: if key in self.daemon_defaults: if given["defaults"][key] != self.daemon_defaults[key]: changes[key] = given["defaults"][key] else: changes[key] = given["defaults"][key] self.daemon_defaults.update(changes) if write: self.wait_write("SETCONFIGS", { "defaults" : self.daemon_defaults }) call_hook("curses_def_opt_change", [ changes ]) if "feeds" in given: self.daemon_feedconf = given["feeds"] if write: self.wait_write("SETCONFIGS", { "feeds" : self.daemon_feedconf }) call_hook("curses_feed_opt_change", [ given["feeds"] ]) self.initd = True
def sync(self, force=False): if force or self.tagcore.changes: sel = self.callbacks["get_var"]("selected") self.tagcore.lock.acquire_read() self.tagcore.ack_changes() # Get sorted ids, along with their (unsorted) positions in the # original lists. sorted_ids = [(x, x.id, i) for (i, x) in enumerate(self)] sorted_ids.sort(key=lambda x: x[1]) tagcore_sorted_ids = list(enumerate(self.tagcore)) tagcore_sorted_ids.sort(key=lambda x: x[1]) new_ids = [] current_stories = [] old_stories = [] for story, s_id, place in sorted_ids: while tagcore_sorted_ids and s_id > tagcore_sorted_ids[0][1]: new_ids.append(tagcore_sorted_ids.pop(0)) if not tagcore_sorted_ids or s_id < tagcore_sorted_ids[0][1]: if sel and (not sel.is_tag) and (s_id == sel.id): # If we preserve the selection in an "undead" state, then # we keep set tagcore changed so that the next sync operation # will re-evaluate it. self.tagcore.changed() place = -1 current_stories.append((place, story)) else: old_stories.append(story) else: place = tagcore_sorted_ids.pop(0)[0] current_stories.append((place, story)) # Grab any remaining new items new_ids += tagcore_sorted_ids self.tagcore.lock.release_read() new_stories = [(p, Story(self, x, self.callbacks)) for (p, x) in new_ids] call_hook("curses_stories_added", [self, [x for (p, x) in new_stories]]) del self[:] conf = config.get_conf() if conf["update"]["style"] == "maintain" or self.tagcore.was_reset: self.tagcore.was_reset = False current_stories += new_stories current_stories.sort() self.extend([x[1] for x in current_stories]) else: current_stories.sort() new_stories.sort() if conf["update"]["style"] == "append": current_stories += new_stories self.extend([x[1] for x in current_stories]) else: new_stories += current_stories self.extend([x[1] for x in new_stories]) for story in old_stories: story.die() # Properly dispose of the remaining stories call_hook("curses_stories_removed", [self, old_stories]) # Trigger a refresh so that classes above (i.e. TagList) will remap # items self.need_refresh() # Pass the sync onto story objects for s in self: s.sync() self.updates_pending = 0
def cmd_sync(self, socket = None, args = None): needs_syncto = False if not self.sent_config: f, fname = mkstemp() os.close(f) call_hook("daemon_syncfrom", [ "conf", fname ]) conf_stat = os.stat(self.backend.conf_path) sync_stat = os.stat(fname) log.debug('conf: %s sync: %s' % (conf_stat.st_mtime, sync_stat.st_mtime)) diff = sync_stat.st_mtime - conf_stat.st_mtime # Will be empty tempfile if syncfrom failed. if sync_stat.st_size != 0: if diff > 0: log.debug("conf: We are older") parse_locks() shutil.move(fname, self.backend.conf_path) config.parse() parse_unlocks() # Echo these changes to all connected sockets that care for socket in self.backend.watches["config"]: self.backend.in_configs({}, socket) elif diff == 0: log.debug("conf: We are equal") os.unlink(fname) else: log.debug("conf: We are newer") os.unlink(fname) self.fresh_config = True needs_syncto = True else: os.unlink(fname) if not self.sent_content: f, fname = mkstemp() os.close(f) call_hook("daemon_syncfrom", [ "db", fname ]) diff = self.time_diff(fname) if diff > 0: # Lock feeds to make sure nothing's in flight wlock_all() # Close the file so we can replace it. self.backend.shelf.close() shutil.move(fname, self.backend.feed_path) self.backend.shelf.open() # First half of wunlock_all, release these locks so # fetch threads can get locks for feed in sorted(allfeeds.feeds.keys()): allfeeds.feeds[feed].lock.release_write() # Force feeds to be repopulated from disk, which will handle # communicating changes to connections self.backend.fetch.fetch(True, True) self.backend.fetch.reap(True) # Complete wunlock_all() feed_lock.release_write() # Equal, just clear it up elif diff == 0: os.unlink(fname) # If we're actually newer on a syncfrom then make syncto happen # next time. This can happen on init. else: os.unlink(fname) self.fresh_content = True needs_syncto = True if needs_syncto: self.cmd_syncto() self.reset()
def prot_attributes(self, d): call_hook("curses_attributes", [ d ])
def cmd_sync(self, socket=None, args=None): needs_syncto = False if not self.sent_config: f, fname = mkstemp() os.close(f) call_hook("daemon_syncfrom", ["conf", fname]) conf_stat = os.stat(self.backend.conf_path) sync_stat = os.stat(fname) log.debug('conf: %s sync: %s' % (conf_stat.st_mtime, sync_stat.st_mtime)) diff = sync_stat.st_mtime - conf_stat.st_mtime # Will be empty tempfile if syncfrom failed. if sync_stat.st_size != 0: if diff > 0: log.debug("conf: We are older") parse_locks() shutil.move(fname, self.backend.conf_path) config.parse() parse_unlocks() # Echo these changes to all connected sockets that care for socket in self.backend.watches["config"]: self.backend.in_configs({}, socket) elif diff == 0: log.debug("conf: We are equal") os.unlink(fname) else: log.debug("conf: We are newer") os.unlink(fname) self.fresh_config = True needs_syncto = True else: os.unlink(fname) if not self.sent_content: f, fname = mkstemp() os.close(f) call_hook("daemon_syncfrom", ["db", fname]) diff = self.time_diff(fname) if diff > 0: # Lock feeds to make sure nothing's in flight wlock_all() # Close the file so we can replace it. self.backend.shelf.close() shutil.move(fname, self.backend.feed_path) self.backend.shelf.open() # Clear out all of the currently tagged items. Usually on # update, we're able to discard items that we have in old # content, but aren't in new. But since we just replaced all of # our old content with a totally fresh copy, we might not know # they exist. Can't use reset() because we don't want to lose # configuration. alltags.clear_tags() # First half of wunlock_all, release these locks so # fetch threads can get locks for feed in sorted(allfeeds.feeds.keys()): allfeeds.feeds[feed].lock.release_write() # Complete wunlock_all() feed_lock.release_write() # Force feeds to be repopulated from disk, which will handle # communicating changes to connections self.backend.fetch.fetch(True, True) self.backend.fetch.reap(True) # Equal, just clear it up elif diff == 0: os.unlink(fname) # If we're actually newer on a syncfrom then make syncto happen # next time. This can happen on init. else: os.unlink(fname) self.fresh_content = True needs_syncto = True if needs_syncto: self.cmd_syncto() self.reset()
def check(self): config_script = { 'VERSION': { '*': [('VERSION', CANTO_PROTOCOL_COMPATIBLE)] }, 'CONFIGS': { '*': [('CONFIGS', { "CantoCurses": config.template_config })] }, } config_backend = TestBackend("config", config_script) config.init(config_backend, CANTO_PROTOCOL_COMPATIBLE) config_backend.inject("NEWTAGS", ["maintag:Slashdot", "maintag:reddit"]) tagcore_script = {} tag_backend = TestBackend("tagcore", tagcore_script) on_hook("curses_items_removed", self.on_items_removed) on_hook("curses_items_added", self.on_items_added) on_hook("curses_new_tagcore", self.on_new_tagcore) on_hook("curses_del_tagcore", self.on_del_tagcore) on_hook("curses_attributes", self.on_attributes) on_hook("curses_update_complete", self.on_update_complete) on_hook("curses_tag_updated", self.on_tag_updated) # 1. Previously existing tags in config should be populated on init self.reset_flags() tag_updater.init(tag_backend) for tag in config.vars["strtags"]: for tc in alltagcores: if tc.tag == tag: break else: raise Exception("Couldn't find TC for tag %s" % tag) self.compare_flags(NEW_TC) self.reset_flags() # 2. Getting empty ITEMS responses should cause no events tag_backend.inject("ITEMS", {"maintag:Slashdot": []}) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ITEMS", {"maintag:reddit": []}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(0) # 3. Getting a non-empty ITEMS response should cause items_added tag_backend.inject("ITEMS", {"maintag:Slashdot": ["id1", "id2"]}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_ADDED) # 4. Getting attributes should cause attributes hook self.reset_flags() id1_content = { "title": "id1", "canto-state": [], "canto-tags": [], "link": "id1-link", "enclosures": "" } id2_content = { "title": "id2", "canto-state": [], "canto-tags": [], "link": "id2-link", "enclosures": "" } all_content = {"id1": id1_content, "id2": id2_content} tag_backend.inject("ATTRIBUTES", all_content) self.compare_flags(ATTRIBUTES) self.compare_var("attributes", all_content) id1_got = tag_updater.get_attributes("id1") if id1_got != id1_content: raise Exception("Bad content: wanted %s - got %s" % (id1_content, id1_got)) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 5. Removing an item should *NOT* cause its attributes to be forgotten # that happens on stories_removed, and should cause ITEMS_REMOVED self.reset_flags() tag_backend.inject("ITEMS", {"maintag:Slashdot": ["id1"]}) tag_backend.inject("ITEMSDONE", {}) self.compare_flags(ITEMS_REMOVED) id2_got = tag_updater.get_attributes("id2") if id2_got != id2_content: raise Exception("Bad content: wanted %s - got %s" % (id2_content, id2_got)) # 6. Getting a stories_removed hook should make it forget attributes self.reset_flags() call_hook("curses_stories_removed", [FakeTag("maintag:Slashdot"), [FakeStory("id2")]]) if "id2" in tag_updater.attributes: raise Exception("Expected id2 to be removed, but it isn't!") self.compare_flags(0) # 7. Getting attributes for non-existent IDs should return empty id2_got = tag_updater.get_attributes("id2") if id2_got != {}: raise Exception( "Expected non-existent id to return empty! Got %s" % id2_got) self.compare_flags(0) # 8. Getting stories_removed for item still in tag should do nothing call_hook("curses_stories_removed", [FakeTag("maintag:Slashdot"), [FakeStory("id1")]]) if "id1" not in tag_updater.attributes: raise Exception("Expected id1 to remain in attributes!") self.compare_flags(0) # 9. Config adding a tag should create a new tagcore config_backend.inject("NEWTAGS", ["maintag:Test1"]) self.compare_flags(NEW_TC) # 10. Config removing an empty tag should delete a tagcore self.reset_flags() config_backend.inject("DELTAGS", ["maintag:reddit"]) self.compare_flags(DEL_TC) self.compare_var("del_tc", "maintag:reddit") # 11. Config removing an populated tag should delete a tagcore and # cause items_removed. NOTE for now tagcores are never deleted, they # just exist empty self.reset_flags() config_backend.inject("DELTAGS", ["maintag:Slashdot"]) self.compare_flags(DEL_TC | ITEMS_REMOVED) self.compare_var("del_tc", "maintag:Slashdot") self.compare_var("oir_tctag", "maintag:Slashdot") self.compare_var("oir_tcids", ["id1"]) # 12. Update should cause all tags to generate a tag_update # hook call on ITEMS, and update_complete when all done. self.reset_flags() tag_updater.update() tag_backend.inject("ITEMS", {"maintag:Test1": ["id3", "id4"]}) tag_backend.inject("ITEMSDONE", {}) tag_backend.inject("ATTRIBUTES", { "id3": { "test": "test" }, "id4": { "test": "test" } }) print(tag_updater.updating) self.compare_flags(TAG_UPDATED | UPDATE_COMPLETE | ITEMS_ADDED | ATTRIBUTES) self.compare_var("otu_tag", "maintag:Test1") return True