From 3dad2071ff07770fe3b33ff2bc2b5fef25930a42 Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Sat, 13 Apr 2024 23:17:42 -0400 Subject: [PATCH] refactor: extract configuration handling to own class --- spud/base.py | 9 +++++++-- spud/cli.py | 11 +++++++++-- spud/config.py | 36 ++++++++++++++++++++++++++++++++++++ tests/test_config.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 spud/config.py create mode 100644 tests/test_config.py diff --git a/spud/base.py b/spud/base.py index bd95cdc..23c64bf 100644 --- a/spud/base.py +++ b/spud/base.py @@ -5,5 +5,10 @@ Models and datastructures import pydantic -class Configuration(pydantic.BaseModel): - """Command line application configuration options""" +class ContainerMetadata(pydantic.BaseModel): + """Metadata pulled from container status check""" + + name: str + status: str + image: str + started: int diff --git a/spud/cli.py b/spud/cli.py index 4cdd78c..0c943b6 100644 --- a/spud/cli.py +++ b/spud/cli.py @@ -10,7 +10,7 @@ import pathlib import click import uvicorn -from spud.base import Configuration +from spud.config import Configuration DEFAULT_CONFIGURATION_PATH = "~/.config/spud/config.json" @@ -27,7 +27,14 @@ def cli(context, config): """CLI root""" context.ensure_object(dict) config = config if config is not None else DEFAULT_CONFIGURATION_PATH - context.obj["config_path"] = pathlib.Path(config).expanduser() + config_path = pathlib.Path(config).expanduser() + + context.obj["config_path"] = config_path + + if config_path.exists(): + context.obj["config"] = Configuration.from_file(config_path) + else: + context.obj["config"] = None @click.command() diff --git a/spud/config.py b/spud/config.py new file mode 100644 index 0000000..0d8e255 --- /dev/null +++ b/spud/config.py @@ -0,0 +1,36 @@ +import json +import pathlib + +import pydantic + + +class Configuration(pydantic.BaseModel): + """Command line application configuration options""" + + @classmethod + def from_file(cls, path: pathlib.Path) -> "Configuration": + """ + Generates a Configuration object from the file found at {path}. + + Raises if: + - The file does not exist + - The file is not valid JSON + - The file does not respect the expected schema + """ + if not path.exists(): + raise RuntimeError(f"Configuration file not found: {str(path)}.") + + with open(path, "r", encoding="utf8") as config_file: + config_data = config_file.read() + + try: + parsed_configuration = json.loads(config_data) + return cls(**parsed_configuration) + except json.decoder.JSONDecodeError as exc: + raise RuntimeError( + f"Configuration file is not valid JSON ({str(path)})." + ) from exc + except pydantic.ValidationError as exc: + raise RuntimeError( + f"Configuration file has wrong schema ({str(path)})." + ) from exc diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..0dbadce --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,32 @@ +import json + +import pytest + +from spud.config import Configuration + + +@pytest.fixture(name="sample_config") +def sample_config_fixture(): + return {} + + +def test_from_file_creates_configuration_from_file(tmpdir, sample_config): + config_file = tmpdir / "config.json" + config_file.write(json.dumps(sample_config)) + + config = Configuration.from_file(config_file) + + assert config.model_dump() == sample_config + + +def test_from_file_raises_if_file_not_found(tmpdir): + with pytest.raises(RuntimeError): + Configuration.from_file(tmpdir / "nonfile.json") + + +def test_from_file_raises_if_file_not_json(tmpdir): + config_file = tmpdir / "config.json" + config_file.write("notjson") + + with pytest.raises(RuntimeError): + Configuration.from_file(config_file)