Source code for gnomish_army_knife.database.combat_log
"""
A module implementing a combat-log state data structure.
"""
# built-in
from collections import defaultdict
from datetime import datetime
from pathlib import Path
from threading import Event
from typing import Any
# third-party
from vcorelib import DEFAULT_ENCODING
from vcorelib.dict.codec import BasicDictCodec as _BasicDictCodec
from vcorelib.io import JsonObject as _JsonObject
from vcorelib.logging import LoggerMixin
# internal
from gnomish_army_knife.database.event import (
CombatLogEvent,
CombatLogEventHandler,
)
from gnomish_army_knife.database.queue import CombatLogQueueHandler
from gnomish_army_knife.paths import combat_log_datetime, combat_log_slug
from gnomish_army_knife.schemas import GakDictCodec
VERSION_EVENT = "COMBAT_LOG_VERSION"
[docs]
class CombatLogState(GakDictCodec, _BasicDictCodec, LoggerMixin):
"""The top-level configuration object for the package."""
[docs]
def init(self, data: _JsonObject) -> None:
"""Initialize this instance."""
self.data = data
self.data.setdefault("files", {})
LoggerMixin.__init__(self)
self.handlers: dict[str, CombatLogEventHandler] = {}
self.missing_handlers: dict[str, set[str]] = defaultdict(set)
self.queue = CombatLogQueueHandler()
@property
def files(self) -> dict[str, Any]:
"""Get combat log file data."""
return self.data["files"] # type: ignore
[docs]
def process_event(self, key: str, event: CombatLogEvent) -> None:
"""Process a combat-log event."""
file_data: dict[str, Any] = self.data["files"][key] # type: ignore
if event.name in self.handlers:
self.handlers[event.name](event)
elif event.name not in self.missing_handlers[key]:
self.missing_handlers[key].add(event.name)
file_data["missing_handlers"].append(event.name)
# Service queues.
self.queue.handle(event)
file_data["event_totals"][event.name] += 1
[docs]
def process_line(self, key: str, line: str, log_date: datetime) -> None:
"""Process a line from a combat log file."""
del log_date
self.process_event(key, CombatLogEvent.from_line(line))
[docs]
def process_log(self, path: Path, stop: Event = None) -> None:
"""Process a combat log file."""
key = combat_log_slug(path)
file_data = self.files.setdefault(key, {"state": "processing"})
# Return early if this log has already been processed.
if file_data["state"] == "reached_eof":
if file_data["position"] >= path.stat().st_size:
return
file_data.setdefault("position", 0)
# Log some information about this file.
date = combat_log_datetime(key)
file_data["datetime"] = str(date)
file_data["timestamp"] = date.timestamp()
file_data["event_totals"] = defaultdict(int)
file_data["missing_handlers"] = []
self.logger.info(
"Processing log '%s' (%s old) from position %d.",
date,
datetime.now() - date,
file_data["position"],
)
with path.open(encoding=DEFAULT_ENCODING) as log:
# Go back to a possible previous iteration's starting point.
log.seek(file_data["position"])
reached_eof = False
while not reached_eof and (stop is None or not stop.is_set()):
line = log.readline()
if line.rstrip():
self.process_line(key, line, date)
else:
file_data["state"] = "reached_eof"
reached_eof = True
# Always update position after a line is processed.
file_data["position"] = log.tell()
self.logger.info(
"Finished processing log '%s' (%d event types weren't handled).",
date,
len(file_data["missing_handlers"]),
)
self.logger.debug(file_data)