""" Infobase: structured database. Infobase is a structured database which contains multiple sites. Each site is an independent collection of objects. """ import web import datetime import simplejson import common import config import readquery import writequery # important: this is required here to setup _loadhooks and unloadhooks import cache class Infobase: """Infobase contains multiple sites.""" def __init__(self, store, secret_key): self.store = store self.secret_key = secret_key self.sites = {} self.event_listeners = [] if config.startup_hook: config.startup_hook(self) def create(self, sitename): """Creates a new site with the sitename.""" site = Site(self, sitename, self.store.create(sitename), self.secret_key) site.bootstrap() self.sites[sitename] = site return site def get(self, sitename): """Returns the site with the given name.""" if sitename in self.sites: site = self.sites[sitename] else: store = self.store.get(sitename) if store is None: return None site = Site(self, sitename, self.store.get(sitename), self.secret_key) self.sites[sitename] = site return site def delete(self, sitename): """Deletes the site with the given name.""" if sitename in self.sites: del self.sites[sitename] return self.store.delete(sitename) def add_event_listener(self, listener): self.event_listeners.append(listener) def remove_event_listener(self, listener): try: self.event_listeners.remove(listener) except ValueError: pass def fire_event(self, event): for listener in self.event_listeners: try: listener(event) except: common.record_exception() pass class Site: """A site of infobase.""" def __init__(self, _infobase, sitename, store, secret_key): self._infobase = _infobase self.sitename = sitename self.store = store self.cache = cache.Cache() self.store.set_cache(self.cache) import account self.account_manager = account.AccountManager(self, secret_key) self._triggers = {} store.store.set_listener(self._log_store_action) store.seq.set_listener(self._log_store_action) def _log_store_action(self, name, data): event = web.storage(name=name, ip=web.ctx.ip, author=None, data=data, sitename=self.sitename, timestamp=None) self._infobase.fire_event(event) def get_account_manager(self): return self.account_manager def get_store(self): return self.store.get_store() def get_seq(self): return self.store.seq def delete(self): return self._infobase.delete(self.sitename) def get(self, key, revision=None): return self.store.get(key, revision) withKey = get def _get_thing(self, key, revision=None): json = self.get(key, revision) return json and common.Thing.from_json(self.store, key, json) def _get_many_things(self, keys): json = self.get_many(keys) d = simplejson.loads(json) return dict((k, common.Thing.from_dict(self.store, k, doc)) for k, doc in d.items()) def get_many(self, keys): return self.store.get_many(keys) def new_key(self, type, kw=None): return self.store.new_key(type, kw or {}) def write(self, query, timestamp=None, comment=None, data=None, ip=None, author=None, action=None, _internal=False): timestamp = timestamp or datetime.datetime.utcnow() author = author or self.get_account_manager().get_user() p = writequery.WriteQueryProcessor(self.store, author) items = p.process(query) items = (item for item in items if item) changeset = self.store.save_many(items, timestamp, comment, data, ip, author and author.key, action=action) result = changeset.get('docs', []) created = [r['key'] for r in result if r and r['revision'] == 1] updated = [r['key'] for r in result if r and r['revision'] != 1] result2 = web.storage(created=created, updated=updated) if not _internal: event_data = dict(comment=comment, data=data, query=query, result=result2, changeset=changeset) self._fire_event("write", timestamp, ip, author and author.key, event_data) self._fire_triggers(result) return result2 def save(self, key, doc, timestamp=None, comment=None, data=None, ip=None, author=None, action=None): timestamp = timestamp or datetime.datetime.utcnow() author = author or self.get_account_manager().get_user() ip = ip or web.ctx.get('ip', '127.0.0.1') #@@ why to have key argument at all? doc['key'] = key p = writequery.SaveProcessor(self.store, author) doc = p.process(key, doc) if not doc: return {} else: changeset = self.store.save(key, doc, timestamp, comment, data, ip, author and author.key, action=action) saved_docs = changeset.get("docs") saved_doc = saved_docs[0] result={"key": saved_doc['key'], "revision": saved_doc['revision']} event_data = dict(comment=comment, key=key, query=doc, result=result, changeset=changeset) self._fire_event("save", timestamp, ip, author and author.key, event_data) self._fire_triggers([saved_doc]) return result def save_many(self, query, timestamp=None, comment=None, data=None, ip=None, author=None, action=None): timestamp = timestamp or datetime.datetime.utcnow() author = author or self.get_account_manager().get_user() ip = ip or web.ctx.get('ip', '127.0.0.1') p = writequery.SaveProcessor(self.store, author) items = p.process_many(query) if not items: return [] changeset = self.store.save_many(items, timestamp, comment, data, ip, author and author.key, action=action) saved_docs = changeset.get('docs') result = [{"key": doc["key"], "revision": doc['revision']} for doc in saved_docs] event_data = dict(comment=comment, query=query, result=result, changeset=changeset) self._fire_event("save_many", timestamp, ip, author and author.key, event_data) self._fire_triggers(saved_docs) return result def _fire_event(self, name, timestamp, ip, username, data): event = common.Event(self.sitename, name, timestamp, ip, username, data) self._infobase.fire_event(event) def things(self, query): return readquery.run_things_query(self.store, query) def versions(self, query): try: q = readquery.make_versions_query(self.store, query) except ValueError: # ValueError is raised if unknown keys are used in the query. # Invalid keys shouldn't make the query fail, instead the it should result in no match. return [] return self.store.versions(q) def recentchanges(self, query): return self.store.recentchanges(query) def get_change(self, id): return self.store.get_change(id) def get_permissions(self, key): author = self.get_account_manager().get_user() engine = writequery.PermissionEngine(self.store) perm = engine.has_permission(author, key) return web.storage(write=perm, admin=perm) def bootstrap(self, admin_password='admin123'): import bootstrap web.ctx.ip = '127.0.0.1' import cache cache.loadhook() bootstrap.bootstrap(self, admin_password) def add_trigger(self, type, func): """Registers a trigger to call func when object of specified type is modified. If type=None is specified then the trigger is called for every modification. func is called with old object and new object as arguments. old object will be None if the object is newly created. """ self._triggers.setdefault(type, []).append(func) def _fire_triggers(self, result): """Executes all required triggers on write.""" def fire_trigger(type, old, new): triggers = self._triggers.get(type['key'], []) + self._triggers.get(None, []) for t in triggers: try: t(self, old, new) except: print >> web.debug, 'Failed to execute trigger', t import traceback traceback.print_exc() if not self._triggers: return created = [doc['key'] for doc in result if doc and doc['revision'] == 1] updated = [doc['key'] for doc in result if doc and doc['revision'] != 1] things = dict((doc['key'], doc) for doc in result) for key in created: thing = things[key] fire_trigger(thing['type'], None, thing) for key in updated: thing = things[key] # old_data (the second argument) is not used anymore. # TODO: Remove the old_data argument. fire_trigger(thing['type'], None, thing) #old = self._get_thing(key, thing.revision-1) #if old.type.key == thing.type.key: # fire_trigger(thing.type, old, thing) #else: # fire_trigger(old.type, old, thing) # fire_trigger(thing.type, old, thing)