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
Пример #4
0
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
Пример #5
0
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)