Source code for kerno.state

"""Classes that store outgoing state.

These are typically returned by *action* layer code to controllers.
"""
from __future__ import annotations  # allows forward references; python 3.7+
from abc import ABCMeta
from collections import OrderedDict
from copy import copy
from typing import Any, List, Optional, Union

from kerno.typing import DictStr
from kerno.web.to_dict import to_dict, reuse_dict


[docs]class UIMessage: """Represents a message to be displayed to the user in the UI.""" LEVELS = ["danger", "warning", "info", "success"] # The default to_dict() works fine for this class. def __init__( self, level: str = "danger", title: str = "", plain: str = "", html: str = "", ) -> None: """Constructor. ``level`` must be one of ("danger", "warning", "info", "success"). """ args_are_valid = (plain and not html) or (html and not plain) assert args_are_valid if level == "error": level = "danger" assert ( level in self.LEVELS ), 'Unknown message level: "{0}". ' "Possible levels are {1}".format( level, self.LEVELS ) self.level = level self.title = title self.plain = plain self.html = html def __repr__(self) -> str: return '<{} "{}">'.format(self.__class__.__name__, self.title)
[docs] def to_dict(self) -> DictStr: # noqa return copy(self.__dict__)
[docs] @classmethod def from_payload(cls, payload: Union[str, DictStr]) -> UIMessage: # noqa if isinstance(payload, str): return cls(plain=payload) else: return cls(**payload)
[docs]class UICommand: """Represents a message telling the UI to do something.""" __slots__ = ("name", "payload") def __init__(self, name: str, payload: DictStr) -> None: """Construct a message to the UI. ``name`` is the name of a command to be performed in the UI. ``payload`` is the data to run the command with. """ self.name = name self.payload = payload def __repr__(self): return '<UICommand "{}">'.format(self.name)
[docs]@to_dict.register(obj=UICommand, flavor="") def uicommand_to_dict( obj: UICommand, flavor: str = "", **kw ) -> OrderedDict[str, Any]: """Convert to dict a UICommand instance.""" return OrderedDict((("name", obj.name), ("payload", obj.payload)))
[docs]class Returnable(metaclass=ABCMeta): """Base class for Rezulto and for MalbonaRezulto. Returnable is the base class for what Actions should return. It contains: - messages: Grave UI messages. - toasts: UI messages that disappear automatically after a while. - commands: messages to the UI, e. g., add an entity to your models - debug: A dict with information that is not displayed to the end user. - redirect: URL or screen to redirect to. Subclasses overload the ``status_int`` and ``level`` static variables. """ level = "danger" status_int = 500 # HTTP response code indicating server bug/failure def __init__( self, commands: List[UICommand] = None, debug: DictStr = None, redirect: str = "", **kw, ): # noqa self.messages: List[UIMessage] = [] self.toasts: List[UIMessage] = [] self.commands = commands or [] self.debug = debug or {} self.redirect = redirect for k, v in kw.items(): setattr(self, k, v) def __repr__(self) -> str: return "<{} status: {}>".format( self.__class__.__name__, self.status_int )
[docs] def add_message(self, level: str = "", **kw) -> UIMessage: """Add to the grave messages to be displayed to the user on the UI.""" msg = UIMessage(level=level or self.level, **kw) self.messages.append(msg) return msg
[docs] def add_toast(self, level: str = "", **kw) -> UIMessage: """Add to the quick messages to be displayed to the user on the UI.""" msg = UIMessage(level=level or self.level, **kw) self.toasts.append(msg) return msg
[docs] def add_command(self, **kw) -> UICommand: """Add to the commands for the UI to perform.""" cmd = UICommand(**kw) self.commands.append(cmd) return cmd
[docs]@to_dict.register(obj=Returnable, flavor="") def returnable_to_dict(obj, flavor="", **kw): """Convert instance to a dictionary, usually for JSON output.""" amap = reuse_dict( obj=obj, keys=kw.get("keys", ("level", "status_int", "debug", "redirect")), sort=False, ) amap["messages"] = [reuse_dict(obj=msg) for msg in obj.messages] amap["toasts"] = [reuse_dict(obj=msg) for msg in obj.toasts] amap["commands"] = [to_dict(uicommand) for uicommand in obj.commands] return amap
[docs]class Rezulto(Returnable): """Well-organized successful response object. When your action succeeds you should return a Rezulto. Unsuccessful operations raise MalbonaRezulto instead. """ level = "success" status_int = 200 # HTTP response code indicating success
[docs]class MalbonaRezulto(Returnable, Exception): """Base class for exceptions raised by actions.""" level = "danger" status_int = 400 # HTTP response code indicating invalid request def __init__( self, status_int: int = 400, title: str = "", plain: str = "", html: str = "", level: str = "danger", invalid: Optional[DictStr] = None, **kw, ): # noqa Returnable.__init__(self, **kw) self.status_int = status_int self.invalid = invalid or {} if title or plain or html: self.add_toast(title=title, level=level, plain=plain, html=html)
[docs]@to_dict.register(obj=MalbonaRezulto, flavor="") def malbona_to_dict(obj: MalbonaRezulto, flavor: str = "", **kw) -> DictStr: """Convert a MalbonaRezulto to a dictionary.""" amap = returnable_to_dict(obj=obj, flavor="", **kw) amap["invalid"] = obj.invalid return amap