Skip to content

modules

Top-level package for Cfg4Py.

cli

Command

Source code in cfg4py/cli.py
class Command:
    def __init__(self):
        self.resource_path = os.path.normpath(
            os.path.join(os.path.dirname(__file__), "resources/")
        )
        self.yaml = YAML(typ="safe")  # default, if not specfied, is 'rt' (round-trip)
        self.yaml.default_flow_style = False

        with open(
            os.path.join(self.resource_path, "template.yaml"), "r", encoding="utf-8"
        ) as f:
            self.templates = self.yaml.load(f)

        self.transformed = self._transform()

    def build(self, config_dir: str):
        """Compile configuration files into python script, which is used by IDE's
        auto-complete function

        Args:
            config_dir: The folder where your configuration files located

        Returns:

        """
        if not os.path.exists(config_dir):
            print(f"path {config_dir} not exists")
            sys.exit(-1)

        count = 0
        for f in os.listdir(config_dir):
            if (
                f.startswith("default")
                or f.startswith("dev")
                or f.startswith("test")
                or f.startswith("production")
            ):
                print(f"found {f}")
                count += 1

        if count > 0:
            print(f"{count} files found in total")
        else:
            print("the folder contains no valid configuration files")
            sys.exit(-1)

        try:
            init(config_dir)
            sys.path.insert(0, config_dir)
            from schema import Config  # type: ignore # noqa

            output_file = f"{os.path.join(config_dir, 'schema')}"
            msg = f"Config file is built with success and saved at {output_file}"
            print(msg)
        except Exception as e:  # pragma: no cover
            logging.exception(e)
            print("Config file built failure.")

    def _choose_dest_dir(self, dst):
        if dst is None:
            dst = input("Where should I save configuration files?\n")

        if os.path.exists(dst):
            for f in os.listdir(dst):
                msg = f"The folder already contains {f}, please choose clean one."

                if f in ["defaults.yaml", "dev.yaml", "test.yaml", "production.yaml"]:
                    print(msg)
                    return None
            return dst
        else:
            create = input("Path not exists, create('Q' to exit)? [Y/n]")
            if create.upper() == "Y":
                os.makedirs(dst, exist_ok=True)
                return dst
            elif create.upper() == "Q":
                sys.exit(-1)
            else:
                return None

    def scaffold(self, dst: Optional[str]):
        """Creates initial configuration files based on our choices.
        Args:
            dst:

        Returns:

        """
        print("Creating a configuration boilerplate:")
        dst = self._choose_dest_dir(dst)
        while dst is None:
            dst = self._choose_dest_dir(dst)

        yaml = YAML(typ="safe")  # default, if not specfied, is 'rt' (round-trip)
        with open(
            os.path.join(self.resource_path, "template.yaml"), "r", encoding="utf-8"
        ) as f:
            templates = yaml.load(f)

        print("Which flavors do you want?")
        print("-" * 20)
        prompt = """
        0  - console + rotating file logging
        10 - redis/redis-py (gh://andymccurdy/redis-py)
        11 - redis/aioredis (gh://aio-libs/aioredis)
        20 - mysql/PyMySQL (gh://PyMySQL/PyMySQL)
        30 - postgres/asyncpg (gh://MagicStack/asyncpg)
        31 - postgres/psycopg2 (gh://psycopg/psycopg2)
        40 - mq/pika (gh://pika/pika)
        50 - mongodb/pymongo (gh://mongodb/mongo-python-driver)
        """
        print(prompt)
        chooses = input(
            "Please choose flavors by index, separated each by a comma(,):\n"
        )
        flavors = {}
        mapping = {
            "0": "logging",
            "1": "redis",
            "2": "mysql",
            "3": "postgres",
            "4": "mq",
            "5": "mongodb",
        }
        for index in chooses.strip(" ").split(","):
            if index == "0":
                flavors["logging"] = templates["logging"]
                continue

            try:
                major = mapping[index[0]]
                minor = int(index[1])
                flavors[major] = list(templates[major][minor].values())[0]
            except (ValueError, KeyError):
                print(f"Wrong index {index}, skipped.")
                continue

        with open(os.path.join(dst, "defaults.yaml"), "w", encoding="utf-8") as f:
            f.writelines(
                "#auto generated by Cfg4Py: https://github.com/jieyu-tech/cfg4py\n"
            )
            yaml.dump(flavors, f)

        print(f"Cfg4Py has generated the following files under {dst}:")
        print("defaults.yaml")
        for name in ["dev.yaml", "test.yaml", "production.yaml"]:
            with open(os.path.join(dst, name), "w", encoding="utf8") as f:
                f.writelines(
                    "#auto generated by Cfg4Py: https://github.com/jieyu-tech/cfg4py\n"
                )
                print(name)

        with open(os.path.join(dst, "defaults.yaml"), "r", encoding="utf-8") as f:
            print("Content in defaults.yaml")
            for line in f.readlines():
                print(line.replace("\n", ""))

    def _show_supported_config(self):
        print("Support the following configurations:")
        for key in self.templates.keys():
            item = self.templates.get(key)
            if isinstance(item, dict):
                print(f"  {key}")
            elif isinstance(item, list):
                sub_keys = []
                for sub_item in item:
                    sub_keys.append(f"{key}/{list(sub_item.keys())[0]}")
                print(f"  {key}: {', '.join(sub_keys)}")

    def _transform(self):
        transformed = {}
        for key in self.templates:
            if isinstance(self.templates[key], dict):
                transformed[key] = self.templates[key]
            elif isinstance(self.templates[key], list):
                for item in self.templates[key]:
                    item_key = list(item.keys())[0]
                    transformed[f"{key}/{item_key}"] = item

        return transformed

    def hint(self, what: str = None, usage: bool = False):
        """show a cheat sheet for configurations.
        for example:
            cfg4py hint mysql
        this will print how to configure PyMySQL
        :param what
        :param usage
        """

        if what is None or (
            (what not in self.templates) and what not in self.transformed
        ):
            return self._show_supported_config()

        usage_key = f"{what}_usage"

        if usage_key in self.templates and usage:
            print("Usage:", self.templates.get(usage_key))

        if what in self.templates:
            self.yaml.dump(self.templates[what], sys.stdout)

        if usage_key in self.transformed and usage:
            print("Usage:", self.transformed.get(usage_key))

        if what in self.transformed:
            self.yaml.dump(self.transformed[what], sys.stdout)

    def set_server_role(self):
        print("please add the following line into your .bashrc:\n")
        print(f"export {envar}=DEV\n")
        msg = "You need to change DEV to TEST | PRODUCTION according to its actual role\
         accordingly"
        print(msg)

    def version(self):
        from cfg4py import __version__

        print(
            "Easy config module support code complete, cascading design and apative deployment"
        )
        print(f"version: {__version__}")

