Source code for bacommon.bs._clouddialog
# Released under the MIT License. See LICENSE for details.
#
"""Simple cloud-defined UIs for things like notifications."""
from __future__ import annotations
import datetime
from enum import Enum
from dataclasses import dataclass, field
from typing import Annotated, override, assert_never
from efro.dataclassio import ioprepped, IOAttrs, IOMultiType
from bacommon.bs._displayitem import DisplayItemWrapper
[docs]
class CloudDialogTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
BASIC = 'b'
[docs]
class CloudDialog(IOMultiType[CloudDialogTypeID]):
"""Small self-contained ui bit provided by the cloud.
These take care of updating and/or dismissing themselves based on
user input. Useful for things such as inbox messages. For more
complex UI construction, look at :class:`CloudUI`.
"""
[docs]
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(cls, type_id: CloudDialogTypeID) -> type[CloudDialog]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
out: type[CloudDialog]
t = CloudDialogTypeID
if type_id is t.UNKNOWN:
out = UnknownCloudDialog
elif type_id is t.BASIC:
out = BasicCloudDialog
else:
# Important to make sure we provide all types.
assert_never(type_id)
return out
[docs]
@override
@classmethod
def get_unknown_type_fallback(cls) -> CloudDialog:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return UnknownCloudDialog()
[docs]
@ioprepped
@dataclass
class UnknownCloudDialog(CloudDialog):
"""Fallback type for unrecognized entries."""
[docs]
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.UNKNOWN
[docs]
class BasicCloudDialogComponentTypeID(Enum):
"""Type ID for each of our subclasses."""
UNKNOWN = 'u'
TEXT = 't'
LINK = 'l'
BS_CLASSIC_TOURNEY_RESULT = 'ct'
DISPLAY_ITEMS = 'di'
EXPIRE_TIME = 'd'
[docs]
class BasicCloudDialogComponent(IOMultiType[BasicCloudDialogComponentTypeID]):
"""Top level class for our multitype."""
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
# Require child classes to supply this themselves. If we did a
# full type registry/lookup here it would require us to import
# everything and would prevent lazy loading.
raise NotImplementedError()
[docs]
@override
@classmethod
def get_type(
cls, type_id: BasicCloudDialogComponentTypeID
) -> type[BasicCloudDialogComponent]:
"""Return the subclass for each of our type-ids."""
# pylint: disable=cyclic-import
t = BasicCloudDialogComponentTypeID
if type_id is t.UNKNOWN:
return BasicCloudDialogComponentUnknown
if type_id is t.TEXT:
return BasicCloudDialogComponentText
if type_id is t.LINK:
return BasicCloudDialogComponentLink
if type_id is t.BS_CLASSIC_TOURNEY_RESULT:
return BasicCloudDialogBsClassicTourneyResult
if type_id is t.DISPLAY_ITEMS:
return BasicCloudDialogDisplayItems
if type_id is t.EXPIRE_TIME:
return BasicCloudDialogExpireTime
# Important to make sure we provide all types.
assert_never(type_id)
[docs]
@override
@classmethod
def get_unknown_type_fallback(cls) -> BasicCloudDialogComponent:
# If we encounter some future message type we don't know
# anything about, drop in a placeholder.
return BasicCloudDialogComponentUnknown()
[docs]
@ioprepped
@dataclass
class BasicCloudDialogComponentUnknown(BasicCloudDialogComponent):
"""An unknown basic client component type.
In practice these should never show up since the master-server
generates these on the fly for the client and so should not send
clients one they can't digest.
"""
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.UNKNOWN
[docs]
@ioprepped
@dataclass
class BasicCloudDialogComponentText(BasicCloudDialogComponent):
"""Show some text in the inbox message."""
text: str
subs: list[str] = field(
default_factory=list
)
scale: float = 1.0
color: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0)
spacing_top: float = 0.0
spacing_bottom: float = 0.0
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.TEXT
[docs]
@ioprepped
@dataclass
class BasicCloudDialogComponentLink(BasicCloudDialogComponent):
"""Show a link in the inbox message."""
url: str
label: str
subs: list[str] = field(
default_factory=list
)
spacing_top: float = 0.0
spacing_bottom: float = 0.0
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.LINK
[docs]
@ioprepped
@dataclass
class BasicCloudDialogBsClassicTourneyResult(BasicCloudDialogComponent):
"""Show info about a classic tourney."""
tournament_id: str
game: str
players: int
rank: int
trophy: str | None
prizes: list[DisplayItemWrapper]
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.BS_CLASSIC_TOURNEY_RESULT
[docs]
@ioprepped
@dataclass
class BasicCloudDialogDisplayItems(BasicCloudDialogComponent):
"""Show some display-items."""
items: list[DisplayItemWrapper]
width: float = 100.0
spacing_top: float = 0.0
spacing_bottom: float = 0.0
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.DISPLAY_ITEMS
[docs]
@ioprepped
@dataclass
class BasicCloudDialogExpireTime(BasicCloudDialogComponent):
"""Show expire-time."""
time: datetime.datetime
spacing_top: float = 0.0
spacing_bottom: float = 0.0
[docs]
@override
@classmethod
def get_type_id(cls) -> BasicCloudDialogComponentTypeID:
return BasicCloudDialogComponentTypeID.EXPIRE_TIME
[docs]
@ioprepped
@dataclass
class BasicCloudDialog(CloudDialog):
"""A basic UI for the client."""
[docs]
class InteractionStyle(Enum):
"""Overall interaction styles we support."""
UNKNOWN = 'u'
BUTTON_POSITIVE = 'p'
BUTTON_POSITIVE_NEGATIVE = 'pn'
components: list[BasicCloudDialogComponent]
interaction_style: InteractionStyle = InteractionStyle.BUTTON_POSITIVE
button_label_positive: ButtonLabel = ButtonLabel.OK
button_label_negative: ButtonLabel = ButtonLabel.CANCEL
[docs]
@override
@classmethod
def get_type_id(cls) -> CloudDialogTypeID:
return CloudDialogTypeID.BASIC
[docs]
def contains_unknown_elements(self) -> bool:
"""Whether something within us is an unknown type or enum."""
return (
self.interaction_style is self.InteractionStyle.UNKNOWN
or self.button_label_positive is self.ButtonLabel.UNKNOWN
or self.button_label_negative is self.ButtonLabel.UNKNOWN
or any(
c.get_type_id() is BasicCloudDialogComponentTypeID.UNKNOWN
for c in self.components
)
)
[docs]
@ioprepped
@dataclass
class CloudDialogWrapper:
"""Wrapper for a CloudDialog and its common data."""
id: str
createtime: datetime.datetime
ui: CloudDialog
[docs]
class CloudDialogAction(Enum):
"""Types of actions we can run."""
BUTTON_PRESS_POSITIVE = 'p'
BUTTON_PRESS_NEGATIVE = 'n'
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum