Remove the unneeded full tracking of history

Also use setdefault() to save a bunch of pain
Prevent a silly exhaustive history search on all channel times rather than just current users
This commit is contained in:
Marc Di Luzio 2024-08-13 00:12:30 +01:00
parent 503e899f19
commit f00216fffc
3 changed files with 40 additions and 68 deletions

View file

@ -105,7 +105,7 @@ class _Config():
@property @property
def score_factors(self) -> _ScoreFactors: def score_factors(self) -> _ScoreFactors:
return _ScoreFactors(self._dict.get(_Key.SCORE_FACTORS, {})) return _ScoreFactors(self._dict.setdefault(_Key.SCORE_FACTORS, {}))
def _migrate(dict: dict): def _migrate(dict: dict):

View file

@ -176,7 +176,7 @@ def members_to_groups(matchees: list[Member],
return [] return []
# Walk from the start of history until now trying to match up groups # Walk from the start of history until now trying to match up groups
for oldest_relevant_datetime in st.get_history_timestamps() + [datetime.now()]: for oldest_relevant_datetime in st.get_history_timestamps(matchees) + [datetime.now()]:
# Attempt with each starting matchee # Attempt with each starting matchee
for shifted_matchees in iterate_all_shifts(matchees): for shifted_matchees in iterate_all_shifts(matchees):

View file

@ -14,7 +14,7 @@ logger.setLevel(logging.INFO)
# Warning: Changing any of the below needs proper thought to ensure backwards compatibility # Warning: Changing any of the below needs proper thought to ensure backwards compatibility
_VERSION = 3 _VERSION = 4
def _migrate_to_v1(d: dict): def _migrate_to_v1(d: dict):
@ -33,9 +33,9 @@ def _migrate_to_v2(d: dict):
return datetime.strftime(datetime.strptime(ts, _TIME_FORMAT_OLD), _TIME_FORMAT) return datetime.strftime(datetime.strptime(ts, _TIME_FORMAT_OLD), _TIME_FORMAT)
# Adjust all the history keys # Adjust all the history keys
d[_Key.HISTORY] = { d[_Key._HISTORY] = {
old_to_new_ts(ts): entry old_to_new_ts(ts): entry
for ts, entry in d[_Key.HISTORY].items() for ts, entry in d[_Key._HISTORY].items()
} }
# Adjust all the user parts # Adjust all the user parts
for user in d[_Key.USERS].values(): for user in d[_Key.USERS].values():
@ -57,11 +57,17 @@ def _migrate_to_v3(d: dict):
d[_Key.TASKS] = {} d[_Key.TASKS] = {}
def _migrate_to_v4(d: dict):
"""v4 removed verbose history tracking"""
del d[_Key._HISTORY]
# Set of migration functions to apply # Set of migration functions to apply
_MIGRATIONS = [ _MIGRATIONS = [
_migrate_to_v1, _migrate_to_v1,
_migrate_to_v2, _migrate_to_v2,
_migrate_to_v3, _migrate_to_v3,
_migrate_to_v4,
] ]
@ -75,10 +81,6 @@ class _Key(str):
"""Various keys used in the schema""" """Various keys used in the schema"""
VERSION = "version" VERSION = "version"
HISTORY = "history"
GROUPS = "groups"
MEMBERS = "members"
USERS = "users" USERS = "users"
SCOPES = "scopes" SCOPES = "scopes"
MATCHES = "matches" MATCHES = "matches"
@ -94,6 +96,9 @@ class _Key(str):
# Unused # Unused
_MATCHEES = "matchees" _MATCHEES = "matchees"
_HISTORY = "history"
_GROUPS = "groups"
_MEMBERS = "members"
_TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f" _TIME_FORMAT = "%Y-%m-%d %H:%M:%S.%f"
@ -105,20 +110,6 @@ _SCHEMA = Schema(
# The current version # The current version
_Key.VERSION: Use(int), _Key.VERSION: Use(int),
_Key.HISTORY: {
# A datetime
Optional(str): {
_Key.GROUPS: [
{
_Key.MEMBERS: [
# The ID of each matchee in the match
Use(int)
]
}
]
}
},
_Key.USERS: { _Key.USERS: {
# User ID as string # User ID as string
Optional(str): { Optional(str): {
@ -156,7 +147,6 @@ _SCHEMA = Schema(
# Empty but schema-valid internal dict # Empty but schema-valid internal dict
_EMPTY_DICT = { _EMPTY_DICT = {
_Key.HISTORY: {},
_Key.USERS: {}, _Key.USERS: {},
_Key.TASKS: {}, _Key.TASKS: {},
_Key.VERSION: _VERSION _Key.VERSION: _VERSION
@ -192,9 +182,22 @@ class State():
dict = self._dict dict = self._dict
_SCHEMA.validate(dict) _SCHEMA.validate(dict)
def get_history_timestamps(self) -> list[datetime]: def get_history_timestamps(self, users: list[Member]) -> list[datetime]:
"""Grab all timestamps in the history""" """Grab all timestamps in the history"""
return sorted([ts_to_datetime(dt) for dt in self._history.keys()]) others = [m.id for m in users]
# Fetch all the interaction times in history
# But only for interactions in the given user group
times = set()
for data in (data for id, data in self._users.items() if int(id) in others):
matches = data.get(_Key.MATCHES, {})
for ts in (ts for id, ts in matches.items() if int(id) in others):
times.add(ts)
# Convert to datetimes and sort
datetimes = [ts_to_datetime(ts) for ts in times]
datetimes.sort()
return datetimes
def get_user_matches(self, id: int) -> list[int]: def get_user_matches(self, id: int) -> list[int]:
return self._users.get(str(id), {}).get(_Key.MATCHES, {}) return self._users.get(str(id), {}).get(_Key.MATCHES, {})
@ -203,36 +206,21 @@ class State():
"""Log the groups""" """Log the groups"""
ts = datetime_to_ts(ts or datetime.now()) ts = datetime_to_ts(ts or datetime.now())
with self._safe_wrap() as safe_state: with self._safe_wrap() as safe_state:
# Grab or create the hitory item for this set of groups
history_item = {}
safe_state._history[ts] = history_item
history_item_groups = []
history_item[_Key.GROUPS] = history_item_groups
for group in groups: for group in groups:
# Add the group data
history_item_groups.append({
_Key.MEMBERS: [m.id for m in group]
})
# Update the matchee data with the matches # Update the matchee data with the matches
for m in group: for m in group:
matchee = safe_state._users.get(str(m.id), {}) matchee = safe_state._users.setdefault(str(m.id), {})
matchee_matches = matchee.get(_Key.MATCHES, {}) matchee_matches = matchee.setdefault(_Key.MATCHES, {})
for o in (o for o in group if o.id != m.id): for o in (o for o in group if o.id != m.id):
matchee_matches[str(o.id)] = ts matchee_matches[str(o.id)] = ts
matchee[_Key.MATCHES] = matchee_matches
safe_state._users[str(m.id)] = matchee
def set_user_scope(self, id: str, scope: str, value: bool = True): def set_user_scope(self, id: str, scope: str, value: bool = True):
"""Add an auth scope to a user""" """Add an auth scope to a user"""
with self._safe_wrap() as safe_state: with self._safe_wrap() as safe_state:
# Dive in # Dive in
user = safe_state._users.get(str(id), {}) user = safe_state._users.setdefault(str(id), {})
scopes = user.get(_Key.SCOPES, []) scopes = user.setdefault(_Key.SCOPES, [])
# Set the value # Set the value
if value and scope not in scopes: if value and scope not in scopes:
@ -240,10 +228,6 @@ class State():
elif not value and scope in scopes: elif not value and scope in scopes:
scopes.remove(scope) scopes.remove(scope)
# Roll out
user[_Key.SCOPES] = scopes
safe_state._users[str(id)] = user
def get_user_has_scope(self, id: str, scope: str) -> bool: def get_user_has_scope(self, id: str, scope: str) -> bool:
""" """
Check if a user has an auth scope Check if a user has an auth scope
@ -277,8 +261,8 @@ class State():
"""Reactivate any users who've passed their reactivation time on this channel""" """Reactivate any users who've passed their reactivation time on this channel"""
with self._safe_wrap() as safe_state: with self._safe_wrap() as safe_state:
for user in safe_state._users.values(): for user in safe_state._users.values():
channels = user.get(_Key.CHANNELS, {}) channels = user.setdefault(_Key.CHANNELS, {})
channel = channels.get(str(channel_id), {}) channel = channels.setdefault(str(channel_id), {})
if channel and not channel[_Key.ACTIVE]: if channel and not channel[_Key.ACTIVE]:
reactivate = channel.get(_Key.REACTIVATE, None) reactivate = channel.get(_Key.REACTIVATE, None)
# Check if we've gone past the reactivation time and re-activate # Check if we've gone past the reactivation time and re-activate
@ -316,8 +300,8 @@ class State():
def set_channel_match_task(self, channel_id: str, members_min: int, weekday: int, hour: int, set: bool) -> bool: def set_channel_match_task(self, channel_id: str, members_min: int, weekday: int, hour: int, set: bool) -> bool:
"""Set up a match task on a channel""" """Set up a match task on a channel"""
with self._safe_wrap() as safe_state: with self._safe_wrap() as safe_state:
channel = safe_state._tasks.get(str(channel_id), {}) channel = safe_state._tasks.setdefault(str(channel_id), {})
matches = channel.get(_Key.MATCH_TASKS, []) matches = channel.setdefault(_Key.MATCH_TASKS, [])
found = False found = False
for match in matches: for match in matches:
@ -340,9 +324,6 @@ class State():
_Key.HOUR: hour, _Key.HOUR: hour,
}) })
# Roll back out, saving the entries in case they're new
channel[_Key.MATCH_TASKS] = matches
safe_state._tasks[str(channel_id)] = channel
return True return True
# We did not manage to remove the schedule (or add it? though that should be impossible) # We did not manage to remove the schedule (or add it? though that should be impossible)
@ -353,10 +334,6 @@ class State():
"""Only to be used to get the internal dict as a copy""" """Only to be used to get the internal dict as a copy"""
return copy.deepcopy(self._dict) return copy.deepcopy(self._dict)
@property
def _history(self) -> dict[str]:
return self._dict[_Key.HISTORY]
@property @property
def _users(self) -> dict[str]: def _users(self) -> dict[str]:
return self._dict[_Key.USERS] return self._dict[_Key.USERS]
@ -369,18 +346,13 @@ class State():
"""Set a user channel property helper""" """Set a user channel property helper"""
with self._safe_wrap() as safe_state: with self._safe_wrap() as safe_state:
# Dive in # Dive in
user = safe_state._users.get(str(id), {}) user = safe_state._users.setdefault(str(id), {})
channels = user.get(_Key.CHANNELS, {}) channels = user.setdefault(_Key.CHANNELS, {})
channel = channels.get(str(channel_id), {}) channel = channels.setdefault(str(channel_id), {})
# Set the value # Set the value
channel[key] = value channel[key] = value
# Unroll
channels[str(channel_id)] = channel
user[_Key.CHANNELS] = channels
safe_state._users[str(id)] = user
@contextmanager @contextmanager
def _safe_wrap(self): def _safe_wrap(self):
"""Safely run any function wrapped in a validate""" """Safely run any function wrapped in a validate"""