build(self, config_dir)

Compile configuration files into python script, which is used by IDE's auto-complete function

Parameters:

Name Type Description Default
config_dir str

The folder where your configuration files located

required
Source code in cfg4py/cli.py
def build(self, config_dir: str):
    """Compile configuration files into python script, which is used by IDE's
    auto-complete function

    Args:
        config_dir: The folder where your configuration files located

    Returns:

    """
    if not os.path.exists(config_dir):
        print(f"path {config_dir} not exists")
        sys.exit(-1)

    count = 0
    for f in os.listdir(config_dir):
        if (
            f.startswith("default")
            or f.startswith("dev")
            or f.startswith("test")
            or f.startswith("production")
        ):
            print(f"found {f}")
            count += 1

    if count > 0:
        print(f"{count} files found in total")
    else:
        print("the folder contains no valid configuration files")
        sys.exit(-1)

    try:
        init(config_dir)
        sys.path.insert(0, config_dir)
        from schema import Config  # type: ignore # noqa

        output_file = f"{os.path.join(config_dir, 'schema')}"
        msg = f"Config file is built with success and saved at {output_file}"
        print(msg)
    except Exception as e:  # pragma: no cover
        logging.exception(e)
        print("Config file built failure.")

hint(self, what=None, usage=False)

show a cheat sheet for configurations. for example: cfg4py hint mysql this will print how to configure PyMySQL :param what :param usage

