rewrite (core): Split and refactor Lion and config.
This commit is contained in:
@@ -150,9 +150,9 @@ class BabelCog(LionCog):
|
||||
async def cog_load(self):
|
||||
if not self.bot.core:
|
||||
raise ValueError("CoreCog must be loaded first!")
|
||||
self.bot.core.guild_settings.attach(LocaleSettings.ForceLocale)
|
||||
self.bot.core.guild_settings.attach(LocaleSettings.GuildLocale)
|
||||
self.bot.core.user_settings.attach(LocaleSettings.UserLocale)
|
||||
self.bot.core.guild_config.register_model_setting(LocaleSettings.ForceLocale)
|
||||
self.bot.core.guild_config.register_model_setting(LocaleSettings.GuildLocale)
|
||||
self.bot.core.user_config.register_model_setting(LocaleSettings.UserLocale)
|
||||
|
||||
async def cog_unload(self):
|
||||
pass
|
||||
@@ -180,12 +180,12 @@ class BabelCog(LionCog):
|
||||
"""
|
||||
locale = None
|
||||
if ctx.guild:
|
||||
forced = ctx.alion.guild_settings['force_locale'].value
|
||||
guild_locale = ctx.alion.guild_settings['guild_locale'].value
|
||||
forced = ctx.lguild.config.get('force_locale').value
|
||||
guild_locale = ctx.lguild.config.get('guild_locale').value
|
||||
if forced:
|
||||
locale = guild_locale
|
||||
|
||||
locale = locale or ctx.alion.user_settings['user_locale'].value
|
||||
locale = locale or ctx.luser.config.get('user_locale').value
|
||||
if ctx.interaction:
|
||||
locale = locale or ctx.interaction.locale.value
|
||||
if ctx.guild:
|
||||
|
||||
@@ -11,8 +11,9 @@ from settings.groups import SettingGroup
|
||||
|
||||
from .data import CoreData
|
||||
from .lion import Lions
|
||||
from .guild_settings import GuildSettings
|
||||
from .user_settings import UserSettings
|
||||
from .lion_guild import GuildConfig
|
||||
from .lion_member import MemberConfig
|
||||
from .lion_user import UserConfig
|
||||
|
||||
|
||||
class CoreCog(LionCog):
|
||||
@@ -20,7 +21,7 @@ class CoreCog(LionCog):
|
||||
self.bot = bot
|
||||
self.data = CoreData()
|
||||
bot.db.load_registry(self.data)
|
||||
self.lions = Lions(bot)
|
||||
self.lions = Lions(bot, self.data)
|
||||
|
||||
self.app_config: Optional[CoreData.AppConfig] = None
|
||||
self.bot_config: Optional[CoreData.BotConfig] = None
|
||||
@@ -35,19 +36,13 @@ class CoreCog(LionCog):
|
||||
|
||||
# Some ModelSetting registries
|
||||
# These are for more convenient direct access
|
||||
self.guild_settings = GuildSettings
|
||||
self.user_settings = UserSettings
|
||||
self.guild_config = GuildConfig
|
||||
self.user_config = UserConfig
|
||||
self.member_config = MemberConfig
|
||||
|
||||
self.app_cmd_cache: list[discord.app_commands.AppCommand] = []
|
||||
self.cmd_name_cache: dict[str, discord.app_commands.AppCommand] = {}
|
||||
|
||||
async def bot_check_once(self, ctx: LionContext): # type: ignore
|
||||
lion = await self.lions.fetch(ctx.guild.id if ctx.guild else 0, ctx.author.id)
|
||||
if ctx.guild:
|
||||
await lion.touch_discord_models(ctx.author) # type: ignore # Type checker doesn't recognise guard
|
||||
ctx.alion = lion
|
||||
return True
|
||||
|
||||
async def cog_load(self):
|
||||
# Fetch (and possibly create) core data rows.
|
||||
conn = await self.bot.db.get_connection()
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from settings.groups import ModelSettings, SettingDotDict
|
||||
from .data import CoreData
|
||||
|
||||
|
||||
class GuildSettings(ModelSettings):
|
||||
_settings = SettingDotDict()
|
||||
model = CoreData.Guild
|
||||
209
src/core/lion.py
209
src/core/lion.py
@@ -1,153 +1,96 @@
|
||||
from typing import Optional
|
||||
from cachetools import LRUCache
|
||||
import datetime
|
||||
import discord
|
||||
|
||||
from meta import LionCog, LionBot, LionContext
|
||||
from settings import InteractiveSetting
|
||||
from utils.lib import utc_now
|
||||
from data import WeakCache
|
||||
|
||||
from .data import CoreData
|
||||
|
||||
from .user_settings import UserSettings
|
||||
from .guild_settings import GuildSettings
|
||||
|
||||
|
||||
class Lion:
|
||||
"""
|
||||
A Lion is a high level representation of a Member in the LionBot paradigm.
|
||||
|
||||
All members interacted with by the application should be available as Lions.
|
||||
It primarily provides an interface to the User and Member data.
|
||||
Lion also provides centralised access to various Member properties and methods,
|
||||
that would normally be served by other cogs.
|
||||
|
||||
Many Lion methods may only be used when the required cogs and extensions are loaded.
|
||||
A Lion may exist without a Bot instance or a Member in cache,
|
||||
although the functionality available will be more limited.
|
||||
|
||||
There is no guarantee that a corresponding discord Member actually exists.
|
||||
"""
|
||||
__slots__ = ('bot', 'data', 'user_data', 'guild_data', '_member', '__weakref__')
|
||||
|
||||
def __init__(self, bot: LionBot, data: CoreData.Member, user_data: CoreData.User, guild_data: CoreData.Guild):
|
||||
self.bot = bot
|
||||
self.data = data
|
||||
self.user_data = user_data
|
||||
self.guild_data = guild_data
|
||||
|
||||
self._member: Optional[discord.Member] = None
|
||||
|
||||
# Data properties
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return (self.data.guildid, self.data.userid)
|
||||
|
||||
@property
|
||||
def guildid(self):
|
||||
return self.data.guildid
|
||||
|
||||
@property
|
||||
def userid(self):
|
||||
return self.data.userid
|
||||
|
||||
@classmethod
|
||||
def get(cls, guildid, userid):
|
||||
return cls._cache_.get((guildid, userid), None)
|
||||
|
||||
# ModelSettings interfaces
|
||||
@property
|
||||
def guild_settings(self):
|
||||
return GuildSettings(self.guildid, self.guild_data, bot=self.bot)
|
||||
|
||||
@property
|
||||
def user_settings(self):
|
||||
return UserSettings(self.userid, self.user_data, bot=self.bot)
|
||||
|
||||
# Setting interfaces
|
||||
# Each of these return an initialised member setting
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
pass
|
||||
|
||||
# Time utilities
|
||||
@property
|
||||
def now(self):
|
||||
"""
|
||||
Returns current time-zone aware time for the member.
|
||||
"""
|
||||
pass
|
||||
|
||||
# Discord data cache
|
||||
async def touch_discord_models(self, member: discord.Member):
|
||||
"""
|
||||
Update the stored discord data from the given user or member object.
|
||||
Intended to be used when we get member data from events that may not be available in cache.
|
||||
"""
|
||||
# Can we do these in one query?
|
||||
if member.guild and (self.guild_data.name != member.guild.name):
|
||||
await self.guild_data.update(name=member.guild.name)
|
||||
|
||||
avatar_key = member.avatar.key if member.avatar else None
|
||||
await self.user_data.update(avatar_hash=avatar_key, name=member.name, last_seen=utc_now())
|
||||
|
||||
if member.display_name != self.data.display_name:
|
||||
await self.data.update(display_name=member.display_name)
|
||||
|
||||
async def get_member(self) -> Optional[discord.Member]:
|
||||
"""
|
||||
Retrieve the member object for this Lion, if possible.
|
||||
|
||||
If the guild or member cannot be retrieved, returns None.
|
||||
"""
|
||||
guild = self.bot.get_guild(self.guildid)
|
||||
if guild is not None:
|
||||
member = guild.get_member(self.userid)
|
||||
if member is None:
|
||||
try:
|
||||
member = await guild.fetch_member(self.userid)
|
||||
except discord.HTTPException:
|
||||
pass
|
||||
return member
|
||||
from .lion_guild import LionGuild
|
||||
from .lion_user import LionUser
|
||||
from .lion_member import LionMember
|
||||
|
||||
|
||||
class Lions(LionCog):
|
||||
def __init__(self, bot: LionBot):
|
||||
def __init__(self, bot: LionBot, data: CoreData):
|
||||
self.bot = bot
|
||||
self.data = data
|
||||
|
||||
# Full Lions cache
|
||||
# Don't expire Lions with strong references
|
||||
self._cache_: WeakCache[tuple[int, int], 'Lion'] = WeakCache(LRUCache(5000))
|
||||
# Caches
|
||||
# Using WeakCache so strong references stay consistent
|
||||
self.lion_guilds = WeakCache(LRUCache(2500))
|
||||
self.lion_users = WeakCache(LRUCache(2000))
|
||||
self.lion_members = WeakCache(LRUCache(5000))
|
||||
|
||||
self._settings_: dict[str, InteractiveSetting] = {}
|
||||
|
||||
async def fetch(self, guildid, userid) -> Lion:
|
||||
async def bot_check_once(self, ctx: LionContext):
|
||||
"""
|
||||
Fetch or create the given Member.
|
||||
If the guild or user row doesn't exist, also creates it.
|
||||
Relies on the core cog existing, to retrieve the core data.
|
||||
Insert the high-level Lion objects into context before command execution.
|
||||
|
||||
Creates the objects if they do not already exist.
|
||||
Updates relevant saved data from the Discord models,
|
||||
and updates last seen for the LionUser (for data lifetime).
|
||||
"""
|
||||
# TODO: Find a way to reduce this to one query, while preserving cache
|
||||
lion = self._cache_.get((guildid, userid))
|
||||
if lion is None:
|
||||
if self.bot.core:
|
||||
data = self.bot.core.data
|
||||
if ctx.guild:
|
||||
# TODO: Consider doing all updates in one query, maybe with a View trigger on Member
|
||||
lmember = ctx.lmember = await self.fetch_member(ctx.guild.id, ctx.author.id, ctx.author)
|
||||
await lmember.touch_discord_model(ctx.author)
|
||||
|
||||
ctx.luser = lmember.luser
|
||||
await ctx.luser.touch_discord_model(ctx.author, seen=True)
|
||||
|
||||
ctx.lguild = lmember.lguild
|
||||
await ctx.lguild.touch_discord_model(ctx.guild)
|
||||
|
||||
ctx.alion = lmember
|
||||
else:
|
||||
raise ValueError("Cannot fetch Lion before core module is attached.")
|
||||
ctx.lmember = ctx.alion = None
|
||||
ctx.lguild = None
|
||||
luser = ctx.luser = await self.fetch_user(ctx.author.id, ctx.author)
|
||||
|
||||
guild = await data.Guild.fetch_or_create(guildid)
|
||||
user = await data.User.fetch_or_create(userid)
|
||||
member = await data.Member.fetch_or_create(guildid, userid)
|
||||
lion = Lion(self.bot, member, user, guild)
|
||||
self._cache_[(guildid, userid)] = lion
|
||||
return lion
|
||||
await luser.touch_discord_model(ctx.author)
|
||||
|
||||
def add_model_setting(self, setting: InteractiveSetting):
|
||||
self._settings_[setting.__class__.__name__] = setting
|
||||
return setting
|
||||
ctx.alion = luser
|
||||
return True
|
||||
|
||||
async def fetch_user(self, userid, user: Optional[discord.User] = None) -> LionUser:
|
||||
"""
|
||||
Fetch the given LionUser, hitting cache if possible.
|
||||
|
||||
Creates the LionUser if it does not exist.
|
||||
"""
|
||||
if (luser := self.lion_users.get(userid, None)) is None:
|
||||
data = await self.data.User.fetch_or_create(userid)
|
||||
luser = LionUser(self.bot, data, user=user)
|
||||
self.lion_users[userid] = luser
|
||||
return luser
|
||||
|
||||
async def fetch_guild(self, guildid, guild: Optional[discord.Guild] = None) -> LionGuild:
|
||||
"""
|
||||
Fetch the given LionGuild, hitting cache if possible.
|
||||
|
||||
Creates the LionGuild if it does not exist.
|
||||
"""
|
||||
if (lguild := self.lion_guilds.get(guildid, None)) is None:
|
||||
data = await self.data.Guild.fetch_or_create(guildid)
|
||||
lguild = LionGuild(self.bot, data, guild=guild)
|
||||
self.lion_guilds[guildid] = lguild
|
||||
return lguild
|
||||
|
||||
async def fetch_member(self, guildid, userid, member: Optional[discord.Member] = None) -> LionMember:
|
||||
"""
|
||||
Fetch the given LionMember, using cache for data if possible.
|
||||
|
||||
|
||||
Creates the LionGuild, LionUser, and LionMember if they do not already exist.
|
||||
"""
|
||||
# TODO: Can we do this more efficiently with one query, while keeping cache? Multiple joins?
|
||||
key = (guildid, userid)
|
||||
if (lmember := self.lion_members.get(key, None)) is None:
|
||||
lguild = await self.fetch_guild(guildid, member.guild if member is not None else None)
|
||||
luser = await self.fetch_user(userid, member)
|
||||
data = await self.data.Member.fetch_or_create(guildid, userid)
|
||||
lmember = LionMember(self.bot, data, lguild, luser, member)
|
||||
self.lion_members[key] = lmember
|
||||
return lmember
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
from settings.groups import ModelSettings, SettingDotDict
|
||||
from .data import CoreData
|
||||
|
||||
|
||||
class UserSettings(ModelSettings):
|
||||
_settings = SettingDotDict()
|
||||
model = CoreData.User
|
||||
2
src/gui
2
src/gui
Submodule src/gui updated: 4f3d5740a3...2beff63ddc
@@ -8,7 +8,9 @@ from discord.ext.commands import Context
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .LionBot import LionBot
|
||||
from core.lion import Lion
|
||||
from core.lion_member import LionMember
|
||||
from core.lion_user import LionUser
|
||||
from core.lion_guild import LionGuild
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -45,7 +47,10 @@ class LionContext(Context['LionBot']):
|
||||
Extends Context to add Lion-specific methods and attributes.
|
||||
Also adds several contextual wrapped utilities for simpler user during command invocation.
|
||||
"""
|
||||
alion: 'Lion'
|
||||
luser: 'LionUser'
|
||||
lguild: 'LionGuild'
|
||||
lmember: 'LionMember'
|
||||
alion: 'LionUser | LionMember'
|
||||
|
||||
def __repr__(self):
|
||||
parts = {}
|
||||
|
||||
@@ -415,7 +415,7 @@ class Economy(LionCog):
|
||||
).on_conflict(ignore=True)
|
||||
else:
|
||||
# With only one target, we can take a simpler path, and make better use of local caches.
|
||||
await self.bot.core.lions.fetch(ctx.guild.id, target.id)
|
||||
await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
||||
# Now we are certain these members have a database row
|
||||
|
||||
# Perform the appropriate action
|
||||
@@ -889,7 +889,7 @@ class Economy(LionCog):
|
||||
|
||||
t = self.bot.translator.t
|
||||
Member = self.bot.core.data.Member
|
||||
target_lion = await self.bot.core.lions.fetch(ctx.guild.id, target.id)
|
||||
target_lion = await self.bot.core.lions.fetch_member(ctx.guild.id, target.id)
|
||||
|
||||
# TODO: Add a "Send thanks" button to the DM?
|
||||
# Alternative flow could be waiting until the target user presses accept
|
||||
|
||||
@@ -11,7 +11,7 @@ from babel.translator import LazyStr
|
||||
from ..data import ShopData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from core.lion import Lion
|
||||
from core.lion_member import LionMember
|
||||
|
||||
|
||||
class ShopCog(LionCog):
|
||||
@@ -65,7 +65,7 @@ class Customer:
|
||||
self.bot = bot
|
||||
self.data = shop_data
|
||||
|
||||
self.lion: 'Lion' = lion
|
||||
self.lion: 'LionMember' = lion
|
||||
|
||||
# A list of InventoryItems held by this customer
|
||||
self.inventory = inventory
|
||||
@@ -84,7 +84,7 @@ class Customer:
|
||||
|
||||
@classmethod
|
||||
async def fetch(cls, bot: LionBot, shop_data: ShopData, guildid: int, userid: int):
|
||||
lion = await bot.core.lions.fetch(guildid, userid)
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
inventory = await shop_data.MemberInventoryInfo.fetch_inventory_info(guildid, userid)
|
||||
return cls(bot, shop_data, lion, inventory)
|
||||
|
||||
@@ -92,7 +92,7 @@ class Customer:
|
||||
"""
|
||||
Refresh the data for this member.
|
||||
"""
|
||||
self.lion = await self.bot.core.lions.fetch(self.guildid, self.userid)
|
||||
self.lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||
await self.lion.data.refresh()
|
||||
self.inventory = await self.data.MemberInventoryInfo.fetch_inventory_info(self.guildid, self.userid)
|
||||
return self
|
||||
|
||||
@@ -184,7 +184,7 @@ class ColourShop(Shop):
|
||||
)
|
||||
|
||||
# Ensure the customer member actually exists
|
||||
member = await self.customer.lion.get_member()
|
||||
member = await self.customer.lion.fetch_member()
|
||||
if member is None:
|
||||
raise SafeCancellation(
|
||||
t(_p(
|
||||
|
||||
@@ -15,7 +15,7 @@ async def get_goals_card(
|
||||
):
|
||||
data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
lion = await bot.core.lions.fetch(guildid, userid)
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
today = lion.today
|
||||
|
||||
# Calculate periodid and select the correct model
|
||||
@@ -63,7 +63,7 @@ async def get_goals_card(
|
||||
sessions_complete = 0.5
|
||||
|
||||
# Get member profile
|
||||
if member := await lion.get_member():
|
||||
if member := await lion.fetch_member():
|
||||
username = (member.display_name, member.discriminator)
|
||||
avatar = member.avatar.key
|
||||
else:
|
||||
|
||||
@@ -14,7 +14,7 @@ from ..lib import apply_month_offset
|
||||
async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> MonthlyStatsCard:
|
||||
data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
lion = await bot.core.lions.fetch(guildid, userid)
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
today = lion.today
|
||||
month_start = today.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
target = apply_month_offset(month_start, offset)
|
||||
@@ -77,7 +77,7 @@ async def get_monthly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
monthly[i][day.day - 1] = stat / 3600
|
||||
|
||||
# Get member profile
|
||||
if member := await lion.get_member():
|
||||
if member := await lion.fetch_member():
|
||||
username = (member.display_name, member.discriminator)
|
||||
else:
|
||||
username = (lion.data.display_name, '#????')
|
||||
|
||||
@@ -16,7 +16,7 @@ async def get_stats_card(bot: LionBot, userid: int, guildid: int):
|
||||
# TODO: Leaderboard rankings
|
||||
guildid = guildid or 0
|
||||
|
||||
lion = await bot.core.lions.fetch(guildid, userid)
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
|
||||
# Calculate the period timestamps, i.e. start time for each summary period
|
||||
# TODO: Don't do the alltime one like this, not efficient anymore
|
||||
|
||||
@@ -12,7 +12,7 @@ from ..data import StatsData
|
||||
async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int, mode: CardMode) -> WeeklyStatsCard:
|
||||
data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
lion = await bot.core.lions.fetch(guildid, userid)
|
||||
lion = await bot.core.lions.fetch_member(guildid, userid)
|
||||
today = lion.today
|
||||
week_start = today - timedelta(days=today.weekday()) - timedelta(weeks=offset)
|
||||
days = [week_start + timedelta(i) for i in range(-7, 7 if offset else (today.weekday() + 1))]
|
||||
@@ -34,7 +34,7 @@ async def get_weekly_card(bot: LionBot, userid: int, guildid: int, offset: int,
|
||||
day_stats.append(0)
|
||||
|
||||
# Get member profile
|
||||
if member := await lion.get_member():
|
||||
if member := await lion.fetch_member():
|
||||
username = (member.display_name, member.discriminator)
|
||||
else:
|
||||
username = (lion.data.display_name, '#????')
|
||||
|
||||
@@ -21,7 +21,7 @@ from babel.translator import ctx_translator, LazyStr
|
||||
from babel.utils import local_month
|
||||
from gui.cards import WeeklyGoalCard, WeeklyStatsCard, MonthlyGoalCard, MonthlyStatsCard
|
||||
from gui.base import CardMode
|
||||
from core.lion import Lion
|
||||
from core.lion_member import LionMember
|
||||
|
||||
from ..graphics.weekly import get_weekly_card
|
||||
from ..graphics.monthly import get_monthly_card
|
||||
@@ -338,7 +338,7 @@ class WeeklyMonthlyUI(StatsUI):
|
||||
self.data: StatsData = bot.get_cog('StatsCog').data
|
||||
|
||||
# State
|
||||
self.lion: Optional[Lion] = None
|
||||
self.lion: Optional[LionMember] = None
|
||||
|
||||
self._stat_page: StatPage = StatPage.WEEKLY_VOICE
|
||||
self._week_offset = 0
|
||||
@@ -859,7 +859,7 @@ class WeeklyMonthlyUI(StatsUI):
|
||||
"""
|
||||
self._original = interaction
|
||||
self._showing_global = False
|
||||
self.lion = await self.bot.core.lions.fetch(self.guildid, self.userid)
|
||||
self.lion = await self.bot.core.lions.fetch_member(self.guildid, self.userid)
|
||||
|
||||
# TODO: Switch to using data cache in reload to calculate global/local
|
||||
|
||||
|
||||
@@ -130,8 +130,8 @@ class TasklistCog(LionCog):
|
||||
|
||||
async def cog_load(self):
|
||||
await self.data.init()
|
||||
self.bot.core.guild_settings.attach(self.settings.task_reward)
|
||||
self.bot.core.guild_settings.attach(self.settings.task_reward_limit)
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.task_reward)
|
||||
self.bot.core.guild_config.register_model_setting(self.settings.task_reward_limit)
|
||||
|
||||
# TODO: Better method for getting single load
|
||||
# Or better, unloading crossloaded group
|
||||
|
||||
@@ -88,6 +88,70 @@ class ModelSetting(ModelData, BaseSetting):
|
||||
...
|
||||
|
||||
|
||||
class ModelConfig:
|
||||
"""
|
||||
A ModelConfig provides a central point of configuration for any object described by a single Model.
|
||||
|
||||
An instance of a ModelConfig represents configuration for a single object
|
||||
(given by a single row of the corresponding Model).
|
||||
|
||||
The ModelConfig also supports registration of non-model configuration,
|
||||
to support associated settings (e.g. list-settings) for the object.
|
||||
|
||||
This is an ABC, and must be subclassed for each object-type.
|
||||
"""
|
||||
settings: SettingDotDict
|
||||
_model_settings: set
|
||||
model: Type[RowModel]
|
||||
|
||||
def __init__(self, parent_id, row, **kwargs):
|
||||
self.parent_id = parent_id
|
||||
self.row = row
|
||||
self.kwargs = kwargs
|
||||
|
||||
@classmethod
|
||||
def register_setting(cls, setting_cls):
|
||||
"""
|
||||
Decorator to register a non-model setting as part of the object configuration.
|
||||
|
||||
The setting class may be re-accessed through the `settings` class attr.
|
||||
|
||||
Subclasses may provide alternative access pathways to key non-model settings.
|
||||
"""
|
||||
cls.settings[setting_cls.setting_id] = setting_cls
|
||||
return setting_cls
|
||||
|
||||
@classmethod
|
||||
def register_model_setting(cls, model_setting_cls):
|
||||
"""
|
||||
Decorator to register a model setting as part of the object configuration.
|
||||
|
||||
The setting class may be accessed through the `settings` class attr.
|
||||
|
||||
A fresh setting instance may also be retrieved (using cached data)
|
||||
through the `get` instance method.
|
||||
|
||||
Subclasses are recommended to provide model settings as properties
|
||||
for simplified access and type checking.
|
||||
"""
|
||||
cls._model_settings.add(model_setting_cls.setting_id)
|
||||
return cls.register_setting(model_setting_cls)
|
||||
|
||||
def get(self, setting_id):
|
||||
"""
|
||||
Retrieve a freshly initialised copy of the given model-setting.
|
||||
|
||||
The given `setting_id` must have been previously registered through `register_model_setting`.
|
||||
This uses cached data, and so is not guaranteed to be up-to-date.
|
||||
"""
|
||||
if setting_id not in self._model_settings:
|
||||
# TODO: Log
|
||||
raise ValueError
|
||||
setting_cls = self.settings[setting_id]
|
||||
data = setting_cls._read_from_row(self.parent_id, self.row, **self.kwargs)
|
||||
return setting_cls(self.parent_id, data, **self.kwargs)
|
||||
|
||||
|
||||
class ModelSettings:
|
||||
"""
|
||||
A ModelSettings instance aggregates multiple `ModelSetting` instances
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import NamedTuple, Optional, Sequence, Union, overload, List
|
||||
import datetime
|
||||
import iso8601 # type: ignore
|
||||
import pytz
|
||||
import re
|
||||
from contextvars import Context
|
||||
|
||||
@@ -706,3 +707,34 @@ def parse_duration(string: str) -> Optional[int]:
|
||||
seconds += int(match.group('value')) * multiplier
|
||||
|
||||
return seconds if found else None
|
||||
|
||||
|
||||
class Timezoned:
|
||||
"""
|
||||
ABC mixin for objects with a set timezone.
|
||||
|
||||
Provides several useful localised properties.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
@property
|
||||
def timezone(self) -> pytz.timezone:
|
||||
"""
|
||||
Must be implemented by the deriving class!
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def now(self):
|
||||
"""
|
||||
Return the current time localised to the object's timezone.
|
||||
"""
|
||||
return datetime.datetime.now(tz=self.timezone)
|
||||
|
||||
@property
|
||||
def today(self):
|
||||
"""
|
||||
Return the start of the day localised to the object's timezone.
|
||||
"""
|
||||
now = self.now
|
||||
return now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
Reference in New Issue
Block a user