Source code for logdict

"""LogDict class with JSON log storage format and simple versioning."""

import json, os
from collections.abc import MutableMapping
from typing import Any, Tuple

[docs]class LogDict(MutableMapping): """Map-like object with append-only-file logging for persistence. All set and delete operations are written to append-only log and saved either real-time or upon request in line-based JSON format. Special version key and :meth:`slice` can be used to implement versioning. See :class:`collections.abc.MutableMapping` for interface details. Returns: LogDict: A class instance. """
[docs] @classmethod def load(cls, filename: str, logging: bool=False) -> 'LogDict': """Create a LogDict, init from file and optionally start logging. Args: filename (str): Filename, read into object if exists logging (bool): Set to True to have append-only file log, or to False to explicitly :meth:`save` contents. Returns: LogDict: A new object """ ld = cls() if os.path.exists(filename): with open(filename, 'r', encoding='utf8') as fin: for l in fin.readlines(): t = json.loads(l) if len(t)==2: ld[t[0]] = t[1] else: del ld[t[0]] if logging: ld.startLogging(filename) return ld
def __init__(self, *args, **kwargs): self.__d = dict() # dict backend self.log = list() # log of operations self.__logfile = None self.update(dict(*args, **kwargs)) # use supplied update to init
[docs] def startLogging(self, filename: str) -> None: """Start AOF logging. Does nothing if already logging, so safe to call multiple times. Args: filename (str): File to write to """ if not self.__logfile or self.__logfile.closed: self.__logfile = open(filename, 'at', encoding='utf8', buffering=1)
[docs] def endLogging(self) -> None: """End AOF logging. Does nothing if already closed, so safe to call multiple times. """ if self.__logfile and not self.__logfile.closed: self.__logfile.close()
[docs] def save(self, filename: str) -> None: """Save log to file. Use :meth:`create` to restore from a saved log. Args: filename (str): File to write to """ with open(filename, 'w', encoding='utf8') as fout: for t in self.log: json.dump(t, fout) fout.write('\n')
def __del__(self): if self.__logfile: self.endLogging() def __setitem__(self, key, value): self.log.append((key, value)) # tuple for set if self.__logfile: json.dump((key, value), self.__logfile) self.__logfile.write('\n') self.__d[key] = value def __delitem__(self, key): self.log.append((key,)) # single item tuple for del if self.__logfile: json.dump((key,), self.__logfile) self.__logfile.write('\n') del self.__d[key]
[docs] def slice(self, start: Tuple[Any, Any]=None, end: Tuple[Any, Any]=None) -> 'LogDict': """Create a new LogDict from a slice of operations log. As all mutations are logged, you can easily use key changes as markers to construct a partial LogDict. Just specify beginning and end (or None to use log start/end). Deleting non-existing nodes is automatically skipped. Args: start (tuple): A (key,value) pair for set, (key,) for del or None end (tuple): A (key,value) pair for set, (key,) for del or None Returns: LogDict: A copy with slice of the log and appropriate content. """ ld = self.__class__() # make work with children i1 = self.log.index(start) if start else 0 i2 = self.log.index(end) if end else len(self.log) for t in self.log[i1:i2]: if len(t)==2: ld[t[0]] = t[1] elif t[0] in ld: del ld[t[0]] return ld
def __getitem__(self, key): return self.__d[key] def __iter__(self): return iter(self.__d) def __len__(self): return len(self.__d)