def update(self): """Update the database. If the database is fresh, it will be initialized. If the database is already up-to-date, nothing will be done. It is thus safe to call :meth:`update` without knowing if an update is necessary or not. """ # Compatibility for databases without micro_version (obsolete since 0.13.0) if not self.r.exists('micro_version') and self.r.exists('Settings'): self.r.set('micro_version', 0) version = self.r.get('micro_version') # If fresh, initialize database if not version: settings = self.create_settings() self.r.oset(settings.id, settings) self.r.set('micro_version', 1) self.do_update() return version = int(version) r = JSONRedis(self.r.r) r.caching = False if version < 2: settings = r.oget('Settings') settings['feedback_url'] = None r.oset(settings['id'], settings) self.r.set('micro_version', 2) self.do_update()
def do_update(self): db_version = self.r.get('version') # If fresh, initialize database if not db_version: self.r.set('version', 5) return db_version = int(db_version) r = JSONRedis(self.r.r) r.caching = False # Deprecated since 0.12.0 if db_version < 5: users = r.omget(r.lrange('users', 0, -1)) for user in users: user['email'] = None r.omset({u['id']: u for u in users}) r.set('version', 5)
def __init__(self, redis_url='', email='bot@localhost', smtp_url='', render_email_auth_message=None): check_email(email) try: # pylint: disable=pointless-statement; port errors are only triggered on access urlparse(smtp_url).port except builtins.ValueError: raise ValueError('smtp_url_invalid') self.redis_url = redis_url try: self.r = StrictRedis.from_url(self.redis_url) except builtins.ValueError: raise ValueError('redis_url_invalid') self.r = JSONRedis(self.r, self._encode, self._decode) self.types = {'User': User, 'Settings': Settings, 'AuthRequest': AuthRequest} self.user = None self.users = JSONRedisMapping(self.r, 'users') self.email = email self.smtp_url = smtp_url self.render_email_auth_message = render_email_auth_message
class Application: """See :ref:`Application`. .. attribute:: user Current :class:`User`. ``None`` means anonymous access. .. attribute:: users Map of all :class:`User` s. .. attribute:: redis_url See ``--redis-url`` command line option. .. attribute:: email Sender email address to use for outgoing email. Defaults to ``bot@localhost``. .. attribute:: smtp_url See ``--smtp-url`` command line option. .. attribute:: render_email_auth_message Hook function of the form *render_email_auth_message(email, auth_request, auth)*, responsible for rendering an email message for the authentication request *auth_request*. *email* is the email address to authenticate and *auth* is the secret authentication code. .. attribute:: r :class:`Redis` database. More precisely a :class:`JSONRedis` instance. """ def __init__(self, redis_url='', email='bot@localhost', smtp_url='', render_email_auth_message=None): check_email(email) try: # pylint: disable=pointless-statement; port errors are only triggered on access urlparse(smtp_url).port except builtins.ValueError: raise ValueError('smtp_url_invalid') self.redis_url = redis_url try: self.r = StrictRedis.from_url(self.redis_url) except builtins.ValueError: raise ValueError('redis_url_invalid') self.r = JSONRedis(self.r, self._encode, self._decode) self.types = {'User': User, 'Settings': Settings, 'AuthRequest': AuthRequest} self.user = None self.users = JSONRedisMapping(self.r, 'users') self.email = email self.smtp_url = smtp_url self.render_email_auth_message = render_email_auth_message @property def settings(self): """App :class:`Settings`.""" return self.r.oget('Settings') def update(self): """Update the database. If the database is fresh, it will be initialized. If the database is already up-to-date, nothing will be done. It is thus safe to call :meth:`update` without knowing if an update is necessary or not. """ # Compatibility for databases without micro_version (obsolete since 0.13.0) if not self.r.exists('micro_version') and self.r.exists('Settings'): self.r.set('micro_version', 0) version = self.r.get('micro_version') # If fresh, initialize database if not version: settings = self.create_settings() self.r.oset(settings.id, settings) self.r.set('micro_version', 1) self.do_update() return version = int(version) r = JSONRedis(self.r.r) r.caching = False if version < 2: settings = r.oget('Settings') settings['feedback_url'] = None r.oset(settings['id'], settings) self.r.set('micro_version', 2) self.do_update() def do_update(self): """Subclass API: Perform the database update. May be overridden by subclass. Called by :meth:`update`, which takes care of updating (or initializing) micro specific data. The default implementation does nothing. """ pass def create_settings(self): """Subclass API: Create and return the app :class:`Settings`. *id* must be set to ``Settings``. Must be overridden by subclass. Called by :meth:`update` when initializing the database. """ raise NotImplementedError() def authenticate(self, secret): """Authenticate an :class:`User` (device) with *secret*. The identified user is set as current *user* and returned. If the authentication fails, an :exc:`AuthenticationError` is raised. """ id = self.r.hget('auth_secret_map', secret) if not id: raise AuthenticationError() self.user = self.users[id.decode()] return self.user def login(self, code=None): """See :http:post:`/api/login`. The logged-in user is set as current *user*. """ if code: id = self.r.hget('auth_secret_map', code) if not id: raise ValueError('code_invalid') user = self.users[id.decode()] else: id = 'User:'******'Guest', email=None, auth_secret=randstr()) self.r.oset(user.id, user) self.r.rpush('users', user.id) self.r.hset('auth_secret_map', user.auth_secret, user.id) # Promote first user to staff if len(self.users) == 1: settings = self.settings # pylint: disable=protected-access; Settings is a friend settings._staff = [user.id] self.r.oset(settings.id, settings) return self.authenticate(user.auth_secret) def get_object(self, id, default=KeyError): """Get the :class:`Object` given by *id*. *default* is the value to return if no object with *id* is found. If it is an :exc:`Exception`, it is raised instead. """ object = self.r.oget(id) if object is None: object = default if isinstance(object, Exception): raise object return object @staticmethod def _encode(object): try: return object.json() except AttributeError: raise TypeError() def _decode(self, json): try: type = json.pop('__type__') except KeyError: return json type = self.types[type] return type(app=self, **json)
def setUp(self) -> None: self.r = JSONRedis(StrictRedis(db=15), encode=Cat.encode, decode=Cat.decode) self.r.flushdb()
class JSONRedisTestCase(TestCase): def setUp(self) -> None: self.r = JSONRedis(Redis(db=15), encode=Cat.encode, decode=Cat.decode) self.r.flushdb()
def do_update(self): db_version = self.r.get('version') # If fresh, initialize database if not db_version: self.r.set('version', 5) return db_version = int(db_version) r = JSONRedis(self.r.r) r.caching = False if db_version < 2: users = r.omget(r.lrange('users', 0, -1)) for user in users: user['name'] = 'Guest' user['authors'] = [user['id']] r.omset({u['id']: u for u in users}) r.set('version', 2) if db_version < 3: meetings = r.omget(r.lrange('meetings', 0, -1)) for meeting in meetings: meeting['time'] = None meeting['location'] = None items = r.omget(r.lrange(meeting['id'] + '.items', 0, -1)) for item in items: item['duration'] = None r.omset({i['id']: i for i in items}) r.omset({m['id']: m for m in meetings}) r.set('version', 3) if db_version < 4: meeting_ids = r.lrange('meetings', 0, -1) objects = r.omget(chain( ['Settings'], r.lrange('users', 0, -1), meeting_ids, chain.from_iterable(r.lrange(i + b'.items', 0, -1) for i in meeting_ids) )) for object in objects: object['trashed'] = False r.omset({o['id']: o for o in objects}) r.set('version', 4) if db_version < 5: users = r.omget(r.lrange('users', 0, -1)) for user in users: user['email'] = None r.omset({u['id']: u for u in users}) r.set('version', 5)
def do_update(self): version = self.r.get('version') if not version: self.r.set('version', 8) return version = int(version) r = JSONRedis(self.r.r) r.caching = False # Deprecated since 0.14.0 if version < 7: now = time() lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: r.zadd('{}.lists'.format(lst['authors'][0]), {lst['id']: -now}) r.set('version', 7) # Deprecated since 0.23.0 if version < 8: lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: users_key = '{}.users'.format(lst['id']) self.r.zadd(users_key, {lst['authors'][0].encode(): 0}) events = r.omget(r.lrange('{}.activity.items'.format(lst['id']), 0, -1)) for event in reversed(events): t = parse_isotime(event['time'], aware=True).timestamp() self.r.zadd(users_key, {event['user'].encode(): -t}) r.set('version', 8)
def do_update(self): version = self.r.get('version') if not version: self.r.set('version', 7) return version = int(version) r = JSONRedis(self.r.r) r.caching = False # Deprecated since 0.3.0 if version < 2: lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: lst['features'] = [] items = r.omget(r.lrange('{}.items'.format(lst['id']), 0, -1)) for item in items: item['checked'] = False r.omset({item['id']: item for item in items}) r.omset({lst['id']: lst for lst in lists}) r.set('version', 2) # Deprecated since 0.5.0 if version < 3: lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: lst['activity'] = (Activity('{}.activity'.format(lst['id']), app=self, subscriber_ids=[]).json()) r.omset({lst['id']: lst for lst in lists}) r.set('version', 3) # Deprecated since 0.6.0 if version < 4: items = r.omget([ id for list_id in r.lrange('lists', 0, -1) for id in r.lrange('{}.items'.format(list_id.decode()), 0, -1) ]) for item in items: item['location'] = None r.omset({item['id']: item for item in items}) r.set('version', 4) # Deprecated since 0.7.0 if version < 5: items = r.omget([ id for list_id in r.lrange('lists', 0, -1) for id in r.lrange('{}.items'.format(list_id.decode()), 0, -1) ]) for item in items: item['resource'] = None r.omset({item['id']: item for item in items}) r.set('version', 5) # Deprecated since 0.11.0 if version < 6: lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: lst['mode'] = 'collaborate' r.omset({lst['id']: lst for lst in lists}) r.set('version', 6) # Deprecated since 0.14.0 if version < 7: now = time() lists = r.omget(r.lrange('lists', 0, -1)) for lst in lists: r.zadd('{}.lists'.format(lst['authors'][0]), {lst['id']: -now}) r.set('version', 7)