From 4c37dd53d14f004a6aeec25e37b31c88b4c21a02 Mon Sep 17 00:00:00 2001 From: Himanshu Sharma Date: Sun, 1 Nov 2020 17:14:40 +0530 Subject: [PATCH] Added command `create-config` (#169) * feature: Added command functionality * test: Added tests for command and helper functions * feature: Added methods to dump, remove config file * fix: Updated max-attributes in pylintrc NOTE: The command by default, doesn't create/overwrite if a config file already exists. --- pylintrc | 2 +- pyms/cmd/main.py | 32 +++++++++++++++++++++++ pyms/config/__init__.py | 4 +-- pyms/config/conf.py | 56 ++++++++++++++++++++++++++++++++++++++--- tests/common.py | 14 +++++++++++ tests/test_cmd.py | 11 ++++++++ tests/test_config.py | 24 +++++++++++++++++- 7 files changed, 135 insertions(+), 8 deletions(-) diff --git a/pylintrc b/pylintrc index 19f3676..9f285fd 100644 --- a/pylintrc +++ b/pylintrc @@ -400,7 +400,7 @@ valid-metaclass-classmethod-first-arg=mcs max-args=7 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=8 # Maximum number of boolean expressions in a if statement max-bool-expr=5 diff --git a/pyms/cmd/main.py b/pyms/cmd/main.py index eea8f0e..0802f4e 100755 --- a/pyms/cmd/main.py +++ b/pyms/cmd/main.py @@ -5,10 +5,12 @@ import argparse import os import sys +from distutils.util import strtobool from pyms.crypt.fernet import Crypt from pyms.flask.services.swagger import merge_swagger_file from pyms.utils import check_package_exists, import_from, utils +from pyms.config import create_conf_file class Command: @@ -18,6 +20,7 @@ class Command: args = [] + # flake8: noqa: C901 def __init__(self, *args, **kwargs): arguments = kwargs.get("arguments", False) autorun = kwargs.get("autorun", True) @@ -53,6 +56,9 @@ def __init__(self, *args, **kwargs): "-f", "--file", default=os.path.join('project', 'swagger', 'swagger.yaml'), help='Swagger file path') + parser_create_config = commands.add_parser('create-config', help='Generate a config file') + parser_create_config.add_argument("create_config", action='store_true', help='Generate a config file') + parser.add_argument("-v", "--verbose", default="", type=str, help="Verbose ") args = parser.parse_args(arguments) @@ -74,6 +80,10 @@ def __init__(self, *args, **kwargs): self.file = args.file except AttributeError: self.merge_swagger = False + try: + self.create_config = args.create_config + except Exception: + self.create_config = False self.verbose = len(args.verbose) if autorun: # pragma: no cover result = self.run() @@ -91,6 +101,8 @@ def run(self): if self.create_key: path = crypt._loader.get_path_from_env() # pylint: disable=protected-access pwd = self.get_input('Type a password to generate the key file: ') + # Should use yes_no_input insted of get input below + # the result should be validated for Yes (Y|y) rather allowing anything other than 'n' generate_file = self.get_input('Do you want to generate a file in {}? [Y/n]'.format(path)) generate_file = generate_file.lower() != "n" key = crypt.generate_key(pwd, generate_file) @@ -99,6 +111,8 @@ def run(self): else: self.print_ok("Key generated: {}".format(key)) if self.encrypt: + # Spoted Unhandle exceptions - The encrypt function throws FileDoesNotExistException, ValueError + # which are not currently handled encrypted = crypt.encrypt(self.encrypt) self.print_ok("Encrypted OK: {}".format(encrypted)) if self.startproject: @@ -113,8 +127,26 @@ def run(self): except FileNotFoundError as ex: self.print_error(ex.__str__()) return False + if self.create_config: + use_requests = self.yes_no_input('Do you want to use request') + use_swagger = self.yes_no_input('Do you want to use swagger') + try: + conf_file_path = create_conf_file(use_requests, use_swagger) + self.print_ok(f'Config file "{conf_file_path}" created') + return True + except Exception as ex: + self.print_error(ex.__str__()) + return False return True + def yes_no_input(self, msg=""): # pragma: no cover + answer = input(utils.colored_text(f'{msg}{"?" if not msg.endswith("?") else ""} [Y/n] :', utils.Colors.BLUE, True)) # nosec + try: + return strtobool(answer) + except ValueError: + self.print_error('Invalid input, Please answer with a "Y" or "n"') + self.yes_no_input(msg) + @staticmethod def print_ok(msg=""): print(utils.colored_text(msg, utils.Colors.BRIGHT_GREEN, True)) diff --git a/pyms/config/__init__.py b/pyms/config/__init__.py index 57f58ac..763ae86 100644 --- a/pyms/config/__init__.py +++ b/pyms/config/__init__.py @@ -1,4 +1,4 @@ -from .conf import get_conf from .confile import ConfFile +from .conf import get_conf, create_conf_file -__all__ = ['get_conf', 'ConfFile'] +__all__ = ['get_conf', 'create_conf_file', 'ConfFile'] diff --git a/pyms/config/conf.py b/pyms/config/conf.py index a15adf2..9fb55cc 100644 --- a/pyms/config/conf.py +++ b/pyms/config/conf.py @@ -1,11 +1,13 @@ -import logging import os - +import logging +from typing import Union +import yaml +from pyms.utils import utils from pyms.config.confile import ConfFile from pyms.constants import PYMS_CONFIG_WHITELIST_KEYWORDS, CONFIGMAP_FILE_ENVIRONMENT_LEGACY, \ - CONFIGMAP_FILE_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT_LEGACY, LOGGER_NAME + CONFIGMAP_FILE_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT_LEGACY, LOGGER_NAME, \ + DEFAULT_CONFIGMAP_FILENAME from pyms.exceptions import ServiceDoesNotExistException, ConfigErrorException, AttrDoesNotExistException -from pyms.utils import utils logger = logging.getLogger(LOGGER_NAME) @@ -144,3 +146,49 @@ def __verify_deprecated_env_variables(config): except AttrDoesNotExistException: pass logger.warning(msg) + + +def create_conf_file(use_requests: bool = False, use_swagger: bool = False) -> Union[Exception, str]: + """ + Creates a configuration file defining + + :param use_requests: Do you want to use requests, defaults to False + :type use_requests: bool, optional + :param use_swagger: Do you want to use swagger, defaults to False + :type use_swagger: bool, optional + :raises FileExistsError: Config file already exists + :raises IOError: Config file creation failed. + :return: Raises FileExistsError or IOError OR returns config_file_path + :rtype: Union[Exception, str] + """ + # Try using env value for config file, if not found use default + CONFIG_FILE = os.getenv(CONFIGMAP_FILE_ENVIRONMENT, None) + if not CONFIG_FILE: + CONFIG_FILE = DEFAULT_CONFIGMAP_FILENAME + # Prevent overwriting existing file + if os.path.exists(CONFIG_FILE): + raise FileExistsError("Config file already exists at '{}'".format(os.path.abspath(CONFIG_FILE))) + # Create config dict + config = {"pyms": {}} + # add services + if use_requests: + if not config["pyms"].get("services", None): + config["pyms"]["services"] = {} + config["pyms"]["services"]["requests"] = {"data": ""} + if use_swagger: + if not config["pyms"].get("services", None): + config["pyms"]["services"] = {} + config["pyms"]["services"]["swagger"] = {"path": "", "file": "swagger.yaml"} + # add Basic Flask config + config["pyms"]["config"] = { + "DEBUG": True, + "TESTING": False, + "APP_NAME": "Python Microservice", + "APPLICATION_ROOT": "" + } + try: + with open(CONFIG_FILE, 'w', encoding='utf-8') as config_file: + config_file.write(yaml.dump(config, default_flow_style=False, default_style=None, sort_keys=False)) + except Exception as ex: + raise ex + return CONFIG_FILE diff --git a/tests/common.py b/tests/common.py index 46d913a..4d8ecbc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,4 +1,6 @@ +import os from pyms.flask.app import Microservice +from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT, DEFAULT_CONFIGMAP_FILENAME class MyMicroserviceNoSingleton(Microservice): _singleton = False @@ -6,3 +8,15 @@ class MyMicroserviceNoSingleton(Microservice): class MyMicroservice(Microservice): pass + + +def remove_conf_file(): + """ + Remove the YAML config file + """ + CONFIG_FILE = os.getenv(CONFIGMAP_FILE_ENVIRONMENT, None) + if not CONFIG_FILE: + CONFIG_FILE = DEFAULT_CONFIGMAP_FILENAME + # Dlete file, if exists + if os.path.exists(CONFIG_FILE): + os.remove(CONFIG_FILE) diff --git a/tests/test_cmd.py b/tests/test_cmd.py index 049630c..1f5cb81 100644 --- a/tests/test_cmd.py +++ b/tests/test_cmd.py @@ -11,6 +11,7 @@ from pyms.exceptions import FileDoesNotExistException, PackageNotExists from pyms.crypt.fernet import Crypt from pyms.flask.services.swagger import get_bundled_specs +from tests.common import remove_conf_file class TestCmd(unittest.TestCase): @@ -75,3 +76,13 @@ def test_merge_swagger_error(self): cmd = Command(arguments=arguments, autorun=False) with pytest.raises(ResolutionError) as excinfo: cmd.run() + + @patch('pyms.cmd.main.Command.yes_no_input', return_value=True) + def test_create_config_all(self, input): + # Remove config file if already exists for test + remove_conf_file() + arguments = ["create-config"] + cmd = Command(arguments=arguments, autorun=False) + assert cmd.run() + assert not cmd.run() + remove_conf_file() diff --git a/tests/test_config.py b/tests/test_config.py index 66829b6..e0f8e4f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -3,7 +3,7 @@ import unittest from unittest import mock -from pyms.config import get_conf, ConfFile +from pyms.config import get_conf, ConfFile, create_conf_file from pyms.config.conf import validate_conf from pyms.constants import CONFIGMAP_FILE_ENVIRONMENT, CONFIGMAP_FILE_ENVIRONMENT_LEGACY, LOGGER_NAME, CONFIG_BASE, \ CRYPT_FILE_KEY_ENVIRONMENT, CRYPT_FILE_KEY_ENVIRONMENT_LEGACY @@ -190,3 +190,25 @@ def all_validate_conf_combinations(self, legacy=False): validate_conf() os.environ[config_env] = os.path.join(self.BASE_DIR, "config-tests-debug-off.yml") validate_conf() + + +class ConfiFileTest(unittest.TestCase): + BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + + def setUp(self): + os.environ[CONFIGMAP_FILE_ENVIRONMENT] = os.path.join(self.BASE_DIR, "config-file.yml") + + def tearDown(self): + del os.environ[CONFIGMAP_FILE_ENVIRONMENT] + + def test_create_config(self): + create_conf_file(use_requests=True, use_swagger=True) + self.assertTrue(os.path.exists(os.environ[CONFIGMAP_FILE_ENVIRONMENT])) + validate_conf() + + def test_create_config_failure(self): + with self.assertRaises(FileExistsError): + create_conf_file(use_requests=True, use_swagger=True) + # Delete the file + if os.path.exists(os.environ[CONFIGMAP_FILE_ENVIRONMENT]): + os.remove(os.environ[CONFIGMAP_FILE_ENVIRONMENT])