Source code in cfg4py/cli.py
def hint(self, what: str = None, usage: bool = False):
    """show a cheat sheet for configurations.
    for example:
        cfg4py hint mysql
    this will print how to configure PyMySQL
    :param what
    :param usage
    """

    if what is None or (
        (what not in self.templates) and what not in self.transformed
    ):
        return self._show_supported_config()

    usage_key = f"{what}_usage"

    if usage_key in self.templates and usage:
        print("Usage:", self.templates.get(usage_key))

    if what in self.templates:
        self.yaml.dump(self.templates[what], sys.stdout)

    if usage_key in self.transformed and usage:
        print("Usage:", self.transformed.get(usage_key))

    if what in self.transformed:
        self.yaml.dump(self.transformed[what], sys.stdout)

scaffold(self, dst)

Creates initial configuration files based on our choices.

Parameters:

Name Type Description Default
dst Optional[str] required
Source code in cfg4py/cli.py
def scaffold(self, dst: Optional[str]):
    """Creates initial configuration files based on our choices.
    Args:
        dst:

    Returns:

    """
    print("Creating a configuration boilerplate:")
    dst = self._choose_dest_dir(dst)
    while dst is None:
        dst = self._choose_dest_dir(dst)

    yaml = YAML(typ="safe")  # default, if not specfied, is 'rt' (round-trip)
    with open(
        os.path.join(self.resource_path, "template.yaml"), "r", encoding="utf-8"
    ) as f:
        templates = yaml.load(f)

    print("Which flavors do you want?")
    print("-" * 20)
    prompt = """
    0  - console + rotating file logging
    10 - redis/redis-py (gh://andymccurdy/redis-py)
    11 - redis/aioredis (gh://aio-libs/aioredis)
    20 - mysql/PyMySQL (gh://PyMySQL/PyMySQL)
    30 - postgres/asyncpg (gh://MagicStack/asyncpg)
    31 - postgres/psycopg2 (gh://psycopg/psycopg2)
    40 - mq/pika (gh://pika/pika)
    50 - mongodb/pymongo (gh://mongodb/mongo-python-driver)
    """
    print(prompt)
    chooses = input(
        "Please choose flavors by index, separated each by a comma(,):\n"
    )
    flavors = {}
    mapping = {
        "0": "logging",
        "1": "redis",
        "2": "mysql",
        "3": "postgres",
        "4": "mq",
        "5": "mongodb",
    }
    for index in chooses.strip(" ").split(","):
        if index == "0":
            flavors["logging"] = templates["logging"]
            continue

        try:
            major = mapping[index[0]]
            minor = int(index[1])
            flavors[major] = list(templates[major][minor].values())[0]
        except (ValueError, KeyError):
            print(f"Wrong index {index}, skipped.")
            continue

    with open(os.path.join(dst, "defaults.yaml"), "w", encoding="utf-8") as f:
        f.writelines(
            "#auto generated by Cfg4Py: https://github.com/jieyu-tech/cfg4py\n"
        )
        yaml.dump(flavors, f)

    print(f"Cfg4Py has generated the following files under {dst}:")
    print("defaults.yaml")
    for name in ["dev.yaml", "test.yaml", "production.yaml"]:
        with open(os.path.join(dst, name), "w", encoding="utf8") as f:
            f.writelines(
                "#auto generated by Cfg4Py: https://github.com/jieyu-tech/cfg4py\n"
            )
            print(name)

    with open(os.path.join(dst, "defaults.yaml"), "r", encoding="utf-8") as f:
        print("Content in defaults.yaml")
        for line in f.readlines():
            print(line.replace("\n", ""))

core

Main module.

LocalConfigChangeHandler (FileSystemEventHandler)

Source code in cfg4py/core.py
class LocalConfigChangeHandler(FileSystemEventHandler):
    def dispatch(self, event):
        if not isinstance(event, FileModifiedEvent):
            return

        ext = os.path.splitext(event.src_path)
        if ext in [".yml", ".yaml"]:
            _load_from_local_file()

dispatch(self, event)

Dispatches events to the appropriate methods.

:param event: The event object representing the file system event. :type event: :class:FileSystemEvent

