From a93fe8edfccbaa87c5c6cbc2cd6a1d0a4cde32f8 Mon Sep 17 00:00:00 2001 From: Leonardo Rossetti Date: Jun 29 2022 13:31:24 +0000 Subject: [PATCH 1/2] config module --- diff --git a/src/kojid_cloud_scheduler/config.py b/src/kojid_cloud_scheduler/config.py new file mode 100644 index 0000000..49be459 --- /dev/null +++ b/src/kojid_cloud_scheduler/config.py @@ -0,0 +1,57 @@ +import os +import configparser + +from .errors import KCSError + + +class Config: + """ + "Data class" which stores parsed configuration. + """ + + def __init__(self, koji_hub_url): + """ + Create a new Config object instance. + + Parameters + ---------- + koji_hub_url: str + Koji-Hub base URL to use for client requests + """ + self.koji_hub_url = koji_hub_url + + @classmethod + def from_path(cls, path): + """ + Parse a configparser file from a path. + + Paramaters + ---------- + path: str + The file path to load the file from + + Raises + ------ + kojid_cloud_scheduler.errors.KCSError + + Return + ------ + Config + A new Config object instance + """ + config = configparser.ConfigParser() + + if not os.path.exists(path): + raise KCSError(f'Unable to open config file: {path}') + + try: + config.read(path) + except configparser.Error as e: + raise KCSError(str(e)) + + try: + cfg = config['kojid_cloud_scheduler'] + except KeyError: + raise KCSError('Config file is missing a "kojid_cloud_scheduler" section') + + return cls(cfg['koji_hub_url']) diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..ecf7802 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,28 @@ +import pytest + +from kojid_cloud_scheduler import config, errors + + +def test_init(): + c = config.Config('https://koji-hub:8443/kojihub') + + assert c.koji_hub_url == 'https://koji-hub:8443/kojihub' + + +def test_from_path_error_path(): + with pytest.raises(errors.KCSError) as e: + config.Config.from_path('/something') + + assert e.value.code == 1 + assert str(e.value) == 'Unable to open config file: /something' + + +def test_from_path_error_parse(mocker): + fn = mocker.Mock(return_value=True) + mocker.patch('os.path.exists', fn) + + with pytest.raises(errors.KCSError) as e: + config.Config.from_path('/something') + + assert e.value.code == 1 + assert str(e.value) == 'Config file is missing a "kojid_cloud_scheduler" section' From 3dd77eb3bde736ff3f6d069ae5ec31f23ca259f2 Mon Sep 17 00:00:00 2001 From: Leonardo Rossetti Date: Jun 29 2022 15:58:16 +0000 Subject: [PATCH 2/2] cli test subcommand + config usage --- diff --git a/poetry.lock b/poetry.lock index 4170e6b..3d2a4d0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -40,10 +40,21 @@ python-versions = ">=3.5.0" unicode_backport = ["unicodedata2"] [[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] name = "colorama" version = "0.4.4" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" @@ -267,7 +278,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.10" -content-hash = "fad70e809d8180abbaec8db51b1abf838778fde8b8c6f70c5da6301daa9dffcd" +content-hash = "f204041c82d62991e712679ef0b7beea6106b09b30e16c765a7bf4cd4a746655" [metadata.files] atomicwrites = [ @@ -286,6 +297,10 @@ charset-normalizer = [ {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, ] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, diff --git a/pyproject.toml b/pyproject.toml index f4d6e41..26c55e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ packages = [ [tool.poetry.dependencies] python = "^3.10" koji = "^1.29.0" +click = "^8.1.3" [tool.poetry.dev-dependencies] pytest = "^7.1.2" diff --git a/src/kojid_cloud_scheduler/cli.py b/src/kojid_cloud_scheduler/cli.py index 73205d1..4bbb5a5 100644 --- a/src/kojid_cloud_scheduler/cli.py +++ b/src/kojid_cloud_scheduler/cli.py @@ -1,2 +1,88 @@ +import os +import sys +import configparser + +import click + +from .errors import KCSError +from .config import Config +from .kojiclient import KojiClient + + +@click.group() +@click.option('--config', '-c', default='/etc/kojid-cloud-scheduler/config.ini') +@click.pass_context +def cli(ctx, config): + """ + CLI main fuction, all subcommands are grouped in this one. + + It handles the configuration fikle path cli argument and passes it over to + orher subcommants. + + Parameters + ---------- + ctx: click.Context + The click library context object + config: string + The confg file path + + Raises + ------ + kojid_cloud_scheduler.errors.KCSError + """ + ctx.ensure_object(dict) + + if not os.path.exists(config): + click.echo(f'[ERR] Unable to open configuration file: {config}') + ctx.exit(1) + + with open(config, 'r') as f: + try: + ctx.obj['config'] = Config.from_path(config) + except KCSError as e: + click.echo(f'[ERR] {e}', err=True) + ctx.exit(e.code) + + +@click.command() +@click.pass_context +def test(ctx): + """ + Test subcommand function which runs a CLI sanity check. + + It handles errors and exists as soon as one is raised. + + Paramaters + ---------- + ctx: click.Context + Click framework context object, passed from the root CLI function + """ + cfg = ctx.obj['config'] + + kclient = KojiClient(cfg.koji_hub_url) + kclient.connect() + try: + kclient.ping() + except KCSError as e: + click.echo(f'[ERR] {e}', err=True) + ctx.exit(e.code) + + +cli.add_command(test) + + def run(): - print('TODO: cli implementation') + """ + Main CLI function which runs the whole thing. + + It writes the error message to stderr in case an KCSError is risen. + + Raises + ------ + errors.KCSError + """ + try: + cli() + except KCSError as e: + sys.stderr.write(f'{e}\n') + sys.exit(e.code) diff --git a/test/conftest.py b/test/conftest.py index e684a42..df39821 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,3 +1,5 @@ +import os + import pytest @@ -30,3 +32,10 @@ def list_tasks_result(): 'weight': 1.5653944408333333 } ] + + +@pytest.fixture +def fixtures(): + cwd = os.path.dirname(os.path.realpath(__file__)) + + return f'{cwd}/fixtures' diff --git a/test/fixtures/config.ini b/test/fixtures/config.ini new file mode 100644 index 0000000..e713fc4 --- /dev/null +++ b/test/fixtures/config.ini @@ -0,0 +1,2 @@ +[kojid_cloud_scheduler] +koji_hub_url = https://koji-hub:8443/kojihub diff --git a/test/test_cli_test.py b/test/test_cli_test.py new file mode 100644 index 0000000..fb09b13 --- /dev/null +++ b/test/test_cli_test.py @@ -0,0 +1,36 @@ +from click.testing import CliRunner +import requests + +from kojid_cloud_scheduler import cli, errors + + +def test_ok(mocker, fixtures): + runner = CliRunner() + + m = mocker.patch('kojid_cloud_scheduler.cli.KojiClient') + m.return_value.ping.return_value = 'Hello World' + + args = [ + '--config', f'{fixtures}/config.ini', + 'test' + ] + result = runner.invoke(cli.cli, args) + + assert result.exit_code == 0 + assert result.output == '' + + +def test_ping_error(mocker, fixtures): + runner = CliRunner() + + m = mocker.patch('kojid_cloud_scheduler.cli.KojiClient') + m.return_value.ping.side_effect = errors.KCSError('mock') + + args = [ + '--config', f'{fixtures}/config.ini', + 'test' + ] + result = runner.invoke(cli.cli, args) + + assert result.exit_code == 1 + assert result.output == '[ERR] mock\n'