diff --git a/requirements.txt b/requirements.txt index 58a76c9..4d6ed49 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ ckanapi==4.6 +defopt==6.3.0 hdx-python-country==3.1.3 ndg-httpsclient==0.5.1 pyasn1==0.4.8 diff --git a/setup.cfg b/setup.cfg index 6cb1f2c..6715695 100755 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ python_requires = >=3.6 install_requires = ckanapi >= 4.6 hdx-python-country>=3.1.3 + defopt ndg-httpsclient pyasn1 pyOpenSSL diff --git a/src/hdx/facades/infer_arguments.py b/src/hdx/facades/infer_arguments.py new file mode 100755 index 0000000..6750cc1 --- /dev/null +++ b/src/hdx/facades/infer_arguments.py @@ -0,0 +1,115 @@ +"""Facade to simplify project setup that calls project main function with kwargs""" +import logging +import sys +from inspect import BoundArguments +from typing import Any, Callable, List, Optional, Tuple + +import defopt +from hdx.utilities.easy_logging import setup_logging +from hdx.utilities.useragent import UserAgent + +from hdx.api import __version__ +from hdx.api.configuration import Configuration + +logger = logging.getLogger(__name__) +setup_logging(log_file="errors.log") + + +def _bind_known(fn: Callable) -> Tuple[Callable, BoundArguments, List[str]]: + parser = defopt._create_parser(fn, cli_options="all") + with defopt._colorama_text(): + args, argv = parser.parse_known_args() + parsed_argv = vars(args) + try: + func = parsed_argv.pop("_func") + except KeyError: + # Workaround for http://bugs.python.org/issue9253#msg186387 (and + # https://bugs.python.org/issue29298 which blocks using required=True). + parser.error("too few arguments") + sig = defopt.signature(func) + ba = sig.bind_partial() + ba.arguments.update(parsed_argv) + return func, ba, argv + + +def _run_known(func: Callable, ba: BoundArguments) -> Any: + sig = defopt.signature(func) + (raises,) = [ + # typing_inspect does not allow fetching metadata; see e.g. ti#82. + arg + for arg in getattr(sig.return_annotation, "__metadata__", []) + if isinstance(arg, defopt._Raises) + ] + # The function call should occur here to minimize effects on the traceback. + try: + return func(*ba.args, **ba.kwargs) + except raises as e: + sys.exit(e) + + +def _create_configuration( + user_agent: Optional[str] = None, + user_agent_config_yaml: Optional[str] = None, + user_agent_lookup: Optional[str] = None, + hdx_url: Optional[str] = None, + hdx_site: Optional[str] = None, + hdx_read_only: bool = False, + hdx_key: Optional[str] = None, + hdx_config_json: Optional[str] = None, + hdx_config_yaml: Optional[str] = None, + project_config_json: Optional[str] = None, + project_config_yaml: Optional[str] = None, + hdx_base_config_json: Optional[str] = None, + hdx_base_config_yaml: Optional[str] = None, +) -> str: + """ + Create HDX configuration + + Args: + user_agent (Optional[str]): User agent string. HDXPythonLibrary/X.X.X- is prefixed. Must be supplied if remoteckan is not. + user_agent_config_yaml (Optional[str]): Path to YAML user agent configuration. Ignored if user_agent supplied. Defaults to ~/.useragent.yml. + user_agent_lookup (Optional[str]): Lookup key for YAML. Ignored if user_agent supplied. + hdx_url (Optional[str]): HDX url to use. Overrides hdx_site. + hdx_site (Optional[str]): HDX site to use eg. prod, test. + hdx_read_only (bool): Whether to access HDX in read only mode. Defaults to False. + hdx_key (Optional[str]): Your HDX key. Ignored if hdx_read_only = True. + hdx_config_json (Optional[str]): Path to JSON HDX configuration OR + hdx_config_yaml (Optional[str]): Path to YAML HDX configuration + project_config_json (Optional[str]): Path to JSON Project configuration OR + project_config_yaml (Optional[str]): Path to YAML Project configuration + hdx_base_config_json (Optional[str]): Path to JSON HDX base configuration OR + hdx_base_config_yaml (Optional[str]): Path to YAML HDX base configuration. Defaults to library's internal hdx_base_configuration.yml. + + Returns: + str: HDX site url + + """ + arguments = locals() + return Configuration._create(**arguments) + + +def facade(projectmainfn: Callable[[Any], None]): + """Facade to simplify project setup that calls project main function. It infers + command line arguments from the passed in function using defopt. The function passed + in should have either type hints or a docstring from which to infer the command + line arguments. + + Args: + projectmainfn ((Any) -> None): main function of project + + Returns: + None + """ + + # + # Setting up configuration + # + func, ba, argv = _bind_known(projectmainfn) + site_url = defopt.run(_create_configuration, argv=argv, cli_options="all") + + logger.info("--------------------------------------------------") + logger.info(f"> Using HDX Python API Library {__version__}") + logger.info(f"> HDX Site: {site_url}") + + UserAgent.user_agent = Configuration.read().user_agent + _run_known(func, ba) diff --git a/tests/hdx/facades/__init__.py b/tests/hdx/facades/__init__.py index 4f5e990..ddd2f44 100755 --- a/tests/hdx/facades/__init__.py +++ b/tests/hdx/facades/__init__.py @@ -1,3 +1,5 @@ +from typing import Optional + from hdx.api.configuration import Configuration @@ -36,3 +38,7 @@ def my_testfnkw(**kwargs): elif fn == "exc": testresult.actual_result = Configuration.read().get_hdx_site_url() raise ValueError("Some failure!") + + +def my_testfnia(mydata: Optional[str] = None) -> str: + testresult.actual_result = mydata diff --git a/tests/hdx/facades/test_infer_arguments.py b/tests/hdx/facades/test_infer_arguments.py new file mode 100755 index 0000000..e91eefa --- /dev/null +++ b/tests/hdx/facades/test_infer_arguments.py @@ -0,0 +1,73 @@ +"""Simple Facade Tests""" +import sys + +import pytest +from hdx.utilities.useragent import UserAgent + +from hdx.api.configuration import ConfigurationError +from hdx.facades.infer_arguments import facade + +from . import my_testfnia, testresult + + +class TestInferArguments: + def test_facade(self, monkeypatch, hdx_config_yaml, project_config_yaml): + UserAgent.clear_global() + mydata = "hello" + monkeypatch.setattr( + sys, + "argv", + [ + "test", + "--mydata", + mydata, + "--hdx-site", + "prod", + "--hdx-key", + "123", + "--user-agent", + "test", + ], + ) + testresult.actual_result = None + facade(my_testfnia) + assert testresult.actual_result == mydata + + UserAgent.clear_global() + monkeypatch.setattr( + sys, + "argv", + [ + "test", + "--mydata", + mydata, + "--hdx-site", + "prod", + "--user-agent", + "test", + ], + ) + with pytest.raises(ConfigurationError): + facade(my_testfnia) + + UserAgent.clear_global() + monkeypatch.setattr( + sys, + "argv", + [ + "test", + "--mydata", + mydata, + "--hdx-site", + "prod", + "--hdx-key", + "123", + "--user-agent", + "test", + "--lala", + "what", + ], + ) + with pytest.raises(SystemExit): + facade(my_testfnia) + UserAgent.clear_global()