class AbstractPersister(ABC): def __init__(self): self.handler = ErrorHandler(module="AbstractPersister", method="__init__") @abc.abstractmethod def load(self, key=None): save_module_name = self.handler.module self.handler.module = "Base Persister" self.handler.method = "load" self.handler.log(message="Validating key: {}".format(key)) if key is None: raise ValueError("Key must be present to load a persisted game.") self.handler.module = save_module_name @abc.abstractmethod def save(self, key=None, jsonstr=None): save_module_name = self.handler.module self.handler.module = "Base Persister" self.handler.method = "save" self.handler.log(message="Base persistence processing") self.handler.log(message="Validating key: {}".format(key)) if key is None: raise ValueError("Key must be present to persist game.") self.handler.log(message="Validating json: {}".format(jsonstr)) if jsonstr is None: raise ValueError("JSON is badly formed or not present") self.handler.log( message="Saving key {} with {} to persister".format(key, jsonstr)) self.handler.module = save_module_name
class TestErrorHandler(TestCase): def setUp(self): self.error_handler = ErrorHandler(module="TestErrorHandler", method="setUp") self.app = app.test_client() if app.config["PYTHON_VERSION_MAJOR"] < 3: self.logging_type = io.BytesIO else: self.logging_type = io.StringIO self.logger = self.error_handler.logger self.lhStdout = self.logger.handlers[0] self.current_log_level = self.logger.getEffectiveLevel() self.log_capture = self.logging_type() self.sh = logging.StreamHandler(stream=self.log_capture) self.logger.setLevel(logging.INFO) self.logger.addHandler(hdlr=self.sh) self.logger.removeHandler(self.lhStdout) def tearDown(self): self.logger.addHandler(self.lhStdout) self.logger.removeHandler(self.sh) self.logger.setLevel(self.current_log_level) def test_eh_instantiation(self): eh = ErrorHandler(module="test_module", method="test_method") self.assertIsInstance(eh, ErrorHandler) self.assertEqual(eh.module, "test_module") self.assertEqual(eh.method, "test_method") def test_eh_get_logger(self): logger = self.error_handler.logger self.assertIsInstance(logger, logging.RootLogger) def test_eh_error(self): self.error_handler.method = "test_eh_error" result = self.error_handler.error( status=400, exception="This is an exception for test purposes only", message="This is a message for test purposes only") self.assertIsInstance(result, Response) self.assertEqual(result.status_code, 400) def test_eh_logging_info(self): self.error_handler.method = "test_eh_logging_info" test_message = "This is a test message for logging" eval_message = "{}: {}: {}\n".format(self.error_handler.module, self.error_handler.method, test_message) self.error_handler.logger.setLevel(logging.INFO) self.error_handler.log(logger=logging.info, message=test_message) logged_output = self.log_capture.getvalue() self.assertEqual(logged_output, eval_message) def test_eh_logging_debug(self): self.error_handler.method = "test_eh_logging_debug" test_message = "This is a debug message for logging" eval_message = "{}: {}: {}\n".format(self.error_handler.module, self.error_handler.method, test_message) self.error_handler.logger.setLevel(logging.DEBUG) self.error_handler.log(logger=logging.debug, message=test_message) logged_output = self.log_capture.getvalue() self.assertEqual(logged_output, eval_message)
class Configurator(object): """ Provides a configuration control to enable the Python Cowbull Server to execute_load configuration from a set of variables or from a configuration file. """ def __init__(self): self.app = None self.configuration = {} self.error_handler = None default_config_file = "./python_cowbull_server/defaults.config" self.env_vars = self._load_defaults(default_config_file) def execute_load(self, app): if app is None: raise ValueError( "The Flask app must be passed to the Configurator") if not isinstance(app, Flask): raise TypeError("Expected a Flask object, received {0}".format( type(app))) self.app = app self.app.config["PYTHON_VERSION_MAJOR"] = sys.version_info[0] self.app.config["LOGGING_FORMAT"] = os.getenv( "logging_format", os.getenv("LOGGING_FORMAT", "%(asctime)s %(levelname)s: %(message)s")) self.app.config["LOGGING_LEVEL"] = os.getenv( "logging_level", os.getenv("LOGGING_LEVEL", logging.INFO)) self.error_handler = ErrorHandler( module="Configurator", method="__init__", level=self.app.config["LOGGING_LEVEL"], format=self.app.config["LOGGING_FORMAT"]) self.error_handler.log( message="Initialized logging (level: {}, format: {})".format( self.app.config["LOGGING_LEVEL"], self.app.config["LOGGING_FORMAT"]), logger=logging.info) self.error_handler.log(message="Configuring environment variables.", logger=logging.info) self.configuration = {} config_file = self._set_config(source=os.getenv, name="COWBULL_CONFIG") self.app.config["COWBULL_CONFIG"] = config_file self.error_handler.log(message="Loading configuration from: {}".format( config_file if config_file else "environment variables")) source = {} if config_file: _file = open(config_file, 'r') try: source = json.load(_file) except: raise finally: _file.close() self.load_variables(source=source) def get_variables(self): return [ ("LOGGING_LEVEL", "An integer representing the Python " "logging level (e.g. 10 for debug, 20 for warning, etc.)"), ("LOGGING_FORMAT", "The format for logs. The default is >> " "%(asctime)s %(levelname)s: %(message)s"), ("COWBULL_CONFIG", "A path and filename of a configuration file " "used to set env. vars. e.g. /path/to/the/file.cfg") ]\ + [(i["name"], i["description"]) for i in self.env_vars] def dump_variables(self): return [ ("LOGGING_LEVEL", self.app.config["LOGGING_LEVEL"]), ("LOGGING_FORMAT", self.app.config["LOGGING_FORMAT"]), ("COWBULL_CONFIG", self.app.config["COWBULL_CONFIG"]) ] \ + \ [(i["name"], self.app.config[i["name"]]) for i in self.env_vars] def print_variables(self): print('') print('=' * 80) print('=', ' ' * 30, 'CONFIGURATION', ' ' * 31, '=') print('=' * 80) print( 'The following environment variables may be set to dynamically configure the' ) print( 'server. Alternately, these can be defined in a file and passed using the env.' ) print( 'var. COWBULL_CONFIG. Please note, the file must be a JSON data object.' ) print('') print( 'Please note 1: Env. Var. names can be *ALL* lowercase or *ALL* uppercase.' ) print( 'Please note 2: FLASK_ env. vars. are ignored when using gUnicorn.' ) print('') print('-' * 80) print('| Current configuration set:') print('-' * 80) for name, val in self.dump_variables(): outstr = "| {:20s} | {}".format(name, val) print(outstr) print('-' * 80) print('') def load_variables(self, source=None): if source: _fetch = source.get else: _fetch = os.getenv for item in self.env_vars: self.error_handler.log(method="load_variables", message="Processing {} of type {}".format( item.get("name", "no name provided"), item.get("caster", "string")), logger=logging.debug) if isinstance(item, dict): self.error_handler.log(method="load_variables", message="Item is a dict: {}".format( item.get("name", "unknown name")), logger=logging.debug) self._set_config(source=_fetch, **item) elif isinstance(item, str): self.error_handler.log(method="load_variables", message="Item is a string: {}".format( item.get("name", "unknown name")), logger=logging.debug) self._set_config(name=item, source=_fetch) elif isinstance(item, list): self.error_handler.log(method="load_variables", message="Item is a list: {}".format( item.get("name", "unknown name")), logger=logging.debug) self.load_variables(source=item) else: raise TypeError( "Unexpected item in configuration: {}, type: {}".format( item, type(item))) def _load_defaults(self, source): if not source: raise ValueError("Source for _load_defaults is None!") f = None try: f = open(source, 'r') _defaults = json.load(f) except IOError: raise IOError("The source file for _load_defaults was not found!") finally: if f: f.close() _persister_default = { "engine_name": "redis", "parameters": { "host": "{0}".format(_defaults["redis_host"]), "port": _defaults["redis_port"], "db": 0 } } _persister = { "name": "PERSISTER", "description": "The persistence engine object", "required": False, "default": json.dumps(_persister_default), "caster": PersistenceEngine } _flask_host = { "name": "FLASK_HOST", "description": "For debug purposes, defines the Flask host. Default is all traffic *not* localhost", "required": False, "default": "{0}".format(_defaults["flask_host"]) } _flask_port = { "name": "FLASK_PORT", "description": "For debug purposes, the port Flask should serve on. Default is 5000", "required": False, "default": _defaults["flask_port"], "caster": int } _flask_debug = { "name": "FLASK_DEBUG", "description": "For debug purposes, set Flask into debug mode.", "required": False, "default": _defaults["flask_debug"], "caster": bool } _cowbull_dry_run = { "name": "COWBULL_DRY_RUN", "description": "Do not run the server, simply report the configuration that would " "be used to run it.", "required": False, "default": False, "caster": bool } _cowbull_custom_modes = { "name": "COWBULL_CUSTOM_MODES", "description": "A file which defines additional " "modes to be defined in addition to the default modes. The " "file must be a list of JSON objects containing mode " "definitions. Each object must contain (at a minimum): mode, digits, and " "priority", "required": False, "default": None, "caster": self._load_from_json } return [ _persister, _flask_host, _flask_port, _flask_debug, _cowbull_dry_run, _cowbull_custom_modes ] # http://sonarqube:9000/project/issues?id=cowbull_server&issues=AWiRMKAZaAhZ-jY-ujHl&open=AWiRMKAZaAhZ-jY-ujHl def _set_config(self, **kwargs): check_kwargs(parameter_list=[ "source", "name", "description", "required", "default", "errmsg", "caster", "choices" ], caller="Configurator-_set_config", **kwargs) source = kwargs.get("source", None) name = kwargs.get("name", None) description = kwargs.get("description", None) required = kwargs.get("required", None) default = kwargs.get("default", None) errmsg = kwargs.get("errmsg", None) caster = kwargs.get("caster", None) choices = kwargs.get("choices", None) self.error_handler.log( method="_set_config", message= "In _set_config -- source: {}, name: {}, description: {}, required: {}, default: {}, errmsg: {}, caster: {}, choices: {}" .format(source, name, description, required, default, errmsg, caster, choices), logger=logging.debug) value = source(name.lower(), source(name.upper(), None)) if required and value is None and default is None: raise ValueError( errmsg or "Problem fetching config item: " "{}. It is required and was not found or the value was None.". format(name)) self.error_handler.log(method="_set_config", message="Before casting: {}".format(value), logger=logging.debug) if value is None: value = default if caster: if caster == PersistenceEngine: if not isinstance(value, dict): value = json.loads(str(value).replace("'", "")) value = caster(**value) elif caster == bool and isinstance(value, str): if value.lower() == "false": value = False else: value = True else: value = caster(value) self.error_handler.log(method="_set_config", message="After casting: {}".format(value), logger=logging.debug) if choices and value not in choices: raise ValueError( errmsg or "The configuration value for {}({}) is not in the list of choices: {}" .format(name, value, choices)) self.app.config[name] = value self.error_handler.log( message="In _set_config Set app.config[{}] = {}".format( name, value), logger=logging.debug) return value def _load_from_json(self, json_file_name): if not json_file_name: return None f = None return_value = None try: f = open(json_file_name, 'r') return_value = json.load(f) except Exception as e: self.error_handler.error(module="Configurator.py", method="_load_from_json", status=500, exception=repr(e), message="An exception occurred!") raise IOError( "A JSON file ({}) cannot be loaded. Exception: {}".format( json_file_name, repr(e))) finally: if f: f.close() return return_value
class Configurator(object): """ Provides a configuration control to enable the Python Cowbull Server to execute_load configuration from a set of variables or from a configuration file. """ def __init__(self): self.app = None self.configuration = {} self.error_handler = None self.env_vars = [{ "name": "PERSISTER", "description": "The persistence engine object", "required": False, "default": '{"engine_name": "redis", "parameters": {"host": "localhost", "port": 6379, "db": 0}}', "caster": PersistenceEngine }, { "name": "FLASK_HOST", "description": "For debug purposes, defines the Flask host. Default is 0.0.0.0", "required": False, "default": "0.0.0.0" }, { "name": "FLASK_PORT", "description": "For debug purposes, the port Flask should serve on. Default is 5000", "required": False, "default": 5000, "caster": int }, { "name": "FLASK_DEBUG", "description": "For debug purposes, set Flask into debug mode.", "required": False, "default": True, "caster": bool }, { "name": "COWBULL_DRY_RUN", "description": "Do not run the server, simply report the configuration that would " "be used to run it.", "required": False, "default": False, "caster": bool }, { "name": "COWBULL_CUSTOM_MODES", "description": "A file which defines additional " "modes to be defined in addition to the default modes. The " "file must be a list of JSON objects containing mode " "definitions. Each object must contain (at a minimum): mode, digits, and " "priority", "required": False, "default": None, "caster": self._load_from_json }] def execute_load(self, app): if app is None: raise ValueError( "The Flask app must be passed to the Configurator") if not isinstance(app, Flask): raise TypeError("Expected a Flask object") self.app = app self.app.config["PYTHON_VERSION_MAJOR"] = sys.version_info[0] self.app.config["LOGGING_FORMAT"] = os.getenv( "logging_format", os.getenv("LOGGING_FORMAT", "%(asctime)s %(levelname)s: %(message)s")) self.app.config["LOGGING_LEVEL"] = os.getenv( "logging_level", os.getenv("LOGGING_LEVEL", logging.WARNING)) self.error_handler = ErrorHandler( module="Configurator", method="__init__", level=self.app.config["LOGGING_LEVEL"], format=self.app.config["LOGGING_FORMAT"]) self.error_handler.log( message="Initialized logging (level: {}, format: {})".format( self.app.config["LOGGING_LEVEL"], self.app.config["LOGGING_FORMAT"]), logger=logging.info) self.error_handler.log(message="Configuring environment variables.", logger=logging.info) self.configuration = {} config_file = self._set_config(source=os.getenv, name="COWBULL_CONFIG") self.app.config["COWBULL_CONFIG"] = config_file self.error_handler.log(message="Loading configuration from: {}".format( config_file if config_file else "environment variables")) source = {} if config_file: _file = open(config_file, 'r') try: source = json.load(_file) except: raise finally: _file.close() self.load_variables(source=source) def get_variables(self): return [ ("LOGGING_LEVEL", "An integer representing the Python " "logging level (e.g. 10 for debug, 20 for warning, etc.)"), ("LOGGING_FORMAT", "The format for logs. The default is >> " "%(asctime)s %(levelname)s: %(message)s"), ("COWBULL_CONFIG", "A path and filename of a configuration file " "used to set env. vars. e.g. /path/to/the/file.cfg") ]\ + [(i["name"], i["description"]) for i in self.env_vars] def dump_variables(self): return [ ("LOGGING_LEVEL", self.app.config["LOGGING_LEVEL"]), ("LOGGING_FORMAT", self.app.config["LOGGING_FORMAT"]), ("COWBULL_CONFIG", self.app.config["COWBULL_CONFIG"]) ] \ + \ [(i["name"], self.app.config[i["name"]]) for i in self.env_vars] def print_variables(self): print('') print('=' * 80) print('=', ' ' * 30, 'CONFIGURATION', ' ' * 31, '=') print('=' * 80) print( 'The following environment variables may be set to dynamically configure the' ) print( 'server. Alternately, these can be defined in a file and passed using the env.' ) print( 'var. COWBULL_CONFIG. Please note, the file must be a JSON data object.' ) print('') print( 'Please note. Env. Var. names can be *ALL* lowercase or *ALL* uppercase.' ) print('') print('-' * 80) print('| Current configuration set:') print('-' * 80) for name, val in self.dump_variables(): outstr = "| {:20s} | {}".format(name, val) print(outstr) print('-' * 80) print('') def load_variables(self, source=None): if source: _fetch = source.get else: _fetch = os.getenv for item in self.env_vars: if isinstance(item, dict): self._set_config(source=_fetch, **item) elif isinstance(item, str): self._set_config(name=item, source=_fetch) elif isinstance(item, list): self.load_variables(source=item) else: raise TypeError( "Unexpected item in configuration: {}, type: {}".format( item, type(item))) def _set_config(self, source=None, name=None, description=None, required=None, default=None, errmsg=None, caster=None, choices=None): value = source(name.lower(), source(name.upper(), None)) if required and value is None and default is None: raise ValueError( errmsg or "Problem fetching config item: " "{}. It is required and was not found or the value was None.". format(name)) if value is None: value = default if caster: if caster == PersistenceEngine: if not isinstance(value, dict): # # Added .replace to remove single quotes being added by PyCharm # value = json.loads(str(value).replace("'", "")) value = caster(**value) else: value = caster(value) if choices: if value not in choices: raise ValueError( errmsg or "The configuration value for {}({}) is not in the list of choices: ()" .format(name, value, choices)) self.app.config[name] = value return value def _load_from_json(self, json_file_name): if not json_file_name: return None f = None return_value = None try: f = open(json_file_name, 'r') return_value = json.load(f) except Exception as e: self.error_handler.error(module="Configurator.py", method="_load_from_json", status=500, exception=repr(e), message="An exception occurred!") raise IOError( "A JSON file ({}) cannot be loaded. Exception: {}".format( json_file_name, repr(e))) finally: if f: f.close() return return_value
class PersistenceEngine(object): def __init__(self, **kwargs): self.handler = ErrorHandler(module="PersistenceEngine", method="__init__") self.handler.log(message="Getting persistence engine arguments") engine_name = kwargs.get('engine_name', None) parameters = kwargs.get('parameters', None) if not engine_name: raise ValueError( "'engine_name' must be defined for the persistence engine") if not isinstance(parameters, dict): raise TypeError("'parameters' must be a dictionary of objects") self._engine_name = engine_name self._parameters = parameters self._persister = None # Step 1 - Build path self.handler.log(message="Build import path") cwd = getcwd() extension_path = "{}/{}".format(cwd, "PersistenceExtensions") self.handler.log( message="Added {} to import path".format(extension_path)) path.append(extension_path) # Step 2 - get the persisters and choose the right one self.handler.log(message="Building persisters and validators") persisters = [ filefound[:-3] for filefound in listdir(extension_path) if filefound.endswith(".py") ] validators = [filefound.lower() for filefound in persisters] self.handler.log(message="Persisters: {}".format(persisters)) self.handler.log(message="Validators: {}".format(validators)) try: self.handler.log(message="Set persister") self._engine_name = persisters[validators.index(self._engine_name)] self.handler.log(message="Importing Persister from {}".format( self._engine_name)) self._persister = importlib.import_module(self._engine_name) self.handler.log(message="Persistence engine set to {}".format( self._engine_name)) except ValueError: if self._engine_name.lower() == 'redis': self.handler.log(message="Redis selected") else: self.handler.log( message="Persister {} not found, defaulting to Redis". format(self._engine_name)) self._engine_name = "RedisPersist" self.handler.log(message="Importing RedisPersist") from Persistence import Redis self._persister = Redis self.handler.log(message="Persistence engine defaulted to Redis") except Exception: raise if not issubclass(self._persister.Persister, AbstractPersister): raise TypeError( "The persister must be a subclass of an AbstractPersister!") else: self.handler.log( message= "{} validated as a concrete implementation of an AbstractPersister" .format(self._engine_name)) self.handler.log(message="Instantiating Persister") self._persister = self._persister.Persister(**self._parameters) return @property def engine_name(self): return self._engine_name @property def parameters(self): return self._parameters @property def persister(self): return self._persister def __repr__(self): return "<persister>{}".format(self._engine_name) # # 'Private' methods # def _validate_persister_functions(self): _key = "abc" _content = '{"foo":"bar"}' self._persister.save(_key, _content) _fetch_key = self._persister.load(_key) if str(_content) != str(_fetch_key): raise ValueError("Persister {} was unable to save {} ({})!".format( self._engine_name, _key, type(_key))) else: self.handler.log( message="Confirmed save and load methods work correctly.")
def test_he_check_warn(self): eh = ErrorHandler(module="TEST", method="TEST-METHOD", level=logging.ERROR) message = "Message for testing purposes only" eh.log(status=404, message=message, logger=logging.warning)