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()
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)