Source code in cfg4py/core.py
def dispatch(self, event):
    if not isinstance(event, FileModifiedEvent):
        return

    ext = os.path.splitext(event.src_path)
    if ext in [".yml", ".yaml"]:
        _load_from_local_file()

config_remote_fetcher(fetcher, interval=300)

config a remote configuration fetcher, which will pull the settings on every refresh_interval

Parameters:

Name Type Description Default
fetcher RemoteConfigFetcher

sub class of RemoteConfigFetcher

required
interval int

how long should cfg4py to pull the configuration from remote

300
Source code in cfg4py/core.py
def config_remote_fetcher(fetcher: RemoteConfigFetcher, interval: int = 300):
    """
    config a remote configuration fetcher, which will pull the settings on every
     `refresh_interval`
    Args:
        fetcher: sub class of `RemoteConfigFetcher`
        interval: how long should cfg4py to pull the configuration from remote

    Returns:

    """
    global _remote_fetcher
    _remote_fetcher = fetcher

    _scheduler.add_job(_refresh, "interval", seconds=interval)
    _scheduler.start()

enable_logging(level=20, log_file=None, file_size=10, file_count=7)

Enable basic log function for the application

if log_file is None, then it'll provide console logging, otherwise, the console logging is turned off, all events will be logged into the provided file.

Parameters:

Name Type Description Default
level

the log level, one of logging.DEBUG, logging.INFO, logging.WARNING,

20
log_file

the absolute file path for the log.

None
file_size

file size in MB unit

10
file_count

how many backup files leaved in disk

7

Returns:

Type Description

None

Source code in cfg4py/core.py
def enable_logging(level=logging.INFO, log_file=None, file_size=10, file_count=7):
    """
    Enable basic log function for the application

    if log_file is None, then it'll provide console logging, otherwise, the console
    logging is turned off, all events will be logged into the provided file.

    Args:
        level: the log level, one of logging.DEBUG, logging.INFO, logging.WARNING,
        logging.Error
        log_file: the absolute file path for the log.
        file_size: file size in MB unit
        file_count: how many backup files leaved in disk

    Returns:
        None
    """

    assert file_count > 0
    assert file_size > 0

    from logging import handlers

    formatter = logging.Formatter(
        "%(asctime)s %(levelname)-1.1s %(filename)s:%(lineno)s | %(message)s"
    )

    _logger = logging.getLogger()
    _logger.setLevel(level)

    if log_file is None:
        console = logging.StreamHandler()
        console.setFormatter(formatter)
        _logger.addHandler(console)
    else:
        file_dir = os.path.dirname(log_file)
        os.makedirs(file_dir, exist_ok=True)
        rotating_file = handlers.RotatingFileHandler(
            log_file, maxBytes=1024 * 1024 * file_size, backupCount=file_count
        )
        rotating_file.setFormatter(formatter)
        _logger.addHandler(rotating_file)

init(local_cfg_path=None, dump_on_change=True, strict=False)

create cfg object.

Parameters:

Name Type Description Default
local_cfg_path str

the directory name where your configuration files exist

None
dump_on_change

if configuration is updated, whether or not to dump them into log file

True
Source code in cfg4py/core.py
def init(local_cfg_path: str = None, dump_on_change=True, strict=False):
    """
    create cfg object.
    Args:
        local_cfg_path: the directory name where your configuration files exist
        dump_on_change: if configuration is updated, whether or not to dump them into
         log file

    Returns:
    """
    global _local_config_dir, _dump_on_change, _remote_fetcher, _local_observer
    global _cfg_obj, _cfg_local, _cfg_remote
    global _strict

    _strict = strict
    _dump_on_change = dump_on_change
    if local_cfg_path:
        _local_config_dir = os.path.expanduser(local_cfg_path)

        _cfg_local = _load_from_local_file()
        update_config(_mixin(_cfg_remote, _cfg_local))

        try:
            # handle local configuration file change, this may not be available on some platform, like apple m1
            _local_observer = Observer()
            _local_observer.schedule(
                LocalConfigChangeHandler(), _local_config_dir, recursive=False
            )
            _local_observer.start()
        except Exception as e:
            logger.exception(e)
            logger.warning("failed to watch file changes. Hot-reload is not available")

    return _cfg_obj