Source code for AFL.automation.shared.PersistentConfig

import json
import datetime
import pathlib
import copy
import warnings
from collections.abc import MutableMapping

[docs] class PersistentConfig(MutableMapping): ''' A dictionary-like class that serializes changes to disk This class provides dictionary-like setters and getters (e.g., [] and .update()) but, by default, all modifications are written into a file in json format with the root keys as timestamps. Modifications can be blocked by locking the config, and writing to disk can be disabled by setting the appropriate member attributes (see constructor). On instantiation, if provided with a previously saved configuration file, PersistentConfig will load the file's contents into memory and use the more recent configuration. '''
[docs] def __init__( self, path, defaults=None, overrides=None, lock=False, write=True, max_history=10000, datetime_key_format='%y/%d/%m %H:%M:%S.%f' ): '''Constructor Parameters --------- path: str or pathlib.Path File path to file in which this config is or will be stored. This file will be created if it does not exist defaults: dict Default values to use if no saved config is available or if parameters are missing from a saved config. overrides: dict Values to use that override parameters in a saved config. These parameters can be changed after the PersistentConfig object is instantiated. lock: bool If True, an AttributeError will be raised if the user attempts to modify the config write: bool If False, all writing to the config file will be disabled and a warning will be emitted each time the config is modified. datetime_key_format: str String defining the root level keys of the json-serialized file. See https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior. ''' self.path = pathlib.Path(path) self.datetime_key_format = datetime_key_format self.write = write self.lock = False # In case of True, only lock configuration at end of constructor self.max_history = max_history need_update=False if self.path.exists(): with open(self.path,'r') as f: self.history = json.load(f) key = self._get_sorted_history_keys()[-1] #use latest key self.config = copy.deepcopy(self.history[key]) else: self.config = {} self.history = {self._get_datetime_key():{}} need_update=True if defaults is not None: #cannot use .update because we don't want to clobber existing values for k,v in defaults.items(): if k not in self.config: self.config[k] = v need_update=True if overrides is not None: #use dict.update method rather than PersistentConfig.update self.config.update(overrides) need_update=True if need_update: self._update_history() self.lock = lock #In case of True, only lock configuration at end of constructor
def __str__(self): return f'<PersistentConfig entries: {len(self.config)} last_saved: {self._get_sorted_history_keys()[-1]}>' def __repr__(self): return self.__str__()
[docs] def __getitem__(self,key): '''Dictionary-like getter via config["param"]''' return self.config[key]
[docs] def __setitem__(self,key,value): '''Dictionary-like setter via config["param"]=value Changes will be written to PersistentConfig.path if PersistentConfig.write is True (default). ''' if self.lock: raise AttributeError( ''' Attempting to change locked config. Set self.lock to False to make changes to config. ''' ) self.config[key] = value self._update_history()
[docs] def toJSON(self): ''' Serialize the config to json ''' return json.dumps(self.config)
def __iter__(self): for key,value in self.config.items(): yield key,value def __len__(self): return len(self.config) def __delitem__(self,key): del self.config[key] self._update_history()
[docs] def update(self,update_dict): '''Update several values in config at once Changes will be written to PersistentConfig.path if PersistentConfig.write is True (default). ''' if self.lock: raise AttributeError( ''' Attempting to change locked config. Set self.lock to False to make changes to config. ''' ) self.config.update(update_dict) self._update_history()
[docs] def revert(self,nth=None,datetime_key=None): '''Revert config to a historical config Parameters ---------- nth: int, **optional*** Integer index of historical value to revert to. Can be negative to count from end of history. Note that -1 will correspond to the current config. datetime_key: str, **optional** datetime formatted string as defined by datetime_key_format ''' if nth is not None: key = list(self._get_sorted_history_keys())[nth] #supports negative indexing elif datetime_key is not None: key = datetime_key else: raise ValueError('Must supply nth or datetime_key!') self.config = copy.deepcopy(self.history[key]) self._update_history()
[docs] def get_historical_values(self,key,convert_to_datetime=False): '''Convenience method for gathering historical values of a parameter ''' dates = [] values = [] for date,config in self.history.items(): if key in config: if convert_to_datetime: dates.append(self._decode_datetime_key(date)) else: dates.append(date) values.append(config[key]) return dates,values
def _get_datetime_key(self): return datetime.datetime.now().strftime(self.datetime_key_format) def _decode_datetime_key(self,key): return datetime.datetime.strptime(key,self.datetime_key_format) def _encode_datetime_key(self,key): return datetime.datetime.strftime(key,self.datetime_key_format) def _get_sorted_history_keys(self): #get latest key by converting to datetime object keys = sorted(map(self._decode_datetime_key,self.history.keys())) #convert back to formatted string keys = list(map(self._encode_datetime_key,keys)) return keys def _update_history(self): if self.write: if len(self.history)>self.max_history: keys = self._get_sorted_history_keys() #print(f'History reached max # of entries ( removing oldest key: {keys[0]}') # delete all keys more than max history for key in keys[:-self.max_history]: del self.history[key] key = self._get_datetime_key() self.history[key] = copy.deepcopy(self.config) with open(self.path,'w') as f: json.dump(self.history,f,indent=4) else: warnings.warn( ''' PersistentConfig writing disabled. To save changes to config, set self.write to True. ''' )