Skip to content

Commit

Permalink
Lift get_config_data from npg_notifications. Improve error handling b…
Browse files Browse the repository at this point in the history
…y checking for a necessary config file. Use a dataclass to validate all properties are set in conf.
  • Loading branch information
nerdstrike committed Dec 17, 2024
1 parent d3dca7b commit 2577fc8
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
98 changes: 98 additions & 0 deletions src/npg_porch_cli/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import configparser
from dataclasses import dataclass
import json
import pathlib

"""Common utility functions for the package."""

DEFAULT_CONF_FILE_TYPE = "ini"


def get_config_data(conf_file_path: str, conf_file_section: str = None):
"""
Parses a configuration file and returns its content.
Allows for two types of configuration files, 'ini' and 'json'. The type of
the file is determined from the extension of the file name. In case of no
extension an 'ini' type is assumed.
The content of the file is not cached, so subsequent calls to get data from
the same configuration file result in re-reading and re-parsing of the file.
Args:
conf_file_path:
A configuration file with database connection details.
conf_file_section:
The section of the configuration file. Optional. Should be defined
for 'ini' files.
Returns:
For an 'ini' file returns the content of the given section of the file as
a dictionary.
For a 'json' file, if the conf_file_section argument is not defined, the
content of a file as a Python object is returned. If the conf_file_section
argument is defined, the object returned by the parser is assumed to be
a dictionary that has the value of the 'conf_file_section' argument as a key.
The value corresponding to this key is returned.
"""

path = pathlib.Path(conf_file_path)

conf_file_extension = path.suffix
if conf_file_extension:
conf_file_extension = conf_file_extension[1:]
else:
conf_file_extension = DEFAULT_CONF_FILE_TYPE

if conf_file_extension == DEFAULT_CONF_FILE_TYPE:
if not conf_file_section:
raise Exception(
"'conf_file_section' argument is not given, "
"it should be defined for '{DEFAULT_CONF_FILE_TYPE}' "
"configuration file."
)

config = configparser.ConfigParser()
with open(conf_file_path) as cf:
if not cf.readable():
raise Exception(f"{path} has a permissions issue")

config.read_file(cf)

return {i[0]: i[1] for i in config[conf_file_section].items()}

elif conf_file_extension == "json":
conf: dict = json.load(conf_file_path)
if conf_file_section:
if isinstance(conf, dict) is False:
raise Exception(f"{conf_file_path} does not have sections.")
if conf_file_section in conf.keys:
conf = conf[conf_file_section]
else:
raise Exception(
f"{conf_file_path} does not contain {conf_file_section} key"
)

return conf

else:
raise Exception(f"Parsing for '{conf_file_extension}' files is not implemented")


@dataclass(frozen=True, kw_only=True)
class PorchClientConfig:
"""
Suggested config file content for interacting with a Porch server instance
"""

api_url: str
pipeline_name: str
pipeline_uri: str
pipeline_version: str
npg_porch_token: str

@classmethod
def from_config_file(cls, conf_file_path: str, conf_file_section: str = "PORCH"):
conf = get_config_data(conf_file_path, conf_file_section=conf_file_section)
return cls(**conf)
17 changes: 17 additions & 0 deletions tests/data/conf.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[STUFF]

logging = INFO

[PORCH]

api_url = https://porch.dnapipelines.sanger.ac.uk
pipeline_name = test_pipeline
pipeline_uri = https://test.pipeline.com
pipeline_version = 9.9.9
npg_porch_token = 0123456789abcdef0123456789abcdef

[PARTIALPORCH]

api_url = https://porch.dnapipelines.sanger.ac.uk
pipeline_name = test_pipeline
pipeline_uri = https://test.pipeline.com
36 changes: 36 additions & 0 deletions tests/test_porch_client_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pytest import raises
from npg_porch_cli.config import get_config_data, PorchClientConfig


def test_config_file_reading():
conf = get_config_data(
conf_file_path="tests/data/conf.ini", conf_file_section="STUFF"
)

assert conf == {"logging": "INFO"}


def test_conf_obj():
config_obj = PorchClientConfig.from_config_file("tests/data/conf.ini")
assert config_obj.pipeline_name == "test_pipeline"
assert config_obj.pipeline_version == "9.9.9"

with raises(Exception):
PorchClientConfig.from_config_file(
"tests/data/conf.ini", conf_file_section="ABSENT"
)

with raises(Exception):
PorchClientConfig.from_config_file("notafile", conf_file_section="ABSENT")

with raises(TypeError) as e:
PorchClientConfig.from_config_file(
"tests/data/conf.ini", conf_file_section="STUFF"
)
assert e.match("unexpected keyword argument 'logging'")

with raises(Exception) as e:
PorchClientConfig.from_config_file(
"tests/data/conf.ini", conf_file_section="PARTIALPORCH"
)
assert e.match("missing 2 required keyword-only arguments")

0 comments on commit 2577fc8

Please sign in to comment.